Reduce overhead to fire events (#95163)
This commit is contained in:
parent
9354df975c
commit
5059cee53f
4 changed files with 81 additions and 68 deletions
|
@ -82,7 +82,7 @@ from .exceptions import (
|
|||
)
|
||||
from .helpers.aiohttp_compat import restore_original_aiohttp_cancel_behavior
|
||||
from .helpers.json import json_dumps
|
||||
from .util import dt as dt_util, location, ulid as ulid_util
|
||||
from .util import dt as dt_util, location
|
||||
from .util.async_ import (
|
||||
cancelling,
|
||||
run_callback_threadsafe,
|
||||
|
@ -91,6 +91,7 @@ from .util.async_ import (
|
|||
from .util.json import JsonObjectType
|
||||
from .util.read_only_dict import ReadOnlyDict
|
||||
from .util.timeout import TimeoutManager
|
||||
from .util.ulid import ulid, ulid_at_time
|
||||
from .util.unit_system import (
|
||||
_CONF_UNIT_SYSTEM_IMPERIAL,
|
||||
_CONF_UNIT_SYSTEM_US_CUSTOMARY,
|
||||
|
@ -874,7 +875,7 @@ class Context:
|
|||
id: str | None = None, # pylint: disable=redefined-builtin
|
||||
) -> None:
|
||||
"""Init the context."""
|
||||
self.id = id or ulid_util.ulid()
|
||||
self.id = id or ulid()
|
||||
self.user_id = user_id
|
||||
self.parent_id = parent_id
|
||||
self.origin_event: Event | None = None
|
||||
|
@ -926,10 +927,14 @@ class Event:
|
|||
self.data = data or {}
|
||||
self.origin = origin
|
||||
self.time_fired = time_fired or dt_util.utcnow()
|
||||
self.context: Context = context or Context(
|
||||
id=ulid_util.ulid_at_time(dt_util.utc_to_timestamp(self.time_fired))
|
||||
)
|
||||
if not context:
|
||||
context = Context(
|
||||
id=ulid_at_time(dt_util.utc_to_timestamp(self.time_fired))
|
||||
)
|
||||
self.context = context
|
||||
self._as_dict: ReadOnlyDict[str, Any] | None = None
|
||||
if not context.origin_event:
|
||||
context.origin_event = self
|
||||
|
||||
def as_dict(self) -> ReadOnlyDict[str, Any]:
|
||||
"""Create a dict representation of this Event.
|
||||
|
@ -973,6 +978,8 @@ class EventBus:
|
|||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize a new event bus."""
|
||||
self._listeners: dict[str, list[_FilterableJob]] = {}
|
||||
self._match_all_listeners: list[_FilterableJob] = []
|
||||
self._listeners[MATCH_ALL] = self._match_all_listeners
|
||||
self._hass = hass
|
||||
|
||||
@callback
|
||||
|
@ -1019,20 +1026,19 @@ class EventBus:
|
|||
)
|
||||
|
||||
listeners = self._listeners.get(event_type, [])
|
||||
match_all_listeners = self._match_all_listeners
|
||||
|
||||
# EVENT_HOMEASSISTANT_CLOSE should go only to this listeners
|
||||
match_all_listeners = self._listeners.get(MATCH_ALL)
|
||||
if match_all_listeners is not None and event_type != EVENT_HOMEASSISTANT_CLOSE:
|
||||
if not listeners and not match_all_listeners:
|
||||
return
|
||||
|
||||
# EVENT_HOMEASSISTANT_CLOSE should not be sent to MATCH_ALL listeners
|
||||
if event_type != EVENT_HOMEASSISTANT_CLOSE:
|
||||
listeners = match_all_listeners + listeners
|
||||
|
||||
event = Event(event_type, event_data, origin, time_fired, context)
|
||||
if not event.context.origin_event:
|
||||
event.context.origin_event = event
|
||||
|
||||
_LOGGER.debug("Bus:Handling %s", event)
|
||||
|
||||
if not listeners:
|
||||
return
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_LOGGER.debug("Bus:Handling %s", event)
|
||||
|
||||
for job, event_filter, run_immediately in listeners:
|
||||
if event_filter is not None:
|
||||
|
@ -1195,7 +1201,7 @@ class EventBus:
|
|||
self._listeners[event_type].remove(filterable_job)
|
||||
|
||||
# delete event_type list if empty
|
||||
if not self._listeners[event_type]:
|
||||
if not self._listeners[event_type] and event_type != MATCH_ALL:
|
||||
self._listeners.pop(event_type)
|
||||
except (KeyError, ValueError):
|
||||
# KeyError is key event_type listener did not exist
|
||||
|
@ -1630,7 +1636,7 @@ class StateMachine:
|
|||
# https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6323
|
||||
timestamp = time.time()
|
||||
now = dt_util.utc_from_timestamp(timestamp)
|
||||
context = Context(id=ulid_util.ulid_at_time(timestamp))
|
||||
context = Context(id=ulid_at_time(timestamp))
|
||||
else:
|
||||
now = dt_util.utcnow()
|
||||
|
||||
|
|
|
@ -296,6 +296,7 @@ async def test_service_calls_off_mode(
|
|||
|
||||
device.set_setpoint_heat.reset_mock()
|
||||
device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError
|
||||
caplog.clear()
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
|
@ -308,8 +309,9 @@ async def test_service_calls_off_mode(
|
|||
)
|
||||
device.set_setpoint_cool.assert_called_with(95)
|
||||
device.set_setpoint_heat.assert_called_with(77)
|
||||
assert "Invalid temperature" in caplog.messages[-1]
|
||||
assert "Invalid temperature" in caplog.text
|
||||
|
||||
caplog.clear()
|
||||
reset_mock(device)
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
|
@ -436,6 +438,7 @@ async def test_service_calls_cool_mode(
|
|||
device.set_setpoint_cool.assert_called_with(95)
|
||||
device.set_setpoint_heat.assert_called_with(77)
|
||||
|
||||
caplog.clear()
|
||||
device.set_setpoint_cool.reset_mock()
|
||||
device.set_setpoint_cool.side_effect = aiosomecomfort.SomeComfortError
|
||||
await hass.services.async_call(
|
||||
|
@ -450,7 +453,7 @@ async def test_service_calls_cool_mode(
|
|||
)
|
||||
device.set_setpoint_cool.assert_called_with(95)
|
||||
device.set_setpoint_heat.assert_called_with(77)
|
||||
assert "Invalid temperature" in caplog.messages[-1]
|
||||
assert "Invalid temperature" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
await hass.services.async_call(
|
||||
|
@ -467,6 +470,7 @@ async def test_service_calls_cool_mode(
|
|||
reset_mock(device)
|
||||
|
||||
device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError
|
||||
caplog.clear()
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
|
@ -478,7 +482,7 @@ async def test_service_calls_cool_mode(
|
|||
device.set_hold_cool.assert_called_once_with(True, 12)
|
||||
device.set_hold_heat.assert_not_called()
|
||||
device.set_setpoint_heat.assert_not_called()
|
||||
assert "Temperature out of range" in caplog.messages[-1]
|
||||
assert "Temperature out of range" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
|
||||
|
@ -512,6 +516,7 @@ async def test_service_calls_cool_mode(
|
|||
|
||||
device.raw_ui_data["StatusHeat"] = 2
|
||||
device.raw_ui_data["StatusCool"] = 2
|
||||
caplog.clear()
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
|
@ -521,7 +526,7 @@ async def test_service_calls_cool_mode(
|
|||
)
|
||||
device.set_hold_cool.assert_called_once_with(True)
|
||||
device.set_hold_heat.assert_not_called()
|
||||
assert "Couldn't set permanent hold" in caplog.messages[-1]
|
||||
assert "Couldn't set permanent hold" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
|
||||
|
@ -536,6 +541,7 @@ async def test_service_calls_cool_mode(
|
|||
device.set_hold_cool.assert_called_once_with(False)
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError
|
||||
|
||||
|
@ -548,7 +554,7 @@ async def test_service_calls_cool_mode(
|
|||
|
||||
device.set_hold_heat.assert_not_called()
|
||||
device.set_hold_cool.assert_called_once_with(False)
|
||||
assert "Can not stop hold mode" in caplog.messages[-1]
|
||||
assert "Can not stop hold mode" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
|
||||
|
@ -566,6 +572,8 @@ async def test_service_calls_cool_mode(
|
|||
device.set_hold_heat.assert_not_called()
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError
|
||||
|
||||
device.raw_ui_data["StatusHeat"] = 2
|
||||
|
@ -580,9 +588,10 @@ async def test_service_calls_cool_mode(
|
|||
|
||||
device.set_hold_cool.assert_called_once_with(True)
|
||||
device.set_hold_heat.assert_not_called()
|
||||
assert "Couldn't set permanent hold" in caplog.messages[-1]
|
||||
assert "Couldn't set permanent hold" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
device.raw_ui_data["StatusHeat"] = 2
|
||||
device.raw_ui_data["StatusCool"] = 2
|
||||
|
@ -597,7 +606,7 @@ async def test_service_calls_cool_mode(
|
|||
|
||||
device.set_hold_cool.assert_not_called()
|
||||
device.set_hold_heat.assert_not_called()
|
||||
assert "Invalid system mode returned" in caplog.messages[-2]
|
||||
assert "Invalid system mode returned" in caplog.text
|
||||
|
||||
|
||||
async def test_service_calls_heat_mode(
|
||||
|
@ -638,8 +647,9 @@ async def test_service_calls_heat_mode(
|
|||
)
|
||||
device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 59)
|
||||
device.set_hold_heat.reset_mock()
|
||||
assert "Invalid temperature" in caplog.messages[-1]
|
||||
assert "Invalid temperature" in caplog.text
|
||||
|
||||
caplog.clear()
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
|
@ -667,7 +677,7 @@ async def test_service_calls_heat_mode(
|
|||
)
|
||||
device.set_setpoint_cool.assert_called_with(95)
|
||||
device.set_setpoint_heat.assert_called_with(77)
|
||||
assert "Invalid temperature" in caplog.messages[-1]
|
||||
assert "Invalid temperature" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
device.raw_ui_data["StatusHeat"] = 2
|
||||
|
@ -696,6 +706,7 @@ async def test_service_calls_heat_mode(
|
|||
device.set_setpoint_heat.assert_called_once()
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError
|
||||
|
||||
|
@ -710,7 +721,7 @@ async def test_service_calls_heat_mode(
|
|||
)
|
||||
device.set_hold_heat.assert_called_once_with(True)
|
||||
device.set_hold_cool.assert_not_called()
|
||||
assert "Couldn't set permanent hold" in caplog.messages[-1]
|
||||
assert "Couldn't set permanent hold" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
|
||||
|
@ -726,6 +737,7 @@ async def test_service_calls_heat_mode(
|
|||
device.set_setpoint_cool.assert_not_called()
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError
|
||||
|
||||
|
@ -739,9 +751,10 @@ async def test_service_calls_heat_mode(
|
|||
device.set_hold_heat.assert_called_once_with(True, 22)
|
||||
device.set_hold_cool.assert_not_called()
|
||||
device.set_setpoint_cool.assert_not_called()
|
||||
assert "Temperature out of range" in caplog.messages[-1]
|
||||
assert "Temperature out of range" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
|
@ -765,7 +778,7 @@ async def test_service_calls_heat_mode(
|
|||
)
|
||||
|
||||
device.set_hold_heat.assert_called_once_with(False)
|
||||
assert "Can not stop hold mode" in caplog.messages[-1]
|
||||
assert "Can not stop hold mode" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
device.raw_ui_data["StatusHeat"] = 2
|
||||
|
@ -844,6 +857,7 @@ async def test_service_calls_auto_mode(
|
|||
device.set_setpoint_heat.assert_called_once_with(77)
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError
|
||||
device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError
|
||||
|
@ -855,9 +869,10 @@ async def test_service_calls_auto_mode(
|
|||
blocking=True,
|
||||
)
|
||||
device.set_setpoint_heat.assert_not_called()
|
||||
assert "Invalid temperature" in caplog.messages[-1]
|
||||
assert "Invalid temperature" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError
|
||||
device.set_setpoint_cool.side_effect = aiosomecomfort.SomeComfortError
|
||||
|
@ -872,9 +887,10 @@ async def test_service_calls_auto_mode(
|
|||
blocking=True,
|
||||
)
|
||||
device.set_setpoint_heat.assert_not_called()
|
||||
assert "Invalid temperature" in caplog.messages[-1]
|
||||
assert "Invalid temperature" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
device.set_hold_heat.side_effect = None
|
||||
device.set_hold_cool.side_effect = None
|
||||
|
@ -893,6 +909,7 @@ async def test_service_calls_auto_mode(
|
|||
device.set_hold_heat.assert_called_once_with(True)
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError
|
||||
device.raw_ui_data["StatusHeat"] = 2
|
||||
|
@ -906,7 +923,7 @@ async def test_service_calls_auto_mode(
|
|||
)
|
||||
device.set_hold_cool.assert_called_once_with(True)
|
||||
device.set_hold_heat.assert_called_once_with(True)
|
||||
assert "Couldn't set permanent hold" in caplog.messages[-1]
|
||||
assert "Couldn't set permanent hold" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
device.set_setpoint_heat.side_effect = None
|
||||
|
@ -923,6 +940,7 @@ async def test_service_calls_auto_mode(
|
|||
device.set_hold_heat.assert_called_once_with(True, 22)
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
|
@ -946,9 +964,10 @@ async def test_service_calls_auto_mode(
|
|||
|
||||
device.set_hold_heat.assert_not_called()
|
||||
device.set_hold_cool.assert_called_once_with(False)
|
||||
assert "Can not stop hold mode" in caplog.messages[-1]
|
||||
assert "Can not stop hold mode" in caplog.text
|
||||
|
||||
reset_mock(device)
|
||||
caplog.clear()
|
||||
|
||||
device.raw_ui_data["StatusHeat"] = 2
|
||||
device.raw_ui_data["StatusCool"] = 2
|
||||
|
@ -978,7 +997,7 @@ async def test_service_calls_auto_mode(
|
|||
|
||||
device.set_hold_cool.assert_called_once_with(True)
|
||||
device.set_hold_heat.assert_not_called()
|
||||
assert "Couldn't set permanent hold" in caplog.messages[-1]
|
||||
assert "Couldn't set permanent hold" in caplog.text
|
||||
|
||||
|
||||
async def test_async_update_errors(
|
||||
|
|
|
@ -49,7 +49,7 @@ async def test_setup_no_mqtt(
|
|||
async def test_setup_with_pub(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None:
|
||||
"""Test the setup with subscription."""
|
||||
# Should start off with no listeners for all events
|
||||
assert hass.bus.async_listeners().get("*") is None
|
||||
assert not hass.bus.async_listeners().get("*")
|
||||
|
||||
assert await add_eventstream(hass, pub_topic="bar")
|
||||
await hass.async_block_till_done()
|
||||
|
|
|
@ -11,7 +11,11 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import assert_setup_component, async_fire_time_changed
|
||||
from tests.common import (
|
||||
assert_setup_component,
|
||||
async_capture_events,
|
||||
async_fire_time_changed,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -222,9 +226,9 @@ async def test_start_stop(mock_pilight_error, hass: HomeAssistant) -> None:
|
|||
|
||||
|
||||
@patch("pilight.pilight.Client", PilightDaemonSim)
|
||||
@patch("homeassistant.core._LOGGER.debug")
|
||||
async def test_receive_code(mock_debug, hass: HomeAssistant) -> None:
|
||||
async def test_receive_code(hass: HomeAssistant) -> None:
|
||||
"""Check if code receiving via pilight daemon works."""
|
||||
events = async_capture_events(hass, pilight.EVENT)
|
||||
with assert_setup_component(4):
|
||||
assert await async_setup_component(hass, pilight.DOMAIN, {pilight.DOMAIN: {}})
|
||||
|
||||
|
@ -239,18 +243,13 @@ async def test_receive_code(mock_debug, hass: HomeAssistant) -> None:
|
|||
},
|
||||
**PilightDaemonSim.test_message["message"],
|
||||
)
|
||||
debug_log_call = mock_debug.call_args_list[-1]
|
||||
|
||||
# Check if all message parts are put on event bus
|
||||
for key, value in expected_message.items():
|
||||
assert str(key) in str(debug_log_call)
|
||||
assert str(value) in str(debug_log_call)
|
||||
assert events[0].data == expected_message
|
||||
|
||||
|
||||
@patch("pilight.pilight.Client", PilightDaemonSim)
|
||||
@patch("homeassistant.core._LOGGER.debug")
|
||||
async def test_whitelist_exact_match(mock_debug, hass: HomeAssistant) -> None:
|
||||
async def test_whitelist_exact_match(hass: HomeAssistant) -> None:
|
||||
"""Check whitelist filter with matched data."""
|
||||
events = async_capture_events(hass, pilight.EVENT)
|
||||
with assert_setup_component(4):
|
||||
whitelist = {
|
||||
"protocol": [PilightDaemonSim.test_message["protocol"]],
|
||||
|
@ -272,18 +271,14 @@ async def test_whitelist_exact_match(mock_debug, hass: HomeAssistant) -> None:
|
|||
},
|
||||
**PilightDaemonSim.test_message["message"],
|
||||
)
|
||||
debug_log_call = mock_debug.call_args_list[-1]
|
||||
|
||||
# Check if all message parts are put on event bus
|
||||
for key, value in expected_message.items():
|
||||
assert str(key) in str(debug_log_call)
|
||||
assert str(value) in str(debug_log_call)
|
||||
assert events[0].data == expected_message
|
||||
|
||||
|
||||
@patch("pilight.pilight.Client", PilightDaemonSim)
|
||||
@patch("homeassistant.core._LOGGER.debug")
|
||||
async def test_whitelist_partial_match(mock_debug, hass: HomeAssistant) -> None:
|
||||
async def test_whitelist_partial_match(hass: HomeAssistant) -> None:
|
||||
"""Check whitelist filter with partially matched data, should work."""
|
||||
events = async_capture_events(hass, pilight.EVENT)
|
||||
with assert_setup_component(4):
|
||||
whitelist = {
|
||||
"protocol": [PilightDaemonSim.test_message["protocol"]],
|
||||
|
@ -303,18 +298,15 @@ async def test_whitelist_partial_match(mock_debug, hass: HomeAssistant) -> None:
|
|||
},
|
||||
**PilightDaemonSim.test_message["message"],
|
||||
)
|
||||
debug_log_call = mock_debug.call_args_list[-1]
|
||||
|
||||
# Check if all message parts are put on event bus
|
||||
for key, value in expected_message.items():
|
||||
assert str(key) in str(debug_log_call)
|
||||
assert str(value) in str(debug_log_call)
|
||||
assert events[0].data == expected_message
|
||||
|
||||
|
||||
@patch("pilight.pilight.Client", PilightDaemonSim)
|
||||
@patch("homeassistant.core._LOGGER.debug")
|
||||
async def test_whitelist_or_match(mock_debug, hass: HomeAssistant) -> None:
|
||||
async def test_whitelist_or_match(hass: HomeAssistant) -> None:
|
||||
"""Check whitelist filter with several subsection, should work."""
|
||||
events = async_capture_events(hass, pilight.EVENT)
|
||||
|
||||
with assert_setup_component(4):
|
||||
whitelist = {
|
||||
"protocol": [
|
||||
|
@ -337,18 +329,15 @@ async def test_whitelist_or_match(mock_debug, hass: HomeAssistant) -> None:
|
|||
},
|
||||
**PilightDaemonSim.test_message["message"],
|
||||
)
|
||||
debug_log_call = mock_debug.call_args_list[-1]
|
||||
|
||||
# Check if all message parts are put on event bus
|
||||
for key, value in expected_message.items():
|
||||
assert str(key) in str(debug_log_call)
|
||||
assert str(value) in str(debug_log_call)
|
||||
assert events[0].data == expected_message
|
||||
|
||||
|
||||
@patch("pilight.pilight.Client", PilightDaemonSim)
|
||||
@patch("homeassistant.core._LOGGER.debug")
|
||||
async def test_whitelist_no_match(mock_debug, hass: HomeAssistant) -> None:
|
||||
async def test_whitelist_no_match(hass: HomeAssistant) -> None:
|
||||
"""Check whitelist filter with unmatched data, should not work."""
|
||||
events = async_capture_events(hass, pilight.EVENT)
|
||||
|
||||
with assert_setup_component(4):
|
||||
whitelist = {
|
||||
"protocol": ["wrong_protocol"],
|
||||
|
@ -360,9 +349,8 @@ async def test_whitelist_no_match(mock_debug, hass: HomeAssistant) -> None:
|
|||
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
debug_log_call = mock_debug.call_args_list[-1]
|
||||
|
||||
assert "Event pilight_received" not in debug_log_call
|
||||
assert len(events) == 0
|
||||
|
||||
|
||||
async def test_call_rate_delay_throttle_enabled(hass: HomeAssistant) -> None:
|
||||
|
|
Loading…
Add table
Reference in a new issue