mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 14:58:11 -04:00
Add time platform to LaMetric (#181982)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
co-authored by
Claude
Franck Nijhof
parent
d53398b741
commit
712f5fa726
@@ -14,6 +14,7 @@ PLATFORMS = [
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.TIME,
|
||||
Platform.UPDATE,
|
||||
]
|
||||
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
"bluetooth": {
|
||||
"default": "mdi:bluetooth"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"screensaver_end_time": {
|
||||
"default": "mdi:clock-end"
|
||||
},
|
||||
"screensaver_start_time": {
|
||||
"default": "mdi:clock-start"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -96,6 +96,14 @@
|
||||
"bluetooth": {
|
||||
"name": "Bluetooth"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"screensaver_end_time": {
|
||||
"name": "Screensaver end time"
|
||||
},
|
||||
"screensaver_start_time": {
|
||||
"name": "Screensaver start time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Support for LaMetric times."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, replace
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from demetriek import (
|
||||
DisplayScreensaverModes,
|
||||
DisplayScreensaverTimeBased,
|
||||
ScreensaverMode,
|
||||
)
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
|
||||
from .entity import LaMetricEntity
|
||||
from .helpers import lametric_exception_handler
|
||||
|
||||
# The SKY reports a time based screensaver mode, but is not known to support
|
||||
# scheduling it.
|
||||
MODEL_SKY = "sa5"
|
||||
|
||||
# Both entities write the same pair of start and end time, so one at a time.
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
# Any date will do, it only carries the time arithmetic below.
|
||||
TIME_ANCHOR = date(2000, 1, 1)
|
||||
|
||||
|
||||
def _shift(value: time, offset: timedelta) -> time:
|
||||
"""Shift a time of day by an offset, wrapping around midnight."""
|
||||
return (datetime.combine(TIME_ANCHOR, value) + offset).time()
|
||||
|
||||
|
||||
def _utc_offset() -> timedelta:
|
||||
"""Return the current offset of the Home Assistant time zone from UTC.
|
||||
|
||||
The device stores the screensaver times in UTC. Converting both ways with
|
||||
the same, current offset makes reading the exact inverse of writing, also
|
||||
around a DST change, where going by today's date would not be.
|
||||
"""
|
||||
return dt_util.now().utcoffset() or timedelta()
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LaMetricTimeEntityDescription(TimeEntityDescription):
|
||||
"""Class describing LaMetric time entities."""
|
||||
|
||||
value_fn: Callable[[DisplayScreensaverTimeBased], time | None]
|
||||
times_fn: Callable[
|
||||
[DisplayScreensaverTimeBased, time], tuple[time | None, time | None]
|
||||
]
|
||||
|
||||
|
||||
TIMES = [
|
||||
LaMetricTimeEntityDescription(
|
||||
key="screensaver_start_time",
|
||||
translation_key="screensaver_start_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda mode: mode.start_time,
|
||||
times_fn=lambda mode, value: (value, mode.end_time),
|
||||
),
|
||||
LaMetricTimeEntityDescription(
|
||||
key="screensaver_end_time",
|
||||
translation_key="screensaver_end_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda mode: mode.end_time,
|
||||
times_fn=lambda mode, value: (mode.start_time, value),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LaMetricConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up LaMetric time based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
if coordinator.data.model == MODEL_SKY:
|
||||
return
|
||||
|
||||
screensaver = coordinator.data.display.screensaver
|
||||
if not screensaver or not screensaver.modes:
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
LaMetricTimeEntity(
|
||||
coordinator=coordinator,
|
||||
description=description,
|
||||
)
|
||||
for description in TIMES
|
||||
)
|
||||
|
||||
|
||||
class LaMetricTimeEntity(LaMetricEntity, TimeEntity):
|
||||
"""Representation of a LaMetric time."""
|
||||
|
||||
entity_description: LaMetricTimeEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LaMetricDataUpdateCoordinator,
|
||||
description: LaMetricTimeEntityDescription,
|
||||
) -> None:
|
||||
"""Initiate LaMetric Time."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}"
|
||||
|
||||
@property
|
||||
def _modes(self) -> DisplayScreensaverModes:
|
||||
"""Return the screensaver modes of the device."""
|
||||
screensaver = self.coordinator.data.display.screensaver
|
||||
if TYPE_CHECKING:
|
||||
assert screensaver is not None
|
||||
assert screensaver.modes is not None
|
||||
return screensaver.modes
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> time | None:
|
||||
"""Return the time value."""
|
||||
if (value := self.entity_description.value_fn(self._modes.time_based)) is None:
|
||||
return None
|
||||
|
||||
return _shift(value, _utc_offset())
|
||||
|
||||
@lametric_exception_handler
|
||||
@override
|
||||
async def async_set_value(self, value: time) -> None:
|
||||
"""Change to new time value."""
|
||||
modes = self._modes
|
||||
new_time = _shift(value, -_utc_offset())
|
||||
start_time, end_time = self.entity_description.times_fn(
|
||||
modes.time_based, new_time
|
||||
)
|
||||
|
||||
# The device only takes both times at once, so the one left untouched
|
||||
# is sent along. A device that never had its times set reports
|
||||
# neither; until the other one is set too, it gets the new time.
|
||||
display = await self.coordinator.lametric.display(
|
||||
screensaver_mode=ScreensaverMode.TIME_BASED,
|
||||
screensaver_start_time=new_time if start_time is None else start_time,
|
||||
screensaver_end_time=new_time if end_time is None else end_time,
|
||||
)
|
||||
|
||||
# Writing the times always switches the device to the time based mode,
|
||||
# even when asked not to. Switch back to when dark if that was active;
|
||||
# the times stay stored.
|
||||
if modes.when_dark is not None and modes.when_dark.enabled:
|
||||
display = await self.coordinator.lametric.display(
|
||||
screensaver_mode=ScreensaverMode.WHEN_DARK,
|
||||
screensaver_mode_enabled=True,
|
||||
)
|
||||
|
||||
# The device answers with its new state, use that right away. A refresh
|
||||
# requested shortly after the previous one is held back, and the next
|
||||
# write would send a stale time along otherwise.
|
||||
self.coordinator.async_set_updated_data(
|
||||
replace(self.coordinator.data, display=display)
|
||||
)
|
||||
@@ -92,6 +92,8 @@ def mock_lametric(device_fixture: str) -> Generator[MagicMock]:
|
||||
lametric.device.return_value = Device.from_json(
|
||||
load_fixture(f"{device_fixture}.json", DOMAIN)
|
||||
)
|
||||
# The device answers a display write with its display state.
|
||||
lametric.display.return_value = lametric.device.return_value.display
|
||||
yield lametric
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"modes": {
|
||||
"time_based": {
|
||||
"enabled": true,
|
||||
"end_time": "06:30:00",
|
||||
"local_end_time": "07:30:00",
|
||||
"local_start_time": "01:00:39",
|
||||
"start_time": "00:00:39"
|
||||
},
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
'screen_off': None,
|
||||
'time_based': dict({
|
||||
'enabled': True,
|
||||
'end_time': None,
|
||||
'local_end_time': None,
|
||||
'end_time': '06:30:00',
|
||||
'local_end_time': '07:30:00',
|
||||
'local_start_time': '01:00:39',
|
||||
'start_time': '00:00:39',
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[time][time.frenck_s_lametric_screensaver_end_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'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.frenck_s_lametric_screensaver_end_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Screensaver end time',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Screensaver end time',
|
||||
'platform': 'lametric',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'screensaver_end_time',
|
||||
'unique_id': 'SA110405124500W00BS9-screensaver_end_time',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[time][time.frenck_s_lametric_screensaver_end_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: "Frenck's LaMetric Screensaver end time",
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'time.frenck_s_lametric_screensaver_end_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '22:30:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[time][time.frenck_s_lametric_screensaver_start_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'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.frenck_s_lametric_screensaver_start_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Screensaver start time',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Screensaver start time',
|
||||
'platform': 'lametric',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'screensaver_start_time',
|
||||
'unique_id': 'SA110405124500W00BS9-screensaver_start_time',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[time][time.frenck_s_lametric_screensaver_start_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: "Frenck's LaMetric Screensaver start time",
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'time.frenck_s_lametric_screensaver_start_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '16:00:39',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,327 @@
|
||||
"""Tests for the LaMetric time platform."""
|
||||
|
||||
from dataclasses import replace
|
||||
from datetime import time
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
from demetriek import LaMetricConnectionError, LaMetricError, ScreensaverMode
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.time import DOMAIN as TIME_DOMAIN, SERVICE_SET_VALUE
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_TIME,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
ENTITY_START_TIME = "time.frenck_s_lametric_screensaver_start_time"
|
||||
ENTITY_END_TIME = "time.frenck_s_lametric_screensaver_end_time"
|
||||
|
||||
pytestmark = pytest.mark.freeze_time("2025-01-15 12:00:00+00:00")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("init_integration", [Platform.TIME], indirect=True)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the LaMetric screensaver time entities."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "expected_start", "expected_end"),
|
||||
[
|
||||
(ENTITY_START_TIME, time(4, 0), time(6, 30)),
|
||||
(ENTITY_END_TIME, time(0, 0, 39), time(4, 0)),
|
||||
],
|
||||
ids=["start_time", "end_time"],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_value(
|
||||
hass: HomeAssistant,
|
||||
mock_lametric: MagicMock,
|
||||
entity_id: str,
|
||||
expected_start: time,
|
||||
expected_end: time,
|
||||
) -> None:
|
||||
"""Test setting a screensaver time sends both times, in UTC.
|
||||
|
||||
The device rejects a time based write that carries only one of the times,
|
||||
so the time that was not changed is sent along unaltered.
|
||||
"""
|
||||
await hass.services.async_call(
|
||||
TIME_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_TIME: "20:00:00"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_lametric.display.assert_called_once_with(
|
||||
screensaver_mode=ScreensaverMode.TIME_BASED,
|
||||
screensaver_start_time=expected_start,
|
||||
screensaver_end_time=expected_end,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("now", "value", "expected"),
|
||||
[
|
||||
# The evening before the switch to summer time, when the UTC date is
|
||||
# already the next day.
|
||||
("2026-03-08 07:30:00+00:00", "03:30:00", time(11, 30)),
|
||||
# The day of the switch itself, in the hour that does not exist.
|
||||
("2026-03-08 20:00:00+00:00", "02:30:00", time(9, 30)),
|
||||
],
|
||||
ids=["evening_before_switch", "nonexistent_time"],
|
||||
)
|
||||
async def test_set_value_around_dst(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_lametric: MagicMock,
|
||||
now: str,
|
||||
value: str,
|
||||
expected: time,
|
||||
) -> None:
|
||||
"""Test a screensaver time reads back as set, also around a DST change."""
|
||||
freezer.move_to(now)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
TIME_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: ENTITY_START_TIME, ATTR_TIME: value},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_lametric.display.call_args.kwargs["screensaver_start_time"] == expected
|
||||
|
||||
time_based = mock_lametric.device.return_value.display.screensaver.modes.time_based
|
||||
time_based.start_time = expected
|
||||
await mock_config_entry.runtime_data.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_START_TIME)
|
||||
assert state
|
||||
assert state.state == value
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_values_in_quick_succession(
|
||||
hass: HomeAssistant,
|
||||
mock_lametric: MagicMock,
|
||||
) -> None:
|
||||
"""Test setting both times right after each other keeps both.
|
||||
|
||||
A refresh requested shortly after the previous one is held back, so the
|
||||
second write has to use what the device answered to the first.
|
||||
"""
|
||||
display = mock_lametric.device.return_value.display
|
||||
assert display.screensaver
|
||||
assert display.screensaver.modes
|
||||
mock_lametric.display.return_value = replace(
|
||||
display,
|
||||
screensaver=replace(
|
||||
display.screensaver,
|
||||
modes=replace(
|
||||
display.screensaver.modes,
|
||||
time_based=replace(
|
||||
display.screensaver.modes.time_based, start_time=time(4, 0)
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
for entity_id, value in (
|
||||
(ENTITY_START_TIME, "20:00:00"),
|
||||
(ENTITY_END_TIME, "21:00:00"),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
TIME_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_TIME: value},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_lametric.display.call_args == call(
|
||||
screensaver_mode=ScreensaverMode.TIME_BASED,
|
||||
screensaver_start_time=time(4, 0),
|
||||
screensaver_end_time=time(5, 0),
|
||||
)
|
||||
|
||||
|
||||
async def test_unknown_times(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_lametric: MagicMock,
|
||||
) -> None:
|
||||
"""Test a device that has no screensaver times configured."""
|
||||
time_based = mock_lametric.device.return_value.display.screensaver.modes.time_based
|
||||
time_based.start_time = None
|
||||
time_based.end_time = None
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_START_TIME)
|
||||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
state = hass.states.get(ENTITY_END_TIME)
|
||||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_no_screensaver_support(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_lametric: MagicMock,
|
||||
) -> None:
|
||||
"""Test devices that do not report a screensaver get no time entities."""
|
||||
mock_lametric.device.return_value.display.screensaver = None
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(ENTITY_START_TIME) is None
|
||||
assert hass.states.get(ENTITY_END_TIME) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"entity_id", [ENTITY_START_TIME, ENTITY_END_TIME], ids=["start_time", "end_time"]
|
||||
)
|
||||
async def test_set_value_without_both_times(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_lametric: MagicMock,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Test setting a screensaver time on a device that has none configured.
|
||||
|
||||
The device only takes both times at once, so the new time is sent for
|
||||
both, until the other one is set as well.
|
||||
"""
|
||||
time_based = mock_lametric.device.return_value.display.screensaver.modes.time_based
|
||||
time_based.start_time = None
|
||||
time_based.end_time = None
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
TIME_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_TIME: "20:00:00"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_lametric.display.assert_called_once_with(
|
||||
screensaver_mode=ScreensaverMode.TIME_BASED,
|
||||
screensaver_start_time=time(4, 0),
|
||||
screensaver_end_time=time(4, 0),
|
||||
)
|
||||
|
||||
|
||||
async def test_set_value_keeps_when_dark(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_lametric: MagicMock,
|
||||
) -> None:
|
||||
"""Test setting a screensaver time leaves the when dark mode active.
|
||||
|
||||
Writing the times always switches the device to the time based mode, so
|
||||
the when dark mode is switched back on afterwards.
|
||||
"""
|
||||
modes = mock_lametric.device.return_value.display.screensaver.modes
|
||||
modes.time_based.enabled = False
|
||||
modes.when_dark.enabled = True
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
TIME_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: ENTITY_START_TIME, ATTR_TIME: "20:00:00"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_lametric.display.mock_calls == [
|
||||
call(
|
||||
screensaver_mode=ScreensaverMode.TIME_BASED,
|
||||
screensaver_start_time=time(4, 0),
|
||||
screensaver_end_time=time(6, 30),
|
||||
),
|
||||
call(
|
||||
screensaver_mode=ScreensaverMode.WHEN_DARK,
|
||||
screensaver_mode_enabled=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["device_sa5"])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_sky_has_no_times(hass: HomeAssistant) -> None:
|
||||
"""Test the SKY gets no screensaver time entities."""
|
||||
assert hass.states.get("time.spyfly_s_lametric_sky_screensaver_start_time") is None
|
||||
assert hass.states.get("time.spyfly_s_lametric_sky_screensaver_end_time") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error_message", "expected_state"),
|
||||
[
|
||||
(
|
||||
LaMetricError,
|
||||
"Invalid response from the LaMetric device",
|
||||
"16:00:39",
|
||||
),
|
||||
(
|
||||
LaMetricConnectionError,
|
||||
"Error communicating with the LaMetric device",
|
||||
STATE_UNAVAILABLE,
|
||||
),
|
||||
],
|
||||
ids=["error", "connection_error"],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_time_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_lametric: MagicMock,
|
||||
side_effect: type[Exception],
|
||||
error_message: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test error handling of the LaMetric times."""
|
||||
mock_lametric.display.side_effect = side_effect
|
||||
|
||||
with pytest.raises(HomeAssistantError, match=error_message):
|
||||
await hass.services.async_call(
|
||||
TIME_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: ENTITY_START_TIME, ATTR_TIME: "20:00:00"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_START_TIME)
|
||||
assert state
|
||||
assert state.state == expected_state
|
||||
Reference in New Issue
Block a user