From 0a0975b5d94b975605586fef22888a091daa0f95 Mon Sep 17 00:00:00 2001 From: Martin Fuchs <39280548+fucm@users.noreply.github.com> Date: Sat, 13 Apr 2019 23:53:36 +0200 Subject: [PATCH] Add support for Stiebel Eltron heat pumps (#21199) * Start with Stiebel Eltron heatpump * STE HP * Add read of operating mode * Add read-write operation mode * Further extract ModBus access * Separation of platform and API * Last changes * Use modbus hub * Update module doc with config * Clean up platform code * Cleanup and update to dev2 of pystiebeleltron * Remove slave configuration * Add translation of states * Make name parameter optional * Consolidate platform * Correct .coveragerc after conflict * Prepare component for sensor platform * Fix issues found in review * Remove custom states and map to existing HA states * Force update, when values are modified * Update CODEOWNERS and requirements_all.txt * Fix .coveragerc file * Exclude stiebel_eltron components in .coveragerc * Break out to module level constant * Rename constant * Removed REQ and DEP constant. --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/stiebel_eltron/__init__.py | 59 +++++++ .../components/stiebel_eltron/climate.py | 149 ++++++++++++++++++ .../components/stiebel_eltron/manifest.json | 14 ++ requirements_all.txt | 3 + 6 files changed, 227 insertions(+) create mode 100644 homeassistant/components/stiebel_eltron/__init__.py create mode 100644 homeassistant/components/stiebel_eltron/climate.py create mode 100644 homeassistant/components/stiebel_eltron/manifest.json diff --git a/.coveragerc b/.coveragerc index ba6f27d3655..53fe5f306b2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -559,6 +559,7 @@ omit = homeassistant/components/srp_energy/sensor.py homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py + homeassistant/components/stiebel_eltron/* homeassistant/components/stride/notify.py homeassistant/components/supervisord/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 2b45acea2e1..8775e886787 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -195,6 +195,7 @@ homeassistant/components/spaceapi/* @fabaff homeassistant/components/spider/* @peternijssen homeassistant/components/sql/* @dgomes homeassistant/components/statistics/* @fabaff +homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/sun/* @home-assistant/core homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff diff --git a/homeassistant/components/stiebel_eltron/__init__.py b/homeassistant/components/stiebel_eltron/__init__.py new file mode 100644 index 00000000000..52dc2d84891 --- /dev/null +++ b/homeassistant/components/stiebel_eltron/__init__.py @@ -0,0 +1,59 @@ +"""The component for STIEBEL ELTRON heat pumps with ISGWeb Modbus module.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.modbus import ( + CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +DOMAIN = 'stiebel_eltron' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): cv.string, + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + + +def setup(hass, config): + """Set up the STIEBEL ELTRON unit. + + Will automatically load climate platform. + """ + name = config[DOMAIN][CONF_NAME] + modbus_client = hass.data[MODBUS_DOMAIN][config[DOMAIN][CONF_HUB]] + + hass.data[DOMAIN] = { + 'name': name, + 'ste_data': StiebelEltronData(name, modbus_client) + } + + discovery.load_platform(hass, 'climate', DOMAIN, {}, config) + return True + + +class StiebelEltronData: + """Get the latest data and update the states.""" + + def __init__(self, name, modbus_client): + """Init the STIEBEL ELTRON data object.""" + from pystiebeleltron import pystiebeleltron + self.api = pystiebeleltron.StiebelEltronAPI(modbus_client, 1) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update unit data.""" + if not self.api.update(): + _LOGGER.warning("Modbus read failed") + else: + _LOGGER.debug("Data updated successfully") diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py new file mode 100644 index 00000000000..fc6038d95ad --- /dev/null +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -0,0 +1,149 @@ +"""Support for stiebel_eltron climate platform.""" +import logging + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_ECO, STATE_MANUAL, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE) +from homeassistant.const import ( + ATTR_TEMPERATURE, STATE_OFF, STATE_ON, TEMP_CELSIUS) + +from . import DOMAIN as STE_DOMAIN + +DEPENDENCIES = ['stiebel_eltron'] + +_LOGGER = logging.getLogger(__name__) + + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE +OPERATION_MODES = [STATE_AUTO, STATE_MANUAL, STATE_ECO, STATE_OFF] + +# Mapping STIEBEL ELTRON states to homeassistant states. +STE_TO_HA_STATE = {'AUTOMATIC': STATE_AUTO, + 'MANUAL MODE': STATE_MANUAL, + 'STANDBY': STATE_ECO, + 'DAY MODE': STATE_ON, + 'SETBACK MODE': STATE_ON, + 'DHW': STATE_OFF, + 'EMERGENCY OPERATION': STATE_ON} + +# Mapping homeassistant states to STIEBEL ELTRON states. +HA_TO_STE_STATE = {value: key for key, value in STE_TO_HA_STATE.items()} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the StiebelEltron platform.""" + name = hass.data[STE_DOMAIN]['name'] + ste_data = hass.data[STE_DOMAIN]['ste_data'] + + add_entities([StiebelEltron(name, ste_data)], True) + + +class StiebelEltron(ClimateDevice): + """Representation of a STIEBEL ELTRON heat pump.""" + + def __init__(self, name, ste_data): + """Initialize the unit.""" + self._name = name + self._target_temperature = None + self._current_temperature = None + self._current_humidity = None + self._operation_modes = OPERATION_MODES + self._current_operation = None + self._filter_alarm = None + self._force_update = False + self._ste_data = ste_data + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + def update(self): + """Update unit attributes.""" + self._ste_data.update(no_throttle=self._force_update) + self._force_update = False + + self._target_temperature = self._ste_data.api.get_target_temp() + self._current_temperature = self._ste_data.api.get_current_temp() + self._current_humidity = self._ste_data.api.get_current_humidity() + self._filter_alarm = self._ste_data.api.get_filter_alarm_status() + self._current_operation = self._ste_data.api.get_operation() + + _LOGGER.debug("Update %s, current temp: %s", self._name, + self._current_temperature) + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return { + 'filter_alarm': self._filter_alarm + } + + @property + def name(self): + """Return the name of the climate device.""" + return self._name + + # Handle SUPPORT_TARGET_TEMPERATURE + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 0.1 + + @property + def min_temp(self): + """Return the minimum temperature.""" + return 10.0 + + @property + def max_temp(self): + """Return the maximum temperature.""" + return 30.0 + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temperature = kwargs.get(ATTR_TEMPERATURE) + if target_temperature is not None: + _LOGGER.debug("set_temperature: %s", target_temperature) + self._ste_data.api.set_target_temp(target_temperature) + self._force_update = True + + @property + def current_humidity(self): + """Return the current humidity.""" + return float("{0:.1f}".format(self._current_humidity)) + + # Handle SUPPORT_OPERATION_MODE + @property + def operation_list(self): + """List of the operation modes.""" + return self._operation_modes + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return STE_TO_HA_STATE.get(self._current_operation) + + def set_operation_mode(self, operation_mode): + """Set new operation mode.""" + new_mode = HA_TO_STE_STATE.get(operation_mode) + _LOGGER.debug("set_operation_mode: %s -> %s", self._current_operation, + new_mode) + self._ste_data.api.set_operation(new_mode) + self._force_update = True diff --git a/homeassistant/components/stiebel_eltron/manifest.json b/homeassistant/components/stiebel_eltron/manifest.json new file mode 100644 index 00000000000..0f8b586a9c2 --- /dev/null +++ b/homeassistant/components/stiebel_eltron/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "stiebel_eltron", + "name": "STIEBEL ELTRON", + "documentation": "https://www.home-assistant.io/components/stiebel_eltron", + "requirements": [ + "pystiebeleltron==0.0.1.dev2" + ], + "dependencies": [ + "modbus" + ], + "codeowners": [ + "@fucm" + ] +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index c37cceb1a12..6ab9c8d1d1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1281,6 +1281,9 @@ pysonos==0.0.10 # homeassistant.components.spc pyspcwebgw==0.4.0 +# homeassistant.components.stiebel_eltron +pystiebeleltron==0.0.1.dev2 + # homeassistant.components.stride pystride==0.1.7