Skip to content

Commit

Permalink
Merge pull request #10 from konnected-io/20210705_feat_add_noonlight_…
Browse files Browse the repository at this point in the history
…service_call

Add service to create alarms
  • Loading branch information
snicker authored Jul 10, 2021
2 parents 0127565 + fd78fa0 commit df714c5
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 63 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
88 changes: 87 additions & 1 deletion custom_components/noonlight/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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({
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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)
)
16 changes: 16 additions & 0 deletions custom_components/noonlight/services.yaml
Original file line number Diff line number Diff line change
@@ -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"
94 changes: 32 additions & 62 deletions custom_components/noonlight/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)


Expand All @@ -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."""
Expand All @@ -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
Expand All @@ -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

0 comments on commit df714c5

Please sign in to comment.