diff --git a/custom_components/crestron/media_player.py b/custom_components/crestron/media_player.py index 904dfac..0d998d7 100644 --- a/custom_components/crestron/media_player.py +++ b/custom_components/crestron/media_player.py @@ -151,16 +151,27 @@ def source(self) -> str | None: # --- Commands --------------------------------------------------------- + def _apply_optimistic(self, updates: dict[str, Any]) -> None: + data = dict(self.coordinator.data or {}) + zone_state = dict(data.get(self._zone, {})) + zone_state.update(updates) + data[self._zone] = zone_state + self.coordinator.async_set_updated_data(data) + async def async_turn_on(self) -> None: await self._hub.command(f"{self._zone} ON") + self._apply_optimistic({"power": f"{self._zone} POWER ON"}) await self.coordinator.async_request_refresh() async def async_turn_off(self) -> None: await self._hub.command(f"{self._zone} OFF") + self._apply_optimistic({"power": f"{self._zone} POWER OFF"}) await self.coordinator.async_request_refresh() async def async_set_volume_level(self, volume: float) -> None: - await self._hub.command(f"{self._zone} VOLUME SET {round(volume * 100)}") + level = round(volume * 100) + await self._hub.command(f"{self._zone} VOLUME SET {level}") + self._apply_optimistic({"volume": level}) await self.coordinator.async_request_refresh() async def async_volume_up(self) -> None: @@ -182,4 +193,7 @@ async def async_select_source(self, source: str) -> None: _LOGGER.warning("Unknown Crestron source: %s", source) return await self._hub.command(f"{self._zone} {code}") + self._apply_optimistic( + {"power": f"{self._zone} POWER ON", "source": code} + ) await self.coordinator.async_request_refresh() diff --git a/tests/test_media_player.py b/tests/test_media_player.py index fc586aa..542b8e2 100644 --- a/tests/test_media_player.py +++ b/tests/test_media_player.py @@ -21,6 +21,11 @@ def _make_hub(zone_data: dict | None = None) -> MagicMock: hub.coordinator.last_update_success = True hub.coordinator.async_request_refresh = AsyncMock() hub.coordinator.async_add_listener = MagicMock() + + def _set(new_data: dict) -> None: + hub.coordinator.data = new_data + + hub.coordinator.async_set_updated_data = MagicMock(side_effect=_set) return hub @@ -114,3 +119,37 @@ async def test_async_select_unknown_source_is_noop() -> None: zone = CrestronZone(hub, "KITCHEN") await zone.async_select_source("Bogus") hub.command.assert_not_awaited() + + +async def test_turn_on_optimistically_updates_state() -> None: + hub = _make_hub({"power": "KITCHEN POWER OFF"}) + zone = CrestronZone(hub, "KITCHEN") + await zone.async_turn_on() + assert hub.coordinator.data["KITCHEN"]["power"].endswith("ON") + assert zone.state == MediaPlayerState.ON + + +async def test_turn_off_optimistically_updates_state() -> None: + hub = _make_hub({"power": "KITCHEN POWER ON"}) + zone = CrestronZone(hub, "KITCHEN") + await zone.async_turn_off() + assert hub.coordinator.data["KITCHEN"]["power"].endswith("OFF") + assert zone.state == MediaPlayerState.OFF + + +async def test_set_volume_optimistically_updates_level() -> None: + hub = _make_hub({"power": "KITCHEN POWER ON"}) + zone = CrestronZone(hub, "KITCHEN") + await zone.async_set_volume_level(0.25) + assert hub.coordinator.data["KITCHEN"]["volume"] == 25 + assert zone.volume_level == 0.25 + + +async def test_select_source_optimistically_updates_source_and_power() -> None: + hub = _make_hub({"power": "KITCHEN POWER OFF"}) + zone = CrestronZone(hub, "KITCHEN") + await zone.async_select_source("Chromecast") + assert hub.coordinator.data["KITCHEN"]["source"] == "CHROMECAST" + assert hub.coordinator.data["KITCHEN"]["power"].endswith("ON") + assert zone.source == "Chromecast" + assert zone.state == MediaPlayerState.ON