Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for the Foobot indoor air quality monitor."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 import logging
7 from typing import Any
8 
9 import aiohttp
10 from foobot_async import FoobotClient
11 import voluptuous as vol
12 
14  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
15  SensorDeviceClass,
16  SensorEntity,
17  SensorEntityDescription,
18 )
19 from homeassistant.const import (
20  ATTR_TEMPERATURE,
21  CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
22  CONCENTRATION_PARTS_PER_BILLION,
23  CONCENTRATION_PARTS_PER_MILLION,
24  CONF_TOKEN,
25  CONF_USERNAME,
26  PERCENTAGE,
27  UnitOfTemperature,
28 )
29 from homeassistant.core import HomeAssistant
30 from homeassistant.exceptions import PlatformNotReady
31 from homeassistant.helpers import config_validation as cv
32 from homeassistant.helpers.aiohttp_client import async_get_clientsession
33 from homeassistant.helpers.entity_platform import AddEntitiesCallback
34 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
35 from homeassistant.util import Throttle
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 ATTR_HUMIDITY = "humidity"
40 ATTR_PM2_5 = "PM2.5"
41 ATTR_CARBON_DIOXIDE = "CO2"
42 ATTR_VOLATILE_ORGANIC_COMPOUNDS = "VOC"
43 ATTR_FOOBOT_INDEX = "index"
44 
45 SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
47  key="pm",
48  name=ATTR_PM2_5,
49  native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
50  icon="mdi:cloud",
51  ),
53  key="tmp",
54  name=ATTR_TEMPERATURE,
55  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
56  device_class=SensorDeviceClass.TEMPERATURE,
57  ),
59  key="hum",
60  name=ATTR_HUMIDITY,
61  native_unit_of_measurement=PERCENTAGE,
62  icon="mdi:water-percent",
63  ),
65  key="co2",
66  name=ATTR_CARBON_DIOXIDE,
67  native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
68  icon="mdi:molecule-co2",
69  ),
71  key="voc",
72  name=ATTR_VOLATILE_ORGANIC_COMPOUNDS,
73  native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
74  icon="mdi:cloud",
75  ),
77  key="allpollu",
78  name=ATTR_FOOBOT_INDEX,
79  native_unit_of_measurement=PERCENTAGE,
80  icon="mdi:percent",
81  ),
82 )
83 
84 SCAN_INTERVAL = timedelta(minutes=10)
85 PARALLEL_UPDATES = 1
86 
87 TIMEOUT = 10
88 
89 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
90  {vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_USERNAME): cv.string}
91 )
92 
93 
95  hass: HomeAssistant,
96  config: ConfigType,
97  async_add_entities: AddEntitiesCallback,
98  discovery_info: DiscoveryInfoType | None = None,
99 ) -> None:
100  """Set up the devices associated with the account."""
101  token: str = config[CONF_TOKEN]
102  username: str = config[CONF_USERNAME]
103 
104  client = FoobotClient(
105  token, username, async_get_clientsession(hass), timeout=TIMEOUT
106  )
107  entities: list[FoobotSensor] = []
108  try:
109  devices: list[dict[str, Any]] = await client.get_devices()
110  _LOGGER.debug("The following devices were found: %s", devices)
111  for device in devices:
112  foobot_data = FoobotData(client, device["uuid"])
113  entities.extend(
114  [
115  FoobotSensor(foobot_data, device, description)
116  for description in SENSOR_TYPES
117  ]
118  )
119  except (
120  aiohttp.client_exceptions.ClientConnectorError,
121  TimeoutError,
122  FoobotClient.TooManyRequests,
123  FoobotClient.InternalError,
124  ) as err:
125  _LOGGER.exception("Failed to connect to foobot servers")
126  raise PlatformNotReady from err
127  except FoobotClient.ClientError:
128  _LOGGER.error("Failed to fetch data from foobot servers")
129  return
130  async_add_entities(entities, True)
131 
132 
134  """Implementation of a Foobot sensor."""
135 
136  def __init__(
137  self,
138  data: FoobotData,
139  device: dict[str, Any],
140  description: SensorEntityDescription,
141  ) -> None:
142  """Initialize the sensor."""
143  self.entity_descriptionentity_description = description
144  self.foobot_datafoobot_data = data
145 
146  self._attr_name_attr_name = f"Foobot {device['name']} {description.name}"
147  self._attr_unique_id_attr_unique_id = f"{device['uuid']}_{description.key}"
148 
149  @property
150  def native_value(self) -> float | None:
151  """Return the state of the device."""
152  return self.foobot_datafoobot_data.data.get(self.entity_descriptionentity_description.key)
153 
154  async def async_update(self) -> None:
155  """Get the latest data."""
156  await self.foobot_datafoobot_data.async_update()
157 
158 
160  """Get data from Foobot API."""
161 
162  def __init__(self, client: FoobotClient, uuid: str) -> None:
163  """Initialize the data object."""
164  self._client_client = client
165  self._uuid_uuid = uuid
166  self.datadata: dict[str, float] = {}
167 
168  @Throttle(SCAN_INTERVAL)
169  async def async_update(self) -> bool:
170  """Get the data from Foobot API."""
171  interval = SCAN_INTERVAL.total_seconds()
172  try:
173  response: list[dict[str, Any]] = await self._client_client.get_last_data(
174  self._uuid_uuid, interval, interval + 1
175  )
176  except (
177  aiohttp.client_exceptions.ClientConnectorError,
178  TimeoutError,
179  self._client_client.TooManyRequests,
180  self._client_client.InternalError,
181  ):
182  _LOGGER.debug("Couldn't fetch data")
183  return False
184  _LOGGER.debug("The data response is: %s", response)
185  self.datadata = {k: round(v, 1) for k, v in response[0].items()}
186  return True
None __init__(self, FoobotClient client, str uuid)
Definition: sensor.py:162
None __init__(self, FoobotData data, dict[str, Any] device, SensorEntityDescription description)
Definition: sensor.py:141
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:99
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)