conson-xp 1.18.0__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.
Files changed (176) hide show
  1. conson_xp-1.18.0.dist-info/METADATA +412 -0
  2. conson_xp-1.18.0.dist-info/RECORD +176 -0
  3. conson_xp-1.18.0.dist-info/WHEEL +4 -0
  4. conson_xp-1.18.0.dist-info/entry_points.txt +5 -0
  5. conson_xp-1.18.0.dist-info/licenses/LICENSE +29 -0
  6. xp/__init__.py +9 -0
  7. xp/cli/__init__.py +5 -0
  8. xp/cli/__main__.py +6 -0
  9. xp/cli/commands/__init__.py +153 -0
  10. xp/cli/commands/conbus/__init__.py +25 -0
  11. xp/cli/commands/conbus/conbus.py +128 -0
  12. xp/cli/commands/conbus/conbus_actiontable_commands.py +233 -0
  13. xp/cli/commands/conbus/conbus_autoreport_commands.py +108 -0
  14. xp/cli/commands/conbus/conbus_blink_commands.py +163 -0
  15. xp/cli/commands/conbus/conbus_config_commands.py +29 -0
  16. xp/cli/commands/conbus/conbus_custom_commands.py +57 -0
  17. xp/cli/commands/conbus/conbus_datapoint_commands.py +113 -0
  18. xp/cli/commands/conbus/conbus_discover_commands.py +61 -0
  19. xp/cli/commands/conbus/conbus_event_commands.py +81 -0
  20. xp/cli/commands/conbus/conbus_lightlevel_commands.py +207 -0
  21. xp/cli/commands/conbus/conbus_linknumber_commands.py +102 -0
  22. xp/cli/commands/conbus/conbus_modulenumber_commands.py +104 -0
  23. xp/cli/commands/conbus/conbus_msactiontable_commands.py +94 -0
  24. xp/cli/commands/conbus/conbus_output_commands.py +163 -0
  25. xp/cli/commands/conbus/conbus_raw_commands.py +62 -0
  26. xp/cli/commands/conbus/conbus_receive_commands.py +59 -0
  27. xp/cli/commands/conbus/conbus_scan_commands.py +58 -0
  28. xp/cli/commands/file_commands.py +186 -0
  29. xp/cli/commands/homekit/__init__.py +3 -0
  30. xp/cli/commands/homekit/homekit.py +118 -0
  31. xp/cli/commands/homekit/homekit_start_commands.py +43 -0
  32. xp/cli/commands/module_commands.py +187 -0
  33. xp/cli/commands/reverse_proxy_commands.py +178 -0
  34. xp/cli/commands/server/__init__.py +3 -0
  35. xp/cli/commands/server/server_commands.py +135 -0
  36. xp/cli/commands/telegram/__init__.py +5 -0
  37. xp/cli/commands/telegram/telegram.py +41 -0
  38. xp/cli/commands/telegram/telegram_blink_commands.py +79 -0
  39. xp/cli/commands/telegram/telegram_checksum_commands.py +112 -0
  40. xp/cli/commands/telegram/telegram_discover_commands.py +41 -0
  41. xp/cli/commands/telegram/telegram_linknumber_commands.py +86 -0
  42. xp/cli/commands/telegram/telegram_parse_commands.py +75 -0
  43. xp/cli/commands/telegram/telegram_version_commands.py +52 -0
  44. xp/cli/main.py +87 -0
  45. xp/cli/utils/__init__.py +1 -0
  46. xp/cli/utils/click_tree.py +57 -0
  47. xp/cli/utils/datapoint_type_choice.py +57 -0
  48. xp/cli/utils/decorators.py +351 -0
  49. xp/cli/utils/error_handlers.py +201 -0
  50. xp/cli/utils/formatters.py +312 -0
  51. xp/cli/utils/module_type_choice.py +56 -0
  52. xp/cli/utils/serial_number_type.py +52 -0
  53. xp/cli/utils/system_function_choice.py +57 -0
  54. xp/cli/utils/xp_module_type.py +53 -0
  55. xp/connection/__init__.py +13 -0
  56. xp/connection/exceptions.py +22 -0
  57. xp/models/__init__.py +36 -0
  58. xp/models/actiontable/__init__.py +1 -0
  59. xp/models/actiontable/actiontable.py +43 -0
  60. xp/models/actiontable/msactiontable_xp20.py +53 -0
  61. xp/models/actiontable/msactiontable_xp24.py +58 -0
  62. xp/models/actiontable/msactiontable_xp33.py +65 -0
  63. xp/models/conbus/__init__.py +1 -0
  64. xp/models/conbus/conbus.py +87 -0
  65. xp/models/conbus/conbus_autoreport.py +67 -0
  66. xp/models/conbus/conbus_blink.py +80 -0
  67. xp/models/conbus/conbus_client_config.py +55 -0
  68. xp/models/conbus/conbus_connection_status.py +40 -0
  69. xp/models/conbus/conbus_custom.py +58 -0
  70. xp/models/conbus/conbus_datapoint.py +89 -0
  71. xp/models/conbus/conbus_discover.py +64 -0
  72. xp/models/conbus/conbus_event_raw.py +47 -0
  73. xp/models/conbus/conbus_lightlevel.py +52 -0
  74. xp/models/conbus/conbus_linknumber.py +54 -0
  75. xp/models/conbus/conbus_output.py +57 -0
  76. xp/models/conbus/conbus_raw.py +45 -0
  77. xp/models/conbus/conbus_receive.py +42 -0
  78. xp/models/conbus/conbus_writeconfig.py +60 -0
  79. xp/models/homekit/__init__.py +1 -0
  80. xp/models/homekit/homekit_accessory.py +35 -0
  81. xp/models/homekit/homekit_config.py +106 -0
  82. xp/models/homekit/homekit_conson_config.py +86 -0
  83. xp/models/log_entry.py +130 -0
  84. xp/models/protocol/__init__.py +1 -0
  85. xp/models/protocol/conbus_protocol.py +312 -0
  86. xp/models/response.py +42 -0
  87. xp/models/telegram/__init__.py +1 -0
  88. xp/models/telegram/action_type.py +31 -0
  89. xp/models/telegram/datapoint_type.py +82 -0
  90. xp/models/telegram/event_telegram.py +140 -0
  91. xp/models/telegram/event_type.py +15 -0
  92. xp/models/telegram/input_action_type.py +69 -0
  93. xp/models/telegram/input_type.py +17 -0
  94. xp/models/telegram/module_type.py +188 -0
  95. xp/models/telegram/module_type_code.py +205 -0
  96. xp/models/telegram/output_telegram.py +103 -0
  97. xp/models/telegram/reply_telegram.py +297 -0
  98. xp/models/telegram/system_function.py +116 -0
  99. xp/models/telegram/system_telegram.py +94 -0
  100. xp/models/telegram/telegram.py +28 -0
  101. xp/models/telegram/telegram_type.py +19 -0
  102. xp/models/telegram/timeparam_type.py +51 -0
  103. xp/models/write_config_type.py +33 -0
  104. xp/services/__init__.py +26 -0
  105. xp/services/actiontable/__init__.py +1 -0
  106. xp/services/actiontable/actiontable_serializer.py +273 -0
  107. xp/services/actiontable/msactiontable_serializer.py +7 -0
  108. xp/services/actiontable/msactiontable_xp20_serializer.py +169 -0
  109. xp/services/actiontable/msactiontable_xp24_serializer.py +120 -0
  110. xp/services/actiontable/msactiontable_xp33_serializer.py +239 -0
  111. xp/services/conbus/__init__.py +1 -0
  112. xp/services/conbus/actiontable/__init__.py +1 -0
  113. xp/services/conbus/actiontable/actiontable_download_service.py +158 -0
  114. xp/services/conbus/actiontable/actiontable_list_service.py +91 -0
  115. xp/services/conbus/actiontable/actiontable_show_service.py +89 -0
  116. xp/services/conbus/actiontable/actiontable_upload_service.py +211 -0
  117. xp/services/conbus/actiontable/msactiontable_service.py +232 -0
  118. xp/services/conbus/conbus_blink_all_service.py +181 -0
  119. xp/services/conbus/conbus_blink_service.py +158 -0
  120. xp/services/conbus/conbus_custom_service.py +156 -0
  121. xp/services/conbus/conbus_datapoint_queryall_service.py +182 -0
  122. xp/services/conbus/conbus_datapoint_service.py +170 -0
  123. xp/services/conbus/conbus_discover_service.py +312 -0
  124. xp/services/conbus/conbus_event_raw_service.py +181 -0
  125. xp/services/conbus/conbus_output_service.py +194 -0
  126. xp/services/conbus/conbus_raw_service.py +122 -0
  127. xp/services/conbus/conbus_receive_service.py +115 -0
  128. xp/services/conbus/conbus_scan_service.py +150 -0
  129. xp/services/conbus/write_config_service.py +194 -0
  130. xp/services/homekit/__init__.py +1 -0
  131. xp/services/homekit/homekit_cache_service.py +307 -0
  132. xp/services/homekit/homekit_conbus_service.py +93 -0
  133. xp/services/homekit/homekit_config_validator.py +310 -0
  134. xp/services/homekit/homekit_conson_validator.py +121 -0
  135. xp/services/homekit/homekit_dimminglight.py +182 -0
  136. xp/services/homekit/homekit_dimminglight_service.py +148 -0
  137. xp/services/homekit/homekit_hap_service.py +342 -0
  138. xp/services/homekit/homekit_lightbulb.py +120 -0
  139. xp/services/homekit/homekit_lightbulb_service.py +86 -0
  140. xp/services/homekit/homekit_module_service.py +56 -0
  141. xp/services/homekit/homekit_outlet.py +168 -0
  142. xp/services/homekit/homekit_outlet_service.py +121 -0
  143. xp/services/homekit/homekit_service.py +359 -0
  144. xp/services/log_file_service.py +309 -0
  145. xp/services/module_type_service.py +257 -0
  146. xp/services/protocol/__init__.py +21 -0
  147. xp/services/protocol/conbus_event_protocol.py +360 -0
  148. xp/services/protocol/conbus_protocol.py +318 -0
  149. xp/services/protocol/protocol_factory.py +78 -0
  150. xp/services/protocol/telegram_protocol.py +264 -0
  151. xp/services/reverse_proxy_service.py +435 -0
  152. xp/services/server/__init__.py +1 -0
  153. xp/services/server/base_server_service.py +366 -0
  154. xp/services/server/cp20_server_service.py +65 -0
  155. xp/services/server/device_service_factory.py +94 -0
  156. xp/services/server/server_service.py +428 -0
  157. xp/services/server/xp130_server_service.py +67 -0
  158. xp/services/server/xp20_server_service.py +92 -0
  159. xp/services/server/xp230_server_service.py +58 -0
  160. xp/services/server/xp24_server_service.py +245 -0
  161. xp/services/server/xp33_server_service.py +535 -0
  162. xp/services/telegram/__init__.py +1 -0
  163. xp/services/telegram/telegram_blink_service.py +138 -0
  164. xp/services/telegram/telegram_checksum_service.py +149 -0
  165. xp/services/telegram/telegram_datapoint_service.py +82 -0
  166. xp/services/telegram/telegram_discover_service.py +277 -0
  167. xp/services/telegram/telegram_link_number_service.py +216 -0
  168. xp/services/telegram/telegram_output_service.py +322 -0
  169. xp/services/telegram/telegram_service.py +380 -0
  170. xp/services/telegram/telegram_version_service.py +288 -0
  171. xp/utils/__init__.py +12 -0
  172. xp/utils/checksum.py +61 -0
  173. xp/utils/dependencies.py +531 -0
  174. xp/utils/event_helper.py +31 -0
  175. xp/utils/serialization.py +205 -0
  176. xp/utils/time_utils.py +134 -0
