Add Homee valve platform (#139188)

This commit is contained in:
Markus Adrario 2025-02-25 12:06:24 +01:00 committed by GitHub
parent d197acc069
commit 661b55d6eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 310 additions and 14 deletions

View File

@ -20,6 +20,7 @@ PLATFORMS = [
Platform.LIGHT, Platform.LIGHT,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
Platform.VALVE,
] ]
type HomeeConfigEntry = ConfigEntry[Homee] type HomeeConfigEntry = ConfigEntry[Homee]

View File

@ -205,6 +205,11 @@
"watchdog": { "watchdog": {
"name": "Watchdog" "name": "Watchdog"
} }
},
"valve": {
"valve_position": {
"name": "Valve position"
}
} }
}, },
"exceptions": { "exceptions": {

View File

@ -0,0 +1,81 @@
"""The Homee valve platform."""
from pyHomee.const import AttributeType
from pyHomee.model import HomeeAttribute
from homeassistant.components.valve import (
ValveDeviceClass,
ValveEntity,
ValveEntityDescription,
ValveEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeeConfigEntry
from .entity import HomeeEntity
VALVE_DESCRIPTIONS = {
AttributeType.CURRENT_VALVE_POSITION: ValveEntityDescription(
key="valve_position",
device_class=ValveDeviceClass.WATER,
)
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the Homee platform for the valve component."""
async_add_entities(
HomeeValve(attribute, config_entry, VALVE_DESCRIPTIONS[attribute.type])
for node in config_entry.runtime_data.nodes
for attribute in node.attributes
if attribute.type in VALVE_DESCRIPTIONS
)
class HomeeValve(HomeeEntity, ValveEntity):
"""Representation of a Homee valve."""
_attr_reports_position = True
def __init__(
self,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: ValveEntityDescription,
) -> None:
"""Initialize a Homee valve entity."""
super().__init__(attribute, entry)
self.entity_description = description
self._attr_translation_key = description.key
@property
def supported_features(self) -> ValveEntityFeature:
"""Return the supported features."""
if self._attribute.editable:
return ValveEntityFeature.SET_POSITION
return ValveEntityFeature(0)
@property
def current_valve_position(self) -> int | None:
"""Return the current valve position."""
return int(self._attribute.current_value)
@property
def is_closing(self) -> bool:
"""Return if the valve is closing."""
return self._attribute.target_value < self._attribute.current_value
@property
def is_opening(self) -> bool:
"""Return if the valve is opening."""
return self._attribute.target_value > self._attribute.current_value
async def async_set_valve_position(self, position: int) -> None:
"""Move the valve to a specific position."""
await self.async_set_value(position)

View File

@ -0,0 +1,51 @@
{
"id": 1,
"name": "Test Valve",
"profile": 3011,
"image": "nodeicon_valve",
"favorite": 0,
"order": 27,
"protocol": 3,
"routing": 0,
"state": 1,
"state_changed": 1736188706,
"added": 1610308228,
"history": 1,
"cube_type": 3,
"note": "",
"services": 7,
"phonetic_name": "",
"owner": 2,
"security": 0,
"attributes": [
{
"id": 1,
"node_id": 1,
"instance": 0,
"minimum": 0,
"maximum": 100,
"current_value": 0.0,
"target_value": 0.0,
"last_value": 0.0,
"unit": "%",
"step_value": 1.0,
"editable": 1,
"type": 18,
"state": 1,
"last_changed": 1711796633,
"changed_by": 1,
"changed_by_id": 0,
"based_on": 1,
"data": "",
"name": "",
"options": {
"automations": ["step"],
"history": {
"day": 1,
"week": 26,
"month": 6
}
}
}
]
}

View File

@ -0,0 +1,51 @@
# serializer version: 1
# name: test_valve_snapshot[valve.test_valve_valve_position-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'valve',
'entity_category': None,
'entity_id': 'valve.test_valve_valve_position',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <ValveDeviceClass.WATER: 'water'>,
'original_icon': None,
'original_name': 'Valve position',
'platform': 'homee',
'previous_unique_id': None,
'supported_features': <ValveEntityFeature: 4>,
'translation_key': 'valve_position',
'unique_id': '00055511EECC-1-1',
'unit_of_measurement': None,
})
# ---
# name: test_valve_snapshot[valve.test_valve_valve_position-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_position': 0,
'device_class': 'water',
'friendly_name': 'Test Valve Valve position',
'supported_features': <ValveEntityFeature: 4>,
}),
'context': <ANY>,
'entity_id': 'valve.test_valve_valve_position',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'closed',
})
# ---

View File

@ -1,9 +1,8 @@
"""Test homee sensors.""" """Test homee sensors."""
from datetime import timedelta from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.homee.const import ( from homeassistant.components.homee.const import (
@ -12,13 +11,18 @@ from homeassistant.components.homee.const import (
WINDOW_MAP, WINDOW_MAP,
WINDOW_MAP_REVERSED, WINDOW_MAP_REVERSED,
) )
from homeassistant.const import LIGHT_LUX from homeassistant.const import LIGHT_LUX, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from . import async_update_attribute_value, build_mock_node, setup_integration from . import async_update_attribute_value, build_mock_node, setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture(autouse=True)
def enable_all_entities(entity_registry_enabled_by_default: None) -> None:
"""Make sure all entities are enabled."""
async def test_up_down_values( async def test_up_down_values(
@ -110,19 +114,12 @@ async def test_sensor_snapshot(
mock_homee: MagicMock, mock_homee: MagicMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test the multisensor snapshot.""" """Test the multisensor snapshot."""
mock_homee.nodes = [build_mock_node("sensors.json")] mock_homee.nodes = [build_mock_node("sensors.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0] mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
await setup_integration(hass, mock_config_entry) with patch("homeassistant.components.homee.PLATFORMS", [Platform.SENSOR]):
entity_registry.async_update_entity( await setup_integration(hass, mock_config_entry)
"sensor.test_multisensor_node_state", disabled_by=None
)
await hass.async_block_till_done()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)

View File

@ -0,0 +1,110 @@
"""Test Homee valves."""
from unittest.mock import MagicMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.valve import (
DOMAIN as VALVE_DOMAIN,
SERVICE_SET_VALVE_POSITION,
STATE_CLOSED,
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
ValveEntityFeature,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import build_mock_node, setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_valve_set_position(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test set valve position service."""
mock_homee.nodes = [build_mock_node("valve.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
VALVE_DOMAIN,
SERVICE_SET_VALVE_POSITION,
{ATTR_ENTITY_ID: "valve.test_valve_valve_position", "position": 100},
)
mock_homee.set_value.assert_called_once_with(1, 1, 100)
@pytest.mark.parametrize(
("current_value", "target_value", "state"),
[
(0.0, 0.0, STATE_CLOSED),
(0.0, 100.0, STATE_OPENING),
(100.0, 0.0, STATE_CLOSING),
(100.0, 100.0, STATE_OPEN),
],
)
async def test_opening_closing(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
current_value: float,
target_value: float,
state: str,
) -> None:
"""Test if opening/closing is detected correctly."""
mock_homee.nodes = [build_mock_node("valve.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
await setup_integration(hass, mock_config_entry)
valve = mock_homee.nodes[0].attributes[0]
valve.current_value = current_value
valve.target_value = target_value
valve.add_on_changed_listener.call_args_list[0][0][0](valve)
await hass.async_block_till_done()
assert hass.states.get("valve.test_valve_valve_position").state == state
async def test_supported_features(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test supported features."""
mock_homee.nodes = [build_mock_node("valve.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
await setup_integration(hass, mock_config_entry)
valve = mock_homee.nodes[0].attributes[0]
attributes = hass.states.get("valve.test_valve_valve_position").attributes
assert attributes["supported_features"] == ValveEntityFeature.SET_POSITION
valve.editable = 0
valve.add_on_changed_listener.call_args_list[0][0][0](valve)
await hass.async_block_till_done()
attributes = hass.states.get("valve.test_valve_valve_position").attributes
assert attributes["supported_features"] == ValveEntityFeature(0)
async def test_valve_snapshot(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the valve snapshots."""
mock_homee.nodes = [build_mock_node("valve.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
with patch("homeassistant.components.homee.PLATFORMS", [Platform.VALVE]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)