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.
- simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.cpython-38.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/app_widgets.py +19 -1
- simo/core/base_types.py +2 -0
- simo/core/controllers.py +154 -4
- simo/core/management/_hub_template/hub/supervisor.conf +4 -0
- simo/core/management/commands/__pycache__/gateways_manager.cpython-38.pyc +0 -0
- simo/core/middleware.py +14 -7
- simo/core/models.py +5 -3
- simo/core/signal_receivers.py +71 -7
- simo/core/tasks.py +1 -1
- simo/core/templates/core/auto_night_day_script.py +62 -0
- simo/core/templates/core/auto_state_script.py +78 -0
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/controllers.py +20 -119
- simo/fleet/forms.py +101 -0
- simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/__pycache__/models.cpython-38.pyc +0 -0
- simo/generic/app_widgets.py +0 -18
- simo/generic/base_types.py +0 -2
- simo/generic/controllers.py +2 -262
- simo/generic/forms.py +0 -49
- simo/generic/gateways.py +9 -119
- simo/generic/models.py +1 -0
- simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/generic/scripting/example.py +66 -0
- simo/generic/scripting/helpers.py +66 -10
- simo/notifications/__pycache__/admin.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
- simo/notifications/admin.py +7 -3
- simo/users/__pycache__/admin.cpython-38.pyc +0 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/admin.py +25 -5
- simo/users/api.py +34 -11
- simo/users/migrations/0035_instanceuser_last_seen_speed_kmh_and_more.py +23 -0
- simo/users/migrations/0036_instanceuser_phone_on_charge_user_phone_on_charge.py +23 -0
- simo/users/migrations/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.py +53 -0
- simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-38.pyc +0 -0
- simo/users/models.py +14 -57
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/METADATA +1 -1
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/RECORD +58 -50
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/WHEEL +1 -1
- simo/scripting.py +0 -39
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/LICENSE.md +0 -0
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/entry_points.txt +0 -0
- {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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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'
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/generic/app_widgets.py
CHANGED
|
@@ -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')
|
simo/generic/base_types.py
CHANGED
|
@@ -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"),
|
simo/generic/controllers.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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',
|