conson-xp 2.0.2__py3-none-any.whl → 2.0.4__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.4
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.4.dist-info/METADATA,sha256=CJMAU_3xThp1pBfy62H2HtM-hDsN8T9UxxqFRTflo0o,11319
2
+ conson_xp-2.0.4.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
+ conson_xp-2.0.4.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-2.0.4.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=8uU6iQSfgIQU3EC4KbiWh-hGKhEpZZZ7YaahwbEQvaE,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
@@ -22,7 +22,7 @@ xp/cli/commands/conbus/conbus_linknumber_commands.py,sha256=3bkik8nHXY89XfzeUnKW
22
22
  xp/cli/commands/conbus/conbus_modulenumber_commands.py,sha256=fOIO0f85iDJBfGnRCEN8zK6j4i43uuF_9YKQc_nQ39A,3628
23
23
  xp/cli/commands/conbus/conbus_msactiontable_commands.py,sha256=oMHfrxDR9yj0Pvx92ZHnwYU3CZqwNNvfAfEoLuo_jhQ,11585
24
24
  xp/cli/commands/conbus/conbus_output_commands.py,sha256=XMEMb5tM0ul7lwhwoo4QRgWprXZ31qswXBno8KCeFMo,5342
25
- xp/cli/commands/conbus/conbus_raw_commands.py,sha256=o1dVZqiRw7J3_8Bge7DJucFyAqt7GNP-Azf-f3ir3SU,2019
25
+ xp/cli/commands/conbus/conbus_raw_commands.py,sha256=bCFF1q61VSfdwS_Ch7jTbv-Q-t6n_EwEHRw_wlvNrr0,1984
26
26
  xp/cli/commands/conbus/conbus_receive_commands.py,sha256=LdxoOUdM5atkC8fFlp-GK7HQ7oxTnjrSFDx-lQ3nme0,1925
27
27
  xp/cli/commands/conbus/conbus_scan_commands.py,sha256=QuRT4Vx5TqervCOUsZ4wcxvy7Jwra4hl2-DIt8cDcoE,1828
28
28
  xp/cli/commands/file_commands.py,sha256=dx6a4xNxuReXq7dOyL8ebn6lyifzDm0d0OsuDjyr_90,5547
@@ -136,7 +136,7 @@ xp/services/conbus/conbus_event_raw_service.py,sha256=viXuEXw165-RytdqC76wQShJLD
136
136
  xp/services/conbus/conbus_export_actiontable_service.py,sha256=d6Y2RDiVOH-jTMWOIE_gY_WBJnHFrbJGguk-WZWbNPs,11818
137
137
  xp/services/conbus/conbus_export_service.py,sha256=RP8nADTIs4FGUf_BFLRZMtEJZdXV94zg3QrlWaDnhKA,17536
138
138
  xp/services/conbus/conbus_output_service.py,sha256=e57bRkLgPnJuB8hkllNh0kgGkjPt9IK75tuBxd_bOkE,9361
139
- xp/services/conbus/conbus_raw_service.py,sha256=OQuV521VOQraf2PGF2B9868vh7sDgmfc19YebrkZnyw,5844
139
+ xp/services/conbus/conbus_raw_service.py,sha256=xmFotLJqPU8CSDumB2gnxqLW7OO-Jlc7WyS0f2cgayA,5967
140
140
  xp/services/conbus/conbus_receive_service.py,sha256=TFf3W65brGsy6QZICpIs0Xy9bgqyL1vgQuhS_eHuIZs,5416
141
141
  xp/services/conbus/conbus_scan_service.py,sha256=_Ka0OUDNYhDgZIR49Q0P5GTxJq6RcAAX2DVqEDdtb5U,6888
142
142
  xp/services/conbus/write_config_service.py,sha256=BCfmLNPRDpwSwRMRYJvx2FXA8IZsdgmyeTXIYvmb4ys,9004
@@ -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.4.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.4"
8
8
  __manufacturer__ = "salchichon"
9
9
  __model__ = "xp.cli"
10
10
  __serial__ = "2025.09.23.000"
@@ -14,25 +14,24 @@ from xp.services.conbus.conbus_raw_service import ConbusRawService
14
14
 
15
15
 
16
16
  @conbus.command("raw")
17
- @click.argument("raw_telegrams")
17
+ @click.argument("telegrams", nargs=-1, required=True)
18
18
  @click.pass_context
19
19
  @connection_command()
