hass-core/homeassistant/components/waterfurnace.py
Sean Dague 8c0b50b5df Bump waterfurnace to 1.0
This bumps to the new version of the waterfurnace API. In the new
version the unit id is no longer manually set by the user, instead it
is retrieved from the service after login. This is less error prone as
it turns out discovering the correct unit id is hard from an end user
perspective.

Breaking change on the config, as the unit parameter is removed from
config. However I believe the number of users is very low (possibly
only 2), so adaptation should be easy.
2018-12-05 07:03:27 -05:00

157 lines
4.9 KiB
Python

"""
Support for Waterfurnace component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/waterfurnace/
"""
from datetime import timedelta
import logging
import time
import threading
import voluptuous as vol
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
REQUIREMENTS = ["waterfurnace==1.0.0"]
_LOGGER = logging.getLogger(__name__)
DOMAIN = "waterfurnace"
UPDATE_TOPIC = 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, base_config):
"""Set up waterfurnace platform."""
import waterfurnace.waterfurnace as wf
config = base_config.get(DOMAIN)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
wfconn = wf.WaterFurnace(username, password)
# NOTE(sdague): login will throw an exception if this doesn't
# work, which will abort the setup.
try:
wfconn.login()
except wf.WFCredentialError:
_LOGGER.error("Invalid credentials for waterfurnace login.")
return False
hass.data[DOMAIN] = WaterFurnaceData(hass, wfconn)
hass.data[DOMAIN].start()
discovery.load_platform(hass, '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."""
import waterfurnace.waterfurnace as wf
self._fails += 1
if self._fails > MAX_FAILS:
_LOGGER.error(
"Failed to refresh login credentials. Thread stopped.")
self.hass.components.persistent_notification.create(
"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.seconds)
try:
self.client.login()
self.data = self.client.read()
except wf.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."""
import waterfurnace.waterfurnace as wf
@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 wf.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:
self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC)
time.sleep(SCAN_INTERVAL.seconds)