conson-xp 1.51.1__py3-none-any.whl → 1.52.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: conson-xp
3
- Version: 1.51.1
3
+ Version: 1.52.0
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -1,8 +1,8 @@
1
- conson_xp-1.51.1.dist-info/METADATA,sha256=15wTMgVInRD7kUjYvZe3AtZPgmPGG_XcXsaGRB3Vjds,11448
2
- conson_xp-1.51.1.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
- conson_xp-1.51.1.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-1.51.1.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=PTmuTUb4TXiPuSSkKX3UT63IhH_fx4EpMP33cMsA6Nc,182
1
+ conson_xp-1.52.0.dist-info/METADATA,sha256=eOYsythIuuDYadskAlya_jpbvYD2Wu-6MymZA0encfg,11448
2
+ conson_xp-1.52.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
+ conson_xp-1.52.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-1.52.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=wi3ok8DMPwweHsqxB5KwuT1FJynHp6v2d2Vt06prmpw,182
6
6
  xp/cli/__init__.py,sha256=QjnKB1KaI2aIyKlzrnvCwfbBuUj8HNgwNMvNJVQofbI,81
7
7
  xp/cli/__main__.py,sha256=l2iKwMdat5rTGd3JWs-uGksnYYDDffp_Npz05QdKEeU,117
8
8
  xp/cli/commands/__init__.py,sha256=G7A1KFRSV0CEeDTqr_khu-K9_sc01CTI2KSfkFcaBRM,4949
@@ -84,7 +84,7 @@ xp/models/config/__init__.py,sha256=gEZnX9eE3DjFtLtF32riEjJQLypqQRbyPauBI4Cowbs,
84
84
  xp/models/config/conson_module_config.py,sha256=t1G0LnNNMnjs3ahhz4-Z_5SlEv2FCrcRq13OmvZ2pvA,3009
85
85
  xp/models/homekit/__init__.py,sha256=5HDSOClCu0ArK3IICn3_LDMMLBAzLjBxUUSF73bxSSk,34
86
86
  xp/models/homekit/homekit_accessory.py,sha256=ANjDWlFxeNTstl7lKdmf6vMOC0wc005vpiD6awRcptA,1052
87
- xp/models/homekit/homekit_config.py,sha256=EqoiZ1E6l9bBjxKqK1nxVGRfFY5ZtRHH-jZhYtRH2gU,3048
87
+ xp/models/homekit/homekit_config.py,sha256=pgZOnocue60LjV8ce46MyJ3mo5CqLix6TmT64qxPOks,3267
88
88
  xp/models/log_entry.py,sha256=tAiNwouCP2d4jKiHJY9a-2iAi8LWTpG-TZsOPDIstlA,4423
89
89
  xp/models/protocol/__init__.py,sha256=TJ_CJKchA-xgQiv5vCo_ndBBZjrcaTmjT74bR0T-5Cw,38
90
90
  xp/models/protocol/conbus_protocol.py,sha256=hF78N5xvBzMiyWoKd8i_avA8kJ1As_9Pplkw1GMqKzk,9145
@@ -185,11 +185,12 @@ xp/services/telegram/telegram_output_service.py,sha256=9deqtcPndRqJ-3XQUWlJhXaVc
185
185
  xp/services/telegram/telegram_service.py,sha256=jPu0Xrh3IpvqPLyuQT5Vf8HHw00vBingONHdxf_9TkI,13315
186
186
  xp/services/telegram/telegram_version_service.py,sha256=oXnZ_K7OQ7xD-GEj3zDYp52KlkqVuHpO4bf7gMlC_w4,10574
187
187
  xp/services/term/__init__.py,sha256=BIeOK042bMR-0l6MA80wdW5VuHlpWOXtRER9IG5ilQA,245
188
- xp/services/term/homekit_service.py,sha256=3g6Twhg6wcRT2E01Daa2J41lzdyX6gVjfz5wgutOvgQ,19307
188
+ xp/services/term/homekit_accessory_driver.py,sha256=vCkeXHwIBwUIuUGoTdflbIOm8EQhT33pX8j4Smhr1co,5759
189
+ xp/services/term/homekit_service.py,sha256=QZnAzgOVIZ8vlfa1ehIp4Q1zZyIAj2f0SB1W8wdmemo,21895
189
190
  xp/services/term/protocol_monitor_service.py,sha256=5YBI0Nu7B7gMhaTbUhL6k9LSRfnCIj6CwrCYHiMHavA,10067
190
191
  xp/services/term/state_monitor_service.py,sha256=EK9tNBfamAIV0z0EMsXDYWC-rXv6l6k_bHsC8xyEFSo,17116
191
192
  xp/term/__init__.py,sha256=Xg2DhBeI3xQJLfc7_BPWI1por-rUXemyer5OtOt9Cus,51
192
- xp/term/homekit.py,sha256=IYJEVhQUFUcmC_TubYxcvlU5IKF-2sL_giBNKO35fjU,3642
193
+ xp/term/homekit.py,sha256=xN2BiMHHF3tPR96ixg_qO5y6fDNTkjEzau3YDTNuk1U,3711
193
194
  xp/term/homekit.tcss,sha256=qeR_OV8D_9Mxb-aPNz-MH0ZJOsdCk-fJ-zv6CQV5ihw,1382
194
195
  xp/term/protocol.py,sha256=6MX3mduLei-AgLGaIe8lfOSu4Hi0y3KGePFFM2ssstc,3475
195
196
  xp/term/protocol.tcss,sha256=r_KfxrbpycGHLVXqZc6INBBcUJME0hLrAZkF1oqnab4,2126
@@ -203,10 +204,10 @@ xp/term/widgets/room_list.py,sha256=3q3otusnQn4qFRbTY0-QbpMP3vPmywM0izYRA_KjXn0,
203
204
  xp/term/widgets/status_footer.py,sha256=biV3EzfVSgm1T7Ofi88LXsTFCkD5mI_6Cpe-RpuOSxA,3429
204
205
  xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
205
206
  xp/utils/checksum.py,sha256=Px1S3dFGA-_plavBxrq3IqmprNlgtNDunE3whg6Otwg,1722
206
- xp/utils/dependencies.py,sha256=XHOAq5nIbXD8oq4FBp3wWvinDa4Ti7cUktJtUB0z51A,25339
207
+ xp/utils/dependencies.py,sha256=McCgnBrg0Jw98_bFQv3uPG0bYkGKHC-mFJPmILlvv5k,25754
207
208
  xp/utils/event_helper.py,sha256=zD0K3TPfGEThU9vUNlDtglTai3Cmm30727iwjDZy6Dk,1007
208
209
  xp/utils/logging.py,sha256=wJ1d-yg97NiZUrt2F8iDMcmnHVwC-PErcI-7dpyiRDc,3777
209
210
  xp/utils/serialization.py,sha256=TS1OwpTOemSvXsCGw3js4JkYYFEqkzrPe8V9QYQefdw,4684
210
211
  xp/utils/state_machine.py,sha256=W9AY4ntRZnFeHAa5d43hm37j53uJPlqkRvWTPiBhJ_0,2464
211
212
  xp/utils/time_utils.py,sha256=K17godWpL18VEypbTlvNOEDG6R3huYnf29yjkcnwRpU,3796
212
- conson_xp-1.51.1.dist-info/RECORD,,
213
+ conson_xp-1.52.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -4,7 +4,7 @@ XP CLI tool for remote console bus operations.
4
4
  conson-xp package.
5
5
  """
6
6
 
7
- __version__ = "1.51.1"
7
+ __version__ = "1.52.0"
8
8
  __manufacturer__ = "salchichon"
9
9
  __model__ = "xp.cli"
10
10
  __serial__ = "2025.09.23.000"
@@ -16,10 +16,14 @@ class NetworkConfig(BaseModel):
16
16
  Attributes:
17
17
  ip: IP address for the network connection.
18
18
  port: Port number for the network connection.
19
+ pincode: HomeKit pairing code (format: XXX-XX-XXX).
20
+ accessory_state_file: Path to file for persisting accessory state.
19
21
  """
20
22
 
21
23
  ip: Union[IPvAnyAddress, IPv4Address, IPv6Address, str] = "127.0.0.1"
22
24
  port: int = 51826
25
+ pincode: str = "031-45-154"
26
+ accessory_state_file: str = "./accessory.state"
23
27
 
24
28
 
25
29
  class RoomConfig(BaseModel):
@@ -0,0 +1,168 @@
1
+ """HomeKit Accessory Driver for pyhap integration."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Callable, Dict, Optional
6
+
7
+ from pyhap.accessory import Accessory, Bridge
8
+ from pyhap.accessory_driver import AccessoryDriver
9
+ from pyhap.const import CATEGORY_LIGHTBULB, CATEGORY_OUTLET
10
+
11
+ from xp.models.homekit.homekit_config import HomekitConfig
12
+
13
+
14
+ class XPAccessory(Accessory):
15
+ """Single accessory wrapping a Conbus output."""
16
+
17
+ def __init__(
18
+ self,
19
+ driver: "HomekitAccessoryDriver",
20
+ name: str,
21
+ service_type: str,
22
+ aid: int,
23
+ ) -> None:
24
+ """
25
+ Initialize the XP accessory.
26
+
27
+ Args:
28
+ driver: HomekitAccessoryDriver instance.
29
+ name: Accessory name (unique identifier and display name).
30
+ service_type: Service type ('light', 'outlet', 'dimminglight').
31
+ aid: Accessory ID for HomeKit.
32
+ """
33
+ super().__init__(driver._driver, name, aid=aid)
34
+ self._hk_driver = driver
35
+ self._accessory_id = name
36
+ self.logger = logging.getLogger(__name__)
37
+
38
+ if service_type == "dimminglight":
39
+ self.category = CATEGORY_LIGHTBULB
40
+ serv = self.add_preload_service("Lightbulb", chars=["On", "Brightness"])
41
+ # Note: Brightness setter_callback deferred to future update
42
+ elif service_type == "outlet":
43
+ self.category = CATEGORY_OUTLET
44
+ serv = self.add_preload_service("Outlet")
45
+ else:
46
+ self.category = CATEGORY_LIGHTBULB
47
+ serv = self.add_preload_service("Lightbulb")
48
+
49
+ self._char_on = serv.configure_char("On", setter_callback=self._set_on)
50
+
51
+ def _set_on(self, value: bool) -> None:
52
+ """
53
+ Handle HomeKit set on/off request.
54
+
55
+ Args:
56
+ value: True for on, False for off.
57
+ """
58
+ if self._hk_driver._on_set:
59
+ self._hk_driver._on_set(self._accessory_id, value)
60
+
61
+ def update_state(self, is_on: bool) -> None:
62
+ """
63
+ Update accessory state from Conbus event.
64
+
65
+ Args:
66
+ is_on: True if accessory is on, False otherwise.
67
+ """
68
+ self._char_on.set_value(is_on)
69
+
70
+
71
+ class HomekitAccessoryDriver:
72
+ """Wrapper around pyhap AccessoryDriver."""
73
+
74
+ def __init__(self, homekit_config: HomekitConfig) -> None:
75
+ """
76
+ Initialize the HomeKit accessory driver.
77
+
78
+ Args:
79
+ homekit_config: HomekitConfig with network and accessory settings.
80
+ """
81
+ self.logger = logging.getLogger(__name__)
82
+ self._homekit_config = homekit_config
83
+ self._driver: Optional[AccessoryDriver] = None
84
+ self._accessories: Dict[str, XPAccessory] = {}
85
+ self._on_set: Optional[Callable[[str, bool], None]] = None
86
+
87
+ def set_callback(self, on_set: Callable[[str, bool], None]) -> None:
88
+ """
89
+ Set callback for HomeKit set events.
90
+
91
+ Args:
92
+ on_set: Callback(accessory_name, is_on) called when HomeKit app toggles.
93
+ """
94
+ self._on_set = on_set
95
+
96
+ def _setup_bridge(self, config: HomekitConfig) -> None:
97
+ """
98
+ Set up HomeKit bridge with accessories.
99
+
100
+ Args:
101
+ config: HomekitConfig with accessory definitions.
102
+ """
103
+ assert self._driver is not None
104
+ bridge = Bridge(self._driver, config.bridge.name)
105
+ aid = 2 # Bridge is 1
106
+
107
+ for acc_config in config.accessories:
108
+ accessory = XPAccessory(
109
+ driver=self,
110
+ name=acc_config.name,
111
+ service_type=acc_config.service,
112
+ aid=aid,
113
+ )
114
+ bridge.add_accessory(accessory)
115
+ self._accessories[acc_config.name] = accessory
116
+ aid += 1
117
+
118
+ self._driver.add_accessory(bridge)
119
+
120
+ async def start(self) -> None:
121
+ """Start the AccessoryDriver (non-blocking)."""
122
+ try:
123
+ # Enable pyhap debug logging
124
+ pyhap_logger = logging.getLogger("pyhap")
125
+ pyhap_logger.setLevel(logging.DEBUG)
126
+
127
+ # Create driver with the running event loop
128
+ loop = asyncio.get_running_loop()
129
+ config = self._homekit_config
130
+ pincode = config.homekit.pincode.encode()
131
+ self.logger.info(
132
+ f"Starting HAP driver on {config.homekit.ip}:{config.homekit.port} with pincode {config.homekit.pincode}"
133
+ )
134
+ self._driver = AccessoryDriver(
135
+ loop=loop,
136
+ address=str(config.homekit.ip),
137
+ port=config.homekit.port,
138
+ pincode=pincode,
139
+ persist_file=config.homekit.accessory_state_file,
140
+ )
141
+ self._setup_bridge(config)
142
+ await self._driver.async_start()
143
+ self.logger.info("AccessoryDriver started successfully")
144
+ except Exception as e:
145
+ self.logger.error(f"Error starting AccessoryDriver: {e}", exc_info=True)
146
+
147
+ async def stop(self) -> None:
148
+ """Stop the AccessoryDriver."""
149
+ if not self._driver:
150
+ return
151
+ try:
152
+ await self._driver.async_stop()
153
+ self.logger.info("AccessoryDriver stopped successfully")
154
+ except Exception as e:
155
+ self.logger.error(f"Error stopping AccessoryDriver: {e}", exc_info=True)
156
+
157
+ def update_state(self, accessory_name: str, is_on: bool) -> None:
158
+ """
159
+ Update accessory state from Conbus event.
160
+
161
+ Args:
162
+ accessory_name: Accessory name to update.
163
+ is_on: True if accessory is on, False otherwise.
164
+ """
165
+ if acc := self._accessories.get(accessory_name):
166
+ acc.update_state(is_on)
167
+ else:
168
+ self.logger.warning(f"Unknown accessory name: {accessory_name}")
@@ -18,6 +18,7 @@ from xp.models.term.connection_state import ConnectionState
18
18
  from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
19
19
  from xp.services.telegram.telegram_output_service import TelegramOutputService
20
20
  from xp.services.telegram.telegram_service import TelegramService
21
+ from xp.services.term.homekit_accessory_driver import HomekitAccessoryDriver
21
22
 
22
23
 
23
24
  class HomekitService:
@@ -50,6 +51,7 @@ class HomekitService:
50
51
  homekit_config: HomekitConfig,
51
52
  conson_config: ConsonModuleListConfig,
52
53
  telegram_service: TelegramService,
54
+ accessory_driver: HomekitAccessoryDriver,
53
55
  ) -> None:
54
56
  """
