diff --git a/.coveragerc b/.coveragerc index e4d5bca95d3..ef421af6875 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/homeassistant/components/sensor/waterfurnace.py b/homeassistant/components/sensor/waterfurnace.py new file mode 100644 index 00000000000..7d8c71f8d51 --- /dev/null +++ b/homeassistant/components/sensor/waterfurnace.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() diff --git a/homeassistant/components/waterfurnace.py b/homeassistant/components/waterfurnace.py new file mode 100644 index 00000000000..18e6cff020c --- /dev/null +++ b/homeassistant/components/waterfurnace.py @@ -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) diff --git a/requirements_all.txt b/requirements_all.txt index c88f87528ce..1c1f46db3c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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