python-omnilogic-local 1.0.0__tar.gz → 1.1.0__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-1.0.0 → python_omnilogic_local-1.1.0}/PKG-INFO +1 -1
- python_omnilogic_local-1.1.0/pyomnilogic_local/api/__init__.py +36 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/api/api.py +41 -32
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/api/constants.py +2 -2
- python_omnilogic_local-1.1.0/pyomnilogic_local/api/message.py +143 -0
- python_omnilogic_local-1.1.0/pyomnilogic_local/api/protocol.py +348 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/chlorinator.py +3 -6
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/pcap_utils.py +1 -1
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/models/leadmessage.py +4 -1
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/omnitypes.py +2 -1
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyproject.toml +1 -1
- python_omnilogic_local-1.0.0/pyomnilogic_local/api/__init__.py +0 -14
- python_omnilogic_local-1.0.0/pyomnilogic_local/api/protocol.py +0 -473
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/LICENSE +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/README.md +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/__init__.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/_base.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/api/exceptions.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/api/mock_api.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/backyard.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/bow.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/chlorinator_equip.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/__init__.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/cli.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/debug/__init__.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/debug/commands.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/__init__.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/backyard.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/bows.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/chlorinators.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/commands.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/csads.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/filters.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/groups.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/heaters.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/lights.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/pumps.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/relays.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/schedules.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/sensors.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/get/valves.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/cli/utils.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/collections.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/colorlogiclight.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/csad.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/csad_equip.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/decorators.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/filter.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/groups.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/heater.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/heater_equip.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/models/__init__.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/models/const.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/models/exceptions.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/models/filter_diagnostics.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/models/mspconfig.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/models/telemetry.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/omnilogic.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/pump.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/py.typed +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/relay.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/schedule.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/sensor.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/system.py +0 -0
- {python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-omnilogic-local
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
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>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""API module for interacting with Hayward OmniLogic pool controllers.
|
|
2
|
+
|
|
3
|
+
This module provides the OmniLogicAPI class, which allows for local
|
|
4
|
+
control and monitoring of Hayward OmniLogic and OmniHub pool controllers
|
|
5
|
+
over a local network connection via the UDP XML API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .api import OmniLogicAPI
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
OmniCommandError,
|
|
13
|
+
OmniConnectionError,
|
|
14
|
+
OmniFragmentationError,
|
|
15
|
+
OmniLogicError,
|
|
16
|
+
OmniMessageFormatError,
|
|
17
|
+
OmniProtocolError,
|
|
18
|
+
OmniTimeoutError,
|
|
19
|
+
OmniValidationError,
|
|
20
|
+
)
|
|
21
|
+
from .message import OmniLogicMessage
|
|
22
|
+
from .protocol import OmniLogicProtocol
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"OmniCommandError",
|
|
26
|
+
"OmniConnectionError",
|
|
27
|
+
"OmniFragmentationError",
|
|
28
|
+
"OmniLogicAPI",
|
|
29
|
+
"OmniLogicError",
|
|
30
|
+
"OmniLogicMessage",
|
|
31
|
+
"OmniLogicProtocol",
|
|
32
|
+
"OmniMessageFormatError",
|
|
33
|
+
"OmniProtocolError",
|
|
34
|
+
"OmniTimeoutError",
|
|
35
|
+
"OmniValidationError",
|
|
36
|
+
]
|
|
@@ -111,30 +111,39 @@ class OmniLogicAPI:
|
|
|
111
111
|
self.controller_port = controller_port
|
|
112
112
|
self.response_timeout = response_timeout
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
async def async_send_message(self, message_type: MessageType, message: str | None, need_response: Literal[True]) -> str: ...
|
|
116
|
-
@overload
|
|
117
|
-
async def async_send_message(self, message_type: MessageType, message: str | None, need_response: Literal[False]) -> None: ...
|
|
118
|
-
async def async_send_message(self, message_type: MessageType, message: str | None, need_response: bool = False) -> str | None:
|
|
114
|
+
async def async_send(self, message_type: MessageType, message: str) -> None:
|
|
119
115
|
"""Send a message via the Hayward Omni UDP protocol along with properly handling timeouts and responses.
|
|
120
116
|
|
|
121
117
|
Args:
|
|
122
118
|
message_type (MessageType): A selection from MessageType indicating what type of communication you are sending
|
|
123
|
-
message (str
|
|
124
|
-
|
|
119
|
+
message (str): The XML body of the message to deliver
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
None
|
|
123
|
+
"""
|
|
124
|
+
loop = asyncio.get_running_loop()
|
|
125
|
+
transport, protocol = await loop.create_datagram_endpoint(OmniLogicProtocol, remote_addr=(self.controller_ip, self.controller_port))
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
await protocol.async_send(message_type, message)
|
|
129
|
+
finally:
|
|
130
|
+
transport.close()
|
|
131
|
+
|
|
132
|
+
async def async_send_and_receive(self, message_type: MessageType, message: str) -> str:
|
|
133
|
+
"""Convenience method to send a message and receive a response without needing to specify need_response every time.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
message_type (MessageType): A selection from MessageType indicating what type of communication you are sending
|
|
137
|
+
message (str): The message payload to send.
|
|
125
138
|
|
|
126
139
|
Returns:
|
|
127
|
-
str
|
|
140
|
+
str: The response body sent from the Omni
|
|
128
141
|
"""
|
|
129
142
|
loop = asyncio.get_running_loop()
|
|
130
143
|
transport, protocol = await loop.create_datagram_endpoint(OmniLogicProtocol, remote_addr=(self.controller_ip, self.controller_port))
|
|
131
144
|
|
|
132
|
-
resp: str | None = None
|
|
133
145
|
try:
|
|
134
|
-
|
|
135
|
-
resp = await protocol.send_and_receive(message_type, message, response_timeout=self.response_timeout)
|
|
136
|
-
else:
|
|
137
|
-
await protocol.send_message(message_type, message)
|
|
146
|
+
resp = await protocol.async_send_and_receive(message_type, message)
|
|
138
147
|
finally:
|
|
139
148
|
transport.close()
|
|
140
149
|
|
|
@@ -164,7 +173,7 @@ class OmniLogicAPI:
|
|
|
164
173
|
|
|
165
174
|
_LOGGER.debug("Sending RequestConfiguration with body: %s", req_body)
|
|
166
175
|
|
|
167
|
-
resp = await self.
|
|
176
|
+
resp = await self.async_send_and_receive(MessageType.REQUEST_CONFIGURATION, req_body)
|
|
168
177
|
|
|
169
178
|
_LOGGER.debug("Received response for RequestConfiguration: %s", resp)
|
|
170
179
|
|
|
@@ -206,7 +215,7 @@ class OmniLogicAPI:
|
|
|
206
215
|
|
|
207
216
|
_LOGGER.debug("Sending GetUIFilterDiagnosticInfo with body: %s", req_body)
|
|
208
217
|
|
|
209
|
-
resp = await self.
|
|
218
|
+
resp = await self.async_send_and_receive(MessageType.GET_FILTER_DIAGNOSTIC_INFO, req_body)
|
|
210
219
|
|
|
211
220
|
_LOGGER.debug("Received response for GetUIFilterDiagnosticInfo: %s", resp)
|
|
212
221
|
|
|
@@ -235,7 +244,7 @@ class OmniLogicAPI:
|
|
|
235
244
|
|
|
236
245
|
_LOGGER.debug("Sending RequestTelemetryData with body: %s", req_body)
|
|
237
246
|
|
|
238
|
-
resp = await self.
|
|
247
|
+
resp = await self.async_send_and_receive(MessageType.GET_TELEMETRY, req_body)
|
|
239
248
|
|
|
240
249
|
_LOGGER.debug("Received response for RequestTelemetryData: %s", resp)
|
|
241
250
|
|
|
@@ -276,7 +285,7 @@ class OmniLogicAPI:
|
|
|
276
285
|
|
|
277
286
|
_LOGGER.debug("Sending SetUIHeaterCmd with body: %s", req_body)
|
|
278
287
|
|
|
279
|
-
return await self.
|
|
288
|
+
return await self.async_send(MessageType.SET_HEATER_COMMAND, req_body)
|
|
280
289
|
|
|
281
290
|
async def async_set_solar_heater(
|
|
282
291
|
self,
|
|
@@ -311,7 +320,7 @@ class OmniLogicAPI:
|
|
|
311
320
|
|
|
312
321
|
_LOGGER.debug("Sending SetUISolarSetPointCmd with body: %s", req_body)
|
|
313
322
|
|
|
314
|
-
return await self.
|
|
323
|
+
return await self.async_send(MessageType.SET_SOLAR_SET_POINT_COMMAND, req_body)
|
|
315
324
|
|
|
316
325
|
async def async_set_heater_mode(
|
|
317
326
|
self,
|
|
@@ -346,7 +355,7 @@ class OmniLogicAPI:
|
|
|
346
355
|
|
|
347
356
|
_LOGGER.debug("Sending SetUIHeaterModeCmd with body: %s", req_body)
|
|
348
357
|
|
|
349
|
-
return await self.
|
|
358
|
+
return await self.async_send(MessageType.SET_HEATER_MODE_COMMAND, req_body)
|
|
350
359
|
|
|
351
360
|
async def async_set_heater_enable(
|
|
352
361
|
self,
|
|
@@ -381,7 +390,7 @@ class OmniLogicAPI:
|
|
|
381
390
|
|
|
382
391
|
_LOGGER.debug("Sending SetHeaterEnable with body: %s", req_body)
|
|
383
392
|
|
|
384
|
-
return await self.
|
|
393
|
+
return await self.async_send(MessageType.SET_HEATER_ENABLED, req_body)
|
|
385
394
|
|
|
386
395
|
async def async_set_equipment(
|
|
387
396
|
self,
|
|
@@ -443,7 +452,7 @@ class OmniLogicAPI:
|
|
|
443
452
|
|
|
444
453
|
_LOGGER.debug("Sending SetUIEquipmentCmd with body: %s", req_body)
|
|
445
454
|
|
|
446
|
-
return await self.
|
|
455
|
+
return await self.async_send(MessageType.SET_EQUIPMENT, req_body)
|
|
447
456
|
|
|
448
457
|
async def async_set_filter_speed(self, pool_id: int, equipment_id: int, speed: int) -> None:
|
|
449
458
|
"""Set the speed for a variable speed filter/pump.
|
|
@@ -471,7 +480,7 @@ class OmniLogicAPI:
|
|
|
471
480
|
|
|
472
481
|
_LOGGER.debug("Sending SetUIFilterSpeedCmd with body: %s", req_body)
|
|
473
482
|
|
|
474
|
-
return await self.
|
|
483
|
+
return await self.async_send(MessageType.SET_FILTER_SPEED, req_body)
|
|
475
484
|
|
|
476
485
|
async def async_set_light_show(
|
|
477
486
|
self,
|
|
@@ -543,7 +552,7 @@ class OmniLogicAPI:
|
|
|
543
552
|
|
|
544
553
|
_LOGGER.debug("Sending SetStandAloneLightShow with body: %s", req_body)
|
|
545
554
|
|
|
546
|
-
return await self.
|
|
555
|
+
return await self.async_send(MessageType.SET_STANDALONE_LIGHT_SHOW, req_body)
|
|
547
556
|
|
|
548
557
|
async def async_set_chlorinator_enable(self, pool_id: int, enabled: int | bool) -> None:
|
|
549
558
|
body_element = ET.Element("Request", {"xmlns": XML_NAMESPACE})
|
|
@@ -561,7 +570,7 @@ class OmniLogicAPI:
|
|
|
561
570
|
|
|
562
571
|
_LOGGER.debug("Sending SetCHLOREnable with body: %s", req_body)
|
|
563
572
|
|
|
564
|
-
return await self.
|
|
573
|
+
return await self.async_send(MessageType.SET_CHLOR_ENABLED, req_body)
|
|
565
574
|
|
|
566
575
|
# This is used to set the ORP target value on a CSAD
|
|
567
576
|
async def async_set_csad_orp_target_level(
|
|
@@ -587,7 +596,7 @@ class OmniLogicAPI:
|
|
|
587
596
|
|
|
588
597
|
_LOGGER.debug("Sending SetUICSADORPTargetLevel with body: %s", req_body)
|
|
589
598
|
|
|
590
|
-
return await self.
|
|
599
|
+
return await self.async_send(MessageType.SET_CSAD_ORP_TARGET, req_body)
|
|
591
600
|
|
|
592
601
|
# This is used to set the pH target value on a CSAD
|
|
593
602
|
async def async_set_csad_target_value(
|
|
@@ -613,7 +622,7 @@ class OmniLogicAPI:
|
|
|
613
622
|
|
|
614
623
|
_LOGGER.debug("Sending UISetCSADTargetValue with body: %s", req_body)
|
|
615
624
|
|
|
616
|
-
return await self.
|
|
625
|
+
return await self.async_send(MessageType.SET_CSAD_TARGET_VALUE, req_body)
|
|
617
626
|
|
|
618
627
|
async def async_set_chlorinator_params(
|
|
619
628
|
self,
|
|
@@ -656,7 +665,7 @@ class OmniLogicAPI:
|
|
|
656
665
|
|
|
657
666
|
_LOGGER.debug("Sending SetCHLORParams with body: %s", req_body)
|
|
658
667
|
|
|
659
|
-
return await self.
|
|
668
|
+
return await self.async_send(MessageType.SET_CHLOR_PARAMS, req_body)
|
|
660
669
|
|
|
661
670
|
async def async_set_chlorinator_superchlorinate(
|
|
662
671
|
self,
|
|
@@ -681,7 +690,7 @@ class OmniLogicAPI:
|
|
|
681
690
|
|
|
682
691
|
_LOGGER.debug("Sending SetUISuperCHLORCmd with body: %s", req_body)
|
|
683
692
|
|
|
684
|
-
return await self.
|
|
693
|
+
return await self.async_send(MessageType.SET_SUPERCHLORINATE, req_body)
|
|
685
694
|
|
|
686
695
|
async def async_restore_idle_state(self) -> None:
|
|
687
696
|
body_element = ET.Element("Request", {"xmlns": XML_NAMESPACE})
|
|
@@ -695,7 +704,7 @@ class OmniLogicAPI:
|
|
|
695
704
|
|
|
696
705
|
_LOGGER.debug("Sending RestoreIdleState with body: %s", req_body)
|
|
697
706
|
|
|
698
|
-
return await self.
|
|
707
|
+
return await self.async_send(MessageType.RESTORE_IDLE_STATE, req_body)
|
|
699
708
|
|
|
700
709
|
async def async_set_spillover(
|
|
701
710
|
self,
|
|
@@ -738,7 +747,7 @@ class OmniLogicAPI:
|
|
|
738
747
|
|
|
739
748
|
_LOGGER.debug("Sending SetUISpilloverCmd with body: %s", req_body)
|
|
740
749
|
|
|
741
|
-
return await self.
|
|
750
|
+
return await self.async_send(MessageType.SET_SPILLOVER, req_body)
|
|
742
751
|
|
|
743
752
|
async def async_set_group_enable(
|
|
744
753
|
self,
|
|
@@ -781,7 +790,7 @@ class OmniLogicAPI:
|
|
|
781
790
|
|
|
782
791
|
_LOGGER.debug("Sending RunGroupCmd with body: %s", req_body)
|
|
783
792
|
|
|
784
|
-
return await self.
|
|
793
|
+
return await self.async_send(MessageType.RUN_GROUP_CMD, req_body)
|
|
785
794
|
|
|
786
795
|
async def async_edit_schedule(
|
|
787
796
|
self,
|
|
@@ -854,4 +863,4 @@ class OmniLogicAPI:
|
|
|
854
863
|
|
|
855
864
|
_LOGGER.debug("Sending EditUIScheduleCmd with body: %s", req_body)
|
|
856
865
|
|
|
857
|
-
return await self.
|
|
866
|
+
return await self.async_send(MessageType.EDIT_SCHEDULE, req_body)
|
{python_omnilogic_local-1.0.0 → python_omnilogic_local-1.1.0}/pyomnilogic_local/api/constants.py
RENAMED
|
@@ -11,9 +11,9 @@ PROTOCOL_VERSION = "1.19" # Current protocol version
|
|
|
11
11
|
BLOCK_MESSAGE_HEADER_OFFSET = 8 # Offset to skip block message header and get to payload
|
|
12
12
|
|
|
13
13
|
# Timing Constants (in seconds)
|
|
14
|
-
OMNI_RETRANSMIT_TIME = 2
|
|
14
|
+
OMNI_RETRANSMIT_TIME = 2 # 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 =
|
|
16
|
+
ACK_WAIT_TIMEOUT = OMNI_RETRANSMIT_TIME * 2 # Timeout waiting for ACK response, 0.5 showed to be just a tad too short in some cases.
|
|
17
17
|
DEFAULT_RESPONSE_TIMEOUT = OMNI_RETRANSMIT_TIME * OMNI_RETRANSMIT_COUNT # Default timeout for receiving responses
|
|
18
18
|
|
|
19
19
|
# Network Constants
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import struct
|
|
5
|
+
import time
|
|
6
|
+
from typing import Self
|
|
7
|
+
|
|
8
|
+
from pyomnilogic_local.omnitypes import ClientType, MessageType
|
|
9
|
+
|
|
10
|
+
from .constants import (
|
|
11
|
+
PROTOCOL_HEADER_FORMAT,
|
|
12
|
+
PROTOCOL_HEADER_SIZE,
|
|
13
|
+
PROTOCOL_VERSION,
|
|
14
|
+
)
|
|
15
|
+
from .exceptions import OmniMessageFormatError
|
|
16
|
+
|
|
17
|
+
_LOGGER = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OmniLogicMessage:
|
|
21
|
+
"""A protocol message for communication with the OmniLogic controller.
|
|
22
|
+
|
|
23
|
+
Handles serialization and deserialization of message headers and payloads.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
header_format = PROTOCOL_HEADER_FORMAT
|
|
27
|
+
id: int
|
|
28
|
+
type: MessageType
|
|
29
|
+
payload: bytes
|
|
30
|
+
client_type: ClientType = ClientType.SIMPLE
|
|
31
|
+
version: str = PROTOCOL_VERSION
|
|
32
|
+
timestamp: int
|
|
33
|
+
reserved_1: int = 0
|
|
34
|
+
compressed: bool = False
|
|
35
|
+
reserved_2: int = 0
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
msg_id: int,
|
|
40
|
+
msg_type: MessageType,
|
|
41
|
+
payload: str | None = None,
|
|
42
|
+
version: str = PROTOCOL_VERSION,
|
|
43
|
+
timestamp: int | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Initialize a new OmniLogicMessage.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
msg_id: Unique message identifier.
|
|
49
|
+
msg_type: Type of message being sent.
|
|
50
|
+
payload: Optional string payload (XML or command body).
|
|
51
|
+
version: Protocol version string.
|
|
52
|
+
timestamp: Optional timestamp for the message.
|
|
53
|
+
"""
|
|
54
|
+
self.id = msg_id
|
|
55
|
+
self.type = msg_type
|
|
56
|
+
# If we are speaking the XML API, it seems like we need client_type 0, otherwise we need client_type 1
|
|
57
|
+
self.client_type = ClientType.XML if payload is not None else ClientType.SIMPLE
|
|
58
|
+
# The Hayward API terminates it's messages with a null character
|
|
59
|
+
payload = f"{payload}\x00" if payload is not None else ""
|
|
60
|
+
self.payload = bytes(payload, "utf-8")
|
|
61
|
+
|
|
62
|
+
self.version = version
|
|
63
|
+
self.timestamp = timestamp if timestamp is not None else int(time.time())
|
|
64
|
+
|
|
65
|
+
def __bytes__(self) -> bytes:
|
|
66
|
+
"""Serialize the message to bytes for UDP transmission.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Byte representation of the message.
|
|
70
|
+
"""
|
|
71
|
+
header = struct.pack(
|
|
72
|
+
self.header_format,
|
|
73
|
+
self.id, # Msg id
|
|
74
|
+
self.timestamp,
|
|
75
|
+
bytes(self.version, "ascii"), # version string
|
|
76
|
+
self.type.value, # OpID/msgType
|
|
77
|
+
self.client_type.value, # Client type
|
|
78
|
+
0, # reserved
|
|
79
|
+
self.compressed, # compressed
|
|
80
|
+
0, # reserved
|
|
81
|
+
)
|
|
82
|
+
return header + self.payload
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
"""Return a string representation of the message for debugging."""
|
|
86
|
+
if self.compressed or self.type is MessageType.MSP_BLOCKMESSAGE:
|
|
87
|
+
return f"ID: {self.id}, Type: {self.type.name}, Compressed: {self.compressed}, Client: {self.client_type.name}"
|
|
88
|
+
return (
|
|
89
|
+
f"ID: {self.id}, Type: {self.type.name}, Compressed: {self.compressed}, Client: {self.client_type.name}, "
|
|
90
|
+
f"Body: {self.payload[:-1].decode('utf-8')}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_bytes(cls, data: bytes) -> Self:
|
|
95
|
+
"""Parse a message from its byte representation.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
data: Byte data received from the controller.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
OmniLogicMessage instance.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
OmniMessageFormatException: If the message format is invalid.
|
|
105
|
+
"""
|
|
106
|
+
if len(data) < PROTOCOL_HEADER_SIZE:
|
|
107
|
+
msg = f"Message too short: {len(data)} bytes, expected at least {PROTOCOL_HEADER_SIZE}"
|
|
108
|
+
raise OmniMessageFormatError(msg)
|
|
109
|
+
|
|
110
|
+
# split the header and data
|
|
111
|
+
header = data[:PROTOCOL_HEADER_SIZE]
|
|
112
|
+
rdata: bytes = data[PROTOCOL_HEADER_SIZE:]
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
(msg_id, tstamp, vers, msg_type, client_type, res1, compressed, res2) = struct.unpack(cls.header_format, header)
|
|
116
|
+
except struct.error as exc:
|
|
117
|
+
msg = f"Failed to unpack message header: {exc}"
|
|
118
|
+
raise OmniMessageFormatError(msg) from exc
|
|
119
|
+
|
|
120
|
+
# Validate message type
|
|
121
|
+
try:
|
|
122
|
+
message_type_enum = MessageType(msg_type)
|
|
123
|
+
except ValueError as exc:
|
|
124
|
+
msg = f"Unknown message type: {msg_type}: {exc}"
|
|
125
|
+
raise OmniMessageFormatError(msg) from exc
|
|
126
|
+
|
|
127
|
+
# Validate client type
|
|
128
|
+
try:
|
|
129
|
+
client_type_enum = ClientType(int(client_type))
|
|
130
|
+
except ValueError as exc:
|
|
131
|
+
msg = f"Unknown client type: {client_type}: {exc}"
|
|
132
|
+
raise OmniMessageFormatError(msg) from exc
|
|
133
|
+
|
|
134
|
+
message = cls(msg_id=msg_id, msg_type=message_type_enum, version=vers.decode("utf-8"))
|
|
135
|
+
message.timestamp = tstamp
|
|
136
|
+
message.client_type = client_type_enum
|
|
137
|
+
message.reserved_1 = res1
|
|
138
|
+
# There are some messages that are ALWAYS compressed although they do not return a 1 in their LeadMessage
|
|
139
|
+
message.compressed = compressed == 1 or message.type in [MessageType.MSP_TELEMETRY_UPDATE]
|
|
140
|
+
message.reserved_2 = res2
|
|
141
|
+
message.payload = rdata
|
|
142
|
+
|
|
143
|
+
return message
|