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.
- simo/automation/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/automation/gateways.py +12 -10
- simo/core/__pycache__/admin.cpython-312.pyc +0 -0
- simo/core/__pycache__/auto_urls.cpython-312.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/core/__pycache__/models.cpython-312.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/core/__pycache__/views.cpython-312.pyc +0 -0
- simo/core/admin.py +5 -2
- simo/core/auto_urls.py +4 -1
- simo/core/controllers.py +42 -5
- simo/core/models.py +32 -16
- simo/core/serializers.py +2 -2
- simo/core/tasks.py +8 -1
- simo/core/templates/admin/core/component_change_form.html +1 -1
- simo/core/templates/admin/wizard/discovery.html +3 -4
- simo/core/templates/admin/wizard/wizard_add.html +1 -1
- simo/core/views.py +26 -2
- simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/managers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/socket_consumers.cpython-312.pyc +0 -0
- simo/fleet/api.py +26 -3
- simo/fleet/base_types.py +1 -0
- simo/fleet/controllers.py +240 -7
- simo/fleet/custom_dali_operations.py +275 -0
- simo/fleet/forms.py +132 -3
- simo/fleet/managers.py +3 -1
- simo/fleet/migrations/0045_alter_colonel_type_customdalidevice.py +29 -0
- simo/fleet/migrations/0046_delete_customdalidevice.py +16 -0
- simo/fleet/migrations/0047_customdalidevice.py +28 -0
- simo/fleet/migrations/0048_remove_customdalidevice_colonel_and_more.py +28 -0
- simo/fleet/migrations/__pycache__/0045_alter_colonel_type_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0046_delete_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0047_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0048_remove_customdalidevice_colonel_and_more.cpython-312.pyc +0 -0
- simo/fleet/models.py +54 -9
- simo/fleet/serializers.py +15 -1
- simo/fleet/socket_consumers.py +6 -0
- simo/fleet/tasks.py +22 -2
- simo/fleet/templates/fleet/controllers_info/RoomZonePresenceSensor.md +8 -0
- simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/generic/controllers.py +99 -43
- simo/generic/forms.py +13 -10
- simo/generic/gateways.py +91 -2
- simo/generic/migrations/0003_auto_20250409_1404.py +33 -0
- simo/generic/migrations/__pycache__/0003_auto_20250409_1404.cpython-312.pyc +0 -0
- simo/users/__pycache__/api.cpython-312.pyc +0 -0
- simo/users/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
- simo/users/api.py +71 -18
- simo/users/dynamic_settings.py +1 -1
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/METADATA +1 -1
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/RECORD +64 -52
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/WHEEL +0 -0
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/entry_points.txt +0 -0
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/licenses/LICENSE.md +0 -0
- {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 .
|
|
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
|
|
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
|
|
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()
|