mirror of https://github.com/home-assistant/core
165 lines
5.0 KiB
Python
165 lines
5.0 KiB
Python
"""Support for Waterfurnaces."""
|
|
|
|
from datetime import timedelta
|
|
import logging
|
|
import threading
|
|
import time
|
|
|
|
import voluptuous as vol
|
|
from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException
|
|
|
|
from homeassistant.components import persistent_notification
|
|
from homeassistant.const import (
|
|
CONF_PASSWORD,
|
|
CONF_USERNAME,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
Platform,
|
|
)
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers import config_validation as cv, discovery
|
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DOMAIN = "waterfurnace"
|
|
UPDATE_TOPIC = f"{DOMAIN}_update"
|
|
SCAN_INTERVAL = timedelta(seconds=10)
|
|
ERROR_INTERVAL = timedelta(seconds=300)
|
|
MAX_FAILS = 10
|
|
NOTIFICATION_ID = "waterfurnace_website_notification"
|
|
NOTIFICATION_TITLE = "WaterFurnace website status"
|
|
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
|
|
def setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
|
|
"""Set up waterfurnace platform."""
|
|
|
|
config = base_config[DOMAIN]
|
|
|
|
username = config[CONF_USERNAME]
|
|
password = config[CONF_PASSWORD]
|
|
|
|
wfconn = WaterFurnace(username, password)
|
|
# NOTE(sdague): login will throw an exception if this doesn't
|
|
# work, which will abort the setup.
|
|
try:
|
|
wfconn.login()
|
|
except WFCredentialError:
|
|
_LOGGER.error("Invalid credentials for waterfurnace login")
|
|
return False
|
|
|
|
hass.data[DOMAIN] = WaterFurnaceData(hass, wfconn)
|
|
hass.data[DOMAIN].start()
|
|
|
|
discovery.load_platform(hass, Platform.SENSOR, DOMAIN, {}, config)
|
|
return True
|
|
|
|
|
|
class WaterFurnaceData(threading.Thread):
|
|
"""WaterFurnace Data collector.
|
|
|
|
This is implemented as a dedicated thread polling a websocket in a
|
|
tight loop. The websocket will shut itself from the server side if
|
|
a packet is not sent at least every 30 seconds. The reading is
|
|
cheap, the login is less cheap, so keeping this open and polling
|
|
on a very regular cadence is actually the least io intensive thing
|
|
to do.
|
|
"""
|
|
|
|
def __init__(self, hass, client):
|
|
"""Initialize the data object."""
|
|
super().__init__()
|
|
self.hass = hass
|
|
self.client = client
|
|
self.unit = self.client.gwid
|
|
self.data = None
|
|
self._shutdown = False
|
|
self._fails = 0
|
|
|
|
def _reconnect(self):
|
|
"""Reconnect on a failure."""
|
|
|
|
self._fails += 1
|
|
if self._fails > MAX_FAILS:
|
|
_LOGGER.error("Failed to refresh login credentials. Thread stopped")
|
|
persistent_notification.create(
|
|
self.hass,
|
|
(
|
|
"Error:<br/>Connection to waterfurnace website failed "
|
|
"the maximum number of times. Thread has stopped"
|
|
),
|
|
title=NOTIFICATION_TITLE,
|
|
notification_id=NOTIFICATION_ID,
|
|
)
|
|
|
|
self._shutdown = True
|
|
return
|
|
|
|
# sleep first before the reconnect attempt
|
|
_LOGGER.debug("Sleeping for fail # %s", self._fails)
|
|
time.sleep(self._fails * ERROR_INTERVAL.total_seconds())
|
|
|
|
try:
|
|
self.client.login()
|
|
self.data = self.client.read()
|
|
except WFException:
|
|
_LOGGER.exception("Failed to reconnect attempt %s", self._fails)
|
|
else:
|
|
_LOGGER.debug("Reconnected to furnace")
|
|
self._fails = 0
|
|
|
|
def run(self):
|
|
"""Thread run loop."""
|
|
|
|
@callback
|
|
def register():
|
|
"""Connect to hass for shutdown."""
|
|
|
|
def shutdown(event):
|
|
"""Shutdown the thread."""
|
|
_LOGGER.debug("Signaled to shutdown")
|
|
self._shutdown = True
|
|
self.join()
|
|
|
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
|
|
|
|
self.hass.add_job(register)
|
|
|
|
# This does a tight loop in sending read calls to the
|
|
# websocket. That's a blocking call, which returns pretty
|
|
# quickly (1 second). It's important that we do this
|
|
# frequently though, because if we don't call the websocket at
|
|
# least every 30 seconds the server side closes the
|
|
# connection.
|
|
while True:
|
|
if self._shutdown:
|
|
_LOGGER.debug("Graceful shutdown")
|
|
return
|
|
|
|
try:
|
|
self.data = self.client.read()
|
|
|
|
except WFException:
|
|
# WFExceptions are things the WF library understands
|
|
# that pretty much can all be solved by logging in and
|
|
# back out again.
|
|
_LOGGER.exception("Failed to read data, attempting to recover")
|
|
self._reconnect()
|
|
|
|
else:
|
|
dispatcher_send(self.hass, UPDATE_TOPIC)
|
|
time.sleep(SCAN_INTERVAL.total_seconds())
|