Update PyVicare to 2.13.0 (#57700)

* Update PyVicare to 2.x

With PyViCare 2.8.1 a breaking change was introduced which required changes on sensor and binary_sensor platforms:
- Circuit, Burner and Compressor have been separated out from the "main" device
- Multiple circuits and burners allow "duplicate sensors". We add the circuit or burner number as suffix now

At the same time the sensors are now created only when available:
During entity creation we can check if the value is provided for the user's device.

Sensors are not created by heating type anymore but instead the new API structure is reflected, providing device, burner or circuit sensors.

For details of breaking changes from PyViCare 1.x to 2.x please see https://github.com/somm15/PyViCare#breaking-changes-in-version-2x

* Integrate review comments

* variables cleanup

* Update unique ids

The unique ids shall not depend on the name but on the entity
description key (which should not change) and the id of the circuit,
burner or device.
This commit is contained in:
Hans Oischinger 2021-10-25 13:43:43 +02:00 committed by GitHub
parent be4b1d15ec
commit 66ae116023
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 661 additions and 441 deletions

View file

@ -1,16 +1,12 @@
"""The ViCare integration.""" """The ViCare integration."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
import enum
import logging import logging
from typing import Generic, TypeVar from typing import Callable
from PyViCare.PyViCare import PyViCare
from PyViCare.PyViCareDevice import Device from PyViCare.PyViCareDevice import Device
from PyViCare.PyViCareFuelCell import FuelCell
from PyViCare.PyViCareGazBoiler import GazBoiler
from PyViCare.PyViCareHeatPump import HeatPump
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
@ -24,55 +20,51 @@ from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.storage import STORAGE_DIR 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__) _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() @dataclass()
class ViCareRequiredKeysMixin(Generic[ApiT]): class ViCareRequiredKeysMixin:
"""Mixin for required keys.""" """Mixin for required keys."""
value_getter: Callable[[ApiT], bool] value_getter: Callable[[Device], bool]
class HeatingType(enum.Enum):
"""Possible options for heating type."""
generic = "generic"
gas = "gas"
heatpump = "heatpump"
fuelcell = "fuelcell"
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.All(
{ cv.deprecated(CONF_CIRCUIT),
vol.Required(CONF_USERNAME): cv.string, vol.Schema(
vol.Required(CONF_PASSWORD): cv.string, {
vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=60): vol.All( vol.Required(CONF_PASSWORD): cv.string,
cv.time_period, lambda value: value.total_seconds() vol.Required(CONF_CLIENT_ID): cv.string,
), vol.Optional(CONF_SCAN_INTERVAL, default=60): vol.All(
vol.Optional(CONF_CIRCUIT): int, cv.time_period, lambda value: value.total_seconds()
vol.Optional(CONF_NAME, default="ViCare"): cv.string, ),
vol.Optional(CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE): cv.enum( vol.Optional(
HeatingType 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, extra=vol.ALLOW_EXTRA,
@ -83,34 +75,40 @@ def setup(hass, config):
"""Create the ViCare component.""" """Create the ViCare component."""
conf = config[DOMAIN] conf = config[DOMAIN]
params = {"token_file": hass.config.path(STORAGE_DIR, "vicare_token.save")} 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["cacheDuration"] = conf.get(CONF_SCAN_INTERVAL)
params["client_id"] = conf.get(CONF_CLIENT_ID) 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] = {}
hass.data[DOMAIN][VICARE_API] = vicare_api
hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] 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: for platform in PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, config) discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True 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

View file

@ -4,12 +4,12 @@ from __future__ import annotations
from contextlib import suppress from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from typing import Union
from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError from PyViCare.PyViCareUtils import (
from PyViCare.PyViCareDevice import Device PyViCareInvalidDataError,
from PyViCare.PyViCareGazBoiler import GazBoiler PyViCareNotSupportedFeatureError,
from PyViCare.PyViCareHeatPump import HeatPump PyViCareRateLimitError,
)
import requests import requests
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
@ -18,36 +18,25 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from . import ( from . import ViCareRequiredKeysMixin
DOMAIN as VICARE_DOMAIN, from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME
VICARE_API,
VICARE_HEATING_TYPE,
VICARE_NAME,
ApiT,
HeatingType,
ViCareRequiredKeysMixin,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_CIRCULATION_PUMP_ACTIVE = "circulationpump_active" SENSOR_CIRCULATION_PUMP_ACTIVE = "circulationpump_active"
# gas sensors
SENSOR_BURNER_ACTIVE = "burner_active" SENSOR_BURNER_ACTIVE = "burner_active"
# heatpump sensors
SENSOR_COMPRESSOR_ACTIVE = "compressor_active" SENSOR_COMPRESSOR_ACTIVE = "compressor_active"
@dataclass @dataclass
class ViCareBinarySensorEntityDescription( class ViCareBinarySensorEntityDescription(
BinarySensorEntityDescription, ViCareRequiredKeysMixin[ApiT] BinarySensorEntityDescription, ViCareRequiredKeysMixin
): ):
"""Describes ViCare binary sensor entity.""" """Describes ViCare binary sensor entity."""
SENSOR_TYPES_GENERIC: tuple[ViCareBinarySensorEntityDescription[Device]] = ( CIRCUIT_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = (
ViCareBinarySensorEntityDescription[Device]( ViCareBinarySensorEntityDescription(
key=SENSOR_CIRCULATION_PUMP_ACTIVE, key=SENSOR_CIRCULATION_PUMP_ACTIVE,
name="Circulation pump active", name="Circulation pump active",
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
@ -55,80 +44,133 @@ SENSOR_TYPES_GENERIC: tuple[ViCareBinarySensorEntityDescription[Device]] = (
), ),
) )
SENSOR_TYPES_GAS: tuple[ViCareBinarySensorEntityDescription[GazBoiler]] = ( BURNER_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = (
ViCareBinarySensorEntityDescription[GazBoiler]( ViCareBinarySensorEntityDescription(
key=SENSOR_BURNER_ACTIVE, key=SENSOR_BURNER_ACTIVE,
name="Burner active", name="Burner active",
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
value_getter=lambda api: api.getBurnerActive(), value_getter=lambda api: api.getActive(),
), ),
) )
SENSOR_TYPES_HEATPUMP: tuple[ViCareBinarySensorEntityDescription[HeatPump]] = ( COMPRESSOR_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = (
ViCareBinarySensorEntityDescription[HeatPump]( ViCareBinarySensorEntityDescription(
key=SENSOR_COMPRESSOR_ACTIVE, key=SENSOR_COMPRESSOR_ACTIVE,
name="Compressor active", name="Compressor active",
device_class=DEVICE_CLASS_POWER, 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 = { def _build_entity(name, vicare_api, device_config, sensor):
HeatingType.gas: [SENSOR_BURNER_ACTIVE], """Create a ViCare binary sensor entity."""
HeatingType.heatpump: [SENSOR_COMPRESSOR_ACTIVE], try:
HeatingType.fuelcell: [SENSOR_BURNER_ACTIVE], 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(
def setup_platform(hass, config, add_entities, discovery_info=None): name,
"""Create the ViCare sensor devices.""" vicare_api,
if discovery_info is None: device_config,
return sensor,
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
]
) )
DescriptionT = Union[ async def _entities_from_descriptions(
ViCareBinarySensorEntityDescription[Device], hass, name, all_devices, sensor_descriptions, iterables
ViCareBinarySensorEntityDescription[GazBoiler], ):
ViCareBinarySensorEntityDescription[HeatPump], """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): class ViCareBinarySensor(BinarySensorEntity):
"""Representation of a ViCare sensor.""" """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.""" """Initialize the sensor."""
self.entity_description = description self.entity_description = description
self._attr_name = f"{name} {description.name}" self._attr_name = name
self._api = api self._api = api
self.entity_description = description
self._device_config = device_config
self._state = None 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 @property
def available(self): def available(self):
"""Return True if entity is available.""" """Return True if entity is available."""
@ -136,8 +178,13 @@ class ViCareBinarySensor(BinarySensorEntity):
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique ID.""" """Return unique ID for this device."""
return f"{self._api.service.id}-{self.entity_description.key}" 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 @property
def is_on(self): def is_on(self):
@ -155,3 +202,5 @@ class ViCareBinarySensor(BinarySensorEntity):
_LOGGER.error("Unable to decode data from ViCare server") _LOGGER.error("Unable to decode data from ViCare server")
except PyViCareRateLimitError as limit_exception: except PyViCareRateLimitError as limit_exception:
_LOGGER.error("Vicare API rate limit exceeded: %s", 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)

View file

@ -2,7 +2,11 @@
from contextlib import suppress from contextlib import suppress
import logging import logging
from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError from PyViCare.PyViCareUtils import (
PyViCareInvalidDataError,
PyViCareNotSupportedFeatureError,
PyViCareRateLimitError,
)
import requests import requests
import voluptuous as vol 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.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
from . import ( from .const import (
DOMAIN as VICARE_DOMAIN, CONF_HEATING_TYPE,
DOMAIN,
VICARE_API, VICARE_API,
VICARE_HEATING_TYPE, VICARE_CIRCUITS,
VICARE_DEVICE_CONFIG,
VICARE_NAME, VICARE_NAME,
HeatingType,
) )
_LOGGER = logging.getLogger(__name__) _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( async def async_setup_platform(
hass, hass_config, async_add_entities, discovery_info=None hass, hass_config, async_add_entities, discovery_info=None
): ):
"""Create the ViCare climate devices.""" """Create the ViCare climate devices."""
# Legacy setup. Remove after configuration.yaml deprecation end
if discovery_info is None: if discovery_info is None:
return return
vicare_api = hass.data[VICARE_DOMAIN][VICARE_API]
heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] name = hass.data[DOMAIN][VICARE_NAME]
async_add_entities( all_devices = []
[
ViCareClimate( for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]:
f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", suffix = ""
vicare_api, if len(hass.data[DOMAIN][VICARE_CIRCUITS]) > 1:
heating_type, 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() platform = entity_platform.async_get_current_platform()
@ -121,11 +141,13 @@ async def async_setup_platform(
class ViCareClimate(ClimateEntity): class ViCareClimate(ClimateEntity):
"""Representation of the ViCare heating climate device.""" """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.""" """Initialize the climate device."""
self._name = name self._name = name
self._state = None self._state = None
self._api = api self._api = api
self._circuit = circuit
self._device_config = device_config
self._attributes = {} self._attributes = {}
self._target_temperature = None self._target_temperature = None
self._current_mode = None self._current_mode = None
@ -134,16 +156,31 @@ class ViCareClimate(ClimateEntity):
self._heating_type = heating_type self._heating_type = heating_type
self._current_action = None 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): def update(self):
"""Let HA know there has been an update from the ViCare API.""" """Let HA know there has been an update from the ViCare API."""
try: try:
_room_temperature = None _room_temperature = None
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
_room_temperature = self._api.getRoomTemperature() _room_temperature = self._circuit.getRoomTemperature()
_supply_temperature = None _supply_temperature = None
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
_supply_temperature = self._api.getSupplyTemperature() _supply_temperature = self._circuit.getSupplyTemperature()
if _room_temperature is not None: if _room_temperature is not None:
self._current_temperature = _room_temperature self._current_temperature = _room_temperature
@ -153,13 +190,13 @@ class ViCareClimate(ClimateEntity):
self._current_temperature = None self._current_temperature = None
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
self._current_program = self._api.getActiveProgram() self._current_program = self._circuit.getActiveProgram()
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
self._target_temperature = self._api.getCurrentDesiredTemperature() self._target_temperature = self._circuit.getCurrentDesiredTemperature()
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
self._current_mode = self._api.getActiveMode() self._current_mode = self._circuit.getActiveMode()
# Update the generic device attributes # Update the generic device attributes
self._attributes = {} self._attributes = {}
@ -171,26 +208,33 @@ class ViCareClimate(ClimateEntity):
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
self._attributes[ self._attributes[
"heating_curve_slope" "heating_curve_slope"
] = self._api.getHeatingCurveSlope() ] = self._circuit.getHeatingCurveSlope()
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
self._attributes[ self._attributes[
"heating_curve_shift" "heating_curve_shift"
] = self._api.getHeatingCurveShift() ] = self._circuit.getHeatingCurveShift()
self._current_action = False
# Update the specific device attributes # Update the specific device attributes
if self._heating_type == HeatingType.gas: with suppress(PyViCareNotSupportedFeatureError):
with suppress(PyViCareNotSupportedFeatureError): for burner in self._api.burners:
self._current_action = self._api.getBurnerActive() self._current_action = self._current_action or burner.getActive()
elif self._heating_type == HeatingType.heatpump:
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
self._current_action = self._api.getCompressorActive() for compressor in self._api.compressors:
self._current_action = (
self._current_action or compressor.getActive()
)
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("Unable to retrieve data from ViCare server") _LOGGER.error("Unable to retrieve data from ViCare server")
except PyViCareRateLimitError as limit_exception: except PyViCareRateLimitError as limit_exception:
_LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception)
except ValueError: except ValueError:
_LOGGER.error("Unable to decode data from ViCare server") _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 @property
def supported_features(self): def supported_features(self):
@ -231,7 +275,7 @@ class ViCareClimate(ClimateEntity):
) )
_LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode)
self._api.setMode(vicare_mode) self._circuit.setMode(vicare_mode)
@property @property
def hvac_modes(self): def hvac_modes(self):
@ -263,7 +307,7 @@ class ViCareClimate(ClimateEntity):
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperatures.""" """Set new target temperatures."""
if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: 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 self._target_temperature = temp
@property @property
@ -285,8 +329,8 @@ class ViCareClimate(ClimateEntity):
) )
_LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program)
self._api.deactivateProgram(self._current_program) self._circuit.deactivateProgram(self._current_program)
self._api.activateProgram(vicare_program) self._circuit.activateProgram(vicare_program)
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
@ -298,4 +342,4 @@ class ViCareClimate(ClimateEntity):
if vicare_mode not in VICARE_TO_HA_HVAC_HEATING: if vicare_mode not in VICARE_TO_HA_HVAC_HEATING:
raise ValueError(f"Cannot set invalid vicare mode: {vicare_mode}") raise ValueError(f"Cannot set invalid vicare mode: {vicare_mode}")
self._api.setMode(vicare_mode) self._circuit.setMode(vicare_mode)

