From 4075f5600144f6afaad3254f1b80f45b8615d5d4 Mon Sep 17 00:00:00 2001 From: Christopher D Heiser <10488026+cdheiser@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:38:59 +0000 Subject: [PATCH 1/2] Skip volume/source poll for powered-off zones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crestron only responds to VOLUME/SOURCE queries when a zone is powered on. Previously we queried all three for every zone every poll, so each off zone burned COMMAND_TIMEOUT_SECONDS waiting for a response that never comes — three off zones = ~15s per poll. Now we query POWER first and only follow up with VOLUME/SOURCE when the zone is on. Also bumps POLL_INTERVAL_SECONDS 15 → 30 and tightens COMMAND_TIMEOUT_SECONDS 5 → 2 so any unexpected silence stalls less. Co-Authored-By: Claude Opus 4.7 --- custom_components/crestron/const.py | 4 ++-- custom_components/crestron/hub.py | 18 +++++++++++------- tests/test_hub.py | 16 +++++++++------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/custom_components/crestron/const.py b/custom_components/crestron/const.py index 07dbcf1..f6192de 100644 --- a/custom_components/crestron/const.py +++ b/custom_components/crestron/const.py @@ -14,7 +14,7 @@ "iTunes": "ITUNES", } -POLL_INTERVAL_SECONDS = 15 +POLL_INTERVAL_SECONDS = 30 REBOOT_COOLDOWN_SECONDS = 180 -COMMAND_TIMEOUT_SECONDS = 5.0 +COMMAND_TIMEOUT_SECONDS = 2.0 CONNECT_TIMEOUT_SECONDS = 5.0 diff --git a/custom_components/crestron/hub.py b/custom_components/crestron/hub.py index 778fef7..6706c98 100644 --- a/custom_components/crestron/hub.py +++ b/custom_components/crestron/hub.py @@ -235,13 +235,17 @@ async def _async_poll(self) -> dict[str, dict[str, Any]]: power = await self.request(f"{zone} POWER") if power is not None: zone_state["power"] = power - volume_line = await self.request(f"{zone} VOLUME CHECK") - if volume_line: - with contextlib.suppress(ValueError): - zone_state["volume"] = int(volume_line.split()[-1]) - source_line = await self.request(f"{zone} SOURCE") - if source_line: - zone_state["source"] = source_line.split()[-1] + # Crestron only responds to VOLUME/SOURCE when the zone is on, + # so skip them otherwise — each silent query wastes a full + # command timeout. + if power and power.upper().endswith("ON"): + volume_line = await self.request(f"{zone} VOLUME CHECK") + if volume_line: + with contextlib.suppress(ValueError): + zone_state["volume"] = int(volume_line.split()[-1]) + source_line = await self.request(f"{zone} SOURCE") + if source_line: + zone_state["source"] = source_line.split()[-1] except ConnectionError as exc: had_error = True _LOGGER.debug("Poll failed for zone %s: %s", zone, exc) diff --git a/tests/test_hub.py b/tests/test_hub.py index a67810e..d4f740b 100644 --- a/tests/test_hub.py +++ b/tests/test_hub.py @@ -217,6 +217,7 @@ async def test_poll_collects_state_for_all_zones( ) -> None: entry = _make_entry(["KITCHEN", "DECK"]) entry.add_to_hass(hass) + # DECK is off, so only POWER is queried for it — no VOLUME/SOURCE lines. open_stub.enqueue( 2000, [ @@ -224,8 +225,6 @@ async def test_poll_collects_state_for_all_zones( "KITCHEN VOLUME CURRENT 40\r\n", "KITCHEN SOURCE CHROMECAST\r\n", "DECK POWER OFF\r\n", - "DECK VOLUME CURRENT 0\r\n", - "DECK SOURCE ITUNES\r\n", ], ) @@ -240,8 +239,11 @@ async def test_poll_collects_state_for_all_zones( "volume": 40, "source": "CHROMECAST", } - assert data["DECK"] == { - "power": "DECK POWER OFF", - "volume": 0, - "source": "ITUNES", - } + assert data["DECK"] == {"power": "DECK POWER OFF"} + # Only 4 lines consumed (3 KITCHEN + 1 DECK POWER); VOLUME/SOURCE skipped for OFF zone. + assert open_stub.writers[0].writes == [ + "KITCHEN POWER\r\n", + "KITCHEN VOLUME CHECK\r\n", + "KITCHEN SOURCE\r\n", + "DECK POWER\r\n", + ] From 42249ff332c2ad91a4002376c545aa58cfce7c5c Mon Sep 17 00:00:00 2001 From: Christopher D Heiser <10488026+cdheiser@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:44:58 +0000 Subject: [PATCH 2/2] Drop misleading poll comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: the statement that Crestron only responds to VOLUME/SOURCE when on is wrong in general — setting SOURCE as a command turns the zone on. Only the *query* behavior is what this branch exploits, and the code is self-explanatory. Co-Authored-By: Claude Opus 4.7 --- custom_components/crestron/hub.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/custom_components/crestron/hub.py b/custom_components/crestron/hub.py index 6706c98..3a31ee8 100644 --- a/custom_components/crestron/hub.py +++ b/custom_components/crestron/hub.py @@ -235,9 +235,6 @@ async def _async_poll(self) -> dict[str, dict[str, Any]]: power = await self.request(f"{zone} POWER") if power is not None: zone_state["power"] = power - # Crestron only responds to VOLUME/SOURCE when the zone is on, - # so skip them otherwise — each silent query wastes a full - # command timeout. if power and power.upper().endswith("ON"): volume_line = await self.request(f"{zone} VOLUME CHECK") if volume_line: