simo 2.4.2__py3-none-any.whl → 2.5.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/backups/tasks.py +11 -1
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/events.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.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__/serializers.cpython-38.pyc +0 -0
- simo/core/__pycache__/socket_consumers.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 +4 -4
- simo/core/api.py +20 -4
- simo/core/app_widgets.py +5 -0
- simo/core/controllers.py +4 -3
- simo/core/events.py +13 -4
- simo/core/forms.py +2 -0
- simo/core/management/commands/gateways_manager.py +0 -3
- simo/core/middleware.py +12 -6
- simo/core/models.py +26 -6
- simo/core/serializers.py +17 -17
- simo/core/socket_consumers.py +6 -2
- simo/core/static/admin/js/codemirror-init.js +1 -0
- simo/core/tasks.py +10 -7
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.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/controllers.py +86 -22
- simo/fleet/forms.py +84 -9
- simo/fleet/migrations/0039_auto_20241016_1047.py +28 -0
- simo/fleet/migrations/0040_alter_colonel_pwm_frequency.py +18 -0
- simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-38.pyc +0 -0
- simo/fleet/models.py +6 -2
- simo/fleet/socket_consumers.py +13 -5
- 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/controllers.py +45 -2
- simo/generic/forms.py +81 -7
- simo/generic/models.py +0 -1
- simo/generic/scripting/__init__.py +16 -0
- simo/generic/scripting/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/generic/scripting/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/generic/scripting/helpers.py +35 -0
- simo/generic/scripting/serializers.py +77 -0
- simo/generic/templates/admin/controller_widgets/weather_forecast.html +2 -2
- simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
- simo/notifications/utils.py +30 -12
- simo/scripting.py +2 -2
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/__pycache__/managers.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/users/__pycache__/utils.cpython-38.pyc +0 -0
- simo/users/api.py +36 -7
- simo/users/managers.py +5 -1
- simo/users/migrations/0034_instanceuser_last_seen_location_and_more.py +24 -0
- simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-38.pyc +0 -0
- simo/users/models.py +37 -32
- simo/users/serializers.py +11 -8
- simo/users/utils.py +14 -3
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/METADATA +1 -1
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/RECORD +73 -60
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/WHEEL +1 -1
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/LICENSE.md +0 -0
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/entry_points.txt +0 -0
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/top_level.txt +0 -0
simo/backups/tasks.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os, subprocess, json, uuid, datetime, shutil, pytz
|
|
2
|
-
from datetime import datetime,
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from django.utils import timezone
|
|
3
4
|
from celeryc import celery_app
|
|
4
5
|
from simo.conf import dynamic_settings
|
|
5
6
|
from simo.core.utils.helpers import get_random_string
|
|
@@ -407,8 +408,17 @@ def restore_backup(backup_id):
|
|
|
407
408
|
subprocess.run('reboot', shell=True)
|
|
408
409
|
|
|
409
410
|
|
|
411
|
+
@celery_app.task
|
|
412
|
+
def clean_old_logs():
|
|
413
|
+
from .models import BackupLog
|
|
414
|
+
BackupLog.objects.filter(
|
|
415
|
+
datetime__lt=timezone.now() - timedelta(days=90)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
410
419
|
@celery_app.on_after_finalize.connect
|
|
411
420
|
def setup_periodic_tasks(sender, **kwargs):
|
|
412
421
|
sender.add_periodic_task(60 * 60, check_backups.s())
|
|
413
422
|
# perform auto backup every 12 hours
|
|
414
423
|
sender.add_periodic_task(60 * 60 * 12, perform_backup.s())
|
|
424
|
+
sender.add_periodic_task(60 * 60, clean_old_logs.s())
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/core/admin.py
CHANGED
|
@@ -232,11 +232,11 @@ class ComponentPermissionInline(admin.TabularInline):
|
|
|
232
232
|
return qs.filter(role__instance__in=request.user.instances)
|
|
233
233
|
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
def has_delete_permission(self, request, obj=None):
|
|
236
|
+
return False
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
def has_add_permission(self, request, obj=None):
|
|
239
|
+
return False
|
|
240
240
|
|
|
241
241
|
|
|
242
242
|
@admin.register(Component)
|
simo/core/api.py
CHANGED
|
@@ -167,6 +167,12 @@ def get_components_queryset(instance, user):
|
|
|
167
167
|
).values('id').first()
|
|
168
168
|
if main_alarm_group:
|
|
169
169
|
c_ids.add(main_alarm_group['id'])
|
|
170
|
+
state = Component.objects.filter(
|
|
171
|
+
zone__instance=instance,
|
|
172
|
+
base_type='state-select', config__is_main=True
|
|
173
|
+
).values('id').first()
|
|
174
|
+
if state:
|
|
175
|
+
c_ids.add(state['id'])
|
|
170
176
|
|
|
171
177
|
user_role = user.get_role(instance)
|
|
172
178
|
|
|
@@ -369,7 +375,7 @@ class ComponentHistoryViewSet(InstanceMixin, viewsets.ReadOnlyModelViewSet):
|
|
|
369
375
|
if not component.controller:
|
|
370
376
|
return
|
|
371
377
|
|
|
372
|
-
history_display_example = component.controller.
|
|
378
|
+
history_display_example = component.controller._history_display([component.value])
|
|
373
379
|
if not history_display_example:
|
|
374
380
|
return None
|
|
375
381
|
|
|
@@ -393,9 +399,9 @@ class ComponentHistoryViewSet(InstanceMixin, viewsets.ReadOnlyModelViewSet):
|
|
|
393
399
|
values = []
|
|
394
400
|
for item in history_items:
|
|
395
401
|
values.append(item.value)
|
|
396
|
-
val = component.controller.
|
|
402
|
+
val = component.controller._history_display(values)
|
|
397
403
|
else:
|
|
398
|
-
val = component.controller.
|
|
404
|
+
val = component.controller._history_display([])
|
|
399
405
|
|
|
400
406
|
if not val:
|
|
401
407
|
val = prev_val
|
|
@@ -425,7 +431,7 @@ class ComponentHistoryViewSet(InstanceMixin, viewsets.ReadOnlyModelViewSet):
|
|
|
425
431
|
component=component, date__lt=start_from, type='value'
|
|
426
432
|
).order_by('date').last()
|
|
427
433
|
if last_event:
|
|
428
|
-
prev_val = component.controller.
|
|
434
|
+
prev_val = component.controller._history_display([last_event.value])
|
|
429
435
|
else:
|
|
430
436
|
prev_val = history_display_example
|
|
431
437
|
|
|
@@ -544,6 +550,15 @@ class SettingsViewSet(InstanceMixin, viewsets.GenericViewSet):
|
|
|
544
550
|
if main_alarm_group:
|
|
545
551
|
main_alarm_group_id = main_alarm_group.id
|
|
546
552
|
|
|
553
|
+
main_state = Component.objects.filter(
|
|
554
|
+
zone__instance=self.instance,
|
|
555
|
+
base_type='state-select', config__is_main=True
|
|
556
|
+
).first()
|
|
557
|
+
if main_state:
|
|
558
|
+
main_state = main_state.id
|
|
559
|
+
else:
|
|
560
|
+
main_state = None
|
|
561
|
+
|
|
547
562
|
return RESTResponse({
|
|
548
563
|
'hub_uid': dynamic_settings['core__hub_uid'],
|
|
549
564
|
'instance_name': self.instance.name,
|
|
@@ -553,6 +568,7 @@ class SettingsViewSet(InstanceMixin, viewsets.GenericViewSet):
|
|
|
553
568
|
'last_event': last_event,
|
|
554
569
|
'weather_forecast': wf_comp_id,
|
|
555
570
|
'main_alarm_group': main_alarm_group_id,
|
|
571
|
+
'main_state': main_state,
|
|
556
572
|
# TODO: Remove these two when the app is updated for everybody.
|
|
557
573
|
'remote_http': dynamic_settings['core__remote_http'],
|
|
558
574
|
'local_http': 'https://%s' % get_self_ip(),
|
simo/core/app_widgets.py
CHANGED
simo/core/controllers.py
CHANGED
|
@@ -302,7 +302,7 @@ class ControllerBase(ABC):
|
|
|
302
302
|
from .models import Component
|
|
303
303
|
Component.objects.bulk_send(bulk_send_map)
|
|
304
304
|
|
|
305
|
-
def
|
|
305
|
+
def _history_display(self, values):
|
|
306
306
|
assert type(values) in (list, tuple)
|
|
307
307
|
|
|
308
308
|
if type(self.component.value) in (int, float):
|
|
@@ -445,7 +445,7 @@ class MultiSensor(ControllerBase):
|
|
|
445
445
|
))
|
|
446
446
|
return value
|
|
447
447
|
|
|
448
|
-
def
|
|
448
|
+
def _history_display(self, values):
|
|
449
449
|
assert type(values) in (list, tuple)
|
|
450
450
|
|
|
451
451
|
vectors = []
|
|
@@ -704,6 +704,8 @@ class RGBWLight(ControllerBase, TimerMixin, OnOffPokerMixin):
|
|
|
704
704
|
else:
|
|
705
705
|
if len(color) != 7:
|
|
706
706
|
raise ValidationError("Bad color value!")
|
|
707
|
+
if 'scenes' not in value:
|
|
708
|
+
value['scenes'] = self.component.value['scenes']
|
|
707
709
|
return value
|
|
708
710
|
|
|
709
711
|
def turn_off(self):
|
|
@@ -722,7 +724,6 @@ class RGBWLight(ControllerBase, TimerMixin, OnOffPokerMixin):
|
|
|
722
724
|
self.send(self.component.value)
|
|
723
725
|
|
|
724
726
|
|
|
725
|
-
|
|
726
727
|
class MultiSwitchBase(ControllerBase):
|
|
727
728
|
|
|
728
729
|
def _validate_val(self, value, occasion=None):
|
simo/core/events.py
CHANGED
|
@@ -3,6 +3,7 @@ import sys
|
|
|
3
3
|
import json
|
|
4
4
|
import traceback
|
|
5
5
|
import pytz
|
|
6
|
+
import inspect
|
|
6
7
|
from django.contrib.contenttypes.models import ContentType
|
|
7
8
|
from django.conf import settings
|
|
8
9
|
import paho.mqtt.client as mqtt
|
|
@@ -92,7 +93,9 @@ def get_event_obj(payload, model_class=None, gateway=None):
|
|
|
92
93
|
|
|
93
94
|
class OnChangeMixin:
|
|
94
95
|
|
|
96
|
+
_on_change_function = None
|
|
95
97
|
on_change_fields = ('value', )
|
|
98
|
+
_mqtt_client = None
|
|
96
99
|
|
|
97
100
|
def get_instance(self):
|
|
98
101
|
# default for component
|
|
@@ -127,10 +130,16 @@ class OnChangeMixin:
|
|
|
127
130
|
|
|
128
131
|
self.refresh_from_db()
|
|
129
132
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
if inspect.getfullargspec(self._on_change_function).args:
|
|
134
|
+
try:
|
|
135
|
+
self._on_change_function(self)
|
|
136
|
+
except Exception:
|
|
137
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
138
|
+
else:
|
|
139
|
+
try:
|
|
140
|
+
self._on_change_function()
|
|
141
|
+
except Exception:
|
|
142
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
134
143
|
|
|
135
144
|
def on_change(self, function):
|
|
136
145
|
if function:
|
simo/core/forms.py
CHANGED
|
@@ -212,6 +212,8 @@ class ComponentAdminForm(forms.ModelForm):
|
|
|
212
212
|
controller_type = None
|
|
213
213
|
has_icon = True
|
|
214
214
|
has_alarm = True
|
|
215
|
+
# do not allow modification via app of these fields
|
|
216
|
+
app_exclude_fields = []
|
|
215
217
|
|
|
216
218
|
# fields that can be edited via SIMO.io app by instance owners.
|
|
217
219
|
# Users who have is_owner enabled on their user role.
|
simo/core/middleware.py
CHANGED
|
@@ -26,16 +26,22 @@ def introduce_instance(instance, request=None):
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def get_current_instance(request=None):
|
|
29
|
-
|
|
30
|
-
if
|
|
31
|
-
from simo.core.models import Instance
|
|
29
|
+
from simo.core.models import Instance
|
|
30
|
+
if request and request.session.get('instance_id'):
|
|
32
31
|
instance = Instance.objects.filter(
|
|
33
|
-
id=request.session['instance_id']
|
|
32
|
+
id=request.session['instance_id'], is_active=True
|
|
34
33
|
).first()
|
|
35
34
|
if not instance:
|
|
36
35
|
del request.session['instance_id']
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
introduce_instance(instance, request)
|
|
37
|
+
|
|
38
|
+
instance = getattr(_thread_locals, 'instance', None)
|
|
39
|
+
|
|
40
|
+
if not instance:
|
|
41
|
+
from .models import Instance
|
|
42
|
+
instance = Instance.objects.filter(is_active=True).first()
|
|
43
|
+
if instance:
|
|
44
|
+
introduce_instance(instance)
|
|
39
45
|
return instance
|
|
40
46
|
|
|
41
47
|
|
simo/core/models.py
CHANGED
|
@@ -406,8 +406,8 @@ def is_in_alarm(self):
|
|
|
406
406
|
|
|
407
407
|
_controller_initiated = False
|
|
408
408
|
|
|
409
|
-
|
|
410
|
-
|
|
409
|
+
|
|
410
|
+
|
|
411
411
|
_obj_ct_id = 0
|
|
412
412
|
|
|
413
413
|
class Meta:
|
|
@@ -517,11 +517,13 @@ def is_in_alarm(self):
|
|
|
517
517
|
actor.last_action = timezone.now()
|
|
518
518
|
actor.save()
|
|
519
519
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
['value', 'arm_status', 'battery_level', 'alive', 'meta']
|
|
523
|
-
):
|
|
520
|
+
changing_fields = ['value', 'arm_status', 'battery_level', 'alive', 'meta']
|
|
521
|
+
if any(f in dirty_fields for f in changing_fields):
|
|
524
522
|
self.last_change = timezone.now()
|
|
523
|
+
if 'update_fields' in kwargs \
|
|
524
|
+
and 'last_change' not in kwargs['update_fields']:
|
|
525
|
+
kwargs['update_fields'].append('last_change')
|
|
526
|
+
|
|
525
527
|
|
|
526
528
|
modifying_fields = (
|
|
527
529
|
'name', 'icon', 'zone', 'category', 'config', 'meta',
|
|
@@ -529,6 +531,9 @@ def is_in_alarm(self):
|
|
|
529
531
|
)
|
|
530
532
|
if any(f in dirty_fields for f in modifying_fields):
|
|
531
533
|
self.last_modified = timezone.now()
|
|
534
|
+
if 'update_fields' in kwargs \
|
|
535
|
+
and 'last_modified' not in kwargs['update_fields']:
|
|
536
|
+
kwargs['update_fields'].append('last_modified')
|
|
532
537
|
|
|
533
538
|
obj = super().save(*args, **kwargs)
|
|
534
539
|
|
|
@@ -581,6 +586,21 @@ def is_in_alarm(self):
|
|
|
581
586
|
return False
|
|
582
587
|
return perm.write
|
|
583
588
|
|
|
589
|
+
def get_controller_methods(self):
|
|
590
|
+
c_methods = []
|
|
591
|
+
for m in inspect.getmembers(
|
|
592
|
+
self.controller, predicate=inspect.ismethod
|
|
593
|
+
):
|
|
594
|
+
method = m[0]
|
|
595
|
+
if method.startswith('_'):
|
|
596
|
+
continue
|
|
597
|
+
if method in ('info', 'set'):
|
|
598
|
+
continue
|
|
599
|
+
c_methods.append(method)
|
|
600
|
+
if self.alarm_category:
|
|
601
|
+
c_methods.extend(['arm', 'disarm'])
|
|
602
|
+
return c_methods
|
|
603
|
+
|
|
584
604
|
|
|
585
605
|
class ComponentHistory(models.Model):
|
|
586
606
|
component = models.ForeignKey(
|
simo/core/serializers.py
CHANGED
|
@@ -27,6 +27,8 @@ from .forms import ComponentAdminForm
|
|
|
27
27
|
from .models import Category, Zone, Icon, ComponentHistory
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
|
|
31
|
+
|
|
30
32
|
class TimestampField(serializers.Field):
|
|
31
33
|
|
|
32
34
|
def to_representation(self, value):
|
|
@@ -318,7 +320,7 @@ class ComponentSerializer(FormSerializer):
|
|
|
318
320
|
form_field = form[field_name]
|
|
319
321
|
|
|
320
322
|
cls = form_field.field.__class__
|
|
321
|
-
if
|
|
323
|
+
if isinstance(form_field.field.widget, forms.Textarea):
|
|
322
324
|
serializer_field_class = TextAreaSerializerField
|
|
323
325
|
else:
|
|
324
326
|
try:
|
|
@@ -416,16 +418,19 @@ class ComponentSerializer(FormSerializer):
|
|
|
416
418
|
controller_uid=controller_uid, instance=instance,
|
|
417
419
|
**kwargs
|
|
418
420
|
)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
421
|
+
|
|
422
|
+
user_role = self.context['request'].user.get_role(
|
|
423
|
+
self.context['instance']
|
|
424
|
+
)
|
|
425
|
+
for field_name in list(form.fields.keys()):
|
|
426
|
+
if field_name in form.app_exclude_fields:
|
|
427
|
+
del form.fields[field_name]
|
|
428
|
+
continue
|
|
429
|
+
if field_name in form.basic_fields:
|
|
430
|
+
continue
|
|
431
|
+
if self.context['request'].user.is_master or user_role.is_superuser:
|
|
432
|
+
continue
|
|
433
|
+
del form.fields[field_name]
|
|
429
434
|
|
|
430
435
|
if form_key is not None:
|
|
431
436
|
self.context['forms'][form_key] = form
|
|
@@ -492,12 +497,7 @@ class ComponentSerializer(FormSerializer):
|
|
|
492
497
|
raise serializers.ValidationError(form.errors)
|
|
493
498
|
|
|
494
499
|
def get_controller_methods(self, obj):
|
|
495
|
-
|
|
496
|
-
obj.controller, predicate=inspect.ismethod
|
|
497
|
-
) if not m[0].startswith('_')]
|
|
498
|
-
if obj.alarm_category:
|
|
499
|
-
c_methods.extend(['arm', 'disarm'])
|
|
500
|
-
return c_methods
|
|
500
|
+
return obj.get_controller_methods()
|
|
501
501
|
|
|
502
502
|
def get_info(self, obj):
|
|
503
503
|
if obj.controller:
|
simo/core/socket_consumers.py
CHANGED
|
@@ -71,7 +71,9 @@ class LogConsumer(AsyncWebsocketConsumer):
|
|
|
71
71
|
if not role or not role.is_superuser:
|
|
72
72
|
return self.close()
|
|
73
73
|
|
|
74
|
-
self.log_file_path =
|
|
74
|
+
self.log_file_path = await sync_to_async(
|
|
75
|
+
get_log_file_path, thread_sensitive=True
|
|
76
|
+
)(self.obj)
|
|
75
77
|
self.log_file = open(self.log_file_path)
|
|
76
78
|
lines = [l.rstrip('\n') for l in self.log_file]
|
|
77
79
|
|
|
@@ -105,7 +107,9 @@ class LogConsumer(AsyncWebsocketConsumer):
|
|
|
105
107
|
try:
|
|
106
108
|
line = self.log_file.readline()
|
|
107
109
|
except:
|
|
108
|
-
self.log_file_path =
|
|
110
|
+
self.log_file_path = await sync_to_async(
|
|
111
|
+
get_log_file_path, thread_sensitive=True
|
|
112
|
+
)(self.obj)
|
|
109
113
|
self.log_file = open(self.log_file_path)
|
|
110
114
|
continue
|
|
111
115
|
if not line:
|
simo/core/tasks.py
CHANGED
|
@@ -211,7 +211,11 @@ def sync_with_remote():
|
|
|
211
211
|
).first()
|
|
212
212
|
if weather_component:
|
|
213
213
|
weather_component.track_history = False
|
|
214
|
-
weather_component.controller.set(
|
|
214
|
+
weather_component.controller.set(
|
|
215
|
+
weather_forecast.pop('current', None)
|
|
216
|
+
)
|
|
217
|
+
weather_component.meta['forecast'] = weather_forecast
|
|
218
|
+
weather_component.save()
|
|
215
219
|
|
|
216
220
|
for email, options in users_data.items():
|
|
217
221
|
|
|
@@ -380,15 +384,14 @@ def low_battery_notifications():
|
|
|
380
384
|
zone__instance=instance,
|
|
381
385
|
battery_level__isnull=False, battery_level__lt=20
|
|
382
386
|
):
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if users:
|
|
387
|
+
iusers = comp.zone.instance.instance_users.filter(
|
|
388
|
+
is_active=True, role__is_owner=True
|
|
389
|
+
)
|
|
390
|
+
if iusers:
|
|
388
391
|
notify_users(
|
|
389
392
|
comp.zone.instance, 'warning',
|
|
390
393
|
f"Low battery ({comp.battery_level}%) on {comp}",
|
|
391
|
-
component=comp,
|
|
394
|
+
component=comp, instance_users=iusers
|
|
392
395
|
)
|
|
393
396
|
|
|
394
397
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/fleet/controllers.py
CHANGED
|
@@ -10,8 +10,7 @@ from simo.core.controllers import (
|
|
|
10
10
|
Switch as BaseSwitch, Dimmer as BaseDimmer,
|
|
11
11
|
MultiSensor as BaseMultiSensor, RGBWLight as BaseRGBWLight
|
|
12
12
|
)
|
|
13
|
-
from simo.
|
|
14
|
-
from simo.core.app_widgets import NumericSensorWidget
|
|
13
|
+
from simo.core.app_widgets import NumericSensorWidget, AirQualityWidget
|
|
15
14
|
from simo.core.controllers import Lock, ControllerBase, SingleSwitchWidget
|
|
16
15
|
from simo.core.utils.helpers import heat_index
|
|
17
16
|
from simo.core.utils.serialization import (
|
|
@@ -23,7 +22,7 @@ from .gateways import FleetGatewayHandler
|
|
|
23
22
|
from .forms import (
|
|
24
23
|
ColonelPinChoiceField,
|
|
25
24
|
ColonelBinarySensorConfigForm, ColonelButtonConfigForm,
|
|
26
|
-
ColonelSwitchConfigForm, ColonelPWMOutputConfigForm,
|
|
25
|
+
ColonelSwitchConfigForm, ColonelPWMOutputConfigForm, DCDriverConfigForm,
|
|
27
26
|
ColonelNumericSensorConfigForm, ColonelRGBLightConfigForm,
|
|
28
27
|
ColonelDHTSensorConfigForm, DS18B20SensorConfigForm,
|
|
29
28
|
BME680SensorConfigForm, MPC9808SensorConfigForm, ENS160SensorConfigForm,
|
|
@@ -239,6 +238,7 @@ class ENS160AirQualitySensor(FleeDeviceMixin, BaseMultiSensor):
|
|
|
239
238
|
gateway_class = FleetGatewayHandler
|
|
240
239
|
config_form = ENS160SensorConfigForm
|
|
241
240
|
name = "ENS160 Air Quality Sensor (I2C)"
|
|
241
|
+
app_widget = AirQualityWidget
|
|
242
242
|
|
|
243
243
|
default_value = [
|
|
244
244
|
["CO2", 0, "ppm"],
|
|
@@ -386,42 +386,87 @@ class PWMOutput(FadeMixin, FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
|
|
|
386
386
|
value = conf.get('min', 0)
|
|
387
387
|
|
|
388
388
|
if value >= conf.get('max', 100):
|
|
389
|
-
|
|
390
|
-
pwm_value = 0
|
|
391
|
-
else:
|
|
392
|
-
pwm_value = 1023
|
|
389
|
+
pwm_value = 0
|
|
393
390
|
elif value <= conf.get('min', 100):
|
|
394
|
-
|
|
395
|
-
pwm_value = 1023
|
|
396
|
-
else:
|
|
397
|
-
pwm_value = 0
|
|
391
|
+
pwm_value = 1023
|
|
398
392
|
else:
|
|
399
393
|
val_amplitude = conf.get('max', 100) - conf.get('min', 0)
|
|
400
394
|
val_relative = value / val_amplitude
|
|
401
|
-
pwm_amplitude = conf.get('duty_max', 1023) - conf.get('duty_min', 0.0)
|
|
402
|
-
pwm_value = conf.get('duty_min', 0.0) + pwm_amplitude * val_relative
|
|
403
395
|
|
|
404
|
-
|
|
405
|
-
|
|
396
|
+
duty_max = 1023 - (conf.get('device_min', 0) * 0.01 * 1023)
|
|
397
|
+
duty_min = 1023 - conf.get('device_max', 100) * 0.01 * 1023
|
|
398
|
+
|
|
399
|
+
pwm_amplitude = duty_max - duty_min
|
|
400
|
+
pwm_value = duty_min + pwm_amplitude * val_relative
|
|
401
|
+
|
|
402
|
+
pwm_value = duty_max - pwm_value + duty_min
|
|
406
403
|
|
|
407
404
|
return pwm_value
|
|
408
405
|
|
|
409
406
|
def _prepare_for_set(self, pwm_value):
|
|
410
407
|
conf = self.component.config
|
|
411
|
-
|
|
408
|
+
duty_max = 1023 - (conf.get('device_min', 0) * 0.01 * 1023)
|
|
409
|
+
duty_min = 1023 - conf.get('device_max', 100) * 0.01 * 1023
|
|
410
|
+
|
|
411
|
+
if pwm_value > duty_max:
|
|
412
412
|
value = conf.get('max', 100)
|
|
413
|
-
elif pwm_value <
|
|
413
|
+
elif pwm_value < duty_min:
|
|
414
414
|
value = conf.get('min', 0)
|
|
415
415
|
else:
|
|
416
|
-
pwm_amplitude =
|
|
417
|
-
relative_value = (pwm_value -
|
|
416
|
+
pwm_amplitude =duty_max - duty_min
|
|
417
|
+
relative_value = (pwm_value - duty_min) / pwm_amplitude
|
|
418
418
|
val_amplitude = conf.get('max', 100) - conf.get('min', 0)
|
|
419
419
|
value = conf.get('min', 0) + val_amplitude * relative_value
|
|
420
420
|
|
|
421
|
-
|
|
422
|
-
value = conf.get('max', 100) - value + conf.get('min', 0)
|
|
421
|
+
value = conf.get('max', 100) - value + conf.get('min', 0)
|
|
423
422
|
|
|
424
|
-
return value
|
|
423
|
+
return round(value, 3)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class DCDriver(FadeMixin, FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
|
|
427
|
+
name = "0 - 24V DC Driver"
|
|
428
|
+
config_form = DCDriverConfigForm
|
|
429
|
+
default_value_units = 'V'
|
|
430
|
+
|
|
431
|
+
def _prepare_for_send(self, value):
|
|
432
|
+
conf = self.component.config
|
|
433
|
+
if value >= conf.get('max', 24):
|
|
434
|
+
value = conf.get('max', 24)
|
|
435
|
+
elif value < conf.get('min', 0):
|
|
436
|
+
value = conf.get('min', 0)
|
|
437
|
+
|
|
438
|
+
if value >= conf.get('max', 24):
|
|
439
|
+
pwm_value = 1023
|
|
440
|
+
elif value <= conf.get('min', 100):
|
|
441
|
+
pwm_value = 0
|
|
442
|
+
else:
|
|
443
|
+
val_amplitude = conf.get('max', 24) - conf.get('min', 0)
|
|
444
|
+
val_relative = value / val_amplitude
|
|
445
|
+
|
|
446
|
+
duty_max = conf.get('device_max', 24) / 24 * 1023
|
|
447
|
+
duty_min = conf.get('device_min', 0) / 24 * 1023
|
|
448
|
+
|
|
449
|
+
pwm_amplitude = duty_max - duty_min
|
|
450
|
+
pwm_value = duty_min + pwm_amplitude * val_relative
|
|
451
|
+
|
|
452
|
+
return pwm_value
|
|
453
|
+
|
|
454
|
+
def _prepare_for_set(self, pwm_value):
|
|
455
|
+
conf = self.component.config
|
|
456
|
+
duty_max = conf.get('device_max', 24) / 24 * 1023
|
|
457
|
+
duty_min = conf.get('device_min', 0) / 24 * 1023
|
|
458
|
+
|
|
459
|
+
if pwm_value > duty_max:
|
|
460
|
+
value = conf.get('max', 24)
|
|
461
|
+
elif pwm_value < duty_min:
|
|
462
|
+
value = conf.get('min', 0)
|
|
463
|
+
else:
|
|
464
|
+
pwm_amplitude = duty_max - duty_min
|
|
465
|
+
relative_value = (pwm_value - duty_min) / pwm_amplitude
|
|
466
|
+
val_amplitude = conf.get('max', 24) - conf.get('min', 0)
|
|
467
|
+
value = conf.get('min', 0) + val_amplitude * relative_value
|
|
468
|
+
|
|
469
|
+
return round(value, 3)
|
|
425
470
|
|
|
426
471
|
|
|
427
472
|
class RGBLight(FleeDeviceMixin, BasicOutputMixin, BaseRGBWLight):
|
|
@@ -460,6 +505,25 @@ class DualMotorValve(FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
|
|
|
460
505
|
|
|
461
506
|
self.component.save()
|
|
462
507
|
|
|
508
|
+
def _prepare_for_send(self, value):
|
|
509
|
+
conf = self.component.config
|
|
510
|
+
if value >= conf.get('max', 100):
|
|
511
|
+
value = conf.get('max', 100)
|
|
512
|
+
elif value < conf.get('min', 0):
|
|
513
|
+
value = conf.get('min', 0)
|
|
514
|
+
val_amplitude = conf.get('max', 100) - conf.get('min', 0)
|
|
515
|
+
return ((value - conf.get('min', 0)) / val_amplitude) * 100
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _prepare_for_set(self, value):
|
|
519
|
+
conf = self.component.config
|
|
520
|
+
if value > conf.get('max', 100):
|
|
521
|
+
value = conf.get('max', 100)
|
|
522
|
+
elif value < conf.get('min', 0.0):
|
|
523
|
+
value = conf.get('min', 0)
|
|
524
|
+
val_amplitude = conf.get('max', 100) - conf.get('min', 0)
|
|
525
|
+
return conf.get('min', 0) + (value / 100) * val_amplitude
|
|
526
|
+
|
|
463
527
|
|
|
464
528
|
class Blinds(FleeDeviceMixin, BasicOutputMixin, GenericBlinds):
|
|
465
529
|
gateway_class = FleetGatewayHandler
|