conson-xp 1.52.0__py3-none-any.whl → 2.0.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 (38) hide show
  1. {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/METADATA +1 -11
  2. {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/RECORD +19 -38
  3. xp/__init__.py +1 -1
  4. xp/cli/commands/__init__.py +0 -4
  5. xp/cli/commands/term/term_commands.py +1 -1
  6. xp/cli/main.py +0 -3
  7. xp/models/protocol/conbus_protocol.py +30 -25
  8. xp/models/term/accessory_state.py +1 -1
  9. xp/services/protocol/__init__.py +2 -3
  10. xp/services/protocol/conbus_event_protocol.py +5 -5
  11. xp/services/term/homekit_accessory_driver.py +5 -2
  12. xp/services/term/homekit_service.py +118 -11
  13. xp/term/homekit.py +140 -8
  14. xp/term/homekit.tcss +4 -4
  15. xp/term/widgets/room_list.py +61 -3
  16. xp/utils/dependencies.py +24 -154
  17. xp/cli/commands/homekit/__init__.py +0 -3
  18. xp/cli/commands/homekit/homekit.py +0 -120
  19. xp/cli/commands/homekit/homekit_start_commands.py +0 -44
  20. xp/services/homekit/__init__.py +0 -1
  21. xp/services/homekit/homekit_cache_service.py +0 -313
  22. xp/services/homekit/homekit_conbus_service.py +0 -99
  23. xp/services/homekit/homekit_config_validator.py +0 -327
  24. xp/services/homekit/homekit_conson_validator.py +0 -130
  25. xp/services/homekit/homekit_dimminglight.py +0 -189
  26. xp/services/homekit/homekit_dimminglight_service.py +0 -155
  27. xp/services/homekit/homekit_hap_service.py +0 -351
  28. xp/services/homekit/homekit_lightbulb.py +0 -125
  29. xp/services/homekit/homekit_lightbulb_service.py +0 -91
  30. xp/services/homekit/homekit_module_service.py +0 -60
  31. xp/services/homekit/homekit_outlet.py +0 -175
  32. xp/services/homekit/homekit_outlet_service.py +0 -127
  33. xp/services/homekit/homekit_service.py +0 -371
  34. xp/services/protocol/protocol_factory.py +0 -84
  35. xp/services/protocol/telegram_protocol.py +0 -270
  36. {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/WHEEL +0 -0
  37. {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/entry_points.txt +0 -0
  38. {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,371 +0,0 @@
1
- """
2
- HomeKit Service for Apple HomeKit integration.
3
-
4
- This module provides the main service for HomeKit integration.
5
- """
6
-
7
- # Install asyncio reactor before importing reactor
8
-
9
- import asyncio
10
- import logging
11
- import threading
12
-
13
- from bubus import EventBus
14
- from twisted.internet.posixbase import PosixReactorBase
15
-
16
- from xp.models import ConbusClientConfig
17
- from xp.models.protocol.conbus_protocol import (
18
- ConnectionFailedEvent,
19
- ConnectionLostEvent,
20
- ConnectionMadeEvent,
21
- LightLevelReceivedEvent,
22
- ModuleDiscoveredEvent,
23
- ModuleStateChangedEvent,
24
- OutputStateReceivedEvent,
25
- TelegramReceivedEvent,
26
- )
27
- from xp.services import TelegramService
28
- from xp.services.homekit.homekit_cache_service import HomeKitCacheService
29
- from xp.services.homekit.homekit_conbus_service import HomeKitConbusService
30
- from xp.services.homekit.homekit_dimminglight_service import HomeKitDimmingLightService
31
- from xp.services.homekit.homekit_hap_service import HomekitHapService
32
- from xp.services.homekit.homekit_lightbulb_service import HomeKitLightbulbService
33
- from xp.services.homekit.homekit_outlet_service import HomeKitOutletService
34
- from xp.services.protocol.protocol_factory import TelegramFactory
35
-
36
-
37
- class HomeKitService:
38
- """
39
- Main HomeKit service for Apple HomeKit integration.
40
-
41
- Attributes:
42
- cli_config: Conbus client configuration.
43
- reactor: Twisted reactor instance.
44
- telegram_factory: Telegram factory for protocol.
45
- protocol: Telegram protocol instance.
46
- event_bus: Event bus for inter-service communication.
47
- lightbulb_service: Lightbulb service instance.
48
- dimminglight_service: Dimming light service instance.
49
- outlet_service: Outlet service instance.
50
- cache_service: Cache service instance.
51
- conbus_service: Conbus service instance.
52
- module_factory: HAP service instance.
53
- telegram_service: Telegram service instance.
54
- logger: Logger instance.
55
- """
56
-
57
- def __init__(
58
- self,
59
- cli_config: ConbusClientConfig,
60
- event_bus: EventBus,
61
- telegram_factory: TelegramFactory,
62
- reactor: PosixReactorBase,
63
- lightbulb_service: HomeKitLightbulbService,
64
- outlet_service: HomeKitOutletService,
65
- dimminglight_service: HomeKitDimmingLightService,
66
- cache_service: HomeKitCacheService,
67
- conbus_service: HomeKitConbusService,
68
- module_factory: HomekitHapService,
69
- telegram_service: TelegramService,
70
- ):
71
- """
72
- Initialize the HomeKit service.
73
-
74
- Args:
75
- cli_config: Conbus client configuration.
76
- event_bus: Event bus instance.
77
- telegram_factory: Telegram factory instance.
78
- reactor: Twisted reactor instance.
79
- lightbulb_service: Lightbulb service instance.
80
- outlet_service: Outlet service instance.
81
- dimminglight_service: Dimming light service instance.
82
- cache_service: Cache service instance.
83
- conbus_service: Conbus service instance.
84
- module_factory: HAP service instance.
85
- telegram_service: Telegram service instance.
86
- """
87
- self.cli_config = cli_config.conbus
88
- self.reactor = reactor
89
- self.telegram_factory = telegram_factory
90
- self.protocol = telegram_factory.telegram_protocol
91
- self.event_bus = event_bus
92
- self.lightbulb_service = lightbulb_service
93
- self.dimminglight_service = dimminglight_service
94
- self.outlet_service = outlet_service
95
- self.cache_service = cache_service
96
- self.conbus_service = conbus_service
97
- self.module_factory = module_factory
98
- self.telegram_service = telegram_service
99
- self.logger = logging.getLogger(__name__)
100
-
101
- # Register event handlers
102
- self.event_bus.on(ConnectionMadeEvent, self.handle_connection_made)
103
- self.event_bus.on(ConnectionFailedEvent, self.handle_connection_failed)
104
- self.event_bus.on(ConnectionLostEvent, self.handle_connection_lost)
105
- self.event_bus.on(TelegramReceivedEvent, self.handle_telegram_received)
106
- self.event_bus.on(ModuleDiscoveredEvent, self.handle_module_discovered)
107
-
108
- def start(self) -> None:
109
- """Start the HomeKit service."""
110
- self.logger.info("Starting HomeKit service.")
111
- self.logger.debug("start")
112
-
113
- # Run reactor in its own dedicated thread
114
- self.logger.info("Starting reactor in dedicated thread.")
115
- reactor_thread = threading.Thread(
116
- target=self._run_reactor_in_thread, daemon=True, name="ReactorThread"
117
- )
118
- reactor_thread.start()
119
-
120
- # Keep MainThread alive while reactor thread runs
121
- self.logger.info("Reactor thread started, MainThread waiting.")
122
- reactor_thread.join()
123
-
124
- def _run_reactor_in_thread(self) -> None:
125
- """Run reactor in dedicated thread with its own event loop."""
126
- self.logger.info("Reactor thread starting.")
127
-
128
- # The asyncio reactor already has an event loop set up
129
- # We just need to use it
130
-
131
- # Connect to TCP server
132
- self.logger.info(
133
- f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
134
- )
135
- self.reactor.connectTCP(
136
- self.cli_config.ip, self.cli_config.port, self.telegram_factory
137
- )
138
-
139
- # Schedule module factory to start after reactor is running
140
- # Use callLater(0) to ensure event loop is actually running
141
- self.reactor.callLater(0, self._start_module_factory)
142
-
143
- # Run the reactor (which now uses asyncio underneath)
144
- self.logger.info("Starting reactor event loop.")
145
- self.reactor.run()
146
-
147
- def _start_module_factory(self) -> None:
148
- """
149
- Start module factory after reactor starts.
150
-
151
- Creates and schedules an async task to start the HAP service.
152
- """
153
- self.logger.info("Starting module factory.")
154
- self.logger.debug("callWhenRunning executed, scheduling async task")
155
-
156
- # Run HAP-python driver asynchronously in the reactor's event loop
157
- async def async_start() -> None:
158
- """Start the HAP service asynchronously."""
159
- self.logger.info("async_start executing.")
160
- try:
161
- await self.module_factory.async_start()
162
- self.logger.info("Module factory started successfully")
163
- except Exception as e:
164
- self.logger.error(f"Error starting module factory: {e}", exc_info=True)
165
-
166
- # Schedule on reactor's event loop (which is asyncio)
167
- try:
168
- task = asyncio.create_task(async_start())
169
- self.logger.debug(f"Created module factory task: {task}")
170
- task.add_done_callback(
171
- lambda t: self.logger.debug(f"Module factory task completed: {t}")
172
- )
173
- except Exception as e:
174
- self.logger.error(f"Error creating async task: {e}", exc_info=True)
175
-
176
- # Event handlers
177
- def handle_connection_made(self, event: ConnectionMadeEvent) -> None:
178
- """Handle connection established - send initial telegram.
179
-
180
- Args:
181
- event: Connection made event.
182
- """
183
- self.logger.debug("Connection established successfully")
184
- self.logger.debug("Sending initial discovery telegram: S0000000000F01D00")
185
- event.protocol.sendFrame(b"S0000000000F01D00")
186
-
187
- def handle_connection_failed(self, event: ConnectionFailedEvent) -> None:
188
- """
189
- Handle connection failed.
190
-
191
- Args:
192
- event: Connection failed event.
193
- """
194
- self.logger.error(f"Connection failed: {event.reason}")
195
-
196
- def handle_connection_lost(self, event: ConnectionLostEvent) -> None:
197
- """
198
- Handle connection lost.
199
-
200
- Args:
201
- event: Connection lost event.
202
- """
203
- self.logger.warning(
204
- f"Connection lost: {event.reason if hasattr(event, 'reason') else 'Unknown reason'}"
205
- )
206
-
207
- def handle_telegram_received(self, event: TelegramReceivedEvent) -> str:
208
- """
209
- Handle received telegram events.
210
-
211
- Args:
212
- event: Telegram received event.
213
-
214
- Returns:
215
- Frame data from the event.
216
- """
217
- self.logger.debug(
218
- f"handle_telegram_received ENTERED with telegram: {event.telegram}"
219
- )
220
-
221
- # Check if telegram is Reply (R) with Discover function (F01D)
222
- if event.telegram_type in ("E"):
223
- self.dispatch_event_telegram_received_event(event)
224
- return event.frame
225
-
226
- # Check if telegram is Reply (R) with Discover function (F01D)
227
- if event.telegram_type in ("R") and "F01D" in event.telegram:
228
- self.dispatch_module_discovered_event(event)
229
- return event.frame
230
-
231
- # Check if telegram is Reply (R) with Read Datapoint (F02) OUTPUT_STATE (D12)
232
- if event.telegram_type in ("R") and "F02D12" in event.telegram:
233
- self.dispatch_output_state_event(event)
234
- return event.frame
235
-
236
- # Check if telegram is Reply (R) with Read Datapoint (F02) LIGHT_LEVEL (D15)
237
- if event.telegram_type in ("R") and "F02D15" in event.telegram:
238
- self.dispatch_light_level_event(event)
239
- return event.frame
240
-
241
- self.logger.warning(f"Unhandled telegram received: {event.telegram}")
242
- self.logger.info(f"telegram_received unhandled event {event}")
243
- return event.frame
244
-
245
- def dispatch_light_level_event(self, event: TelegramReceivedEvent) -> None:
246
- """
247
- Dispatch light level received event.
248
-
249
- Args:
250
- event: Telegram received event.
251
- """
252
- self.logger.debug("Light level Datapoint, parsing telegram.")
253
- reply_telegram = self.telegram_service.parse_reply_telegram(event.frame)
254
- self.logger.debug(
255
- f"Parsed telegram: "
256
- f"serial={reply_telegram.serial_number}, "
257
- f"type={reply_telegram.datapoint_type}, "
258
- f"value={reply_telegram.data_value}"
259
- )
260
- self.logger.debug("About to dispatch LightLevelReceivedEvent")
261
- self.event_bus.dispatch(
262
- LightLevelReceivedEvent(
263
- serial_number=reply_telegram.serial_number,
264
- datapoint_type=reply_telegram.datapoint_type,
265
- data_value=reply_telegram.data_value,
266
- )
267
- )
268
- self.logger.debug("LightLevelReceivedEvent dispatched successfully")
269
-
270
- def dispatch_output_state_event(self, event: TelegramReceivedEvent) -> None:
271
- """
272
- Dispatch output state received event.
273
-
274
- Args:
275
- event: Telegram received event.
276
- """
277
- self.logger.debug("Module Read Datapoint, parsing telegram.")
278
- reply_telegram = self.telegram_service.parse_reply_telegram(event.frame)
279
- self.logger.debug(
280
- f"Parsed telegram: "
281
- f"serial={reply_telegram.serial_number}, "
282
- f"type={reply_telegram.datapoint_type}, "
283
- f"value={reply_telegram.data_value}"
284
- )
285
- self.logger.debug("About to dispatch OutputStateReceivedEvent")
286
- self.event_bus.dispatch(
287
- OutputStateReceivedEvent(
288
- serial_number=reply_telegram.serial_number,
289
- datapoint_type=reply_telegram.datapoint_type,
290
- data_value=reply_telegram.data_value,
291
- )
292
- )
293
- self.logger.debug("OutputStateReceivedEvent dispatched successfully")
294
-
295
- def dispatch_event_telegram_received_event(
296
- self, event: TelegramReceivedEvent
297
- ) -> None:
298
- """
299
- Dispatch event telegram received event.
300
-
301
- Args:
302
- event: Telegram received event.
303
- """
304
- self.logger.debug("Event telegram received, parsing.")
305
-
306
- # Parse event telegram to extract module information
307
- event_telegram = self.telegram_service.parse_event_telegram(event.frame)
308
-
309
- self.logger.debug(
310
- f"Parsed event: "
311
- f"module_type={event_telegram.module_type}, "
312
- f"link={event_telegram.link_number}, "
313
- f"input={event_telegram.input_number}"
314
- )
315
-
316
- # Dispatch ModuleStateChangedEvent for cache refresh
317
- self.event_bus.dispatch(
318
- ModuleStateChangedEvent(
319
- module_type_code=event_telegram.module_type,
320
- link_number=event_telegram.link_number,
321
- input_number=event_telegram.input_number,
322
- telegram_event_type=(
323
- event_telegram.event_type.value
324
- if event_telegram.event_type
325
- else "M"
326
- ),
327
- )
328
- )
329
- self.logger.debug("ModuleStateChangedEvent dispatched successfully")
330
-
331
- def dispatch_module_discovered_event(self, event: TelegramReceivedEvent) -> None:
332
- """
333
- Dispatch module discovered event.
334
-
335
- Args:
336
- event: Telegram received event.
337
- """
338
- self.logger.debug("Module discovered, dispatching ModuleDiscoveredEvent")
339
- self.event_bus.dispatch(
340
- ModuleDiscoveredEvent(
341
- frame=event.frame,
342
- telegram=event.telegram,
343
- payload=event.payload,
344
- telegram_type=event.telegram_type,
345
- serial_number=event.serial_number,
346
- checksum=event.checksum,
347
- protocol=event.protocol,
348
- )
349
- )
350
- self.logger.debug("ModuleDiscoveredEvent dispatched successfully")
351
-
352
- def handle_module_discovered(self, event: ModuleDiscoveredEvent) -> str:
353
- """
354
- Handle module discovered event.
355
-
356
- Args:
357
- event: Module discovered event.
358
-
359
- Returns:
360
- Serial number of the discovered module.
361
- """
362
- self.logger.debug("Handling module discovered event")
363
-
364
- # Replace R with S and F01D with F02D00
365
- new_telegram = event.telegram.replace("R", "S", 1).replace(
366
- "F01D", "F02D00", 1
367
- ) # module type
368
-
369
- self.logger.debug(f"Sending module type request: {new_telegram}")
370
- event.protocol.sendFrame(new_telegram.encode())
371
- return event.serial_number
@@ -1,84 +0,0 @@
1
- """
2
- Protocol Factory for Twisted protocol creation.
3
-
4
- This module provides factory classes for protocol instantiation.
5
- """
6
-
7
- import logging
8
-
9
- from bubus import EventBus
10
- from twisted.internet import protocol
11
- from twisted.internet.interfaces import IAddress, IConnector
12
- from twisted.python.failure import Failure
13
-
14
- from xp.models.protocol.conbus_protocol import (
15
- ConnectionFailedEvent,
16
- ConnectionLostEvent,
17
- )
18
- from xp.services.protocol import TelegramProtocol
19
-
20
-
21
- class TelegramFactory(protocol.ClientFactory):
22
- """
23
- Factory for creating Telegram protocol instances.
24
-
25
- Attributes:
26
- event_bus: Event bus for dispatching protocol events.
27
- telegram_protocol: Protocol instance to use.
28
- connector: Connection connector instance.
29
- logger: Logger instance for this factory.
30
- """
31
-
32
- def __init__(
33
- self,
34
- event_bus: EventBus,
35
- telegram_protocol: TelegramProtocol,
36
- connector: IConnector,
37
- ) -> None:
38
- """
39
- Initialize TelegramFactory.
40
-
41
- Args:
42
- event_bus: Event bus for protocol events.
43
- telegram_protocol: Protocol instance to use for connections.
44
- connector: Connection connector for managing connections.
45
- """
46
- self.event_bus = event_bus
47
- self.telegram_protocol = telegram_protocol
48
- self.connector = connector
49
- self.logger = logging.getLogger(__name__)
50
-
51
- def buildProtocol(self, addr: IAddress) -> TelegramProtocol:
52
- """
53
- Build protocol instance for connection.
54
-
55
- Args:
56
- addr: Address of the connection.
57
-
58
- Returns:
59
- Telegram protocol instance for this connection.
60
- """
61
- self.logger.debug(f"buildProtocol: {addr}")
62
- return self.telegram_protocol
63
-
64
- def clientConnectionFailed(self, connector: IConnector, reason: Failure) -> None:
65
- """
66
- Handle connection failure event.
67
-
68
- Args:
69
- connector: Connection connector instance.
70
- reason: Failure reason details.
71
- """
72
- self.event_bus.dispatch(ConnectionFailedEvent(reason=str(reason)))
73
- self.connector.stop()
74
-
75
- def clientConnectionLost(self, connector: IConnector, reason: Failure) -> None:
76
- """
77
- Handle connection lost event.
78
-
79
- Args:
80
- connector: Connection connector instance.
81
- reason: Reason for connection loss.
82
- """
83
- self.event_bus.dispatch(ConnectionLostEvent(reason=str(reason)))
84
- self.connector.stop()