Skip to content

Commit df714c5

Browse files
authored
Merge pull request #10 from konnected-io/20210705_feat_add_noonlight_service_call
Add service to create alarms
2 parents 0127565 + fd78fa0 commit df714c5

File tree

4 files changed

+137
-63
lines changed

4 files changed

+137
-63
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Noonlight connects to emergency 9-1-1 services in all 50 U.S. states. Backed by
1414

1515
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.
1616

17+
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".
18+
1719
**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!
1820

1921
The _Noonlight Switch_ can be activated by any Home Assistant automation, just like any type of switch! [See examples below](#automation-examples).

custom_components/noonlight/__init__.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
from homeassistant.exceptions import HomeAssistantError
1313
import homeassistant.helpers.config_validation as cv
1414
from homeassistant.helpers.discovery import async_load_platform
15-
from homeassistant.helpers.event import async_track_point_in_utc_time
15+
from homeassistant.helpers.event import (
16+
async_track_point_in_utc_time, async_track_time_interval)
1617
from homeassistant.helpers.aiohttp_client import async_get_clientsession
1718
import homeassistant.util.dt as dt_util
1819

1920
DOMAIN = 'noonlight'
2021

2122
EVENT_NOONLIGHT_TOKEN_REFRESHED = 'noonlight_token_refreshed'
23+
EVENT_NOONLIGHT_ALARM_CANCELED = 'noonlight_alarm_canceled'
24+
EVENT_NOONLIGHT_ALARM_CREATED = 'noonlight_alarm_created'
2225

2326
NOTIFICATION_TOKEN_UPDATE_FAILURE = 'noonlight_token_update_failure'
2427
NOTIFICATION_TOKEN_UPDATE_SUCCESS = 'noonlight_token_update_success'
@@ -30,6 +33,15 @@
3033
CONF_API_ENDPOINT = 'api_endpoint'
3134
CONF_TOKEN_ENDPOINT = 'token_endpoint'
3235

36+
CONST_ALARM_STATUS_ACTIVE = 'ACTIVE'
37+
CONST_ALARM_STATUS_CANCELED = 'CANCELED'
38+
CONST_NOONLIGHT_HA_SERVICE_CREATE_ALARM = 'create_alarm'
39+
CONST_NOONLIGHT_SERVICE_TYPES = (
40+
nl.NOONLIGHT_SERVICES_POLICE,
41+
nl.NOONLIGHT_SERVICES_FIRE,
42+
nl.NOONLIGHT_SERVICES_MEDICAL
43+
)
44+
3345
_LOGGER = logging.getLogger(__name__)
3446

3547
CONFIG_SCHEMA = vol.Schema({
@@ -53,6 +65,14 @@ async def async_setup(hass, config):
5365
noonlight_integration = NoonlightIntegration(hass, conf)
5466
hass.data[DOMAIN] = noonlight_integration
5567

68+
async def handle_create_alarm_service(call):
69+
"""Create a noonlight alarm from a service"""
70+
service = call.data.get('service', None)
71+
await noonlight_integration.create_alarm(alarm_types=[service])
72+
73+
hass.services.async_register(DOMAIN,
74+
CONST_NOONLIGHT_HA_SERVICE_CREATE_ALARM, handle_create_alarm_service)
75+
5676
async def check_api_token(now):
5777
"""Check if the current API token has expired and renew if so."""
5878
next_check_interval = TOKEN_CHECK_INTERVAL
@@ -116,6 +136,7 @@ def __init__(self, hass, conf):
116136
self.hass = hass
117137
self.config = conf
118138
self._access_token_response = {}
139+
self._alarm = None
119140
self._time_to_renew = timedelta(hours=2)
120141
self._websession = async_get_clientsession(self.hass)
121142
self.client = nl.NoonlightClient(token=self.access_token,
@@ -177,6 +198,7 @@ async def check_api_token(self, force_renew=False):
177198
token_response = await resp.json()
178199
if 'token' in token_response and 'expires' in token_response:
179200
self._set_token_response(token_response)
201+
_LOGGER.debug("Token set: {}".format(self.access_token))
180202
_LOGGER.debug("Token renewed, expires at {0} ({1:.1f}h)"
181203
.format(self.access_token_expiry,
182204
self.access_token_expires_in
@@ -199,3 +221,67 @@ def _set_token_response(self, token_response):
199221
token_response['expires'] = dt_util.utc_from_timestamp(0)
200222
self.client.set_token(token=token_response.get('token'))
201223
self._access_token_response = token_response
224+
225+
async def update_alarm_status(self):
226+
"""Update the status of the current alarm."""
227+
if self._alarm is not None:
228+
return await self._alarm.get_status()
229+
230+
async def create_alarm(self, alarm_types=[nl.NOONLIGHT_SERVICES_POLICE]):
231+
"""Create a new alarm"""
232+
services = {}
233+
for alarm_type in alarm_types or ():
234+
if alarm_type in CONST_NOONLIGHT_SERVICE_TYPES:
235+
services[alarm_type] = True
236+
if self._alarm is None:
237+
try:
238+
alarm_body = {
239+
'location.coordinates': {
240+
'lat': self.latitude,
241+
'lng': self.longitude,
242+
'accuracy': 5
243+
}
244+
}
245+
if len(services) > 0:
246+
alarm_body['services'] = services
247+
self._alarm = await self.client.create_alarm(
248+
body=alarm_body
249+
)
250+
except nl.NoonlightClient.ClientError as client_error:
251+
persistent_notification.create(
252+
self.hass,
253+
"Failed to send an alarm to Noonlight!\n\n"
254+
"({}: {})".format(type(client_error).__name__,
255+
str(client_error)),
256+
"Noonlight Alarm Failure",
257+
NOTIFICATION_ALARM_CREATE_FAILURE)
258+
if self._alarm and self._alarm.status == CONST_ALARM_STATUS_ACTIVE:
259+
self.hass.helpers.dispatcher.async_dispatcher_send(
260+
EVENT_NOONLIGHT_ALARM_CREATED)
261+
_LOGGER.debug(
262+
'noonlight alarm has been initiated. '
263+
'id: %s status: %s',
264+
self._alarm.id,
265+
self._alarm.status)
266+
cancel_interval = None
267+
268+
async def check_alarm_status_interval(now):
269+
_LOGGER.debug('checking alarm status...')
270+
if await self.update_alarm_status() == \
271+
CONST_ALARM_STATUS_CANCELED:
272+
_LOGGER.debug(
273+
'alarm %s has been canceled!',
274+
self._alarm.id)
275+
if cancel_interval is not None:
276+
cancel_interval()
277+
if self._alarm is not None:
278+
if self._alarm.status == \
279+
CONST_ALARM_STATUS_CANCELED:
280+
self._alarm = None
281+
self.hass.helpers.dispatcher.async_dispatcher_send(
282+
EVENT_NOONLIGHT_ALARM_CANCELED)
283+
cancel_interval = async_track_time_interval(
284+
self.hass,
285+
check_alarm_status_interval,
286+
timedelta(seconds=15)
287+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
create_alarm:
2+
name: Create Alarm
3+
description: Notifies Noonlight of an alarm with specific services.
4+
fields:
5+
service:
6+
name: Service
7+
description: Service that the alarm should call (police, fire, medical)
8+
required: true
9+
example: "police"
10+
default: "police"
11+
selector:
12+
select:
13+
options:
14+
- "police"
15+
- "fire"
16+
- "medical"

custom_components/noonlight/switch.py

Lines changed: 32 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,19 @@
33

44
from datetime import timedelta
55

6-
from noonlight import NoonlightClient
7-
86
from homeassistant.components import persistent_notification
97
try:
108
from homeassistant.components.switch import SwitchEntity
119
except ImportError:
1210
from homeassistant.components.switch import SwitchDevice as SwitchEntity
13-
from homeassistant.helpers.event import async_track_time_interval
1411

1512
from . import (DOMAIN, EVENT_NOONLIGHT_TOKEN_REFRESHED,
13+
EVENT_NOONLIGHT_ALARM_CANCELED,
14+
EVENT_NOONLIGHT_ALARM_CREATED,
1615
NOTIFICATION_ALARM_CREATE_FAILURE)
1716

1817
DEFAULT_NAME = 'Noonlight Switch'
1918

20-
CONST_ALARM_STATUS_ACTIVE = 'ACTIVE'
21-
CONST_ALARM_STATUS_CANCELED = 'CANCELED'
22-
2319
_LOGGER = logging.getLogger(__name__)
2420

2521

@@ -32,10 +28,24 @@ async def async_setup_platform(
3228

3329
def noonlight_token_refreshed():
3430
noonlight_switch.schedule_update_ha_state()
31+
32+
def noonlight_alarm_canceled():
33+
noonlight_switch._state = False
34+
noonlight_switch.schedule_update_ha_state()
35+
36+
def noonlight_alarm_created():
37+
noonlight_switch._state = True
38+
noonlight_switch.schedule_update_ha_state()
3539

3640
hass.helpers.dispatcher.async_dispatcher_connect(
3741
EVENT_NOONLIGHT_TOKEN_REFRESHED, noonlight_token_refreshed)
3842

43+
hass.helpers.dispatcher.async_dispatcher_connect(
44+
EVENT_NOONLIGHT_ALARM_CANCELED, noonlight_alarm_canceled)
45+
46+
hass.helpers.dispatcher.async_dispatcher_connect(
47+
EVENT_NOONLIGHT_ALARM_CREATED, noonlight_alarm_created)
48+
3949

4050
class NoonlightSwitch(SwitchEntity):
4151
"""Representation of a Noonlight alarm switch."""
@@ -44,7 +54,6 @@ def __init__(self, noonlight_integration):
4454
"""Initialize the Noonlight switch."""
4555
self.noonlight = noonlight_integration
4656
self._name = DEFAULT_NAME
47-
self._alarm = None
4857
self._state = False
4958

5059
@property
@@ -57,69 +66,30 @@ def available(self):
5766
"""Ensure that the Noonlight access token is valid."""
5867
return self.noonlight.access_token_expires_in.total_seconds() > 0
5968

69+
@property
70+
def extra_state_attributes(self):
71+
"""Return the current alarm attributes, when active."""
72+
attr = {}
73+
if self.noonlight._alarm is not None:
74+
alarm = self.noonlight._alarm
75+
attr['alarm_status'] = alarm.status
76+
attr['alarm_id'] = alarm.id
77+
attr['alarm_services'] = alarm.services
78+
return attr
79+
6080
@property
6181
def is_on(self):
6282
"""Return the status of the switch."""
6383
return self._state
6484

65-
async def update_alarm_status(self):
66-
"""Update the status of the current alarm."""
67-
if self._alarm is not None:
68-
return await self._alarm.get_status()
69-
7085
async def async_turn_on(self, **kwargs):
71-
"""Activate an alarm."""
72-
# [TODO] read list of monitored sensors, use sensor type to determine
73-
# whether medical, fire, or police should be notified
74-
if self._alarm is None:
75-
try:
76-
self._alarm = await self.noonlight.client.create_alarm(
77-
body={
78-
'location.coordinates': {
79-
'lat': self.noonlight.latitude,
80-
'lng': self.noonlight.longitude,
81-
'accuracy': 5
82-
}
83-
}
84-
)
85-
except NoonlightClient.ClientError as client_error:
86-
persistent_notification.create(
87-
self.hass,
88-
"Failed to send an alarm to Noonlight!\n\n"
89-
"({}: {})".format(type(client_error).__name__,
90-
str(client_error)),
91-
"Noonlight Alarm Failure",
92-
NOTIFICATION_ALARM_CREATE_FAILURE)
93-
if self._alarm and self._alarm.status == CONST_ALARM_STATUS_ACTIVE:
94-
_LOGGER.debug(
95-
'noonlight alarm has been initiated. '
96-
'id: %s status: %s',
97-
self._alarm.id,
98-
self._alarm.status)
86+
"""Activate an alarm. Defaults to `police` services."""
87+
if self.noonlight._alarm is None:
88+
await self.noonlight.create_alarm()
89+
if self.noonlight._alarm is not None:
9990
self._state = True
100-
cancel_interval = None
101-
102-
async def check_alarm_status_interval(now):
103-
_LOGGER.debug('checking alarm status...')
104-
if await self.update_alarm_status() == \
105-
CONST_ALARM_STATUS_CANCELED:
106-
_LOGGER.debug(
107-
'alarm %s has been canceled!',
108-
self._alarm.id)
109-
if cancel_interval is not None:
110-
cancel_interval()
111-
await self.async_turn_off()
112-
self.schedule_update_ha_state()
113-
cancel_interval = async_track_time_interval(
114-
self.hass,
115-
check_alarm_status_interval,
116-
timedelta(seconds=15)
117-
)
11891

11992
async def async_turn_off(self, **kwargs):
12093
"""Turn off the switch if the active alarm is canceled."""
121-
if self._alarm is not None:
122-
if self._alarm.status == CONST_ALARM_STATUS_CANCELED:
123-
self._alarm = None
124-
if self._alarm is None:
94+
if self.noonlight._alarm is None:
12595
self._state = False

0 commit comments

Comments
 (0)