From c884337f85041eb2cbcd13c9fc8a90b68256354b Mon Sep 17 00:00:00 2001 From: MoonDevLT <107535193+MoonDevLT@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:36:17 +0200 Subject: [PATCH] Enhance connection error exception handling for light entities of lunatone (#184294) --- homeassistant/components/lunatone/light.py | 94 +++++++++++++------ .../components/lunatone/quality_scale.yaml | 4 +- .../components/lunatone/strings.json | 6 ++ tests/components/lunatone/test_light.py | 90 ++++++++++++++++++ 4 files changed, 161 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/lunatone/light.py b/homeassistant/components/lunatone/light.py index a2c267a7b7b5..11058aaa6d31 100644 --- a/homeassistant/components/lunatone/light.py +++ b/homeassistant/components/lunatone/light.py @@ -2,6 +2,7 @@ from typing import Any, override +import aiohttp from lunatone_rest_api_client import DALIBroadcast from lunatone_rest_api_client.models import LineStatus @@ -15,6 +16,7 @@ from homeassistant.components.light import ( brightness_supported, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -191,38 +193,54 @@ class LunatoneLight( @override async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" - if brightness_supported(self.supported_color_modes): - if ATTR_COLOR_TEMP_KELVIN in kwargs: - await self._device.fade_to_color_temperature( - kwargs[ATTR_COLOR_TEMP_KELVIN] - ) - if ATTR_RGB_COLOR in kwargs: - await self._device.fade_to_rgbw_color( - tuple(color / 255 for color in kwargs[ATTR_RGB_COLOR]) - ) - if ATTR_RGBW_COLOR in kwargs: - rgbw_color = tuple(color / 255 for color in kwargs[ATTR_RGBW_COLOR]) - await self._device.fade_to_rgbw_color(rgbw_color[:-1], rgbw_color[-1]) - if ATTR_BRIGHTNESS in kwargs or not self.is_on: - await self._device.fade_to_brightness( - brightness_to_value( - self.BRIGHTNESS_SCALE, - kwargs.get(ATTR_BRIGHTNESS, self._last_brightness), + try: + if brightness_supported(self.supported_color_modes): + if ATTR_COLOR_TEMP_KELVIN in kwargs: + await self._device.fade_to_color_temperature( + kwargs[ATTR_COLOR_TEMP_KELVIN] ) - ) - else: - await self._device.switch_on() + if ATTR_RGB_COLOR in kwargs: + await self._device.fade_to_rgbw_color( + tuple(color / 255 for color in kwargs[ATTR_RGB_COLOR]) + ) + if ATTR_RGBW_COLOR in kwargs: + rgbw_color = tuple(color / 255 for color in kwargs[ATTR_RGBW_COLOR]) + await self._device.fade_to_rgbw_color( + rgbw_color[:-1], rgbw_color[-1] + ) + if ATTR_BRIGHTNESS in kwargs or not self.is_on: + await self._device.fade_to_brightness( + brightness_to_value( + self.BRIGHTNESS_SCALE, + kwargs.get(ATTR_BRIGHTNESS, self._last_brightness), + ) + ) + else: + await self._device.switch_on() + except aiohttp.ClientConnectionError as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_turn_on_light", + translation_placeholders={"entity": self.entity_id}, + ) from ex await self.coordinator.async_refresh() @override async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" - if brightness_supported(self.supported_color_modes): - if self.brightness: - self._last_brightness = self.brightness - await self._device.fade_to_brightness(0) - else: - await self._device.switch_off() + try: + if brightness_supported(self.supported_color_modes): + if self.brightness: + self._last_brightness = self.brightness + await self._device.fade_to_brightness(0) + else: + await self._device.switch_off() + except aiohttp.ClientConnectionError as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_turn_off_light", + translation_placeholders={"entity": self.entity_id}, + ) from ex await self.coordinator.async_refresh() @@ -294,13 +312,29 @@ class LunatoneLineBroadcastLight( @override async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the line to turn on.""" - await self._broadcast.fade_to_brightness( - brightness_to_value(self.BRIGHTNESS_SCALE, kwargs.get(ATTR_BRIGHTNESS, 255)) - ) + try: + await self._broadcast.fade_to_brightness( + brightness_to_value( + self.BRIGHTNESS_SCALE, kwargs.get(ATTR_BRIGHTNESS, 255) + ) + ) + except aiohttp.ClientConnectionError as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_turn_on_light", + translation_placeholders={"entity": self.entity_id}, + ) from ex await self.coordinator.async_refresh() @override async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the line to turn off.""" - await self._broadcast.fade_to_brightness(0) + try: + await self._broadcast.fade_to_brightness(0) + except aiohttp.ClientConnectionError as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_turn_off_light", + translation_placeholders={"entity": self.entity_id}, + ) from ex await self.coordinator.async_refresh() diff --git a/homeassistant/components/lunatone/quality_scale.yaml b/homeassistant/components/lunatone/quality_scale.yaml index 9162e9832f6f..2057676c72e9 100644 --- a/homeassistant/components/lunatone/quality_scale.yaml +++ b/homeassistant/components/lunatone/quality_scale.yaml @@ -38,9 +38,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: - status: exempt - comment: no actions + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: status: exempt diff --git a/homeassistant/components/lunatone/strings.json b/homeassistant/components/lunatone/strings.json index b8bd08bf68ec..877a0d9bcdf7 100644 --- a/homeassistant/components/lunatone/strings.json +++ b/homeassistant/components/lunatone/strings.json @@ -54,6 +54,12 @@ } }, "exceptions": { + "cannot_turn_off_light": { + "message": "Unable to connect to the device and turn off {entity}" + }, + "cannot_turn_on_light": { + "message": "Unable to connect to the device and turn on {entity}" + }, "missing_device_info": { "message": "Unable to read device information. Please verify the device's network connection." } diff --git a/tests/components/lunatone/test_light.py b/tests/components/lunatone/test_light.py index d56306b4375e..3b910f2ab8aa 100644 --- a/tests/components/lunatone/test_light.py +++ b/tests/components/lunatone/test_light.py @@ -3,6 +3,7 @@ import copy from unittest.mock import AsyncMock +import aiohttp from freezegun.api import FrozenDateTimeFactory from lunatone_rest_api_client.models import LineStatus import pytest @@ -24,6 +25,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from . import setup_integration @@ -96,6 +98,48 @@ async def test_turn_on_off( assert state.state == STATE_OFF +async def test_turn_on_off_with_connection_error( + hass: HomeAssistant, + mock_lunatone_info: AsyncMock, + mock_lunatone_devices: AsyncMock, + mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a connection error is raised while turning on and off the light.""" + device_id = 1 + entity_id = f"light.device_{device_id}" + + await setup_integration(hass, mock_config_entry) + + device = mock_config_entry.runtime_data.coordinator_devices.data[0][device_id] + device.switch_on.side_effect = aiohttp.ClientConnectionError() + + with pytest.raises( + HomeAssistantError, + match=f"Unable to connect to the device and turn on {entity_id}", + ): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + + device.switch_off.side_effect = aiohttp.ClientConnectionError() + + with pytest.raises( + HomeAssistantError, + match=f"Unable to connect to the device and turn off {entity_id}", + ): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + async def test_turn_on_off_with_brightness( hass: HomeAssistant, mock_lunatone_info: AsyncMock, @@ -221,6 +265,52 @@ async def test_turn_on_off_broadcast( assert state.state == "off" +async def test_turn_on_off_broadcast_with_connection_error( + hass: HomeAssistant, + mock_lunatone_info: AsyncMock, + mock_lunatone_devices: AsyncMock, + mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, + mock_lunatone_dali_broadcast: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a connection error is raised while turning on and off the broadcast light.""" + line_id = mock_lunatone_dali_broadcast.line + entity_id = f"light.dali_line_{line_id}" + + await setup_integration(hass, mock_config_entry) + + mock_lunatone_dali_broadcast.fade_to_brightness.side_effect = ( + aiohttp.ClientConnectionError() + ) + + with pytest.raises( + HomeAssistantError, + match=f"Unable to connect to the device and turn on {entity_id}", + ): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + + mock_lunatone_dali_broadcast.fade_to_brightness.side_effect = ( + aiohttp.ClientConnectionError() + ) + + with pytest.raises( + HomeAssistantError, + match=f"Unable to connect to the device and turn off {entity_id}", + ): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + async def test_line_broadcast_available_status( hass: HomeAssistant, mock_lunatone_info: AsyncMock,