Add time entity to balboa (#138248)

This commit is contained in:
Nathan Spencer 2025-02-14 12:50:51 -07:00 committed by GitHub
parent 58797a14e7
commit b916fbe1fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 331 additions and 0 deletions

View File

@ -24,6 +24,7 @@ PLATFORMS = [
Platform.FAN,
Platform.LIGHT,
Platform.SELECT,
Platform.TIME,
]

View File

@ -78,6 +78,14 @@
"high": "High"
}
}
},
"time": {
"filter_cycle_start": {
"name": "Filter cycle {index} start"
},
"filter_cycle_end": {
"name": "Filter cycle {index} end"
}
}
}
}

View File

@ -0,0 +1,56 @@
"""Support for Balboa times."""
from __future__ import annotations
from datetime import time
import itertools
from typing import Any
from pybalboa import SpaClient
from homeassistant.components.time import TimeEntity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BalboaConfigEntry
from .entity import BalboaEntity
FILTER_CYCLE = "filter_cycle_"
async def async_setup_entry(
hass: HomeAssistant,
entry: BalboaConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the spa's times."""
spa = entry.runtime_data
async_add_entities(
BalboaTimeEntity(spa, index, period)
for index, period in itertools.product((1, 2), ("start", "end"))
)
class BalboaTimeEntity(BalboaEntity, TimeEntity):
"""Representation of a Balboa time entity."""
entity_category = EntityCategory.CONFIG
def __init__(self, spa: SpaClient, index: int, period: str) -> None:
"""Initialize a Balboa time entity."""
super().__init__(spa, f"{FILTER_CYCLE}{index}_{period}")
self.index = index
self.period = period
self._attr_translation_key = f"{FILTER_CYCLE}{period}"
self._attr_translation_placeholders = {"index": str(index)}
@property
def native_value(self) -> time | None:
"""Return the value reported by the time."""
return getattr(self._client, f"{FILTER_CYCLE}{self.index}_{self.period}")
async def async_set_value(self, value: time) -> None:
"""Change the time."""
args: dict[str, Any] = {self.period: value}
await self._client.configure_filter_cycle(self.index, **args)

View File

@ -3,6 +3,7 @@
from __future__ import annotations
from collections.abc import Callable, Generator
from datetime import time
from unittest.mock import AsyncMock, MagicMock, patch
from pybalboa.enums import HeatMode, LowHighRange
@ -48,7 +49,11 @@ def client_fixture() -> Generator[MagicMock]:
client.blowers = []
client.circulation_pump.state = 0
client.filter_cycle_1_running = False
client.filter_cycle_1_start = time(8, 0)
client.filter_cycle_1_end = time(9, 0)
client.filter_cycle_2_running = False
client.filter_cycle_2_start = time(19, 0)
client.filter_cycle_2_end = time(21, 30)
client.temperature_unit = 1
client.temperature = 10
client.temperature_minimum = 10

View File

@ -0,0 +1,189 @@
# serializer version: 1
# name: test_times[time.fakespa_filter_cycle_1_end-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': 'time',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'time.fakespa_filter_cycle_1_end',
'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': 'Filter cycle 1 end',
'platform': 'balboa',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'filter_cycle_end',
'unique_id': 'FakeSpa-filter_cycle_1_end-c0ffee',
'unit_of_measurement': None,
})
# ---
# name: test_times[time.fakespa_filter_cycle_1_end-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'FakeSpa Filter cycle 1 end',
}),
'context': <ANY>,
'entity_id': 'time.fakespa_filter_cycle_1_end',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '09:00:00',
})
# ---
# name: test_times[time.fakespa_filter_cycle_1_start-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': 'time',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'time.fakespa_filter_cycle_1_start',
'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': 'Filter cycle 1 start',
'platform': 'balboa',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'filter_cycle_start',
'unique_id': 'FakeSpa-filter_cycle_1_start-c0ffee',
'unit_of_measurement': None,
})
# ---
# name: test_times[time.fakespa_filter_cycle_1_start-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'FakeSpa Filter cycle 1 start',
}),
'context': <ANY>,
'entity_id': 'time.fakespa_filter_cycle_1_start',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '08:00:00',
})
# ---
# name: test_times[time.fakespa_filter_cycle_2_end-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': 'time',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'time.fakespa_filter_cycle_2_end',
'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': 'Filter cycle 2 end',
'platform': 'balboa',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'filter_cycle_end',
'unique_id': 'FakeSpa-filter_cycle_2_end-c0ffee',
'unit_of_measurement': None,
})
# ---
# name: test_times[time.fakespa_filter_cycle_2_end-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'FakeSpa Filter cycle 2 end',
}),
'context': <ANY>,
'entity_id': 'time.fakespa_filter_cycle_2_end',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '21:30:00',
})
# ---
# name: test_times[time.fakespa_filter_cycle_2_start-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': 'time',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'time.fakespa_filter_cycle_2_start',
'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': 'Filter cycle 2 start',
'platform': 'balboa',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'filter_cycle_start',
'unique_id': 'FakeSpa-filter_cycle_2_start-c0ffee',
'unit_of_measurement': None,
})
# ---
# name: test_times[time.fakespa_filter_cycle_2_start-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'FakeSpa Filter cycle 2 start',
}),
'context': <ANY>,
'entity_id': 'time.fakespa_filter_cycle_2_start',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '19:00:00',
})
# ---

View File

@ -0,0 +1,72 @@
"""Tests of the times of the balboa integration."""
from __future__ import annotations
from datetime import time
from unittest.mock import MagicMock, patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.time import (
ATTR_TIME,
DOMAIN as TIME_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
from tests.common import snapshot_platform
ENTITY_TIME = "time.fakespa_"
async def test_times(
hass: HomeAssistant,
client: MagicMock,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test spa times."""
with patch("homeassistant.components.balboa.PLATFORMS", [Platform.TIME]):
entry = await init_integration(hass)
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
@pytest.mark.parametrize(
("filter_cycle", "period", "value"),
[
(1, "start", "08:00:00"),
(1, "end", "09:00:00"),
(2, "start", "19:00:00"),
(2, "end", "21:30:00"),
],
)
async def test_time(
hass: HomeAssistant, client: MagicMock, filter_cycle: int, period: str, value: str
) -> None:
"""Test spa filter cycle time."""
await init_integration(hass)
time_entity = f"{ENTITY_TIME}filter_cycle_{filter_cycle}_{period}"
# check the expected state of the time entity
state = hass.states.get(time_entity)
assert state.state == value
new_time = time(hour=7, minute=0)
await hass.services.async_call(
TIME_DOMAIN,
SERVICE_SET_VALUE,
service_data={ATTR_TIME: new_time},
blocking=True,
target={ATTR_ENTITY_ID: time_entity},
)
# check we made a call with the right parameters
client.configure_filter_cycle.assert_called_with(filter_cycle, **{period: new_time})