Skip to content

Commit

Permalink
Merge pull request #11 from meringu/service-alerts
Browse files Browse the repository at this point in the history
Add service alerts
  • Loading branch information
make-all committed May 16, 2024
2 parents ef8a867 + 5937306 commit 76ae8a4
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 2 deletions.
12 changes: 12 additions & 0 deletions custom_components/metlink/MetlinkAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

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

Expand Down Expand Up @@ -48,3 +49,14 @@ async def get_predictions(self, stop_id):
) as r:
r.raise_for_status()
return await r.json()

async def get_service_alerts(self):
"""Information about unforeseen events affecting routes, stops, or the network."""
headers = {"Accept": CONTENT_TYPE_JSON, APIKEY_HEADER: self._key}
_LOGGER.debug(f"Metlink request for service alerts")
async with self._session.get(
SERVICE_ALERTS_URL,
headers=headers,
) as r:
r.raise_for_status()
return await r.json()
22 changes: 22 additions & 0 deletions custom_components/metlink/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

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

CONF_STOPS = "stops"
CONF_STOP_ID = "stop_id"
Expand All @@ -23,23 +24,44 @@

ATTR_ACCESSIBLE = "wheelchair_accessible"
ATTR_AIMED = "aimed"
ATTR_ALERT = "alert"
ATTR_ALERT_CAUSE = "alert_cause"
ATTR_ALERT_COUNT = "alert_count"
ATTR_ALERT_DESCRIPTION = "alert_description"
ATTR_ALERT_EFFECT = "alert_effect"
ATTR_ALERT_HEADER = "alert_header"
ATTR_ALERT_SEVERITY_LEVEL = "alert_severity_level"
ATTR_ALERT_URL = "alert_url"
ATTR_ARRIVAL = "arrival"
ATTR_CAUSE = "cause"
ATTR_CLOSED = "closed"
ATTR_DELAY = "delay"
ATTR_DEPARTURE = "departure"
ATTR_DEPARTURES = "departures"
ATTR_DESCRIPTION = "description"
ATTR_DESCRIPTION_TEXT = "description_text"
ATTR_DESTINATION = "destination"
ATTR_DESTINATION_ID = "destination_id"
ATTR_DIRECTION = "direction"
ATTR_EFFECT = "effect"
ATTR_ENTITY = "entity"
ATTR_EXPECTED = "expected"
ATTR_FAREZONE = "farezone"
ATTR_HEADER_TEXT = "header_text"
ATTR_INFORMED_ENTITY = "informed_entity"
ATTR_LANGUAGE = "language"
ATTR_MONITORED = "monitored"
ATTR_NAME = "name"
ATTR_OPERATOR = "operator"
ATTR_ORIGIN = "origin"
ATTR_SERVICE = "service_id"
ATTR_SEVERITY_LEVEL = "severity_level"
ATTR_STATUS = "status"
ATTR_STOP = "stop_id"
ATTR_STOP_NAME = "stop_name"
ATTR_TEXT = "text"
ATTR_TRANSLATION = "translation"
ATTR_TRIP = "trip"
ATTR_TRIP_ID = "trip_id"
ATTR_URL = "url"
ATTR_VEHICLE = "vehicle_id"
98 changes: 96 additions & 2 deletions custom_components/metlink/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,41 @@
from .const import (
ATTR_ACCESSIBLE,
ATTR_AIMED,
ATTR_ALERT_CAUSE,
ATTR_ALERT_COUNT,
ATTR_ALERT_DESCRIPTION,
ATTR_ALERT_EFFECT,
ATTR_ALERT_HEADER,
ATTR_ALERT_SEVERITY_LEVEL,
ATTR_ALERT_URL,
ATTR_ALERT,
ATTR_CAUSE,
ATTR_DELAY,
ATTR_DEPARTURE,
ATTR_DEPARTURES,
ATTR_DESCRIPTION_TEXT,
ATTR_DESCRIPTION,
ATTR_DESTINATION,
ATTR_DESTINATION_ID,
ATTR_DESTINATION,
ATTR_EFFECT,
ATTR_ENTITY,
ATTR_EXPECTED,
ATTR_HEADER_TEXT,
ATTR_INFORMED_ENTITY,
ATTR_LANGUAGE,
ATTR_MONITORED,
ATTR_NAME,
ATTR_OPERATOR,
ATTR_SERVICE,
ATTR_SEVERITY_LEVEL,
ATTR_STATUS,
ATTR_STOP,
ATTR_STOP_NAME,
ATTR_STOP,
ATTR_TEXT,
ATTR_TRANSLATION,
ATTR_TRIP_ID,
ATTR_TRIP,
ATTR_URL,
ATTR_VEHICLE,
ATTRIBUTION,
CONF_DEST,
Expand All @@ -55,6 +76,7 @@
CONF_STOP_ID,
CONF_STOPS,
DOMAIN,
LANG,
)

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