View file

@ -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",
}

View file

@ -3,6 +3,6 @@
"name": "Viessmann ViCare", "name": "Viessmann ViCare",
"documentation": "https://www.home-assistant.io/integrations/vicare", "documentation": "https://www.home-assistant.io/integrations/vicare",
"codeowners": ["@oischinger"], "codeowners": ["@oischinger"],
"requirements": ["PyViCare==1.0.0"], "requirements": ["PyViCare==2.13.0"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View file

@ -4,16 +4,19 @@ from __future__ import annotations
from contextlib import suppress from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from typing import Union
from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError from PyViCare.PyViCareUtils import (
from PyViCare.PyViCareDevice import Device PyViCareInvalidDataError,
from PyViCare.PyViCareFuelCell import FuelCell PyViCareNotSupportedFeatureError,
from PyViCare.PyViCareGazBoiler import GazBoiler PyViCareRateLimitError,
from PyViCare.PyViCareHeatPump import HeatPump )
import requests import requests
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.components.sensor import (
STATE_CLASS_TOTAL_INCREASING,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
@ -24,21 +27,13 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
TIME_HOURS, TIME_HOURS,
) )
import homeassistant.util.dt as dt_util
from . import ( from . import ViCareRequiredKeysMixin
DOMAIN as VICARE_DOMAIN, from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME
VICARE_API,
VICARE_HEATING_TYPE,
VICARE_NAME,
ApiT,
HeatingType,
ViCareRequiredKeysMixin,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_TYPE_TEMPERATURE = "temperature"
SENSOR_OUTSIDE_TEMPERATURE = "outside_temperature" SENSOR_OUTSIDE_TEMPERATURE = "outside_temperature"
SENSOR_SUPPLY_TEMPERATURE = "supply_temperature" SENSOR_SUPPLY_TEMPERATURE = "supply_temperature"
SENSOR_RETURN_TEMPERATURE = "return_temperature" SENSOR_RETURN_TEMPERATURE = "return_temperature"
@ -76,308 +71,340 @@ SENSOR_POWER_PRODUCTION_THIS_YEAR = "power_production_this_year"
@dataclass @dataclass
class ViCareSensorEntityDescription( class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysMixin):
SensorEntityDescription, ViCareRequiredKeysMixin[ApiT]
):
"""Describes ViCare sensor entity.""" """Describes ViCare sensor entity."""
SENSOR_TYPES_GENERIC: tuple[ViCareSensorEntityDescription[Device], ...] = ( GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
ViCareSensorEntityDescription[Device]( ViCareSensorEntityDescription(
key=SENSOR_OUTSIDE_TEMPERATURE, key=SENSOR_OUTSIDE_TEMPERATURE,
name="Outside Temperature", name="Outside Temperature",
native_unit_of_measurement=TEMP_CELSIUS, native_unit_of_measurement=TEMP_CELSIUS,
value_getter=lambda api: api.getOutsideTemperature(), value_getter=lambda api: api.getOutsideTemperature(),
device_class=DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
), ),
ViCareSensorEntityDescription[Device]( ViCareSensorEntityDescription(
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](
key=SENSOR_RETURN_TEMPERATURE, key=SENSOR_RETURN_TEMPERATURE,
name="Return Temperature", name="Return Temperature",
native_unit_of_measurement=TEMP_CELSIUS, native_unit_of_measurement=TEMP_CELSIUS,
value_getter=lambda api: api.getReturnTemperature(), value_getter=lambda api: api.getReturnTemperature(),
device_class=DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
), ),
) ViCareSensorEntityDescription(
key=SENSOR_BOILER_TEMPERATURE,
SENSOR_TYPES_FUELCELL: tuple[ViCareSensorEntityDescription[FuelCell], ...] = ( name="Boiler Temperature",
ViCareSensorEntityDescription[FuelCell]( 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, key=SENSOR_POWER_PRODUCTION_CURRENT,
name="Power production current", name="Power production current",
native_unit_of_measurement=POWER_WATT, native_unit_of_measurement=POWER_WATT,
value_getter=lambda api: api.getPowerProductionCurrent(), value_getter=lambda api: api.getPowerProductionCurrent(),
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_TOTAL_INCREASING,
), ),
ViCareSensorEntityDescription[FuelCell]( ViCareSensorEntityDescription(
key=SENSOR_POWER_PRODUCTION_TODAY, key=SENSOR_POWER_PRODUCTION_TODAY,
name="Power production today", name="Power production today",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
value_getter=lambda api: api.getPowerProductionToday(), value_getter=lambda api: api.getPowerProductionToday(),
device_class=DEVICE_CLASS_ENERGY, device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
), ),
ViCareSensorEntityDescription[FuelCell]( ViCareSensorEntityDescription(
key=SENSOR_POWER_PRODUCTION_THIS_WEEK, key=SENSOR_POWER_PRODUCTION_THIS_WEEK,
name="Power production this week", name="Power production this week",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
value_getter=lambda api: api.getPowerProductionThisWeek(), value_getter=lambda api: api.getPowerProductionThisWeek(),
device_class=DEVICE_CLASS_ENERGY, device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
), ),
ViCareSensorEntityDescription[FuelCell]( ViCareSensorEntityDescription(
key=SENSOR_POWER_PRODUCTION_THIS_MONTH, key=SENSOR_POWER_PRODUCTION_THIS_MONTH,
name="Power production this month", name="Power production this month",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
value_getter=lambda api: api.getPowerProductionThisMonth(), value_getter=lambda api: api.getPowerProductionThisMonth(),
device_class=DEVICE_CLASS_ENERGY, device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
), ),
ViCareSensorEntityDescription[FuelCell]( ViCareSensorEntityDescription(
key=SENSOR_POWER_PRODUCTION_THIS_YEAR, key=SENSOR_POWER_PRODUCTION_THIS_YEAR,
name="Power production this year", name="Power production this year",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
value_getter=lambda api: api.getPowerProductionThisYear(), value_getter=lambda api: api.getPowerProductionThisYear(),
device_class=DEVICE_CLASS_ENERGY, 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 = { BURNER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
HeatingType.gas: [ ViCareSensorEntityDescription(
SENSOR_BOILER_TEMPERATURE, key=SENSOR_BURNER_STARTS,
SENSOR_BURNER_HOURS, name="Burner Starts",
SENSOR_BURNER_MODULATION, icon="mdi:counter",
SENSOR_BURNER_STARTS, value_getter=lambda api: api.getStarts(),
SENSOR_DHW_GAS_CONSUMPTION_TODAY, ),
SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, ViCareSensorEntityDescription(
SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, key=SENSOR_BURNER_HOURS,
SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, name="Burner Hours",
SENSOR_GAS_CONSUMPTION_TODAY, icon="mdi:counter",
SENSOR_GAS_CONSUMPTION_THIS_WEEK, native_unit_of_measurement=TIME_HOURS,
SENSOR_GAS_CONSUMPTION_THIS_MONTH, value_getter=lambda api: api.getHours(),
SENSOR_GAS_CONSUMPTION_THIS_YEAR, ),
], ViCareSensorEntityDescription(
HeatingType.heatpump: [ key=SENSOR_BURNER_MODULATION,
SENSOR_COMPRESSOR_STARTS, name="Burner Modulation",
SENSOR_COMPRESSOR_HOURS, icon="mdi:percent",
SENSOR_COMPRESSOR_HOURS_LOADCLASS1, native_unit_of_measurement=PERCENTAGE,
SENSOR_COMPRESSOR_HOURS_LOADCLASS2, value_getter=lambda api: api.getModulation(),
SENSOR_COMPRESSOR_HOURS_LOADCLASS3, ),
SENSOR_COMPRESSOR_HOURS_LOADCLASS4, )
SENSOR_COMPRESSOR_HOURS_LOADCLASS5,
SENSOR_RETURN_TEMPERATURE, COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
], ViCareSensorEntityDescription(
HeatingType.fuelcell: [ key=SENSOR_COMPRESSOR_STARTS,
# gas name="Compressor Starts",
SENSOR_BOILER_TEMPERATURE, icon="mdi:counter",
SENSOR_BURNER_HOURS, value_getter=lambda api: api.getStarts(),
SENSOR_BURNER_MODULATION, ),
SENSOR_BURNER_STARTS, ViCareSensorEntityDescription(
SENSOR_DHW_GAS_CONSUMPTION_TODAY, key=SENSOR_COMPRESSOR_HOURS,
SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, name="Compressor Hours",
SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, icon="mdi:counter",
SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, native_unit_of_measurement=TIME_HOURS,
SENSOR_GAS_CONSUMPTION_TODAY, value_getter=lambda api: api.getHours(),
SENSOR_GAS_CONSUMPTION_THIS_WEEK, ),
SENSOR_GAS_CONSUMPTION_THIS_MONTH, ViCareSensorEntityDescription(
SENSOR_GAS_CONSUMPTION_THIS_YEAR, key=SENSOR_COMPRESSOR_HOURS_LOADCLASS1,
# fuel cell name="Compressor Hours Load Class 1",
SENSOR_POWER_PRODUCTION_CURRENT, icon="mdi:counter",
SENSOR_POWER_PRODUCTION_TODAY, native_unit_of_measurement=TIME_HOURS,
SENSOR_POWER_PRODUCTION_THIS_WEEK, value_getter=lambda api: api.getHoursLoadClass1(),
SENSOR_POWER_PRODUCTION_THIS_MONTH, ),
SENSOR_POWER_PRODUCTION_THIS_YEAR, 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.""" """Create the ViCare sensor devices."""
if discovery_info is None: if discovery_info is None:
return return
vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] name = hass.data[DOMAIN][VICARE_NAME]
heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] 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: for description in CIRCUIT_SENSORS:
sensors.extend(SENSORS_BY_HEATINGTYPE[heating_type]) for circuit in api.circuits:
suffix = ""
add_entities( if len(api.circuits) > 1:
[ suffix = f" {circuit.id}"
ViCareSensor(hass.data[VICARE_DOMAIN][VICARE_NAME], vicare_api, description) entity = await hass.async_add_executor_job(
for description in ( _build_entity,
*SENSOR_TYPES_GENERIC, f"{name} {description.name}{suffix}",
*SENSOR_TYPES_GAS, circuit,
*SENSOR_TYPES_HEATPUMP, hass.data[DOMAIN][VICARE_DEVICE_CONFIG],
*SENSOR_TYPES_FUELCELL, 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[ try:
ViCareSensorEntityDescription[Device], _entities_from_descriptions(
ViCareSensorEntityDescription[GazBoiler], hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors
ViCareSensorEntityDescription[HeatPump], )
ViCareSensorEntityDescription[FuelCell], except PyViCareNotSupportedFeatureError:
] _LOGGER.info("No compressors found")
async_add_entities(all_devices)
class ViCareSensor(SensorEntity): class ViCareSensor(SensorEntity):
"""Representation of a ViCare sensor.""" """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.""" """Initialize the sensor."""
self.entity_description = description self.entity_description = description
self._attr_name = f"{name} {description.name}" self._attr_name = name
self._api = api self._api = api
self._device_config = device_config
self._state = None 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 @property
def available(self): def available(self):
@ -386,16 +413,27 @@ class ViCareSensor(SensorEntity):
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique ID.""" """Return unique ID for this device."""
return f"{self._api.service.id}-{self.entity_description.key}" 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 @property
def native_value(self): def native_value(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._state return self._state
@property
def last_reset(self):
"""Return the time when the sensor was last reset."""
return self._last_reset
def update(self): def update(self):
"""Update state of sensor.""" """Update state of sensor."""
self._last_reset = dt_util.start_of_local_day()
try: try:
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
self._state = self.entity_description.value_getter(self._api) 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") _LOGGER.error("Unable to decode data from ViCare server")
except PyViCareRateLimitError as limit_exception: except PyViCareRateLimitError as limit_exception:
_LOGGER.error("Vicare API rate limit exceeded: %s", 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)

View file

@ -2,7 +2,11 @@
from contextlib import suppress from contextlib import suppress
import logging import logging
from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError from PyViCare.PyViCareUtils import (
PyViCareInvalidDataError,
PyViCareNotSupportedFeatureError,
PyViCareRateLimitError,
)
import requests import requests
from homeassistant.components.water_heater import ( 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 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__) _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.""" """Create the ViCare water_heater devices."""
if discovery_info is None: if discovery_info is None:
return return
vicare_api = hass.data[VICARE_DOMAIN][VICARE_API]
heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] name = hass.data[DOMAIN][VICARE_NAME]
add_entities(
[ all_devices = []
ViCareWater( for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]:
f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", suffix = ""
vicare_api, if len(hass.data[DOMAIN][VICARE_CIRCUITS]) > 1:
heating_type, 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): class ViCareWater(WaterHeaterEntity):
"""Representation of the ViCare domestic hot water device.""" """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.""" """Initialize the DHW water_heater device."""
self._name = name self._name = name
self._state = None self._state = None
self._api = api self._api = api
self._circuit = circuit
self._device_config = device_config
self._attributes = {} self._attributes = {}
self._target_temperature = None self._target_temperature = None
self._current_temperature = None self._current_temperature = None
@ -84,11 +117,11 @@ class ViCareWater(WaterHeaterEntity):
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
self._target_temperature = ( self._target_temperature = (
self._api.getDomesticHotWaterConfiguredTemperature() self._api.getDomesticHotWaterDesiredTemperature()
) )
with suppress(PyViCareNotSupportedFeatureError): with suppress(PyViCareNotSupportedFeatureError):
self._current_mode = self._api.getActiveMode() self._current_mode = self._circuit.getActiveMode()
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("Unable to retrieve data from ViCare server") _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) _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception)
except ValueError: except ValueError:
_LOGGER.error("Unable to decode data from ViCare server") _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 @property
def supported_features(self): def supported_features(self):

View file

@ -55,7 +55,7 @@ PyTransportNSW==0.1.1
PyTurboJPEG==1.6.1 PyTurboJPEG==1.6.1
# homeassistant.components.vicare # homeassistant.components.vicare
PyViCare==1.0.0 PyViCare==2.13.0
# homeassistant.components.xiaomi_aqara # homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.13.4 PyXiaomiGateway==0.13.4