Files
core/homeassistant/components/velbus/entity.py
T

111 lines
3.8 KiB
Python

"""Support for Velbus devices."""
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import TYPE_CHECKING, Any, Concatenate, override
from velbusaio.channels import Channel as VelbusChannel
from velbusaio.properties import Property as VelbusProperty
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
# device identifiers for modules
# (DOMAIN, module_address)
# device identifiers for channels that are subdevices of a module
# (DOMAIN, f"{module_address}-{channel_number}")
class VelbusEntity(Entity):
"""Representation of a Velbus entity."""
_attr_has_entity_name = True
_attr_should_poll: bool = False
def __init__(self, channel: VelbusChannel | VelbusProperty) -> None:
"""Initialize a Velbus entity."""
self._channel = channel
self._module_address = str(channel.get_module_address())
self._attr_name = channel.get_name()
serial = channel.get_module_serial() or self._module_address
self._attr_unique_id = f"{serial}-{channel.get_channel_number()}"
def _get_identifier(self) -> str:
"""Return the identifier of the entity."""
if not self._channel.is_sub_device():
return self._module_address
return f"{self._module_address}-{self._channel.get_channel_number()}"
@property
@override
def device_info(self) -> DeviceInfo:
"""Return device info, linking a sub-device to its module device."""
channel = self._channel
device_info = DeviceInfo(
identifiers={(DOMAIN, self._get_identifier())},
manufacturer="Velleman",
model=channel.get_module_type_name(),
model_id=str(channel.get_module_type()),
name=channel.get_full_name(),
sw_version=channel.get_module_sw_version(),
serial_number=channel.get_module_serial(),
)
if channel.is_sub_device():
config_entry = self.platform.config_entry
if TYPE_CHECKING:
assert config_entry
device_info["via_device_id"] = dr.async_get_device_id_by_identifier(
self.hass,
(DOMAIN, self._module_address),
config_entry_id=config_entry.entry_id,
)
return device_info
@override
async def async_added_to_hass(self) -> None:
"""Add listener for state changes."""
self._channel.on_status_update(self._on_update)
@override
async def async_will_remove_from_hass(self) -> None:
"""Remove listener for state changes."""
self._channel.remove_on_status_update(self._on_update)
async def _on_update(self) -> None:
"""Handle status updates from the channel."""
self.async_write_ha_state()
@property
@override
def available(self) -> bool:
"""Return if entity is available."""
return self._channel.is_connected()
def api_call[_T: VelbusEntity, **_P](
func: Callable[Concatenate[_T, _P], Awaitable[None]],
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
"""Catch command exceptions."""
@wraps(func)
async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
"""Wrap all command methods."""
try:
await func(self, *args, **kwargs)
except OSError as exc:
entity_name = self.name if isinstance(self.name, str) else "Unknown"
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_call_failed",
translation_placeholders={
"entity": entity_name,
},
) from exc
return cmd_wrapper