Add MQTT lock subentry support (#150860)

Co-authored-by: Norbert Rittel <norbert@rittel.de>
This commit is contained in:
Jan Bouwhuis
2025-08-25 15:37:36 +02:00
committed by GitHub
parent 58d4fd0b75
commit dea5e7454a
6 changed files with 252 additions and 24 deletions

View File

@@ -149,6 +149,7 @@ from .const import (
CONF_CERTIFICATE,
CONF_CLIENT_CERT,
CONF_CLIENT_KEY,
CONF_CODE_FORMAT,
CONF_COLOR_MODE_STATE_TOPIC,
CONF_COLOR_MODE_VALUE_TEMPLATE,
CONF_COLOR_TEMP_COMMAND_TEMPLATE,
@@ -217,15 +218,18 @@ from .const import (
CONF_OSCILLATION_VALUE_TEMPLATE,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_CLOSE,
CONF_PAYLOAD_LOCK,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_PAYLOAD_OPEN,
CONF_PAYLOAD_OSCILLATION_OFF,
CONF_PAYLOAD_OSCILLATION_ON,
CONF_PAYLOAD_PRESS,
CONF_PAYLOAD_RESET,
CONF_PAYLOAD_RESET_PERCENTAGE,
CONF_PAYLOAD_RESET_PRESET_MODE,
CONF_PAYLOAD_STOP,
CONF_PAYLOAD_STOP_TILT,
CONF_PAYLOAD_UNLOCK,
CONF_PERCENTAGE_COMMAND_TEMPLATE,
CONF_PERCENTAGE_COMMAND_TOPIC,
CONF_PERCENTAGE_STATE_TOPIC,
@@ -262,12 +266,17 @@ from .const import (
CONF_SPEED_RANGE_MIN,
CONF_STATE_CLOSED,
CONF_STATE_CLOSING,
CONF_STATE_JAMMED,
CONF_STATE_LOCKED,
CONF_STATE_LOCKING,
CONF_STATE_OFF,
CONF_STATE_ON,
CONF_STATE_OPEN,
CONF_STATE_OPENING,
CONF_STATE_STOPPED,
CONF_STATE_TOPIC,
CONF_STATE_UNLOCKED,
CONF_STATE_UNLOCKING,
CONF_STATE_VALUE_TEMPLATE,
CONF_SUGGESTED_DISPLAY_PRECISION,
CONF_SUPPORTED_COLOR_MODES,
@@ -328,6 +337,7 @@ from .const import (
DEFAULT_ON_COMMAND_TYPE,
DEFAULT_PAYLOAD_AVAILABLE,
DEFAULT_PAYLOAD_CLOSE,
DEFAULT_PAYLOAD_LOCK,
DEFAULT_PAYLOAD_NOT_AVAILABLE,
DEFAULT_PAYLOAD_OFF,
DEFAULT_PAYLOAD_ON,
@@ -337,6 +347,7 @@ from .const import (
DEFAULT_PAYLOAD_PRESS,
DEFAULT_PAYLOAD_RESET,
DEFAULT_PAYLOAD_STOP,
DEFAULT_PAYLOAD_UNLOCK,
DEFAULT_PORT,
DEFAULT_POSITION_CLOSED,
DEFAULT_POSITION_OPEN,
@@ -345,7 +356,12 @@ from .const import (
DEFAULT_QOS,
DEFAULT_SPEED_RANGE_MAX,
DEFAULT_SPEED_RANGE_MIN,
DEFAULT_STATE_JAMMED,
DEFAULT_STATE_LOCKED,
DEFAULT_STATE_LOCKING,
DEFAULT_STATE_STOPPED,
DEFAULT_STATE_UNLOCKED,
DEFAULT_STATE_UNLOCKING,
DEFAULT_TILT_CLOSED_POSITION,
DEFAULT_TILT_MAX,
DEFAULT_TILT_MIN,
@@ -458,6 +474,7 @@ SUBENTRY_PLATFORMS = [
Platform.COVER,
Platform.FAN,
Platform.LIGHT,
Platform.LOCK,
Platform.NOTIFY,
Platform.SENSOR,
Platform.SWITCH,
@@ -1148,6 +1165,7 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
is_schema_default=True,
),
},
Platform.LOCK.value: {},
}
PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = {
Platform.BINARY_SENSOR.value: {
@@ -2664,6 +2682,93 @@ PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = {
section="advanced_settings",
),
},
Platform.LOCK.value: {
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,
validator=validate(cv.template),
error="invalid_template",
),
CONF_STATE_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=False,
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_FORMAT: PlatformField(
selector=TEXT_SELECTOR,
required=False,
validator=validate(cv.is_regex),
error="invalid_regular_expression",
),
CONF_PAYLOAD_LOCK: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_PAYLOAD_LOCK,
section="lock_payload_settings",
),
CONF_PAYLOAD_UNLOCK: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_PAYLOAD_UNLOCK,
section="lock_payload_settings",
),
CONF_PAYLOAD_OPEN: PlatformField(
selector=TEXT_SELECTOR,
required=False,
section="lock_payload_settings",
),
CONF_PAYLOAD_RESET: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_PAYLOAD_RESET,
section="lock_payload_settings",
),
CONF_STATE_LOCKED: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_STATE_LOCKED,
section="lock_payload_settings",
),
CONF_STATE_UNLOCKED: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_STATE_UNLOCKED,
section="lock_payload_settings",
),
CONF_STATE_LOCKING: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_STATE_LOCKING,
section="lock_payload_settings",
),
CONF_STATE_UNLOCKING: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_STATE_UNLOCKING,
section="lock_payload_settings",
),
CONF_STATE_JAMMED: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_STATE_JAMMED,
section="lock_payload_settings",
),
CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
},
}
ENTITY_CONFIG_VALIDATOR: dict[
str,
@@ -2675,6 +2780,7 @@ ENTITY_CONFIG_VALIDATOR: dict[
Platform.COVER.value: validate_cover_platform_config,
Platform.FAN.value: validate_fan_platform_config,
Platform.LIGHT.value: validate_light_platform_config,
Platform.LOCK.value: None,
Platform.NOTIFY.value: None,
Platform.SENSOR.value: validate_sensor_platform_config,
Platform.SWITCH.value: None,

View File

@@ -31,6 +31,7 @@ CONF_AVAILABILITY_TEMPLATE = "availability_template"
CONF_AVAILABILITY_TOPIC = "availability_topic"
CONF_BROKER = "broker"
CONF_BIRTH_MESSAGE = "birth_message"
CONF_CODE_FORMAT = "code_format"
CONF_COMMAND_TEMPLATE = "command_template"
CONF_COMMAND_TOPIC = "command_topic"
CONF_DISCOVERY_PREFIX = "discovery_prefix"
@@ -127,6 +128,7 @@ CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template"
CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"
CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template"
CONF_PAYLOAD_CLOSE = "payload_close"
CONF_PAYLOAD_LOCK = "payload_lock"
CONF_PAYLOAD_OPEN = "payload_open"
CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off"
CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on"
@@ -135,6 +137,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_UNLOCK = "payload_unlock"
CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"
CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"
CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"
@@ -168,11 +171,16 @@ CONF_SPEED_RANGE_MAX = "speed_range_max"
CONF_SPEED_RANGE_MIN = "speed_range_min"
CONF_STATE_CLOSED = "state_closed"
CONF_STATE_CLOSING = "state_closing"
CONF_STATE_JAMMED = "state_jammed"
CONF_STATE_LOCKED = "state_locked"
CONF_STATE_LOCKING = "state_locking"
CONF_STATE_OFF = "state_off"
CONF_STATE_ON = "state_on"
CONF_STATE_OPEN = "state_open"
CONF_STATE_OPENING = "state_opening"
CONF_STATE_STOPPED = "state_stopped"
CONF_STATE_UNLOCKED = "state_unlocked"
CONF_STATE_UNLOCKING = "state_unlocking"
CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision"
CONF_SUPPORTED_COLOR_MODES = "supported_color_modes"
CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE = "swing_horizontal_mode_command_template"
@@ -254,6 +262,7 @@ DEFAULT_ON_COMMAND_TYPE = "last"
DEFAULT_QOS = 0
DEFAULT_PAYLOAD_AVAILABLE = "online"
DEFAULT_PAYLOAD_CLOSE = "CLOSE"
DEFAULT_PAYLOAD_LOCK = "LOCK"
DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline"
DEFAULT_PAYLOAD_OFF = "OFF"
DEFAULT_PAYLOAD_ON = "ON"
@@ -263,6 +272,8 @@ DEFAULT_PAYLOAD_OSCILLATE_ON = "oscillate_on"
DEFAULT_PAYLOAD_PRESS = "PRESS"
DEFAULT_PAYLOAD_STOP = "STOP"
DEFAULT_PAYLOAD_RESET = "None"
DEFAULT_PAYLOAD_UNLOCK = "UNLOCK"
DEFAULT_PORT = 1883
DEFAULT_RETAIN = False
DEFAULT_TILT_CLOSED_POSITION = 0
@@ -277,7 +288,14 @@ DEFAULT_POSITION_OPEN = 100
DEFAULT_RETAIN = False
DEFAULT_SPEED_RANGE_MAX = 100
DEFAULT_SPEED_RANGE_MIN = 1
DEFAULT_STATE_LOCKED = "LOCKED"
DEFAULT_STATE_LOCKING = "LOCKING"
DEFAULT_STATE_OPEN = "OPEN"
DEFAULT_STATE_OPENING = "OPENING"
DEFAULT_STATE_STOPPED = "stopped"
DEFAULT_STATE_UNLOCKED = "UNLOCKED"
DEFAULT_STATE_UNLOCKING = "UNLOCKING"
DEFAULT_STATE_JAMMED = "JAMMED"
DEFAULT_WHITE_SCALE = 255
COVER_PAYLOAD = "cover"

View File

@@ -27,12 +27,31 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import subscription
from .config import MQTT_RW_SCHEMA
from .const import (
CONF_CODE_FORMAT,
CONF_COMMAND_TEMPLATE,
CONF_COMMAND_TOPIC,
CONF_PAYLOAD_LOCK,
CONF_PAYLOAD_OPEN,
CONF_PAYLOAD_RESET,
CONF_PAYLOAD_UNLOCK,
CONF_STATE_JAMMED,
CONF_STATE_LOCKED,
CONF_STATE_LOCKING,
CONF_STATE_OPEN,
CONF_STATE_OPENING,
CONF_STATE_TOPIC,
CONF_STATE_UNLOCKED,
CONF_STATE_UNLOCKING,
DEFAULT_PAYLOAD_LOCK,
DEFAULT_PAYLOAD_RESET,
DEFAULT_PAYLOAD_UNLOCK,
DEFAULT_STATE_JAMMED,
DEFAULT_STATE_LOCKED,
DEFAULT_STATE_LOCKING,
DEFAULT_STATE_OPEN,
DEFAULT_STATE_OPENING,
DEFAULT_STATE_UNLOCKED,
DEFAULT_STATE_UNLOCKING,
)
from .entity import MqttEntity, async_setup_entity_entry_helper
from .models import (
@@ -47,31 +66,7 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
CONF_CODE_FORMAT = "code_format"
CONF_PAYLOAD_LOCK = "payload_lock"
CONF_PAYLOAD_UNLOCK = "payload_unlock"
CONF_PAYLOAD_OPEN = "payload_open"
CONF_STATE_LOCKED = "state_locked"
CONF_STATE_LOCKING = "state_locking"
CONF_STATE_UNLOCKED = "state_unlocked"
CONF_STATE_UNLOCKING = "state_unlocking"
CONF_STATE_JAMMED = "state_jammed"
DEFAULT_NAME = "MQTT Lock"
DEFAULT_PAYLOAD_LOCK = "LOCK"
DEFAULT_PAYLOAD_UNLOCK = "UNLOCK"
DEFAULT_PAYLOAD_OPEN = "OPEN"
DEFAULT_PAYLOAD_RESET = "None"
DEFAULT_STATE_LOCKED = "LOCKED"
DEFAULT_STATE_LOCKING = "LOCKING"
DEFAULT_STATE_OPEN = "OPEN"
DEFAULT_STATE_OPENING = "OPENING"
DEFAULT_STATE_UNLOCKED = "UNLOCKED"
DEFAULT_STATE_UNLOCKING = "UNLOCKING"
DEFAULT_STATE_JAMMED = "JAMMED"
MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset(
{

View File

@@ -308,6 +308,7 @@
"data": {
"blue_template": "Blue template",
"brightness_template": "Brightness template",
"code_format": "Code format",
"command_template": "Command template",
"command_topic": "Command topic",
"command_off_template": "Command \"off\" template",
@@ -340,6 +341,7 @@
"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_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)",
"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.",
@@ -596,6 +598,31 @@
"brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value."
}
},
"lock_payload_settings": {
"name": "Lock payload settings",
"data": {
"payload_lock": "Payload \"lock\"",
"payload_open": "Payload \"open\"",
"payload_reset": "Payload \"reset\"",
"payload_unlock": "Payload \"unlock\"",
"state_jammed": "State \"jammed\"",
"state_locked": "State \"locked\"",
"state_locking": "State \"locking\"",
"state_unlocked": "State \"unlocked\"",
"state_unlocking": "State \"unlocking\""
},
"data_description": {
"payload_lock": "The payload sent when a \"lock\" command is issued.",
"payload_open": "The payload sent when an \"open\" command is issued. Set this payload if your lock supports the \"open\" action.",
"payload_reset": "The payload received at the state topic that resets the lock to an unknown state.",
"payload_unlock": "The payload sent when an \"unlock\" command is issued.",
"state_jammed": "The payload received at the state topic that represents the \"jammed\" state.",
"state_locked": "The payload received at the state topic that represents the \"locked\" state.",
"state_locking": "The payload received at the state topic that represents the \"locking\" state.",
"state_unlocked": "The payload received at the state topic that represents the \"unlocked\" state.",
"state_unlocking": "The payload received at the state topic that represents the \"unlocking\" state."
}
},
"fan_direction_settings": {
"name": "Direction settings",
"data": {
@@ -911,6 +938,7 @@
"fan_speed_range_max_must_be_greater_than_speed_range_min": "Speed range max must be greater than speed range min",
"fan_preset_mode_reset_in_preset_modes_list": "Payload \"reset preset mode\" is not a valid as a preset mode",
"invalid_input": "Invalid value",
"invalid_regular_expression": "Must be a valid regular expression",
"invalid_subscribe_topic": "Invalid subscribe topic",
"invalid_template": "Invalid template",
"invalid_supported_color_modes": "Invalid supported color modes selection",
@@ -1201,6 +1229,7 @@
"cover": "[%key:component::cover::title%]",
"fan": "[%key:component::fan::title%]",
"light": "[%key:component::light::title%]",
"lock": "[%key:component::lock::title%]",
"notify": "[%key:component::notify::title%]",
"sensor": "[%key:component::sensor::title%]",
"switch": "[%key:component::switch::title%]"

View File

@@ -316,6 +316,31 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME = {
},
}
MOCK_SUBENTRY_LOCK_COMPONENT = {
"3faf1318016c46c5aea26707eeb6f100": {
"platform": "lock",
"name": "Lock",
"command_topic": "test-topic",
"state_topic": "test-topic",
"command_template": "{{ value }}",
"value_template": "{{ value_json.value }}",
"code_format": "^\\d{4}$",
"payload_open": "OPEN",
"payload_lock": "LOCK",
"payload_unlock": "UNLOCK",
"payload_reset": "None",
"state_jammed": "JAMMED",
"state_locked": "LOCKED",
"state_locking": "LOCKING",
"state_unlocked": "UNLOCKED",
"state_unlocking": "UNLOCKING",
"retain": False,
"entity_category": None,
"entity_picture": "https://example.com/3faf1318016c46c5aea26707eeb6f100",
"optimistic": True,
},
}
MOCK_SUBENTRY_SENSOR_COMPONENT = {
"e9261f6feed443e7b7d5f3fbe2a47412": {
"platform": "sensor",
@@ -459,6 +484,10 @@ MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_LIGHT_BASIC_KELVIN_COMPONENT,
}
MOCK_LOCK_SUBENTRY_DATA_SINGLE = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_LOCK_COMPONENT,
}
MOCK_SENSOR_SUBENTRY_DATA_SINGLE = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_SENSOR_COMPONENT,

View File

@@ -41,6 +41,7 @@ from .common import (
MOCK_COVER_SUBENTRY_DATA_SINGLE,
MOCK_FAN_SUBENTRY_DATA_SINGLE,
MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE,
MOCK_LOCK_SUBENTRY_DATA_SINGLE,
MOCK_NOTIFY_SUBENTRY_DATA_MULTI,
MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME,
MOCK_NOTIFY_SUBENTRY_DATA_SINGLE,
@@ -3347,6 +3348,55 @@ async def test_migrate_of_incompatible_config_entry(
),
"Milk notifier Basic light",
),
(
MOCK_LOCK_SUBENTRY_DATA_SINGLE,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
{"name": "Lock"},
{},
(),
{
"command_topic": "test-topic",
"command_template": "{{ value }}",
"state_topic": "test-topic",
"value_template": "{{ value_json.value }}",
"code_format": "^\\d{4}$",
"optimistic": True,
"retain": False,
"lock_payload_settings": {
"payload_open": "OPEN",
"payload_lock": "LOCK",
"payload_unlock": "UNLOCK",
"payload_reset": "None",
"state_jammed": "JAMMED",
"state_locked": "LOCKED",
"state_locking": "LOCKING",
"state_unlocked": "UNLOCKED",
"state_unlocking": "UNLOCKING",
},
},
(
(
{"command_topic": "test-topic#invalid"},
{"command_topic": "invalid_publish_topic"},
),
(
{
"command_topic": "test-topic",
"state_topic": "test-topic#invalid",
},
{"state_topic": "invalid_subscribe_topic"},
),
(
{
"command_topic": "test-topic",
"code_format": "(",
},
{"code_format": "invalid_regular_expression"},
),
),
"Milk notifier Lock",
),
# MOCK_LOCK_SUBENTRY_DATA_SINGLE
],
ids=[
"binary_sensor",
@@ -3362,6 +3412,7 @@ async def test_migrate_of_incompatible_config_entry(
"sensor_total",
"switch",
"light_basic_kelvin",
"lock",
],
)
async def test_subentry_configflow(