20
- def send_raw_telegrams(ctx: Context, raw_telegrams: str) -> None:
20
+ def send_raw_telegrams(ctx: Context, telegrams: tuple[str, ...]) -> None:
21
21
  r"""
22
22
  Send raw telegram sequence to Conbus server.
23
23
 
24
- Accepts a string containing one or more telegrams in format <...>.
25
- Multiple telegrams should be concatenated without separators.
24
+ Accepts one or more telegrams in format S2113010000F02D (without <> nor CHECKSUM) as separate arguments.
26
25
 
27
26
  Args:
28
27
  ctx: Click context object.
29
- raw_telegrams: Raw telegram string(s).
28
+ telegrams: Raw telegram string(s).
30
29
 
31
30
  Examples:
32
31
  \b
33
- xp conbus raw '<S2113010000F02D12>'
34
- xp conbus raw '<S2113010000F02D12><S2113010001F02D12><S2113010002F02D12>'
35
- xp conbus raw '<S0012345003F02D12FM>...<S0012345009F02D12FF>'
32
+ xp conbus raw 'S2113010000F02D'
33
+ xp conbus raw 'S2113010000F02D' 'S2113010001F02D'
34
+ xp conbus raw 'S0012345003F02D12F' 'S0012345009F02D12'
36
35
  """
37
36
  service: ConbusRawService = (
38
37
  ctx.obj.get("container").get_container().resolve(ConbusRawService)
@@ -62,8 +61,8 @@ def send_raw_telegrams(ctx: Context, raw_telegrams: str) -> None:
62
61
  service.on_progress.connect(on_progress)
63
62
  service.on_finish.connect(on_finish)
64
63
  # Setup
65
- service.send_raw_telegram(
66
- raw_input=raw_telegrams,
64
+ service.send_raw_telegrams(
65
+ telegrams=list(telegrams),
67
66
  timeout_seconds=5.0,
68
67
  )
69
68
  # Start (blocks until completion)
@@ -51,7 +51,7 @@ class ConbusRawService:
51
51
  self.conbus_protocol.on_timeout.connect(self.timeout)
52
52
  self.conbus_protocol.on_failed.connect(self.failed)
53
53
 
54
- self.raw_input: str = ""
54
+ self.telegrams: list[str] = []
55
55
  self.service_response: ConbusRawResponse = ConbusRawResponse(
56
56
  success=False,
57
57
  )
@@ -60,8 +60,11 @@ class ConbusRawService:
60
60
 
61
61
  def connection_made(self) -> None:
62
62
  """Handle connection established event."""
63
- self.logger.debug(f"Connection established, sending {self.raw_input}")
64
- self.conbus_protocol.send_raw_telegram(self.raw_input)
63
+ self.logger.debug(
64
+ f"Connection established, sending {len(self.telegrams)} telegrams"
65
+ )
66
+ for telegram in self.telegrams:
67
+ self.conbus_protocol.send_raw_telegram(telegram)
65
68
 
66
69
  def telegram_sent(self, telegram_sent: str) -> None:
67
70
  """
@@ -108,22 +111,22 @@ class ConbusRawService:
108
111
  self.service_response.error = message
109
112
  self.on_finish.emit(self.service_response)
110
113
 
111
- def send_raw_telegram(
114
+ def send_raw_telegrams(
112
115
  self,
113
- raw_input: str,
116
+ telegrams: list[str],
114
117
  timeout_seconds: Optional[float] = None,
115
118
  ) -> None:
116
119
  """
117
- Send a raw telegram string to the Conbus server.
120
+ Send raw telegrams to the Conbus server.
118
121
 
119
122
  Args:
120
- raw_input: Raw telegram string to send.
123
+ telegrams: List of raw telegram strings to send.
121
124
  timeout_seconds: Timeout in seconds.
122
125
  """
123
- self.logger.info("Starting send_raw_telegram")
126
+ self.logger.info(f"Starting send_raw_telegrams with {len(telegrams)} telegrams")
124
127
  if timeout_seconds:
125
128
  self.conbus_protocol.timeout_seconds = timeout_seconds
126
- self.raw_input = raw_input
129
+ self.telegrams = telegrams
127
130
 
128
131
  def set_timeout(self, timeout_seconds: float) -> None:
129
132
  """
@@ -151,7 +154,7 @@ class ConbusRawService:
151
154
  """
152
155
  # Reset state for singleton reuse
153
156
  self.service_response = ConbusRawResponse(success=False)
154
- self.raw_input = ""
157
+ self.telegrams = []
155
158
  return self
156
159
 
157
160
  def __exit__(
@@ -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
  """