simo 2.5.3__py3-none-any.whl → 2.5.5__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 (59) 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__/middleware.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  6. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  8. simo/core/app_widgets.py +19 -1
  9. simo/core/base_types.py +2 -0
  10. simo/core/controllers.py +154 -4
  11. simo/core/management/_hub_template/hub/supervisor.conf +4 -0
  12. simo/core/management/commands/__pycache__/gateways_manager.cpython-38.pyc +0 -0
  13. simo/core/middleware.py +14 -7
  14. simo/core/models.py +5 -3
  15. simo/core/signal_receivers.py +71 -7
  16. simo/core/tasks.py +1 -1
  17. simo/core/templates/core/auto_night_day_script.py +62 -0
  18. simo/core/templates/core/auto_state_script.py +78 -0
  19. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  20. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  21. simo/fleet/controllers.py +20 -119
  22. simo/fleet/forms.py +101 -0
  23. simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
  24. simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
  25. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  26. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  27. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  28. simo/generic/__pycache__/models.cpython-38.pyc +0 -0
  29. simo/generic/app_widgets.py +0 -18
  30. simo/generic/base_types.py +0 -2
  31. simo/generic/controllers.py +2 -262
  32. simo/generic/forms.py +0 -49
  33. simo/generic/gateways.py +9 -119
  34. simo/generic/models.py +1 -0
  35. simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
  36. simo/generic/scripting/example.py +66 -0
  37. simo/generic/scripting/helpers.py +66 -10
  38. simo/notifications/__pycache__/admin.cpython-38.pyc +0 -0
  39. simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
  40. simo/notifications/admin.py +7 -3
  41. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  42. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  43. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  44. simo/users/admin.py +25 -5
  45. simo/users/api.py +34 -11
  46. simo/users/migrations/0035_instanceuser_last_seen_speed_kmh_and_more.py +23 -0
  47. simo/users/migrations/0036_instanceuser_phone_on_charge_user_phone_on_charge.py +23 -0
  48. simo/users/migrations/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.py +53 -0
  49. simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-38.pyc +0 -0
  50. simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-38.pyc +0 -0
  51. simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-38.pyc +0 -0
  52. simo/users/models.py +14 -57
  53. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/METADATA +1 -1
  54. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/RECORD +58 -50
  55. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/WHEEL +1 -1
  56. simo/scripting.py +0 -39
  57. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/LICENSE.md +0 -0
  58. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/entry_points.txt +0 -0
  59. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/top_level.txt +0 -0
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"),
@@ -37,14 +37,14 @@ from simo.core.utils.config_values import (
37
37
  from .gateways import GenericGatewayHandler, DummyGatewayHandler
38
38
  from .app_widgets import (
39
39
  ScriptWidget, ThermostatWidget, AlarmGroupWidget, IPCameraWidget,
40
- WeatherForecastWidget, GateWidget, BlindsWidget, SlidesWidget,
40
+ WeatherForecastWidget,
41
41
  WateringWidget, StateSelectWidget, AlarmClockWidget
42
42
  )
43
43
  from .forms import (
44
44
  ScriptConfigForm, PresenceLightingConfigForm,
45
45
  ThermostatConfigForm, AlarmGroupConfigForm,
46
46
  IPCameraConfigForm, WeatherForecastForm, GateConfigForm,
47
- BlindsConfigForm, WateringConfigForm, StateSelectForm,
47
+ WateringConfigForm, StateSelectForm,
48
48
  AlarmClockConfigForm
49
49
  )
50
50
  from .scripting import get_current_state
@@ -558,266 +558,6 @@ class IPCamera(ControllerBase):
558
558
  )
559
559
 
560
560
 
