Add MQTT alarm control panel subentry support (#150395)

Co-authored-by: Norbert Rittel <norbert@rittel.de>
This commit is contained in:
Jan Bouwhuis
2025-08-27 11:00:31 +02:00
committed by GitHub
parent e894a03c43
commit 0d29b2d5a7
6 changed files with 578 additions and 56 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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"

View File

@@ -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 hasnt 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%]",

View File

@@ -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,

View File

@@ -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"