From 9b7780edf0be9ff5a86c1aad71ac22a90b48c523 Mon Sep 17 00:00:00 2001 From: Mateusz Korniak Date: Tue, 22 Jan 2019 14:14:27 +0100 Subject: [PATCH] Ecoal (esterownik.pl) static fuel boiler and pump controller (#18480) - matkor * Starting work on ecoal boiler controller iface. * Sending some values/states to controller. * Basic status parsing, and simple settings. * Platform configuration. * Temp sensors seems be working. * Switch from separate h/m/s to datetime. * Vocabulary updates. * secondary_central_heating_pump -> central_heating_pump2 * Pumps as switches. * Optional enabling pumps via config. * requests==2.20.1 added to REQUIREMENTS. * Optional enabling temp sensors from configuration yaml. * autopep8, black, pylint. * flake8. * pydocstyle * All style checkers again. * requests==2.20.1 required by homeassistant.components.sensor.ecoal_boiler. * Verify / set switches in update(). Code cleanup. * script/lint + travis issues. * Cleanup, imperative mood. * pylint, travis. * Updated .coveragerc. * Using configuration consts from homeassistant.const * typo. * Replace global ECOAL_CONTR with hass.data[DATA_ECOAL_BOILER]. Remove requests from REQUIREMENTS. * Killed .update()/reread_update() in Entities __init__()s. Removed debug/comments. * Removed debug/comments. * script/lint fixes. * script/gen_requirements_all.py run. * Travis fixes. * Configuration now validated. * Split controller code to separate package. * Replace in module docs with link to https://home-assistant.io . * Correct component module path in .coveragerc. More vals from const.py. Use dict[key] for required config keys. Check if credentials are correct during component setup. Renamed add_devices to add_entities. * Sensor/switch depends on ecoal_boiler component. EcoalSwitch inherits from SwitchDevice. Killed same as default should_poll(). Remove not neede schedule_update_ha_state() calls from turn_on/off. * lint fixes. * Move sensors/switches configuration to component setup. * Lint fixes. * Invalidating ecoal iface cache instead of force read in turn_on/off(). * Fail component setup before adding any platform entities. Kill NOTE. * Disallow setting entity names from config file, use code defined default names. * Rework configuration file to use monitored_conditions like in rainmachine component. * Killed pylint exception. Log error when connection to controller fails. * A few fixes. * Linted. --- .coveragerc | 3 + homeassistant/components/ecoal_boiler.py | 98 +++++++++++++++++++ .../components/sensor/ecoal_boiler.py | 63 ++++++++++++ .../components/switch/ecoal_boiler.py | 85 ++++++++++++++++ requirements_all.txt | 3 + 5 files changed, 252 insertions(+) create mode 100644 homeassistant/components/ecoal_boiler.py create mode 100644 homeassistant/components/sensor/ecoal_boiler.py create mode 100644 homeassistant/components/switch/ecoal_boiler.py diff --git a/.coveragerc b/.coveragerc index 237cbf8f8b7..e9627277905 100644 --- a/.coveragerc +++ b/.coveragerc @@ -91,6 +91,9 @@ omit = homeassistant/components/eight_sleep.py homeassistant/components/*/eight_sleep.py + homeassistant/components/ecoal_boiler.py + homeassistant/components/*/ecoal_boiler.py + homeassistant/components/ecobee.py homeassistant/components/*/ecobee.py diff --git a/homeassistant/components/ecoal_boiler.py b/homeassistant/components/ecoal_boiler.py new file mode 100644 index 00000000000..bd08024e64a --- /dev/null +++ b/homeassistant/components/ecoal_boiler.py @@ -0,0 +1,98 @@ +""" +Component to control ecoal/esterownik.pl coal/wood boiler controller. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ecoal_boiler/ +""" +import logging + +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + CONF_MONITORED_CONDITIONS, CONF_SENSORS, + CONF_SWITCHES) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +REQUIREMENTS = ['ecoaliface==0.4.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "ecoal_boiler" +DATA_ECOAL_BOILER = 'data_' + DOMAIN + +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "admin" + + +# Available pump ids with assigned HA names +# Available as switches +AVAILABLE_PUMPS = { + "central_heating_pump": "Central heating pump", + "central_heating_pump2": "Central heating pump2", + "domestic_hot_water_pump": "Domestic hot water pump", +} + +# Available temp sensor ids with assigned HA names +# Available as sensors +AVAILABLE_SENSORS = { + "outdoor_temp": 'Outdoor temperature', + "indoor_temp": 'Indoor temperature', + "indoor2_temp": 'Indoor temperature 2', + "domestic_hot_water_temp": 'Domestic hot water temperature', + "target_domestic_hot_water_temp": 'Target hot water temperature', + "feedwater_in_temp": 'Feedwater input temperature', + "feedwater_out_temp": 'Feedwater output temperature', + "target_feedwater_temp": 'Target feedwater temperature', + "fuel_feeder_temp": 'Fuel feeder temperature', + "exhaust_temp": 'Exhaust temperature', +} + +SWITCH_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_PUMPS)): + vol.All(cv.ensure_list, [vol.In(AVAILABLE_PUMPS)]) +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_SENSORS)): + vol.All(cv.ensure_list, [vol.In(AVAILABLE_SENSORS)]) +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_USERNAME, + default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, + default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, hass_config): + """Set up global ECoalController instance same for sensors and switches.""" + from ecoaliface.simple import ECoalController + + conf = hass_config[DOMAIN] + host = conf[CONF_HOST] + username = conf[CONF_USERNAME] + passwd = conf[CONF_PASSWORD] + # Creating ECoalController instance makes HTTP request to controller. + ecoal_contr = ECoalController(host, username, passwd) + if ecoal_contr.version is None: + # Wrong credentials nor network config + _LOGGER.error("Unable to read controller status from %s@%s" + " (wrong host/credentials)", username, host, ) + return False + _LOGGER.debug("Detected controller version: %r @%s", + ecoal_contr.version, host, ) + hass.data[DATA_ECOAL_BOILER] = ecoal_contr + # Setup switches + switches = conf[CONF_SWITCHES][CONF_MONITORED_CONDITIONS] + load_platform(hass, 'switch', DOMAIN, switches, hass_config) + # Setup temp sensors + sensors = conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS] + load_platform(hass, 'sensor', DOMAIN, sensors, hass_config) + return True diff --git a/homeassistant/components/sensor/ecoal_boiler.py b/homeassistant/components/sensor/ecoal_boiler.py new file mode 100644 index 00000000000..de81d16470c --- /dev/null +++ b/homeassistant/components/sensor/ecoal_boiler.py @@ -0,0 +1,63 @@ +""" +Allows reading temperatures from ecoal/esterownik.pl controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.ecoal_boiler/ +""" +import logging + +from homeassistant.components.ecoal_boiler import ( + DATA_ECOAL_BOILER, AVAILABLE_SENSORS, ) +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ecoal_boiler'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the ecoal sensors.""" + if discovery_info is None: + return + devices = [] + ecoal_contr = hass.data[DATA_ECOAL_BOILER] + for sensor_id in discovery_info: + name = AVAILABLE_SENSORS[sensor_id] + devices.append(EcoalTempSensor(ecoal_contr, name, sensor_id)) + add_entities(devices, True) + + +class EcoalTempSensor(Entity): + """Representation of a temperature sensor using ecoal status data.""" + + def __init__(self, ecoal_contr, name, status_attr): + """Initialize the sensor.""" + self._ecoal_contr = ecoal_contr + self._name = name + self._status_attr = status_attr + self._state = None + + @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 unit_of_measurement(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + # Old values read 0.5 back can still be used + status = self._ecoal_contr.get_cached_status() + self._state = getattr(status, self._status_attr) diff --git a/homeassistant/components/switch/ecoal_boiler.py b/homeassistant/components/switch/ecoal_boiler.py new file mode 100644 index 00000000000..d8d6c98bb8b --- /dev/null +++ b/homeassistant/components/switch/ecoal_boiler.py @@ -0,0 +1,85 @@ +""" +Allows to configuration ecoal (esterownik.pl) pumps as switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.ecoal_boiler/ +""" +import logging +from typing import Optional + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.ecoal_boiler import ( + DATA_ECOAL_BOILER, AVAILABLE_PUMPS, ) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ecoal_boiler'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up switches based on ecoal interface.""" + if discovery_info is None: + return + ecoal_contr = hass.data[DATA_ECOAL_BOILER] + switches = [] + for pump_id in discovery_info: + name = AVAILABLE_PUMPS[pump_id] + switches.append(EcoalSwitch(ecoal_contr, name, pump_id)) + add_entities(switches, True) + + +class EcoalSwitch(SwitchDevice): + """Representation of Ecoal switch.""" + + def __init__(self, ecoal_contr, name, state_attr): + """ + Initialize switch. + + Sets HA switch to state as read from controller. + """ + self._ecoal_contr = ecoal_contr + self._name = name + self._state_attr = state_attr + # Ecoalcotroller holds convention that same postfix is used + # to set attribute + # set_() + # as attribute name in status instance: + # status. + self._contr_set_fun = getattr(self._ecoal_contr, "set_" + state_attr) + # No value set, will be read from controller instead + self._state = None + + @property + def name(self) -> Optional[str]: + """Return the name of the switch.""" + return self._name + + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + status = self._ecoal_contr.get_cached_status() + self._state = getattr(status, self._state_attr) + + def invalidate_ecoal_cache(self): + """Invalidate ecoal interface cache. + + Forces that next read from ecaol interface to not use cache. + """ + self._ecoal_contr.status = None + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs) -> None: + """Turn the device on.""" + self._contr_set_fun(1) + self.invalidate_ecoal_cache() + + def turn_off(self, **kwargs) -> None: + """Turn the device off.""" + self._contr_set_fun(0) + self.invalidate_ecoal_cache() diff --git a/requirements_all.txt b/requirements_all.txt index 22ff8887370..c44c442334f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,6 +339,9 @@ dsmr_parser==0.12 # homeassistant.components.sensor.dweet dweepy==0.3.0 +# homeassistant.components.ecoal_boiler +ecoaliface==0.4.0 + # homeassistant.components.edp_redy edp_redy==0.0.3