diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 5d5c5548be1..c1571c2f91b 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -1,16 +1,12 @@ """The ViCare integration.""" from __future__ import annotations -from collections.abc import Callable from dataclasses import dataclass -import enum import logging -from typing import Generic, TypeVar +from typing import Callable +from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device -from PyViCare.PyViCareFuelCell import FuelCell -from PyViCare.PyViCareGazBoiler import GazBoiler -from PyViCare.PyViCareHeatPump import HeatPump import voluptuous as vol from homeassistant.const import ( @@ -24,55 +20,51 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR +from .const import ( + CONF_CIRCUIT, + CONF_HEATING_TYPE, + DEFAULT_HEATING_TYPE, + DOMAIN, + HEATING_TYPE_TO_CREATOR_METHOD, + PLATFORMS, + VICARE_API, + VICARE_CIRCUITS, + VICARE_DEVICE_CONFIG, + VICARE_NAME, + HeatingType, +) + _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"] - -DOMAIN = "vicare" -VICARE_API = "api" -VICARE_NAME = "name" -VICARE_HEATING_TYPE = "heating_type" - -CONF_CIRCUIT = "circuit" -CONF_HEATING_TYPE = "heating_type" -DEFAULT_HEATING_TYPE = "generic" - - -ApiT = TypeVar("ApiT", bound=Device) - @dataclass() -class ViCareRequiredKeysMixin(Generic[ApiT]): +class ViCareRequiredKeysMixin: """Mixin for required keys.""" - value_getter: Callable[[ApiT], bool] - - -class HeatingType(enum.Enum): - """Possible options for heating type.""" - - generic = "generic" - gas = "gas" - heatpump = "heatpump" - fuelcell = "fuelcell" + value_getter: Callable[[Device], bool] CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=60): vol.All( - cv.time_period, lambda value: value.total_seconds() - ), - vol.Optional(CONF_CIRCUIT): int, - vol.Optional(CONF_NAME, default="ViCare"): cv.string, - vol.Optional(CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE): cv.enum( - HeatingType - ), - } + DOMAIN: vol.All( + cv.deprecated(CONF_CIRCUIT), + vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=60): vol.All( + cv.time_period, lambda value: value.total_seconds() + ), + vol.Optional( + CONF_CIRCUIT + ): int, # Ignored: All circuits are now supported. Will be removed when switching to Setup via UI. + vol.Optional(CONF_NAME, default="ViCare"): cv.string, + vol.Optional( + CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE + ): cv.enum(HeatingType), + } + ), ) }, extra=vol.ALLOW_EXTRA, @@ -83,34 +75,40 @@ def setup(hass, config): """Create the ViCare component.""" conf = config[DOMAIN] params = {"token_file": hass.config.path(STORAGE_DIR, "vicare_token.save")} - if conf.get(CONF_CIRCUIT) is not None: - params["circuit"] = conf[CONF_CIRCUIT] params["cacheDuration"] = conf.get(CONF_SCAN_INTERVAL) params["client_id"] = conf.get(CONF_CLIENT_ID) - heating_type = conf[CONF_HEATING_TYPE] - - try: - if heating_type == HeatingType.gas: - vicare_api = GazBoiler(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) - elif heating_type == HeatingType.heatpump: - vicare_api = HeatPump(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) - elif heating_type == HeatingType.fuelcell: - vicare_api = FuelCell(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) - else: - 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] - hass.data[DOMAIN][VICARE_HEATING_TYPE] = heating_type + setup_vicare_api(hass, conf, hass.data[DOMAIN]) + + hass.data[DOMAIN][CONF_HEATING_TYPE] = conf[CONF_HEATING_TYPE] for platform in PLATFORMS: discovery.load_platform(hass, platform, DOMAIN, {}, config) return True + + +def setup_vicare_api(hass, conf, entity_data): + """Set up PyVicare API.""" + vicare_api = PyViCare() + vicare_api.setCacheDuration(conf[CONF_SCAN_INTERVAL]) + vicare_api.initWithCredentials( + conf[CONF_USERNAME], + conf[CONF_PASSWORD], + conf[CONF_CLIENT_ID], + hass.config.path(STORAGE_DIR, "vicare_token.save"), + ) + + device = vicare_api.devices[0] + for device in vicare_api.devices: + _LOGGER.info( + "Found device: %s (online: %s)", device.getModel(), str(device.isOnline()) + ) + entity_data[VICARE_DEVICE_CONFIG] = device + entity_data[VICARE_API] = getattr( + device, HEATING_TYPE_TO_CREATOR_METHOD[conf[CONF_HEATING_TYPE]] + )() + entity_data[VICARE_CIRCUITS] = entity_data[VICARE_API].circuits diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 88d6e3ac06a..d025d2b1ba6 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -4,12 +4,12 @@ from __future__ import annotations from contextlib import suppress from dataclasses import dataclass import logging -from typing import Union -from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError -from PyViCare.PyViCareDevice import Device -from PyViCare.PyViCareGazBoiler import GazBoiler -from PyViCare.PyViCareHeatPump import HeatPump +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareNotSupportedFeatureError, + PyViCareRateLimitError, +) import requests from homeassistant.components.binary_sensor import ( @@ -18,36 +18,25 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) -from . import ( - DOMAIN as VICARE_DOMAIN, - VICARE_API, - VICARE_HEATING_TYPE, - VICARE_NAME, - ApiT, - HeatingType, - ViCareRequiredKeysMixin, -) +from . import ViCareRequiredKeysMixin +from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME _LOGGER = logging.getLogger(__name__) SENSOR_CIRCULATION_PUMP_ACTIVE = "circulationpump_active" - -# gas sensors SENSOR_BURNER_ACTIVE = "burner_active" - -# heatpump sensors SENSOR_COMPRESSOR_ACTIVE = "compressor_active" @dataclass class ViCareBinarySensorEntityDescription( - BinarySensorEntityDescription, ViCareRequiredKeysMixin[ApiT] + BinarySensorEntityDescription, ViCareRequiredKeysMixin ): """Describes ViCare binary sensor entity.""" -SENSOR_TYPES_GENERIC: tuple[ViCareBinarySensorEntityDescription[Device]] = ( - ViCareBinarySensorEntityDescription[Device]( +CIRCUIT_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( + ViCareBinarySensorEntityDescription( key=SENSOR_CIRCULATION_PUMP_ACTIVE, name="Circulation pump active", device_class=DEVICE_CLASS_POWER, @@ -55,80 +44,133 @@ SENSOR_TYPES_GENERIC: tuple[ViCareBinarySensorEntityDescription[Device]] = ( ), ) -SENSOR_TYPES_GAS: tuple[ViCareBinarySensorEntityDescription[GazBoiler]] = ( - ViCareBinarySensorEntityDescription[GazBoiler]( +BURNER_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( + ViCareBinarySensorEntityDescription( key=SENSOR_BURNER_ACTIVE, name="Burner active", device_class=DEVICE_CLASS_POWER, - value_getter=lambda api: api.getBurnerActive(), + value_getter=lambda api: api.getActive(), ), ) -SENSOR_TYPES_HEATPUMP: tuple[ViCareBinarySensorEntityDescription[HeatPump]] = ( - ViCareBinarySensorEntityDescription[HeatPump]( +COMPRESSOR_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( + ViCareBinarySensorEntityDescription( key=SENSOR_COMPRESSOR_ACTIVE, name="Compressor active", device_class=DEVICE_CLASS_POWER, - value_getter=lambda api: api.getCompressorActive(), + value_getter=lambda api: api.getActive(), ), ) -SENSORS_GENERIC = [SENSOR_CIRCULATION_PUMP_ACTIVE] -SENSORS_BY_HEATINGTYPE = { - HeatingType.gas: [SENSOR_BURNER_ACTIVE], - HeatingType.heatpump: [SENSOR_COMPRESSOR_ACTIVE], - HeatingType.fuelcell: [SENSOR_BURNER_ACTIVE], -} +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 - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Create the ViCare sensor devices.""" - if discovery_info is None: - return - - vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] - heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] - - sensors = SENSORS_GENERIC.copy() - - if heating_type != HeatingType.generic: - sensors.extend(SENSORS_BY_HEATINGTYPE[heating_type]) - - add_entities( - [ - ViCareBinarySensor( - hass.data[VICARE_DOMAIN][VICARE_NAME], vicare_api, description - ) - for description in ( - *SENSOR_TYPES_GENERIC, - *SENSOR_TYPES_GAS, - *SENSOR_TYPES_HEATPUMP, - ) - if description.key in sensors - ] + return ViCareBinarySensor( + name, + vicare_api, + device_config, + sensor, ) -DescriptionT = Union[ - ViCareBinarySensorEntityDescription[Device], - ViCareBinarySensorEntityDescription[GazBoiler], - ViCareBinarySensorEntityDescription[HeatPump], -] +async def _entities_from_descriptions( + hass, name, all_devices, sensor_descriptions, iterables +): + """Create entities from descriptions and list of burners/circuits.""" + for description in sensor_descriptions: + for current in iterables: + suffix = "" + if len(iterables) > 1: + suffix = f" {current.id}" + entity = await hass.async_add_executor_job( + _build_entity, + f"{name} {description.name}{suffix}", + current, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + description, + ) + if entity is not None: + all_devices.append(entity) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Create the ViCare binary sensor devices.""" + if discovery_info is None: + return + + name = hass.data[DOMAIN][VICARE_NAME] + api = hass.data[DOMAIN][VICARE_API] + + all_devices = [] + + for description in CIRCUIT_SENSORS: + for circuit in api.circuits: + suffix = "" + if len(api.circuits) > 1: + suffix = f" {circuit.id}" + entity = await hass.async_add_executor_job( + _build_entity, + f"{name} {description.name}{suffix}", + circuit, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + description, + ) + if entity is not None: + all_devices.append(entity) + + try: + _entities_from_descriptions( + hass, name, all_devices, BURNER_SENSORS, api.burners + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No burners found") + + try: + _entities_from_descriptions( + hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No compressors found") + + async_add_entities(all_devices) class ViCareBinarySensor(BinarySensorEntity): """Representation of a ViCare sensor.""" - entity_description: DescriptionT + entity_description: ViCareBinarySensorEntityDescription - def __init__(self, name, api, description: DescriptionT): + def __init__( + self, name, api, device_config, description: ViCareBinarySensorEntityDescription + ): """Initialize the sensor.""" self.entity_description = description - self._attr_name = f"{name} {description.name}" + self._attr_name = name self._api = api + self.entity_description = description + self._device_config = device_config self._state = None + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self._device_config.getConfig().serial)}, + "name": self._device_config.getModel(), + "manufacturer": "Viessmann", + "model": (DOMAIN, self._device_config.getModel()), + } + @property def available(self): """Return True if entity is available.""" @@ -136,8 +178,13 @@ class ViCareBinarySensor(BinarySensorEntity): @property def unique_id(self): - """Return a unique ID.""" - return f"{self._api.service.id}-{self.entity_description.key}" + """Return unique ID for this device.""" + tmp_id = ( + f"{self._device_config.getConfig().serial}-{self.entity_description.key}" + ) + if hasattr(self._api, "id"): + return f"{tmp_id}-{self._api.id}" + return tmp_id @property def is_on(self): @@ -155,3 +202,5 @@ class ViCareBinarySensor(BinarySensorEntity): _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) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index a6aa757e124..ba32e65ee52 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -2,7 +2,11 @@ from contextlib import suppress import logging -from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareNotSupportedFeatureError, + PyViCareRateLimitError, +) import requests import voluptuous as vol @@ -21,12 +25,13 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.helpers import entity_platform -from . import ( - DOMAIN as VICARE_DOMAIN, +from .const import ( + CONF_HEATING_TYPE, + DOMAIN, VICARE_API, - VICARE_HEATING_TYPE, + VICARE_CIRCUITS, + VICARE_DEVICE_CONFIG, VICARE_NAME, - HeatingType, ) _LOGGER = logging.getLogger(__name__) @@ -87,23 +92,38 @@ HA_TO_VICARE_PRESET_HEATING = { } +def _build_entity(name, vicare_api, circuit, device_config, heating_type): + """Create a ViCare climate entity.""" + _LOGGER.debug("Found device %s", name) + return ViCareClimate(name, vicare_api, device_config, circuit, heating_type) + + async def async_setup_platform( hass, hass_config, async_add_entities, discovery_info=None ): """Create the ViCare climate devices.""" + # Legacy setup. Remove after configuration.yaml deprecation end if discovery_info is None: return - vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] - heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] - async_add_entities( - [ - ViCareClimate( - f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", - vicare_api, - heating_type, - ) - ] - ) + + name = hass.data[DOMAIN][VICARE_NAME] + all_devices = [] + + for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]: + suffix = "" + if len(hass.data[DOMAIN][VICARE_CIRCUITS]) > 1: + suffix = f" {circuit.id}" + entity = _build_entity( + f"{name} Heating{suffix}", + hass.data[DOMAIN][VICARE_API], + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + circuit, + hass.data[DOMAIN][CONF_HEATING_TYPE], + ) + if entity is not None: + all_devices.append(entity) + + async_add_entities(all_devices) platform = entity_platform.async_get_current_platform() @@ -121,11 +141,13 @@ async def async_setup_platform( class ViCareClimate(ClimateEntity): """Representation of the ViCare heating climate device.""" - def __init__(self, name, api, heating_type): + def __init__(self, name, api, circuit, device_config, heating_type): """Initialize the climate device.""" self._name = name self._state = None self._api = api + self._circuit = circuit + self._device_config = device_config self._attributes = {} self._target_temperature = None self._current_mode = None @@ -134,16 +156,31 @@ class ViCareClimate(ClimateEntity): self._heating_type = heating_type self._current_action = None + @property + def unique_id(self): + """Return unique ID for this device.""" + return f"{self._device_config.getConfig().serial}-climate-{self._circuit.id}" + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self._device_config.getConfig().serial)}, + "name": self._device_config.getModel(), + "manufacturer": "Viessmann", + "model": (DOMAIN, self._device_config.getModel()), + } + def update(self): """Let HA know there has been an update from the ViCare API.""" try: _room_temperature = None with suppress(PyViCareNotSupportedFeatureError): - _room_temperature = self._api.getRoomTemperature() + _room_temperature = self._circuit.getRoomTemperature() _supply_temperature = None with suppress(PyViCareNotSupportedFeatureError): - _supply_temperature = self._api.getSupplyTemperature() + _supply_temperature = self._circuit.getSupplyTemperature() if _room_temperature is not None: self._current_temperature = _room_temperature @@ -153,13 +190,13 @@ class ViCareClimate(ClimateEntity): self._current_temperature = None with suppress(PyViCareNotSupportedFeatureError): - self._current_program = self._api.getActiveProgram() + self._current_program = self._circuit.getActiveProgram() with suppress(PyViCareNotSupportedFeatureError): - self._target_temperature = self._api.getCurrentDesiredTemperature() + self._target_temperature = self._circuit.getCurrentDesiredTemperature() with suppress(PyViCareNotSupportedFeatureError): - self._current_mode = self._api.getActiveMode() + self._current_mode = self._circuit.getActiveMode() # Update the generic device attributes self._attributes = {} @@ -171,26 +208,33 @@ class ViCareClimate(ClimateEntity): with suppress(PyViCareNotSupportedFeatureError): self._attributes[ "heating_curve_slope" - ] = self._api.getHeatingCurveSlope() + ] = self._circuit.getHeatingCurveSlope() with suppress(PyViCareNotSupportedFeatureError): self._attributes[ "heating_curve_shift" - ] = self._api.getHeatingCurveShift() + ] = self._circuit.getHeatingCurveShift() + self._current_action = False # Update the specific device attributes - if self._heating_type == HeatingType.gas: - with suppress(PyViCareNotSupportedFeatureError): - self._current_action = self._api.getBurnerActive() - elif self._heating_type == HeatingType.heatpump: - with suppress(PyViCareNotSupportedFeatureError): - self._current_action = self._api.getCompressorActive() + 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) @property def supported_features(self): @@ -231,7 +275,7 @@ class ViCareClimate(ClimateEntity): ) _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) - self._api.setMode(vicare_mode) + self._circuit.setMode(vicare_mode) @property def hvac_modes(self): @@ -263,7 +307,7 @@ class ViCareClimate(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: - self._api.setProgramTemperature(self._current_program, temp) + self._circuit.setProgramTemperature(self._current_program, temp) self._target_temperature = temp @property @@ -285,8 +329,8 @@ class ViCareClimate(ClimateEntity): ) _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) - self._api.deactivateProgram(self._current_program) - self._api.activateProgram(vicare_program) + self._circuit.deactivateProgram(self._current_program) + self._circuit.activateProgram(vicare_program) @property def extra_state_attributes(self): @@ -298,4 +342,4 @@ class ViCareClimate(ClimateEntity): if vicare_mode not in VICARE_TO_HA_HVAC_HEATING: raise ValueError(f"Cannot set invalid vicare mode: {vicare_mode}") - self._api.setMode(vicare_mode) + self._circuit.setMode(vicare_mode) diff --git a/homeassistant/components/vicare/const.py b/homeassistant/components/vicare/const.py new file mode 100644 index 00000000000..2336ef40eaa --- /dev/null +++ b/homeassistant/components/vicare/const.py @@ -0,0 +1,39 @@ +"""Constants for the ViCare integration.""" +import enum + +DOMAIN = "vicare" + +PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"] + +VICARE_DEVICE_CONFIG = "device_conf" +VICARE_API = "api" +VICARE_NAME = "name" +VICARE_CIRCUITS = "circuits" + +CONF_CIRCUIT = "circuit" +CONF_HEATING_TYPE = "heating_type" + +DEFAULT_SCAN_INTERVAL = 60 + + +class HeatingType(enum.Enum): + """Possible options for heating type.""" + + auto = "auto" + gas = "gas" + oil = "oil" + pellets = "pellets" + heatpump = "heatpump" + fuelcell = "fuelcell" + + +DEFAULT_HEATING_TYPE = HeatingType.auto + +HEATING_TYPE_TO_CREATOR_METHOD = { + HeatingType.auto: "asAutoDetectDevice", + HeatingType.gas: "asGazBoiler", + HeatingType.fuelcell: "asFuelCell", + HeatingType.heatpump: "asHeatPump", + HeatingType.oil: "asOilBoiler", + HeatingType.pellets: "asPelletsBoiler", +} diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 88e9a1e4e4b..38344fff70b 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,6 +3,6 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==1.0.0"], + "requirements": ["PyViCare==2.13.0"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index c7e318a6c16..68bb1ff2363 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -4,16 +4,19 @@ from __future__ import annotations from contextlib import suppress from dataclasses import dataclass import logging -from typing import Union -from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError -from PyViCare.PyViCareDevice import Device -from PyViCare.PyViCareFuelCell import FuelCell -from PyViCare.PyViCareGazBoiler import GazBoiler -from PyViCare.PyViCareHeatPump import HeatPump +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareNotSupportedFeatureError, + PyViCareRateLimitError, +) import requests -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -24,21 +27,13 @@ from homeassistant.const import ( TEMP_CELSIUS, TIME_HOURS, ) +import homeassistant.util.dt as dt_util -from . import ( - DOMAIN as VICARE_DOMAIN, - VICARE_API, - VICARE_HEATING_TYPE, - VICARE_NAME, - ApiT, - HeatingType, - ViCareRequiredKeysMixin, -) +from . import ViCareRequiredKeysMixin +from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME _LOGGER = logging.getLogger(__name__) -SENSOR_TYPE_TEMPERATURE = "temperature" - SENSOR_OUTSIDE_TEMPERATURE = "outside_temperature" SENSOR_SUPPLY_TEMPERATURE = "supply_temperature" SENSOR_RETURN_TEMPERATURE = "return_temperature" @@ -76,308 +71,340 @@ SENSOR_POWER_PRODUCTION_THIS_YEAR = "power_production_this_year" @dataclass -class ViCareSensorEntityDescription( - SensorEntityDescription, ViCareRequiredKeysMixin[ApiT] -): +class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysMixin): """Describes ViCare sensor entity.""" -SENSOR_TYPES_GENERIC: tuple[ViCareSensorEntityDescription[Device], ...] = ( - ViCareSensorEntityDescription[Device]( +GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( + ViCareSensorEntityDescription( key=SENSOR_OUTSIDE_TEMPERATURE, name="Outside Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getOutsideTemperature(), device_class=DEVICE_CLASS_TEMPERATURE, ), - ViCareSensorEntityDescription[Device]( - key=SENSOR_SUPPLY_TEMPERATURE, - name="Supply Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - value_getter=lambda api: api.getSupplyTemperature(), - device_class=DEVICE_CLASS_TEMPERATURE, - ), -) - -SENSOR_TYPES_GAS: tuple[ViCareSensorEntityDescription[GazBoiler], ...] = ( - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_BOILER_TEMPERATURE, - name="Boiler Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - value_getter=lambda api: api.getBoilerTemperature(), - device_class=DEVICE_CLASS_TEMPERATURE, - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_BURNER_MODULATION, - name="Burner modulation", - icon="mdi:percent", - native_unit_of_measurement=PERCENTAGE, - value_getter=lambda api: api.getBurnerModulation(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_DHW_GAS_CONSUMPTION_TODAY, - name="Hot water gas consumption today", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionDomesticHotWaterToday(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, - name="Hot water gas consumption this week", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisWeek(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, - name="Hot water gas consumption this month", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisMonth(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, - name="Hot water gas consumption this year", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisYear(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_GAS_CONSUMPTION_TODAY, - name="Heating gas consumption today", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionHeatingToday(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_GAS_CONSUMPTION_THIS_WEEK, - name="Heating gas consumption this week", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionHeatingThisWeek(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_GAS_CONSUMPTION_THIS_MONTH, - name="Heating gas consumption this month", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionHeatingThisMonth(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_GAS_CONSUMPTION_THIS_YEAR, - name="Heating gas consumption this year", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionHeatingThisYear(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_BURNER_STARTS, - name="Burner Starts", - icon="mdi:counter", - value_getter=lambda api: api.getBurnerStarts(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_BURNER_HOURS, - name="Burner Hours", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getBurnerHours(), - ), -) - -SENSOR_TYPES_HEATPUMP: tuple[ViCareSensorEntityDescription[HeatPump], ...] = ( - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_STARTS, - name="Compressor Starts", - icon="mdi:counter", - value_getter=lambda api: api.getCompressorStarts(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS, - name="Compressor Hours", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHours(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS1, - name="Compressor Hours Load Class 1", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHoursLoadClass1(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS2, - name="Compressor Hours Load Class 2", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHoursLoadClass2(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS3, - name="Compressor Hours Load Class 3", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHoursLoadClass3(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS4, - name="Compressor Hours Load Class 4", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHoursLoadClass4(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS5, - name="Compressor Hours Load Class 5", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHoursLoadClass5(), - ), - ViCareSensorEntityDescription[HeatPump]( + ViCareSensorEntityDescription( key=SENSOR_RETURN_TEMPERATURE, name="Return Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getReturnTemperature(), device_class=DEVICE_CLASS_TEMPERATURE, ), -) - -SENSOR_TYPES_FUELCELL: tuple[ViCareSensorEntityDescription[FuelCell], ...] = ( - ViCareSensorEntityDescription[FuelCell]( + ViCareSensorEntityDescription( + key=SENSOR_BOILER_TEMPERATURE, + name="Boiler Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getBoilerTemperature(), + device_class=DEVICE_CLASS_TEMPERATURE, + ), + ViCareSensorEntityDescription( + key=SENSOR_DHW_GAS_CONSUMPTION_TODAY, + name="Hot water gas consumption today", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionDomesticHotWaterToday(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, + name="Hot water gas consumption this week", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisWeek(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, + name="Hot water gas consumption this month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisMonth(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, + name="Hot water gas consumption this year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisYear(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_GAS_CONSUMPTION_TODAY, + name="Heating gas consumption today", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionHeatingToday(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_GAS_CONSUMPTION_THIS_WEEK, + name="Heating gas consumption this week", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionHeatingThisWeek(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_GAS_CONSUMPTION_THIS_MONTH, + name="Heating gas consumption this month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionHeatingThisMonth(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_GAS_CONSUMPTION_THIS_YEAR, + name="Heating gas consumption this year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionHeatingThisYear(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_CURRENT, name="Power production current", native_unit_of_measurement=POWER_WATT, value_getter=lambda api: api.getPowerProductionCurrent(), device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - ViCareSensorEntityDescription[FuelCell]( + ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_TODAY, name="Power production today", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionToday(), device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - ViCareSensorEntityDescription[FuelCell]( + ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_THIS_WEEK, name="Power production this week", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisWeek(), device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - ViCareSensorEntityDescription[FuelCell]( + ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_THIS_MONTH, name="Power production this month", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisMonth(), device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - ViCareSensorEntityDescription[FuelCell]( + ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_THIS_YEAR, name="Power production this year", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisYear(), device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), ) -SENSORS_GENERIC = [SENSOR_OUTSIDE_TEMPERATURE, SENSOR_SUPPLY_TEMPERATURE] +CIRCUIT_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( + ViCareSensorEntityDescription( + key=SENSOR_SUPPLY_TEMPERATURE, + name="Supply Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getSupplyTemperature(), + ), +) -SENSORS_BY_HEATINGTYPE = { - HeatingType.gas: [ - SENSOR_BOILER_TEMPERATURE, - SENSOR_BURNER_HOURS, - SENSOR_BURNER_MODULATION, - SENSOR_BURNER_STARTS, - SENSOR_DHW_GAS_CONSUMPTION_TODAY, - SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, - SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, - SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, - SENSOR_GAS_CONSUMPTION_TODAY, - SENSOR_GAS_CONSUMPTION_THIS_WEEK, - SENSOR_GAS_CONSUMPTION_THIS_MONTH, - SENSOR_GAS_CONSUMPTION_THIS_YEAR, - ], - HeatingType.heatpump: [ - SENSOR_COMPRESSOR_STARTS, - SENSOR_COMPRESSOR_HOURS, - SENSOR_COMPRESSOR_HOURS_LOADCLASS1, - SENSOR_COMPRESSOR_HOURS_LOADCLASS2, - SENSOR_COMPRESSOR_HOURS_LOADCLASS3, - SENSOR_COMPRESSOR_HOURS_LOADCLASS4, - SENSOR_COMPRESSOR_HOURS_LOADCLASS5, - SENSOR_RETURN_TEMPERATURE, - ], - HeatingType.fuelcell: [ - # gas - SENSOR_BOILER_TEMPERATURE, - SENSOR_BURNER_HOURS, - SENSOR_BURNER_MODULATION, - SENSOR_BURNER_STARTS, - SENSOR_DHW_GAS_CONSUMPTION_TODAY, - SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, - SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, - SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, - SENSOR_GAS_CONSUMPTION_TODAY, - SENSOR_GAS_CONSUMPTION_THIS_WEEK, - SENSOR_GAS_CONSUMPTION_THIS_MONTH, - SENSOR_GAS_CONSUMPTION_THIS_YEAR, - # fuel cell - SENSOR_POWER_PRODUCTION_CURRENT, - SENSOR_POWER_PRODUCTION_TODAY, - SENSOR_POWER_PRODUCTION_THIS_WEEK, - SENSOR_POWER_PRODUCTION_THIS_MONTH, - SENSOR_POWER_PRODUCTION_THIS_YEAR, - ], -} +BURNER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( + ViCareSensorEntityDescription( + key=SENSOR_BURNER_STARTS, + name="Burner Starts", + icon="mdi:counter", + value_getter=lambda api: api.getStarts(), + ), + ViCareSensorEntityDescription( + key=SENSOR_BURNER_HOURS, + name="Burner Hours", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHours(), + ), + ViCareSensorEntityDescription( + key=SENSOR_BURNER_MODULATION, + name="Burner Modulation", + icon="mdi:percent", + native_unit_of_measurement=PERCENTAGE, + value_getter=lambda api: api.getModulation(), + ), +) + +COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_STARTS, + name="Compressor Starts", + icon="mdi:counter", + value_getter=lambda api: api.getStarts(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS, + name="Compressor Hours", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHours(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS_LOADCLASS1, + name="Compressor Hours Load Class 1", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHoursLoadClass1(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS_LOADCLASS2, + name="Compressor Hours Load Class 2", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHoursLoadClass2(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS_LOADCLASS3, + name="Compressor Hours Load Class 3", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHoursLoadClass3(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS_LOADCLASS4, + name="Compressor Hours Load Class 4", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHoursLoadClass4(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS_LOADCLASS5, + name="Compressor Hours Load Class 5", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHoursLoadClass5(), + ), +) -def setup_platform(hass, config, add_entities, discovery_info=None): +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, + ) + + +async def _entities_from_descriptions( + hass, name, all_devices, sensor_descriptions, iterables +): + """Create entities from descriptions and list of burners/circuits.""" + for description in sensor_descriptions: + for current in iterables: + suffix = "" + if len(iterables) > 1: + suffix = f" {current.id}" + entity = await hass.async_add_executor_job( + _build_entity, + f"{name} {description.name}{suffix}", + current, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + description, + ) + if entity is not None: + all_devices.append(entity) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the ViCare sensor devices.""" if discovery_info is None: return - vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] - heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] + name = hass.data[DOMAIN][VICARE_NAME] + api = hass.data[DOMAIN][VICARE_API] - sensors = SENSORS_GENERIC.copy() + all_devices = [] + for description in GLOBAL_SENSORS: + entity = await hass.async_add_executor_job( + _build_entity, + f"{name} {description.name}", + api, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + description, + ) + if entity is not None: + all_devices.append(entity) - if heating_type != HeatingType.generic: - sensors.extend(SENSORS_BY_HEATINGTYPE[heating_type]) - - add_entities( - [ - ViCareSensor(hass.data[VICARE_DOMAIN][VICARE_NAME], vicare_api, description) - for description in ( - *SENSOR_TYPES_GENERIC, - *SENSOR_TYPES_GAS, - *SENSOR_TYPES_HEATPUMP, - *SENSOR_TYPES_FUELCELL, + for description in CIRCUIT_SENSORS: + for circuit in api.circuits: + suffix = "" + if len(api.circuits) > 1: + suffix = f" {circuit.id}" + entity = await hass.async_add_executor_job( + _build_entity, + f"{name} {description.name}{suffix}", + circuit, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + description, ) - if description.key in sensors - ] - ) + if entity is not None: + all_devices.append(entity) + try: + _entities_from_descriptions( + hass, name, all_devices, BURNER_SENSORS, api.burners + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No burners found") -DescriptionT = Union[ - ViCareSensorEntityDescription[Device], - ViCareSensorEntityDescription[GazBoiler], - ViCareSensorEntityDescription[HeatPump], - ViCareSensorEntityDescription[FuelCell], -] + try: + _entities_from_descriptions( + hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No compressors found") + + async_add_entities(all_devices) class ViCareSensor(SensorEntity): """Representation of a ViCare sensor.""" - entity_description: DescriptionT + entity_description: ViCareSensorEntityDescription - def __init__(self, name, api, description: DescriptionT): + def __init__( + self, name, api, device_config, description: ViCareSensorEntityDescription + ): """Initialize the sensor.""" self.entity_description = description - self._attr_name = f"{name} {description.name}" + self._attr_name = name self._api = api + self._device_config = device_config self._state = None + self._last_reset = dt_util.utcnow() + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self._device_config.getConfig().serial)}, + "name": self._device_config.getModel(), + "manufacturer": "Viessmann", + "model": (DOMAIN, self._device_config.getModel()), + } @property def available(self): @@ -386,16 +413,27 @@ class ViCareSensor(SensorEntity): @property def unique_id(self): - """Return a unique ID.""" - return f"{self._api.service.id}-{self.entity_description.key}" + """Return unique ID for this device.""" + tmp_id = ( + f"{self._device_config.getConfig().serial}-{self.entity_description.key}" + ) + if hasattr(self._api, "id"): + return f"{tmp_id}-{self._api.id}" + return tmp_id @property def native_value(self): """Return the state of the sensor.""" return self._state + @property + def last_reset(self): + """Return the time when the sensor was last reset.""" + return self._last_reset + def update(self): """Update state of sensor.""" + self._last_reset = dt_util.start_of_local_day() try: with suppress(PyViCareNotSupportedFeatureError): self._state = self.entity_description.value_getter(self._api) @@ -405,3 +443,5 @@ class ViCareSensor(SensorEntity): _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) diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 557d5257427..524ea52c756 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -2,7 +2,11 @@ from contextlib import suppress import logging -from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareNotSupportedFeatureError, + PyViCareRateLimitError, +) import requests from homeassistant.components.water_heater import ( @@ -11,7 +15,14 @@ from homeassistant.components.water_heater import ( ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS -from . import DOMAIN as VICARE_DOMAIN, VICARE_API, VICARE_HEATING_TYPE, VICARE_NAME +from .const import ( + CONF_HEATING_TYPE, + DOMAIN, + VICARE_API, + VICARE_CIRCUITS, + VICARE_DEVICE_CONFIG, + VICARE_NAME, +) _LOGGER = logging.getLogger(__name__) @@ -43,31 +54,53 @@ HA_TO_VICARE_HVAC_DHW = { } -def setup_platform(hass, config, add_entities, discovery_info=None): +def _build_entity(name, vicare_api, circuit, device_config, heating_type): + """Create a ViCare water_heater entity.""" + _LOGGER.debug("Found device %s", name) + return ViCareWater( + name, + vicare_api, + circuit, + device_config, + heating_type, + ) + + +async def async_setup_platform(hass, config, async_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] - heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] - add_entities( - [ - ViCareWater( - f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", - vicare_api, - heating_type, - ) - ] - ) + + name = hass.data[DOMAIN][VICARE_NAME] + + all_devices = [] + for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]: + suffix = "" + if len(hass.data[DOMAIN][VICARE_CIRCUITS]) > 1: + suffix = f" {circuit.id}" + entity = _build_entity( + f"{name} Water{suffix}", + hass.data[DOMAIN][VICARE_API], + circuit, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][CONF_HEATING_TYPE], + ) + if entity is not None: + all_devices.append(entity) + + async_add_entities(all_devices) class ViCareWater(WaterHeaterEntity): """Representation of the ViCare domestic hot water device.""" - def __init__(self, name, api, heating_type): + def __init__(self, name, api, circuit, device_config, heating_type): """Initialize the DHW water_heater device.""" self._name = name self._state = None self._api = api + self._circuit = circuit + self._device_config = device_config self._attributes = {} self._target_temperature = None self._current_temperature = None @@ -84,11 +117,11 @@ class ViCareWater(WaterHeaterEntity): with suppress(PyViCareNotSupportedFeatureError): self._target_temperature = ( - self._api.getDomesticHotWaterConfiguredTemperature() + self._api.getDomesticHotWaterDesiredTemperature() ) with suppress(PyViCareNotSupportedFeatureError): - self._current_mode = self._api.getActiveMode() + self._current_mode = self._circuit.getActiveMode() except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") @@ -96,6 +129,23 @@ class ViCareWater(WaterHeaterEntity): _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) + + @property + def unique_id(self): + """Return unique ID for this device.""" + return f"{self._device_config.getConfig().serial}-water-{self._circuit.id}" + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self._device_config.getConfig().serial)}, + "name": self._device_config.getModel(), + "manufacturer": "Viessmann", + "model": (DOMAIN, self._device_config.getModel()), + } @property def supported_features(self): diff --git a/requirements_all.txt b/requirements_all.txt index a1af8fda1b8..fe91683cc6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -55,7 +55,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.1 # homeassistant.components.vicare -PyViCare==1.0.0 +PyViCare==2.13.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4