ucapi-framework 1.9.8__tar.gz → 1.9.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/PKG-INFO +1 -1
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/pyproject.toml +1 -1
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_device.py +181 -6
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/device.py +10 -1
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework.egg-info/PKG-INFO +1 -1
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/README.md +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/setup.cfg +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_button_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_climate_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_config.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_cover_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_discovery.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_driver.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_factory_pattern.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_helpers.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_ir_emitter_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_light_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_media_player_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_migration.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_remote_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_select_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_sensor_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_setup.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_switch_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/tests/test_voice_assistant_entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/__init__.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/config.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/discovery.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/driver.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/__init__.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/button.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/climate.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/cover.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/ir_emitter.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/light.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/media_player.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/remote.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/select.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/sensor.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/switch.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entities/voice_assistant.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/entity.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/helpers.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/migration.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework/setup.py +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework.egg-info/SOURCES.txt +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework.egg-info/dependency_links.txt +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework.egg-info/requires.txt +0 -0
- {ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework.egg-info/top_level.txt +0 -0
|
@@ -1582,6 +1582,27 @@ class TestExternalClientDevice:
|
|
|
1582
1582
|
|
|
1583
1583
|
await device.disconnect()
|
|
1584
1584
|
|
|
1585
|
+
@pytest.mark.asyncio
|
|
1586
|
+
@pytest.mark.parametrize("initially_connected", [False, True])
|
|
1587
|
+
async def test_connect_returns_false_while_watchdog_waits_to_reconnect(
|
|
1588
|
+
self, mock_device_config, initially_connected
|
|
1589
|
+
):
|
|
1590
|
+
"""A running watchdog must not imply a successful connection."""
|
|
1591
|
+
device = ConcreteExternalClientDevice(mock_device_config, watchdog_interval=60)
|
|
1592
|
+
device.connect_client_should_fail = not initially_connected
|
|
1593
|
+
|
|
1594
|
+
try:
|
|
1595
|
+
assert await device.connect() is initially_connected
|
|
1596
|
+
watchdog_task = device._watchdog_task
|
|
1597
|
+
device._mock_client_connected = False
|
|
1598
|
+
|
|
1599
|
+
assert await device.connect() is False
|
|
1600
|
+
assert device.is_connected is False
|
|
1601
|
+
assert device.connect_client_called == 1
|
|
1602
|
+
assert device._watchdog_task is watchdog_task
|
|
1603
|
+
finally:
|
|
1604
|
+
await device.disconnect()
|
|
1605
|
+
|
|
1585
1606
|
@pytest.mark.asyncio
|
|
1586
1607
|
async def test_connect_error_emits_error_event(self):
|
|
1587
1608
|
"""Test that connection errors emit ERROR event."""
|
|
@@ -1735,13 +1756,20 @@ class TestExternalClientDevice:
|
|
|
1735
1756
|
errors = []
|
|
1736
1757
|
device.events.on(DeviceEvents.ERROR, lambda *args: errors.append(args))
|
|
1737
1758
|
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
assert any("Max reconnection attempts reached" in str(e) for e in errors)
|
|
1759
|
+
try:
|
|
1760
|
+
# The watchdog must exit rather than start another retry batch.
|
|
1761
|
+
await asyncio.wait_for(device._watchdog_task, timeout=1)
|
|
1762
|
+
await asyncio.sleep(0.2)
|
|
1743
1763
|
|
|
1744
|
-
|
|
1764
|
+
assert device.connect_client_called == 3
|
|
1765
|
+
assert (
|
|
1766
|
+
errors.count(
|
|
1767
|
+
(device_config.identifier, "Max reconnection attempts reached")
|
|
1768
|
+
)
|
|
1769
|
+
== 1
|
|
1770
|
+
)
|
|
1771
|
+
finally:
|
|
1772
|
+
await device.disconnect()
|
|
1745
1773
|
|
|
1746
1774
|
@pytest.mark.asyncio
|
|
1747
1775
|
async def test_reconnect_infinite_attempts(self):
|
|
@@ -1903,3 +1931,150 @@ class TestExternalClientDevice:
|
|
|
1903
1931
|
assert device.connect_client_called == initial_connect_calls
|
|
1904
1932
|
|
|
1905
1933
|
await device.disconnect()
|
|
1934
|
+
|
|
1935
|
+
@pytest.mark.asyncio
|
|
1936
|
+
async def test_failed_initial_connect_keeps_retrying_until_the_device_is_reachable(
|
|
1937
|
+
self,
|
|
1938
|
+
):
|
|
1939
|
+
"""Test that a first connect that fails (e.g. network not up at boot) is retried by the watchdog."""
|
|
1940
|
+
device_config = Mock()
|
|
1941
|
+
device_config.identifier = "test-ext-17"
|
|
1942
|
+
device_config.name = "Test External Device"
|
|
1943
|
+
device_config.address = "192.168.1.100"
|
|
1944
|
+
|
|
1945
|
+
device = ConcreteExternalClientDevice(
|
|
1946
|
+
device_config,
|
|
1947
|
+
watchdog_interval=0.1,
|
|
1948
|
+
reconnect_delay=0.05,
|
|
1949
|
+
max_reconnect_attempts=0,
|
|
1950
|
+
)
|
|
1951
|
+
connected_events = []
|
|
1952
|
+
device.events.on(
|
|
1953
|
+
DeviceEvents.CONNECTED, lambda *args: connected_events.append(args)
|
|
1954
|
+
)
|
|
1955
|
+
|
|
1956
|
+
device.connect_client_should_fail = True
|
|
1957
|
+
assert await device.connect() is False
|
|
1958
|
+
assert device._is_connected is False
|
|
1959
|
+
|
|
1960
|
+
await asyncio.sleep(0.2)
|
|
1961
|
+
device.connect_client_should_fail = False
|
|
1962
|
+
await asyncio.sleep(0.4)
|
|
1963
|
+
|
|
1964
|
+
assert device._is_connected is True
|
|
1965
|
+
assert connected_events == [(device_config.identifier,)]
|
|
1966
|
+
|
|
1967
|
+
await device.disconnect()
|
|
1968
|
+
|
|
1969
|
+
@pytest.mark.asyncio
|
|
1970
|
+
async def test_failed_initial_connect_respects_the_reconnect_attempt_limit(self):
|
|
1971
|
+
"""Test that retries after a failed first connect stop at max_reconnect_attempts."""
|
|
1972
|
+
device_config = Mock()
|
|
1973
|
+
device_config.identifier = "test-ext-18"
|
|
1974
|
+
device_config.name = "Test External Device"
|
|
1975
|
+
device_config.address = "192.168.1.100"
|
|
1976
|
+
|
|
1977
|
+
device = ConcreteExternalClientDevice(
|
|
1978
|
+
device_config,
|
|
1979
|
+
watchdog_interval=0.05,
|
|
1980
|
+
reconnect_delay=0.01,
|
|
1981
|
+
max_reconnect_attempts=2,
|
|
1982
|
+
)
|
|
1983
|
+
errors = []
|
|
1984
|
+
device.events.on(DeviceEvents.ERROR, lambda *args: errors.append(args))
|
|
1985
|
+
|
|
1986
|
+
device.connect_client_should_fail = True
|
|
1987
|
+
await device.connect()
|
|
1988
|
+
try:
|
|
1989
|
+
await asyncio.wait_for(device._watchdog_task, timeout=1)
|
|
1990
|
+
await asyncio.sleep(0.1)
|
|
1991
|
+
|
|
1992
|
+
assert device.connect_client_called == 3
|
|
1993
|
+
assert (
|
|
1994
|
+
errors.count(
|
|
1995
|
+
(device_config.identifier, "Max reconnection attempts reached")
|
|
1996
|
+
)
|
|
1997
|
+
== 1
|
|
1998
|
+
)
|
|
1999
|
+
|
|
2000
|
+
# An explicit connect can start a new connection after giving up.
|
|
2001
|
+
device.connect_client_should_fail = False
|
|
2002
|
+
assert await device.connect() is True
|
|
2003
|
+
assert device.is_connected is True
|
|
2004
|
+
assert device.connect_client_called == 4
|
|
2005
|
+
assert not device._watchdog_task.done()
|
|
2006
|
+
finally:
|
|
2007
|
+
await device.disconnect()
|
|
2008
|
+
|
|
2009
|
+
@pytest.mark.asyncio
|
|
2010
|
+
async def test_failed_initial_connect_does_not_retry_when_reconnection_is_disabled(
|
|
2011
|
+
self,
|
|
2012
|
+
):
|
|
2013
|
+
"""Test that max_reconnect_attempts=None still means no retries after a failed first connect."""
|
|
2014
|
+
device_config = Mock()
|
|
2015
|
+
device_config.identifier = "test-ext-19"
|
|
2016
|
+
device_config.name = "Test External Device"
|
|
2017
|
+
device_config.address = "192.168.1.100"
|
|
2018
|
+
|
|
2019
|
+
device = ConcreteExternalClientDevice(
|
|
2020
|
+
device_config,
|
|
2021
|
+
watchdog_interval=0.05,
|
|
2022
|
+
reconnect_delay=0.01,
|
|
2023
|
+
max_reconnect_attempts=None,
|
|
2024
|
+
)
|
|
2025
|
+
|
|
2026
|
+
device.connect_client_should_fail = True
|
|
2027
|
+
await device.connect()
|
|
2028
|
+
device.connect_client_should_fail = False
|
|
2029
|
+
await asyncio.sleep(0.3)
|
|
2030
|
+
|
|
2031
|
+
assert device.connect_client_called == 1
|
|
2032
|
+
assert device._is_connected is False
|
|
2033
|
+
assert device._watchdog_task is None
|
|
2034
|
+
|
|
2035
|
+
@pytest.mark.asyncio
|
|
2036
|
+
async def test_failed_initial_connect_does_not_retry_when_the_watchdog_is_disabled(
|
|
2037
|
+
self,
|
|
2038
|
+
):
|
|
2039
|
+
"""Test that enable_watchdog=False still means no retries after a failed first connect."""
|
|
2040
|
+
device_config = Mock()
|
|
2041
|
+
device_config.identifier = "test-ext-20"
|
|
2042
|
+
device_config.name = "Test External Device"
|
|
2043
|
+
device_config.address = "192.168.1.100"
|
|
2044
|
+
|
|
2045
|
+
device = ConcreteExternalClientDevice(
|
|
2046
|
+
device_config, enable_watchdog=False, reconnect_delay=0.01
|
|
2047
|
+
)
|
|
2048
|
+
|
|
2049
|
+
device.connect_client_should_fail = True
|
|
2050
|
+
await device.connect()
|
|
2051
|
+
device.connect_client_should_fail = False
|
|
2052
|
+
await asyncio.sleep(0.2)
|
|
2053
|
+
|
|
2054
|
+
assert device.connect_client_called == 1
|
|
2055
|
+
assert device._is_connected is False
|
|
2056
|
+
assert device._watchdog_task is None
|
|
2057
|
+
|
|
2058
|
+
@pytest.mark.asyncio
|
|
2059
|
+
async def test_disconnect_stops_retries_after_a_failed_initial_connect(self):
|
|
2060
|
+
"""Test that disconnect() stops the retrying started by a failed first connect."""
|
|
2061
|
+
device_config = Mock()
|
|
2062
|
+
device_config.identifier = "test-ext-21"
|
|
2063
|
+
device_config.name = "Test External Device"
|
|
2064
|
+
device_config.address = "192.168.1.100"
|
|
2065
|
+
|
|
2066
|
+
device = ConcreteExternalClientDevice(
|
|
2067
|
+
device_config,
|
|
2068
|
+
watchdog_interval=0.05,
|
|
2069
|
+
reconnect_delay=0.01,
|
|
2070
|
+
max_reconnect_attempts=0,
|
|
2071
|
+
)
|
|
2072
|
+
|
|
2073
|
+
device.connect_client_should_fail = True
|
|
2074
|
+
await device.connect()
|
|
2075
|
+
await device.disconnect()
|
|
2076
|
+
calls_after_disconnect = device.connect_client_called
|
|
2077
|
+
await asyncio.sleep(0.2)
|
|
2078
|
+
|
|
2079
|
+
assert device.connect_client_called == calls_after_disconnect
|
|
2080
|
+
assert device._watchdog_task is None
|
|
@@ -1105,7 +1105,7 @@ class ExternalClientDevice(BaseDeviceInterface):
|
|
|
1105
1105
|
|
|
1106
1106
|
if self._watchdog_task and not self._watchdog_task.done():
|
|
1107
1107
|
_LOG.debug("[%s] Watchdog already running, skipping", self.log_id)
|
|
1108
|
-
return
|
|
1108
|
+
return False
|
|
1109
1109
|
|
|
1110
1110
|
_LOG.debug("[%s] Connecting via external client", self.log_id)
|
|
1111
1111
|
self.events.emit(DeviceEvents.CONNECTING, self.identifier)
|
|
@@ -1117,6 +1117,13 @@ class ExternalClientDevice(BaseDeviceInterface):
|
|
|
1117
1117
|
self._watchdog_task = asyncio.create_task(self._watchdog_loop())
|
|
1118
1118
|
return True
|
|
1119
1119
|
|
|
1120
|
+
# First connect failed (e.g. network not up yet at boot): let the
|
|
1121
|
+
# watchdog keep retrying instead of giving up for good.
|
|
1122
|
+
if self._enable_watchdog and self._max_reconnect_attempts is not None:
|
|
1123
|
+
_LOG.info("[%s] Initial connect failed, watchdog will retry", self.log_id)
|
|
1124
|
+
self._stop_watchdog.clear()
|
|
1125
|
+
self._watchdog_task = asyncio.create_task(self._watchdog_loop())
|
|
1126
|
+
|
|
1120
1127
|
return False
|
|
1121
1128
|
|
|
1122
1129
|
async def _connect_client_internal(self) -> bool:
|
|
@@ -1243,6 +1250,8 @@ class ExternalClientDevice(BaseDeviceInterface):
|
|
|
1243
1250
|
self.log_id,
|
|
1244
1251
|
self._max_reconnect_attempts,
|
|
1245
1252
|
)
|
|
1253
|
+
# Stop monitoring so the next watchdog tick cannot reset the limit.
|
|
1254
|
+
self._stop_watchdog.set()
|
|
1246
1255
|
self.events.emit(
|
|
1247
1256
|
DeviceEvents.ERROR,
|
|
1248
1257
|
self.identifier,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ucapi_framework-1.9.8 → ucapi_framework-1.9.9}/ucapi_framework.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|