Skip to content

Commit 76ae8a4

Browse files
authored
Merge pull request #11 from meringu/service-alerts
Add service alerts
2 parents ef8a867 + 5937306 commit 76ae8a4

File tree

3 files changed

+130
-2
lines changed

3 files changed

+130
-2
lines changed

custom_components/metlink/MetlinkAPI.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
BASE_URL = "https://api.opendata.metlink.org.nz/v1"
2121
PREDICTIONS_URL = BASE_URL + "/stop-predictions"
22+
SERVICE_ALERTS_URL = BASE_URL + "/gtfs-rt/servicealerts"
2223
STOP_PARAM = "stop_id"
2324
APIKEY_HEADER = "X-Api-Key"
2425

@@ -48,3 +49,14 @@ async def get_predictions(self, stop_id):
4849
) as r:
4950
r.raise_for_status()
5051
return await r.json()
52+
53+
async def get_service_alerts(self):
54+
"""Information about unforeseen events affecting routes, stops, or the network."""
55+
headers = {"Accept": CONTENT_TYPE_JSON, APIKEY_HEADER: self._key}
56+
_LOGGER.debug(f"Metlink request for service alerts")
57+
async with self._session.get(
58+
SERVICE_ALERTS_URL,
59+
headers=headers,
60+
) as r:
61+
r.raise_for_status()
62+
return await r.json()

custom_components/metlink/const.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
DOMAIN = "metlink"
1616
ATTRIBUTION = "Data provided by Greater Wellington Regional Council"
17+
LANG = "en" # API only provides English translations
1718

1819
CONF_STOPS = "stops"
1920
CONF_STOP_ID = "stop_id"
@@ -23,23 +24,44 @@
2324

2425
ATTR_ACCESSIBLE = "wheelchair_accessible"
2526
ATTR_AIMED = "aimed"
27+
ATTR_ALERT = "alert"
28+
ATTR_ALERT_CAUSE = "alert_cause"
29+
ATTR_ALERT_COUNT = "alert_count"
30+
ATTR_ALERT_DESCRIPTION = "alert_description"
31+
ATTR_ALERT_EFFECT = "alert_effect"
32+
ATTR_ALERT_HEADER = "alert_header"
33+
ATTR_ALERT_SEVERITY_LEVEL = "alert_severity_level"
34+
ATTR_ALERT_URL = "alert_url"
2635
ATTR_ARRIVAL = "arrival"
36+
ATTR_CAUSE = "cause"
2737
ATTR_CLOSED = "closed"
2838
ATTR_DELAY = "delay"
2939
ATTR_DEPARTURE = "departure"
3040
ATTR_DEPARTURES = "departures"
3141
ATTR_DESCRIPTION = "description"
42+
ATTR_DESCRIPTION_TEXT = "description_text"
3243
ATTR_DESTINATION = "destination"
3344
ATTR_DESTINATION_ID = "destination_id"
3445
ATTR_DIRECTION = "direction"
46+
ATTR_EFFECT = "effect"
47+
ATTR_ENTITY = "entity"
3548
ATTR_EXPECTED = "expected"
3649
ATTR_FAREZONE = "farezone"
50+
ATTR_HEADER_TEXT = "header_text"
51+
ATTR_INFORMED_ENTITY = "informed_entity"
52+
ATTR_LANGUAGE = "language"
3753
ATTR_MONITORED = "monitored"
3854
ATTR_NAME = "name"
3955
ATTR_OPERATOR = "operator"
4056
ATTR_ORIGIN = "origin"
4157
ATTR_SERVICE = "service_id"
58+
ATTR_SEVERITY_LEVEL = "severity_level"
4259
ATTR_STATUS = "status"
4360
ATTR_STOP = "stop_id"
4461
ATTR_STOP_NAME = "stop_name"
62+
ATTR_TEXT = "text"
63+
ATTR_TRANSLATION = "translation"
64+
ATTR_TRIP = "trip"
65+
ATTR_TRIP_ID = "trip_id"
66+
ATTR_URL = "url"
4567
ATTR_VEHICLE = "vehicle_id"

custom_components/metlink/sensor.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,41 @@
3333
from .const import (
3434
ATTR_ACCESSIBLE,
3535
ATTR_AIMED,
36+
ATTR_ALERT_CAUSE,
37+
ATTR_ALERT_COUNT,
38+
ATTR_ALERT_DESCRIPTION,
39+
ATTR_ALERT_EFFECT,
40+
ATTR_ALERT_HEADER,
41+
ATTR_ALERT_SEVERITY_LEVEL,
42+
ATTR_ALERT_URL,
43+
ATTR_ALERT,
44+
ATTR_CAUSE,
3645
ATTR_DELAY,
3746
ATTR_DEPARTURE,
3847
ATTR_DEPARTURES,
48+
ATTR_DESCRIPTION_TEXT,
3949
ATTR_DESCRIPTION,
40-
ATTR_DESTINATION,
4150
ATTR_DESTINATION_ID,
51+
ATTR_DESTINATION,
52+
ATTR_EFFECT,
53+
ATTR_ENTITY,
4254
ATTR_EXPECTED,
55+
ATTR_HEADER_TEXT,
56+
ATTR_INFORMED_ENTITY,
57+
ATTR_LANGUAGE,
4358
ATTR_MONITORED,
4459
ATTR_NAME,
4560
ATTR_OPERATOR,
4661
ATTR_SERVICE,
62+
ATTR_SEVERITY_LEVEL,
4763
ATTR_STATUS,
48-
ATTR_STOP,
4964
ATTR_STOP_NAME,
65+
ATTR_STOP,
66+
ATTR_TEXT,
67+
ATTR_TRANSLATION,
68+
ATTR_TRIP_ID,
69+
ATTR_TRIP,
70+
ATTR_URL,
5071
ATTR_VEHICLE,
5172
ATTRIBUTION,
5273
CONF_DEST,
@@ -55,6 +76,7 @@
5576
CONF_STOP_ID,
5677
CONF_STOPS,
5778
DOMAIN,
79+
LANG,
5880
)
5981

6082
_LOGGER = logging.getLogger(__name__)
@@ -129,6 +151,12 @@ def metlink_unique_id(d: Dict):
129151
uid = uid + "_d" + slug(d["dest_filter"])
130152
return uid
131153

154+
def get_translation(translations: Dict) -> str:
155+
for translation in translations.get(ATTR_TRANSLATION, {}):
156+
if translation.get(ATTR_LANGUAGE) == LANG:
157+
return translation.get(ATTR_TEXT, "")
158+
159+
return ""
132160

133161
class MetlinkSensor(Entity):
134162
"""Representation of a Metlink Stop sensor."""
@@ -194,6 +222,7 @@ async def async_update(self):
194222

195223
num = 0
196224
try:
225+
alerts = await self.metlink.get_service_alerts()
197226
data = await self.metlink.get_predictions(self.stop_id)
198227

199228
for departure in data[ATTR_DEPARTURES]:
@@ -214,6 +243,16 @@ async def async_update(self):
214243
if time is None:
215244
time = departure[ATTR_DEPARTURE].get(ATTR_AIMED)
216245

246+
# enumerate the service alerts to find any that are relevant to the trip.
247+
trip_alerts = []
248+
if ATTR_TRIP_ID in departure:
249+
trip_id = departure[ATTR_TRIP_ID]
250+
for entity in alerts[ATTR_ENTITY]:
251+
alert = entity[ATTR_ALERT]
252+
informed_entities = alert[ATTR_INFORMED_ENTITY]
253+
if any(informed_entity.get(ATTR_TRIP, {}).get(ATTR_TRIP_ID) == trip_id for informed_entity in informed_entities):
254+
trip_alerts.append(alert)
255+
217256
name = f"{departure[ATTR_SERVICE]} {dest}"
218257
if num == 1:
219258
# First record is the next departure, so use that
@@ -274,6 +313,44 @@ async def async_update(self):
274313
self.attrs[ATTR_MONITORED + suffix] = departure[ATTR_MONITORED]
275314
self.attrs[ATTR_VEHICLE + suffix] = departure[ATTR_VEHICLE]
276315

