conson-xp 2.0.2__py3-none-any.whl → 2.0.3__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: 2.0.2
3
+ Version: 2.0.3
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-2.0.2.dist-info/METADATA,sha256=9UzUpaHd8pegRLDFIPEzaMHr2L3ziBK7QhA2EYqSJ28,11319
2
- conson_xp-2.0.2.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
- conson_xp-2.0.2.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-2.0.2.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=qQYrRExAxcFFDY2HISDUiyYNIaz1ywu0tBMS6dAtJ9I,181
1
+ conson_xp-2.0.3.dist-info/METADATA,sha256=MacSE_YD6_kKpZQfPXnzZAs9YbYR__cvxg1d4ISQLXg,11319
2
+ conson_xp-2.0.3.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
+ conson_xp-2.0.3.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-2.0.3.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=Cd7Ua5HD9WOxxHs-xepjLjWZf4c-ue1kISZWRKiqgwM,181
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=9TGP3uTxAU-s-kVChvQ-Fn3-HVYj-QpeQ05Is-20HRo,4788
@@ -166,8 +166,8 @@ xp/services/telegram/telegram_output_service.py,sha256=9deqtcPndRqJ-3XQUWlJhXaVc
166
166
  xp/services/telegram/telegram_service.py,sha256=jPu0Xrh3IpvqPLyuQT5Vf8HHw00vBingONHdxf_9TkI,13315
167
167
  xp/services/telegram/telegram_version_service.py,sha256=oXnZ_K7OQ7xD-GEj3zDYp52KlkqVuHpO4bf7gMlC_w4,10574
168
168
  xp/services/term/__init__.py,sha256=BIeOK042bMR-0l6MA80wdW5VuHlpWOXtRER9IG5ilQA,245
169
- xp/services/term/homekit_accessory_driver.py,sha256=jHhHHOOvzdDjuYys1cUZHcH2uwZy_Y6o5aGAQqXGAVg,5935
170
- xp/services/term/homekit_service.py,sha256=8aEsmC-EwKTxf0Hxp0aFBdo4doUvhVHeVwfuuc1ttWw,28925
169
+ xp/services/term/homekit_accessory_driver.py,sha256=asIZz-1aJQv7m0oP28vwMWPh8x6WsMHN9pxJ0jIdUPk,7833
170
+ xp/services/term/homekit_service.py,sha256=FNm01Sp5F4bjJqRLMpl_FGbPgeuUkWxcu7sCBOihR0c,30892
171
171
  xp/services/term/protocol_monitor_service.py,sha256=5YBI0Nu7B7gMhaTbUhL6k9LSRfnCIj6CwrCYHiMHavA,10067
172
172
  xp/services/term/state_monitor_service.py,sha256=EK9tNBfamAIV0z0EMsXDYWC-rXv6l6k_bHsC8xyEFSo,17116
173
173
  xp/term/__init__.py,sha256=Xg2DhBeI3xQJLfc7_BPWI1por-rUXemyer5OtOt9Cus,51
@@ -191,4 +191,4 @@ xp/utils/logging.py,sha256=wJ1d-yg97NiZUrt2F8iDMcmnHVwC-PErcI-7dpyiRDc,3777
191
191
  xp/utils/serialization.py,sha256=TS1OwpTOemSvXsCGw3js4JkYYFEqkzrPe8V9QYQefdw,4684
192
192
  xp/utils/state_machine.py,sha256=W9AY4ntRZnFeHAa5d43hm37j53uJPlqkRvWTPiBhJ_0,2464
193
193
  xp/utils/time_utils.py,sha256=K17godWpL18VEypbTlvNOEDG6R3huYnf29yjkcnwRpU,3796
194
- conson_xp-2.0.2.dist-info/RECORD,,
194
+ conson_xp-2.0.3.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__ = "2.0.2"
7
+ __version__ = "2.0.3"
8
8
  __manufacturer__ = "salchichon"
9
9
  __model__ = "xp.cli"
10
10
  __serial__ = "2025.09.23.000"
@@ -10,9 +10,18 @@ from pyhap.const import CATEGORY_LIGHTBULB, CATEGORY_OUTLET
10
10
 
11
11
  from xp.models.homekit.homekit_config import HomekitConfig
12
12
 
13
+ # Callback type: (accessory_name, is_on, brightness_or_none)
14
+ OnSetCallback = Callable[[str, bool, Optional[int]], None]
15
+
13
16
 
