Add Homee wind_monitoring_state to numbers (#139848)

This commit is contained in:
Markus Adrario 2025-04-21 21:21:15 +02:00 committed by GitHub
parent 80f34620c8
commit f0cf620854
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 225 additions and 78 deletions

View File

@ -1,5 +1,8 @@
"""The Homee number platform."""
from collections.abc import Callable
from dataclasses import dataclass
from pyHomee.const import AttributeType
from pyHomee.model import HomeeAttribute
@ -8,7 +11,7 @@ from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.const import EntityCategory, UnitOfSpeed
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@ -18,69 +21,89 @@ from .entity import HomeeEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class HomeeNumberEntityDescription(NumberEntityDescription):
"""A class that describes Homee number entities."""
native_value_fn: Callable[[float], float] = lambda value: value
set_native_value_fn: Callable[[float], float] = lambda value: value
NUMBER_DESCRIPTIONS = {
AttributeType.DOWN_POSITION: NumberEntityDescription(
AttributeType.DOWN_POSITION: HomeeNumberEntityDescription(
key="down_position",
entity_category=EntityCategory.CONFIG,
),
AttributeType.DOWN_SLAT_POSITION: NumberEntityDescription(
AttributeType.DOWN_SLAT_POSITION: HomeeNumberEntityDescription(
key="down_slat_position",
entity_category=EntityCategory.CONFIG,
),
AttributeType.DOWN_TIME: NumberEntityDescription(
AttributeType.DOWN_TIME: HomeeNumberEntityDescription(
key="down_time",
device_class=NumberDeviceClass.DURATION,
entity_category=EntityCategory.CONFIG,
),
AttributeType.ENDPOSITION_CONFIGURATION: NumberEntityDescription(
AttributeType.ENDPOSITION_CONFIGURATION: HomeeNumberEntityDescription(
key="endposition_configuration",
entity_category=EntityCategory.CONFIG,
),
AttributeType.MOTION_ALARM_CANCELATION_DELAY: NumberEntityDescription(
AttributeType.MOTION_ALARM_CANCELATION_DELAY: HomeeNumberEntityDescription(
key="motion_alarm_cancelation_delay",
device_class=NumberDeviceClass.DURATION,
entity_category=EntityCategory.CONFIG,
),
AttributeType.OPEN_WINDOW_DETECTION_SENSIBILITY: NumberEntityDescription(
AttributeType.OPEN_WINDOW_DETECTION_SENSIBILITY: HomeeNumberEntityDescription(
key="open_window_detection_sensibility",
entity_category=EntityCategory.CONFIG,
),
AttributeType.POLLING_INTERVAL: NumberEntityDescription(
AttributeType.POLLING_INTERVAL: HomeeNumberEntityDescription(
key="polling_interval",
device_class=NumberDeviceClass.DURATION,
entity_category=EntityCategory.CONFIG,
),
AttributeType.SHUTTER_SLAT_TIME: NumberEntityDescription(
AttributeType.SHUTTER_SLAT_TIME: HomeeNumberEntityDescription(
key="shutter_slat_time",
device_class=NumberDeviceClass.DURATION,
entity_category=EntityCategory.CONFIG,
),
AttributeType.SLAT_MAX_ANGLE: NumberEntityDescription(
AttributeType.SLAT_MAX_ANGLE: HomeeNumberEntityDescription(
key="slat_max_angle",
entity_category=EntityCategory.CONFIG,
),
AttributeType.SLAT_MIN_ANGLE: NumberEntityDescription(
AttributeType.SLAT_MIN_ANGLE: HomeeNumberEntityDescription(
key="slat_min_angle",
entity_category=EntityCategory.CONFIG,
),
AttributeType.SLAT_STEPS: NumberEntityDescription(
AttributeType.SLAT_STEPS: HomeeNumberEntityDescription(
key="slat_steps",
entity_category=EntityCategory.CONFIG,
),
AttributeType.TEMPERATURE_OFFSET: NumberEntityDescription(
AttributeType.TEMPERATURE_OFFSET: HomeeNumberEntityDescription(
key="temperature_offset",
entity_category=EntityCategory.CONFIG,
),
AttributeType.UP_TIME: NumberEntityDescription(
AttributeType.UP_TIME: HomeeNumberEntityDescription(
key="up_time",
device_class=NumberDeviceClass.DURATION,
entity_category=EntityCategory.CONFIG,
),
AttributeType.WAKE_UP_INTERVAL: NumberEntityDescription(
AttributeType.WAKE_UP_INTERVAL: HomeeNumberEntityDescription(
key="wake_up_interval",
device_class=NumberDeviceClass.DURATION,
entity_category=EntityCategory.CONFIG,
),
AttributeType.WIND_MONITORING_STATE: HomeeNumberEntityDescription(
key="wind_monitoring_state",
device_class=NumberDeviceClass.WIND_SPEED,
entity_category=EntityCategory.CONFIG,
native_min_value=0,
native_max_value=22.5,
native_step=2.5,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
native_value_fn=lambda value: value * 2.5,
set_native_value_fn=lambda value: value / 2.5,
),
}
@ -102,20 +125,25 @@ async def async_setup_entry(
class HomeeNumber(HomeeEntity, NumberEntity):
"""Representation of a Homee number."""
entity_description: HomeeNumberEntityDescription
def __init__(
self,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: NumberEntityDescription,
description: HomeeNumberEntityDescription,
) -> None:
"""Initialize a Homee number entity."""
super().__init__(attribute, entry)
self.entity_description = description
self._attr_translation_key = description.key
self._attr_native_unit_of_measurement = HOMEE_UNIT_TO_HA_UNIT[attribute.unit]
self._attr_native_min_value = attribute.minimum
self._attr_native_max_value = attribute.maximum
self._attr_native_step = attribute.step_value
self._attr_native_unit_of_measurement = (
description.native_unit_of_measurement
or HOMEE_UNIT_TO_HA_UNIT[attribute.unit]
)
self._attr_native_min_value = description.native_min_value or attribute.minimum
self._attr_native_max_value = description.native_max_value or attribute.maximum
self._attr_native_step = description.native_step or attribute.step_value
@property
def available(self) -> bool:
@ -123,10 +151,12 @@ class HomeeNumber(HomeeEntity, NumberEntity):
return super().available and self._attribute.editable
@property
def native_value(self) -> int:
def native_value(self) -> float | None:
"""Return the native value of the number."""
return int(self._attribute.current_value)
return self.entity_description.native_value_fn(self._attribute.current_value)
async def async_set_native_value(self, value: float) -> None:
"""Set the selected value."""
await self.async_set_homee_value(value)
await self.async_set_homee_value(
self.entity_description.set_native_value_fn(value)
)

View File

@ -189,6 +189,9 @@
},
"wake_up_interval": {
"name": "Wake-up interval"
},
"wind_monitoring_state": {
"name": "Threshold for wind trigger"
}
},
"select": {

View File

@ -19,7 +19,7 @@
"security": 0,
"attributes": [
{
"id": 2,
"id": 1,
"node_id": 1,
"instance": 0,
"minimum": 0,
@ -40,7 +40,7 @@
"name": ""
},
{
"id": 3,
"id": 2,
"node_id": 1,
"instance": 0,
"minimum": -75,
@ -61,7 +61,7 @@
"name": ""
},
{
"id": 4,
"id": 3,
"node_id": 1,
"instance": 0,
"minimum": 4,
@ -82,7 +82,7 @@
"name": ""
},
{
"id": 5,
"id": 4,
"node_id": 1,
"instance": 0,
"minimum": 0,
@ -103,7 +103,7 @@
"name": ""
},
{
"id": 6,
"id": 5,
"node_id": 1,
"instance": 0,
"minimum": 1,
@ -124,7 +124,7 @@
"name": ""
},
{
"id": 7,
"id": 6,
"node_id": 1,
"instance": 0,
"minimum": 0,
@ -145,7 +145,7 @@
"name": ""
},
{
"id": 8,
"id": 7,
"node_id": 1,
"instance": 0,
"minimum": 5,
@ -166,7 +166,7 @@
"name": ""
},
{
"id": 9,
"id": 8,
"node_id": 1,
"instance": 0,
"minimum": 0,
@ -187,7 +187,7 @@
"name": ""
},
{
"id": 10,
"id": 9,
"node_id": 1,
"instance": 0,
"minimum": -127,
@ -208,7 +208,7 @@
"name": ""
},
{
"id": 11,
"id": 10,
"node_id": 1,
"instance": 0,
"minimum": -127,
@ -229,7 +229,7 @@
"name": ""
},
{
"id": 12,
"id": 11,
"node_id": 1,
"instance": 0,
"minimum": 1,
@ -250,7 +250,7 @@
"name": ""
},
{
"id": 13,
"id": 12,
"node_id": 1,
"instance": 0,
"minimum": -5,
@ -271,7 +271,7 @@
"name": ""
},
{
"id": 14,
"id": 13,
"node_id": 1,
"instance": 0,
"minimum": 4,
@ -292,7 +292,7 @@
"name": ""
},
{
"id": 15,
"id": 14,
"node_id": 1,
"instance": 0,
"minimum": 30,
@ -313,7 +313,7 @@
"name": ""
},
{
"id": 16,
"id": 15,
"node_id": 1,
"instance": 0,
"minimum": 0,
@ -332,6 +332,27 @@
"based_on": 1,
"data": "fixed_value",
"name": ""
},
{
"id": 16,
"node_id": 1,
"instance": 0,
"minimum": 0,
"maximum": 9,
"current_value": 2.0,
"target_value": 2.0,
"last_value": 2.0,
"unit": "n/a",
"step_value": 1.0,
"editable": 1,
"type": 338,
"state": 1,
"last_changed": 1684668852,
"changed_by": 1,
"changed_by_id": 0,
"based_on": 1,
"data": "",
"name": ""
}
]
}

View File

@ -34,7 +34,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'down_time',
'unique_id': '00055511EECC-1-4',
'unique_id': '00055511EECC-1-3',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
@ -54,7 +54,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '57',
'state': '57.0',
})
# ---
# name: test_number_snapshot[number.test_number_down_position-entry]
@ -92,7 +92,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'down_position',
'unique_id': '00055511EECC-1-2',
'unique_id': '00055511EECC-1-1',
'unit_of_measurement': '%',
})
# ---
@ -111,7 +111,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
'state': '100.0',
})
# ---
# name: test_number_snapshot[number.test_number_down_slat_position-entry]
@ -149,7 +149,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'down_slat_position',
'unique_id': '00055511EECC-1-3',
'unique_id': '00055511EECC-1-2',
'unit_of_measurement': '°',
})
# ---
@ -168,7 +168,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '38',
'state': '38.0',
})
# ---
# name: test_number_snapshot[number.test_number_end_position-entry]
@ -206,7 +206,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'endposition_configuration',
'unique_id': '00055511EECC-1-5',
'unique_id': '00055511EECC-1-4',
'unit_of_measurement': None,
})
# ---
@ -224,7 +224,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '129',
'state': '129.0',
})
# ---
# name: test_number_snapshot[number.test_number_maximum_slat_angle-entry]
@ -262,7 +262,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'slat_max_angle',
'unique_id': '00055511EECC-1-10',
'unique_id': '00055511EECC-1-9',
'unit_of_measurement': '°',
})
# ---
@ -281,7 +281,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '75',
'state': '75.0',
})
# ---
# name: test_number_snapshot[number.test_number_minimum_slat_angle-entry]
@ -319,7 +319,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'slat_min_angle',
'unique_id': '00055511EECC-1-11',
'unique_id': '00055511EECC-1-10',
'unit_of_measurement': '°',
})
# ---
@ -338,7 +338,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-75',
'state': '-75.0',
})
# ---
# name: test_number_snapshot[number.test_number_motion_alarm_delay-entry]
@ -376,7 +376,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'motion_alarm_cancelation_delay',
'unique_id': '00055511EECC-1-6',
'unique_id': '00055511EECC-1-5',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
@ -434,7 +434,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'polling_interval',
'unique_id': '00055511EECC-1-8',
'unique_id': '00055511EECC-1-7',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
@ -454,7 +454,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30',
'state': '30.0',
})
# ---
# name: test_number_snapshot[number.test_number_slat_steps-entry]
@ -492,7 +492,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'slat_steps',
'unique_id': '00055511EECC-1-12',
'unique_id': '00055511EECC-1-11',
'unit_of_measurement': None,
})
# ---
@ -510,7 +510,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '6',
'state': '6.0',
})
# ---
# name: test_number_snapshot[number.test_number_slat_turn_duration-entry]
@ -548,7 +548,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'shutter_slat_time',
'unique_id': '00055511EECC-1-9',
'unique_id': '00055511EECC-1-8',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
@ -568,7 +568,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
'state': '1.6',
})
# ---
# name: test_number_snapshot[number.test_number_temperature_offset-entry]
@ -606,7 +606,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'temperature_offset',
'unique_id': '00055511EECC-1-13',
'unique_id': '00055511EECC-1-12',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
@ -628,6 +628,64 @@
'state': 'unavailable',
})
# ---
# name: test_number_snapshot[number.test_number_threshold_for_wind_trigger-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 22.5,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 2.5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.test_number_threshold_for_wind_trigger',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.WIND_SPEED: 'wind_speed'>,
'original_icon': None,
'original_name': 'Threshold for wind trigger',
'platform': 'homee',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'wind_monitoring_state',
'unique_id': '00055511EECC-1-16',
'unit_of_measurement': <UnitOfSpeed.METERS_PER_SECOND: 'm/s'>,
})
# ---
# name: test_number_snapshot[number.test_number_threshold_for_wind_trigger-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'wind_speed',
'friendly_name': 'Test Number Threshold for wind trigger',
'max': 22.5,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 2.5,
'unit_of_measurement': <UnitOfSpeed.METERS_PER_SECOND: 'm/s'>,
}),
'context': <ANY>,
'entity_id': 'number.test_number_threshold_for_wind_trigger',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '5.0',
})
# ---
# name: test_number_snapshot[number.test_number_up_movement_duration-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -663,7 +721,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'up_time',
'unique_id': '00055511EECC-1-14',
'unique_id': '00055511EECC-1-13',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
@ -683,7 +741,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '57',
'state': '57.0',
})
# ---
# name: test_number_snapshot[number.test_number_wake_up_interval-entry]
@ -721,7 +779,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'wake_up_interval',
'unique_id': '00055511EECC-1-15',
'unique_id': '00055511EECC-1-14',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
@ -741,7 +799,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '600',
'state': '600.0',
})
# ---
# name: test_number_snapshot[number.test_number_window_open_sensibility-entry]
@ -779,7 +837,7 @@
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'open_window_detection_sensibility',
'unique_id': '00055511EECC-1-7',
'unique_id': '00055511EECC-1-6',
'unit_of_measurement': None,
})
# ---
@ -797,6 +855,6 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
'state': '3.0',
})
# ---

