Add select platform to Husqvarna Automower (#113816)

* Add select platform to Husqvarna Automower

* docstring

* address review

* pin headlight_modes list

* Apply suggestions from code review

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Apply review

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Thomas55555 2024-03-21 17:07:09 +01:00 committed by GitHub
parent b4c36d4676
commit 63221356f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 192 additions and 3 deletions

View File

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

View File

@ -8,6 +8,11 @@
"default": "mdi:debug-step-into"
}
},
"select": {
"headlight_mode": {
"default": "mdi:car-light-high"
}
},
"sensor": {
"number_of_charging_cycles": {
"default": "mdi:battery-sync-outline"

View File

@ -0,0 +1,70 @@
"""Creates a select entity for the headlight of the mower."""
import logging
from aioautomower.exceptions import ApiException
from aioautomower.model import HeadlightModes
from homeassistant.components.select import SelectEntity
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 AutomowerControlEntity
_LOGGER = logging.getLogger(__name__)
HEADLIGHT_MODES: list = [
HeadlightModes.ALWAYS_OFF.lower(),
HeadlightModes.ALWAYS_ON.lower(),
HeadlightModes.EVENING_AND_NIGHT.lower(),
HeadlightModes.EVENING_ONLY.lower(),
]
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up select platform."""
coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
AutomowerSelectEntity(mower_id, coordinator)
for mower_id in coordinator.data
if coordinator.data[mower_id].capabilities.headlights
)
class AutomowerSelectEntity(AutomowerControlEntity, SelectEntity):
"""Defining the headlight mode entity."""
_attr_options = HEADLIGHT_MODES
_attr_entity_category = EntityCategory.CONFIG
_attr_translation_key = "headlight_mode"
def __init__(
self,
mower_id: str,
coordinator: AutomowerDataUpdateCoordinator,
) -> None:
"""Set up select platform."""
super().__init__(mower_id, coordinator)
self._attr_unique_id = f"{mower_id}_headlight_mode"
@property
def current_option(self) -> str:
"""Return the current option for the entity."""
return self.mower_attributes.headlight.mode.lower()
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
try:
await self.coordinator.api.set_headlight_mode(self.mower_id, option.upper())
except ApiException as exception:
raise HomeAssistantError(
f"Command couldn't be sent to the command queue: {exception}"
) from exception

View File

@ -37,9 +37,15 @@
"name": "Returning to dock"
}
},
"switch": {
"enable_schedule": {
"name": "Enable schedule"
"select": {
"headlight_mode": {
"name": "Headlight mode",
"state": {
"always_on": "Always on",
"always_off": "Always off",
"evening_only": "Evening only",
"evening_and_night": "Evening and night"
}
}
},
"sensor": {
@ -79,6 +85,11 @@
"demo": "Demo"
}
}
},
"switch": {
"enable_schedule": {
"name": "Enable schedule"
}
}
}
}

View File

@ -0,0 +1,102 @@
"""Tests for select platform."""
from datetime import timedelta
from unittest.mock import AsyncMock
from aioautomower.exceptions import ApiException
from aioautomower.model import HeadlightModes
from aioautomower.utils import mower_list_to_dictionary_dataclass
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.husqvarna_automower.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from . import setup_integration
from .const import TEST_MOWER_ID
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
load_json_value_fixture,
)
async def test_select_states(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test states of headlight mode select."""
values = mower_list_to_dictionary_dataclass(
load_json_value_fixture("mower.json", DOMAIN)
)
await setup_integration(hass, mock_config_entry)
state = hass.states.get("select.test_mower_1_headlight_mode")
assert state is not None
assert state.state == "evening_only"
for state, expected_state in [
(
HeadlightModes.ALWAYS_OFF,
"always_off",
),
(HeadlightModes.ALWAYS_ON, "always_on"),
(HeadlightModes.EVENING_AND_NIGHT, "evening_and_night"),
]:
values[TEST_MOWER_ID].headlight.mode = state
mock_automower_client.get_status.return_value = values
freezer.tick(timedelta(minutes=5))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("select.test_mower_1_headlight_mode")
assert state.state == expected_state
@pytest.mark.parametrize(
("service"),
[
("always_on"),
("always_off"),
("evening_only"),
("evening_and_night"),
],
)
async def test_select_commands(
hass: HomeAssistant,
service: str,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test select commands for headlight mode."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
domain="select",
service="select_option",
service_data={
"entity_id": "select.test_mower_1_headlight_mode",
"option": service,
},
blocking=True,
)
mocked_method = mock_automower_client.set_headlight_mode
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="select",
service="select_option",
service_data={
"entity_id": "select.test_mower_1_headlight_mode",
"option": service,
},
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