Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion homeassistant/components/avea/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,16 @@ def turn_off(self, **kwargs: Any) -> None:

def update(self) -> None:
"""Fetch new state data for this light."""
if (brightness := self._light.get_brightness()) is not None:
connected = self._light.connect()

try:
brightness = self._light.get_brightness()
rgb_color = self._light.get_rgb()
finally:
if connected:
self._light.disconnect()

if brightness is not None:
self._attr_is_on = brightness != 0
self._attr_brightness = round(255 * (brightness / 4095))
self._attr_hs_color = color_util.color_RGB_to_hs(*rgb_color)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avea.Bulb.get_rgb() returns a 3-tuple of integer RGB values by contract. I do not plan to add a guard here unless you want explicit validation despite that contract.

The reason is that applying a fresh brightness value while skipping a failed color refresh would publish mixed-freshness state, which is the inconsistency this bugfix is trying to avoid. If the RGB read/conversion unexpectedly fails, failing the whole refresh is preferable to updating only part of the light state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same concern as the earlier RGB thread. I am keeping the production code unchanged here because avea.Bulb.get_rgb() returns an RGB 3-tuple by contract, including when it falls back to cached values after a failed connection attempt.

The failed pre-connect fallback path is now covered by test, so we verify that brightness and color are both still applied from the returned cached values. Adding a guard that skips only color would allow mixed-freshness state, which this PR is trying to avoid.

48 changes: 47 additions & 1 deletion tests/components/avea/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from collections.abc import AsyncGenerator
from datetime import timedelta
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, call, patch

from freezegun.api import FrozenDateTimeFactory
import pytest
Expand All @@ -28,7 +28,9 @@ def mock_bulb() -> MagicMock:
bulb = MagicMock()
bulb.name = "Bedroom"
bulb.brightness = 0
bulb.connect.return_value = True
bulb.get_brightness.return_value = 0
bulb.get_rgb.return_value = (0, 0, 0)
return bulb


Expand Down Expand Up @@ -117,13 +119,57 @@ async def test_update_state(
assert state.attributes[ATTR_BRIGHTNESS] is None

bulb = setup_integration
bulb.reset_mock()
bulb.connect.return_value = True
bulb.get_brightness.return_value = 2048
bulb.get_rgb.return_value = (0, 255, 0)

freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)

bulb.connect.assert_called_once()
bulb.get_brightness.assert_called_once()
bulb.get_rgb.assert_called_once()
bulb.disconnect.assert_called_once()

bulb.assert_has_calls(
[
call.connect(),
call.get_brightness(),
call.get_rgb(),
call.disconnect(),
]
)

state = hass.states.get("light.bedroom")
assert state is not None
assert state.state == STATE_ON
assert state.attributes[ATTR_BRIGHTNESS] == 128
assert state.attributes[ATTR_HS_COLOR] == (120.0, 100.0)


async def test_update_state_uses_cached_values_when_connect_fails(
hass: HomeAssistant, setup_integration: MagicMock, freezer: FrozenDateTimeFactory
) -> None:
"""Test updating the entity state when the shared connection fails."""
bulb = setup_integration
bulb.reset_mock()
bulb.connect.return_value = False
bulb.get_brightness.return_value = 2048
bulb.get_rgb.return_value = (0, 255, 0)

freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)

bulb.connect.assert_called_once()
bulb.get_brightness.assert_called_once()
bulb.get_rgb.assert_called_once()
bulb.disconnect.assert_not_called()

state = hass.states.get("light.bedroom")
assert state is not None
assert state.state == STATE_ON
assert state.attributes[ATTR_BRIGHTNESS] == 128
assert state.attributes[ATTR_HS_COLOR] == (120.0, 100.0)