Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Waterfurnaces."""
2 
3 from datetime import timedelta
4 import logging
5 import threading
6 import time
7 
8 import voluptuous as vol
9 from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException
10 
11 from homeassistant.components import persistent_notification
12 from homeassistant.const import (
13  CONF_PASSWORD,
14  CONF_USERNAME,
15  EVENT_HOMEASSISTANT_STOP,
16  Platform,
17 )
18 from homeassistant.core import HomeAssistant, callback
19 from homeassistant.helpers import config_validation as cv, discovery
20 from homeassistant.helpers.dispatcher import dispatcher_send
21 from homeassistant.helpers.typing import ConfigType
22 
23 _LOGGER = logging.getLogger(__name__)
24 
25 DOMAIN = "waterfurnace"
26 UPDATE_TOPIC = f"{DOMAIN}_update"
27 SCAN_INTERVAL = timedelta(seconds=10)
28 ERROR_INTERVAL = timedelta(seconds=300)
29 MAX_FAILS = 10
30 NOTIFICATION_ID = "waterfurnace_website_notification"
31 NOTIFICATION_TITLE = "WaterFurnace website status"
32 
33 
34 CONFIG_SCHEMA = vol.Schema(
35  {
36  DOMAIN: vol.Schema(
37  {
38  vol.Required(CONF_PASSWORD): cv.string,
39  vol.Required(CONF_USERNAME): cv.string,
40  }
41  )
42  },
43  extra=vol.ALLOW_EXTRA,
44 )
45 
46 
47 def setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
48  """Set up waterfurnace platform."""
49 
50  config = base_config[DOMAIN]
51 
52  username = config[CONF_USERNAME]
53  password = config[CONF_PASSWORD]
54 
55  wfconn = WaterFurnace(username, password)
56  # NOTE(sdague): login will throw an exception if this doesn't
57  # work, which will abort the setup.
58  try:
59  wfconn.login()
60  except WFCredentialError:
61  _LOGGER.error("Invalid credentials for waterfurnace login")
62  return False
63 
64  hass.data[DOMAIN] = WaterFurnaceData(hass, wfconn)
65  hass.data[DOMAIN].start()
66 
67  discovery.load_platform(hass, Platform.SENSOR, DOMAIN, {}, config)
68  return True
69 
70 
71 class WaterFurnaceData(threading.Thread):
72  """WaterFurnace Data collector.
73 
74  This is implemented as a dedicated thread polling a websocket in a
75  tight loop. The websocket will shut itself from the server side if
76  a packet is not sent at least every 30 seconds. The reading is
77  cheap, the login is less cheap, so keeping this open and polling
78  on a very regular cadence is actually the least io intensive thing
79  to do.
80  """
81 
82  def __init__(self, hass, client):
83  """Initialize the data object."""
84  super().__init__()
85  self.hasshass = hass
86  self.clientclient = client
87  self.unitunit = self.clientclient.gwid
88  self.datadata = None
89  self._shutdown_shutdown = False
90  self._fails_fails = 0
91 
92  def _reconnect(self):
93  """Reconnect on a failure."""
94 
95  self._fails_fails += 1
96  if self._fails_fails > MAX_FAILS:
97  _LOGGER.error("Failed to refresh login credentials. Thread stopped")
98  persistent_notification.create(
99  self.hasshass,
100  (
101  "Error:<br/>Connection to waterfurnace website failed "
102  "the maximum number of times. Thread has stopped"
103  ),
104  title=NOTIFICATION_TITLE,
105  notification_id=NOTIFICATION_ID,
106  )
107 
108  self._shutdown_shutdown = True
109  return
110 
111  # sleep first before the reconnect attempt
112  _LOGGER.debug("Sleeping for fail # %s", self._fails_fails)
113  time.sleep(self._fails_fails * ERROR_INTERVAL.total_seconds())
114 
115  try:
116  self.clientclient.login()
117  self.datadata = self.clientclient.read()
118  except WFException:
119  _LOGGER.exception("Failed to reconnect attempt %s", self._fails_fails)
120  else:
121  _LOGGER.debug("Reconnected to furnace")
122  self._fails_fails = 0
123 
124  def run(self):
125  """Thread run loop."""
126 
127  @callback
128  def register():
129  """Connect to hass for shutdown."""
130 
131  def shutdown(event):
132  """Shutdown the thread."""
133  _LOGGER.debug("Signaled to shutdown")
134  self._shutdown_shutdown = True
135  self.join()
136 
137  self.hasshass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
138 
139  self.hasshass.add_job(register)
140 
141  # This does a tight loop in sending read calls to the
142  # websocket. That's a blocking call, which returns pretty
143  # quickly (1 second). It's important that we do this
144  # frequently though, because if we don't call the websocket at
145  # least every 30 seconds the server side closes the
146  # connection.
147  while True:
148  if self._shutdown_shutdown:
149  _LOGGER.debug("Graceful shutdown")
150  return
151 
152  try:
153  self.datadata = self.clientclient.read()
154 
155  except WFException:
156  # WFExceptions are things the WF library understands
157  # that pretty much can all be solved by logging in and
158  # back out again.
159  _LOGGER.exception("Failed to read data, attempting to recover")
160  self._reconnect_reconnect()
161 
162  else:
163  dispatcher_send(self.hasshass, UPDATE_TOPIC)
164  time.sleep(SCAN_INTERVAL.total_seconds())
def register(HomeAssistant hass, Heos controller)
Definition: services.py:29
bool setup(HomeAssistant hass, ConfigType base_config)
Definition: __init__.py:47
None dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:137