2020-02-18 21:51:10 +00:00
|
|
|
"""Provides device automations for MQTT."""
|
|
|
|
import logging
|
2020-07-14 17:30:30 +00:00
|
|
|
from typing import Callable, List, Optional
|
2020-02-18 21:51:10 +00:00
|
|
|
|
|
|
|
import attr
|
|
|
|
import voluptuous as vol
|
|
|
|
|
|
|
|
from homeassistant.components import mqtt
|
|
|
|
from homeassistant.components.automation import AutomationActionType
|
|
|
|
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
|
|
|
|
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
|
|
|
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
from homeassistant.helpers import config_validation as cv
|
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
|
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
|
|
|
|
|
|
|
from . import (
|
|
|
|
ATTR_DISCOVERY_HASH,
|
2020-03-10 04:59:38 +00:00
|
|
|
ATTR_DISCOVERY_TOPIC,
|
2020-02-18 21:51:10 +00:00
|
|
|
CONF_CONNECTIONS,
|
|
|
|
CONF_DEVICE,
|
|
|
|
CONF_IDENTIFIERS,
|
|
|
|
CONF_PAYLOAD,
|
|
|
|
CONF_QOS,
|
|
|
|
DOMAIN,
|
2020-03-12 01:00:47 +00:00
|
|
|
cleanup_device_registry,
|
2020-04-01 17:00:40 +00:00
|
|
|
debug_info,
|
2020-08-17 16:54:56 +00:00
|
|
|
trigger as mqtt_trigger,
|
2020-02-18 21:51:10 +00:00
|
|
|
)
|
|
|
|
from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
CONF_AUTOMATION_TYPE = "automation_type"
|
|
|
|
CONF_DISCOVERY_ID = "discovery_id"
|
|
|
|
CONF_SUBTYPE = "subtype"
|
|
|
|
CONF_TOPIC = "topic"
|
|
|
|
DEFAULT_ENCODING = "utf-8"
|
|
|
|
DEVICE = "device"
|
|
|
|
|
|
|
|
MQTT_TRIGGER_BASE = {
|
|
|
|
# Trigger when MQTT message is received
|
|
|
|
CONF_PLATFORM: DEVICE,
|
|
|
|
CONF_DOMAIN: DOMAIN,
|
|
|
|
}
|
|
|
|
|
|
|
|
TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_PLATFORM): DEVICE,
|
|
|
|
vol.Required(CONF_DOMAIN): DOMAIN,
|
|
|
|
vol.Required(CONF_DEVICE_ID): str,
|
|
|
|
vol.Required(CONF_DISCOVERY_ID): str,
|
|
|
|
vol.Required(CONF_TYPE): cv.string,
|
|
|
|
vol.Required(CONF_SUBTYPE): cv.string,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_AUTOMATION_TYPE): str,
|
|
|
|
vol.Required(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
|
|
|
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
|
|
|
vol.Optional(CONF_PAYLOAD, default=None): vol.Any(None, cv.string),
|
|
|
|
vol.Required(CONF_TYPE): cv.string,
|
|
|
|
vol.Required(CONF_SUBTYPE): cv.string,
|
|
|
|
},
|
|
|
|
mqtt.validate_device_has_at_least_one_identifier,
|
|
|
|
)
|
|
|
|
|
|
|
|
DEVICE_TRIGGERS = "mqtt_device_triggers"
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s(slots=True)
|
|
|
|
class TriggerInstance:
|
|
|
|
"""Attached trigger settings."""
|
|
|
|
|
2020-07-14 17:30:30 +00:00
|
|
|
action: AutomationActionType = attr.ib()
|
|
|
|
automation_info: dict = attr.ib()
|
|
|
|
trigger: "Trigger" = attr.ib()
|
|
|
|
remove: Optional[CALLBACK_TYPE] = attr.ib(default=None)
|
2020-02-18 21:51:10 +00:00
|
|
|
|
|
|
|
async def async_attach_trigger(self):
|
|
|
|
"""Attach MQTT trigger."""
|
|
|
|
mqtt_config = {
|
2020-08-17 16:54:56 +00:00
|
|
|
mqtt_trigger.CONF_TOPIC: self.trigger.topic,
|
|
|
|
mqtt_trigger.CONF_ENCODING: DEFAULT_ENCODING,
|
|
|
|
mqtt_trigger.CONF_QOS: self.trigger.qos,
|
2020-02-18 21:51:10 +00:00
|
|
|
}
|
|
|
|
if self.trigger.payload:
|
|
|
|
mqtt_config[CONF_PAYLOAD] = self.trigger.payload
|
|
|
|
|
|
|
|
if self.remove:
|
|
|
|
self.remove()
|
2020-08-17 16:54:56 +00:00
|
|
|
self.remove = await mqtt_trigger.async_attach_trigger(
|
2020-02-18 21:51:10 +00:00
|
|
|
self.trigger.hass, mqtt_config, self.action, self.automation_info,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s(slots=True)
|
|
|
|
class Trigger:
|
|
|
|
"""Device trigger settings."""
|
|
|
|
|
2020-07-14 17:30:30 +00:00
|
|
|
device_id: str = attr.ib()
|
|
|
|
discovery_data: dict = attr.ib()
|
|
|
|
hass: HomeAssistantType = attr.ib()
|
|
|
|
payload: str = attr.ib()
|
|
|
|
qos: int = attr.ib()
|
|
|
|
remove_signal: Callable[[], None] = attr.ib()
|
|
|
|
subtype: str = attr.ib()
|
|
|
|
topic: str = attr.ib()
|
|
|
|
type: str = attr.ib()
|
|
|
|
trigger_instances: List[TriggerInstance] = attr.ib(factory=list)
|
2020-02-18 21:51:10 +00:00
|
|
|
|
|
|
|
async def add_trigger(self, action, automation_info):
|
|
|
|
"""Add MQTT trigger."""
|
|
|
|
instance = TriggerInstance(action, automation_info, self)
|
|
|
|
self.trigger_instances.append(instance)
|
|
|
|
|
|
|
|
if self.topic is not None:
|
|
|
|
# If we know about the trigger, subscribe to MQTT topic
|
|
|
|
await instance.async_attach_trigger()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_remove() -> None:
|
|
|
|
"""Remove trigger."""
|
|
|
|
if instance not in self.trigger_instances:
|
|
|
|
raise HomeAssistantError("Can't remove trigger twice")
|
|
|
|
|
|
|
|
if instance.remove:
|
|
|
|
instance.remove()
|
|
|
|
self.trigger_instances.remove(instance)
|
|
|
|
|
|
|
|
return async_remove
|
|
|
|
|
2020-02-25 04:46:02 +00:00
|
|
|
async def update_trigger(self, config, discovery_hash, remove_signal):
|
2020-02-18 21:51:10 +00:00
|
|
|
"""Update MQTT device trigger."""
|
2020-02-25 04:46:02 +00:00
|
|
|
self.remove_signal = remove_signal
|
2020-02-18 21:51:10 +00:00
|
|
|
self.type = config[CONF_TYPE]
|
|
|
|
self.subtype = config[CONF_SUBTYPE]
|
|
|
|
self.payload = config[CONF_PAYLOAD]
|
|
|
|
self.qos = config[CONF_QOS]
|
2020-05-28 22:50:23 +00:00
|
|
|
topic_changed = self.topic != config[CONF_TOPIC]
|
|
|
|
self.topic = config[CONF_TOPIC]
|
2020-02-18 21:51:10 +00:00
|
|
|
|
2020-05-28 22:50:23 +00:00
|
|
|
# Unsubscribe+subscribe if this trigger is in use and topic has changed
|
|
|
|
# If topic is same unsubscribe+subscribe will execute in the wrong order
|
|
|
|
# because unsubscribe is done with help of async_create_task
|
|
|
|
if topic_changed:
|
|
|
|
for trig in self.trigger_instances:
|
|
|
|
await trig.async_attach_trigger()
|
2020-02-18 21:51:10 +00:00
|
|
|
|
|
|
|
def detach_trigger(self):
|
|
|
|
"""Remove MQTT device trigger."""
|
|
|
|
# Mark trigger as unknown
|
|
|
|
self.topic = None
|
2020-02-25 04:46:02 +00:00
|
|
|
|
2020-02-18 21:51:10 +00:00
|
|
|
# Unsubscribe if this trigger is in use
|
|
|
|
for trig in self.trigger_instances:
|
|
|
|
if trig.remove:
|
|
|
|
trig.remove()
|
|
|
|
trig.remove = None
|
|
|
|
|
|
|
|
|
|
|
|
async def _update_device(hass, config_entry, config):
|
|
|
|
"""Update device registry."""
|
|
|
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
|
|
|
config_entry_id = config_entry.entry_id
|
|
|
|
device_info = mqtt.device_info_from_config(config[CONF_DEVICE])
|
|
|
|
|
|
|
|
if config_entry_id is not None and device_info is not None:
|
|
|
|
device_info["config_entry_id"] = config_entry_id
|
|
|
|
device_registry.async_get_or_create(**device_info)
|
|
|
|
|
|
|
|
|
2020-02-25 04:46:02 +00:00
|
|
|
async def async_setup_trigger(hass, config, config_entry, discovery_data):
|
2020-02-18 21:51:10 +00:00
|
|
|
"""Set up the MQTT device trigger."""
|
|
|
|
config = TRIGGER_DISCOVERY_SCHEMA(config)
|
2020-02-25 04:46:02 +00:00
|
|
|
discovery_hash = discovery_data[ATTR_DISCOVERY_HASH]
|
2020-02-18 21:51:10 +00:00
|
|
|
discovery_id = discovery_hash[1]
|
|
|
|
remove_signal = None
|
|
|
|
|
|
|
|
async def discovery_update(payload):
|
|
|
|
"""Handle discovery update."""
|
|
|
|
_LOGGER.info(
|
|
|
|
"Got update for trigger with hash: %s '%s'", discovery_hash, payload
|
|
|
|
)
|
|
|
|
if not payload:
|
|
|
|
# Empty payload: Remove trigger
|
|
|
|
_LOGGER.info("Removing trigger: %s", discovery_hash)
|
2020-04-01 17:00:40 +00:00
|
|
|
debug_info.remove_trigger_discovery_data(hass, discovery_hash)
|
2020-02-18 21:51:10 +00:00
|
|
|
if discovery_id in hass.data[DEVICE_TRIGGERS]:
|
|
|
|
device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id]
|
|
|
|
device_trigger.detach_trigger()
|
|
|
|
clear_discovery_hash(hass, discovery_hash)
|
|
|
|
remove_signal()
|
2020-03-12 01:00:47 +00:00
|
|
|
await cleanup_device_registry(hass, device.id)
|
2020-02-18 21:51:10 +00:00
|
|
|
else:
|
|
|
|
# Non-empty payload: Update trigger
|
|
|
|
_LOGGER.info("Updating trigger: %s", discovery_hash)
|
2020-04-01 17:00:40 +00:00
|
|
|
debug_info.update_trigger_discovery_data(hass, discovery_hash, payload)
|
2020-02-18 21:51:10 +00:00
|
|
|
config = TRIGGER_DISCOVERY_SCHEMA(payload)
|
|
|
|
await _update_device(hass, config_entry, config)
|
|
|
|
device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id]
|
2020-02-25 04:46:02 +00:00
|
|
|
await device_trigger.update_trigger(config, discovery_hash, remove_signal)
|
2020-02-18 21:51:10 +00:00
|
|
|
|
|
|
|
remove_signal = async_dispatcher_connect(
|
|
|
|
hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_update
|
|
|
|
)
|
|
|
|
|
|
|
|
await _update_device(hass, config_entry, config)
|
|
|
|
|
|
|
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
|
|
|
device = device_registry.async_get_device(
|
|
|
|
{(DOMAIN, id_) for id_ in config[CONF_DEVICE][CONF_IDENTIFIERS]},
|
|
|
|
{tuple(x) for x in config[CONF_DEVICE][CONF_CONNECTIONS]},
|
|
|
|
)
|
|
|
|
|
|
|
|
if device is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
if DEVICE_TRIGGERS not in hass.data:
|
|
|
|
hass.data[DEVICE_TRIGGERS] = {}
|
|
|
|
if discovery_id not in hass.data[DEVICE_TRIGGERS]:
|
|
|
|
hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger(
|
|
|
|
hass=hass,
|
|
|
|
device_id=device.id,
|
2020-03-10 04:59:38 +00:00
|
|
|
discovery_data=discovery_data,
|
2020-02-18 21:51:10 +00:00
|
|
|
type=config[CONF_TYPE],
|
|
|
|
subtype=config[CONF_SUBTYPE],
|
|
|
|
topic=config[CONF_TOPIC],
|
|
|
|
payload=config[CONF_PAYLOAD],
|
|
|
|
qos=config[CONF_QOS],
|
2020-02-25 04:46:02 +00:00
|
|
|
remove_signal=remove_signal,
|
2020-02-18 21:51:10 +00:00
|
|
|
)
|
|
|
|
else:
|
2020-02-25 04:46:02 +00:00
|
|
|
await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger(
|
|
|
|
config, discovery_hash, remove_signal
|
|
|
|
)
|
2020-04-01 17:00:40 +00:00
|
|
|
debug_info.add_trigger_discovery_data(
|
|
|
|
hass, discovery_hash, discovery_data, device.id
|
|
|
|
)
|
2020-02-25 04:46:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def async_device_removed(hass: HomeAssistant, device_id: str):
|
|
|
|
"""Handle the removal of a device."""
|
|
|
|
triggers = await async_get_triggers(hass, device_id)
|
|
|
|
for trig in triggers:
|
|
|
|
device_trigger = hass.data[DEVICE_TRIGGERS].pop(trig[CONF_DISCOVERY_ID])
|
|
|
|
if device_trigger:
|
2020-03-10 04:59:38 +00:00
|
|
|
discovery_hash = device_trigger.discovery_data[ATTR_DISCOVERY_HASH]
|
|
|
|
discovery_topic = device_trigger.discovery_data[ATTR_DISCOVERY_TOPIC]
|
|
|
|
|
2020-04-01 17:00:40 +00:00
|
|
|
debug_info.remove_trigger_discovery_data(hass, discovery_hash)
|
2020-02-25 04:46:02 +00:00
|
|
|
device_trigger.detach_trigger()
|
2020-03-10 04:59:38 +00:00
|
|
|
clear_discovery_hash(hass, discovery_hash)
|
2020-02-25 04:46:02 +00:00
|
|
|
device_trigger.remove_signal()
|
2020-03-10 04:59:38 +00:00
|
|
|
mqtt.publish(
|
|
|
|
hass, discovery_topic, "", retain=True,
|
|
|
|
)
|
2020-02-18 21:51:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
|
|
|
|
"""List device triggers for MQTT devices."""
|
|
|
|
triggers = []
|
|
|
|
|
|
|
|
if DEVICE_TRIGGERS not in hass.data:
|
|
|
|
return triggers
|
|
|
|
|
|
|
|
for discovery_id, trig in hass.data[DEVICE_TRIGGERS].items():
|
|
|
|
if trig.device_id != device_id or trig.topic is None:
|
|
|
|
continue
|
|
|
|
|
|
|
|
trigger = {
|
|
|
|
**MQTT_TRIGGER_BASE,
|
|
|
|
"device_id": device_id,
|
|
|
|
"type": trig.type,
|
|
|
|
"subtype": trig.subtype,
|
|
|
|
"discovery_id": discovery_id,
|
|
|
|
}
|
|
|
|
triggers.append(trigger)
|
|
|
|
|
|
|
|
return triggers
|
|
|
|
|
|
|
|
|
|
|
|
async def async_attach_trigger(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
config: ConfigType,
|
|
|
|
action: AutomationActionType,
|
|
|
|
automation_info: dict,
|
|
|
|
) -> CALLBACK_TYPE:
|
|
|
|
"""Attach a trigger."""
|
|
|
|
if DEVICE_TRIGGERS not in hass.data:
|
|
|
|
hass.data[DEVICE_TRIGGERS] = {}
|
|
|
|
config = TRIGGER_SCHEMA(config)
|
|
|
|
device_id = config[CONF_DEVICE_ID]
|
|
|
|
discovery_id = config[CONF_DISCOVERY_ID]
|
|
|
|
|
|
|
|
if discovery_id not in hass.data[DEVICE_TRIGGERS]:
|
|
|
|
hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger(
|
|
|
|
hass=hass,
|
|
|
|
device_id=device_id,
|
2020-03-10 04:59:38 +00:00
|
|
|
discovery_data=None,
|
2020-02-25 04:46:02 +00:00
|
|
|
remove_signal=None,
|
2020-02-18 21:51:10 +00:00
|
|
|
type=config[CONF_TYPE],
|
|
|
|
subtype=config[CONF_SUBTYPE],
|
|
|
|
topic=None,
|
|
|
|
payload=None,
|
|
|
|
qos=None,
|
|
|
|
)
|
|
|
|
return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger(
|
|
|
|
action, automation_info
|
|
|
|
)
|