python-izone 1.3.6__tar.gz → 1.3.7__tar.gz

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.
Files changed (45) hide show
  1. {python_izone-1.3.6 → python_izone-1.3.7}/PKG-INFO +1 -1
  2. {python_izone-1.3.6 → python_izone-1.3.7}/docs/release-notes/v1.3.6.md +20 -48
  3. python_izone-1.3.7/docs/release-notes/v1.3.7.md +7 -0
  4. {python_izone-1.3.6 → python_izone-1.3.7}/pizone/__init__.py +7 -1
  5. {python_izone-1.3.6 → python_izone-1.3.7}/pizone/discovery.py +10 -6
  6. {python_izone-1.3.6 → python_izone-1.3.7}/pizone/exceptions.py +8 -0
  7. {python_izone-1.3.6 → python_izone-1.3.7}/pizone/version.py +2 -2
  8. {python_izone-1.3.6 → python_izone-1.3.7}/tests/test_discovery_api.py +4 -3
  9. {python_izone-1.3.6 → python_izone-1.3.7}/.gitattributes +0 -0
  10. {python_izone-1.3.6 → python_izone-1.3.7}/.github/workflows/main.yml +0 -0
  11. {python_izone-1.3.6 → python_izone-1.3.7}/.gitignore +0 -0
  12. {python_izone-1.3.6 → python_izone-1.3.7}/.pre-commit-config.yaml +0 -0
  13. {python_izone-1.3.6 → python_izone-1.3.7}/.python-version +0 -0
  14. {python_izone-1.3.6 → python_izone-1.3.7}/.vscode/.ropeproject/config.py +0 -0
  15. {python_izone-1.3.6 → python_izone-1.3.7}/.vscode/extensions.json +0 -0
  16. {python_izone-1.3.6 → python_izone-1.3.7}/.vscode/launch.json +0 -0
  17. {python_izone-1.3.6 → python_izone-1.3.7}/.vscode/settings.json +0 -0
  18. {python_izone-1.3.6 → python_izone-1.3.7}/README.md +0 -0
  19. {python_izone-1.3.6 → python_izone-1.3.7}/api_docs/AC-DOC-1401-11_iZoneEthernetInterface.pdf +0 -0
  20. {python_izone-1.3.6 → python_izone-1.3.7}/api_docs/iPower_JSON_datastrings_Out.h +0 -0
  21. {python_izone-1.3.6 → python_izone-1.3.7}/api_docs/iZone_JSON_datastrings.h +0 -0
  22. {python_izone-1.3.6 → python_izone-1.3.7}/docs/release-notes/v1.3.4.md +0 -0
  23. {python_izone-1.3.6 → python_izone-1.3.7}/docs/release-notes/v1.3.5.md +0 -0
  24. {python_izone-1.3.6 → python_izone-1.3.7}/licence.txt +0 -0
  25. {python_izone-1.3.6 → python_izone-1.3.7}/pizone/const.py +0 -0
  26. {python_izone-1.3.6 → python_izone-1.3.7}/pizone/controller.py +0 -0
  27. {python_izone-1.3.6 → python_izone-1.3.7}/pizone/power.py +0 -0
  28. {python_izone-1.3.6 → python_izone-1.3.7}/pizone/py.typed +0 -0
  29. {python_izone-1.3.6 → python_izone-1.3.7}/pizone/types.py +0 -0
  30. {python_izone-1.3.6 → python_izone-1.3.7}/pizone/zone.py +0 -0
  31. {python_izone-1.3.6 → python_izone-1.3.7}/pyproject.toml +0 -0
  32. {python_izone-1.3.6 → python_izone-1.3.7}/scripts/check +0 -0
  33. {python_izone-1.3.6 → python_izone-1.3.7}/scripts/coverage +0 -0
  34. {python_izone-1.3.6 → python_izone-1.3.7}/tests/__init__.py +0 -0
  35. {python_izone-1.3.6 → python_izone-1.3.7}/tests/conftest.py +0 -0
  36. {python_izone-1.3.6 → python_izone-1.3.7}/tests/http_fakes.py +0 -0
  37. {python_izone-1.3.6 → python_izone-1.3.7}/tests/power_data.py +0 -0
  38. {python_izone-1.3.6 → python_izone-1.3.7}/tests/resources.py +0 -0
  39. {python_izone-1.3.6 → python_izone-1.3.7}/tests/test_controller.py +0 -0
  40. {python_izone-1.3.6 → python_izone-1.3.7}/tests/test_discovery.py +0 -0
  41. {python_izone-1.3.6 → python_izone-1.3.7}/tests/test_fullstack.py +0 -0
  42. {python_izone-1.3.6 → python_izone-1.3.7}/tests/test_http.py +0 -0
  43. {python_izone-1.3.6 → python_izone-1.3.7}/tests/test_power.py +0 -0
  44. {python_izone-1.3.6 → python_izone-1.3.7}/tests/test_zone.py +0 -0
  45. {python_izone-1.3.6 → python_izone-1.3.7}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-izone
