Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for Nederlandse Spoorwegen public transport."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime, timedelta
6 import logging
7 
8 import ns_api
9 from ns_api import RequestParametersError
10 import requests
11 import voluptuous as vol
12 
14  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
15  SensorEntity,
16 )
17 from homeassistant.const import CONF_API_KEY, CONF_NAME
18 from homeassistant.core import HomeAssistant
19 from homeassistant.exceptions import PlatformNotReady
21 from homeassistant.helpers.entity_platform import AddEntitiesCallback
22 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
23 from homeassistant.util import Throttle
24 
25 _LOGGER = logging.getLogger(__name__)
26 
27 CONF_ROUTES = "routes"
28 CONF_FROM = "from"
29 CONF_TO = "to"
30 CONF_VIA = "via"
31 CONF_TIME = "time"
32 
33 
34 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
35 
36 ROUTE_SCHEMA = vol.Schema(
37  {
38  vol.Required(CONF_NAME): cv.string,
39  vol.Required(CONF_FROM): cv.string,
40  vol.Required(CONF_TO): cv.string,
41  vol.Optional(CONF_VIA): cv.string,
42  vol.Optional(CONF_TIME): cv.time,
43  }
44 )
45 
46 ROUTES_SCHEMA = vol.All(cv.ensure_list, [ROUTE_SCHEMA])
47 
48 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
49  {vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_ROUTES): ROUTES_SCHEMA}
50 )
51 
52 
54  hass: HomeAssistant,
55  config: ConfigType,
56  add_entities: AddEntitiesCallback,
57  discovery_info: DiscoveryInfoType | None = None,
58 ) -> None:
59  """Set up the departure sensor."""
60 
61  nsapi = ns_api.NSAPI(config[CONF_API_KEY])
62 
63  try:
64  stations = nsapi.get_stations()
65  except (
66  requests.exceptions.ConnectionError,
67  requests.exceptions.HTTPError,
68  ) as error:
69  _LOGGER.error("Could not connect to the internet: %s", error)
70  raise PlatformNotReady from error
71  except RequestParametersError as error:
72  _LOGGER.error("Could not fetch stations, please check configuration: %s", error)
73  return
74 
75  sensors = []
76  for departure in config.get(CONF_ROUTES, {}):
77  if not valid_stations(
78  stations,
79  [departure.get(CONF_FROM), departure.get(CONF_VIA), departure.get(CONF_TO)],
80  ):
81  continue
82  sensors.append(
84  nsapi,
85  departure.get(CONF_NAME),
86  departure.get(CONF_FROM),
87  departure.get(CONF_TO),
88  departure.get(CONF_VIA),
89  departure.get(CONF_TIME),
90  )
91  )
92  add_entities(sensors, True)
93 
94 
95 def valid_stations(stations, given_stations):
96  """Verify the existence of the given station codes."""
97  for station in given_stations:
98  if station is None:
99  continue
100  if not any(s.code == station.upper() for s in stations):
101  _LOGGER.warning("Station '%s' is not a valid station", station)
102  return False
103  return True
104 
105 
107  """Implementation of a NS Departure Sensor."""
108 
109  _attr_attribution = "Data provided by NS"
110  _attr_icon = "mdi:train"
111 
112  def __init__(self, nsapi, name, departure, heading, via, time):
113  """Initialize the sensor."""
114  self._nsapi_nsapi = nsapi
115  self._name_name = name
116  self._departure_departure = departure
117  self._via_via = via
118  self._heading_heading = heading
119  self._time_time = time
120  self._state_state = None
121  self._trips_trips = None
122 
123  @property
124  def name(self):
125  """Return the name of the sensor."""
126  return self._name_name
127 
128  @property
129  def native_value(self):
130  """Return the next departure time."""
131  return self._state_state
132 
133  @property
135  """Return the state attributes."""
136  if not self._trips_trips:
137  return None
138 
139  if self._trips_trips[0].trip_parts:
140  route = [self._trips_trips[0].departure]
141  route.extend(k.destination for k in self._trips_trips[0].trip_parts)
142 
143  # Static attributes
144  attributes = {
145  "going": self._trips_trips[0].going,
146  "departure_time_planned": None,
147  "departure_time_actual": None,
148  "departure_delay": False,
149  "departure_platform_planned": self._trips_trips[0].departure_platform_planned,
150  "departure_platform_actual": self._trips_trips[0].departure_platform_actual,
151  "arrival_time_planned": None,
152  "arrival_time_actual": None,
153  "arrival_delay": False,
154  "arrival_platform_planned": self._trips_trips[0].arrival_platform_planned,
155  "arrival_platform_actual": self._trips_trips[0].arrival_platform_actual,
156  "next": None,
157  "status": self._trips_trips[0].status.lower(),
158  "transfers": self._trips_trips[0].nr_transfers,
159  "route": route,
160  "remarks": None,
161  }
162 
163  # Planned departure attributes
164  if self._trips_trips[0].departure_time_planned is not None:
165  attributes["departure_time_planned"] = self._trips_trips[
166  0
167  ].departure_time_planned.strftime("%H:%M")
168 
169  # Actual departure attributes
170  if self._trips_trips[0].departure_time_actual is not None:
171  attributes["departure_time_actual"] = self._trips_trips[
172  0
173  ].departure_time_actual.strftime("%H:%M")
174 
175  # Delay departure attributes
176  if (
177  attributes["departure_time_planned"]
178  and attributes["departure_time_actual"]
179  and attributes["departure_time_planned"]
180  != attributes["departure_time_actual"]
181  ):
182  attributes["departure_delay"] = True
183 
184  # Planned arrival attributes
185  if self._trips_trips[0].arrival_time_planned is not None:
186  attributes["arrival_time_planned"] = self._trips_trips[
187  0
188  ].arrival_time_planned.strftime("%H:%M")
189 
190  # Actual arrival attributes
191  if self._trips_trips[0].arrival_time_actual is not None:
192  attributes["arrival_time_actual"] = self._trips_trips[
193  0
194  ].arrival_time_actual.strftime("%H:%M")
195 
196  # Delay arrival attributes
197  if (
198  attributes["arrival_time_planned"]
199  and attributes["arrival_time_actual"]
200  and attributes["arrival_time_planned"] != attributes["arrival_time_actual"]
201  ):
202  attributes["arrival_delay"] = True
203 
204  # Next attributes
205  if len(self._trips_trips) > 1:
206  if self._trips_trips[1].departure_time_actual is not None:
207  attributes["next"] = self._trips_trips[1].departure_time_actual.strftime(
208  "%H:%M"
209  )
210  elif self._trips_trips[1].departure_time_planned is not None:
211  attributes["next"] = self._trips_trips[1].departure_time_planned.strftime(
212  "%H:%M"
213  )
214 
215  return attributes
216 
217  @Throttle(MIN_TIME_BETWEEN_UPDATES)
218  def update(self) -> None:
219  """Get the trip information."""
220 
221  # If looking for a specific trip time, update around that trip time only.
222  if self._time_time and (
223  (datetime.now() + timedelta(minutes=30)).time() < self._time_time
224  or (datetime.now() - timedelta(minutes=30)).time() > self._time_time
225  ):
226  self._state_state = None
227  self._trips_trips = None
228  return
229 
230  # Set the search parameter to search from a specific trip time
231  # or to just search for next trip.
232  if self._time_time:
233  trip_time = (
234  datetime.today()
235  .replace(hour=self._time_time.hour, minute=self._time_time.minute)
236  .strftime("%d-%m-%Y %H:%M")
237  )
238  else:
239  trip_time = datetime.now().strftime("%d-%m-%Y %H:%M")
240 
241  try:
242  self._trips_trips = self._nsapi_nsapi.get_trips(
243  trip_time, self._departure_departure, self._via_via, self._heading_heading, True, 0, 2
244  )
245  if self._trips_trips:
246  if self._trips_trips[0].departure_time_actual is None:
247  planned_time = self._trips_trips[0].departure_time_planned
248  self._state_state = planned_time.strftime("%H:%M")
249  else:
250  actual_time = self._trips_trips[0].departure_time_actual
251  self._state_state = actual_time.strftime("%H:%M")
252  except (
253  requests.exceptions.ConnectionError,
254  requests.exceptions.HTTPError,
255  ) as error:
256  _LOGGER.error("Couldn't fetch trip info: %s", error)
def __init__(self, nsapi, name, departure, heading, via, time)
Definition: sensor.py:112
def add_entities(account, async_add_entities, tracked)
Definition: sensor.py:40
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:58
def valid_stations(stations, given_stations)
Definition: sensor.py:95
bool time(HomeAssistant hass, dt_time|str|None before=None, dt_time|str|None after=None, str|Container[str]|None weekday=None)
Definition: condition.py:802