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.
Files changed (37) hide show
  1. {python-izone-1.2.9 → python_izone-1.2.10}/.github/workflows/main.yml +4 -4
  2. python_izone-1.2.10/.pre-commit-config.yaml +6 -0
  3. {python-izone-1.2.9 → python_izone-1.2.10}/.pylintrc +3 -7
  4. {python-izone-1.2.9 → python_izone-1.2.10}/.vscode/settings.json +14 -2
  5. {python-izone-1.2.9 → python_izone-1.2.10}/PKG-INFO +14 -5
  6. python_izone-1.2.10/pizone/__init__.py +22 -0
  7. {python-izone-1.2.9 → python_izone-1.2.10}/pizone/controller.py +78 -59
  8. {python-izone-1.2.9 → python_izone-1.2.10}/pizone/discovery.py +191 -155
  9. {python-izone-1.2.9 → python_izone-1.2.10}/pizone/power.py +12 -13
  10. python_izone-1.2.10/pizone/version.py +24 -0
  11. {python-izone-1.2.9 → python_izone-1.2.10}/pizone/zone.py +12 -12
  12. {python-izone-1.2.9 → python_izone-1.2.10}/pyproject.toml +3 -3
  13. python_izone-1.2.10/pytest.ini +5 -0
  14. {python-izone-1.2.9 → python_izone-1.2.10}/python_izone.egg-info/PKG-INFO +14 -5
  15. {python-izone-1.2.9 → python_izone-1.2.10}/python_izone.egg-info/SOURCES.txt +2 -0
  16. {python-izone-1.2.9 → python_izone-1.2.10}/python_izone.egg-info/requires.txt +2 -3
  17. {python-izone-1.2.9 → python_izone-1.2.10}/setup.cfg +4 -5
  18. python_izone-1.2.10/tests/__init__.py +1 -0
  19. {python-izone-1.2.9 → python_izone-1.2.10}/tests/conftest.py +38 -25
  20. {python-izone-1.2.9 → python_izone-1.2.10}/tests/test_discovery.py +109 -22
  21. {python-izone-1.2.9 → python_izone-1.2.10}/tests/test_fullstack.py +2 -1
  22. python-izone-1.2.9/pizone/__init__.py +0 -22
  23. python-izone-1.2.9/pizone/version.py +0 -5
  24. python-izone-1.2.9/tests/__init__.py +0 -1
  25. {python-izone-1.2.9 → python_izone-1.2.10}/.gitattributes +0 -0
  26. {python-izone-1.2.9 → python_izone-1.2.10}/.gitignore +0 -0
  27. {python-izone-1.2.9 → python_izone-1.2.10}/.vscode/.ropeproject/config.py +0 -0
  28. {python-izone-1.2.9 → python_izone-1.2.10}/.vscode/launch.json +0 -0
  29. {python-izone-1.2.9 → python_izone-1.2.10}/README.md +0 -0
  30. {python-izone-1.2.9 → python_izone-1.2.10}/api_docs/AC-DOC-1401-11_iZoneEthernetInterface.pdf +0 -0
  31. {python-izone-1.2.9 → python_izone-1.2.10}/api_docs/iPower_JSON_datastrings_Out.h +0 -0
  32. {python-izone-1.2.9 → python_izone-1.2.10}/api_docs/iZone_JSON_datastrings.h +0 -0
  33. {python-izone-1.2.9 → python_izone-1.2.10}/licence.txt +0 -0
  34. {python-izone-1.2.9 → python_izone-1.2.10}/python_izone.egg-info/dependency_links.txt +0 -0
  35. {python-izone-1.2.9 → python_izone-1.2.10}/python_izone.egg-info/top_level.txt +0 -0
  36. {python-izone-1.2.9 → python_izone-1.2.10}/setup.py +0 -0
  37. {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.9", "3.10"]
11
+ python-version: ["3.14"]
12
12
 
13
13
  steps:
14
- - uses: actions/checkout@v3
14
+ - uses: actions/checkout@v4
15
15
  - name: Set up Python ${{ matrix.python-version }}
16
- uses: actions/setup-python@v4
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.9'
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 }}
@@ -0,0 +1,6 @@
1
+ repos:
2
+ - repo: https://github.com/psf/black
3
+ rev: 25.1.0
4
+ hooks:
5
+ - id: black
6
+ args: [--check, --diff]
@@ -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.9
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.pythonPath": "venv\\Scripts\\python.exe",
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
1
+ Metadata-Version: 2.4
2
2
  Name: python-izone
3
- Version: 1.2.9
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.9
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.9
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
- License-File: licence.txt
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, List, Optional, Union
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
- } # type: Dict[str, List[Fan]]
65
+ }
62
66
 
63
67
  def __init__(
64
- self, discovery, device_uid: str, device_ip: str, is_v2: bool, is_ipower: bool
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._discovery = discovery
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 = [] # type: List[Zone]
92
- self.fan_modes = [] # type: List[Controller.Fan]
93
- self._system_settings = {} # type: Controller.ControllerData
94
- self._power = None # type: Optional[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._initialised = False
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
- initialised."""
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._initialised = True
124
- self._discovery.create_task(self._poll_loop())
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._discovery.is_closed:
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 exeption.", exc_info=True)
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) -> Optional[Power]:
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._discovery
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) -> Optional[float]:
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) -> Optional[float]:
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) -> Optional[float]:
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 maxium, or 30 if eco lock not set"""
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 unmatching device ID")
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._discovery.controller_update(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._discovery.power_update(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._discovery.create_task(self._retry_connection())
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(self, state, command, value, send=None):
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._discovery.controller_update(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._initialised:
456
+ if not self._initialized:
441
457
  return
442
- self._discovery.controller_disconnected(self, ex)
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._discovery.controller_update(self)
472
+ self._event_coordinator.controller_update(self)
457
473
  for zone in self.zones:
458
- self._discovery.zone_update(self, zone)
459
- self._discovery.power_update(self)
460
- self._discovery.controller_reconnected(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._discovery.session
472
- async with self._sending_lock, session.get(
473
- f"http://{self.device_ip}/{resource}",
474
- timeout=Controller.REQUEST_TIMEOUT,
475
- ) as response:
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. Implimented rough and dirty
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