core/homeassistant/components/waterfurnace.py

138 lines
4.5 KiB
Python
Raw Normal View History

"""
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==0.4.0"]
_LOGGER = logging.getLogger(__name__)
DOMAIN = "waterfurnace"
UPDATE_TOPIC = DOMAIN + "_update"
CONF_UNIT = "unit"
SCAN_INTERVAL = timedelta(seconds=10)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_UNIT): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, base_config):
"""Setup waterfurnace platform."""
import waterfurnace.waterfurnace as wf
config = base_config.get(DOMAIN)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
unit = config.get(CONF_UNIT)
wfconn = wf.WaterFurnace(username, password, unit)
# 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 = client.unit
self.data = None
self._shutdown = False
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")
try:
self.client.login()
except Exception: # pylint: disable=broad-except
# nested exception handling, something really bad
# happened during the login, which means we're not
# in a recoverable state. Stop the thread so we
# don't do just keep poking at the service.
_LOGGER.error(
"Failed to refresh login credentials. Thread stopped.")
return
else:
_LOGGER.error(
"Lost our connection to websocket, trying again")
time.sleep(SCAN_INTERVAL.seconds)
else:
self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC)
time.sleep(SCAN_INTERVAL.seconds)