561
- class Gate(ControllerBase, TimerMixin):
562
- name = _("Gate")
563
- base_type = 'gate'
564
- gateway_class = GenericGatewayHandler
565
- app_widget = GateWidget
566
- config_form = GateConfigForm
567
- admin_widget_template = 'admin/controller_widgets/gate.html'
568
- default_config = {}
569
-
570
- @property
571
- def default_value(self):
572
- return 'closed'
573
-
574
- def _validate_val(self, value, occasion=None):
575
- if occasion == BEFORE_SEND:
576
- if self.component.config.get('action_method') == 'click':
577
- if value != 'call':
578
- raise ValidationError(
579
- 'Gate component understands only one command: '
580
- '"call". You have provided: "%s"' % (str(value))
581
- )
582
- else:
583
- if value not in ('call', 'open', 'close'):
584
- raise ValidationError(
585
- 'This gate component understands only 3 commands: '
586
- '"open", "close" and "call". You have provided: "%s"' %
587
- (str(value))
588
- )
589
- elif occasion == BEFORE_SET and value not in (
590
- 'closed', 'open', 'open_moving', 'closed_moving'
591
- ):
592
- raise ValidationError(
593
- 'Gate component can only be in 4 states: '
594
- '"closed", "closed", "open_moving", "closed_moving". '
595
- 'You have provided: "%s"' % (str(value))
596
- )
597
- return value
598
-
599
- def _set_on_the_move(self):
600
- def cancel_move():
601
- start_value = self.component.value
602
- start_sensor_value = self.component.config.get('sensor_value')
603
- move_duration = self.component.config.get(
604
- 'gate_open_duration', 30
605
- ) * 1000
606
- # stay in moving state for user defined amount of seconds
607
- time.sleep(move_duration / 1000)
608
- self.component.refresh_from_db()
609
- if time.time() - self.component.config.get('last_call', 0) \
610
- < move_duration / 1000:
611
- # There was another call in between of this wait,
612
- # so we must skip this in favor of the new cancel_move function
613
- # that is currently running in parallel.
614
- return
615
-
616
- # If it is no longer on the move this process becomes obsolete
617
- # For example when open/close binary sensor detects closed event
618
- # gate value is immediately set to closed.
619
- if not self.component.value.endswith('moving'):
620
- return
621
-
622
- # Started from closed, sensor already picked up open event
623
- # therefore this must now be considered as open.
624
- if start_value.startswith('closed') \
625
- and self.component.value == 'open_moving' \
626
- and self.component.config.get('sensor_value'):
627
- self.component.set('open')
628
- return
629
-
630
- # In all other occasions we wait for another move_duration
631
- # and finish move anyways.
632
- time.sleep(move_duration / 1000)
633
- self.component.refresh_from_db()
634
- if self.component.value.endswith('moving'):
635
- self.component.set(self.component.value[:-7])
636
-
637
- self.component.refresh_from_db()
638
- self.component.config['last_call'] = time.time()
639
- self.component.save(update_fields=['config'])
640
-
641
- if not self.component.value.endswith('_moving'):
642
- self.component.set(self.component.value + '_moving')
643
- threading.Thread(target=cancel_move, daemon=True).start()
644
-
645
- def open(self):
646
- self.send('open')
647
-
648
- def close(self):
649
- self.send('close')
650
-
651
- def call(self):
652
- self.send('call')
653
-
654
- # TODO: This was in gateway class, however it
655
- # needs to be moved here or part of it back to the gateway
656
- # as we no longer have Event object.
657
- # if msg.topic == Event.TOPIC:
658
- # if isinstance(component.controller, Switch):
659
- # value_change = payload['data'].get('value')
660
- # if not value_change:
661
- # return
662
- #
663
- # # Handle Gate switches
664
- # for gate in Component.objects.filter(
665
- # controller_uid=Gate.uid, config__action_switch=component.id
666
- # ):
667
- # if gate.config.get('action_method') == 'toggle':
668
- # gate.controller._set_on_the_move()
669
- # else:
670
- # if value_change.get('new') == False:
671
- # # Button released
672
- # # set stopped position if it was moving, or set moving if not.
673
- # if gate.value.endswith('moving'):
674
- # if gate.config.get('sensor_value'):
675
- # gate.set('open')
676
- # else:
677
- # gate.set('closed')
678
- # else:
679
- # gate.controller._set_on_the_move()
680
- #
681
- # return
682
- #
683
- # elif isinstance(component.controller, BinarySensor):
684
- # value_change = payload['data'].get('value')
685
- # if not value_change:
686
- # return
687
- # # Handle Gate binary sensors
688
- # for gate in Component.objects.filter(
689
- # controller_uid=Gate.uid,
690
- # config__open_closed_sensor=component.id
691
- # ):
692
- # gate.config['sensor_value'] = component.value
693
- # gate.save(update_fields=['config'])
694
- # # If sensor goes from False to True, while gate is moving
695
- # # it usually means that gate just started the move and must stay in the move
696
- # # user defined amount of seconds to represent actual gate movement.
697
- # # Open state therefore is reached only after user defined duration.
698
- # # If it was not in the move, then it simply means that it was
699
- # # opened in some other way and we set it to open immediately.
700
- # if component.value:
701
- # if gate.value.endswith('moving'):
702
- # print("SET OPEN MOVING!")
703
- # gate.set('open_moving')
704
- # else:
705
- # gate.set('open')
706
- # # if binary sensor detects gate close event
707
- # # we set gate value to closed immediately as it means that
708
- # # gate is now truly closed and no longer moving.
709
- # else:
710
- # gate.set('closed')
711
-
712
-
713
- class Blinds(ControllerBase, TimerMixin):
714
- name = _("Blind")
715
- base_type = 'blinds'
716
- gateway_class = GenericGatewayHandler
717
- config_form = BlindsConfigForm
718
- admin_widget_template = 'admin/controller_widgets/blinds.html'
719
- default_config = {}
720
-
721
- @property
722
- def app_widget(self):
723
- if self.component.config.get('control_mode') == 'slide':
724
- return SlidesWidget
725
- else:
726
- return BlindsWidget
727
-
728
- @property
729
- def default_value(self):
730
- # target and current positions in milliseconds, angle in degrees (0 - 180)
731
- return {'target': 0, 'position': 0, 'angle': 0}
732
-
733
- def _validate_val(self, value, occasion=None):
734
-
735
- if occasion == BEFORE_SEND:
736
- if isinstance(value, int) or isinstance(value, float):
737
- # legacy support
738
- value = {'target': int(value)}
739
- if 'target' not in value:
740
- raise ValidationError("Target value is required!")
741
- target = value.get('target')
742
- if type(target) not in (float, int):
743
- raise ValidationError(
744
- "Bad target position for blinds to go."
745
- )
746
- if target > self.component.config.get('open_duration') * 1000:
747
- raise ValidationError(
748
- "Target value lower than %d expected, "
749
- "%d received instead" % (
750
- self.component.config['open_duration'] * 1000,
751
- target
752
- )
753
- )
754
- if 'angle' in value:
755
- try:
756
- angle = int(value['angle'])
757
- except:
758
- raise ValidationError(
759
- "Integer between 0 - 180 is required for blinds angle."
760
- )
761
- if angle < 0 or angle > 180:
762
- raise ValidationError(
763
- "Integer between 0 - 180 is required for blinds angle."
764
- )
765
- else:
766
- value['angle'] = self.component.value.get('angle', 0)
767
-
768
- elif occasion == BEFORE_SET:
769
- if not isinstance(value, dict):
770
- raise ValidationError("Dictionary is expected")
771
- for key, val in value.items():
772
- if key not in ('target', 'position', 'angle'):
773
- raise ValidationError(
774
- "'target', 'position' or 'angle' parameters are expected."
775
- )
776
- if key == 'position':
777
- if val < 0:
778
- raise ValidationError(
779
- "Positive integer expected for blind position"
780
- )
781
- if val > self.component.config.get('open_duration') * 1000:
782
- raise ValidationError(
783
- "Positive value is to big. Must be lower than %d, "
784
- "but you have provided %d" % (
785
- self.component.config.get('open_duration') * 1000, val
786
- )
787
- )
788
-
789
- self.component.refresh_from_db()
790
- if 'target' not in value:
791
- value['target'] = self.component.value.get('target')
792
- if 'position' not in value:
793
- value['position'] = self.component.value.get('position')
794
- if 'angle' not in value:
795
- value['angle'] = self.component.value.get('angle')
796
-
797
- return value
798
-
799
- def open(self):
800
- send_val = {'target': 0}
801
- angle = self.component.value.get('angle')
802
- if angle is not None and 0 <= angle <= 180:
803
- send_val['angle'] = angle
804
- self.send(send_val)
805
-
806
- def close(self):
807
- send_val = {'target': self.component.config['open_duration'] * 1000}
808
- angle = self.component.value.get('angle')
809
- if angle is not None and 0 <= angle <= 180:
810
- send_val['angle'] = angle
811
- self.send(send_val)
812
-
813
- def stop(self):
814
- send_val = {'target': -1}
815
- angle = self.component.value.get('angle')
816
- if angle is not None and 0 <= angle <= 180:
817
- send_val['angle'] = angle
818
- self.send(send_val)
819
-
820
-
821
561
  class Watering(ControllerBase):
822
562
  STATUS_CHOICES = (
823
563
  'stopped', 'running_program', 'running_custom',