diff --git a/docs/util.rst b/docs/util.rst index 5f18a0cb..1499c06c 100644 --- a/docs/util.rst +++ b/docs/util.rst @@ -162,6 +162,30 @@ Example # It's possible to get statistics about the limiter and the corresponding windows print(limiter.info()) + # There is a counter that keeps track of the total skips that can be reset + print('Counter:') + print(limiter.total_skips) + limiter.reset() # Can be reset + print(limiter.total_skips) + + +Recommendation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Limiting external requests to an external API works well with the leaky bucket algorithm (maybe with some initial hits). +For limiting notifications the best results can be achieved by combining both algorithms. +Fixed window elastic expiry will notify but block until an issue is resolved, +that's why it's more suited for small intervals. Leaky bucket will allow hits even while the issue persists, +that's why it's more suited for larger intervals. + +.. exec_code:: + + from HABApp.util import RateLimiter + + limiter = RateLimiter('MyNotifications') + limiter.parse_limits('5 in 1 minute', algorithm='fixed_window_elastic_expiry') + limiter.parse_limits("20 in 1 hour", algorithm='leaky_bucket') + Documentation ^^^^^^^^^^^^^^^^^^ diff --git a/readme.md b/readme.md index ba865437..42badb74 100644 --- a/readme.md +++ b/readme.md @@ -127,7 +127,7 @@ MyOpenhabRule() ``` # Changelog -#### 23.12.0-DEV (2023-XX-XX) +#### 24.01.0-DEV (2024-XX-XX) - Added HABApp.util.RateLimiter - Added CompressedMidnightRotatingFileHandler - Updated dependencies diff --git a/run/conf_testing/rules/openhab/test_persistence.py b/run/conf_testing/rules/openhab/test_persistence.py index 983d204f..e5d5cb0a 100644 --- a/run/conf_testing/rules/openhab/test_persistence.py +++ b/run/conf_testing/rules/openhab/test_persistence.py @@ -1,11 +1,16 @@ from __future__ import annotations from datetime import datetime, timedelta -from typing import Final, Any +from typing import TYPE_CHECKING, Any, Final -from HABApp.openhab.definitions.helpers import OpenhabPersistenceData +from HABAppTests import ItemWaiter, TestBaseRule + +from HABApp.core.connections import Connections from HABApp.openhab.items import NumberItem -from HABAppTests import TestBaseRule + + +if TYPE_CHECKING: + from HABApp.openhab.definitions.helpers import OpenhabPersistenceData class TestPersistenceBase(TestBaseRule): @@ -20,6 +25,9 @@ def __init__(self, service_name: str, item_name: str): def set_up(self): i = NumberItem.get_item(self.item_name) + if i.value is None: + i.oh_post_update(0) + ItemWaiter(self.item_name).wait_for_state(0) i.oh_post_update(int(i.value) + 1 if i.value < 10 else 0) def test_service_available(self): @@ -64,3 +72,30 @@ def test_get(self): TestMapDB() + + +class TestInMemory(TestPersistenceBase): + + def __init__(self): + super().__init__('inmemory', 'RRD4J_Item') + + if Connections.get('openhab').context.version >= (4, 1): + self.add_test('InMemory', self.test_in_memory) + else: + print('Skip "TestInMemory" because of no InMemoryDb') + + def test_in_memory(self): + now = datetime.now().replace(microsecond=0) + t1 = now - timedelta(milliseconds=100) + t2 = now + timedelta(milliseconds=100) + + self.set_persistence_data(t1, 5) + self.set_persistence_data(now, 6) + self.set_persistence_data(t2, 7) + value = self.get_persistence_data(now - timedelta(milliseconds=200), now + timedelta(milliseconds=200)) + + objs = value.get_data() + assert objs == {t1.timestamp(): 5, now.timestamp(): 6, t2.timestamp(): 7} + + +TestInMemory() diff --git a/src/HABApp/__version__.py b/src/HABApp/__version__.py index b0cd1b76..bae0faf9 100644 --- a/src/HABApp/__version__.py +++ b/src/HABApp/__version__.py @@ -10,4 +10,4 @@ # Development versions contain the DEV-COUNTER postfix: # - 23.09.0.DEV-1 -__version__ = '23.12.0.DEV-1' +__version__ = '24.01.0.DEV-1' diff --git a/src/HABApp/mqtt/connection/handler.py b/src/HABApp/mqtt/connection/handler.py index ebb63b94..4c0be234 100644 --- a/src/HABApp/mqtt/connection/handler.py +++ b/src/HABApp/mqtt/connection/handler.py @@ -72,6 +72,9 @@ async def on_disconnected(self, connection: MqttConnection, context: CONTEXT_TYP assert context is not None connection.log.info('Disconnected') + # remove this check when https://github.com/sbtinstruments/aiomqtt/pull/249 gets merged + if not context._lock.locked(): + await context._lock.acquire() await context.__aexit__(None, None, None) diff --git a/src/HABApp/openhab/connection/connection.py b/src/HABApp/openhab/connection/connection.py index 00a3d825..54bf6ee5 100644 --- a/src/HABApp/openhab/connection/connection.py +++ b/src/HABApp/openhab/connection/connection.py @@ -33,6 +33,8 @@ class OpenhabContext: session: aiohttp.ClientSession session_options: dict[str, Any] + workaround_small_floats: bool + CONTEXT_TYPE: TypeAlias = Optional[OpenhabContext] diff --git a/src/HABApp/openhab/connection/handler/handler.py b/src/HABApp/openhab/connection/handler/handler.py index 2bba83e7..6958547c 100644 --- a/src/HABApp/openhab/connection/handler/handler.py +++ b/src/HABApp/openhab/connection/handler/handler.py @@ -172,7 +172,9 @@ async def on_connecting(self, connection: OpenhabConnection): version=vers, is_oh3=vers < (4, 0), waited_for_openhab=False, created_items={}, created_things={}, - session=self.session, session_options=self.options + session=self.session, session_options=self.options, + + workaround_small_floats=vers < (4, 1) ) # during startup we get OpenhabCredentialsInvalidError even though credentials are correct diff --git a/src/HABApp/openhab/connection/handler/helper.py b/src/HABApp/openhab/connection/handler/helper.py index eccf3fc7..07a6a2a4 100644 --- a/src/HABApp/openhab/connection/handler/helper.py +++ b/src/HABApp/openhab/connection/handler/helper.py @@ -4,14 +4,17 @@ from typing import Any from HABApp.core.items import BaseValueItem -from HABApp.core.types import RGB, HSB +from HABApp.core.types import HSB, RGB -def convert_to_oh_type(obj: Any) -> str: +def convert_to_oh_type(obj: Any, scientific_floats=False) -> str: if isinstance(obj, (str, int, bool)): return str(obj) if isinstance(obj, float): + if scientific_floats: + return str(obj) + v = str(obj) if 'e-' not in v: return v diff --git a/src/HABApp/openhab/connection/plugins/out.py b/src/HABApp/openhab/connection/plugins/out.py index eaa46fb4..21a2c2e3 100644 --- a/src/HABApp/openhab/connection/plugins/out.py +++ b/src/HABApp/openhab/connection/plugins/out.py @@ -75,6 +75,8 @@ async def queue_worker(self): queue: Final = self.queue to_str: Final = convert_to_oh_type + scientific_floats = not self.plugin_connection.context.workaround_small_floats + while True: try: while True: @@ -84,7 +86,7 @@ async def queue_worker(self): item = item._name if not isinstance(state, str): - state = to_str(state) + state = to_str(state, scientific_floats=scientific_floats) if is_cmd: await post(f'/rest/items/{item:s}', data=state) diff --git a/src/HABApp/openhab/definitions/helpers/persistence_data.py b/src/HABApp/openhab/definitions/helpers/persistence_data.py index 03e78163..5cbc5613 100644 --- a/src/HABApp/openhab/definitions/helpers/persistence_data.py +++ b/src/HABApp/openhab/definitions/helpers/persistence_data.py @@ -3,6 +3,7 @@ from HABApp.openhab.definitions.rest import ItemHistoryResp + OPTIONAL_DT = Optional[datetime] diff --git a/src/HABApp/util/rate_limiter/limiter.py b/src/HABApp/util/rate_limiter/limiter.py index c34a092a..7b6aaead 100644 --- a/src/HABApp/util/rate_limiter/limiter.py +++ b/src/HABApp/util/rate_limiter/limiter.py @@ -33,7 +33,7 @@ def __init__(self, name: str): @property def total_skips(self) -> int: - """A user counter to track skips which can be manually reset""" + """A counter to track skips which can be manually reset""" return self._skips_total def __repr__(self): @@ -146,7 +146,7 @@ def info(self) -> 'LimiterInfo': ) def reset(self) -> 'Limiter': - """Reset the manual skip counter""" + """Reset the skip counter""" self._skips_total = 0 return self diff --git a/src/HABApp/util/rate_limiter/limits/leaky_bucket.py b/src/HABApp/util/rate_limiter/limits/leaky_bucket.py index ed91eeb9..fe39bb2e 100644 --- a/src/HABApp/util/rate_limiter/limits/leaky_bucket.py +++ b/src/HABApp/util/rate_limiter/limits/leaky_bucket.py @@ -15,7 +15,7 @@ def __init__(self, allowed: int, interval: int, hits: int = 0): super().__init__(allowed, interval, hits) self.drop_interval: Final = interval / allowed - self.next_drop: float = -1.0 + self.next_drop: float = monotonic() + self.drop_interval def repr_text(self): return f'drop_interval={self.drop_interval:.1f}s' diff --git a/tests/test_openhab/test_items/test_commands.py b/tests/test_openhab/test_items/test_commands.py index 88031b67..99de6c8c 100644 --- a/tests/test_openhab/test_items/test_commands.py +++ b/tests/test_openhab/test_items/test_commands.py @@ -13,7 +13,7 @@ def test_OnOff(cls): c = cls('item_name') assert not c.is_on() - if not __version__.startswith('23.12.0'): + if not __version__.startswith('24.01.0'): assert not c.is_off() c.set_value(OnOffValue('ON')) diff --git a/tests/test_openhab/test_plugins/test_load_items.py b/tests/test_openhab/test_plugins/test_load_items.py index 8a124adf..d548da7d 100644 --- a/tests/test_openhab/test_plugins/test_load_items.py +++ b/tests/test_openhab/test_plugins/test_load_items.py @@ -78,7 +78,9 @@ async def test_item_sync(monkeypatch, ir: ItemRegistry, test_logs): waited_for_openhab=False, created_items={}, created_things={}, - session=None, session_options=None + session=None, session_options=None, + + workaround_small_floats=False ) # initial item create await LoadOpenhabItemsPlugin().on_connected(context)