forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add waterfurnace platform (home-assistant#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.
- Loading branch information
Showing
4 changed files
with
256 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters