zencontrol-python 0.1.6__py3-none-any.whl → 0.1.7__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.
@@ -9,7 +9,7 @@ if str(ROOT) not in sys.path:
9
9
 
10
10
  import yaml
11
11
 
12
- from zencontrol.api.models import ZenAddress, ZenController
12
+ from zencontrol import ZenAddress, ZenController
13
13
  from zencontrol.api.protocol import ZenProtocol
14
14
  from zencontrol.api.types import ZenAddressType, ZenEventCode
15
15
  from zencontrol.utils import run_with_keyboard_interrupt
examples/mqtt_bridge.py CHANGED
@@ -10,12 +10,18 @@ import zencontrol
10
10
  from zencontrol import ZenController, ZenProtocol, ZenClient, ZenColour, ZenColourType, ZenProfile, ZenLight, ZenGroup, ZenButton, ZenMotionSensor, ZenSystemVariable, ZenTimeoutError, ZenAddressType
11
11
  from zencontrol.api.types import Const as ApiConst
12
12
  import aiomqtt
13
- from colorama import Fore, Back, Style
14
13
  import logging
15
14
  from logging.handlers import RotatingFileHandler
16
15
  import math
17
16
  import pickle
18
17
  import traceback
18
+ try:
19
+ from colorama import Fore, Style
20
+ except ImportError:
21
+ class _NoColor:
22
+ def __getattr__(self, _name: str) -> str:
23
+ return ""
24
+ Fore = Style = _NoColor() # type: ignore[assignment]
19
25
 
20
26
  class RateLimiter:
21
27
  """Rate limiter to control concurrent coroutine execution"""
zencontrol/__init__.py CHANGED
@@ -33,33 +33,53 @@ Example usage:
33
33
  """
34
34
 
35
35
  # High-level interface (recommended for most users)
36
+ # API-level models (used by zen_api)
37
+ from .api.models import DiscoveredController, ZenAddress, ZenColour, ZenInstance
38
+ from .api.protocol import ZenProtocol
39
+
40
+ # Shared types and exceptions
41
+ from .api.types import (
42
+ ZenAddressType,
43
+ ZenColourType,
44
+ ZenEventCode,
45
+ ZenEventMask,
46
+ ZenEventMode,
47
+ ZenInstanceType,
48
+ )
49
+ from .exceptions import (
50
+ ZenConfigurationError,
51
+ ZenConnectionError,
52
+ ZenError,
53
+ ZenResponseError,
54
+ ZenTimeoutError,
55
+ )
36
56
  from .interface import (
57
+ ZenAbsoluteInput,
58
+ ZenButton,
37
59
  ZenControl,
38
60
  ZenController,
39
- ZenProfile,
40
- ZenLight,
41
61
  ZenGroup,
42
- ZenButton,
43
- ZenAbsoluteInput,
62
+ ZenLight,
44
63
  ZenMotionSensor,
64
+ ZenProfile,
45
65
  ZenSystemVariable,
46
66
  )
47
67
 
48
- # API-level models (used by zen_api)
49
- from .api.models import ZenAddress, ZenInstance, ZenColour, DiscoveredController
50
- from .api.protocol import ZenProtocol
51
-
52
68
  # Low-level models (used by zen_io)
53
- from .io import ZenClient, ZenListener, ZenEvent, Request, Response, ResponseType, RequestType
54
-
55
- # Shared types and exceptions
56
- from .api.types import ZenAddressType, ZenInstanceType, ZenColourType, ZenEventCode, ZenEventMask, ZenEventMode
57
- from .exceptions import ZenError, ZenTimeoutError, ZenResponseError, ZenConnectionError, ZenConfigurationError
69
+ from .io import (
70
+ Request,
71
+ RequestType,
72
+ Response,
73
+ ResponseType,
74
+ ZenClient,
75
+ ZenEvent,
76
+ ZenListener,
77
+ )
58
78
 
59
79
  # Utilities
60
80
  from .utils import run_with_keyboard_interrupt
61
81
 
62
- __version__ = "0.1.6"
82
+ __version__ = "0.1.7"
63
83
  __author__ = "Simon Wright"
64
84
 
65
85
  # Public API - these are the main classes users should import
@@ -8,9 +8,22 @@ This module contains models and types that belong to the API layer:
8
8
  - Types and enums used by the API layer
9
9
  """
