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.

Files changed (91) hide show
  1. simo/__pycache__/settings.cpython-312.pyc +0 -0
  2. simo/asgi.py +25 -6
  3. simo/automation/__pycache__/controllers.cpython-312.pyc +0 -0
  4. simo/automation/controllers.py +18 -2
  5. simo/automation/forms.py +15 -24
  6. simo/automation/gateways.py +32 -16
  7. simo/core/__pycache__/admin.cpython-312.pyc +0 -0
  8. simo/core/__pycache__/base_types.cpython-312.pyc +0 -0
  9. simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
  10. simo/core/__pycache__/forms.cpython-312.pyc +0 -0
  11. simo/core/__pycache__/models.cpython-312.pyc +0 -0
  12. simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
  13. simo/core/__pycache__/signal_receivers.cpython-312.pyc +0 -0
  14. simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
  15. simo/core/admin.py +5 -4
  16. simo/core/base_types.py +191 -18
  17. simo/core/controllers.py +259 -26
  18. simo/core/forms.py +10 -2
  19. simo/core/management/_hub_template/hub/nginx.conf +23 -50
  20. simo/core/management/_hub_template/hub/supervisor.conf +15 -0
  21. simo/core/mcp.py +154 -0
  22. simo/core/migrations/0051_instance_ai_memory.py +18 -0
  23. simo/core/migrations/__pycache__/0051_instance_ai_memory.cpython-312.pyc +0 -0
  24. simo/core/models.py +3 -0
  25. simo/core/serializers.py +120 -0
  26. simo/core/signal_receivers.py +1 -1
  27. simo/core/tasks.py +1 -3
  28. simo/core/utils/__pycache__/type_constants.cpython-312.pyc +0 -0
  29. simo/core/utils/type_constants.py +78 -17
  30. simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
  31. simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
  32. simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
  33. simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
  34. simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
  35. simo/fleet/__pycache__/gateways.cpython-312.pyc +0 -0
  36. simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
  37. simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
  38. simo/fleet/admin.py +5 -1
  39. simo/fleet/api.py +2 -27
  40. simo/fleet/base_types.py +35 -4
  41. simo/fleet/controllers.py +162 -156
  42. simo/fleet/forms.py +58 -88
  43. simo/fleet/gateways.py +8 -15
  44. simo/fleet/migrations/0055_colonel_is_vo_active_colonel_last_wake_and_more.py +28 -0
  45. simo/fleet/migrations/0056_delete_customdalidevice.py +16 -0
  46. simo/fleet/migrations/__pycache__/0055_colonel_is_vo_active_colonel_last_wake_and_more.cpython-312.pyc +0 -0
  47. simo/fleet/migrations/__pycache__/0056_delete_customdalidevice.cpython-312.pyc +0 -0
  48. simo/fleet/models.py +13 -72
  49. simo/fleet/serializers.py +1 -48
  50. simo/fleet/socket_consumers.py +100 -39
  51. simo/fleet/tasks.py +2 -22
  52. simo/fleet/voice_assistant.py +903 -0
  53. simo/generic/__pycache__/base_types.cpython-312.pyc +0 -0
  54. simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
  55. simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
  56. simo/generic/base_types.py +70 -10
  57. simo/generic/controllers.py +104 -17
  58. simo/generic/gateways.py +10 -10
  59. simo/mcp_server/__init__.py +0 -0
  60. simo/mcp_server/__pycache__/__init__.cpython-312.pyc +0 -0
  61. simo/mcp_server/__pycache__/admin.cpython-312.pyc +0 -0
  62. simo/mcp_server/__pycache__/models.cpython-312.pyc +0 -0
  63. simo/mcp_server/admin.py +18 -0
  64. simo/mcp_server/app.py +4 -0
  65. simo/mcp_server/auth.py +34 -0
  66. simo/mcp_server/dummy.py +22 -0
  67. simo/mcp_server/migrations/0001_initial.py +30 -0
  68. simo/mcp_server/migrations/0002_alter_instanceaccesstoken_date_expired.py +18 -0
  69. simo/mcp_server/migrations/0003_instanceaccesstoken_issuer.py +18 -0
  70. simo/mcp_server/migrations/__init__.py +0 -0
  71. simo/mcp_server/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  72. simo/mcp_server/migrations/__pycache__/0002_alter_instanceaccesstoken_date_expired.cpython-312.pyc +0 -0
  73. simo/mcp_server/migrations/__pycache__/0003_instanceaccesstoken_issuer.cpython-312.pyc +0 -0
  74. simo/mcp_server/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  75. simo/mcp_server/models.py +27 -0
  76. simo/mcp_server/server.py +60 -0
  77. simo/mcp_server/tasks.py +19 -0
  78. simo/multimedia/__pycache__/base_types.cpython-312.pyc +0 -0
  79. simo/multimedia/__pycache__/controllers.cpython-312.pyc +0 -0
  80. simo/multimedia/base_types.py +29 -4
  81. simo/multimedia/controllers.py +66 -19
  82. simo/settings.py +1 -0
  83. simo/users/__pycache__/utils.cpython-312.pyc +0 -0
  84. simo/users/utils.py +10 -0
  85. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/METADATA +11 -4
  86. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/RECORD +90 -64
  87. simo/fleet/custom_dali_operations.py +0 -287
  88. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/WHEEL +0 -0
  89. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/entry_points.txt +0 -0
  90. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/licenses/LICENSE.md +0 -0
  91. {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
- BASE_TYPES = {
4
- 'dali': _("Dali Device"),
5
- 'room-sensor': _("Room Sensor"),
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, CustomDaliDevice
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, RoomSensorDeviceConfigForm,
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
- Lock state is monitored by capturing adv data
643
- periodically transmitted by the lock.
644
- This data includes information about it's lock/unlock position
645
- also if there are any new events in it that we are not yet aware of.
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 = 'dali'
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
- started_with['base_type'] = controller_cls.base_type
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
- Hook up brand new replacement device to the dali line
786
- and execute this command on existing (dead) component instance,
787
- so that it can be replaced by the new physical device.
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 RoomSensor(FleetDeviceMixin, ControllerBase):
888
+ class Sentinel(FleetDeviceMixin, ControllerBase):
863
889
  gateway_class = FleetGatewayHandler
864
- config_form = RoomSensorDeviceConfigForm
865
- name = "Room Sensor"
866
- base_type = 'room-sensor'
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
- if self.component.config.get('colonel'):
898
- GatewayObjectCommand(
899
- self.component.gateway, self.component, set_val=value
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['humidity']
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
- if form_cleaned_data['device'].startswith('wifi'):
1060
- colonel = Colonel.objects.filter(
1061
- id=form_cleaned_data['device'][5:]
1062
- ).first()
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
- if form.cleaned_data['device'].startswith('wifi'):
1092
- form.instance.alive = False
1093
- form.instance.config['colonel'] = int(
1094
- form.cleaned_data['device'][5:]
1095
- )
1096
- new_component = form.save()
1097
- GatewayObjectCommand(
1098
- new_component.gateway, Colonel(
1099
- id=new_component.config['colonel']
1100
- ), command='finalize',
1101
- data={
1102
- 'permanent_id': new_component.id,
1103
- 'comp_config': {
1104
- 'type': cls.uid.split('.')[-1],
1105
- 'family': new_component.controller.family,
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
- ).publish()
1110
- else:
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
- if self.component.config['device'].startswith('wifi'):
1144
- GatewayObjectCommand(
1145
- self.component.gateway, Colonel(
1146
- id=self.component.config['colonel']
1147
- ), command='call', method='repaint', id=self.component.id
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
- if self.component.config['device'].startswith('wifi'):
1163
- GatewayObjectCommand(
1164
- self.component.gateway, Colonel(
1165
- id=self.component.config['colonel']
1166
- ), command='call', method='finish_repaint',
1167
- id=self.component.id
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
- if self.component.config['device'].startswith('wifi'):
1183
- GatewayObjectCommand(
1184
- self.component.gateway, Colonel(
1185
- id=self.component.config['colonel']
1186
- ), command='call', method='cancel_repaint',
1187
- id=self.component.id
1188
- ).publish()
1189
- else:
1190
- dali_device = CustomDaliDevice.objects.filter(
1191
- id=self.component.config['device'][5:]
1192
- ).first()
1193
- from .custom_dali_operations import Frame
1194
- frame = Frame(40, bytes(bytearray(5)))
1195
- frame[8:11] = 15 # command to custom dali device
1196
- frame[12:15] = 5 # action to perform: cancel repaint
1197
- frame[16:18] = self.component.config['slot']
1198
- dali_device.transmit(frame)
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