3
- Version: 1.3.6
3
+ Version: 1.3.7
4
4
  Summary: A python interface to the iZone airconditioner controller
5
5
  Project-URL: Homepage, https://github.com/Swamp-Ig/pizone
6
6
  Author-email: Penny Wood <pypl@ninjateaparty.com>
@@ -4,86 +4,60 @@ Released 2026-07-19.
4
4
 
5
5
  **Compare range:** https://github.com/Swamp-Ig/pizone/compare/v1.3.5...v1.3.6
6
6
 
7
- This is a **pre-release** for integrators (especially Home Assistant Phase 2
8
- work). It is not aimed at end users as a standalone upgrade. Prefer waiting for
9
- a future stable line unless you are developing against the new discovery API.
7
+ This is a **pre-release** for integrators adopting the newer discovery API. It is not aimed at end users as a standalone upgrade. Prefer waiting for a future stable line unless you are developing against that API.
10
8
 
11
9
  ## Since v1.3.5
12
10
 
13
11
  - Discovery scan wait (`SCAN_TIMEOUT`) reduced from 20s to 5s
14
- - `close()` waits until the UDP transport has finished tearing down before
15
- returning, so a following `create_discovery()` can rebind `:7005` immediately
16
- (fixes delete → HomeKit rediscovery `EADDRINUSE` races)
17
- - Failed `create_discovery` / `start_discovery` no longer leaves a sticky
18
- process singleton that blocked retries until process restart
19
- - `PLACEHOLDER_DEVICE_UID` is no longer exported from the public package API
20
- (`UnpairedBridgeError` remains the supported signal)
12
+ - `close()` waits until the UDP transport has finished tearing down before returning, so a following `create_discovery()` can rebind `:7005` immediately (fixes delete → HomeKit rediscovery `EADDRINUSE` races)
13
+ - Failed `create_discovery` / `start_discovery` no longer leaves a sticky process singleton that blocked retries until process restart
14
+ - `PLACEHOLDER_DEVICE_UID` is no longer exported from the public package API (`UnpairedBridgeError` remains the supported signal)
21
15
 
22
16
  Everything from v1.3.5 below still applies.
23
17
 
24
18
  ## Summary (since v1.3.4)
25
19
 
