-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
225 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
"""ONYX API event thread.""" | ||
import asyncio | ||
import logging | ||
import threading | ||
from random import uniform | ||
|
||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator | ||
|
||
from .api_connector import APIConnector | ||
from .const import MAX_BACKOFF_TIME | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class EventThread(threading.Thread): | ||
"""The event thread for asynchronous updates.""" | ||
|
||
def __init__( | ||
self, | ||
api: APIConnector, | ||
coordinator: DataUpdateCoordinator, | ||
force_update: bool = False, | ||
backoff=True, | ||
): | ||
threading.Thread.__init__(self, name="HellaOnyx") | ||
self._api = api | ||
self._coordinator = coordinator | ||
self._force_update = force_update | ||
self._backoff = backoff | ||
|
||
async def _update(self): | ||
"""Listen for updates.""" | ||
while True: | ||
backoff = int(uniform(0, MAX_BACKOFF_TIME) * 60) | ||
try: | ||
async for device in self._api.events(self._force_update): | ||
self._api.updated_device(device) | ||
self._coordinator.async_set_updated_data(device) | ||
except Exception as ex: | ||
_LOGGER.error( | ||
"connection reset: %s, restarting with backoff of %s seconds (%s)", | ||
ex, | ||
backoff, | ||
self._backoff, | ||
) | ||
if self._backoff: | ||
await asyncio.sleep(backoff) | ||
else: | ||
break | ||
|
||
def run(self): | ||
"""Start the thread.""" | ||
loop = asyncio.new_event_loop() | ||
asyncio.set_event_loop(loop) | ||
loop.create_task(self._update()) | ||
loop.run_forever() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
"""Test for the EventThread.""" | ||
|
||
from unittest.mock import AsyncMock, patch | ||
|
||
import pytest | ||
from onyx_client.data.device_mode import DeviceMode | ||
from onyx_client.data.numeric_value import NumericValue | ||
from onyx_client.device.shutter import Shutter | ||
from onyx_client.enum.action import Action | ||
from onyx_client.enum.device_type import DeviceType | ||
|
||
from custom_components.hella_onyx import ( | ||
EventThread, | ||
) | ||
from custom_components.hella_onyx.api_connector import ( | ||
UnknownStateException, | ||
) | ||
from custom_components.hella_onyx.const import MAX_BACKOFF_TIME | ||
|
||
|
||
class TestEventThread: | ||
@pytest.fixture | ||
def api(self): | ||
yield MockAPI() | ||
|
||
@pytest.fixture | ||
def coordinator(self): | ||
yield AsyncMock() | ||
|
||
@pytest.fixture | ||
def thread(self, api, coordinator): | ||
yield EventThread(api, coordinator, force_update=False, backoff=False) | ||
|
||
@pytest.mark.asyncio | ||
async def test_update(self, thread, api, coordinator): | ||
api.called = False | ||
await thread._update() | ||
assert api.is_called | ||
assert not api.is_force_update | ||
assert coordinator.async_set_updated_data.called | ||
|
||
@pytest.mark.asyncio | ||
async def test_update_force_update(self, thread, api, coordinator): | ||
thread._force_update = True | ||
api.called = False | ||
await thread._update() | ||
assert api.is_called | ||
assert api.is_force_update | ||
assert coordinator.async_set_updated_data.called | ||
|
||
@pytest.mark.asyncio | ||
async def test_update_invalid_device(self, thread, api, coordinator): | ||
api.called = False | ||
api.fail_device = True | ||
await thread._update() | ||
assert api.is_called | ||
assert not api.is_force_update | ||
assert coordinator.async_set_updated_data.called | ||
|
||
@pytest.mark.asyncio | ||
async def test_update_none_device(self, thread, api, coordinator): | ||
api.called = False | ||
api.none_device = True | ||
await thread._update() | ||
assert api.is_called | ||
assert not api.is_force_update | ||
assert coordinator.async_set_updated_data.called | ||
|
||
@pytest.mark.asyncio | ||
async def test_update_connection_error(self, thread, api, coordinator): | ||
api.called = False | ||
api.fail = True | ||
await thread._update() | ||
assert api.is_called | ||
assert not api.is_force_update | ||
assert not coordinator.async_set_updated_data.called | ||
|
||
@pytest.mark.asyncio | ||
async def test_update_backoff(self, thread, api, coordinator): | ||
api.called = False | ||
|
||
async def sleep_called(backoff: int): | ||
assert backoff > 0 | ||
assert backoff / 60 < MAX_BACKOFF_TIME | ||
thread._backoff = False | ||
|
||
with patch("asyncio.sleep", new=sleep_called): | ||
thread._backoff = True | ||
api.fail = True | ||
assert thread._backoff | ||
await thread._update() | ||
assert api.is_called | ||
assert not api.is_force_update | ||
assert not thread._backoff | ||
assert not coordinator.async_set_updated_data.called | ||
|
||
|
||
class MockAPI: | ||
def __init__(self): | ||
self.called = False | ||
self.force_update = False | ||
self.fail = False | ||
self.fail_device = False | ||
self.none_device = False | ||
|
||
@property | ||
def is_called(self): | ||
return self.called | ||
|
||
@property | ||
def is_force_update(self): | ||
return self.force_update | ||
|
||
def device(self, uuid: str): | ||
self.called = True | ||
if self.none_device: | ||
return None | ||
if self.fail_device: | ||
raise UnknownStateException("ERROR") | ||
numeric = NumericValue(10, 10, 10, False, None) | ||
return Shutter( | ||
"uuid", "name", None, None, None, None, numeric, numeric, numeric | ||
) | ||
|
||
def updated_device(self, device): | ||
self.called = True | ||
|
||
async def events(self, force_update: bool): | ||
self.called = True | ||
self.force_update = force_update | ||
if self.fail: | ||
raise NotImplementedError() | ||
yield Shutter( | ||
"uuid", | ||
"other", | ||
DeviceType.RAFFSTORE_90, | ||
DeviceMode(DeviceType.RAFFSTORE_90), | ||
list(Action), | ||
) |