55
57
  Initialize the HomeKit service.
@@ -59,12 +61,14 @@ class HomekitService:
59
61
  homekit_config: HomekitConfig for accessory configuration.
60
62
  conson_config: ConsonModuleListConfig for module configuration.
61
63
  telegram_service: TelegramService for parsing telegrams.
64
+ accessory_driver: HomekitAccessoryDriver for pyhap integration.
62
65
  """
63
66
  self.logger = logging.getLogger(__name__)
64
67
  self._conbus_protocol = conbus_protocol
65
68
  self._homekit_config = homekit_config
66
69
  self._conson_config = conson_config
67
70
  self._telegram_service = telegram_service
71
+ self._accessory_driver = accessory_driver
68
72
  self._connection_state = ConnectionState.DISCONNECTED
69
73
  self._state_machine = ConnectionState.create_state_machine()
70
74
 
@@ -74,6 +78,9 @@ class HomekitService:
74
78
  # Action key to accessory ID mapping
75
79
  self._action_map: Dict[str, str] = {}
76
80
 
81
+ # Set up HomeKit callback
82
+ self._accessory_driver.set_callback(self._on_homekit_set)
83
+
77
84
  # Connect to protocol signals
78
85
  self._connect_signals()
79
86
 
@@ -152,6 +159,27 @@ class HomekitService:
152
159
  return accessory
153
160
  return None
154
161
 
162
+ def _find_accessory_config_by_output(
163
+ self, serial_number: str, output: int
164
+ ) -> Optional[HomekitAccessoryConfig]:
165
+ """
166
+ Find accessory config by serial number and output.
167
+
168
+ Args:
169
+ serial_number: Module serial number.
170
+ output: Output number (1-based).
171
+
172
+ Returns:
173
+ HomekitAccessoryConfig if found, None otherwise.
174
+ """
175
+ for accessory in self._homekit_config.accessories:
176
+ if (
177
+ accessory.serial_number == serial_number
178
+ and accessory.output_number == output - 1
179
+ ):
180
+ return accessory
181
+ return None
182
+
155
183
  def _connect_signals(self) -> None:
156
184
  """Connect to protocol signals."""
157
185
  self._conbus_protocol.on_connection_made.connect(self._on_connection_made)
@@ -242,6 +270,34 @@ class HomekitService:
242
270
  self.on_connection_state_changed.emit(self._connection_state)
243
271
  self.on_status_message.emit("Disconnected")
244
272
 
273
+ async def start(self) -> None:
274
+ """Start the service and AccessoryDriver."""
275
+ self.connect()
276
+ await self._accessory_driver.start()
277
+
278
+ async def stop(self) -> None:
279
+ """Stop the AccessoryDriver and cleanup."""
280
+ await self._accessory_driver.stop()
281
+ self.cleanup()
282
+
283
+ def _on_homekit_set(self, accessory_name: str, is_on: bool) -> None:
284
+ """
285
+ Handle HomeKit app toggle request.
286
+
287
+ Args:
288
+ accessory_name: Accessory name from HomeKit.
289
+ is_on: True for on, False for off.
290
+ """
291
+ config = self._find_accessory_config(accessory_name)
292
+ if config:
293
+ action = config.on_action if is_on else config.off_action
294
+ self._conbus_protocol.send_raw_telegram(action)
295
+ self.on_status_message.emit(
296
+ f"HomeKit: {accessory_name} {'ON' if is_on else 'OFF'}"
297
+ )
298
+ else:
299
+ self.logger.warning(f"No config found for accessory: {accessory_name}")
300
+
245
301
  def toggle_connection(self) -> None:
246
302
  """
247
303
  Toggle connection state between connected and disconnected.
@@ -406,6 +462,13 @@ class HomekitService:
406
462
  # Update dimming state for dimmable modules
407
463
  if state.is_dimmable():
408
464
  state.dimming_state = "-" if not is_on else ""
465
+
466
+ # Sync to HomeKit
467
+ config = self._find_accessory_config_by_output(
468
+ serial_number, state.output
469
+ )
470
+ if config:
471
+ self._accessory_driver.update_state(config.name, is_on)
409
472
  else:
410
473
  state.output_state = "?"
411
474
 
@@ -463,6 +526,13 @@ class HomekitService:
463
526
  if state.is_dimmable():
464
527
  state.dimming_state = "-" if not is_on else ""
465
528
 
529
+ # Sync to HomeKit
530
+ config = self._find_accessory_config_by_output(
531
+ state.serial_number, state.output
532
+ )
533
+ if config:
534
+ self._accessory_driver.update_state(config.name, is_on)
535
+
466
536
  state.last_update = datetime.now()
467
537
  self.on_module_state_changed.emit(state)
468
538
 
xp/term/homekit.py CHANGED
@@ -68,14 +68,14 @@ class HomekitApp(App[None]):
68
68
  """
69
69
  Initialize app after UI is mounted.
70
70
 
71
- Delays connection by 0.5s to let UI render first. Sets up automatic screen
72
- refresh every second to update elapsed times.
71
+ Delays connection by 0.5s to let UI render first. Starts the AccessoryDriver and
72
+ sets up automatic screen refresh every second to update elapsed times.
73
73
  """
