simo 2.5.3__py3-none-any.whl → 2.5.4__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 (47) hide show
  1. simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
  2. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  3. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  4. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  6. simo/core/app_widgets.py +19 -1
  7. simo/core/base_types.py +2 -0
  8. simo/core/controllers.py +154 -4
  9. simo/core/management/_hub_template/hub/supervisor.conf +4 -0
  10. simo/core/signal_receivers.py +8 -5
  11. simo/core/tasks.py +1 -1
  12. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  13. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  14. simo/fleet/controllers.py +20 -119
  15. simo/fleet/forms.py +101 -0
  16. simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
  17. simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
  18. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  19. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  20. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  21. simo/generic/app_widgets.py +0 -18
  22. simo/generic/base_types.py +0 -2
  23. simo/generic/controllers.py +2 -262
  24. simo/generic/forms.py +0 -49
  25. simo/generic/gateways.py +5 -118
  26. simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
  27. simo/generic/scripting/example.py +67 -0
  28. simo/generic/scripting/helpers.py +66 -10
  29. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  30. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  31. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  32. simo/users/admin.py +25 -5
  33. simo/users/api.py +25 -11
  34. simo/users/migrations/0035_instanceuser_last_seen_speed_kmh_and_more.py +23 -0
  35. simo/users/migrations/0036_instanceuser_phone_on_charge_user_phone_on_charge.py +23 -0
  36. simo/users/migrations/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.py +53 -0
  37. simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-38.pyc +0 -0
  38. simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-38.pyc +0 -0
  39. simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-38.pyc +0 -0
  40. simo/users/models.py +12 -55
  41. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/METADATA +1 -1
  42. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/RECORD +46 -40
  43. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/WHEEL +1 -1
  44. simo/scripting.py +0 -39
  45. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/LICENSE.md +0 -0
  46. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/entry_points.txt +0 -0
  47. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/top_level.txt +0 -0
Binary file
simo/core/app_widgets.py CHANGED
@@ -104,4 +104,22 @@ class LockWidget(BaseAppWidget):
104
104
  class AirQualityWidget(BaseAppWidget):
105
105
  uid = 'air-quality'
106
106
  name = _("Air Quality")
107
- size = [2, 2]
107
+ size = [2, 2]
108
+
109
+
110
+ class GateWidget(BaseAppWidget):
111
+ uid = 'gate'
112
+ name = _('Gate')
113
+ size = [2, 1]
114
+
115
+
116
+ class BlindsWidget(BaseAppWidget):
117
+ uid = 'blinds'
118
+ name = _('Blinds')
119
+ size = [4, 1]
120
+
121
+
122
+ class SlidesWidget(BaseAppWidget):
123
+ uid = 'slides'
124
+ name = _('Slides')
125
+ size = [2, 1]
simo/core/base_types.py CHANGED
@@ -18,4 +18,6 @@ BASE_TYPES = {
18
18
  'switch-quadruple': _("Switch Quadruple"),
19
19
  'switch-quintuple': _("Switch Quintuple"),
20
20
  'lock': _("Lock"),
21
+ 'gate': _("Gate"),
22
+ 'blinds': _("Blinds"),
21
23
  }
simo/core/controllers.py CHANGED
@@ -777,10 +777,8 @@ class Switch(MultiSwitchBase, TimerMixin, OnOffPokerMixin):
777
777
  Gateway specific implementation is very welcome of this!
778
778
  '''
779
779
  self.turn_on()
780
- def toggle_back():
781
- time.sleep(0.5)
782
- self.turn_off()
783
- threading.Thread(target=toggle_back).start()
780
+ time.sleep(0.5)
781
+ self.turn_off()
784
782
 
785
783
 
786
784
  class DoubleSwitch(MultiSwitchBase):
@@ -899,3 +897,155 @@ class Lock(Switch):
899
897
  self.component.save(
900
898
  update_fields=['change_init_by', 'change_init_date']
901
899
  )
900
+
901
+
902
+ class Blinds(ControllerBase, TimerMixin):
903
+ name = _("Blind")
904
+ base_type = 'blinds'
905
+ admin_widget_template = 'admin/controller_widgets/blinds.html'
906
+ default_config = {}
907
+
908
+ @property
909
+ def app_widget(self):
910
+ if self.component.config.get('control_mode') == 'slide':
911
+ return SlidesWidget
912
+ else:
913
+ return BlindsWidget
914
+
915
+ @property
916
+ def default_value(self):
917
+ # target and current positions in milliseconds, angle in degrees (0 - 180)
918
+ return {'target': 0, 'position': 0, 'angle': 0}
919
+
920
+ def _validate_val(self, value, occasion=None):
921
+
922
+ if occasion == BEFORE_SEND:
923
+ if isinstance(value, int) or isinstance(value, float):
924
+ # legacy support
925
+ value = {'target': int(value)}
926
+ if 'target' not in value:
927
+ raise ValidationError("Target value is required!")
928
+ target = value.get('target')
929
+ if type(target) not in (float, int):
930
+ raise ValidationError(
931
+ "Bad target position for blinds to go."
932
+ )
933
+ if target > self.component.config.get('open_duration') * 1000:
934
+ raise ValidationError(
935
+ "Target value lower than %d expected, "
936
+ "%d received instead" % (
937
+ self.component.config['open_duration'] * 1000,
938
+ target
939
+ )
940
+ )
941
+ if 'angle' in value:
942
+ try:
943
+ angle = int(value['angle'])
944
+ except:
945
+ raise ValidationError(
946
+ "Integer between 0 - 180 is required for blinds angle."
947
+ )
948
+ if angle < 0 or angle > 180:
949
+ raise ValidationError(
950
+ "Integer between 0 - 180 is required for blinds angle."
951
+ )
952
+ else:
953
+ value['angle'] = self.component.value.get('angle', 0)
954
+
955
+ elif occasion == BEFORE_SET:
956
+ if not isinstance(value, dict):
957
+ raise ValidationError("Dictionary is expected")
958
+ for key, val in value.items():
959
+ if key not in ('target', 'position', 'angle'):
960
+ raise ValidationError(
961
+ "'target', 'position' or 'angle' parameters are expected."
962
+ )
963
+ if key == 'position':
964
+ if val < 0:
965
+ raise ValidationError(
966
+ "Positive integer expected for blind position"
967
+ )
968
+ if val > self.component.config.get('open_duration') * 1000:
969
+ raise ValidationError(
970
+ "Positive value is to big. Must be lower than %d, "
971
+ "but you have provided %d" % (
972
+ self.component.config.get('open_duration') * 1000, val
973
+ )
974
+ )
975
+
976
+ self.component.refresh_from_db()
977
+ if 'target' not in value:
978
+ value['target'] = self.component.value.get('target')
979
+ if 'position' not in value:
980
+ value['position'] = self.component.value.get('position')
981
+ if 'angle' not in value:
982
+ value['angle'] = self.component.value.get('angle')
983
+
984
+ return value
985
+
986
+ def open(self):
987
+ send_val = {'target': 0}
988
+ angle = self.component.value.get('angle')
989
+ if angle is not None and 0 <= angle <= 180:
990
+ send_val['angle'] = angle
991
+ self.send(send_val)
992
+
993
+ def close(self):
994
+ send_val = {'target': self.component.config['open_duration'] * 1000}
995
+ angle = self.component.value.get('angle')
996
+ if angle is not None and 0 <= angle <= 180:
997
+ send_val['angle'] = angle
998
+ self.send(send_val)
999
+
1000
+ def stop(self):
1001
+ send_val = {'target': -1}
1002
+ angle = self.component.value.get('angle')
1003
+ if angle is not None and 0 <= angle <= 180:
1004
+ send_val['angle'] = angle
1005
+ self.send(send_val)
1006
+
1007
+
1008
+ class Gate(ControllerBase, TimerMixin):
1009
+ name = _("Gate")
1010
+ base_type = 'gate'
1011
+ app_widget = GateWidget
1012
+ admin_widget_template = 'admin/controller_widgets/gate.html'
1013
+ default_config = {}
1014
+
1015
+ @property
1016
+ def default_value(self):
1017
+ return 'closed'
1018
+
1019
+ def _validate_val(self, value, occasion=None):
1020
+ if occasion == BEFORE_SEND:
1021
+ if self.component.config.get('action_method') == 'click':
1022
+ if value != 'call':
1023
+ raise ValidationError(
1024
+ 'Gate component understands only one command: '
1025
+ '"call". You have provided: "%s"' % (str(value))
1026
+ )
1027
+ else:
1028
+ if value not in ('call', 'open', 'close'):
1029
+ raise ValidationError(
1030
+ 'This gate component understands only 3 commands: '
1031
+ '"open", "close" and "call". You have provided: "%s"' %
1032
+ (str(value))
1033
+ )
1034
+ elif occasion == BEFORE_SET and value not in (
1035
+ 'closed', 'open', 'open_moving', 'closed_moving'
1036
+ ):
1037
+ raise ValidationError(
1038
+ 'Gate component can only be in 4 states: '
1039
+ '"closed", "closed", "open_moving", "closed_moving". '
1040
+ 'You have provided: "%s"' % (str(value))
1041
+ )
1042
+ return value
1043
+
1044
+ def open(self):
1045
+ self.send('open')
1046
+
1047
+ def close(self):
1048
+ self.send('close')
1049
+
1050
+ def call(self):
1051
+ self.send('call')
@@ -27,6 +27,8 @@ stdout_logfile_backups=3
27
27
  redirect_stderr=true
28
28
  autostart=true
29
29
  autorestart=true
30
+ stopasgroup=true
31
+ killasgroup=true
30
32
 
31
33
 
32
34
  [program:simo-gateways]
@@ -41,6 +43,8 @@ stdout_logfile_backups=3
41
43
  redirect_stderr=true
42
44
  autostart=true
43
45
  autorestart=true
46
+ stopasgroup=true
47
+ killasgroup=true
44
48
 
45
49
  [program:simo-celery-beat]
46
50
  directory={{ project_dir }}/hub/
@@ -26,10 +26,12 @@ def create_instance_defaults(sender, instance, created, **kwargs):
26
26
  # Create default zones
27
27
 
28
28
  for zone_name in (
29
- 'Living Room', 'Kitchen', 'Bathroom', 'Porch', 'Garage', 'Yard', 'Other'
29
+ 'Living Room', 'Kitchen', 'Bathroom', 'Porch', 'Garage', 'Yard',
30
30
  ):
31
31
  Zone.objects.create(instance=instance, name=zone_name)
32
32
 
33
+ other_zone = Zone.objects.create(instance=instance, name='Other')
34
+
33
35
  core_dir_path = os.path.dirname(os.path.realpath(__file__))
34
36
  imgs_folder = os.path.join(
35
37
  core_dir_path, 'static/defaults/category_headers'
@@ -40,7 +42,7 @@ def create_instance_defaults(sender, instance, created, **kwargs):
40
42
  os.makedirs(categories_media_dir)
41
43
 
42
44
  # Create default categories
43
-
45
+ climate_category = None
44
46
  for i, data in enumerate([
45
47
  ("All", 'star'), ("Climate", 'temperature-half'),
46
48
  ("Lights", 'lightbulb'), ("Security", 'eye'),
@@ -52,13 +54,15 @@ def create_instance_defaults(sender, instance, created, **kwargs):
52
54
  settings.MEDIA_ROOT, 'categories', "%s.jpg" % data[0].lower()
53
55
  )
54
56
  )
55
- Category.objects.create(
57
+ cat = Category.objects.create(
56
58
  instance=instance,
57
59
  name=data[0], icon=Icon.objects.get(slug=data[1]),
58
60
  all=i == 0, header_image=os.path.join(
59
61
  'categories', "%s.jpg" % data[0].lower()
60
62
  ), order=i + 10
61
63
  )
64
+ if cat.name == 'Climate':
65
+ climate_category = cat
62
66
 
63
67
  # Create generic gateway and components
64
68
 
@@ -71,8 +75,7 @@ def create_instance_defaults(sender, instance, created, **kwargs):
71
75
  )
72
76
  dummy.start()
73
77
  weather_icon = Icon.objects.get(slug='cloud-bolt-sun')
74
- other_zone = Zone.objects.get(name='Other', instance=instance)
75
- climate_category = Category.objects.get(name='Climate', instance=instance)
78
+
76
79
  Component.objects.create(
77
80
  name='Weather', icon=weather_icon,
78
81
  zone=other_zone,
simo/core/tasks.py CHANGED
@@ -143,7 +143,7 @@ def sync_with_remote():
143
143
 
144
144
  for user in User.objects.filter(
145
145
  Q(roles__instance=instance) | Q(is_master=True)
146
- ).exclude(email__in=('system@simo.io', 'device@simo.io')):
146
+ ).exclude(email__in=('system@simo.io', 'device@simo.io')).distinct():
147
147
  is_superuser = False
148
148
  user_role = user.get_role(instance)
149
149
  if user_role and user_role.is_superuser:
Binary file
simo/fleet/controllers.py CHANGED
@@ -8,7 +8,8 @@ from simo.core.controllers import (
8
8
  Button as BaseButton,
9
9
  NumericSensor as BaseNumericSensor,
10
10
  Switch as BaseSwitch, Dimmer as BaseDimmer,
11
- MultiSensor as BaseMultiSensor, RGBWLight as BaseRGBWLight
11
+ MultiSensor as BaseMultiSensor, RGBWLight as BaseRGBWLight,
12
+ Blinds as BaseBlinds, Gate as BaseGate
12
13
  )
13
14
  from simo.core.app_widgets import NumericSensorWidget, AirQualityWidget
14
15
  from simo.core.controllers import Lock, ControllerBase, SingleSwitchWidget
@@ -16,8 +17,7 @@ from simo.core.utils.helpers import heat_index
16
17
  from simo.core.utils.serialization import (
17
18
  serialize_form_data, deserialize_form_data
18
19
  )
19
- from simo.generic.controllers import Blinds as GenericBlinds
20
- from .models import Colonel, ColonelPin
20
+ from .models import Colonel
21
21
  from .gateways import FleetGatewayHandler
22
22
  from .forms import (
23
23
  ColonelPinChoiceField,
@@ -26,7 +26,8 @@ from .forms import (
26
26
  ColonelNumericSensorConfigForm, ColonelRGBLightConfigForm,
27
27
  ColonelDHTSensorConfigForm, DS18B20SensorConfigForm,
28
28
  BME680SensorConfigForm, MPC9808SensorConfigForm, ENS160SensorConfigForm,
29
- DualMotorValveForm, BlindsConfigForm, BurglarSmokeDetectorConfigForm,
29
+ DualMotorValveForm, BlindsConfigForm, GateConfigForm,
30
+ BurglarSmokeDetectorConfigForm,
30
31
  TTLockConfigForm, DALIDeviceConfigForm, DaliLampForm, DaliGearGroupForm,
31
32
  DaliSwitchConfigForm,
32
33
  DaliOccupancySensorConfigForm, DALILightSensorConfigForm,
@@ -71,9 +72,6 @@ class FleeDeviceMixin:
71
72
  config[key] = val
72
73
  return config
73
74
 
74
- def _fix_pin_relations(self):
75
- pass
76
-
77
75
 
78
76
  class BasicSensorMixin:
79
77
  gateway_class = FleetGatewayHandler
@@ -83,21 +81,6 @@ class BasicSensorMixin:
83
81
  self.component.config['pin_no'],
84
82
  ]
85
83
 
86
- @atomic
87
- def _fix_pin_relations(self):
88
- colonel = Colonel.objects.filter(
89
- id=self.component.config.get('colonel', 0)
90
- ).first()
91
- if not colonel:
92
- return
93
- cp = ColonelPin.objects.filter(
94
- colonel=colonel, no=self.component.config['pin_no']
95
- ).first()
96
- if self.component.config.get('pin') != cp.id:
97
- self.component.config['pin'] = cp.id
98
- self.component.save()
99
-
100
-
101
84
  class BinarySensor(FleeDeviceMixin, BasicSensorMixin, BaseBinarySensor):
102
85
  config_form = ColonelBinarySensorConfigForm
103
86
 
@@ -116,26 +99,6 @@ class BurglarSmokeDetector(BinarySensor):
116
99
  self.component.config['sensor_pin_no']
117
100
  ]
118
101
 
119
- @atomic
120
- def _fix_pin_relations(self):
121
- colonel = Colonel.objects.filter(
122
- id=self.component.config.get('colonel', 0)
123
- ).first()
124
- if not colonel:
125
- return
126
- cp = ColonelPin.objects.filter(
127
- colonel=colonel, no=self.component.config['power_pin_no']
128
- ).first()
129
- if self.component.config.get('power_pin') != cp.id:
130
- self.component.config['power_pin'] = cp.id
131
- self.component.save()
132
- cp = ColonelPin.objects.filter(
133
- colonel=colonel, no=self.component.config['sensor_pin_no']
134
- ).first()
135
- if self.component.config.get('sensor_pin') != cp.id:
136
- self.component.config['sensor_pin'] = cp.id
137
- self.component.save()
138
-
139
102
 
140
103
  # class AnalogSensor(FleeDeviceMixin, BasicSensorMixin, BaseNumericSensor):
141
104
  # config_form = ColonelNumericSensorConfigForm
@@ -281,34 +244,6 @@ class BasicOutputMixin:
281
244
  pins.append(ctrl['pin_no'])
282
245
  return pins
283
246
 
284
- @atomic
285
- def _fix_pin_relations(self):
286
- colonel = Colonel.objects.filter(
287
- id=self.component.config.get('colonel', 0)
288
- ).first()
289
- if not colonel:
290
- return
291
- cp = ColonelPin.objects.filter(
292
- colonel=colonel, no=self.component.config['output_pin_no']
293
- ).first()
294
- if self.component.config.get('output_pin') != cp.id:
295
- self.component.config['output_pin'] = cp.id
296
-
297
- for ctrl in self.component.config.get('controls', []):
298
- if 'pin_no' not in ctrl:
299
- continue
300
- if not ctrl.get('input').startswith('pin-'):
301
- continue
302
- cp = ColonelPin.objects.filter(
303
- colonel=colonel, no=ctrl['pin_no']
304
- ).first()
305
- if not cp:
306
- continue
307
- ctrl['input'] = f'pin-{cp.id}'
308
-
309
- self.component.save()
310
-
311
-
312
247
  def _ctrl(self, ctrl_no, ctrl_event, method):
313
248
  GatewayObjectCommand(
314
249
  self.component.gateway,
@@ -485,26 +420,6 @@ class DualMotorValve(FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
485
420
  self.component.config['close_pin_no']
486
421
  ]
487
422
 
488
- @atomic
489
- def _fix_pin_relations(self):
490
- colonel = Colonel.objects.filter(
491
- id=self.component.config.get('colonel', 0)
492
- ).first()
493
- if not colonel:
494
- return
495
- cp = ColonelPin.objects.filter(
496
- colonel=colonel, no=self.component.config['open_pin_no']
497
- ).first()
498
- if self.component.config.get('open_pin') != cp.id:
499
- self.component.config['open_pin'] = cp.id
500
- cp = ColonelPin.objects.filter(
501
- colonel=colonel, no=self.component.config['close_pin_no']
502
- ).first()
503
- if self.component.config.get('close_pin') != cp.id:
504
- self.component.config['close_pin'] = cp.id
505
-
506
- self.component.save()
507
-
508
423
  def _prepare_for_send(self, value):
509
424
  conf = self.component.config
510
425
  if value >= conf.get('max', 100):
@@ -525,7 +440,7 @@ class DualMotorValve(FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
525
440
  return conf.get('min', 0) + (value / 100) * val_amplitude
526
441
 
527
442
 
528
- class Blinds(FleeDeviceMixin, BasicOutputMixin, GenericBlinds):
443
+ class Blinds(FleeDeviceMixin, BasicOutputMixin, BaseBlinds):
529
444
  gateway_class = FleetGatewayHandler
530
445
  config_form = BlindsConfigForm
531
446
 
@@ -539,35 +454,21 @@ class Blinds(FleeDeviceMixin, BasicOutputMixin, GenericBlinds):
539
454
  pins.append(ctrl['pin_no'])
540
455
  return pins
541
456
 
542
- @atomic
543
- def _fix_pin_relations(self):
544
- colonel = Colonel.objects.filter(
545
- id=self.component.config.get('colonel', 0)
546
- ).first()
547
- if not colonel:
548
- return
549
- cp = ColonelPin.objects.filter(
550
- colonel=colonel, no=self.component.config['open_pin_no']
551
- ).first()
552
- if self.component.config.get('open_pin') != cp.id:
553
- self.component.config['open_pin'] = cp.id
554
- cp = ColonelPin.objects.filter(
555
- colonel=colonel, no=self.component.config['close_pin_no']
556
- ).first()
557
- if self.component.config.get('close_pin') != cp.id:
558
- self.component.config['close_pin'] = cp.id
457
+
458
+ class Gate(FleeDeviceMixin, BasicOutputMixin, BaseGate):
459
+ gateway_class = FleetGatewayHandler
460
+ config_form = GateConfigForm
461
+
462
+ def _get_occupied_pins(self):
463
+ pins = [
464
+ self.component.config['control_pin_no'],
465
+ self.component.config['sensor_pin_no']
466
+ ]
559
467
  for ctrl in self.component.config.get('controls', []):
560
- if 'pin_no' not in ctrl:
561
- continue
562
- if not ctrl.get('input').startswith('pin-'):
563
- continue
564
- cp = ColonelPin.objects.filter(
565
- colonel=colonel, no=ctrl['pin_no']
566
- ).first()
567
- if not cp:
568
- continue
569
- ctrl['input'] = f'pin-{cp.id}'
570
- self.component.save()
468
+ if 'pin_no' in ctrl:
469
+ pins.append(ctrl['pin_no'])
470
+ return pins
471
+
571
472
 
572
473
 
573
474
  class TTLock(FleeDeviceMixin, Lock):
simo/fleet/forms.py CHANGED
@@ -1256,6 +1256,107 @@ class BlindsConfigForm(ColonelComponentForm):
1256
1256
  return obj
1257
1257
 
1258
1258
 
1259
+ class GateConfigForm(ColonelComponentForm):
1260
+ control_pin = Select2ModelChoiceField(
1261
+ label="Control Relay Port",
1262
+ queryset=ColonelPin.objects.filter(output=True),
1263
+ url='autocomplete-colonel-pins',
1264
+ forward=[
1265
+ forward.Self(),
1266
+ forward.Field('colonel'),
1267
+ forward.Const({'output': True}, 'filters')
1268
+ ]
1269
+ )
1270
+ open_action = forms.ChoiceField(
1271
+ choices=(('HIGH', "HIGH"), ('LOW', "LOW")), initial='HIGH'
1272
+ )
1273
+ control_method = forms.ChoiceField(
1274
+ choices=(('pulse', "Pulse"), ('hold', "Hold")), initial="pulse",
1275
+ help_text="What your gate motor expects to receive as control command?"
1276
+ )
1277
+ sensor_pin = Select2ModelChoiceField(
1278
+ label='Gate open/closed sensor port',
1279
+ queryset=ColonelPin.objects.filter(input=True),
1280
+ url='autocomplete-colonel-pins',
1281
+ forward=[
1282
+ forward.Self(),
1283
+ forward.Field('colonel'),
1284
+ forward.Const({'input': True}, 'filters')
1285
+ ]
1286
+ )
1287
+ closed_value = forms.ChoiceField(
1288
+ label='Gate closed value',
1289
+ choices=(("LOW", "LOW"), ('HIGH', "HIGH")), initial="LOW",
1290
+ help_text="What is the input sensor value, "
1291
+ "when your gate is in closed position?"
1292
+ )
1293
+ open_duration = forms.FloatField(
1294
+ initial=30, min_value=1, max_value=600,
1295
+ help_text="How much time in seconds does it take for your gate "
1296
+ "to go from fully closed to fully open?"
1297
+ )
1298
+
1299
+ controls = FormsetField(
1300
+ formset_factory(
1301
+ ControlForm, can_delete=True, can_order=True, extra=0, max_num=2
1302
+ )
1303
+ )
1304
+
1305
+ def clean(self):
1306
+ super().clean()
1307
+
1308
+ if self.cleaned_data.get('control_pin') \
1309
+ and self.cleaned_data.get('sensor_pin') \
1310
+ and self.cleaned_data['control_pin'] == self.cleaned_data['sensor_pin']:
1311
+ self.add_error(
1312
+ 'sensor_pin', "Can't be the same as control port!"
1313
+ )
1314
+
1315
+ if self.cleaned_data.get('control_pin'):
1316
+ self._clean_pin('control_pin')
1317
+ if self.cleaned_data.get('sensor_pin'):
1318
+ self._clean_pin('sensor_pin')
1319
+
1320
+ if 'controls' in self.cleaned_data:
1321
+
1322
+ self._clean_controls()
1323
+
1324
+ if self.cleaned_data.get('control_pin') and self.cleaned_data.get('controls'):
1325
+ for ctrl in self.cleaned_data['controls']:
1326
+ if not ctrl['input'].startswith('pin'):
1327
+ continue
1328
+ if int(ctrl['input'][4:]) == self.cleaned_data['control_pin'].id:
1329
+ self.add_error(
1330
+ "control_pin",
1331
+ "Can't be used as control pin at the same time!"
1332
+ )
1333
+
1334
+ if self.cleaned_data.get('sensor_pin') and self.cleaned_data.get('controls'):
1335
+ for ctrl in self.cleaned_data['controls']:
1336
+ if not ctrl['input'].startswith('pin'):
1337
+ continue
1338
+ if int(ctrl['input'][4:]) == self.cleaned_data['sensor_pin'].id:
1339
+ self.add_error(
1340
+ "sensor_pin",
1341
+ "Can't be used as control pin at the same time!"
1342
+ )
1343
+ return self.cleaned_data
1344
+
1345
+ def save(self, commit=True):
1346
+ if 'control_pin' in self.cleaned_data:
1347
+ self.instance.config['control_pin_no'] = \
1348
+ self.cleaned_data['control_pin'].no
1349
+ if 'sensor_pin' in self.cleaned_data:
1350
+ self.instance.config['sensor_pin_no'] = \
1351
+ self.cleaned_data['sensor_pin'].no
1352
+ obj = super().save(commit=commit)
1353
+ if commit and self.cleaned_data.get('controls'):
1354
+ GatewayObjectCommand(
1355
+ self.instance.gateway, obj, command='watch_buttons'
1356
+ ).publish()
1357
+ return obj
1358
+
1359
+
1259
1360
  class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
1260
1361
  power_pin = Select2ModelChoiceField(
1261
1362
  label="Power port",
@@ -38,24 +38,6 @@ class WeatherForecastWidget(BaseAppWidget):
38
38
  size = [4, 2]
39
39
 
40
40
 
41
- class GateWidget(BaseAppWidget):
42
- uid = 'gate'
43
- name = _('Gate')
44
- size = [2, 1]
45
-
46
-
47
- class BlindsWidget(BaseAppWidget):
48
- uid = 'blinds'
49
- name = _('Blinds')
50
- size = [4, 1]
51
-
52
-
53
- class SlidesWidget(BaseAppWidget):
54
- uid = 'slides'
55
- name = _('Slides')
56
- size = [2, 1]
57
-
58
-
59
41
  class WateringWidget(BaseAppWidget):
60
42
  uid = 'watering'
61
43
  name = _('Watering')
@@ -6,8 +6,6 @@ BASE_TYPES = {
6
6
  'alarm-group': _("Alarm Group"),
7
7
  'ip-camera': _("IP Camera"),
8
8
  'weather-forecast': _("Weather Forecast"),
9
- 'gate': _("Gate"),
10
- 'blinds': _("Blinds"),
11
9
  'watering': _("Watering"),
12
10
  'state-select': _("State Select"),
13
11
  'alarm-clock': _("Alarm Clock"),