simo 2.8.14__py3-none-any.whl → 2.9.1__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 (56) hide show
  1. simo/__pycache__/settings.cpython-312.pyc +0 -0
  2. simo/automation/__pycache__/helpers.cpython-312.pyc +0 -0
  3. simo/core/__pycache__/admin.cpython-312.pyc +0 -0
  4. simo/core/__pycache__/auto_urls.cpython-312.pyc +0 -0
  5. simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
  6. simo/core/__pycache__/forms.cpython-312.pyc +0 -0
  7. simo/core/__pycache__/models.cpython-312.pyc +0 -0
  8. simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
  9. simo/core/__pycache__/views.cpython-312.pyc +0 -0
  10. simo/core/admin.py +5 -2
  11. simo/core/auto_urls.py +4 -1
  12. simo/core/controllers.py +1 -3
  13. simo/core/forms.py +1 -1
  14. simo/core/models.py +32 -16
  15. simo/core/serializers.py +2 -5
  16. simo/core/templates/admin/core/component_change_form.html +1 -1
  17. simo/core/templates/admin/msg_page.html +1 -0
  18. simo/core/templates/admin/wizard/discovery.html +3 -4
  19. simo/core/templates/admin/wizard/wizard_add.html +1 -1
  20. simo/core/views.py +26 -2
  21. simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
  22. simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
  23. simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
  24. simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
  25. simo/fleet/__pycache__/managers.cpython-312.pyc +0 -0
  26. simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
  27. simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
  28. simo/fleet/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  29. simo/fleet/api.py +26 -3
  30. simo/fleet/base_types.py +1 -0
  31. simo/fleet/controllers.py +240 -7
  32. simo/fleet/custom_dali_operations.py +275 -0
  33. simo/fleet/forms.py +132 -3
  34. simo/fleet/managers.py +3 -1
  35. simo/fleet/migrations/0045_alter_colonel_type_customdalidevice.py +29 -0
  36. simo/fleet/migrations/0046_delete_customdalidevice.py +16 -0
  37. simo/fleet/migrations/0047_customdalidevice.py +28 -0
  38. simo/fleet/migrations/0048_remove_customdalidevice_colonel_and_more.py +28 -0
  39. simo/fleet/migrations/__pycache__/0045_alter_colonel_type_customdalidevice.cpython-312.pyc +0 -0
  40. simo/fleet/migrations/__pycache__/0046_delete_customdalidevice.cpython-312.pyc +0 -0
  41. simo/fleet/migrations/__pycache__/0047_customdalidevice.cpython-312.pyc +0 -0
  42. simo/fleet/migrations/__pycache__/0048_remove_customdalidevice_colonel_and_more.cpython-312.pyc +0 -0
  43. simo/fleet/models.py +54 -9
  44. simo/fleet/serializers.py +15 -1
  45. simo/fleet/socket_consumers.py +6 -0
  46. simo/fleet/tasks.py +22 -2
  47. simo/fleet/templates/fleet/controllers_info/RoomZonePresenceSensor.md +8 -0
  48. simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
  49. simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
  50. simo/settings.py +1 -1
  51. {simo-2.8.14.dist-info → simo-2.9.1.dist-info}/METADATA +3 -2
  52. {simo-2.8.14.dist-info → simo-2.9.1.dist-info}/RECORD +56 -46
  53. {simo-2.8.14.dist-info → simo-2.9.1.dist-info}/WHEEL +1 -1
  54. {simo-2.8.14.dist-info → simo-2.9.1.dist-info}/entry_points.txt +0 -0
  55. {simo-2.8.14.dist-info → simo-2.9.1.dist-info/licenses}/LICENSE.md +0 -0
  56. {simo-2.8.14.dist-info → simo-2.9.1.dist-info}/top_level.txt +0 -0
Binary file
Binary file
Binary file
Binary file
simo/core/admin.py CHANGED
@@ -366,7 +366,7 @@ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
366
366
  pop_fields_from_form(ctx['form'])
367
367
  if ctx['form'].is_valid():
368
368
  if ctx['form'].controller.is_discoverable:
369
- ctx['form'].controller.init_discovery(
369
+ ctx['form'].controller._init_discovery(
370
370
  ctx['form'].cleaned_data
371
371
  )
372
372
  ctx['discovery_msg'] = ctx['form'].controller.discovery_msg
@@ -383,6 +383,9 @@ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
383
383
  'components-list',
384
384
  kwargs={'instance_slug': instance.slug}
385
385
  )
