python-omnilogic-local 0.26.0__tar.gz → 0.27.1__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_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/PKG-INFO +1 -1
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/api.py +2 -8
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/constants.py +2 -2
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/mock_api.py +8 -2
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/protocol.py +42 -16
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/models/filter_diagnostics.py +12 -2
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/models/leadmessage.py +1 -1
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/models/mspconfig.py +6 -1
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/models/telemetry.py +7 -3
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyproject.toml +3 -8
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/LICENSE +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/README.md +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/__init__.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/_base.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/__init__.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/exceptions.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/backyard.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/bow.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/chlorinator.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/chlorinator_equip.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/__init__.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/cli.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/debug/__init__.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/debug/commands.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/__init__.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/backyard.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/bows.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/chlorinators.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/commands.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/csads.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/filters.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/groups.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/heaters.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/lights.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/pumps.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/relays.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/schedules.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/sensors.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/valves.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/pcap_utils.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/utils.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/collections.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/colorlogiclight.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/csad.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/csad_equip.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/decorators.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/filter.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/groups.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/heater.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/heater_equip.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/models/__init__.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/models/const.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/models/exceptions.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/omnilogic.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/omnitypes.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/pump.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/py.typed +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/relay.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/schedule.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/sensor.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/system.py +0 -0
- {python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-omnilogic-local
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.1
|
|
4
4
|
Summary: A library for local control of Hayward OmniHub/OmniLogic pool controllers using their local API
|
|
5
5
|
Author: Chris Jowett, djtimca, garionphx
|
|
6
6
|
Author-email: Chris Jowett <421501+cryptk@users.noreply.github.com>
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/api.py
RENAMED
|
@@ -8,11 +8,7 @@ from typing import TYPE_CHECKING, Literal, overload
|
|
|
8
8
|
from pyomnilogic_local.models.filter_diagnostics import FilterDiagnostics
|
|
9
9
|
from pyomnilogic_local.models.mspconfig import MSPConfig
|
|
10
10
|
from pyomnilogic_local.models.telemetry import Telemetry
|
|
11
|
-
from pyomnilogic_local.omnitypes import
|
|
12
|
-
ColorLogicBrightness,
|
|
13
|
-
ColorLogicSpeed,
|
|
14
|
-
MessageType,
|
|
15
|
-
)
|
|
11
|
+
from pyomnilogic_local.omnitypes import ColorLogicBrightness, ColorLogicSpeed, MessageType
|
|
16
12
|
|
|
17
13
|
from .constants import (
|
|
18
14
|
DEFAULT_CONTROLLER_PORT,
|
|
@@ -117,10 +113,8 @@ class OmniLogicAPI:
|
|
|
117
113
|
|
|
118
114
|
@overload
|
|
119
115
|
async def async_send_message(self, message_type: MessageType, message: str | None, need_response: Literal[True]) -> str: ...
|
|
120
|
-
|
|
121
116
|
@overload
|
|
122
117
|
async def async_send_message(self, message_type: MessageType, message: str | None, need_response: Literal[False]) -> None: ...
|
|
123
|
-
|
|
124
118
|
async def async_send_message(self, message_type: MessageType, message: str | None, need_response: bool = False) -> str | None:
|
|
125
119
|
"""Send a message via the Hayward Omni UDP protocol along with properly handling timeouts and responses.
|
|
126
120
|
|
|
@@ -138,7 +132,7 @@ class OmniLogicAPI:
|
|
|
138
132
|
resp: str | None = None
|
|
139
133
|
try:
|
|
140
134
|
if need_response:
|
|
141
|
-
resp = await protocol.send_and_receive(message_type, message)
|
|
135
|
+
resp = await protocol.send_and_receive(message_type, message, response_timeout=self.response_timeout)
|
|
142
136
|
else:
|
|
143
137
|
await protocol.send_message(message_type, message)
|
|
144
138
|
finally:
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/constants.py
RENAMED
|
@@ -13,8 +13,8 @@ BLOCK_MESSAGE_HEADER_OFFSET = 8 # Offset to skip block message header and get t
|
|
|
13
13
|
# Timing Constants (in seconds)
|
|
14
14
|
OMNI_RETRANSMIT_TIME = 2.1 # Time Omni waits before retransmitting a packet
|
|
15
15
|
OMNI_RETRANSMIT_COUNT = 5 # Number of retransmit attempts (6 total including initial)
|
|
16
|
-
ACK_WAIT_TIMEOUT =
|
|
17
|
-
DEFAULT_RESPONSE_TIMEOUT =
|
|
16
|
+
ACK_WAIT_TIMEOUT = 1 # Timeout waiting for ACK response, 0.5 showed to be just a tad too short in some cases.
|
|
17
|
+
DEFAULT_RESPONSE_TIMEOUT = OMNI_RETRANSMIT_TIME * OMNI_RETRANSMIT_COUNT # Default timeout for receiving responses
|
|
18
18
|
|
|
19
19
|
# Network Constants
|
|
20
20
|
DEFAULT_CONTROLLER_PORT = 10444 # Default UDP port for OmniLogic communication
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/mock_api.py
RENAMED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any, Literal, overload
|
|
8
|
+
from typing import Any, Literal, TypedDict, overload
|
|
9
9
|
|
|
10
10
|
from pyomnilogic_local.models.mspconfig import MSPConfig
|
|
11
11
|
from pyomnilogic_local.models.telemetry import Telemetry
|
|
@@ -13,6 +13,12 @@ from pyomnilogic_local.models.telemetry import Telemetry
|
|
|
13
13
|
_LOGGER = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class SimData(TypedDict):
|
|
17
|
+
telemetry: str
|
|
18
|
+
msp_config: str
|
|
19
|
+
filepath: str
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
class OmniLogicMockAPI:
|
|
17
23
|
"""Drop-in replacement for OmniLogicAPI that serves pre-recorded data from one or more JSON files.
|
|
18
24
|
|
|
@@ -43,7 +49,7 @@ class OmniLogicMockAPI:
|
|
|
43
49
|
"""
|
|
44
50
|
paths = filepath.split(",")
|
|
45
51
|
|
|
46
|
-
self._sim_data: list[
|
|
52
|
+
self._sim_data: list[SimData] = []
|
|
47
53
|
for fp in paths:
|
|
48
54
|
path = Path(fp)
|
|
49
55
|
if not path.exists():
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/protocol.py
RENAMED
|
@@ -15,6 +15,7 @@ from pyomnilogic_local.omnitypes import ClientType, MessageType
|
|
|
15
15
|
from .constants import (
|
|
16
16
|
ACK_WAIT_TIMEOUT,
|
|
17
17
|
BLOCK_MESSAGE_HEADER_OFFSET,
|
|
18
|
+
DEFAULT_RESPONSE_TIMEOUT,
|
|
18
19
|
MAX_FRAGMENT_WAIT_TIME,
|
|
19
20
|
MAX_QUEUE_SIZE,
|
|
20
21
|
OMNI_RETRANSMIT_COUNT,
|
|
@@ -25,11 +26,7 @@ from .constants import (
|
|
|
25
26
|
XML_ENCODING,
|
|
26
27
|
XML_NAMESPACE,
|
|
27
28
|
)
|
|
28
|
-
from .exceptions import
|
|
29
|
-
OmniFragmentationError,
|
|
30
|
-
OmniMessageFormatError,
|
|
31
|
-
OmniTimeoutError,
|
|
32
|
-
)
|
|
29
|
+
from .exceptions import OmniFragmentationError, OmniMessageFormatError, OmniTimeoutError
|
|
33
30
|
|
|
34
31
|
_LOGGER = logging.getLogger(__name__)
|
|
35
32
|
|
|
@@ -225,21 +222,34 @@ class OmniLogicProtocol(asyncio.DatagramProtocol):
|
|
|
225
222
|
Exception: If a protocol error occurs.
|
|
226
223
|
"""
|
|
227
224
|
# Wait for either an ACK message or an error
|
|
228
|
-
|
|
225
|
+
# Race condition: datagram_received() calls put_nowait() synchronously, so data_task may
|
|
226
|
+
# already be done when wait_for fires its timeout CancelledError. In that case we catch
|
|
227
|
+
# the cancellation, skip re-looping, and fall through to check the result below. If the
|
|
228
|
+
# result is our ACK we return normally, suppressing the CancelledError so wait_for treats
|
|
229
|
+
# the call as successful. If it isn't, we re-raise after the loop.
|
|
230
|
+
cancelled: asyncio.CancelledError | None = None
|
|
231
|
+
retry = True
|
|
232
|
+
while retry:
|
|
229
233
|
# Wait for either a message or an error
|
|
230
234
|
data_task = asyncio.create_task(self.data_queue.get())
|
|
231
235
|
error_task = asyncio.create_task(self.error_queue.get())
|
|
232
|
-
|
|
236
|
+
try:
|
|
237
|
+
done, pending = await asyncio.wait([data_task, error_task], return_when=asyncio.FIRST_COMPLETED)
|
|
238
|
+
except asyncio.CancelledError as exc:
|
|
239
|
+
retry = False
|
|
240
|
+
cancelled = exc
|
|
241
|
+
done = {t for t in (data_task, error_task) if t.done()}
|
|
242
|
+
pending = {t for t in (data_task, error_task) if not t.done()}
|
|
233
243
|
|
|
234
244
|
# Cancel any pending tasks to avoid "Task was destroyed but it is pending" warnings
|
|
235
245
|
for task in pending:
|
|
236
246
|
task.cancel()
|
|
237
247
|
|
|
238
248
|
if error_task in done:
|
|
239
|
-
|
|
240
|
-
if isinstance(
|
|
241
|
-
raise
|
|
242
|
-
_LOGGER.error("Unknown error occurred during communication with OmniLogic: %s",
|
|
249
|
+
err = error_task.result()
|
|
250
|
+
if isinstance(err, Exception):
|
|
251
|
+
raise err
|
|
252
|
+
_LOGGER.error("Unknown error occurred during communication with OmniLogic: %s", err)
|
|
243
253
|
if data_task in done:
|
|
244
254
|
message = data_task.result()
|
|
245
255
|
if message.id == ack_id:
|
|
@@ -251,6 +261,9 @@ class OmniLogicProtocol(asyncio.DatagramProtocol):
|
|
|
251
261
|
await self.data_queue.put(message)
|
|
252
262
|
return
|
|
253
263
|
|
|
264
|
+
if cancelled is not None:
|
|
265
|
+
raise cancelled
|
|
266
|
+
|
|
254
267
|
async def _ensure_sent(
|
|
255
268
|
self,
|
|
256
269
|
message: OmniLogicMessage,
|
|
@@ -299,6 +312,7 @@ class OmniLogicProtocol(asyncio.DatagramProtocol):
|
|
|
299
312
|
msg_type: MessageType,
|
|
300
313
|
payload: str | None,
|
|
301
314
|
msg_id: int | None = None,
|
|
315
|
+
response_timeout: float = DEFAULT_RESPONSE_TIMEOUT,
|
|
302
316
|
) -> str:
|
|
303
317
|
"""Send a message and wait for a response, returning the response payload as a string.
|
|
304
318
|
|
|
@@ -306,12 +320,13 @@ class OmniLogicProtocol(asyncio.DatagramProtocol):
|
|
|
306
320
|
msg_type: Type of message to send.
|
|
307
321
|
payload: Optional payload string.
|
|
308
322
|
msg_id: Optional message ID.
|
|
323
|
+
response_timeout: Timeout in seconds to wait for the response.
|
|
309
324
|
|
|
310
325
|
Returns:
|
|
311
326
|
Response payload as a string.
|
|
312
327
|
"""
|
|
313
328
|
await self.send_message(msg_type, payload, msg_id)
|
|
314
|
-
return await self._receive_file()
|
|
329
|
+
return await self._receive_file(response_timeout=response_timeout)
|
|
315
330
|
|
|
316
331
|
# Send a message that you do NOT need a response to
|
|
317
332
|
async def send_message(
|
|
@@ -346,11 +361,14 @@ class OmniLogicProtocol(asyncio.DatagramProtocol):
|
|
|
346
361
|
req_body = ET.tostring(body_element, xml_declaration=True, encoding=XML_ENCODING)
|
|
347
362
|
await self.send_message(MessageType.XML_ACK, req_body, msg_id)
|
|
348
363
|
|
|
349
|
-
async def _receive_file(self) -> str:
|
|
364
|
+
async def _receive_file(self, response_timeout: float = DEFAULT_RESPONSE_TIMEOUT) -> str:
|
|
350
365
|
"""Wait for and reassemble a full response from the controller.
|
|
351
366
|
|
|
352
367
|
Handles single and multi-block (LeadMessage/BlockMessage) responses.
|
|
353
368
|
|
|
369
|
+
Args:
|
|
370
|
+
response_timeout: Timeout in seconds to wait for the initial response.
|
|
371
|
+
|
|
354
372
|
Returns:
|
|
355
373
|
Response payload as a string.
|
|
356
374
|
|
|
@@ -359,13 +377,21 @@ class OmniLogicProtocol(asyncio.DatagramProtocol):
|
|
|
359
377
|
OmniFragmentationException: If fragment reassembly fails.
|
|
360
378
|
"""
|
|
361
379
|
# wait for the initial packet.
|
|
362
|
-
|
|
380
|
+
try:
|
|
381
|
+
message = await asyncio.wait_for(self.data_queue.get(), response_timeout)
|
|
382
|
+
except TimeoutError as exc:
|
|
383
|
+
msg = f"Timeout waiting for response from controller: {exc}"
|
|
384
|
+
raise OmniTimeoutError(msg) from exc
|
|
363
385
|
|
|
364
386
|
# If messages have to be re-transmitted, we can sometimes receive multiple ACKs. The first one would be handled by
|
|
365
387
|
# self._ensure_sent, but if any subsequent ACKs are sent to us, we need to dump them and wait for a "real" message.
|
|
366
388
|
while message.type in [MessageType.ACK, MessageType.XML_ACK]:
|
|
367
389
|
_LOGGER.debug("Skipping duplicate ACK message")
|
|
368
|
-
|
|
390
|
+
try:
|
|
391
|
+
message = await asyncio.wait_for(self.data_queue.get(), response_timeout)
|
|
392
|
+
except TimeoutError as exc:
|
|
393
|
+
msg = f"Timeout waiting for response from controller: {exc}"
|
|
394
|
+
raise OmniTimeoutError(msg) from exc
|
|
369
395
|
|
|
370
396
|
await self._send_ack(message.id)
|
|
371
397
|
|
|
@@ -403,7 +429,7 @@ class OmniLogicProtocol(asyncio.DatagramProtocol):
|
|
|
403
429
|
|
|
404
430
|
# We need to wait long enough for the Omni to get through all of it's retries before we bail out.
|
|
405
431
|
try:
|
|
406
|
-
resp = await asyncio.wait_for(self.data_queue.get(),
|
|
432
|
+
resp = await asyncio.wait_for(self.data_queue.get(), response_timeout)
|
|
407
433
|
except TimeoutError as exc:
|
|
408
434
|
msg = f"Timeout receiving fragment: got {len(data_fragments)}/{leadmsg.msg_block_count} fragments: {exc}"
|
|
409
435
|
raise OmniFragmentationError(msg) from exc
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, ValidationError
|
|
4
4
|
from xmltodict import parse as xml_parse
|
|
5
5
|
|
|
6
|
+
from pyomnilogic_local.models.exceptions import OmniParsingError
|
|
7
|
+
|
|
6
8
|
# Example Filter Diagnostics XML:
|
|
7
9
|
#
|
|
8
10
|
# <?xml version="1.0" encoding="UTF-8" ?>
|
|
@@ -46,6 +48,7 @@ class FilterDiagnosticsParameters(BaseModel):
|
|
|
46
48
|
|
|
47
49
|
class FilterDiagnostics(BaseModel):
|
|
48
50
|
model_config = ConfigDict(from_attributes=True)
|
|
51
|
+
_raw: str = PrivateAttr(default="")
|
|
49
52
|
|
|
50
53
|
name: str = Field(alias="Name")
|
|
51
54
|
parameters: list[FilterDiagnosticsParameter] = Field(alias="Parameters")
|
|
@@ -64,4 +67,11 @@ class FilterDiagnostics(BaseModel):
|
|
|
64
67
|
# The XML nests the Parameter entries under a Parameters entry, this is annoying to work with. Here we are adjusting the data to
|
|
65
68
|
# remove that extra level in the data
|
|
66
69
|
data["Response"]["Parameters"] = data["Response"]["Parameters"]["Parameter"]
|
|
67
|
-
|
|
70
|
+
try:
|
|
71
|
+
instance = FilterDiagnostics.model_validate(data["Response"])
|
|
72
|
+
instance._raw = xml
|
|
73
|
+
except ValidationError as exc:
|
|
74
|
+
msg = f"Failed to parse Filter Diagnostics: {exc}"
|
|
75
|
+
raise OmniParsingError(msg) from exc
|
|
76
|
+
else:
|
|
77
|
+
return instance
|
|
@@ -9,6 +9,7 @@ from pydantic import (
|
|
|
9
9
|
BaseModel,
|
|
10
10
|
ConfigDict,
|
|
11
11
|
Field,
|
|
12
|
+
PrivateAttr,
|
|
12
13
|
ValidationError,
|
|
13
14
|
computed_field,
|
|
14
15
|
model_validator,
|
|
@@ -410,6 +411,7 @@ type MSPConfigType = MSPSystem | MSPEquipmentType
|
|
|
410
411
|
|
|
411
412
|
class MSPConfig(BaseModel):
|
|
412
413
|
model_config = ConfigDict(from_attributes=True)
|
|
414
|
+
_raw: str = PrivateAttr(default="")
|
|
413
415
|
|
|
414
416
|
system: MSPSystem = Field(alias="System")
|
|
415
417
|
backyard: MSPBackyard = Field(alias="Backyard")
|
|
@@ -459,7 +461,10 @@ class MSPConfig(BaseModel):
|
|
|
459
461
|
),
|
|
460
462
|
)
|
|
461
463
|
try:
|
|
462
|
-
|
|
464
|
+
instance = MSPConfig.model_validate(data["MSPConfig"], from_attributes=True)
|
|
465
|
+
instance._raw = xml
|
|
463
466
|
except ValidationError as exc:
|
|
464
467
|
msg = f"Failed to parse MSP Configuration: {exc}"
|
|
465
468
|
raise OmniParsingError(msg) from exc
|
|
469
|
+
else:
|
|
470
|
+
return instance
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
from typing import Any, SupportsInt, cast, overload
|
|
5
5
|
|
|
6
|
-
from pydantic import BaseModel, ConfigDict, Field, ValidationError, computed_field
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, ValidationError, computed_field
|
|
7
7
|
from xmltodict import parse as xml_parse
|
|
8
8
|
|
|
9
9
|
from pyomnilogic_local.omnitypes import (
|
|
@@ -492,6 +492,7 @@ class Telemetry(BaseModel):
|
|
|
492
492
|
"""
|
|
493
493
|
|
|
494
494
|
model_config = ConfigDict(from_attributes=True)
|
|
495
|
+
_raw: str = PrivateAttr(default="")
|
|
495
496
|
|
|
496
497
|
version: str = Field(alias="@version")
|
|
497
498
|
backyard: TelemetryBackyard = Field(alias="Backyard")
|
|
@@ -525,7 +526,7 @@ class Telemetry(BaseModel):
|
|
|
525
526
|
|
|
526
527
|
try:
|
|
527
528
|
newvalue = int(value)
|
|
528
|
-
except
|
|
529
|
+
except ValueError, TypeError:
|
|
529
530
|
newvalue = value
|
|
530
531
|
|
|
531
532
|
return key, newvalue
|
|
@@ -552,10 +553,13 @@ class Telemetry(BaseModel):
|
|
|
552
553
|
),
|
|
553
554
|
)
|
|
554
555
|
try:
|
|
555
|
-
|
|
556
|
+
instance = Telemetry.model_validate(data["STATUS"])
|
|
557
|
+
instance._raw = xml
|
|
556
558
|
except ValidationError as exc:
|
|
557
559
|
msg = f"Failed to parse Telemetry: {exc}"
|
|
558
560
|
raise OmniParsingError(msg) from exc
|
|
561
|
+
else:
|
|
562
|
+
return instance
|
|
559
563
|
|
|
560
564
|
def get_telem_by_systemid(self, system_id: int) -> TelemetryType | None:
|
|
561
565
|
for field_name, value in self:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-omnilogic-local"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.27.1"
|
|
4
4
|
description = "A library for local control of Hayward OmniHub/OmniLogic pool controllers using their local API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14.2"
|
|
@@ -43,14 +43,9 @@ dev = [
|
|
|
43
43
|
|
|
44
44
|
[tool.mypy]
|
|
45
45
|
python_version = "3.14"
|
|
46
|
-
plugins = [
|
|
47
|
-
|
|
48
|
-
]
|
|
49
|
-
# follow_imports = "silent"
|
|
46
|
+
plugins = ["pydantic.mypy"]
|
|
47
|
+
follow_imports = "normal"
|
|
50
48
|
strict = true
|
|
51
|
-
ignore_missing_imports = true
|
|
52
|
-
disallow_subclassing_any = false
|
|
53
|
-
warn_return_any = false
|
|
54
49
|
|
|
55
50
|
[tool.ruff]
|
|
56
51
|
line-length = 140
|
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/__init__.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/api/exceptions.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/backyard.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/chlorinator.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/__init__.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/bows.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/csads.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/filters.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/groups.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/heaters.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/lights.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/pumps.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/relays.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/sensors.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/get/valves.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/pcap_utils.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/cli/utils.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/collections.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/colorlogiclight.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/csad_equip.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/decorators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/heater_equip.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/models/__init__.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/models/const.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/omnilogic.py
RENAMED
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/omnitypes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-0.26.0 → python_omnilogic_local-0.27.1}/pyomnilogic_local/schedule.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|