Home Assistant Unofficial Reference 2024.12.1
util.py
Go to the documentation of this file.
1 """Shared utilities for different supported platforms."""
2 
3 from datetime import datetime, timedelta
4 from http import HTTPStatus
5 import logging
6 from typing import Any
7 
8 import aiohttp
9 from buienradar.buienradar import parse_data
10 from buienradar.constants import (
11  ATTRIBUTION,
12  CONDITION,
13  CONTENT,
14  DATA,
15  FORECAST,
16  HUMIDITY,
17  MESSAGE,
18  PRESSURE,
19  STATIONNAME,
20  STATUS_CODE,
21  SUCCESS,
22  TEMPERATURE,
23  VISIBILITY,
24  WINDAZIMUTH,
25  WINDSPEED,
26 )
27 from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url
28 
29 from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
30 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
31 from homeassistant.helpers.aiohttp_client import async_get_clientsession
32 from homeassistant.helpers.event import async_track_point_in_utc_time
33 from homeassistant.util import dt as dt_util
34 
35 from .const import DEFAULT_TIMEOUT, SCHEDULE_NOK, SCHEDULE_OK
36 
37 __all__ = ["BrData"]
38 _LOGGER = logging.getLogger(__name__)
39 
40 """
41 Log at WARN level after WARN_THRESHOLD failures, otherwise log at
42 DEBUG level.
43 """
44 WARN_THRESHOLD = 4
45 
46 
47 def threshold_log(count: int, *args, **kwargs) -> None:
48  """Log at warn level after WARN_THRESHOLD failures, debug otherwise."""
49  if count >= WARN_THRESHOLD:
50  _LOGGER.warning(*args, **kwargs)
51  else:
52  _LOGGER.debug(*args, **kwargs)
53 
54 
55 class BrData:
56  """Get the latest data and updates the states."""
57 
58  # Initialize to warn immediately if the first call fails.
59  load_error_count: int = WARN_THRESHOLD
60  rain_error_count: int = WARN_THRESHOLD
61 
62  def __init__(self, hass: HomeAssistant, coordinates, timeframe, devices) -> None:
63  """Initialize the data object."""
64  self.devicesdevices = devices
65  self.datadata: dict[str, Any] | None = {}
66  self.hasshass = hass
67  self.coordinatescoordinates = coordinates
68  self.timeframetimeframe = timeframe
69  self.unsub_schedule_updateunsub_schedule_update: CALLBACK_TYPE | None = None
70 
71  async def update_devices(self):
72  """Update all devices/sensors."""
73  if not self.devicesdevices:
74  return
75 
76  # Update all devices
77  for dev in self.devicesdevices:
78  dev.data_updated(self)
79 
80  @callback
81  def async_schedule_update(self, minute=1):
82  """Schedule an update after minute minutes."""
83  _LOGGER.debug("Scheduling next update in %s minutes", minute)
84  nxt = dt_util.utcnow() + timedelta(minutes=minute)
86  self.hasshass, self.async_updateasync_update, nxt
87  )
88 
89  async def get_data(self, url):
90  """Load data from specified url."""
91  _LOGGER.debug("Calling url: %s", url)
92  result = {SUCCESS: False, MESSAGE: None}
93  resp = None
94  try:
95  websession = async_get_clientsession(self.hasshass)
96  async with websession.get(
97  url, timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT)
98  ) as resp:
99  result[STATUS_CODE] = resp.status
100  result[CONTENT] = await resp.text()
101  if resp.status == HTTPStatus.OK:
102  result[SUCCESS] = True
103  else:
104  result[MESSAGE] = f"Got http statuscode: {resp.status}"
105 
106  return result
107  except (TimeoutError, aiohttp.ClientError) as err:
108  result[MESSAGE] = str(err)
109  return result
110  finally:
111  if resp is not None:
112  resp.release()
113 
114  async def _async_update(self):
115  """Update the data from buienradar."""
116  content = await self.get_dataget_data(JSON_FEED_URL)
117 
118  if content.get(SUCCESS) is not True:
119  # unable to get the data
120  self.load_error_countload_error_count += 1
122  self.load_error_countload_error_count,
123  "Unable to retrieve json data from Buienradar (Msg: %s, status: %s)",
124  content.get(MESSAGE),
125  content.get(STATUS_CODE),
126  )
127  return None
128  self.load_error_countload_error_count = 0
129 
130  # rounding coordinates prevents unnecessary redirects/calls
131  lat = self.coordinatescoordinates[CONF_LATITUDE]
132  lon = self.coordinatescoordinates[CONF_LONGITUDE]
133  rainurl = json_precipitation_forecast_url(lat, lon)
134  raincontent = await self.get_dataget_data(rainurl)
135 
136  if raincontent.get(SUCCESS) is not True:
137  self.rain_error_countrain_error_count += 1
138  # unable to get the data
140  self.rain_error_countrain_error_count,
141  "Unable to retrieve rain data from Buienradar (Msg: %s, status: %s)",
142  raincontent.get(MESSAGE),
143  raincontent.get(STATUS_CODE),
144  )
145  return None
146  self.rain_error_countrain_error_count = 0
147 
148  result = parse_data(
149  content.get(CONTENT),
150  raincontent.get(CONTENT),
151  self.coordinatescoordinates[CONF_LATITUDE],
152  self.coordinatescoordinates[CONF_LONGITUDE],
153  self.timeframetimeframe,
154  False,
155  )
156 
157  _LOGGER.debug("Buienradar parsed data: %s", result)
158  if result.get(SUCCESS) is not True:
159  if int(datetime.now().strftime("%H")) > 0:
160  _LOGGER.warning(
161  "Unable to parse data from Buienradar. (Msg: %s)",
162  result.get(MESSAGE),
163  )
164  return None
165 
166  return result[DATA]
167 
168  async def async_update(self, *_):
169  """Update the data from buienradar and schedule the next update."""
170  data = await self._async_update_async_update()
171 
172  if data is None:
173  self.async_schedule_updateasync_schedule_update(SCHEDULE_NOK)
174  return
175 
176  self.datadata = data
177  await self.update_devicesupdate_devices()
178  self.async_schedule_updateasync_schedule_update(SCHEDULE_OK)
179 
180  @property
181  def attribution(self):
182  """Return the attribution."""
183  return self.datadata.get(ATTRIBUTION)
184 
185  @property
186  def stationname(self):
187  """Return the name of the selected weatherstation."""
188  return self.datadata.get(STATIONNAME)
189 
190  @property
191  def condition(self):
192  """Return the condition."""
193  return self.datadata.get(CONDITION)
194 
195  @property
196  def temperature(self):
197  """Return the temperature, or None."""
198  try:
199  return float(self.datadata.get(TEMPERATURE))
200  except (ValueError, TypeError):
201  return None
202 
203  @property
204  def pressure(self):
205  """Return the pressure, or None."""
206  try:
207  return float(self.datadata.get(PRESSURE))
208  except (ValueError, TypeError):
209  return None
210 
211  @property
212  def humidity(self):
213  """Return the humidity, or None."""
214  try:
215  return int(self.datadata.get(HUMIDITY))
216  except (ValueError, TypeError):
217  return None
218 
219  @property
220  def visibility(self):
221  """Return the visibility, or None."""
222  try:
223  return int(self.datadata.get(VISIBILITY))
224  except (ValueError, TypeError):
225  return None
226 
227  @property
228  def wind_speed(self):
229  """Return the windspeed, or None."""
230  try:
231  return float(self.datadata.get(WINDSPEED))
232  except (ValueError, TypeError):
233  return None
234 
235  @property
236  def wind_bearing(self):
237  """Return the wind bearing, or None."""
238  try:
239  return int(self.datadata.get(WINDAZIMUTH))
240  except (ValueError, TypeError):
241  return None
242 
243  @property
244  def forecast(self):
245  """Return the forecast data."""
246  return self.datadata.get(FORECAST)
def async_schedule_update(self, minute=1)
Definition: util.py:81
None __init__(self, HomeAssistant hass, coordinates, timeframe, devices)
Definition: util.py:62
None threshold_log(int count, *args, **kwargs)
Definition: util.py:47
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
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)
CALLBACK_TYPE async_track_point_in_utc_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)
Definition: event.py:1542