Skip to content

Commit

Permalink
Align itemwatcher timedelta with eascheduler
Browse files Browse the repository at this point in the history
  • Loading branch information
spacemanspiff2007 committed Nov 20, 2024
1 parent 66a57e3 commit f85504f
Show file tree
Hide file tree
Showing 14 changed files with 87 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repos:


- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.3
rev: v0.7.4
hooks:
- id: ruff
name: ruff unused imports
Expand Down
3 changes: 3 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ ignore = [

# https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
"RUF005", # Consider {expression} instead of concatenation

# https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt
"PT007", # Wrong values type in @pytest.mark.parametrize expected {values} of {row}
]


Expand Down
14 changes: 7 additions & 7 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ Example
# Specify the location where your HABApp instance is running
location:
# The coordinates are used to calculate the Sunrise/Sunset etc
latitude: 0.0
longitude: 0.0
elevation: 0.0
# The country and optional subdivision is used to calculate the holidays
country: DE # ISO 3166-1 Alpha-2 country code - here Germany
subdivision: BE # ISO 3166-2 Subdivision code or alias - here Berlin
# The coordinates are used to calculate the Sunrise/Sunset etc
latitude: 0.0
longitude: 0.0
elevation: 0.0
# The country and optional subdivision is used to calculate the holidays
country: DE # ISO 3166-1 Alpha-2 country code - here Germany
subdivision: BE # ISO 3166-2 Subdivision code or alias - here Berlin
openhab:
Expand Down
2 changes: 1 addition & 1 deletion requirements_setup.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ watchdog == 6.0.0
ujson == 5.10.0
aiomqtt == 2.3.0

eascheduler == 0.2.0
eascheduler == 0.2.1

immutables == 0.21
easyconfig == 0.3.2
Expand Down
4 changes: 2 additions & 2 deletions run/conf_testing/lib/HABAppTests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@ def find_astro_sun_thing() -> str:


def get_bytes_text(value):
if isinstance(value, bytes) and len(value) > 100 * 1024:
return b2a_hex(value[0:40]).decode() + ' ... ' + b2a_hex(value[-40:]).decode()
if isinstance(value, bytes) and len(value) > 300:
return b2a_hex(value[:40]).decode() + ' ... ' + b2a_hex(value[-40:]).decode()
return value
10 changes: 5 additions & 5 deletions src/HABApp/core/items/base_item.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from eascheduler.builder.helper import HINT_POS_TIMEDELTA, get_pos_timedelta_secs
from whenever import Instant

from HABApp.core.const.hints import TYPE_EVENT_CALLBACK
Expand All @@ -10,7 +11,6 @@
)
from HABApp.core.internals.item_registry import ItemRegistryItem
from HABApp.core.lib import InstantView
from HABApp.core.lib.parameters import TH_POSITIVE_TIME_DIFF, get_positive_time_diff

from .base_item_times import ChangedTime, ItemNoChangeWatch, ItemNoUpdateWatch, UpdatedTime
from .tmp_data import add_tmp_data as _add_tmp_data
Expand Down Expand Up @@ -63,24 +63,24 @@ def __repr__(self) -> str:
ret += f'{", " if ret else ""}{k}: {getattr(self, k)}'
return f'<{self.__class__.__name__} {ret:s}>'

def watch_change(self, secs: TH_POSITIVE_TIME_DIFF) -> ItemNoChangeWatch:
def watch_change(self, secs: HINT_POS_TIMEDELTA) -> ItemNoChangeWatch:
"""Generate an event if the item does not change for a certain period of time.
Has to be called from inside a rule function.
:param secs: secs after which the event will occur, max 1 decimal digit for floats
:return: The watch obj which can be used to cancel the watch
"""
secs = get_positive_time_diff(secs, round_digits=1)
secs = round(get_pos_timedelta_secs(secs), 1)
return self._last_change.add_watch(secs)

def watch_update(self, secs: TH_POSITIVE_TIME_DIFF) -> ItemNoUpdateWatch:
def watch_update(self, secs: HINT_POS_TIMEDELTA) -> ItemNoUpdateWatch:
"""Generate an event if the item does not receive and update for a certain period of time.
Has to be called from inside a rule function.
:param secs: secs after which the event will occur, max 1 decimal digit for floats
:return: The watch obj which can be used to cancel the watch
"""
secs = get_positive_time_diff(secs, round_digits=1)
secs = round(get_pos_timedelta_secs(secs), 1)
return self._last_update.add_watch(secs)

