diff --git a/readme.md b/readme.md index ae3c2634..245981e7 100644 --- a/readme.md +++ b/readme.md @@ -128,6 +128,10 @@ MyOpenhabRule() ``` # Changelog +#### 1.1.2 (2023-06-19) +- Re-added `ItemStateEventFilter` +- Improved parsing of `DateTime` values + #### 1.1.1 (2023-06-16) - Fixed a bug where the rule context was not found @@ -136,7 +140,7 @@ This is a breaking change! - Renamed `GroupItemStateChangedEvent` to `GroupStateChangedEvent` - Groups issue a `GroupStateUpdateEvent` when the state updates on OH3 (consistent with OH4 behavior) - Groups work now with `ValueUpdateEvent` and `ValueChangedEvent` as expected -- Renamed `ItemStateEvent` to `ItemStateUpdatedEvent` +- ~~Renamed `ItemStateEvent` to `ItemStateUpdatedEvent`~~ - Ignored ItemStateEvent on OH4 - Fewer warnings for long-running functions (execution of took too long) - `Thing` status and status_detail are now an Enum diff --git a/src/HABApp/__version__.py b/src/HABApp/__version__.py index 771d5726..e973e12f 100644 --- a/src/HABApp/__version__.py +++ b/src/HABApp/__version__.py @@ -1,4 +1,4 @@ # Version scheme: # X.X[.X] or X.X[.X].DEV-X -__version__ = '1.1.1' +__version__ = '1.1.2' diff --git a/src/HABApp/openhab/events/__init__.py b/src/HABApp/openhab/events/__init__.py index e35a7b63..ee939f6e 100644 --- a/src/HABApp/openhab/events/__init__.py +++ b/src/HABApp/openhab/events/__init__.py @@ -4,4 +4,5 @@ from .channel_events import ChannelTriggeredEvent, ChannelDescriptionChangedEvent from .thing_events import ThingStatusInfoChangedEvent, ThingStatusInfoEvent, \ ThingFirmwareStatusInfoEvent, ThingAddedEvent, ThingRemovedEvent, ThingUpdatedEvent, ThingConfigStatusInfoEvent -from .event_filters import ItemStateUpdatedEventFilter, ItemStateChangedEventFilter, ItemCommandEventFilter +from .event_filters import ItemStateUpdatedEventFilter, ItemStateEventFilter, ItemStateChangedEventFilter, \ + ItemCommandEventFilter diff --git a/src/HABApp/openhab/events/event_filters.py b/src/HABApp/openhab/events/event_filters.py index 1146fbaf..aa89c117 100644 --- a/src/HABApp/openhab/events/event_filters.py +++ b/src/HABApp/openhab/events/event_filters.py @@ -2,7 +2,13 @@ from HABApp.core.const import MISSING from HABApp.core.events.filter.event import TypeBoundEventFilter -from . import ItemStateChangedEvent, ItemStateUpdatedEvent, ItemCommandEvent +from . import ItemStateChangedEvent, ItemStateEvent, ItemStateUpdatedEvent, ItemCommandEvent + + +# Todo: Drop this when we go OH4.0 only +class ItemStateEventFilter(TypeBoundEventFilter): + def __init__(self, value: Any = MISSING): + super().__init__(ItemStateEvent, value=value) class ItemStateUpdatedEventFilter(TypeBoundEventFilter): diff --git a/src/HABApp/openhab/items/datetime_item.py b/src/HABApp/openhab/items/datetime_item.py index b6bd0f19..63957174 100644 --- a/src/HABApp/openhab/items/datetime_item.py +++ b/src/HABApp/openhab/items/datetime_item.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import TYPE_CHECKING, Optional, FrozenSet, Mapping +from HABApp.core.const.const import PYTHON_311 from HABApp.openhab.items.base_item import OpenhabItem, MetaData if TYPE_CHECKING: @@ -24,7 +25,16 @@ class DatetimeItem(OpenhabItem): @staticmethod def _state_from_oh_str(state: str): - dt = datetime.strptime(state, '%Y-%m-%dT%H:%M:%S.%f%z') + # see implementation im map_values.py + if PYTHON_311: + dt = datetime.fromisoformat(state) + else: + pos_dot = state.find('.') + pos_plus = state.find('+') + if pos_plus - pos_dot > 6: + state = state[:pos_dot + 7] + state[pos_plus:] + dt = datetime.strptime(state, '%Y-%m-%dT%H:%M:%S.%f%z') + # all datetime objs from openHAB have a timezone set so we can't easily compare them # --> TypeError: can't compare offset-naive and offset-aware datetime dt = dt.astimezone(tz=None) # Changes datetime object so it uses system timezone diff --git a/src/HABApp/openhab/map_values.py b/src/HABApp/openhab/map_values.py index c17547a1..19cdcf84 100644 --- a/src/HABApp/openhab/map_values.py +++ b/src/HABApp/openhab/map_values.py @@ -1,16 +1,12 @@ -import datetime +from datetime import datetime +from HABApp.core.const.const import PYTHON_311 from HABApp.openhab.definitions import HSBValue, OnOffValue, OpenClosedValue, PercentValue, QuantityValue, RawValue, \ UpDownValue from HABApp.openhab.definitions.values import PointValue def map_openhab_values(openhab_type: str, openhab_value: str): - # because we preprocess the string value can be None. - # Either remove the preprocessing or remove this here - if openhab_value is None: - return None - assert isinstance(openhab_type, str), type(openhab_type) assert isinstance(openhab_value, str), type(openhab_value) @@ -29,17 +25,26 @@ def map_openhab_values(openhab_type: str, openhab_value: str): if openhab_type == "String": return openhab_value + if openhab_type == "HSB": + return HSBValue(openhab_value) + if openhab_type == "DateTime": - dt = datetime.datetime.strptime(openhab_value, '%Y-%m-%dT%H:%M:%S.%f%z') + # see implementation im datetime_item.py + if PYTHON_311: + dt = datetime.fromisoformat(openhab_value) + else: + pos_dot = openhab_value.find('.') + pos_plus = openhab_value.find('+') + if pos_plus - pos_dot > 6: + openhab_value = openhab_value[:pos_dot + 7] + openhab_value[pos_plus:] + dt = datetime.strptime(openhab_value, '%Y-%m-%dT%H:%M:%S.%f%z') + # all datetimes from openHAB have a timezone set, so we can't easily compare them # --> TypeError: can't compare offset-naive and offset-aware datetimes dt = dt.astimezone(tz=None) # Changes datetime object so it uses system timezone dt = dt.replace(tzinfo=None) # Removes timezone awareness return dt - if openhab_type == "HSB": - return HSBValue(openhab_value) - if openhab_type == 'OnOff': return OnOffValue(openhab_value) diff --git a/tests/test_openhab/test_events/test_oh_filters.py b/tests/test_openhab/test_events/test_oh_filters.py index 3b8423b6..6f04a9ac 100644 --- a/tests/test_openhab/test_events/test_oh_filters.py +++ b/tests/test_openhab/test_events/test_oh_filters.py @@ -6,7 +6,10 @@ def test_class_annotations(): """EventFilter relies on the class annotations, so we test that every event has those""" - exclude = ('OpenhabEvent', 'ItemStateChangedEventFilter', 'ItemStateUpdatedEventFilter', 'ItemCommandEventFilter') + exclude = ( + 'OpenhabEvent', + 'ItemStateChangedEventFilter', 'ItemStateUpdatedEventFilter', 'ItemStateEventFilter', + 'ItemCommandEventFilter') for cls in get_module_classes('HABApp.openhab.events', exclude).values(): check_class_annotations( cls, init_alias={'initial_value': 'value', 'group_names': 'groups', 'thing_type': 'type'} diff --git a/tests/test_openhab/test_openhab_datatypes.py b/tests/test_openhab/test_openhab_datatypes.py index 779a0c67..7ec4b4e0 100644 --- a/tests/test_openhab/test_openhab_datatypes.py +++ b/tests/test_openhab/test_openhab_datatypes.py @@ -1,5 +1,8 @@ from datetime import datetime +import pytest + +from HABApp.openhab.items import DatetimeItem, NumberItem from HABApp.openhab.map_values import map_openhab_values @@ -8,25 +11,51 @@ def test_type_none(): assert map_openhab_values('Number', 'NULL') is None -def test_type_number(): - assert 0 == map_openhab_values('Number', '0') - assert -99 == map_openhab_values('Number', '-99') - assert 99 == map_openhab_values('Number', '99') +@pytest.mark.parametrize('value, target', (('0', 0), ('-15', -15), ('55', 55), )) +def test_type_number(value: str, target: int): + ret = NumberItem._state_from_oh_str(value) + assert ret == target + assert isinstance(ret, int) + + ret = map_openhab_values('Number', value) + assert ret == target + assert isinstance(ret, int) + + +@pytest.mark.parametrize( + 'value, target', (('0.0', 0.0), ('-99.99', -99.99), ('99.99', 99.99), ('0', 0), ('-15', -15), ('55', 55), ) +) +def test_type_decimal(value: str, target: int): + ret = NumberItem._state_from_oh_str(value) + assert ret == target + assert type(ret) is target.__class__ + + ret = map_openhab_values('Decimal', value) + assert ret == target + assert type(ret) is target.__class__ + + +def __get_dt_parms(): + # We have to build the offset str dynamically otherwise we will fail during CI because it's in another timezone + now = datetime.now() + offset_secs = int(now.astimezone().tzinfo.utcoffset(now).total_seconds()) + hours = offset_secs // 3600 + minutes = (offset_secs - 3600 * hours) // 60 + assert offset_secs - hours * 3600 - minutes * 60 == 0 + offset_str = f'{hours:02d}:{minutes:02d}' -def test_type_decimal(): - assert 0.0 == map_openhab_values('Decimal', '0.0') - assert -99.99 == map_openhab_values('Decimal', '-99.99') - assert 99.99 == map_openhab_values('Decimal', '99.99') - assert 5 == map_openhab_values('Decimal', '5') + return ( + pytest.param(f'2023-06-17T15:31:04.754673068+{offset_str}', datetime(2023, 6, 17, 15, 31, 4, 754673), id='T1'), + pytest.param(f'2023-06-17T15:31:04.754673+{offset_str}', datetime(2023, 6, 17, 15, 31, 4, 754673), id='T2'), + pytest.param(f'2023-06-17T15:31:04.754+{offset_str}', datetime(2023, 6, 17, 15, 31, 4, 754000), id='T3'), + ) -def test_type_datetime(): - # test now - _now = datetime.now().replace(microsecond=456000) - _in = _now.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + _now.astimezone(None).strftime('%z') - # 2019-10-09T07:37:00.000+0200 - assert map_openhab_values('DateTime', _in) == _now +@pytest.mark.parametrize('value, target', __get_dt_parms()) +def test_type_datetime(value: str, target: datetime): + assert DatetimeItem._state_from_oh_str(value) == target + assert map_openhab_values('DateTime', value) == target def test_quantity():