@@ -0,0 +1,56 @@
1
+ """HomeKit Module Service.
2
+
3
+ This module provides service implementation for HomeKit module management.
4
+ """
5
+
6
+ import logging
7
+ from typing import Optional
8
+
9
+ from xp.models.homekit.homekit_conson_config import (
10
+ ConsonModuleConfig,
11
+ ConsonModuleListConfig,
12
+ )
13
+
14
+
15
+ class HomekitModuleService:
16
+ """Service for managing HomeKit module configurations.
17
+
18
+ Attributes:
19
+ logger: Logger instance.
20
+ conson_modules_config: Conson module list configuration.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ conson_modules_config: ConsonModuleListConfig,
26
+ ):
27
+ """Initialize the HomeKit module service.
28
+
29
+ Args:
30
+ conson_modules_config: Conson module list configuration.
31
+ """
32
+ # Set up logging
33
+ self.logger = logging.getLogger(__name__)
34
+ self.conson_modules_config = conson_modules_config
35
+
36
+ def get_module_by_serial(self, serial_number: str) -> Optional[ConsonModuleConfig]:
37
+ """Get a module by its serial number.
38
+
39
+ Args:
40
+ serial_number: Serial number of the module to find.
41
+
42
+ Returns:
43
+ Module configuration if found, None otherwise.
44
+ """
45
+ module = next(
46
+ (
47
+ module
48
+ for module in self.conson_modules_config.root
49
+ if module.serial_number == serial_number
50
+ ),
51
+ None,
52
+ )
53
+ self.logger.debug(
54
+ f"Module search by serial '{serial_number}': {'found' if module else 'not found'}"
55
+ )
56
+ return module
@@ -0,0 +1,168 @@
1
+ """HomeKit Outlet Accessory.
2
+
3
+ This module provides an outlet accessory for HomeKit integration.
4
+ """
5
+
6
+ import logging
7
+
8
+ from bubus import EventBus
9
+ from pyhap.accessory import Accessory
10
+ from pyhap.accessory_driver import AccessoryDriver
11
+ from pyhap.const import CATEGORY_OUTLET
12
+
13
+ from xp.models.homekit.homekit_config import HomekitAccessoryConfig
14
+ from xp.models.homekit.homekit_conson_config import ConsonModuleConfig
15
+ from xp.models.protocol.conbus_protocol import (
16
+ OutletGetInUseEvent,
17
+ OutletGetOnEvent,
18
+ OutletSetInUseEvent,
19
+ OutletSetOnEvent,
20
+ )
21
+
22
+
23
+ class Outlet(Accessory):
24
+ """HomeKit outlet accessory.
25
+
26
+ Attributes:
27
+ category: HomeKit category (CATEGORY_OUTLET).
28
+ event_bus: Event bus for inter-service communication.
29
+ logger: Logger instance.
30
+ identifier: Unique identifier for the accessory.
31
+ accessory: Accessory configuration.
32
+ module: Module configuration.
33
+ is_on: Current on/off state.
34
+ is_in_use: Current in-use state.
35
+ char_on: On characteristic.
36
+ char_outlet_in_use: Outlet in-use characteristic.
37
+ """
38
+
39
+ category = CATEGORY_OUTLET
40
+ event_bus: EventBus
41
+
42
+ def __init__(
43
+ self,
44
+ driver: AccessoryDriver,
45
+ module: ConsonModuleConfig,
46
+ accessory: HomekitAccessoryConfig,
47
+ event_bus: EventBus,
48
+ ):
49
+ """Initialize the outlet accessory.
50
+
51
+ Args:
52
+ driver: HAP accessory driver.
53
+ module: Module configuration.
54
+ accessory: Accessory configuration.
55
+ event_bus: Event bus for inter-service communication.
56
+ """
57
+ super().__init__(driver=driver, display_name=accessory.description)
58
+
59
+ self.logger = logging.getLogger(__name__)
60
+
61
+ identifier = f"{module.serial_number}.{accessory.output_number:02d}"
62
+ version = accessory.id
63
+ manufacturer = "Conson"
64
+ model = ("XP24_outlet",)
65
+
66
+ self.identifier = identifier
67
+ self.accessory = accessory
68
+ self.module = module
69
+
70
+ self.event_bus = event_bus
71
+ self.logger.info(
72
+ "Creating Outlet { serial_number : %s, output_number: %s }",
73
+ module.serial_number,
74
+ accessory.output_number,
75
+ )
76
+ self.is_on = False
77
+ self.is_in_use = False
78
+
79
+ serv_outlet = self.add_preload_service("Outlet")
80
+ self.set_info_service(version, manufacturer, model, identifier)
81
+ self.char_on = serv_outlet.configure_char(
82
+ "On", setter_callback=self.set_on, getter_callback=self.get_on
83
+ )
84
+ self.char_outlet_in_use = serv_outlet.configure_char(
85
+ "OutletInUse",
86
+ setter_callback=self.set_outlet_in_use,
87
+ getter_callback=self.get_outlet_in_use,
88
+ )
89
+
90
+ def set_outlet_in_use(self, value: bool) -> None:
91
+ """Set the in-use state of the outlet.
92
+
93
+ Args:
94
+ value: True if in use, False otherwise.
95
+ """
96
+ self.logger.debug(f"set_outlet_in_use {value}")
97
+
98
+ self.is_in_use = value
99
+ self.event_bus.dispatch(
100
+ OutletSetInUseEvent(
101
+ serial_number=self.accessory.serial_number,
102
+ output_number=self.accessory.output_number,
103
+ module=self.module,
104
+ accessory=self.accessory,
105
+ value=value,
106
+ )
107
+ )
108
+ self.logger.debug(f"set_outlet_in_use {value} end")
109
+
110
+ def get_outlet_in_use(self) -> bool:
111
+ """Get the in-use state of the outlet.
112
+
113
+ Returns:
114
+ True if in use, False otherwise.
115
+ """
116
+ # Emit event and get response
117
+ self.logger.debug("get_outlet_in_use")
118
+
119
+ # Dispatch event from HAP thread (thread-safe)
120
+ self.event_bus.dispatch(
121
+ OutletGetInUseEvent(
122
+ serial_number=self.accessory.serial_number,
123
+ output_number=self.accessory.output_number,
124
+ module=self.module,
125
+ accessory=self.accessory,
126
+ )
127
+ )
128
+ return self.is_in_use
129
+
130
+ def set_on(self, value: bool) -> None:
131
+ """Set the on/off state of the outlet.
132
+
133
+ Args:
134
+ value: True to turn on, False to turn off.
135
+ """
136
+ # Emit set event
137
+ self.logger.debug(f"set_on {value} {self.is_on}")
138
+
139
+ self.is_on = value
140
+ self.event_bus.dispatch(
141
+ OutletSetOnEvent(
142
+ serial_number=self.accessory.serial_number,
143
+ output_number=self.accessory.output_number,
144
+ module=self.module,
145
+ accessory=self.accessory,
146
+ value=value,
147
+ )
148
+ )
149
+
150
+ def get_on(self) -> bool:
151
+ """Get the on/off state of the outlet.
152
+
153
+ Returns:
154
+ True if on, False if off.
155
+ """
156
+ # Emit event and get response
157
+ self.logger.debug("get_on")
158
+
159
+ # Dispatch event from HAP thread (thread-safe)
160
+ self.event_bus.dispatch(
161
+ OutletGetOnEvent(
162
+ serial_number=self.accessory.serial_number,
163
+ output_number=self.accessory.output_number,
164
+ module=self.module,
165
+ accessory=self.accessory,
166
+ )
167
+ )
168
+ return self.is_on
@@ -0,0 +1,121 @@
1
+ """HomeKit Outlet Service.
2
+
3
+ This module provides service implementation for outlet accessories.
4
+ """
5
+
6
+ import logging
7
+
8
+ from bubus import EventBus
9
+
10
+ from xp.models.protocol.conbus_protocol import (
11
+ OutletGetInUseEvent,
12
+ OutletGetOnEvent,
13
+ OutletSetOnEvent,
14
+ ReadDatapointEvent,
15
+ SendActionEvent,
16
+ )
17
+ from xp.models.telegram.datapoint_type import DataPointType
18
+
19
+
20
+ class HomeKitOutletService:
21
+ """Outlet service for HomeKit.
22
+
23
+ Attributes:
24
+ event_bus: Event bus for inter-service communication.
25
+ logger: Logger instance.
26
+ """
27
+
28
+ event_bus: EventBus
29
+
30
+ def __init__(self, event_bus: EventBus):
31
+ """Initialize the outlet service.
32
+
33
+ Args:
34
+ event_bus: Event bus instance.
35
+ """
36
+ self.event_bus = event_bus
37
+ self.logger = logging.getLogger(__name__)
38
+
39
+ # Register event handlers
40
+ self.event_bus.on(OutletGetOnEvent, self.handle_outlet_get_on)
41
+ self.event_bus.on(OutletSetOnEvent, self.handle_outlet_set_on)
42
+ self.event_bus.on(OutletGetInUseEvent, self.handle_outlet_get_in_use)
43
+
44
+ def handle_outlet_get_on(self, event: OutletGetOnEvent) -> bool:
45
+ """Handle outlet get on event.
46
+
47
+ Args:
48
+ event: Outlet get on event.
49
+
50
+ Returns:
51
+ True if request was dispatched successfully.
52
+ """
53
+ self.logger.debug(
54
+ f"Getting outlet state for serial {event.serial_number}, output {event.output_number}"
55
+ )
56
+
57
+ datapoint_type = DataPointType.MODULE_OUTPUT_STATE
58
+ read_datapoint = ReadDatapointEvent(
59
+ serial_number=event.serial_number, datapoint_type=datapoint_type
60
+ )
61
+
62
+ self.logger.debug(f"Dispatching ReadDatapointEvent for {event.serial_number}")
63
+ self.event_bus.dispatch(read_datapoint)
64
+ self.logger.debug(f"Dispatched ReadDatapointEvent for {event.serial_number}")
65
+ return True
66
+
67
+ def handle_outlet_set_on(self, event: OutletSetOnEvent) -> bool:
68
+ """Handle outlet set on event.
69
+
70
+ Args:
71
+ event: Outlet set on event.
72
+
73
+ Returns:
74
+ True if command was sent successfully.
75
+ """
76
+ self.logger.info(
77
+ f"Setting outlet "
78
+ f"for serial {event.serial_number}, "
79
+ f"output {event.output_number} "
80
+ f"to {'ON' if event.value else 'OFF'}"
81
+ )
82
+ self.logger.debug(f"outlet_set_on {event}")
83
+
84
+ send_action = SendActionEvent(
85
+ serial_number=event.serial_number,
86
+ output_number=event.output_number,
87
+ value=event.value,
88
+ on_action=event.accessory.on_action,
89
+ off_action=event.accessory.off_action,
90
+ )
91
+
92
+ self.logger.debug(f"Dispatching SendActionEvent for {event.serial_number}")
93
+ self.event_bus.dispatch(send_action)
94
+ self.logger.info(
95
+ f"Outlet set command sent successfully for {event.serial_number}"
96
+ )
97
+ return True
98
+
99
+ def handle_outlet_get_in_use(self, event: OutletGetInUseEvent) -> bool:
100
+ """Handle outlet get in-use event.
101
+
102
+ Args:
103
+ event: Outlet get in-use event.
104
+
105
+ Returns:
106
+ True if request was dispatched successfully.
107
+ """
108
+ self.logger.info(
109
+ f"Getting outlet in-use status for serial {event.serial_number}"
110
+ )
111
+ self.logger.debug(f"outlet_get_in_use {event}")
112
+
113
+ datapoint_type = DataPointType.MODULE_STATE
114
+ read_datapoint = ReadDatapointEvent(
115
+ serial_number=event.serial_number, datapoint_type=datapoint_type
116
+ )
117
+
118
+ self.logger.debug(f"Dispatching ReadDatapointEvent for {event.serial_number}")
119
+ self.event_bus.dispatch(read_datapoint)
120
+ self.logger.debug("Dispatching ReadDatapointEvent (timeout: 2s)")
121
+ return True
@@ -0,0 +1,359 @@
1
+ """HomeKit Service for Apple HomeKit integration.
2
+
3
+ This module provides the main service for HomeKit integration.
4
+ """
5
+
6
+ # Install asyncio reactor before importing reactor
7
+
8
+ import asyncio
9
+ import logging
10
+ import threading
11
+
12
+ from bubus import EventBus
13
+ from twisted.internet.posixbase import PosixReactorBase
14
+
15
+ from xp.models import ConbusClientConfig
16
+ from xp.models.protocol.conbus_protocol import (
17
+ ConnectionFailedEvent,
18
+ ConnectionLostEvent,
19
+ ConnectionMadeEvent,
20
+ LightLevelReceivedEvent,
21
+ ModuleDiscoveredEvent,
22
+ ModuleStateChangedEvent,
23
+ OutputStateReceivedEvent,
24
+ TelegramReceivedEvent,
25
+ )
26
+ from xp.services import TelegramService
27
+ from xp.services.homekit.homekit_cache_service import HomeKitCacheService
28
+ from xp.services.homekit.homekit_conbus_service import HomeKitConbusService
29
+ from xp.services.homekit.homekit_dimminglight_service import HomeKitDimmingLightService
30
+ from xp.services.homekit.homekit_hap_service import HomekitHapService
31
+ from xp.services.homekit.homekit_lightbulb_service import HomeKitLightbulbService
32
+ from xp.services.homekit.homekit_outlet_service import HomeKitOutletService
33
+ from xp.services.protocol.protocol_factory import TelegramFactory
34
+
35
+
36
+ class HomeKitService:
37
+ """Main HomeKit service for Apple HomeKit integration.
38
+
39
+ Attributes:
40
+ cli_config: Conbus client configuration.
41
+ reactor: Twisted reactor instance.
42
+ telegram_factory: Telegram factory for protocol.
43
+ protocol: Telegram protocol instance.
44
+ event_bus: Event bus for inter-service communication.
45
+ lightbulb_service: Lightbulb service instance.
46
+ dimminglight_service: Dimming light service instance.
47
+ outlet_service: Outlet service instance.
48
+ cache_service: Cache service instance.
49
+ conbus_service: Conbus service instance.
50
+ module_factory: HAP service instance.
51
+ telegram_service: Telegram service instance.
52
+ logger: Logger instance.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ cli_config: ConbusClientConfig,
58
+ event_bus: EventBus,
59
+ telegram_factory: TelegramFactory,
60
+ reactor: PosixReactorBase,
61
+ lightbulb_service: HomeKitLightbulbService,
62
+ outlet_service: HomeKitOutletService,
63
+ dimminglight_service: HomeKitDimmingLightService,
64
+ cache_service: HomeKitCacheService,
65
+ conbus_service: HomeKitConbusService,
66
+ module_factory: HomekitHapService,
67
+ telegram_service: TelegramService,
68
+ ):
69
+ """Initialize the HomeKit service.
70
+
71
+ Args:
72
+ cli_config: Conbus client configuration.
73
+ event_bus: Event bus instance.
74
+ telegram_factory: Telegram factory instance.
75
+ reactor: Twisted reactor instance.
76
+ lightbulb_service: Lightbulb service instance.
77
+ outlet_service: Outlet service instance.
78
+ dimminglight_service: Dimming light service instance.
79
+ cache_service: Cache service instance.
80
+ conbus_service: Conbus service instance.
81
+ module_factory: HAP service instance.
82
+ telegram_service: Telegram service instance.
83
+ """
84
+ self.cli_config = cli_config.conbus
85
+ self.reactor = reactor
86
+ self.telegram_factory = telegram_factory
87
+ self.protocol = telegram_factory.telegram_protocol
88
+ self.event_bus = event_bus
89
+ self.lightbulb_service = lightbulb_service
90
+ self.dimminglight_service = dimminglight_service
91
+ self.outlet_service = outlet_service
92
+ self.cache_service = cache_service
93
+ self.conbus_service = conbus_service
94
+ self.module_factory = module_factory
95
+ self.telegram_service = telegram_service
96
+ self.logger = logging.getLogger(__name__)
97
+
98
+ # Register event handlers
99
+ self.event_bus.on(ConnectionMadeEvent, self.handle_connection_made)
100
+ self.event_bus.on(ConnectionFailedEvent, self.handle_connection_failed)
101
+ self.event_bus.on(ConnectionLostEvent, self.handle_connection_lost)
102
+ self.event_bus.on(TelegramReceivedEvent, self.handle_telegram_received)
103
+ self.event_bus.on(ModuleDiscoveredEvent, self.handle_module_discovered)
104
+
105
+ def start(self) -> None:
106
+ """Start the HomeKit service."""
107
+ self.logger.info("Starting HomeKit service.")
108
+ self.logger.debug("start")
109
+
110
+ # Run reactor in its own dedicated thread
111
+ self.logger.info("Starting reactor in dedicated thread.")
112
+ reactor_thread = threading.Thread(
113
+ target=self._run_reactor_in_thread, daemon=True, name="ReactorThread"
114
+ )
115
+ reactor_thread.start()
116
+
117
+ # Keep MainThread alive while reactor thread runs
118
+ self.logger.info("Reactor thread started, MainThread waiting.")
119
+ reactor_thread.join()
120
+
121
+ def _run_reactor_in_thread(self) -> None:
122
+ """Run reactor in dedicated thread with its own event loop."""
123
+ self.logger.info("Reactor thread starting.")
124
+
125
+ # The asyncio reactor already has an event loop set up
126
+ # We just need to use it
127
+
128
+ # Connect to TCP server
129
+ self.logger.info(
130
+ f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
131
+ )
132
+ self.reactor.connectTCP(
133
+ self.cli_config.ip, self.cli_config.port, self.telegram_factory
134
+ )
135
+
136
+ # Schedule module factory to start after reactor is running
137
+ # Use callLater(0) to ensure event loop is actually running
138
+ self.reactor.callLater(0, self._start_module_factory)
139
+
140
+ # Run the reactor (which now uses asyncio underneath)
141
+ self.logger.info("Starting reactor event loop.")
142
+ self.reactor.run()
143
+
144
+ def _start_module_factory(self) -> None:
145
+ """Start module factory after reactor starts.
146
+
147
+ Creates and schedules an async task to start the HAP service.
148
+ """
149
+ self.logger.info("Starting module factory.")
150
+ self.logger.debug("callWhenRunning executed, scheduling async task")
151
+
152
+ # Run HAP-python driver asynchronously in the reactor's event loop
153
+ async def async_start() -> None:
154
+ """Start the HAP service asynchronously."""
155
+ self.logger.info("async_start executing.")
156
+ try:
157
+ await self.module_factory.async_start()
158
+ self.logger.info("Module factory started successfully")
159
+ except Exception as e:
160
+ self.logger.error(f"Error starting module factory: {e}", exc_info=True)
161
+
162
+ # Schedule on reactor's event loop (which is asyncio)
163
+ try:
164
+ task = asyncio.create_task(async_start())
165
+ self.logger.debug(f"Created module factory task: {task}")
166
+ task.add_done_callback(
167
+ lambda t: self.logger.debug(f"Module factory task completed: {t}")
168
+ )
169
+ except Exception as e:
170
+ self.logger.error(f"Error creating async task: {e}", exc_info=True)
171
+
172
+ # Event handlers
173
+ def handle_connection_made(self, event: ConnectionMadeEvent) -> None:
174
+ """Handle connection established - send initial telegram.
175
+
176
+ Args:
177
+ event: Connection made event.
178
+ """
179
+ self.logger.debug("Connection established successfully")
180
+ self.logger.debug("Sending initial discovery telegram: S0000000000F01D00")
181
+ event.protocol.sendFrame(b"S0000000000F01D00")
182
+
183
+ def handle_connection_failed(self, event: ConnectionFailedEvent) -> None:
184
+ """Handle connection failed.
185
+
186
+ Args:
187
+ event: Connection failed event.
188
+ """
189
+ self.logger.error(f"Connection failed: {event.reason}")
190
+
191
+ def handle_connection_lost(self, event: ConnectionLostEvent) -> None:
192
+ """Handle connection lost.
193
+
194
+ Args:
195
+ event: Connection lost event.
196
+ """
197
+ self.logger.warning(
198
+ f"Connection lost: {event.reason if hasattr(event, 'reason') else 'Unknown reason'}"
199
+ )
200
+
201
+ def handle_telegram_received(self, event: TelegramReceivedEvent) -> str:
202
+ """Handle received telegram events.
203
+
204
+ Args:
205
+ event: Telegram received event.
206
+
207
+ Returns:
208
+ Frame data from the event.
209
+ """
210
+ self.logger.debug(
211
+ f"handle_telegram_received ENTERED with telegram: {event.telegram}"
212
+ )
213
+
214
+ # Check if telegram is Reply (R) with Discover function (F01D)
215
+ if event.telegram_type in ("E"):
216
+ self.dispatch_event_telegram_received_event(event)
217
+ return event.frame
218
+
219
+ # Check if telegram is Reply (R) with Discover function (F01D)
220
+ if event.telegram_type in ("R") and "F01D" in event.telegram:
221
+ self.dispatch_module_discovered_event(event)
222
+ return event.frame
223
+
224
+ # Check if telegram is Reply (R) with Read Datapoint (F02) OUTPUT_STATE (D12)
225
+ if event.telegram_type in ("R") and "F02D12" in event.telegram:
226
+ self.dispatch_output_state_event(event)
227
+ return event.frame
228
+
229
+ # Check if telegram is Reply (R) with Read Datapoint (F02) LIGHT_LEVEL (D15)
230
+ if event.telegram_type in ("R") and "F02D15" in event.telegram:
231
+ self.dispatch_light_level_event(event)
232
+ return event.frame
233
+
234
+ self.logger.warning(f"Unhandled telegram received: {event.telegram}")
235
+ self.logger.info(f"telegram_received unhandled event {event}")
236
+ return event.frame
237
+
238
+ def dispatch_light_level_event(self, event: TelegramReceivedEvent) -> None:
239
+ """Dispatch light level received event.
240
+
241
+ Args:
242
+ event: Telegram received event.
243
+ """
244
+ self.logger.debug("Light level Datapoint, parsing telegram.")
245
+ reply_telegram = self.telegram_service.parse_reply_telegram(event.frame)
246
+ self.logger.debug(
247
+ f"Parsed telegram: "
248
+ f"serial={reply_telegram.serial_number}, "
249
+ f"type={reply_telegram.datapoint_type}, "
250
+ f"value={reply_telegram.data_value}"
251
+ )
252
+ self.logger.debug("About to dispatch LightLevelReceivedEvent")
253
+ self.event_bus.dispatch(
254
+ LightLevelReceivedEvent(
255
+ serial_number=reply_telegram.serial_number,
256
+ datapoint_type=reply_telegram.datapoint_type,
257
+ data_value=reply_telegram.data_value,
258
+ )
259
+ )
260
+ self.logger.debug("LightLevelReceivedEvent dispatched successfully")
261
+
262
+ def dispatch_output_state_event(self, event: TelegramReceivedEvent) -> None:
263
+ """Dispatch output state received event.
264
+
265
+ Args:
266
+ event: Telegram received event.
267
+ """
268
+ self.logger.debug("Module Read Datapoint, parsing telegram.")
269
+ reply_telegram = self.telegram_service.parse_reply_telegram(event.frame)
270
+ self.logger.debug(
271
+ f"Parsed telegram: "
272
+ f"serial={reply_telegram.serial_number}, "
273
+ f"type={reply_telegram.datapoint_type}, "
274
+ f"value={reply_telegram.data_value}"
275
+ )
276
+ self.logger.debug("About to dispatch OutputStateReceivedEvent")
277
+ self.event_bus.dispatch(
278
+ OutputStateReceivedEvent(
279
+ serial_number=reply_telegram.serial_number,
280
+ datapoint_type=reply_telegram.datapoint_type,
281
+ data_value=reply_telegram.data_value,
282
+ )
283
+ )
284
+ self.logger.debug("OutputStateReceivedEvent dispatched successfully")
285
+
286
+ def dispatch_event_telegram_received_event(
287
+ self, event: TelegramReceivedEvent
288
+ ) -> None:
289
+ """Dispatch event telegram received event.
290
+
291
+ Args:
292
+ event: Telegram received event.
293
+ """
294
+ self.logger.debug("Event telegram received, parsing.")
295
+
296
+ # Parse event telegram to extract module information
297
+ event_telegram = self.telegram_service.parse_event_telegram(event.frame)
298
+
299
+ self.logger.debug(
300
+ f"Parsed event: "
301
+ f"module_type={event_telegram.module_type}, "
302
+ f"link={event_telegram.link_number}, "
303
+ f"input={event_telegram.input_number}"
304
+ )
305
+
306
+ # Dispatch ModuleStateChangedEvent for cache refresh
307
+ self.event_bus.dispatch(
308
+ ModuleStateChangedEvent(
309
+ module_type_code=event_telegram.module_type,
310
+ link_number=event_telegram.link_number,
311
+ input_number=event_telegram.input_number,
312
+ telegram_event_type=(
313
+ event_telegram.event_type.value
314
+ if event_telegram.event_type
315
+ else "M"
316
+ ),
317
+ )
318
+ )
319
+ self.logger.debug("ModuleStateChangedEvent dispatched successfully")
320
+
321
+ def dispatch_module_discovered_event(self, event: TelegramReceivedEvent) -> None:
322
+ """Dispatch module discovered event.
323
+
324
+ Args:
325
+ event: Telegram received event.
326
+ """
327
+ self.logger.debug("Module discovered, dispatching ModuleDiscoveredEvent")
328
+ self.event_bus.dispatch(
329
+ ModuleDiscoveredEvent(
330
+ frame=event.frame,
331
+ telegram=event.telegram,
332
+ payload=event.payload,
333
+ telegram_type=event.telegram_type,
334
+ serial_number=event.serial_number,
335
+ checksum=event.checksum,
336
+ protocol=event.protocol,
337
+ )
338
+ )
339
+ self.logger.debug("ModuleDiscoveredEvent dispatched successfully")
340
+
341
+ def handle_module_discovered(self, event: ModuleDiscoveredEvent) -> str:
342
+ """Handle module discovered event.
343
+
344
+ Args:
345
+ event: Module discovered event.
346
+
347
+ Returns:
348
+ Serial number of the discovered module.
349
+ """
350
+ self.logger.debug("Handling module discovered event")
351
+
352
+ # Replace R with S and F01D with F02D00
353
+ new_telegram = event.telegram.replace("R", "S", 1).replace(
354
+ "F01D", "F02D00", 1
355
+ ) # module type
356
+
357
+ self.logger.debug(f"Sending module type request: {new_telegram}")
358
+ event.protocol.sendFrame(new_telegram.encode())
359
+ return event.serial_number