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,148 @@
1
+ """HomeKit Dimming Light Service.
2
+
3
+ This module provides service implementation for dimming light accessories.
4
+ """
5
+
6
+ import logging
7
+
8
+ from bubus import EventBus
9
+
10
+ from xp.models.protocol.conbus_protocol import (
11
+ DimmingLightGetBrightnessEvent,
12
+ DimmingLightGetOnEvent,
13
+ DimmingLightSetBrightnessEvent,
14
+ DimmingLightSetOnEvent,
15
+ ReadDatapointEvent,
16
+ SendWriteConfigEvent,
17
+ )
18
+ from xp.models.telegram.datapoint_type import DataPointType
19
+
20
+
21
+ class HomeKitDimmingLightService:
22
+ """Dimming light service for HomeKit.
23
+
24
+ Attributes:
25
+ event_bus: Event bus for inter-service communication.
26
+ logger: Logger instance.
27
+ """
28
+
29
+ event_bus: EventBus
30
+
31
+ def __init__(self, event_bus: EventBus) -> None:
32
+ """Initialize the dimming light service.
33
+
34
+ Args:
35
+ event_bus: Event bus instance.
36
+ """
37
+ self.logger = logging.getLogger(__name__)
38
+ self.event_bus = event_bus
39
+
40
+ # Register event handlers
41
+ self.event_bus.on(DimmingLightGetOnEvent, self.handle_dimminglight_get_on)
42
+ self.event_bus.on(DimmingLightSetOnEvent, self.handle_dimminglight_set_on)
43
+ self.event_bus.on(
44
+ DimmingLightSetBrightnessEvent, self.handle_dimminglight_set_brightness
45
+ )
46
+ self.event_bus.on(
47
+ DimmingLightGetBrightnessEvent, self.handle_dimminglight_get_brightness
48
+ )
49
+
50
+ def handle_dimminglight_get_on(self, event: DimmingLightGetOnEvent) -> None:
51
+ """Handle dimming light get on event.
52
+
53
+ Args:
54
+ event: Dimming light get on event.
55
+ """
56
+ self.logger.info(
57
+ f"Getting dimming light state for serial {event.serial_number}, output {event.output_number}"
58
+ )
59
+ self.logger.debug(f"dimminglight_get_on {event}")
60
+
61
+ read_datapoint = ReadDatapointEvent(
62
+ serial_number=event.serial_number,
63
+ datapoint_type=DataPointType.MODULE_OUTPUT_STATE,
64
+ )
65
+ self.logger.debug(f"Dispatching ReadDatapointEvent for {event.serial_number}")
66
+ self.event_bus.dispatch(read_datapoint)
67
+ self.logger.debug(f"Dispatched ReadDatapointEvent for {event.serial_number}")
68
+
69
+ def handle_dimminglight_set_on(self, event: DimmingLightSetOnEvent) -> None:
70
+ """Handle dimming light set on event.
71
+
72
+ Args:
73
+ event: Dimming light set on event.
74
+ """
75
+ brightness = event.brightness if event.value else 0
76
+ self.logger.debug(
77
+ f"Setting on light for "
78
+ f"serial {event.serial_number}, "
79
+ f"output {event.output_number}, "
80
+ f"event_value: {event.value}, "
81
+ f"state: {'ON' if event.value else 'OFF'}, "
82
+ f"brightness: {brightness}"
83
+ )
84
+ self.logger.debug(f"dimminglight_set_on {event}")
85
+
86
+ datapoint_type = DataPointType.MODULE_LIGHT_LEVEL
87
+ send_action = SendWriteConfigEvent(
88
+ serial_number=event.serial_number,
89
+ output_number=event.output_number,
90
+ datapoint_type=datapoint_type,
91
+ value=brightness,
92
+ )
93
+
94
+ self.logger.debug(f"Dispatching SendWriteConfigEvent for {event.serial_number}")
95
+ self.event_bus.dispatch(send_action)
96
+ self.logger.debug(f"Dispatched SendWriteConfigEvent for {event.serial_number}")
97
+
98
+ def handle_dimminglight_set_brightness(
99
+ self, event: DimmingLightSetBrightnessEvent
100
+ ) -> None:
101
+ """Handle dimming light set brightness event.
102
+
103
+ Args:
104
+ event: Dimming light set brightness event.
105
+ """
106
+ self.logger.info(
107
+ f"Setting dimming light brightness"
108
+ f"serial {event.serial_number}, "
109
+ f"output {event.output_number} "
110
+ f"to {event.brightness}"
111
+ )
112
+ self.logger.debug(f"dimminglight_set_brightness {event}")
113
+
114
+ datapoint_type = DataPointType.MODULE_LIGHT_LEVEL
115
+ send_action = SendWriteConfigEvent(
116
+ serial_number=event.serial_number,
117
+ output_number=event.output_number,
118
+ datapoint_type=datapoint_type,
119
+ value=event.brightness,
120
+ )
121
+
122
+ self.logger.debug(f"Dispatching SendWriteConfigEvent for {event.serial_number}")
123
+ self.event_bus.dispatch(send_action)
124
+ self.logger.debug(f"Dispatched SendWriteConfigEvent for {event.serial_number}")
125
+
126
+ def handle_dimminglight_get_brightness(
127
+ self, event: DimmingLightGetBrightnessEvent
128
+ ) -> None:
129
+ """Handle dimming light get brightness event.
130
+
131
+ Args:
132
+ event: Dimming light get brightness event.
133
+ """
134
+ self.logger.info(
135
+ f"Getting dimming light brightness "
136
+ f"for serial {event.serial_number}, "
137
+ f"output {event.output_number}"
138
+ )
139
+ self.logger.debug(f"dimminglight_get_brightness {event}")
140
+
141
+ datapoint_type = DataPointType.MODULE_LIGHT_LEVEL
142
+ read_datapoint = ReadDatapointEvent(
143
+ serial_number=event.serial_number, datapoint_type=datapoint_type
144
+ )
145
+
146
+ self.logger.debug(f"Dispatching ReadDatapointEvent for {event.serial_number}")
147
+ self.event_bus.dispatch(read_datapoint)
148
+ self.logger.debug(f"Dispatched ReadDatapointEvent for {event.serial_number}")
@@ -0,0 +1,342 @@
1
+ """HomeKit HAP Service for Apple HomeKit integration.
2
+
3
+ This module provides the main HAP (HomeKit Accessory Protocol) service.
4
+ """
5
+
6
+ import logging
7
+ import signal
8
+ import threading
9
+ from datetime import datetime
10
+ from typing import Dict, List, Optional
11
+
12
+ from bubus import EventBus
13
+ from pyhap.accessory import Bridge
14
+ from pyhap.accessory_driver import AccessoryDriver
15
+ from typing_extensions import Union
16
+
17
+ import xp
18
+ from xp.models.homekit.homekit_accessory import TemperatureSensor
19
+ from xp.models.homekit.homekit_config import (
20
+ HomekitAccessoryConfig,
21
+ HomekitConfig,
22
+ RoomConfig,
23
+ )
24
+ from xp.models.protocol.conbus_protocol import (
25
+ LightLevelReceivedEvent,
26
+ ModuleStateChangedEvent,
27
+ OutputStateReceivedEvent,
28
+ ReadDatapointEvent,
29
+ )
30
+ from xp.models.telegram.datapoint_type import DataPointType
31
+ from xp.services.homekit.homekit_dimminglight import DimmingLight
32
+ from xp.services.homekit.homekit_lightbulb import LightBulb
33
+ from xp.services.homekit.homekit_module_service import HomekitModuleService
34
+ from xp.services.homekit.homekit_outlet import Outlet
35
+
36
+
37
+ class HomekitHapService:
38
+ """HomeKit HAP service.
39
+
40
+ Manages HAP accessory protocol, handles bridge and accessory setup,
41
+ and processes HomeKit events for device state synchronization.
42
+
43
+ Attributes:
44
+ event_bus: Event bus for inter-service communication.
45
+ last_activity: Timestamp of last service activity.
46
+ logger: Logger instance.
47
+ config: HomeKit configuration.
48
+ accessory_registry: Registry of accessories by identifier.
49
+ module_registry: Registry of accessories by module key.
50
+ modules: Module service for module lookup.
51
+ driver: HAP accessory driver.
52
+ """
53
+
54
+ event_bus: EventBus
55
+
56
+ def __init__(
57
+ self,
58
+ homekit_config: HomekitConfig,
59
+ module_service: HomekitModuleService,
60
+ event_bus: EventBus,
61
+ ):
62
+ """Initialize the HomeKit HAP service.
63
+
64
+ Args:
65
+ homekit_config: HomeKit configuration.
66
+ module_service: Module service for dependency injection.
67
+ event_bus: Event bus for dependency injection.
68
+ """
69
+ self.last_activity: Optional[datetime] = None
70
+
71
+ # Set up logging
72
+ self.logger = logging.getLogger(__name__)
73
+
74
+ # Load configuration
75
+ self.config = homekit_config
76
+ self.accessory_registry: Dict[str, Union[LightBulb, Outlet, DimmingLight]] = {}
77
+ self.module_registry: Dict[
78
+ tuple[int, int], List[Union[LightBulb, Outlet, DimmingLight]]
79
+ ] = {}
80
+
81
+ # Service dependencies
82
+ self.modules = module_service
83
+ self.event_bus = event_bus
84
+
85
+ # Subscribe to events
86
+ self.event_bus.on(ModuleStateChangedEvent, self.handle_module_state_changed)
87
+ self.event_bus.on(OutputStateReceivedEvent, self.handle_output_state_received)
88
+ self.event_bus.on(LightLevelReceivedEvent, self.handle_light_level_received)
89
+
90
+ # We want SIGTERM (terminate) to be handled by the driver itself,
91
+ # so that it can gracefully stop the accessory, server and advertising.
92
+ driver = AccessoryDriver(
93
+ port=self.config.homekit.port,
94
+ )
95
+ signal.signal(signal.SIGTERM, driver.signal_handler)
96
+ self.driver: AccessoryDriver = driver
97
+
98
+ async def async_start(self) -> None:
99
+ """Start the HAP service asynchronously."""
100
+ self.logger.info("Loading accessories.")
101
+ self.build_bridge()
102
+ self.logger.info("Accessories loaded successfully")
103
+
104
+ # Start HAP-python in a separate thread to avoid event loop conflicts
105
+ self.logger.info("Starting HAP-python driver in separate thread.")
106
+ hap_thread = threading.Thread(
107
+ target=self._run_driver_in_thread, daemon=True, name="HAP-Python"
108
+ )
109
+ hap_thread.start()
110
+ self.logger.info("HAP-python driver thread started")
111
+
112
+ def _run_driver_in_thread(self) -> None:
113
+ """Run the HAP-python driver in a separate thread with its own event loop."""
114
+ try:
115
+ self.logger.info("HAP-python thread starting, creating new event loop.")
116
+ # Create a new event loop for this thread
117
+
118
+ self.logger.info("Starting HAP-python driver.")
119
+ self.driver.start()
120
+ self.logger.info("HAP-python driver started successfully")
121
+ except Exception as e:
122
+ self.logger.error(f"HAP-python driver error: {e}", exc_info=True)
123
+
124
+ def handle_output_state_received(self, event: OutputStateReceivedEvent) -> str:
125
+ """Handle output state received event.
126
+
127
+ Args:
128
+ event: Output state received event.
129
+
130
+ Returns:
131
+ Data value from the event.
132
+ """
133
+ self.logger.debug(f"Received OutputStateReceivedEvent {event}")
134
+ output_number = 0
135
+ for output in event.data_value[::-1]:
136
+ if output == "x":
137
+ break
138
+ identifier = f"{event.serial_number}.{output_number:02X}"
139
+ accessory = self.accessory_registry.get(identifier)
140
+
141
+ if not accessory:
142
+ self.logger.warning(f"Invalid accessory: {identifier} (not found)")
143
+ else:
144
+ accessory.is_on = True if output == "1" else False
145
+ output_number += 1
146
+
147
+ self.logger.debug(
148
+ f"handle_output_state_received "
149
+ f"serial_number: {event.serial_number}, "
150
+ f"data_vale: {event.data_value}"
151
+ )
152
+ return event.data_value
153
+
154
+ def handle_light_level_received(self, event: LightLevelReceivedEvent) -> str:
155
+ """Handle light level received event.
156
+
157
+ Args:
158
+ event: Light level received event.
159
+
160
+ Returns:
161
+ Data value from the event.
162
+ """
163
+ # Parse response format like "00:050,01:025,02:100"
164
+ self.logger.debug("Received LightLevelReceivedEvent", extra={"event": event})
165
+ output_number = 0
166
+ for output_data in event.data_value.split(","):
167
+ if ":" in output_data:
168
+ output_str, level_str = output_data.split(":")
169
+ level_str = level_str.replace("[%]", "")
170
+ output_number = int(output_str)
171
+ brightness = int(level_str)
172
+ identifier = f"{event.serial_number}.{output_number:02X}"
173
+ accessory = self.accessory_registry.get(identifier)
174
+
175
+ if not accessory:
176
+ self.logger.warning(
177
+ f"Invalid accessory: {event.serial_number} (not found)"
178
+ )
179
+ elif not isinstance(accessory, DimmingLight):
180
+ self.logger.warning(
181
+ f"Invalid accessory: {event.serial_number} (not dimming light)"
182
+ )
183
+ else:
184
+ accessory.brightness = brightness
185
+
186
+ output_number += 1
187
+
188
+ self.logger.debug(
189
+ f"handle_light_level_received "
190
+ f"serial_number: {event.serial_number}, "
191
+ f"data_vale: {event.data_value}"
192
+ )
193
+ return event.data_value
194
+
195
+ def build_bridge(self) -> None:
196
+ """Build the HomeKit bridge with all configured accessories."""
197
+ bridge_config = self.config.bridge
198
+ bridge = Bridge(self.driver, bridge_config.name)
199
+ bridge.set_info_service(
200
+ xp.__version__, xp.__manufacturer__, xp.__model__, xp.__serial__
201
+ )
202
+
203
+ for room in bridge_config.rooms:
204
+ self.add_room(bridge, room)
205
+
206
+ self.driver.add_accessory(accessory=bridge)
207
+
208
+ def add_room(self, bridge: Bridge, room: RoomConfig) -> None:
209
+ """Add a room with its accessories to the bridge.
210
+
211
+ Args:
212
+ bridge: HAP bridge instance.
213
+ room: Room configuration.
214
+ """
215
+ temperature = TemperatureSensor(self.driver, room.name)
216
+ bridge.add_accessory(temperature)
217
+
218
+ for accessory_name in room.accessories:
219
+ homekit_accessory = self.get_accessory_by_name(accessory_name)
220
+ if homekit_accessory is None:
221
+ self.logger.warning("Accessory '{}' not found".format(accessory_name))
222
+ continue
223
+
224
+ accessory = self.get_accessory(homekit_accessory)
225
+ if accessory:
226
+ bridge.add_accessory(accessory)
227
+ # Add to accessory_registry
228
+ self.accessory_registry[accessory.identifier] = accessory
229
+
230
+ # Add to module_registry for event-driven lookup
231
+ module_key = (
232
+ accessory.module.module_type_code,
233
+ accessory.module.link_number,
234
+ )
235
+ if module_key not in self.module_registry:
236
+ self.module_registry[module_key] = []
237
+ self.module_registry[module_key].append(accessory)
238
+
239
+ def get_accessory(
240
+ self, homekit_accessory: HomekitAccessoryConfig
241
+ ) -> Union[LightBulb, Outlet, DimmingLight, None]:
242
+ """Get an accessory instance from configuration.
243
+
244
+ Args:
245
+ homekit_accessory: HomeKit accessory configuration.
246
+
247
+ Returns:
248
+ Accessory instance or None if not found or invalid service type.
249
+ """
250
+ module_config = self.modules.get_module_by_serial(
251
+ homekit_accessory.serial_number
252
+ )
253
+ if module_config is None:
254
+ self.logger.warning(f"Accessory '{homekit_accessory.name}' not found")
255
+ return None
256
+
257
+ if homekit_accessory.service == "lightbulb":
258
+ return LightBulb(
259
+ driver=self.driver,
260
+ module=module_config,
261
+ accessory=homekit_accessory,
262
+ event_bus=self.event_bus,
263
+ )
264
+
265
+ if homekit_accessory.service == "outlet":
266
+ return Outlet(
267
+ driver=self.driver,
268
+ module=module_config,
269
+ accessory=homekit_accessory,
270
+ event_bus=self.event_bus,
271
+ )
272
+
273
+ if homekit_accessory.service == "dimminglight":
274
+ return DimmingLight(
275
+ driver=self.driver,
276
+ module=module_config,
277
+ accessory=homekit_accessory,
278
+ event_bus=self.event_bus,
279
+ )
280
+
281
+ self.logger.warning(f"Accessory '{homekit_accessory.name}' not found")
282
+ return None
283
+
284
+ def get_accessory_by_name(self, name: str) -> Optional[HomekitAccessoryConfig]:
285
+ """Get an accessory configuration by name.
286
+
287
+ Args:
288
+ name: Name of the accessory to find.
289
+
290
+ Returns:
291
+ Accessory configuration if found, None otherwise.
292
+ """
293
+ return next(
294
+ (module for module in self.config.accessories if module.name == name), None
295
+ )
296
+
297
+ def handle_module_state_changed(self, event: ModuleStateChangedEvent) -> None:
298
+ """Handle module state change by refreshing affected accessories.
299
+
300
+ Args:
301
+ event: Module state changed event.
302
+ """
303
+ self.logger.debug(
304
+ f"Module state changed: module_type={event.module_type_code}, "
305
+ f"link={event.link_number}, input={event.input_number}"
306
+ )
307
+
308
+ # O(1) lookup using module_registry
309
+ module_key = (event.module_type_code, event.link_number)
310
+ affected_accessories = self.module_registry.get(module_key, [])
311
+
312
+ if not affected_accessories:
313
+ self.logger.debug(
314
+ f"No accessories found for module_type={event.module_type_code}, "
315
+ f"link={event.link_number}"
316
+ )
317
+ return
318
+
319
+ # Request cache refresh for each affected accessory
320
+ for accessory in affected_accessories:
321
+ self.logger.info(
322
+ f"Requesting cache refresh for accessory: {accessory.identifier}"
323
+ )
324
+
325
+ # Request OUTPUT_STATE refresh
326
+ self.event_bus.dispatch(
327
+ ReadDatapointEvent(
328
+ serial_number=accessory.module.serial_number,
329
+ datapoint_type=DataPointType.MODULE_OUTPUT_STATE,
330
+ refresh_cache=True,
331
+ )
332
+ )
333
+
334
+ # If dimming light, also refresh LIGHT_LEVEL
335
+ if isinstance(accessory, DimmingLight):
336
+ self.event_bus.dispatch(
337
+ ReadDatapointEvent(
338
+ serial_number=accessory.module.serial_number,
339
+ datapoint_type=DataPointType.MODULE_LIGHT_LEVEL,
340
+ refresh_cache=True,
341
+ )
342
+ )
@@ -0,0 +1,120 @@
1
+ """HomeKit Light Bulb Accessory.
2
+
3
+ This module provides a light bulb 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_LIGHTBULB
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
+ LightBulbGetOnEvent,
17
+ LightBulbSetOnEvent,
18
+ )
19
+
20
+
21
+ class LightBulb(Accessory):
22
+ """HomeKit light bulb accessory.
23
+
24
+ Attributes:
25
+ category: HomeKit category (CATEGORY_LIGHTBULB).
26
+ event_bus: Event bus for inter-service communication.
27
+ logger: Logger instance.
28
+ identifier: Unique identifier for the accessory.
29
+ accessory: Accessory configuration.
30
+ module: Module configuration.
31
+ is_on: Current on/off state.
32
+ char_on: On characteristic.
33
+ """
34
+
35
+ category = CATEGORY_LIGHTBULB
36
+ event_bus: EventBus
37
+
38
+ def __init__(
39
+ self,
40
+ driver: AccessoryDriver,
41
+ module: ConsonModuleConfig,
42
+ accessory: HomekitAccessoryConfig,
43
+ event_bus: EventBus,
44
+ ):
45
+ """Initialize the light bulb accessory.
46
+
47
+ Args:
48
+ driver: HAP accessory driver.
49
+ module: Module configuration.
50
+ accessory: Accessory configuration.
51
+ event_bus: Event bus for inter-service communication.
52
+ """
53
+ super().__init__(driver, accessory.description)
54
+
55
+ self.logger = logging.getLogger(__name__)
56
+
57
+ identifier = f"{module.serial_number}.{accessory.output_number:02d}"
58
+ version = accessory.id
59
+ manufacturer = "Conson"
60
+ model = ("XP24_lightbulb",)
61
+
62
+ self.identifier = identifier
63
+ self.accessory = accessory
64
+ self.module = module
65
+ self.event_bus = event_bus
66
+ self.is_on = False
67
+
68
+ self.logger.info(
69
+ "Creating Lightbulb { serial_number : %s, output_number: %s }",
70
+ module.serial_number,
71
+ accessory.output_number,
72
+ )
73
+
74
+ serv_light = self.add_preload_service("Lightbulb")
75
+
76
+ self.set_info_service(version, manufacturer, model, identifier)
77
+
78
+ self.char_on = serv_light.configure_char(
79
+ "On", getter_callback=self.get_on, setter_callback=self.set_on
80
+ )
81
+
82
+ def set_on(self, value: bool) -> None:
83
+ """Set the on/off state of the light bulb.
84
+
85
+ Args:
86
+ value: True to turn on, False to turn off.
87
+ """
88
+ # Emit set event
89
+ self.logger.debug(f"set_on {value}")
90
+ if self.is_on != value:
91
+ self.is_on = value
92
+ self.event_bus.dispatch(
93
+ LightBulbSetOnEvent(
94
+ serial_number=self.accessory.serial_number,
95
+ output_number=self.accessory.output_number,
96
+ module=self.module,
97
+ accessory=self.accessory,
98
+ value=value,
99
+ )
100
+ )
101
+
102
+ def get_on(self) -> bool:
103
+ """Get the on/off state of the light bulb.
104
+
105
+ Returns:
106
+ True if on, False if off.
107
+ """
108
+ # Emit event and get response
109
+ self.logger.debug("get_on")
110
+ self.event_bus.dispatch(
111
+ LightBulbGetOnEvent(
112
+ serial_number=self.accessory.serial_number,
113
+ output_number=self.accessory.output_number,
114
+ module=self.module,
115
+ accessory=self.accessory,
116
+ )
117
+ )
118
+ self.logger.debug(f"get_on from dispatch: {self.is_on}")
119
+
120
+ return self.is_on
@@ -0,0 +1,86 @@
1
+ """HomeKit Light Bulb Service.
2
+
3
+ This module provides service implementation for light bulb accessories.
4
+ """
5
+
6
+ import logging
7
+
8
+ from bubus import EventBus
9
+
10
+ from xp.models.protocol.conbus_protocol import (
11
+ LightBulbGetOnEvent,
12
+ LightBulbSetOnEvent,
13
+ ReadDatapointEvent,
14
+ SendActionEvent,
15
+ )
16
+ from xp.models.telegram.datapoint_type import DataPointType
17
+
18
+
19
+ class HomeKitLightbulbService:
20
+ """Lightbulb service for HomeKit.
21
+
22
+ Attributes:
23
+ event_bus: Event bus for inter-service communication.
24
+ logger: Logger instance.
25
+ """
26
+
27
+ event_bus: EventBus
28
+
29
+ def __init__(self, event_bus: EventBus):
30
+ """Initialize the lightbulb service.
31
+
32
+ Args:
33
+ event_bus: Event bus instance.
34
+ """
35
+ self.event_bus = event_bus
36
+ self.logger = logging.getLogger(__name__)
37
+
38
+ # Register event handlers
39
+ self.event_bus.on(LightBulbGetOnEvent, self.handle_lightbulb_get_on)
40
+ self.event_bus.on(LightBulbSetOnEvent, self.handle_lightbulb_set_on)
41
+
42
+ def handle_lightbulb_get_on(self, event: LightBulbGetOnEvent) -> None:
43
+ """Handle lightbulb get on event.
44
+
45
+ Args:
46
+ event: Lightbulb get on event.
47
+ """
48
+ self.logger.info(
49
+ f"Getting lightbulb state for serial {event.serial_number}, output {event.output_number}"
50
+ )
51
+ self.logger.debug(f"lightbulb_get_on {event}")
52
+
53
+ datapoint_type = DataPointType.MODULE_OUTPUT_STATE
54
+ read_datapoint = ReadDatapointEvent(
55
+ serial_number=event.serial_number, datapoint_type=datapoint_type
56
+ )
57
+
58
+ self.logger.debug(f"Dispatching ReadDatapointEvent for {event.serial_number}")
59
+ self.event_bus.dispatch(read_datapoint)
60
+ self.logger.debug(f"Dispatched ReadDatapointEvent for {event.serial_number}")
61
+
62
+ def handle_lightbulb_set_on(self, event: LightBulbSetOnEvent) -> None:
63
+ """Handle lightbulb set on event.
64
+
65
+ Args:
66
+ event: Lightbulb set on event.
67
+ """
68
+ self.logger.info(
69
+ f"Setting lightbulb "
70
+ f"for serial {event.serial_number}, "
71
+ f"output {event.output_number} "
72
+ f"to {'ON' if event.value else 'OFF'}"
73
+ )
74
+ self.logger.debug(f"lightbulb_set_on {event}")
75
+
76
+ send_action = SendActionEvent(
77
+ serial_number=event.serial_number,
78
+ output_number=event.output_number,
79
+ value=event.value,
80
+ on_action=event.accessory.on_action,
81
+ off_action=event.accessory.off_action,
82
+ )
83
+
84
+ self.logger.debug(f"Dispatching SendActionEvent for {event.serial_number}")
85
+ self.event_bus.dispatch(send_action)
86
+ self.logger.debug(f"Dispatched SendActionEvent for {event.serial_number}")