From 0d29b2d5a7b79649480ea305eca8367d0c767579 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 27 Aug 2025 11:00:31 +0200 Subject: [PATCH] Add MQTT alarm control panel subentry support (#150395) Co-authored-by: Norbert Rittel --- .../components/mqtt/alarm_control_panel.py | 90 ++++----- homeassistant/components/mqtt/config_flow.py | 188 +++++++++++++++++- homeassistant/components/mqtt/const.py | 34 +++- homeassistant/components/mqtt/strings.json | 53 ++++- tests/components/mqtt/common.py | 84 ++++++++ tests/components/mqtt/test_config_flow.py | 185 ++++++++++++++++- 6 files changed, 578 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 64b1a6b05fa..72b92cdcb9d 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -7,10 +7,7 @@ import logging import voluptuous as vol from homeassistant.components import alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import ( - AlarmControlPanelEntityFeature, - AlarmControlPanelState, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelState from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CODE, CONF_NAME, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant, callback @@ -21,12 +18,33 @@ from homeassistant.helpers.typing import ConfigType from . import subscription from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import ( + ALARM_CONTROL_PANEL_SUPPORTED_FEATURES, + CONF_CODE_ARM_REQUIRED, + CONF_CODE_DISARM_REQUIRED, + CONF_CODE_TRIGGER_REQUIRED, CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_ARM_AWAY, + CONF_PAYLOAD_ARM_CUSTOM_BYPASS, + CONF_PAYLOAD_ARM_HOME, + CONF_PAYLOAD_ARM_NIGHT, + CONF_PAYLOAD_ARM_VACATION, + CONF_PAYLOAD_DISARM, + CONF_PAYLOAD_TRIGGER, CONF_RETAIN, CONF_STATE_TOPIC, CONF_SUPPORTED_FEATURES, + DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE, + DEFAULT_PAYLOAD_ARM_AWAY, + DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS, + DEFAULT_PAYLOAD_ARM_HOME, + DEFAULT_PAYLOAD_ARM_NIGHT, + DEFAULT_PAYLOAD_ARM_VACATION, + DEFAULT_PAYLOAD_DISARM, + DEFAULT_PAYLOAD_TRIGGER, PAYLOAD_NONE, + REMOTE_CODE, + REMOTE_CODE_TEXT, ) from .entity import MqttEntity, async_setup_entity_entry_helper from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage @@ -37,26 +55,6 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 -_SUPPORTED_FEATURES = { - "arm_home": AlarmControlPanelEntityFeature.ARM_HOME, - "arm_away": AlarmControlPanelEntityFeature.ARM_AWAY, - "arm_night": AlarmControlPanelEntityFeature.ARM_NIGHT, - "arm_vacation": AlarmControlPanelEntityFeature.ARM_VACATION, - "arm_custom_bypass": AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, - "trigger": AlarmControlPanelEntityFeature.TRIGGER, -} - -CONF_CODE_ARM_REQUIRED = "code_arm_required" -CONF_CODE_DISARM_REQUIRED = "code_disarm_required" -CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required" -CONF_PAYLOAD_DISARM = "payload_disarm" -CONF_PAYLOAD_ARM_HOME = "payload_arm_home" -CONF_PAYLOAD_ARM_AWAY = "payload_arm_away" -CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night" -CONF_PAYLOAD_ARM_VACATION = "payload_arm_vacation" -CONF_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass" -CONF_PAYLOAD_TRIGGER = "payload_trigger" - MQTT_ALARM_ATTRIBUTES_BLOCKED = frozenset( { alarm.ATTR_CHANGED_BY, @@ -65,44 +63,40 @@ MQTT_ALARM_ATTRIBUTES_BLOCKED = frozenset( } ) -DEFAULT_COMMAND_TEMPLATE = "{{action}}" -DEFAULT_ARM_NIGHT = "ARM_NIGHT" -DEFAULT_ARM_VACATION = "ARM_VACATION" -DEFAULT_ARM_AWAY = "ARM_AWAY" -DEFAULT_ARM_HOME = "ARM_HOME" -DEFAULT_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS" -DEFAULT_DISARM = "DISARM" -DEFAULT_TRIGGER = "TRIGGER" DEFAULT_NAME = "MQTT Alarm" -REMOTE_CODE = "REMOTE_CODE" -REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT" - PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_SUPPORTED_FEATURES, default=list(_SUPPORTED_FEATURES)): [ - vol.In(_SUPPORTED_FEATURES) - ], + vol.Optional( + CONF_SUPPORTED_FEATURES, + default=list(ALARM_CONTROL_PANEL_SUPPORTED_FEATURES), + ): [vol.In(ALARM_CONTROL_PANEL_SUPPORTED_FEATURES)], vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_DISARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_TRIGGER_REQUIRED, default=True): cv.boolean, vol.Optional( - CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE + CONF_COMMAND_TEMPLATE, default=DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE ): cv.template, vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_NAME): vol.Any(cv.string, None), - vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, - vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, - vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string, vol.Optional( - CONF_PAYLOAD_ARM_VACATION, default=DEFAULT_ARM_VACATION + CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_PAYLOAD_ARM_AWAY ): cv.string, vol.Optional( - CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_ARM_CUSTOM_BYPASS + CONF_PAYLOAD_ARM_HOME, default=DEFAULT_PAYLOAD_ARM_HOME ): cv.string, - vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, - vol.Optional(CONF_PAYLOAD_TRIGGER, default=DEFAULT_TRIGGER): cv.string, + vol.Optional( + CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_PAYLOAD_ARM_NIGHT + ): cv.string, + vol.Optional( + CONF_PAYLOAD_ARM_VACATION, default=DEFAULT_PAYLOAD_ARM_VACATION + ): cv.string, + vol.Optional( + CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS + ): cv.string, + vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_PAYLOAD_DISARM): cv.string, + vol.Optional(CONF_PAYLOAD_TRIGGER, default=DEFAULT_PAYLOAD_TRIGGER): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, @@ -152,7 +146,9 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): ).async_render for feature in self._config[CONF_SUPPORTED_FEATURES]: - self._attr_supported_features |= _SUPPORTED_FEATURES[feature] + self._attr_supported_features |= ALARM_CONTROL_PANEL_SUPPORTED_FEATURES[ + feature + ] if (code := self._config.get(CONF_CODE)) is None: self._attr_code_format = None diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a8a4c2e9538..b85b01f92c3 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -71,6 +71,7 @@ from homeassistant.const import ( ATTR_SW_VERSION, CONF_BRIGHTNESS, CONF_CLIENT_ID, + CONF_CODE, CONF_DEVICE, CONF_DEVICE_CLASS, CONF_DISCOVERY, @@ -129,6 +130,7 @@ from homeassistant.util.unit_conversion import TemperatureConverter from .addon import get_addon_manager from .client import MqttClientSetup from .const import ( + ALARM_CONTROL_PANEL_SUPPORTED_FEATURES, ATTR_PAYLOAD, ATTR_QOS, ATTR_RETAIN, @@ -149,7 +151,10 @@ from .const import ( CONF_CERTIFICATE, CONF_CLIENT_CERT, CONF_CLIENT_KEY, + CONF_CODE_ARM_REQUIRED, + CONF_CODE_DISARM_REQUIRED, CONF_CODE_FORMAT, + CONF_CODE_TRIGGER_REQUIRED, CONF_COLOR_MODE_STATE_TOPIC, CONF_COLOR_MODE_VALUE_TEMPLATE, CONF_COLOR_TEMP_COMMAND_TEMPLATE, @@ -216,6 +221,11 @@ from .const import ( CONF_OSCILLATION_COMMAND_TOPIC, CONF_OSCILLATION_STATE_TOPIC, CONF_OSCILLATION_VALUE_TEMPLATE, + CONF_PAYLOAD_ARM_AWAY, + CONF_PAYLOAD_ARM_CUSTOM_BYPASS, + CONF_PAYLOAD_ARM_HOME, + CONF_PAYLOAD_ARM_NIGHT, + CONF_PAYLOAD_ARM_VACATION, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_CLOSE, CONF_PAYLOAD_LOCK, @@ -229,6 +239,7 @@ from .const import ( CONF_PAYLOAD_RESET_PRESET_MODE, CONF_PAYLOAD_STOP, CONF_PAYLOAD_STOP_TILT, + CONF_PAYLOAD_TRIGGER, CONF_PAYLOAD_UNLOCK, CONF_PERCENTAGE_COMMAND_TEMPLATE, CONF_PERCENTAGE_COMMAND_TOPIC, @@ -280,6 +291,7 @@ from .const import ( CONF_STATE_VALUE_TEMPLATE, CONF_SUGGESTED_DISPLAY_PRECISION, CONF_SUPPORTED_COLOR_MODES, + CONF_SUPPORTED_FEATURES, CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE, CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC, CONF_SWING_HORIZONTAL_MODE_LIST, @@ -329,12 +341,18 @@ from .const import ( CONF_XY_VALUE_TEMPLATE, CONFIG_ENTRY_MINOR_VERSION, CONFIG_ENTRY_VERSION, + DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE, DEFAULT_BIRTH, DEFAULT_CLIMATE_INITIAL_TEMPERATURE, DEFAULT_DISCOVERY, DEFAULT_ENCODING, DEFAULT_KEEPALIVE, DEFAULT_ON_COMMAND_TYPE, + DEFAULT_PAYLOAD_ARM_AWAY, + DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS, + DEFAULT_PAYLOAD_ARM_HOME, + DEFAULT_PAYLOAD_ARM_NIGHT, + DEFAULT_PAYLOAD_ARM_VACATION, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_CLOSE, DEFAULT_PAYLOAD_LOCK, @@ -347,6 +365,7 @@ from .const import ( DEFAULT_PAYLOAD_PRESS, DEFAULT_PAYLOAD_RESET, DEFAULT_PAYLOAD_STOP, + DEFAULT_PAYLOAD_TRIGGER, DEFAULT_PAYLOAD_UNLOCK, DEFAULT_PORT, DEFAULT_POSITION_CLOSED, @@ -370,6 +389,8 @@ from .const import ( DEFAULT_WILL, DEFAULT_WS_PATH, DOMAIN, + REMOTE_CODE, + REMOTE_CODE_TEXT, SUPPORTED_PROTOCOLS, TRANSPORT_TCP, TRANSPORT_WEBSOCKETS, @@ -468,6 +489,7 @@ KEY_UPLOAD_SELECTOR = FileSelector( # Subentry selectors SUBENTRY_PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, @@ -573,6 +595,21 @@ TIMEOUT_SELECTOR = NumberSelector( NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0) ) +# Alarm control panel selectors +ALARM_CONTROL_PANEL_FEATURES_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=list(ALARM_CONTROL_PANEL_SUPPORTED_FEATURES), + multiple=True, + translation_key="alarm_control_panel_features", + ) +) +ALARM_CONTROL_PANEL_CODE_MODE = SelectSelector( + SelectSelectorConfig( + options=["local_code", "remote_code", "remote_code_text"], + translation_key="alarm_control_panel_code_mode", + ) +) + # Climate specific selectors CLIMATE_MODE_SELECTOR = SelectSelector( SelectSelectorConfig( @@ -729,6 +766,25 @@ HUMIDITY_SELECTOR = vol.All( vol.Coerce(int), ) +_CODE_VALIDATION_MODE = { + "remote_code": REMOTE_CODE, + "remote_code_text": REMOTE_CODE_TEXT, +} + + +@callback +def default_alarm_control_panel_code(config: dict[str, Any]) -> str: + """Return alarm control panel code based on the stored code and code mode.""" + code: str + if config["alarm_control_panel_code_mode"] in _CODE_VALIDATION_MODE: + # Return magic value for remote code validation + return _CODE_VALIDATION_MODE[config["alarm_control_panel_code_mode"]] + if (code := config.get(CONF_CODE, "")) in _CODE_VALIDATION_MODE.values(): + # Remove magic value for remote code validation + return "" + + return code + @callback def temperature_default_from_celsius_to_system_default( @@ -925,6 +981,7 @@ class PlatformField: vol.UNDEFINED ) is_schema_default: bool = False + include_in_config: bool = False exclude_from_reconfig: bool = False exclude_from_config: bool = False conditions: tuple[dict[str, Any], ...] | None = None @@ -995,6 +1052,23 @@ SHARED_PLATFORM_ENTITY_FIELDS: dict[str, PlatformField] = { } PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = { + Platform.ALARM_CONTROL_PANEL.value: { + CONF_SUPPORTED_FEATURES: PlatformField( + selector=ALARM_CONTROL_PANEL_FEATURES_SELECTOR, + required=True, + default=lambda config: config.get( + CONF_SUPPORTED_FEATURES, list(ALARM_CONTROL_PANEL_SUPPORTED_FEATURES) + ), + ), + "alarm_control_panel_code_mode": PlatformField( + selector=ALARM_CONTROL_PANEL_CODE_MODE, + required=True, + exclude_from_config=True, + default=lambda config: config[CONF_CODE].lower() + if config.get(CONF_CODE) in (REMOTE_CODE, REMOTE_CODE_TEXT) + else "local_code", + ), + }, Platform.BINARY_SENSOR.value: { CONF_DEVICE_CLASS: PlatformField( selector=BINARY_SENSOR_DEVICE_CLASS_SELECTOR, @@ -1168,6 +1242,92 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = { Platform.LOCK.value: {}, } PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = { + Platform.ALARM_CONTROL_PANEL: { + CONF_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + ), + CONF_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + default=DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + ), + CONF_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_CODE: PlatformField( + selector=PASSWORD_SELECTOR, + required=True, + include_in_config=True, + default=default_alarm_control_panel_code, + conditions=({"alarm_control_panel_code_mode": "local_code"},), + ), + CONF_CODE_ARM_REQUIRED: PlatformField( + selector=BOOLEAN_SELECTOR, + required=True, + default=True, + ), + CONF_CODE_DISARM_REQUIRED: PlatformField( + selector=BOOLEAN_SELECTOR, + required=True, + default=True, + ), + CONF_CODE_TRIGGER_REQUIRED: PlatformField( + selector=BOOLEAN_SELECTOR, + required=True, + default=True, + ), + CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + CONF_PAYLOAD_ARM_HOME: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ARM_HOME, + section="alarm_control_panel_payload_settings", + ), + CONF_PAYLOAD_ARM_AWAY: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ARM_AWAY, + section="alarm_control_panel_payload_settings", + ), + CONF_PAYLOAD_ARM_NIGHT: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ARM_NIGHT, + section="alarm_control_panel_payload_settings", + ), + CONF_PAYLOAD_ARM_VACATION: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ARM_VACATION, + section="alarm_control_panel_payload_settings", + ), + CONF_PAYLOAD_ARM_CUSTOM_BYPASS: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS, + section="alarm_control_panel_payload_settings", + ), + CONF_PAYLOAD_TRIGGER: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_TRIGGER, + section="alarm_control_panel_payload_settings", + ), + }, Platform.BINARY_SENSOR.value: { CONF_STATE_TOPIC: PlatformField( selector=TEXT_SELECTOR, @@ -2774,6 +2934,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ str, Callable[[dict[str, Any]], dict[str, str]] | None, ] = { + Platform.ALARM_CONTROL_PANEL: None, Platform.BINARY_SENSOR.value: None, Platform.BUTTON.value: None, Platform.CLIMATE.value: validate_climate_platform_config, @@ -2969,13 +3130,24 @@ def data_schema_from_fields( data_schema: dict[Any, Any] = {} all_data_element_options: set[Any] = set() no_reconfig_options: set[Any] = set() + + defaults: dict[str, Any] = {} + for field_name, field_details in data_schema_fields.items(): + default = defaults[field_name] = get_default(field_details) + if not field_details.include_in_config or component_data is None: + continue + component_data[field_name] = default + for schema_section in sections: + # Always calculate the default values + # Getting the default value may update the subentry data, + # even when and option is filtered out data_schema_element = { - vol.Required(field_name, default=get_default(field_details)) + vol.Required(field_name, default=defaults[field_name]) if field_details.required else vol.Optional( field_name, - default=get_default(field_details) + default=defaults[field_name] if field_details.default is not None else vol.UNDEFINED, ): field_details.selector(component_data_with_user_input or {}) @@ -3024,12 +3196,16 @@ def data_schema_from_fields( ) # Reset all fields from the component_data not in the schema + # except for options that should stay included 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: + if ( + field in component_data + and not data_schema_fields[field].include_in_config + ): del component_data[field] return vol.Schema(data_schema) @@ -3591,6 +3767,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): for field, platform_field in data_schema_fields.items() if field in (set(component_data) - set(config)) and not platform_field.exclude_from_reconfig + and not platform_field.include_in_config ): component_data.pop(field) component_data.update(merged_user_input) @@ -3906,7 +4083,10 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): ) component_data.update(subentry_default_data) for key, platform_field in platform_fields.items(): - if not platform_field.exclude_from_config: + if ( + not platform_field.exclude_from_config + or platform_field.include_in_config + ): continue if key in component_data: component_data.pop(key) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 2128b55c4b0..d1feb25b281 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -4,6 +4,7 @@ import logging import jinja2 +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.const import CONF_DISCOVERY, CONF_PAYLOAD, Platform from homeassistant.exceptions import TemplateError @@ -31,7 +32,10 @@ CONF_AVAILABILITY_TEMPLATE = "availability_template" CONF_AVAILABILITY_TOPIC = "availability_topic" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" +CONF_CODE_ARM_REQUIRED = "code_arm_required" +CONF_CODE_DISARM_REQUIRED = "code_disarm_required" CONF_CODE_FORMAT = "code_format" +CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required" CONF_COMMAND_TEMPLATE = "command_template" CONF_COMMAND_TOPIC = "command_topic" CONF_DISCOVERY_PREFIX = "discovery_prefix" @@ -127,7 +131,13 @@ CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template" CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" +CONF_PAYLOAD_ARM_AWAY = "payload_arm_away" +CONF_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass" +CONF_PAYLOAD_ARM_HOME = "payload_arm_home" +CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night" +CONF_PAYLOAD_ARM_VACATION = "payload_arm_vacation" CONF_PAYLOAD_CLOSE = "payload_close" +CONF_PAYLOAD_DISARM = "payload_disarm" CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_OPEN = "payload_open" CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off" @@ -137,6 +147,7 @@ CONF_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage" CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode" CONF_PAYLOAD_STOP = "payload_stop" CONF_PAYLOAD_STOP_TILT = "payload_stop_tilt" +CONF_PAYLOAD_TRIGGER = "payload_trigger" CONF_PAYLOAD_UNLOCK = "payload_unlock" CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" @@ -247,6 +258,7 @@ CONF_CONFIGURATION_URL = "configuration_url" CONF_OBJECT_ID = "object_id" CONF_SUPPORT_URL = "support_url" +DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE = "{{action}}" DEFAULT_BRIGHTNESS = False DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_CLIMATE_INITIAL_TEMPERATURE = 21.0 @@ -260,8 +272,15 @@ DEFAULT_FLASH_TIME_SHORT = 2 DEFAULT_OPTIMISTIC = False DEFAULT_ON_COMMAND_TYPE = "last" DEFAULT_QOS = 0 + +DEFAULT_PAYLOAD_ARM_AWAY = "ARM_AWAY" +DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS" +DEFAULT_PAYLOAD_ARM_HOME = "ARM_HOME" +DEFAULT_PAYLOAD_ARM_NIGHT = "ARM_NIGHT" +DEFAULT_PAYLOAD_ARM_VACATION = "ARM_VACATION" DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_CLOSE = "CLOSE" +DEFAULT_PAYLOAD_DISARM = "DISARM" DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" DEFAULT_PAYLOAD_OFF = "OFF" @@ -270,10 +289,10 @@ DEFAULT_PAYLOAD_OPEN = "OPEN" DEFAULT_PAYLOAD_OSCILLATE_OFF = "oscillate_off" DEFAULT_PAYLOAD_OSCILLATE_ON = "oscillate_on" DEFAULT_PAYLOAD_PRESS = "PRESS" -DEFAULT_PAYLOAD_STOP = "STOP" DEFAULT_PAYLOAD_RESET = "None" +DEFAULT_PAYLOAD_STOP = "STOP" +DEFAULT_PAYLOAD_TRIGGER = "TRIGGER" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" - DEFAULT_PORT = 1883 DEFAULT_RETAIN = False DEFAULT_TILT_CLOSED_POSITION = 0 @@ -303,6 +322,17 @@ TILT_PAYLOAD = "tilt" VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"] +ALARM_CONTROL_PANEL_SUPPORTED_FEATURES = { + "arm_home": AlarmControlPanelEntityFeature.ARM_HOME, + "arm_away": AlarmControlPanelEntityFeature.ARM_AWAY, + "arm_night": AlarmControlPanelEntityFeature.ARM_NIGHT, + "arm_vacation": AlarmControlPanelEntityFeature.ARM_VACATION, + "arm_custom_bypass": AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, + "trigger": AlarmControlPanelEntityFeature.TRIGGER, +} +REMOTE_CODE = "REMOTE_CODE" +REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT" + PROTOCOL_31 = "3.1" PROTOCOL_311 = "3.1.1" PROTOCOL_5 = "5" diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 3844cf8d669..fa615ed1f91 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -243,6 +243,7 @@ "title": "Configure MQTT device \"{mqtt_device}\"", "description": "Please configure specific details for {platform} entity \"{entity}\":", "data": { + "alarm_control_panel_code_mode": "Alarm code validation mode", "climate_feature_action": "Current action support", "climate_feature_current_humidity": "Current humidity support", "climate_feature_current_temperature": "Current temperature support", @@ -263,10 +264,12 @@ "schema": "Schema", "state_class": "State class", "suggested_display_precision": "Suggested display precision", + "supported_features": "Supported features", "temperature_unit": "Temperature unit", "unit_of_measurement": "Unit of measurement" }, "data_description": { + "alarm_control_panel_code_mode": "Configures how the alarm control panel validates the code. A local code is configured with the entity and is validated by Home Assistant. A remote code is sent to the device and validated remotely. [Learn more.]({url}#code)", "climate_feature_action": "The climate supports reporting the current action.", "climate_feature_current_humidity": "The climate supports reporting the current humidity.", "climate_feature_current_temperature": "The climate supports reporting the current temperature.", @@ -287,6 +290,7 @@ "schema": "The schema to use. [Learn more.]({url}#comparison-of-light-mqtt-schemas)", "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)", "suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)", + "supported_features": "The features that the entity supports.", "temperature_unit": "This determines the native unit of measurement the MQTT climate device works with.", "unit_of_measurement": "Defines the unit of measurement of the sensor, if any." }, @@ -308,7 +312,11 @@ "data": { "blue_template": "Blue template", "brightness_template": "Brightness template", + "code": "Alarm code", "code_format": "Code format", + "code_arm_required": "Code arm required", + "code_disarm_required": "Code disarm required", + "code_trigger_required": "Code trigger required", "command_template": "Command template", "command_topic": "Command topic", "command_off_template": "Command \"off\" template", @@ -341,10 +349,14 @@ "data_description": { "blue_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract blue color from the state payload value. Expected result of the template is an integer from 0-255 range.", "brightness_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract brightness from the state payload value. Expected result of the template is an integer from 0-255 range.", + "code": "Specifies a code to enable or disable the alarm in the frontend. Note that this blocks sending MQTT message commands to the remote device if the code validation fails. [Learn more.]({url}#code)", "code_format": "A regular expression to validate a supplied code when it is set during the action to open, lock or unlock the MQTT lock. [Learn more.]({url}#code_format)", + "code_arm_required": "If set, the code is required to arm the alarm. If not set, the code is not validated.", + "code_disarm_required": "If set, the code is required to disarm the alarm. If not set, the code is not validated.", + "code_trigger_required": "If set, the code is required to manually trigger the alarm. If not set, the code is not validated.", "command_off_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"off\" state changes. Available variables are: `state` and `transition`.", "command_on_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"on\" state changes. Available variables: `state`, `brightness`, `color_temp`, `red`, `green`, `blue`, `hue`, `sat`, `flash`, `transition` and `effect`. Values `red`, `green`, `blue` and `brightness` are provided as integers from range 0-255. Value of `hue` is provided as float from range 0-360. Value of `sat` is provided as float from range 0-100. Value of `color_temp` is provided as integer representing Kelvin units.", - "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.", + "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. [Learn more.]({url}#command_template)", "command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)", "color_temp_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract color temperature in Kelvin from the state payload value. Expected result of the template is an integer.", "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)", @@ -394,6 +406,27 @@ "transition": "Enable the transition feature for this light" } }, + "alarm_control_panel_payload_settings": { + "name": "Alarm control panel payload settings", + "data": { + "payload_arm_away": "Payload \"arm away\"", + "payload_arm_custom_bypass": "Payload \"arm custom bypass\"", + "payload_arm_disarm": "Payload \"disarm\"", + "payload_arm_home": "Payload \"arm home\"", + "payload_arm_night": "Payload \"arm night\"", + "payload_arm_vacation": "Payload \"arm vacation\"", + "payload_trigger": "Payload \"trigger alarm\"" + }, + "data_description": { + "payload_arm_away": "The payload sent when an \"arm away\" command is issued.", + "payload_arm_custom_bypass": "The payload sent when an \"arm custom bypass\" command is issued.", + "payload_arm_disarm": "The payload sent when a \"disarm\" command is issued.", + "payload_arm_home": "The payload sent when an \"arm home\" command is issued.", + "payload_arm_night": "The payload sent when an \"arm night\" command is issued.", + "payload_arm_vacation": "The payload sent when an \"arm vacation\" command is issued.", + "payload_trigger": "The payload sent when a \"trigger alarm\" command is issued." + } + }, "climate_action_settings": { "name": "Current action settings", "data": { @@ -1070,6 +1103,23 @@ } }, "selector": { + "alarm_control_panel_code_mode": { + "options": { + "local_code": "Local code validation", + "remote_code": "Numeric remote code validation", + "remote_code_text": "Text remote code validation" + } + }, + "alarm_control_panel_features": { + "options": { + "arm_away": "[%key:component::alarm_control_panel::services::alarm_arm_away::name%]", + "arm_custom_bypass": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::name%]", + "arm_home": "[%key:component::alarm_control_panel::services::alarm_arm_home::name%]", + "arm_night": "[%key:component::alarm_control_panel::services::alarm_arm_night::name%]", + "arm_vacation": "[%key:component::alarm_control_panel::services::alarm_arm_vacation::name%]", + "trigger": "[%key:component::alarm_control_panel::services::alarm_trigger::name%]" + } + }, "climate_modes": { "options": { "off": "[%key:common::state::off%]", @@ -1223,6 +1273,7 @@ }, "platform": { "options": { + "alarm_control_panel": "[%key:component::alarm_control_panel::title%]", "binary_sensor": "[%key:component::binary_sensor::title%]", "button": "[%key:component::button::title%]", "climate": "[%key:component::climate::title%]", diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index b3a93ec0cf2..417b1465aa3 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -70,6 +70,78 @@ DEFAULT_CONFIG_DEVICE_INFO_MAC = { "configuration_url": "http://example.com", } +MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_LOCAL_CODE = { + "4b06357ef8654e8d9c54cee5bb0e9391": { + "platform": "alarm_control_panel", + "name": "Alarm", + "entity_category": "config", + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code": "1234", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + "supported_features": ["arm_home", "arm_away", "trigger"], + "retain": False, + "entity_picture": "https://example.com/4b06357ef8654e8d9c54cee5bb0e9391", + }, +} +MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_REMOTE_CODE = { + "4b06357ef8654e8d9c54cee5bb0e9392": { + "platform": "alarm_control_panel", + "name": "Alarm", + "entity_category": None, + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code": "REMOTE_CODE", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + "supported_features": ["arm_home", "arm_away", "arm_custom_bypass"], + "retain": False, + "entity_picture": "https://example.com/4b06357ef8654e8d9c54cee5bb0e9392", + }, +} +MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_REMOTE_CODE_TEXT = { + "4b06357ef8654e8d9c54cee5bb0e9393": { + "platform": "alarm_control_panel", + "name": "Alarm", + "entity_category": None, + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code": "REMOTE_CODE_TEXT", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + "supported_features": ["arm_home", "arm_away", "arm_vacation"], + "retain": False, + "entity_picture": "https://example.com/4b06357ef8654e8d9c54cee5bb0e9393", + }, +} MOCK_SUBENTRY_BINARY_SENSOR_COMPONENT = { "5b06357ef8654e8d9c54cee5bb0e939b": { "platform": "binary_sensor", @@ -444,6 +516,18 @@ MOCK_NOTIFY_SUBENTRY_DATA_MULTI = { "components": MOCK_SUBENTRY_NOTIFY_COMPONENT1 | MOCK_SUBENTRY_NOTIFY_COMPONENT2, } | MOCK_SUBENTRY_AVAILABILITY_DATA +MOCK_ALARM_CONTROL_PANEL_LOCAL_CODE_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, + "components": MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_LOCAL_CODE, +} +MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_TEXT_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}}, + "components": MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_REMOTE_CODE_TEXT, +} +MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}}, + "components": MOCK_SUBENTRY_ALARM_CONTROL_PANEL_COMPONENT_REMOTE_CODE, +} MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 2}}, "components": MOCK_SUBENTRY_BINARY_SENSOR_COMPONENT, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 1c99d9da45f..b46b1557aee 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -33,6 +33,9 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .common import ( + MOCK_ALARM_CONTROL_PANEL_LOCAL_CODE_SUBENTRY_DATA_SINGLE, + MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_SUBENTRY_DATA_SINGLE, + MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_TEXT_SUBENTRY_DATA_SINGLE, MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_BUTTON_SUBENTRY_DATA_SINGLE, MOCK_CLIMATE_HIGH_LOW_SUBENTRY_DATA_SINGLE, @@ -2665,6 +2668,113 @@ async def test_migrate_of_incompatible_config_entry( "entity_name", ), [ + ( + MOCK_ALARM_CONTROL_PANEL_LOCAL_CODE_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Alarm"}, + { + "entity_category": "config", + "supported_features": ["arm_home", "arm_away", "trigger"], + "alarm_control_panel_code_mode": "local_code", + }, + (), + { + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code": "1234", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "retain": False, + "alarm_control_panel_payload_settings": { + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + }, + }, + ( + ( + { + "state_topic": "test-topic", + "command_topic": "test-topic#invalid", + }, + {"command_topic": "invalid_publish_topic"}, + ), + ( + { + "command_topic": "test-topic", + "state_topic": "test-topic#invalid", + }, + {"state_topic": "invalid_subscribe_topic"}, + ), + ), + "Milk notifier Alarm", + ), + ( + MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 1}}, + {"name": "Alarm"}, + { + "supported_features": ["arm_home", "arm_away", "arm_custom_bypass"], + "alarm_control_panel_code_mode": "remote_code", + }, + (), + { + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "retain": False, + "alarm_control_panel_payload_settings": { + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + }, + }, + (), + "Milk notifier Alarm", + ), + ( + MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_TEXT_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 2}}, + {"name": "Alarm"}, + { + "supported_features": ["arm_home", "arm_away", "arm_vacation"], + "alarm_control_panel_code_mode": "remote_code_text", + }, + (), + { + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{action}}", + "value_template": "{{ value_json.value }}", + "code_arm_required": True, + "code_disarm_required": True, + "code_trigger_required": True, + "retain": False, + "alarm_control_panel_payload_settings": { + "payload_arm_away": "ARM_AWAY", + "payload_arm_custom_bypass": "ARM_CUSTOM_BYPASS", + "payload_arm_home": "ARM_HOME", + "payload_arm_night": "ARM_NIGHT", + "payload_arm_vacation": "ARM_VACATION", + "payload_trigger": "TRIGGER", + }, + }, + (), + "Milk notifier Alarm", + ), ( MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, {"name": "Milk notifier", "mqtt_settings": {"qos": 2}}, @@ -3399,6 +3509,9 @@ async def test_migrate_of_incompatible_config_entry( # MOCK_LOCK_SUBENTRY_DATA_SINGLE ], ids=[ + "alarm_control_panel_local_code", + "alarm_control_panel_remote_code", + "alarm_control_panel_remote_code_text", "binary_sensor", "button", "climate_single", @@ -3830,6 +3943,67 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites( "removed_options", ), [ + ( + ( + ConfigSubentryData( + data=MOCK_ALARM_CONTROL_PANEL_LOCAL_CODE_SUBENTRY_DATA_SINGLE, + subentry_type="device", + title="Mock subentry", + ), + ), + (), + { + "alarm_control_panel_code_mode": "remote_code", + "supported_features": ["arm_home", "arm_away", "arm_custom_bypass"], + }, + { + "command_topic": "test-topic1-updated", + "command_template": "{{ value }}", + "state_topic": "test-topic1-updated", + "value_template": "{{ value }}", + "retain": True, + }, + { + "command_topic": "test-topic1-updated", + "command_template": "{{ value }}", + "state_topic": "test-topic1-updated", + "value_template": "{{ value }}", + "retain": True, + "code": "REMOTE_CODE", + }, + {"entity_picture"}, + ), + ( + ( + ConfigSubentryData( + data=MOCK_ALARM_CONTROL_PANEL_REMOTE_CODE_SUBENTRY_DATA_SINGLE, + subentry_type="device", + title="Mock subentry", + ), + ), + (), + { + "alarm_control_panel_code_mode": "local_code", + "supported_features": ["arm_home", "arm_away", "arm_custom_bypass"], + }, + { + "command_topic": "test-topic1-updated", + "command_template": "{{ value }}", + "state_topic": "test-topic1-updated", + "value_template": "{{ value }}", + "code": "1234", + "retain": True, + }, + { + "command_topic": "test-topic1-updated", + "command_template": "{{ value }}", + "state_topic": "test-topic1-updated", + "value_template": "{{ value }}", + "code": "1234", + "retain": True, + }, + {"entity_picture"}, + ), ( ( ConfigSubentryData( @@ -4053,7 +4227,15 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites( {"entity_picture"}, ), ], - ids=["notify", "sensor", "light_basic", "climate_single", "climate_high_low"], + ids=[ + "alarm_control_panel_local_code", + "alarm_control_panel_remote_code", + "notify", + "sensor", + "light_basic", + "climate_single", + "climate_high_low", + ], ) async def test_subentry_reconfigure_edit_entity_single_entity( hass: HomeAssistant, @@ -4123,7 +4305,6 @@ async def test_subentry_reconfigure_edit_entity_single_entity( user_input={}, ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "entity_platform_config" # entity platform config flow step assert result["step_id"] == "entity_platform_config"