Skip to content

Commit

Permalink
- Added config for country
Browse files Browse the repository at this point in the history
- reworked last_change and last_update
  • Loading branch information
spacemanspiff2007 committed Oct 9, 2024
1 parent efd5984 commit 40eeb51
Show file tree
Hide file tree
Showing 15 changed files with 221 additions and 26 deletions.
10 changes: 10 additions & 0 deletions docs/class_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,13 @@ ItemNoChangeWatch
:members:
:inherited-members:
:member-order: groupwise



InstantView
======================================

.. autoclass:: HABApp.core.lib.InstantView
:members:
:inherited-members:
:member-order: groupwise
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ def log(msg: str) -> None:
'canonical_url': '',
# 'analytics_id': 'UA-XXXXXXX-1', # Provided by Google in your dashboard
'logo_only': False,
'display_version': True,
'prev_next_buttons_location': 'bottom',
'style_external_links': False,
# 'vcs_pageview_mode': '',
Expand Down Expand Up @@ -338,7 +337,8 @@ def setup(app) -> None:
# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html
if IS_RTD_BUILD:
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None)
'python': ('https://docs.python.org/3', None),
'whenever': ('https://whenever.readthedocs.io/en/stable', None)
}

# Don't show warnings for missing python references since these are created via intersphinx during the RTD build
Expand Down
25 changes: 20 additions & 5 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,24 +171,30 @@ All items have two additional timestamps set which can be used to simplify rule
* The time when the item was last updated
* The time when the item was last changed.

It's possible to compare these values directly with deltas without having to do calculations withs timestamps


.. exec_code::

# ------------ hide: start ------------
from whenever import Instant
from whenever import Instant, patch_current_time
from HABApp.core.items import Item
from rule_runner import SimpleRuleRunner

runner = SimpleRuleRunner()
runner.set_up()

item = Item.get_create_item('Item_Name', initial_value='old_value')
item = Item.get_create_item('Item_Name', initial_value='value')
item._last_change.instant = Instant.from_utc(2024, 4, 30, 10, 30)
item._last_update.instant = Instant.from_utc(2024, 4, 30, 12, 16)
item._last_update.instant = Instant.from_utc(2024, 4, 30, 10, 31)

p = patch_current_time(item._last_update.instant.add(minutes=1), keep_ticking=False)
p.__enter__()

# ------------ hide: stop -------------
import HABApp
from HABApp.core.items import Item
from HABApp.rule.scheduler import minutes, seconds

class TimestampRule(HABApp.Rule):
def __init__(self):
Expand All @@ -197,12 +203,21 @@ All items have two additional timestamps set which can be used to simplify rule
self.my_item = Item.get_item('Item_Name')

# Access of timestamps
print(f'Last update: {self.my_item.last_update}')
print(f'Last change: {self.my_item.last_change}')

# It's possible to compare directly with the most common (time-) deltas through the operator
if self.my_item.last_update >= minutes(1):
print('Item was updated in the last minute')

# There are also functions available which support both building the delta directly and using an object
if self.my_item.last_change.newer_than(minutes=2, seconds=30):
print('Item was changed in the last 1min 30s')
if self.my_item.last_change.older_than(seconds(30)):
print('Item was changed before 30s')

TimestampRule()

# ------------ hide: start ------------
p.__exit__(None, None, None)
runner.tear_down()
# ------------ hide: stop -------------

Expand Down
6 changes: 3 additions & 3 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Packages required to build the documentation
sphinx == 7.4.7
sphinx-autodoc-typehints == 2.3.0
sphinx_rtd_theme == 2.0.0
sphinx == 8.0.2
sphinx-autodoc-typehints == 2.5.0
sphinx_rtd_theme == 3.0.0
sphinx-exec-code == 0.12
autodoc_pydantic == 2.2.0
sphinx-copybutton == 0.5.2
3 changes: 3 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ Changelog:

Migration of rules:
- Search for ``self.run.at`` and replace with ``self.run.once``
- ``item.last_update`` and ``item.last_change`` can now directly used to check if it's newer/older than a delta.
Replace ``item.last_update > datetime_obj`` with ``item.last_update > timedelta_obj`` or
``item.last_update.newer_than(minutes=10)``

#### 24.08.1 (2024-08-02)
- Fixed a possible infinite loop during thing re-sync
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
# -----------------------------------------------------------------------------
# Packages for source formatting
# -----------------------------------------------------------------------------
pre-commit == 3.8.0
ruff == 0.6.8
pre-commit == 4.0.1
ruff == 0.6.9
autotyping == 24.9.0
# -----------------------------------------------------------------------------
# Packages for other developement tasks
Expand Down
2 changes: 1 addition & 1 deletion requirements_setup.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
aiohttp == 3.10.8
aiohttp == 3.10.9
pydantic == 2.9.2
msgspec == 0.18.6
bidict == 0.23.1
Expand Down
9 changes: 5 additions & 4 deletions src/HABApp/core/items/base_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
uses_item_registry,
)
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
Expand Down Expand Up @@ -46,18 +47,18 @@ def __init__(self, name: str) -> None:
self._last_update: UpdatedTime = UpdatedTime(self._name, _now)

@property
def last_change(self) -> dt_datetime:
def last_change(self) -> InstantView:
"""
:return: Timestamp of the last time when the item has been changed (read only)
"""
return self._last_change.instant.to_system_tz().local().py_datetime()
return InstantView(self._last_change.instant)

@property
def last_update(self) -> dt_datetime:
def last_update(self) -> InstantView:
"""
:return: Timestamp of the last time when the item has been updated (read only)
"""
return self._last_update.instant.to_system_tz().local().py_datetime()
return InstantView(self._last_update.instant)

def __repr__(self) -> str:
ret = ''
Expand Down
1 change: 1 addition & 0 deletions src/HABApp/core/lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .priority_list import PriorityList
from .timeout import Timeout, TimeoutNotRunningError
from .value_change import ValueChange
from .instant_view import InstantView
109 changes: 109 additions & 0 deletions src/HABApp/core/lib/instant_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from __future__ import annotations

from datetime import datetime as dt_datetime
from datetime import timedelta as dt_timedelta
from operator import ge, gt, le, lt
from typing import TYPE_CHECKING, Any, Final, TypeAlias, overload

from whenever import Instant, TimeDelta


if TYPE_CHECKING:
from collections.abc import Callable


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


