Skip to content

Commit 935bc41

Browse files
committed
fix: handle animation for lights
1 parent 90066fc commit 935bc41

File tree

2 files changed

+151
-0
lines changed

2 files changed

+151
-0
lines changed

custom_components/hella_onyx/sensors/light.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""The ONYX light entity."""
22
import asyncio
33
import logging
4+
import time
5+
from datetime import timedelta
46
from math import ceil
57
from typing import Any
68

@@ -10,12 +12,18 @@
1012
LightEntityFeature,
1113
ATTR_BRIGHTNESS,
1214
)
15+
from homeassistant.helpers.event import (
16+
track_point_in_utc_time,
17+
)
1318
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
19+
from homeassistant.util import utcnow
20+
from onyx_client.data.animation_value import AnimationValue
1421
from onyx_client.enum.action import Action
1522
from onyx_client.enum.device_type import DeviceType
1623
from onyx_client.data.numeric_value import NumericValue
1724

1825
from custom_components.hella_onyx.api_connector import APIConnector
26+
from custom_components.hella_onyx.const import INCREASED_INTERVAL_DELTA
1927
from custom_components.hella_onyx.sensors.onyx_entity import OnyxEntity
2028

2129
_LOGGER = logging.getLogger(__name__)
@@ -82,6 +90,8 @@ def brightness(self) -> int | None:
8290
self._uuid,
8391
brightness,
8492
)
93+
if brightness.animation is not None and len(brightness.animation.keyframes) > 0:
94+
self._start_update_device(brightness.animation)
8595
return brightness.value / brightness.maximum * 255
8696

8797
@property
@@ -137,6 +147,64 @@ def turn_off(self, **kwargs: Any) -> None:
137147
self.hass.loop,
138148
)
139149

150+
def _start_update_device(self, animation: AnimationValue):
151+
"""Start the update loop."""
152+
keyframes = len(animation.keyframes)
153+
keyframe = animation.keyframes[keyframes - 1]
154+
155+
current_time = time.time()
156+
end_time = animation.start + keyframe.duration + keyframe.delay
157+
delta = end_time - current_time
158+
moving = current_time < end_time
159+
160+
_LOGGER.debug(
161+
"updating device %s with current_time %s and end_time %s: %s",
162+
self._uuid,
163+
current_time,
164+
end_time,
165+
moving,
166+
)
167+
168+
if moving:
169+
track_point_in_utc_time(
170+
self.hass,
171+
self._end_update_device,
172+
utcnow() + timedelta(seconds=delta + INCREASED_INTERVAL_DELTA),
173+
)
174+
else:
175+
_LOGGER.debug("end update device %s due to too old data", self._uuid)
176+
self._end_update_device()
177+
178+
def _end_update_device(self, *args: Any):
179+
"""Call STOP to update the device values on ONYX."""
180+
animation = self._actual_brightness.animation
181+
keyframe = (
182+
animation.keyframes[len(animation.keyframes) - 1]
183+
if animation is not None and len(animation.keyframes) > 0
184+
else None
185+
)
186+
end_time = (
187+
(animation.start + keyframe.duration + keyframe.delay)
188+
if keyframe is not None
189+
else None
190+
)
191+
192+
current_time = time.time()
193+
194+
if current_time > end_time:
195+
_LOGGER.debug(
196+
"calling stop to force update device %s",
197+
self._uuid,
198+
)
199+
asyncio.run_coroutine_threadsafe(
200+
self.api.send_device_command_action(
201+
self._uuid,
202+
Action.STOP,
203+
),
204+
self.hass.loop,
205+
)
206+
self.async_write_ha_state()
207+
140208
@property
141209
def _actual_brightness(self) -> int:
142210
"""Get the actual brightness."""
@@ -146,6 +214,7 @@ def _actual_brightness(self) -> int:
146214
brightness.minimum,
147215
brightness.maximum,
148216
brightness.read_only,
217+
brightness.animation,
149218
)
150219

151220
def _get_dim_duration(self, target) -> int:

tests/sensors/test_light.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
from unittest.mock import MagicMock, patch
33

44
import pytest
5+
import time
6+
import pytz
7+
from datetime import datetime
58
from homeassistant.components.light import (
69
ColorMode,
710
brightness_supported,
811
)
912
from homeassistant.core import HomeAssistant
1013
from onyx_client.data.numeric_value import NumericValue
14+
from onyx_client.data.animation_value import AnimationValue
15+
from onyx_client.data.animation_keyframe import AnimationKeyframe
1116
from onyx_client.device.light import Light
1217
from onyx_client.enum.action import Action
1318
from onyx_client.enum.device_type import DeviceType
@@ -92,6 +97,29 @@ def test_brightness(self, api, entity, device):
9297
assert entity.brightness == 25.5
9398
assert api.device.called
9499

100+
def test_brightness_with_animation(self, api, entity, device):
101+
animation = AnimationValue(
102+
start=0,
103+
current_value=0,
104+
keyframes=[
105+
AnimationKeyframe(
106+
interpolation="linear", duration=10, delay=0, value=10
107+
)
108+
],
109+
)
110+
device.actual_brightness = NumericValue(
111+
value=10,
112+
minimum=0,
113+
maximum=100,
114+
read_only=False,
115+
animation=animation,
116+
)
117+
api.device.return_value = device
118+
with patch.object(entity, "_start_update_device") as mock_start_update_device:
119+
assert entity.brightness == 25.5
120+
mock_start_update_device.assert_called_with(animation)
121+
assert api.device.called
122+
95123
def test_is_on(self, api, entity, device):
96124
device.actual_brightness = NumericValue(
97125
value=10, minimum=0, maximum=100, read_only=False
@@ -198,3 +226,57 @@ def test__get_dim_duration_invalid_value(self, api, entity, device):
198226
api.device.return_value = device
199227
assert entity._get_dim_duration(90) == 5450
200228
assert api.device.called
229+
230+
@patch("asyncio.run_coroutine_threadsafe")
231+
def test_start_update_device_end(self, api, entity, device):
232+
current_time = time.mktime(datetime.now(pytz.timezone("UTC")).timetuple())
233+
animation = AnimationValue(
234+
start=current_time - 100,
235+
current_value=0,
236+
keyframes=[
237+
AnimationKeyframe(
238+
interpolation="linear",
239+
value=0,
240+
duration=10,
241+
delay=0,
242+
)
243+
],
244+
)
245+
with patch.object(entity, "_end_update_device") as mock_end_update_device:
246+
entity._start_update_device(animation)
247+
mock_end_update_device.assert_called()
248+
249+
@patch("asyncio.run_coroutine_threadsafe")
250+
def test__end_update_device(self, mock_run_coroutine_threadsafe, api, entity):
251+
entity._device.actual_brightness.animation = AnimationValue(
252+
time.time() - 1000, 10, [AnimationKeyframe("linear", 0, 100, 90)]
253+
)
254+
with patch.object(entity, "async_write_ha_state") as mock_async_write_ha_state:
255+
entity._end_update_device()
256+
assert mock_async_write_ha_state.called
257+
assert mock_run_coroutine_threadsafe.called
258+
api.send_device_command_action.assert_called_with("uuid", Action.STOP)
259+
260+
@patch("asyncio.run_coroutine_threadsafe")
261+
def test__end_update_device_within_time(
262+
self, mock_run_coroutine_threadsafe, api, entity
263+
):
264+
entity._device.actual_brightness.animation = AnimationValue(
265+
time.time(), 10, [AnimationKeyframe("linear", 0, 20000, 90)]
266+
)
267+
with patch.object(entity, "async_write_ha_state") as mock_async_write_ha_state:
268+
entity._end_update_device()
269+
assert mock_async_write_ha_state.called
270+
assert not mock_run_coroutine_threadsafe.called
271+
272+
@patch("asyncio.run_coroutine_threadsafe")
273+
def test__end_update_device_within_time_using_delay(
274+
self, mock_run_coroutine_threadsafe, api, entity
275+
):
276+
entity._device.actual_brightness.animation = AnimationValue(
277+
time.time() - 100, 10, [AnimationKeyframe("linear", 100000, 10, 90)]
278+
)
279+
with patch.object(entity, "async_write_ha_state") as mock_async_write_ha_state:
280+
entity._end_update_device()
281+
assert mock_async_write_ha_state.called
282+
assert not mock_run_coroutine_threadsafe.called

0 commit comments

Comments
 (0)