core/homeassistant/components/mqtt/alarm_control_panel.py

349 lines
13 KiB
Python
Raw Normal View History

"""Control a MQTT alarm."""
from __future__ import annotations
import functools
2015-09-18 15:30:34 +00:00
import logging
import re
2015-09-18 15:30:34 +00:00
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
2019-07-31 19:25:30 +00:00
CONF_CODE,
CONF_NAME,
CONF_VALUE_TEMPLATE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
2019-07-31 19:25:30 +00:00
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_VACATION,
STATE_ALARM_ARMING,
2019-07-31 19:25:30 +00:00
STATE_ALARM_DISARMED,
STATE_ALARM_DISARMING,
2019-07-31 19:25:30 +00:00
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
2015-09-18 15:30:34 +00:00
from . import subscription
from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
from .const import (
CONF_COMMAND_TEMPLATE,
CONF_COMMAND_TOPIC,
CONF_ENCODING,
CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC,
Add option to disable MQTT Alarm Control Panel supported features (#98363) * Make MQTT Alarm Control Panel features conditional The MQTT Alarm Control Panel currently enables all features (arm home, arm away, arm night, arm vacation, arm custom bypass) unconditionally. This clutters the interface and can even be potentially dangerous, by enabling modes that the remote alarm may not support. Make all the features conditional, by adding a new "supported_features" configuration option, comprising a list of the supported features as options. Feature enablement seems inconsistent across the MQTT component; this implementation is most alike to the Humidifier modes option, but using a generic "supported_features" name that other implementations may reuse in the future. The default value of this new setting remains to be all features, which while it may be overly expansive, is necessary to maintain backwards compatibility. * Apply suggestions from code review * Use vol.Optional() instead of vol.Required() for "supported_features". * Move the initialization of _attr_supported_features to _setup_from_config. Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> * Apply suggestions from emontnemery's code review * Use vol.In() instead of cv.multi_seelct() * Remove superfluous _attr_supported_features initializers, already present in the base class. Co-authored-by: Erik Montnemery <erik@montnemery.com> * Add invalid config tests for the MQTT Alarm Control Panel * Set expected_features to None in the invalid MQTT Alarm Control Panel tests * Add another expected_features=None in the invalid tests Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> --------- Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> Co-authored-by: Erik Montnemery <erik@montnemery.com>
2023-08-18 06:23:48 +00:00
CONF_SUPPORTED_FEATURES,
)
from .debug_info import log_messages
from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic
2015-09-18 15:30:34 +00:00
_LOGGER = logging.getLogger(__name__)
Add option to disable MQTT Alarm Control Panel supported features (#98363) * Make MQTT Alarm Control Panel features conditional The MQTT Alarm Control Panel currently enables all features (arm home, arm away, arm night, arm vacation, arm custom bypass) unconditionally. This clutters the interface and can even be potentially dangerous, by enabling modes that the remote alarm may not support. Make all the features conditional, by adding a new "supported_features" configuration option, comprising a list of the supported features as options. Feature enablement seems inconsistent across the MQTT component; this implementation is most alike to the Humidifier modes option, but using a generic "supported_features" name that other implementations may reuse in the future. The default value of this new setting remains to be all features, which while it may be overly expansive, is necessary to maintain backwards compatibility. * Apply suggestions from code review * Use vol.Optional() instead of vol.Required() for "supported_features". * Move the initialization of _attr_supported_features to _setup_from_config. Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> * Apply suggestions from emontnemery's code review * Use vol.In() instead of cv.multi_seelct() * Remove superfluous _attr_supported_features initializers, already present in the base class. Co-authored-by: Erik Montnemery <erik@montnemery.com> * Add invalid config tests for the MQTT Alarm Control Panel * Set expected_features to None in the invalid MQTT Alarm Control Panel tests * Add another expected_features=None in the invalid tests Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> --------- Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> Co-authored-by: Erik Montnemery <erik@montnemery.com>
2023-08-18 06:23:48 +00:00
_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,
}
2019-07-31 19:25:30 +00:00
CONF_CODE_ARM_REQUIRED = "code_arm_required"
CONF_CODE_DISARM_REQUIRED = "code_disarm_required"
CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required"
2019-07-31 19:25:30 +00:00
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"
2019-07-31 19:25:30 +00:00
MQTT_ALARM_ATTRIBUTES_BLOCKED = frozenset(
{
alarm.ATTR_CHANGED_BY,
alarm.ATTR_CODE_ARM_REQUIRED,
alarm.ATTR_CODE_FORMAT,
}
)
2019-07-31 19:25:30 +00:00
DEFAULT_COMMAND_TEMPLATE = "{{action}}"
DEFAULT_ARM_NIGHT = "ARM_NIGHT"
DEFAULT_ARM_VACATION = "ARM_VACATION"
2019-07-31 19:25:30 +00:00
DEFAULT_ARM_AWAY = "ARM_AWAY"
DEFAULT_ARM_HOME = "ARM_HOME"
DEFAULT_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS"
2019-07-31 19:25:30 +00:00
DEFAULT_DISARM = "DISARM"
DEFAULT_TRIGGER = "TRIGGER"
2019-07-31 19:25:30 +00:00
DEFAULT_NAME = "MQTT Alarm"
REMOTE_CODE = "REMOTE_CODE"
REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT"
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
{
Add option to disable MQTT Alarm Control Panel supported features (#98363) * Make MQTT Alarm Control Panel features conditional The MQTT Alarm Control Panel currently enables all features (arm home, arm away, arm night, arm vacation, arm custom bypass) unconditionally. This clutters the interface and can even be potentially dangerous, by enabling modes that the remote alarm may not support. Make all the features conditional, by adding a new "supported_features" configuration option, comprising a list of the supported features as options. Feature enablement seems inconsistent across the MQTT component; this implementation is most alike to the Humidifier modes option, but using a generic "supported_features" name that other implementations may reuse in the future. The default value of this new setting remains to be all features, which while it may be overly expansive, is necessary to maintain backwards compatibility. * Apply suggestions from code review * Use vol.Optional() instead of vol.Required() for "supported_features". * Move the initialization of _attr_supported_features to _setup_from_config. Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> * Apply suggestions from emontnemery's code review * Use vol.In() instead of cv.multi_seelct() * Remove superfluous _attr_supported_features initializers, already present in the base class. Co-authored-by: Erik Montnemery <erik@montnemery.com> * Add invalid config tests for the MQTT Alarm Control Panel * Set expected_features to None in the invalid MQTT Alarm Control Panel tests * Add another expected_features=None in the invalid tests Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> --------- Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> Co-authored-by: Erik Montnemery <erik@montnemery.com>
2023-08-18 06:23:48 +00:00
vol.Optional(CONF_SUPPORTED_FEATURES, default=list(_SUPPORTED_FEATURES)): [
vol.In(_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
): 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
): cv.string,
vol.Optional(
CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_ARM_CUSTOM_BYPASS
): 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_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
2019-07-31 19:25:30 +00:00
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)
2019-07-31 19:25:30 +00:00
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT alarm control panel through YAML and through MQTT discovery."""
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
2019-07-31 19:25:30 +00:00
)
await async_setup_entry_helper(hass, alarm.DOMAIN, setup, DISCOVERY_SCHEMA)
2019-07-31 19:25:30 +00:00
async def _async_setup_entity(
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry,
discovery_data: DiscoveryInfoType | None = None,
) -> None:
"""Set up the MQTT Alarm Control Panel platform."""
async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)])
2015-09-18 15:30:34 +00:00
2021-01-09 16:46:53 +00:00
class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
"""Representation of a MQTT alarm status."""
2016-03-07 19:21:43 +00:00
_default_name = DEFAULT_NAME
_entity_id_format = alarm.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_ALARM_ATTRIBUTES_BLOCKED
def __init__(
self,
hass: HomeAssistant,
config: ConfigType,
config_entry: ConfigEntry,
discovery_data: DiscoveryInfoType | None,
) -> None:
"""Init the MQTT Alarm Control Panel."""
self._state: str | None = None
2021-01-09 16:46:53 +00:00
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
2015-09-18 15:30:34 +00:00
2021-01-09 16:46:53 +00:00
@staticmethod
def config_schema() -> vol.Schema:
2021-01-09 16:46:53 +00:00
"""Return the config schema."""
return DISCOVERY_SCHEMA
def _setup_from_config(self, config: ConfigType) -> None:
"""(Re)Setup the entity."""
self._value_template = MqttValueTemplate(
config.get(CONF_VALUE_TEMPLATE),
entity=self,
).async_render_with_possible_json_value
self._command_template = MqttCommandTemplate(
config[CONF_COMMAND_TEMPLATE], entity=self
).async_render
Add option to disable MQTT Alarm Control Panel supported features (#98363) * Make MQTT Alarm Control Panel features conditional The MQTT Alarm Control Panel currently enables all features (arm home, arm away, arm night, arm vacation, arm custom bypass) unconditionally. This clutters the interface and can even be potentially dangerous, by enabling modes that the remote alarm may not support. Make all the features conditional, by adding a new "supported_features" configuration option, comprising a list of the supported features as options. Feature enablement seems inconsistent across the MQTT component; this implementation is most alike to the Humidifier modes option, but using a generic "supported_features" name that other implementations may reuse in the future. The default value of this new setting remains to be all features, which while it may be overly expansive, is necessary to maintain backwards compatibility. * Apply suggestions from code review * Use vol.Optional() instead of vol.Required() for "supported_features". * Move the initialization of _attr_supported_features to _setup_from_config. Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> * Apply suggestions from emontnemery's code review * Use vol.In() instead of cv.multi_seelct() * Remove superfluous _attr_supported_features initializers, already present in the base class. Co-authored-by: Erik Montnemery <erik@montnemery.com> * Add invalid config tests for the MQTT Alarm Control Panel * Set expected_features to None in the invalid MQTT Alarm Control Panel tests * Add another expected_features=None in the invalid tests Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> --------- Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> Co-authored-by: Erik Montnemery <erik@montnemery.com>
2023-08-18 06:23:48 +00:00
for feature in self._config[CONF_SUPPORTED_FEATURES]:
self._attr_supported_features |= _SUPPORTED_FEATURES[feature]
def _prepare_subscribe_topics(self) -> None:
"""(Re)Subscribe to topics."""
@callback
@log_messages(self.hass, self.entity_id)
def message_received(msg: ReceiveMessage) -> None:
"""Run when new MQTT message has been received."""
payload = self._value_template(msg.payload)
if payload not in (
2019-07-31 19:25:30 +00:00
STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_VACATION,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
2019-07-31 19:25:30 +00:00
STATE_ALARM_PENDING,
STATE_ALARM_ARMING,
STATE_ALARM_DISARMING,
2019-07-31 19:25:30 +00:00
STATE_ALARM_TRIGGERED,
):
_LOGGER.warning("Received unexpected payload: %s", msg.payload)
2015-10-14 06:08:12 +00:00
return
self._state = str(payload)
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
2015-09-18 15:30:34 +00:00
self._sub_state = subscription.async_prepare_subscribe_topics(
2019-07-31 19:25:30 +00:00
self.hass,
self._sub_state,
{
"state_topic": {
"topic": self._config[CONF_STATE_TOPIC],
"msg_callback": message_received,
"qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None,
2019-07-31 19:25:30 +00:00
}
},
)
async def _subscribe_topics(self) -> None:
"""(Re)Subscribe to topics."""
await subscription.async_subscribe_topics(self.hass, self._sub_state)
2015-09-18 15:30:34 +00:00
@property
def state(self) -> str | None:
2016-03-07 19:21:43 +00:00
"""Return the state of the device."""
2015-09-18 15:30:34 +00:00
return self._state
@property
def code_format(self) -> alarm.CodeFormat | None:
"""Return one or more digits/characters."""
code: str | None
2021-10-20 18:31:00 +00:00
if (code := self._config.get(CONF_CODE)) is None:
return None
if code == REMOTE_CODE or (isinstance(code, str) and re.search("^\\d+$", code)):
return alarm.CodeFormat.NUMBER
return alarm.CodeFormat.TEXT
@property
def code_arm_required(self) -> bool:
"""Whether the code is required for arm actions."""
return bool(self._config[CONF_CODE_ARM_REQUIRED])
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.
This method is a coroutine.
"""
code_required: bool = self._config[CONF_CODE_DISARM_REQUIRED]
2019-07-31 19:25:30 +00:00
if code_required and not self._validate_code(code, "disarming"):
2015-10-14 06:08:12 +00:00
return
payload: str = self._config[CONF_PAYLOAD_DISARM]
await self._publish(code, payload)
2015-09-18 15:30:34 +00:00
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command.
This method is a coroutine.
"""
code_required: bool = self._config[CONF_CODE_ARM_REQUIRED]
2019-07-31 19:25:30 +00:00
if code_required and not self._validate_code(code, "arming home"):
2015-10-14 06:08:12 +00:00
return
action: str = self._config[CONF_PAYLOAD_ARM_HOME]
await self._publish(code, action)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command.
2015-09-18 15:30:34 +00:00
This method is a coroutine.
"""
code_required: bool = self._config[CONF_CODE_ARM_REQUIRED]
2019-07-31 19:25:30 +00:00
if code_required and not self._validate_code(code, "arming away"):
2015-10-14 06:08:12 +00:00
return
action: str = self._config[CONF_PAYLOAD_ARM_AWAY]
await self._publish(code, action)
2015-10-14 06:08:12 +00:00
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command.
This method is a coroutine.
"""
code_required: bool = self._config[CONF_CODE_ARM_REQUIRED]
2019-07-31 19:25:30 +00:00
if code_required and not self._validate_code(code, "arming night"):
return
action: str = self._config[CONF_PAYLOAD_ARM_NIGHT]
await self._publish(code, action)
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
"""Send arm vacation command.
This method is a coroutine.
"""
code_required: bool = self._config[CONF_CODE_ARM_REQUIRED]
if code_required and not self._validate_code(code, "arming vacation"):
return
action: str = self._config[CONF_PAYLOAD_ARM_VACATION]
await self._publish(code, action)
async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
"""Send arm custom bypass command.
This method is a coroutine.
"""
code_required: bool = self._config[CONF_CODE_ARM_REQUIRED]
if code_required and not self._validate_code(code, "arming custom bypass"):
return
action: str = self._config[CONF_PAYLOAD_ARM_CUSTOM_BYPASS]
await self._publish(code, action)
async def async_alarm_trigger(self, code: str | None = None) -> None:
"""Send trigger command.
This method is a coroutine.
"""
code_required: bool = self._config[CONF_CODE_TRIGGER_REQUIRED]
if code_required and not self._validate_code(code, "triggering"):
return
action: str = self._config[CONF_PAYLOAD_TRIGGER]
await self._publish(code, action)
async def _publish(self, code: str | None, action: str) -> None:
"""Publish via mqtt."""
variables = {"action": action, "code": code}
payload = self._command_template(None, variables=variables)
2022-02-04 16:35:32 +00:00
await self.async_publish(
2019-07-31 19:25:30 +00:00
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
2019-07-31 19:25:30 +00:00
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
2019-07-31 19:25:30 +00:00
)
def _validate_code(self, code: str | None, state: str) -> bool:
2016-03-07 15:45:21 +00:00
"""Validate given code."""
conf_code: str | None = self._config.get(CONF_CODE)
check = bool(
conf_code is None
or code == conf_code
or (conf_code == REMOTE_CODE and code)
or (conf_code == REMOTE_CODE_TEXT and code)
)
2015-10-14 06:08:12 +00:00
if not check:
2019-07-31 19:25:30 +00:00
_LOGGER.warning("Wrong code entered for %s", state)
2015-10-14 06:08:12 +00:00
return check