Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for the NOAA Tides and Currents API."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime, timedelta
6 import logging
7 from typing import TYPE_CHECKING, Any, Literal, TypedDict
8 
9 import noaa_coops as coops
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_NAME, CONF_TIME_ZONE, CONF_UNIT_SYSTEM
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.unit_system import METRIC_SYSTEM
24 
25 if TYPE_CHECKING:
26  from pandas import Timestamp
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 CONF_STATION_ID = "station_id"
31 
32 DEFAULT_NAME = "NOAA Tides"
33 DEFAULT_TIMEZONE = "lst_ldt"
34 
35 SCAN_INTERVAL = timedelta(minutes=60)
36 
37 TIMEZONES = ["gmt", "lst", "lst_ldt"]
38 UNIT_SYSTEMS = ["english", "metric"]
39 
40 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
41  {
42  vol.Required(CONF_STATION_ID): cv.string,
43  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
44  vol.Optional(CONF_TIME_ZONE, default=DEFAULT_TIMEZONE): vol.In(TIMEZONES),
45  vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNIT_SYSTEMS),
46  }
47 )
48 
49 
51  hass: HomeAssistant,
52  config: ConfigType,
53  add_entities: AddEntitiesCallback,
54  discovery_info: DiscoveryInfoType | None = None,
55 ) -> None:
56  """Set up the NOAA Tides and Currents sensor."""
57  station_id = config[CONF_STATION_ID]
58  name = config.get(CONF_NAME)
59  timezone = config.get(CONF_TIME_ZONE)
60 
61  if CONF_UNIT_SYSTEM in config:
62  unit_system = config[CONF_UNIT_SYSTEM]
63  elif hass.config.units is METRIC_SYSTEM:
64  unit_system = UNIT_SYSTEMS[1]
65  else:
66  unit_system = UNIT_SYSTEMS[0]
67 
68  try:
69  station = coops.Station(station_id, unit_system)
70  except KeyError:
71  _LOGGER.error("NOAA Tides Sensor station_id %s does not exist", station_id)
72  return
73  except requests.exceptions.ConnectionError as exception:
74  _LOGGER.error(
75  "Connection error during setup in NOAA Tides Sensor for station_id: %s",
76  station_id,
77  )
78  raise PlatformNotReady from exception
79 
80  noaa_sensor = NOAATidesAndCurrentsSensor(
81  name, station_id, timezone, unit_system, station
82  )
83 
84  add_entities([noaa_sensor], True)
85 
86 
87 class NOAATidesData(TypedDict):
88  """Representation of a single tide."""
89 
90  time_stamp: list[Timestamp]
91  hi_lo: list[Literal["L", "H"]]
92  predicted_wl: list[float]
93 
94 
96  """Representation of a NOAA Tides and Currents sensor."""
97 
98  _attr_attribution = "Data provided by NOAA"
99 
100  def __init__(self, name, station_id, timezone, unit_system, station) -> None:
101  """Initialize the sensor."""
102  self._name_name = name
103  self._station_id_station_id = station_id
104  self._timezone_timezone = timezone
105  self._unit_system_unit_system = unit_system
106  self._station_station = station
107  self.datadata: NOAATidesData | None = None
108 
109  @property
110  def name(self) -> str:
111  """Return the name of the sensor."""
112  return self._name_name
113 
114  @property
115  def extra_state_attributes(self) -> dict[str, Any]:
116  """Return the state attributes of this device."""
117  attr: dict[str, Any] = {}
118  if self.datadata is None:
119  return attr
120  if self.datadata["hi_lo"][1] == "H":
121  attr["high_tide_time"] = self.datadata["time_stamp"][1].strftime(
122  "%Y-%m-%dT%H:%M"
123  )
124  attr["high_tide_height"] = self.datadata["predicted_wl"][1]
125  attr["low_tide_time"] = self.datadata["time_stamp"][2].strftime(
126  "%Y-%m-%dT%H:%M"
127  )
128  attr["low_tide_height"] = self.datadata["predicted_wl"][2]
129  elif self.datadata["hi_lo"][1] == "L":
130  attr["low_tide_time"] = self.datadata["time_stamp"][1].strftime(
131  "%Y-%m-%dT%H:%M"
132  )
133  attr["low_tide_height"] = self.datadata["predicted_wl"][1]
134  attr["high_tide_time"] = self.datadata["time_stamp"][2].strftime(
135  "%Y-%m-%dT%H:%M"
136  )
137  attr["high_tide_height"] = self.datadata["predicted_wl"][2]
138  return attr
139 
140  @property
141  def native_value(self):
142  """Return the state of the device."""
143  if self.datadata is None:
144  return None
145  api_time = self.datadata["time_stamp"][0]
146  if self.datadata["hi_lo"][0] == "H":
147  tidetime = api_time.strftime("%-I:%M %p")
148  return f"High tide at {tidetime}"
149  if self.datadata["hi_lo"][0] == "L":
150  tidetime = api_time.strftime("%-I:%M %p")
151  return f"Low tide at {tidetime}"
152  return None
153 
154  def update(self) -> None:
155  """Get the latest data from NOAA Tides and Currents API."""
156  begin = datetime.now()
157  delta = timedelta(days=2)
158  end = begin + delta
159  try:
160  df_predictions = self._station_station.get_data(
161  begin_date=begin.strftime("%Y%m%d %H:%M"),
162  end_date=end.strftime("%Y%m%d %H:%M"),
163  product="predictions",
164  datum="MLLW",
165  interval="hilo",
166  units=self._unit_system_unit_system,
167  time_zone=self._timezone_timezone,
168  )
169  api_data = df_predictions.head()
170  self.datadata = NOAATidesData(
171  time_stamp=list(api_data.index),
172  hi_lo=list(api_data["hi_lo"].values),
173  predicted_wl=list(api_data["predicted_wl"].values),
174  )
175  _LOGGER.debug("Data = %s", api_data)
176  _LOGGER.debug(
177  "Recent Tide data queried with start time set to %s",
178  begin.strftime("%m-%d-%Y %H:%M"),
179  )
180  except ValueError as err:
181  _LOGGER.error("Check NOAA Tides and Currents: %s", err.args)
182  self.datadata = None
None __init__(self, name, station_id, timezone, unit_system, station)
Definition: sensor.py:100
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:55