simo 2.8.15__py3-none-any.whl → 2.10.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 (64) hide show
  1. simo/automation/__pycache__/gateways.cpython-312.pyc +0 -0
  2. simo/automation/gateways.py +12 -10
  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__/models.cpython-312.pyc +0 -0
  7. simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
  8. simo/core/__pycache__/tasks.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 +42 -5
  13. simo/core/models.py +32 -16
  14. simo/core/serializers.py +2 -2
  15. simo/core/tasks.py +8 -1
  16. simo/core/templates/admin/core/component_change_form.html +1 -1
  17. simo/core/templates/admin/wizard/discovery.html +3 -4
  18. simo/core/templates/admin/wizard/wizard_add.html +1 -1
  19. simo/core/views.py +26 -2
  20. simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
  21. simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
  22. simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
  23. simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
  24. simo/fleet/__pycache__/managers.cpython-312.pyc +0 -0
  25. simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
  26. simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
  27. simo/fleet/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  28. simo/fleet/api.py +26 -3
  29. simo/fleet/base_types.py +1 -0
  30. simo/fleet/controllers.py +240 -7
  31. simo/fleet/custom_dali_operations.py +275 -0
  32. simo/fleet/forms.py +132 -3
  33. simo/fleet/managers.py +3 -1
  34. simo/fleet/migrations/0045_alter_colonel_type_customdalidevice.py +29 -0
  35. simo/fleet/migrations/0046_delete_customdalidevice.py +16 -0
  36. simo/fleet/migrations/0047_customdalidevice.py +28 -0
  37. simo/fleet/migrations/0048_remove_customdalidevice_colonel_and_more.py +28 -0
  38. simo/fleet/migrations/__pycache__/0045_alter_colonel_type_customdalidevice.cpython-312.pyc +0 -0
  39. simo/fleet/migrations/__pycache__/0046_delete_customdalidevice.cpython-312.pyc +0 -0
  40. simo/fleet/migrations/__pycache__/0047_customdalidevice.cpython-312.pyc +0 -0
  41. simo/fleet/migrations/__pycache__/0048_remove_customdalidevice_colonel_and_more.cpython-312.pyc +0 -0
  42. simo/fleet/models.py +54 -9
  43. simo/fleet/serializers.py +15 -1
  44. simo/fleet/socket_consumers.py +6 -0
  45. simo/fleet/tasks.py +22 -2
  46. simo/fleet/templates/fleet/controllers_info/RoomZonePresenceSensor.md +8 -0
  47. simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
  48. simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
  49. simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
  50. simo/generic/controllers.py +99 -43
  51. simo/generic/forms.py +13 -10
  52. simo/generic/gateways.py +91 -2
  53. simo/generic/migrations/0003_auto_20250409_1404.py +33 -0
  54. simo/generic/migrations/__pycache__/0003_auto_20250409_1404.cpython-312.pyc +0 -0
  55. simo/users/__pycache__/api.cpython-312.pyc +0 -0
  56. simo/users/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  57. simo/users/api.py +71 -18
  58. simo/users/dynamic_settings.py +1 -1
  59. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/METADATA +1 -1
  60. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/RECORD +64 -52
  61. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/WHEEL +0 -0
  62. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/entry_points.txt +0 -0
  63. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/licenses/LICENSE.md +0 -0
  64. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/top_level.txt +0 -0
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
@@ -0,0 +1,275 @@
1
+ from django.utils import timezone
2
+ from simo.core.models import Component
3
+ from .models import Interface, CustomDaliDevice
4
+ from .controllers import (
5
+ TempHumSensor, AirQualitySensor, AmbientLightSensor,
6
+ RoomPresenceSensor, RoomZonePresenceSensor
7
+ )
8
+
9
+
10
+ class Frame:
11
+ """A DALI frame.
12
+
13
+ A Frame consists of one start bit, n data bits, and one stop
14
+ condition. The most significant bit is always transmitted first.
15
+
16
+ Instances of this object are mutable.
17
+ """
18
+
19
+ def __init__(self, bits, data=0):
20
+ """Initialise a Frame with the supplied number of data bits.
21
+
22
+ :parameter bits: the number of data bits in the Frame
23
+ :parameter data: initial data for the Frame as an integer or
24
+ an iterable sequence of integers
25
+ """
26
+ if not isinstance(bits, int):
27
+ raise TypeError(
28
+ "Number of bits must be an integer")
29
+ if bits < 1:
30
+ raise ValueError(
31
+ "Frames must contain at least 1 data bit")
32
+ self._bits = bits
33
+ if isinstance(data, int):
34
+ self._data = data
35
+ else:
36
+ self._data = int.from_bytes(data, 'big')
37
+ if self._data < 0:
38
+ raise ValueError("Initial data must not be negative")
39
+ if len(self.pack) > bits:
40
+ raise ValueError(
41
+ "Initial data will not fit in {} bits".format(bits))
42
+ self._error = False
43
+
44
+ @property
45
+ def error(self):
46
+ """Frame was received with a framing error."""
47
+ return self._error
48
+
49
+ def __len__(self):
50
+ return self._bits
51
+
52
+ def __eq__(self, other):
53
+ try:
54
+ return self._bits == other._bits and self._data == other._data
55
+ except Exception:
56
+ return False
57
+
58
+ def __ne__(self, other):
59
+ try:
60
+ return self._bits != other._bits or self._data != other._data
61
+ except Exception:
62
+ return True
63
+
64
+ def _readslice(self, key):
65
+ """Check that a slice is valid, return indices
66
+
67
+ The slice must have indices that are integers. The indices
68
+ must be in the range 0..(len(self)-1).
69
+ """
70
+ if not isinstance(key.start, int) or not isinstance(key.stop, int):
71
+ raise TypeError("slice indices must be integers")
72
+ if key.step not in (None, 1):
73
+ raise TypeError("slice with step not supported")
74
+ hi = max(key.start, key.stop)
75
+ lo = min(key.start, key.stop)
76
+ if hi < 0 or lo < 0:
77
+ raise IndexError("slice indices must be >= 0")
78
+ if hi >= self._bits or lo >= self._bits:
79
+ raise IndexError("slice index out of range")
80
+ return hi, lo
81
+
82
+ def __getitem__(self, key):
83
+ """Read a bit or group of bits from the frame
84
+
85
+ If the key is an integer, return that bit as True or False or
86
+ raise IndexError if the key is out of bounds.
87
+
88
+ If the key is a slice, return that slice as an integer or
89
+ raise IndexError if out of bounds. We abuse the slice
90
+ mechanism slightly such that slice(5,7) and slice(7,5) are
91
+ treated the same. Slices with a step or a negative index are
92
+ not supported.
93
+ """
94
+ if isinstance(key, slice):
95
+ hi, lo = self._readslice(key)
96
+ d = self._data >> lo
97
+ return d & ((1 << (hi + 1 - lo)) - 1)
98
+ elif isinstance(key, int):
99
+ if key < 0 or key >= self._bits:
100
+ raise IndexError("index out of range")
101
+ return (self._data & (1 << key)) != 0
102
+ raise TypeError
103
+
104
+ def __setitem__(self, key, value):
105
+ """Write a bit or a group of bits to the frame
106
+
107
+ If the key is an integer, set that bit to the truth value of
108
+ value or raise IndexError if the key is out of bounds.
109
+
110
+ If the key is a slice, value must be an integer that fits
111
+ within the slice; set that slice to value or raise IndexError
112
+ if out of bounds. We abuse the slice mechanism slightly such
113
+ that slice(5,7) and slice(7,5) are treated the same. Slices
114
+ with a step or a negative index are not supported.
115
+ """
116
+ if isinstance(key, slice):
117
+ hi, lo = self._readslice(key)
118
+ if not isinstance(value, int):
119
+ raise TypeError("value must be an integer")
120
+ if len(bin(value)) - 2 > (hi + 1 - lo):
121
+ raise ValueError("value will not fit in supplied slice")
122
+ if value < 0:
123
+ raise ValueError("value must not be negative")
124
+ template = ((1 << hi + 1 - lo) - 1) << lo
125
+ mask = ((1 << self._bits) - 1) ^ template
126
+ self._data = self._data & mask | (value << lo)
127
+ elif isinstance(key, int):
128
+ if key < 0 or key >= self._bits:
129
+ raise IndexError("index out of range")
130
+ if value:
131
+ self._data = self._data | (1 << key)
132
+ else:
133
+ self._data = self._data \
134
+ & (((1 << self._bits) - 1) ^ (1 << key))
135
+ else:
136
+ raise TypeError
137
+
138
+ def __contains__(self, item):
139
+ if item is True:
140
+ return self._data != 0
141
+ if item is False:
142
+ return self._data != (1 << self._bits) - 1
143
+ return False
144
+
145
+ def __add__(self, other):
146
+ try:
147
+ return Frame(self._bits + other._bits,
148
+ self._data << other._bits | other._data)
149
+ except Exception:
150
+ raise TypeError("Frame can only be added to another Frame")
151
+
152
+ @property
153
+ def as_integer(self):
154
+ """The contents of the frame represented as an integer."""
155
+ return self._data
156
+
157
+ @property
158
+ def as_byte_sequence(self):
159
+ """The contents of the frame represented as a sequence.
160
+
161
+ Returns a sequence of integers each in the range 0..255
162
+ representing the data in the frame, with the most-significant
163
+ bits first. If the frame is not an exact multiple of 8 bits
164
+ long, the first element in the sequence contains fewer than 8
165
+ bits.
166
+ """
167
+ return list(self.pack)
168
+
169
+ @property
170
+ def pack(self):
171
+ """The contents of the frame represented as a byte string.
172
+
173
+ If the frame is not an exact multiple of 8 bits long, the
174
+ first byte in the string will contain fewer than 8 bits.
175
+ """
176
+ return self._data.to_bytes(
177
+ (len(self) // 8) + (1 if len(self) % 8 else 0),
178
+ 'big')
179
+
180
+ def pack_len(self, l):
181
+ """The contents of the frame represented as a fixed length byte string.
182
+
183
+ The least significant bit of the frame is aligned to the end
184
+ of the byte string. The start of the byte string is padded
185
+ with zeroes.
186
+
187
+ If the frame will not fit in the byte string, raises
188
+ OverflowError.
189
+ """
190
+ return self._data.to_bytes(l, 'big')
191
+
192
+ def __str__(self):
193
+ return "{}({},{})".format(self.__class__.__name__, len(self),
194
+ self.as_byte_sequence)
195
+
196
+
197
+ def process_frame(colonel_id, interface_no, data):
198
+ interface = Interface.objects.filter(
199
+ colonel_id=colonel_id, no=interface_no
200
+ ).first()
201
+ if not interface:
202
+ return
203
+
204
+ data = bytes.fromhex(data)
205
+ frame = Frame(len(data) * 8, data)
206
+
207
+ device_address = frame[0:7]
208
+ device = CustomDaliDevice.objects.filter(
209
+ random_address=device_address, instance=interface.colonel.instance
210
+ ).first()
211
+ if not device:
212
+ return
213
+
214
+ device.interface = interface
215
+ device.last_seen = timezone.now()
216
+ device.save()
217
+
218
+ print("Frame received: ", frame.pack)
219
+
220
+ if frame[8:11] == 0:
221
+ # climate and air quality data
222
+ temp = (frame[12:21] - 512) / 10
223
+ humidity = round(frame[22:27] / 64 * 100)
224
+ comp = Component.objects.filter(
225
+ controller_uid=TempHumSensor.uid, config__dali_device=device.id
226
+ ).first()
227
+ if comp:
228
+ comp.controller._receive_from_device({'temp': temp, 'humidity': humidity})
229
+ voc = frame[28:38]
230
+ comp = Component.objects.filter(
231
+ controller_uid=AirQualitySensor.uid, config__dali_device=device.id
232
+ ).first()
233
+ if comp:
234
+ comp.controller._receive_from_device(voc)
235
+
236
+ elif frame[8:11] == 1:
237
+ comp = Component.objects.filter(
238
+ controller_uid=AmbientLightSensor.uid, config__dali_device=device.id
239
+ ).first()
240
+ if comp:
241
+ comp.controller._receive_from_device(frame[12:22] * 2)
242
+ comp = Component.objects.filter(
243
+ controller_uid=RoomPresenceSensor.uid, config__dali_device=device.id
244
+ ).first()
245
+ if comp:
246
+ comp.controller._receive_from_device(frame[23])
247
+
248
+ zone_sensors = {}
249
+ for slot in range(8):
250
+ comp = Component.objects.filter(
251
+ controller_uid=RoomZonePresenceSensor.uid,
252
+ config__dali_device=device.id, config__slot=slot
253
+ ).first()
254
+ if comp:
255
+ zone_sensors[slot] = comp
256
+
257
+ for slot in range(8):
258
+ if frame[24 + slot * 2]:
259
+ comp = zone_sensors.get(slot)
260
+ if comp:
261
+ comp.controller._receive_from_device(frame[25 + slot * 2])
262
+ else:
263
+ # component no longer exists, probably deleted by user
264
+ # need to inform device about that!
265
+ f = Frame(40, bytes(bytearray(5)))
266
+ f[8:11] = 15 # command to custom dali device
267
+ f[12:15] = 2 # action to perform: delete zone sensor
268
+ f[16:18] = slot
269
+ device.transmit(f)
270
+ elif zone_sensors.get(slot):
271
+ # not yet picked up by the device itself or
272
+ # was never successfully created
273
+ if zone_sensors[slot].alive:
274
+ zone_sensors[slot].alive = False
275
+ zone_sensors[slot].save()