Standardize yeelight exception handling (#56362)

This commit is contained in:
J. Nick Koston 2021-09-17 19:25:19 -10:00 committed by GitHub
parent 4160a5ee3b
commit bad6b2f7f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 239 additions and 183 deletions

View File

@ -6,6 +6,7 @@ import contextlib
from datetime import timedelta
from ipaddress import IPv4Address, IPv6Address
import logging
import socket
from urllib.parse import urlparse
from async_upnp_client.search import SsdpSearchListener
@ -163,7 +164,9 @@ UPDATE_REQUEST_PROPERTIES = [
"active_mode",
]
BULB_EXCEPTIONS = (BulbException, asyncio.TimeoutError)
BULB_NETWORK_EXCEPTIONS = (socket.error, asyncio.TimeoutError)
BULB_EXCEPTIONS = (BulbException, *BULB_NETWORK_EXCEPTIONS)
PLATFORMS = ["binary_sensor", "light"]
@ -582,6 +585,11 @@ class YeelightDevice:
"""Return true is device is available."""
return self._available
@callback
def async_mark_unavailable(self):
"""Set unavailable on api call failure due to a network issue."""
self._available = False
@property
def model(self):
"""Return configured/autodetected device model."""
@ -642,26 +650,6 @@ class YeelightDevice:
return self._device_type
async def async_turn_on(
self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None
):
"""Turn on device."""
try:
await self.bulb.async_turn_on(
duration=duration, light_type=light_type, power_mode=power_mode
)
except BULB_EXCEPTIONS as ex:
_LOGGER.error("Unable to turn the bulb on: %s", ex)
async def async_turn_off(self, duration=DEFAULT_TRANSITION, light_type=None):
"""Turn off device."""
try:
await self.bulb.async_turn_off(duration=duration, light_type=light_type)
except BULB_EXCEPTIONS as ex:
_LOGGER.error(
"Unable to turn the bulb off: %s, %s: %s", self._host, self.name, ex
)
async def _async_update_properties(self):
"""Read new properties from the device."""
if not self.bulb:

View File

@ -6,7 +6,7 @@ import math
import voluptuous as vol
import yeelight
from yeelight import Bulb, Flow, RGBTransition, SleepTransition, flows
from yeelight import Bulb, BulbException, Flow, RGBTransition, SleepTransition, flows
from yeelight.enums import BulbType, LightType, PowerMode, SceneClass
from homeassistant.components.light import (
@ -34,6 +34,7 @@ from homeassistant.components.light import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -49,7 +50,7 @@ from . import (
ATTR_COUNT,
ATTR_MODE_MUSIC,
ATTR_TRANSITIONS,
BULB_EXCEPTIONS,
BULB_NETWORK_EXCEPTIONS,
CONF_FLOW_PARAMS,
CONF_MODE_MUSIC,
CONF_NIGHTLIGHT_SWITCH,
@ -242,8 +243,18 @@ def _async_cmd(func):
try:
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
return await func(self, *args, **kwargs)
except BULB_EXCEPTIONS as ex:
_LOGGER.error("Error when calling %s: %s", func, ex)
except BULB_NETWORK_EXCEPTIONS as ex:
# A network error happened, the bulb is likely offline now
self.device.async_mark_unavailable()
self.async_write_ha_state()
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {ex}"
) from ex
except BulbException as ex:
# The bulb likely responded but had an error
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {ex}"
) from ex
return _async_wrap
@ -375,7 +386,7 @@ def _async_setup_services(hass: HomeAssistant):
_async_set_auto_delay_off_scene,
)
platform.async_register_entity_service(
SERVICE_SET_MUSIC_MODE, SERVICE_SCHEMA_SET_MUSIC_MODE, "set_music_mode"
SERVICE_SET_MUSIC_MODE, SERVICE_SCHEMA_SET_MUSIC_MODE, "async_set_music_mode"
)
@ -509,9 +520,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
@property
def effect(self):
"""Return the current effect."""
if not self.device.is_color_flow_enabled:
return None
return self._effect
return self._effect if self.device.is_color_flow_enabled else None
@property
def _bulb(self) -> Bulb:
@ -519,9 +528,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
@property
def _properties(self) -> dict:
if self._bulb is None:
return {}
return self._bulb.last_properties
return self._bulb.last_properties if self._bulb else {}
def _get_property(self, prop, default=None):
return self._properties.get(prop, default)
@ -564,83 +571,88 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
"""Update light properties."""
await self.device.async_update()
def set_music_mode(self, music_mode) -> None:
async def async_set_music_mode(self, music_mode) -> None:
"""Set the music mode on or off."""
if music_mode:
try:
self._bulb.start_music()
except AssertionError as ex:
_LOGGER.error(ex)
else:
self._bulb.stop_music()
try:
await self._async_set_music_mode(music_mode)
except AssertionError as ex:
_LOGGER.error("Unable to turn on music mode, consider disabling it: %s", ex)
@_async_cmd
async def _async_set_music_mode(self, music_mode) -> None:
"""Set the music mode on or off wrapped with _async_cmd."""
bulb = self._bulb
method = bulb.stop_music if not music_mode else bulb.start_music
await self.hass.async_add_executor_job(method)
@_async_cmd
async def async_set_brightness(self, brightness, duration) -> None:
"""Set bulb brightness."""
if brightness:
if math.floor(self.brightness) == math.floor(brightness):
_LOGGER.debug("brightness already set to: %s", brightness)
# Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not
# hit the rate limit
return
if not brightness:
return
if math.floor(self.brightness) == math.floor(brightness):
_LOGGER.debug("brightness already set to: %s", brightness)
# Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not
# hit the rate limit
return
_LOGGER.debug("Setting brightness: %s", brightness)
await self._bulb.async_set_brightness(
brightness / 255 * 100, duration=duration, light_type=self.light_type
)
_LOGGER.debug("Setting brightness: %s", brightness)
await self._bulb.async_set_brightness(
brightness / 255 * 100, duration=duration, light_type=self.light_type
)
@_async_cmd
async def async_set_hs(self, hs_color, duration) -> None:
"""Set bulb's color."""
if hs_color and COLOR_MODE_HS in self.supported_color_modes:
if self.color_mode == COLOR_MODE_HS and self.hs_color == hs_color:
_LOGGER.debug("HS already set to: %s", hs_color)
# Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not
# hit the rate limit
return
if not hs_color or COLOR_MODE_HS not in self.supported_color_modes:
return
if self.color_mode == COLOR_MODE_HS and self.hs_color == hs_color:
_LOGGER.debug("HS already set to: %s", hs_color)
# Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not
# hit the rate limit
return
_LOGGER.debug("Setting HS: %s", hs_color)
await self._bulb.async_set_hsv(
hs_color[0], hs_color[1], duration=duration, light_type=self.light_type
)
_LOGGER.debug("Setting HS: %s", hs_color)
await self._bulb.async_set_hsv(
hs_color[0], hs_color[1], duration=duration, light_type=self.light_type
)
@_async_cmd
async def async_set_rgb(self, rgb, duration) -> None:
"""Set bulb's color."""
if rgb and COLOR_MODE_RGB in self.supported_color_modes:
if self.color_mode == COLOR_MODE_RGB and self.rgb_color == rgb:
_LOGGER.debug("RGB already set to: %s", rgb)
# Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not
# hit the rate limit
return
if not rgb or COLOR_MODE_RGB not in self.supported_color_modes:
return
if self.color_mode == COLOR_MODE_RGB and self.rgb_color == rgb:
_LOGGER.debug("RGB already set to: %s", rgb)
# Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not
# hit the rate limit
return
_LOGGER.debug("Setting RGB: %s", rgb)
await self._bulb.async_set_rgb(
*rgb, duration=duration, light_type=self.light_type
)
_LOGGER.debug("Setting RGB: %s", rgb)
await self._bulb.async_set_rgb(
*rgb, duration=duration, light_type=self.light_type
)
@_async_cmd
async def async_set_colortemp(self, colortemp, duration) -> None:
"""Set bulb's color temperature."""
if colortemp and COLOR_MODE_COLOR_TEMP in self.supported_color_modes:
temp_in_k = mired_to_kelvin(colortemp)
if not colortemp or COLOR_MODE_COLOR_TEMP not in self.supported_color_modes:
return
temp_in_k = mired_to_kelvin(colortemp)
if (
self.color_mode == COLOR_MODE_COLOR_TEMP
and self.color_temp == colortemp
):
_LOGGER.debug("Color temp already set to: %s", temp_in_k)
# Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not
# hit the rate limit
return
if self.color_mode == COLOR_MODE_COLOR_TEMP and self.color_temp == colortemp:
_LOGGER.debug("Color temp already set to: %s", temp_in_k)
# Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not
# hit the rate limit
return
await self._bulb.async_set_color_temp(
temp_in_k, duration=duration, light_type=self.light_type
)
await self._bulb.async_set_color_temp(
temp_in_k, duration=duration, light_type=self.light_type
)
@_async_cmd
async def async_set_default(self) -> None:
@ -650,37 +662,33 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
@_async_cmd
async def async_set_flash(self, flash) -> None:
"""Activate flash."""
if flash:
if int(self._bulb.last_properties["color_mode"]) != 1:
_LOGGER.error("Flash supported currently only in RGB mode")
return
if not flash:
return
if int(self._bulb.last_properties["color_mode"]) != 1:
_LOGGER.error("Flash supported currently only in RGB mode")
return
transition = int(self.config[CONF_TRANSITION])
if flash == FLASH_LONG:
count = 1
duration = transition * 5
if flash == FLASH_SHORT:
count = 1
duration = transition * 2
transition = int(self.config[CONF_TRANSITION])
if flash == FLASH_LONG:
count = 1
duration = transition * 5
if flash == FLASH_SHORT:
count = 1
duration = transition * 2
red, green, blue = color_util.color_hs_to_RGB(*self.hs_color)
red, green, blue = color_util.color_hs_to_RGB(*self.hs_color)
transitions = []
transitions.append(
RGBTransition(255, 0, 0, brightness=10, duration=duration)
)
transitions.append(SleepTransition(duration=transition))
transitions.append(
RGBTransition(
red, green, blue, brightness=self.brightness, duration=duration
)
transitions = []
transitions.append(RGBTransition(255, 0, 0, brightness=10, duration=duration))
transitions.append(SleepTransition(duration=transition))
transitions.append(
RGBTransition(
red, green, blue, brightness=self.brightness, duration=duration
)
)
flow = Flow(count=count, transitions=transitions)
try:
await self._bulb.async_start_flow(flow, light_type=self.light_type)
except BULB_EXCEPTIONS as ex:
_LOGGER.error("Unable to set flash: %s", ex)
flow = Flow(count=count, transitions=transitions)
await self._bulb.async_start_flow(flow, light_type=self.light_type)
@_async_cmd
async def async_set_effect(self, effect) -> None:
@ -707,11 +715,17 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
else:
return
try:
await self._bulb.async_start_flow(flow, light_type=self.light_type)
self._effect = effect
except BULB_EXCEPTIONS as ex:
_LOGGER.error("Unable to set effect: %s", ex)
await self._bulb.async_start_flow(flow, light_type=self.light_type)
self._effect = effect
@_async_cmd
async def _async_turn_on(self, duration) -> None:
"""Turn on the bulb for with a transition duration wrapped with _async_cmd."""
await self._bulb.async_turn_on(
duration=duration,
light_type=self.light_type,
power_mode=self._turn_on_power_mode,
)
async def async_turn_on(self, **kwargs) -> None:
"""Turn the bulb on."""
@ -727,46 +741,31 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
if not self.is_on:
await self.device.async_turn_on(
duration=duration,
light_type=self.light_type,
power_mode=self._turn_on_power_mode,
)
await self._async_turn_on(duration)
if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
try:
await self.hass.async_add_executor_job(
self.set_music_mode, self.config[CONF_MODE_MUSIC]
)
except BULB_EXCEPTIONS as ex:
_LOGGER.error(
"Unable to turn on music mode, consider disabling it: %s", ex
)
await self.async_set_music_mode(True)
try:
# values checked for none in methods
await self.async_set_hs(hs_color, duration)
await self.async_set_rgb(rgb, duration)
await self.async_set_colortemp(colortemp, duration)
await self.async_set_brightness(brightness, duration)
await self.async_set_flash(flash)
await self.async_set_effect(effect)
except BULB_EXCEPTIONS as ex:
_LOGGER.error("Unable to set bulb properties: %s", ex)
return
await self.async_set_hs(hs_color, duration)
await self.async_set_rgb(rgb, duration)
await self.async_set_colortemp(colortemp, duration)
await self.async_set_brightness(brightness, duration)
await self.async_set_flash(flash)
await self.async_set_effect(effect)
# save the current state if we had a manual change.
if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
try:
await self.async_set_default()
except BULB_EXCEPTIONS as ex:
_LOGGER.error("Unable to set the defaults: %s", ex)
return
await self.async_set_default()
# Some devices (mainly nightlights) will not send back the on state so we need to force a refresh
if not self.is_on:
await self.device.async_update(True)
@_async_cmd
async def _async_turn_off(self, duration) -> None:
"""Turn off with a given transition duration wrapped with _async_cmd."""
await self._bulb.async_turn_off(duration=duration, light_type=self.light_type)
async def async_turn_off(self, **kwargs) -> None:
"""Turn off."""
if not self.is_on:
@ -776,39 +775,30 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
await self.device.async_turn_off(duration=duration, light_type=self.light_type)
await self._async_turn_off(duration)
# Some devices will not send back the off state so we need to force a refresh
if self.is_on:
await self.device.async_update(True)
@_async_cmd
async def async_set_mode(self, mode: str):
"""Set a power mode."""
try:
await self._bulb.async_set_power_mode(PowerMode[mode.upper()])
except BULB_EXCEPTIONS as ex:
_LOGGER.error("Unable to set the power mode: %s", ex)
await self._bulb.async_set_power_mode(PowerMode[mode.upper()])
@_async_cmd
async def async_start_flow(self, transitions, count=0, action=ACTION_RECOVER):
"""Start flow."""
try:
flow = Flow(
count=count, action=Flow.actions[action], transitions=transitions
)
await self._bulb.async_start_flow(flow, light_type=self.light_type)
except BULB_EXCEPTIONS as ex:
_LOGGER.error("Unable to set effect: %s", ex)
flow = Flow(count=count, action=Flow.actions[action], transitions=transitions)
await self._bulb.async_start_flow(flow, light_type=self.light_type)
@_async_cmd
async def async_set_scene(self, scene_class, *args):
"""
Set the light directly to the specified state.
If the light is off, it will first be turned on.
"""
try:
await self._bulb.async_set_scene(scene_class, *args)
except BULB_EXCEPTIONS as ex:
_LOGGER.error("Unable to set scene: %s", ex)
await self._bulb.async_set_scene(scene_class, *args)
class YeelightColorLightSupport(YeelightGenericLight):

View File

@ -125,6 +125,7 @@ def _mocked_bulb(cannot_connect=False):
)
type(bulb).get_model_specs = MagicMock(return_value=_MODEL_SPECS[MODEL])
bulb.capabilities = CAPABILITIES.copy()
bulb.available = True
bulb.last_properties = PROPERTIES.copy()
bulb.music_mode = False
bulb.async_get_properties = AsyncMock()