386
+ ctx['finish_url'] = reverse(
387
+ 'finish-discovery',
388
+ ) + f"?uid={ctx['form'].controller.uid}"
386
389
  return render(
387
390
  request, 'admin/wizard/discovery.html', ctx
388
391
  )
@@ -486,7 +489,7 @@ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
486
489
  def info(self, obj):
487
490
  if not obj.controller:
488
491
  return
489
- info = obj.controller.info()
492
+ info = obj.controller.info(obj)
490
493
  if not info:
491
494
  return
492
495
  return mark_safe(
simo/core/auto_urls.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from django.urls import path
2
2
  from .views import (
3
- get_timestamp, upgrade, restart, reboot, set_instance, delete_instance
3
+ get_timestamp, upgrade, restart, reboot, set_instance, delete_instance,
4
+ finish_discovery
4
5
  )
5
6
  from .autocomplete_views import (
6
7
  IconModelAutocomplete,
@@ -31,6 +32,8 @@ urlpatterns = [
31
32
  ComponentAutocomplete.as_view(), name='autocomplete-component'
32
33
  ),
33
34
  path('set-instance/<slug:instance_slug>/', set_instance, name='set-instance'),
35
+ path('finish-discovery/',
36
+ finish_discovery, name='finish-discovery'),
34
37
  path('upgrade/', upgrade, name='upgrade'),
35
38
  path('restart/', restart, name='restart'),
36
39
  path('reboot/', reboot, name='reboot'),
simo/core/controllers.py CHANGED
@@ -111,9 +111,7 @@ class ControllerBase(ABC):
111
111
  @classmethod
112
112
  def is_discoverable(cls):
113
113
  return hasattr(
114
- cls, 'init_discovery'
115
- ) and hasattr(
116
- cls, '_process_discovery'
114
+ cls, '_init_discovery'
117
115
  )
118
116
 
119
117
  @classmethod
simo/core/forms.py CHANGED
@@ -284,7 +284,7 @@ class ComponentAdminForm(forms.ModelForm):
284
284
  patched to send all keys, even for checkboxes, via
285
285
  input[type="hidden"] fields or some JS magic.
286
286
  """
287
- # TODO: When creating new component, via admin interface and disabling show_in_app, show_in_app field get's removed, cince there is no "input[type="hidden"] fields or some JS magic" logic implemented.
287
+ # TODO: When creating new component, via admin interface and disabling any boolean field, field get's removed and effectively receives it's default value, which is usually True, because "input[type="hidden"] fields or some JS magic" logic is not yet implemented.
288
288
  if data is None:
289
289
  # not a form submission, don't modify self.fields
290
290
  return
simo/core/models.py CHANGED
@@ -294,28 +294,44 @@ class Gateway(DirtyFieldsMixin, models.Model, SimoAdminMixin):
294
294
  )
295
295
  if result:
296
296
  self.refresh_from_db()
297
- if not isinstance(result, dict) and isinstance(result, Iterable):
298
- for res in result:
299
- if isinstance(res, models.Model):
300
- if res.pk not in self.discovery['result']:
301
- self.discovery['result'].append(res.pk)
302
- else:
303
- if res not in self.discovery['result']:
304
- self.discovery['result'].append(res)
305
- else:
306
- if isinstance(result, models.Model):
307
- if result.pk not in self.discovery['result']:
308
- self.discovery['result'].append(result.pk)
309
- else:
310
- if result not in self.discovery['result']:
311
- self.discovery['result'].append(result)
312
-
297
+ self.append_discovery_result(result)
313
298
 
314
299
  self.save(update_fields=['discovery'])
315
300
 
301
+
316
302
  def finish_discovery(self):
303
+ from .utils.type_constants import CONTROLLER_TYPES_MAP
317
304
  self.discovery['finished'] = time.time()
318
305
  self.save(update_fields=['discovery'])
306
+ ControllerClass = CONTROLLER_TYPES_MAP.get(
307
+ self.discovery['controller_uid']
308
+ )
309
+ if hasattr(ControllerClass, '_finish_discovery'):
310
+ result = ControllerClass._finish_discovery(
311
+ self.discovery['init_data']
312
+ )
313
+ self.append_discovery_result(result)
314
+ self.save(update_fields=['discovery'])
315
+
316
+
317
+ def append_discovery_result(self, result):
318
+ if not isinstance(result, dict) and isinstance(result, Iterable):
319
+ for res in result:
320
+ if isinstance(res, models.Model):
321
+ if res.pk not in self.discovery['result']:
322
+ self.discovery['result'].append(res.pk)
323
+ else:
324
+ if res not in self.discovery['result']:
325
+ self.discovery['result'].append(res)
326
+ else:
327
+ if isinstance(result, models.Model):
328
+ if result.pk not in self.discovery['result']:
329
+ self.discovery['result'].append(result.pk)
330
+ else:
331
+ if result not in self.discovery['result']:
332
+ self.discovery['result'].append(result)
333
+
334
+
319
335
 
320
336
 
321
337
  class Component(DirtyFieldsMixin, models.Model, SimoAdminMixin, OnChangeMixin):
simo/core/serializers.py CHANGED
@@ -310,7 +310,6 @@ class ComponentSerializer(FormSerializer):
310
310
  field_mapping = {
311
311
  HiddenField: HiddenSerializerField,
312
312
  forms.TypedChoiceField: serializers.ChoiceField,
313
- forms.FloatField: serializers.FloatField,
314
313
  forms.SlugField: serializers.CharField,
315
314
  PlainLocationField: LocationSerializer,
316
315
  forms.ModelChoiceField: ComponentPrimaryKeyRelatedField,
@@ -323,8 +322,6 @@ class ComponentSerializer(FormSerializer):
323
322
  PasswordField: PasswordSerializer
324
323
  }
325
324
 
326
-
327
-
328
325
  def __init__(self, *args, **kwargs):
329
326
  super().__init__(*args, **kwargs)
330
327
  # Set proper instance for OPTIONS request
@@ -547,7 +544,7 @@ class ComponentSerializer(FormSerializer):
547
544
  )
548
545
  if form.is_valid():
549
546
  if form.controller.is_discoverable:
550
- form.controller.init_discovery(form.cleaned_data)
547
+ form.controller._init_discovery(form.cleaned_data)
551
548
  return form.save(commit=False)
552
549
  return form.save(commit=True)
553
550
  raise serializers.ValidationError(form.errors)
@@ -557,7 +554,7 @@ class ComponentSerializer(FormSerializer):
557
554
 
558
555
  def get_info(self, obj):
559
556
  if obj.controller:
560
- return obj.controller.info()
557
+ return obj.controller.info(obj)
561
558
 
562
559
  def get_read_only(self, obj):
563
560
  user = self.context.get('user')
@@ -56,7 +56,7 @@
56
56
  <h2 style="background: linear-gradient(0.3turn, #5c7ca5, #5a95df);">Info</h2>
57
57
  <div class="form-row">
58
58
  {% if original.controller %}
59
- {{ original.info|markdownify }}
59
+ {{ original.info|markdownify|linebreaksbr }}
60
60
  {% endif %}
61
61
  </div>
62
62
  </div>
@@ -25,6 +25,7 @@
25
25
  {{ msg|safe }}
26
26
  </p>
27
27
  <p class="mb-0">{{ suggestion|safe }}</p>
28
+ <p><a href="{% url 'logout' %}">Logout >></a></p>
28
29
  </div>
29
30
 
30
31
  </div>
@@ -64,8 +64,7 @@
64
64
 
65
65
  <div id="running-discovery">
66
66
  <p style="margin: 30px 15px">
67
- <i class="fas fa-spinner fa-spin fa-lg" style="margin-right: 10px"></i> DISCOVERY MODE ACTIVATED! <br>
68
- Your new components will appear down bellow once discovered.
67
+ <i class="fas fa-spinner fa-spin fa-lg" style="margin-right: 10px"></i> DISCOVERY MODE ACTIVATED!
69
68
  </p>
70
69
  {% if discovery_msg %}
71
70
  <p style="margin: 30px 15px">{{ discovery_msg }}</p>
@@ -108,8 +107,8 @@
108
107
 
109
108
  <div class="submit-row" style="text-align: left;">
110
109
 
111
- <a href="{% url 'admin:core_component_changelist' %}" class="cancel-link" style="display: block; padding:9px 15px">
112
- <i class="fa fa-check"></i> Done
110
+ <a href="{{ finish_url }}" class="cancel-link" style="display: block; padding:9px 15px">
111
+ <i class="fa fa-check"></i> Finish
113
112
  </a>
114
113
 
115
114
  </div>
@@ -63,7 +63,7 @@
63
63
  {% if info %}
64
64
  <div class="form-row">
65
65
  <div class="markdownified-info">
66
- {{ info|markdownify }}
66
+ {{ info|markdownify|linebreaksbr }}
67
67
  </div>
68
68
  </div>
69
69
  {% endif %}
simo/core/views.py CHANGED
@@ -9,7 +9,7 @@ from django.http import (
9
9
  )
10
10
  from django.contrib import messages
11
11
  from simo.conf import dynamic_settings
12
- from .models import Instance
12
+ from .models import Instance, Component, Gateway
13
13
  from .tasks import update as update_task, supervisor_restart, hardware_reboot
14
14
  from .middleware import introduce_instance
15
15
 
@@ -91,4 +91,28 @@ def hub_info(request):
91
91
  request.get_host()
92
92
  ):
93
93
  data['hub_secret'] = dynamic_settings['core__hub_secret']
94
- return JsonResponse(data)
94
+ return JsonResponse(data)
95
+
96
+
97
+ @login_required
98
+ def finish_discovery(request):
99
+ # finish discovery function for admin discovery view
100
+ if not request.user.is_authenticated:
101
+ raise Http404()
102
+ if not request.user.is_master:
103
+ raise Http404()
104
+ result = None
105
+ for gateway in Gateway.objects.filter(
106
+ discovery__start__gt=time.time() - 60 * 60, # no more than an hour
107
+ discovery__controller_uid=request.GET['uid']
108
+ ):
109
+ gateway.finish_discovery()
110
+ for res in gateway.discovery['result']:
111
+ comp = Component.objects.filter(
112
+ controller_uid=request.GET['uid'], pk=res
113
+ ).first()
114
+ if comp:
115
+ result = comp
116
+ if result:
117
+ return redirect(result.get_admin_url())
118
+ return reverse('admin:core_component_changelist')
Binary file
simo/fleet/api.py CHANGED
@@ -4,11 +4,13 @@ from rest_framework import viewsets
4
4
  from rest_framework.response import Response as RESTResponse
5
5
  from rest_framework.decorators import action
6
6
  from rest_framework.exceptions import ValidationError as APIValidationError
7
+ from django_filters.rest_framework import DjangoFilterBackend
7
8
  from simo.core.api import InstanceMixin
8
9
  from simo.core.permissions import IsInstanceSuperuser
9
- from .models import InstanceOptions, Colonel, Interface
10
+ from .models import InstanceOptions, Colonel, Interface, CustomDaliDevice
10
11
  from .serializers import (
11
- InstanceOptionsSerializer, ColonelSerializer, ColonelInterfaceSerializer
12
+ InstanceOptionsSerializer, ColonelSerializer, ColonelInterfaceSerializer,
13
+ CustomDaliDeviceSerializer
12
14
  )
13
15
 
14
16
 
@@ -99,4 +101,25 @@ class InterfaceViewSet(
99
101
  return permissions
100
102
 
101
103
  def get_queryset(self):
102
- return Interface.objects.filter(colonel__instance=self.instance)
104
+ return Interface.objects.filter(colonel__instance=self.instance)
105
+
106
+
107
+ class CustomDaliDeviceViewSet(
108
+ InstanceMixin,
109
+ viewsets.mixins.RetrieveModelMixin, viewsets.mixins.UpdateModelMixin,
110
+ viewsets.mixins.ListModelMixin, viewsets.mixins.CreateModelMixin,
111
+ viewsets.GenericViewSet
112
+ ):
113
+ url = 'fleet/custom-dali-devices'
114
+ basename = 'custom-dali-devices'
115
+ serializer_class = CustomDaliDeviceSerializer
116
+ filter_backends = [DjangoFilterBackend]
117
+ filterset_fields = ['random_address', 'name']
118
+
119
+ def get_permissions(self):
120
+ permissions = super().get_permissions()
121
+ permissions.append(IsInstanceSuperuser())
122
+ return permissions
123
+
124
+ def get_queryset(self):
125
+ return CustomDaliDevice.objects.filter(instance=self.instance)
simo/fleet/base_types.py CHANGED
@@ -2,4 +2,5 @@ from django.utils.translation import gettext_lazy as _
2
2
 
3
3
  BASE_TYPES = {
4
4
  'dali': _("Dali Device"),
5
+ 'room-sensor': _("Room Sensor"),
5
6
  }
simo/fleet/controllers.py CHANGED
@@ -1,4 +1,4 @@
1
- import json
1
+ import json, ast
2
2
  from django.utils.translation import gettext_lazy as _
3
3
  from django.db.transaction import atomic
4
4
  from simo.core.middleware import get_current_instance
@@ -9,7 +9,7 @@ from simo.core.controllers import (
9
9
  NumericSensor as BaseNumericSensor,
10
10
  Switch as BaseSwitch, Dimmer as BaseDimmer,
11
11
  MultiSensor as BaseMultiSensor, RGBWLight as BaseRGBWLight,
12
- Blinds as BaseBlinds, Gate as BaseGate
12
+ Blinds as BaseBlinds, Gate as BaseGate,
13
13
  )
14
14
  from simo.core.app_widgets import NumericSensorWidget, AirQualityWidget
15
15
  from simo.core.controllers import Lock, ControllerBase, SingleSwitchWidget
@@ -17,7 +17,8 @@ from simo.core.utils.helpers import heat_index
17
17
  from simo.core.utils.serialization import (
18
18
  serialize_form_data, deserialize_form_data
19
19
  )
20
- from .models import Colonel
20
+ from simo.core.forms import BaseComponentForm
21
+ from .models import Colonel, CustomDaliDevice
21
22
  from .gateways import FleetGatewayHandler
22
23
  from .forms import (
23
24
  ColonelPinChoiceField,
@@ -31,7 +32,8 @@ from .forms import (
31
32
  TTLockConfigForm, DALIDeviceConfigForm, DaliLampForm, DaliGearGroupForm,
32
33
  DaliSwitchConfigForm,
33
34
  DaliOccupancySensorConfigForm, DALILightSensorConfigForm,
34
- DALIButtonConfigForm
35
+ DALIButtonConfigForm, RoomSensorDeviceConfigForm,
36
+ RoomZonePresenceConfigForm
35
37
  )
36
38
 
37
39
 
@@ -465,7 +467,7 @@ class TTLock(FleeDeviceMixin, Lock):
465
467
  discovery_msg = _("Please activate your TTLock so it can be discovered.")
466
468
 
467
469
  @classmethod
468
- def init_discovery(self, form_cleaned_data):
470
+ def _init_discovery(self, form_cleaned_data):
469
471
  from simo.core.models import Gateway
470
472
  print("INIT discovery form cleaned data: ", form_cleaned_data)
471
473
  print("Serialized form: ", serialize_form_data(form_cleaned_data))
@@ -663,7 +665,7 @@ class DALIDevice(FleeDeviceMixin, ControllerBase):
663
665
  return value
664
666
 
665
667
  @classmethod
666
- def init_discovery(self, form_cleaned_data):
668
+ def _init_discovery(self, form_cleaned_data):
667
669
  from simo.core.models import Gateway
668
670
  gateway = Gateway.objects.filter(type=self.gateway_class.uid).first()
669
671
  gateway.start_discovery(
@@ -824,4 +826,235 @@ class DALIButton(BaseButton, DALIDevice):
824
826
  family = 'dali'
825
827
  manual_add = False
826
828
  name = 'DALI Button'
827
- config_form = DALIButtonConfigForm
829
+ config_form = DALIButtonConfigForm
830
+
831
+
832
+ class RoomSensor(FleeDeviceMixin, ControllerBase):
833
+ gateway_class = FleetGatewayHandler
834
+ config_form = RoomSensorDeviceConfigForm
835
+ name = "Room Sensor"
836
+ base_type = 'room-sensor'
837
+ default_value = 0
838
+ app_widget = NumericSensorWidget
839
+
840
+ def _validate_val(self, value, occasion=None):
841
+ return value
842
+
843
+
844
+ class AirQualitySensor(FleeDeviceMixin, BaseMultiSensor):
845
+ gateway_class = FleetGatewayHandler
846
+ config_form = BaseComponentForm
847
+ name = "Air Quality Sensor"
848
+ app_widget = AirQualityWidget
849
+ manual_add = False
850
+
851
+ default_value = [
852
+ ["TVOC", 0, "ppb"],
853
+ ["AQI (UBA)", 0, ""]
854
+ ]
855
+
856
+ def _receive_from_device(self, value, *args, **kwargs):
857
+ aqi = 5
858
+ if value < 812:
859
+ aqi = 4
860
+ if value < 325:
861
+ aqi = 3
862
+ if value < 162:
863
+ aqi = 2
864
+ if value < 65:
865
+ aqi = 1
866
+ value = [
867
+ ["TVOC", value, "ppb"],
868
+ ["AQI (UBA)", aqi, ""]
869
+ ]
870
+ return super()._receive_from_device(value, *args, **kwargs)
871
+
872
+ def get_tvoc(self):
873
+ try:
874
+ for entry in self.component.value:
875
+ if entry[0] == 'TVOC':
876
+ return entry[1]
877
+ except:
878
+ return
879
+
880
+ def get_aqi(self):
881
+ try:
882
+ for entry in self.component.value:
883
+ if entry[0] == 'AQI (UBA)':
884
+ return entry[1]
885
+ except:
886
+ return
887
+
888
+
889
+ class TempHumSensor(FleeDeviceMixin, BasicSensorMixin, BaseMultiSensor):
890
+ gateway_class = FleetGatewayHandler
891
+ config_form = BaseComponentForm
892
+ name = "Temperature & Humidity sensor"
893
+ app_widget = NumericSensorWidget
894
+ manual_add = False
895
+
896
+ def __init__(self, *args, **kwargs):
897
+ super().__init__(*args, **kwargs)
898
+ self.sys_temp_units = 'C'
899
+ if hasattr(self.component, 'zone') \
900
+ and self.component.zone.instance.units_of_measure == 'imperial':
901
+ self.sys_temp_units = 'F'
902
+
903
+ @property
904
+ def default_value(self):
905
+ return [
906
+ ['temperature', 0, self.sys_temp_units],
907
+ ['humidity', 20, '%'],
908
+ ['real_feel', 0, self.sys_temp_units]
909
+ ]
910
+
911
+ def _receive_from_device(self, value, *args, **kwargs):
912
+
913
+ if isinstance(value, dict):
914
+ temp = value['temp']
915
+ humidity = value['humidity']
916
+ else:
917
+ buf = bytes.fromhex(value)
918
+ humidity = (
919
+ (buf[0] << 12) | (buf[1] << 4) | (buf[2] >> 4)
920
+ )
921
+ humidity = (humidity * 100) / 0x100000
922
+ humidity = int(round(humidity, 0))
923
+ temp = ((buf[2] & 0xF) << 16) | (buf[3] << 8) | buf[4]
924
+ temp = ((temp * 200.0) / 0x100000) - 50
925
+ temp = round(temp, 1)
926
+
927
+ new_val = [
928
+ ['temperature', temp, self.sys_temp_units],
929
+ ['humidity', humidity, '%'],
930
+ ['real_feel', 0, self.sys_temp_units]
931
+ ]
932
+
933
+ if self.sys_temp_units == 'F':
934
+ new_val[0][1] = round((new_val[0][1] * 9 / 5) + 32, 1)
935
+
936
+ real_feel = heat_index(
937
+ new_val[0][1], new_val[1][1], self.sys_temp_units == 'F'
938
+ )
939
+ new_val[2] = ['real_feel', real_feel, self.sys_temp_units]
940
+
941
+ return super()._receive_from_device(new_val, *args, **kwargs)
942
+
943
+
944
+ class AmbientLightSensor(FleeDeviceMixin, BaseNumericSensor):
945
+ gateway_class = FleetGatewayHandler
946
+ name = "Ambient lighting sensor"
947
+ manual_add = False
948
+ default_config = {
949
+ 'widget': 'numeric-sensor',
950
+ 'value_units': 'lux',
951
+ 'limits': [
952
+ {"name": "Dark", "value": 20},
953
+ {"name": "Moderate", "value": 300},
954
+ {"name": "Bright", "value": 800},
955
+ ]
956
+ }
957
+
958
+
959
+ class RoomPresenceSensor(FleeDeviceMixin, BaseBinarySensor):
960
+ gateway_class = FleetGatewayHandler
961
+ name = "Human presence sensor"
962
+ manual_add = False
963
+
964
+
965
+ class RoomZonePresenceSensor(FleeDeviceMixin, BaseBinarySensor):
966
+ gateway_class = FleetGatewayHandler
967
+ add_form = RoomZonePresenceConfigForm
968
+ config_form = BaseComponentForm
969
+ name = "Room zone presence"
970
+ discovery_msg = _(
971
+ "Move vigorously in particular zone of the room, "
972
+ "where presence needs to be detected. "
973
+ "Your movements are being recorded. "
974
+ "Hit Done, once you are done."
975
+ )
976
+
977
+ @classmethod
978
+ def _init_discovery(self, form_cleaned_data):
979
+ from simo.core.models import Gateway
980
+ gateway = Gateway.objects.filter(type=self.gateway_class.uid).first()
981
+ gateway.start_discovery(
982
+ self.uid, serialize_form_data(form_cleaned_data),
983
+ timeout=60
984
+ )
985
+ if form_cleaned_data['device'].startswith('wifi'):
986
+ colonel = Colonel.objects.filter(
987
+ id=form_cleaned_data['device'][5:]
988
+ ).first()
989
+ GatewayObjectCommand(
990
+ gateway, colonel,
991
+ command='discover', type=self.uid.split('.')[-1],
992
+ ).publish()
993
+ else:
994
+ dali_device = CustomDaliDevice.objects.filter(
995
+ id=form_cleaned_data['device'][5:]
996
+ ).first()
997
+ from .custom_dali_operations import Frame
998
+ frame = Frame(40, bytes(bytearray(5)))
999
+ frame[8:11] = 15 # command to custom dali device
1000
+ frame[12:15] = 0 # action to perform: start room zone discovery
1001
+ dali_device.transmit(frame)
1002
+
1003
+
1004
+ @classmethod
1005
+ @atomic
1006
+ def _finish_discovery(cls, started_with):
1007
+ started_with = deserialize_form_data(started_with)
1008
+ form = cls.add_form(
1009
+ controller_uid=cls.uid, data=started_with
1010
+ )
1011
+ form.is_valid()
1012
+ if form.cleaned_data['device'].startswith('wifi'):
1013
+ form.instance.alive = False
1014
+ form.instance.config['colonel'] = int(
1015
+ form.cleaned_data['device'][5:]
1016
+ )
1017
+ new_component = form.save()
1018
+ GatewayObjectCommand(
1019
+ new_component.gateway, Colonel(
1020
+ id=new_component.config['colonel']
1021
+ ), command='finalize',
1022
+ data={
1023
+ 'comp_config': {
1024
+ 'type': str(cls).split('.')[-1],
1025
+ 'family': new_component.controller.family,
1026
+ 'config': json.loads(json.dumps(new_component.config))
1027
+ }
1028
+ }
1029
+ ).publish()
1030
+ else:
1031
+ from simo.core.models import Component
1032
+ dali_device = CustomDaliDevice.objects.filter(
1033
+ id=form.cleaned_data['device'][5:]
1034
+ ).first()
1035
+ free_slots = {0, 1, 2, 3, 4, 5, 6, 7}
1036
+ for comp in Component.objects.filter(
1037
+ controller_uid=cls.uid, config__dali_device=dali_device.id
1038
+ ):
1039
+ try:
1040
+ free_slots.remove(int(comp.config['slot']))
1041
+ except:
1042
+ continue
1043
+ if not free_slots:
1044
+ return []
1045
+
1046
+ form.instance.alive = False
1047
+ form.instance.config['dali_device'] = int(
1048
+ form.cleaned_data['device'][5:]
1049
+ )
1050
+ form.instance.config['slot'] = free_slots.pop()
1051
+ new_component = form.save()
1052
+ from .custom_dali_operations import Frame
1053
+ frame = Frame(40, bytes(bytearray(5)))
1054
+ frame[8:11] = 15 # command to custom dali device
1055
+ frame[12:15] = 1 # action to perform: stop room zone discovery
1056
+ frame[16:18] = new_component.config['slot']
1057
+ dali_device.transmit(frame)
1058
+
1059
+ print("NEW COMPONENT: ", new_component)
1060
+ return new_component