1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Add HDFury number platform (#163381)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Glenn de Haan
2026-02-18 19:22:57 +01:00
committed by GitHub
parent 15cb102c39
commit e9039cec24
8 changed files with 365 additions and 0 deletions
@@ -7,6 +7,7 @@ from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
PLATFORMS = [
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
@@ -5,6 +5,14 @@
"default": "mdi:connection"
}
},
"number": {
"oled_fade": {
"default": "mdi:cellphone-information"
},
"reboot_timer": {
"default": "mdi:timer-refresh"
}
},
"select": {
"opmode": {
"default": "mdi:cogs"
+101
View File
@@ -0,0 +1,101 @@
"""Number platform for HDFury Integration."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from hdfury import HDFuryAPI, HDFuryError
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import HDFuryConfigEntry
from .entity import HDFuryEntity
PARALLEL_UPDATES = 1
@dataclass(kw_only=True, frozen=True)
class HDFuryNumberEntityDescription(NumberEntityDescription):
"""Description for HDFury number entities."""
set_value_fn: Callable[[HDFuryAPI, str], Awaitable[None]]
NUMBERS: tuple[HDFuryNumberEntityDescription, ...] = (
HDFuryNumberEntityDescription(
key="oledfade",
translation_key="oled_fade",
mode=NumberMode.BOX,
native_min_value=1,
native_max_value=100,
native_step=1,
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_oled_fade(value),
),
HDFuryNumberEntityDescription(
key="reboottimer",
translation_key="reboot_timer",
mode=NumberMode.BOX,
native_min_value=0,
native_max_value=100,
native_step=1,
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_reboot_timer(value),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: HDFuryConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up numbers using the platform schema."""
coordinator = entry.runtime_data
async_add_entities(
HDFuryNumber(coordinator, description)
for description in NUMBERS
if description.key in coordinator.data.config
)
class HDFuryNumber(HDFuryEntity, NumberEntity):
"""Base HDFury Number Class."""
entity_description: HDFuryNumberEntityDescription
@property
def native_value(self) -> float:
"""Return the current number value."""
return float(self.coordinator.data.config[self.entity_description.key])
async def async_set_native_value(self, value: float) -> None:
"""Set Number Value Event."""
try:
await self.entity_description.set_value_fn(
self.coordinator.client, str(int(value))
)
except HDFuryError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
) from error
await self.coordinator.async_request_refresh()
@@ -40,6 +40,14 @@
"name": "Issue hotplug"
}
},
"number": {
"oled_fade": {
"name": "OLED fade timer"
},
"reboot_timer": {
"name": "Restart timer"
}
},
"select": {
"opmode": {
"name": "Operation mode",
+2
View File
@@ -103,7 +103,9 @@ def mock_hdfury_client() -> Generator[AsyncMock]:
"mutetx1": "1",
"relay": "0",
"macaddr": "c7:1c:df:9d:f6:40",
"reboottimer": "0",
"oled": "1",
"oledfade": "30",
}
)
@@ -24,6 +24,8 @@
'mutetx0': '1',
'mutetx1': '1',
'oled': '1',
'oledfade': '30',
'reboottimer': '0',
'relay': '0',
'tx0plus5': '1',
'tx1plus5': '1',
@@ -0,0 +1,121 @@
# serializer version: 1
# name: test_number_entities[number.hdfury_vrroom_02_oled_fade_timer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 1,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'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.hdfury_vrroom_02_oled_fade_timer',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'OLED fade timer',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'OLED fade timer',
'platform': 'hdfury',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'oled_fade',
'unique_id': '000123456789_oledfade',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_number_entities[number.hdfury_vrroom_02_oled_fade_timer-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'HDFury VRROOM-02 OLED fade timer',
'max': 100,
'min': 1,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.hdfury_vrroom_02_oled_fade_timer',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30.0',
})
# ---
# name: test_number_entities[number.hdfury_vrroom_02_restart_timer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'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.hdfury_vrroom_02_restart_timer',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Restart timer',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Restart timer',
'platform': 'hdfury',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'reboot_timer',
'unique_id': '000123456789_reboottimer',
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
})
# ---
# name: test_number_entities[number.hdfury_vrroom_02_restart_timer-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'HDFury VRROOM-02 Restart timer',
'max': 100,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'number.hdfury_vrroom_02_restart_timer',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
+122
View File
@@ -0,0 +1,122 @@
"""Tests for the HDFury number platform."""
from datetime import timedelta
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from hdfury import HDFuryError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
async def test_number_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test HDFury number entities."""
await setup_integration(hass, mock_config_entry, [Platform.NUMBER])
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("entity_id", "method"),
[
("number.hdfury_vrroom_02_oled_fade_timer", "set_oled_fade"),
("number.hdfury_vrroom_02_restart_timer", "set_reboot_timer"),
],
)
async def test_number_set_value(
hass: HomeAssistant,
mock_hdfury_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
method: str,
) -> None:
"""Test setting a device number value."""
await setup_integration(hass, mock_config_entry, [Platform.NUMBER])
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 50},
blocking=True,
)
getattr(mock_hdfury_client, method).assert_awaited_once_with("50")
@pytest.mark.parametrize(
("entity_id", "method"),
[
("number.hdfury_vrroom_02_oled_fade_timer", "set_oled_fade"),
("number.hdfury_vrroom_02_restart_timer", "set_reboot_timer"),
],
)
async def test_number_error(
hass: HomeAssistant,
mock_hdfury_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
method: str,
) -> None:
"""Test set number value raises HomeAssistantError on API failure."""
getattr(mock_hdfury_client, method).side_effect = HDFuryError()
await setup_integration(hass, mock_config_entry, [Platform.NUMBER])
with pytest.raises(
HomeAssistantError,
match="An error occurred while communicating with HDFury device",
):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 50},
blocking=True,
)
@pytest.mark.parametrize(
("entity_id"),
[
("number.hdfury_vrroom_02_oled_fade_timer"),
("number.hdfury_vrroom_02_restart_timer"),
],
)
async def test_number_entities_unavailable_on_error(
hass: HomeAssistant,
mock_hdfury_client: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
entity_id: str,
) -> None:
"""Test API error causes entities to become unavailable."""
await setup_integration(hass, mock_config_entry, [Platform.NUMBER])
mock_hdfury_client.get_info.side_effect = HDFuryError()
freezer.tick(timedelta(seconds=61))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE