Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Sensor for checking the status of London air."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 from http import HTTPStatus
7 import logging
8 
9 import requests
10 import voluptuous as vol
11 
13  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
14  SensorEntity,
15 )
16 from homeassistant.core import HomeAssistant
18 from homeassistant.helpers.entity_platform import AddEntitiesCallback
19 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
20 from homeassistant.util import Throttle
21 
22 _LOGGER = logging.getLogger(__name__)
23 
24 CONF_LOCATIONS = "locations"
25 
26 SCAN_INTERVAL = timedelta(minutes=30)
27 
28 AUTHORITIES = [
29  "Barking and Dagenham",
30  "Bexley",
31  "Brent",
32  "Bromley",
33  "Camden",
34  "City of London",
35  "Croydon",
36  "Ealing",
37  "Enfield",
38  "Greenwich",
39  "Hackney",
40  "Haringey",
41  "Harrow",
42  "Havering",
43  "Hillingdon",
44  "Islington",
45  "Kensington and Chelsea",
46  "Kingston",
47  "Lambeth",
48  "Lewisham",
49  "Merton",
50  "Redbridge",
51  "Richmond",
52  "Southwark",
53  "Sutton",
54  "Tower Hamlets",
55  "Wandsworth",
56  "Westminster",
57 ]
58 
59 URL = "http://api.erg.kcl.ac.uk/AirQuality/Hourly/MonitoringIndex/GroupName=London/Json"
60 
61 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
62  {
63  vol.Optional(CONF_LOCATIONS, default=AUTHORITIES): vol.All(
64  cv.ensure_list, [vol.In(AUTHORITIES)]
65  )
66  }
67 )
68 
69 
71  hass: HomeAssistant,
72  config: ConfigType,
73  add_entities: AddEntitiesCallback,
74  discovery_info: DiscoveryInfoType | None = None,
75 ) -> None:
76  """Set up the London Air sensor."""
77  data = APIData()
78  data.update()
79 
80  add_entities((AirSensor(name, data) for name in config[CONF_LOCATIONS]), True)
81 
82 
83 class APIData:
84  """Get the latest data for all authorities."""
85 
86  def __init__(self) -> None:
87  """Initialize the AirData object."""
88  self.datadata = None
89 
90  # Update only once in scan interval.
91  @Throttle(SCAN_INTERVAL)
92  def update(self):
93  """Get the latest data from TFL."""
94  response = requests.get(URL, timeout=10)
95  if response.status_code != HTTPStatus.OK:
96  _LOGGER.warning("Invalid response from API")
97  else:
98  self.datadata = parse_api_response(response.json())
99 
100 
102  """Single authority air sensor."""
103 
104  ICON = "mdi:cloud-outline"
105 
106  def __init__(self, name, api_data):
107  """Initialize the sensor."""
108  self._name_name = name
109  self._api_data_api_data = api_data
110  self._site_data_site_data = None
111  self._state_state = None
112  self._updated_updated = None
113 
114  @property
115  def name(self):
116  """Return the name of the sensor."""
117  return self._name_name
118 
119  @property
120  def native_value(self):
121  """Return the state of the sensor."""
122  return self._state_state
123 
124  @property
125  def site_data(self):
126  """Return the dict of sites data."""
127  return self._site_data_site_data
128 
129  @property
130  def icon(self):
131  """Icon to use in the frontend, if any."""
132  return self.ICONICON
133 
134  @property
136  """Return other details about the sensor state."""
137  attrs = {}
138  attrs["updated"] = self._updated_updated
139  attrs["sites"] = len(self._site_data_site_data) if self._site_data_site_data is not None else 0
140  attrs["data"] = self._site_data_site_data
141  return attrs
142 
143  def update(self) -> None:
144  """Update the sensor."""
145  sites_status: list = []
146  self._api_data_api_data.update()
147  if self._api_data_api_data.data:
148  self._site_data_site_data = self._api_data_api_data.data[self._name_name]
149  self._updated_updated = self._site_data_site_data[0]["updated"]
150  sites_status.extend(
151  site["pollutants_status"]
152  for site in self._site_data_site_data
153  if site["pollutants_status"] != "no_species_data"
154  )
155 
156  if sites_status:
157  self._state_state = max(set(sites_status), key=sites_status.count)
158  else:
159  self._state_state = None
160 
161 
162 def parse_species(species_data):
163  """Iterate over list of species at each site."""
164  parsed_species_data = []
165  quality_list = []
166  for species in species_data:
167  if species["@AirQualityBand"] != "No data":
168  species_dict = {}
169  species_dict["description"] = species["@SpeciesDescription"]
170  species_dict["code"] = species["@SpeciesCode"]
171  species_dict["quality"] = species["@AirQualityBand"]
172  species_dict["index"] = species["@AirQualityIndex"]
173  species_dict["summary"] = (
174  f"{species_dict['code']} is {species_dict['quality']}"
175  )
176  parsed_species_data.append(species_dict)
177  quality_list.append(species_dict["quality"])
178  return parsed_species_data, quality_list
179 
180 
181 def parse_site(entry_sites_data):
182  """Iterate over all sites at an authority."""
183  authority_data = []
184  for site in entry_sites_data:
185  site_data = {}
186  species_data = []
187 
188  site_data["updated"] = site["@BulletinDate"]
189  site_data["latitude"] = site["@Latitude"]
190  site_data["longitude"] = site["@Longitude"]
191  site_data["site_code"] = site["@SiteCode"]
192  site_data["site_name"] = site["@SiteName"].split("-")[-1].lstrip()
193  site_data["site_type"] = site["@SiteType"]
194 
195  if isinstance(site["Species"], dict):
196  species_data = [site["Species"]]
197  else:
198  species_data = site["Species"]
199 
200  parsed_species_data, quality_list = parse_species(species_data)
201 
202  if not parsed_species_data:
203  parsed_species_data.append("no_species_data")
204  site_data["pollutants"] = parsed_species_data
205 
206  if quality_list:
207  site_data["pollutants_status"] = max(
208  set(quality_list), key=quality_list.count
209  )
210  site_data["number_of_pollutants"] = len(quality_list)
211  else:
212  site_data["pollutants_status"] = "no_species_data"
213  site_data["number_of_pollutants"] = 0
214 
215  authority_data.append(site_data)
216  return authority_data
217 
218 
219 def parse_api_response(response):
220  """Parse return dict or list of data from API."""
221  data = dict.fromkeys(AUTHORITIES)
222  for authority in AUTHORITIES:
223  for entry in response["HourlyAirQualityIndex"]["LocalAuthority"]:
224  if entry["@LocalAuthorityName"] == authority:
225  entry_sites_data = []
226  if "Site" in entry:
227  if isinstance(entry["Site"], dict):
228  entry_sites_data = [entry["Site"]]
229  else:
230  entry_sites_data = entry["Site"]
231 
232  data[authority] = parse_site(entry_sites_data)
233 
234  return data
def add_entities(account, async_add_entities, tracked)
Definition: sensor.py:40
def parse_site(entry_sites_data)
Definition: sensor.py:181
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:75