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,44 +0,0 @@
1
- """API server start command."""
2
-
3
- import sys
4
-
5
- import click
6
- from click import Context
7
-
8
- from xp.cli.commands.homekit.homekit import homekit
9
- from xp.services.homekit.homekit_service import HomeKitService
10
-
11
-
12
- @homekit.command("start")
13
- @click.pass_context
14
- def homekit_start(ctx: Context) -> None:
15
- r"""
16
- Start the HomeKit server.
17
-
18
- This command starts the XP Protocol HomeKit server using HAP-python.
19
- The server provides HomeKit endpoints for Conbus operations.
20
-
21
- Args:
22
- ctx: Click context object.
23
-
24
- Examples:
25
- \b
26
- # Start server on default host and port
27
- xp homekit start
28
- """
29
- click.echo("Starting XP Protocol HomeKit server...")
30
-
31
- try:
32
- service: HomeKitService = (
33
- ctx.obj.get("container").get_container().resolve(HomeKitService)
34
- )
35
- service.start() # Blocking call - reactor.run() never returns
36
-
37
- except KeyboardInterrupt:
38
- click.echo("\nShutting down server...")
39
- except Exception as e:
40
- click.echo(
41
- click.style(f"Error starting server: {e}", fg="red"),
42
- err=True,
43
- )
44
- sys.exit(1)
@@ -1 +0,0 @@
1
- """HomeKit integration services."""
@@ -1,313 +0,0 @@
1
- """Bubus cache service for caching datapoint responses."""
2
-
3
- import json
4
- import logging
5
- from datetime import datetime
6
- from pathlib import Path
7
- from typing import Any, TypedDict, Union
8
-
9
- from bubus import EventBus
10
-
11
- from xp.models.protocol.conbus_protocol import (
12
- LightLevelReceivedEvent,
13
- OutputStateReceivedEvent,
14
- ReadDatapointEvent,
15
- ReadDatapointFromProtocolEvent,
16
- )
17
- from xp.models.telegram.datapoint_type import DataPointType
18
-
19
- # Cache file configuration
20
- CACHE_DIR = Path(".cache")
21
- CACHE_FILE = CACHE_DIR / "homekit_cache.json"
22
-
23
-
24
- class CacheEntry(TypedDict):
25
- """
26
- Cache entry type definition.
27
-
28
- Attributes:
29
- event: The cached event (OutputStateReceivedEvent or LightLevelReceivedEvent).
30
- timestamp: When the event was cached.
31
- """
32
-
33
- event: Union[OutputStateReceivedEvent, LightLevelReceivedEvent]
34
- timestamp: datetime
35
-
36
-
37
- class HomeKitCacheService:
38
- """
39
- Cache service that intercepts bubus protocol messages to reduce redundant queries.
40
-
41
- Caches OutputStateReceivedEvent and LightLevelReceivedEvent responses.
42
- When a ReadDatapointEvent is received, checks cache and either:
43
- - Returns cached response if available (cache hit)
44
- - Forwards to protocol via ReadDatapointFromProtocolEvent (cache miss)
45
- """
46
-
47
- def __init__(self, event_bus: EventBus, enable_persistence: bool = True):
48
- """
49
- Initialize the HomeKit cache service.
50
-
51
- Args:
52
- event_bus: Event bus for inter-service communication.
53
- enable_persistence: Whether to persist cache to disk.
54
- """
55
- self.logger = logging.getLogger(__name__)
56
- self.event_bus = event_bus
57
- self.cache: dict[tuple[str, DataPointType], CacheEntry] = {}
58
- self.enable_persistence = enable_persistence
59
-
60
- # Load cache from disk
61
- if self.enable_persistence:
62
- self._load_cache()
63
-
64
- # Register event handlers
65
- # Note: These must be registered BEFORE HomeKitConbusService registers its handlers
66
- self.event_bus.on(ReadDatapointEvent, self.handle_read_datapoint_event)
67
- self.event_bus.on(
68
- OutputStateReceivedEvent, self.handle_output_state_received_event
69
- )
70
- self.event_bus.on(
71
- LightLevelReceivedEvent, self.handle_light_level_received_event
72
- )
73
-
74
- self.logger.info(
75
- f"HomeKitCacheService initialized with {len(self.cache)} cached entries"
76
- )
77
-
78
- def _serialize_cache_key(self, key: tuple[str, DataPointType]) -> str:
79
- """Serialize cache key to JSON-compatible string."""
80
- serial_number, datapoint_type = key
81
- return f"{serial_number}.{datapoint_type.value}"
82
-
83
- def _deserialize_cache_key(self, key_str: str) -> tuple[str, DataPointType]:
84
- """Deserialize cache key from JSON string."""
85
- serial_number, datapoint_type_str = key_str.rsplit(".", 1)
86
- return (serial_number, DataPointType(datapoint_type_str))
87
-
88
- def _serialize_cache(self) -> dict[str, Any]:
89
- """Serialize cache to JSON-compatible dict."""
90
- serialized = {}
91
- for key, entry in self.cache.items():
92
- key_str = self._serialize_cache_key(key)
93
- serialized[key_str] = {
94
- "event": entry["event"].model_dump(mode="json"),
95
- "timestamp": entry["timestamp"].isoformat(),
96
- }
97
- return serialized
98
-
99
- def _deserialize_cache(
100
- self, data: dict[str, Any]
101
- ) -> dict[tuple[str, DataPointType], CacheEntry]:
102
- """Deserialize cache from JSON dict."""
103
- cache: dict[tuple[str, DataPointType], CacheEntry] = {}
104
- for key_str, entry_data in data.items():
105
- try:
106
- key = self._deserialize_cache_key(key_str)
107
- event_data = entry_data["event"]
108
-
109
- # Reconstruct event based on datapoint_type
110
- if key[1] == DataPointType.MODULE_OUTPUT_STATE:
111
- event = OutputStateReceivedEvent(**event_data)
112
- elif key[1] == DataPointType.MODULE_LIGHT_LEVEL:
113
- event = LightLevelReceivedEvent(**event_data)
114
- else:
115
- self.logger.warning(f"Unknown datapoint type in cache: {key[1]}")
116
- continue
117
-
118
- cache[key] = {
119
- "event": event,
120
- "timestamp": datetime.fromisoformat(entry_data["timestamp"]),
121
- }
122
- except Exception as e:
123
- self.logger.warning(f"Failed to deserialize cache entry {key_str}: {e}")
124
- continue
125
-
126
- return cache
127
-
128
- def _load_cache(self) -> None:
129
- """Load cache from disk."""
130
- if not CACHE_FILE.exists():
131
- self.logger.debug("No cache file found, starting with empty cache")
132
- return
133
-
134
- try:
135
- with CACHE_FILE.open("r") as f:
136
- data = json.load(f)
137
-
138
- self.cache = self._deserialize_cache(data)
139
- self.logger.info(f"Loaded {len(self.cache)} entries from cache file")
140
- except Exception as e:
141
- self.logger.error(f"Failed to load cache from disk: {e}")
142
- self.cache = {}
143
-
144
- def _save_cache(self) -> None:
145
- """Save cache to disk atomically."""
146
- if not self.enable_persistence:
147
- return
148
-
149
- try:
150
- # Ensure cache directory exists
151
- CACHE_DIR.mkdir(parents=True, exist_ok=True)
152
-
153
- # Atomic write: write to temp file, then rename
154
- temp_file = CACHE_FILE.with_suffix(".tmp")
155
- with temp_file.open("w") as f:
156
- json.dump(self._serialize_cache(), f, indent=2)
157
-
158
- # Atomic rename
159
- temp_file.replace(CACHE_FILE)
160
-
161
- self.logger.debug(f"Saved {len(self.cache)} entries to cache file")
162
- except Exception as e:
163
- self.logger.error(f"Failed to save cache to disk: {e}")
164
-
165
- def _get_cache_key(
166
- self, serial_number: str, datapoint_type: DataPointType
167
- ) -> tuple[str, DataPointType]:
168
- """Generate cache key from serial number and datapoint type."""
169
- return (serial_number, datapoint_type)
170
-
171
- def _cache_event(
172
- self, event: Union[OutputStateReceivedEvent, LightLevelReceivedEvent]
173
- ) -> None:
174
- """Store an event in the cache."""
175
- cache_key = self._get_cache_key(event.serial_number, event.datapoint_type)
176
- cache_entry: CacheEntry = {
177
- "event": event,
178
- "timestamp": datetime.now(),
179
- }
180
- self.cache[cache_key] = cache_entry
181
- self.logger.debug(
182
- f"Cached event: "
183
- f"serial={event.serial_number}, "
184
- f"type={event.datapoint_type}, "
185
- f"value={event.data_value}"
186
- )
187
-
188
- # Persist to disk
189
- self._save_cache()
190
-
191
- def _get_cached_event(
192
- self, serial_number: str, datapoint_type: DataPointType
193
- ) -> Union[OutputStateReceivedEvent, LightLevelReceivedEvent, None]:
194
- """Retrieve an event from the cache if it exists."""
195
- cache_key = self._get_cache_key(serial_number, datapoint_type)
196
- cache_entry = self.cache.get(cache_key)
197
-
198
- if cache_entry:
199
- self.logger.debug(
200
- f"Cache hit: " f"serial={serial_number}, " f"type={datapoint_type}"
201
- )
202
- return cache_entry["event"]
203
-
204
- self.logger.debug(f"Cache miss: serial={serial_number}, type={datapoint_type}")
205
- return None
206
-
207
- def handle_read_datapoint_event(self, event: ReadDatapointEvent) -> None:
208
- """
209
- Handle ReadDatapointEvent by checking cache or refresh flag.
210
-
211
- On refresh_cache=True: invalidate cache and force protocol query
212
- On cache hit: dispatch cached response event
213
- On cache miss: forward to protocol via ReadDatapointFromProtocolEvent
214
-
215
- Args:
216
- event: Read datapoint event with serial number, datapoint type, and refresh flag.
217
- """
218
- self.logger.debug(
219
- f"Handling ReadDatapointEvent: "
220
- f"serial={event.serial_number}, "
221
- f"type={event.datapoint_type}, "
222
- f"refresh_cache={event.refresh_cache}"
223
- )
224
-
225
- # Check if cache refresh requested
226
- if event.refresh_cache:
227
- self.logger.info(
228
- f"Cache refresh requested: "
229
- f"serial={event.serial_number}, "
230
- f"type={event.datapoint_type}"
231
- )
232
- # Invalidate cache entry
233
- cache_key = self._get_cache_key(event.serial_number, event.datapoint_type)
234
- if cache_key in self.cache:
235
- del self.cache[cache_key]
236
- self.logger.debug(f"Invalidated cache entry: {cache_key}")
237
- # Persist invalidation
238
- self._save_cache()
239
-
240
- # Normal cache lookup flow
241
- cached_event = self._get_cached_event(event.serial_number, event.datapoint_type)
242
-
243
- if cached_event:
244
- # Cache hit - dispatch the cached event
245
- self.logger.debug(
246
- f"Returning cached response: "
247
- f"serial={event.serial_number}, "
248
- f"type={event.datapoint_type}"
249
- )
250
- self.event_bus.dispatch(cached_event)
251
- return
252
-
253
- # Cache miss - forward to protocol
254
- self.logger.debug(
255
- f"Forwarding to protocol: "
256
- f"serial={event.serial_number}, "
257
- f"type={event.datapoint_type}"
258
- )
259
- self.event_bus.dispatch(
260
- ReadDatapointFromProtocolEvent(
261
- serial_number=event.serial_number,
262
- datapoint_type=event.datapoint_type,
263
- )
264
- )
265
-
266
- def handle_output_state_received_event(
267
- self, event: OutputStateReceivedEvent
268
- ) -> None:
269
- """
270
- Cache OutputStateReceivedEvent for future queries.
271
-
272
- Args:
273
- event: Output state received event to cache.
274
- """
275
- self.logger.debug(
276
- f"Caching OutputStateReceivedEvent: "
277
- f"serial={event.serial_number}, "
278
- f"type={event.datapoint_type}, "
279
- f"value={event.data_value}"
280
- )
281
- self._cache_event(event)
282
-
283
- def handle_light_level_received_event(self, event: LightLevelReceivedEvent) -> None:
284
- """
285
- Cache LightLevelReceivedEvent for future queries.
286
-
287
- Args:
288
- event: Light level received event to cache.
289
- """
290
- self.logger.debug(
291
- f"Caching LightLevelReceivedEvent: "
292
- f"serial={event.serial_number}, "
293
- f"type={event.datapoint_type}, "
294
- f"value={event.data_value}"
295
- )
296
- self._cache_event(event)
297
-
298
- def clear_cache(self) -> None:
299
- """Clear all cached entries."""
300
- self.logger.info("Clearing cache")
301
- self.cache.clear()
302
- self._save_cache()
303
-
304
- def get_cache_stats(self) -> dict[str, int]:
305
- """
306
- Get cache statistics.
307
-
308
- Returns:
309
- Dictionary with cache statistics including total_entries.
310
- """
311
- return {
312
- "total_entries": len(self.cache),
313
- }
@@ -1,99 +0,0 @@
1
- """
2
- HomeKit Conbus Service for protocol communication.
3
-
4
- This module bridges HomeKit events with the Conbus protocol for device control.
5
- """
6
-
7
- import logging
8
-
9
- from bubus import EventBus
10
-
11
- from xp.models.protocol.conbus_protocol import (
12
- ReadDatapointFromProtocolEvent,
13
- SendActionEvent,
14
- SendWriteConfigEvent,
15
- )
16
- from xp.models.telegram.datapoint_type import DataPointType
17
- from xp.models.telegram.system_function import SystemFunction
18
- from xp.services.protocol.telegram_protocol import TelegramProtocol
19
-
20
-
21
- class HomeKitConbusService:
22
- """
23
- Service for bridging HomeKit events with Conbus protocol.
24
-
25
- Attributes:
26
- event_bus: Event bus for inter-service communication.
27
- telegram_protocol: Protocol for sending telegrams.
28
- logger: Logger instance.
29
- """
30
-
31
- event_bus: EventBus
32
-
33
- def __init__(self, event_bus: EventBus, telegram_protocol: TelegramProtocol):
34
- """
35
- Initialize the HomeKit Conbus service.
36
-
37
- Args:
38
- event_bus: Event bus instance.
39
- telegram_protocol: Telegram protocol instance.
40
- """
41
- self.logger = logging.getLogger(__name__)
42
- self.event_bus = event_bus
43
- self.telegram_protocol = telegram_protocol
44
-
45
- # Register event handlers
46
- self.event_bus.on(
47
- ReadDatapointFromProtocolEvent, self.handle_read_datapoint_request
48
- )
49
- self.event_bus.on(SendActionEvent, self.handle_send_action_event)
50
- self.event_bus.on(SendWriteConfigEvent, self.handle_send_write_config_event)
51
-
52
- def handle_read_datapoint_request(
53
- self, event: ReadDatapointFromProtocolEvent
54
- ) -> None:
55
- """
56
- Handle request to read datapoint from protocol.
57
-
58
- Args:
59
- event: Read datapoint event with serial number and datapoint type.
60
- """
61
- self.logger.debug(f"read_datapoint_request {event}")
62
-
63
- system_function = SystemFunction.READ_DATAPOINT.value
64
- datapoint_value = event.datapoint_type.value
65
- telegram = f"S{event.serial_number}F{system_function}D{datapoint_value}"
66
- self.telegram_protocol.sendFrame(telegram.encode())
67
-
68
- def handle_send_write_config_event(self, event: SendWriteConfigEvent) -> None:
69
- """
70
- Handle send write config event.
71
-
72
- Args:
73
- event: Write config event with configuration data.
74
- """
75
- self.logger.debug(f"send_write_config_event {event}")
76
-
77
- # Format data as output_number:level (e.g., "02:050")
78
- system_function = SystemFunction.WRITE_CONFIG.value
79
- datapoint_type = DataPointType.MODULE_LIGHT_LEVEL.value
80
- config_data = f"{event.output_number:02d}:{event.value:03d}"
81
- telegram = (
82
- f"S{event.serial_number}F{system_function}D{datapoint_type}{config_data}"
83
- )
84
- self.telegram_protocol.sendFrame(telegram.encode())
85
-
86
- def handle_send_action_event(self, event: SendActionEvent) -> None:
87
- """
88
- Handle send action event.
89
-
90
- Args:
91
- event: Send action event with action data.
92
- """
93
- self.logger.debug(f"send_action_event {event}")
94
-
95
- telegram = event.on_action if event.value else event.off_action
96
- telegram_make = f"{telegram}M"
97
- telegram_break = f"{telegram}B"
98
- self.telegram_protocol.sendFrame(telegram_make.encode())
99
- self.telegram_protocol.sendFrame(telegram_break.encode())