Homee sensor (#135447)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Markus Adrario 2025-01-21 14:02:42 +00:00 committed by GitHub
parent 032940f1a9
commit fb96ef99d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 525 additions and 8 deletions

View File

@ -14,7 +14,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.COVER] PLATFORMS = [Platform.COVER, Platform.SENSOR]
type HomeeConfigEntry = ConfigEntry[Homee] type HomeeConfigEntry = ConfigEntry[Homee]

View File

@ -1,4 +1,60 @@
"""Constants for the homee integration.""" """Constants for the homee integration."""
from homeassistant.const import (
LIGHT_LUX,
PERCENTAGE,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
)
# General # General
DOMAIN = "homee" DOMAIN = "homee"
# Sensor mappings
HOMEE_UNIT_TO_HA_UNIT = {
"": None,
"n/a": None,
"text": None,
"%": PERCENTAGE,
"lx": LIGHT_LUX,
"klx": LIGHT_LUX,
"A": UnitOfElectricCurrent.AMPERE,
"V": UnitOfElectricPotential.VOLT,
"kWh": UnitOfEnergy.KILO_WATT_HOUR,
"W": UnitOfPower.WATT,
"m/s": UnitOfSpeed.METERS_PER_SECOND,
"km/h": UnitOfSpeed.KILOMETERS_PER_HOUR,
"°F": UnitOfTemperature.FAHRENHEIT,
"°C": UnitOfTemperature.CELSIUS,
"K": UnitOfTemperature.KELVIN,
"s": UnitOfTime.SECONDS,
"min": UnitOfTime.MINUTES,
"h": UnitOfTime.HOURS,
"L": UnitOfVolume.LITERS,
}
OPEN_CLOSE_MAP = {
0.0: "open",
1.0: "closed",
2.0: "partial",
3.0: "opening",
4.0: "closing",
}
OPEN_CLOSE_MAP_REVERSED = {
0.0: "closed",
1.0: "open",
2.0: "partial",
3.0: "cosing",
4.0: "opening",
}
WINDOW_MAP = {
0.0: "closed",
1.0: "open",
2.0: "tilted",
}
WINDOW_MAP_REVERSED = {0.0: "open", 1.0: "closed", 2.0: "tilted"}

View File

@ -1,6 +1,6 @@
"""Base Entities for Homee integration.""" """Base Entities for Homee integration."""
from pyHomee.const import AttributeType, NodeProfile, NodeState from pyHomee.const import AttributeState, AttributeType, NodeProfile, NodeState
from pyHomee.model import HomeeAttribute, HomeeNode from pyHomee.model import HomeeAttribute, HomeeNode
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@ -11,6 +11,56 @@ from .const import DOMAIN
from .helpers import get_name_for_enum from .helpers import get_name_for_enum
class HomeeEntity(Entity):
"""Represents a Homee entity consisting of a single HomeeAttribute."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, attribute: HomeeAttribute, entry: HomeeConfigEntry) -> None:
"""Initialize the wrapper using a HomeeAttribute and target entity."""
self._attribute = attribute
self._attr_unique_id = (
f"{entry.runtime_data.settings.uid}-{attribute.node_id}-{attribute.id}"
)
self._entry = entry
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
}
)
self._host_connected = entry.runtime_data.connected
async def async_added_to_hass(self) -> None:
"""Add the homee attribute entity to home assistant."""
self.async_on_remove(
self._attribute.add_on_changed_listener(self._on_node_updated)
)
self.async_on_remove(
await self._entry.runtime_data.add_connection_listener(
self._on_connection_changed
)
)
@property
def available(self) -> bool:
"""Return the availability of the underlying node."""
return (self._attribute.state == AttributeState.NORMAL) and self._host_connected
async def async_update(self) -> None:
"""Update entity from homee."""
homee = self._entry.runtime_data
await homee.update_attribute(self._attribute.node_id, self._attribute.id)
def _on_node_updated(self, attribute: HomeeAttribute) -> None:
self.schedule_update_ha_state()
async def _on_connection_changed(self, connected: bool) -> None:
self._host_connected = connected
self.schedule_update_ha_state()
class HomeeNodeEntity(Entity): class HomeeNodeEntity(Entity):
"""Representation of an Entity that uses more than one HomeeAttribute.""" """Representation of an Entity that uses more than one HomeeAttribute."""
@ -20,7 +70,7 @@ class HomeeNodeEntity(Entity):
def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None: def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None:
"""Initialize the wrapper using a HomeeNode and target entity.""" """Initialize the wrapper using a HomeeNode and target entity."""
self._node = node self._node = node
self._attr_unique_id = f"{entry.runtime_data.settings.uid}-{node.id}" self._attr_unique_id = f"{entry.unique_id}-{node.id}"
self._entry = entry self._entry = entry
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
@ -41,6 +91,23 @@ class HomeeNodeEntity(Entity):
) )
) )
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
# Homee hub has id -1, but is identified only by the UID.
if self._node.id == -1:
return DeviceInfo(
identifiers={(DOMAIN, self._entry.runtime_data.settings.uid)},
)
return DeviceInfo(
identifiers={(DOMAIN, f"{self._entry.unique_id}-{self._node.id}")},
name=self._node.name,
model=get_name_for_enum(NodeProfile, self._node.profile),
sw_version=self._get_software_version(),
via_device=(DOMAIN, self._entry.runtime_data.settings.uid),
)
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return the availability of the underlying node.""" """Return the availability of the underlying node."""

View File

@ -1,16 +1,16 @@
"""Helper functions for the homee custom component.""" """Helper functions for the homee custom component."""
from enum import IntEnum
import logging import logging
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def get_name_for_enum(att_class, att_id) -> str: def get_name_for_enum(att_class: type[IntEnum], att_id: int) -> str | None:
"""Return the enum item name for a given integer.""" """Return the enum item name for a given integer."""
try: try:
attribute_name = att_class(att_id).name item = att_class(att_id)
except ValueError: except ValueError:
_LOGGER.warning("Value %s does not exist in %s", att_id, att_class.__name__) _LOGGER.warning("Value %s does not exist in %s", att_id, att_class.__name__)
return "Unknown" return None
return item.name.lower()
return attribute_name

View File

@ -0,0 +1,12 @@
{
"entity": {
"sensor": {
"link_quality": {
"default": "mdi:signal"
},
"window_position": {
"default": "mdi:window-closed"
}
}
}
}

View File

@ -0,0 +1,303 @@
"""The homee sensor platform."""
from collections.abc import Callable
from dataclasses import dataclass
from pyHomee.const import AttributeType, NodeState
from pyHomee.model import HomeeAttribute, HomeeNode
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeeConfigEntry
from .const import (
HOMEE_UNIT_TO_HA_UNIT,
OPEN_CLOSE_MAP,
OPEN_CLOSE_MAP_REVERSED,
WINDOW_MAP,
WINDOW_MAP_REVERSED,
)
from .entity import HomeeEntity, HomeeNodeEntity
from .helpers import get_name_for_enum
def get_open_close_value(attribute: HomeeAttribute) -> str | None:
"""Return the open/close value."""
vals = OPEN_CLOSE_MAP if not attribute.is_reversed else OPEN_CLOSE_MAP_REVERSED
return vals.get(attribute.current_value)
def get_window_value(attribute: HomeeAttribute) -> str | None:
"""Return the states of a window open sensor."""
vals = WINDOW_MAP if not attribute.is_reversed else WINDOW_MAP_REVERSED
return vals.get(attribute.current_value)
@dataclass(frozen=True, kw_only=True)
class HomeeSensorEntityDescription(SensorEntityDescription):
"""A class that describes Homee sensor entities."""
value_fn: Callable[[HomeeAttribute], str | float | None] = (
lambda value: value.current_value
)
native_unit_of_measurement_fn: Callable[[str], str | None] = (
lambda homee_unit: HOMEE_UNIT_TO_HA_UNIT[homee_unit]
)
SENSOR_DESCRIPTIONS: dict[AttributeType, HomeeSensorEntityDescription] = {
AttributeType.ACCUMULATED_ENERGY_USE: HomeeSensorEntityDescription(
key="energy",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
AttributeType.BATTERY_LEVEL: HomeeSensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.BRIGHTNESS: HomeeSensorEntityDescription(
key="brightness",
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=(
lambda attribute: attribute.current_value * 1000
if attribute.unit == "klx"
else attribute.current_value
),
),
AttributeType.CURRENT: HomeeSensorEntityDescription(
key="current",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.CURRENT_ENERGY_USE: HomeeSensorEntityDescription(
key="power",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.CURRENT_VALVE_POSITION: HomeeSensorEntityDescription(
key="valve_position",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.DAWN: HomeeSensorEntityDescription(
key="dawn",
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.DEVICE_TEMPERATURE: HomeeSensorEntityDescription(
key="device_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.LEVEL: HomeeSensorEntityDescription(
key="level",
device_class=SensorDeviceClass.VOLUME,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.LINK_QUALITY: HomeeSensorEntityDescription(
key="link_quality",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.POSITION: HomeeSensorEntityDescription(
key="position",
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.RAIN_FALL_LAST_HOUR: HomeeSensorEntityDescription(
key="rainfall_hour",
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.RAIN_FALL_TODAY: HomeeSensorEntityDescription(
key="rainfall_day",
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.RELATIVE_HUMIDITY: HomeeSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.TEMPERATURE: HomeeSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.TOTAL_ACCUMULATED_ENERGY_USE: HomeeSensorEntityDescription(
key="total_energy",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
AttributeType.TOTAL_CURRENT: HomeeSensorEntityDescription(
key="total_current",
device_class=SensorDeviceClass.CURRENT,
),
AttributeType.TOTAL_CURRENT_ENERGY_USE: HomeeSensorEntityDescription(
key="total_power",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.TOTAL_VOLTAGE: HomeeSensorEntityDescription(
key="total_voltage",
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.UP_DOWN: HomeeSensorEntityDescription(
key="up_down",
device_class=SensorDeviceClass.ENUM,
options=[
"open",
"closed",
"partial",
"opening",
"closing",
],
value_fn=get_open_close_value,
),
AttributeType.UV: HomeeSensorEntityDescription(
key="uv",
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.VOLTAGE: HomeeSensorEntityDescription(
key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.WIND_SPEED: HomeeSensorEntityDescription(
key="wind_speed",
device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.WINDOW_POSITION: HomeeSensorEntityDescription(
key="window_position",
device_class=SensorDeviceClass.ENUM,
options=["closed", "open", "tilted"],
value_fn=get_window_value,
),
}
@dataclass(frozen=True, kw_only=True)
class HomeeNodeSensorEntityDescription(SensorEntityDescription):
"""Describes Homee node sensor entities."""
value_fn: Callable[[HomeeNode], str | None]
NODE_SENSOR_DESCRIPTIONS: tuple[HomeeNodeSensorEntityDescription, ...] = (
HomeeNodeSensorEntityDescription(
key="state",
device_class=SensorDeviceClass.ENUM,
options=[
"available",
"unavailable",
"update_in_progress",
"waiting_for_attributes",
"initializing",
"user_interaction_required",
"password_required",
"host_unavailable",
"delete_in_progress",
"cosi_connected",
"blocked",
"waiting_for_wakeup",
"remote_node_deleted",
"firmware_update_in_progress",
],
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda node: get_name_for_enum(NodeState, node.state),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_devices: AddEntitiesCallback,
) -> None:
"""Add the homee platform for the sensor components."""
devices: list[HomeeSensor | HomeeNodeSensor] = []
for node in config_entry.runtime_data.nodes:
# Node properties that are sensors.
devices.extend(
HomeeNodeSensor(node, config_entry, description)
for description in NODE_SENSOR_DESCRIPTIONS
)
# Node attributes that are sensors.
devices.extend(
HomeeSensor(attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type])
for attribute in node.attributes
if attribute.type in SENSOR_DESCRIPTIONS and not attribute.editable
)
if devices:
async_add_devices(devices)
class HomeeSensor(HomeeEntity, SensorEntity):
"""Representation of a homee sensor."""
entity_description: HomeeSensorEntityDescription
def __init__(
self,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: HomeeSensorEntityDescription,
) -> None:
"""Initialize a homee sensor entity."""
super().__init__(attribute, entry)
self.entity_description = description
self._attr_translation_key = description.key
if attribute.instance > 0:
self._attr_translation_key = f"{description.translation_key}_instance"
self._attr_translation_placeholders = {"instance": str(attribute.instance)}
@property
def native_value(self) -> float | str | None:
"""Return the native value of the sensor."""
return self.entity_description.value_fn(self._attribute)
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the native unit of the sensor."""
return self.entity_description.native_unit_of_measurement_fn(
self._attribute.unit
)
class HomeeNodeSensor(HomeeNodeEntity, SensorEntity):
"""Represents a sensor based on a node's property."""
entity_description: HomeeNodeSensorEntityDescription
def __init__(
self,
node: HomeeNode,
entry: HomeeConfigEntry,
description: HomeeNodeSensorEntityDescription,
) -> None:
"""Initialize a homee node sensor entity."""
super().__init__(node, entry)
self.entity_description = description
self._attr_translation_key = f"node_{description.key}"
self._node = node
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
@property
def native_value(self) -> str | None:
"""Return the sensors value."""
return self.entity_description.value_fn(self._node)

View File

@ -24,5 +24,84 @@
} }
} }
} }
},
"entity": {
"sensor": {
"brightness_instance": {
"name": "Illuminance {instance}"
},
"current_instance": {
"name": "Current {instance}"
},
"dawn": {
"name": "Dawn"
},
"device_temperature": {
"name": "Device temperature"
},
"energy_instance": {
"name": "Energy {instance}"
},
"level": {
"name": "Level"
},
"link_quality": {
"name": "Link quality"
},
"node_state": {
"name": "Node state"
},
"position": {
"name": "Position"
},
"power_instance": {
"name": "Power {instance}"
},
"rainfall_hour": {
"name": "Rainfall last hour"
},
"rainfall_day": {
"name": "Rainfall today"
},
"total_current": {
"name": "Total current"
},
"total_energy": {
"name": "Total energy"
},
"total_power": {
"name": "Total power"
},
"total_voltage": {
"name": "Total voltage"
},
"up_down": {
"name": "State",
"state": {
"open": "[%key:common::state::open%]",
"closed": "[%key:common::state::closed%]",
"partial": "Partially open",
"opening": "Opening",
"closing": "Closing"
}
},
"uv": {
"name": "Ultraviolet"
},
"valve_position": {
"name": "Valve position"
},
"voltage_instance": {
"name": "Voltage {instance}"
},
"window_position": {
"name": "Window position",
"state": {
"closed": "[%key:common::state::closed%]",
"open": "[%key:common::state::open%]",
"tilted": "Tilted"
}
}
}
} }
} }