python-izone 1.3.1__py3-none-any.whl → 1.3.2__py3-none-any.whl

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.
pizone/controller.py CHANGED
@@ -122,7 +122,7 @@ class Controller:
122
122
  """Load system, zone, and optional power data from the device.
123
123
 
124
124
  Raises:
125
- ConnectionError: If any initial HTTP request fails.
125
+ ConnectionError: If a required HTTP request fails.
126
126
  KeyError: If a required field is missing from a device response.
127
127
  """
128
128
  await self._refresh_system(notify=False)
@@ -131,24 +131,58 @@ class Controller:
131
131
  str(self._system_settings.get("FanAuto", "disabled"))
132
132
  ]
133
133
 
134
+ await self._probe_v2_api()
135
+
134
136
  zone_count = int(self._system_settings["NoOfZones"])
135
137
  self.zones = [Zone(self, i) for i in range(zone_count)]
136
138
 
137
- if self._is_ipower:
138
- self._power = Power(self)
139
- await asyncio.gather(
140
- self._refresh_zones(notify=False),
141
- self._power.init(),
142
- )
143
- if self._power.enabled:
144
- await self._refresh_power(notify=False)
145
- else:
146
- self._power = None
147
- await self._refresh_zones(notify=False)
139
+ await self._refresh_zones(notify=False)
140
+
141
+ await self._probe_power()
148
142
 
149
143
  self._initialized = True
150
144
  self._discovery_service.create_task(self._poll_loop())
151
145
 
146
+ async def _probe_v2_api(self) -> None:
147
+ """Detect V2 API support; non-fatal on failure."""
148
+ try:
149
+ response = await self._send_command_async(
150
+ "iZoneRequestV2",
151
+ {"iZoneV2Request": {"Type": 1, "No": 0, "No1": 0}},
152
+ mark_disconnected=False,
153
+ )
154
+ data = json.loads(response)
155
+ uid = data["AirStreamDeviceUId"]
156
+ self._is_v2 = uid == self._device_uid and "SystemV2" in data
157
+ except (ConnectionError, ControllerCommandError, json.JSONDecodeError, KeyError):
158
+ self._is_v2 = False
159
+
160
+ async def _probe_power(self) -> None:
161
+ """Probe power monitor endpoint when discovery hinted iPower; non-fatal."""
162
+ if not self._is_ipower:
163
+ self._power = None
164
+ return
165
+
166
+ try:
167
+ power = Power(self)
168
+ await power.init(mark_disconnected=False)
169
+ if power.enabled:
170
+ self._power = power
171
+ return
172
+ _LOG.warning(
173
+ "Power monitor disabled on uid=%s; skipping power support",
174
+ self._device_uid,
175
+ )
176
+ except (ConnectionError, ControllerCommandError, json.JSONDecodeError, KeyError):
177
+ _LOG.warning(
178
+ "Power monitor probe failed for uid=%s",
179
+ self._device_uid,
180
+ exc_info=True,
181
+ )
182
+
183
+ self._is_ipower = False
184
+ self._power = None
185
+
152
186
  async def _poll_loop(self) -> None:
153
187
  while True:
154
188
  try:
@@ -206,6 +240,11 @@ class Controller:
206
240
  """Return whether this is a v2 controller."""
207
241
  return self._is_v2
208
242
 
243
+ @property
244
+ def is_ipower(self) -> bool:
245
+ """Return whether power monitoring is available."""
246
+ return self._is_ipower
247
+
209
248
  @property
210
249
  def discovery(self) -> DiscoveryService:
211
250
  """Discovery service for this controller."""
@@ -630,10 +669,14 @@ class Controller:
630
669
  raise ConnectionError("Unable to connect to the controller") from ex
631
670
 
632
671
  async def _send_command_async(
633
- self, command: str, data: dict[str, Any]
672
+ self, command: str, data: dict[str, Any], *, mark_disconnected: bool = True
634
673
  ) -> str:
635
674
  """Send a command to the device via HTTP POST.
636
675
 
676
+ Args:
677
+ mark_disconnected: When ``False``, transport failures do not mark
678
+ the controller disconnected.
679
+
637
680
  Raises:
638
681
  ConnectionError: If the device cannot be reached or the HTTP request fails.
639
682
  ControllerCommandError: If the device returns HTTP 4xx or an
@@ -665,7 +708,8 @@ class Controller:
665
708
  )
666
709
  result = await response.text(encoding="latin_1")
667
710
  except (asyncio.TimeoutError, aiohttp.ClientError) as ex:
668
- self._failed_connection(ex)
711
+ if mark_disconnected:
712
+ self._failed_connection(ex)
669
713
  raise ConnectionError("Unable to connect to controller") from ex
670
714
 
671
715
  if len(result) >= 7 and result[:6] == "{ERROR":
pizone/power.py CHANGED
@@ -191,15 +191,21 @@ class Power:
191
191
  self._devices = tuple(PowerDevice(self, i) for i in range(5))
192
192
  self._groups: tuple[PowerGroup, ...] | None = None
193
193
 
194
- async def init(self) -> None:
194
+ async def init(self, *, mark_disconnected: bool = True) -> None:
195
195
  """Load power monitor configuration from the device.
196
196
 
197
+ Args:
198
+ mark_disconnected: When ``False``, connection failures during this
199
+ request do not mark the controller disconnected.
200
+
197
201
  Raises:
198
202
  ConnectionError: If the HTTP request fails.
199
203
  json.JSONDecodeError: If the device response is not valid JSON.
200
204
  KeyError: If the response is missing required fields.
201
205
  """
202
- self._config = await self._do_request(1, "PowerMonitorConfig")
206
+ self._config = await self._do_request(
207
+ 1, "PowerMonitorConfig", mark_disconnected=mark_disconnected
208
+ )
203
209
  gdict: dict[int, list[PowerChannel]] = {}
204
210
  for dev in self.devices:
205
211
  for chan in dev.channels:
@@ -226,9 +232,15 @@ class Power:
226
232
  self._status = status
227
233
  return True
228
234
 
229
- async def _do_request(self, req_type: int, result: str) -> dict[str, Any]:
235
+ async def _do_request(
236
+ self, req_type: int, result: str, *, mark_disconnected: bool = True
237
+ ) -> dict[str, Any]:
230
238
  """Send a power monitor request to the device.
231
239
 
240
+ Args:
241
+ mark_disconnected: When ``False``, connection failures do not mark
242
+ the controller disconnected.
243
+
232
244
  Raises:
233
245
  ConnectionError: If the HTTP request fails.
234
246
  json.JSONDecodeError: If the device response is not valid JSON.
@@ -236,7 +248,9 @@ class Power:
236
248
  """
237
249
  # pylint: disable=protected-access
238
250
  datas = await self._controller._send_command_async(
239
- "PowerRequest", {"PowerRequest": {"Type": req_type, "No": 0, "No1": 0}}
251
+ "PowerRequest",
252
+ {"PowerRequest": {"Type": req_type, "No": 0, "No1": 0}},
253
+ mark_disconnected=mark_disconnected,
240
254
  )
241
255
  data = json.loads(datas)
242
256
  return cast(dict[str, Any], data[result])
pizone/version.py CHANGED
@@ -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.1'
22
- __version_tuple__ = version_tuple = (1, 3, 1)
21
+ __version__ = version = '1.3.2'
22
+ __version_tuple__ = version_tuple = (1, 3, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-izone
3
- Version: 1.3.1
3
+ Version: 1.3.2
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,12 @@
1
+ pizone/__init__.py,sha256=xRQgtXzCtsGhJDBOzDxV105Trv2NCFe8Em6fZEqZ4bc,997
2
+ pizone/controller.py,sha256=hO2B7sIZt8Djt_Nq0_ov7ocNxBy9Nk_DtkcEmixFt2s,26114
3
+ pizone/discovery.py,sha256=tvZs5npvL2y41vyYxn5V6EDCI9v27_Z-vy_ezSN-RA4,18133
4
+ pizone/exceptions.py,sha256=mWsvXzBNh6mKOPs0QFLqgO6ipa6Se6PEGhFTwr9_ehA,362
5
+ pizone/power.py,sha256=4kWS_XilZki7dLzlJ0cKYoz8s6pS76bAtrHhUXTZ_eg,9039
6
+ pizone/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ pizone/version.py,sha256=3UteJz_pk76FGRZGb0537LBHxRlaYKkTw4QYKVpfrjs,520
8
+ pizone/zone.py,sha256=rDv-Q9k9HGIzK3arBxpKoAa-xRGETQkgBOTulMdOhqc,7163
9
+ python_izone-1.3.2.dist-info/METADATA,sha256=pEMLJ6CTDnghdy4FJ8wIducmpD-ZxAnVPv26t347FuQ,2657
10
+ python_izone-1.3.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
11
+ python_izone-1.3.2.dist-info/licenses/licence.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
12
+ python_izone-1.3.2.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- pizone/__init__.py,sha256=xRQgtXzCtsGhJDBOzDxV105Trv2NCFe8Em6fZEqZ4bc,997
2
- pizone/controller.py,sha256=mR7-tJkdkXx-s7M32ZWpOUED8Gw_7NPTkUnLjnPhB2Q,24568
3
- pizone/discovery.py,sha256=tvZs5npvL2y41vyYxn5V6EDCI9v27_Z-vy_ezSN-RA4,18133
4
- pizone/exceptions.py,sha256=mWsvXzBNh6mKOPs0QFLqgO6ipa6Se6PEGhFTwr9_ehA,362
5
- pizone/power.py,sha256=Rp5ZoT-L8YR0mraF8YNaUqfgZTvgSZSNuBT0U5Nzt70,8536
6
- pizone/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- pizone/version.py,sha256=x_Vew2VGpHa9c663j9Zg8FzEkVKHvv3k0XiN4-Ja9uw,520
8
- pizone/zone.py,sha256=rDv-Q9k9HGIzK3arBxpKoAa-xRGETQkgBOTulMdOhqc,7163
9
- python_izone-1.3.1.dist-info/METADATA,sha256=oN4I7S2YE8C3PtBfakmqkdr6rYs-tZsENG_M4WNM1a0,2657
10
- python_izone-1.3.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
11
- python_izone-1.3.1.dist-info/licenses/licence.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
12
- python_izone-1.3.1.dist-info/RECORD,,