Skip to content

Commit

Permalink
0.18.0 (#189)
Browse files Browse the repository at this point in the history
Changes and Bugfixes:
- Fixed a bug where the item watchers would not work after deleting and immediately adding an item again (e.g. *.items file change)
  Closes #184
- BaseWatch has now a function which listens exactly to this event
- ItemTimes.add_watch accepts a timedelta, too
- Item registry: renamed set_item to add_item which also raises an exception if the item already exists
- If a file depends on another file that doesn't exist and that file gets added the depending file will be loaded correctly (fixes #186)
- A file with duplicate rule names will no longer break the file load (fixes #187)
- EventBusListener can filter on event properties
- creating/canceling an item watch gets now logged
- Updated requirements
- added some tests and documentation
  • Loading branch information
spacemanspiff2007 authored Dec 22, 2020
1 parent ba4cb7b commit fd39a2a
Show file tree
Hide file tree
Showing 46 changed files with 886 additions and 285 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/run_tox.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Tests

on: [push]
on: [push, pull_request]

jobs:
test:
Expand Down
2 changes: 1 addition & 1 deletion HABApp/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.17.1'
__version__ = '0.18.0'
37 changes: 31 additions & 6 deletions HABApp/core/Items.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@


class ItemNotFoundException(Exception):
pass
def __init__(self, name: str):
super().__init__(f'Item {name} does not exist!')
self.name: str = name


class ItemAlreadyExistsError(Exception):
def __init__(self, name: str):
super().__init__(f'Item {name} does already exist and can not be added again!')
self.name: str = name


def item_exists(name: str) -> bool:
Expand All @@ -17,7 +25,7 @@ def get_item(name: str) -> __BaseItem:
try:
return _ALL_ITEMS[name]
except KeyError:
raise ItemNotFoundException(f'Item {name} does not exist!') from None
raise ItemNotFoundException(name) from None


def get_all_items() -> typing.List[__BaseItem]:
Expand All @@ -30,16 +38,33 @@ def get_all_item_names() -> typing.List[str]:

def create_item(name: str, item_factory, initial_value=None) -> __BaseItem:
assert issubclass(item_factory, __BaseItem), item_factory
_ALL_ITEMS[name] = new_item = item_factory(name, initial_value=initial_value)
new_item = item_factory(name, initial_value=initial_value)
add_item(new_item)
return new_item


def set_item(item: __BaseItem):
def add_item(item: __BaseItem):
assert isinstance(item, __BaseItem), type(item)
_ALL_ITEMS[item.name] = item
name = item.name

existing = _ALL_ITEMS.get(name)
if existing is not None:
# adding the same item multiple times will not cause an exception
if existing is item:
return None

# adding a new item with the same name raises an exception
raise ItemAlreadyExistsError(name)

_ALL_ITEMS[name] = item
item._on_item_add()


def pop_item(name: str) -> __BaseItem:
item = _ALL_ITEMS.pop(name)
try:
item = _ALL_ITEMS.pop(name)
except KeyError:
raise ItemNotFoundException(name) from None

item._on_item_remove()
return item
41 changes: 38 additions & 3 deletions HABApp/core/event_bus_listener.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import HABApp
from HABApp.core.events import AllEvents
from . import WrappedFunction
from typing import Optional, Any


class EventBusListener:
def __init__(self, topic, callback, event_type=AllEvents):
def __init__(self, topic, callback, event_type=AllEvents,
prop_name1: Optional[str] = None, prop_value1: Optional[Any] = None,
prop_name2: Optional[str] = None, prop_value2: Optional[Any] = None,
):
assert isinstance(topic, str), type(topic)
assert isinstance(callback, WrappedFunction)
assert prop_name1 is None or isinstance(prop_name1, str), prop_name1
assert prop_name2 is None or isinstance(prop_name2, str), prop_name2

self.topic: str = topic
self.func: WrappedFunction = callback

self.event_filter = event_type

# Property filters
self.prop_name1 = prop_name1
self.prop_value1 = prop_value1
self.prop_name2 = prop_name2
self.prop_value2 = prop_value2

self.__is_all: bool = self.event_filter is AllEvents
self.__is_single: bool = not isinstance(self.event_filter, (list, tuple, set))

Expand All @@ -25,12 +37,28 @@ def notify_listeners(self, event):
# single filter
if self.__is_single:
if isinstance(event, self.event_filter):
# If we have property filters wie only trigger when value is set accordingly
if self.prop_name1 is not None:
if getattr(event, self.prop_name1, None) != self.prop_value1:
return None
if self.prop_name2 is not None:
if getattr(event, self.prop_name2, None) != self.prop_value2:
return None

self.func.run(event)
return None

# Make it possible to specify multiple classes
for cls in self.event_filter:
if isinstance(event, cls):
# If we have property filters wie only trigger when value is set accordingly
if self.prop_name1 is not None:
if getattr(event, self.prop_name1, None) != self.prop_value1:
return None
if self.prop_name2 is not None:
if getattr(event, self.prop_name2, None) != self.prop_value2:
return None

self.func.run(event)
return None

Expand All @@ -42,5 +70,12 @@ def desc(self):
# return description
_type = str(self.event_filter)
if _type.startswith("<class '"):
_type = _type[8:-2]
return f'"{self.topic}" (type {_type})'
_type = _type[8:-2].split('.')[-1]

_val = ''
if self.prop_name1 is not None:
_val += f', {self.prop_name1}=={self.prop_value1}'
if self.prop_name2 is not None:
_val += f', {self.prop_name2}=={self.prop_value2}'

return f'"{self.topic}" (type {_type}{_val})'
4 changes: 4 additions & 0 deletions HABApp/core/files/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def process(files: typing.List[Path], load_next: bool = True):
with LOCK:
ALL[name] = obj

# find all which have this file as dependency and are not valid so it can be checked again
for _f in filter(lambda x: not x.is_valid and name in x.properties.depends_on, ALL.values()):
_f.is_checked = False

if not load_next:
return None

Expand Down
16 changes: 9 additions & 7 deletions HABApp/core/items/base_item.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import datetime
import logging
import typing

import tzlocal
from pytz import utc

import HABApp
from .base_item_times import BaseWatch, ChangedTime, UpdatedTime
from .tmp_data import add_tmp_data as _add_tmp_data
from .tmp_data import restore_tmp_data as _restore_tmp_data


class BaseItem:
Expand Down Expand Up @@ -107,11 +108,12 @@ def listen_event(self, callback: typing.Callable[[typing.Any], typing.Any],
rule = HABApp.rule.get_parent_rule()
return rule.listen_event(self._name, callback=callback, event_type=event_type)

def _on_item_add(self):
"""This function gets automatically called when the item is added to the item registry
"""
_restore_tmp_data(self)

def _on_item_remove(self):
"""This function gets called when the item is removed from the item registry
"""This function gets automatically called when the item is removed from the item registry
"""
if self._last_change.tasks or self._last_update.tasks:
w = HABApp.core.logger.HABAppWarning(logging.getLogger('HABApp.Item'))
w.add(f'Item {self._name} has been removed even though it has item watchers. '
f'If it will be added again the watchers have to be created again, too!')
w.dump()
_add_tmp_data(self)
27 changes: 19 additions & 8 deletions HABApp/core/items/base_item_times.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import asyncio
import datetime
import logging
import typing
from datetime import timedelta

from HABApp.core.wrapper import log_exception
from .base_item_watch import BaseWatch, ItemNoChangeWatch, ItemNoUpdateWatch
from ..const import loop

log = logging.getLogger('HABApp')


class ItemTimes:
WATCH: typing.Union[typing.Type[ItemNoUpdateWatch], typing.Type[ItemNoChangeWatch]]
Expand All @@ -24,29 +28,36 @@ def set(self, dt: datetime.datetime, events=True):
asyncio.run_coroutine_threadsafe(self.schedule_events(), loop)
return None

def add_watch(self, secs: typing.Union[int, float]) -> BaseWatch:
def add_watch(self, secs: typing.Union[int, float, timedelta]) -> BaseWatch:
if isinstance(secs, timedelta):
secs = secs.total_seconds()
assert secs > 0, secs

# don't add the watch two times
for t in self.tasks:
if not t._fut.is_canceled and t._fut.secs == secs:
if not t.fut.is_canceled and t.fut.secs == secs:
log.warning(f'Watcher {self.WATCH.__name__} ({t.fut.secs}s) for {self.name} has already been created')
return t

w = self.WATCH(self.name, secs)
self.tasks.append(w)
log.debug(f'Added {self.WATCH.__name__} ({w.fut.secs}s) for {self.name}')
return w

@log_exception
async def schedule_events(self):
clean = False
canceled = []
for t in self.tasks:
if t._fut.is_canceled:
clean = True
if t.fut.is_canceled:
canceled.append(t)
else:
t._fut.reset()
t.fut.reset()

# remove canceled tasks
if clean:
self.tasks = [t for t in self.tasks if not t._fut.is_canceled]
if canceled:
for c in canceled:
self.tasks.remove(c)
log.debug(f'Removed {self.WATCH.__name__} ({c.fut.secs}s) for {self.name}')
return None


Expand Down
22 changes: 17 additions & 5 deletions HABApp/core/items/base_item_watch.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,41 @@
import asyncio
import logging
import typing

import HABApp
from ..const import loop
from HABApp.core.lib import PendingFuture
from ..const import loop
from ..events import ItemNoChangeEvent, ItemNoUpdateEvent

log = logging.getLogger('HABApp')


class BaseWatch:
EVENT: typing.Union[typing.Type[ItemNoUpdateEvent], typing.Type[ItemNoChangeEvent]]

def __init__(self, name: str, secs: typing.Union[int, float]):
self._fut = PendingFuture(self._post_event, secs)
self._name: str = name
self.fut = PendingFuture(self._post_event, secs)
self.name: str = name

async def _post_event(self):
HABApp.core.EventBus.post_event(self._name, self.EVENT(self._name, self._fut.secs))
HABApp.core.EventBus.post_event(self.name, self.EVENT(self.name, self.fut.secs))

async def __cancel_watch(self):
self._fut.cancel()
self.fut.cancel()
log.debug(f'Canceled {self.__class__.__name__} ({self.fut.secs}s) for {self.name}')

def cancel(self):
"""Cancel the item watch"""
asyncio.run_coroutine_threadsafe(self.__cancel_watch(), loop)

def listen_event(self, callback: typing.Callable[[typing.Any], typing.Any]) -> 'HABApp.core.EventBusListener':
rule = HABApp.rule.get_parent_rule()
cb = HABApp.core.WrappedFunction(callback, name=rule._get_cb_name(callback))
listener = HABApp.core.EventBusListener(
self.name, cb, self.EVENT, 'seconds', self.fut.secs
)
return rule._add_event_listener(listener)


class ItemNoUpdateWatch(BaseWatch):
EVENT = ItemNoUpdateEvent
Expand Down
2 changes: 1 addition & 1 deletion HABApp/core/items/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def get_create_item(cls, name: str, initial_value=None):
item = HABApp.core.Items.get_item(name)
except HABApp.core.Items.ItemNotFoundException:
item = cls(name, initial_value)
HABApp.core.Items.set_item(item)
HABApp.core.Items.add_item(item)

assert isinstance(item, cls), f'{cls} != {type(item)}'
return item
2 changes: 1 addition & 1 deletion HABApp/core/items/item_aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def get_create_item(cls, name: str):
item = HABApp.core.Items.get_item(name)
except HABApp.core.Items.ItemNotFoundException:
item = cls(name)
HABApp.core.Items.set_item(item)
HABApp.core.Items.add_item(item)

assert isinstance(item, cls), f'{cls} != {type(item)}'
return item
Expand Down
Loading

0 comments on commit fd39a2a

Please sign in to comment.