conson-xp 1.51.0__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.
- {conson_xp-1.51.0.dist-info → conson_xp-1.52.0.dist-info}/METADATA +2 -1
- {conson_xp-1.51.0.dist-info → conson_xp-1.52.0.dist-info}/RECORD +19 -13
- xp/__init__.py +1 -1
- xp/cli/commands/term/term_commands.py +23 -0
- xp/models/homekit/homekit_config.py +6 -0
- xp/models/term/__init__.py +2 -0
- xp/models/term/accessory_state.py +50 -0
- xp/services/homekit/homekit_config_validator.py +1 -1
- xp/services/term/homekit_accessory_driver.py +168 -0
- xp/services/term/homekit_service.py +582 -0
- xp/services/term/state_monitor_service.py +1 -1
- xp/term/homekit.py +116 -0
- xp/term/homekit.tcss +86 -0
- xp/term/widgets/room_list.py +232 -0
- xp/term/widgets/status_footer.py +6 -3
- xp/utils/dependencies.py +31 -0
- {conson_xp-1.51.0.dist-info → conson_xp-1.52.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.51.0.dist-info → conson_xp-1.52.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.51.0.dist-info → conson_xp-1.52.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: conson-xp
|
|
3
|
-
Version: 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
|
|
@@ -450,6 +450,7 @@ xp telegram version
|
|
|
450
450
|
|
|
451
451
|
|
|
452
452
|
xp term
|
|
453
|
+
xp term homekit
|
|
453
454
|
xp term protocol
|
|
454
455
|
xp term state
|
|
455
456
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
conson_xp-1.
|
|
2
|
-
conson_xp-1.
|
|
3
|
-
conson_xp-1.
|
|
4
|
-
conson_xp-1.
|
|
5
|
-
xp/__init__.py,sha256=
|
|
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
|
|
@@ -43,7 +43,7 @@ xp/cli/commands/telegram/telegram_parse_commands.py,sha256=xCDRRFgj41RtStvwROfi-
|
|
|
43
43
|
xp/cli/commands/telegram/telegram_version_commands.py,sha256=hAMjSAa7zfMNfNFln63sKeNPcmW89bISkcVs6BgsqOg,1558
|
|
44
44
|
xp/cli/commands/term/__init__.py,sha256=1NNST_8YJfj5LCujQISwQflK6LyEn7mDmZpMpvI9d-o,116
|
|
45
45
|
xp/cli/commands/term/term.py,sha256=gjvsv2OE-F_KNWQrWi04fXQ5cGo0l8P-Ortbb5KTA-A,309
|
|
46
|
-
xp/cli/commands/term/term_commands.py,sha256=
|
|
46
|
+
xp/cli/commands/term/term_commands.py,sha256=Z3pq_xzP0j5YfYFOwASaZUxXDkikSV_E4cFjMb69LUU,1796
|
|
47
47
|
xp/cli/main.py,sha256=Wbtji5ddW3IEoAfecHrEPk8W_w1bGD20B-NqAWfI_F4,1968
|
|
48
48
|
xp/cli/utils/__init__.py,sha256=gTGIj60Uai0iE7sr9_TtEpl04fD7krtTzbbigXUsUVU,46
|
|
49
49
|
xp/cli/utils/click_tree.py,sha256=sr4l9RWTCnASkdvkJKnRRxWSQPlF1DbFdBNu9gL7Ekc,1693
|
|
@@ -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=
|
|
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
|
|
@@ -105,7 +105,8 @@ xp/models/telegram/system_telegram.py,sha256=064AlFi_WghoYlVDUtIIbvwtZyUEZj7_auK
|
|
|
105
105
|
xp/models/telegram/telegram.py,sha256=-kNloBlwMJ5w1-FAMSLzBPnyOGUNEBG3SG2d0eTi2PY,847
|
|
106
106
|
xp/models/telegram/telegram_type.py,sha256=IjGEosbs7IDqYT7ktn-FcKS-kAJ4eXW-KJGkkoAGysw,428
|
|
107
107
|
xp/models/telegram/timeparam_type.py,sha256=z5EQ32SQjDi7zKshtkvDzqaMfPMUeXCWKEGI5VgvBvU,1142
|
|
108
|
-
xp/models/term/__init__.py,sha256=
|
|
108
|
+
xp/models/term/__init__.py,sha256=VVZsEyXBEr-TnBlrFFifZ6PjJHUl2kwnRUZx_kC2Ljg,343
|
|
109
|
+
xp/models/term/accessory_state.py,sha256=GcMtCxOeHpc3IPDO1F9j2I6rdhaNV75iQ2md--XY6jo,1650
|
|
109
110
|
xp/models/term/connection_state.py,sha256=oYcst01uH35kO541jGuXMqvJ2iduiHYryUsMK0d89pQ,1807
|
|
110
111
|
xp/models/term/module_state.py,sha256=i7u8y_B5ScMRULQb_kMSD_wwKzbrLHlkECsTgNS46PQ,939
|
|
111
112
|
xp/models/term/protocol_keys_config.py,sha256=tSlkxEwgQuVRYLTaUNd569osQsNCdb9ED4InNgX9rKo,1223
|
|
@@ -145,7 +146,7 @@ xp/services/conbus/write_config_service.py,sha256=BCfmLNPRDpwSwRMRYJvx2FXA8IZsdg
|
|
|
145
146
|
xp/services/homekit/__init__.py,sha256=xAMKmln_AmEFdOOJGKWYi96seRlKDQpKx3-hm7XbdIo,36
|
|
146
147
|
xp/services/homekit/homekit_cache_service.py,sha256=z1TB6icEqd1paoilVTewuFL0lXVCQbvrOJkJvvQECJY,11060
|
|
147
148
|
xp/services/homekit/homekit_conbus_service.py,sha256=XPKv7Mit1rn7XLaQZcKmlMMUlyj-o0J2z8XBH3NaEIM,3390
|
|
148
|
-
xp/services/homekit/homekit_config_validator.py,sha256=
|
|
149
|
+
xp/services/homekit/homekit_config_validator.py,sha256=jf09jHIFbZg7YpDbGsGHT1p4a1vpUED2xR6iZN19cfM,10875
|
|
149
150
|
xp/services/homekit/homekit_conson_validator.py,sha256=tmUxBzytX9FbUWTR1XdbAi_qb_whAdGPSaml98Czszg,3858
|
|
150
151
|
xp/services/homekit/homekit_dimminglight.py,sha256=EzfGhy3zZkbFPfN72Dh_eSb5mJQOpxGi6ZwnyEOSHxU,5819
|
|
151
152
|
xp/services/homekit/homekit_dimminglight_service.py,sha256=0Ve6cXtY7v7JCv7gibOBWjPfCU7KK4Lk6GjIu9_GhyE,5282
|
|
@@ -184,9 +185,13 @@ xp/services/telegram/telegram_output_service.py,sha256=9deqtcPndRqJ-3XQUWlJhXaVc
|
|
|
184
185
|
xp/services/telegram/telegram_service.py,sha256=jPu0Xrh3IpvqPLyuQT5Vf8HHw00vBingONHdxf_9TkI,13315
|
|
185
186
|
xp/services/telegram/telegram_version_service.py,sha256=oXnZ_K7OQ7xD-GEj3zDYp52KlkqVuHpO4bf7gMlC_w4,10574
|
|
186
187
|
xp/services/term/__init__.py,sha256=BIeOK042bMR-0l6MA80wdW5VuHlpWOXtRER9IG5ilQA,245
|
|
188
|
+
xp/services/term/homekit_accessory_driver.py,sha256=vCkeXHwIBwUIuUGoTdflbIOm8EQhT33pX8j4Smhr1co,5759
|
|
189
|
+
xp/services/term/homekit_service.py,sha256=QZnAzgOVIZ8vlfa1ehIp4Q1zZyIAj2f0SB1W8wdmemo,21895
|
|
187
190
|
xp/services/term/protocol_monitor_service.py,sha256=5YBI0Nu7B7gMhaTbUhL6k9LSRfnCIj6CwrCYHiMHavA,10067
|
|
188
|
-
xp/services/term/state_monitor_service.py,sha256=
|
|
191
|
+
xp/services/term/state_monitor_service.py,sha256=EK9tNBfamAIV0z0EMsXDYWC-rXv6l6k_bHsC8xyEFSo,17116
|
|
189
192
|
xp/term/__init__.py,sha256=Xg2DhBeI3xQJLfc7_BPWI1por-rUXemyer5OtOt9Cus,51
|
|
193
|
+
xp/term/homekit.py,sha256=xN2BiMHHF3tPR96ixg_qO5y6fDNTkjEzau3YDTNuk1U,3711
|
|
194
|
+
xp/term/homekit.tcss,sha256=qeR_OV8D_9Mxb-aPNz-MH0ZJOsdCk-fJ-zv6CQV5ihw,1382
|
|
190
195
|
xp/term/protocol.py,sha256=6MX3mduLei-AgLGaIe8lfOSu4Hi0y3KGePFFM2ssstc,3475
|
|
191
196
|
xp/term/protocol.tcss,sha256=r_KfxrbpycGHLVXqZc6INBBcUJME0hLrAZkF1oqnab4,2126
|
|
192
197
|
xp/term/state.py,sha256=FBpYV_bWYJh9o17qcMx6sHgUARQS-uNOtUt6G7Vs1n8,3274
|
|
@@ -195,13 +200,14 @@ xp/term/widgets/__init__.py,sha256=ftWmN_fmjxy2E8Qfm-YSRmzKfgL0KTBCTpgvYWCPbUY,2
|
|
|
195
200
|
xp/term/widgets/help_menu.py,sha256=KLkdIXfhARLFNEs2lv1u0sYBz9LzOCcDLxbMGzc7e5Y,1812
|
|
196
201
|
xp/term/widgets/modules_list.py,sha256=qAG-n0nK0YdNE9v4C3-sHgxLvF1i1FR7v_GArdaoUQw,7831
|
|
197
202
|
xp/term/widgets/protocol_log.py,sha256=E68QmSMpOFrvrPTo_gOQVfyiDqY5c_y8fkNKnQw6Vwo,2650
|
|
198
|
-
xp/term/widgets/
|
|
203
|
+
xp/term/widgets/room_list.py,sha256=3q3otusnQn4qFRbTY0-QbpMP3vPmywM0izYRA_KjXn0,7871
|
|
204
|
+
xp/term/widgets/status_footer.py,sha256=biV3EzfVSgm1T7Ofi88LXsTFCkD5mI_6Cpe-RpuOSxA,3429
|
|
199
205
|
xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
|
|
200
206
|
xp/utils/checksum.py,sha256=Px1S3dFGA-_plavBxrq3IqmprNlgtNDunE3whg6Otwg,1722
|
|
201
|
-
xp/utils/dependencies.py,sha256=
|
|
207
|
+
xp/utils/dependencies.py,sha256=McCgnBrg0Jw98_bFQv3uPG0bYkGKHC-mFJPmILlvv5k,25754
|
|
202
208
|
xp/utils/event_helper.py,sha256=zD0K3TPfGEThU9vUNlDtglTai3Cmm30727iwjDZy6Dk,1007
|
|
203
209
|
xp/utils/logging.py,sha256=wJ1d-yg97NiZUrt2F8iDMcmnHVwC-PErcI-7dpyiRDc,3777
|
|
204
210
|
xp/utils/serialization.py,sha256=TS1OwpTOemSvXsCGw3js4JkYYFEqkzrPe8V9QYQefdw,4684
|
|
205
211
|
xp/utils/state_machine.py,sha256=W9AY4ntRZnFeHAa5d43hm37j53uJPlqkRvWTPiBhJ_0,2464
|
|
206
212
|
xp/utils/time_utils.py,sha256=K17godWpL18VEypbTlvNOEDG6R3huYnf29yjkcnwRpU,3796
|
|
207
|
-
conson_xp-1.
|
|
213
|
+
conson_xp-1.52.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
|
@@ -48,3 +48,26 @@ def state_monitor(ctx: Context) -> None:
|
|
|
48
48
|
|
|
49
49
|
# Resolve StateMonitorApp from container and run
|
|
50
50
|
ctx.obj.get("container").get_container().resolve(StateMonitorApp).run()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@term.command("homekit")
|
|
54
|
+
@click.pass_context
|
|
55
|
+
def homekit_monitor(ctx: Context) -> None:
|
|
56
|
+
r"""
|
|
57
|
+
Start TUI for HomeKit accessory monitoring.
|
|
58
|
+
|
|
59
|
+
Displays HomeKit rooms and accessories with real-time state updates
|
|
60
|
+
in an interactive terminal interface. Press action keys (a-z) to
|
|
61
|
+
toggle accessories.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
ctx: Click context object.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
\b
|
|
68
|
+
xp term homekit
|
|
69
|
+
"""
|
|
70
|
+
from xp.term.homekit import HomekitApp
|
|
71
|
+
|
|
72
|
+
# Resolve HomekitApp from container and run
|
|
73
|
+
ctx.obj.get("container").get_container().resolve(HomekitApp).run()
|
|
@@ -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):
|
|
@@ -61,6 +65,7 @@ class HomekitAccessoryConfig(BaseModel):
|
|
|
61
65
|
service: Service type for the accessory.
|
|
62
66
|
on_action: on code for the accessory.
|
|
63
67
|
off_action: off code for the accessory.
|
|
68
|
+
toggle_action: Optional toggle action code for the accessory.
|
|
64
69
|
hap_accessory: Optional HAP accessory identifier.
|
|
65
70
|
"""
|
|
66
71
|
|
|
@@ -72,6 +77,7 @@ class HomekitAccessoryConfig(BaseModel):
|
|
|
72
77
|
service: str
|
|
73
78
|
on_action: str
|
|
74
79
|
off_action: str
|
|
80
|
+
toggle_action: Optional[str] = None
|
|
75
81
|
hap_accessory: Optional[int] = None
|
|
76
82
|
|
|
77
83
|
|
xp/models/term/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Terminal UI models."""
|
|
2
2
|
|
|
3
|
+
from xp.models.term.accessory_state import AccessoryState
|
|
3
4
|
from xp.models.term.module_state import ModuleState
|
|
4
5
|
from xp.models.term.protocol_keys_config import (
|
|
5
6
|
ProtocolKeyConfig,
|
|
@@ -7,6 +8,7 @@ from xp.models.term.protocol_keys_config import (
|
|
|
7
8
|
)
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
11
|
+
"AccessoryState",
|
|
10
12
|
"ModuleState",
|
|
11
13
|
"ProtocolKeyConfig",
|
|
12
14
|
"ProtocolKeysConfig",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Accessory state data model for Homekit TUI."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AccessoryState:
|
|
10
|
+
"""
|
|
11
|
+
State of a HomeKit accessory for TUI display.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
room_name: Room containing the accessory (e.g., "Salon").
|
|
15
|
+
accessory_name: Accessory display name (e.g., "Variateur salon").
|
|
16
|
+
action: Action key (a-z) for toggle control.
|
|
17
|
+
output_state: Output state ("ON", "OFF", "?").
|
|
18
|
+
dimming_state: Dimming percentage for dimmable modules, "-" if OFF, empty otherwise.
|
|
19
|
+
module_name: Module identifier (e.g., "A12").
|
|
20
|
+
serial_number: Module serial number.
|
|
21
|
+
module_type: Module type (e.g., "XP24", "XP33LED").
|
|
22
|
+
error_status: Status code ("OK" or error like "E10").
|
|
23
|
+
output: Module output number (1-based for display).
|
|
24
|
+
sort: Sort accessories according to homekit.yml configuration.
|
|
25
|
+
last_update: Last communication timestamp. None if never updated.
|
|
26
|
+
toggle_action: Raw toggle action telegram (e.g., "E02L12I02").
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
room_name: str
|
|
30
|
+
accessory_name: str
|
|
31
|
+
action: str
|
|
32
|
+
output_state: str
|
|
33
|
+
dimming_state: str
|
|
34
|
+
module_name: str
|
|
35
|
+
serial_number: str
|
|
36
|
+
module_type: str
|
|
37
|
+
error_status: str
|
|
38
|
+
output: int
|
|
39
|
+
sort: int
|
|
40
|
+
last_update: Optional[datetime] = None
|
|
41
|
+
toggle_action: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
def is_dimmable(self) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Check if accessory is dimmable.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
True if module type is XP33LR or XP33LED, False otherwise.
|
|
49
|
+
"""
|
|
50
|
+
return self.module_type in ("XP33LR", "XP33LED")
|
|
@@ -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}")
|