mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 08:17:08 +00:00
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.
This commit is contained in:
parent
dd81af4cd5
commit
8c78a210ef
@ -247,6 +247,9 @@ omit =
|
|||||||
homeassistant/components/volvooncall.py
|
homeassistant/components/volvooncall.py
|
||||||
homeassistant/components/*/volvooncall.py
|
homeassistant/components/*/volvooncall.py
|
||||||
|
|
||||||
|
homeassistant/components/waterfurnace.py
|
||||||
|
homeassistant/components/*/waterfurnace.py
|
||||||
|
|
||||||
homeassistant/components/*/webostv.py
|
homeassistant/components/*/webostv.py
|
||||||
|
|
||||||
homeassistant/components/wemo.py
|
homeassistant/components/wemo.py
|
||||||
|
114
homeassistant/components/sensor/waterfurnace.py
Normal file
114
homeassistant/components/sensor/waterfurnace.py
Normal file
@ -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()
|
136
homeassistant/components/waterfurnace.py
Normal file
136
homeassistant/components/waterfurnace.py
Normal file
@ -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
|
# homeassistant.components.cloud
|
||||||
warrant==0.6.1
|
warrant==0.6.1
|
||||||
|
|
||||||
|
# homeassistant.components.waterfurnace
|
||||||
|
waterfurnace==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.media_player.gpmdp
|
# homeassistant.components.media_player.gpmdp
|
||||||
websocket-client==0.37.0
|
websocket-client==0.37.0
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user