316+
# Trip alerts
317+
self.attrs[ATTR_ALERT_COUNT + suffix] = len(trip_alerts)
318+
num_alert = 0
319+
for alert in trip_alerts:
320+
alert_suffix = f"_{num_alert}"
321+
322+
self.attrs[ATTR_ALERT_HEADER + suffix + alert_suffix] = get_translation(alert.get(ATTR_HEADER_TEXT, {}))
323+
self.attrs[ATTR_ALERT_DESCRIPTION + suffix + alert_suffix] = get_translation(alert.get(ATTR_DESCRIPTION_TEXT, {}))
324+
self.attrs[ATTR_ALERT_URL + suffix + alert_suffix] = get_translation(alert.get(ATTR_URL, {}))
325+
self.attrs[ATTR_ALERT_CAUSE + suffix + alert_suffix] = alert.get(ATTR_CAUSE, "")
326+
self.attrs[ATTR_ALERT_EFFECT + suffix + alert_suffix] = alert.get(ATTR_EFFECT, "")
327+
self.attrs[ATTR_ALERT_SEVERITY_LEVEL + suffix + alert_suffix] = alert.get(ATTR_SEVERITY_LEVEL, "")
328+
329+
num_alert += 1
330+
331+
# Clear out old alerts
332+
to_remove = []
333+
for alert_prefix in [
334+
ATTR_ALERT_HEADER,
335+
ATTR_ALERT_DESCRIPTION,
336+
ATTR_ALERT_URL,
337+
ATTR_ALERT_CAUSE,
338+
ATTR_ALERT_EFFECT,
339+
ATTR_ALERT_SEVERITY_LEVEL,
340+
]:
341+
prefix = f"{alert_prefix}{suffix}_"
342+
for attr in self.attrs:
343+
if attr.startswith(prefix):
344+
try:
345+
if int(attr.removeprefix(prefix)) >= len(trip_alerts):
346+
# we have an attribute outside of the range of the current alerts
347+
to_remove.append(attr)
348+
except ValueError as ex:
349+
pass
350+
351+
for attr in to_remove:
352+
self.attrs.pop(attr)
353+
277354
self._available = True
278355
# Clear out the unused slots
279356
for i in range(num, self.num_departures):
@@ -295,6 +372,23 @@ async def async_update(self):
295372
self.attrs.pop(ATTR_DESTINATION_ID + suffix, None)
296373
self.attrs.pop(ATTR_ACCESSIBLE + suffix, None)
297374
self.attrs.pop(ATTR_DELAY + suffix, None)
375+
self.attrs.pop(ATTR_ALERT_COUNT + suffix, None)
376+
to_remove = []
377+
for alert_prefix in [
378+
ATTR_ALERT_HEADER,
379+
ATTR_ALERT_DESCRIPTION,
380+
ATTR_ALERT_URL,
381+
ATTR_ALERT_CAUSE,
382+
ATTR_ALERT_EFFECT,
383+
ATTR_ALERT_SEVERITY_LEVEL,
384+
]:
385+
prefix = f"{alert_prefix}{suffix}"
386+
for attr in self.attrs:
387+
if attr.startswith(prefix):
388+
to_remove.append(attr)
389+
390+
for attr in to_remove:
391+
self.attrs.pop(attr)
298392

299393
# set the sensor to unavailable on errors, but leave previous data in
300394
# attributes, so temporary network issues do not cause glitches.

0 commit comments

Comments
 (0)