View File

@ -1,7 +1,9 @@
"""Test the Yeelight light."""
import asyncio
import logging
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
import pytest
from yeelight import (
BulbException,
BulbType,
@ -28,6 +30,7 @@ from homeassistant.components.light import (
ATTR_RGB_COLOR,
ATTR_TRANSITION,
FLASH_LONG,
FLASH_SHORT,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
@ -82,8 +85,16 @@ from homeassistant.components.yeelight.light import (
YEELIGHT_MONO_EFFECT_LIST,
YEELIGHT_TEMP_ONLY_EFFECT_LIST,
)
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
CONF_NAME,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.color import (
@ -122,6 +133,7 @@ CONFIG_ENTRY_DATA = {
async def test_services(hass: HomeAssistant, caplog):
"""Test Yeelight services."""
assert await async_setup_component(hass, "homeassistant", {})
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
@ -140,13 +152,16 @@ async def test_services(hass: HomeAssistant, caplog):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_LIGHT).state == STATE_ON
assert hass.states.get(ENTITY_NIGHTLIGHT).state == STATE_OFF
async def _async_test_service(
service,
data,
method,
payload=None,
domain=DOMAIN,
failure_side_effect=BulbException,
failure_side_effect=HomeAssistantError,
):
err_count = len([x for x in caplog.records if x.levelno == logging.ERROR])
@ -174,11 +189,8 @@ async def test_services(hass: HomeAssistant, caplog):
else:
mocked_method = MagicMock(side_effect=failure_side_effect)
setattr(mocked_bulb, method, mocked_method)
await hass.services.async_call(domain, service, data, blocking=True)
assert (
len([x for x in caplog.records if x.levelno == logging.ERROR])
== err_count + 1
)
with pytest.raises(failure_side_effect):
await hass.services.async_call(domain, service, data, blocking=True)
# turn_on rgb_color
brightness = 100
@ -303,7 +315,50 @@ async def test_services(hass: HomeAssistant, caplog):
mocked_bulb.async_start_flow.assert_called_once() # flash
mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main)
# turn_on color_temp - flash short
brightness = 100
color_temp = 200
transition = 1
mocked_bulb.start_music.reset_mock()
mocked_bulb.async_set_brightness.reset_mock()
mocked_bulb.async_set_color_temp.reset_mock()
mocked_bulb.async_start_flow.reset_mock()
mocked_bulb.async_stop_flow.reset_mock()
mocked_bulb.last_properties["power"] = "off"
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
ATTR_BRIGHTNESS: brightness,
ATTR_COLOR_TEMP: color_temp,
ATTR_FLASH: FLASH_SHORT,
ATTR_EFFECT: EFFECT_STOP,
ATTR_TRANSITION: transition,
},
blocking=True,
)
mocked_bulb.async_turn_on.assert_called_once_with(
duration=transition * 1000,
light_type=LightType.Main,
power_mode=PowerMode.NORMAL,
)
mocked_bulb.async_turn_on.reset_mock()
mocked_bulb.start_music.assert_called_once()
mocked_bulb.async_set_brightness.assert_called_once_with(
brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main
)
mocked_bulb.async_set_color_temp.assert_called_once_with(
color_temperature_mired_to_kelvin(color_temp),
duration=transition * 1000,
light_type=LightType.Main,
)
mocked_bulb.async_set_hsv.assert_not_called()
mocked_bulb.async_set_rgb.assert_not_called()
mocked_bulb.async_start_flow.assert_called_once() # flash
mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main)
# turn_on nightlight
await _async_test_service(
SERVICE_TURN_ON,
@ -318,6 +373,7 @@ async def test_services(hass: HomeAssistant, caplog):
)
mocked_bulb.last_properties["power"] = "on"
assert hass.states.get(ENTITY_LIGHT).state != STATE_UNAVAILABLE
# turn_off
await _async_test_service(
SERVICE_TURN_OFF,
@ -393,12 +449,16 @@ async def test_services(hass: HomeAssistant, caplog):
)
# set_music_mode failure enable
await _async_test_service(
mocked_bulb.start_music = MagicMock(side_effect=AssertionError)
assert "Unable to turn on music mode, consider disabling it" not in caplog.text
await hass.services.async_call(
DOMAIN,
SERVICE_SET_MUSIC_MODE,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "true"},
"start_music",
failure_side_effect=AssertionError,
blocking=True,
)
assert mocked_bulb.start_music.mock_calls == [call()]
assert "Unable to turn on music mode, consider disabling it" in caplog.text
# set_music_mode disable
await _async_test_service(
@ -417,18 +477,35 @@ async def test_services(hass: HomeAssistant, caplog):
)
# test _cmd wrapper error handler
mocked_bulb.last_properties["power"] = "off"
err_count = len([x for x in caplog.records if x.levelno == logging.ERROR])
type(mocked_bulb).turn_on = MagicMock()
type(mocked_bulb).set_brightness = MagicMock(side_effect=BulbException)
mocked_bulb.available = True
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 50},
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ENTITY_LIGHT},
blocking=True,
)
assert (
len([x for x in caplog.records if x.levelno == logging.ERROR]) == err_count + 1
)
assert hass.states.get(ENTITY_LIGHT).state == STATE_OFF
mocked_bulb.async_turn_on = AsyncMock()
mocked_bulb.async_set_brightness = AsyncMock(side_effect=BulbException)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 50},
blocking=True,
)
assert hass.states.get(ENTITY_LIGHT).state == STATE_OFF
mocked_bulb.async_set_brightness = AsyncMock(side_effect=asyncio.TimeoutError)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 55},
blocking=True,
)
assert hass.states.get(ENTITY_LIGHT).state == STATE_UNAVAILABLE
async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant):