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.

Files changed (124) hide show
  1. simo/__pycache__/asgi.cpython-38.pyc +0 -0
  2. simo/__pycache__/settings.cpython-38.pyc +0 -0
  3. simo/__pycache__/wsgi.cpython-38.pyc +0 -0
  4. simo/asgi.py +1 -1
  5. simo/core/__init__.py +1 -0
  6. simo/core/__pycache__/__init__.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/apps.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/auto_urls.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/dynamic_settings.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  18. simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
  19. simo/core/__pycache__/managers.cpython-38.pyc +0 -0
  20. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  21. simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
  22. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  23. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  24. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  25. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  26. simo/core/admin.py +26 -26
  27. simo/core/api.py +22 -2
  28. simo/core/api_meta.py +23 -13
  29. simo/core/app_widgets.py +6 -0
  30. simo/core/apps.py +13 -0
  31. simo/core/auto_urls.py +2 -3
  32. simo/core/base_types.py +1 -0
  33. simo/core/controllers.py +57 -0
  34. simo/core/dynamic_settings.py +0 -8
  35. simo/core/form_fields.py +93 -0
  36. simo/core/forms.py +16 -101
  37. simo/core/gateways.py +1 -1
  38. simo/core/managers.py +14 -1
  39. simo/core/migrations/0037_auto_20240606_1057.py +33 -0
  40. simo/core/migrations/0038_remove_instance_cover_image_and_more.py +30 -0
  41. simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-38.pyc +0 -0
  42. simo/core/migrations/__pycache__/0038_remove_instance_cover_image_and_more.cpython-38.pyc +0 -0
  43. simo/core/models.py +30 -16
  44. simo/core/permissions.py +6 -3
  45. simo/core/serializers.py +77 -5
  46. simo/core/signal_receivers.py +25 -0
  47. simo/core/static/admin/css/simo.css +14 -0
  48. simo/core/tasks.py +82 -49
  49. simo/core/templates/admin/controller_widgets/button.html +8 -0
  50. simo/core/templates/admin/core/component_change_form.html +97 -0
  51. simo/core/templates/admin/formset_widget.html +88 -118
  52. simo/core/templates/admin/formset_widget_old.html +122 -0
  53. simo/core/templates/admin/user_tools.html +0 -3
  54. simo/core/templates/admin/wizard/wizard_add.html +16 -9
  55. simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
  56. simo/core/utils/__pycache__/cache.cpython-38.pyc +0 -0
  57. simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
  58. simo/core/utils/admin.py +11 -0
  59. simo/core/utils/cache.py +15 -0
  60. simo/core/utils/formsets.py +11 -18
  61. simo/core/views.py +2 -85
  62. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  63. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  64. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  65. simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
  66. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  67. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  68. simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
  69. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  70. simo/fleet/auto_urls.py +7 -1
  71. simo/fleet/controllers.py +194 -31
  72. simo/fleet/forms.py +223 -87
  73. simo/fleet/gateways.py +53 -2
  74. simo/fleet/migrations/0036_auto_20240605_0702.py +68 -0
  75. simo/fleet/migrations/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.py +27 -0
  76. simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-38.pyc +0 -0
  77. simo/fleet/migrations/__pycache__/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.cpython-38.pyc +0 -0
  78. simo/fleet/models.py +35 -6
  79. simo/fleet/socket_consumers.py +1 -1
  80. simo/fleet/templates/fleet/controllers_info/button.md +16 -0
  81. simo/fleet/utils.py +31 -1
  82. simo/fleet/views.py +45 -0
  83. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  84. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  85. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  86. simo/generic/controllers.py +61 -16
  87. simo/generic/forms.py +0 -3
  88. simo/generic/gateways.py +2 -0
  89. simo/generic/templates/admin/controller_widgets/blinds.html +2 -1
  90. simo/generic/templates/admin/controller_widgets/weather_forecast.html +1 -1
  91. simo/generic/templates/generic/controllers_info/dummy.md +3 -0
  92. simo/generic/templates/generic/controllers_info/stateselect.md +2 -0
  93. simo/management/__init__.py +0 -0
  94. simo/management/__pycache__/__init__.cpython-38.pyc +0 -0
  95. simo/management/__pycache__/on_http_start.cpython-38.pyc +0 -0
  96. simo/{_hub_template → management/_hub_template}/hub/nginx.conf +2 -2
  97. simo/{auto_update.py → management/auto_update.py} +3 -0
  98. simo/{cli.py → management/copy_template.py} +3 -16
  99. simo/management/install.py +258 -0
  100. simo/{on_http_start.py → management/on_http_start.py} +22 -2
  101. simo/settings.py +20 -4
  102. simo/users/__init__.py +1 -0
  103. simo/users/__pycache__/__init__.cpython-38.pyc +0 -0
  104. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  105. simo/users/__pycache__/apps.cpython-38.pyc +0 -0
  106. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  107. simo/users/apps.py +9 -0
  108. simo/users/migrations/__pycache__/0029_alter_instanceuser_options_instanceuser_order.cpython-38.pyc +0 -0
  109. simo/users/migrations/__pycache__/0030_alter_instanceuser_options_remove_instanceuser_order.cpython-38.pyc +0 -0
  110. simo/users/models.py +16 -3
  111. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/METADATA +5 -3
  112. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/RECORD +122 -95
  113. simo-2.1.2.dist-info/entry_points.txt +2 -0
  114. simo/__pycache__/on_http_start.cpython-38.pyc +0 -0
  115. simo/wsgi.py +0 -7
  116. /simo/{_hub_template → management/_hub_template}/hub/asgi.py +0 -0
  117. /simo/{_hub_template → management/_hub_template}/hub/celeryc.py +0 -0
  118. /simo/{_hub_template → management/_hub_template}/hub/manage.py +0 -0
  119. /simo/{_hub_template → management/_hub_template}/hub/settings.py +0 -0
  120. /simo/{_hub_template → management/_hub_template}/hub/supervisor.conf +0 -0
  121. /simo/{_hub_template → management/_hub_template}/hub/urls.py +0 -0
  122. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/LICENSE.md +0 -0
  123. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/WHEEL +0 -0
  124. {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 TTLock
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
+ ]
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
- input=data.get('input'), output=data.get('output'),
259
- capacitive=data.get('capacitive'), adc=data.get('adc'),
260
- native=data.get('native'), note=data.get('note')
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
- id__in=instance.config.get('members', [])
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)
@@ -102,7 +102,7 @@ class FleetConsumer(AsyncWebsocketConsumer):
102
102
  )
103
103
  if not new:
104
104
  for key, val in defaults.items():
105
- if key in ('new', ):
105
+ if key == 'name':
106
106
  continue
107
107
  setattr(colonel, key, val)
108
108
  colonel.save()
@@ -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]
@@ -223,7 +223,7 @@ class Thermostat(ControllerBase):
223
223
  def default_config(self):
224
224
  min = 3
225
225
  max = 36
226
- if dynamic_settings['core__units_of_measure'] == 'imperial':
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 dynamic_settings['core__units_of_measure'] == 'imperial':
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 type(value) not in (float, int):
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 value > self.component.config.get('open_duration') * 1000:
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
- value
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 'position' parameter expected."
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
- self.send(0)
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.send(self.component.config['open_duration'] * 1000)
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
- self.send(-1)
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 = DummyGatewayHandler
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 global_preferences.core__units_of_measure == 'metric' %}C{% else %}F{% endif %}
12
+ {{ obj.value.current.temp }}ᴼ {% if obj__zone__instance__units_of_measure == 'metric' %}C{% else %}F{% endif %}
13
13
  </div>
@@ -0,0 +1,3 @@
1
+ Dummy components do not represent any real devices;
2
+ however, they are handy when you are in the process
3
+ of building custom, script-based systems.
@@ -0,0 +1,2 @@
1
+ This component does not represent any real device;
2
+ however it is super handy if you are in a process of building state aware systems.
File without changes
@@ -4,7 +4,7 @@ server{
4
4
 
5
5
  charset utf-8;
6
6
 
7
- client_max_body_size 20M;
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 20M;
58
+ client_max_body_size 100M;
59
59
 
60
60
 
61
61
  location /protected/static {