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.

Files changed (73) hide show
  1. simo/backups/tasks.py +11 -1
  2. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  3. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  4. simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  6. simo/core/__pycache__/events.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  14. simo/core/admin.py +4 -4
  15. simo/core/api.py +20 -4
  16. simo/core/app_widgets.py +5 -0
  17. simo/core/controllers.py +4 -3
  18. simo/core/events.py +13 -4
  19. simo/core/forms.py +2 -0
  20. simo/core/management/commands/gateways_manager.py +0 -3
  21. simo/core/middleware.py +12 -6
  22. simo/core/models.py +26 -6
  23. simo/core/serializers.py +17 -17
  24. simo/core/socket_consumers.py +6 -2
  25. simo/core/static/admin/js/codemirror-init.js +1 -0
  26. simo/core/tasks.py +10 -7
  27. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  28. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  29. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  30. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  31. simo/fleet/controllers.py +86 -22
  32. simo/fleet/forms.py +84 -9
  33. simo/fleet/migrations/0039_auto_20241016_1047.py +28 -0
  34. simo/fleet/migrations/0040_alter_colonel_pwm_frequency.py +18 -0
  35. simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-38.pyc +0 -0
  36. simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-38.pyc +0 -0
  37. simo/fleet/models.py +6 -2
  38. simo/fleet/socket_consumers.py +13 -5
  39. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  40. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  41. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  42. simo/generic/__pycache__/models.cpython-38.pyc +0 -0
  43. simo/generic/controllers.py +45 -2
  44. simo/generic/forms.py +81 -7
  45. simo/generic/models.py +0 -1
  46. simo/generic/scripting/__init__.py +16 -0
  47. simo/generic/scripting/__pycache__/__init__.cpython-38.pyc +0 -0
  48. simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
  49. simo/generic/scripting/__pycache__/serializers.cpython-38.pyc +0 -0
  50. simo/generic/scripting/helpers.py +35 -0
  51. simo/generic/scripting/serializers.py +77 -0
  52. simo/generic/templates/admin/controller_widgets/weather_forecast.html +2 -2
  53. simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
  54. simo/notifications/utils.py +30 -12
  55. simo/scripting.py +2 -2
  56. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  57. simo/users/__pycache__/managers.cpython-38.pyc +0 -0
  58. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  59. simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
  60. simo/users/__pycache__/utils.cpython-38.pyc +0 -0
  61. simo/users/api.py +36 -7
  62. simo/users/managers.py +5 -1
  63. simo/users/migrations/0034_instanceuser_last_seen_location_and_more.py +24 -0
  64. simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-38.pyc +0 -0
  65. simo/users/models.py +37 -32
  66. simo/users/serializers.py +11 -8
  67. simo/users/utils.py +14 -3
  68. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/METADATA +1 -1
  69. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/RECORD +73 -60
  70. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/WHEEL +1 -1
  71. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/LICENSE.md +0 -0
  72. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/entry_points.txt +0 -0
  73. {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, timezone
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
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
- # def has_delete_permission(self, request, obj=None):
236
- # return False
235
+ def has_delete_permission(self, request, obj=None):
236
+ return False
237
237
 
238
- # def has_add_permission(self, request, obj=None):
239
- # return False
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.history_display([component.value])
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.history_display(values)
402
+ val = component.controller._history_display(values)
397
403
  else:
398
- val = component.controller.history_display([])
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.history_display([last_event.value])
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
@@ -100,3 +100,8 @@ class LockWidget(BaseAppWidget):
100
100
  name = _("Lock")
101
101
  size = [2, 2]
102
102
 
103
+
104
+ class AirQualityWidget(BaseAppWidget):
105
+ uid = 'air-quality'
106
+ name = _("Air Quality")
107
+ size = [2, 2]
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 history_display(self, values):
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 history_display(self, values):
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
- try:
131
- self._on_change_function(self)
132
- except Exception:
133
- print(traceback.format_exc(), file=sys.stderr)
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.
@@ -17,9 +17,6 @@ from simo.core.models import Gateway
17
17
  from simo.core.loggers import get_gw_logger
18
18
 
19
19
 
20
-
21
-
22
-
23
20
  class GatewayRunHandler(multiprocessing.Process):
24
21
  gateway = None
25
22
  logger = None
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
- instance = getattr(_thread_locals, 'instance', None)
30
- if not instance and request and request.session.get('instance_id'):
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
- else:
38
- introduce_instance(instance, request)
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
- _mqtt_client = None
410
- _on_change_function = None
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
- if any(
521
- f in dirty_fields for f in
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 field_name == 'notes':
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
- # only masters and superusers can fully manage components via app
420
- # others can only change basic fields
421
- if not self.context['request'].user.is_master:
422
- user_role = self.context['request'].user.get_role(
423
- self.context['instance']
424
- )
425
- if not user_role.is_superuser:
426
- for field_name in list(form.fields.keys()):
427
- if field_name not in form.basic_fields:
428
- del form.fields[field_name]
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
- c_methods = [m[0] for m in inspect.getmembers(
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:
@@ -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 = get_log_file_path(self.obj)
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 = get_log_file_path(self.obj)
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:
@@ -20,6 +20,7 @@
20
20
  var editor = CodeMirror.fromTextArea(
21
21
  element, settings
22
22
  );
23
+ editor.setSize(650, "100%");
23
24
  editor.on('change', function(editor){
24
25
  $(element).val(editor.doc.getValue());
25
26
  });
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(weather_forecast)
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
- users = User.objects.filter(
384
- roles__is_owner=True, roles__instance=comp.zone.instance,
385
- instance_roles__is_active=True
386
- ).distinct()
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, users=users
394
+ component=comp, instance_users=iusers
392
395
  )
393
396
 
394
397
 
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.conf import dynamic_settings
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
- if conf.get('inverse'):
390
- pwm_value = 0
391
- else:
392
- pwm_value = 1023
389
+ pwm_value = 0
393
390
  elif value <= conf.get('min', 100):
394
- if conf.get('inverse'):
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
- if conf.get('inverse'):
405
- pwm_value = conf.get('duty_max', 1023) - pwm_value + conf.get('duty_min')
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
- if pwm_value > conf.get('duty_max', 1023):
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 < conf.get('duty_min', 0.0):
413
+ elif pwm_value < duty_min:
414
414
  value = conf.get('min', 0)
415
415
  else:
416
- pwm_amplitude = conf.get('duty_max', 1023) - conf.get('duty_min', 0.0)
417
- relative_value = (pwm_value - conf.get('duty_min', 0.0)) / pwm_amplitude
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
- if self.component.config.get('inverse'):
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