simo 2.0.42__py3-none-any.whl → 2.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of simo might be problematic. Click here for more details.
- simo/__pycache__/asgi.cpython-38.pyc +0 -0
- simo/__pycache__/settings.cpython-38.pyc +0 -0
- simo/__pycache__/wsgi.cpython-38.pyc +0 -0
- simo/asgi.py +1 -1
- simo/core/__init__.py +1 -0
- simo/core/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
- simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/core/__pycache__/apps.cpython-38.pyc +0 -0
- simo/core/__pycache__/auto_urls.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__/dynamic_settings.cpython-38.pyc +0 -0
- simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/core/__pycache__/managers.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.cpython-38.pyc +0 -0
- simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
- simo/core/__pycache__/serializers.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/__pycache__/views.cpython-38.pyc +0 -0
- simo/core/admin.py +26 -26
- simo/core/api.py +22 -2
- simo/core/api_meta.py +23 -13
- simo/core/app_widgets.py +6 -0
- simo/core/apps.py +13 -0
- simo/core/auto_urls.py +2 -3
- simo/core/base_types.py +1 -0
- simo/core/controllers.py +57 -0
- simo/core/dynamic_settings.py +0 -8
- simo/core/form_fields.py +93 -0
- simo/core/forms.py +16 -101
- simo/core/gateways.py +1 -1
- simo/core/managers.py +14 -1
- simo/core/migrations/0037_auto_20240606_1057.py +33 -0
- simo/core/migrations/0038_remove_instance_cover_image_and_more.py +30 -0
- simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0038_remove_instance_cover_image_and_more.cpython-38.pyc +0 -0
- simo/core/models.py +30 -16
- simo/core/permissions.py +6 -3
- simo/core/serializers.py +77 -5
- simo/core/signal_receivers.py +25 -0
- simo/core/static/admin/css/simo.css +14 -0
- simo/core/tasks.py +82 -49
- simo/core/templates/admin/controller_widgets/button.html +8 -0
- simo/core/templates/admin/core/component_change_form.html +97 -0
- simo/core/templates/admin/formset_widget.html +88 -118
- simo/core/templates/admin/formset_widget_old.html +122 -0
- simo/core/templates/admin/user_tools.html +0 -3
- simo/core/templates/admin/wizard/wizard_add.html +16 -9
- simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/cache.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
- simo/core/utils/admin.py +11 -0
- simo/core/utils/cache.py +15 -0
- simo/core/utils/formsets.py +11 -18
- simo/core/views.py +2 -85
- simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
- simo/fleet/auto_urls.py +7 -1
- simo/fleet/controllers.py +194 -31
- simo/fleet/forms.py +223 -87
- simo/fleet/gateways.py +53 -2
- simo/fleet/migrations/0036_auto_20240605_0702.py +68 -0
- simo/fleet/migrations/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.py +27 -0
- simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.cpython-38.pyc +0 -0
- simo/fleet/models.py +35 -6
- simo/fleet/socket_consumers.py +1 -1
- simo/fleet/templates/fleet/controllers_info/button.md +16 -0
- simo/fleet/utils.py +31 -1
- simo/fleet/views.py +45 -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/controllers.py +61 -16
- simo/generic/forms.py +0 -3
- simo/generic/gateways.py +2 -0
- simo/generic/templates/admin/controller_widgets/blinds.html +2 -1
- simo/generic/templates/admin/controller_widgets/weather_forecast.html +1 -1
- simo/generic/templates/generic/controllers_info/dummy.md +3 -0
- simo/generic/templates/generic/controllers_info/stateselect.md +2 -0
- simo/management/__init__.py +0 -0
- simo/management/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/management/__pycache__/on_http_start.cpython-38.pyc +0 -0
- simo/{_hub_template → management/_hub_template}/hub/nginx.conf +2 -2
- simo/{auto_update.py → management/auto_update.py} +3 -0
- simo/{cli.py → management/copy_template.py} +3 -16
- simo/management/install.py +258 -0
- simo/{on_http_start.py → management/on_http_start.py} +22 -2
- simo/settings.py +20 -4
- simo/users/__init__.py +1 -0
- simo/users/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/users/__pycache__/admin.cpython-38.pyc +0 -0
- simo/users/__pycache__/apps.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/apps.py +9 -0
- simo/users/migrations/__pycache__/0029_alter_instanceuser_options_instanceuser_order.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0030_alter_instanceuser_options_remove_instanceuser_order.cpython-38.pyc +0 -0
- simo/users/models.py +16 -3
- {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/METADATA +5 -3
- {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/RECORD +122 -95
- simo-2.1.2.dist-info/entry_points.txt +2 -0
- simo/__pycache__/on_http_start.cpython-38.pyc +0 -0
- simo/wsgi.py +0 -7
- /simo/{_hub_template → management/_hub_template}/hub/asgi.py +0 -0
- /simo/{_hub_template → management/_hub_template}/hub/celeryc.py +0 -0
- /simo/{_hub_template → management/_hub_template}/hub/manage.py +0 -0
- /simo/{_hub_template → management/_hub_template}/hub/settings.py +0 -0
- /simo/{_hub_template → management/_hub_template}/hub/supervisor.conf +0 -0
- /simo/{_hub_template → management/_hub_template}/hub/urls.py +0 -0
- {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/LICENSE.md +0 -0
- {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/WHEEL +0 -0
- {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/top_level.txt +0 -0
simo/fleet/gateways.py
CHANGED
|
@@ -22,7 +22,20 @@ class FleetGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
def run(self, exit):
|
|
25
|
-
from simo.fleet.controllers import
|
|
25
|
+
from simo.fleet.controllers import (
|
|
26
|
+
Switch, PWMOutput, RGBLight, Blinds, DALIGearGroup, DALILamp, TTLock
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
self.buttons_on_watch = set()
|
|
30
|
+
for component in Component.objects.filter(
|
|
31
|
+
controller_uid__in=(
|
|
32
|
+
Switch.uid, PWMOutput.uid, RGBLight.uid, Blinds.uid,
|
|
33
|
+
DALIGearGroup.uid, DALILamp.uid
|
|
34
|
+
)
|
|
35
|
+
):
|
|
36
|
+
self.watch_buttons(component)
|
|
37
|
+
|
|
38
|
+
|
|
26
39
|
self.door_sensors_on_watch = set()
|
|
27
40
|
for lock in Component.objects.filter(controller_uid=TTLock.uid):
|
|
28
41
|
if not lock.config.get('door_sensor'):
|
|
@@ -34,6 +47,7 @@ class FleetGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
34
47
|
continue
|
|
35
48
|
self.door_sensors_on_watch.add(door_sensor.id)
|
|
36
49
|
door_sensor.on_change(self.on_door_sensor)
|
|
50
|
+
|
|
37
51
|
super().run(exit)
|
|
38
52
|
|
|
39
53
|
|
|
@@ -44,11 +58,16 @@ class FleetGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
44
58
|
door_sensor = get_event_obj(payload, Component)
|
|
45
59
|
if not door_sensor:
|
|
46
60
|
return
|
|
47
|
-
print("Adding door sensor to lock watch!")
|
|
48
61
|
if door_sensor.id in self.door_sensors_on_watch:
|
|
49
62
|
return
|
|
63
|
+
print("Adding new door sensor to lock watch!")
|
|
50
64
|
self.door_sensors_on_watch.add(door_sensor.id)
|
|
51
65
|
door_sensor.on_change(self.on_door_sensor)
|
|
66
|
+
if payload.get('command') == 'watch_buttons':
|
|
67
|
+
component = get_event_obj(payload, Component)
|
|
68
|
+
if not component:
|
|
69
|
+
return
|
|
70
|
+
self.watch_buttons(component)
|
|
52
71
|
|
|
53
72
|
def on_door_sensor(self, sensor):
|
|
54
73
|
from simo.fleet.controllers import TTLock
|
|
@@ -96,3 +115,35 @@ class FleetGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
96
115
|
type=gw.discovery['controller_uid'],
|
|
97
116
|
i=form_cleaned_data['interface'].no
|
|
98
117
|
).publish()
|
|
118
|
+
|
|
119
|
+
def watch_buttons(self, component):
|
|
120
|
+
for i, ctrl in enumerate(component.config.get('controls', [])):
|
|
121
|
+
if not ctrl.get('input', '').startswith('button'):
|
|
122
|
+
continue
|
|
123
|
+
button = Component.objects.filter(id=ctrl['input'][7:]).first()
|
|
124
|
+
if not button:
|
|
125
|
+
continue
|
|
126
|
+
if button.id in self.buttons_on_watch:
|
|
127
|
+
continue
|
|
128
|
+
if button.config.get('colonel') == component.config.get('colonel'):
|
|
129
|
+
# button is on a same colonel, therefore colonel handles
|
|
130
|
+
# all control actions and we do not need to do it here
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
def button_action(btn):
|
|
134
|
+
self.button_action(component, btn)
|
|
135
|
+
print(f"Binding button {button} to {component}!")
|
|
136
|
+
button.on_change(button_action)
|
|
137
|
+
self.buttons_on_watch.add(button.id)
|
|
138
|
+
|
|
139
|
+
def button_action(self, comp, btn):
|
|
140
|
+
comp.refresh_from_db()
|
|
141
|
+
for j, ctrl in enumerate(comp.config.get('controls', [])):
|
|
142
|
+
if ctrl['input'] == f'button-{btn.id}':
|
|
143
|
+
method = ctrl.get('method', 'momentary')
|
|
144
|
+
print(
|
|
145
|
+
f"Button [{j}] {btn}: {btn.value} on {comp} "
|
|
146
|
+
f"| Btn type: {method}"
|
|
147
|
+
)
|
|
148
|
+
comp.controller._ctrl(j, btn.value, method)
|
|
149
|
+
break
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Generated by Django 3.2.9 on 2024-06-05 07:02
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def forwards_func(apps, schema_editor):
|
|
7
|
+
from simo.fleet.utils import GPIO_PINS
|
|
8
|
+
Component = apps.get_model("core", "Component")
|
|
9
|
+
Colonel = apps.get_model('fleet', "Colonel")
|
|
10
|
+
ColonelPin = apps.get_model('fleet', "ColonelPin")
|
|
11
|
+
for comp in Component.objects.filter(config__has_key='controls'):
|
|
12
|
+
if not isinstance(comp.config['controls'], list):
|
|
13
|
+
continue
|
|
14
|
+
for ctrl in comp.config['controls']:
|
|
15
|
+
if 'pin_no' in ctrl and 'pin' in ctrl:
|
|
16
|
+
ctrl['input'] = f"pin-{ctrl['pin']}"
|
|
17
|
+
del ctrl['pin']
|
|
18
|
+
if 'pin' in ctrl and 'pin_no' not in ctrl:
|
|
19
|
+
# time to fix legacy remainings..
|
|
20
|
+
colonel = Colonel.objects.filter(
|
|
21
|
+
id=comp.config.get('colonel', 0)
|
|
22
|
+
).first()
|
|
23
|
+
if not colonel:
|
|
24
|
+
# Colonel no longer exists, there is no point in
|
|
25
|
+
# continuing with this
|
|
26
|
+
comp.config['controls'] = []
|
|
27
|
+
continue
|
|
28
|
+
try:
|
|
29
|
+
pin_no = ctrl['pin']
|
|
30
|
+
except:
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
pin_data = {}
|
|
34
|
+
for no, pin_data in GPIO_PINS.get(colonel.type).items():
|
|
35
|
+
if no == pin_no:
|
|
36
|
+
break
|
|
37
|
+
pin, new = ColonelPin.objects.get_or_create(
|
|
38
|
+
colonel=colonel, no=pin_no,
|
|
39
|
+
defaults={
|
|
40
|
+
'input': pin_data.get('input'),
|
|
41
|
+
'output': pin_data.get('output'),
|
|
42
|
+
'capacitive': pin_data.get('capacitive'),
|
|
43
|
+
'adc': pin_data.get('adc'),
|
|
44
|
+
'native': pin_data.get('native'),
|
|
45
|
+
'note': pin_data.get('note')
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
ctrl['pin_no'] = pin.no
|
|
50
|
+
ctrl['input'] = f'pin-{pin.id}'
|
|
51
|
+
del ctrl['pin']
|
|
52
|
+
|
|
53
|
+
comp.save()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def reverse_func(apps, schema_editor):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Migration(migrations.Migration):
|
|
61
|
+
|
|
62
|
+
dependencies = [
|
|
63
|
+
('fleet', '0035_auto_20240514_0855'),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
operations = [
|
|
67
|
+
migrations.RunPython(forwards_func, reverse_func, elidable=True),
|
|
68
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-06-12 08:14
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('fleet', '0036_auto_20240605_0702'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterModelOptions(
|
|
14
|
+
name='colonelpin',
|
|
15
|
+
options={'ordering': ('colonel', 'no')},
|
|
16
|
+
),
|
|
17
|
+
migrations.AlterField(
|
|
18
|
+
model_name='colonelpin',
|
|
19
|
+
name='no',
|
|
20
|
+
field=models.PositiveIntegerField(db_index=True),
|
|
21
|
+
),
|
|
22
|
+
migrations.AlterField(
|
|
23
|
+
model_name='interfaceaddress',
|
|
24
|
+
name='address_type',
|
|
25
|
+
field=models.CharField(choices=[('i2c', 'I2C'), ('dali-gear', 'DALI Gear'), ('dali-group', 'DALI Gear Group'), ('dali-device', 'DALI Control Device')], db_index=True, max_length=100),
|
|
26
|
+
),
|
|
27
|
+
]
|
|
Binary file
|
|
Binary file
|
simo/fleet/models.py
CHANGED
|
@@ -203,7 +203,7 @@ class ColonelPin(models.Model):
|
|
|
203
203
|
colonel = models.ForeignKey(
|
|
204
204
|
Colonel, related_name='pins', on_delete=models.CASCADE
|
|
205
205
|
)
|
|
206
|
-
no = models.PositiveIntegerField()
|
|
206
|
+
no = models.PositiveIntegerField(db_index=True)
|
|
207
207
|
label = models.CharField(db_index=True, max_length=200)
|
|
208
208
|
input = models.BooleanField(default=False, db_index=True)
|
|
209
209
|
output = models.BooleanField(default=False, db_index=True)
|
|
@@ -227,6 +227,7 @@ class ColonelPin(models.Model):
|
|
|
227
227
|
|
|
228
228
|
class Meta:
|
|
229
229
|
unique_together = 'colonel', 'no'
|
|
230
|
+
ordering = 'colonel', 'no'
|
|
230
231
|
indexes = [
|
|
231
232
|
models.Index(
|
|
232
233
|
fields=["occupied_by_content_type", "occupied_by_id"]
|
|
@@ -255,9 +256,11 @@ def after_colonel_save(sender, instance, created, *args, **kwargs):
|
|
|
255
256
|
for no, data in GPIO_PINS.get(instance.type).items():
|
|
256
257
|
ColonelPin.objects.get_or_create(
|
|
257
258
|
colonel=instance, no=no,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
defaults = {
|
|
260
|
+
'input': data.get('input'), 'output': data.get('output'),
|
|
261
|
+
'capacitive': data.get('capacitive'), 'adc': data.get('adc'),
|
|
262
|
+
'native': data.get('native'), 'note': data.get('note')
|
|
263
|
+
}
|
|
261
264
|
)
|
|
262
265
|
fleet_gateway, new = Gateway.objects.get_or_create(
|
|
263
266
|
type='simo.fleet.gateways.FleetGatewayHandler'
|
|
@@ -266,6 +269,31 @@ def after_colonel_save(sender, instance, created, *args, **kwargs):
|
|
|
266
269
|
fleet_gateway.start()
|
|
267
270
|
|
|
268
271
|
|
|
272
|
+
@receiver(post_save, sender=Component)
|
|
273
|
+
def post_component_save(sender, instance, created, *args, **kwargs):
|
|
274
|
+
if not instance.controller_uid.startswith('simo.fleet'):
|
|
275
|
+
return
|
|
276
|
+
if 'config' not in instance.get_dirty_fields():
|
|
277
|
+
return
|
|
278
|
+
colonel = Colonel.objects.filter(id=instance.config.get('colonel', 0)).first()
|
|
279
|
+
if not colonel:
|
|
280
|
+
return
|
|
281
|
+
colonel.components.add(instance)
|
|
282
|
+
from .controllers import (
|
|
283
|
+
TTLock, DALILamp, DALIGearGroup, DALIRelay, DALIOccupancySensor,
|
|
284
|
+
DALILightSensor, DALIButton
|
|
285
|
+
)
|
|
286
|
+
if instance.controller and instance.controller_cls in (
|
|
287
|
+
TTLock, DALILamp, DALIGearGroup, DALIRelay, DALIOccupancySensor,
|
|
288
|
+
DALILightSensor, DALIButton
|
|
289
|
+
):
|
|
290
|
+
return
|
|
291
|
+
colonel.rebuild_occupied_pins()
|
|
292
|
+
colonel.save()
|
|
293
|
+
colonel.update_config()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
269
297
|
@receiver(pre_delete, sender=Component)
|
|
270
298
|
def post_component_delete(sender, instance, *args, **kwargs):
|
|
271
299
|
if not instance.controller_uid.startswith('simo.fleet'):
|
|
@@ -274,7 +302,7 @@ def post_component_delete(sender, instance, *args, **kwargs):
|
|
|
274
302
|
from .controllers import DALIGearGroup
|
|
275
303
|
if instance.controller_uid == DALIGearGroup.uid:
|
|
276
304
|
for comp in Component.objects.filter(
|
|
277
|
-
|
|
305
|
+
id__in=instance.config.get('members', [])
|
|
278
306
|
):
|
|
279
307
|
instance.controller._modify_member_group(
|
|
280
308
|
comp, instance.config.get('da', 0), remove=True
|
|
@@ -360,7 +388,8 @@ class InterfaceAddress(models.Model):
|
|
|
360
388
|
db_index=True, max_length=100, choices=(
|
|
361
389
|
('i2c', "I2C"),
|
|
362
390
|
('dali-gear', "DALI Gear"),
|
|
363
|
-
('dali-group', "DALI Gear Group")
|
|
391
|
+
('dali-group', "DALI Gear Group"),
|
|
392
|
+
('dali-device', "DALI Control Device"),
|
|
364
393
|
)
|
|
365
394
|
)
|
|
366
395
|
address = models.JSONField(db_index=True)
|
simo/fleet/socket_consumers.py
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
This component represents a physical button that can be used with other components in the system as a control input.
|
|
2
|
+
Usually, it is better to use the input ports of a colonel board directly as input controls.
|
|
3
|
+
However, if you want to control something that is connected to a different colonel or control more than one component with a single button, then this component provides a way to do it.
|
|
4
|
+
|
|
5
|
+
- Create a button first, then use it as a control input on components that you want to control.
|
|
6
|
+
- Use GND as a reference.
|
|
7
|
+
- PULL -> UP if used with SIMO.io input port module.
|
|
8
|
+
|
|
9
|
+
{% if component.controller.bonded_gear %}
|
|
10
|
+
---
|
|
11
|
+
### Bonded gear:
|
|
12
|
+
|
|
13
|
+
{% for comp in component.controller.bonded_gear %}
|
|
14
|
+
- [{{comp.id}}] {{ comp }}
|
|
15
|
+
{% endfor %}
|
|
16
|
+
{% endif %}
|
simo/fleet/utils.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from simo.core.utils.cache import get_cached_data
|
|
2
|
+
from simo.core.middleware import get_current_instance
|
|
3
|
+
|
|
1
4
|
GPIO_PIN_DEFAULTS = {
|
|
2
5
|
'output': True, 'input': True, 'default_pull': 'FLOATING',
|
|
3
6
|
'native': True, 'adc': False,
|
|
@@ -118,4 +121,31 @@ for no, data in BASE_ESP32_GPIO_PINS.items():
|
|
|
118
121
|
|
|
119
122
|
INTERFACES_PINS_MAP = {
|
|
120
123
|
1: [13, 23], 2: [32, 33]
|
|
121
|
-
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_all_control_input_choices():
|
|
128
|
+
'''
|
|
129
|
+
This is called multiple times by component form,
|
|
130
|
+
so we cache the data to speed things up!
|
|
131
|
+
'''
|
|
132
|
+
def get_control_input_choices():
|
|
133
|
+
from .models import ColonelPin
|
|
134
|
+
from simo.core.models import Component
|
|
135
|
+
pins_qs = ColonelPin.objects.all()
|
|
136
|
+
|
|
137
|
+
buttons_qs = Component.objects.filter(
|
|
138
|
+
base_type='button'
|
|
139
|
+
).select_related('zone')
|
|
140
|
+
|
|
141
|
+
return [(f'pin-{pin.id}', str(pin)) for pin in pins_qs] + \
|
|
142
|
+
[(f'button-{button.id}',
|
|
143
|
+
f"{button.zone.name} | {button.name}"
|
|
144
|
+
if button.zone else button.name)
|
|
145
|
+
for button in buttons_qs]
|
|
146
|
+
|
|
147
|
+
instance = get_current_instance()
|
|
148
|
+
|
|
149
|
+
return get_cached_data(
|
|
150
|
+
f'{instance.id}-fleet-control-inputs', get_control_input_choices, 10
|
|
151
|
+
)
|
simo/fleet/views.py
CHANGED
|
@@ -2,6 +2,7 @@ from django.http import HttpResponse, Http404
|
|
|
2
2
|
from django.db.models import Q
|
|
3
3
|
from dal import autocomplete
|
|
4
4
|
from simo.core.utils.helpers import search_queryset
|
|
5
|
+
from simo.core.models import Component
|
|
5
6
|
from .models import Colonel, ColonelPin, Interface
|
|
6
7
|
|
|
7
8
|
|
|
@@ -61,3 +62,47 @@ class InterfaceSelectAutocomplete(autocomplete.Select2QuerySetView):
|
|
|
61
62
|
qs = qs.filter(**self.forwarded.get('filters'))
|
|
62
63
|
|
|
63
64
|
return qs
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ControlInputSelectAutocomplete(autocomplete.Select2ListView):
|
|
68
|
+
|
|
69
|
+
def get_list(self):
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
colonel = Colonel.objects.get(
|
|
73
|
+
pk=self.forwarded.get("colonel")
|
|
74
|
+
)
|
|
75
|
+
pins_qs = ColonelPin.objects.filter(colonel=colonel)
|
|
76
|
+
except:
|
|
77
|
+
pins_qs = ColonelPin.objects.all()
|
|
78
|
+
|
|
79
|
+
if self.forwarded.get('self') and self.forwarded['self'].startswith('pin-'):
|
|
80
|
+
pins_qs = pins_qs.filter(
|
|
81
|
+
Q(occupied_by_id=None) | Q(id=int(self.forwarded['self'][4:]))
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
pins_qs = pins_qs.filter(occupied_by_id=None)
|
|
85
|
+
|
|
86
|
+
if self.forwarded.get('pin_filters'):
|
|
87
|
+
pins_qs = pins_qs.filter(**self.forwarded.get('pin_filters'))
|
|
88
|
+
|
|
89
|
+
if self.q:
|
|
90
|
+
pins_qs = search_queryset(pins_qs, self.q, ('label',))
|
|
91
|
+
|
|
92
|
+
buttons_qs = Component.objects.filter(
|
|
93
|
+
base_type='button'
|
|
94
|
+
).select_related('zone')
|
|
95
|
+
|
|
96
|
+
if self.forwarded.get('button_filters'):
|
|
97
|
+
buttons_qs = buttons_qs.filter(**self.forwarded.get('button_filters'))
|
|
98
|
+
|
|
99
|
+
if self.q:
|
|
100
|
+
buttons_qs = search_queryset(
|
|
101
|
+
buttons_qs, self.q, ('name', 'zone__name', 'category__name')
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return [(f'pin-{pin.id}', str(pin)) for pin in pins_qs] + \
|
|
105
|
+
[(f'button-{button.id}',
|
|
106
|
+
f"{button.zone.name} | {button.name}"
|
|
107
|
+
if button.zone else button.name)
|
|
108
|
+
for button in buttons_qs]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/generic/controllers.py
CHANGED
|
@@ -223,7 +223,7 @@ class Thermostat(ControllerBase):
|
|
|
223
223
|
def default_config(self):
|
|
224
224
|
min = 3
|
|
225
225
|
max = 36
|
|
226
|
-
if
|
|
226
|
+
if self.component.zone.instance.units_of_measure == 'imperial':
|
|
227
227
|
min = 36
|
|
228
228
|
max = 100
|
|
229
229
|
return {
|
|
@@ -237,7 +237,7 @@ class Thermostat(ControllerBase):
|
|
|
237
237
|
raise ValidationError("This component type does not accept set value!")
|
|
238
238
|
|
|
239
239
|
def _get_default_user_config(self):
|
|
240
|
-
if
|
|
240
|
+
if self.component.zone.instance.units_of_measure == 'imperial':
|
|
241
241
|
target_temp = 70
|
|
242
242
|
low_target = 60
|
|
243
243
|
high_target = 75
|
|
@@ -681,31 +681,51 @@ class Blinds(ControllerBase, TimerMixin):
|
|
|
681
681
|
|
|
682
682
|
@property
|
|
683
683
|
def default_value(self):
|
|
684
|
-
# target and current positions in milliseconds
|
|
685
|
-
return {'target': 0, 'position': 0}
|
|
684
|
+
# target and current positions in milliseconds, angle in degrees (0 - 180)
|
|
685
|
+
return {'target': 0, 'position': 0, 'angle': 0}
|
|
686
686
|
|
|
687
687
|
def _validate_val(self, value, occasion=None):
|
|
688
|
+
|
|
688
689
|
if occasion == BEFORE_SEND:
|
|
689
|
-
if
|
|
690
|
+
if isinstance(value, int) or isinstance(value, float):
|
|
691
|
+
# legacy support
|
|
692
|
+
value = {'target': int(value)}
|
|
693
|
+
if 'target' not in value:
|
|
694
|
+
raise ValidationError("Target value is required!")
|
|
695
|
+
target = value.get('target')
|
|
696
|
+
if type(target) not in (float, int):
|
|
690
697
|
raise ValidationError(
|
|
691
|
-
"target position for blinds to go."
|
|
698
|
+
"Bad target position for blinds to go."
|
|
692
699
|
)
|
|
693
|
-
if
|
|
700
|
+
if target > self.component.config.get('open_duration') * 1000:
|
|
694
701
|
raise ValidationError(
|
|
695
702
|
"Target value lower than %d expected, "
|
|
696
703
|
"%d received instead" % (
|
|
697
704
|
self.component.config['open_duration'] * 1000,
|
|
698
|
-
|
|
705
|
+
target
|
|
699
706
|
)
|
|
700
707
|
)
|
|
708
|
+
if 'angle' in value:
|
|
709
|
+
try:
|
|
710
|
+
angle = int(value['angle'])
|
|
711
|
+
except:
|
|
712
|
+
raise ValidationError(
|
|
713
|
+
"Integer between 0 - 180 is required for blinds angle."
|
|
714
|
+
)
|
|
715
|
+
if angle < 0 or angle > 180:
|
|
716
|
+
raise ValidationError(
|
|
717
|
+
"Integer between 0 - 180 is required for blinds angle."
|
|
718
|
+
)
|
|
719
|
+
else:
|
|
720
|
+
value['angle'] = self.component.value['angle']
|
|
701
721
|
|
|
702
722
|
elif occasion == BEFORE_SET:
|
|
703
723
|
if not isinstance(value, dict):
|
|
704
724
|
raise ValidationError("Dictionary is expected")
|
|
705
725
|
for key, val in value.items():
|
|
706
|
-
if key not in ('target', 'position'):
|
|
726
|
+
if key not in ('target', 'position', 'angle'):
|
|
707
727
|
raise ValidationError(
|
|
708
|
-
"'target' or '
|
|
728
|
+
"'target', 'position' or 'angle' parameters are expected."
|
|
709
729
|
)
|
|
710
730
|
if key == 'position':
|
|
711
731
|
if val < 0:
|
|
@@ -725,17 +745,31 @@ class Blinds(ControllerBase, TimerMixin):
|
|
|
725
745
|
value['target'] = self.component.value.get('target')
|
|
726
746
|
if 'position' not in value:
|
|
727
747
|
value['position'] = self.component.value.get('position')
|
|
748
|
+
if 'angle' not in value:
|
|
749
|
+
value['angle'] = self.component.value.get('angle')
|
|
728
750
|
|
|
729
751
|
return value
|
|
730
752
|
|
|
731
753
|
def open(self):
|
|
732
|
-
|
|
754
|
+
send_val = {'target': 0}
|
|
755
|
+
angle = self.component.value.get('angle')
|
|
756
|
+
if angle is not None and 0 <= angle <= 180:
|
|
757
|
+
send_val['angle'] = angle
|
|
758
|
+
self.send(send_val)
|
|
733
759
|
|
|
734
760
|
def close(self):
|
|
735
|
-
self.
|
|
761
|
+
send_val = {'target': self.component.config['open_duration'] * 1000}
|
|
762
|
+
angle = self.component.value.get('angle')
|
|
763
|
+
if angle is not None and 0 <= angle <= 180:
|
|
764
|
+
send_val['angle'] = angle
|
|
765
|
+
self.send(send_val)
|
|
736
766
|
|
|
737
767
|
def stop(self):
|
|
738
|
-
|
|
768
|
+
send_val = {'target': -1}
|
|
769
|
+
angle = self.component.value.get('angle')
|
|
770
|
+
if angle is not None and 0 <= angle <= 180:
|
|
771
|
+
send_val['angle'] = angle
|
|
772
|
+
self.send(send_val)
|
|
739
773
|
|
|
740
774
|
|
|
741
775
|
class Watering(ControllerBase):
|
|
@@ -1433,10 +1467,8 @@ class AlarmClock(ControllerBase):
|
|
|
1433
1467
|
return current_value
|
|
1434
1468
|
|
|
1435
1469
|
|
|
1436
|
-
# ----------- Dummy controllers -----------------------------
|
|
1437
|
-
|
|
1438
1470
|
class StateSelect(ControllerBase):
|
|
1439
|
-
gateway_class =
|
|
1471
|
+
gateway_class = GenericGatewayHandler
|
|
1440
1472
|
name = _("State select")
|
|
1441
1473
|
base_type = 'state-select'
|
|
1442
1474
|
app_widget = StateSelectWidget
|
|
@@ -1452,40 +1484,51 @@ class StateSelect(ControllerBase):
|
|
|
1452
1484
|
return value
|
|
1453
1485
|
|
|
1454
1486
|
|
|
1487
|
+
# ----------- Dummy controllers -----------------------------
|
|
1488
|
+
|
|
1455
1489
|
class DummyBinarySensor(BinarySensor):
|
|
1456
1490
|
gateway_class = DummyGatewayHandler
|
|
1491
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
|
1457
1492
|
|
|
1458
1493
|
|
|
1459
1494
|
class DummyNumericSensor(NumericSensor):
|
|
1460
1495
|
gateway_class = DummyGatewayHandler
|
|
1496
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
|
1461
1497
|
|
|
1462
1498
|
|
|
1463
1499
|
class DummyMultiSensor(MultiSensor):
|
|
1464
1500
|
gateway_class = DummyGatewayHandler
|
|
1501
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
|
1465
1502
|
|
|
1466
1503
|
|
|
1467
1504
|
class DummySwitch(Switch):
|
|
1468
1505
|
gateway_class = DummyGatewayHandler
|
|
1506
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
|
1469
1507
|
|
|
1470
1508
|
|
|
1471
1509
|
class DummyDoubleSwitch(DoubleSwitch):
|
|
1472
1510
|
gateway_class = DummyGatewayHandler
|
|
1511
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
|
1473
1512
|
|
|
1474
1513
|
|
|
1475
1514
|
class DummyTripleSwitch(TripleSwitch):
|
|
1476
1515
|
gateway_class = DummyGatewayHandler
|
|
1516
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
|
1477
1517
|
|
|
1478
1518
|
|
|
1479
1519
|
class DummyQuadrupleSwitch(QuadrupleSwitch):
|
|
1480
1520
|
gateway_class = DummyGatewayHandler
|
|
1521
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
|
1481
1522
|
|
|
1482
1523
|
|
|
1483
1524
|
class DummyQuintupleSwitch(QuintupleSwitch):
|
|
1484
1525
|
gateway_class = DummyGatewayHandler
|
|
1526
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
|
1485
1527
|
|
|
1486
1528
|
|
|
1487
1529
|
class DummyDimmer(Dimmer):
|
|
1488
1530
|
gateway_class = DummyGatewayHandler
|
|
1531
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
|
1489
1532
|
|
|
1490
1533
|
def _prepare_for_send(self, value):
|
|
1491
1534
|
if self.component.config.get('inverse'):
|
|
@@ -1495,7 +1538,9 @@ class DummyDimmer(Dimmer):
|
|
|
1495
1538
|
|
|
1496
1539
|
class DummyDimmerPlus(DimmerPlus):
|
|
1497
1540
|
gateway_class = DummyGatewayHandler
|
|
1541
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
|
1498
1542
|
|
|
1499
1543
|
|
|
1500
1544
|
class DummyRGBWLight(RGBWLight):
|
|
1501
1545
|
gateway_class = DummyGatewayHandler
|
|
1546
|
+
info_template_path = 'generic/controllers_info/dummy.md'
|
simo/generic/forms.py
CHANGED
|
@@ -273,9 +273,6 @@ class ThermostatConfigForm(BaseComponentForm):
|
|
|
273
273
|
|
|
274
274
|
def __init__(self, *args, **kwargs):
|
|
275
275
|
super().__init__(*args, **kwargs)
|
|
276
|
-
if dynamic_settings['core__units_of_measure'] == 'imperial':
|
|
277
|
-
self.fields['min'].initial = 36
|
|
278
|
-
self.fields['max'].initial = 100
|
|
279
276
|
if self.instance.pk:
|
|
280
277
|
self.fields['mode'].initial = \
|
|
281
278
|
self.instance.config['user_config']['mode']
|
simo/generic/gateways.py
CHANGED
|
@@ -12,6 +12,7 @@ from django.db import connection as db_connection
|
|
|
12
12
|
from django.db.models import Q
|
|
13
13
|
import paho.mqtt.client as mqtt
|
|
14
14
|
from simo.core.models import Component
|
|
15
|
+
from simo.core.middleware import introduce_instance
|
|
15
16
|
from simo.core.gateways import BaseObjectCommandsGatewayHandler
|
|
16
17
|
from simo.core.forms import BaseGatewayForm
|
|
17
18
|
from simo.core.utils.logs import StreamToLogger
|
|
@@ -131,6 +132,7 @@ class ScriptRunHandler(multiprocessing.Process):
|
|
|
131
132
|
self.component = Component.objects.get(id=self.component_id)
|
|
132
133
|
tz = pytz.timezone(self.component.zone.instance.timezone)
|
|
133
134
|
timezone.activate(tz)
|
|
135
|
+
introduce_instance(self.component.zone.instance)
|
|
134
136
|
self.logger = get_component_logger(self.component)
|
|
135
137
|
sys.stdout = StreamToLogger(self.logger, logging.INFO)
|
|
136
138
|
sys.stderr = StreamToLogger(self.logger, logging.ERROR)
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
<span style="bottom: 5px; position: relative;">
|
|
28
28
|
position: <strong id='blinds_position'>{{ obj.value.position }}</strong>,
|
|
29
|
-
target: <strong id='blinds_target'>{{ obj.value.target }}</strong
|
|
29
|
+
target: <strong id='blinds_target'>{{ obj.value.target }}</strong>,
|
|
30
|
+
angle: <strong id='blinds_angle'>{{ obj.value.angle }}</strong>
|
|
30
31
|
</span>
|
|
31
32
|
|
|
32
33
|
</div>
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
position: relative;
|
|
10
10
|
bottom: -4px;
|
|
11
11
|
"></span>
|
|
12
|
-
{{ obj.value.current.temp }}ᴼ {% if
|
|
12
|
+
{{ obj.value.current.temp }}ᴼ {% if obj__zone__instance__units_of_measure == 'metric' %}C{% else %}F{% endif %}
|
|
13
13
|
</div>
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
@@ -4,7 +4,7 @@ server{
|
|
|
4
4
|
|
|
5
5
|
charset utf-8;
|
|
6
6
|
|
|
7
|
-
client_max_body_size
|
|
7
|
+
client_max_body_size 100M;
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
location /protected/static {
|
|
@@ -55,7 +55,7 @@ server{
|
|
|
55
55
|
|
|
56
56
|
ssl_session_cache shared:TLS:2m;
|
|
57
57
|
|
|
58
|
-
client_max_body_size
|
|
58
|
+
client_max_body_size 100M;
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
location /protected/static {
|