Skip to content

Commit

Permalink
0.31.1 (#239)
Browse files Browse the repository at this point in the history
* Added EventListenerGroup
* - Z-Wave table doesn't show the linked items any more
* add option to get metadata to async_get_items
* Added support for item metadata
* Add option to search for item metadata
* fixes #238
  • Loading branch information
spacemanspiff2007 authored Oct 29, 2021
1 parent 3b21a95 commit 96535bb
Show file tree
Hide file tree
Showing 34 changed files with 815 additions and 146 deletions.
61 changes: 61 additions & 0 deletions _doc/util.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,67 @@ Documentation
.. autoclass:: Statistics
:members:

EventListenerGroup
------------------------------
EventListenerGroup is a helper class which allows to subscribe to multiple items at once.
All subscriptions can be canceled together, too.
This is useful if e.g. something has to be done once after a sensor reports a value.

Example
^^^^^^^^^^^^^^^^^^
This is a rule which will turn on the lights once (!) in a room on the first movement in the morning.
The lights will only turn on after 4 and before 8 and two movement sensors are used to pick up movement.


.. exec_code::

# ------------ hide: start ------------
import HABApp
from rule_runner import SimpleRuleRunner
runner = SimpleRuleRunner()
runner.set_up()
HABApp.core.Items.add_item(HABApp.openhab.items.SwitchItem('RoomLights'))
HABApp.core.Items.add_item(HABApp.openhab.items.NumberItem('MovementSensor1'))
HABApp.core.Items.add_item(HABApp.openhab.items.NumberItem('MovementSensor2'))
# ------------ hide: stop -------------
from datetime import time

from HABApp import Rule
from HABApp.core.events import ValueChangeEvent
from HABApp.openhab.items import SwitchItem, NumberItem
from HABApp.util import EventListenerGroup


class EventListenerGroupExample(Rule):
def __init__(self):
super().__init__()
self.lights = SwitchItem.get_item('RoomLights')
self.sensor_move_1 = NumberItem.get_item('MovementSensor1')
self.sensor_move_1 = NumberItem.get_item('MovementSensor2')

# use the defaults so we don't have to pass the callback and event filter in add_listener
self.group = EventListenerGroup(default_callback=self.sensor_changed, default_event_filter=ValueChangeEvent).\
add_listener(self.sensor_move_1).add_listener(self.sensor_move_1)

self.run.on_every_day(time(4), self.listen_sensors)
self.run.on_every_day(time(8), self.sensors_cancel)

def listen_sensors(self):
self.listeners.listen()

def sensors_cancel(self):
self.listeners.cancel()

def sensor_changed(self, event):
self.listeners.cancel()
self.lights.on()


EventListenerGroupExample()


.. autoclass:: EventListenerGroup
:members:

MultiModeItem
------------------------------
Expand Down
4 changes: 2 additions & 2 deletions conf_testing/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ openhab:
connection:
host: localhost
port: 8080
user: ''
password: ''
user: 'asdf'
password: 'asdf'
general:
listen_only: false
wait_for_openhab: true
Expand Down
11 changes: 8 additions & 3 deletions conf_testing/lib/HABAppTests/openhab_tmp_item.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import time
from typing import List, Optional

from functools import wraps
import HABApp
from . import get_random_name, EventWaiter

Expand All @@ -9,6 +9,7 @@ class OpenhabTmpItem:
@staticmethod
def use(type: str, name: Optional[str] = None, arg_name: str = 'item'):
def decorator(func):
@wraps(func)
def new_func(*args, **kwargs):
assert arg_name not in kwargs, f'arg {arg_name} already set'
item = OpenhabTmpItem(type, name)
Expand All @@ -21,10 +22,14 @@ def new_func(*args, **kwargs):
return decorator

@staticmethod
def create(type: str, name: Optional[str] = None):
def create(type: str, name: Optional[str] = None, arg_name: Optional[str] = None):
def decorator(func):
@wraps(func)
def new_func(*args, **kwargs):
with OpenhabTmpItem(type, name):
with OpenhabTmpItem(type, name) as f:
if arg_name is not None:
assert arg_name not in kwargs, f'arg {arg_name} already set'
kwargs[arg_name] = f
return func(*args, **kwargs)
return new_func
return decorator
Expand Down
2 changes: 1 addition & 1 deletion conf_testing/rules/openhab/test_event_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_quantity_type_events(self, dimension):
ItemWaiter(item) as item_waiter:

for state in get_openhab_test_states('Number'):
self.openhab.post_update(item_name, f'{state} {unit_of_dimension[dimension]}')
self.openhab.post_update(item_name, f'{state} {unit_of_dimension[dimension]}'.strip())
event_watier.wait_for_event(value=state)
item_waiter.wait_for_state(state)

Expand Down
8 changes: 4 additions & 4 deletions conf_testing/rules/openhab/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,12 @@ def test_post_update(self, oh_type, values):
self.openhab.send_command(item, value)
waiter.wait_for_state(value)

def test_umlaute(self):
@OpenhabTmpItem.use('String')
def test_umlaute(self, item: OpenhabTmpItem):
LABEL = 'äöß'
NAME = 'TestUmlaute'

self.openhab.create_item('String', NAME, label=LABEL)
ret = self.openhab.get_item(NAME)
self.openhab.create_item('String', item.name, label=LABEL)
ret = self.openhab.get_item(item.name)
assert ret.label == LABEL, f'"{LABEL}" != "{ret.label}"'

def test_openhab_item_not_found(self):
Expand Down
59 changes: 55 additions & 4 deletions conf_testing/rules/openhab/test_items.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
from HABApp.openhab.items import StringItem, GroupItem
from HABAppTests import TestBaseRule, OpenhabTmpItem
# ----------------------------------------------------------------------------------------------------------------------
# This rule requires the following item:
# String TestString (TestGroup) [TestTag] {meta1="test" [key="value"]}
# ----------------------------------------------------------------------------------------------------------------------
import asyncio

from immutables import Map

from HABApp.core.const import loop
from HABApp.openhab.interface_async import async_get_items
from HABApp.openhab.items import GroupItem, StringItem
from HABAppTests import OpenhabTmpItem, TestBaseRule


class OpenhabItems(TestBaseRule):
Expand All @@ -10,10 +20,51 @@ def __init__(self):
self.add_test('ApiDoc', self.test_api)
self.add_test('MemberTags', self.test_tags)
self.add_test('MemberGroups', self.test_groups)
self.add_test('TestExisting', self.test_existing)

self.item_number = OpenhabTmpItem('Number')
self.item_switch = OpenhabTmpItem('Switch')

self.item_group = OpenhabTmpItem('Group')
self.item_string = OpenhabTmpItem('String')

def set_up(self):
self.item_number.create_item(label='No metadata')

self.item_switch.create_item()
self.openhab.set_metadata(
self.item_switch.name, 'homekit', 'HeatingThresholdTemperature', {'minValue': 0.5, 'maxValue': 20})

self.item_group.create_item(label='MyGrpValue [%s]', category='text', tags=['DocItem'],
group_function='AND', group_function_params=['VALUE_TRUE', 'VALUE_FALSE'])
self.item_string.create_item(label='MyStrValue [%s]', category='text', tags=['DocItem'],
groups=[self.item_group.name])

self.openhab.set_metadata(self.item_string.name, 'ns1', 'v1', {'key11': 'value11', 'key12': 'value12'})
self.openhab.set_metadata(self.item_string.name, 'ns2', 'v2', {'key2': 'value2'})
self.openhab.set_metadata(self.item_group.name, 'ns3', 'v3', {})

def tear_down(self):
self.item_string.remove()
self.item_switch.remove()

def test_existing(self):
item = StringItem.get_item('TestString')
assert item.tags == frozenset(['TestTag'])
assert item.groups == frozenset(['TestGroup'])
assert list(item.metadata.keys()) == ['meta1']
assert item.metadata['meta1'].value == 'test'
assert item.metadata['meta1'].config == Map({'key': 'value'})

def test_api(self):
with OpenhabTmpItem('String') as item:
self.openhab.get_item(item.name)
self.openhab.get_item(self.item_string.name)

self.openhab.get_item(self.item_number.name, all_metadata=True)
self.openhab.get_item(self.item_string.name, all_metadata=True)
self.openhab.get_item(self.item_switch.name, all_metadata=True)

self.openhab.get_item(self.item_group.name, all_metadata=True)
asyncio.run_coroutine_threadsafe(async_get_items(all_metadata=True), loop).result()

@OpenhabTmpItem.use('String', arg_name='oh_item')
def test_tags(self, oh_item: OpenhabTmpItem):
Expand Down
5 changes: 5 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ MyOpenhabRule()
```

# Changelog
#### 0.31.1 (29.10.2021)
- Added support for item metadata
- Added possibility to search for items by metadata
- Added EventListenerGroup to subscribe/cancel multiple listeners at once

#### 0.31.0 (08.10.2021)
- added self.get_items to easily search for items in a rule
- added full support for tags and groups on OpenhabItem
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ stackprinter==0.2.5
voluptuous==0.12.1
watchdog==2.1.2
ujson==4.0.2
immutables==0.16

# Packages to run source tests
pytest==6.2.4
Expand Down
1 change: 1 addition & 0 deletions requirements_setup.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ stackprinter==0.2.5
voluptuous==0.12.1
watchdog==2.1.2
ujson==4.0.2
immutables==0.16
2 changes: 1 addition & 1 deletion src/HABApp/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.31.0'
__version__ = '0.31.1'
5 changes: 3 additions & 2 deletions src/HABApp/core/event_bus_listener.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Any, Optional

import HABApp
from HABApp.core import WrappedFunction
from HABApp.core.events import AllEvents
from . import WrappedFunction
from typing import Optional, Any


class EventBusListener:
Expand Down
3 changes: 2 additions & 1 deletion src/HABApp/core/wrappedfunction.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,14 @@ def __format_traceback(self, e: Exception, *args, **kwargs):

async def async_run(self, *args, **kwargs):

async_context.set('WrappedFunction')
token = async_context.set('WrappedFunction')

try:
await self._func(*args, **kwargs)
except Exception as e:
self.__format_traceback(e, *args, **kwargs)

async_context.reset(token)
return None

def __run(self, *args, **kwargs):
Expand Down
14 changes: 12 additions & 2 deletions src/HABApp/openhab/connection_handler/func_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,17 @@ async def async_item_exists(item) -> bool:
return ret.status == 200


async def async_get_items(include_habapp_meta=False, disconnect_on_error=False) -> Optional[List[Dict[str, Any]]]:
async def async_get_items(include_habapp_meta=False, metadata: Optional[str] = None, all_metadata=False,
disconnect_on_error=False) -> Optional[List[Dict[str, Any]]]:
params = None
if include_habapp_meta:
params = {'metadata': 'HABApp'}
if metadata is not None:
if params is not None:
raise ValueError('Use include_habapp_meta or metadata')
params = {'metadata': metadata}
if all_metadata:
params = {'metadata': '.+'}

try:
resp = await get('items', disconnect_on_error=disconnect_on_error, params=params)
Expand All @@ -67,8 +74,11 @@ async def async_get_items(include_habapp_meta=False, disconnect_on_error=False)
return None


async def async_get_item(item: str, metadata: Optional[str] = None) -> dict:
async def async_get_item(item: str, metadata: Optional[str] = None, all_metadata=False) -> dict:
params = None if metadata is None else {'metadata': metadata}
if all_metadata:
params = {'metadata': '.+'}

ret = await get(f'items/{item:s}', params=params, log_404=False)
if ret.status == 404:
raise ItemNotFoundError.from_name(item)
Expand Down
6 changes: 4 additions & 2 deletions src/HABApp/openhab/connection_handler/func_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,12 @@ def validate(_in):
return fut.result()


def get_item(item_name: str, metadata: Optional[str] = None) -> OpenhabItemDefinition:
def get_item(item_name: str, metadata: Optional[str] = None, all_metadata=False) -> OpenhabItemDefinition:
"""Return the complete OpenHAB item definition
:param item_name: name of the item or item
:param metadata: metadata to include (optional, comma separated or search expression)
:param all_metadata: if true the result will include all item metadata
:return:
"""
if isinstance(item_name, HABApp.openhab.items.base_item.BaseValueItem):
Expand All @@ -131,7 +132,8 @@ def get_item(item_name: str, metadata: Optional[str] = None) -> OpenhabItemDefin
if async_context.get(None) is not None:
raise AsyncContextError(get_item)

fut = asyncio.run_coroutine_threadsafe(async_get_item(item_name, metadata=metadata), loop)
fut = asyncio.run_coroutine_threadsafe(
async_get_item(item_name, metadata=metadata, all_metadata=all_metadata), loop)
data = fut.result()
return OpenhabItemDefinition.parse_obj(data)

Expand Down
Loading

0 comments on commit 96535bb

Please sign in to comment.