simo 2.0.31__py3-none-any.whl → 2.0.33__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 +25 -2
  13. simo/core/controllers.py +11 -7
  14. simo/core/forms.py +1 -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/__pycache__/0003_create_default_zones_and_categories.cpython-38.pyc +0 -0
  24. simo/core/migrations/__pycache__/0004_create_generic.cpython-38.pyc +0 -0
  25. simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-38.pyc +0 -0
  26. simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-38.pyc +0 -0
  27. simo/core/migrations/__pycache__/0033_auto_20240509_0821.cpython-38.pyc +0 -0
  28. simo/core/migrations/__pycache__/0034_component_error_msg.cpython-38.pyc +0 -0
  29. simo/core/migrations/__pycache__/0035_remove_instance_share_location.cpython-38.pyc +0 -0
  30. simo/core/models.py +25 -23
  31. simo/core/serializers.py +5 -3
  32. simo/core/signal_receivers.py +82 -1
  33. simo/core/tasks.py +7 -4
  34. simo/fleet/__pycache__/admin.cpython-38.pyc +0 -0
  35. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  36. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  37. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  38. simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
  39. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  40. simo/fleet/admin.py +25 -6
  41. simo/fleet/controllers.py +82 -37
  42. simo/fleet/forms.py +142 -10
  43. simo/fleet/migrations/0035_auto_20240514_0855.py +32 -0
  44. simo/fleet/migrations/__pycache__/0035_auto_20240514_0855.cpython-38.pyc +0 -0
  45. simo/fleet/models.py +96 -82
  46. simo/fleet/serializers.py +8 -1
  47. simo/fleet/socket_consumers.py +3 -15
  48. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  49. simo/generic/gateways.py +11 -5
  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.31.dist-info → simo-2.0.33.dist-info}/METADATA +1 -1
  59. {simo-2.0.31.dist-info → simo-2.0.33.dist-info}/RECORD +62 -54
  60. {simo-2.0.31.dist-info → simo-2.0.33.dist-info}/LICENSE.md +0 -0
  61. {simo-2.0.31.dist-info → simo-2.0.33.dist-info}/WHEEL +0 -0
  62. {simo-2.0.31.dist-info → simo-2.0.33.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']}
@@ -298,14 +282,19 @@ class Gateway(DirtyFieldsMixin, models.Model, SimoAdminMixin):
298
282
  if not isinstance(result, dict) and isinstance(result, Iterable):
299
283
  for res in result:
300
284
  if isinstance(res, models.Model):
301
- self.discovery['result'].append(res.pk)
285
+ if res.pk not in self.discovery['result']:
286
+ self.discovery['result'].append(res.pk)
302
287
  else:
303
- self.discovery['result'].append(res)
288
+ if res not in self.discovery['result']:
289
+ self.discovery['result'].append(res)
304
290
  else:
305
291
  if isinstance(result, models.Model):
306
- self.discovery['result'].append(result.pk)
292
+ if result.pk not in self.discovery['result']:
293
+ self.discovery['result'].append(result.pk)
307
294
  else:
308
- self.discovery['result'].append(result)
295
+ if result not in self.discovery['result']:
296
+ self.discovery['result'].append(result)
297
+
309
298
 
310
299
  self.save(update_fields=['discovery'])
311
300
 
@@ -359,11 +348,17 @@ class Component(DirtyFieldsMixin, models.Model, SimoAdminMixin, OnChangeMixin):
359
348
  on_delete=models.SET_NULL
360
349
  )
361
350
  last_change = models.DateTimeField(
362
- null=True, editable=False, auto_now_add=True
351
+ null=True, editable=False, auto_now_add=True,
352
+ help_text="Last time component state was changed."
353
+ )
354
+ last_modified = models.DateTimeField(
355
+ auto_now_add=True, db_index=True, editable=False,
356
+ help_text="Last time component was modified."
363
357
  )
364
358
 
365
359
  last_update = models.DateTimeField(auto_now=True)
366
360
  alive = models.BooleanField(default=True)
361
+ error_msg = models.TextField(null=True, blank=True, editable=False)
367
362
  battery_level = models.PositiveIntegerField(null=True, editable=False)
368
363
 
369
364
  show_in_app = models.BooleanField(default=True, db_index=True)
@@ -480,7 +475,7 @@ def is_in_alarm(self):
480
475
  else:
481
476
  self.arm_status = 'disarmed'
482
477
 
483
- dirty_fields = self.get_dirty_fields()
478
+ dirty_fields = self.get_dirty_fields(check_relationship=True)
484
479
 
485
480
  if self.pk:
486
481
  actor = get_current_user()
@@ -503,6 +498,13 @@ def is_in_alarm(self):
503
498
  actor.last_action = timezone.now()
504
499
  actor.save()
505
500
 