View File

@ -2,6 +2,7 @@
from unittest.mock import MagicMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.number import (
@ -18,24 +19,62 @@ from . import build_mock_node, setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_set_value(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
async def setup_numbers(
hass: HomeAssistant, mock_homee: MagicMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test set_value service."""
"""Set up the number platform."""
mock_homee.nodes = [build_mock_node("numbers.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
await setup_integration(hass, mock_config_entry)
@pytest.mark.parametrize(
("entity_id", "expected"),
[
("number.test_number_down_position", 100.0),
("number.test_number_threshold_for_wind_trigger", 5.0),
],
)
async def test_value_fn(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
expected: float,
) -> None:
"""Test the value_fn of the number entity."""
await setup_numbers(hass, mock_homee, mock_config_entry)
assert hass.states.get(entity_id).state == str(expected)
@pytest.mark.parametrize(
("entity_id", "attribute_index", "value", "expected"),
[
("number.test_number_down_position", 0, 90, 90),
("number.test_number_threshold_for_wind_trigger", 15, 7.5, 3),
],
)
async def test_set_value(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
attribute_index: int,
value: float,
expected: float,
) -> None:
"""Test set_value service."""
await setup_numbers(hass, mock_homee, mock_config_entry)
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: "number.test_number_down_position", ATTR_VALUE: 90},
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value},
blocking=True,
)
number = mock_homee.nodes[0].attributes[0]
mock_homee.set_value.assert_called_once_with(number.node_id, number.id, 90)
number = mock_homee.nodes[0].attributes[attribute_index]
mock_homee.set_value.assert_called_once_with(number.node_id, number.id, expected)
async def test_set_value_not_editable(
@ -44,9 +83,7 @@ async def test_set_value_not_editable(
mock_config_entry: MockConfigEntry,
) -> None:
"""Test set_value if attribute is not editable."""
mock_homee.nodes = [build_mock_node("numbers.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
await setup_integration(hass, mock_config_entry)
await setup_numbers(hass, mock_homee, mock_config_entry)
await hass.services.async_call(
NUMBER_DOMAIN,
@ -66,9 +103,7 @@ async def test_number_snapshot(
snapshot: SnapshotAssertion,
) -> None:
"""Test the multisensor snapshot."""
mock_homee.nodes = [build_mock_node("numbers.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
with patch("homeassistant.components.homee.PLATFORMS", [Platform.NUMBER]):
await setup_integration(hass, mock_config_entry)
await setup_numbers(hass, mock_homee, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)