""" Support for repeating alerts when conditions are met. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alert/ """ import asyncio from datetime import datetime, timedelta import logging import os import voluptuous as vol from homeassistant.core import callback from homeassistant.config import load_yaml_config_file from homeassistant.const import ( CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers import service, event from homeassistant.util.async import run_callback_threadsafe import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DOMAIN = 'alert' ENTITY_ID_FORMAT = DOMAIN + '.{}' CONF_CAN_ACK = 'can_acknowledge' CONF_NOTIFIERS = 'notifiers' CONF_REPEAT = 'repeat' CONF_SKIP_FIRST = 'skip_first' DEFAULT_CAN_ACK = True DEFAULT_SKIP_FIRST = False ALERT_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_STATE, default=STATE_ON): cv.string, vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean, vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, vol.Required(CONF_NOTIFIERS): cv.ensure_list}) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ cv.slug: ALERT_SCHEMA, }), }, extra=vol.ALLOW_EXTRA) ALERT_SERVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids, }) def is_on(hass, entity_id): """Return if the alert is firing and not acknowledged.""" return hass.states.is_state(entity_id, STATE_ON) def turn_on(hass, entity_id): """Reset the alert.""" run_callback_threadsafe( hass.loop, async_turn_on, hass, entity_id).result() @callback def async_turn_on(hass, entity_id): """Async reset the alert.""" data = {ATTR_ENTITY_ID: entity_id} hass.async_add_job( hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) def turn_off(hass, entity_id): """Acknowledge alert.""" run_callback_threadsafe( hass.loop, async_turn_off, hass, entity_id).result() @callback def async_turn_off(hass, entity_id): """Async acknowledge the alert.""" data = {ATTR_ENTITY_ID: entity_id} hass.async_add_job( hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) def toggle(hass, entity_id): """Toggle acknowledgement of alert.""" run_callback_threadsafe(hass.loop, async_toggle, hass, entity_id) @callback def async_toggle(hass, entity_id): """Async toggle acknowledgement of alert.""" data = {ATTR_ENTITY_ID: entity_id} hass.async_add_job( hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data)) @asyncio.coroutine def async_setup(hass, config): """Set up the Alert component.""" alerts = config.get(DOMAIN) all_alerts = {} @asyncio.coroutine def async_handle_alert_service(service_call): """Handle calls to alert services.""" alert_ids = service.extract_entity_ids(hass, service_call) for alert_id in alert_ids: alert = all_alerts[alert_id] if service_call.service == SERVICE_TURN_ON: yield from alert.async_turn_on() elif service_call.service == SERVICE_TOGGLE: yield from alert.async_toggle() else: yield from alert.async_turn_off() # Setup alerts for entity_id, alert in alerts.items(): entity = Alert(hass, entity_id, alert[CONF_NAME], alert[CONF_ENTITY_ID], alert[CONF_STATE], alert[CONF_REPEAT], alert[CONF_SKIP_FIRST], alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK]) all_alerts[entity.entity_id] = entity # Read descriptions descriptions = yield from hass.loop.run_in_executor( None, load_yaml_config_file, os.path.join( os.path.dirname(__file__), 'services.yaml')) descriptions = descriptions.get(DOMAIN, {}) # Setup service calls hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service, descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, descriptions.get(SERVICE_TURN_ON), schema=ALERT_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, descriptions.get(SERVICE_TOGGLE), schema=ALERT_SERVICE_SCHEMA) tasks = [alert.async_update_ha_state() for alert in all_alerts.values()] if tasks: yield from asyncio.wait(tasks, loop=hass.loop) return True class Alert(ToggleEntity): """Representation of an alert.""" def __init__(self, hass, entity_id, name, watched_entity_id, state, repeat, skip_first, notifiers, can_ack): """Initialize the alert.""" self.hass = hass self._name = name self._alert_state = state self._skip_first = skip_first self._notifiers = notifiers self._can_ack = can_ack self._delay = [timedelta(minutes=val) for val in repeat] self._next_delay = 0 self._firing = False self._ack = False self._cancel = None self.entity_id = ENTITY_ID_FORMAT.format(entity_id) event.async_track_state_change( hass, watched_entity_id, self.watched_entity_change) @property def name(self): """Return the name of the alert.""" return self._name @property def should_poll(self): """HASS need not poll these entities.""" return False @property def state(self): """Return the alert status.""" if self._firing: if self._ack: return STATE_OFF return STATE_ON return STATE_IDLE @property def hidden(self): """Hide the alert when it is not firing.""" return not self._can_ack or not self._firing @asyncio.coroutine def watched_entity_change(self, entity, from_state, to_state): """Determine if the alert should start or stop.""" _LOGGER.debug("Watched entity (%s) has changed", entity) if to_state.state == self._alert_state and not self._firing: yield from self.begin_alerting() if to_state.state != self._alert_state and self._firing: yield from self.end_alerting() @asyncio.coroutine def begin_alerting(self): """Begin the alert procedures.""" _LOGGER.debug("Beginning Alert: %s", self._name) self._ack = False self._firing = True self._next_delay = 0 if not self._skip_first: yield from self._notify() else: yield from self._schedule_notify() self.hass.async_add_job(self.async_update_ha_state) @asyncio.coroutine def end_alerting(self): """End the alert procedures.""" _LOGGER.debug("Ending Alert: %s", self._name) self._cancel() self._ack = False self._firing = False self.hass.async_add_job(self.async_update_ha_state) @asyncio.coroutine def _schedule_notify(self): """Schedule a notification.""" delay = self._delay[self._next_delay] next_msg = datetime.now() + delay self._cancel = \ event.async_track_point_in_time(self.hass, self._notify, next_msg) self._next_delay = min(self._next_delay + 1, len(self._delay) - 1) @asyncio.coroutine def _notify(self, *args): """Send the alert notification.""" if not self._firing: return if not self._ack: _LOGGER.info("Alerting: %s", self._name) for target in self._notifiers: yield from self.hass.services.async_call( 'notify', target, {'message': self._name}) yield from self._schedule_notify() @asyncio.coroutine def async_turn_on(self): """Async Unacknowledge alert.""" _LOGGER.debug("Reset Alert: %s", self._name) self._ack = False yield from self.async_update_ha_state() @asyncio.coroutine def async_turn_off(self): """Async Acknowledge alert.""" _LOGGER.debug("Acknowledged Alert: %s", self._name) self._ack = True yield from self.async_update_ha_state() @asyncio.coroutine def async_toggle(self): """Async toggle alert.""" if self._ack: return self.async_turn_on() return self.async_turn_off()