python-izone 1.3.5__tar.gz → 1.3.6__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.5 → python_izone-1.3.6}/PKG-INFO +1 -1
  2. python_izone-1.3.6/docs/release-notes/v1.3.5.md +85 -0
  3. python_izone-1.3.6/docs/release-notes/v1.3.6.md +98 -0
  4. {python_izone-1.3.5 → python_izone-1.3.6}/pizone/__init__.py +0 -2
  5. {python_izone-1.3.5 → python_izone-1.3.6}/pizone/discovery.py +16 -3
  6. {python_izone-1.3.5 → python_izone-1.3.6}/pizone/version.py +2 -2
  7. {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_discovery_api.py +162 -3
  8. python_izone-1.3.5/docs/release-notes/v1.3.5.md +0 -92
  9. {python_izone-1.3.5 → python_izone-1.3.6}/.gitattributes +0 -0
  10. {python_izone-1.3.5 → python_izone-1.3.6}/.github/workflows/main.yml +0 -0
  11. {python_izone-1.3.5 → python_izone-1.3.6}/.gitignore +0 -0
  12. {python_izone-1.3.5 → python_izone-1.3.6}/.pre-commit-config.yaml +0 -0
  13. {python_izone-1.3.5 → python_izone-1.3.6}/.python-version +0 -0
  14. {python_izone-1.3.5 → python_izone-1.3.6}/.vscode/.ropeproject/config.py +0 -0
  15. {python_izone-1.3.5 → python_izone-1.3.6}/.vscode/extensions.json +0 -0
  16. {python_izone-1.3.5 → python_izone-1.3.6}/.vscode/launch.json +0 -0
  17. {python_izone-1.3.5 → python_izone-1.3.6}/.vscode/settings.json +0 -0
  18. {python_izone-1.3.5 → python_izone-1.3.6}/README.md +0 -0
  19. {python_izone-1.3.5 → python_izone-1.3.6}/api_docs/AC-DOC-1401-11_iZoneEthernetInterface.pdf +0 -0
  20. {python_izone-1.3.5 → python_izone-1.3.6}/api_docs/iPower_JSON_datastrings_Out.h +0 -0
  21. {python_izone-1.3.5 → python_izone-1.3.6}/api_docs/iZone_JSON_datastrings.h +0 -0
  22. {python_izone-1.3.5 → python_izone-1.3.6}/docs/release-notes/v1.3.4.md +0 -0
  23. {python_izone-1.3.5 → python_izone-1.3.6}/licence.txt +0 -0
  24. {python_izone-1.3.5 → python_izone-1.3.6}/pizone/const.py +0 -0
  25. {python_izone-1.3.5 → python_izone-1.3.6}/pizone/controller.py +0 -0
  26. {python_izone-1.3.5 → python_izone-1.3.6}/pizone/exceptions.py +0 -0
  27. {python_izone-1.3.5 → python_izone-1.3.6}/pizone/power.py +0 -0
  28. {python_izone-1.3.5 → python_izone-1.3.6}/pizone/py.typed +0 -0
  29. {python_izone-1.3.5 → python_izone-1.3.6}/pizone/types.py +0 -0
  30. {python_izone-1.3.5 → python_izone-1.3.6}/pizone/zone.py +0 -0
  31. {python_izone-1.3.5 → python_izone-1.3.6}/pyproject.toml +0 -0
  32. {python_izone-1.3.5 → python_izone-1.3.6}/scripts/check +0 -0
  33. {python_izone-1.3.5 → python_izone-1.3.6}/scripts/coverage +0 -0
  34. {python_izone-1.3.5 → python_izone-1.3.6}/tests/__init__.py +0 -0
  35. {python_izone-1.3.5 → python_izone-1.3.6}/tests/conftest.py +0 -0
  36. {python_izone-1.3.5 → python_izone-1.3.6}/tests/http_fakes.py +0 -0
  37. {python_izone-1.3.5 → python_izone-1.3.6}/tests/power_data.py +0 -0
  38. {python_izone-1.3.5 → python_izone-1.3.6}/tests/resources.py +0 -0
  39. {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_controller.py +0 -0
  40. {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_discovery.py +0 -0
  41. {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_fullstack.py +0 -0
  42. {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_http.py +0 -0
  43. {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_power.py +0 -0
  44. {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_zone.py +0 -0
  45. {python_izone-1.3.5 → python_izone-1.3.6}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-izone
3
- Version: 1.3.5
3
+ Version: 1.3.6
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>
@@ -0,0 +1,85 @@
1
+ # Release notes — v1.3.5
2
+
3
+ Released 2026-07-17.
4
+
5
+ **Compare range:** https://github.com/Swamp-Ig/pizone/compare/v1.3.4...v1.3.5
6
+
7
+ This is a **pre-release**. It includes work toward a better Home Assistant
8
+ integration (stored bridge IP, improved discovery, and cleaner refresh
9
+ timing), plus reliability fixes you can use today. Home Assistant still uses
10
+ the same library entry points as 1.3.4. The older API remains available; a
11
+ future major release will remove it in favour of the new discovery and refresh
12
+ API below.
13
+
14
+ ## Summary
15
+
16
+ Since v1.3.4:
17
+
18
+ - HTTP connections are closed after each GET, which greatly reduces stalls on
19
+ some iZone bridges ([home-assistant/core#176536](https://github.com/home-assistant/core/issues/176536))
20
+ - A new discovery and refresh API is available for integrators (the older API
21
+ still works for now)
22
+ - iPower monitoring is off unless you opt in with `ENABLE_POWER = True`
23
+ - Bridges that are not paired to an air conditioner (UID `000000000`) are
24
+ ignored or rejected instead of showing up as broken controllers
25
+
26
+ ## Bridge connection reliability
27
+
28
+ Some ASH bridges misbehave when HTTP connections are reused. This release:
29
+
30
+ - Sends `Connection: close` on every HTTP GET (polling and discovery probes)
31
+ - Closes TCP connections after each request when the library creates its own
32
+ aiohttp session
33
+
34
+ This reduces how often the bridge stops responding. It does not fix every
35
+ hub-side stall; replacing faulty hardware may still be needed.
36
+
37
+ ## New discovery and refresh API (optional for now)
38
+
39
+ Integrators can adopt a clearer model without background poll loops inside the
40
+ library:
41
+
42
+ - `create_discovery()` with callbacks when a controller is found or its IP
43
+ changes
44
+ - `discover_by_host`, `discover_by_uid`, `discover_all` to find bridges on the
45
+ network
46
+ - `create_controller(uid, host)` to connect to a known bridge
47
+ - `refresh_all`, `refresh_system`, `refresh_zones`, and related methods so the
48
+ caller controls when to poll
49
+
50
+ The previous API (`discovery()`, `fetch_controller`, `Listener`, and so on)
51
+ still works and is what Home Assistant uses today.
52
+
53
+ ## iPower opt-in
54
+
55
+ `ENABLE_POWER` now defaults to `False`. Power monitor HTTP traffic only runs
56
+ after you set `pizone.ENABLE_POWER = True` before creating or refreshing a
57
+ controller. This avoids extra requests on bridges that advertise iPower but are
58
+ not set up for it. The default may change if more field data shows power
59
+ monitoring is reliable enough to enable by default.
60
+
61
+ ## Unpaired bridges
62
+
63
+ Wireless ASH bridges with no AC paired report device UID `000000000`. The
64
+ library now:
65
+
66
+ - Ignores them during passive UDP discovery
67
+ - Raises `UnpairedBridgeError` if you try to connect to that UID on purpose
68
+ - Refuses to cache them after an IP lookup
69
+
70
+ Paired bridges that temporarily report fault-shaped settings (AC off, reboot,
71
+ and so on) are unchanged: they keep their real UID and show as disconnected
72
+ until the AC data recovers.
73
+
74
+ ## Other changes
75
+
76
+ - UDP `CHANGED_*` notifications are recognized and ignored (no spurious refresh)
77
+ - Discovery verifies bridge identity over HTTP before trusting UDP replies
78
+ - Development tooling moved to Ruff for lint and format
79
+
80
+ ## Home Assistant
81
+
82
+ If you maintain the iZone integration or test against this library, pin
83
+ `python-izone==1.3.5` for the connection reuse fix on the current integration
84
+ code path. The new discovery API is included for upcoming integration work but
85
+ is not required for a version bump alone.
@@ -0,0 +1,98 @@
1
+ # Release notes — v1.3.6
2
+
3
+ Released 2026-07-19.
4
+
5
+ **Compare range:** https://github.com/Swamp-Ig/pizone/compare/v1.3.5...v1.3.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.
10
+
11
+ ## Since v1.3.5
12
+
13
+ - 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)
21
+
22
+ Everything from v1.3.5 below still applies.
23
+
24
+ ## Summary (since v1.3.4)
25
+
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)
30
+ - 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
33
+ - Faster discovery scans and reliable UDP close → recreate (this release)
34
+
35
+ ## Bridge connection reliability
36
+
37
+ Some ASH bridges misbehave when HTTP connections are reused. This release
38
+ line:
39
+
40
+ - 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
43
+
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.
46
+
47
+ ## New discovery and refresh API (optional for now)
48
+
49
+ Integrators can adopt a clearer model without background poll loops inside the
50
+ library:
51
+
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
56
+ - `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
59
+
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.
63
+
64
+ The previous API (`discovery()`, `fetch_controller`, `Listener`, and so on)
65
+ still works.
66
+
67
+ ## iPower opt-in
68
+
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.
74
+
75
+ ## Unpaired bridges
76
+
77
+ Wireless ASH bridges with no AC paired report device UID `000000000`. The
78
+ library now:
79
+
80
+ - Ignores them during passive UDP discovery
81
+ - Raises `UnpairedBridgeError` if you try to connect to that UID on purpose
82
+ - Refuses to cache them after an IP lookup
83
+
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.
87
+
88
+ ## Other changes
89
+
90
+ - UDP `CHANGED_*` notifications are recognized and ignored (no spurious refresh)
91
+ - Discovery verifies bridge identity over HTTP before trusting UDP replies
92
+ - Development tooling moved to Ruff for lint and format
93
+
94
+ ## Home Assistant
95
+
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.
@@ -29,7 +29,6 @@ from types import ModuleType
29
29
  from typing import Any
30
30
 
31
31
  from . import power as power_mod
32
- from .const import PLACEHOLDER_DEVICE_UID
33
32
  from .controller import Controller
34
33
  from .discovery import DiscoveryService, Listener, create_discovery, discovery
35
34
  from .exceptions import ControllerCommandError, ResponseDecodeError, UnpairedBridgeError
@@ -60,7 +59,6 @@ sys.modules[__name__].__class__ = _PizoneModule
60
59
 
61
60
  __all__ = [
62
61
  "ENABLE_POWER",
63
- "PLACEHOLDER_DEVICE_UID",
64
62
  "BatteryLevel",
65
63
  "Controller",
66
64
  "ControllerCommandError",
@@ -50,7 +50,7 @@ DISCOVERY_RESCAN = 20.0
50
50
  RESCAN_COOLDOWN = 5.0
51
51
 
52
52
  # disposition: 1.4 — active discover_by_uid / discover_all wait
53
- SCAN_TIMEOUT = 20.0
53
+ SCAN_TIMEOUT = 5.0
54
54
 
55
55
  _LOG = logging.getLogger("pizone.discovery")
56
56
 
@@ -756,8 +756,13 @@ class DiscoveryService:
756
756
  return
757
757
  _LOG.info("Close called on discovery service.")
758
758
  self._close_task = current
759
- if self._transport:
759
+ if self._transport is not None:
760
+ # DatagramTransport.close() only schedules sock release via
761
+ # call_soon(_call_connection_lost). Wait until connection_lost
762
+ # clears _transport so a following create_discovery can bind :7005.
760
763
  self._transport.close()
764
+ while self._transport is not None:
765
+ await asyncio.sleep(0)
761
766
 
762
767
  pending = [task for task in list(self._tasks) if task is not current]
763
768
  for task in pending:
@@ -773,6 +778,7 @@ class DiscoveryService:
773
778
 
774
779
  def _on_connection_lost(self, exc: Exception | None) -> None:
775
780
  _LOG.debug("Connection Lost")
781
+ self._transport = None
776
782
  if not self._close_task:
777
783
  _LOG.error("Connection Lost unexpectedly: %s", repr(exc))
778
784
  self.create_task(self.close())
@@ -1002,5 +1008,12 @@ async def create_discovery(
1002
1008
  on_endpoint_discovered=on_endpoint_discovered,
1003
1009
  )
1004
1010
  _active_discovery = service
1005
- await service.start_discovery()
1011
+ try:
1012
+ await service.start_discovery()
1013
+ except BaseException:
1014
+ # close() clears the global when it finishes; clear again if close never ran.
1015
+ with suppress(BaseException):
1016
+ await service.close()
1017
+ _clear_discovery_global(service)
1018
+ raise
1006
1019
  return service
@@ -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.5'
22
- __version_tuple__ = version_tuple = (1, 3, 5)
21
+ __version__ = version = '1.3.6'
22
+ __version_tuple__ = version_tuple = (1, 3, 6)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -7,21 +7,22 @@
7
7
  # (sticky within a function until the next disposition tag).
8
8
 
9
9
  import asyncio
10
+ import errno
10
11
  import sys
11
12
  from typing import cast
12
- from unittest.mock import AsyncMock, MagicMock, patch
13
+ from unittest.mock import AsyncMock, patch
13
14
 
14
15
  from aiohttp import ClientSession
15
16
  import pytest
16
17
 
17
18
  from pizone import (
18
- PLACEHOLDER_DEVICE_UID,
19
19
  Controller,
20
20
  ControllerCommandError,
21
21
  ControllerEndpoint,
22
22
  UnpairedBridgeError,
23
23
  create_discovery,
24
24
  )
25
+ from pizone.const import PLACEHOLDER_DEVICE_UID
25
26
  from pizone.discovery import DiscoveryService
26
27
 
27
28
  from .conftest import MockController, MockDiscoveryService
@@ -66,6 +67,164 @@ async def test_create_discovery_singleton() -> None:
66
67
  assert discovery_module._active_discovery is None
67
68
 
68
69
 
70
+ # disposition: 1.4
71
+ @pytest.mark.asyncio
72
+ async def test_close_then_immediate_create_discovery_rebinds() -> None:
73
+ """close() must release UDP :7005 before returning so a new create can bind.
74
+
75
+ Reproduces the delete→HomeKit race: stop discovery then immediately start
76
+ again on the same event loop without awaiting an extra sleep.
77
+
78
+ Use an injected session (as Home Assistant does) so close() does not await
79
+ an owned ClientSession — that await can mask the teardown race.
80
+ """
81
+ session = ClientSession()
82
+ try:
83
+ first = await create_discovery(session=session)
84
+ await first.close()
85
+ assert discovery_module._active_discovery is None
86
+
87
+ second = await create_discovery(session=session)
88
+ try:
89
+ assert second is not first
90
+ assert discovery_module._active_discovery is second
91
+ finally:
92
+ await second.close()
93
+ assert discovery_module._active_discovery is None
94
+ finally:
95
+ leftover = discovery_module._active_discovery
96
+ if leftover is not None:
97
+ await leftover.close()
98
+ await session.close()
99
+
100
+
101
+ # disposition: 1.4
102
+ @pytest.mark.asyncio
103
+ async def test_owned_session_close_still_needs_udp_teardown_wait() -> None:
104
+ """Owned-session await must not be treated as sufficient UDP teardown.
105
+
106
+ CPython asyncio schedules ``DatagramTransport`` sock close via
107
+ ``call_soon(_call_connection_lost)``. The next ``await`` (owned
108
+ ``ClientSession.close``) usually runs that callback first — FIFO
109
+ ``call_soon`` behaviour, not a language/compiler guarantee. Defer the
110
+ callback so only waiting on ``connection_lost`` (Fix A) makes rebind safe.
111
+ """
112
+ loop = asyncio.get_running_loop()
113
+ real_call_soon = loop.call_soon
114
+ teardown_delay = 0.05
115
+
116
+ def call_soon(callback: object, *args: object, context: object = None) -> object:
117
+ if getattr(callback, "__name__", None) == "_call_connection_lost":
118
+ return loop.call_later(teardown_delay, callback, *args)
119
+ if context is not None:
120
+ return real_call_soon(callback, *args, context=context)
121
+ return real_call_soon(callback, *args)
122
+
123
+ first = await create_discovery()
124
+ try:
125
+ with patch.object(loop, "call_soon", call_soon):
126
+ await first.close()
127
+ assert discovery_module._active_discovery is None
128
+
129
+ second = await create_discovery()
130
+ try:
131
+ assert second is not first
132
+ assert discovery_module._active_discovery is second
133
+ finally:
134
+ await second.close()
135
+ assert discovery_module._active_discovery is None
136
+ finally:
137
+ await asyncio.sleep(teardown_delay + 0.01)
138
+ leftover = discovery_module._active_discovery
139
+ if leftover is not None:
140
+ await leftover.close()
141
+
142
+
143
+ # disposition: 1.4
144
+ @pytest.mark.asyncio
145
+ async def test_create_discovery_failed_start_allows_retry() -> None:
146
+ """Failed start_discovery must not leave the process singleton set.
147
+
148
+ Otherwise later create_discovery calls raise RuntimeError until process
149
+ restart (HomeKit rediscovery / SETUP_RETRY poisoned after EADDRINUSE).
150
+ """
151
+ try:
152
+ with (
153
+ patch.object(
154
+ DiscoveryService,
155
+ "start_discovery",
156
+ AsyncMock(
157
+ side_effect=OSError(errno.EADDRINUSE, "Address already in use")
158
+ ),
159
+ ),
160
+ pytest.raises(OSError, match="Address already in use"),
161
+ ):
162
+ await create_discovery()
163
+
164
+ assert discovery_module._active_discovery is None
165
+
166
+ with patch.object(DiscoveryService, "start_discovery", AsyncMock()):
167
+ disco = await create_discovery()
168
+ try:
169
+ assert disco is discovery_module._active_discovery
170
+ finally:
171
+ await disco.close()
172
+ assert discovery_module._active_discovery is None
173
+ finally:
174
+ leftover = discovery_module._active_discovery
175
+ if leftover is not None:
176
+ await leftover.close()
177
+
178
+
179
+ # disposition: 1.4
180
+ @pytest.mark.asyncio
181
+ async def test_failed_start_holds_singleton_until_close_finishes() -> None:
182
+ """Overlapping create during failed-start cleanup must hit already-created.
183
+
184
+ ``create_discovery`` must not clear the process singleton before ``close()``
185
+ finishes, so a client that serializes create→close→create cannot bind
186
+ against a service still tearing down.
187
+ """
188
+ original_close = DiscoveryService.close
189
+ close_entered = asyncio.Event()
190
+ finish_close = asyncio.Event()
191
+ held_during_close: list[bool] = []
192
+
193
+ async def gated_close(self: DiscoveryService) -> None:
194
+ held_during_close.append(discovery_module._active_discovery is self)
195
+ close_entered.set()
196
+ await finish_close.wait()
197
+ await original_close(self)
198
+
199
+ try:
200
+ with (
201
+ patch.object(
202
+ DiscoveryService,
203
+ "start_discovery",
204
+ AsyncMock(
205
+ side_effect=OSError(errno.EADDRINUSE, "Address already in use")
206
+ ),
207
+ ),
208
+ patch.object(DiscoveryService, "close", gated_close),
209
+ ):
210
+ create_task = asyncio.create_task(create_discovery())
211
+ await asyncio.wait_for(close_entered.wait(), timeout=1)
212
+ assert discovery_module._active_discovery is not None
213
+ with pytest.raises(RuntimeError, match="already created"):
214
+ await create_discovery()
215
+ finish_close.set()
216
+ with pytest.raises(OSError, match="Address already in use"):
217
+ await create_task
218
+
219
+ assert held_during_close == [True]
220
+ assert discovery_module._active_discovery is None
221
+ finally:
222
+ finish_close.set()
223
+ leftover = discovery_module._active_discovery
224
+ if leftover is not None:
225
+ await leftover.close()
226
+
227
+
69
228
  # disposition: 1.4
70
229
  @pytest.mark.asyncio
71
230
  async def test_discover_by_host() -> None:
@@ -464,7 +623,7 @@ async def test_discover_all_includes_closed_controllers() -> None:
464
623
  @pytest.mark.asyncio
465
624
  async def test_scan_sends_broadcast() -> None:
466
625
  service = MockDiscoveryService(legacy_pathway=False)
467
- service._transport = MagicMock()
626
+ await service.start_discovery()
468
627
  with patch.object(service, "_send_broadcasts") as send_broadcasts:
469
628
  await service.scan()
470
629
  send_broadcasts.assert_called_once()
@@ -1,92 +0,0 @@
1
- # Release notes — v1.3.5
2
-
3
- Released 2026-07-17. **Beta release** for the upcoming Home Assistant iZone
4
- Phase 2 integration work (izone 1.4 program). The version number stays on the
5
- **1.3.x** line for now; the program name is “1.4” internally.
6
-
7
- **Compare range:** https://github.com/Swamp-Ig/pizone/compare/v1.3.4...v1.3.5
8
-
9
- ## Beta status
10
-
11
- This release ships the full dual-track library on `main` so Home Assistant can
12
- pin a real PyPI version while Phase 2a/2b land. The **current HA integration
13
- still uses the legacy API** (`discovery()`, `fetch_*`, `Listener`). The new
14
- consumer-driven API is available for new integration code but is not required
15
- for a pin-only HA bump.
16
-
17
- **`1.4.0`** (planned after HA Phase 2 merges) will remove the legacy surface
18
- (`discovery()`, `fetch_*`, `Listener`, and related poll/scan loops).
19
-
20
- ## Summary
21
-
22
- Since `v1.3.4`, this release adds the izone 1.4 discovery and refresh model
23
- alongside the legacy path, gates iPower behind an opt-in flag, rejects
24
- unpaired-bridge placeholder UIDs, and mitigates HTTP keepalive stalls on some
25
- ASH bridges.
26
-
27
- ## Connection: close (core#176536)
28
-
29
- - HTTP **GET** requests send `Connection: close` (controller reads and
30
- discovery probes)
31
- - When pizone owns the aiohttp session, `TCPConnector(force_close=True)` is used
32
- - Mitigation for hubs that stall under connection reuse; does not fix hub-side
33
- stalls reported in [home-assistant/core#176536](https://github.com/home-assistant/core/issues/176536)
34
-
35
- ## New 1.4 API (dual-track — beta)
36
-
37
- Available now; legacy path remains for existing consumers:
38
-
39
- - **`create_discovery()`** — one-shot discovery service with callbacks
40
- - **`discover_by_host`**, **`discover_by_uid`**, **`discover_all`** — ephemeral
41
- or service-scoped endpoint discovery with HTTP verify
42
- - **`create_controller(uid, host)`** — explicit controller factory after
43
- discovery
44
- - **`refresh_all`**, **`refresh_system`**, **`refresh_zones`**, etc. —
45
- consumer-owned polling on the 1.4 path (no library debounce loops)
46
- - **`ControllerEndpoint`**, **`on_endpoint_discovered`**, **`on_address_changed`**
47
- callbacks for integration discovery flows
48
-
49
- Legacy **`discovery()`**, **`fetch_controller`**, **`fetch_controllers`**, and
50
- **`Listener`** continue to work unchanged on the deprecated pathway.
51
-
52
- ## Power gate
53
-
54
- - **`ENABLE_POWER`** defaults to **`False`**; no iPower HTTP I/O until set at
55
- runtime (for example `pizone.ENABLE_POWER = True` before create/refresh)
56
- - Home Assistant does not expose power entities yet; this avoids surprise I/O on
57
- bridges that advertise iPower in UDP
58
-
59
- ## Unpaired bridge UID
60
-
61
- - **`PLACEHOLDER_DEVICE_UID`** (`"000000000"`) — exact match for ASH bridges
62
- with no paired AC
63
- - Passive UDP discovery ignores this UID (no cache, no notify)
64
- - **`discover_by_uid`** / **`create_controller`** reject immediately with
65
- **`UnpairedBridgeError`** (no network I/O)
66
- - **`discover_by_host`** raises after HTTP probe if the UID is the placeholder
67
- (no cache)
68
-
69
- ## UDP and discovery (1.4 path)
70
-
71
- - **`CHANGED_*`** push datagrams recognized and ignored (no refresh)
72
- - ASPort passive discovery caches unclaimed endpoints and fans out
73
- **`on_endpoint_discovered`** for new or host-changed endpoints
74
- - **`discover_all`** HTTP-verifies collected hosts and dedupes notifications
75
-
76
- ## Other
77
-
78
- - Live fault-shaped **SystemSettings** fixtures for unpaired and paired cold-restart
79
- specimens (tests)
80
- - Test suite labelled with disposition comments (`1.4` vs `deprecate`) for
81
- dual-track cleanup
82
- - Ruff replaces Black and pylint for lint and format
83
-
84
- ## For Home Assistant integrators
85
-
86
- Pin **`python-izone==1.3.5`** for:
87
-
88
- 1. **Immediate:** Connection: close mitigation on the existing legacy integration
89
- path (pin-only bump)
90
- 2. **Upcoming:** Access to the 1.4 API when Phase 2 runtime and config flow land
91
-
92
- Do not expect breaking changes on the legacy API until **`1.4.0`**.
File without changes
File without changes
File without changes
File without changes
File without changes