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.

Files changed (62) hide show
  1. simo/cli.py +2 -17
  2. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  3. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  4. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  6. simo/core/__pycache__/managers.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  11. simo/core/admin.py +2 -2
  12. simo/core/api.py +46 -4
  13. simo/core/controllers.py +14 -7
  14. simo/core/forms.py +2 -2
  15. simo/core/managers.py +1 -5
  16. simo/core/migrations/0003_create_default_zones_and_categories.py +2 -39
  17. simo/core/migrations/0004_create_generic.py +22 -21
  18. simo/core/migrations/0013_auto_20231003_0754.py +45 -43
  19. simo/core/migrations/0018_auto_20231005_0622.py +18 -16
  20. simo/core/migrations/0033_auto_20240509_0821.py +25 -0
  21. simo/core/migrations/0034_component_error_msg.py +18 -0
  22. simo/core/migrations/0035_remove_instance_share_location.py +17 -0
  23. simo/core/migrations/0036_auto_20240521_0823.py +22 -0
  24. simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-38.pyc +0 -0
  25. simo/core/migrations/__pycache__/0004_create_generic.cpython-38.pyc +0 -0
  26. simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-38.pyc +0 -0
  27. simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-38.pyc +0 -0
  28. simo/core/migrations/__pycache__/0033_auto_20240509_0821.cpython-38.pyc +0 -0
  29. simo/core/migrations/__pycache__/0034_component_error_msg.cpython-38.pyc +0 -0
  30. simo/core/migrations/__pycache__/0035_remove_instance_share_location.cpython-38.pyc +0 -0
  31. simo/core/migrations/__pycache__/0036_auto_20240521_0823.cpython-38.pyc +0 -0
  32. simo/core/models.py +26 -26
  33. simo/core/serializers.py +12 -9
  34. simo/core/signal_receivers.py +82 -1
  35. simo/core/tasks.py +7 -4
  36. simo/fleet/__pycache__/admin.cpython-38.pyc +0 -0
  37. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  38. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  39. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  40. simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
  41. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  42. simo/fleet/admin.py +25 -6
  43. simo/fleet/controllers.py +97 -36
  44. simo/fleet/forms.py +162 -9
  45. simo/fleet/migrations/0035_auto_20240514_0855.py +32 -0
  46. simo/fleet/migrations/__pycache__/0035_auto_20240514_0855.cpython-38.pyc +0 -0
  47. simo/fleet/models.py +97 -82
  48. simo/fleet/serializers.py +8 -1
  49. simo/fleet/socket_consumers.py +3 -15
  50. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  51. simo/users/__pycache__/utils.cpython-38.pyc +0 -0
  52. simo/users/migrations/0003_create_roles_and_system_user.py +24 -23
  53. simo/users/migrations/0019_auto_20231221_1155.py +9 -8
  54. simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-38.pyc +0 -0
  55. simo/users/migrations/__pycache__/0019_auto_20231221_1155.cpython-38.pyc +0 -0
  56. simo/users/models.py +6 -7
  57. simo/users/utils.py +0 -4
  58. {simo-2.0.32.dist-info → simo-2.0.34.dist-info}/METADATA +1 -1
  59. {simo-2.0.32.dist-info → simo-2.0.34.dist-info}/RECORD +62 -52
  60. {simo-2.0.32.dist-info → simo-2.0.34.dist-info}/LICENSE.md +0 -0
  61. {simo-2.0.32.dist-info → simo-2.0.34.dist-info}/WHEEL +0 -0
  62. {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'].append(res.pk)
283
+ if res.pk not in self.discovery['result']:
284
+ self.discovery['result'].append(res.pk)
302
285
  else:
303
- self.discovery['result'].append(res)
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'].append(result.pk)
290
+ if result.pk not in self.discovery['result']:
291
+ self.discovery['result'].append(result.pk)
307
292
  else:
308
- self.discovery['result'].append(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', 'header_image', 'header_image_thumb'
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 {'discovery': 'started'}
439
- instance = form.save(commit=True)
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):
@@ -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 .models import Instance, Gateway, Component
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
- if instance.share_location:
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
- print("Responded with: ", json.dumps(response.json()))
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
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, DaliSwitchForm
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
- pass
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
- controller_cls = globals().get(data['result']['type'])
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
- 'config': {
550
- 'type': cls.uid.split('.')[-1],
551
- 'config': new_component.config,
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
- class DALIGear(DALIDevice):
568
- manual_add = False
569
-
570
- def _send_to_device(self, value):
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
- set_val=value,
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(DALIGear, BaseSwitch):
598
+ class DALILamp(BaseDimmer, DALIDevice):
580
599
  family = 'dali'
581
600
  manual_add = False
582
601
  name = 'DALI Lamp'
583
- config_form = DaliSwitchForm
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 DALIDimmableLamp(DALIGear, BaseDimmer):
640
+ class DALIOccupancySensor(BaseBinarySensor, DALIDevice):
587
641
  family = 'dali'
588
642
  manual_add = False
589
- name = 'DALI Dimmable Lamp'
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