def listen_event(self, callback: TYPE_EVENT_CALLBACK,
Expand Down
9 changes: 4 additions & 5 deletions src/HABApp/core/lib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from . import parameters
from .exceptions import HINT_EXCEPTION, format_exception
from .funcs import list_files, sort_files
from .instant_view import InstantView
from .pending_future import PendingFuture
from .single_task import SingleTask
from .rgb_hsv import hsb_to_rgb, rgb_to_hsb
from .exceptions import format_exception, HINT_EXCEPTION
from .priority_list import PriorityList
from .rgb_hsv import hsb_to_rgb, rgb_to_hsb
from .single_task import SingleTask
from .timeout import Timeout, TimeoutNotRunningError
from .value_change import ValueChange
from .instant_view import InstantView
62 changes: 45 additions & 17 deletions src/HABApp/core/lib/instant_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,48 @@
from collections.abc import Callable


HINT_OBJ: TypeAlias = dt_timedelta | TimeDelta | int | str | Instant


class InstantView:
__slots__ = ('_instant',)
__slots__ = ('_instant', )

def __init__(self, instant: Instant) -> None:
self._instant: Final = instant

def delta(self, now: Instant | None = None) -> TimeDelta:
"""Return the delta between the instant and now
@classmethod
def now(cls) -> InstantView:
"""Create a new instance with the current time"""
return cls(Instant.now())

:param now: optional instant to compare to
def delta_now(self, now: InstantView | Instant | None = None) -> TimeDelta:
"""Return the delta between now and the instant.
The delta will be positive, e.g.
if the InstantView is from 5 seconds ago this will return a timedelta with 5 seconds.
:param now: optional instant to compare to instead of now, must be newer than the instant of the instant view
"""

if now is None:
now = Instant.now()
return now - self._instant
match now:
case None:
ref = Instant.now()
case InstantView():
ref = now._instant
case Instant():
ref = now
case _:
msg = f'Invalid type: {type(now).__name__}'
raise TypeError(msg)

if ref < self._instant:
msg = f'Reference instant must be newer than the instant of the {self.__class__.__name__}'
raise ValueError(msg)

return ref - self._instant

def py_timedelta(self, now: Instant | None = None) -> dt_timedelta:
def py_timedelta(self, now: InstantView | Instant | None = None) -> dt_timedelta:
"""Return the timedelta between the instant and now
:param now: optional instant to compare to
"""
return self.delta(now).py_timedelta()
return self.delta_now(now).py_timedelta()

def py_datetime(self) -> dt_datetime:
"""Return the datetime of the instant"""
Expand All @@ -46,6 +63,9 @@ def __repr__(self) -> str:
return f'InstantView({self._instant.to_system_tz()})'

def _cmp(self, op: Callable[[Any, Any], bool], obj: HINT_OBJ | None, **kwargs: float) -> bool:

td: TimeDelta | None = None

match obj:
case None:
if days := kwargs.get('days', 0):
Expand All @@ -59,14 +79,19 @@ def _cmp(self, op: Callable[[Any, Any], bool], obj: HINT_OBJ | None, **kwargs: f
td = TimeDelta(seconds=obj)
case str():
td = TimeDelta.parse_common_iso(obj)
case Instant():

if td is None:
if isinstance(obj, InstantView):
obj = obj._instant
if isinstance(obj, Instant):
# If the compare the instant the logic is the other way around
# view > delta(3) -> view older than 3 seconds
# view > instant(-3) -> view newer than 3 seconds
# view > instant(-3) -> view newer than the instant 3 seconds ago
return {ge: le, gt: lt, le: ge, lt: gt}[op](self._instant, obj)
case _:
msg = f'Invalid type: {type(obj).__name__}'
raise TypeError(msg)

if td is None:
msg = f'Invalid type: {type(obj).__name__}'
raise TypeError(msg)

if td <= TimeDelta.ZERO:
msg = 'Delta must be positive since instant is in the past'
Expand Down Expand Up @@ -110,3 +135,6 @@ def __eq__(self, other: InstantView | Instant) -> bool:
if isinstance(other, Instant):
return self._instant == other
return NotImplemented


HINT_OBJ: TypeAlias = dt_timedelta | TimeDelta | int | str | Instant | InstantView
1 change: 0 additions & 1 deletion src/HABApp/core/lib/parameters/__init__.py

This file was deleted.

22 changes: 0 additions & 22 deletions src/HABApp/core/lib/parameters/positive_time_diff.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ async def cleanup_items(keep_items: set[str]):
all_items = await async_get_items()

to_delete: dict[str, HABAppThingPluginData] = {}
for cfg in filter(_filter_items, all_items):
for cfg in filter(_filter_items, all_items): # type: ItemResp
name = cfg.name
if name not in keep_items:
to_delete[name] = cfg['metadata']['HABApp']
to_delete[name] = cfg.metadata['HABApp']

if not to_delete:
return None
Expand Down
7 changes: 4 additions & 3 deletions src/HABApp/util/listener_groups/listener_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
if TYPE_CHECKING:
from collections.abc import Callable, Iterable

from eascheduler.builder.helper import HINT_POS_TIMEDELTA

from HABApp.core.internals import EventFilterBase
from HABApp.core.lib.parameters import TH_POSITIVE_TIME_DIFF


class ListenerCreatorNotFoundError(Exception):
Expand Down Expand Up @@ -130,7 +131,7 @@ def add_listener(self, item: BaseItem | Iterable[BaseItem], callback: Callable[[
return self

def add_no_update_watcher(self, item: BaseItem | Iterable[BaseItem], callback: Callable[[Any], Any],
seconds: TH_POSITIVE_TIME_DIFF, alias: str | None = None
seconds: HINT_POS_TIMEDELTA, alias: str | None = None
) -> EventListenerGroup:
"""Add an no update watcher to the group. On ``listen`` this will create a no update watcher and
the corresponding event listener that will trigger the callback
Expand All @@ -146,7 +147,7 @@ def add_no_update_watcher(self, item: BaseItem | Iterable[BaseItem], callback: C
return self

def add_no_change_watcher(self, item: BaseItem | Iterable[BaseItem], callback: Callable[[Any], Any],
seconds: TH_POSITIVE_TIME_DIFF, alias: str | None = None
seconds: HINT_POS_TIMEDELTA, alias: str | None = None
) -> EventListenerGroup:
"""Add a no change watcher to the group. On ``listen`` this will create a no change watcher and
the corresponding event listener that will trigger the callback
Expand Down
2 changes: 1 addition & 1 deletion tests/test_core/test_item_watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async def test_multiple_add(parent_rule: DummyRule, test_logs: LogCollector) ->
assert w1 is not w2
w2.fut.cancel()

test_logs.add_ignored('HABApp', 'WARNING', 'Watcher ItemNoChangeWatch (5s) for test has already been created')
test_logs.add_ignored('HABApp', 'WARNING', 'Watcher ItemNoChangeWatch (5.0s) for test has already been created')

await asyncio.sleep(0.01)

Expand Down
13 changes: 12 additions & 1 deletion tests/test_core/test_lib/test_instant_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ def test_cmp_obj(view: InstantView) -> None:
assert view > 'PT59S'
assert view > 59

assert view < Instant.now()
assert view < InstantView.now()
assert view == Instant.now().subtract(minutes=1)


def test_cmp_funcs(view: InstantView) -> None:
assert view.older_than(seconds=59)
Expand All @@ -44,9 +48,16 @@ def test_cmp_funcs(view: InstantView) -> None:


def test_delta_funcs(view: InstantView) -> None:
assert view.delta() == seconds(60)
assert view.delta_now() == seconds(60)
assert view.py_timedelta() == dt_timedelta(seconds=60)

assert view.delta_now(Instant.now()) == seconds(60)
assert view.delta_now(InstantView.now()) == seconds(60)

with pytest.raises(ValueError) as e:
view.delta_now(Instant.now().subtract(minutes=2))
assert str(e.value) == 'Reference instant must be newer than the instant of the InstantView'


def test_convert() -> None:
s = SystemDateTime(2021, 1, 2, 10, 11, 12)
Expand Down

0 comments on commit f85504f

Please sign in to comment.