From 1757ba8f8b5d8d9fe3b6d75c2f6ad09ae63f6464 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Tue, 7 Feb 2023 15:15:01 +0000 Subject: [PATCH] avoid exceptions raising up to HA core --- custom_components/vicare/__init__.py | 79 +++++++- custom_components/vicare/binary_sensor.py | 72 +++----- custom_components/vicare/button.py | 70 +++---- custom_components/vicare/climate.py | 213 ++++++++++------------ custom_components/vicare/sensor.py | 94 ++++------ custom_components/vicare/water_heater.py | 79 +++----- 6 files changed, 279 insertions(+), 328 deletions(-) diff --git a/custom_components/vicare/__init__.py b/custom_components/vicare/__init__.py index b177a4c..94d6dc0 100644 --- a/custom_components/vicare/__init__.py +++ b/custom_components/vicare/__init__.py @@ -2,16 +2,25 @@ from __future__ import annotations from collections.abc import Callable +from contextlib import contextmanager from dataclasses import dataclass import logging +import typing from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device - +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareRateLimitError, + PyViCareInvalidCredentialsError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.storage import STORAGE_DIR +import requests from .const import ( CONF_HEATING_TYPE, @@ -24,9 +33,44 @@ HeatingType, ) +if typing.TYPE_CHECKING: + from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig + + _LOGGER = logging.getLogger(__name__) +class ViCareError(Exception): + """A typed exception to identify errors raised from ViCare code""" + + +class ViCareEntity: + """Abstract base entity class for ViCare entities""" + + _logger: 'logging.Logger' # using the logger from the inheriting class + _device_config: 'PyViCareDeviceConfig' + + @property + def device_info(self) -> DeviceInfo: + """Return device info for this device.""" + return DeviceInfo( + identifiers={(DOMAIN, self._device_config.getConfig().serial)}, + name=self._device_config.getModel(), + manufacturer="Viessmann", + model=self._device_config.getModel(), + configuration_url="https://developer.viessmann.com/", + ) + + def _internal_update(self): + """virtual abstract: override to actually do the update""" + raise NotImplementedError() + + def update(self): + """Let HA know there has been an update from the ViCare API.""" + with managed_exceptions(self._logger): + self._internal_update() + + @dataclass() class ViCareRequiredKeysMixin: """Mixin for required keys.""" @@ -42,6 +86,27 @@ class ViCareRequiredKeysMixinWithSet: value_setter: Callable[[Device], bool] +@contextmanager +def managed_exceptions(logger: 'logging.Logger'): + try: + yield + except requests.exceptions.ConnectionError: + logger.error("Unable to retrieve data from ViCare server") + except PyViCareRateLimitError as limit_exception: + logger.error("Vicare API rate limit exceeded: %s", limit_exception) + except PyViCareInvalidDataError as invalid_data_exception: + logger.error("Invalid data from Vicare server: %s", invalid_data_exception) + except ValueError: + logger.error("Unable to decode data from ViCare server") + except ViCareError as error: + logger.error("ViCare error: %s", str(error)) + except Exception as error: + if logger.isEnabledFor(logging.DEBUG): + raise error + else: + logger.error("Unexpected %s: %s", type(error).__name__, str(error)) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from config entry.""" _LOGGER.debug("Setting up ViCare component") @@ -49,11 +114,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = {} hass.data[DOMAIN][entry.entry_id] = {} - await hass.async_add_executor_job(setup_vicare_api, hass, entry) - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + try: + await hass.async_add_executor_job(setup_vicare_api, hass, entry) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + except PyViCareInvalidCredentialsError as auth_error: + raise ConfigEntryAuthFailed from auth_error + except Exception as error: + raise ConfigEntryNotReady from error - return True def vicare_login(hass, entry_data): diff --git a/custom_components/vicare/binary_sensor.py b/custom_components/vicare/binary_sensor.py index 3f54e5b..c65bd72 100644 --- a/custom_components/vicare/binary_sensor.py +++ b/custom_components/vicare/binary_sensor.py @@ -5,13 +5,7 @@ from dataclasses import dataclass import logging -from PyViCare.PyViCareUtils import ( - PyViCareInvalidDataError, - PyViCareNotSupportedFeatureError, - PyViCareRateLimitError, -) -import requests - +from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -19,10 +13,9 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ViCareRequiredKeysMixin +from . import ViCareEntity, ViCareRequiredKeysMixin, managed_exceptions from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME _LOGGER = logging.getLogger(__name__) @@ -98,22 +91,22 @@ class ViCareBinarySensorEntityDescription( def _build_entity(name, vicare_api, device_config, sensor): """Create a ViCare binary sensor entity.""" - try: - sensor.value_getter(vicare_api) - _LOGGER.debug("Found entity %s", name) - except PyViCareNotSupportedFeatureError: - _LOGGER.info("Feature not supported %s", name) - return None - except AttributeError: - _LOGGER.debug("Attribute Error %s", name) - return None - - return ViCareBinarySensor( - name, - vicare_api, - device_config, - sensor, - ) + with managed_exceptions(_LOGGER): + try: + sensor.value_getter(vicare_api) + _LOGGER.debug("Found entity %s", name) + return ViCareBinarySensor( + name, + vicare_api, + device_config, + sensor, + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("Feature not supported %s", name) + return None + except AttributeError: + _LOGGER.debug("Attribute Error %s", name) + return None async def _entities_from_descriptions( @@ -182,8 +175,9 @@ async def async_setup_entry( async_add_entities(entities) -class ViCareBinarySensor(BinarySensorEntity): +class ViCareBinarySensor(ViCareEntity, BinarySensorEntity): """Representation of a ViCare sensor.""" + _logger = _LOGGER entity_description: ViCareBinarySensorEntityDescription @@ -198,17 +192,6 @@ def __init__( self._device_config = device_config self._state = None - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self._device_config.getConfig().serial)}, - name=self._device_config.getModel(), - manufacturer="Viessmann", - model=self._device_config.getModel(), - configuration_url="https://developer.viessmann.com/", - ) - @property def available(self): """Return True if entity is available.""" @@ -229,16 +212,7 @@ def is_on(self): """Return the state of the sensor.""" return self._state - def update(self): + def _internal_update(self): """Update state of sensor.""" - try: - with suppress(PyViCareNotSupportedFeatureError): - self._state = self.entity_description.value_getter(self._api) - except requests.exceptions.ConnectionError: - _LOGGER.error("Unable to retrieve data from ViCare server") - except ValueError: - _LOGGER.error("Unable to decode data from ViCare server") - except PyViCareRateLimitError as limit_exception: - _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) - except PyViCareInvalidDataError as invalid_data_exception: - _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) + with suppress(PyViCareNotSupportedFeatureError): + self._state = self.entity_description.value_getter(self._api) diff --git a/custom_components/vicare/button.py b/custom_components/vicare/button.py index 95be680..37d43f7 100644 --- a/custom_components/vicare/button.py +++ b/custom_components/vicare/button.py @@ -5,20 +5,14 @@ from dataclasses import dataclass import logging -from PyViCare.PyViCareUtils import ( - PyViCareInvalidDataError, - PyViCareNotSupportedFeatureError, - PyViCareRateLimitError, -) -import requests - +from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ViCareRequiredKeysMixinWithSet +from . import ViCareEntity, ViCareRequiredKeysMixinWithSet, managed_exceptions from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME _LOGGER = logging.getLogger(__name__) @@ -47,23 +41,23 @@ class ViCareButtonEntityDescription( def _build_entity(name, vicare_api, device_config, description): """Create a ViCare button entity.""" - _LOGGER.debug("Found device %s", name) - try: - description.value_getter(vicare_api) - _LOGGER.debug("Found entity %s", name) - except PyViCareNotSupportedFeatureError: - _LOGGER.info("Feature not supported %s", name) - return None - except AttributeError: - _LOGGER.debug("Attribute Error %s", name) - return None - - return ViCareButton( - name, - vicare_api, - device_config, - description, - ) + with managed_exceptions(_LOGGER): + _LOGGER.debug("Found device %s", name) + try: + description.value_getter(vicare_api) + _LOGGER.debug("Found entity %s", name) + return ViCareButton( + name, + vicare_api, + device_config, + description, + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("Feature not supported %s", name) + return None + except AttributeError: + _LOGGER.debug("Attribute Error %s", name) + return None async def async_setup_entry( @@ -91,8 +85,9 @@ async def async_setup_entry( async_add_entities(entities) -class ViCareButton(ButtonEntity): +class ViCareButton(ViCareEntity, ButtonEntity): """Representation of a ViCare button.""" + _logger = _LOGGER entity_description: ViCareButtonEntityDescription @@ -106,28 +101,9 @@ def __init__( def press(self) -> None: """Handle the button press.""" - try: + with managed_exceptions(_LOGGER): with suppress(PyViCareNotSupportedFeatureError): self.entity_description.value_setter(self._api) - except requests.exceptions.ConnectionError: - _LOGGER.error("Unable to retrieve data from ViCare server") - except ValueError: - _LOGGER.error("Unable to decode data from ViCare server") - except PyViCareRateLimitError as limit_exception: - _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) - except PyViCareInvalidDataError as invalid_data_exception: - _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) - - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self._device_config.getConfig().serial)}, - name=self._device_config.getModel(), - manufacturer="Viessmann", - model=self._device_config.getModel(), - configuration_url="https://developer.viessmann.com/", - ) @property def unique_id(self) -> str: diff --git a/custom_components/vicare/climate.py b/custom_components/vicare/climate.py index 678bf13..631083b 100644 --- a/custom_components/vicare/climate.py +++ b/custom_components/vicare/climate.py @@ -7,13 +7,8 @@ from PyViCare.PyViCareUtils import ( PyViCareCommandError, - PyViCareInvalidDataError, PyViCareNotSupportedFeatureError, - PyViCareRateLimitError, ) -import requests -import voluptuous as vol - from homeassistant.components.climate import ( PRESET_COMFORT, PRESET_ECO, @@ -33,9 +28,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +import voluptuous as vol +from . import ViCareEntity, ViCareError, managed_exceptions from .const import ( CONF_HEATING_TYPE, DOMAIN, @@ -154,8 +150,9 @@ async def async_setup_entry( async_add_entities(entities) -class ViCareClimate(ClimateEntity): +class ViCareClimate(ViCareEntity, ClimateEntity): """Representation of the ViCare heating climate device.""" + _logger = _LOGGER _attr_precision = PRECISION_TENTHS _attr_supported_features = ( @@ -183,83 +180,62 @@ def unique_id(self) -> str: """Return unique ID for this device.""" return f"{self._device_config.getConfig().serial}-{self._circuit.id}" - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self._device_config.getConfig().serial)}, - name=self._device_config.getModel(), - manufacturer="Viessmann", - model=self._device_config.getModel(), - configuration_url="https://developer.viessmann.com/", - ) - - def update(self) -> None: + def _internal_update(self): """Let HA know there has been an update from the ViCare API.""" - try: - _room_temperature = None - with suppress(PyViCareNotSupportedFeatureError): - _room_temperature = self._circuit.getRoomTemperature() - - _supply_temperature = None - with suppress(PyViCareNotSupportedFeatureError): - _supply_temperature = self._circuit.getSupplyTemperature() - - if _room_temperature is not None: - self._current_temperature = _room_temperature - elif _supply_temperature is not None: - self._current_temperature = _supply_temperature - else: - self._current_temperature = None - - with suppress(PyViCareNotSupportedFeatureError): - self._current_program = self._circuit.getActiveProgram() - - with suppress(PyViCareNotSupportedFeatureError): - self._target_temperature = self._circuit.getCurrentDesiredTemperature() - - with suppress(PyViCareNotSupportedFeatureError): - self._current_mode = self._circuit.getActiveMode() - - # Update the generic device attributes - self._attributes = {} - - self._attributes["room_temperature"] = _room_temperature - self._attributes["active_vicare_program"] = self._current_program - self._attributes["active_vicare_mode"] = self._current_mode - - with suppress(PyViCareNotSupportedFeatureError): - self._attributes[ - "heating_curve_slope" - ] = self._circuit.getHeatingCurveSlope() - - with suppress(PyViCareNotSupportedFeatureError): - self._attributes[ - "heating_curve_shift" - ] = self._circuit.getHeatingCurveShift() - - self._attributes["vicare_modes"] = self._circuit.getModes() - - self._current_action = False - # Update the specific device attributes - with suppress(PyViCareNotSupportedFeatureError): - for burner in self._api.burners: - self._current_action = self._current_action or burner.getActive() - - with suppress(PyViCareNotSupportedFeatureError): - for compressor in self._api.compressors: - self._current_action = ( - self._current_action or compressor.getActive() - ) - - except requests.exceptions.ConnectionError: - _LOGGER.error("Unable to retrieve data from ViCare server") - except PyViCareRateLimitError as limit_exception: - _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) - except ValueError: - _LOGGER.error("Unable to decode data from ViCare server") - except PyViCareInvalidDataError as invalid_data_exception: - _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) + _room_temperature = None + with suppress(PyViCareNotSupportedFeatureError): + _room_temperature = self._circuit.getRoomTemperature() + + _supply_temperature = None + with suppress(PyViCareNotSupportedFeatureError): + _supply_temperature = self._circuit.getSupplyTemperature() + + if _room_temperature is not None: + self._current_temperature = _room_temperature + elif _supply_temperature is not None: + self._current_temperature = _supply_temperature + else: + self._current_temperature = None + + with suppress(PyViCareNotSupportedFeatureError): + self._current_program = self._circuit.getActiveProgram() + + with suppress(PyViCareNotSupportedFeatureError): + self._target_temperature = self._circuit.getCurrentDesiredTemperature() + + with suppress(PyViCareNotSupportedFeatureError): + self._current_mode = self._circuit.getActiveMode() + + # Update the generic device attributes + self._attributes = {} + + self._attributes["room_temperature"] = _room_temperature + self._attributes["active_vicare_program"] = self._current_program + self._attributes["active_vicare_mode"] = self._current_mode + + with suppress(PyViCareNotSupportedFeatureError): + self._attributes[ + "heating_curve_slope" + ] = self._circuit.getHeatingCurveSlope() + + with suppress(PyViCareNotSupportedFeatureError): + self._attributes[ + "heating_curve_shift" + ] = self._circuit.getHeatingCurveShift() + + self._attributes["vicare_modes"] = self._circuit.getModes() + + self._current_action = False + # Update the specific device attributes + with suppress(PyViCareNotSupportedFeatureError): + for burner in self._api.burners: + self._current_action = self._current_action or burner.getActive() + + with suppress(PyViCareNotSupportedFeatureError): + for compressor in self._api.compressors: + self._current_action = ( + self._current_action or compressor.getActive() + ) @property def name(self): @@ -283,15 +259,16 @@ def hvac_mode(self) -> HVACMode | None: def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set a new hvac mode on the ViCare API.""" - if "vicare_modes" not in self._attributes: - raise ValueError("Cannot set hvac mode when vicare_modes are not known") + with managed_exceptions(_LOGGER): + if "vicare_modes" not in self._attributes: + raise ViCareError("Cannot set hvac mode when vicare_modes are not known") - vicare_mode = self.vicare_mode_from_hvac_mode(hvac_mode) - if vicare_mode is None: - raise ValueError(f"Cannot set invalid hvac mode: {hvac_mode}") + vicare_mode = self.vicare_mode_from_hvac_mode(hvac_mode) + if vicare_mode is None: + raise ViCareError(f"Cannot set invalid hvac mode: {hvac_mode}") - _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) - self._circuit.setMode(vicare_mode) + _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) + self._circuit.setMode(vicare_mode) def vicare_mode_from_hvac_mode(self, hvac_mode): """Return the corresponding vicare mode for an hvac_mode.""" @@ -344,8 +321,9 @@ def target_temperature_step(self) -> float: def set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: - self._circuit.setProgramTemperature(self._current_program, temp) - self._target_temperature = temp + with managed_exceptions(_LOGGER): + self._circuit.setProgramTemperature(self._current_program, temp) + self._target_temperature = temp @property def preset_mode(self): @@ -359,22 +337,23 @@ def preset_modes(self): def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode and deactivate any existing programs.""" - vicare_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode) - if vicare_program is None: - raise ValueError( - f"Cannot set invalid vicare program: {preset_mode}/{vicare_program}" - ) - - _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) - if self._current_program != VICARE_PROGRAM_NORMAL: - # We can't deactivate "normal" - try: - self._circuit.deactivateProgram(self._current_program) - except PyViCareCommandError: - _LOGGER.debug("Unable to deactivate program %s", self._current_program) - if vicare_program != VICARE_PROGRAM_NORMAL: - # And we can't explicitly activate normal, either - self._circuit.activateProgram(vicare_program) + with managed_exceptions(_LOGGER): + vicare_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode) + if vicare_program is None: + raise ViCareError( + f"Cannot set invalid vicare program: {preset_mode}/{vicare_program}" + ) + + _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) + if self._current_program != VICARE_PROGRAM_NORMAL: + # We can't deactivate "normal" + try: + self._circuit.deactivateProgram(self._current_program) + except PyViCareCommandError: + _LOGGER.debug("Unable to deactivate program %s", self._current_program) + if vicare_program != VICARE_PROGRAM_NORMAL: + # And we can't explicitly activate normal, either + self._circuit.activateProgram(vicare_program) @property def extra_state_attributes(self): @@ -383,15 +362,17 @@ def extra_state_attributes(self): def set_vicare_mode(self, vicare_mode): """Service function to set vicare modes directly.""" - if vicare_mode not in self._attributes["vicare_modes"]: - raise ValueError(f"Cannot set invalid vicare mode: {vicare_mode}.") + with managed_exceptions(_LOGGER): + if vicare_mode not in self._attributes["vicare_modes"]: + raise ViCareError(f"Cannot set invalid vicare mode: {vicare_mode}.") - self._circuit.setMode(vicare_mode) + self._circuit.setMode(vicare_mode) def set_heating_curve(self, shift, slope): """Service function to set vicare modes directly.""" - if not 0.2 <= round(float(slope),1) <= 3.5: - raise ValueError(f"Cannot set invalid heating curve slope: {slope}.") - if not -13 <= int(shift) <= 40: - raise ValueError(f"Cannot set invalid heating curve shift: {shift}.") - self._circuit.setHeatingCurve(int(shift), round(float(slope),1)) \ No newline at end of file + with managed_exceptions(_LOGGER): + if not 0.2 <= round(float(slope),1) <= 3.5: + raise ViCareError(f"Cannot set invalid heating curve slope: {slope}.") + if not -13 <= int(shift) <= 40: + raise ViCareError(f"Cannot set invalid heating curve shift: {shift}.") + self._circuit.setHeatingCurve(int(shift), round(float(slope),1)) \ No newline at end of file diff --git a/custom_components/vicare/sensor.py b/custom_components/vicare/sensor.py index 4c61ed0..5e34245 100644 --- a/custom_components/vicare/sensor.py +++ b/custom_components/vicare/sensor.py @@ -7,13 +7,7 @@ import logging from PyViCare.PyViCareDevice import Device -from PyViCare.PyViCareUtils import ( - PyViCareInvalidDataError, - PyViCareNotSupportedFeatureError, - PyViCareRateLimitError, -) -import requests - +from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -30,10 +24,9 @@ VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ViCareRequiredKeysMixin +from . import ViCareEntity, ViCareRequiredKeysMixin, managed_exceptions from .const import ( DOMAIN, VICARE_API, @@ -571,23 +564,23 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM def _build_entity(name, vicare_api, device_config, sensor): """Create a ViCare sensor entity.""" - _LOGGER.debug("Found device %s", name) - try: - sensor.value_getter(vicare_api) - _LOGGER.debug("Found entity %s", name) - except PyViCareNotSupportedFeatureError: - _LOGGER.info("Feature not supported %s", name) - return None - except AttributeError: - _LOGGER.debug("Attribute Error %s", name) - return None - - return ViCareSensor( - name, - vicare_api, - device_config, - sensor, - ) + with managed_exceptions(_LOGGER): + _LOGGER.debug("Found device %s", name) + try: + sensor.value_getter(vicare_api) + _LOGGER.debug("Found entity %s", name) + return ViCareSensor( + name, + vicare_api, + device_config, + sensor, + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("Feature not supported %s", name) + return None + except AttributeError: + _LOGGER.debug("Attribute Error %s", name) + return None async def _entities_from_descriptions( @@ -655,8 +648,9 @@ async def async_setup_entry( async_add_entities(entities) -class ViCareSensor(SensorEntity): +class ViCareSensor(ViCareEntity, SensorEntity): """Representation of a ViCare sensor.""" + _logger = _LOGGER entity_description: ViCareSensorEntityDescription @@ -670,17 +664,6 @@ def __init__( self._device_config = device_config self._state = None - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self._device_config.getConfig().serial)}, - name=self._device_config.getModel(), - manufacturer="Viessmann", - model=self._device_config.getModel(), - configuration_url="https://developer.viessmann.com/", - ) - @property def available(self): """Return True if entity is available.""" @@ -701,26 +684,17 @@ def native_value(self): """Return the state of the sensor.""" return self._state - def update(self): + def _internal_update(self): """Update state of sensor.""" - try: - with suppress(PyViCareNotSupportedFeatureError): - self._state = self.entity_description.value_getter(self._api) - - if self.entity_description.unit_getter: - vicare_unit = self.entity_description.unit_getter(self._api) - if vicare_unit is not None: - self._attr_device_class = VICARE_UNIT_TO_DEVICE_CLASS.get( - vicare_unit - ) - self._attr_native_unit_of_measurement = ( - VICARE_UNIT_TO_UNIT_OF_MEASUREMENT.get(vicare_unit) - ) - except requests.exceptions.ConnectionError: - _LOGGER.error("Unable to retrieve data from ViCare server") - except ValueError: - _LOGGER.error("Unable to decode data from ViCare server") - except PyViCareRateLimitError as limit_exception: - _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) - except PyViCareInvalidDataError as invalid_data_exception: - _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) + with suppress(PyViCareNotSupportedFeatureError): + self._state = self.entity_description.value_getter(self._api) + + if self.entity_description.unit_getter: + vicare_unit = self.entity_description.unit_getter(self._api) + if vicare_unit is not None: + self._attr_device_class = VICARE_UNIT_TO_DEVICE_CLASS.get( + vicare_unit + ) + self._attr_native_unit_of_measurement = ( + VICARE_UNIT_TO_UNIT_OF_MEASUREMENT.get(vicare_unit) + ) diff --git a/custom_components/vicare/water_heater.py b/custom_components/vicare/water_heater.py index 1ad5c0d..b42df41 100644 --- a/custom_components/vicare/water_heater.py +++ b/custom_components/vicare/water_heater.py @@ -3,13 +3,7 @@ import logging from typing import Any -from PyViCare.PyViCareUtils import ( - PyViCareInvalidDataError, - PyViCareNotSupportedFeatureError, - PyViCareRateLimitError, -) -import requests - +from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError from homeassistant.components.water_heater import ( WaterHeaterEntity, WaterHeaterEntityFeature, @@ -22,9 +16,9 @@ TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import ViCareEntity, managed_exceptions from .const import ( CONF_HEATING_TYPE, DOMAIN, @@ -67,11 +61,13 @@ def _get_circuits(vicare_api): """Return the list of circuits.""" - try: - return vicare_api.circuits - except PyViCareNotSupportedFeatureError: - _LOGGER.info("No circuits found") - return [] + with managed_exceptions(_LOGGER): + try: + return vicare_api.circuits + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No circuits found") + + return [] async def async_setup_entry( @@ -102,8 +98,9 @@ async def async_setup_entry( async_add_entities(entities) -class ViCareWater(WaterHeaterEntity): +class ViCareWater(ViCareEntity, WaterHeaterEntity): """Representation of the ViCare domestic hot water device.""" + _logger = _LOGGER _attr_precision = PRECISION_TENTHS _attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE @@ -121,47 +118,26 @@ def __init__(self, name, api, circuit, device_config, heating_type): self._current_mode = None self._heating_type = heating_type - def update(self) -> None: + def _internal_update(self): """Let HA know there has been an update from the ViCare API.""" - try: - with suppress(PyViCareNotSupportedFeatureError): - self._current_temperature = ( - self._api.getDomesticHotWaterStorageTemperature() - ) - - with suppress(PyViCareNotSupportedFeatureError): - self._target_temperature = ( - self._api.getDomesticHotWaterDesiredTemperature() - ) - - with suppress(PyViCareNotSupportedFeatureError): - self._current_mode = self._circuit.getActiveMode() - - except requests.exceptions.ConnectionError: - _LOGGER.error("Unable to retrieve data from ViCare server") - except PyViCareRateLimitError as limit_exception: - _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) - except ValueError: - _LOGGER.error("Unable to decode data from ViCare server") - except PyViCareInvalidDataError as invalid_data_exception: - _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) + with suppress(PyViCareNotSupportedFeatureError): + self._current_temperature = ( + self._api.getDomesticHotWaterStorageTemperature() + ) + + with suppress(PyViCareNotSupportedFeatureError): + self._target_temperature = ( + self._api.getDomesticHotWaterDesiredTemperature() + ) + + with suppress(PyViCareNotSupportedFeatureError): + self._current_mode = self._circuit.getActiveMode() @property def unique_id(self) -> str: """Return unique ID for this device.""" return f"{self._device_config.getConfig().serial}-{self._circuit.id}" - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self._device_config.getConfig().serial)}, - name=self._device_config.getModel(), - manufacturer="Viessmann", - model=self._device_config.getModel(), - configuration_url="https://developer.viessmann.com/", - ) - @property def name(self): """Return the name of the water_heater device.""" @@ -185,8 +161,9 @@ def target_temperature(self): def set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: - self._api.setDomesticHotWaterTemperature(temp) - self._target_temperature = temp + with managed_exceptions(_LOGGER): + self._api.setDomesticHotWaterTemperature(temp) + self._target_temperature = temp @property def min_temp(self): @@ -206,7 +183,7 @@ def target_temperature_step(self) -> float: @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return VICARE_TO_HA_HVAC_DHW.get(self._current_mode) + return VICARE_TO_HA_HVAC_DHW.get(self._current_mode) # type: ignore @property def operation_list(self):