diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 03f758dbdce..a8a4c2e9538 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -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, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 1dfdb8dac53..2128b55c4b0 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -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" diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 727e689798e..00771ce521f 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -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( { diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 77a476bf40c..3844cf8d669 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -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%]" diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index fdaed0c323f..b3a93ec0cf2 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -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, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 3b4f090aef3..1c99d9da45f 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -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(