10
10
 
11
- from .models import ZenController, ZenAddress, ZenInstance, ZenColour, ZenProfile, DiscoveredController
11
+ from .models import (
12
+ DiscoveredController,
13
+ ZenAddress,
14
+ ZenColour,
15
+ ZenController,
16
+ ZenInstance,
17
+ ZenProfile,
18
+ )
12
19
  from .protocol import ZenProtocol
13
- from .types import ZenAddressType, ZenInstanceType, ZenColourType, ZenEventMask, ZenEventMode
20
+ from .types import (
21
+ ZenAddressType,
22
+ ZenColourType,
23
+ ZenEventMask,
24
+ ZenEventMode,
25
+ ZenInstanceType,
26
+ )
14
27
 
15
28
  __all__ = [
16
29
  # API-level models
zencontrol/api/models.py CHANGED
@@ -12,10 +12,16 @@ import socket
12
12
  import struct
13
13
  import time
14
14
  from dataclasses import dataclass, field
15
- from typing import Any, Self
15
+ from typing import TYPE_CHECKING, Any, Self
16
16
 
17
17
  from ..io import ZenClient
18
- from .types import ZenAddressType, ZenInstanceType, ZenColourType, Const
18
+ from .types import Const, ZenAddressType, ZenColourType, ZenInstanceType
19
+
20
+ if TYPE_CHECKING:
21
+ # Addresses are only ever built from registered controllers, which are
22
+ # interface-layer objects. Type-only import; at runtime the interface layer
23
+ # imports this module, so importing it here would be circular.
24
+ from ..interface.interface import ZenController as ZenInterfaceController
19
25
 
20
26
 
21
27
  DEFAULT_CONTROLLER_PORT = 5108
@@ -33,8 +39,13 @@ class DiscoveredController:
33
39
 
34
40
  @dataclass
35
41
  class ZenController:
36
- """Represents a ZenControl controller
37
-
42
+ """Base class holding a controller's config and transport state.
43
+
44
+ ``zencontrol.ZenController`` (the interface layer) subclasses this and adds
45
+ entity state; that subclass is what ``ZenControl.add_controller()`` returns
46
+ and what registered controllers always are. This base exists so the API
47
+ layer can talk about controllers without importing the interface layer.
48
+
38
49
  The 'host' field can be any resolvable hostname or IP address.
39
50
  The 'ip' property will resolve the hostname to an IP address and cache it.
40
51
  """
@@ -55,7 +66,7 @@ class ZenController:
55
66
  client: ZenClient | None = None
56
67
  _ip: str | None = field(init=False, repr=False, default=None)
57
68
 
58
- def __post_init__(self):
69
+ def __post_init__(self) -> None:
59
70
  self._update_mac_bytes(self.mac)
60
71
 
61
72
  def __setattr__(self, name: str, value: object) -> None:
@@ -104,14 +115,14 @@ class ZenController:
104
115
  @dataclass(slots=True)
105
116
  class ZenAddress:
106
117
  """Represents a DALI address"""
107
- controller: ZenController
118
+ controller: ZenInterfaceController
108
119
  type: ZenAddressType
109
120
  number: int
110
121
  label: str | None = field(default=None, init=False)
111
122
  serial: str | None = field(default=None, init=False)
112
123
 
113
124
  @classmethod
114
- def broadcast(cls, controller: ZenController) -> Self:
125
+ def broadcast(cls, controller: ZenInterfaceController) -> Self:
115
126
  return cls(controller=controller, type=ZenAddressType.BROADCAST, number=255)
116
127
 
117
128
  def ecg(self) -> int:
@@ -152,7 +163,7 @@ class ZenAddress:
152
163
  """Return a stable HA-friendly identifier for this address."""
153
164
  return f"{self.type.name.casefold()}{self.number}"
154
165
 
155
- def __post_init__(self):
166
+ def __post_init__(self) -> None:
156
167
  match self.type:
157
168
  case ZenAddressType.BROADCAST:
158
169
  if self.number != 255:
@@ -176,7 +187,7 @@ class ZenInstance:
176
187
  number: int
177
188
  active: bool | None = None
178
189
  error: bool | None = None
179
- def __post_init__(self):
190
+ def __post_init__(self) -> None:
180
191
  if not 0 <= self.number < Const.MAX_INSTANCE:
181
192
  raise ValueError(f"Instance number must be between 0 and {Const.MAX_INSTANCE-1}, received {self.number}")
182
193
 
@@ -202,7 +213,11 @@ class ZenColour:
202
213
  @classmethod
203
214
  def from_bytes(cls, data: bytes) -> Self | None:
204
215
  match list(data):
205
- case [ZenColourType.RGBWAF.value, r, g, b, w, a, f]:
216
+ case [ZenColourType.RGBWAF.value, r, g, b, *rest] if len(rest) <= 3:
217
+ # COLOUR_CHANGED_EVENT from a fixture with fewer than six channels
218
+ # carries only channels + 1 bytes, so an RGB fixture sends
219
+ # [0x80, R, G, B]. Channels the fixture does not have stay None.
220
+ w, a, f = (list(rest) + [None, None, None])[:3]
206
221
  return cls(type=ZenColourType.RGBWAF, r=r, g=g, b=b, w=w, a=a, f=f)
207
222
  case [ZenColourType.TC.value, hi, lo] | [ZenColourType.TC.value, hi, lo, *_]:
208
223
  if len(data) not in (3, 7):
@@ -215,7 +230,7 @@ class ZenColour:
215
230
  case _:
216
231
  return None
217
232
 
218
- def __post_init__(self):
233
+ def __post_init__(self) -> None:
219
234
  match self.type:
220
235
  case ZenColourType.TC:
221
236
  kelvin = self.kelvin
@@ -310,6 +325,6 @@ class ZenProfile:
310
325
  address: ZenAddress
311
326
  profile: int
312
327
 
313
- def __post_init__(self):
328
+ def __post_init__(self) -> None:
314
329
  if not (0 <= self.profile <= 255):
315
330
  raise ValueError(f"Profile must be 0-255, got {self.profile}")
@@ -1,29 +1,57 @@
1
- from collections.abc import Callable, Awaitable, Coroutine
2
1
  import asyncio
3
- import socket
2
+ import logging
4
3
  import struct
5
4
  import time
6
- import logging
7
5
  import traceback
6
+ from collections.abc import Awaitable, Callable, Coroutine
8
7
  from datetime import datetime as dt
9
- from typing import Any, Self, Literal, overload
10
- from enum import Enum
11
- from colorama import Fore, Back, Style
12
- from dataclasses import dataclass, field
13
-
14
- from ..io import ZenClient, ZenListener, ZenEvent, Request, Response, ResponseType, RequestType, EventConst, ClientConst
8
+ from typing import TYPE_CHECKING, Any, Literal, Self, overload
9
+
10
+ from ..exceptions import ZenTimeoutError
11
+ from ..io import (
12
+ ClientConst,
13
+ EventConst,
14
+ Request,
15
+ RequestType,
16
+ Response,
17
+ ResponseType,
18
+ ZenClient,
19
+ ZenEvent,
20
+ ZenListener,
21
+ )
22
+ from ..utils import local_ip_for_remote
15
23
  from .models import (
16
24
  DEFAULT_CONTROLLER_PORT,
17
25
  DiscoveredController,
18
- ZenController,
19
26
  ZenAddress,
20
- ZenInstance,
21
27
  ZenColour,
22
- ZenProfile,
28
+ ZenController,
29
+ ZenInstance,
23
30
  )
24
- from .types import ZenAddressType, ZenInstanceType, ZenColourType, ZenEventCode, ZenEventMask, ZenEventMode, ZenErrorCode, Const
25
- from ..exceptions import ZenError, ZenTimeoutError
26
- from ..utils import local_ip_for_remote
31
+ from .types import (
32
+ Const,
33
+ ZenAddressType,
34
+ ZenErrorCode,
35
+ ZenEventCode,
36
+ ZenEventMask,
37
+ ZenEventMode,
38
+ ZenInstanceType,
39
+ )
40
+
41
+ # Magic import to avoid dependency on colorama
42
+ try:
43
+ from colorama import Fore, Style
44
+ except ImportError:
45
+ class _NoColor:
46
+ def __getattr__(self, _name: str) -> str:
47
+ return ""
48
+ Fore = Style = _NoColor() # type: ignore[assignment]
49
+
50
+ if TYPE_CHECKING:
51
+ # Registered controllers are always interface-layer objects, so anything
52
+ # that hands out ZenAddress instances is typed with that class. Type-only
53
+ # import; importing the interface layer at runtime would be circular.
54
+ from ..interface.interface import ZenController as ZenInterfaceController
27
55
 
28
56
 
29
57
  def _mac_bytes_to_str(mac: bytes) -> str:
@@ -218,7 +246,7 @@ class ZenProtocol:
218
246
  unicast: bool = False,
219
247
  listen_ip: str | None = None,
220
248
  listen_port: int | None = None,
221
- cache: dict[bytes, dict[str, Any]] | None = None):
249
+ cache: dict[bytes, dict[str, Any]] | None = None) -> None:
222
250
  self.logger = logger or logging.getLogger('null')
223
251
  if logger is None:
224
252
  self.logger.addHandler(logging.NullHandler())
@@ -236,7 +264,7 @@ class ZenProtocol:
236
264
  self.local_ip = self.listen_ip
237
265
  # Setup event monitoring using ZenListener
238
266
  self.event_listener: ZenListener | None = None
239
- self.event_task = None
267
+ self.event_task: asyncio.Task[None] | None = None
240
268
  # Prevents double on_disconnect if stop races with listener death
241
269
  self._disconnect_notified = False
242
270
 
@@ -254,7 +282,7 @@ class ZenProtocol:
254
282
  self.disconnect_callback: Callable[[], Awaitable[None]] | None = None
255
283
 
256
284
  # Controllers will be assigned later
257
- self.controllers = []
285
+ self.controllers: list[ZenInterfaceController] = []
258
286
  # Controllers identified via multicast but not yet registered
259
287
  self.identified_controllers: list[DiscoveredController] = []
260
288
  self._discovery_pending_macs: set[str] = set()
@@ -267,11 +295,11 @@ class ZenProtocol:
267
295
  # Fire-and-forget work (delayed events, refresh timers, motion holds)
268
296
  self._bg_tasks: set[asyncio.Task[Any]] = set()
269
297
 
270
- async def __aenter__(self):
298
+ async def __aenter__(self) -> Self:
271
299
  """Async context manager entry"""
272
300
  return self
273
301
 
274
- async def __aexit__(self, exc_type, exc_val, exc_tb):
302
+ async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
275
303
  """Async context manager exit"""
276
304
  await self.aclose()
277
305
 
@@ -306,7 +334,7 @@ class ZenProtocol:
306
334
  await asyncio.gather(*tasks, return_exceptions=True)
307
335
  self._bg_tasks.clear()
308
336
 
309
- async def aclose(self):
337
+ async def aclose(self) -> None:
310
338
  """Stop monitoring, cancel background tasks, and close UDP clients."""
311
339
  await self.stop_event_monitoring()
312
340
  await self._cancel_background_tasks()
@@ -321,7 +349,7 @@ class ZenProtocol:
321
349
  pass
322
350
  self.clear_entity_cache()
323
351
 
324
- def set_controllers(self, controllers: list[ZenController]):
352
+ def set_controllers(self, controllers: list[ZenInterfaceController]) -> None:
325
353
  self.controllers = controllers # Used to match events to controllers, and include controller objects in callbacks
326
354
  # Drop identified entries that are now registered
327
355
  for ctrl in controllers:
@@ -438,8 +466,6 @@ class ZenProtocol:
438
466
  ) -> (bytes | str | list[int] | int | bool) | None:
439
467
  request: Request = Request(command=command, data=[address] + (data or []), request_type=RequestType.BASIC)
440
468
  response_data, response_code = await self._send_packet(controller, request, cacheable=cacheable)
441
- if response_data is None and response_code is None:
442
- return None
443
469
  match response_code:
444
470
  case 0xA0: # OK
445
471
  match return_type:
@@ -527,7 +553,7 @@ class ZenProtocol:
527
553
  pass # Answer is in data bytes
528
554
  case 0xA2: # NO_ANSWER
529
555
  if response_data is not None and len(response_data) > 0:
530
- self.logger.error(f"No answer with code: {response_data}")
556
+ self.logger.error("No answer with code: %r", response_data)
531
557
  return None
532
558
  case 0xA3: # ERROR
533
559
  self._log_response_error(response_data)
@@ -565,7 +591,7 @@ class ZenProtocol:
565
591
  if self.print_traffic:
566
592
  cached_packet = bytes([cached_response_type & 0xFF, len(cached_data) & 0xFF]) + cached_data
567
593
  print(Fore.MAGENTA + f"FOUND: [----, {' '.join(f'0x{b:02X}' for b in cache_key)}, ----] "
568
- + Fore.RED + Style.DIM + f" CACHE HIT"
594
+ + Fore.RED + Style.DIM + " CACHE HIT"
569
595
  + Style.BRIGHT + Fore.CYAN + f" [{' '.join(f'0x{b:02X}' for b in cached_packet)}, ----]"
570
596
  + Style.RESET_ALL)
571
597
  return cached_data, cached_response_type
@@ -661,7 +687,7 @@ class ZenProtocol:
661
687
  profile_change_callback: Callable[..., Awaitable[None]] | None = None,
662
688
  system_variable_change_callback: Callable[..., Awaitable[None]] | None = None,
663
689
  disconnect_callback: Callable[[], Awaitable[None]] | None = None,
664
- ):
690
+ ) -> None:
665
691
  self.button_press_callback = button_press_callback
666
692
  self.button_hold_callback = button_hold_callback
667
693
  self.absolute_input_callback = absolute_input_callback
@@ -674,7 +700,7 @@ class ZenProtocol:
674
700
  self.system_variable_change_callback = system_variable_change_callback
675
701
  self.disconnect_callback = disconnect_callback
676
702
 
677
- async def start_event_monitoring(self):
703
+ async def start_event_monitoring(self) -> None:
678
704
  if self.event_task and not self.event_task.done():
679
705
  # Event monitoring already running
680
706
  return
@@ -714,7 +740,7 @@ class ZenProtocol:
714
740
  except Exception as err:
715
741
  self.logger.error(f"disconnect_callback error: {err}")
716
742
 
717
- async def _async_event_listener(self):
743
+ async def _async_event_listener(self) -> None:
718
744
  """Async event listener using ZenListener"""
719
745
  # True unless stop_event_monitoring cancelled us intentionally
720
746
  unexpected = True
@@ -750,7 +776,7 @@ class ZenProtocol:
750
776
  if unexpected:
751
777
  await self.notify_disconnect()
752
778
 
753
- def get_controller_by_ip_mac(self, ip: str | None = None, mac: bytes | None = None):
779
+ def get_controller_by_ip_mac(self, ip: str | None = None, mac: bytes | None = None) -> ZenInterfaceController | None:
754
780
  """Find a controller by IP address or MAC address"""
755
781
  if ip is not None:
756
782
  for ctrl in self.controllers:
@@ -763,7 +789,7 @@ class ZenProtocol:
763
789
  return None
764
790
 
765
791
  def _ecd_address_from_target(
766
- self, controller: ZenController, target: int
792
+ self, controller: ZenInterfaceController, target: int
767
793
  ) -> ZenAddress | None:
768
794
  """Map an event target byte to an ECD ZenAddress, or None if out of range."""
769
795
  number = target - 64
@@ -853,7 +879,7 @@ class ZenProtocol:
853
879
  pass
854
880
  temp.client = None
855
881
 
856
- async def _process_zen_event(self, event: ZenEvent):
882
+ async def _process_zen_event(self, event: ZenEvent) -> None:
857
883
  """Process received ZenEvent from ZenListener"""
858
884
  typecast = "unicast" if self.unicast else "multicast"
859
885
 
@@ -864,7 +890,6 @@ class ZenProtocol:
864
890
  return
865
891
 
866
892
  ip_address = event.ip_address
867
- ip_port = event.ip_port
868
893
  target = event.target
869
894
  payload = event.payload
870
895
  event_code = event.event_code
@@ -890,9 +915,6 @@ class ZenProtocol:
890
915
  if len(payload) < min_payload:
891
916
  self.logger.warning(f"Event {event_enum.name} payload too short: {len(payload)} < {min_payload}")
892
917
  return
893
- # Get event name from ZenEventCode, with fallback to unknown, underscores replaced with spaces, and first letter capitalized
894
- event_name = event_enum.name.replace("_", " ").title()
895
-
896
918
  # Print the raw packet
897
919
  # if self.print_traffic:
898
920
  # print(Fore.MAGENTA + f"{typecast.upper()} {ip_address}:" +
@@ -1054,7 +1076,7 @@ class ZenProtocol:
1054
1076
  pass
1055
1077
 
1056
1078
 
1057
- async def stop_event_monitoring(self):
1079
+ async def stop_event_monitoring(self) -> None:
1058
1080
  """Stop listening for events"""
1059
1081
  if self.event_task:
1060
1082
  task = self.event_task
@@ -1114,8 +1136,9 @@ class ZenProtocol:
1114
1136
  return None
1115
1137
  return ZenEventMode.from_byte(response[0]).enabled
1116
1138
 
1117
- async def dali_add_tpi_event_filter(self, address: ZenAddress|ZenInstance, filter: ZenEventMask = ZenEventMask.all_events()) -> bool | None:
1139
+ async def dali_add_tpi_event_filter(self, address: ZenAddress|ZenInstance, filter: ZenEventMask | None = None) -> bool | None:
1118
1140
  """Stop specific events from an address/instance from being sent. Events in mask will be muted. Returns true if filter was added successfully."""
1141
+ if filter is None: filter = ZenEventMask.all_events()
1119
1142
  instance_number = 0xFF
1120
1143
  if isinstance(address, ZenInstance):
1121
1144
  instance: ZenInstance = address
@@ -1127,8 +1150,9 @@ class ZenProtocol:
1127
1150
  [instance_number, filter.upper(), filter.lower()],
1128
1151
  return_type='bool')
1129
1152
 
1130
- async def dali_clear_tpi_event_filter(self, address: ZenAddress|ZenInstance, unfilter: ZenEventMask = ZenEventMask.all_events()) -> bool | None:
1153
+ async def dali_clear_tpi_event_filter(self, address: ZenAddress|ZenInstance, unfilter: ZenEventMask | None = None) -> bool | None:
1131
1154
  """Allow specific events from an address/instance to be sent again. Events in mask will be unmuted. Returns true if filter was cleared successfully."""
1155
+ if unfilter is None: unfilter = ZenEventMask.all_events()
1132
1156
  instance_number = 0xFF
1133
1157
  if isinstance(address, ZenInstance):
1134
1158
  instance: ZenInstance = address
@@ -1185,8 +1209,9 @@ class ZenProtocol:
1185
1209
 
1186
1210
  return results
1187
1211
 
1188
- async def tpi_event_emit(self, controller: ZenController, mode: ZenEventMode = ZenEventMode(enabled=True, filtering=False, unicast=False, multicast=True)) -> bool:
1212
+ async def tpi_event_emit(self, controller: ZenController, mode: ZenEventMode | None = None) -> bool:
1189
1213
  """Enable or disable TPI Event emission. Returns True if successful, else False."""
1214
+ if mode is None: mode = ZenEventMode(enabled=True, filtering=False, unicast=False, multicast=True)
1190
1215
  mask = mode.bitmask()
1191
1216
  # response = await self._send_basic(controller, self.CMD["ENABLE_TPI_EVENT_EMIT"], 0x00) # disable first to clear any existing state... I think this is a bug?
1192
1217
  response = await self._send_basic(controller, self.CMD["ENABLE_TPI_EVENT_EMIT"], mask)
@@ -1195,7 +1220,7 @@ class ZenProtocol:
1195
1220
  return True
1196
1221
  return False
1197
1222
 
1198
- async def set_tpi_event_unicast_address(self, controller: ZenController, ipaddr: str | None = None, port: int | None = None):
1223
+ async def set_tpi_event_unicast_address(self, controller: ZenController, ipaddr: str | None = None, port: int | None = None) -> bytes | None:
1199
1224
  """Configure TPI Events for Unicast mode with IP and port as defined in the ZenController instance."""
1200
1225
  data = [0,0,0,0,0,0]
1201
1226
  if port is not None:
@@ -1215,7 +1240,7 @@ class ZenProtocol:
1215
1240
  raise ValueError
1216
1241
  data[2:6] = ip_bytes
1217
1242
  except ValueError:
1218
- raise ValueError("Invalid IP address format")
1243
+ raise ValueError("Invalid IP address format") from None
1219
1244
 
1220
1245
  return await self._send_dynamic(controller, self.CMD["SET_TPI_EVENT_UNICAST_ADDRESS"], data)
1221
1246
 
@@ -1244,7 +1269,7 @@ class ZenProtocol:
1244
1269
  }
1245
1270
  return None
1246
1271
 
1247
- async def query_group_numbers(self, controller: ZenController) -> list[ZenAddress]:
1272
+ async def query_group_numbers(self, controller: ZenInterfaceController) -> list[ZenAddress]:
1248
1273
  """Query a controller for groups."""
1249
1274
  groups = await self._send_basic(controller, self.CMD["QUERY_GROUP_NUMBERS"], return_type='list')
1250
1275
  zen_groups = []
@@ -1443,7 +1468,7 @@ class ZenProtocol:
1443
1468
  return zen_groups
1444
1469
  return []
1445
1470
 
1446
- async def query_dali_addresses_with_instances(self, controller: ZenController, start_address: int=0) -> list[ZenAddress]: # TODO: automate iteration over start_address=0, start_address=60, etc.
1471
+ async def query_dali_addresses_with_instances(self, controller: ZenInterfaceController, start_address: int=0) -> list[ZenAddress]: # TODO: automate iteration over start_address=0, start_address=60, etc.
1447
1472
  """Query for DALI addresses that have instances associated with them.
1448
1473
 
1449
1474
  Due to payload restrictions, this needs to be called multiple times with different
@@ -1509,7 +1534,7 @@ class ZenProtocol:
1509
1534
  return f"{response[0]}.{response[1]}.{response[2]}"
1510
1535
  return None
1511
1536
 
1512
- async def query_control_gear_dali_addresses(self, controller: ZenController) -> list[ZenAddress]:
1537
+ async def query_control_gear_dali_addresses(self, controller: ZenInterfaceController) -> list[ZenAddress]:
1513
1538
  """Query which DALI control gear addresses are present in the database. Returns a list of ZenAddress instances."""
1514
1539
  response = await self._send_basic(controller, self.CMD["QUERY_CONTROL_GEAR_DALI_ADDRESSES"])
1515
1540
  if response and len(response) == 8: # 8 data bytes representing addresses 0-63
zencontrol/api/types.py CHANGED
@@ -7,9 +7,9 @@ This module contains types and enums that belong to the API layer:
7
7
  - Constants used by the API layer
8
8
  """
9
9
 
10
+ from dataclasses import dataclass
10
11
  from enum import Enum
11
12
  from typing import Self
12
- from dataclasses import dataclass
13
13
 
14
14
 
15
15
  class ZenAddressType(Enum):
@@ -105,7 +105,7 @@ class ZenEventMask:
105
105
  level_change_v2: bool = False
106
106
 
107
107
  @classmethod
108
- def all_events(cls):
108
+ def all_events(cls) -> Self:
109
109
  # Exclude deprecated level_change / group_level_change — use level_change_v2 (spec V2.001.121+)
110
110
  return cls(
111
111
  button_press = True,
zencontrol/exceptions.py CHANGED
@@ -6,6 +6,7 @@ This module defines all custom exceptions used throughout the library.
6
6
 
7
7
  from typing import Any
8
8
 
9
+
9
10
  class ZenError(Exception):
10
11
  """Base exception for Zen protocol errors"""
11
12
  pass
@@ -8,14 +8,14 @@ This module contains models that belong to the zen_interface layer:
8
8
  """
9
9
 
10
10
  from .interface import (
11
+ ZenAbsoluteInput,
12
+ ZenButton,
11
13
  ZenControl,
12
14
  ZenController,
13
- ZenProfile,
14
- ZenLight,
15
15
  ZenGroup,
16
- ZenButton,
17
- ZenAbsoluteInput,
16
+ ZenLight,
18
17
  ZenMotionSensor,
18
+ ZenProfile,
19
19
  ZenSystemVariable,
20
20
  )
21
21