26
- - HTTP connections are closed after each GET, which greatly reduces stalls on
27
- some iZone bridges ([home-assistant/core#176536](https://github.com/home-assistant/core/issues/176536))
28
- - A new discovery and refresh API is available for integrators (the older API
29
- still works for now)
20
+ - HTTP connections are closed after each GET, which greatly reduces stalls on some iZone bridges ([home-assistant/core#176536](https://github.com/home-assistant/core/issues/176536))
21
+ - A new discovery and refresh API is available for integrators (the older API still works for now)
30
22
  - iPower monitoring is off unless you opt in with `ENABLE_POWER = True`
31
- - Bridges that are not paired to an air conditioner (UID `000000000`) are
32
- ignored or rejected instead of showing up as broken controllers
23
+ - Bridges that are not paired to an air conditioner (UID `000000000`) are ignored or rejected instead of showing up as broken controllers
33
24
  - Faster discovery scans and reliable UDP close → recreate (this release)
34
25
 
35
26
  ## Bridge connection reliability
36
27
 
37
- Some ASH bridges misbehave when HTTP connections are reused. This release
38
- line:
28
+ Some ASH bridges misbehave when HTTP connections are reused. This release line:
39
29
 
40
30
  - Sends `Connection: close` on every HTTP GET (polling and discovery probes)
41
- - Closes TCP connections after each request when the library creates its own
42
- aiohttp session
31
+ - Closes TCP connections after each request when the library creates its own aiohttp session
43
32
 
44
- This reduces how often the bridge stops responding. It does not fix every
45
- hub-side stall; replacing faulty hardware may still be needed.
33
+ This reduces how often the bridge stops responding. It does not fix every hub-side stall; replacing faulty hardware may still be needed.
46
34
 
47
35
  ## New discovery and refresh API (optional for now)
48
36
 
49
- Integrators can adopt a clearer model without background poll loops inside the
50
- library:
37
+ Integrators can adopt a clearer model without background poll loops inside the library:
51
38
 
52
- - `create_discovery()` with callbacks when a controller is found or its IP
53
- changes
54
- - `discover_by_host`, `discover_by_uid`, `discover_all` to find bridges on the
55
- network
39
+ - `create_discovery()` with callbacks when a controller is found or its IP changes
40
+ - `discover_by_host`, `discover_by_uid`, `discover_all` to find bridges on the network
56
41
  - `create_controller(uid, host)` to connect to a known bridge
57
- - `refresh_all`, `refresh_system`, `refresh_zones`, and related methods so the
58
- caller controls when to poll
42
+ - `refresh_all`, `refresh_system`, `refresh_zones`, and related methods so the caller controls when to poll
59
43
 
60
- `close()` on the discovery service now waits for UDP teardown so the next
61
- `create_discovery()` can bind the discovery port again without racing the OS
62
- socket release.
44
+ `close()` on the discovery service now waits for UDP teardown so the next `create_discovery()` can bind the discovery port again without racing the OS socket release.
63
45
 
64
- The previous API (`discovery()`, `fetch_controller`, `Listener`, and so on)
65
- still works.
46
+ The previous API (`discovery()`, `fetch_controller`, `Listener`, and so on) still works.
66
47
 
67
48
  ## iPower opt-in
68
49
 
69
- `ENABLE_POWER` now defaults to `False`. Power monitor HTTP traffic only runs
70
- after you set `pizone.ENABLE_POWER = True` before creating or refreshing a
71
- controller. This avoids extra requests on bridges that advertise iPower but are
72
- not set up for it. The default may change if more field data shows power
73
- monitoring is reliable enough to enable by default.
50
+ `ENABLE_POWER` now defaults to `False`. Power monitor HTTP traffic only runs after you set `pizone.ENABLE_POWER = True` before creating or refreshing a controller. This avoids extra requests on bridges that advertise iPower but are not set up for it. The default may change if more field data shows power monitoring is reliable enough to enable by default.
74
51
 
75
52
  ## Unpaired bridges
76
53
 
77
- Wireless ASH bridges with no AC paired report device UID `000000000`. The
78
- library now:
54
+ Wireless ASH bridges with no AC paired report device UID `000000000`. The library now:
79
55
 
80
56
  - Ignores them during passive UDP discovery
81
57
  - Raises `UnpairedBridgeError` if you try to connect to that UID on purpose
82
58
  - Refuses to cache them after an IP lookup
83
59
 
84
- Paired bridges that temporarily report fault-shaped settings (AC off, reboot,
85
- and so on) are unchanged: they keep their real UID and show as disconnected
86
- until the AC data recovers.
60
+ Paired bridges that temporarily report fault-shaped settings (AC off, reboot, and so on) are unchanged: they keep their real UID and show as disconnected until the AC data recovers.
87
61
 
88
62
  ## Other changes
89
63
 
@@ -93,6 +67,4 @@ until the AC data recovers.
93
67
 
94
68
  ## Home Assistant
95
69
 
96
- Pin `python-izone==1.3.6` when adopting the Phase 2 discovery / coordinator
97
- runtime. This pre-release is intended for that integration work, not as a
98
- general end-user library upgrade on the legacy path alone.
70
+ Pin `python-izone==1.3.6` when moving the iZone integration onto the newer discovery and coordinator path. This pre-release is intended for that integration work, not as a general end-user library upgrade on the legacy path alone.
@@ -0,0 +1,7 @@
1
+ # Release notes — v1.3.7
2
+
3
+ **Compare range:** https://github.com/Swamp-Ig/pizone/compare/v1.3.6...v1.3.7
4
+
5
+ ## Since v1.3.6
6
+
7
+ - Add `ControllerAlreadyClaimedError` for duplicate controller claim/discovery attempts (replaces bare `RuntimeError` with message `already created` on controller paths; discovery-service singleton errors unchanged)
@@ -31,7 +31,12 @@ from typing import Any
31
31
  from . import power as power_mod
32
32
  from .controller import Controller
33
33
  from .discovery import DiscoveryService, Listener, create_discovery, discovery
34
- from .exceptions import ControllerCommandError, ResponseDecodeError, UnpairedBridgeError
34
+ from .exceptions import (
35
+ ControllerAlreadyClaimedError,
36
+ ControllerCommandError,
37
+ ResponseDecodeError,
38
+ UnpairedBridgeError,
39
+ )
35
40
  from .power import BatteryLevel, Power, PowerChannel, PowerDevice, PowerGroup
36
41
  from .types import ControllerEndpoint
37
42
  from .zone import Zone
@@ -61,6 +66,7 @@ __all__ = [
61
66
  "ENABLE_POWER",
62
67
  "BatteryLevel",
63
68
  "Controller",
69
+ "ControllerAlreadyClaimedError",
64
70
  "ControllerCommandError",
65
71
  "ControllerEndpoint",
66
72
  "DiscoveryService",
@@ -24,7 +24,7 @@ import ifaddr
24
24
 
25
25
  from .const import PLACEHOLDER_DEVICE_UID
26
26
  from .controller import Controller
27
- from .exceptions import raise_if_placeholder_uid
27
+ from .exceptions import ControllerAlreadyClaimedError, raise_if_placeholder_uid
28
28
  from .types import ControllerEndpoint
29
29
  from .zone import Zone
30
30
 
@@ -405,7 +405,9 @@ class DiscoveryService:
405
405
  def _ensure_endpoint_available(self, endpoint: ControllerEndpoint) -> None:
406
406
  """Raise if *endpoint* is already claimed."""
407
407
  if endpoint.uid in self._claimed_endpoints:
408
- raise RuntimeError(f"Controller {endpoint.uid} already created")
408
+ raise ControllerAlreadyClaimedError(
409
+ f"Controller {endpoint.uid} already created"
410
+ )
409
411
 
410
412
  # disposition: 1.4
411
413
  def _claim_endpoint(self, endpoint: ControllerEndpoint) -> None:
@@ -451,7 +453,9 @@ class DiscoveryService:
451
453
  """
452
454
  claimed = self._endpoint_by_host(self._claimed_endpoints, host)
453
455
  if claimed is not None:
454
- raise RuntimeError(f"Controller {claimed.uid} already created")
456
+ raise ControllerAlreadyClaimedError(
457
+ f"Controller {claimed.uid} already created"
458
+ )
455
459
  known = self._endpoint_by_host(self._known_endpoints, host)
456
460
  if known is not None:
457
461
  return known
@@ -476,7 +480,7 @@ class DiscoveryService:
476
480
  """
477
481
  raise_if_placeholder_uid(uid)
478
482
  if uid in self._claimed_endpoints:
479
- raise RuntimeError(f"Controller {uid} already created")
483
+ raise ControllerAlreadyClaimedError(f"Controller {uid} already created")
480
484
  endpoint = self._known_endpoints.get(uid)
481
485
  if endpoint is not None:
482
486
  return endpoint
@@ -569,7 +573,7 @@ class DiscoveryService:
569
573
 
570
574
  Raises:
571
575
  UnpairedBridgeError: If *uid* is the unpaired placeholder (no I/O).
572
- RuntimeError: If a controller with *uid* already exists.
576
+ ControllerAlreadyClaimedError: If a controller with *uid* already exists.
573
577
  ConnectionError: If the controller cannot be reached after
574
578
  address fallback.
575
579
  ControllerCommandError: If the device rejects the protocol request.
@@ -579,7 +583,7 @@ class DiscoveryService:
579
583
  """
580
584
  raise_if_placeholder_uid(uid)
581
585
  if uid in self._claimed_endpoints:
582
- raise RuntimeError(f"Controller {uid} already created")
586
+ raise ControllerAlreadyClaimedError(f"Controller {uid} already created")
583
587
  pending_address_changed: ControllerEndpoint | None = None
584
588
 
585
589
  try:
@@ -28,6 +28,14 @@ class UnpairedBridgeError(ValueError):
28
28
  """
29
29
 
30
30
 
31
+ class ControllerAlreadyClaimedError(Exception):
32
+ """A controller UID or host is already claimed on this discovery service.
33
+
34
+ Raised when ``discover_by_host``, ``discover_by_uid``, or ``create_controller``
35
+ would duplicate an active controller registration.
36
+ """
37
+
38
+
31
39
  def raise_if_placeholder_uid(uid: str) -> None:
32
40
  """Raise :exc:`UnpairedBridgeError` if *uid* is the unpaired placeholder."""
33
41
  if uid == PLACEHOLDER_DEVICE_UID:
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.3.6'
22
- __version_tuple__ = version_tuple = (1, 3, 6)
21
+ __version__ = version = '1.3.7'
22
+ __version_tuple__ = version_tuple = (1, 3, 7)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -17,6 +17,7 @@ import pytest
17
17
 
18
18
  from pizone import (
19
19
  Controller,
20
+ ControllerAlreadyClaimedError,
20
21
  ControllerCommandError,
21
22
  ControllerEndpoint,
22
23
  UnpairedBridgeError,
@@ -282,7 +283,7 @@ async def test_discover_by_host_raises_if_controller_exists() -> None:
282
283
  ClientSession,
283
284
  FakeHttpSession(get_error=OSError("should not probe")),
284
285
  )
285
- with pytest.raises(RuntimeError, match="already created"):
286
+ with pytest.raises(ControllerAlreadyClaimedError, match="already created"):
286
287
  await service.discover_by_host("10.0.0.90")
287
288
  await service.close()
288
289
 
@@ -324,7 +325,7 @@ async def test_discover_by_uid_raises_if_controller_exists() -> None:
324
325
  scan = AsyncMock()
325
326
  with (
326
327
  patch.object(service, "scan", scan),
327
- pytest.raises(RuntimeError, match="already created"),
328
+ pytest.raises(ControllerAlreadyClaimedError, match="already created"),
328
329
  ):
329
330
  await service.discover_by_uid("000025841")
330
331
  scan.assert_not_awaited()
@@ -656,7 +657,7 @@ async def test_create_controller_raises_if_uid_exists() -> None:
656
657
  FakeHttpSession(get_response=_system_settings_response("000025841")),
657
658
  )
658
659
  await service.create_controller("000025841", "10.0.0.90")
659
- with pytest.raises(RuntimeError, match="already created"):
660
+ with pytest.raises(ControllerAlreadyClaimedError, match="already created"):
660
661
  await service.create_controller("000025841", "10.0.0.90")
661
662
  await service.close()
662
663
 
File without changes
File without changes
File without changes
File without changes
File without changes