init
This commit is contained in:
380
custom_components/alarmo/automations.py
Normal file
380
custom_components/alarmo/automations.py
Normal file
@@ -0,0 +1,380 @@
|
||||
"""Automations."""
|
||||
|
||||
import re
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_TYPE,
|
||||
ATTR_SERVICE,
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_SERVICE_DATA,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.template import Template, is_template_string
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.components.binary_sensor.device_condition import (
|
||||
ENTITY_CONDITIONS,
|
||||
)
|
||||
|
||||
from . import const
|
||||
from .helpers import (
|
||||
friendly_name_for_entity_id,
|
||||
)
|
||||
from .sensors import (
|
||||
STATE_OPEN,
|
||||
STATE_CLOSED,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from .alarm_control_panel import AlarmoBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_ARM_FAILURE = "arm_failure"
|
||||
|
||||
|
||||
def validate_area(trigger, area_id, hass):
|
||||
"""Validate area for trigger."""
|
||||
if const.ATTR_AREA not in trigger:
|
||||
return False
|
||||
elif trigger[const.ATTR_AREA]:
|
||||
return trigger[const.ATTR_AREA] == area_id
|
||||
elif len(hass.data[const.DOMAIN]["areas"]) == 1:
|
||||
return True
|
||||
else:
|
||||
return area_id is None
|
||||
|
||||
|
||||
def validate_modes(trigger, mode):
|
||||
"""Validate modes for trigger."""
|
||||
if const.ATTR_MODES not in trigger:
|
||||
return False
|
||||
elif not trigger[const.ATTR_MODES]:
|
||||
return True
|
||||
else:
|
||||
return mode in trigger[const.ATTR_MODES]
|
||||
|
||||
|
||||
def validate_trigger(trigger, to_state, from_state=None):
|
||||
"""Validate trigger condition."""
|
||||
if const.ATTR_EVENT not in trigger:
|
||||
return False
|
||||
elif trigger[const.ATTR_EVENT] == "untriggered" and from_state == "triggered":
|
||||
return True
|
||||
elif trigger[const.ATTR_EVENT] == to_state:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class AutomationHandler:
|
||||
"""Handle automations."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant):
|
||||
"""Initialize automation handler."""
|
||||
self.hass = hass
|
||||
self._config = None
|
||||
self._subscriptions = []
|
||||
self._sensorTranslationCache = {}
|
||||
self._alarmTranslationCache = {}
|
||||
self._sensorTranslationLang = None
|
||||
self._alarmTranslationLang = None
|
||||
|
||||
def async_update_config():
|
||||
"""Automation config updated, reload the configuration."""
|
||||
self._config = self.hass.data[const.DOMAIN][
|
||||
"coordinator"
|
||||
].store.async_get_automations()
|
||||
|
||||
self._subscriptions.append(
|
||||
async_dispatcher_connect(
|
||||
hass, "alarmo_automations_updated", async_update_config
|
||||
)
|
||||
)
|
||||
async_update_config()
|
||||
|
||||
@callback
|
||||
async def async_alarm_state_changed(
|
||||
area_id: str, old_state: str, new_state: str
|
||||
):
|
||||
if not old_state:
|
||||
# ignore automations at startup/restoring
|
||||
return
|
||||
|
||||
if area_id:
|
||||
alarm_entity = self.hass.data[const.DOMAIN]["areas"][area_id]
|
||||
else:
|
||||
alarm_entity = self.hass.data[const.DOMAIN]["master"]
|
||||
|
||||
if not alarm_entity:
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"state of %s is updated from %s to %s",
|
||||
alarm_entity.entity_id,
|
||||
old_state,
|
||||
new_state,
|
||||
)
|
||||
|
||||
if new_state in const.ARM_MODES:
|
||||
# we don't distinguish between armed modes for automations
|
||||
# they are handled separately
|
||||
new_state = "armed"
|
||||
|
||||
for automation_id, config in self._config.items():
|
||||
if not config[const.ATTR_ENABLED]:
|
||||
continue
|
||||
for trigger in config[const.ATTR_TRIGGERS]:
|
||||
if (
|
||||
validate_area(trigger, area_id, self.hass)
|
||||
and validate_modes(trigger, alarm_entity._arm_mode)
|
||||
and validate_trigger(trigger, new_state, old_state)
|
||||
):
|
||||
await self.async_execute_automation(automation_id, alarm_entity)
|
||||
|
||||
self._subscriptions.append(
|
||||
async_dispatcher_connect(
|
||||
self.hass, "alarmo_state_updated", async_alarm_state_changed
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
async def async_handle_event(event: str, area_id: str, args: dict = {}):
|
||||
if event != const.EVENT_FAILED_TO_ARM:
|
||||
return
|
||||
if area_id:
|
||||
alarm_entity = self.hass.data[const.DOMAIN]["areas"][area_id]
|
||||
else:
|
||||
alarm_entity = self.hass.data[const.DOMAIN]["master"]
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s has failed to arm",
|
||||
alarm_entity.entity_id,
|
||||
)
|
||||
|
||||
for automation_id, config in self._config.items():
|
||||
if not config[const.ATTR_ENABLED]:
|
||||
continue
|
||||
for trigger in config[const.ATTR_TRIGGERS]:
|
||||
if (
|
||||
validate_area(trigger, area_id, self.hass)
|
||||
and validate_modes(trigger, alarm_entity._arm_mode)
|
||||
and validate_trigger(trigger, EVENT_ARM_FAILURE)
|
||||
):
|
||||
await self.async_execute_automation(automation_id, alarm_entity)
|
||||
|
||||
self._subscriptions.append(
|
||||
async_dispatcher_connect(self.hass, "alarmo_event", async_handle_event)
|
||||
)
|
||||
|
||||
def __del__(self):
|
||||
"""Prepare for removal."""
|
||||
while len(self._subscriptions):
|
||||
self._subscriptions.pop()()
|
||||
|
||||
async def async_execute_automation(
|
||||
self, automation_id: str, alarm_entity: AlarmoBaseEntity
|
||||
):
|
||||
"""Execute the specified automation."""
|
||||
# automation is a dict of AutomationEntry
|
||||
_LOGGER.debug(
|
||||
"Executing automation %s",
|
||||
automation_id,
|
||||
)
|
||||
|
||||
actions = self._config[automation_id][const.ATTR_ACTIONS]
|
||||
for action in actions:
|
||||
try:
|
||||
service_data = copy.copy(action[CONF_SERVICE_DATA])
|
||||
|
||||
if action.get(ATTR_ENTITY_ID):
|
||||
service_data[ATTR_ENTITY_ID] = action[ATTR_ENTITY_ID]
|
||||
|
||||
if self._config[automation_id][CONF_TYPE] == const.ATTR_NOTIFICATION:
|
||||
# replace wildcards within service_data struct
|
||||
for key, val in service_data.items():
|
||||
if type(val) is str:
|
||||
service_data[key] = await self.replace_wildcards_in_string(
|
||||
val, alarm_entity
|
||||
)
|
||||
elif type(val) is dict:
|
||||
for subkey, subval in service_data[key].items():
|
||||
if type(subval) is str:
|
||||
service_data[key][
|
||||
subkey
|
||||
] = await self.replace_wildcards_in_string(
|
||||
subval, alarm_entity
|
||||
)
|
||||
|
||||
domain, service = action[ATTR_SERVICE].split(".")
|
||||
|
||||
await self.hass.async_create_task(
|
||||
self.hass.services.async_call(
|
||||
domain,
|
||||
service,
|
||||
service_data,
|
||||
blocking=False,
|
||||
context={},
|
||||
)
|
||||
)
|
||||
except HomeAssistantError as e:
|
||||
_LOGGER.error(
|
||||
"Execution of action %s failed, reason: %s",
|
||||
automation_id,
|
||||
e,
|
||||
)
|
||||
|
||||
def get_automations_by_area(self, area_id: str):
|
||||
"""Get automations for specified area."""
|
||||
result = []
|
||||
for automation_id, config in self._config.items():
|
||||
if any(
|
||||
el[const.ATTR_AREA] == area_id for el in config[const.ATTR_TRIGGERS]
|
||||
):
|
||||
result.append(automation_id)
|
||||
|
||||
return result
|
||||
|
||||
async def replace_wildcards_in_string(
|
||||
self, input: str, alarm_entity: AlarmoBaseEntity
|
||||
):
|
||||
"""Look for wildcards in string and replace them with content."""
|
||||
# process wildcard '{{open_sensors}}'
|
||||
res = re.search(r"{{open_sensors(\|lang=([^}]+))?(\|format=short)?}}", input)
|
||||
if res:
|
||||
lang = res.group(2) if res.group(2) else "en"
|
||||
names_only = True if res.group(3) else False
|
||||
|
||||
open_sensors = ""
|
||||
if alarm_entity.open_sensors:
|
||||
parts = []
|
||||
for entity_id, status in alarm_entity.open_sensors.items():
|
||||
if names_only:
|
||||
parts.append(friendly_name_for_entity_id(entity_id, self.hass))
|
||||
else:
|
||||
parts.append(
|
||||
await self.async_get_open_sensor_string(
|
||||
entity_id, status, lang
|
||||
)
|
||||
)
|
||||
open_sensors = ", ".join(parts)
|
||||
input = input.replace(res.group(0), open_sensors)
|
||||
|
||||
# process wildcard '{{bypassed_sensors}}'
|
||||
if "{{bypassed_sensors}}" in input:
|
||||
bypassed_sensors = ""
|
||||
if alarm_entity.bypassed_sensors and len(alarm_entity.bypassed_sensors):
|
||||
parts = []
|
||||
for entity_id in alarm_entity.bypassed_sensors:
|
||||
name = friendly_name_for_entity_id(entity_id, self.hass)
|
||||
parts.append(name)
|
||||
bypassed_sensors = ", ".join(parts)
|
||||
input = input.replace("{{bypassed_sensors}}", bypassed_sensors)
|
||||
|
||||
# process wildcard '{{arm_mode}}'
|
||||
res = re.search(r"{{arm_mode(\|lang=([^}]+))?}}", input)
|
||||
if res:
|
||||
lang = res.group(2) if res.group(2) else "en"
|
||||
arm_mode = await self.async_get_arm_mode_string(alarm_entity.arm_mode, lang)
|
||||
|
||||
input = input.replace(res.group(0), arm_mode)
|
||||
|
||||
# process wildcard '{{changed_by}}'
|
||||
if "{{changed_by}}" in input:
|
||||
changed_by = alarm_entity.changed_by if alarm_entity.changed_by else ""
|
||||
input = input.replace("{{changed_by}}", changed_by)
|
||||
|
||||
# process wildcard '{{delay}}'
|
||||
if "{{delay}}" in input:
|
||||
delay = str(alarm_entity.delay) if alarm_entity.delay else ""
|
||||
input = input.replace("{{delay}}", delay)
|
||||
|
||||
# process HA templates
|
||||
if is_template_string(input):
|
||||
input = Template(input, self.hass).async_render()
|
||||
|
||||
return input
|
||||
|
||||
async def async_get_open_sensor_string(
|
||||
self, entity_id: str, state: str, language: str
|
||||
):
|
||||
"""Get translation for sensor states."""
|
||||
if self._sensorTranslationCache and self._sensorTranslationLang == language:
|
||||
translations = self._sensorTranslationCache
|
||||
else:
|
||||
translations = await async_get_translations(
|
||||
self.hass, language, "device_automation", ["binary_sensor"]
|
||||
)
|
||||
|
||||
self._sensorTranslationCache = translations
|
||||
self._sensorTranslationLang = language
|
||||
|
||||
entity = self.hass.states.get(entity_id)
|
||||
|
||||
device_type = (
|
||||
entity.attributes["device_class"]
|
||||
if entity and "device_class" in entity.attributes
|
||||
else None
|
||||
)
|
||||
|
||||
if state == STATE_OPEN:
|
||||
translation_key = (
|
||||
f"component.binary_sensor.device_automation.condition_type.{ENTITY_CONDITIONS[device_type][0]['type']}"
|
||||
if device_type in ENTITY_CONDITIONS
|
||||
else None
|
||||
)
|
||||
if translation_key and translation_key in translations:
|
||||
string = translations[translation_key]
|
||||
else:
|
||||
string = "{entity_name} is open"
|
||||
elif state == STATE_CLOSED:
|
||||
translation_key = (
|
||||
f"component.binary_sensor.device_automation.condition_type.{ENTITY_CONDITIONS[device_type][1]['type']}"
|
||||
if device_type in ENTITY_CONDITIONS
|
||||
else None
|
||||
)
|
||||
if translation_key and translation_key in translations:
|
||||
string = translations[translation_key]
|
||||
else:
|
||||
string = "{entity_name} is closed"
|
||||
|
||||
elif state == STATE_UNAVAILABLE:
|
||||
string = "{entity_name} is unavailable"
|
||||
|
||||
else:
|
||||
string = "{entity_name} is unknown"
|
||||
|
||||
name = friendly_name_for_entity_id(entity_id, self.hass)
|
||||
string = string.replace("{entity_name}", name)
|
||||
|
||||
return string
|
||||
|
||||
async def async_get_arm_mode_string(self, arm_mode: str, language: str):
|
||||
"""Get translation for alarm arm mode."""
|
||||
if self._alarmTranslationCache and self._alarmTranslationLang == language:
|
||||
translations = self._alarmTranslationCache
|
||||
else:
|
||||
translations = await async_get_translations(
|
||||
self.hass, language, "entity_component", ["alarm_control_panel"]
|
||||
)
|
||||
|
||||
self._alarmTranslationCache = translations
|
||||
self._alarmTranslationLang = language
|
||||
|
||||
translation_key = (
|
||||
f"component.alarm_control_panel.entity_component._.state.{arm_mode}"
|
||||
if arm_mode
|
||||
else None
|
||||
)
|
||||
|
||||
if translation_key and translation_key in translations:
|
||||
return translations[translation_key]
|
||||
elif arm_mode:
|
||||
return " ".join(w.capitalize() for w in arm_mode.split("_"))
|
||||
else:
|
||||
return ""
|
||||
Reference in New Issue
Block a user