simo 2.0.32__py3-none-any.whl → 2.0.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of simo might be problematic. Click here for more details.
- simo/cli.py +2 -17
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/managers.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.cpython-38.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/admin.py +2 -2
- simo/core/api.py +46 -4
- simo/core/controllers.py +14 -7
- simo/core/forms.py +2 -2
- simo/core/managers.py +1 -5
- simo/core/migrations/0003_create_default_zones_and_categories.py +2 -39
- simo/core/migrations/0004_create_generic.py +22 -21
- simo/core/migrations/0013_auto_20231003_0754.py +45 -43
- simo/core/migrations/0018_auto_20231005_0622.py +18 -16
- simo/core/migrations/0033_auto_20240509_0821.py +25 -0
- simo/core/migrations/0034_component_error_msg.py +18 -0
- simo/core/migrations/0035_remove_instance_share_location.py +17 -0
- simo/core/migrations/0036_auto_20240521_0823.py +22 -0
- simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0004_create_generic.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0033_auto_20240509_0821.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0034_component_error_msg.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0035_remove_instance_share_location.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0036_auto_20240521_0823.cpython-38.pyc +0 -0
- simo/core/models.py +26 -26
- simo/core/serializers.py +12 -9
- simo/core/signal_receivers.py +82 -1
- simo/core/tasks.py +7 -4
- simo/fleet/__pycache__/admin.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/fleet/admin.py +25 -6
- simo/fleet/controllers.py +97 -36
- simo/fleet/forms.py +162 -9
- simo/fleet/migrations/0035_auto_20240514_0855.py +32 -0
- simo/fleet/migrations/__pycache__/0035_auto_20240514_0855.cpython-38.pyc +0 -0
- simo/fleet/models.py +97 -82
- simo/fleet/serializers.py +8 -1
- simo/fleet/socket_consumers.py +3 -15
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/__pycache__/utils.cpython-38.pyc +0 -0
- simo/users/migrations/0003_create_roles_and_system_user.py +24 -23
- simo/users/migrations/0019_auto_20231221_1155.py +9 -8
- simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0019_auto_20231221_1155.cpython-38.pyc +0 -0
- simo/users/models.py +6 -7
- simo/users/utils.py +0 -4
- {simo-2.0.32.dist-info → simo-2.0.34.dist-info}/METADATA +1 -1
- {simo-2.0.32.dist-info → simo-2.0.34.dist-info}/RECORD +62 -52
- {simo-2.0.32.dist-info → simo-2.0.34.dist-info}/LICENSE.md +0 -0
- {simo-2.0.32.dist-info → simo-2.0.34.dist-info}/WHEEL +0 -0
- {simo-2.0.32.dist-info → simo-2.0.34.dist-info}/top_level.txt +0 -0
simo/core/models.py
CHANGED
|
@@ -1,35 +1,25 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
1
|
import inspect
|
|
4
2
|
import time
|
|
5
3
|
from collections.abc import Iterable
|
|
6
4
|
from django.utils.text import slugify
|
|
7
|
-
from django.core.cache import cache
|
|
8
5
|
from django.utils.functional import cached_property
|
|
9
6
|
from django.urls import reverse_lazy
|
|
10
7
|
from django.utils.translation import gettext_lazy as _
|
|
11
8
|
from django.db import models
|
|
12
|
-
from django.contrib.auth import get_user_model
|
|
13
|
-
from django.conf import settings
|
|
14
9
|
from django.db.models.signals import post_delete
|
|
15
10
|
from django.dispatch import receiver
|
|
16
|
-
from django.utils import timezone
|
|
17
11
|
from timezone_utils.choices import ALL_TIMEZONES_CHOICES
|
|
18
12
|
from location_field.models.plain import PlainLocationField
|
|
19
13
|
from model_utils import FieldTracker
|
|
20
14
|
from dirtyfields import DirtyFieldsMixin
|
|
21
|
-
from easy_thumbnails.fields import ThumbnailerImageField
|
|
22
|
-
from taggit.managers import TaggableManager
|
|
23
15
|
from simo.core.utils.mixins import SimoAdminMixin
|
|
24
16
|
from simo.core.storage import OverwriteStorage
|
|
25
17
|
from simo.core.utils.validators import validate_svg
|
|
18
|
+
from simo.users.models import User
|
|
26
19
|
from .managers import ZonesManager, CategoriesManager, ComponentsManager
|
|
27
20
|
from .events import GatewayObjectCommand, OnChangeMixin
|
|
28
21
|
|
|
29
22
|
|
|
30
|
-
User = get_user_model()
|
|
31
|
-
|
|
32
|
-
|
|
33
23
|
class Icon(DirtyFieldsMixin, models.Model, SimoAdminMixin):
|
|
34
24
|
slug = models.SlugField(unique=True, db_index=True, primary_key=True)
|
|
35
25
|
keywords = models.CharField(max_length=500, blank=True, null=True)
|
|
@@ -93,12 +83,6 @@ class Instance(models.Model, SimoAdminMixin):
|
|
|
93
83
|
max_length=100, default='metric',
|
|
94
84
|
choices=(('metric', "Metric"), ('imperial', "Imperial"))
|
|
95
85
|
)
|
|
96
|
-
share_location = models.BooleanField(
|
|
97
|
-
default=True,
|
|
98
|
-
help_text="Share exact instance location with SIMO.io remote or not?"
|
|
99
|
-
"Sharing it helps better identify if user is at home or not."
|
|
100
|
-
|
|
101
|
-
)
|
|
102
86
|
indoor_climate_sensor = models.ForeignKey(
|
|
103
87
|
'Component', null=True, blank=True, on_delete=models.SET_NULL,
|
|
104
88
|
limit_choices_to={'base_type__in': ['numeric-sensor', 'multi-sensor']}
|
|
@@ -157,13 +141,11 @@ class Category(DirtyFieldsMixin, models.Model, SimoAdminMixin):
|
|
|
157
141
|
upload_to='categories', null=True, blank=True,
|
|
158
142
|
help_text="Will be cropped down to: 830x430"
|
|
159
143
|
)
|
|
160
|
-
header_image_last_change = models.DateTimeField(
|
|
161
|
-
auto_now_add=True, editable=False
|
|
162
|
-
)
|
|
163
144
|
all = models.BooleanField(
|
|
164
145
|
default=False,
|
|
165
146
|
help_text=_("All components automatically belongs to this category")
|
|
166
147
|
)
|
|
148
|
+
last_modified = models.DateTimeField(auto_now=True, editable=False)
|
|
167
149
|
order = models.PositiveIntegerField(
|
|
168
150
|
blank=False, null=False, db_index=True
|
|
169
151
|
)
|
|
@@ -298,14 +280,19 @@ class Gateway(DirtyFieldsMixin, models.Model, SimoAdminMixin):
|
|
|
298
280
|
if not isinstance(result, dict) and isinstance(result, Iterable):
|
|
299
281
|
for res in result:
|
|
300
282
|
if isinstance(res, models.Model):
|
|
301
|
-
self.discovery['result']
|
|
283
|
+
if res.pk not in self.discovery['result']:
|
|
284
|
+
self.discovery['result'].append(res.pk)
|
|
302
285
|
else:
|
|
303
|
-
self.discovery['result']
|
|
286
|
+
if res not in self.discovery['result']:
|
|
287
|
+
self.discovery['result'].append(res)
|
|
304
288
|
else:
|
|
305
289
|
if isinstance(result, models.Model):
|
|
306
|
-
self.discovery['result']
|
|
290
|
+
if result.pk not in self.discovery['result']:
|
|
291
|
+
self.discovery['result'].append(result.pk)
|
|
307
292
|
else:
|
|
308
|
-
self.discovery['result']
|
|
293
|
+
if result not in self.discovery['result']:
|
|
294
|
+
self.discovery['result'].append(result)
|
|
295
|
+
|
|
309
296
|
|
|
310
297
|
self.save(update_fields=['discovery'])
|
|
311
298
|
|
|
@@ -359,11 +346,17 @@ class Component(DirtyFieldsMixin, models.Model, SimoAdminMixin, OnChangeMixin):
|
|
|
359
346
|
on_delete=models.SET_NULL
|
|
360
347
|
)
|
|
361
348
|
last_change = models.DateTimeField(
|
|
362
|
-
null=True, editable=False, auto_now_add=True
|
|
349
|
+
null=True, editable=False, auto_now_add=True,
|
|
350
|
+
help_text="Last time component state was changed."
|
|
351
|
+
)
|
|
352
|
+
last_modified = models.DateTimeField(
|
|
353
|
+
auto_now_add=True, db_index=True, editable=False,
|
|
354
|
+
help_text="Last time component was modified."
|
|
363
355
|
)
|
|
364
356
|
|
|
365
357
|
last_update = models.DateTimeField(auto_now=True)
|
|
366
358
|
alive = models.BooleanField(default=True)
|
|
359
|
+
error_msg = models.TextField(null=True, blank=True, editable=False)
|
|
367
360
|
battery_level = models.PositiveIntegerField(null=True, editable=False)
|
|
368
361
|
|
|
369
362
|
show_in_app = models.BooleanField(default=True, db_index=True)
|
|
@@ -480,7 +473,7 @@ def is_in_alarm(self):
|
|
|
480
473
|
else:
|
|
481
474
|
self.arm_status = 'disarmed'
|
|
482
475
|
|
|
483
|
-
dirty_fields = self.get_dirty_fields()
|
|
476
|
+
dirty_fields = self.get_dirty_fields(check_relationship=True)
|
|
484
477
|
|
|
485
478
|
if self.pk:
|
|
486
479
|
actor = get_current_user()
|
|
@@ -503,6 +496,13 @@ def is_in_alarm(self):
|
|
|
503
496
|
actor.last_action = timezone.now()
|
|
504
497
|
actor.save()
|
|
505
498
|
|
|
499
|
+
modifying_fields = (
|
|
500
|
+
'name', 'icon', 'zone', 'category', 'config', 'meta',
|
|
501
|
+
'value_units', 'slaves', 'show_in_app', 'alarm_category'
|
|
502
|
+
)
|
|
503
|
+
if any(f in dirty_fields for f in modifying_fields):
|
|
504
|
+
self.last_modified = timezone.now()
|
|
505
|
+
|
|
506
506
|
obj = super().save(*args, **kwargs)
|
|
507
507
|
|
|
508
508
|
return obj
|
simo/core/serializers.py
CHANGED
|
@@ -34,7 +34,7 @@ class TimestampField(serializers.Field):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class IconSerializer(serializers.ModelSerializer):
|
|
37
|
-
last_modified = TimestampField()
|
|
37
|
+
last_modified = TimestampField(read_only=True)
|
|
38
38
|
|
|
39
39
|
class Meta:
|
|
40
40
|
model = Icon
|
|
@@ -43,13 +43,17 @@ class IconSerializer(serializers.ModelSerializer):
|
|
|
43
43
|
|
|
44
44
|
class CategorySerializer(serializers.ModelSerializer):
|
|
45
45
|
header_image_thumb = serializers.SerializerMethodField()
|
|
46
|
+
last_modified = TimestampField(read_only=True)
|
|
46
47
|
|
|
47
48
|
class Meta:
|
|
48
49
|
model = Category
|
|
49
50
|
fields = (
|
|
50
|
-
'id', 'name', 'all', 'icon',
|
|
51
|
+
'id', 'name', 'all', 'icon',
|
|
52
|
+
'header_image', 'header_image_thumb',
|
|
53
|
+
'last_modified'
|
|
51
54
|
)
|
|
52
55
|
|
|
56
|
+
|
|
53
57
|
def get_header_image_thumb(self, obj):
|
|
54
58
|
if obj.header_image:
|
|
55
59
|
thumbnailer = get_thumbnailer(obj.header_image.path)
|
|
@@ -60,10 +64,7 @@ class CategorySerializer(serializers.ModelSerializer):
|
|
|
60
64
|
request = get_current_request()
|
|
61
65
|
if request:
|
|
62
66
|
url = request.build_absolute_uri(url)
|
|
63
|
-
return
|
|
64
|
-
'url': url,
|
|
65
|
-
'last_change': obj.header_image_last_change.timestamp()
|
|
66
|
-
}
|
|
67
|
+
return url
|
|
67
68
|
return
|
|
68
69
|
|
|
69
70
|
|
|
@@ -232,6 +233,7 @@ class ComponentManyToManyRelatedField(serializers.Field):
|
|
|
232
233
|
class ComponentSerializer(FormSerializer):
|
|
233
234
|
id = ObjectSerializerMethodField()
|
|
234
235
|
last_change = TimestampField(read_only=True)
|
|
236
|
+
last_modified = TimestampField(read_only=True)
|
|
235
237
|
read_only = serializers.SerializerMethodField()
|
|
236
238
|
app_widget = serializers.SerializerMethodField()
|
|
237
239
|
slaves = serializers.SerializerMethodField()
|
|
@@ -239,9 +241,11 @@ class ComponentSerializer(FormSerializer):
|
|
|
239
241
|
show_in_app = ObjectSerializerMethodField()
|
|
240
242
|
controller_uid = ObjectSerializerMethodField()
|
|
241
243
|
alive = ObjectSerializerMethodField()
|
|
244
|
+
error_msg = ObjectSerializerMethodField()
|
|
242
245
|
value = ObjectSerializerMethodField()
|
|
243
246
|
config = ObjectSerializerMethodField()
|
|
244
247
|
meta = ObjectSerializerMethodField()
|
|
248
|
+
alarm_category = ObjectSerializerMethodField()
|
|
245
249
|
arm_status = ObjectSerializerMethodField()
|
|
246
250
|
battery_level = ObjectSerializerMethodField()
|
|
247
251
|
controller_methods = serializers.SerializerMethodField()
|
|
@@ -435,9 +439,8 @@ class ComponentSerializer(FormSerializer):
|
|
|
435
439
|
if form.is_valid():
|
|
436
440
|
if form.controller.is_discoverable:
|
|
437
441
|
form.controller.init_discovery(form.cleaned_data)
|
|
438
|
-
return
|
|
439
|
-
|
|
440
|
-
return instance
|
|
442
|
+
return form.save(commit=False)
|
|
443
|
+
return form.save(commit=True)
|
|
441
444
|
raise serializers.ValidationError(form.errors)
|
|
442
445
|
|
|
443
446
|
def get_controller_methods(self, obj):
|
simo/core/signal_receivers.py
CHANGED
|
@@ -1,8 +1,89 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
1
3
|
from django.db import transaction
|
|
2
4
|
from django.db.models.signals import post_save, post_delete
|
|
3
5
|
from django.dispatch import receiver
|
|
4
6
|
from django.utils import timezone
|
|
5
|
-
from .
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from simo.users.models import PermissionsRole
|
|
9
|
+
from .models import Instance, Gateway, Component, Icon, Zone, Category
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@receiver(post_save, sender=Instance)
|
|
13
|
+
def create_instance_defaults(sender, instance, created, **kwargs):
|
|
14
|
+
if not created:
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
# Create default zones
|
|
18
|
+
|
|
19
|
+
for zone_name in (
|
|
20
|
+
'Living Room', 'Kitchen', 'Bathroom', 'Porch', 'Garage', 'Yard', 'Other'
|
|
21
|
+
):
|
|
22
|
+
Zone.objects.create(instance=instance, name=zone_name)
|
|
23
|
+
|
|
24
|
+
core_dir_path = os.path.dirname(os.path.realpath(__file__))
|
|
25
|
+
imgs_folder = os.path.join(
|
|
26
|
+
core_dir_path, 'static/defaults/category_headers'
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
categories_media_dir = os.path.join(settings.MEDIA_ROOT, 'categories')
|
|
30
|
+
if not os.path.exists(categories_media_dir):
|
|
31
|
+
os.makedirs(categories_media_dir)
|
|
32
|
+
|
|
33
|
+
# Create default categories
|
|
34
|
+
|
|
35
|
+
for i, data in enumerate([
|
|
36
|
+
("All", 'star'), ("Climate", 'temperature-half'),
|
|
37
|
+
("Lights", 'lightbulb'), ("Security", 'eye'),
|
|
38
|
+
("Watering", 'faucet'), ("Other", 'flag-pennant')
|
|
39
|
+
]):
|
|
40
|
+
shutil.copy(
|
|
41
|
+
os.path.join(imgs_folder, "%s.jpg" % data[0].lower()),
|
|
42
|
+
os.path.join(
|
|
43
|
+
settings.MEDIA_ROOT, 'categories', "%s.jpg" % data[0].lower()
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
Category.objects.create(
|
|
47
|
+
instance=instance,
|
|
48
|
+
name=data[0], icon=Icon.objects.get(slug=data[1]),
|
|
49
|
+
all=i == 0, header_image=os.path.join(
|
|
50
|
+
'categories', "%s.jpg" % data[0].lower()
|
|
51
|
+
), order=i + 10
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Create generic gateway and components
|
|
55
|
+
|
|
56
|
+
generic, new = Gateway.objects.get_or_create(
|
|
57
|
+
type='simo.generic.gateways.GenericGatewayHandler'
|
|
58
|
+
)
|
|
59
|
+
generic.start()
|
|
60
|
+
dummy, new = Gateway.objects.get_or_create(
|
|
61
|
+
type='simo.generic.gateways.DummyGatewayHandler'
|
|
62
|
+
)
|
|
63
|
+
dummy.start()
|
|
64
|
+
weather_icon = Icon.objects.get(slug='cloud-bolt-sun')
|
|
65
|
+
other_zone = Zone.objects.get(name='Other')
|
|
66
|
+
climate_category = Category.objects.get(name='Climate')
|
|
67
|
+
Component.objects.create(
|
|
68
|
+
name='Weather', icon=weather_icon,
|
|
69
|
+
zone=other_zone,
|
|
70
|
+
category=climate_category,
|
|
71
|
+
gateway=generic, base_type='weather-forecast',
|
|
72
|
+
controller_uid='simo.generic.controllers.WeatherForecast',
|
|
73
|
+
config={'is_main': True}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Create default User permission roles
|
|
77
|
+
|
|
78
|
+
PermissionsRole.objects.create(
|
|
79
|
+
instance=instance, name="Admin", is_superuser=True
|
|
80
|
+
)
|
|
81
|
+
PermissionsRole.objects.create(
|
|
82
|
+
instance=instance, name="Owner", is_owner=True, is_default=True
|
|
83
|
+
)
|
|
84
|
+
PermissionsRole.objects.create(
|
|
85
|
+
instance=instance, name="Guest", is_owner=True
|
|
86
|
+
)
|
|
6
87
|
|
|
7
88
|
|
|
8
89
|
@receiver(post_save, sender=Component)
|
simo/core/tasks.py
CHANGED
|
@@ -143,8 +143,7 @@ def sync_with_remote():
|
|
|
143
143
|
).order_by('-date').first()
|
|
144
144
|
if last_event:
|
|
145
145
|
instance_data['last_event'] = last_event.date.timestamp()
|
|
146
|
-
|
|
147
|
-
instance_data['location'] = instance.location
|
|
146
|
+
|
|
148
147
|
if instance.cover_image and not instance.cover_image_synced:
|
|
149
148
|
thumbnailer = get_thumbnailer(instance.cover_image.path)
|
|
150
149
|
cover_imb_path = thumbnailer.get_thumbnail(
|
|
@@ -163,13 +162,17 @@ def sync_with_remote():
|
|
|
163
162
|
print("Faled! Response code: ", response.status_code)
|
|
164
163
|
return
|
|
165
164
|
|
|
166
|
-
|
|
165
|
+
r_json = response.json()
|
|
166
|
+
|
|
167
|
+
print("Responded with: ", json.dumps(r_json))
|
|
168
|
+
|
|
169
|
+
if 'hub_uid' in r_json:
|
|
170
|
+
dynamic_settings['core__hub_uid'] = r_json['hub_uid']
|
|
167
171
|
|
|
168
172
|
for instance in instances:
|
|
169
173
|
instance.cover_image_synced = True
|
|
170
174
|
instance.save()
|
|
171
175
|
|
|
172
|
-
r_json = response.json()
|
|
173
176
|
dynamic_settings['core__remote_http'] = r_json.get('hub_remote_http')
|
|
174
177
|
if 'new_secret' in r_json:
|
|
175
178
|
dynamic_settings['core__hub_secret'] = r_json['new_secret']
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/fleet/admin.py
CHANGED
|
@@ -168,10 +168,29 @@ class ColonelAdmin(admin.ModelAdmin):
|
|
|
168
168
|
return mark_safe('<img src="%s" alt="False">' % static('admin/img/icon-no.svg'))
|
|
169
169
|
|
|
170
170
|
|
|
171
|
+
@admin.register(Interface)
|
|
172
|
+
class InterfaceAdmin(admin.ModelAdmin):
|
|
173
|
+
list_filter = 'colonel', 'type'
|
|
174
|
+
actions = 'broadcast_reset'
|
|
175
|
+
|
|
176
|
+
def broadcast_reset(self, request, queryset):
|
|
177
|
+
broadcasted = 0
|
|
178
|
+
for interface in queryset.filter(
|
|
179
|
+
colonel__socket_connected=True
|
|
180
|
+
):
|
|
181
|
+
interface.broadcast_reset()
|
|
182
|
+
broadcasted += 1
|
|
183
|
+
|
|
184
|
+
if broadcasted:
|
|
185
|
+
self.message_user(
|
|
186
|
+
request,
|
|
187
|
+
f"Reset command was broadcased to {broadcasted} interfaces."
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
self.message_user(
|
|
191
|
+
request,
|
|
192
|
+
f"No reset command was broadcasted, "
|
|
193
|
+
f"probably because they are out of reach at the moment."
|
|
194
|
+
)
|
|
171
195
|
|
|
172
|
-
|
|
173
|
-
# @admin.register(BLEDevice)
|
|
174
|
-
# class BLEDeviceAdmin(admin.ModelAdmin):
|
|
175
|
-
# list_display = ['name', 'mac', 'type', 'last_seen']
|
|
176
|
-
# readonly_fields = list_display + ['addr']
|
|
177
|
-
# fields = readonly_fields
|
|
196
|
+
broadcast_reset.short_description = "Broadcast RESET command"
|
simo/fleet/controllers.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from django.utils.translation import gettext_lazy as _
|
|
2
3
|
from simo.core.events import GatewayObjectCommand
|
|
3
4
|
from simo.core.controllers import (
|
|
@@ -24,7 +25,8 @@ from .forms import (
|
|
|
24
25
|
ColonelDHTSensorConfigForm, DS18B20SensorConfigForm,
|
|
25
26
|
BME680SensorConfigForm, MPC9808SensorConfigForm,
|
|
26
27
|
DualMotorValveForm, BlindsConfigForm, BurglarSmokeDetectorConfigForm,
|
|
27
|
-
TTLockConfigForm, DALIDeviceConfigForm,
|
|
28
|
+
TTLockConfigForm, DALIDeviceConfigForm, DaliLampForm, DaliGearGroupForm,
|
|
29
|
+
DaliOccupancySensorConfigForm, DALILightSensorConfigForm
|
|
28
30
|
)
|
|
29
31
|
|
|
30
32
|
|
|
@@ -182,14 +184,6 @@ class BasicOutputMixin:
|
|
|
182
184
|
pins.append(control_unit['pin_no'])
|
|
183
185
|
return pins
|
|
184
186
|
|
|
185
|
-
def _send_to_device(self, value):
|
|
186
|
-
GatewayObjectCommand(
|
|
187
|
-
self.component.gateway,
|
|
188
|
-
Colonel(id=self.component.config['colonel']),
|
|
189
|
-
set_val=value,
|
|
190
|
-
component_id=self.component.id,
|
|
191
|
-
).publish()
|
|
192
|
-
|
|
193
187
|
|
|
194
188
|
class Switch(FleeDeviceMixin, BasicOutputMixin, BaseSwitch):
|
|
195
189
|
config_form = ColonelSwitchConfigForm
|
|
@@ -240,7 +234,6 @@ class PWMOutput(FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
|
|
|
240
234
|
else:
|
|
241
235
|
pwm_value = 0
|
|
242
236
|
else:
|
|
243
|
-
|
|
244
237
|
val_amplitude = conf.get('max', 100) - conf.get('min', 0)
|
|
245
238
|
val_relative = value / val_amplitude
|
|
246
239
|
pwm_amplitude = conf.get('duty_max', 1023) - conf.get('duty_min', 0.0)
|
|
@@ -306,14 +299,6 @@ class TTLock(FleeDeviceMixin, Lock):
|
|
|
306
299
|
name = 'TTLock'
|
|
307
300
|
discovery_msg = _("Please activate your TTLock so it can be discovered.")
|
|
308
301
|
|
|
309
|
-
def _send_to_device(self, value):
|
|
310
|
-
GatewayObjectCommand(
|
|
311
|
-
self.component.gateway,
|
|
312
|
-
Colonel(id=self.component.config['colonel']),
|
|
313
|
-
set_val=value,
|
|
314
|
-
component_id=self.component.id,
|
|
315
|
-
).publish()
|
|
316
|
-
|
|
317
302
|
@classmethod
|
|
318
303
|
def init_discovery(self, form_cleaned_data):
|
|
319
304
|
from simo.core.models import Gateway
|
|
@@ -508,7 +493,7 @@ class DALIDevice(FleeDeviceMixin, ControllerBase):
|
|
|
508
493
|
app_widget = SingleSwitchWidget
|
|
509
494
|
|
|
510
495
|
def _validate_val(self, value, occasion=None):
|
|
511
|
-
|
|
496
|
+
return value
|
|
512
497
|
|
|
513
498
|
@classmethod
|
|
514
499
|
def init_discovery(self, form_cleaned_data):
|
|
@@ -529,63 +514,139 @@ class DALIDevice(FleeDeviceMixin, ControllerBase):
|
|
|
529
514
|
if data['discovery-result'] == 'fail':
|
|
530
515
|
if data['result'] == 1:
|
|
531
516
|
return {'error': 'DALI interface is unavailable!'}
|
|
517
|
+
elif data['result'] == 2:
|
|
518
|
+
return {'error': 'No new DALI devices were found!'}
|
|
519
|
+
elif data['result'] == 2:
|
|
520
|
+
return {'error': 'DALI line is fully occupied, no more devices can be included!'}
|
|
532
521
|
else:
|
|
533
522
|
return {'error': 'Unknown error!'}
|
|
534
523
|
|
|
535
|
-
|
|
524
|
+
from simo.core.models import Component
|
|
525
|
+
from simo.core.utils.type_constants import CONTROLLER_TYPES_MAP
|
|
526
|
+
controller_uid = 'simo.fleet.controllers.' + data['result']['type']
|
|
527
|
+
if controller_uid not in CONTROLLER_TYPES_MAP:
|
|
528
|
+
return {'error': f"Unknown controller type: {controller_uid}"}
|
|
529
|
+
|
|
530
|
+
comp = Component.objects.filter(
|
|
531
|
+
controller_uid=controller_uid,
|
|
532
|
+
meta__finalization_data__temp_id=data['result']['id']
|
|
533
|
+
).first()
|
|
534
|
+
if comp:
|
|
535
|
+
print(f"{comp} is already created.")
|
|
536
|
+
GatewayObjectCommand(
|
|
537
|
+
comp.gateway, Colonel(
|
|
538
|
+
id=comp.config['colonel']
|
|
539
|
+
), command='finalize',
|
|
540
|
+
data=comp.meta['finalization_data']
|
|
541
|
+
).publish()
|
|
542
|
+
return [comp]
|
|
543
|
+
|
|
544
|
+
controller_cls = CONTROLLER_TYPES_MAP[controller_uid]
|
|
536
545
|
|
|
537
546
|
started_with = deserialize_form_data(started_with)
|
|
538
547
|
started_with['name'] += f" {data['result']['config']['da']}"
|
|
548
|
+
started_with['controller_uid'] = controller_uid
|
|
549
|
+
started_with['base_type'] = controller_cls.base_type
|
|
539
550
|
form = controller_cls.config_form(
|
|
540
551
|
controller_uid=controller_cls.uid, data=started_with
|
|
541
552
|
)
|
|
542
553
|
|
|
543
554
|
if form.is_valid():
|
|
544
555
|
new_component = form.save()
|
|
556
|
+
new_component = Component.objects.get(id=new_component.id)
|
|
545
557
|
new_component.config.update(data.get('result', {}).get('config'))
|
|
558
|
+
|
|
559
|
+
# saving it to meta, for repeated delivery
|
|
546
560
|
new_component.meta['finalization_data'] = {
|
|
547
561
|
'temp_id': data['result']['id'],
|
|
548
562
|
'permanent_id': new_component.id,
|
|
549
|
-
'
|
|
550
|
-
'type':
|
|
551
|
-
'
|
|
552
|
-
|
|
563
|
+
'comp_config': {
|
|
564
|
+
'type': controller_uid.split('.')[-1],
|
|
565
|
+
'family': new_component.controller.family,
|
|
566
|
+
'config': json.loads(json.dumps(new_component.config))
|
|
567
|
+
}
|
|
553
568
|
}
|
|
569
|
+
# Perform default config update on initial component setup
|
|
570
|
+
new_component.meta[
|
|
571
|
+
'finalization_data'
|
|
572
|
+
]['comp_config']['config']['boot_update'] = True
|
|
554
573
|
new_component.save()
|
|
555
574
|
GatewayObjectCommand(
|
|
556
575
|
new_component.gateway, Colonel(
|
|
557
576
|
id=new_component.config['colonel']
|
|
558
577
|
), command='finalize',
|
|
559
|
-
data=new_component.meta['finalization_data']
|
|
578
|
+
data=new_component.meta['finalization_data']
|
|
560
579
|
).publish()
|
|
561
580
|
return [new_component]
|
|
562
581
|
|
|
563
582
|
# Literally impossible, but just in case...
|
|
564
583
|
return {'error': 'INVALID INITIAL DISCOVERY FORM!'}
|
|
565
584
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
585
|
+
def replace(self):
|
|
586
|
+
"""
|
|
587
|
+
Hook up brand new replacement device to the dali line
|
|
588
|
+
and execute this command on existing (dead) component instance,
|
|
589
|
+
so that it can be replaced by the new physical device.
|
|
590
|
+
"""
|
|
571
591
|
GatewayObjectCommand(
|
|
572
592
|
self.component.gateway,
|
|
573
593
|
Colonel(id=self.component.config['colonel']),
|
|
574
|
-
|
|
575
|
-
component_id=self.component.id,
|
|
594
|
+
id=self.component.id, command='call', method='replace'
|
|
576
595
|
).publish()
|
|
577
596
|
|
|
578
597
|
|
|
579
|
-
class DALILamp(
|
|
598
|
+
class DALILamp(BaseDimmer, DALIDevice):
|
|
580
599
|
family = 'dali'
|
|
581
600
|
manual_add = False
|
|
582
601
|
name = 'DALI Lamp'
|
|
583
|
-
config_form =
|
|
602
|
+
config_form = DaliLampForm
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class DALIGearGroup(FleeDeviceMixin, BaseDimmer):
|
|
606
|
+
gateway_class = FleetGatewayHandler
|
|
607
|
+
family = 'dali'
|
|
608
|
+
manual_add = True
|
|
609
|
+
name = 'DALI Gear Group'
|
|
610
|
+
config_form = DaliGearGroupForm
|
|
611
|
+
|
|
612
|
+
def _modify_member_group(self, member, group, remove=False):
|
|
613
|
+
groups = set(member.config.get('groups', []))
|
|
614
|
+
if remove:
|
|
615
|
+
if group in groups:
|
|
616
|
+
groups.remove(group)
|
|
617
|
+
else:
|
|
618
|
+
if group not in groups:
|
|
619
|
+
groups.add(group)
|
|
620
|
+
member.config['groups'] = list(groups)
|
|
621
|
+
member.save()
|
|
622
|
+
colonel = Colonel.objects.filter(
|
|
623
|
+
id=member.config.get('colonel', 0)
|
|
624
|
+
).first()
|
|
625
|
+
if not colonel:
|
|
626
|
+
return
|
|
627
|
+
GatewayObjectCommand(
|
|
628
|
+
member.gateway, colonel, id=member.id,
|
|
629
|
+
command='call', method='update_config',
|
|
630
|
+
args=[member.controller._get_colonel_config()]
|
|
631
|
+
).publish()
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class DALIRelay(BaseSwitch, DALIDevice):
|
|
635
|
+
family = 'dali'
|
|
636
|
+
manual_add = False
|
|
637
|
+
name = 'DALI Relay'
|
|
584
638
|
|
|
585
639
|
|
|
586
|
-
class
|
|
640
|
+
class DALIOccupancySensor(BaseBinarySensor, DALIDevice):
|
|
587
641
|
family = 'dali'
|
|
588
642
|
manual_add = False
|
|
589
|
-
name = 'DALI
|
|
643
|
+
name = 'DALI Occupancy Sensor'
|
|
644
|
+
config_form = DaliOccupancySensorConfigForm
|
|
590
645
|
|
|
591
646
|
|
|
647
|
+
class DALILightSensor(BaseNumericSensor, DALIDevice):
|
|
648
|
+
family = 'dali'
|
|
649
|
+
manual_add = False
|
|
650
|
+
name = 'DALI Light Sensor'
|
|
651
|
+
default_value_units = 'lux'
|
|
652
|
+
config_form = DALILightSensorConfigForm
|