simo 2.11.4__py3-none-any.whl → 3.0.4__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/__pycache__/settings.cpython-312.pyc +0 -0
- simo/asgi.py +25 -6
- simo/automation/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/automation/controllers.py +18 -2
- simo/automation/forms.py +15 -24
- simo/automation/gateways.py +32 -16
- simo/core/__pycache__/admin.cpython-312.pyc +0 -0
- simo/core/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/core/__pycache__/forms.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__/signal_receivers.cpython-312.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/core/admin.py +5 -4
- simo/core/base_types.py +191 -18
- simo/core/controllers.py +259 -26
- simo/core/forms.py +10 -2
- simo/core/management/_hub_template/hub/nginx.conf +23 -50
- simo/core/management/_hub_template/hub/supervisor.conf +15 -0
- simo/core/mcp.py +154 -0
- simo/core/migrations/0051_instance_ai_memory.py +18 -0
- simo/core/migrations/__pycache__/0051_instance_ai_memory.cpython-312.pyc +0 -0
- simo/core/models.py +3 -0
- simo/core/serializers.py +120 -0
- simo/core/signal_receivers.py +1 -1
- simo/core/tasks.py +1 -3
- simo/core/utils/__pycache__/type_constants.cpython-312.pyc +0 -0
- simo/core/utils/type_constants.py +78 -17
- simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
- 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__/gateways.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/admin.py +5 -1
- simo/fleet/api.py +2 -27
- simo/fleet/base_types.py +35 -4
- simo/fleet/controllers.py +162 -156
- simo/fleet/forms.py +58 -88
- simo/fleet/gateways.py +8 -15
- simo/fleet/migrations/0055_colonel_is_vo_active_colonel_last_wake_and_more.py +28 -0
- simo/fleet/migrations/0056_delete_customdalidevice.py +16 -0
- simo/fleet/migrations/__pycache__/0055_colonel_is_vo_active_colonel_last_wake_and_more.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0056_delete_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/models.py +13 -72
- simo/fleet/serializers.py +1 -48
- simo/fleet/socket_consumers.py +100 -39
- simo/fleet/tasks.py +2 -22
- simo/fleet/voice_assistant.py +903 -0
- simo/generic/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/generic/base_types.py +70 -10
- simo/generic/controllers.py +104 -17
- simo/generic/gateways.py +10 -10
- simo/mcp_server/__init__.py +0 -0
- simo/mcp_server/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/mcp_server/__pycache__/admin.cpython-312.pyc +0 -0
- simo/mcp_server/__pycache__/models.cpython-312.pyc +0 -0
- simo/mcp_server/admin.py +18 -0
- simo/mcp_server/app.py +4 -0
- simo/mcp_server/auth.py +34 -0
- simo/mcp_server/dummy.py +22 -0
- simo/mcp_server/migrations/0001_initial.py +30 -0
- simo/mcp_server/migrations/0002_alter_instanceaccesstoken_date_expired.py +18 -0
- simo/mcp_server/migrations/0003_instanceaccesstoken_issuer.py +18 -0
- simo/mcp_server/migrations/__init__.py +0 -0
- simo/mcp_server/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/mcp_server/migrations/__pycache__/0002_alter_instanceaccesstoken_date_expired.cpython-312.pyc +0 -0
- simo/mcp_server/migrations/__pycache__/0003_instanceaccesstoken_issuer.cpython-312.pyc +0 -0
- simo/mcp_server/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/mcp_server/models.py +27 -0
- simo/mcp_server/server.py +60 -0
- simo/mcp_server/tasks.py +19 -0
- simo/multimedia/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/multimedia/base_types.py +29 -4
- simo/multimedia/controllers.py +66 -19
- simo/settings.py +1 -0
- simo/users/__pycache__/utils.cpython-312.pyc +0 -0
- simo/users/utils.py +10 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/METADATA +11 -4
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/RECORD +90 -64
- simo/fleet/custom_dali_operations.py +0 -287
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/WHEEL +0 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/entry_points.txt +0 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/licenses/LICENSE.md +0 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/top_level.txt +0 -0
simo/fleet/base_types.py
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
from django.utils.translation import gettext_lazy as _
|
|
2
|
+
from simo.core.base_types import BaseComponentType
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
'
|
|
6
|
-
|
|
4
|
+
|
|
5
|
+
class DaliDeviceType(BaseComponentType):
|
|
6
|
+
slug = 'dali'
|
|
7
|
+
name = _("Dali Device")
|
|
8
|
+
description = _("DALI bus device discovered and managed by Fleet.")
|
|
9
|
+
purpose = _("Use for DALI-compliant gear integration.")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SentinelType(BaseComponentType):
|
|
13
|
+
slug = 'sentinel'
|
|
14
|
+
name = _("Sentinel")
|
|
15
|
+
description = _("Room environment sensor reporting readings, alarm siren, AI voice assistant.")
|
|
16
|
+
purpose = _("Use to capture ambient conditions of a zone, raise alarms, provice AI voice assistant.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VoiceAssistantType(BaseComponentType):
|
|
20
|
+
slug = 'voice-assistant'
|
|
21
|
+
name = _("Voice Assistant")
|
|
22
|
+
description = _("SIMO AI smart home voice assistant.")
|
|
23
|
+
purpose = _("Control smart home instance using voice commands.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _export_base_types_dict():
|
|
28
|
+
import inspect as _inspect
|
|
29
|
+
mapping = {}
|
|
30
|
+
for _name, _obj in globals().items():
|
|
31
|
+
if _inspect.isclass(_obj) and issubclass(_obj, BaseComponentType) \
|
|
32
|
+
and _obj is not BaseComponentType and getattr(_obj, 'slug', None):
|
|
33
|
+
mapping[_obj.slug] = _obj.name
|
|
34
|
+
return mapping
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
BASE_TYPES = _export_base_types_dict()
|
simo/fleet/controllers.py
CHANGED
|
@@ -13,13 +13,14 @@ from simo.core.controllers import (
|
|
|
13
13
|
Lock, ControllerBase, SingleSwitchWidget
|
|
14
14
|
)
|
|
15
15
|
from simo.core.app_widgets import NumericSensorWidget, AirQualityWidget
|
|
16
|
+
from .base_types import DaliDeviceType, SentinelType, VoiceAssistantType
|
|
16
17
|
from simo.core.utils.helpers import heat_index
|
|
17
18
|
from simo.core.utils.serialization import (
|
|
18
19
|
serialize_form_data, deserialize_form_data
|
|
19
20
|
)
|
|
20
21
|
from simo.core.forms import BaseComponentForm
|
|
21
22
|
from simo.generic.controllers import StateSelect
|
|
22
|
-
from .models import Colonel
|
|
23
|
+
from .models import Colonel
|
|
23
24
|
from .gateways import FleetGatewayHandler
|
|
24
25
|
from .forms import (
|
|
25
26
|
ColonelPinChoiceField,
|
|
@@ -33,14 +34,19 @@ from .forms import (
|
|
|
33
34
|
TTLockConfigForm, DALIDeviceConfigForm, DaliLampForm, DaliGearGroupForm,
|
|
34
35
|
DaliSwitchConfigForm,
|
|
35
36
|
DaliOccupancySensorConfigForm, DALILightSensorConfigForm,
|
|
36
|
-
DALIButtonConfigForm,
|
|
37
|
-
RoomZonePresenceConfigForm
|
|
37
|
+
DALIButtonConfigForm, SentinelDeviceConfigForm,
|
|
38
|
+
RoomZonePresenceConfigForm, VoiceAssistantConfigForm
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
class FleetDeviceMixin:
|
|
42
43
|
|
|
43
44
|
def update_options(self, options):
|
|
45
|
+
"""Update runtime options on the device via the Colonel gateway.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
- options (dict): Device-specific options; merged on the device side.
|
|
49
|
+
"""
|
|
44
50
|
GatewayObjectCommand(
|
|
45
51
|
self.component.gateway,
|
|
46
52
|
Colonel(id=self.component.config['colonel']),
|
|
@@ -50,12 +56,14 @@ class FleetDeviceMixin:
|
|
|
50
56
|
).publish()
|
|
51
57
|
|
|
52
58
|
def disable_controls(self):
|
|
59
|
+
"""Disable device controls temporarily (e.g., lock UI inputs)."""
|
|
53
60
|
options = self.component.meta.get('options', {})
|
|
54
61
|
if options.get('controls_enabled', True) != False:
|
|
55
62
|
options['controls_enabled'] = False
|
|
56
63
|
self.update_options(options)
|
|
57
64
|
|
|
58
65
|
def enable_controls(self):
|
|
66
|
+
"""Enable device controls if previously disabled."""
|
|
59
67
|
options = self.component.meta.get('options', {})
|
|
60
68
|
if options.get('controls_enabled', True) != True:
|
|
61
69
|
options['controls_enabled'] = True
|
|
@@ -86,6 +94,7 @@ class FleetDeviceMixin:
|
|
|
86
94
|
|
|
87
95
|
class BasicSensorMixin:
|
|
88
96
|
gateway_class = FleetGatewayHandler
|
|
97
|
+
accepts_value = False
|
|
89
98
|
|
|
90
99
|
def _get_occupied_pins(self):
|
|
91
100
|
return [
|
|
@@ -565,6 +574,11 @@ class TTLock(FleetDeviceMixin, Lock):
|
|
|
565
574
|
|
|
566
575
|
|
|
567
576
|
def add_code(self, code):
|
|
577
|
+
"""Add a numeric access code to the smart lock.
|
|
578
|
+
|
|
579
|
+
Parameters:
|
|
580
|
+
- code (str|int): 4–8 digit numeric code.
|
|
581
|
+
"""
|
|
568
582
|
code = str(code)
|
|
569
583
|
assert 4 <= len(code) <= 8
|
|
570
584
|
for no in code:
|
|
@@ -580,6 +594,11 @@ class TTLock(FleetDeviceMixin, Lock):
|
|
|
580
594
|
).publish()
|
|
581
595
|
|
|
582
596
|
def delete_code(self, code):
|
|
597
|
+
"""Delete a numeric access code from the lock.
|
|
598
|
+
|
|
599
|
+
Parameters:
|
|
600
|
+
- code (str|int): The exact code to remove.
|
|
601
|
+
"""
|
|
583
602
|
GatewayObjectCommand(
|
|
584
603
|
self.component.gateway,
|
|
585
604
|
Colonel(id=self.component.config['colonel']),
|
|
@@ -588,6 +607,7 @@ class TTLock(FleetDeviceMixin, Lock):
|
|
|
588
607
|
).publish()
|
|
589
608
|
|
|
590
609
|
def clear_codes(self):
|
|
610
|
+
"""Remove all numeric access codes from the lock."""
|
|
591
611
|
GatewayObjectCommand(
|
|
592
612
|
self.component.gateway,
|
|
593
613
|
Colonel(id=self.component.config['colonel']),
|
|
@@ -596,6 +616,7 @@ class TTLock(FleetDeviceMixin, Lock):
|
|
|
596
616
|
).publish()
|
|
597
617
|
|
|
598
618
|
def get_codes(self):
|
|
619
|
+
"""Request the list of numeric access codes from the lock."""
|
|
599
620
|
GatewayObjectCommand(
|
|
600
621
|
self.component.gateway,
|
|
601
622
|
Colonel(id=self.component.config['colonel']),
|
|
@@ -604,6 +625,7 @@ class TTLock(FleetDeviceMixin, Lock):
|
|
|
604
625
|
).publish()
|
|
605
626
|
|
|
606
627
|
def add_fingerprint(self):
|
|
628
|
+
"""Start lock-side enrollment of a new fingerprint."""
|
|
607
629
|
GatewayObjectCommand(
|
|
608
630
|
self.component.gateway,
|
|
609
631
|
Colonel(id=self.component.config['colonel']),
|
|
@@ -612,6 +634,7 @@ class TTLock(FleetDeviceMixin, Lock):
|
|
|
612
634
|
).publish()
|
|
613
635
|
|
|
614
636
|
def delete_fingerprint(self, code):
|
|
637
|
+
"""Delete a fingerprint by its identifier on the lock."""
|
|
615
638
|
GatewayObjectCommand(
|
|
616
639
|
self.component.gateway,
|
|
617
640
|
Colonel(id=self.component.config['colonel']),
|
|
@@ -620,6 +643,7 @@ class TTLock(FleetDeviceMixin, Lock):
|
|
|
620
643
|
).publish()
|
|
621
644
|
|
|
622
645
|
def clear_fingerprints(self):
|
|
646
|
+
"""Remove all fingerprints from the lock."""
|
|
623
647
|
self.component.meta['clear_fingerprints'] = True
|
|
624
648
|
self.component.save(update_fields=['meta'])
|
|
625
649
|
GatewayObjectCommand(
|
|
@@ -630,6 +654,7 @@ class TTLock(FleetDeviceMixin, Lock):
|
|
|
630
654
|
).publish()
|
|
631
655
|
|
|
632
656
|
def get_fingerprints(self):
|
|
657
|
+
"""Request the list of fingerprint identifiers from the lock."""
|
|
633
658
|
GatewayObjectCommand(
|
|
634
659
|
self.component.gateway,
|
|
635
660
|
Colonel(id=self.component.config['colonel']),
|
|
@@ -638,22 +663,12 @@ class TTLock(FleetDeviceMixin, Lock):
|
|
|
638
663
|
).publish()
|
|
639
664
|
|
|
640
665
|
def check_locked_status(self):
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
periodically
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
If anything new is observer, connection is made to the lock
|
|
648
|
-
and reported back to the system.
|
|
649
|
-
This helps to save batteries of a lock,
|
|
650
|
-
however it is not always as timed as we would want to.
|
|
651
|
-
Sometimes it can take even up to 20s for these updates to occur.
|
|
652
|
-
|
|
653
|
-
This method is here to force immediate connection to the lock
|
|
654
|
-
to check it's current status. After this method is called,
|
|
655
|
-
we might expect to receive an update within 2 seconds or less.
|
|
656
|
-
'''
|
|
666
|
+
"""Force an immediate connection to the lock to refresh status.
|
|
667
|
+
|
|
668
|
+
The lock usually reports status periodically to save batteries; this
|
|
669
|
+
method asks the gateway to connect proactively so that an update is
|
|
670
|
+
expected within a couple of seconds.
|
|
671
|
+
"""
|
|
657
672
|
GatewayObjectCommand(
|
|
658
673
|
self.component.gateway,
|
|
659
674
|
Colonel(id=self.component.config['colonel']),
|
|
@@ -687,13 +702,21 @@ class DALIDevice(FleetDeviceMixin, ControllerBase):
|
|
|
687
702
|
name = "DALI Device"
|
|
688
703
|
discovery_msg = _("Please hook up your new DALI device to your DALI bus.")
|
|
689
704
|
|
|
690
|
-
base_type =
|
|
705
|
+
base_type = DaliDeviceType
|
|
691
706
|
default_value = False
|
|
692
707
|
app_widget = SingleSwitchWidget
|
|
693
708
|
|
|
694
709
|
def _validate_val(self, value, occasion=None):
|
|
695
710
|
return value
|
|
696
711
|
|
|
712
|
+
def send(self, value):
|
|
713
|
+
"""Control DALI device on/off.
|
|
714
|
+
|
|
715
|
+
Parameters:
|
|
716
|
+
- value (bool): True to turn on; False to turn off.
|
|
717
|
+
"""
|
|
718
|
+
return super().send(value)
|
|
719
|
+
|
|
697
720
|
@classmethod
|
|
698
721
|
def _init_discovery(self, form_cleaned_data):
|
|
699
722
|
from simo.core.models import Gateway
|
|
@@ -748,7 +771,9 @@ class DALIDevice(FleetDeviceMixin, ControllerBase):
|
|
|
748
771
|
if data['result'].get('di') is not None:
|
|
749
772
|
started_with['name'] += f" - {data['result']['di']}"
|
|
750
773
|
started_with['controller_uid'] = controller_uid
|
|
751
|
-
|
|
774
|
+
# Normalize base type to slug for form
|
|
775
|
+
bt = getattr(controller_cls, 'base_type', None)
|
|
776
|
+
started_with['base_type'] = bt if isinstance(bt, str) else getattr(bt, 'slug', None)
|
|
752
777
|
form = controller_cls.config_form(
|
|
753
778
|
controller_uid=controller_cls.uid, data=started_with
|
|
754
779
|
)
|
|
@@ -781,10 +806,10 @@ class DALIDevice(FleetDeviceMixin, ControllerBase):
|
|
|
781
806
|
return {'error': 'INVALID INITIAL DISCOVERY FORM!'}
|
|
782
807
|
|
|
783
808
|
def replace(self):
|
|
784
|
-
"""
|
|
785
|
-
|
|
786
|
-
and
|
|
787
|
-
|
|
809
|
+
"""Replace a failed DALI device with a brand new one on the line.
|
|
810
|
+
|
|
811
|
+
Connect a new device to the DALI bus and invoke this method on the
|
|
812
|
+
existing (dead) component to transfer configuration to the newcomer.
|
|
788
813
|
"""
|
|
789
814
|
GatewayObjectCommand(
|
|
790
815
|
self.component.gateway,
|
|
@@ -857,13 +882,14 @@ class DALIButton(BaseButton, DALIDevice):
|
|
|
857
882
|
manual_add = False
|
|
858
883
|
name = 'DALI Button'
|
|
859
884
|
config_form = DALIButtonConfigForm
|
|
885
|
+
accepts_value = False
|
|
860
886
|
|
|
861
887
|
|
|
862
|
-
class
|
|
888
|
+
class Sentinel(FleetDeviceMixin, ControllerBase):
|
|
863
889
|
gateway_class = FleetGatewayHandler
|
|
864
|
-
config_form =
|
|
865
|
-
name = "
|
|
866
|
-
base_type =
|
|
890
|
+
config_form = SentinelDeviceConfigForm
|
|
891
|
+
name = "Sentinel"
|
|
892
|
+
base_type = SentinelType
|
|
867
893
|
default_value = 0
|
|
868
894
|
app_widget = NumericSensorWidget
|
|
869
895
|
|
|
@@ -894,21 +920,9 @@ class RoomSiren(FleetDeviceMixin, StateSelect):
|
|
|
894
920
|
self.send('silent')
|
|
895
921
|
|
|
896
922
|
def _send_to_device(self, value):
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
).publish()
|
|
901
|
-
else:
|
|
902
|
-
dali_device = CustomDaliDevice.objects.filter(
|
|
903
|
-
id=self.component.config['dali_device']
|
|
904
|
-
).first()
|
|
905
|
-
from .custom_dali_operations import Frame
|
|
906
|
-
frame = Frame(40, bytes(bytearray(5)))
|
|
907
|
-
frame[8:11] = 15 # command to custom dali device
|
|
908
|
-
frame[12:15] = 6 # action to perform: set value
|
|
909
|
-
frame[16:20] = 0 # device on which to perform value set
|
|
910
|
-
frame[21:24] = self.VALUES_MAP[value]
|
|
911
|
-
dali_device.transmit(frame)
|
|
923
|
+
GatewayObjectCommand(
|
|
924
|
+
self.component.gateway, self.component, set_val=value
|
|
925
|
+
).publish()
|
|
912
926
|
|
|
913
927
|
|
|
914
928
|
class AirQualitySensor(FleetDeviceMixin, BaseMultiSensor):
|
|
@@ -917,6 +931,7 @@ class AirQualitySensor(FleetDeviceMixin, BaseMultiSensor):
|
|
|
917
931
|
name = "Air Quality Sensor"
|
|
918
932
|
app_widget = AirQualityWidget
|
|
919
933
|
manual_add = False
|
|
934
|
+
accepts_value = False
|
|
920
935
|
|
|
921
936
|
default_value = [
|
|
922
937
|
["TVOC", 0, "ppb"],
|
|
@@ -975,14 +990,18 @@ class TempHumSensor(FleetDeviceMixin, BasicSensorMixin, BaseMultiSensor):
|
|
|
975
990
|
return [
|
|
976
991
|
['temperature', 0, self.sys_temp_units],
|
|
977
992
|
['humidity', 20, '%'],
|
|
978
|
-
['real_feel', 0, self.sys_temp_units]
|
|
993
|
+
['real_feel', 0, self.sys_temp_units],
|
|
994
|
+
['temp_raw', 0, 'C'],
|
|
995
|
+
['hum_raw', 0, '%'],
|
|
996
|
+
['core', 0, 'C'],
|
|
997
|
+
['outside', 0, 'C']
|
|
979
998
|
]
|
|
980
999
|
|
|
981
1000
|
def _receive_from_device(self, value, *args, **kwargs):
|
|
982
1001
|
|
|
983
1002
|
if isinstance(value, dict):
|
|
984
1003
|
temp = value['temp']
|
|
985
|
-
humidity = value['
|
|
1004
|
+
humidity = value['hum']
|
|
986
1005
|
else:
|
|
987
1006
|
buf = bytes.fromhex(value)
|
|
988
1007
|
humidity = (
|
|
@@ -997,7 +1016,11 @@ class TempHumSensor(FleetDeviceMixin, BasicSensorMixin, BaseMultiSensor):
|
|
|
997
1016
|
new_val = [
|
|
998
1017
|
['temperature', temp, self.sys_temp_units],
|
|
999
1018
|
['humidity', humidity, '%'],
|
|
1000
|
-
['real_feel', 0, self.sys_temp_units]
|
|
1019
|
+
['real_feel', 0, self.sys_temp_units],
|
|
1020
|
+
['temp_raw', value.get('temp_raw'), 'C'],
|
|
1021
|
+
['hum_raw', value.get('hum_raw'), '%'],
|
|
1022
|
+
['core', value.get('core'), 'C'],
|
|
1023
|
+
['outside', value.get('out'), 'C']
|
|
1001
1024
|
]
|
|
1002
1025
|
|
|
1003
1026
|
if self.sys_temp_units == 'F':
|
|
@@ -1015,6 +1038,7 @@ class AmbientLightSensor(FleetDeviceMixin, BaseNumericSensor):
|
|
|
1015
1038
|
gateway_class = FleetGatewayHandler
|
|
1016
1039
|
name = "Ambient lighting sensor"
|
|
1017
1040
|
manual_add = False
|
|
1041
|
+
default_value_units = 'lux'
|
|
1018
1042
|
default_config = {
|
|
1019
1043
|
'widget': 'numeric-sensor',
|
|
1020
1044
|
'value_units': 'lux',
|
|
@@ -1026,6 +1050,7 @@ class AmbientLightSensor(FleetDeviceMixin, BaseNumericSensor):
|
|
|
1026
1050
|
}
|
|
1027
1051
|
|
|
1028
1052
|
|
|
1053
|
+
|
|
1029
1054
|
class RoomPresenceSensor(FleetDeviceMixin, BaseBinarySensor):
|
|
1030
1055
|
gateway_class = FleetGatewayHandler
|
|
1031
1056
|
name = "Human presence sensor"
|
|
@@ -1056,23 +1081,16 @@ class RoomZonePresenceSensor(FleetDeviceMixin, BaseBinarySensor):
|
|
|
1056
1081
|
self.uid, serialize_form_data(form_cleaned_data),
|
|
1057
1082
|
timeout=60
|
|
1058
1083
|
)
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1084
|
+
colonel = Colonel.objects.filter(
|
|
1085
|
+
id=form_cleaned_data.get('colonel')
|
|
1086
|
+
if isinstance(form_cleaned_data.get('colonel'), int)
|
|
1087
|
+
else getattr(form_cleaned_data.get('colonel'), 'id', None)
|
|
1088
|
+
).first()
|
|
1089
|
+
if colonel:
|
|
1063
1090
|
GatewayObjectCommand(
|
|
1064
1091
|
gateway, colonel,
|
|
1065
1092
|
command='discover', type=self.uid.split('.')[-1],
|
|
1066
1093
|
).publish()
|
|
1067
|
-
else:
|
|
1068
|
-
from .custom_dali_operations import Frame
|
|
1069
|
-
dali_device = CustomDaliDevice.objects.filter(
|
|
1070
|
-
id=form_cleaned_data['device'][5:]
|
|
1071
|
-
).first()
|
|
1072
|
-
frame = Frame(40, bytes(bytearray(5)))
|
|
1073
|
-
frame[8:11] = 15 # command to custom dali device
|
|
1074
|
-
frame[12:15] = 0 # action to perform: start room zone discovery
|
|
1075
|
-
dali_device.transmit(frame)
|
|
1076
1094
|
|
|
1077
1095
|
|
|
1078
1096
|
@classmethod
|
|
@@ -1088,112 +1106,100 @@ class RoomZonePresenceSensor(FleetDeviceMixin, BaseBinarySensor):
|
|
|
1088
1106
|
controller_uid=cls.uid, data=started_with
|
|
1089
1107
|
)
|
|
1090
1108
|
form.is_valid()
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
form.
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
new_component.
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
'
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
'config': json.loads(json.dumps(new_component.config)),
|
|
1107
|
-
}
|
|
1109
|
+
form.instance.alive = False
|
|
1110
|
+
form.instance.config['colonel'] = int(
|
|
1111
|
+
getattr(form.cleaned_data['colonel'], 'id', form.cleaned_data['colonel'])
|
|
1112
|
+
)
|
|
1113
|
+
new_component = form.save()
|
|
1114
|
+
GatewayObjectCommand(
|
|
1115
|
+
new_component.gateway, Colonel(
|
|
1116
|
+
id=new_component.config['colonel']
|
|
1117
|
+
), command='finalize',
|
|
1118
|
+
data={
|
|
1119
|
+
'permanent_id': new_component.id,
|
|
1120
|
+
'comp_config': {
|
|
1121
|
+
'type': cls.uid.split('.')[-1],
|
|
1122
|
+
'family': new_component.controller.family,
|
|
1123
|
+
'config': json.loads(json.dumps(new_component.config)),
|
|
1108
1124
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
from simo.core.models import Component
|
|
1112
|
-
from .custom_dali_operations import Frame
|
|
1113
|
-
dali_device = CustomDaliDevice.objects.filter(
|
|
1114
|
-
id=form.cleaned_data['device'][5:]
|
|
1115
|
-
).first()
|
|
1116
|
-
free_slots = {0, 1, 2, 3, 4, 5, 6, 7}
|
|
1117
|
-
for comp in Component.objects.filter(
|
|
1118
|
-
controller_uid=cls.uid, config__dali_device=dali_device.id
|
|
1119
|
-
):
|
|
1120
|
-
try:
|
|
1121
|
-
free_slots.remove(int(comp.config['slot']))
|
|
1122
|
-
except:
|
|
1123
|
-
continue
|
|
1124
|
-
if not free_slots:
|
|
1125
|
-
return []
|
|
1126
|
-
|
|
1127
|
-
form.instance.alive = False
|
|
1128
|
-
form.instance.config['dali_device'] = int(
|
|
1129
|
-
form.cleaned_data['device'][5:]
|
|
1130
|
-
)
|
|
1131
|
-
form.instance.config['slot'] = free_slots.pop()
|
|
1132
|
-
new_component = form.save()
|
|
1133
|
-
frame = Frame(40, bytes(bytearray(5)))
|
|
1134
|
-
frame[8:11] = 15 # command to custom dali device
|
|
1135
|
-
frame[12:15] = 1 # action to perform: stop room zone discovery
|
|
1136
|
-
frame[16:18] = new_component.config['slot']
|
|
1137
|
-
dali_device.transmit(frame)
|
|
1125
|
+
}
|
|
1126
|
+
).publish()
|
|
1138
1127
|
|
|
1139
1128
|
return new_component
|
|
1140
1129
|
|
|
1141
1130
|
def repaint(self):
|
|
1142
1131
|
"""Repaint included 3D space"""
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
self.component.
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
).publish()
|
|
1149
|
-
else:
|
|
1150
|
-
dali_device = CustomDaliDevice.objects.filter(
|
|
1151
|
-
id=self.component.config['device'][5:]
|
|
1152
|
-
).first()
|
|
1153
|
-
from .custom_dali_operations import Frame
|
|
1154
|
-
frame = Frame(40, bytes(bytearray(5)))
|
|
1155
|
-
frame[8:11] = 15 # command to custom dali device
|
|
1156
|
-
frame[12:15] = 3 # action to perform: repaint
|
|
1157
|
-
frame[16:18] = self.component.config['slot']
|
|
1158
|
-
dali_device.transmit(frame)
|
|
1132
|
+
GatewayObjectCommand(
|
|
1133
|
+
self.component.gateway, Colonel(
|
|
1134
|
+
id=self.component.config['colonel']
|
|
1135
|
+
), command='call', method='repaint', id=self.component.id
|
|
1136
|
+
).publish()
|
|
1159
1137
|
|
|
1160
1138
|
def finish_repaint(self):
|
|
1161
1139
|
"""Finish repainting of 3D space"""
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
self.component.
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
).publish()
|
|
1169
|
-
else:
|
|
1170
|
-
dali_device = CustomDaliDevice.objects.filter(
|
|
1171
|
-
id=self.component.config['device'][5:]
|
|
1172
|
-
).first()
|
|
1173
|
-
from .custom_dali_operations import Frame
|
|
1174
|
-
frame = Frame(40, bytes(bytearray(5)))
|
|
1175
|
-
frame[8:11] = 15 # command to custom dali device
|
|
1176
|
-
frame[12:15] = 4 # action to perform: finish repaint
|
|
1177
|
-
frame[16:18] = self.component.config['slot']
|
|
1178
|
-
dali_device.transmit(frame)
|
|
1140
|
+
GatewayObjectCommand(
|
|
1141
|
+
self.component.gateway, Colonel(
|
|
1142
|
+
id=self.component.config['colonel']
|
|
1143
|
+
), command='call', method='finish_repaint',
|
|
1144
|
+
id=self.component.id
|
|
1145
|
+
).publish()
|
|
1179
1146
|
|
|
1180
1147
|
def cancel_repaint(self):
|
|
1181
1148
|
"""Finish repainting of 3D space"""
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
self.component.
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1149
|
+
GatewayObjectCommand(
|
|
1150
|
+
self.component.gateway, Colonel(
|
|
1151
|
+
id=self.component.config['colonel']
|
|
1152
|
+
), command='call', method='cancel_repaint',
|
|
1153
|
+
id=self.component.id
|
|
1154
|
+
).publish()
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
class SmokeDetector(FleetDeviceMixin, BaseBinarySensor):
|
|
1158
|
+
name = _("Dust/pollution detector")
|
|
1159
|
+
gateway_class = FleetGatewayHandler
|
|
1160
|
+
manual_add = False
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
class VoiceAssistant(FleetDeviceMixin, BaseBinarySensor):
|
|
1164
|
+
base_type = VoiceAssistantType
|
|
1165
|
+
name = _("AI Voice Assistant")
|
|
1166
|
+
gateway_class = FleetGatewayHandler
|
|
1167
|
+
config_form = VoiceAssistantConfigForm
|
|
1168
|
+
manual_add = False
|
|
1169
|
+
default_config = {'voice': 'male', 'enabled': True}
|
|
1170
|
+
|
|
1171
|
+
def arm(self):
|
|
1172
|
+
"""
|
|
1173
|
+
Arming voice assistant is means disabling it,
|
|
1174
|
+
so that it can not be used by invaders.
|
|
1175
|
+
"""
|
|
1176
|
+
self.component.refresh_from_db()
|
|
1177
|
+
self.component.config['enabled'] = False
|
|
1178
|
+
self.component.arm_status = 'armed'
|
|
1179
|
+
self.component.save()
|
|
1180
|
+
GatewayObjectCommand(
|
|
1181
|
+
self.component.gateway, Colonel(
|
|
1182
|
+
id=self.component.config['colonel']
|
|
1183
|
+
), command='call', method='disable',
|
|
1184
|
+
id=self.component.id
|
|
1185
|
+
).publish()
|
|
1186
|
+
|
|
1187
|
+
def disarm(self):
|
|
1188
|
+
"""
|
|
1189
|
+
Disarming voice assistant is means enabling it,
|
|
1190
|
+
so that anyone can use it to control the smart home system.
|
|
1191
|
+
"""
|
|
1192
|
+
self.component.refresh_from_db()
|
|
1193
|
+
self.component.config['enabled'] = True
|
|
1194
|
+
self.component.arm_status = 'disarmed'
|
|
1195
|
+
self.component.save()
|
|
1196
|
+
GatewayObjectCommand(
|
|
1197
|
+
self.component.gateway, Colonel(
|
|
1198
|
+
id=self.component.config['colonel']
|
|
1199
|
+
), command='call', method='enable',
|
|
1200
|
+
id=self.component.id
|
|
1201
|
+
).publish()
|
|
1199
1202
|
|
|
1203
|
+
def is_in_alarm(self):
|
|
1204
|
+
"""Returns always False"""
|
|
1205
|
+
return False
|