mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add sensor as entity platform on MQTT subentries (#139899)
* Add sensor as entity platform on MQTT subentries * Fix typo * Improve device class data description * Tweak * Rework reconfig calculation * Filter out last_reset_value_template if state class is not total * Collapse expire after as advanced setting * Update suggested_display_precision translation strings * Make options and last_reset_template conditional, use sections for advanced settings * Ensure options are removed properly * Improve sensor options label, ensure UOM is set when device class has units * Use helper to apply suggested values from component config * Rename to `Add option` * Fix schema builder not hiding empty sections and removing fields excluded from reconfig * Do not hide advanced settings if values are available or are defaults * Improve spelling and Learn more links * Improve unit of measurement validation * Fix UOM selector and translation strings * Address comments from code review * Remove stale comment * Rename selector constant, split validator * Simplify config validator * Return tuple with config and errors for config validation
This commit is contained in:
parent
3f68e327f3
commit
77bf977d63
@ -27,6 +27,12 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.file_upload import process_uploaded_file
|
||||
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASS_UNITS,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigEntry,
|
||||
@ -45,6 +51,7 @@ from homeassistant.const import (
|
||||
ATTR_SW_VERSION,
|
||||
CONF_CLIENT_ID,
|
||||
CONF_DEVICE,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_DISCOVERY,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
@ -53,10 +60,12 @@ from homeassistant.const import (
|
||||
CONF_PLATFORM,
|
||||
CONF_PORT,
|
||||
CONF_PROTOCOL,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_USERNAME,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.data_entry_flow import AbortFlow, SectionConfig, section
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
@ -99,11 +108,16 @@ from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_DISCOVERY_PREFIX,
|
||||
CONF_ENTITY_PICTURE,
|
||||
CONF_EXPIRE_AFTER,
|
||||
CONF_KEEPALIVE,
|
||||
CONF_LAST_RESET_VALUE_TEMPLATE,
|
||||
CONF_OPTIONS,
|
||||
CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_SUGGESTED_DISPLAY_PRECISION,
|
||||
CONF_TLS_INSECURE,
|
||||
CONF_TRANSPORT,
|
||||
CONF_WILL_MESSAGE,
|
||||
@ -133,6 +147,7 @@ from .models import MqttAvailabilityData, MqttDeviceData, MqttSubentryData
|
||||
from .util import (
|
||||
async_create_certificate_temp_files,
|
||||
get_file_path,
|
||||
learn_more_url,
|
||||
valid_birth_will,
|
||||
valid_publish_topic,
|
||||
valid_qos_schema,
|
||||
@ -217,7 +232,7 @@ KEY_UPLOAD_SELECTOR = FileSelector(
|
||||
)
|
||||
|
||||
# Subentry selectors
|
||||
SUBENTRY_PLATFORMS = [Platform.NOTIFY]
|
||||
SUBENTRY_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
|
||||
SUBENTRY_PLATFORM_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[platform.value for platform in SUBENTRY_PLATFORMS],
|
||||
@ -225,7 +240,6 @@ SUBENTRY_PLATFORM_SELECTOR = SelectSelector(
|
||||
translation_key=CONF_PLATFORM,
|
||||
)
|
||||
)
|
||||
|
||||
TEMPLATE_SELECTOR = TemplateSelector(TemplateSelectorConfig())
|
||||
|
||||
SUBENTRY_AVAILABILITY_SCHEMA = vol.Schema(
|
||||
@ -241,17 +255,109 @@ SUBENTRY_AVAILABILITY_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
# Sensor specific selectors
|
||||
SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[device_class.value for device_class in SensorDeviceClass],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="device_class_sensor",
|
||||
sort=True,
|
||||
)
|
||||
)
|
||||
SENSOR_STATE_CLASS_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[device_class.value for device_class in SensorStateClass],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key=CONF_STATE_CLASS,
|
||||
)
|
||||
)
|
||||
OPTIONS_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[],
|
||||
custom_value=True,
|
||||
multiple=True,
|
||||
)
|
||||
)
|
||||
SUGGESTED_DISPLAY_PRECISION_SELECTOR = NumberSelector(
|
||||
NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=9)
|
||||
)
|
||||
EXPIRE_AFTER_SELECTOR = NumberSelector(
|
||||
NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0)
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def validate_sensor_platform_config(
|
||||
config: dict[str, Any],
|
||||
) -> dict[str, str]:
|
||||
"""Validate the sensor options, state and device class config."""
|
||||
errors: dict[str, str] = {}
|
||||
# Only allow `options` to be set for `enum` sensors
|
||||
# to limit the possible sensor values
|
||||
if config.get(CONF_OPTIONS) is not None:
|
||||
if config.get(CONF_STATE_CLASS) or config.get(CONF_UNIT_OF_MEASUREMENT):
|
||||
errors[CONF_OPTIONS] = "options_not_allowed_with_state_class_or_uom"
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) != SensorDeviceClass.ENUM:
|
||||
errors[CONF_DEVICE_CLASS] = "options_device_class_enum"
|
||||
|
||||
if (
|
||||
(device_class := config.get(CONF_DEVICE_CLASS)) == SensorDeviceClass.ENUM
|
||||
and errors is not None
|
||||
and CONF_OPTIONS not in config
|
||||
):
|
||||
errors[CONF_OPTIONS] = "options_with_enum_device_class"
|
||||
|
||||
if (
|
||||
device_class in DEVICE_CLASS_UNITS
|
||||
and (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is None
|
||||
and errors is not None
|
||||
):
|
||||
# Do not allow an empty unit of measurement in a subentry data flow
|
||||
errors[CONF_UNIT_OF_MEASUREMENT] = "uom_required_for_device_class"
|
||||
return errors
|
||||
|
||||
if (
|
||||
device_class is not None
|
||||
and device_class in DEVICE_CLASS_UNITS
|
||||
and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
|
||||
):
|
||||
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlatformField:
|
||||
"""Stores a platform config field schema, required flag and validator."""
|
||||
|
||||
selector: Selector
|
||||
selector: Selector[Any] | Callable[..., Selector[Any]]
|
||||
required: bool
|
||||
validator: Callable[..., Any]
|
||||
error: str | None = None
|
||||
default: str | int | vol.Undefined = vol.UNDEFINED
|
||||
exclude_from_reconfig: bool = False
|
||||
conditions: tuple[dict[str, Any], ...] | None = None
|
||||
custom_filtering: bool = False
|
||||
section: str | None = None
|
||||
|
||||
|
||||
@callback
|
||||
def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
|
||||
"""Return a context based unit of measurement selector."""
|
||||
if (
|
||||
user_data is None
|
||||
or (device_class := user_data.get(CONF_DEVICE_CLASS)) is None
|
||||
or device_class not in DEVICE_CLASS_UNITS
|
||||
):
|
||||
return TEXT_SELECTOR
|
||||
return SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[str(uom) for uom in DEVICE_CLASS_UNITS[device_class]],
|
||||
sort=True,
|
||||
custom_value=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
COMMON_ENTITY_FIELDS = {
|
||||
@ -264,7 +370,29 @@ COMMON_ENTITY_FIELDS = {
|
||||
|
||||
COMMON_MQTT_FIELDS = {
|
||||
CONF_QOS: PlatformField(QOS_SELECTOR, False, valid_qos_schema, default=0),
|
||||
CONF_RETAIN: PlatformField(BOOLEAN_SELECTOR, False, bool),
|
||||
}
|
||||
|
||||
PLATFORM_ENTITY_FIELDS = {
|
||||
Platform.NOTIFY.value: {},
|
||||
Platform.SENSOR.value: {
|
||||
CONF_DEVICE_CLASS: PlatformField(SENSOR_DEVICE_CLASS_SELECTOR, False, str),
|
||||
CONF_STATE_CLASS: PlatformField(SENSOR_STATE_CLASS_SELECTOR, False, str),
|
||||
CONF_UNIT_OF_MEASUREMENT: PlatformField(
|
||||
unit_of_measurement_selector, False, str, custom_filtering=True
|
||||
),
|
||||
CONF_SUGGESTED_DISPLAY_PRECISION: PlatformField(
|
||||
SUGGESTED_DISPLAY_PRECISION_SELECTOR,
|
||||
False,
|
||||
cv.positive_int,
|
||||
section="advanced_settings",
|
||||
),
|
||||
CONF_OPTIONS: PlatformField(
|
||||
OPTIONS_SELECTOR,
|
||||
False,
|
||||
cv.ensure_list,
|
||||
conditions=({"device_class": "enum"},),
|
||||
),
|
||||
},
|
||||
}
|
||||
PLATFORM_MQTT_FIELDS = {
|
||||
Platform.NOTIFY.value: {
|
||||
@ -274,7 +402,33 @@ PLATFORM_MQTT_FIELDS = {
|
||||
CONF_COMMAND_TEMPLATE: PlatformField(
|
||||
TEMPLATE_SELECTOR, False, cv.template, "invalid_template"
|
||||
),
|
||||
CONF_RETAIN: PlatformField(BOOLEAN_SELECTOR, False, bool),
|
||||
},
|
||||
Platform.SENSOR.value: {
|
||||
CONF_STATE_TOPIC: PlatformField(
|
||||
TEXT_SELECTOR, True, valid_subscribe_topic, "invalid_subscribe_topic"
|
||||
),
|
||||
CONF_VALUE_TEMPLATE: PlatformField(
|
||||
TEMPLATE_SELECTOR, False, cv.template, "invalid_template"
|
||||
),
|
||||
CONF_LAST_RESET_VALUE_TEMPLATE: PlatformField(
|
||||
TEMPLATE_SELECTOR,
|
||||
False,
|
||||
cv.template,
|
||||
"invalid_template",
|
||||
conditions=({CONF_STATE_CLASS: "total"},),
|
||||
),
|
||||
CONF_EXPIRE_AFTER: PlatformField(
|
||||
EXPIRE_AFTER_SELECTOR, False, cv.positive_int, section="advanced_settings"
|
||||
),
|
||||
},
|
||||
}
|
||||
ENTITY_CONFIG_VALIDATOR: dict[
|
||||
str,
|
||||
Callable[[dict[str, Any]], dict[str, str]] | None,
|
||||
] = {
|
||||
Platform.NOTIFY.value: None,
|
||||
Platform.SENSOR.value: validate_sensor_platform_config,
|
||||
}
|
||||
|
||||
MQTT_DEVICE_SCHEMA = vol.Schema(
|
||||
@ -337,38 +491,140 @@ def validate_field(
|
||||
errors[field] = error
|
||||
|
||||
|
||||
@callback
|
||||
def _check_conditions(
|
||||
platform_field: PlatformField, component_data: dict[str, Any] | None = None
|
||||
) -> bool:
|
||||
"""Only include field if one of conditions match, or no conditions are set."""
|
||||
if platform_field.conditions is None or component_data is None:
|
||||
return True
|
||||
return any(
|
||||
all(component_data.get(key) == value for key, value in condition.items())
|
||||
for condition in platform_field.conditions
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def calculate_merged_config(
|
||||
merged_user_input: dict[str, Any],
|
||||
data_schema_fields: dict[str, PlatformField],
|
||||
component_data: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Calculate merged config."""
|
||||
base_schema_fields = {
|
||||
key
|
||||
for key, platform_field in data_schema_fields.items()
|
||||
if _check_conditions(platform_field, component_data)
|
||||
} - set(merged_user_input)
|
||||
return {
|
||||
key: value
|
||||
for key, value in component_data.items()
|
||||
if key not in base_schema_fields
|
||||
} | merged_user_input
|
||||
|
||||
|
||||
@callback
|
||||
def validate_user_input(
|
||||
user_input: dict[str, Any],
|
||||
data_schema_fields: dict[str, PlatformField],
|
||||
errors: dict[str, str],
|
||||
) -> None:
|
||||
component_data: dict[str, Any] | None,
|
||||
config_validator: Callable[[dict[str, Any]], dict[str, str]] | None = None,
|
||||
) -> tuple[dict[str, Any], dict[str, str]]:
|
||||
"""Validate user input."""
|
||||
for field, value in user_input.items():
|
||||
errors: dict[str, str] = {}
|
||||
# Merge sections
|
||||
merged_user_input: dict[str, Any] = {}
|
||||
for key, value in user_input.items():
|
||||
if isinstance(value, dict):
|
||||
merged_user_input.update(value)
|
||||
else:
|
||||
merged_user_input[key] = value
|
||||
|
||||
for field, value in merged_user_input.items():
|
||||
validator = data_schema_fields[field].validator
|
||||
try:
|
||||
validator(value)
|
||||
except (ValueError, vol.Invalid):
|
||||
errors[field] = data_schema_fields[field].error or "invalid_input"
|
||||
|
||||
if config_validator is not None:
|
||||
if TYPE_CHECKING:
|
||||
assert component_data is not None
|
||||
|
||||
errors |= config_validator(
|
||||
calculate_merged_config(
|
||||
merged_user_input, data_schema_fields, component_data
|
||||
),
|
||||
)
|
||||
|
||||
return merged_user_input, errors
|
||||
|
||||
|
||||
@callback
|
||||
def data_schema_from_fields(
|
||||
data_schema_fields: dict[str, PlatformField],
|
||||
reconfig: bool,
|
||||
component_data: dict[str, Any] | None = None,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> vol.Schema:
|
||||
"""Generate data schema from platform fields."""
|
||||
return vol.Schema(
|
||||
{
|
||||
"""Generate custom data schema from platform fields."""
|
||||
component_data_with_user_input = deepcopy(component_data)
|
||||
if component_data_with_user_input is not None and user_input is not None:
|
||||
component_data_with_user_input |= user_input
|
||||
sections: dict[str | None, None] = {
|
||||
field_details.section: None for field_details in data_schema_fields.values()
|
||||
}
|
||||
data_schema: dict[Any, Any] = {}
|
||||
all_data_element_options: set[Any] = set()
|
||||
no_reconfig_options: set[Any] = set()
|
||||
for schema_section in sections:
|
||||
data_schema_element = {
|
||||
vol.Required(field_name, default=field_details.default)
|
||||
if field_details.required
|
||||
else vol.Optional(
|
||||
field_name, default=field_details.default
|
||||
): field_details.selector
|
||||
): field_details.selector(component_data_with_user_input) # type: ignore[operator]
|
||||
if field_details.custom_filtering
|
||||
else field_details.selector
|
||||
for field_name, field_details in data_schema_fields.items()
|
||||
if not field_details.exclude_from_reconfig or not reconfig
|
||||
if field_details.section == schema_section
|
||||
and (not field_details.exclude_from_reconfig or not reconfig)
|
||||
and _check_conditions(field_details, component_data_with_user_input)
|
||||
}
|
||||
)
|
||||
data_element_options = set(data_schema_element)
|
||||
all_data_element_options |= data_element_options
|
||||
no_reconfig_options |= {
|
||||
field_name
|
||||
for field_name, field_details in data_schema_fields.items()
|
||||
if field_details.section == schema_section
|
||||
and field_details.exclude_from_reconfig
|
||||
}
|
||||
if schema_section is None:
|
||||
data_schema.update(data_schema_element)
|
||||
continue
|
||||
collapsed = (
|
||||
not any(
|
||||
(default := data_schema_fields[str(option)].default) is vol.UNDEFINED
|
||||
or component_data_with_user_input[str(option)] != default
|
||||
for option in data_element_options
|
||||
if option in component_data_with_user_input
|
||||
)
|
||||
if component_data_with_user_input is not None
|
||||
else True
|
||||
)
|
||||
data_schema[vol.Optional(schema_section)] = section(
|
||||
vol.Schema(data_schema_element), SectionConfig({"collapsed": collapsed})
|
||||
)
|
||||
|
||||
# Reset all fields from the component_data not in the schema
|
||||
if component_data:
|
||||
filtered_fields = (
|
||||
set(data_schema_fields) - all_data_element_options - no_reconfig_options
|
||||
)
|
||||
for field in filtered_fields:
|
||||
if field in component_data:
|
||||
del component_data[field]
|
||||
return vol.Schema(data_schema)
|
||||
|
||||
|
||||
class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@ -893,20 +1149,56 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
@callback
|
||||
def update_component_fields(
|
||||
self, data_schema: vol.Schema, user_input: dict[str, Any]
|
||||
self,
|
||||
data_schema_fields: dict[str, PlatformField],
|
||||
merged_user_input: dict[str, Any],
|
||||
) -> None:
|
||||
"""Update the componment fields."""
|
||||
if TYPE_CHECKING:
|
||||
assert self._component_id is not None
|
||||
component_data = self._subentry_data["components"][self._component_id]
|
||||
# Remove the fields from the component data if they are not in the user input
|
||||
for field in [
|
||||
form_field
|
||||
for form_field in data_schema.schema
|
||||
if form_field in component_data and form_field not in user_input
|
||||
]:
|
||||
# Remove the fields from the component data
|
||||
# if they are not in the schema and not in the user input
|
||||
config = calculate_merged_config(
|
||||
merged_user_input, data_schema_fields, component_data
|
||||
)
|
||||
for field in (
|
||||
field
|
||||
for field, platform_field in data_schema_fields.items()
|
||||
if field in (set(component_data) - set(config))
|
||||
and not platform_field.exclude_from_reconfig
|
||||
):
|
||||
component_data.pop(field)
|
||||
component_data.update(user_input)
|
||||
component_data.update(merged_user_input)
|
||||
|
||||
@callback
|
||||
def generate_names(self) -> tuple[str, str]:
|
||||
"""Generate the device and full entity name."""
|
||||
if TYPE_CHECKING:
|
||||
assert self._component_id is not None
|
||||
device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
|
||||
if entity_name := self._subentry_data["components"][self._component_id].get(
|
||||
CONF_NAME
|
||||
):
|
||||
full_entity_name: str = f"{device_name} {entity_name}"
|
||||
else:
|
||||
full_entity_name = device_name
|
||||
return device_name, full_entity_name
|
||||
|
||||
@callback
|
||||
def get_suggested_values_from_component(
|
||||
self, data_schema: vol.Schema
|
||||
) -> dict[str, Any]:
|
||||
"""Get suggestions from component data based on the data schema."""
|
||||
if TYPE_CHECKING:
|
||||
assert self._component_id is not None
|
||||
component_data = self._subentry_data["components"][self._component_id]
|
||||
return {
|
||||
field_key: self.get_suggested_values_from_component(value.schema)
|
||||
if isinstance(value, section)
|
||||
else component_data.get(field_key)
|
||||
for field_key, value in data_schema.schema.items()
|
||||
}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@ -956,25 +1248,28 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
data_schema_fields = COMMON_ENTITY_FIELDS
|
||||
entity_name_label: str = ""
|
||||
platform_label: str = ""
|
||||
component_data: dict[str, Any] | None = None
|
||||
if reconfig := (self._component_id is not None):
|
||||
name: str | None = self._subentry_data["components"][
|
||||
self._component_id
|
||||
].get(CONF_NAME)
|
||||
component_data = self._subentry_data["components"][self._component_id]
|
||||
name: str | None = component_data.get(CONF_NAME)
|
||||
platform_label = f"{self._subentry_data['components'][self._component_id][CONF_PLATFORM]} "
|
||||
entity_name_label = f" ({name})" if name is not None else ""
|
||||
data_schema = data_schema_from_fields(data_schema_fields, reconfig=reconfig)
|
||||
if user_input is not None:
|
||||
validate_user_input(user_input, data_schema_fields, errors)
|
||||
merged_user_input, errors = validate_user_input(
|
||||
user_input, data_schema_fields, component_data
|
||||
)
|
||||
if not errors:
|
||||
if self._component_id is None:
|
||||
self._component_id = uuid4().hex
|
||||
self._subentry_data["components"].setdefault(self._component_id, {})
|
||||
self.update_component_fields(data_schema, user_input)
|
||||
return await self.async_step_mqtt_platform_config()
|
||||
self.update_component_fields(data_schema_fields, merged_user_input)
|
||||
return await self.async_step_entity_platform_config()
|
||||
data_schema = self.add_suggested_values_to_schema(data_schema, user_input)
|
||||
elif self.source == SOURCE_RECONFIGURE and self._component_id is not None:
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
data_schema, self._subentry_data["components"][self._component_id]
|
||||
data_schema,
|
||||
self.get_suggested_values_from_component(data_schema),
|
||||
)
|
||||
device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
|
||||
return self.async_show_form(
|
||||
@ -994,9 +1289,11 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
|
||||
entities = [
|
||||
SelectOptionDict(
|
||||
value=key, label=f"{device_name} {component.get(CONF_NAME, '-')}"
|
||||
value=key,
|
||||
label=f"{device_name} {component_data.get(CONF_NAME, '-')}"
|
||||
f" ({component_data[CONF_PLATFORM]})",
|
||||
)
|
||||
for key, component in self._subentry_data["components"].items()
|
||||
for key, component_data in self._subentry_data["components"].items()
|
||||
]
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
@ -1034,6 +1331,61 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
return await self.async_step_summary_menu()
|
||||
return self._show_update_or_delete_form("delete_entity")
|
||||
|
||||
async def async_step_entity_platform_config(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Configure platform entity details."""
|
||||
if TYPE_CHECKING:
|
||||
assert self._component_id is not None
|
||||
component_data = self._subentry_data["components"][self._component_id]
|
||||
platform = component_data[CONF_PLATFORM]
|
||||
data_schema_fields = PLATFORM_ENTITY_FIELDS[platform]
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
data_schema = data_schema_from_fields(
|
||||
data_schema_fields,
|
||||
reconfig=bool(
|
||||
{field for field in data_schema_fields if field in component_data}
|
||||
),
|
||||
component_data=component_data,
|
||||
user_input=user_input,
|
||||
)
|
||||
if not data_schema.schema:
|
||||
return await self.async_step_mqtt_platform_config()
|
||||
if user_input is not None:
|
||||
# Test entity fields against the validator
|
||||
merged_user_input, errors = validate_user_input(
|
||||
user_input,
|
||||
data_schema_fields,
|
||||
component_data,
|
||||
ENTITY_CONFIG_VALIDATOR[platform],
|
||||
)
|
||||
if not errors:
|
||||
self.update_component_fields(data_schema_fields, merged_user_input)
|
||||
return await self.async_step_mqtt_platform_config()
|
||||
|
||||
data_schema = self.add_suggested_values_to_schema(data_schema, user_input)
|
||||
else:
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
data_schema,
|
||||
self.get_suggested_values_from_component(data_schema),
|
||||
)
|
||||
|
||||
device_name, full_entity_name = self.generate_names()
|
||||
return self.async_show_form(
|
||||
step_id="entity_platform_config",
|
||||
data_schema=data_schema,
|
||||
description_placeholders={
|
||||
"mqtt_device": device_name,
|
||||
CONF_PLATFORM: platform,
|
||||
"entity": full_entity_name,
|
||||
"url": learn_more_url(platform),
|
||||
}
|
||||
| (user_input or {}),
|
||||
errors=errors,
|
||||
last_step=False,
|
||||
)
|
||||
|
||||
async def async_step_mqtt_platform_config(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
@ -1041,16 +1393,26 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
errors: dict[str, str] = {}
|
||||
if TYPE_CHECKING:
|
||||
assert self._component_id is not None
|
||||
platform = self._subentry_data["components"][self._component_id][CONF_PLATFORM]
|
||||
component_data = self._subentry_data["components"][self._component_id]
|
||||
platform = component_data[CONF_PLATFORM]
|
||||
data_schema_fields = PLATFORM_MQTT_FIELDS[platform] | COMMON_MQTT_FIELDS
|
||||
data_schema = data_schema_from_fields(
|
||||
data_schema_fields, reconfig=self._component_id is not None
|
||||
data_schema_fields,
|
||||
reconfig=bool(
|
||||
{field for field in data_schema_fields if field in component_data}
|
||||
),
|
||||
component_data=component_data,
|
||||
)
|
||||
if user_input is not None:
|
||||
# Test entity fields against the validator
|
||||
validate_user_input(user_input, data_schema_fields, errors)
|
||||
merged_user_input, errors = validate_user_input(
|
||||
user_input,
|
||||
data_schema_fields,
|
||||
component_data,
|
||||
ENTITY_CONFIG_VALIDATOR[platform],
|
||||
)
|
||||
if not errors:
|
||||
self.update_component_fields(data_schema, user_input)
|
||||
self.update_component_fields(data_schema_fields, merged_user_input)
|
||||
self._component_id = None
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
return await self.async_step_summary_menu()
|
||||
@ -1059,16 +1421,10 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
data_schema = self.add_suggested_values_to_schema(data_schema, user_input)
|
||||
else:
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
data_schema, self._subentry_data["components"][self._component_id]
|
||||
data_schema,
|
||||
self.get_suggested_values_from_component(data_schema),
|
||||
)
|
||||
device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
|
||||
entity_name: str | None
|
||||
if entity_name := self._subentry_data["components"][self._component_id].get(
|
||||
CONF_NAME
|
||||
):
|
||||
full_entity_name: str = f"{device_name} {entity_name}"
|
||||
else:
|
||||
full_entity_name = device_name
|
||||
device_name, full_entity_name = self.generate_names()
|
||||
return self.async_show_form(
|
||||
step_id="mqtt_platform_config",
|
||||
data_schema=data_schema,
|
||||
@ -1076,6 +1432,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"mqtt_device": device_name,
|
||||
CONF_PLATFORM: platform,
|
||||
"entity": full_entity_name,
|
||||
"url": learn_more_url(platform),
|
||||
},
|
||||
errors=errors,
|
||||
last_step=False,
|
||||
@ -1087,12 +1444,12 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""Create a subentry for a new MQTT device."""
|
||||
device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
|
||||
component: dict[str, Any] = next(
|
||||
component_data: dict[str, Any] = next(
|
||||
iter(self._subentry_data["components"].values())
|
||||
)
|
||||
platform = component[CONF_PLATFORM]
|
||||
platform = component_data[CONF_PLATFORM]
|
||||
entity_name: str | None
|
||||
if entity_name := component.get(CONF_NAME):
|
||||
if entity_name := component_data.get(CONF_NAME):
|
||||
full_entity_name: str = f"{device_name} {entity_name}"
|
||||
else:
|
||||
full_entity_name = device_name
|
||||
@ -1151,8 +1508,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
self._component_id = None
|
||||
mqtt_device = self._subentry_data[CONF_DEVICE][CONF_NAME]
|
||||
mqtt_items = ", ".join(
|
||||
f"{mqtt_device} {component.get(CONF_NAME, '-')}"
|
||||
for component in self._subentry_data["components"].values()
|
||||
f"{mqtt_device} {component_data.get(CONF_NAME, '-')} ({component_data[CONF_PLATFORM]})"
|
||||
for component_data in self._subentry_data["components"].values()
|
||||
)
|
||||
menu_options = [
|
||||
"entity",
|
||||
|
@ -86,6 +86,7 @@ CONF_EFFECT_STATE_TOPIC = "effect_state_topic"
|
||||
CONF_EFFECT_TEMPLATE = "effect_template"
|
||||
CONF_EFFECT_VALUE_TEMPLATE = "effect_value_template"
|
||||
CONF_ENTITY_PICTURE = "entity_picture"
|
||||
CONF_EXPIRE_AFTER = "expire_after"
|
||||
CONF_FLASH_TIME_LONG = "flash_time_long"
|
||||
CONF_FLASH_TIME_SHORT = "flash_time_short"
|
||||
CONF_GREEN_TEMPLATE = "green_template"
|
||||
@ -93,6 +94,7 @@ CONF_HS_COMMAND_TEMPLATE = "hs_command_template"
|
||||
CONF_HS_COMMAND_TOPIC = "hs_command_topic"
|
||||
CONF_HS_STATE_TOPIC = "hs_state_topic"
|
||||
CONF_HS_VALUE_TEMPLATE = "hs_value_template"
|
||||
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
|
||||
CONF_MAX_KELVIN = "max_kelvin"
|
||||
CONF_MAX_MIREDS = "max_mireds"
|
||||
CONF_MIN_KELVIN = "min_kelvin"
|
||||
@ -128,6 +130,7 @@ CONF_STATE_CLOSED = "state_closed"
|
||||
CONF_STATE_CLOSING = "state_closing"
|
||||
CONF_STATE_OPEN = "state_open"
|
||||
CONF_STATE_OPENING = "state_opening"
|
||||
CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision"
|
||||
CONF_SUPPORTED_COLOR_MODES = "supported_color_modes"
|
||||
CONF_TEMP_COMMAND_TEMPLATE = "temperature_command_template"
|
||||
CONF_TEMP_COMMAND_TOPIC = "temperature_command_topic"
|
||||
|
@ -123,7 +123,7 @@ from .subscription import (
|
||||
async_subscribe_topics_internal,
|
||||
async_unsubscribe_topics,
|
||||
)
|
||||
from .util import mqtt_config_entry_enabled
|
||||
from .util import learn_more_url, mqtt_config_entry_enabled
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -346,9 +346,6 @@ def async_setup_entity_entry_helper(
|
||||
line = getattr(yaml_config, "__line__", "?")
|
||||
issue_id = hex(hash(frozenset(yaml_config)))
|
||||
yaml_config_str = yaml_dump(yaml_config)
|
||||
learn_more_url = (
|
||||
f"https://www.home-assistant.io/integrations/{domain}.mqtt/"
|
||||
)
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
@ -356,7 +353,7 @@ def async_setup_entity_entry_helper(
|
||||
issue_domain=domain,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
learn_more_url=learn_more_url,
|
||||
learn_more_url=learn_more_url(domain),
|
||||
translation_placeholders={
|
||||
"domain": domain,
|
||||
"config_file": config_file,
|
||||
|
@ -41,7 +41,15 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_RO_SCHEMA
|
||||
from .const import CONF_OPTIONS, CONF_STATE_TOPIC, DOMAIN, PAYLOAD_NONE
|
||||
from .const import (
|
||||
CONF_EXPIRE_AFTER,
|
||||
CONF_LAST_RESET_VALUE_TEMPLATE,
|
||||
CONF_OPTIONS,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_SUGGESTED_DISPLAY_PRECISION,
|
||||
DOMAIN,
|
||||
PAYLOAD_NONE,
|
||||
)
|
||||
from .entity import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import MqttValueTemplate, PayloadSentinel, ReceiveMessage
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
@ -51,10 +59,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
CONF_EXPIRE_AFTER = "expire_after"
|
||||
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
|
||||
CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision"
|
||||
|
||||
MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset(
|
||||
{
|
||||
sensor.ATTR_LAST_RESET,
|
||||
|
@ -198,20 +198,66 @@
|
||||
"component": "Select the entity you want to update."
|
||||
}
|
||||
},
|
||||
"entity_platform_config": {
|
||||
"title": "Configure MQTT device \"{mqtt_device}\"",
|
||||
"description": "Please configure specific details for {platform} entity \"{entity}\":",
|
||||
"data": {
|
||||
"device_class": "Device class",
|
||||
"state_class": "State class",
|
||||
"unit_of_measurement": "Unit of measurement",
|
||||
"options": "Add option"
|
||||
},
|
||||
"data_description": {
|
||||
"device_class": "The device class of the {platform} entity. [Learn more.]({url}#device_class)",
|
||||
"state_class": "The [state_class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)",
|
||||
"unit_of_measurement": "Defines the unit of measurement of the sensor, if any.",
|
||||
"options": "Options for allowed sensor state values. The sensor’s device_class must be set to Enumeration. The options option cannot be used together with State Class or Unit of measurement."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_settings": {
|
||||
"name": "Advanced options",
|
||||
"data": {
|
||||
"suggested_display_precision": "Suggested display precision"
|
||||
},
|
||||
"data_description": {
|
||||
"suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mqtt_platform_config": {
|
||||
"title": "Configure MQTT device \"{mqtt_device}\"",
|
||||
"description": "Please configure MQTT specific details for {platform} entity \"{entity}\":",
|
||||
"data": {
|
||||
"command_topic": "Command topic",
|
||||
"command_template": "Command template",
|
||||
"state_topic": "State topic",
|
||||
"value_template": "Value template",
|
||||
"last_reset_value_template": "Last reset value template",
|
||||
"force_update": "Force update",
|
||||
"retain": "Retain",
|
||||
"qos": "QoS"
|
||||
},
|
||||
"data_description": {
|
||||
"command_topic": "The publishing topic that will be used to control the {platform} entity.",
|
||||
"command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)",
|
||||
"command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic.",
|
||||
"state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)",
|
||||
"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)",
|
||||
"force_update": "Sends update events even if the value hasn’t 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.",
|
||||
"qos": "The QoS value {platform} entity should use."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_settings": {
|
||||
"name": "Advanced settings",
|
||||
"data": {
|
||||
"expire_after": "Expire after"
|
||||
},
|
||||
"data_description": {
|
||||
"expire_after": "If set, it defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. If not set, the sensor's state never expires. [Learn more.]({url}#expire_after)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -225,7 +271,12 @@
|
||||
"invalid_input": "Invalid value",
|
||||
"invalid_subscribe_topic": "Invalid subscribe topic",
|
||||
"invalid_template": "Invalid template",
|
||||
"invalid_url": "Invalid URL"
|
||||
"invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
|
||||
"invalid_url": "Invalid URL",
|
||||
"options_not_allowed_with_state_class_or_uom": "The 'Options' setting is not allowed when state class or unit of measurement are used",
|
||||
"options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class'. If you continue, the existing options will be reset",
|
||||
"options_with_enum_device_class": "Configure options for the enumeration sensor",
|
||||
"uom_required_for_device_class": "The selected device device class requires a unit"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -342,9 +393,70 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"device_class_sensor": {
|
||||
"options": {
|
||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||
"area": "[%key:component::sensor::entity_component::area::name%]",
|
||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
|
||||
"battery": "[%key:component::sensor::entity_component::battery::name%]",
|
||||
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
|
||||
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
|
||||
"current": "[%key:component::sensor::entity_component::current::name%]",
|
||||
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
|
||||
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
|
||||
"date": "[%key:component::sensor::entity_component::date::name%]",
|
||||
"distance": "[%key:component::sensor::entity_component::distance::name%]",
|
||||
"duration": "[%key:component::sensor::entity_component::duration::name%]",
|
||||
"energy": "[%key:component::sensor::entity_component::energy::name%]",
|
||||
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
|
||||
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
|
||||
"enum": "Enumeration",
|
||||
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
|
||||
"gas": "[%key:component::sensor::entity_component::gas::name%]",
|
||||
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
|
||||
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
|
||||
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
|
||||
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
|
||||
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
|
||||
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
|
||||
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
|
||||
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
|
||||
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||
"ph": "[%key:component::sensor::entity_component::ph::name%]",
|
||||
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
|
||||
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
|
||||
"power": "[%key:component::sensor::entity_component::power::name%]",
|
||||
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
|
||||
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
|
||||
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
|
||||
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
|
||||
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
|
||||
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
|
||||
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
|
||||
"speed": "[%key:component::sensor::entity_component::speed::name%]",
|
||||
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
|
||||
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
||||
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
||||
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
|
||||
"volume": "[%key:component::sensor::entity_component::volume::name%]",
|
||||
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
|
||||
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
|
||||
"water": "[%key:component::sensor::entity_component::water::name%]",
|
||||
"weight": "[%key:component::sensor::entity_component::weight::name%]",
|
||||
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
|
||||
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
|
||||
}
|
||||
},
|
||||
"platform": {
|
||||
"options": {
|
||||
"notify": "Notify"
|
||||
"notify": "Notify",
|
||||
"sensor": "Sensor"
|
||||
}
|
||||
},
|
||||
"set_ca_cert": {
|
||||
@ -353,6 +465,13 @@
|
||||
"auto": "Auto",
|
||||
"custom": "Custom"
|
||||
}
|
||||
},
|
||||
"state_class": {
|
||||
"options": {
|
||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@ -411,3 +411,9 @@ def migrate_certificate_file_to_content(file_name_or_auto: str) -> str | None:
|
||||
return certificate_file.read()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def learn_more_url(platform: str) -> str:
|
||||
"""Return the URL for the platform specific MQTT documentation."""
|
||||
return f"https://www.home-assistant.io/integrations/{platform}.mqtt/"
|
||||
|
@ -72,7 +72,7 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT1 = {
|
||||
"name": "Milkman alert",
|
||||
"qos": 0,
|
||||
"command_topic": "test-topic",
|
||||
"command_template": "{{ value_json.value }}",
|
||||
"command_template": "{{ value }}",
|
||||
"entity_picture": "https://example.com/363a7ecad6be4a19b939a016ea93e994",
|
||||
"retain": False,
|
||||
},
|
||||
@ -91,12 +91,47 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME = {
|
||||
"platform": "notify",
|
||||
"qos": 0,
|
||||
"command_topic": "test-topic",
|
||||
"command_template": "{{ value_json.value }}",
|
||||
"command_template": "{{ value }}",
|
||||
"entity_picture": "https://example.com/5269352dd9534c908d22812ea5d714cd",
|
||||
"retain": False,
|
||||
},
|
||||
}
|
||||
|
||||
MOCK_SUBENTRY_SENSOR_COMPONENT = {
|
||||
"e9261f6feed443e7b7d5f3fbe2a47412": {
|
||||
"platform": "sensor",
|
||||
"name": "Energy",
|
||||
"device_class": "enum",
|
||||
"qos": 1,
|
||||
"state_topic": "test-topic",
|
||||
"options": ["low", "medium", "high"],
|
||||
"expire_after": 30,
|
||||
"value_template": "{{ value_json.value }}",
|
||||
"entity_picture": "https://example.com/e9261f6feed443e7b7d5f3fbe2a47412",
|
||||
},
|
||||
}
|
||||
MOCK_SUBENTRY_SENSOR_COMPONENT_STATE_CLASS = {
|
||||
"a0f85790a95d4889924602effff06b6e": {
|
||||
"platform": "sensor",
|
||||
"name": "Energy",
|
||||
"state_class": "measurement",
|
||||
"state_topic": "test-topic",
|
||||
"entity_picture": "https://example.com/a0f85790a95d4889924602effff06b6e",
|
||||
"qos": 0,
|
||||
},
|
||||
}
|
||||
MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET = {
|
||||
"e9261f6feed443e7b7d5f3fbe2a47412": {
|
||||
"platform": "sensor",
|
||||
"name": "Energy",
|
||||
"state_class": "total",
|
||||
"last_reset_value_template": "{{ value_json.value }}",
|
||||
"state_topic": "test-topic",
|
||||
"entity_picture": "https://example.com/e9261f6feed443e7b7d5f3fbe2a47412",
|
||||
"qos": 0,
|
||||
},
|
||||
}
|
||||
|
||||
# Bogus light component just for code coverage
|
||||
# Note that light cannot be setup through the UI yet
|
||||
# The test is for code coverage
|
||||
@ -151,7 +186,7 @@ MOCK_NOTIFY_SUBENTRY_DATA_SINGLE = {
|
||||
},
|
||||
"components": MOCK_SUBENTRY_NOTIFY_COMPONENT1,
|
||||
}
|
||||
MOCK_SUBENTRY_DATA_NOTIFY_NO_NAME = {
|
||||
MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME = {
|
||||
"device": {
|
||||
"name": "Milk notifier",
|
||||
"sw_version": "1.0",
|
||||
@ -162,6 +197,39 @@ MOCK_SUBENTRY_DATA_NOTIFY_NO_NAME = {
|
||||
},
|
||||
"components": MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME,
|
||||
}
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE = {
|
||||
"device": {
|
||||
"name": "Test sensor",
|
||||
"sw_version": "1.0",
|
||||
"hw_version": "2.1 rev a",
|
||||
"model": "Model XL",
|
||||
"model_id": "mn002",
|
||||
"configuration_url": "https://example.com",
|
||||
},
|
||||
"components": MOCK_SUBENTRY_SENSOR_COMPONENT,
|
||||
}
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS = {
|
||||
"device": {
|
||||
"name": "Test sensor",
|
||||
"sw_version": "1.0",
|
||||
"hw_version": "2.1 rev a",
|
||||
"model": "Model XL",
|
||||
"model_id": "mn002",
|
||||
"configuration_url": "https://example.com",
|
||||
},
|
||||
"components": MOCK_SUBENTRY_SENSOR_COMPONENT_STATE_CLASS,
|
||||
}
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE = {
|
||||
"device": {
|
||||
"name": "Test sensor",
|
||||
"sw_version": "1.0",
|
||||
"hw_version": "2.1 rev a",
|
||||
"model": "Model XL",
|
||||
"model_id": "mn002",
|
||||
"configuration_url": "https://example.com",
|
||||
},
|
||||
"components": MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET,
|
||||
}
|
||||
|
||||
MOCK_SUBENTRY_DATA_BAD_COMPONENT_SCHEMA = {
|
||||
"device": {
|
||||
|
@ -18,6 +18,7 @@ from homeassistant import config_entries
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.components.hassio import AddonError
|
||||
from homeassistant.components.mqtt.config_flow import PWD_NOT_CHANGED
|
||||
from homeassistant.components.mqtt.util import learn_more_url
|
||||
from homeassistant.config_entries import ConfigSubentry, ConfigSubentryData
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
@ -33,8 +34,11 @@ from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
|
||||
from .common import (
|
||||
MOCK_NOTIFY_SUBENTRY_DATA_MULTI,
|
||||
MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME,
|
||||
MOCK_NOTIFY_SUBENTRY_DATA_SINGLE,
|
||||
MOCK_SUBENTRY_DATA_NOTIFY_NO_NAME,
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE,
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE,
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, MockMqttReasonCode
|
||||
@ -2613,49 +2617,139 @@ async def test_migrate_of_incompatible_config_entry(
|
||||
(
|
||||
"config_subentries_data",
|
||||
"mock_entity_user_input",
|
||||
"mock_entity_details_user_input",
|
||||
"mock_entity_details_failed_user_input",
|
||||
"mock_mqtt_user_input",
|
||||
"mock_failed_mqtt_user_input",
|
||||
"mock_failed_mqtt_user_input_errors",
|
||||
"entity_name",
|
||||
),
|
||||
[
|
||||
(
|
||||
MOCK_NOTIFY_SUBENTRY_DATA_SINGLE,
|
||||
{"name": "Milkman alert"},
|
||||
None,
|
||||
None,
|
||||
{
|
||||
"command_topic": "test-topic",
|
||||
"command_template": "{{ value_json.value }}",
|
||||
"command_template": "{{ value }}",
|
||||
"qos": 0,
|
||||
"retain": False,
|
||||
},
|
||||
{"command_topic": "test-topic#invalid"},
|
||||
{"command_topic": "invalid_publish_topic"},
|
||||
(
|
||||
(
|
||||
{"command_topic": "test-topic#invalid"},
|
||||
{"command_topic": "invalid_publish_topic"},
|
||||
),
|
||||
),
|
||||
"Milk notifier Milkman alert",
|
||||
),
|
||||
(
|
||||
MOCK_SUBENTRY_DATA_NOTIFY_NO_NAME,
|
||||
MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME,
|
||||
{},
|
||||
None,
|
||||
None,
|
||||
{
|
||||
"command_topic": "test-topic",
|
||||
"command_template": "{{ value_json.value }}",
|
||||
"command_template": "{{ value }}",
|
||||
"qos": 0,
|
||||
"retain": False,
|
||||
},
|
||||
{"command_topic": "test-topic#invalid"},
|
||||
{"command_topic": "invalid_publish_topic"},
|
||||
(
|
||||
(
|
||||
{"command_topic": "test-topic#invalid"},
|
||||
{"command_topic": "invalid_publish_topic"},
|
||||
),
|
||||
),
|
||||
"Milk notifier",
|
||||
),
|
||||
(
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE,
|
||||
{"name": "Energy"},
|
||||
{"device_class": "enum", "options": ["low", "medium", "high"]},
|
||||
(
|
||||
(
|
||||
{
|
||||
"device_class": "energy",
|
||||
"unit_of_measurement": "ppm",
|
||||
},
|
||||
{"unit_of_measurement": "invalid_uom"},
|
||||
),
|
||||
# Trigger options to be shown on the form
|
||||
(
|
||||
{"device_class": "enum"},
|
||||
{"options": "options_with_enum_device_class"},
|
||||
),
|
||||
# Test options are only allowed with device_class enum
|
||||
(
|
||||
{
|
||||
"device_class": "energy",
|
||||
"options": ["less", "more"],
|
||||
},
|
||||
{
|
||||
"device_class": "options_device_class_enum",
|
||||
"unit_of_measurement": "uom_required_for_device_class",
|
||||
},
|
||||
),
|
||||
# Include options again to allow flow with valid data
|
||||
(
|
||||
{"device_class": "enum"},
|
||||
{"options": "options_with_enum_device_class"},
|
||||
),
|
||||
(
|
||||
{
|
||||
"device_class": "enum",
|
||||
"state_class": "measurement",
|
||||
"options": ["less", "more"],
|
||||
},
|
||||
{"options": "options_not_allowed_with_state_class_or_uom"},
|
||||
),
|
||||
),
|
||||
{
|
||||
"state_topic": "test-topic",
|
||||
"value_template": "{{ value_json.value }}",
|
||||
"advanced_settings": {"expire_after": 30},
|
||||
"qos": 1,
|
||||
},
|
||||
(
|
||||
(
|
||||
{"state_topic": "test-topic#invalid"},
|
||||
{"state_topic": "invalid_subscribe_topic"},
|
||||
),
|
||||
),
|
||||
"Test sensor Energy",
|
||||
),
|
||||
(
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS,
|
||||
{"name": "Energy"},
|
||||
{
|
||||
"state_class": "measurement",
|
||||
},
|
||||
(),
|
||||
{
|
||||
"state_topic": "test-topic",
|
||||
},
|
||||
(),
|
||||
"Test sensor Energy",
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"notify_with_entity_name",
|
||||
"notify_no_entity_name",
|
||||
"sensor_options",
|
||||
"sensor_total",
|
||||
],
|
||||
ids=["notify_with_entity_name", "notify_no_entity_name"],
|
||||
)
|
||||
async def test_subentry_configflow(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
config_subentries_data: dict[str, Any],
|
||||
mock_entity_user_input: dict[str, Any],
|
||||
mock_entity_details_user_input: dict[str, Any],
|
||||
mock_entity_details_failed_user_input: tuple[
|
||||
tuple[dict[str, Any], dict[str, str]],
|
||||
],
|
||||
mock_mqtt_user_input: dict[str, Any],
|
||||
mock_failed_mqtt_user_input: dict[str, Any],
|
||||
mock_failed_mqtt_user_input_errors: dict[str, Any],
|
||||
mock_failed_mqtt_user_input: tuple[tuple[dict[str, Any], dict[str, str]],],
|
||||
entity_name: str,
|
||||
) -> None:
|
||||
"""Test the subentry ConfigFlow."""
|
||||
@ -2723,23 +2817,55 @@ async def test_subentry_configflow(
|
||||
| mock_entity_user_input,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "mqtt_platform_config"
|
||||
assert result["errors"] == {}
|
||||
assert result["description_placeholders"] == {
|
||||
"mqtt_device": "Milk notifier",
|
||||
"platform": "notify",
|
||||
"mqtt_device": device_name,
|
||||
"platform": component["platform"],
|
||||
"entity": entity_name,
|
||||
"url": learn_more_url(component["platform"]),
|
||||
}
|
||||
|
||||
# Process entity platform config flow
|
||||
# Process extra step if the platform supports it
|
||||
if mock_entity_details_user_input is not None:
|
||||
# Extra entity details flow step
|
||||
assert result["step_id"] == "entity_platform_config"
|
||||
|
||||
# Test an invalid mqtt user_input case
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=mock_failed_mqtt_user_input,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == mock_failed_mqtt_user_input_errors
|
||||
# First test validators if set of test
|
||||
for failed_user_input, failed_errors in mock_entity_details_failed_user_input:
|
||||
# Test an invalid entity details user input case
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=failed_user_input,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == failed_errors
|
||||
|
||||
# Now try again with valid data
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=mock_entity_details_user_input,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
assert result["description_placeholders"] == {
|
||||
"mqtt_device": device_name,
|
||||
"platform": component["platform"],
|
||||
"entity": entity_name,
|
||||
"url": learn_more_url(component["platform"]),
|
||||
}
|
||||
else:
|
||||
# No details form step
|
||||
assert result["step_id"] == "mqtt_platform_config"
|
||||
|
||||
# Process mqtt platform config flow
|
||||
# Test an invalid mqtt user input case
|
||||
for failed_user_input, failed_errors in mock_failed_mqtt_user_input:
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=failed_user_input,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == failed_errors
|
||||
|
||||
# Try again with a valid configuration
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
@ -2799,8 +2925,12 @@ async def test_subentry_reconfigure_remove_entity(
|
||||
assert len(components) == 2
|
||||
object_list = list(components)
|
||||
component_list = list(components.values())
|
||||
entity_name_0 = f"{device.name} {component_list[0]['name']}"
|
||||
entity_name_1 = f"{device.name} {component_list[1]['name']}"
|
||||
entity_name_0 = (
|
||||
f"{device.name} {component_list[0]['name']} ({component_list[0]['platform']})"
|
||||
)
|
||||
entity_name_1 = (
|
||||
f"{device.name} {component_list[1]['name']} ({component_list[1]['platform']})"
|
||||
)
|
||||
|
||||
for key, component in components.items():
|
||||
unique_entity_id = f"{subentry_id}_{key}"
|
||||
@ -2920,8 +3050,12 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites(
|
||||
assert len(components) == 2
|
||||
object_list = list(components)
|
||||
component_list = list(components.values())
|
||||
entity_name_0 = f"{device.name} {component_list[0]['name']}"
|
||||
entity_name_1 = f"{device.name} {component_list[1]['name']}"
|
||||
entity_name_0 = (
|
||||
f"{device.name} {component_list[0]['name']} ({component_list[0]['platform']})"
|
||||
)
|
||||
entity_name_1 = (
|
||||
f"{device.name} {component_list[1]['name']} ({component_list[1]['platform']})"
|
||||
)
|
||||
|
||||
for key in components:
|
||||
unique_entity_id = f"{subentry_id}_{key}"
|
||||
@ -3000,7 +3134,13 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mqtt_config_subentries_data", "user_input_mqtt"),
|
||||
(
|
||||
"mqtt_config_subentries_data",
|
||||
"user_input_platform_config_validation",
|
||||
"user_input_platform_config",
|
||||
"user_input_mqtt",
|
||||
"removed_options",
|
||||
),
|
||||
[
|
||||
(
|
||||
(
|
||||
@ -3010,21 +3150,66 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites(
|
||||
title="Mock subentry",
|
||||
),
|
||||
),
|
||||
(),
|
||||
None,
|
||||
{
|
||||
"command_topic": "test-topic1-updated",
|
||||
"command_template": "{{ value_json.value }}",
|
||||
"command_template": "{{ value }}",
|
||||
"retain": True,
|
||||
},
|
||||
)
|
||||
{"entity_picture"},
|
||||
),
|
||||
(
|
||||
(
|
||||
ConfigSubentryData(
|
||||
data=MOCK_SENSOR_SUBENTRY_DATA_SINGLE,
|
||||
subentry_type="device",
|
||||
title="Mock subentry",
|
||||
),
|
||||
),
|
||||
(
|
||||
(
|
||||
{
|
||||
"device_class": "battery",
|
||||
"options": [],
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": "invalid",
|
||||
},
|
||||
# Allow to accept options are being removed
|
||||
{
|
||||
"device_class": "options_device_class_enum",
|
||||
"options": "options_not_allowed_with_state_class_or_uom",
|
||||
"unit_of_measurement": "invalid_uom",
|
||||
},
|
||||
),
|
||||
),
|
||||
{
|
||||
"device_class": "battery",
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": "%",
|
||||
"advanced_settings": {"suggested_display_precision": 1},
|
||||
},
|
||||
{
|
||||
"state_topic": "test-topic1-updated",
|
||||
"value_template": "{{ value_json.value }}",
|
||||
},
|
||||
{"options", "expire_after", "entity_picture"},
|
||||
),
|
||||
],
|
||||
ids=["notify"],
|
||||
ids=["notify", "sensor"],
|
||||
)
|
||||
async def test_subentry_reconfigure_edit_entity_single_entity(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
user_input_platform_config_validation: tuple[
|
||||
tuple[dict[str, Any], dict[str, str] | None], ...
|
||||
]
|
||||
| None,
|
||||
user_input_platform_config: dict[str, Any] | None,
|
||||
user_input_mqtt: dict[str, Any],
|
||||
removed_options: tuple[str, ...],
|
||||
) -> None:
|
||||
"""Test the subentry ConfigFlow reconfigure with single entity."""
|
||||
await mqtt_mock_entry()
|
||||
@ -3081,7 +3266,28 @@ async def test_subentry_reconfigure_edit_entity_single_entity(
|
||||
user_input={},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "mqtt_platform_config"
|
||||
|
||||
if user_input_platform_config is None:
|
||||
# Skip entity flow step
|
||||
assert result["step_id"] == "mqtt_platform_config"
|
||||
else:
|
||||
# Additional entity flow step
|
||||
assert result["step_id"] == "entity_platform_config"
|
||||
for entity_validation_config, errors in user_input_platform_config_validation:
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=entity_validation_config,
|
||||
)
|
||||
assert result["step_id"] == "entity_platform_config"
|
||||
assert result.get("errors") == errors
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=user_input_platform_config,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "mqtt_platform_config"
|
||||
|
||||
# submit the new platform specific entity data,
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
@ -3110,6 +3316,142 @@ async def test_subentry_reconfigure_edit_entity_single_entity(
|
||||
for key, value in user_input_mqtt.items():
|
||||
assert new_components[component_id][key] == value
|
||||
|
||||
assert set(component) - set(new_components[component_id]) == removed_options
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"mqtt_config_subentries_data",
|
||||
"user_input_entity_details",
|
||||
"user_input_mqtt",
|
||||
"filtered_out_fields",
|
||||
),
|
||||
[
|
||||
(
|
||||
(
|
||||
ConfigSubentryData(
|
||||
data=MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE,
|
||||
subentry_type="device",
|
||||
title="Mock subentry",
|
||||
),
|
||||
),
|
||||
{
|
||||
"state_class": "measurement",
|
||||
},
|
||||
{
|
||||
"state_topic": "test-topic",
|
||||
},
|
||||
("last_reset_value_template",),
|
||||
),
|
||||
],
|
||||
ids=["sensor_last_reset_template"],
|
||||
)
|
||||
async def test_subentry_reconfigure_edit_entity_reset_fields(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
user_input_entity_details: dict[str, Any],
|
||||
user_input_mqtt: dict[str, Any],
|
||||
filtered_out_fields: tuple[str, ...],
|
||||
) -> None:
|
||||
"""Test the subentry ConfigFlow reconfigure resets filtered out fields."""
|
||||
await mqtt_mock_entry()
|
||||
config_entry: MockConfigEntry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
||||
subentry_id: str
|
||||
subentry: ConfigSubentry
|
||||
subentry_id, subentry = next(iter(config_entry.subentries.items()))
|
||||
result = await config_entry.start_subentry_reconfigure_flow(
|
||||
hass, "device", subentry_id
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "summary_menu"
|
||||
|
||||
# assert we have a device for the subentry
|
||||
device = device_registry.async_get_device(identifiers={(mqtt.DOMAIN, subentry_id)})
|
||||
assert device is not None
|
||||
|
||||
# assert we have an entity for the subentry component
|
||||
components = deepcopy(dict(subentry.data))["components"]
|
||||
assert len(components) == 1
|
||||
|
||||
component_id, component = next(iter(components.items()))
|
||||
for field in filtered_out_fields:
|
||||
assert field in component
|
||||
|
||||
unique_entity_id = f"{subentry_id}_{component_id}"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
domain=component["platform"], platform=mqtt.DOMAIN, unique_id=unique_entity_id
|
||||
)
|
||||
assert entity_id is not None
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry is not None
|
||||
assert entity_entry.config_subentry_id == subentry_id
|
||||
|
||||
# assert menu options, we do not have the option to delete an entity
|
||||
# we have no option to save and finish yet
|
||||
assert result["menu_options"] == [
|
||||
"entity",
|
||||
"update_entity",
|
||||
"device",
|
||||
"availability",
|
||||
]
|
||||
|
||||
# assert we can update the entity, there is no select step
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "update_entity"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "entity"
|
||||
|
||||
# submit the new common entity data, reset entity_picture
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "entity_platform_config"
|
||||
|
||||
# submit the new entity platform config
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=user_input_entity_details,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "mqtt_platform_config"
|
||||
|
||||
# submit the new platform specific mqtt data,
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=user_input_mqtt,
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "summary_menu"
|
||||
|
||||
# finish reconfigure flow
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "save_changes"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
|
||||
# Check we still have out components
|
||||
new_components = deepcopy(dict(subentry.data))["components"]
|
||||
assert len(new_components) == 1
|
||||
|
||||
# Check our update was successful
|
||||
assert "entity_picture" not in new_components[component_id]
|
||||
|
||||
# Check the second component was updated
|
||||
for key, value in user_input_mqtt.items():
|
||||
assert new_components[component_id][key] == value
|
||||
|
||||
# Check field are filtered out correctly
|
||||
for field in filtered_out_fields:
|
||||
assert field not in new_components[component_id]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mqtt_config_subentries_data", "user_input_entity", "user_input_mqtt"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user