simo 2.0.42__py3-none-any.whl → 2.1.2__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 (124) hide show
  1. simo/__pycache__/asgi.cpython-38.pyc +0 -0
  2. simo/__pycache__/settings.cpython-38.pyc +0 -0
  3. simo/__pycache__/wsgi.cpython-38.pyc +0 -0
  4. simo/asgi.py +1 -1
  5. simo/core/__init__.py +1 -0
  6. simo/core/__pycache__/__init__.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/apps.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/auto_urls.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/dynamic_settings.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  18. simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
  19. simo/core/__pycache__/managers.cpython-38.pyc +0 -0
  20. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  21. simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
  22. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  23. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  24. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  25. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  26. simo/core/admin.py +26 -26
  27. simo/core/api.py +22 -2
  28. simo/core/api_meta.py +23 -13
  29. simo/core/app_widgets.py +6 -0
  30. simo/core/apps.py +13 -0
  31. simo/core/auto_urls.py +2 -3
  32. simo/core/base_types.py +1 -0
  33. simo/core/controllers.py +57 -0
  34. simo/core/dynamic_settings.py +0 -8
  35. simo/core/form_fields.py +93 -0
  36. simo/core/forms.py +16 -101
  37. simo/core/gateways.py +1 -1
  38. simo/core/managers.py +14 -1
  39. simo/core/migrations/0037_auto_20240606_1057.py +33 -0
  40. simo/core/migrations/0038_remove_instance_cover_image_and_more.py +30 -0
  41. simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-38.pyc +0 -0
  42. simo/core/migrations/__pycache__/0038_remove_instance_cover_image_and_more.cpython-38.pyc +0 -0
  43. simo/core/models.py +30 -16
  44. simo/core/permissions.py +6 -3
  45. simo/core/serializers.py +77 -5
  46. simo/core/signal_receivers.py +25 -0
  47. simo/core/static/admin/css/simo.css +14 -0
  48. simo/core/tasks.py +82 -49
  49. simo/core/templates/admin/controller_widgets/button.html +8 -0
  50. simo/core/templates/admin/core/component_change_form.html +97 -0
  51. simo/core/templates/admin/formset_widget.html +88 -118
  52. simo/core/templates/admin/formset_widget_old.html +122 -0
  53. simo/core/templates/admin/user_tools.html +0 -3
  54. simo/core/templates/admin/wizard/wizard_add.html +16 -9
  55. simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
  56. simo/core/utils/__pycache__/cache.cpython-38.pyc +0 -0
  57. simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
  58. simo/core/utils/admin.py +11 -0
  59. simo/core/utils/cache.py +15 -0
  60. simo/core/utils/formsets.py +11 -18
  61. simo/core/views.py +2 -85
  62. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  63. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  64. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  65. simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
  66. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  67. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  68. simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
  69. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  70. simo/fleet/auto_urls.py +7 -1
  71. simo/fleet/controllers.py +194 -31
  72. simo/fleet/forms.py +223 -87
  73. simo/fleet/gateways.py +53 -2
  74. simo/fleet/migrations/0036_auto_20240605_0702.py +68 -0
  75. simo/fleet/migrations/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.py +27 -0
  76. simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-38.pyc +0 -0
  77. simo/fleet/migrations/__pycache__/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.cpython-38.pyc +0 -0
  78. simo/fleet/models.py +35 -6
  79. simo/fleet/socket_consumers.py +1 -1
  80. simo/fleet/templates/fleet/controllers_info/button.md +16 -0
  81. simo/fleet/utils.py +31 -1
  82. simo/fleet/views.py +45 -0
  83. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  84. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  85. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  86. simo/generic/controllers.py +61 -16
  87. simo/generic/forms.py +0 -3
  88. simo/generic/gateways.py +2 -0
  89. simo/generic/templates/admin/controller_widgets/blinds.html +2 -1
  90. simo/generic/templates/admin/controller_widgets/weather_forecast.html +1 -1
  91. simo/generic/templates/generic/controllers_info/dummy.md +3 -0
  92. simo/generic/templates/generic/controllers_info/stateselect.md +2 -0
  93. simo/management/__init__.py +0 -0
  94. simo/management/__pycache__/__init__.cpython-38.pyc +0 -0
  95. simo/management/__pycache__/on_http_start.cpython-38.pyc +0 -0
  96. simo/{_hub_template → management/_hub_template}/hub/nginx.conf +2 -2
  97. simo/{auto_update.py → management/auto_update.py} +3 -0
  98. simo/{cli.py → management/copy_template.py} +3 -16
  99. simo/management/install.py +258 -0
  100. simo/{on_http_start.py → management/on_http_start.py} +22 -2
  101. simo/settings.py +20 -4
  102. simo/users/__init__.py +1 -0
  103. simo/users/__pycache__/__init__.cpython-38.pyc +0 -0
  104. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  105. simo/users/__pycache__/apps.cpython-38.pyc +0 -0
  106. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  107. simo/users/apps.py +9 -0
  108. simo/users/migrations/__pycache__/0029_alter_instanceuser_options_instanceuser_order.cpython-38.pyc +0 -0
  109. simo/users/migrations/__pycache__/0030_alter_instanceuser_options_remove_instanceuser_order.cpython-38.pyc +0 -0
  110. simo/users/models.py +16 -3
  111. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/METADATA +5 -3
  112. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/RECORD +122 -95
  113. simo-2.1.2.dist-info/entry_points.txt +2 -0
  114. simo/__pycache__/on_http_start.cpython-38.pyc +0 -0
  115. simo/wsgi.py +0 -7
  116. /simo/{_hub_template → management/_hub_template}/hub/asgi.py +0 -0
  117. /simo/{_hub_template → management/_hub_template}/hub/celeryc.py +0 -0
  118. /simo/{_hub_template → management/_hub_template}/hub/manage.py +0 -0
  119. /simo/{_hub_template → management/_hub_template}/hub/settings.py +0 -0
  120. /simo/{_hub_template → management/_hub_template}/hub/supervisor.conf +0 -0
  121. /simo/{_hub_template → management/_hub_template}/hub/urls.py +0 -0
  122. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/LICENSE.md +0 -0
  123. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/WHEEL +0 -0
  124. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/top_level.txt +0 -0
