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.
- {python_izone-1.3.5 → python_izone-1.3.6}/PKG-INFO +1 -1
- python_izone-1.3.6/docs/release-notes/v1.3.5.md +85 -0
- python_izone-1.3.6/docs/release-notes/v1.3.6.md +98 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/pizone/__init__.py +0 -2
- {python_izone-1.3.5 → python_izone-1.3.6}/pizone/discovery.py +16 -3
- {python_izone-1.3.5 → python_izone-1.3.6}/pizone/version.py +2 -2
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_discovery_api.py +162 -3
- python_izone-1.3.5/docs/release-notes/v1.3.5.md +0 -92
- {python_izone-1.3.5 → python_izone-1.3.6}/.gitattributes +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/.github/workflows/main.yml +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/.gitignore +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/.pre-commit-config.yaml +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/.python-version +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/.vscode/.ropeproject/config.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/.vscode/extensions.json +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/.vscode/launch.json +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/.vscode/settings.json +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/README.md +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/api_docs/AC-DOC-1401-11_iZoneEthernetInterface.pdf +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/api_docs/iPower_JSON_datastrings_Out.h +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/api_docs/iZone_JSON_datastrings.h +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/docs/release-notes/v1.3.4.md +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/licence.txt +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/pizone/const.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/pizone/controller.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/pizone/exceptions.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/pizone/power.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/pizone/py.typed +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/pizone/types.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/pizone/zone.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/pyproject.toml +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/scripts/check +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/scripts/coverage +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/__init__.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/conftest.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/http_fakes.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/power_data.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/resources.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_controller.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_discovery.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_fullstack.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_http.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_power.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/tests/test_zone.py +0 -0
- {python_izone-1.3.5 → python_izone-1.3.6}/uv.lock +0 -0
|
@@ -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 =
|
|
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
|
-
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 3,
|
|
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,
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_izone-1.3.5 → python_izone-1.3.6}/api_docs/AC-DOC-1401-11_iZoneEthernetInterface.pdf
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|