74
74
  import asyncio
75
75
 
76
76
  # Delay connection to let UI render
77
77
  await asyncio.sleep(0.5)
78
- self.homekit_service.connect()
78
+ await self.homekit_service.start()
79
79
 
80
80
  # Set up periodic refresh to update elapsed times
81
81
  self.set_interval(1.0, self._refresh_last_update_column)
@@ -111,6 +111,6 @@ class HomekitApp(App[None]):
111
111
  """Refresh all module data on 'r' key press."""
112
112
  self.homekit_service.refresh_all()
113
113
 
114
- def on_unmount(self) -> None:
115
- """Clean up service when app unmounts."""
116
- self.homekit_service.cleanup()
114
+ async def on_unmount(self) -> None:
115
+ """Stop AccessoryDriver and clean up service when app unmounts."""
116
+ await self.homekit_service.stop()
xp/utils/dependencies.py CHANGED
@@ -77,6 +77,7 @@ from xp.services.telegram.telegram_discover_service import TelegramDiscoverServi
77
77
  from xp.services.telegram.telegram_link_number_service import LinkNumberService
78
78
  from xp.services.telegram.telegram_output_service import TelegramOutputService
79
79
  from xp.services.telegram.telegram_service import TelegramService
80
+ from xp.services.term.homekit_accessory_driver import HomekitAccessoryDriver
80
81
  from xp.services.term.homekit_service import HomekitService
81
82
  from xp.services.term.protocol_monitor_service import ProtocolMonitorService
82
83
  from xp.services.term.state_monitor_service import StateMonitorService
@@ -267,6 +268,14 @@ class ServiceContainer:
267
268
  scope=punq.Scope.singleton,
268
269
  )
269
270
 
271
+ self.container.register(
272
+ HomekitAccessoryDriver,
273
+ factory=lambda: HomekitAccessoryDriver(
274
+ homekit_config=self.container.resolve(HomekitConfig),
275
+ ),
276
+ scope=punq.Scope.singleton,
277
+ )
278
+
270
279
  self.container.register(
271
280
  HomekitService,
272
281
  factory=lambda: HomekitService(
@@ -274,6 +283,7 @@ class ServiceContainer:
274
283
  homekit_config=self.container.resolve(HomekitConfig),
275
284
  conson_config=self.container.resolve(ConsonModuleListConfig),
276
285
  telegram_service=self.container.resolve(TelegramService),
286
+ accessory_driver=self.container.resolve(HomekitAccessoryDriver),
277
287
  ),
278
288
  scope=punq.Scope.singleton,
279
289
  )