Skip to content
This repository has been archived by the owner on Aug 15, 2024. It is now read-only.

avoid exceptions raising up to HA core #123

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 74 additions & 5 deletions custom_components/vicare/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@
from __future__ import annotations

from collections.abc import Callable
from contextlib import contextmanager
from dataclasses import dataclass
import logging
import typing

from PyViCare.PyViCare import PyViCare
from PyViCare.PyViCareDevice import Device

from PyViCare.PyViCareUtils import (
PyViCareInvalidDataError,
PyViCareRateLimitError,
PyViCareInvalidCredentialsError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.storage import STORAGE_DIR
import requests

from .const import (
CONF_HEATING_TYPE,
Expand All @@ -24,9 +33,44 @@
HeatingType,
)

if typing.TYPE_CHECKING:
from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig


_LOGGER = logging.getLogger(__name__)


class ViCareError(Exception):
"""A typed exception to identify errors raised from ViCare code"""


class ViCareEntity:
"""Abstract base entity class for ViCare entities"""

_logger: 'logging.Logger' # using the logger from the inheriting class
_device_config: 'PyViCareDeviceConfig'

@property
def device_info(self) -> DeviceInfo:
"""Return device info for this device."""
return DeviceInfo(
identifiers={(DOMAIN, self._device_config.getConfig().serial)},
name=self._device_config.getModel(),
manufacturer="Viessmann",
model=self._device_config.getModel(),
configuration_url="https://developer.viessmann.com/",
)

def _internal_update(self):
"""virtual abstract: override to actually do the update"""
raise NotImplementedError()

def update(self):
"""Let HA know there has been an update from the ViCare API."""
with managed_exceptions(self._logger):
self._internal_update()


@dataclass()
class ViCareRequiredKeysMixin:
"""Mixin for required keys."""
Expand All @@ -42,18 +86,43 @@ class ViCareRequiredKeysMixinWithSet:
value_setter: Callable[[Device], bool]


@contextmanager
def managed_exceptions(logger: 'logging.Logger'):
try:
yield
except requests.exceptions.ConnectionError:
logger.error("Unable to retrieve data from ViCare server")
except PyViCareRateLimitError as limit_exception:
logger.error("Vicare API rate limit exceeded: %s", limit_exception)
except PyViCareInvalidDataError as invalid_data_exception:
logger.error("Invalid data from Vicare server: %s", invalid_data_exception)
except ValueError:
logger.error("Unable to decode data from ViCare server")
except ViCareError as error:
logger.error("ViCare error: %s", str(error))
except Exception as error:
if logger.isEnabledFor(logging.DEBUG):
raise error
else:
logger.error("Unexpected %s: %s", type(error).__name__, str(error))


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from config entry."""
_LOGGER.debug("Setting up ViCare component")

hass.data[DOMAIN] = {}
hass.data[DOMAIN][entry.entry_id] = {}

await hass.async_add_executor_job(setup_vicare_api, hass, entry)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
try:
await hass.async_add_executor_job(setup_vicare_api, hass, entry)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
except PyViCareInvalidCredentialsError as auth_error:
raise ConfigEntryAuthFailed from auth_error
except Exception as error:
raise ConfigEntryNotReady from error

return True


def vicare_login(hass, entry_data):
Expand Down
72 changes: 23 additions & 49 deletions custom_components/vicare/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,17 @@
from dataclasses import dataclass
import logging

from PyViCare.PyViCareUtils import (
PyViCareInvalidDataError,
PyViCareNotSupportedFeatureError,
PyViCareRateLimitError,
)
import requests

from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import ViCareRequiredKeysMixin
from . import ViCareEntity, ViCareRequiredKeysMixin, managed_exceptions
from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -98,22 +91,22 @@ class ViCareBinarySensorEntityDescription(

def _build_entity(name, vicare_api, device_config, sensor):
"""Create a ViCare binary sensor entity."""
try:
sensor.value_getter(vicare_api)
_LOGGER.debug("Found entity %s", name)
except PyViCareNotSupportedFeatureError:
_LOGGER.info("Feature not supported %s", name)
return None
except AttributeError:
_LOGGER.debug("Attribute Error %s", name)
return None

return ViCareBinarySensor(
name,
vicare_api,
device_config,
sensor,
)
with managed_exceptions(_LOGGER):
try:
sensor.value_getter(vicare_api)
_LOGGER.debug("Found entity %s", name)
return ViCareBinarySensor(
name,
vicare_api,
device_config,
sensor,
)
except PyViCareNotSupportedFeatureError:
_LOGGER.info("Feature not supported %s", name)
return None
except AttributeError:
_LOGGER.debug("Attribute Error %s", name)
return None


async def _entities_from_descriptions(
Expand Down Expand Up @@ -182,8 +175,9 @@ async def async_setup_entry(
async_add_entities(entities)


class ViCareBinarySensor(BinarySensorEntity):
class ViCareBinarySensor(ViCareEntity, BinarySensorEntity):
"""Representation of a ViCare sensor."""
_logger = _LOGGER

entity_description: ViCareBinarySensorEntityDescription

Expand All @@ -198,17 +192,6 @@ def __init__(
self._device_config = device_config
self._state = None

@property
def device_info(self) -> DeviceInfo:
"""Return device info for this device."""
return DeviceInfo(
identifiers={(DOMAIN, self._device_config.getConfig().serial)},
name=self._device_config.getModel(),
manufacturer="Viessmann",
model=self._device_config.getModel(),
configuration_url="https://developer.viessmann.com/",
)

@property
def available(self):
"""Return True if entity is available."""
Expand All @@ -229,16 +212,7 @@ def is_on(self):
"""Return the state of the sensor."""
return self._state

def update(self):
def _internal_update(self):
"""Update state of sensor."""
try:
with suppress(PyViCareNotSupportedFeatureError):
self._state = self.entity_description.value_getter(self._api)
except requests.exceptions.ConnectionError:
_LOGGER.error("Unable to retrieve data from ViCare server")
except ValueError:
_LOGGER.error("Unable to decode data from ViCare server")
except PyViCareRateLimitError as limit_exception:
_LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception)
except PyViCareInvalidDataError as invalid_data_exception:
_LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception)
with suppress(PyViCareNotSupportedFeatureError):
self._state = self.entity_description.value_getter(self._api)
70 changes: 23 additions & 47 deletions custom_components/vicare/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,14 @@
from dataclasses import dataclass
import logging

from PyViCare.PyViCareUtils import (
PyViCareInvalidDataError,
PyViCareNotSupportedFeatureError,
PyViCareRateLimitError,
)
import requests

from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import ViCareRequiredKeysMixinWithSet
from . import ViCareEntity, ViCareRequiredKeysMixinWithSet, managed_exceptions
from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -47,23 +41,23 @@ class ViCareButtonEntityDescription(

def _build_entity(name, vicare_api, device_config, description):
"""Create a ViCare button entity."""
_LOGGER.debug("Found device %s", name)
try:
description.value_getter(vicare_api)
_LOGGER.debug("Found entity %s", name)
except PyViCareNotSupportedFeatureError:
_LOGGER.info("Feature not supported %s", name)
return None
except AttributeError:
_LOGGER.debug("Attribute Error %s", name)
return None

return ViCareButton(
name,
vicare_api,
device_config,
description,
)
with managed_exceptions(_LOGGER):
_LOGGER.debug("Found device %s", name)
try:
description.value_getter(vicare_api)
_LOGGER.debug("Found entity %s", name)
return ViCareButton(
name,
vicare_api,
device_config,
description,
)
except PyViCareNotSupportedFeatureError:
_LOGGER.info("Feature not supported %s", name)
return None
except AttributeError:
_LOGGER.debug("Attribute Error %s", name)
return None


async def async_setup_entry(
Expand Down Expand Up @@ -91,8 +85,9 @@ async def async_setup_entry(
async_add_entities(entities)


class ViCareButton(ButtonEntity):
class ViCareButton(ViCareEntity, ButtonEntity):
"""Representation of a ViCare button."""
_logger = _LOGGER

entity_description: ViCareButtonEntityDescription

Expand All @@ -106,28 +101,9 @@ def __init__(

def press(self) -> None:
"""Handle the button press."""
try:
with managed_exceptions(_LOGGER):
with suppress(PyViCareNotSupportedFeatureError):
self.entity_description.value_setter(self._api)
except requests.exceptions.ConnectionError:
_LOGGER.error("Unable to retrieve data from ViCare server")
except ValueError:
_LOGGER.error("Unable to decode data from ViCare server")
except PyViCareRateLimitError as limit_exception:
_LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception)
except PyViCareInvalidDataError as invalid_data_exception:
_LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception)

@property
def device_info(self) -> DeviceInfo:
"""Return device info for this device."""
return DeviceInfo(
identifiers={(DOMAIN, self._device_config.getConfig().serial)},
name=self._device_config.getModel(),
manufacturer="Viessmann",
model=self._device_config.getModel(),
configuration_url="https://developer.viessmann.com/",
)

@property
def unique_id(self) -> str:
Expand Down
Loading