simo/fleet/forms.py CHANGED
@@ -1,3 +1,4 @@
1
+ import time
1
2
  from django import forms
2
3
  from django.utils.translation import gettext_lazy as _
3
4
  from django.forms import formset_factory
@@ -15,8 +16,9 @@ from simo.core.utils.easing import EASING_CHOICES
15
16
  from simo.core.utils.validators import validate_slaves
16
17
  from simo.core.utils.admin import AdminFormActionForm
17
18
  from simo.core.events import GatewayObjectCommand
19
+ from simo.core.form_fields import Select2ModelChoiceField, Select2ListChoiceField
18
20
  from .models import Colonel, ColonelPin, Interface
19
- from .utils import INTERFACES_PINS_MAP
21
+ from .utils import INTERFACES_PINS_MAP, get_all_control_input_choices
20
22
 
21
23
 
22
24
  class ColonelPinChoiceField(forms.ModelChoiceField):
@@ -122,12 +124,17 @@ class ColonelComponentForm(BaseComponentForm):
122
124
  updated_vals = {}
123
125
  for key, val in control.items():
124
126
  updated_vals[key] = val
125
- if key == 'pin':
126
- pin = ColonelPin.objects.get(
127
- id=self.cleaned_data['controls'][i]['pin']
128
- )
129
- pin_instances[i] = pin
130
- updated_vals['pin_no'] = pin.no
127
+ if key == 'input':
128
+ if val.startswith('pin'):
129
+ pin = ColonelPin.objects.get(
130
+ id=int(self.cleaned_data['controls'][i]['input'][4:])
131
+ )
132
+ pin_instances[i] = pin
133
+ updated_vals['pin_no'] = pin.no
134
+ elif val.startswith('button'):
135
+ updated_vals['button'] = int(
136
+ self.cleaned_data['controls'][i]['input'][7:]
137
+ )
131
138
  elif key == 'touch_threshold':
132
139
  updated_vals[key] = int(val)
133
140
  self.cleaned_data['controls'][i] = updated_vals
@@ -135,6 +142,8 @@ class ColonelComponentForm(BaseComponentForm):
135
142
  pins_in_use = []
136
143
  formset_errors = {}
137
144
  for i, control in enumerate(self.cleaned_data['controls']):
145
+ if not control['input'].startswith('pin'):
146
+ continue
138
147
  if pin_instances[i].colonel != self.cleaned_data['colonel']:
139
148
  formset_errors[i] = {
140
149
  'pin': f"{pin_instances[i]} must be from the same Colonel!"
@@ -159,44 +168,27 @@ class ColonelComponentForm(BaseComponentForm):
159
168
  if 'controls' in self.cleaned_data:
160
169
  del self.cleaned_data['controls']
161
170
 
162
- def save(self, commit=True, update_colonel=True):
163
- obj = super().save(commit)
164
- if commit and 'colonel' in self.cleaned_data and update_colonel:
165
- self.cleaned_data['colonel'].components.add(obj)
166
- self.cleaned_data['colonel'].rebuild_occupied_pins()
167
- self.cleaned_data['colonel'].save()
168
- self.cleaned_data['colonel'].update_config()
169
- return obj
170
-
171
171
 
172
- class ControlPinForm(forms.Form):
173
- pin = ColonelPinChoiceField(
174
- queryset=ColonelPin.objects.filter(input=True),
175
- widget=autocomplete.ListSelect2(
176
- url='autocomplete-colonel-pins',
177
- forward=[
178
- forward.Self(),
179
- forward.Field('colonel'),
180
- forward.Const({'input': True}, 'filters')
181
- ]
182
- )
172
+ class ControlForm(forms.Form):
173
+ input = Select2ListChoiceField(
174
+ choices=get_all_control_input_choices,
175
+ url='autocomplete-control_inputs', forward=[
176
+ forward.Self(), forward.Field('colonel'),
177
+ forward.Const({'input': True}, 'pin_filters')
178
+ ]
183
179
  )
184
- active = forms.ChoiceField(required=True, choices=(
185
- ('LOW', 'LOW'), ('HIGH', 'HIGH')
186
- ), initial='LOW')
187
180
  method = forms.ChoiceField(
181
+ label="Button type",
188
182
  required=True, choices=(
189
183
  ('momentary', "Momentary"), ('toggle', "Toggle"),
190
184
  ),
191
185
  )
192
- debounce = forms.IntegerField(
193
- required=False, help_text="Add debounce value in ms (10 - 200ms)"
194
- )
195
186
  prefix = 'controls'
196
187
 
197
188
 
198
189
  class ColonelBinarySensorConfigForm(ColonelComponentForm):
199
190
  pin = ColonelPinChoiceField(
191
+ label='Port',
200
192
  queryset=ColonelPin.objects.filter(input=True),
201
193
  widget=autocomplete.ListSelect2(
202
194
  url='autocomplete-colonel-pins',
@@ -283,8 +275,42 @@ class ColonelBinarySensorConfigForm(ColonelComponentForm):
283
275
  return super().save(commit=commit)
284
276
 
285
277
 
278
+ class ColonelButtonConfigForm(ColonelComponentForm):
279
+ pin = ColonelPinChoiceField(
280
+ label="Port",
281
+ queryset=ColonelPin.objects.filter(input=True),
282
+ widget=autocomplete.ListSelect2(
283
+ url='autocomplete-colonel-pins',
284
+ forward=[
285
+ forward.Self(),
286
+ forward.Field('colonel'),
287
+ forward.Const({'input': True}, 'filters')
288
+ ]
289
+ )
290
+ )
291
+
292
+ def __init__(self, *args, **kwargs):
293
+ super().__init__(*args, **kwargs)
294
+
295
+ def clean(self):
296
+ super().clean()
297
+ if 'colonel' not in self.cleaned_data:
298
+ return self.cleaned_data
299
+ if 'pin' not in self.cleaned_data:
300
+ return self.cleaned_data
301
+ self._clean_pin('pin')
302
+ return self.cleaned_data
303
+
304
+
305
+ def save(self, commit=True):
306
+ if 'pin' in self.cleaned_data:
307
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
308
+ return super().save(commit=commit)
309
+
310
+
286
311
  class ColonelNumericSensorConfigForm(ColonelComponentForm, NumericSensorForm):
287
312
  pin = ColonelPinChoiceField(
313
+ label="Port",
288
314
  queryset=ColonelPin.objects.filter(adc=True, input=True, native=True),
289
315
  widget=autocomplete.ListSelect2(
290
316
  url='autocomplete-colonel-pins',
@@ -337,6 +363,7 @@ class ColonelNumericSensorConfigForm(ColonelComponentForm, NumericSensorForm):
337
363
 
338
364
  class DS18B20SensorConfigForm(ColonelComponentForm, NumericSensorForm):
339
365
  pin = ColonelPinChoiceField(
366
+ label="Port",
340
367
  queryset=ColonelPin.objects.filter(input=True, native=True),
341
368
  widget=autocomplete.ListSelect2(
342
369
  url='autocomplete-colonel-pins',
@@ -376,6 +403,7 @@ class DS18B20SensorConfigForm(ColonelComponentForm, NumericSensorForm):
376
403
 
377
404
  class ColonelDHTSensorConfigForm(ColonelComponentForm):
378
405
  pin = ColonelPinChoiceField(
406
+ label="Port",
379
407
  queryset=ColonelPin.objects.filter(input=True, native=True),
380
408
  widget=autocomplete.ListSelect2(
381
409
  url='autocomplete-colonel-pins',
@@ -506,6 +534,7 @@ class MPC9808SensorConfigForm(ColonelComponentForm):
506
534
 
507
535
  class ColonelTouchSensorConfigForm(ColonelComponentForm):
508
536
  pin = ColonelPinChoiceField(
537
+ label="Port",
509
538
  queryset=ColonelPin.objects.filter(input=True, capacitive=True),
510
539
  widget=autocomplete.ListSelect2(
511
540
  url='autocomplete-colonel-pins',
@@ -544,16 +573,15 @@ class ColonelTouchSensorConfigForm(ColonelComponentForm):
544
573
 
545
574
 
546
575
  class ColonelSwitchConfigForm(ColonelComponentForm):
547
- output_pin = ColonelPinChoiceField(
576
+ output_pin = Select2ModelChoiceField(
577
+ label="Port",
548
578
  queryset=ColonelPin.objects.filter(output=True),
549
- widget=autocomplete.ListSelect2(
550
- url='autocomplete-colonel-pins',
551
- forward=[
552
- forward.Self(),
553
- forward.Field('colonel'),
554
- forward.Const({'output': True}, 'filters')
555
- ]
556
- )
579
+ url='autocomplete-colonel-pins',
580
+ forward=[
581
+ forward.Self(),
582
+ forward.Field('colonel'),
583
+ forward.Const({'output': True}, 'filters')
584
+ ]
557
585
  )
558
586
  auto_off = forms.FloatField(
559
587
  required=False, min_value=0.01, max_value=1000000000,
@@ -578,7 +606,7 @@ class ColonelSwitchConfigForm(ColonelComponentForm):
578
606
 
579
607
  controls = FormsetField(
580
608
  formset_factory(
581
- ControlPinForm, can_delete=True, can_order=True, extra=0, max_num=10
609
+ ControlForm, can_delete=True, can_order=True, extra=0, max_num=10
582
610
  )
583
611
  )
584
612
 
@@ -605,7 +633,9 @@ class ColonelSwitchConfigForm(ColonelComponentForm):
605
633
 
606
634
  if self.cleaned_data.get('output_pin') and self.cleaned_data.get('controls'):
607
635
  for ctrl in self.cleaned_data['controls']:
608
- if ctrl['pin'] == self.cleaned_data['output_pin']:
636
+ if not ctrl['input'].startswith('pin'):
637
+ continue
638
+ if int(ctrl['input'][4:]) == self.cleaned_data['output_pin'].id:
609
639
  self.add_error(
610
640
  "output_pin",
611
641
  "Can't be used as control pin at the same time!"
@@ -613,18 +643,22 @@ class ColonelSwitchConfigForm(ColonelComponentForm):
613
643
 
614
644
  return self.cleaned_data
615
645
 
616
-
617
646
  def save(self, commit=True):
618
647
  if 'output_pin' in self.cleaned_data:
619
648
  self.instance.config['output_pin_no'] = self.cleaned_data['output_pin'].no
620
649
  obj = super().save(commit=commit)
621
650
  if commit and 'slaves' in self.cleaned_data:
622
651
  obj.slaves.set(self.cleaned_data['slaves'])
652
+ if commit and self.cleaned_data.get('controls'):
653
+ GatewayObjectCommand(
654
+ self.instance.gateway, obj, command='watch_buttons'
655
+ ).publish()
623
656
  return obj
624
657
 
625
658
 
626
659
  class ColonelPWMOutputConfigForm(ColonelComponentForm):
627
660
  output_pin = ColonelPinChoiceField(
661
+ label="Port",
628
662
  queryset=ColonelPin.objects.filter(output=True),
629
663
  widget=autocomplete.ListSelect2(
630
664
  url='autocomplete-colonel-pins',
@@ -671,7 +705,7 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
671
705
  )
672
706
  on_value = forms.FloatField(
673
707
  required=True, initial=100,
674
- help_text="Component ON value when used with toggle switch"
708
+ help_text="ON value when used with toggle switch"
675
709
  )
676
710
 
677
711
  slaves = forms.ModelMultipleChoiceField(
@@ -686,7 +720,7 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
686
720
  )
687
721
  controls = FormsetField(
688
722
  formset_factory(
689
- ControlPinForm, can_delete=True, can_order=True, extra=0, max_num=10
723
+ ControlForm, can_delete=True, can_order=True, extra=0, max_num=10
690
724
  )
691
725
  )
692
726
 
@@ -711,9 +745,12 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
711
745
  self._clean_pin('output_pin')
712
746
  if 'controls' in self.cleaned_data:
713
747
  self._clean_controls()
748
+
714
749
  if self.cleaned_data.get('output_pin') and self.cleaned_data.get('controls'):
715
750
  for ctrl in self.cleaned_data['controls']:
716
- if ctrl['pin'] == self.cleaned_data['output_pin']:
751
+ if not ctrl['input'].startswith('pin'):
752
+ continue
753
+ if int(ctrl['input'][4:]) == self.cleaned_data['output_pin'].id:
717
754
  self.add_error(
718
755
  "output_pin",
719
756
  "Can't be used as control pin at the same time!"
@@ -737,7 +774,7 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
737
774
  if old.config.get('controls') != self.cleaned_data.get('controls'):
738
775
  update_colonel = True
739
776
 
740
- obj = super().save(commit=commit, update_colonel=update_colonel)
777
+ obj = super().save(commit=commit)
741
778
  if commit and 'slaves' in self.cleaned_data:
742
779
  obj.slaves.set(self.cleaned_data['slaves'])
743
780
  if not update_colonel:
@@ -747,11 +784,16 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
747
784
  obj.controller._get_colonel_config()
748
785
  ]
749
786
  ).publish()
787
+ if commit and self.cleaned_data.get('controls'):
788
+ GatewayObjectCommand(
789
+ self.instance.gateway, obj, command='watch_buttons'
790
+ ).publish()
750
791
  return obj
751
792
 
752
793
 
753
794
  class ColonelRGBLightConfigForm(ColonelComponentForm):
754
795
  output_pin = ColonelPinChoiceField(
796
+ label="Port",
755
797
  queryset=ColonelPin.objects.filter(output=True, native=True),
756
798
  widget=autocomplete.ListSelect2(
757
799
  url='autocomplete-colonel-pins',
@@ -800,7 +842,7 @@ class ColonelRGBLightConfigForm(ColonelComponentForm):
800
842
  )
801
843
  controls = FormsetField(
802
844
  formset_factory(
803
- ControlPinForm, can_delete=True, can_order=True, extra=0, max_num=2
845
+ ControlForm, can_delete=True, can_order=True, extra=0, max_num=2
804
846
  )
805
847
  )
806
848
 
@@ -808,7 +850,12 @@ class ColonelRGBLightConfigForm(ColonelComponentForm):
808
850
  if 'output_pin' in self.cleaned_data:
809
851
  self.instance.config['output_pin_no'] = \
810
852
  self.cleaned_data['output_pin'].no
811
- return super().save(commit)
853
+ obj = super().save(commit)
854
+ if commit and self.cleaned_data.get('controls'):
855
+ GatewayObjectCommand(
856
+ self.instance.gateway, obj, command='watch_buttons'
857
+ ).publish()
858
+ return obj
812
859
 
813
860
  def clean_custom_timing(self):
814
861
  custom_timing = self.cleaned_data.get('custom_timing')
@@ -840,7 +887,9 @@ class ColonelRGBLightConfigForm(ColonelComponentForm):
840
887
 
841
888
  if self.cleaned_data.get('output_pin') and self.cleaned_data.get('controls'):
842
889
  for ctrl in self.cleaned_data['controls']:
843
- if ctrl['pin'] == self.cleaned_data['output_pin']:
890
+ if not ctrl['input'].startswith('pin'):
891
+ continue
892
+ if int(ctrl['input'][4:]) == self.cleaned_data['output_pin'].id:
844
893
  self.add_error(
845
894
  "output_pin",
846
895
  "Can't be used as control pin at the same time!"
@@ -867,6 +916,7 @@ class ColonelRGBLightConfigForm(ColonelComponentForm):
867
916
 
868
917
  class DualMotorValveForm(ColonelComponentForm):
869
918
  open_pin = ColonelPinChoiceField(
919
+ label="Open Relay Port",
870
920
  queryset=ColonelPin.objects.filter(output=True),
871
921
  widget=autocomplete.ListSelect2(
872
922
  url='autocomplete-colonel-pins',
@@ -885,6 +935,7 @@ class DualMotorValveForm(ColonelComponentForm):
885
935
  initial=2, help_text="Time in seconds to open."
886
936
  )
887
937
  close_pin = ColonelPinChoiceField(
938
+ label="Close Relay Port",
888
939
  queryset=ColonelPin.objects.filter(output=True),
889
940
  widget=autocomplete.ListSelect2(
890
941
  url='autocomplete-colonel-pins',
@@ -925,11 +976,13 @@ class DualMotorValveForm(ColonelComponentForm):
925
976
  if 'close_pin' in self.cleaned_data:
926
977
  self.instance.config['close_pin_no'] = \
927
978
  self.cleaned_data['close_pin'].no
928
- return super().save(commit=commit)
979
+ obj = super().save(commit=commit)
980
+ return obj
929
981
 
930
982
 
931
983
  class BlindsConfigForm(ColonelComponentForm):
932
984
  open_pin = ColonelPinChoiceField(
985
+ label="Open Relay Port",
933
986
  queryset=ColonelPin.objects.filter(output=True),
934
987
  widget=autocomplete.ListSelect2(
935
988
  url='autocomplete-colonel-pins',
@@ -944,6 +997,7 @@ class BlindsConfigForm(ColonelComponentForm):
944
997
  choices=(('HIGH', "HIGH"), ('LOW', "LOW")),
945
998
  )
946
999
  close_pin = ColonelPinChoiceField(
1000
+ label="Close Relay Port",
947
1001
  queryset=ColonelPin.objects.filter(output=True),
948
1002
  widget=autocomplete.ListSelect2(
949
1003
  url='autocomplete-colonel-pins',
@@ -996,7 +1050,7 @@ class BlindsConfigForm(ColonelComponentForm):
996
1050
  )
997
1051
  controls = FormsetField(
998
1052
  formset_factory(
999
- ControlPinForm, can_delete=True, can_order=True, extra=0, max_num=2
1053
+ ControlForm, can_delete=True, can_order=True, extra=0, max_num=2
1000
1054
  )
1001
1055
  )
1002
1056
 
@@ -1016,8 +1070,8 @@ class BlindsConfigForm(ColonelComponentForm):
1016
1070
  self._clean_pin('close_pin')
1017
1071
 
1018
1072
  if 'controls' in self.cleaned_data:
1019
- if len(self.cleaned_data['controls']) not in (0, 2):
1020
- self.add_error('controls', "Must have 0 or 2 controls")
1073
+ if len(self.cleaned_data['controls']) not in (0, 1, 2):
1074
+ self.add_error('controls', "Must have 0, 1 or 2 controls")
1021
1075
  return self.cleaned_data
1022
1076
 
1023
1077
  if len(self.cleaned_data['controls']) == 2:
@@ -1032,22 +1086,25 @@ class BlindsConfigForm(ColonelComponentForm):
1032
1086
 
1033
1087
  self._clean_controls()
1034
1088
 
1035
- if self.cleaned_data.get('open_pin'):
1089
+ if self.cleaned_data.get('open_pin') and self.cleaned_data.get('controls'):
1036
1090
  for ctrl in self.cleaned_data['controls']:
1037
- if ctrl['pin'] == self.cleaned_data['output_pin']:
1091
+ if not ctrl['input'].startswith('pin'):
1092
+ continue
1093
+ if int(ctrl['input'][4:]) == self.cleaned_data['open_pin'].id:
1038
1094
  self.add_error(
1039
1095
  "open_pin",
1040
1096
  "Can't be used as control pin at the same time!"
1041
1097
  )
1042
1098
 
1043
- if self.cleaned_data.get('close_pin'):
1099
+ if self.cleaned_data.get('close_pin') and self.cleaned_data.get('controls'):
1044
1100
  for ctrl in self.cleaned_data['controls']:
1045
- if ctrl['pin'] == self.cleaned_data['close_pin']:
1101
+ if not ctrl['input'].startswith('pin'):
1102
+ continue
1103
+ if int(ctrl['input'][4:]) == self.cleaned_data['close_pin'].id:
1046
1104
  self.add_error(
1047
1105
  "close_pin",
1048
1106
  "Can't be used as control pin at the same time!"
1049
1107
  )
1050
-
1051
1108
  return self.cleaned_data
1052
1109
 
1053
1110
  def save(self, commit=True):
@@ -1057,11 +1114,17 @@ class BlindsConfigForm(ColonelComponentForm):
1057
1114
  if 'close_pin' in self.cleaned_data:
1058
1115
  self.instance.config['close_pin_no'] = \
1059
1116
  self.cleaned_data['close_pin'].no
1060
- return super().save(commit=commit)
1117
+ obj = super().save(commit=commit)
1118
+ if commit and self.cleaned_data.get('controls'):
1119
+ GatewayObjectCommand(
1120
+ self.instance.gateway, obj, command='watch_buttons'
1121
+ ).publish()
1122
+ return obj
1061
1123
 
1062
1124
 
1063
1125
  class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
1064
1126
  power_pin = ColonelPinChoiceField(
1127
+ label="Port",
1065
1128
  queryset=ColonelPin.objects.filter(output=True),
1066
1129
  widget=autocomplete.ListSelect2(
1067
1130
  url='autocomplete-colonel-pins',
@@ -1073,6 +1136,7 @@ class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
1073
1136
  )
1074
1137
  )
1075
1138
  sensor_pin = ColonelPinChoiceField(
1139
+ label="Port",
1076
1140
  queryset=ColonelPin.objects.filter(input=True),
1077
1141
  widget=autocomplete.ListSelect2(
1078
1142
  url='autocomplete-colonel-pins',
@@ -1144,8 +1208,6 @@ class TTLockConfigForm(ColonelComponentForm):
1144
1208
 
1145
1209
  def save(self, commit=True):
1146
1210
  obj = super(ColonelComponentForm, self).save(commit)
1147
- if commit:
1148
- self.cleaned_data['colonel'].components.add(obj)
1149
1211
  if commit and 'door_sensor' in self.cleaned_data:
1150
1212
  GatewayObjectCommand(
1151
1213
  self.instance.gateway, self.cleaned_data['door_sensor'],
@@ -1190,18 +1252,14 @@ class DALIDeviceConfigForm(ColonelComponentForm):
1190
1252
  )
1191
1253
  return self.cleaned_data
1192
1254
 
1193
- def save(self, commit=True, update_colonel_config=True):
1255
+ def save(self, commit=True):
1194
1256
  if 'interface' in self.cleaned_data:
1195
1257
  self.instance.config['dali_interface'] = \
1196
1258
  self.cleaned_data['interface'].no
1197
-
1198
- # prevent immediate config update on colonel as dali devices are
1199
- # added via pairing process, which uses finalization procedure.
1200
1259
  is_new = not self.instance.pk
1201
- obj = super(BaseComponentForm, self).save(commit=commit)
1260
+ obj = super().save(commit=commit)
1202
1261
  if commit:
1203
- self.cleaned_data['colonel'].components.add(obj)
1204
- if not is_new and update_colonel_config:
1262
+ if not is_new:
1205
1263
  GatewayObjectCommand(
1206
1264
  obj.gateway, self.cleaned_data['colonel'], id=obj.id,
1207
1265
  command='call', method='update_config', args=[
@@ -1212,12 +1270,9 @@ class DALIDeviceConfigForm(ColonelComponentForm):
1212
1270
 
1213
1271
 
1214
1272
  class DaliLampForm(DALIDeviceConfigForm, BaseComponentForm):
1215
- boot_update = forms.BooleanField(
1216
- initial=False, required=False,
1217
- help_text="Update device config on colonel boot."
1218
- )
1219
1273
  fade_time = forms.TypedChoiceField(
1220
- initial=3, coerce=int, choices=(
1274
+ initial=1, coerce=int, choices=(
1275
+ (0, "Instant"),
1221
1276
  (1, "0.7 s"), (2, "1.0 s"), (3, "1.4 s"), (4, "2.0 s"), (5, "2.8 s"),
1222
1277
  (6, "4.0 s"), (7, "5.7 s"), (8, "8.0 s")
1223
1278
  )
@@ -1232,11 +1287,73 @@ class DaliLampForm(DALIDeviceConfigForm, BaseComponentForm):
1232
1287
  "beings dying out when you hit it with lower value than it "
1233
1288
  "is capable of supplying."
1234
1289
  )
1290
+ on_value = forms.FloatField(
1291
+ required=True, initial=100,
1292
+ help_text="ON value when used with toggle switch"
1293
+ )
1235
1294
  auto_off = forms.FloatField(
1236
1295
  required=False, min_value=0.01, max_value=1000000000,
1237
1296
  help_text="If provided, lamp will be turned off after "
1238
1297
  "given amount of seconds after last turn on event."
1239
1298
  )
1299
+ controls = FormsetField(
1300
+ formset_factory(
1301
+ ControlForm, can_delete=True, can_order=True, extra=0, max_num=999
1302
+ )
1303
+ )
1304
+
1305
+ def clean(self):
1306
+ if 'controls' in self.cleaned_data:
1307
+ self._clean_controls()
1308
+ return self.cleaned_data
1309
+
1310
+ def save(self, commit=True):
1311
+ obj = super().save(commit=commit)
1312
+ if commit:
1313
+ if self.cleaned_data.get('controls'):
1314
+ GatewayObjectCommand(
1315
+ self.instance.gateway, obj, command='watch_buttons'
1316
+ ).publish()
1317
+ if self.instance.pk:
1318
+ old_controls = Component.objects.get(
1319
+ pk=self.instance.pk
1320
+ ).config.get('controls')
1321
+ if old_controls != self.cleaned_data.get('controls'):
1322
+ self.cleaned_data['colonel'].update_config()
1323
+ return obj
1324
+
1325
+
1326
+ class DaliSwitchConfigForm(DALIDeviceConfigForm, BaseComponentForm):
1327
+ auto_off = forms.FloatField(
1328
+ required=False, min_value=0.01, max_value=1000000000,
1329
+ help_text="If provided, lamp will be turned off after "
1330
+ "given amount of seconds after last turn on event."
1331
+ )
1332
+ controls = FormsetField(
1333
+ formset_factory(
1334
+ ControlForm, can_delete=True, can_order=True, extra=0, max_num=999
1335
+ )
1336
+ )
1337
+
1338
+ def clean(self):
1339
+ if 'controls' in self.cleaned_data:
1340
+ self._clean_controls()
1341
+ return self.cleaned_data
1342
+
1343
+ def save(self, commit=True):
1344
+ obj = super().save(commit=commit)
1345
+ if commit:
1346
+ if self.cleaned_data.get('controls'):
1347
+ GatewayObjectCommand(
1348
+ self.instance.gateway, obj, command='watch_buttons'
1349
+ ).publish()
1350
+ if self.instance.pk:
1351
+ old_controls = Component.objects.get(
1352
+ pk=self.instance.pk
1353
+ ).config.get('controls')
1354
+ if old_controls != self.cleaned_data.get('controls'):
1355
+ self.cleaned_data['colonel'].update_config()
1356
+ return obj
1240
1357
 
1241
1358
 
1242
1359
  class DaliGearGroupForm(DALIDeviceConfigForm, BaseComponentForm):
@@ -1259,6 +1376,15 @@ class DaliGearGroupForm(DALIDeviceConfigForm, BaseComponentForm):
1259
1376
  )
1260
1377
  )
1261
1378
  )
1379
+ on_value = forms.FloatField(
1380
+ required=True, initial=100,
1381
+ help_text="ON value when used with toggle switch"
1382
+ )
1383
+ controls = FormsetField(
1384
+ formset_factory(
1385
+ ControlForm, can_delete=True, can_order=True, extra=0, max_num=999
1386
+ )
1387
+ )
1262
1388
 
1263
1389
  def clean(self):
1264
1390
  if 'members' in self.cleaned_data:
@@ -1290,15 +1416,16 @@ class DaliGearGroupForm(DALIDeviceConfigForm, BaseComponentForm):
1290
1416
  )
1291
1417
  else:
1292
1418
  self.group_addr = self.instance.config['da']
1419
+ if 'controls' in self.cleaned_data:
1420
+ self._clean_controls()
1293
1421
  return self.cleaned_data
1294
1422
 
1295
- def save(self, commit=True, update_colonel_config=True):
1423
+ def save(self, commit=True):
1296
1424
  old_members = self.instance.config.get('members', [])
1297
1425
  self.instance.config['da'] = self.group_addr
1298
1426
  is_new = not self.instance.pk
1299
- obj = super().save(commit, update_colonel_config=False)
1427
+ obj = super().save(commit)
1300
1428
  if commit:
1301
- self.cleaned_data['colonel'].components.add(obj)
1302
1429
  new_members = obj.config.get('members', [])
1303
1430
  for removed_member in Component.objects.filter(
1304
1431
  id__in=set(old_members) - set(new_members)
@@ -1329,14 +1456,22 @@ class DaliGearGroupForm(DALIDeviceConfigForm, BaseComponentForm):
1329
1456
  obj.controller._get_colonel_config()
1330
1457
  ]
1331
1458
  ).publish()
1459
+
1460
+ if commit:
1461
+ if self.cleaned_data.get('controls'):
1462
+ GatewayObjectCommand(
1463
+ self.instance.gateway, obj, command='watch_buttons'
1464
+ ).publish()
1465
+ if self.instance.pk:
1466
+ old_controls = Component.objects.get(
1467
+ pk=self.instance.pk
1468
+ ).config.get('controls')
1469
+ if old_controls != self.cleaned_data.get('controls'):
1470
+ self.cleaned_data['colonel'].update_config()
1332
1471
  return obj
1333
1472
 
1334
1473
 
1335
1474
  class DaliOccupancySensorConfigForm(DALIDeviceConfigForm, BaseComponentForm):
1336
- boot_update = forms.BooleanField(
1337
- initial=False, required=False,
1338
- help_text="Update device config on colonel boot."
1339
- )
1340
1475
  hold_time = forms.TypedChoiceField(
1341
1476
  initial=3, coerce=int, choices=(
1342
1477
  (1, "10 s"), (2, "20 s"), (3, "30 s"), (4, "40 s"), (5, "50 s"),
@@ -1347,7 +1482,8 @@ class DaliOccupancySensorConfigForm(DALIDeviceConfigForm, BaseComponentForm):
1347
1482
 
1348
1483
 
1349
1484
  class DALILightSensorConfigForm(DALIDeviceConfigForm, BaseComponentForm):
1350
- boot_update = forms.BooleanField(
1351
- initial=False, required=False,
1352
- help_text="Update device config on colonel boot."
1353
- )
1485
+ pass
1486
+
1487
+
1488
+ class DALIButtonConfigForm(DALIDeviceConfigForm, BaseComponentForm):
1489
+ pass