def get_translation(translations: Dict) -> str:
for translation in translations.get(ATTR_TRANSLATION, {}):
if translation.get(ATTR_LANGUAGE) == LANG:
return translation.get(ATTR_TEXT, "")

return ""

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

num = 0
try:
alerts = await self.metlink.get_service_alerts()
data = await self.metlink.get_predictions(self.stop_id)

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

# enumerate the service alerts to find any that are relevant to the trip.
trip_alerts = []
if ATTR_TRIP_ID in departure:
trip_id = departure[ATTR_TRIP_ID]
for entity in alerts[ATTR_ENTITY]:
alert = entity[ATTR_ALERT]
informed_entities = alert[ATTR_INFORMED_ENTITY]
if any(informed_entity.get(ATTR_TRIP, {}).get(ATTR_TRIP_ID) == trip_id for informed_entity in informed_entities):
trip_alerts.append(alert)

name = f"{departure[ATTR_SERVICE]} {dest}"
if num == 1:
# First record is the next departure, so use that
Expand Down Expand Up @@ -274,6 +313,44 @@ async def async_update(self):
self.attrs[ATTR_MONITORED + suffix] = departure[ATTR_MONITORED]
self.attrs[ATTR_VEHICLE + suffix] = departure[ATTR_VEHICLE]

# Trip alerts
self.attrs[ATTR_ALERT_COUNT + suffix] = len(trip_alerts)
num_alert = 0
for alert in trip_alerts:
alert_suffix = f"_{num_alert}"

self.attrs[ATTR_ALERT_HEADER + suffix + alert_suffix] = get_translation(alert.get(ATTR_HEADER_TEXT, {}))
self.attrs[ATTR_ALERT_DESCRIPTION + suffix + alert_suffix] = get_translation(alert.get(ATTR_DESCRIPTION_TEXT, {}))
self.attrs[ATTR_ALERT_URL + suffix + alert_suffix] = get_translation(alert.get(ATTR_URL, {}))
self.attrs[ATTR_ALERT_CAUSE + suffix + alert_suffix] = alert.get(ATTR_CAUSE, "")
self.attrs[ATTR_ALERT_EFFECT + suffix + alert_suffix] = alert.get(ATTR_EFFECT, "")
self.attrs[ATTR_ALERT_SEVERITY_LEVEL + suffix + alert_suffix] = alert.get(ATTR_SEVERITY_LEVEL, "")

num_alert += 1

# Clear out old alerts
to_remove = []
for alert_prefix in [
ATTR_ALERT_HEADER,
ATTR_ALERT_DESCRIPTION,
ATTR_ALERT_URL,
ATTR_ALERT_CAUSE,
ATTR_ALERT_EFFECT,
ATTR_ALERT_SEVERITY_LEVEL,
]:
prefix = f"{alert_prefix}{suffix}_"
for attr in self.attrs:
if attr.startswith(prefix):
try:
if int(attr.removeprefix(prefix)) >= len(trip_alerts):
# we have an attribute outside of the range of the current alerts
to_remove.append(attr)
except ValueError as ex:
pass

for attr in to_remove:
self.attrs.pop(attr)

self._available = True
# Clear out the unused slots
for i in range(num, self.num_departures):
Expand All @@ -295,6 +372,23 @@ async def async_update(self):
self.attrs.pop(ATTR_DESTINATION_ID + suffix, None)
self.attrs.pop(ATTR_ACCESSIBLE + suffix, None)
self.attrs.pop(ATTR_DELAY + suffix, None)
self.attrs.pop(ATTR_ALERT_COUNT + suffix, None)
to_remove = []
for alert_prefix in [
ATTR_ALERT_HEADER,
ATTR_ALERT_DESCRIPTION,
ATTR_ALERT_URL,
ATTR_ALERT_CAUSE,
ATTR_ALERT_EFFECT,
ATTR_ALERT_SEVERITY_LEVEL,
]:
prefix = f"{alert_prefix}{suffix}"
for attr in self.attrs:
if attr.startswith(prefix):
to_remove.append(attr)

for attr in to_remove:
self.attrs.pop(attr)

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

0 comments on commit 76ae8a4

Please sign in to comment.