diff --git a/README.md b/README.md index 535ce0d..82105ce 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Noonlight connects to emergency 9-1-1 services in all 50 U.S. states. Backed by When integrated with Home Assistant, a **Noonlight Alarm** switch will appear in your list of entities. When the Noonlight Alarm switch is turned _on_, this will send an emergency signal to Noonlight. You will be contacted by text and voice at the phone number associated with your Noonlight account. If you confirm the emergency with the Noonlight operator, or if you're unable to respond, Noonlight will dispatch local emergency services to your home using the [longitude and latitude coordinates](https://www.home-assistant.io/docs/configuration/basic/#latitude) specified in your Home Assistant configuration. +Additionally, a new service will be exposed to Home Assistant: `noonlight.create_alarm`, which allows you to explicitly specify the type of emergency service required by the alarm: medical, fire, or police. By default, the switch entity assumes "police". + **False alarm?** No problem. Just tell the Noonlight operator your PIN when you are contacted and the alarm will be canceled. We're glad you're safe! The _Noonlight Switch_ can be activated by any Home Assistant automation, just like any type of switch! [See examples below](#automation-examples). diff --git a/custom_components/noonlight/__init__.py b/custom_components/noonlight/__init__.py index 86b3152..02cc1af 100644 --- a/custom_components/noonlight/__init__.py +++ b/custom_components/noonlight/__init__.py @@ -12,13 +12,16 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.event import ( + async_track_point_in_utc_time, async_track_time_interval) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util DOMAIN = 'noonlight' EVENT_NOONLIGHT_TOKEN_REFRESHED = 'noonlight_token_refreshed' +EVENT_NOONLIGHT_ALARM_CANCELED = 'noonlight_alarm_canceled' +EVENT_NOONLIGHT_ALARM_CREATED = 'noonlight_alarm_created' NOTIFICATION_TOKEN_UPDATE_FAILURE = 'noonlight_token_update_failure' NOTIFICATION_TOKEN_UPDATE_SUCCESS = 'noonlight_token_update_success' @@ -30,6 +33,15 @@ CONF_API_ENDPOINT = 'api_endpoint' CONF_TOKEN_ENDPOINT = 'token_endpoint' +CONST_ALARM_STATUS_ACTIVE = 'ACTIVE' +CONST_ALARM_STATUS_CANCELED = 'CANCELED' +CONST_NOONLIGHT_HA_SERVICE_CREATE_ALARM = 'create_alarm' +CONST_NOONLIGHT_SERVICE_TYPES = ( + nl.NOONLIGHT_SERVICES_POLICE, + nl.NOONLIGHT_SERVICES_FIRE, + nl.NOONLIGHT_SERVICES_MEDICAL + ) + _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({ @@ -53,6 +65,14 @@ async def async_setup(hass, config): noonlight_integration = NoonlightIntegration(hass, conf) hass.data[DOMAIN] = noonlight_integration + async def handle_create_alarm_service(call): + """Create a noonlight alarm from a service""" + service = call.data.get('service', None) + await noonlight_integration.create_alarm(alarm_types=[service]) + + hass.services.async_register(DOMAIN, + CONST_NOONLIGHT_HA_SERVICE_CREATE_ALARM, handle_create_alarm_service) + async def check_api_token(now): """Check if the current API token has expired and renew if so.""" next_check_interval = TOKEN_CHECK_INTERVAL @@ -116,6 +136,7 @@ def __init__(self, hass, conf): self.hass = hass self.config = conf self._access_token_response = {} + self._alarm = None self._time_to_renew = timedelta(hours=2) self._websession = async_get_clientsession(self.hass) self.client = nl.NoonlightClient(token=self.access_token, @@ -177,6 +198,7 @@ async def check_api_token(self, force_renew=False): token_response = await resp.json() if 'token' in token_response and 'expires' in token_response: self._set_token_response(token_response) + _LOGGER.debug("Token set: {}".format(self.access_token)) _LOGGER.debug("Token renewed, expires at {0} ({1:.1f}h)" .format(self.access_token_expiry, self.access_token_expires_in @@ -199,3 +221,67 @@ def _set_token_response(self, token_response): token_response['expires'] = dt_util.utc_from_timestamp(0) self.client.set_token(token=token_response.get('token')) self._access_token_response = token_response + + async def update_alarm_status(self): + """Update the status of the current alarm.""" + if self._alarm is not None: + return await self._alarm.get_status() + + async def create_alarm(self, alarm_types=[nl.NOONLIGHT_SERVICES_POLICE]): + """Create a new alarm""" + services = {} + for alarm_type in alarm_types or (): + if alarm_type in CONST_NOONLIGHT_SERVICE_TYPES: + services[alarm_type] = True + if self._alarm is None: + try: + alarm_body = { + 'location.coordinates': { + 'lat': self.latitude, + 'lng': self.longitude, + 'accuracy': 5 + } + } + if len(services) > 0: + alarm_body['services'] = services + self._alarm = await self.client.create_alarm( + body=alarm_body + ) + except nl.NoonlightClient.ClientError as client_error: + persistent_notification.create( + self.hass, + "Failed to send an alarm to Noonlight!\n\n" + "({}: {})".format(type(client_error).__name__, + str(client_error)), + "Noonlight Alarm Failure", + NOTIFICATION_ALARM_CREATE_FAILURE) + if self._alarm and self._alarm.status == CONST_ALARM_STATUS_ACTIVE: + self.hass.helpers.dispatcher.async_dispatcher_send( + EVENT_NOONLIGHT_ALARM_CREATED) + _LOGGER.debug( + 'noonlight alarm has been initiated. ' + 'id: %s status: %s', + self._alarm.id, + self._alarm.status) + cancel_interval = None + + async def check_alarm_status_interval(now): + _LOGGER.debug('checking alarm status...') + if await self.update_alarm_status() == \ + CONST_ALARM_STATUS_CANCELED: + _LOGGER.debug( + 'alarm %s has been canceled!', + self._alarm.id) + if cancel_interval is not None: + cancel_interval() + if self._alarm is not None: + if self._alarm.status == \ + CONST_ALARM_STATUS_CANCELED: + self._alarm = None + self.hass.helpers.dispatcher.async_dispatcher_send( + EVENT_NOONLIGHT_ALARM_CANCELED) + cancel_interval = async_track_time_interval( + self.hass, + check_alarm_status_interval, + timedelta(seconds=15) + ) diff --git a/custom_components/noonlight/services.yaml b/custom_components/noonlight/services.yaml new file mode 100644 index 0000000..de74b31 --- /dev/null +++ b/custom_components/noonlight/services.yaml @@ -0,0 +1,16 @@ +create_alarm: + name: Create Alarm + description: Notifies Noonlight of an alarm with specific services. + fields: + service: + name: Service + description: Service that the alarm should call (police, fire, medical) + required: true + example: "police" + default: "police" + selector: + select: + options: + - "police" + - "fire" + - "medical" diff --git a/custom_components/noonlight/switch.py b/custom_components/noonlight/switch.py index eed0f8d..36e2b4e 100644 --- a/custom_components/noonlight/switch.py +++ b/custom_components/noonlight/switch.py @@ -3,23 +3,19 @@ from datetime import timedelta -from noonlight import NoonlightClient - from homeassistant.components import persistent_notification try: from homeassistant.components.switch import SwitchEntity except ImportError: from homeassistant.components.switch import SwitchDevice as SwitchEntity -from homeassistant.helpers.event import async_track_time_interval from . import (DOMAIN, EVENT_NOONLIGHT_TOKEN_REFRESHED, + EVENT_NOONLIGHT_ALARM_CANCELED, + EVENT_NOONLIGHT_ALARM_CREATED, NOTIFICATION_ALARM_CREATE_FAILURE) DEFAULT_NAME = 'Noonlight Switch' -CONST_ALARM_STATUS_ACTIVE = 'ACTIVE' -CONST_ALARM_STATUS_CANCELED = 'CANCELED' - _LOGGER = logging.getLogger(__name__) @@ -32,10 +28,24 @@ async def async_setup_platform( def noonlight_token_refreshed(): noonlight_switch.schedule_update_ha_state() + + def noonlight_alarm_canceled(): + noonlight_switch._state = False + noonlight_switch.schedule_update_ha_state() + + def noonlight_alarm_created(): + noonlight_switch._state = True + noonlight_switch.schedule_update_ha_state() hass.helpers.dispatcher.async_dispatcher_connect( EVENT_NOONLIGHT_TOKEN_REFRESHED, noonlight_token_refreshed) + hass.helpers.dispatcher.async_dispatcher_connect( + EVENT_NOONLIGHT_ALARM_CANCELED, noonlight_alarm_canceled) + + hass.helpers.dispatcher.async_dispatcher_connect( + EVENT_NOONLIGHT_ALARM_CREATED, noonlight_alarm_created) + class NoonlightSwitch(SwitchEntity): """Representation of a Noonlight alarm switch.""" @@ -44,7 +54,6 @@ def __init__(self, noonlight_integration): """Initialize the Noonlight switch.""" self.noonlight = noonlight_integration self._name = DEFAULT_NAME - self._alarm = None self._state = False @property @@ -57,69 +66,30 @@ def available(self): """Ensure that the Noonlight access token is valid.""" return self.noonlight.access_token_expires_in.total_seconds() > 0 + @property + def extra_state_attributes(self): + """Return the current alarm attributes, when active.""" + attr = {} + if self.noonlight._alarm is not None: + alarm = self.noonlight._alarm + attr['alarm_status'] = alarm.status + attr['alarm_id'] = alarm.id + attr['alarm_services'] = alarm.services + return attr + @property def is_on(self): """Return the status of the switch.""" return self._state - async def update_alarm_status(self): - """Update the status of the current alarm.""" - if self._alarm is not None: - return await self._alarm.get_status() - async def async_turn_on(self, **kwargs): - """Activate an alarm.""" - # [TODO] read list of monitored sensors, use sensor type to determine - # whether medical, fire, or police should be notified - if self._alarm is None: - try: - self._alarm = await self.noonlight.client.create_alarm( - body={ - 'location.coordinates': { - 'lat': self.noonlight.latitude, - 'lng': self.noonlight.longitude, - 'accuracy': 5 - } - } - ) - except NoonlightClient.ClientError as client_error: - persistent_notification.create( - self.hass, - "Failed to send an alarm to Noonlight!\n\n" - "({}: {})".format(type(client_error).__name__, - str(client_error)), - "Noonlight Alarm Failure", - NOTIFICATION_ALARM_CREATE_FAILURE) - if self._alarm and self._alarm.status == CONST_ALARM_STATUS_ACTIVE: - _LOGGER.debug( - 'noonlight alarm has been initiated. ' - 'id: %s status: %s', - self._alarm.id, - self._alarm.status) + """Activate an alarm. Defaults to `police` services.""" + if self.noonlight._alarm is None: + await self.noonlight.create_alarm() + if self.noonlight._alarm is not None: self._state = True - cancel_interval = None - - async def check_alarm_status_interval(now): - _LOGGER.debug('checking alarm status...') - if await self.update_alarm_status() == \ - CONST_ALARM_STATUS_CANCELED: - _LOGGER.debug( - 'alarm %s has been canceled!', - self._alarm.id) - if cancel_interval is not None: - cancel_interval() - await self.async_turn_off() - self.schedule_update_ha_state() - cancel_interval = async_track_time_interval( - self.hass, - check_alarm_status_interval, - timedelta(seconds=15) - ) async def async_turn_off(self, **kwargs): """Turn off the switch if the active alarm is canceled.""" - if self._alarm is not None: - if self._alarm.status == CONST_ALARM_STATUS_CANCELED: - self._alarm = None - if self._alarm is None: + if self.noonlight._alarm is None: self._state = False