Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Ambient Weather Station Service."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from aioambient import Websocket
8 from aioambient.errors import WebsocketError
9 
10 from homeassistant.config_entries import ConfigEntry
11 from homeassistant.const import (
12  ATTR_LOCATION,
13  ATTR_NAME,
14  CONF_API_KEY,
15  EVENT_HOMEASSISTANT_STOP,
16  Platform,
17 )
18 from homeassistant.core import Event, HomeAssistant, callback
19 from homeassistant.exceptions import ConfigEntryNotReady
21 from homeassistant.helpers.dispatcher import async_dispatcher_send
23 
24 from .const import (
25  ATTR_LAST_DATA,
26  CONF_APP_KEY,
27  LOGGER,
28  TYPE_SOLARRADIATION,
29  TYPE_SOLARRADIATION_LX,
30 )
31 
32 PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
33 
34 DATA_CONFIG = "config"
35 
36 DEFAULT_SOCKET_MIN_RETRY = 15
37 
38 
39 type AmbientStationConfigEntry = ConfigEntry[AmbientStation]
40 
41 
42 @callback
43 def async_wm2_to_lx(value: float) -> int:
44  """Calculate illuminance (in lux)."""
45  return round(value / 0.0079)
46 
47 
48 @callback
49 def async_hydrate_station_data(data: dict[str, Any]) -> dict[str, Any]:
50  """Hydrate station data with addition or normalized data."""
51  if (irradiation := data.get(TYPE_SOLARRADIATION)) is not None:
52  data[TYPE_SOLARRADIATION_LX] = async_wm2_to_lx(irradiation)
53 
54  return data
55 
56 
58  hass: HomeAssistant, entry: AmbientStationConfigEntry
59 ) -> bool:
60  """Set up the Ambient PWS as config entry."""
61  if not entry.unique_id:
62  hass.config_entries.async_update_entry(
63  entry, unique_id=entry.data[CONF_APP_KEY]
64  )
65 
66  ambient = AmbientStation(
67  hass,
68  entry,
69  Websocket(entry.data[CONF_APP_KEY], entry.data[CONF_API_KEY]),
70  )
71 
72  try:
73  await ambient.ws_connect()
74  except WebsocketError as err:
75  LOGGER.error("Config entry failed: %s", err)
76  raise ConfigEntryNotReady from err
77 
78  entry.runtime_data = ambient
79 
80  async def _async_disconnect_websocket(_: Event) -> None:
81  await ambient.websocket.disconnect()
82 
83  entry.async_on_unload(
84  hass.bus.async_listen_once(
85  EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket
86  )
87  )
88 
89  return True
90 
91 
93  hass: HomeAssistant, entry: AmbientStationConfigEntry
94 ) -> bool:
95  """Unload an Ambient PWS config entry."""
96  unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
97  if unload_ok:
98  hass.async_create_task(entry.runtime_data.ws_disconnect(), eager_start=True)
99 
100  return unload_ok
101 
102 
103 async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
104  """Migrate old entry."""
105  version = entry.version
106 
107  LOGGER.debug("Migrating from version %s", version)
108 
109  # 1 -> 2: Unique ID format changed, so delete and re-import:
110  if version == 1:
111  dev_reg = dr.async_get(hass)
112  dev_reg.async_clear_config_entry(entry.entry_id)
113 
114  en_reg = er.async_get(hass)
115  en_reg.async_clear_config_entry(entry.entry_id)
116 
117  version = 2
118  hass.config_entries.async_update_entry(entry, version=version)
119 
120  LOGGER.info("Migration to version %s successful", version)
121 
122  return True
123 
124 
126  """Define a class to handle the Ambient websocket."""
127 
128  def __init__(
129  self, hass: HomeAssistant, entry: ConfigEntry, websocket: Websocket
130  ) -> None:
131  """Initialize."""
132  self._entry_entry = entry
133  self._entry_setup_complete_entry_setup_complete = False
134  self._hass_hass = hass
135  self._ws_reconnect_delay_ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
136  self.stations: dict[str, dict] = {}
137  self.websocketwebsocket = websocket
138 
139  async def ws_connect(self) -> None:
140  """Register handlers and connect to the websocket."""
141 
142  def on_connect() -> None:
143  """Define a handler to fire when the websocket is connected."""
144  LOGGER.info("Connected to websocket")
145 
146  def on_data(data: dict) -> None:
147  """Define a handler to fire when the data is received."""
148  mac = data["macAddress"]
149 
150  # If data has not changed, don't update:
151  if data == self.stations[mac][ATTR_LAST_DATA]:
152  return
153 
154  LOGGER.debug("New data received: %s", data)
155  self.stations[mac][ATTR_LAST_DATA] = async_hydrate_station_data(data)
156  async_dispatcher_send(self._hass_hass, f"ambient_station_data_update_{mac}")
157 
158  def on_disconnect() -> None:
159  """Define a handler to fire when the websocket is disconnected."""
160  LOGGER.info("Disconnected from websocket")
161 
162  def on_subscribed(data: dict) -> None:
163  """Define a handler to fire when the subscription is set."""
164  for station in data["devices"]:
165  if (mac := station["macAddress"]) in self.stations:
166  continue
167 
168  LOGGER.debug("New station subscription: %s", data)
169 
170  self.stations[mac] = {
171  ATTR_LAST_DATA: async_hydrate_station_data(station["lastData"]),
172  ATTR_LOCATION: station.get("info", {}).get("location"),
173  ATTR_NAME: station.get("info", {}).get("name", mac),
174  }
175 
176  # If the websocket disconnects and reconnects, the on_subscribed
177  # handler will get called again; in that case, we don't want to
178  # attempt forward setup of the config entry (because it will have
179  # already been done):
180  if not self._entry_setup_complete_entry_setup_complete:
181  self._hass_hass.async_create_task(
182  self._hass_hass.config_entries.async_forward_entry_setups(
183  self._entry_entry, PLATFORMS
184  ),
185  eager_start=True,
186  )
187  self._entry_setup_complete_entry_setup_complete = True
188  self._ws_reconnect_delay_ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
189 
190  self.websocketwebsocket.on_connect(on_connect)
191  self.websocketwebsocket.on_data(on_data)
192  self.websocketwebsocket.on_disconnect(on_disconnect)
193  self.websocketwebsocket.on_subscribed(on_subscribed)
194 
195  await self.websocketwebsocket.connect()
196 
197  async def ws_disconnect(self) -> None:
198  """Disconnect from the websocket."""
199  await self.websocketwebsocket.disconnect()
None __init__(self, HomeAssistant hass, ConfigEntry entry, Websocket websocket)
Definition: __init__.py:130
bool async_setup_entry(HomeAssistant hass, AmbientStationConfigEntry entry)
Definition: __init__.py:59
dict[str, Any] async_hydrate_station_data(dict[str, Any] data)
Definition: __init__.py:49
bool async_unload_entry(HomeAssistant hass, AmbientStationConfigEntry entry)
Definition: __init__.py:94
bool async_migrate_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:103
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193