From a72d9da9f44942f90403d56ed3b4807e688a9ff8 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Fri, 6 Sep 2019 22:09:03 +0200 Subject: [PATCH] Add Viessmann ViCare Climate platform (#26151) * Add Viessmann ViCare Climate platform * Add water_heater and fix review comments Update to latest PyVicare (0.1.0) * Move PyVicare API creation to component * More review fixes * Return false if api creation fails * Fix logging format * Update PyVicare 0.1.1 to fix json issues * Formatting fixes --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/vicare/__init__.py | 58 +++++ homeassistant/components/vicare/climate.py | 207 ++++++++++++++++++ homeassistant/components/vicare/manifest.json | 9 + .../components/vicare/water_heater.py | 132 +++++++++++ requirements_all.txt | 3 + 7 files changed, 411 insertions(+) create mode 100644 homeassistant/components/vicare/__init__.py create mode 100644 homeassistant/components/vicare/climate.py create mode 100644 homeassistant/components/vicare/manifest.json create mode 100644 homeassistant/components/vicare/water_heater.py diff --git a/.coveragerc b/.coveragerc index e75c4180ef6..e7f155f8923 100644 --- a/.coveragerc +++ b/.coveragerc @@ -692,6 +692,7 @@ omit = homeassistant/components/vesync/const.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py + homeassistant/components/vicare/* homeassistant/components/vizio/media_player.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index 0408bcc8032..5c9673b0838 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -298,6 +298,7 @@ homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe +homeassistant/components/vicare/* @oischinger homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf homeassistant/components/waqi/* @andrey-git diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py new file mode 100644 index 00000000000..9fec04f2328 --- /dev/null +++ b/homeassistant/components/vicare/__init__.py @@ -0,0 +1,58 @@ +"""The ViCare integration.""" +import logging + +import voluptuous as vol +from PyViCare.PyViCareDevice import Device + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME +from homeassistant.helpers import discovery + +_LOGGER = logging.getLogger(__name__) + +VICARE_PLATFORMS = ["climate", "water_heater"] + +DOMAIN = "vicare" +VICARE_API = "api" +VICARE_NAME = "name" + +CONF_CIRCUIT = "circuit" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CIRCUIT): int, + vol.Optional(CONF_NAME, default="ViCare"): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Create the ViCare component.""" + conf = config[DOMAIN] + params = {"token_file": "/tmp/vicare_token.save"} + if conf.get(CONF_CIRCUIT) is not None: + params["circuit"] = conf[CONF_CIRCUIT] + + try: + vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) + except AttributeError: + _LOGGER.error( + "Failed to create PyViCare API client. Please check your credentials." + ) + return False + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][VICARE_API] = vicare_api + hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] + + for platform in VICARE_PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py new file mode 100644 index 00000000000..5727508deb4 --- /dev/null +++ b/homeassistant/components/vicare/climate.py @@ -0,0 +1,207 @@ +"""Viessmann ViCare climate device.""" +import logging + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + PRESET_ECO, + PRESET_COMFORT, + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_AUTO, +) +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE + +from . import DOMAIN as VICARE_DOMAIN +from . import VICARE_API +from . import VICARE_NAME + +_LOGGER = logging.getLogger(__name__) + +VICARE_MODE_DHW = "dhw" +VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_FORCEDREDUCED = "forcedReduced" +VICARE_MODE_FORCEDNORMAL = "forcedNormal" +VICARE_MODE_OFF = "standby" + +VICARE_PROGRAM_ACTIVE = "active" +VICARE_PROGRAM_COMFORT = "comfort" +VICARE_PROGRAM_ECO = "eco" +VICARE_PROGRAM_EXTERNAL = "external" +VICARE_PROGRAM_HOLIDAY = "holiday" +VICARE_PROGRAM_NORMAL = "normal" +VICARE_PROGRAM_REDUCED = "reduced" +VICARE_PROGRAM_STANDBY = "standby" + +VICARE_HOLD_MODE_AWAY = "away" +VICARE_HOLD_MODE_HOME = "home" +VICARE_HOLD_MODE_OFF = "off" + +VICARE_TEMP_HEATING_MIN = 3 +VICARE_TEMP_HEATING_MAX = 37 + +SUPPORT_FLAGS_HEATING = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + +VICARE_TO_HA_HVAC_HEATING = { + VICARE_MODE_DHW: HVAC_MODE_OFF, + VICARE_MODE_DHWANDHEATING: HVAC_MODE_AUTO, + VICARE_MODE_FORCEDREDUCED: HVAC_MODE_OFF, + VICARE_MODE_FORCEDNORMAL: HVAC_MODE_HEAT, + VICARE_MODE_OFF: HVAC_MODE_OFF, +} + +HA_TO_VICARE_HVAC_HEATING = { + HVAC_MODE_HEAT: VICARE_MODE_FORCEDNORMAL, + HVAC_MODE_OFF: VICARE_MODE_FORCEDREDUCED, + HVAC_MODE_AUTO: VICARE_MODE_DHWANDHEATING, +} + +VICARE_TO_HA_PRESET_HEATING = { + VICARE_PROGRAM_COMFORT: PRESET_COMFORT, + VICARE_PROGRAM_ECO: PRESET_ECO, +} + +HA_TO_VICARE_PRESET_HEATING = { + PRESET_COMFORT: VICARE_PROGRAM_COMFORT, + PRESET_ECO: VICARE_PROGRAM_ECO, +} + +PYVICARE_ERROR = "error" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the ViCare climate devices.""" + if discovery_info is None: + return + vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] + add_entities( + [ViCareClimate(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", vicare_api)] + ) + + +class ViCareClimate(ClimateDevice): + """Representation of the ViCare heating climate device.""" + + def __init__(self, name, api): + """Initialize the climate device.""" + self._name = name + self._state = None + self._api = api + self._target_temperature = None + self._current_mode = None + self._current_temperature = None + self._current_program = None + + def update(self): + """Let HA know there has been an update from the ViCare API.""" + _room_temperature = self._api.getRoomTemperature() + if _room_temperature is not None and _room_temperature != "error": + self._current_temperature = _room_temperature + else: + self._current_temperature = self._api.getSupplyTemperature() + self._current_program = self._api.getActiveProgram() + + # The getCurrentDesiredTemperature call can yield 'error' (str) when the system is in standby + desired_temperature = self._api.getCurrentDesiredTemperature() + if desired_temperature == PYVICARE_ERROR: + desired_temperature = None + + self._target_temperature = desired_temperature + + self._current_mode = self._api.getActiveMode() + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS_HEATING + + @property + def name(self): + """Return the name of the climate device.""" + return self._name + + @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 hvac_mode(self): + """Return current hvac mode.""" + return VICARE_TO_HA_HVAC_HEATING.get(self._current_mode) + + def set_hvac_mode(self, hvac_mode): + """Set a new hvac mode on the ViCare API.""" + vicare_mode = HA_TO_VICARE_HVAC_HEATING.get(hvac_mode) + if vicare_mode is None: + _LOGGER.error( + "Cannot set invalid vicare mode: %s / %s", hvac_mode, vicare_mode + ) + return + + _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) + self._api.setMode(vicare_mode) + + @property + def hvac_modes(self): + """Return the list of available hvac modes.""" + return list(HA_TO_VICARE_HVAC_HEATING) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return VICARE_TEMP_HEATING_MIN + + @property + def max_temp(self): + """Return the maximum temperature.""" + return VICARE_TEMP_HEATING_MAX + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + self._api.setProgramTemperature( + self._current_program, self._target_temperature + ) + + @property + def preset_mode(self): + """Return the current preset mode, e.g., home, away, temp.""" + return VICARE_TO_HA_PRESET_HEATING.get(self._current_program) + + @property + def preset_modes(self): + """Return the available preset mode.""" + return list(VICARE_TO_HA_PRESET_HEATING) + + def set_preset_mode(self, preset_mode): + """Set new preset mode and deactivate any existing programs.""" + vicare_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode) + if vicare_program is None: + _LOGGER.error( + "Cannot set invalid vicare program: %s / %s", + preset_mode, + vicare_program, + ) + return + + _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) + self._api.deactivateProgram(self._current_program) + self._api.activateProgram(vicare_program) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json new file mode 100644 index 00000000000..e5f55b20dda --- /dev/null +++ b/homeassistant/components/vicare/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "vicare", + "name": "Viessmann ViCare", + "documentation": "https://www.home-assistant.io/components/vicare", + "dependencies": [], + "codeowners": ["@oischinger"], + "requirements": ["PyViCare==0.1.1"] +} + diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py new file mode 100644 index 00000000000..71c0f6c2aef --- /dev/null +++ b/homeassistant/components/vicare/water_heater.py @@ -0,0 +1,132 @@ +"""Viessmann ViCare water_heater device.""" +import logging + +from homeassistant.components.water_heater import ( + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice, +) +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE + +from . import DOMAIN as VICARE_DOMAIN +from . import VICARE_API +from . import VICARE_NAME + +_LOGGER = logging.getLogger(__name__) + +VICARE_MODE_DHW = "dhw" +VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_FORCEDREDUCED = "forcedReduced" +VICARE_MODE_FORCEDNORMAL = "forcedNormal" +VICARE_MODE_OFF = "standby" + +VICARE_TEMP_WATER_MIN = 10 +VICARE_TEMP_WATER_MAX = 60 + +OPERATION_MODE_ON = "on" +OPERATION_MODE_OFF = "off" + +SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE + +VICARE_TO_HA_HVAC_DHW = { + VICARE_MODE_DHW: OPERATION_MODE_ON, + VICARE_MODE_DHWANDHEATING: OPERATION_MODE_ON, + VICARE_MODE_FORCEDREDUCED: OPERATION_MODE_OFF, + VICARE_MODE_FORCEDNORMAL: OPERATION_MODE_ON, + VICARE_MODE_OFF: OPERATION_MODE_OFF, +} + +HA_TO_VICARE_HVAC_DHW = { + OPERATION_MODE_OFF: VICARE_MODE_OFF, + OPERATION_MODE_ON: VICARE_MODE_DHW, +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the ViCare water_heater devices.""" + if discovery_info is None: + return + vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] + add_entities( + [ViCareWater(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", vicare_api)] + ) + + +class ViCareWater(WaterHeaterDevice): + """Representation of the ViCare domestic hot water device.""" + + def __init__(self, name, api): + """Initialize the DHW water_heater device.""" + self._name = name + self._state = None + self._api = api + self._target_temperature = None + self._current_temperature = None + self._current_mode = None + + def update(self): + """Let HA know there has been an update from the ViCare API.""" + current_temperature = self._api.getDomesticHotWaterStorageTemperature() + if current_temperature is not None and current_temperature != "error": + self._current_temperature = current_temperature + else: + self._current_temperature = None + + self._target_temperature = self._api.getDomesticHotWaterConfiguredTemperature() + + self._current_mode = self._api.getActiveMode() + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS_HEATER + + @property + def name(self): + """Return the name of the water_heater device.""" + return self._name + + @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 + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + self._api.setDomesticHotWaterTemperature(self._target_temperature) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return VICARE_TEMP_WATER_MIN + + @property + def max_temp(self): + """Return the maximum temperature.""" + return VICARE_TEMP_WATER_MAX + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return VICARE_TO_HA_HVAC_DHW.get(self._current_mode) + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return list(HA_TO_VICARE_HVAC_DHW) diff --git a/requirements_all.txt b/requirements_all.txt index 63cd2474125..fc07e144b3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -74,6 +74,9 @@ PyRMVtransport==0.1.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 +# homeassistant.components.vicare +PyViCare==0.1.1 + # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.12.4