Add number platform to Husqvarna Automower (#115125)

* Add number platform to Husqvarna Automower

* use fixture to enable by default

* replace state test with snapshot test

* make property in entity description

* send value as integer

* give the exists functions something to do
pull/116030/head^2
Thomas55555 2024-04-23 14:18:49 +02:00 committed by GitHub
parent fd14695d26
commit b8918d7d17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 239 additions and 0 deletions

View File

@ -21,6 +21,7 @@ PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.DEVICE_TRACKER,
Platform.LAWN_MOWER,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,

View File

@ -8,6 +8,11 @@
"default": "mdi:debug-step-into"
}
},
"number": {
"cutting_height": {
"default": "mdi:grass"
}
},
"select": {
"headlight_mode": {
"default": "mdi:car-light-high"

View File

@ -0,0 +1,95 @@
"""Creates the number entities for the mower."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import logging
from typing import Any
from aioautomower.exceptions import ApiException
from aioautomower.model import MowerAttributes
from aioautomower.session import AutomowerSession
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class AutomowerNumberEntityDescription(NumberEntityDescription):
"""Describes Automower number entity."""
exists_fn: Callable[[MowerAttributes], bool] = lambda _: True
value_fn: Callable[[MowerAttributes], int]
set_value_fn: Callable[[AutomowerSession, str, float], Awaitable[Any]]
NUMBER_TYPES: tuple[AutomowerNumberEntityDescription, ...] = (
AutomowerNumberEntityDescription(
key="cutting_height",
translation_key="cutting_height",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
native_min_value=1,
native_max_value=9,
exists_fn=lambda data: data.cutting_height is not None,
value_fn=lambda data: data.cutting_height,
set_value_fn=lambda session, mower_id, cheight: session.set_cutting_height(
mower_id, int(cheight)
),
),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up number platform."""
coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
AutomowerNumberEntity(mower_id, coordinator, description)
for mower_id in coordinator.data
for description in NUMBER_TYPES
if description.exists_fn(coordinator.data[mower_id])
)
class AutomowerNumberEntity(AutomowerBaseEntity, NumberEntity):
"""Defining the AutomowerNumberEntity with AutomowerNumberEntityDescription."""
entity_description: AutomowerNumberEntityDescription
def __init__(
self,
mower_id: str,
coordinator: AutomowerDataUpdateCoordinator,
description: AutomowerNumberEntityDescription,
) -> None:
"""Set up AutomowerNumberEntity."""
super().__init__(mower_id, coordinator)
self.entity_description = description
self._attr_unique_id = f"{mower_id}_{description.key}"
@property
def native_value(self) -> float:
"""Return the state of the number."""
return self.entity_description.value_fn(self.mower_attributes)
async def async_set_native_value(self, value: float) -> None:
"""Change to new number value."""
try:
await self.entity_description.set_value_fn(
self.coordinator.api, self.mower_id, value
)
except ApiException as exception:
raise HomeAssistantError(
f"Command couldn't be sent to the command queue: {exception}"
) from exception

View File

@ -37,6 +37,11 @@
"name": "Returning to dock"
}
},
"number": {
"cutting_height": {
"name": "Cutting height"
}
},
"select": {
"headlight_mode": {
"name": "Headlight mode",

View File

@ -0,0 +1,56 @@
# serializer version: 1
# name: test_snapshot_number[number.test_mower_1_cutting_height-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 9,
'min': 1,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1.0,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.test_mower_1_cutting_height',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Cutting height',
'platform': 'husqvarna_automower',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'cutting_height',
'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_cutting_height',
'unit_of_measurement': None,
})
# ---
# name: test_snapshot_number[number.test_mower_1_cutting_height-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Mower 1 Cutting height',
'max': 9,
'min': 1,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1.0,
}),
'context': <ANY>,
'entity_id': 'number.test_mower_1_cutting_height',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '4',
})
# ---

View File

@ -0,0 +1,77 @@
"""Tests for number platform."""
from unittest.mock import AsyncMock, patch
from aioautomower.exceptions import ApiException
import pytest
from syrupy import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_number_commands(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test number commands."""
entity_id = "number.test_mower_1_cutting_height"
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
domain="number",
service="set_value",
target={"entity_id": entity_id},
service_data={"value": "3"},
blocking=True,
)
mocked_method = mock_automower_client.set_cutting_height
assert len(mocked_method.mock_calls) == 1
mocked_method.side_effect = ApiException("Test error")
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
domain="number",
service="set_value",
target={"entity_id": entity_id},
service_data={"value": "3"},
blocking=True,
)
assert (
str(exc_info.value)
== "Command couldn't be sent to the command queue: Test error"
)
assert len(mocked_method.mock_calls) == 2
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_snapshot_number(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test states of the number entity."""
with patch(
"homeassistant.components.husqvarna_automower.PLATFORMS",
[Platform.NUMBER],
):
await setup_integration(hass, mock_config_entry)
entity_entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert entity_entries
for entity_entry in entity_entries:
assert hass.states.get(entity_entry.entity_id) == snapshot(
name=f"{entity_entry.entity_id}-state"
)
assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")