mirror of
https://github.com/home-assistant/core.git
synced 2025-10-03 00:39:30 +00:00
Compare commits
2 Commits
2025.10.0b
...
mqtt-json-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e2e907963a | ||
![]() |
104ff0f1e1 |
@@ -73,6 +73,7 @@ ABBREVIATIONS = {
|
|||||||
"fan_mode_stat_t": "fan_mode_state_topic",
|
"fan_mode_stat_t": "fan_mode_state_topic",
|
||||||
"frc_upd": "force_update",
|
"frc_upd": "force_update",
|
||||||
"g_tpl": "green_template",
|
"g_tpl": "green_template",
|
||||||
|
"grp": "group",
|
||||||
"hs_cmd_t": "hs_command_topic",
|
"hs_cmd_t": "hs_command_topic",
|
||||||
"hs_cmd_tpl": "hs_command_template",
|
"hs_cmd_tpl": "hs_command_template",
|
||||||
"hs_stat_t": "hs_state_topic",
|
"hs_stat_t": "hs_state_topic",
|
||||||
|
@@ -106,6 +106,7 @@ CONF_FLASH_TIME_SHORT = "flash_time_short"
|
|||||||
CONF_GET_POSITION_TEMPLATE = "position_template"
|
CONF_GET_POSITION_TEMPLATE = "position_template"
|
||||||
CONF_GET_POSITION_TOPIC = "position_topic"
|
CONF_GET_POSITION_TOPIC = "position_topic"
|
||||||
CONF_GREEN_TEMPLATE = "green_template"
|
CONF_GREEN_TEMPLATE = "green_template"
|
||||||
|
CONF_GROUP = "group"
|
||||||
CONF_HS_COMMAND_TEMPLATE = "hs_command_template"
|
CONF_HS_COMMAND_TEMPLATE = "hs_command_template"
|
||||||
CONF_HS_COMMAND_TOPIC = "hs_command_topic"
|
CONF_HS_COMMAND_TOPIC = "hs_command_topic"
|
||||||
CONF_HS_STATE_TOPIC = "hs_state_topic"
|
CONF_HS_STATE_TOPIC = "hs_state_topic"
|
||||||
|
@@ -546,7 +546,7 @@ class MqttAttributesMixin(Entity):
|
|||||||
_LOGGER.warning("Erroneous JSON: %s", payload)
|
_LOGGER.warning("Erroneous JSON: %s", payload)
|
||||||
else:
|
else:
|
||||||
if isinstance(json_dict, dict):
|
if isinstance(json_dict, dict):
|
||||||
filtered_dict = {
|
filtered_dict: dict[str, Any] = {
|
||||||
k: v
|
k: v
|
||||||
for k, v in json_dict.items()
|
for k, v in json_dict.items()
|
||||||
if k not in MQTT_ATTRIBUTES_BLOCKED
|
if k not in MQTT_ATTRIBUTES_BLOCKED
|
||||||
@@ -1373,6 +1373,7 @@ class MqttEntity(
|
|||||||
_attr_force_update = False
|
_attr_force_update = False
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
_default_entity: str | None = None
|
||||||
_default_name: str | None
|
_default_name: str | None
|
||||||
_entity_id_format: str
|
_entity_id_format: str
|
||||||
_update_registry_entity_id: str | None = None
|
_update_registry_entity_id: str | None = None
|
||||||
@@ -1609,7 +1610,7 @@ class MqttEntity(
|
|||||||
self._attr_entity_registry_enabled_default = bool(
|
self._attr_entity_registry_enabled_default = bool(
|
||||||
config.get(CONF_ENABLED_BY_DEFAULT)
|
config.get(CONF_ENABLED_BY_DEFAULT)
|
||||||
)
|
)
|
||||||
self._attr_icon = config.get(CONF_ICON)
|
self._attr_icon = config.get(CONF_ICON, self._default_entity)
|
||||||
self._attr_entity_picture = config.get(CONF_ENTITY_PICTURE)
|
self._attr_entity_picture = config.get(CONF_ENTITY_PICTURE)
|
||||||
# Set the entity name if needed
|
# Set the entity name if needed
|
||||||
self._set_entity_name(config)
|
self._set_entity_name(config)
|
||||||
|
@@ -23,6 +23,7 @@ from homeassistant.components.light import (
|
|||||||
ATTR_XY_COLOR,
|
ATTR_XY_COLOR,
|
||||||
DEFAULT_MAX_KELVIN,
|
DEFAULT_MAX_KELVIN,
|
||||||
DEFAULT_MIN_KELVIN,
|
DEFAULT_MIN_KELVIN,
|
||||||
|
DOMAIN as LIGHT_DOMAIN,
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
FLASH_LONG,
|
FLASH_LONG,
|
||||||
FLASH_SHORT,
|
FLASH_SHORT,
|
||||||
@@ -34,6 +35,7 @@ from homeassistant.components.light import (
|
|||||||
valid_supported_color_modes,
|
valid_supported_color_modes,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
CONF_BRIGHTNESS,
|
CONF_BRIGHTNESS,
|
||||||
CONF_COLOR_TEMP,
|
CONF_COLOR_TEMP,
|
||||||
CONF_EFFECT,
|
CONF_EFFECT,
|
||||||
@@ -45,7 +47,7 @@ from homeassistant.const import (
|
|||||||
STATE_ON,
|
STATE_ON,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||||
from homeassistant.helpers.json import json_dumps
|
from homeassistant.helpers.json import json_dumps
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
from homeassistant.helpers.typing import ConfigType, VolSchemaType
|
from homeassistant.helpers.typing import ConfigType, VolSchemaType
|
||||||
@@ -62,6 +64,7 @@ from ..const import (
|
|||||||
CONF_FLASH,
|
CONF_FLASH,
|
||||||
CONF_FLASH_TIME_LONG,
|
CONF_FLASH_TIME_LONG,
|
||||||
CONF_FLASH_TIME_SHORT,
|
CONF_FLASH_TIME_SHORT,
|
||||||
|
CONF_GROUP,
|
||||||
CONF_MAX_KELVIN,
|
CONF_MAX_KELVIN,
|
||||||
CONF_MAX_MIREDS,
|
CONF_MAX_MIREDS,
|
||||||
CONF_MIN_KELVIN,
|
CONF_MIN_KELVIN,
|
||||||
@@ -77,6 +80,7 @@ from ..const import (
|
|||||||
DEFAULT_FLASH_TIME_LONG,
|
DEFAULT_FLASH_TIME_LONG,
|
||||||
DEFAULT_FLASH_TIME_SHORT,
|
DEFAULT_FLASH_TIME_SHORT,
|
||||||
DEFAULT_WHITE_SCALE,
|
DEFAULT_WHITE_SCALE,
|
||||||
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from ..entity import MqttEntity
|
from ..entity import MqttEntity
|
||||||
from ..models import ReceiveMessage
|
from ..models import ReceiveMessage
|
||||||
@@ -91,8 +95,6 @@ from .schema_basic import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "mqtt_json"
|
|
||||||
|
|
||||||
DEFAULT_NAME = "MQTT JSON Light"
|
DEFAULT_NAME = "MQTT JSON Light"
|
||||||
|
|
||||||
DEFAULT_FLASH = True
|
DEFAULT_FLASH = True
|
||||||
@@ -115,6 +117,7 @@ _PLATFORM_SCHEMA_BASE = (
|
|||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT
|
CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT
|
||||||
): cv.positive_int,
|
): cv.positive_int,
|
||||||
|
vol.Optional(CONF_GROUP): vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(CONF_MAX_MIREDS): cv.positive_int,
|
vol.Optional(CONF_MAX_MIREDS): cv.positive_int,
|
||||||
vol.Optional(CONF_MIN_MIREDS): cv.positive_int,
|
vol.Optional(CONF_MIN_MIREDS): cv.positive_int,
|
||||||
vol.Optional(CONF_MAX_KELVIN): cv.positive_int,
|
vol.Optional(CONF_MAX_KELVIN): cv.positive_int,
|
||||||
@@ -171,16 +174,20 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
|||||||
|
|
||||||
_fixed_color_mode: ColorMode | str | None = None
|
_fixed_color_mode: ColorMode | str | None = None
|
||||||
_flash_times: dict[str, int | None]
|
_flash_times: dict[str, int | None]
|
||||||
|
_group_member_entity_ids_resolved: bool
|
||||||
_topic: dict[str, str | None]
|
_topic: dict[str, str | None]
|
||||||
_optimistic: bool
|
_optimistic: bool
|
||||||
|
_extra_state_attributes: dict[str, Any] | None = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def config_schema() -> VolSchemaType:
|
def config_schema() -> VolSchemaType:
|
||||||
"""Return the config schema."""
|
"""Return the config schema."""
|
||||||
return DISCOVERY_SCHEMA_JSON
|
return DISCOVERY_SCHEMA_JSON
|
||||||
|
|
||||||
|
@callback
|
||||||
def _setup_from_config(self, config: ConfigType) -> None:
|
def _setup_from_config(self, config: ConfigType) -> None:
|
||||||
"""(Re)Setup the entity."""
|
"""(Re)Setup the entity."""
|
||||||
|
self._group_member_entity_ids_resolved = False
|
||||||
self._color_temp_kelvin = config[CONF_COLOR_TEMP_KELVIN]
|
self._color_temp_kelvin = config[CONF_COLOR_TEMP_KELVIN]
|
||||||
self._attr_min_color_temp_kelvin = (
|
self._attr_min_color_temp_kelvin = (
|
||||||
color_util.color_temperature_mired_to_kelvin(max_mireds)
|
color_util.color_temperature_mired_to_kelvin(max_mireds)
|
||||||
@@ -226,6 +233,43 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
|||||||
else:
|
else:
|
||||||
self._attr_supported_color_modes = {ColorMode.ONOFF}
|
self._attr_supported_color_modes = {ColorMode.ONOFF}
|
||||||
|
|
||||||
|
self._update_extra_state_and_group_info()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_extra_state_and_group_info(self) -> None:
|
||||||
|
"""Set the entity_id property if the light represents a group of lights.
|
||||||
|
|
||||||
|
Setting entity_id in the extra state attributes will show the discover the light
|
||||||
|
as a group and allow to control the member light manually.
|
||||||
|
"""
|
||||||
|
if CONF_GROUP not in self._config:
|
||||||
|
self._attr_extra_state_attributes = self._extra_state_attributes or {}
|
||||||
|
self._default_entity = None
|
||||||
|
return
|
||||||
|
self._default_entity = "mdi:lightbulb-group"
|
||||||
|
entity_registry = er.async_get(self.hass)
|
||||||
|
_group_entity_ids: list[str] = []
|
||||||
|
self._group_member_entity_ids_resolved = True
|
||||||
|
for resource_id in self._config[CONF_GROUP]:
|
||||||
|
if entity_id := entity_registry.async_get_entity_id(
|
||||||
|
LIGHT_DOMAIN, DOMAIN, resource_id
|
||||||
|
):
|
||||||
|
_group_entity_ids.append(entity_id)
|
||||||
|
else:
|
||||||
|
# The ID is not (yet) resolved, so we retry at the next state update.
|
||||||
|
# This can only happen the first time the member entities
|
||||||
|
# are discovered, and added to the entity registry.
|
||||||
|
self._group_member_entity_ids_resolved = False
|
||||||
|
|
||||||
|
entity_attribute: dict[str, Any] = {ATTR_ENTITY_ID: _group_entity_ids}
|
||||||
|
if self._extra_state_attributes is None:
|
||||||
|
self._attr_extra_state_attributes = entity_attribute
|
||||||
|
return
|
||||||
|
|
||||||
|
self._attr_extra_state_attributes = (
|
||||||
|
self._extra_state_attributes | entity_attribute
|
||||||
|
)
|
||||||
|
|
||||||
def _update_color(self, values: dict[str, Any]) -> None:
|
def _update_color(self, values: dict[str, Any]) -> None:
|
||||||
color_mode: str = values["color_mode"]
|
color_mode: str = values["color_mode"]
|
||||||
if not self._supports_color_mode(color_mode):
|
if not self._supports_color_mode(color_mode):
|
||||||
@@ -327,6 +371,21 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
|||||||
with suppress(KeyError):
|
with suppress(KeyError):
|
||||||
self._attr_effect = cast(str, values["effect"])
|
self._attr_effect = cast(str, values["effect"])
|
||||||
|
|
||||||
|
# We update the group info on a received state up, as member
|
||||||
|
if not self._group_member_entity_ids_resolved:
|
||||||
|
self._update_extra_state_and_group_info()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _process_update_extra_state_attributes(
|
||||||
|
self, extra_state_attributes: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Process an the extra state attributes update.
|
||||||
|
|
||||||
|
Add extracted group members if the light represents a group.
|
||||||
|
"""
|
||||||
|
self._extra_state_attributes = extra_state_attributes
|
||||||
|
self._update_extra_state_and_group_info()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _prepare_subscribe_topics(self) -> None:
|
def _prepare_subscribe_topics(self) -> None:
|
||||||
"""(Re)Subscribe to topics."""
|
"""(Re)Subscribe to topics."""
|
||||||
|
@@ -82,6 +82,7 @@ light:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import call, patch
|
from unittest.mock import call, patch
|
||||||
|
|
||||||
@@ -169,6 +170,39 @@ COLOR_MODES_CONFIG = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GROUP_MEMBER_1_TOPIC = "homeassistant/light/member_1/config"
|
||||||
|
GROUP_MEMBER_2_TOPIC = "homeassistant/light/member_2/config"
|
||||||
|
GROUP_TOPIC = "homeassistant/light/group/config"
|
||||||
|
GROUP_DISCOVERY_MEMBER_1_CONFIG = json.dumps(
|
||||||
|
{
|
||||||
|
"schema": "json",
|
||||||
|
"command_topic": "test-command-topic-member1",
|
||||||
|
"unique_id": "very_unique_member1",
|
||||||
|
"name": "member1",
|
||||||
|
"default_entity_id": "light.member1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
GROUP_DISCOVERY_MEMBER_2_CONFIG = json.dumps(
|
||||||
|
{
|
||||||
|
"schema": "json",
|
||||||
|
"command_topic": "test-command-topic-member2",
|
||||||
|
"unique_id": "very_unique_member2",
|
||||||
|
"name": "member2",
|
||||||
|
"default_entity_id": "light.member2",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
GROUP_DISCOVERY_LIGHT_GROUP_CONFIG = json.dumps(
|
||||||
|
{
|
||||||
|
"schema": "json",
|
||||||
|
"command_topic": "test-command-topic-group",
|
||||||
|
"state_topic": "test-state-topic-group",
|
||||||
|
"unique_id": "very_unique_group",
|
||||||
|
"name": "group",
|
||||||
|
"default_entity_id": "light.group",
|
||||||
|
"group": ["very_unique_member1", "very_unique_member2"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class JsonValidator:
|
class JsonValidator:
|
||||||
"""Helper to compare JSON."""
|
"""Helper to compare JSON."""
|
||||||
@@ -1859,6 +1893,69 @@ async def test_white_scale(
|
|||||||
assert state.attributes.get("brightness") == 129
|
assert state.attributes.get("brightness") == 129
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light_group_discovery_members_before_group(
|
||||||
|
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test the discovery of a light group and linked entity IDs.
|
||||||
|
|
||||||
|
The members are discovered first, so they are known in the entity registry.
|
||||||
|
"""
|
||||||
|
await mqtt_mock_entry()
|
||||||
|
# Discover light group members
|
||||||
|
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, GROUP_DISCOVERY_MEMBER_1_CONFIG)
|
||||||
|
async_fire_mqtt_message(hass, GROUP_MEMBER_2_TOPIC, GROUP_DISCOVERY_MEMBER_2_CONFIG)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Discover group
|
||||||
|
async_fire_mqtt_message(hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("light.member1") is not None
|
||||||
|
assert hass.states.get("light.member2") is not None
|
||||||
|
group_state = hass.states.get("light.group")
|
||||||
|
assert group_state is not None
|
||||||
|
assert group_state.attributes.get("entity_id") == ["light.member1", "light.member2"]
|
||||||
|
assert group_state.attributes.get("icon") == "mdi:lightbulb-group"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light_group_discovery_group_before_members(
|
||||||
|
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test the discovery of a light group and linked entity IDs.
|
||||||
|
|
||||||
|
The group is discovered first, so the group members are
|
||||||
|
not (all) known yet in the entity registry.
|
||||||
|
"""
|
||||||
|
await mqtt_mock_entry()
|
||||||
|
|
||||||
|
# Discover group
|
||||||
|
async_fire_mqtt_message(hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Discover light group members
|
||||||
|
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, GROUP_DISCOVERY_MEMBER_1_CONFIG)
|
||||||
|
async_fire_mqtt_message(hass, GROUP_MEMBER_2_TOPIC, GROUP_DISCOVERY_MEMBER_2_CONFIG)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("light.member1") is not None
|
||||||
|
assert hass.states.get("light.member2") is not None
|
||||||
|
|
||||||
|
group_state = hass.states.get("light.group")
|
||||||
|
assert group_state is not None
|
||||||
|
# Members are not added yet, we need a group state update first
|
||||||
|
# to trigger a state update
|
||||||
|
assert not group_state.attributes.get("entity_id")
|
||||||
|
async_fire_mqtt_message(hass, "test-state-topic-group", '{"state": "ON"}')
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
group_state = hass.states.get("light.group")
|
||||||
|
assert group_state is not None
|
||||||
|
assert group_state.attributes.get("entity_id") == ["light.member1", "light.member2"]
|
||||||
|
assert group_state.attributes.get("icon") == "mdi:lightbulb-group"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
[
|
[
|
||||||
@@ -2040,7 +2137,7 @@ async def test_custom_availability_payload(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_setting_attribute_via_mqtt_json_message(
|
async def test_setting_attribute_via_mqtt_json_message_single_light(
|
||||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the setting of attribute via MQTT with JSON payload."""
|
"""Test the setting of attribute via MQTT with JSON payload."""
|
||||||
@@ -2049,6 +2146,52 @@ async def test_setting_attribute_via_mqtt_json_message(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"hass_config",
|
||||||
|
[
|
||||||
|
help_custom_config(
|
||||||
|
light.DOMAIN,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"unique_id": "very_unique_member_1",
|
||||||
|
"name": "Part 1",
|
||||||
|
"default_entity_id": "light.member_1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unique_id": "very_unique_member_2",
|
||||||
|
"name": "Part 2",
|
||||||
|
"default_entity_id": "light.member_2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unique_id": "very_unique_group",
|
||||||
|
"name": "My group",
|
||||||
|
"default_entity_id": "light.my_group",
|
||||||
|
"json_attributes_topic": "attr-topic",
|
||||||
|
"group": [
|
||||||
|
"very_unique_member_1",
|
||||||
|
"very_unique_member_2",
|
||||||
|
"member_3_not_exists",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_setting_attribute_via_mqtt_json_message_light_group(
|
||||||
|
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test the setting of attribute via MQTT with JSON payload."""
|
||||||
|
await mqtt_mock_entry()
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "attr-topic", '{ "val": "100" }')
|
||||||
|
state = hass.states.get("light.my_group")
|
||||||
|
|
||||||
|
assert state and state.attributes.get("val") == "100"
|
||||||
|
assert state.attributes.get("entity_id") == ["light.member_1", "light.member_2"]
|
||||||
|
assert state.attributes.get("icon") == "mdi:lightbulb-group"
|
||||||
|
|
||||||
|
|
||||||
async def test_setting_blocked_attribute_via_mqtt_json_message(
|
async def test_setting_blocked_attribute_via_mqtt_json_message(
|
||||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||||
) -> None:
|
) -> None:
|
||||||
|
Reference in New Issue
Block a user