501
+ modifying_fields = (
502
+ 'name', 'icon', 'zone', 'category', 'config', 'meta',
503
+ 'value_units', 'slaves', 'show_in_app', 'alarm_category'
504
+ )
505
+ if any(f in dirty_fields for f in modifying_fields):
506
+ self.last_modified = timezone.now()
507
+
506
508
  obj = super().save(*args, **kwargs)
507
509
 
508
510
  return obj
simo/core/serializers.py CHANGED
@@ -232,6 +232,7 @@ class ComponentManyToManyRelatedField(serializers.Field):
232
232
  class ComponentSerializer(FormSerializer):
233
233
  id = ObjectSerializerMethodField()
234
234
  last_change = TimestampField(read_only=True)
235
+ last_modified = TimestampField(read_only=True)
235
236
  read_only = serializers.SerializerMethodField()
236
237
  app_widget = serializers.SerializerMethodField()
237
238
  slaves = serializers.SerializerMethodField()
@@ -239,9 +240,11 @@ class ComponentSerializer(FormSerializer):
239
240
  show_in_app = ObjectSerializerMethodField()
240
241
  controller_uid = ObjectSerializerMethodField()
241
242
  alive = ObjectSerializerMethodField()
243
+ error_msg = ObjectSerializerMethodField()
242
244
  value = ObjectSerializerMethodField()
243
245
  config = ObjectSerializerMethodField()
244
246
  meta = ObjectSerializerMethodField()
247
+ alarm_category = ObjectSerializerMethodField()
245
248
  arm_status = ObjectSerializerMethodField()
246
249
  battery_level = ObjectSerializerMethodField()
247
250
  controller_methods = serializers.SerializerMethodField()
@@ -435,9 +438,8 @@ class ComponentSerializer(FormSerializer):
435
438
  if form.is_valid():
436
439
  if form.controller.is_discoverable:
437
440
  form.controller.init_discovery(form.cleaned_data)
438
- return {'discovery': 'started'}
439
- instance = form.save(commit=True)
440
- return instance
441
+ return form.save(commit=False)
442
+ return form.save(commit=True)
441
443
  raise serializers.ValidationError(form.errors)
442
444
 
443
445
  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 response:
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,7 @@ 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
28
29
  )
29
30
 
30
31
 
@@ -182,14 +183,6 @@ class BasicOutputMixin:
182
183
  pins.append(control_unit['pin_no'])
183
184
  return pins
184
185
 
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
186
 
194
187
  class Switch(FleeDeviceMixin, BasicOutputMixin, BaseSwitch):
195
188
  config_form = ColonelSwitchConfigForm
@@ -240,7 +233,6 @@ class PWMOutput(FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
240
233
  else:
241
234
  pwm_value = 0
242
235
  else:
243
-
244
236
  val_amplitude = conf.get('max', 100) - conf.get('min', 0)
245
237
  val_relative = value / val_amplitude
246
238
  pwm_amplitude = conf.get('duty_max', 1023) - conf.get('duty_min', 0.0)
@@ -306,14 +298,6 @@ class TTLock(FleeDeviceMixin, Lock):
306
298
  name = 'TTLock'
307
299
  discovery_msg = _("Please activate your TTLock so it can be discovered.")
308
300
 
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
301
  @classmethod
318
302
  def init_discovery(self, form_cleaned_data):
319
303
  from simo.core.models import Gateway
@@ -508,7 +492,7 @@ class DALIDevice(FleeDeviceMixin, ControllerBase):
508
492
  app_widget = SingleSwitchWidget
509
493
 
510
494
  def _validate_val(self, value, occasion=None):
511
- pass
495
+ return value
512
496
 
513
497
  @classmethod
514
498
  def init_discovery(self, form_cleaned_data):
@@ -529,63 +513,124 @@ class DALIDevice(FleeDeviceMixin, ControllerBase):
529
513
  if data['discovery-result'] == 'fail':
530
514
  if data['result'] == 1:
531
515
  return {'error': 'DALI interface is unavailable!'}
516
+ elif data['result'] == 2:
517
+ return {'error': 'No new DALI devices were found!'}
518
+ elif data['result'] == 2:
519
+ return {'error': 'DALI line is fully occupied, no more devices can be included!'}
532
520
  else:
533
521
  return {'error': 'Unknown error!'}
534
522
 
535
- controller_cls = globals().get(data['result']['type'])
523
+ from simo.core.models import Component
524
+ from simo.core.utils.type_constants import CONTROLLER_TYPES_MAP
525
+ controller_uid = 'simo.fleet.controllers.' + data['result']['type']
526
+ if controller_uid not in CONTROLLER_TYPES_MAP:
527
+ return {'error': f"Unknown controller type: {controller_uid}"}
528
+
529
+ comp = Component.objects.filter(
530
+ controller_uid=controller_uid,
531
+ meta__finalization_data__temp_id=data['result']['id']
532
+ ).first()
533
+ if comp:
534
+ print(f"{comp} is already created.")
535
+ GatewayObjectCommand(
536
+ comp.gateway, Colonel(
537
+ id=comp.config['colonel']
538
+ ), command='finalize',
539
+ data=comp.meta['finalization_data']
540
+ ).publish()
541
+ return [comp]
542
+
543
+ controller_cls = CONTROLLER_TYPES_MAP[controller_uid]
536
544
 
537
545
  started_with = deserialize_form_data(started_with)
538
546
  started_with['name'] += f" {data['result']['config']['da']}"
547
+ started_with['controller_uid'] = controller_uid
548
+ started_with['base_type'] = controller_cls.base_type
539
549
  form = controller_cls.config_form(
540
550
  controller_uid=controller_cls.uid, data=started_with
541
551
  )
542
552
 
543
553
  if form.is_valid():
544
554
  new_component = form.save()
555
+ new_component = Component.objects.get(id=new_component.id)
545
556
  new_component.config.update(data.get('result', {}).get('config'))
557
+
558
+ # saving it to meta, for repeated delivery
546
559
  new_component.meta['finalization_data'] = {
547
560
  'temp_id': data['result']['id'],
548
561
  'permanent_id': new_component.id,
549
- 'config': {
550
- 'type': cls.uid.split('.')[-1],
551
- 'config': new_component.config,
552
- },
562
+ 'comp_config': {
563
+ 'type': controller_uid.split('.')[-1],
564
+ 'family': new_component.controller.family,
565
+ 'config': json.loads(json.dumps(new_component.config))
566
+ }
553
567
  }
568
+ # Perform default config update on initial component setup
569
+ new_component.meta[
570
+ 'finalization_data'
571
+ ]['comp_config']['config']['boot_update'] = True
554
572
  new_component.save()
555
573
  GatewayObjectCommand(
556
574
  new_component.gateway, Colonel(
557
575
  id=new_component.config['colonel']
558
576
  ), command='finalize',
559
- data=new_component.meta['finalization_data'],
577
+ data=new_component.meta['finalization_data']
560
578
  ).publish()
561
579
  return [new_component]
562
580
 
563
581
  # Literally impossible, but just in case...
564
582
  return {'error': 'INVALID INITIAL DISCOVERY FORM!'}
565
583
 
566
-
567
- class DALIGear(DALIDevice):
568
- manual_add = False
569
-
570
- def _send_to_device(self, value):
584
+ def replace(self):
585
+ """
586
+ Hook up brand new replacement device to the dali line
587
+ and execute this command on existing (dead) component instance,
588
+ so that it can be replaced by the new physical device.
589
+ """
571
590
  GatewayObjectCommand(
572
591
  self.component.gateway,
573
592
  Colonel(id=self.component.config['colonel']),
574
- set_val=value,
575
- component_id=self.component.id,
593
+ id=self.component.id, command='call', method='replace'
576
594
  ).publish()
577
595
 
578
596
 
579
- class DALILamp(DALIGear, BaseSwitch):
597
+ class DALILamp(BaseDimmer, DALIDevice):
580
598
  family = 'dali'
581
599
  manual_add = False
582
600
  name = 'DALI Lamp'
583
- config_form = DaliSwitchForm
601
+ config_form = DaliLampForm
584
602
 
585
603
 
586
- class DALIDimmableLamp(DALIGear, BaseDimmer):
604
+ class DALIGearGroup(FleeDeviceMixin, BaseDimmer):
605
+ gateway_class = FleetGatewayHandler
587
606
  family = 'dali'
588
- manual_add = False
589
- name = 'DALI Dimmable Lamp'
607
+ manual_add = True
608
+ name = 'DALI Gear Group'
609
+ config_form = DaliGearGroupForm
610
+
611
+ def _modify_member_group(self, member, group, remove=False):
612
+ groups = set(member.config.get('groups', []))
613
+ if remove:
614
+ if group in groups:
615
+ groups.remove(group)
616
+ else:
617
+ if group not in groups:
618
+ groups.add(group)
619
+ member.config['groups'] = list(groups)
620
+ member.save()
621
+ colonel = Colonel.objects.filter(
622
+ id=member.config.get('colonel', 0)
623
+ ).first()
624
+ if not colonel:
625
+ return
626
+ GatewayObjectCommand(
627
+ member.gateway, colonel, id=member.id,
628
+ command='call', method='update_config',
629
+ args=[member.controller._get_colonel_config()]
630
+ ).publish()
590
631
 
591
632
 
633
+ class DALIRelay(BaseSwitch, DALIDevice):
634
+ family = 'dali'
635
+ manual_add = False
636
+ name = 'DALI Relay'