Move QoS setting to shared device properties in MQTT device subentries configuration (#141369)

* Move QoS setting to shared device properties in MQTT device subentries configuration

* Use kwargs for validate_user_input helper
This commit is contained in:
Jan Bouwhuis 2025-03-26 15:20:08 +01:00 committed by GitHub
parent 63d4efda2e
commit 0de3549e6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 124 additions and 60 deletions

View File

@ -134,6 +134,7 @@ from .const import (
DEFAULT_PORT, DEFAULT_PORT,
DEFAULT_PREFIX, DEFAULT_PREFIX,
DEFAULT_PROTOCOL, DEFAULT_PROTOCOL,
DEFAULT_QOS,
DEFAULT_TRANSPORT, DEFAULT_TRANSPORT,
DEFAULT_WILL, DEFAULT_WILL,
DEFAULT_WS_PATH, DEFAULT_WS_PATH,
@ -368,10 +369,6 @@ COMMON_ENTITY_FIELDS = {
CONF_ENTITY_PICTURE: PlatformField(TEXT_SELECTOR, False, cv.url, "invalid_url"), CONF_ENTITY_PICTURE: PlatformField(TEXT_SELECTOR, False, cv.url, "invalid_url"),
} }
COMMON_MQTT_FIELDS = {
CONF_QOS: PlatformField(QOS_SELECTOR, False, valid_qos_schema, default=0),
}
PLATFORM_ENTITY_FIELDS = { PLATFORM_ENTITY_FIELDS = {
Platform.NOTIFY.value: {}, Platform.NOTIFY.value: {},
Platform.SENSOR.value: { Platform.SENSOR.value: {
@ -431,16 +428,17 @@ ENTITY_CONFIG_VALIDATOR: dict[
Platform.SENSOR.value: validate_sensor_platform_config, Platform.SENSOR.value: validate_sensor_platform_config,
} }
MQTT_DEVICE_SCHEMA = vol.Schema( MQTT_DEVICE_PLATFORM_FIELDS = {
{ ATTR_NAME: PlatformField(TEXT_SELECTOR, False, str),
vol.Required(ATTR_NAME): TEXT_SELECTOR, ATTR_SW_VERSION: PlatformField(TEXT_SELECTOR, False, str),
vol.Optional(ATTR_SW_VERSION): TEXT_SELECTOR, ATTR_HW_VERSION: PlatformField(TEXT_SELECTOR, False, str),
vol.Optional(ATTR_HW_VERSION): TEXT_SELECTOR, ATTR_MODEL: PlatformField(TEXT_SELECTOR, False, str),
vol.Optional(ATTR_MODEL): TEXT_SELECTOR, ATTR_MODEL_ID: PlatformField(TEXT_SELECTOR, False, str),
vol.Optional(ATTR_MODEL_ID): TEXT_SELECTOR, ATTR_CONFIGURATION_URL: PlatformField(TEXT_SELECTOR, False, cv.url, "invalid_url"),
vol.Optional(ATTR_CONFIGURATION_URL): TEXT_SELECTOR, CONF_QOS: PlatformField(
} QOS_SELECTOR, False, int, default=DEFAULT_QOS, section="mqtt_settings"
) ),
}
REAUTH_SCHEMA = vol.Schema( REAUTH_SCHEMA = vol.Schema(
{ {
@ -527,7 +525,8 @@ def calculate_merged_config(
def validate_user_input( def validate_user_input(
user_input: dict[str, Any], user_input: dict[str, Any],
data_schema_fields: dict[str, PlatformField], data_schema_fields: dict[str, PlatformField],
component_data: dict[str, Any] | None, *,
component_data: dict[str, Any] | None = None,
config_validator: Callable[[dict[str, Any]], dict[str, str]] | None = None, config_validator: Callable[[dict[str, Any]], dict[str, str]] | None = None,
) -> tuple[dict[str, Any], dict[str, str]]: ) -> tuple[dict[str, Any], dict[str, str]]:
"""Validate user input.""" """Validate user input."""
@ -566,11 +565,21 @@ def data_schema_from_fields(
reconfig: bool, reconfig: bool,
component_data: dict[str, Any] | None = None, component_data: dict[str, Any] | None = None,
user_input: dict[str, Any] | None = None, user_input: dict[str, Any] | None = None,
device_data: MqttDeviceData | None = None,
) -> vol.Schema: ) -> vol.Schema:
"""Generate custom data schema from platform fields.""" """Generate custom data schema from platform fields or device data."""
component_data_with_user_input = deepcopy(component_data) if device_data is not None:
component_data_with_user_input: dict[str, Any] | None = dict(device_data)
if TYPE_CHECKING:
assert component_data_with_user_input is not None
component_data_with_user_input.update(
component_data_with_user_input.pop("mqtt_settings", {})
)
else:
component_data_with_user_input = deepcopy(component_data)
if component_data_with_user_input is not None and user_input is not None: if component_data_with_user_input is not None and user_input is not None:
component_data_with_user_input |= user_input component_data_with_user_input |= user_input
sections: dict[str | None, None] = { sections: dict[str | None, None] = {
field_details.section: None for field_details in data_schema_fields.values() field_details.section: None for field_details in data_schema_fields.values()
} }
@ -1221,17 +1230,26 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult: ) -> SubentryFlowResult:
"""Add a new MQTT device.""" """Add a new MQTT device."""
errors: dict[str, str] = {} errors: dict[str, Any] = {}
validate_field("configuration_url", cv.url, user_input, errors, "invalid_url") device_data = self._subentry_data[CONF_DEVICE]
if not errors and user_input is not None: data_schema = data_schema_from_fields(
self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, user_input) MQTT_DEVICE_PLATFORM_FIELDS,
if self.source == SOURCE_RECONFIGURE: device_data=device_data,
return await self.async_step_summary_menu() reconfig=True,
return await self.async_step_entity() )
if user_input is not None:
merged_user_input, errors = validate_user_input(
user_input, MQTT_DEVICE_PLATFORM_FIELDS
)
if not errors:
self._subentry_data[CONF_DEVICE] = cast(
MqttDeviceData, merged_user_input
)
if self.source == SOURCE_RECONFIGURE:
return await self.async_step_summary_menu()
return await self.async_step_entity()
data_schema = self.add_suggested_values_to_schema( data_schema = self.add_suggested_values_to_schema(
MQTT_DEVICE_SCHEMA, data_schema, device_data if user_input is None else user_input
self._subentry_data[CONF_DEVICE] if user_input is None else user_input,
) )
return self.async_show_form( return self.async_show_form(
step_id=CONF_DEVICE, step_id=CONF_DEVICE,
@ -1257,7 +1275,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
data_schema = data_schema_from_fields(data_schema_fields, reconfig=reconfig) data_schema = data_schema_from_fields(data_schema_fields, reconfig=reconfig)
if user_input is not None: if user_input is not None:
merged_user_input, errors = validate_user_input( merged_user_input, errors = validate_user_input(
user_input, data_schema_fields, component_data user_input, data_schema_fields, component_data=component_data
) )
if not errors: if not errors:
if self._component_id is None: if self._component_id is None:
@ -1357,8 +1375,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
merged_user_input, errors = validate_user_input( merged_user_input, errors = validate_user_input(
user_input, user_input,
data_schema_fields, data_schema_fields,
component_data, component_data=component_data,
ENTITY_CONFIG_VALIDATOR[platform], config_validator=ENTITY_CONFIG_VALIDATOR[platform],
) )
if not errors: if not errors:
self.update_component_fields(data_schema_fields, merged_user_input) self.update_component_fields(data_schema_fields, merged_user_input)
@ -1395,7 +1413,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
assert self._component_id is not None assert self._component_id is not None
component_data = self._subentry_data["components"][self._component_id] component_data = self._subentry_data["components"][self._component_id]
platform = component_data[CONF_PLATFORM] platform = component_data[CONF_PLATFORM]
data_schema_fields = PLATFORM_MQTT_FIELDS[platform] | COMMON_MQTT_FIELDS data_schema_fields = PLATFORM_MQTT_FIELDS[platform]
data_schema = data_schema_from_fields( data_schema = data_schema_from_fields(
data_schema_fields, data_schema_fields,
reconfig=bool( reconfig=bool(
@ -1408,8 +1426,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
merged_user_input, errors = validate_user_input( merged_user_input, errors = validate_user_input(
user_input, user_input,
data_schema_fields, data_schema_fields,
component_data, component_data=component_data,
ENTITY_CONFIG_VALIDATOR[platform], config_validator=ENTITY_CONFIG_VALIDATOR[platform],
) )
if not errors: if not errors:
self.update_component_fields(data_schema_fields, merged_user_input) self.update_component_fields(data_schema_fields, merged_user_input)

View File

@ -300,6 +300,7 @@ def async_setup_entity_entry_helper(
availability_config = subentry_data.get("availability", {}) availability_config = subentry_data.get("availability", {})
subentry_entities: list[Entity] = [] subentry_entities: list[Entity] = []
device_config = subentry_data["device"].copy() device_config = subentry_data["device"].copy()
device_mqtt_options = device_config.pop("mqtt_settings", {})
device_config["identifiers"] = config_subentry_id device_config["identifiers"] = config_subentry_id
for component_id, component_data in subentry_data["components"].items(): for component_id, component_data in subentry_data["components"].items():
if component_data["platform"] != domain: if component_data["platform"] != domain:
@ -311,6 +312,7 @@ def async_setup_entity_entry_helper(
component_config[CONF_DEVICE] = device_config component_config[CONF_DEVICE] = device_config
component_config.pop("platform") component_config.pop("platform")
component_config.update(availability_config) component_config.update(availability_config)
component_config.update(device_mqtt_options)
try: try:
config = platform_schema_modern(component_config) config = platform_schema_modern(component_config)

View File

@ -420,6 +420,12 @@ class MqttComponentConfig:
discovery_payload: MQTTDiscoveryPayload discovery_payload: MQTTDiscoveryPayload
class DeviceMqttOptions(TypedDict, total=False):
"""Hold the shared MQTT specific options for an MQTT device."""
qos: int
class MqttDeviceData(TypedDict, total=False): class MqttDeviceData(TypedDict, total=False):
"""Hold the data for an MQTT device.""" """Hold the data for an MQTT device."""
@ -430,6 +436,7 @@ class MqttDeviceData(TypedDict, total=False):
hw_version: str hw_version: str
model: str model: str
model_id: str model_id: str
mqtt_settings: DeviceMqttOptions
class MqttAvailabilityData(TypedDict, total=False): class MqttAvailabilityData(TypedDict, total=False):

View File

@ -150,6 +150,17 @@
"hw_version": "The hardware version of the device. E.g. 'v1.0 rev a'.", "hw_version": "The hardware version of the device. E.g. 'v1.0 rev a'.",
"model": "E.g. 'Cleanmaster Pro'.", "model": "E.g. 'Cleanmaster Pro'.",
"model_id": "E.g. '123NK2PRO'." "model_id": "E.g. '123NK2PRO'."
},
"sections": {
"mqtt_settings": {
"name": "MQTT Settings",
"data": {
"qos": "QoS"
},
"data_description": {
"qos": "The QoS value the device's entities should use."
}
}
} }
}, },
"summary_menu": { "summary_menu": {
@ -235,8 +246,7 @@
"value_template": "Value template", "value_template": "Value template",
"last_reset_value_template": "Last reset value template", "last_reset_value_template": "Last reset value template",
"force_update": "Force update", "force_update": "Force update",
"retain": "Retain", "retain": "Retain"
"qos": "QoS"
}, },
"data_description": { "data_description": {
"command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)", "command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)",
@ -245,8 +255,7 @@
"value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value.", "value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value.",
"last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)", "last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)",
"force_update": "Sends update events even if the value hasnt changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", "force_update": "Sends update events even if the value hasnt changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)",
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.", "retain": "Select if values published by the {platform} entity should be retained at the MQTT broker."
"qos": "The QoS value {platform} entity should use."
}, },
"sections": { "sections": {
"advanced_settings": { "advanced_settings": {

View File

@ -70,7 +70,6 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT1 = {
"363a7ecad6be4a19b939a016ea93e994": { "363a7ecad6be4a19b939a016ea93e994": {
"platform": "notify", "platform": "notify",
"name": "Milkman alert", "name": "Milkman alert",
"qos": 0,
"command_topic": "test-topic", "command_topic": "test-topic",
"command_template": "{{ value }}", "command_template": "{{ value }}",
"entity_picture": "https://example.com/363a7ecad6be4a19b939a016ea93e994", "entity_picture": "https://example.com/363a7ecad6be4a19b939a016ea93e994",
@ -81,7 +80,6 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT2 = {
"6494827dac294fa0827c54b02459d309": { "6494827dac294fa0827c54b02459d309": {
"platform": "notify", "platform": "notify",
"name": "The second notifier", "name": "The second notifier",
"qos": 0,
"command_topic": "test-topic2", "command_topic": "test-topic2",
"entity_picture": "https://example.com/6494827dac294fa0827c54b02459d309", "entity_picture": "https://example.com/6494827dac294fa0827c54b02459d309",
}, },
@ -89,7 +87,6 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT2 = {
MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME = { MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME = {
"5269352dd9534c908d22812ea5d714cd": { "5269352dd9534c908d22812ea5d714cd": {
"platform": "notify", "platform": "notify",
"qos": 0,
"command_topic": "test-topic", "command_topic": "test-topic",
"command_template": "{{ value }}", "command_template": "{{ value }}",
"entity_picture": "https://example.com/5269352dd9534c908d22812ea5d714cd", "entity_picture": "https://example.com/5269352dd9534c908d22812ea5d714cd",
@ -102,7 +99,6 @@ MOCK_SUBENTRY_SENSOR_COMPONENT = {
"platform": "sensor", "platform": "sensor",
"name": "Energy", "name": "Energy",
"device_class": "enum", "device_class": "enum",
"qos": 1,
"state_topic": "test-topic", "state_topic": "test-topic",
"options": ["low", "medium", "high"], "options": ["low", "medium", "high"],
"expire_after": 30, "expire_after": 30,
@ -117,7 +113,6 @@ MOCK_SUBENTRY_SENSOR_COMPONENT_STATE_CLASS = {
"state_class": "measurement", "state_class": "measurement",
"state_topic": "test-topic", "state_topic": "test-topic",
"entity_picture": "https://example.com/a0f85790a95d4889924602effff06b6e", "entity_picture": "https://example.com/a0f85790a95d4889924602effff06b6e",
"qos": 0,
}, },
} }
MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET = { MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET = {
@ -128,7 +123,6 @@ MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET = {
"last_reset_value_template": "{{ value_json.value }}", "last_reset_value_template": "{{ value_json.value }}",
"state_topic": "test-topic", "state_topic": "test-topic",
"entity_picture": "https://example.com/e9261f6feed443e7b7d5f3fbe2a47412", "entity_picture": "https://example.com/e9261f6feed443e7b7d5f3fbe2a47412",
"qos": 0,
}, },
} }
@ -139,7 +133,6 @@ MOCK_SUBENTRY_LIGHT_COMPONENT = {
"8131babc5e8d4f44b82e0761d39091a2": { "8131babc5e8d4f44b82e0761d39091a2": {
"platform": "light", "platform": "light",
"name": "Test light", "name": "Test light",
"qos": 1,
"command_topic": "test-topic4", "command_topic": "test-topic4",
"schema": "basic", "schema": "basic",
"entity_picture": "https://example.com/8131babc5e8d4f44b82e0761d39091a2", "entity_picture": "https://example.com/8131babc5e8d4f44b82e0761d39091a2",
@ -149,7 +142,6 @@ MOCK_SUBENTRY_NOTIFY_BAD_SCHEMA = {
"b10b531e15244425a74bb0abb1e9d2c6": { "b10b531e15244425a74bb0abb1e9d2c6": {
"platform": "notify", "platform": "notify",
"name": "Test", "name": "Test",
"qos": 1,
"command_topic": "bad#topic", "command_topic": "bad#topic",
}, },
} }
@ -183,6 +175,7 @@ MOCK_NOTIFY_SUBENTRY_DATA_SINGLE = {
"model": "Model XL", "model": "Model XL",
"model_id": "mn002", "model_id": "mn002",
"configuration_url": "https://example.com", "configuration_url": "https://example.com",
"mqtt_settings": {"qos": 1},
}, },
"components": MOCK_SUBENTRY_NOTIFY_COMPONENT1, "components": MOCK_SUBENTRY_NOTIFY_COMPONENT1,
} }

View File

@ -2616,6 +2616,7 @@ async def test_migrate_of_incompatible_config_entry(
@pytest.mark.parametrize( @pytest.mark.parametrize(
( (
"config_subentries_data", "config_subentries_data",
"mock_device_user_input",
"mock_entity_user_input", "mock_entity_user_input",
"mock_entity_details_user_input", "mock_entity_details_user_input",
"mock_entity_details_failed_user_input", "mock_entity_details_failed_user_input",
@ -2626,13 +2627,13 @@ async def test_migrate_of_incompatible_config_entry(
[ [
( (
MOCK_NOTIFY_SUBENTRY_DATA_SINGLE, MOCK_NOTIFY_SUBENTRY_DATA_SINGLE,
{"name": "Milk notifier", "mqtt_settings": {"qos": 1}},
{"name": "Milkman alert"}, {"name": "Milkman alert"},
None, None,
None, None,
{ {
"command_topic": "test-topic", "command_topic": "test-topic",
"command_template": "{{ value }}", "command_template": "{{ value }}",
"qos": 0,
"retain": False, "retain": False,
}, },
( (
@ -2645,13 +2646,13 @@ async def test_migrate_of_incompatible_config_entry(
), ),
( (
MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME, MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
{}, {},
None, None,
None, None,
{ {
"command_topic": "test-topic", "command_topic": "test-topic",
"command_template": "{{ value }}", "command_template": "{{ value }}",
"qos": 0,
"retain": False, "retain": False,
}, },
( (
@ -2664,6 +2665,7 @@ async def test_migrate_of_incompatible_config_entry(
), ),
( (
MOCK_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_SENSOR_SUBENTRY_DATA_SINGLE,
{"name": "Test sensor", "mqtt_settings": {"qos": 0}},
{"name": "Energy"}, {"name": "Energy"},
{"device_class": "enum", "options": ["low", "medium", "high"]}, {"device_class": "enum", "options": ["low", "medium", "high"]},
( (
@ -2708,7 +2710,6 @@ async def test_migrate_of_incompatible_config_entry(
"state_topic": "test-topic", "state_topic": "test-topic",
"value_template": "{{ value_json.value }}", "value_template": "{{ value_json.value }}",
"advanced_settings": {"expire_after": 30}, "advanced_settings": {"expire_after": 30},
"qos": 1,
}, },
( (
( (
@ -2720,6 +2721,7 @@ async def test_migrate_of_incompatible_config_entry(
), ),
( (
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS, MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS,
{"name": "Test sensor", "mqtt_settings": {"qos": 0}},
{"name": "Energy"}, {"name": "Energy"},
{ {
"state_class": "measurement", "state_class": "measurement",
@ -2743,6 +2745,7 @@ async def test_subentry_configflow(
hass: HomeAssistant, hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_mock_entry: MqttMockHAClientGenerator,
config_subentries_data: dict[str, Any], config_subentries_data: dict[str, Any],
mock_device_user_input: dict[str, Any],
mock_entity_user_input: dict[str, Any], mock_entity_user_input: dict[str, Any],
mock_entity_details_user_input: dict[str, Any], mock_entity_details_user_input: dict[str, Any],
mock_entity_details_failed_user_input: tuple[ mock_entity_details_failed_user_input: tuple[
@ -2753,7 +2756,7 @@ async def test_subentry_configflow(
entity_name: str, entity_name: str,
) -> None: ) -> None:
"""Test the subentry ConfigFlow.""" """Test the subentry ConfigFlow."""
device_name = config_subentries_data["device"]["name"] device_name = mock_device_user_input["name"]
component = next(iter(config_subentries_data["components"].values())) component = next(iter(config_subentries_data["components"].values()))
await mqtt_mock_entry() await mqtt_mock_entry()
@ -2780,14 +2783,7 @@ async def test_subentry_configflow(
result = await hass.config_entries.subentries.async_configure( result = await hass.config_entries.subentries.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input=mock_device_user_input,
"name": device_name,
"sw_version": "1.0",
"hw_version": "2.1 rev a",
"model": "Model XL",
"model_id": "mn002",
"configuration_url": "https://example.com",
},
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "entity" assert result["step_id"] == "entity"
@ -3471,7 +3467,6 @@ async def test_subentry_reconfigure_edit_entity_reset_fields(
}, },
{ {
"command_topic": "test-topic2", "command_topic": "test-topic2",
"qos": 0,
}, },
) )
], ],

View File

@ -1,7 +1,7 @@
"""The tests for shared code of the MQTT platform.""" """The tests for shared code of the MQTT platform."""
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import call, patch
import pytest import pytest
@ -21,7 +21,11 @@ from homeassistant.helpers import (
) )
from homeassistant.util import slugify from homeassistant.util import slugify
from .common import MOCK_SUBENTRY_DATA_BAD_COMPONENT_SCHEMA, MOCK_SUBENTRY_DATA_SET_MIX from .common import (
MOCK_NOTIFY_SUBENTRY_DATA_SINGLE,
MOCK_SUBENTRY_DATA_BAD_COMPONENT_SCHEMA,
MOCK_SUBENTRY_DATA_SET_MIX,
)
from tests.common import MockConfigEntry, async_capture_events, async_fire_mqtt_message from tests.common import MockConfigEntry, async_capture_events, async_fire_mqtt_message
from tests.typing import MqttMockHAClientGenerator from tests.typing import MqttMockHAClientGenerator
@ -547,3 +551,39 @@ async def test_loading_subentry_with_bad_component_schema(
"Schema violation occurred when trying to set up entity from subentry" "Schema violation occurred when trying to set up entity from subentry"
in caplog.text in caplog.text
) )
@pytest.mark.parametrize(
"mqtt_config_subentries_data",
[
(
ConfigSubentryData(
data=MOCK_NOTIFY_SUBENTRY_DATA_SINGLE,
subentry_type="device",
title="Mock subentry",
),
)
],
)
async def test_qos_on_mqt_device_from_subentry(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
mqtt_config_subentries_data: tuple[dict[str, Any]],
device_registry: dr.DeviceRegistry,
) -> None:
"""Test QoS is set correctly on entities from MQTT device."""
mqtt_mock = await mqtt_mock_entry()
entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
subentry_id = next(iter(entry.subentries))
# Each subentry has one device
device = device_registry.async_get_device({("mqtt", subentry_id)})
assert device is not None
assert hass.states.get("notify.milk_notifier_milkman_alert") is not None
await hass.services.async_call(
"notify",
"send_message",
{"entity_id": "notify.milk_notifier_milkman_alert", "message": "Test message"},
)
await hass.async_block_till_done()
assert len(mqtt_mock.async_publish.mock_calls) == 1
mqtt_mock.async_publish.mock_calls[0] = call("test-topic", "Test message", 1, False)