class InstantView:
__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
:param now: optional instant to compare to
"""

if now is None:
now = Instant.now()
return now - self._instant

def py_timedelta(self, now: 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()

def py_datetime(self) -> dt_datetime:
"""Return the datetime of the instant"""
return self._instant.to_system_tz().local().py_datetime()

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:
match obj:
case None:
if days := kwargs.get('days', 0):
kwargs['hours'] = kwargs.get('hours', 0) + days * 24
td = TimeDelta(**kwargs)
case TimeDelta():
td = obj
case dt_timedelta():
td = TimeDelta.from_py_timedelta(obj)
case int():
td = TimeDelta(seconds=obj)
case str():
td = TimeDelta.parse_common_iso(obj)
case Instant():
return op(self._instant, obj)
case _:
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'
raise ValueError(msg)

return op(self._instant, Instant.now() - td)

@overload
def older_than(self, *, days: float = 0, hours: float = 0, minutes: float = 0, seconds: float = 0) -> bool: ...
@overload
def older_than(self, obj: HINT_OBJ) -> bool: ...

def older_than(self, obj=None, **kwargs):
"""Check if the instant is older than the given value"""
return self._cmp(lt, obj, **kwargs)

@overload
def newer_than(self, *, days: float = 0, hours: float = 0, minutes: float = 0, seconds: float = 0) -> bool: ...
@overload
def newer_than(self, obj: HINT_OBJ) -> bool: ...

def newer_than(self, obj=None, **kwargs):
"""Check if the instant is newer than the given value"""
return self._cmp(gt, obj, **kwargs)

def __lt__(self, other: HINT_OBJ) -> bool:
return self._cmp(lt, other)

def __le__(self, other: HINT_OBJ) -> bool:
return self._cmp(le, other)

def __gt__(self, other: HINT_OBJ) -> bool:
return self._cmp(gt, other)

def __ge__(self, other: HINT_OBJ) -> bool:
return self._cmp(ge, other)

def __eq__(self, other: InstantView | Instant) -> bool:
if isinstance(other, InstantView):
return self._instant == other._instant
if isinstance(other, Instant):
return self._instant == other
return NotImplemented
6 changes: 3 additions & 3 deletions src/HABApp/openhab/connection/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import HABApp
from HABApp.core.connections import AutoReconnectPlugin, BaseConnection, Connections, ConnectionStateToEventBusPlugin
from HABApp.core.items.base_valueitem import datetime

from HABApp.core.lib import InstantView

if TYPE_CHECKING:
from HABApp.openhab.items import OpenhabItem, Thing
Expand All @@ -23,8 +23,8 @@ class OpenhabContext:
# true when we waited during connect
waited_for_openhab: bool

created_items: dict[str, tuple[OpenhabItem, datetime]]
created_things: dict[str, tuple[Thing, datetime]]
created_items: dict[str, tuple[OpenhabItem, InstantView]]
created_things: dict[str, tuple[Thing, InstantView]]

session: aiohttp.ClientSession
session_options: dict[str, Any]
Expand Down
3 changes: 2 additions & 1 deletion src/HABApp/openhab/connection/plugins/load_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import HABApp.openhab.events
from HABApp.core.connections import BaseConnectionPlugin
from HABApp.core.internals import uses_item_registry
from HABApp.core.lib import InstantView
from HABApp.openhab.connection.connection import OpenhabConnection, OpenhabContext
from HABApp.openhab.connection.handler import map_null_str
from HABApp.openhab.connection.handler.func_async import async_get_all_items_state, async_get_items, async_get_things
Expand Down Expand Up @@ -99,7 +100,7 @@ async def load_items(self, context: OpenhabContext) -> None:

log.info(f'Updated {items_len:d} Items')

created_items: dict[str, tuple[OpenhabItem, datetime]] = {
created_items: dict[str, tuple[OpenhabItem, InstantView]] = {
i.name: (i, i.last_update) for i in Items.get_items() if isinstance(i, OpenhabItem)
}
context.created_items.update(created_items)
Expand Down
1 change: 1 addition & 0 deletions src/HABApp/rule/scheduler/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from eascheduler.builder import FilterBuilder as filter
from eascheduler.builder import TriggerBuilder as trigger
from eascheduler import add_holiday, get_holiday_name, get_holidays_by_name, get_sun_position, is_holiday, pop_holiday
from whenever import hours, minutes, seconds, milliseconds
10 changes: 5 additions & 5 deletions tests/test_core/test_items/item_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,23 @@ def test_time_value_change(self) -> None:

def test_time_funcs(self):
item = self.get_item()
now1 = datetime.now()

now1 = Instant.now()
time.sleep(0.000_001)

item.set_value(self.ITEM_VALUES[0])

# https://github.com/ariebovenberg/whenever/issues/171
time.sleep(0.000_001)
now2 = datetime.now()
now2 = Instant.now()
time.sleep(0.000_001)

assert now1 < item.last_change < now2, f'\n{now1}\n{item.last_change}\n{now2}'
assert now1 < item.last_update
assert now1 < item.last_update < now2, f'\n{now1}\n{item.last_update}\n{now2}'

item.set_value(self.ITEM_VALUES[0])

time.sleep(0.000_001)
now3 = datetime.now()
now3 = Instant.now()

assert now1 < item.last_change < now2
assert now2 < item.last_update < now3
Expand Down
54 changes: 54 additions & 0 deletions tests/test_core/test_lib/test_instant_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from datetime import timedelta as dt_timedelta

import pytest
from whenever import Instant, SystemDateTime, TimeDelta, patch_current_time, seconds

from HABApp.core.items.base_valueitem import datetime
from HABApp.core.lib.instant_view import InstantView


@pytest.fixture
def view():
now = Instant.now().subtract(minutes=1)
view = InstantView(now.subtract(minutes=1))

with patch_current_time(now, keep_ticking=False):
yield view


def test_methods(view: InstantView):
assert view < seconds(1)
assert not view < seconds(60)
assert view <= seconds(60)

assert view > seconds(61)
assert not view > seconds(60)
assert view >= seconds(60)


def test_cmp_obj(view: InstantView):
assert view < TimeDelta(seconds=59)
assert view < dt_timedelta(seconds=59)
assert view < 'PT59S'
assert view < 59


def test_cmp_funcs(view: InstantView):
assert view.older_than(seconds=59)
assert view.newer_than(seconds=61)


def test_delta_funcs(view: InstantView):
assert view.delta() == seconds(60)
assert view.py_timedelta() == dt_timedelta(seconds=60)


def test_convert():
s = SystemDateTime(2021, 1, 2, 10, 11, 12)
view = InstantView(s.instant())
assert view.py_datetime() == datetime(2021, 1, 2, 10, 11, 12)


def test_repr():
view = InstantView(SystemDateTime(2021, 1, 2, 10, 11, 12).instant())
assert str(view) == 'InstantView(2021-01-02T10:11:12+01:00)'

0 comments on commit 40eeb51

Please sign in to comment.