From 77986dbdb0fb42e8ba0be5cf4cc7898b60fbe9bf Mon Sep 17 00:00:00 2001 From: Rob Raper Date: Sat, 4 Jul 2026 13:02:00 -0700 Subject: [PATCH 1/2] fix zlib decompression fallback --- custom_components/omnilogic_local/__init__.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/custom_components/omnilogic_local/__init__.py b/custom_components/omnilogic_local/__init__.py index 6351bcb..587ca79 100644 --- a/custom_components/omnilogic_local/__init__.py +++ b/custom_components/omnilogic_local/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import zlib from typing import TYPE_CHECKING from homeassistant.const import ( @@ -17,6 +18,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers import entity_registry as er from pyomnilogic_local import OmniLogic +from pyomnilogic_local.api.protocol import OmniLogicProtocol from pyomnilogic_local.omnitypes import OmniType from .const import BACKYARD_SYSTEM_ID, DOMAIN, KEY_COORDINATOR, SUGGESTED_AREA @@ -42,6 +44,38 @@ _LOGGER = logging.getLogger(__name__) +_ORIGINAL_DECODE_PAYLOAD = OmniLogicProtocol._decode_payload + + +def _decode_payload_with_streaming_fallback( + protocol: OmniLogicProtocol, + data: bytes, + compressed: bool, +) -> str: + """Decode controller payloads that fail one-shot zlib decompression. + + Some multi-block OmniLogic responses raise ``Error -5`` with + ``zlib.decompress`` despite being recoverable by a streaming decompressor. + Keep the library's normal path first and use the tested fallback only for + that failure mode. This can be removed once python-omnilogic-local ships + the upstream fix. + """ + try: + return _ORIGINAL_DECODE_PAYLOAD(protocol, data, compressed) + except zlib.error as error: + if not compressed or "incomplete or truncated stream" not in str(error): + raise + + decompressor = zlib.decompressobj() + payload = data.rstrip(b"\x00") + decoded = decompressor.decompress(payload) + decompressor.flush() + _LOGGER.warning("Recovered an OmniLogic compressed response with the streaming zlib workaround") + return decoded.decode("utf-8").strip("\x00") + + +OmniLogicProtocol._decode_payload = _decode_payload_with_streaming_fallback + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OmniLogic Local from a config entry.""" # Create an API instance From 1901e87d2e822e22151a13a632d637101ccd2668 Mon Sep 17 00:00:00 2001 From: Rob Raper Date: Sat, 4 Jul 2026 13:18:59 -0700 Subject: [PATCH 2/2] use exact OmniLogic response boundaries --- custom_components/omnilogic_local/__init__.py | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/custom_components/omnilogic_local/__init__.py b/custom_components/omnilogic_local/__init__.py index 587ca79..8610023 100644 --- a/custom_components/omnilogic_local/__init__.py +++ b/custom_components/omnilogic_local/__init__.py @@ -18,6 +18,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers import entity_registry as er from pyomnilogic_local import OmniLogic +from pyomnilogic_local.api.message import OmniLogicMessage from pyomnilogic_local.api.protocol import OmniLogicProtocol from pyomnilogic_local.omnitypes import OmniType @@ -44,36 +45,38 @@ _LOGGER = logging.getLogger(__name__) -_ORIGINAL_DECODE_PAYLOAD = OmniLogicProtocol._decode_payload +_ORIGINAL_REASSEMBLE_MULTIPART = OmniLogicProtocol._reassemble_multipart -def _decode_payload_with_streaming_fallback( +async def _reassemble_multipart_with_exact_boundary( + protocol: OmniLogicProtocol, + lead_message: OmniLogicMessage, +) -> tuple[bytes, bool]: + """Use the controller's declared response size to remove only padding.""" + lead = protocol._parse_lead_message(lead_message) + payload, compressed = await _ORIGINAL_REASSEMBLE_MULTIPART(protocol, lead_message) + return payload[: lead.msg_size], compressed + + +def _decode_payload_without_trailing_null_strip( protocol: OmniLogicProtocol, data: bytes, compressed: bool, ) -> str: - """Decode controller payloads that fail one-shot zlib decompression. + """Decode compressed payloads without discarding valid zlib trailer bytes. - Some multi-block OmniLogic responses raise ``Error -5`` with - ``zlib.decompress`` despite being recoverable by a streaming decompressor. - Keep the library's normal path first and use the tested fallback only for - that failure mode. This can be removed once python-omnilogic-local ships - the upstream fix. + zlib ignores bytes after a complete stream, including packet padding. This + preserves a valid compressed trailer that happens to end in a null byte and + still raises an error for genuinely incomplete data. This is a temporary + compatibility patch until python-omnilogic-local includes the upstream fix. """ - try: - return _ORIGINAL_DECODE_PAYLOAD(protocol, data, compressed) - except zlib.error as error: - if not compressed or "incomplete or truncated stream" not in str(error): - raise - - decompressor = zlib.decompressobj() - payload = data.rstrip(b"\x00") - decoded = decompressor.decompress(payload) + decompressor.flush() - _LOGGER.warning("Recovered an OmniLogic compressed response with the streaming zlib workaround") - return decoded.decode("utf-8").strip("\x00") + if compressed: + data = zlib.decompress(data) + return data.decode("utf-8").strip("\x00") -OmniLogicProtocol._decode_payload = _decode_payload_with_streaming_fallback +OmniLogicProtocol._reassemble_multipart = _reassemble_multipart_with_exact_boundary +OmniLogicProtocol._decode_payload = _decode_payload_without_trailing_null_strip async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: