python-izone 1.2.9__tar.gz → 1.2.10__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.2.9 → python_izone-1.2.10}/.github/workflows/main.yml +4 -4
- python_izone-1.2.10/.pre-commit-config.yaml +6 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/.pylintrc +3 -7
- {python-izone-1.2.9 → python_izone-1.2.10}/.vscode/settings.json +14 -2
- {python-izone-1.2.9 → python_izone-1.2.10}/PKG-INFO +14 -5
- python_izone-1.2.10/pizone/__init__.py +22 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/pizone/controller.py +78 -59
- {python-izone-1.2.9 → python_izone-1.2.10}/pizone/discovery.py +191 -155
- {python-izone-1.2.9 → python_izone-1.2.10}/pizone/power.py +12 -13
- python_izone-1.2.10/pizone/version.py +24 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/pizone/zone.py +12 -12
- {python-izone-1.2.9 → python_izone-1.2.10}/pyproject.toml +3 -3
- python_izone-1.2.10/pytest.ini +5 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/python_izone.egg-info/PKG-INFO +14 -5
- {python-izone-1.2.9 → python_izone-1.2.10}/python_izone.egg-info/SOURCES.txt +2 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/python_izone.egg-info/requires.txt +2 -3
- {python-izone-1.2.9 → python_izone-1.2.10}/setup.cfg +4 -5
- python_izone-1.2.10/tests/__init__.py +1 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/tests/conftest.py +38 -25
- {python-izone-1.2.9 → python_izone-1.2.10}/tests/test_discovery.py +109 -22
- {python-izone-1.2.9 → python_izone-1.2.10}/tests/test_fullstack.py +2 -1
- python-izone-1.2.9/pizone/__init__.py +0 -22
- python-izone-1.2.9/pizone/version.py +0 -5
- python-izone-1.2.9/tests/__init__.py +0 -1
- {python-izone-1.2.9 → python_izone-1.2.10}/.gitattributes +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/.gitignore +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/.vscode/.ropeproject/config.py +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/.vscode/launch.json +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/README.md +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/api_docs/AC-DOC-1401-11_iZoneEthernetInterface.pdf +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/api_docs/iPower_JSON_datastrings_Out.h +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/api_docs/iZone_JSON_datastrings.h +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/licence.txt +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/python_izone.egg-info/dependency_links.txt +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/python_izone.egg-info/top_level.txt +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/setup.py +0 -0
- {python-izone-1.2.9 → python_izone-1.2.10}/tests/resources.py +0 -0
|
@@ -8,12 +8,12 @@ jobs:
|
|
|
8
8
|
runs-on: ubuntu-latest
|
|
9
9
|
strategy:
|
|
10
10
|
matrix:
|
|
11
|
-
python-version: ["3.
|
|
11
|
+
python-version: ["3.14"]
|
|
12
12
|
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
15
|
- name: Set up Python ${{ matrix.python-version }}
|
|
16
|
-
uses: actions/setup-python@
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
17
|
with:
|
|
18
18
|
python-version: ${{ matrix.python-version }}
|
|
19
19
|
- name: Install dependencies
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
pip install build
|
|
32
32
|
python -m build
|
|
33
33
|
- name: Publish package
|
|
34
|
-
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') && matrix.python-version == '3.
|
|
34
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') && matrix.python-version == '3.14'
|
|
35
35
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
36
36
|
with:
|
|
37
37
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -82,15 +82,11 @@ persistent=yes
|
|
|
82
82
|
|
|
83
83
|
# Minimum Python version to use for version dependent checks. Will default to
|
|
84
84
|
# the version used to run pylint.
|
|
85
|
-
py-version=3.
|
|
85
|
+
py-version=3.14
|
|
86
86
|
|
|
87
87
|
# Discover python modules and packages in the file system subtree.
|
|
88
88
|
recursive=no
|
|
89
89
|
|
|
90
|
-
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
|
91
|
-
# user-friendly hints instead of false-positive error messages.
|
|
92
|
-
suggestion-mode=yes
|
|
93
|
-
|
|
94
90
|
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
|
95
91
|
# active Python interpreter and may run arbitrary code.
|
|
96
92
|
unsafe-load-any-extension=no
|
|
@@ -369,8 +365,8 @@ min-public-methods=2
|
|
|
369
365
|
[EXCEPTIONS]
|
|
370
366
|
|
|
371
367
|
# Exceptions that will emit a warning when caught.
|
|
372
|
-
overgeneral-exceptions=BaseException,
|
|
373
|
-
Exception
|
|
368
|
+
overgeneral-exceptions=builtins.BaseException,
|
|
369
|
+
builtins.Exception
|
|
374
370
|
|
|
375
371
|
|
|
376
372
|
[FORMAT]
|
|
@@ -8,7 +8,19 @@
|
|
|
8
8
|
"python.linting.pylintEnabled": true,
|
|
9
9
|
"python.linting.flake8Enabled": false,
|
|
10
10
|
"python.linting.mypyEnabled": true,
|
|
11
|
-
"python.
|
|
11
|
+
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
|
|
12
12
|
"python.globalModuleInstallation": false,
|
|
13
|
-
"python.formatting.provider": "black"
|
|
13
|
+
"python.formatting.provider": "black",
|
|
14
|
+
"cSpell.words": [
|
|
15
|
+
"inetaddrs",
|
|
16
|
+
"inetaddr",
|
|
17
|
+
"ifaddresses",
|
|
18
|
+
"rescan",
|
|
19
|
+
"pizone",
|
|
20
|
+
"ipower",
|
|
21
|
+
"COOLDOWN",
|
|
22
|
+
"coro",
|
|
23
|
+
"datagram",
|
|
24
|
+
"fanout"
|
|
25
|
+
]
|
|
14
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-izone
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.10
|
|
4
4
|
Summary: A python interface to the iZone airconditioner controller
|
|
5
5
|
Home-page: https://github.com/Swamp-Ig/pizone
|
|
6
6
|
Author: Penny Wood
|
|
@@ -8,16 +8,25 @@ Author-email: pypl@ninjateaparty.com
|
|
|
8
8
|
Keywords: iZone,IoT
|
|
9
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
12
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Topic :: Home Automation
|
|
15
15
|
Classifier: Topic :: System :: Hardware
|
|
16
|
-
Requires-Python: >=3.
|
|
16
|
+
Requires-Python: >=3.14
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: licence.txt
|
|
19
|
+
Requires-Dist: aiohttp>=3.10
|
|
20
|
+
Requires-Dist: netifaces>=0.11
|
|
18
21
|
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest>=6.2.2; extra == "test"
|
|
23
|
+
Requires-Dist: pytest-aio; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-aiohttp; extra == "test"
|
|
19
25
|
Provides-Extra: all
|
|
20
|
-
|
|
26
|
+
Requires-Dist: pytest>=6.2.2; extra == "all"
|
|
27
|
+
Requires-Dist: pytest-aio; extra == "all"
|
|
28
|
+
Requires-Dist: pytest-aiohttp; extra == "all"
|
|
29
|
+
Dynamic: license-file
|
|
21
30
|
|
|
22
31
|
|
|
23
32
|
# piZone - python interface to iZone aircon system
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Interface to the iZone airconditioner controller
|
|
2
|
+
|
|
3
|
+
Interaction mostly through the Controller and Zone classes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .controller import Controller
|
|
7
|
+
from .discovery import DiscoveryService, Listener, discovery
|
|
8
|
+
from .power import BatteryLevel, Power, PowerChannel, PowerDevice, PowerGroup
|
|
9
|
+
from .zone import Zone
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Controller",
|
|
13
|
+
"Zone",
|
|
14
|
+
"DiscoveryService",
|
|
15
|
+
"Listener",
|
|
16
|
+
"discovery",
|
|
17
|
+
"Power",
|
|
18
|
+
"PowerGroup",
|
|
19
|
+
"PowerDevice",
|
|
20
|
+
"PowerChannel",
|
|
21
|
+
"BatteryLevel",
|
|
22
|
+
]
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
"""Controller module"""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import asyncio
|
|
4
6
|
import json
|
|
5
7
|
import logging
|
|
6
8
|
from asyncio import Condition, Lock
|
|
7
9
|
from enum import Enum
|
|
8
10
|
from json.decoder import JSONDecodeError
|
|
9
|
-
from typing import Any, Dict,
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Dict, Union, cast
|
|
10
12
|
|
|
11
13
|
import aiohttp
|
|
12
|
-
from async_timeout import timeout
|
|
13
14
|
|
|
14
15
|
from .power import Power
|
|
15
16
|
from .zone import Zone
|
|
16
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .discovery import DiscoveryService, Listener
|
|
20
|
+
|
|
17
21
|
_LOG = logging.getLogger("pizone.controller")
|
|
18
22
|
|
|
19
23
|
|
|
@@ -58,10 +62,17 @@ class Controller:
|
|
|
58
62
|
"3-speed": [Fan.LOW, Fan.MED, Fan.HIGH, Fan.AUTO],
|
|
59
63
|
"2-speed": [Fan.LOW, Fan.HIGH, Fan.AUTO],
|
|
60
64
|
"var-speed": [Fan.LOW, Fan.MED, Fan.HIGH, Fan.AUTO],
|
|
61
|
-
}
|
|
65
|
+
}
|
|
62
66
|
|
|
63
67
|
def __init__(
|
|
64
|
-
self,
|
|
68
|
+
self,
|
|
69
|
+
discovery_service: DiscoveryService,
|
|
70
|
+
event_coordinator: Listener,
|
|
71
|
+
*,
|
|
72
|
+
device_uid: str,
|
|
73
|
+
device_ip: str,
|
|
74
|
+
is_v2: bool,
|
|
75
|
+
is_ipower: bool,
|
|
65
76
|
) -> None:
|
|
66
77
|
"""Create a controller interface.
|
|
67
78
|
|
|
@@ -83,25 +94,26 @@ class Controller:
|
|
|
83
94
|
device discovered at the given IP address or UId
|
|
84
95
|
"""
|
|
85
96
|
self._ip = device_ip
|
|
86
|
-
self.
|
|
97
|
+
self._discovery_service = discovery_service
|
|
98
|
+
self._event_coordinator = event_coordinator
|
|
87
99
|
self._device_uid = device_uid
|
|
88
100
|
self._is_v2 = is_v2
|
|
89
101
|
self._is_ipower = is_ipower
|
|
90
102
|
|
|
91
|
-
self.zones
|
|
92
|
-
self.fan_modes
|
|
93
|
-
self._system_settings = {}
|
|
94
|
-
self._power
|
|
103
|
+
self.zones: list[Zone] = []
|
|
104
|
+
self.fan_modes: list[Controller.Fan] = []
|
|
105
|
+
self._system_settings: Controller.ControllerData = {}
|
|
106
|
+
self._power: Power | None = None
|
|
95
107
|
|
|
96
|
-
self.
|
|
97
|
-
self._fail_exception = None
|
|
108
|
+
self._initialized: bool = False
|
|
109
|
+
self._fail_exception: Exception | None = None
|
|
98
110
|
|
|
99
111
|
self._sending_lock = Lock()
|
|
100
112
|
self._scan_condition = Condition()
|
|
101
113
|
|
|
102
114
|
async def _initialize(self) -> None:
|
|
103
115
|
"""Initialize the controller, does not complete until the system is
|
|
104
|
-
|
|
116
|
+
initialized."""
|
|
105
117
|
await self._refresh_system(notify=False)
|
|
106
118
|
|
|
107
119
|
self.fan_modes = Controller._VALID_FAN_MODES[
|
|
@@ -120,13 +132,13 @@ class Controller:
|
|
|
120
132
|
else:
|
|
121
133
|
self._power = None
|
|
122
134
|
|
|
123
|
-
self.
|
|
124
|
-
self.
|
|
135
|
+
self._initialized = True
|
|
136
|
+
self._discovery_service.create_task(self._poll_loop())
|
|
125
137
|
|
|
126
138
|
async def _poll_loop(self) -> None:
|
|
127
139
|
while True:
|
|
128
140
|
try:
|
|
129
|
-
async with timeout(Controller.REFRESH_INTERVAL):
|
|
141
|
+
async with asyncio.timeout(Controller.REFRESH_INTERVAL):
|
|
130
142
|
async with self._scan_condition:
|
|
131
143
|
await self._scan_condition.wait()
|
|
132
144
|
# triggered rescan, short delay
|
|
@@ -134,7 +146,7 @@ class Controller:
|
|
|
134
146
|
except asyncio.TimeoutError:
|
|
135
147
|
pass
|
|
136
148
|
|
|
137
|
-
if self.
|
|
149
|
+
if self._discovery_service.is_closed:
|
|
138
150
|
return
|
|
139
151
|
|
|
140
152
|
# pylint: disable=broad-except
|
|
@@ -142,7 +154,7 @@ class Controller:
|
|
|
142
154
|
_LOG.debug("Polling unit %s.", self._device_uid)
|
|
143
155
|
await self._refresh_all()
|
|
144
156
|
except ConnectionError:
|
|
145
|
-
_LOG.debug("Poll failed due to
|
|
157
|
+
_LOG.debug("Poll failed due to exception.", exc_info=True)
|
|
146
158
|
except Exception:
|
|
147
159
|
_LOG.error("Unexpected exception", exc_info=True)
|
|
148
160
|
|
|
@@ -157,7 +169,7 @@ class Controller:
|
|
|
157
169
|
return self._fail_exception is None
|
|
158
170
|
|
|
159
171
|
@property
|
|
160
|
-
def power(self) ->
|
|
172
|
+
def power(self) -> Power | None:
|
|
161
173
|
"""Power info"""
|
|
162
174
|
return self._power
|
|
163
175
|
|
|
@@ -177,9 +189,9 @@ class Controller:
|
|
|
177
189
|
return self._is_v2
|
|
178
190
|
|
|
179
191
|
@property
|
|
180
|
-
def discovery(self):
|
|
192
|
+
def discovery(self) -> Any:
|
|
181
193
|
"""The discovery service"""
|
|
182
|
-
return self.
|
|
194
|
+
return self._discovery_service
|
|
183
195
|
|
|
184
196
|
@property
|
|
185
197
|
def is_on(self) -> bool:
|
|
@@ -197,7 +209,7 @@ class Controller:
|
|
|
197
209
|
return self.Mode.FREE_AIR
|
|
198
210
|
return self.Mode(self._get_system_state("SysMode"))
|
|
199
211
|
|
|
200
|
-
async def set_mode(self, value: Mode):
|
|
212
|
+
async def set_mode(self, value: Mode) -> None:
|
|
201
213
|
"""Set system mode, cooling, heating, etc."""
|
|
202
214
|
if value == Controller.Mode.FREE_AIR:
|
|
203
215
|
if self.free_air:
|
|
@@ -234,7 +246,7 @@ class Controller:
|
|
|
234
246
|
"""Current setting for the sleep timer."""
|
|
235
247
|
return int(self._get_system_state("SleepTimer"))
|
|
236
248
|
|
|
237
|
-
async def set_sleep_timer(self, value: int):
|
|
249
|
+
async def set_sleep_timer(self, value: int) -> None:
|
|
238
250
|
"""The sleep timer.
|
|
239
251
|
Valid settings are 0, 30, 60, 90, 120
|
|
240
252
|
Raises:
|
|
@@ -268,19 +280,19 @@ class Controller:
|
|
|
268
280
|
await self._set_system_state("FreeAir", "FreeAir", "on" if value else "off")
|
|
269
281
|
|
|
270
282
|
@property
|
|
271
|
-
def temp_supply(self) ->
|
|
283
|
+
def temp_supply(self) -> float | None:
|
|
272
284
|
"""Current supply, or in duct, air temperature."""
|
|
273
285
|
return float(self._get_system_state("Supply")) or None
|
|
274
286
|
|
|
275
287
|
@property
|
|
276
|
-
def temp_setpoint(self) ->
|
|
288
|
+
def temp_setpoint(self) -> float | None:
|
|
277
289
|
"""AC unit setpoint temperature.
|
|
278
290
|
This is the unit target temp with with rasMode == RAS,
|
|
279
291
|
or with rasMode == master and ctrlZone == 13.
|
|
280
292
|
"""
|
|
281
293
|
return float(self._get_system_state("Setpoint")) or None
|
|
282
294
|
|
|
283
|
-
async def set_temp_setpoint(self, value: float):
|
|
295
|
+
async def set_temp_setpoint(self, value: float) -> None:
|
|
284
296
|
"""AC unit setpoint temperature.
|
|
285
297
|
This is the unit target temp with with rasMode == RAS,
|
|
286
298
|
or with rasMode == master and ctrlZone == 13.
|
|
@@ -298,7 +310,7 @@ class Controller:
|
|
|
298
310
|
await self._set_system_state("Setpoint", "UnitSetpoint", value, str(value))
|
|
299
311
|
|
|
300
312
|
@property
|
|
301
|
-
def temp_return(self) ->
|
|
313
|
+
def temp_return(self) -> float | None:
|
|
302
314
|
"""The return, or room, air temperature"""
|
|
303
315
|
return float(self._get_system_state("Temp")) or None
|
|
304
316
|
|
|
@@ -314,7 +326,7 @@ class Controller:
|
|
|
314
326
|
|
|
315
327
|
@property
|
|
316
328
|
def temp_max(self) -> float:
|
|
317
|
-
"""The value for the eco lock
|
|
329
|
+
"""The value for the eco lock maximum, or 30 if eco lock not set"""
|
|
318
330
|
return float(self._get_system_state("EcoMax")) if self.eco_lock else 30.0
|
|
319
331
|
|
|
320
332
|
@property
|
|
@@ -368,17 +380,15 @@ class Controller:
|
|
|
368
380
|
|
|
369
381
|
async def _refresh_system(self, notify: bool = True) -> None:
|
|
370
382
|
"""Refresh the system settings."""
|
|
371
|
-
values = await self._get_resource(
|
|
372
|
-
"SystemSettings"
|
|
373
|
-
) # type: Controller.ControllerData # noqa
|
|
383
|
+
values: Controller.ControllerData = await self._get_resource("SystemSettings")
|
|
374
384
|
if self._device_uid != values["AirStreamDeviceUId"]:
|
|
375
|
-
_LOG.error("_refresh_system called with
|
|
385
|
+
_LOG.error("_refresh_system called with non-matching device ID")
|
|
376
386
|
return
|
|
377
387
|
|
|
378
388
|
self._system_settings = values
|
|
379
389
|
|
|
380
390
|
if notify:
|
|
381
|
-
self.
|
|
391
|
+
self._event_coordinator.controller_update(self)
|
|
382
392
|
|
|
383
393
|
async def _refresh_power(self, notify: bool = True) -> None:
|
|
384
394
|
if self._power is None or not self._power.enabled:
|
|
@@ -387,7 +397,7 @@ class Controller:
|
|
|
387
397
|
updated = await self._power.refresh()
|
|
388
398
|
|
|
389
399
|
if updated and notify:
|
|
390
|
-
self.
|
|
400
|
+
self._event_coordinator.power_update(self)
|
|
391
401
|
|
|
392
402
|
async def _refresh_zones(self, notify: bool = True) -> None:
|
|
393
403
|
"""Refresh the Zone information."""
|
|
@@ -396,7 +406,7 @@ class Controller:
|
|
|
396
406
|
*[self._refresh_zone_group(i, notify) for i in range(0, zones, 4)]
|
|
397
407
|
)
|
|
398
408
|
|
|
399
|
-
async def _refresh_zone_group(self, group: int, notify: bool = True):
|
|
409
|
+
async def _refresh_zone_group(self, group: int, notify: bool = True) -> None:
|
|
400
410
|
assert group in [0, 4, 8]
|
|
401
411
|
zone_data_part = await self._get_resource(f"Zones{group + 1}_{group + 4}")
|
|
402
412
|
|
|
@@ -405,25 +415,31 @@ class Controller:
|
|
|
405
415
|
# pylint: disable=protected-access
|
|
406
416
|
self.zones[i + group]._update_zone(zone_data, notify)
|
|
407
417
|
|
|
408
|
-
def _refresh_address(self, address):
|
|
418
|
+
def _refresh_address(self, address: str) -> None:
|
|
409
419
|
"""Called from discovery to update the address"""
|
|
410
420
|
self._ip = address
|
|
411
421
|
# Signal to the retry connection loop to have another go.
|
|
412
422
|
if self._fail_exception:
|
|
413
|
-
self.
|
|
423
|
+
self._discovery_service.create_task(self._retry_connection())
|
|
414
424
|
|
|
415
|
-
def _get_system_state(self, state):
|
|
425
|
+
def _get_system_state(self, state: str) -> Any:
|
|
416
426
|
self._ensure_connected()
|
|
417
427
|
return self._system_settings.get(state)
|
|
418
428
|
|
|
419
|
-
async def _set_system_state(
|
|
429
|
+
async def _set_system_state(
|
|
430
|
+
self,
|
|
431
|
+
state: str,
|
|
432
|
+
command: str,
|
|
433
|
+
value: DictValue,
|
|
434
|
+
send: Any | None = None,
|
|
435
|
+
) -> None:
|
|
420
436
|
if send is None:
|
|
421
437
|
send = value
|
|
422
438
|
await self._send_command_async(command, {command: send})
|
|
423
439
|
|
|
424
440
|
# Update state and trigger rescan
|
|
425
441
|
self._system_settings[state] = value
|
|
426
|
-
self.
|
|
442
|
+
self._event_coordinator.controller_update(self)
|
|
427
443
|
await self.refresh()
|
|
428
444
|
|
|
429
445
|
def _ensure_connected(self) -> None:
|
|
@@ -432,14 +448,14 @@ class Controller:
|
|
|
432
448
|
"Unable to connect to the controller"
|
|
433
449
|
) from self._fail_exception
|
|
434
450
|
|
|
435
|
-
def _failed_connection(self, ex):
|
|
451
|
+
def _failed_connection(self, ex: Exception) -> None:
|
|
436
452
|
if self._fail_exception:
|
|
437
453
|
self._fail_exception = ex
|
|
438
454
|
return
|
|
439
455
|
self._fail_exception = ex
|
|
440
|
-
if not self.
|
|
456
|
+
if not self._initialized:
|
|
441
457
|
return
|
|
442
|
-
self.
|
|
458
|
+
self._event_coordinator.controller_disconnected(self, ex)
|
|
443
459
|
|
|
444
460
|
async def _retry_connection(self) -> None:
|
|
445
461
|
_LOG.info(
|
|
@@ -453,11 +469,11 @@ class Controller:
|
|
|
453
469
|
|
|
454
470
|
self._fail_exception = None
|
|
455
471
|
|
|
456
|
-
self.
|
|
472
|
+
self._event_coordinator.controller_update(self)
|
|
457
473
|
for zone in self.zones:
|
|
458
|
-
self.
|
|
459
|
-
self.
|
|
460
|
-
self.
|
|
474
|
+
self._event_coordinator.zone_update(self, zone)
|
|
475
|
+
self._event_coordinator.power_update(self)
|
|
476
|
+
self._event_coordinator.controller_reconnected(self)
|
|
461
477
|
except ConnectionError:
|
|
462
478
|
# Expected, just carry on.
|
|
463
479
|
_LOG.warning(
|
|
@@ -466,13 +482,16 @@ class Controller:
|
|
|
466
482
|
exc_info=True,
|
|
467
483
|
)
|
|
468
484
|
|
|
469
|
-
async def _get_resource(self, resource: str):
|
|
485
|
+
async def _get_resource(self, resource: str) -> Any:
|
|
470
486
|
try:
|
|
471
|
-
session = self.
|
|
472
|
-
async with
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
487
|
+
session = self._discovery_service.session
|
|
488
|
+
async with (
|
|
489
|
+
self._sending_lock,
|
|
490
|
+
session.get(
|
|
491
|
+
f"http://{self.device_ip}/{resource}",
|
|
492
|
+
timeout=Controller.REQUEST_TIMEOUT,
|
|
493
|
+
) as response,
|
|
494
|
+
):
|
|
476
495
|
try:
|
|
477
496
|
return await response.json(content_type=None)
|
|
478
497
|
except JSONDecodeError as ex:
|
|
@@ -487,9 +506,9 @@ class Controller:
|
|
|
487
506
|
self._failed_connection(ex)
|
|
488
507
|
raise ConnectionError("Unable to connect to the controller") from ex
|
|
489
508
|
|
|
490
|
-
async def _send_command_async(self, command: str, data: Any):
|
|
509
|
+
async def _send_command_async(self, command: str, data: Any) -> str:
|
|
491
510
|
# For some reason aiohttp fragments post requests, which causes
|
|
492
|
-
# the server to fail disgracefully.
|
|
511
|
+
# the server to fail disgracefully. Implemented rough and dirty
|
|
493
512
|
# HTTP POST client.
|
|
494
513
|
loop = asyncio.get_running_loop()
|
|
495
514
|
on_complete = loop.create_future()
|
|
@@ -499,7 +518,7 @@ class Controller:
|
|
|
499
518
|
def __init__(self) -> None:
|
|
500
519
|
self.response = bytearray()
|
|
501
520
|
|
|
502
|
-
def connection_made(self, transport):
|
|
521
|
+
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
|
503
522
|
body = json.dumps(data).encode("latin_1")
|
|
504
523
|
header = (
|
|
505
524
|
f"POST /{command} HTTP/1.1\r\n"
|
|
@@ -508,12 +527,12 @@ class Controller:
|
|
|
508
527
|
"\r\n"
|
|
509
528
|
).encode()
|
|
510
529
|
_LOG.debug("Writing message to %s", device_ip)
|
|
511
|
-
transport.write(header + body)
|
|
530
|
+
cast(asyncio.WriteTransport, transport).write(header + body)
|
|
512
531
|
|
|
513
|
-
def data_received(self, data):
|
|
532
|
+
def data_received(self, data: bytes) -> None:
|
|
514
533
|
self.response += data
|
|
515
534
|
|
|
516
|
-
def eof_received(self):
|
|
535
|
+
def eof_received(self) -> None:
|
|
517
536
|
full = self.response.decode("latin_1")
|
|
518
537
|
if not full:
|
|
519
538
|
on_complete.set_exception(RuntimeError("Empty HTTP Response"))
|
|
@@ -536,7 +555,7 @@ class Controller:
|
|
|
536
555
|
|
|
537
556
|
# The server doesn't tolerate multiple requests in fly concurrently
|
|
538
557
|
try:
|
|
539
|
-
async with self._sending_lock, timeout(Controller.REQUEST_TIMEOUT):
|
|
558
|
+
async with self._sending_lock, asyncio.timeout(Controller.REQUEST_TIMEOUT):
|
|
540
559
|
await loop.create_connection(_PostProtocol, self.device_ip, 80)
|
|
541
560
|
await on_complete
|
|
542
561
|
|