Add waterfurnace platform (#11732)
Add waterfurnace platform This adds support for waterfurnace geothermal systems. This is implemented as a component as there will eventually be some active control elements. This is not done as a climate platform because geothermal systems work best when set at a constant temperature as they are tuned to keep within 0.5 degrees F of a setpoint, and large temperature shifts are slow and expensive. This is done in the Data + Sensors model, with the Data component having a regular update thread. That thread needs to call the read() function at least every 30 seconds otherwise the underlying websocket is closed by the server.pull/11179/merge
parent
dd81af4cd5
commit
8c78a210ef
|
@ -247,6 +247,9 @@ omit =
|
|||
homeassistant/components/volvooncall.py
|
||||
homeassistant/components/*/volvooncall.py
|
||||
|
||||
homeassistant/components/waterfurnace.py
|
||||
homeassistant/components/*/waterfurnace.py
|
||||
|
||||
homeassistant/components/*/webostv.py
|
||||
|
||||
homeassistant/components/wemo.py
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
"""
|
||||
Support for Waterfurnace.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.waterfurnace/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.sensor import ENTITY_ID_FORMAT
|
||||
from homeassistant.components.waterfurnace import (
|
||||
DOMAIN as WF_DOMAIN, UPDATE_TOPIC
|
||||
)
|
||||
from homeassistant.const import TEMP_FAHRENHEIT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import slugify
|
||||
|
||||
|
||||
class WFSensorConfig(object):
|
||||
"""Water Furnace Sensor configuration."""
|
||||
|
||||
def __init__(self, friendly_name, field, icon="mdi:guage",
|
||||
unit_of_measurement=None):
|
||||
"""Initialize configuration."""
|
||||
self.friendly_name = friendly_name
|
||||
self.field = field
|
||||
self.icon = icon
|
||||
self.unit_of_measurement = unit_of_measurement
|
||||
|
||||
|
||||
SENSORS = [
|
||||
WFSensorConfig("Furnace Mode", "mode"),
|
||||
WFSensorConfig("Total Power", "totalunitpower", "mdi:flash", "W"),
|
||||
WFSensorConfig("Active Setpoint", "tstatactivesetpoint",
|
||||
"mdi:thermometer", TEMP_FAHRENHEIT),
|
||||
WFSensorConfig("Leaving Air", "leavingairtemp",
|
||||
"mdi:thermometer", TEMP_FAHRENHEIT),
|
||||
WFSensorConfig("Room Temp", "tstatroomtemp",
|
||||
"mdi:thermometer", TEMP_FAHRENHEIT),
|
||||
WFSensorConfig("Loop Temp", "enteringwatertemp",
|
||||
"mdi:thermometer", TEMP_FAHRENHEIT),
|
||||
WFSensorConfig("Humidity Set Point", "tstathumidsetpoint",
|
||||
"mdi:water-percent", "%"),
|
||||
WFSensorConfig("Humidity", "tstatrelativehumidity",
|
||||
"mdi:water-percent", "%"),
|
||||
]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Waterfurnace sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
sensors = []
|
||||
client = hass.data[WF_DOMAIN]
|
||||
for sconfig in SENSORS:
|
||||
sensors.append(WaterFurnaceSensor(client, sconfig))
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
class WaterFurnaceSensor(Entity):
|
||||
"""Implementing the Waterfurnace sensor."""
|
||||
|
||||
def __init__(self, client, config):
|
||||
"""Initialize the sensor."""
|
||||
self.client = client
|
||||
self._name = config.friendly_name
|
||||
self._attr = config.field
|
||||
self._state = None
|
||||
self._icon = config.icon
|
||||
self._unit_of_measurement = config.unit_of_measurement
|
||||
|
||||
# This ensures that the sensors are isolated per waterfurnace unit
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(
|
||||
'wf_{}_{}'.format(slugify(self.client.unit), slugify(self._attr)))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the units of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
UPDATE_TOPIC, self.async_update_callback)
|
||||
|
||||
@callback
|
||||
def async_update_callback(self):
|
||||
"""Update state."""
|
||||
if self.client.data is not None:
|
||||
self._state = getattr(self.client.data, self._attr, None)
|
||||
self.async_schedule_update_ha_state()
|
|
@ -0,0 +1,136 @@
|
|||
"""
|
||||
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.2.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."""
|
||||
@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 ConnectionError:
|
||||
# attempt to log back in if there was a session expiration.
|
||||
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)
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Error updating waterfurnace data.")
|
||||
time.sleep(SCAN_INTERVAL.seconds)
|
||||
|
||||
else:
|
||||
self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC)
|
||||
time.sleep(SCAN_INTERVAL.seconds)
|
|
@ -1195,6 +1195,9 @@ waqiasync==1.0.0
|
|||
# homeassistant.components.cloud
|
||||
warrant==0.6.1
|
||||
|
||||
# homeassistant.components.waterfurnace
|
||||
waterfurnace==0.2.0
|
||||
|
||||
# homeassistant.components.media_player.gpmdp
|
||||
websocket-client==0.37.0
|
||||
|
||||
|
|
Loading…
Reference in New Issue