14
17
  class XPAccessory(Accessory):
15
- """Single accessory wrapping a Conbus output."""
18
+ """
19
+ Single accessory wrapping a Conbus output.
20
+
21
+ Attributes:
22
+ logger: Logger instance for this accessory.
23
+ current_brightness: Current brightness value 0-100.
24
+ """
16
25
 
17
26
  def __init__(
18
27
  self,
@@ -35,12 +44,19 @@ class XPAccessory(Accessory):
35
44
  super().__init__(driver._driver, display_name, aid=aid)
36
45
  self._hk_driver = driver
37
46
  self._accessory_id = name
47
+ self._is_dimmable = service_type == "dimminglight"
48
+ self._char_brightness: Optional[object] = None
49
+ self._current_brightness: int = 100
38
50
  self.logger = logging.getLogger(__name__)
39
51
 
40
- if service_type == "dimminglight":
52
+ if self._is_dimmable:
41
53
  self.category = CATEGORY_LIGHTBULB
42
54
  serv = self.add_preload_service("Lightbulb", chars=["On", "Brightness"])
43
- # Note: Brightness setter_callback deferred to future update
55
+ self._char_brightness = serv.configure_char(
56
+ "Brightness",
57
+ setter_callback=self._set_brightness,
58
+ value=self._current_brightness,
59
+ )
44
60
  elif service_type == "outlet":
45
61
  self.category = CATEGORY_OUTLET
46
62
  serv = self.add_preload_service("Outlet")
@@ -58,16 +74,36 @@ class XPAccessory(Accessory):
58
74
  value: True for on, False for off.
59
75
  """
60
76
  if self._hk_driver._on_set:
61
- self._hk_driver._on_set(self._accessory_id, value)
77
+ self._hk_driver._on_set(self._accessory_id, value, None)
78
+
79
+ def _set_brightness(self, value: int) -> None:
80
+ """
81
+ Handle HomeKit set brightness request.
82
+
83
+ Args:
84
+ value: Brightness value 0-100.
85
+ """
86
+ if self._hk_driver._on_set:
87
+ self._hk_driver._on_set(self._accessory_id, True, value)
88
+ self._current_brightness = value
62
89
 
63
- def update_state(self, is_on: bool) -> None:
90
+ def update_state(self, is_on: bool, brightness: Optional[int] = None) -> None:
64
91
  """
65
92
  Update accessory state from Conbus event.
66
93
 
67
94
  Args:
68
95
  is_on: True if accessory is on, False otherwise.
96
+ brightness: Optional brightness value 0-100.
69
97
  """
70
98
  self._char_on.set_value(is_on)
99
+ if brightness is not None and self._char_brightness:
100
+ self._char_brightness.set_value(brightness) # type: ignore[attr-defined]
101
+ self._current_brightness = brightness
102
+
103
+ @property
104
+ def current_brightness(self) -> int:
105
+ """Get current brightness value."""
106
+ return self._current_brightness
71
107
 
72
108
 
73
109
  class HomekitAccessoryDriver:
@@ -84,14 +120,15 @@ class HomekitAccessoryDriver:
84
120
  self._homekit_config = homekit_config
85
121
  self._driver: Optional[AccessoryDriver] = None
86
122
  self._accessories: Dict[str, XPAccessory] = {}
87
- self._on_set: Optional[Callable[[str, bool], None]] = None
123
+ self._on_set: Optional[OnSetCallback] = None
88
124
 
89
- def set_callback(self, on_set: Callable[[str, bool], None]) -> None:
125
+ def set_callback(self, on_set: OnSetCallback) -> None:
90
126
  """
91
127
  Set callback for HomeKit set events.
92
128
 
93
129
  Args:
94
- on_set: Callback(accessory_name, is_on) called when HomeKit app toggles.
130
+ on_set: Callback(accessory_name, is_on, brightness) called when HomeKit app changes state.
131
+ brightness is None for on/off only, or 0-100 for dimming.
95
132
  """
96
133
  self._on_set = on_set
97
134
 
@@ -157,15 +194,34 @@ class HomekitAccessoryDriver:
157
194
  except Exception as e:
158
195
  self.logger.error(f"Error stopping AccessoryDriver: {e}", exc_info=True)
159
196
 
160
- def update_state(self, accessory_name: str, is_on: bool) -> None:
197
+ def update_state(
198
+ self, accessory_name: str, is_on: bool, brightness: Optional[int] = None
199
+ ) -> None:
161
200
  """
162
201
  Update accessory state from Conbus event.
163
202
 
164
203
  Args:
165
204
  accessory_name: Accessory name to update.
166
205
  is_on: True if accessory is on, False otherwise.
206
+ brightness: Optional brightness value 0-100.
167
207
  """
168
- if acc := self._accessories.get(accessory_name):
169
- acc.update_state(is_on)
208
+ acc = self._accessories.get(accessory_name)
209
+ if acc:
210
+ acc.update_state(is_on, brightness)
170
211
  else:
171
212
  self.logger.warning(f"Unknown accessory name: {accessory_name}")
213
+
214
+ def get_brightness(self, accessory_name: str) -> int:
215
+ """
216
+ Get current brightness for an accessory.
217
+
218
+ Args:
219
+ accessory_name: Accessory name.
220
+
221
+ Returns:
222
+ Current brightness 0-100, defaults to 100 if not found.
223
+ """
224
+ acc = self._accessories.get(accessory_name)
225
+ if acc:
226
+ return acc.current_brightness
227
+ return 100
@@ -300,23 +300,82 @@ class HomekitService:
300
300
  await self._accessory_driver.stop()
301
301
  self.cleanup()
302
302
 
303
- def _on_homekit_set(self, accessory_name: str, is_on: bool) -> None:
303
+ def _on_homekit_set(
304
+ self, accessory_name: str, is_on: bool, brightness: Optional[int]
305
+ ) -> None:
304
306
  """
305
- Handle HomeKit app toggle request.
307
+ Handle HomeKit app set request (on/off or brightness).
306
308
 
307
309
  Args:
308
310
  accessory_name: Accessory name from HomeKit.
309
311
  is_on: True for on, False for off.
312
+ brightness: Brightness value 0-100, or None for on/off only.
310
313
  """
311
314
  config = self._find_accessory_config(accessory_name)
312
- if config:
315
+ if not config:
316
+ self.logger.warning(f"No config found for accessory: {accessory_name}")
317
+ return
318
+
319
+ if brightness is not None:
320
+ # Handle brightness change
321
+ self._handle_brightness_change(accessory_name, config, brightness)
322
+ else:
323
+ # Handle on/off toggle
313
324
  action = config.on_action if is_on else config.off_action
314
325
  self.send_action(action)
315
326
  self.on_status_message.emit(
316
327
  f"HomeKit: {accessory_name} {'ON' if is_on else 'OFF'}"
317
328
  )
329
+
330
+ def _handle_brightness_change(
331
+ self,
332
+ accessory_name: str,
333
+ config: "HomekitAccessoryConfig",
334
+ target_brightness: int,
335
+ ) -> None:
336
+ """
337
+ Handle brightness change by sending dimup/dimdown actions.
338
+
339
+ Calculates delta from current brightness and sends appropriate
340
+ number of LEVELINC or LEVELDEC commands (step = 10%).
341
+
342
+ Args:
343
+ accessory_name: Accessory name.
344
+ config: Accessory configuration.
345
+ target_brightness: Target brightness 0-100.
346
+ """
347
+ current = self._accessory_driver.get_brightness(accessory_name)
348
+ delta = target_brightness - current
349
+
350
+ if delta == 0:
351
+ return
352
+
353
+ # Determine action and steps (10% per step)
354
+ step_size = 10
355
+ steps = abs(delta) // step_size
356
+
357
+ if delta > 0:
358
+ # Increase brightness
359
+ if not config.dimup_action:
360
+ self.logger.warning(f"No dimup_action for {accessory_name}")
361
+ return
362
+ action = config.dimup_action
363
+ direction = "+"
318
364
  else:
319
- self.logger.warning(f"No config found for accessory: {accessory_name}")
365
+ # Decrease brightness
366
+ if not config.dimdown_action:
367
+ self.logger.warning(f"No dimdown_action for {accessory_name}")
368
+ return
369
+ action = config.dimdown_action
370
+ direction = "-"
371
+
372
+ # Send action for each step
373
+ for _ in range(steps):
374
+ self.send_action(action)
375
+
376
+ self.on_status_message.emit(
377
+ f"HomeKit: {accessory_name} {current}% → {target_brightness}% ({direction}{steps * step_size}%)"
378
+ )
320
379
 
321
380
  def send_action(self, action: str) -> None:
322
381
  """