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.
- 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__/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/signal_receivers.py +8 -5
- simo/core/tasks.py +1 -1
- 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/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 +5 -118
- simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/generic/scripting/example.py +67 -0
- simo/generic/scripting/helpers.py +66 -10
- 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 +25 -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 +12 -55
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/METADATA +1 -1
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/RECORD +46 -40
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/WHEEL +1 -1
- simo/scripting.py +0 -39
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/LICENSE.md +0 -0
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/entry_points.txt +0 -0
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
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
|
-
|
|
781
|
-
|
|
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/
|
simo/core/signal_receivers.py
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
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"),
|