From 4c0659371f6ef983632eb9062e73388540eb51a4 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Sat, 5 Dec 2020 01:27:19 +0100 Subject: [PATCH 01/25] initial commit for added functionality to receive ItemEvents from Openhab --- openhab/client.py | 140 ++++++++++++++++++++++++++++++- openhab/events.py | 41 +++++++++ openhab/items.py | 142 ++++++++++++++++++++++++++++---- test.py | 3 +- tests/test_eventsubscription.py | 114 +++++++++++++++++++++++++ 5 files changed, 422 insertions(+), 18 deletions(-) create mode 100644 openhab/events.py create mode 100644 tests/test_eventsubscription.py diff --git a/openhab/client.py b/openhab/client.py index c125d50..1c2ccd3 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -25,10 +25,17 @@ import typing import warnings +import sseclient +import threading import requests +import weakref +import json +import time from requests.auth import HTTPBasicAuth +from dataclasses import dataclass import openhab.items +import openhab.events __author__ = 'Georges Toth ' __license__ = 'AGPLv3+' @@ -41,7 +48,8 @@ def __init__(self, base_url: str, username: typing.Optional[str] = None, password: typing.Optional[str] = None, http_auth: typing.Optional[requests.auth.AuthBase] = None, - timeout: typing.Optional[float] = None) -> None: + timeout: typing.Optional[float] = None, + autoUpdate: typing.Optional[bool] = False) -> None: """Constructor. Args: @@ -53,14 +61,19 @@ def __init__(self, base_url: str, http_auth (AuthBase, optional): An alternative to username/password pair, is to specify a custom http authentication object of type :class:`requests.auth.AuthBase`. timeout (float, optional): An optional timeout for REST transactions + autoUpdate (bool, optional): Register for Openhab Item Events to actively get informed about changes. Returns: OpenHAB: openHAB class instance. """ self.base_url = base_url - + self.events_url = "{}/events?topics=smarthome/items".format(base_url.strip('/')) + self.autoUpdate=autoUpdate self.session = requests.Session() self.session.headers['accept'] = 'application/json' + #self.registered_items:typing.Dict[str,openhab.items.Item]= {} + self.registered_items = weakref.WeakValueDictionary() + if http_auth is not None: self.session.auth = http_auth @@ -70,6 +83,10 @@ def __init__(self, base_url: str, self.timeout = timeout self.logger = logging.getLogger(__name__) + self.__keep_event_deamon_running__ = False + self.eventListeners:typing.List[typing.Callable] = [] + if self.autoUpdate: + self.__installSSEClient__() @staticmethod def _check_req_return(req: requests.Response) -> None: @@ -88,6 +105,125 @@ def _check_req_return(req: requests.Response) -> None: if not (200 <= req.status_code < 300): req.raise_for_status() + + + def parseItem(self, event:openhab.events.ItemEvent): + if event.itemname in self.registered_items: + item=self.registered_items[event.itemname] + if item is None: + self.logger.warning("item '{}' was removed in all scopes. Ignoring the events coming in for it.".format(event.itemname)) + else: + item._processEvent(event) + else: + self.logger.debug("item '{}' not registered. ignoring the arrived event.".format(event.itemname)) + + + + + + + + + + def parseEvent(self,eventData:typing.Dict): + log=logging.getLogger() + eventreason=eventData["type"] + + + + if eventreason in ["ItemCommandEvent","ItemStateEvent","ItemStateChangedEvent"]: + itemname = eventData["topic"].split("/")[-2] + event=None + payloadData = json.loads(eventData["payload"]) + remoteDatatype = payloadData["type"] + newValue = payloadData["value"] + log.debug("####### new Event arrived:") + log.debug("item name:{}".format(itemname)) + log.debug("type:{}".format(eventreason)) + log.debug("payloadData:{}".format(eventData["payload"])) + + if eventreason =="ItemStateEvent": + event = openhab.events.ItemStateEvent(itemname=itemname, source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype,newValue=newValue,asUpdate=False) + elif eventreason =="ItemCommandEvent": + event = openhab.events.ItemCommandEvent(itemname=itemname, source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype, newValue=newValue) + elif eventreason in ["ItemStateChangedEvent"]: + oldremoteDatatype = payloadData["oldType"] + oldValue = payloadData["oldValue"] + event=openhab.events.ItemStateChangedEvent(itemname=itemname,source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype,newValue=newValue,oldRemoteDatatype=oldremoteDatatype,oldValue=oldValue,asUpdate=False) + log.debug("received ItemStateChanged for '{itemname}'[{olddatatype}->{datatype}]:{oldState}->{newValue}".format(itemname=itemname, olddatatype=oldremoteDatatype, datatype=remoteDatatype, oldState=oldValue, newValue=newValue)) + + else: + log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=itemname, datatype=remoteDatatype, newValue=newValue)) + self.informEventListeners(event) + self.parseItem(event) + else: + log.info("received unknown Event-type in Openhab Event stream: {}".format(eventData)) + + def informEventListeners(self,event:openhab.events.ItemEvent): + for aListener in self.eventListeners: + try: + aListener(event) + except Exception as e: + self.logger.error("error executing Eventlistener for event:{}.".format(event.itemname),e) + + def addEventListener(self, listener:typing.Callable[[openhab.events.ItemEvent],None]): + self.eventListeners.append(listener) + + def removeEventListener(self, listener:typing.Optional[typing.Callable[[openhab.events.ItemEvent],None]]=None): + if listener is None: + self.eventListeners.clear() + elif listener in self.eventListeners: + self.eventListeners.remove(listener) + + + + + def sseDaemonThread(self): + self.logger.info("starting Openhab - Event Deamon") + next_waittime=initial_waittime=0.1 + while self.__keep_event_deamon_running__: + try: + self.logger.info("about to connect to Openhab Events-Stream.") + # response = requests.get(self.events_url, stream=True) + # self.logger.info("connected to Openhab Events-Stream.") + + + import urllib3 + http = urllib3.PoolManager() + response = http.request('GET', self.events_url, preload_content=False) + self.logger.info("connected to Openhab Events-Stream.") + self.sseClient = sseclient.SSEClient(response) + self.logger.info("pr") + + + next_waittime = initial_waittime + for event in self.sseClient.events(): + eventData = json.loads(event.data) + self.parseEvent(eventData) + if not self.__keep_event_deamon_running__: + return + except Exception as e: + self.logger.warning("Lost connection to Openhab Events-Stream.",e) + time.sleep(next_waittime) # aleep a bit and then retry + next_waittime=min(10,next_waittime+0.5) # increase waittime over time up to 10 seconds + + + + + def register_item(self, item: openhab.items.Item): + if not item is None and not item.name is None: + if not item.name in self.registered_items: + #self.registered_items[item.name]=weakref.ref(item) + self.registered_items[item.name] = item + + def __installSSEClient__(self): + """ installs an event Stream to receive all Item events""" + + #now start readerThread + self.__keep_event_deamon_running__=True + self.sseDaemon = threading.Thread(target=self.sseDaemonThread, args=(), daemon=True) + self.sseDaemon.start() + def req_get(self, uri_path: str) -> typing.Any: """Helper method for initiating a HTTP GET request. diff --git a/openhab/events.py b/openhab/events.py new file mode 100644 index 0000000..2cf256d --- /dev/null +++ b/openhab/events.py @@ -0,0 +1,41 @@ +import typing +from dataclasses import dataclass + + +EventType= typing.NewType('EventType', str) +ItemEventType : EventType = EventType("Item") +ItemStateEventType : EventType = EventType("ItemState") +ItemCommandEventType : EventType = EventType("ItemCommand") +ItemStateChangedEventType : EventType = EventType("ItemStateChanged") + + +EventSource= typing.NewType('EventSource', str) +EventSourceInternal : EventSource = EventSource("Internal") +EventSourceOpenhab : EventSource = EventSource("Openhab") + +@dataclass +class ItemEvent(object): + type = ItemEventType + itemname: str + source: EventSource + +@dataclass +class ItemStateEvent(ItemEvent): + type = ItemStateEventType + remoteDatatype: str + newValue: typing.Any + asUpdate:bool + + +@dataclass +class ItemCommandEvent(ItemEvent): + type = ItemCommandEventType + remoteDatatype: str + newValue: typing.Any + + +@dataclass +class ItemStateChangedEvent(ItemStateEvent): + type = ItemStateChangedEventType + oldRemoteDatatype: str + oldValue: typing.Any diff --git a/openhab/items.py b/openhab/items.py index 64f35c9..dfd408b 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -19,7 +19,7 @@ # # pylint: disable=bad-indentation - +from __future__ import annotations import logging import re import typing @@ -27,6 +27,7 @@ import dateutil.parser import openhab.types +import openhab.events __author__ = 'Georges Toth ' __license__ = 'AGPLv3+' @@ -46,16 +47,21 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict) -> N server. """ self.openhab = openhab_conn + self.autoUpdate = self.openhab.autoUpdate self.type_ = None self.group = False self.name = '' self._state = None # type: typing.Optional[typing.Any] self._raw_state = None # type: typing.Optional[typing.Any] # raw state as returned by the server + self._raw_state_event = None # type: typing.str # raw state as received from Serverevent self._members = {} # type: typing.Dict[str, typing.Any] # group members (key = item name), for none-group items it's empty self.logger = logging.getLogger(__name__) self.init_from_json(json_data) + self.openhab.register_item(self) + self.eventListeners: typing.Dict[typing.Callable[[openhab.events.ItemEvent],None],Item.EventListener]={} + #typing.List[typing.Callable] = [] def init_from_json(self, json_data: dict): """Initialize this object from a json configuration as fetched from openHAB. @@ -79,20 +85,27 @@ def init_from_json(self, json_data: dict): self.__set_state(json_data['state']) @property - def state(self) -> typing.Any: + def state(self,fetchFromOpenhab=False) -> typing.Any: """The state property represents the current state of the item. The state is automatically refreshed from openHAB on reading it. Updating the value via this property send an update to the event bus. """ - json_data = self.openhab.get_item_raw(self.name) - self.init_from_json(json_data) + if not self.autoUpdate or fetchFromOpenhab: + json_data = self.openhab.get_item_raw(self.name) + self.init_from_json(json_data) return self._state @state.setter def state(self, value: typing.Any): + oldstate= self._state self.update(value) + if oldstate==self._state: + event=openhab.events.ItemStateEvent( itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_,newValue=self._state, asUpdate=False) + else: + event = openhab.events.ItemStateChangedEvent(itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_, newValue=self._state, oldRemoteDatatype=self.type_,oldValue=oldstate, asUpdate=False) + self._processEvent(event) @property def members(self): @@ -129,7 +142,7 @@ def _validate_value(self, value: typing.Union[str, typing.Type[openhab.types.Com def _parse_rest(self, value: str) -> str: """Parse a REST result into a native object.""" - return value + return (value,"") def _rest_format(self, value: str) -> typing.Union[str, bytes]: """Format a value before submitting to openHAB.""" @@ -143,6 +156,84 @@ def _rest_format(self, value: str) -> typing.Union[str, bytes]: return _value + def _processEvent(self,event:openhab.events.ItemEvent): + if event.source==openhab.events.EventSourceOpenhab: + self.__set_state(value=event.newValue) + event.newValue=self._state + for aListener in self.eventListeners.values(): + if event.type in aListener.listeningTypes: + if aListener.onlyIfEventsourceIsOpenhab and event.source!=openhab.events.EventSourceOpenhab: + break + else: + try: + aListener.callbackfunction(self,event) + except Exception as e: + self.logger.error("error executing Eventlistener for item:{}.".format(event.itemname),e) + + + class EventListener(object): + def __init__(self,listeningTypes:typing.Set[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab): + allTypes = {openhab.events.ItemStateEvent.type, openhab.events.ItemCommandEvent.type, openhab.events.ItemStateChangedEvent.type} + if listeningTypes is None: + self.listeningTypes = allTypes + elif not hasattr(listeningTypes, '__iter__'): + self.listeningTypes = set([listeningTypes]) + elif not listeningTypes: + self.listeningTypes = allTypes + else: + self.listeningTypes = listeningTypes + + self.callbackfunction:typing.Callable[[openhab.events.ItemEvent],None]=listener + self.onlyIfEventsourceIsOpenhab = onlyIfEventsourceIsOpenhab + + def addTypes(self,listeningTypes:typing.Set[openhab.events.EventType]): + if listeningTypes is None: return + elif not hasattr(listeningTypes, '__iter__'): + self.listeningTypes.add(listeningTypes) + elif not listeningTypes: + return + else: + self.listeningTypes.update(listeningTypes) + + def removeTypes(self,listeningTypes:typing.Set[openhab.events.EventType]): + if listeningTypes is None: + self.listeningTypes.clear() + elif not hasattr(listeningTypes, '__iter__'): + self.listeningTypes.remove(listeningTypes) + elif not listeningTypes: + self.listeningTypes.clear() + else: + self.listeningTypes.difference_update(listeningTypes) + + + + + + + def addEventListener(self,types:typing.List[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab=None): + if listener in self.eventListeners: + eventListener= self.eventListeners[listener] + eventListener.addTypes(types) + + if not onlyIfEventsourceIsOpenhab is None: + eventListener.onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab + else: + if not onlyIfEventsourceIsOpenhab is None: + onlyIfEventsourceIsOpenhab=True + eventListener=Item.EventListener(listeningTypes=types,listener=listener,onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab) + self.eventListeners[listener]=eventListener + + def removeEventListener(self,types:typing.List[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None]): + if listener in self.eventListeners: + eventListener = self.eventListeners[listener] + eventListener.removeTypes(types) + if not eventListener.listeningTypes: + self.eventListeners.pop(listener) + + + + + def __set_state(self, value: str) -> None: """Private method for setting the internal state.""" self._raw_state = value @@ -150,7 +241,7 @@ def __set_state(self, value: str) -> None: if value in ('UNDEF', 'NULL'): self._state = None else: - self._state = self._parse_rest(value) + self._state, self._unitOfMeasure = self._parse_rest(value) def __str__(self) -> str: return '<{0} - {1} : {2}>'.format(self.type_, self.name, self._state) @@ -172,12 +263,19 @@ def update(self, value: typing.Any) -> None: value (object): The value to update the item with. The type of the value depends on the item type and is checked accordingly. """ + oldstate = self._state self._validate_value(value) v = self._rest_format(value) self._update(v) + if oldstate == self._state: + event = openhab.events.ItemStateEvent(itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_, newValue=self._state, asUpdate=True) + else: + event = openhab.events.ItemStateChangedEvent(itemname=self.name, source=openhab.events.EventSourceInternal,remoteDatatype=self.type_, newValue=self._state, oldRemoteDatatype=self.type_, oldValue=oldstate, asUpdate=True) + self._processEvent(event) + # noinspection PyTypeChecker def command(self, value: typing.Any) -> None: """Sends the given value as command to the event bus. @@ -186,12 +284,18 @@ def command(self, value: typing.Any) -> None: value (object): The value to send as command to the event bus. The type of the value depends on the item type and is checked accordingly. """ + self._validate_value(value) v = self._rest_format(value) self.openhab.req_post('/items/{}'.format(self.name), data=v) + + event = openhab.events.ItemCommandEvent(itemname=self.name, source=openhab.events.EventSourceInternal,remoteDatatype=self.type_, newValue=self._state) + self._processEvent(event) + + def update_state_null(self) -> None: """Update the state of the item to *NULL*.""" self._update('NULL') @@ -254,7 +358,7 @@ def _parse_rest(self, value): datetime.datetime: The datetime.datetime object as converted from the string parameter. """ - return dateutil.parser.parse(value) + return (dateutil.parser.parse(value),"") def _rest_format(self, value): """Format a value before submitting to openHAB. @@ -326,13 +430,21 @@ def _parse_rest(self, value: str) -> float: Returns: float: The float object as converted from the string parameter. + str: The unit Of Measure or empty string """ - # Items of type NumberItem may contain units of measurement. Here we make sure to strip them off. - # @TODO possibly implement supporting UoM data for NumberItems not sure this would be useful. - m = re.match(r'''^(-?[0-9.]+)''', value) - if m: - return float(m.group(1)) + #m = re.match(r'''^(-?[0-9.]+)''', value) + try: + m= re.match("(-?[0-9.]+)\s?(.*)?$", value) + + if m: + value=m.group(1) + unitOfMeasure = m.group(2) + + logging.getLogger().debug("original value:{}, myvalue:{}, my UoM:{}".format(m,value,unitOfMeasure)) + return (float(value),unitOfMeasure) + except Exception as e: + self.logger.error("error in parsing new value '{}' for '{}'".format(value,self.name),e) raise ValueError('{}: unable to parse value "{}"'.format(self.__class__, value)) @@ -383,7 +495,7 @@ def _parse_rest(self, value: str) -> int: Returns: int: The int object as converted from the string parameter. """ - return int(float(value)) + return (int(float(value)),"") def _rest_format(self, value: typing.Union[str, int]) -> str: """Format a value before submitting to OpenHAB. @@ -431,7 +543,7 @@ def _parse_rest(self, value: str) -> str: Returns: str: The str object as converted from the string parameter. """ - return str(value) + return (str(value),"") def _rest_format(self, value: typing.Union[str, int]) -> str: """Format a value before submitting to openHAB. @@ -478,7 +590,7 @@ def _parse_rest(self, value: str) -> int: Returns: int: The int object as converted from the string parameter. """ - return int(float(value)) + return (int(float(value)),"") def _rest_format(self, value: typing.Union[str, int]) -> str: """Format a value before submitting to openHAB. diff --git a/test.py b/test.py index e25f4f4..5c80710 100644 --- a/test.py +++ b/test.py @@ -22,7 +22,8 @@ import datetime import openhab -base_url = 'http://localhost:8080/rest' +#base_url = 'http://localhost:8080/rest' +base_url = 'http://10.10.20.81:8080/rest' openhab = openhab.OpenHAB(base_url) # fetch all items diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py new file mode 100644 index 0000000..d0cb9a7 --- /dev/null +++ b/tests/test_eventsubscription.py @@ -0,0 +1,114 @@ +from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable + +import openhab +import openhab.events +import time +import openhab.items as items +import logging +log=logging.getLogger() +logging.basicConfig(level=0) + +log.error("xx") +log.warning("www") +log.info("iii") +log.debug("ddddd") + + + +base_url = 'http://10.10.20.81:8080/rest' + + + +testdata:Dict[str,Tuple[str,str]]={'OnOff' : ('ItemStateEvent','{"type":"OnOff","value":"ON"}'), + 'Decimal' : ('ItemStateEvent','{"type":"Decimal","value":"170.0"}'), + 'DateTime' : ('ItemStateEvent','{"type":"DateTime","value":"2020-12-04T15:53:33.968+0100"}'), + 'UnDef' : ('ItemStateEvent','{"type":"UnDef","value":"UNDEF"}'), + 'String' : ('ItemStateEvent','{"type":"String","value":"WANING_GIBBOUS"}'), + 'Quantitykm' : ('ItemStateEvent','{"type":"Quantity","value":"389073.99674024084 km"}'), + 'Quantitykm grad' : ('ItemStateEvent', '{"type":"Quantity","value":"233.32567712620255 °"}'), + 'Quantitywm2' : ('ItemStateEvent', '{"type":"Quantity","value":"0.0 W/m²"}'), + 'Percent' : ('ItemStateEvent', '{"type":"Percent","value":"52"}'), + 'UpDown' : ('ItemStateEvent', '{"type":"UpDown","value":"DOWN"}'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), + # + + 'OnOffChange' : ('ItemStateChangedEvent','{"type":"OnOff","value":"OFF","oldType":"OnOff","oldValue":"ON"}'), + 'DecimalChange' : ('ItemStateChangedEvent','{"type":"Decimal","value":"170.0","oldType":"Decimal","oldValue":"186.0"}'), + 'QuantityChange' : ('ItemStateChangedEvent','{"type":"Quantity","value":"389073.99674024084 km","oldType":"Quantity","oldValue":"389076.56223012594 km"}'), + 'QuantityGradChange' : ('ItemStateChangedEvent', '{"type":"Quantity","value":"233.32567712620255 °","oldType":"Quantity","oldValue":"233.1365666436372 °"}'), + 'DecimalChangeFromNull' : ('ItemStateChangedEvent', '{"type":"Decimal","value":"0.5","oldType":"UnDef","oldValue":"NULL"}'), + 'DecimalChangeFromNullToUNDEF' : ('ItemStateChangedEvent', '{"type":"Decimal","value":"15","oldType":"UnDef","oldValue":"NULL"}'), + 'PercentChange' : ('ItemStateChangedEvent', '{"type":"Percent","value":"52","oldType":"UnDef","oldValue":"NULL"}'), + + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), + 'Datatypechange' : ('ItemStateChangedEvent', '{"type":"OnOff","value":"ON","oldType":"UnDef","oldValue":"NULL"}') + } + +def executeParseCheck(): + for testkey in testdata: + log.info("testing:{}".format(testkey)) + stringToParse=testdata[testkey] + + +if True: + myopenhab = openhab.OpenHAB(base_url,autoUpdate=True) + + # allitems:List[items.Item] = openhab.fetch_all_items() + # for aItem in allitems: + # + # print(aItem) + + itemDimmer=myopenhab.get_item("testroom1_LampDimmer") + print(itemDimmer) + itemAzimuth=myopenhab.get_item("testworld_Azimuth") + print(itemAzimuth) + itemClock=myopenhab.get_item("myClock") + itemDimmer.command(12.5) + + + def onAzimuthChange(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): + log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname,event.newValue, item.state)) + + itemAzimuth.addEventListener(openhab.events.ItemCommandEventType,onAzimuthChange,onlyIfEventsourceIsOpenhab=True) + + + # def onAzimuthChangeAll(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): + # if event.source == openhab.events.EventSourceInternal: + # log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from internal".format(event.itemname,event.newValue, item.state)) + # else: + # log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValue, item.state)) + # + # itemAzimuth.addEventListener(openhab.events.ItemCommandEventType,onAzimuthChangeAll,onlyIfEventsourceIsOpenhab=False) + + #print(itemClock) + while True: + time.sleep(10) + x=0 + x=2 + if x==1: + itemAzimuth=None + elif x==2: + itemAzimuth.command(55.1) \ No newline at end of file From 969f52597ecc361c7dcec12d215face053f5660e Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Tue, 8 Dec 2020 20:32:52 +0100 Subject: [PATCH 02/25] receiving events works. need write testcases still --- openhab/client.py | 38 ++++---- openhab/events.py | 28 ++++++ openhab/items.py | 110 ++++++++++++++++------ tests/test_eventsubscription.py | 159 ++++++++++++++++++++------------ 4 files changed, 225 insertions(+), 110 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index 1c2ccd3..55dd77a 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU General Public License # along with python-openhab. If not, see . # - # pylint: disable=bad-indentation import logging @@ -25,7 +24,7 @@ import typing import warnings -import sseclient +from sseclient import SSEClient import threading import requests import weakref @@ -49,7 +48,8 @@ def __init__(self, base_url: str, password: typing.Optional[str] = None, http_auth: typing.Optional[requests.auth.AuthBase] = None, timeout: typing.Optional[float] = None, - autoUpdate: typing.Optional[bool] = False) -> None: + autoUpdate: typing.Optional[bool] = False, + maxEchoToOpenHAB_ms: typing.Optional[int]=800) -> None: """Constructor. Args: @@ -62,7 +62,7 @@ def __init__(self, base_url: str, specify a custom http authentication object of type :class:`requests.auth.AuthBase`. timeout (float, optional): An optional timeout for REST transactions autoUpdate (bool, optional): Register for Openhab Item Events to actively get informed about changes. - + maxEchoToOpenHAB_ms (int, optional): interpret Events from openHAB with same statevalue as we have coming within maxEchoToOpenhabMS millisends since our update/command as echos of our update//command Returns: OpenHAB: openHAB class instance. """ @@ -81,6 +81,7 @@ def __init__(self, base_url: str, self.session.auth = HTTPBasicAuth(username, password) self.timeout = timeout + self.maxEchoToOpenhabMS=maxEchoToOpenHAB_ms self.logger = logging.getLogger(__name__) self.__keep_event_deamon_running__ = False @@ -113,7 +114,7 @@ def parseItem(self, event:openhab.events.ItemEvent): if item is None: self.logger.warning("item '{}' was removed in all scopes. Ignoring the events coming in for it.".format(event.itemname)) else: - item._processEvent(event) + item._processExternalEvent(event) else: self.logger.debug("item '{}' not registered. ignoring the arrived event.".format(event.itemname)) @@ -143,19 +144,20 @@ def parseEvent(self,eventData:typing.Dict): log.debug("payloadData:{}".format(eventData["payload"])) if eventreason =="ItemStateEvent": - event = openhab.events.ItemStateEvent(itemname=itemname, source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype,newValue=newValue,asUpdate=False) + event = openhab.events.ItemStateEvent(itemname=itemname, source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype,newValueRaw=newValue,unitOfMeasure="",newValue="",asUpdate=False) elif eventreason =="ItemCommandEvent": - event = openhab.events.ItemCommandEvent(itemname=itemname, source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype, newValue=newValue) + event = openhab.events.ItemCommandEvent(itemname=itemname, source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype, newValueRaw=newValue,unitOfMeasure="", newValue="") elif eventreason in ["ItemStateChangedEvent"]: oldremoteDatatype = payloadData["oldType"] oldValue = payloadData["oldValue"] - event=openhab.events.ItemStateChangedEvent(itemname=itemname,source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype,newValue=newValue,oldRemoteDatatype=oldremoteDatatype,oldValue=oldValue,asUpdate=False) + event=openhab.events.ItemStateChangedEvent(itemname=itemname,source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype,newValueRaw=newValue,newValue="",unitOfMeasure="",oldRemoteDatatype=oldremoteDatatype,oldValueRaw=oldValue, oldValue="", oldUnitOfMeasure="",asUpdate=False) log.debug("received ItemStateChanged for '{itemname}'[{olddatatype}->{datatype}]:{oldState}->{newValue}".format(itemname=itemname, olddatatype=oldremoteDatatype, datatype=remoteDatatype, oldState=oldValue, newValue=newValue)) else: - log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=itemname, datatype=remoteDatatype, newValue=newValue)) - self.informEventListeners(event) + log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=itemname, datatype=remoteDatatype, newValueRaw=newValue)) + self.parseItem(event) + self.informEventListeners(event) else: log.info("received unknown Event-type in Openhab Event stream: {}".format(eventData)) @@ -178,30 +180,24 @@ def removeEventListener(self, listener:typing.Optional[typing.Callable[[openhab. + + def sseDaemonThread(self): self.logger.info("starting Openhab - Event Deamon") next_waittime=initial_waittime=0.1 while self.__keep_event_deamon_running__: try: self.logger.info("about to connect to Openhab Events-Stream.") - # response = requests.get(self.events_url, stream=True) - # self.logger.info("connected to Openhab Events-Stream.") - - - import urllib3 - http = urllib3.PoolManager() - response = http.request('GET', self.events_url, preload_content=False) - self.logger.info("connected to Openhab Events-Stream.") - self.sseClient = sseclient.SSEClient(response) - self.logger.info("pr") + messages = SSEClient(self.events_url) next_waittime = initial_waittime - for event in self.sseClient.events(): + for event in messages: eventData = json.loads(event.data) self.parseEvent(eventData) if not self.__keep_event_deamon_running__: return + except Exception as e: self.logger.warning("Lost connection to Openhab Events-Stream.",e) time.sleep(next_waittime) # aleep a bit and then retry diff --git a/openhab/events.py b/openhab/events.py index 2cf256d..204a65a 100644 --- a/openhab/events.py +++ b/openhab/events.py @@ -1,3 +1,25 @@ +# -*- coding: utf-8 -*- +"""python classes for receiving events from openHAB SSE (server side events) REST API.""" + +# +# Alexey Grubauer (c) 2020 +# +# python-openhab is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# python-openhab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with python-openhab. If not, see . +# + +# pylint: disable=bad-indentation + import typing from dataclasses import dataclass @@ -24,7 +46,9 @@ class ItemStateEvent(ItemEvent): type = ItemStateEventType remoteDatatype: str newValue: typing.Any + newValueRaw: str asUpdate:bool + unitOfMeasure: str @dataclass @@ -32,10 +56,14 @@ class ItemCommandEvent(ItemEvent): type = ItemCommandEventType remoteDatatype: str newValue: typing.Any + newValueRaw: typing.Any + unitOfMeasure: str @dataclass class ItemStateChangedEvent(ItemStateEvent): type = ItemStateChangedEventType oldRemoteDatatype: str + oldValueRaw: str oldValue: typing.Any + oldUnitOfMeasure: str diff --git a/openhab/items.py b/openhab/items.py index dfd408b..cf176f0 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -28,6 +28,7 @@ import openhab.types import openhab.events +from datetime import datetime, timedelta __author__ = 'Georges Toth ' __license__ = 'AGPLv3+' @@ -59,6 +60,9 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict) -> N self.logger = logging.getLogger(__name__) self.init_from_json(json_data) + self.lastCommandSent = datetime.fromtimestamp(0) + self.lastUpdateSent = datetime.fromtimestamp(0) + self.openhab.register_item(self) self.eventListeners: typing.Dict[typing.Callable[[openhab.events.ItemEvent],None],Item.EventListener]={} #typing.List[typing.Callable] = [] @@ -101,11 +105,11 @@ def state(self,fetchFromOpenhab=False) -> typing.Any: def state(self, value: typing.Any): oldstate= self._state self.update(value) - if oldstate==self._state: - event=openhab.events.ItemStateEvent( itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_,newValue=self._state, asUpdate=False) - else: - event = openhab.events.ItemStateChangedEvent(itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_, newValue=self._state, oldRemoteDatatype=self.type_,oldValue=oldstate, asUpdate=False) - self._processEvent(event) + # if oldstate==self._state: + # event=openhab.events.ItemStateEvent( itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_,newValueRaw=self._state, asUpdate=False) + # else: + # event = openhab.events.ItemStateChangedEvent(itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_, newValueRaw=self._state, oldRemoteDatatype=self.type_,oldValueRaw=oldstate, asUpdate=False) + # self._processEvent(event) @property def members(self): @@ -156,23 +160,58 @@ def _rest_format(self, value: str) -> typing.Union[str, bytes]: return _value - def _processEvent(self,event:openhab.events.ItemEvent): - if event.source==openhab.events.EventSourceOpenhab: - self.__set_state(value=event.newValue) + def _isMyOwnChange(self, event): + now = datetime.now() + self.logger.debug("_isMyOwnChange:event.source:{}, event.type{}, self._state:{}, event.newValue:{},self.lastCommandSent:{}, self.lastUpdateSent:{} , now:{}".format(event.source,event.type,self._state,event.newValue ,self.lastCommandSent,self.lastUpdateSent,now)) + if event.source == openhab.events.EventSourceOpenhab: + if event.type in [openhab.events.ItemCommandEventType, openhab.events.ItemStateChangedEventType, openhab.events.ItemStateEventType]: + if self._state == event.newValue: + if max(self.lastCommandSent, self.lastUpdateSent) + timedelta(milliseconds=self.openhab.maxEchoToOpenhabMS) > now: + # this is the echo of the command we just sent to openHAB. + return True + return False + else: + return True + + + + def _processExternalEvent(self, event:openhab.events.ItemEvent): + self.logger.info("processing external event") + newValue,uom=self._parse_rest(event.newValueRaw) + event.newValue=newValue + event.unitOfMeasure=uom + if event.type==openhab.events.ItemStateChangedEventType: + oldValue,ouom=self._parse_rest(event.oldValueRaw) + event.oldValue=oldValue + event.oldUnitOfMeasure=ouom + isMyOwnChange=self._isMyOwnChange(event) + self.logger.info("external event:{}".format(event)) + if not isMyOwnChange: + self.__set_state(value=event.newValueRaw) event.newValue=self._state for aListener in self.eventListeners.values(): if event.type in aListener.listeningTypes: - if aListener.onlyIfEventsourceIsOpenhab and event.source!=openhab.events.EventSourceOpenhab: - break - else: + if not isMyOwnChange or (isMyOwnChange and aListener.alsoGetMyEchosFromOpenHAB): try: aListener.callbackfunction(self,event) except Exception as e: self.logger.error("error executing Eventlistener for item:{}.".format(event.itemname),e) + def _processInternalEvent(self,event:openhab.events.ItemEvent): + self.logger.info("processing internal event") + for aListener in self.eventListeners.values(): + if event.type in aListener.listeningTypes: + if aListener.onlyIfEventsourceIsOpenhab: + continue + else: + try: + aListener.callbackfunction(self,event) + except Exception as e: + self.logger.error("error executing Eventlistener for item:{}.".format(event.itemname),e) + class EventListener(object): - def __init__(self,listeningTypes:typing.Set[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab): + def __init__(self,listeningTypes:typing.Set[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab,alsoGetMyEchosFromOpenHAB): allTypes = {openhab.events.ItemStateEvent.type, openhab.events.ItemCommandEvent.type, openhab.events.ItemStateChangedEvent.type} if listeningTypes is None: self.listeningTypes = allTypes @@ -185,6 +224,7 @@ def __init__(self,listeningTypes:typing.Set[openhab.events.EventType],listener:t self.callbackfunction:typing.Callable[[openhab.events.ItemEvent],None]=listener self.onlyIfEventsourceIsOpenhab = onlyIfEventsourceIsOpenhab + self.alsoGetMyEchosFromOpenHAB=alsoGetMyEchosFromOpenHAB def addTypes(self,listeningTypes:typing.Set[openhab.events.EventType]): if listeningTypes is None: return @@ -210,17 +250,15 @@ def removeTypes(self,listeningTypes:typing.Set[openhab.events.EventType]): - def addEventListener(self,types:typing.List[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab=None): + def addEventListener(self,types:typing.List[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab=True,alsoGetMyEchosFromOpenHAB=False): + + if listener in self.eventListeners: eventListener= self.eventListeners[listener] eventListener.addTypes(types) - - if not onlyIfEventsourceIsOpenhab is None: - eventListener.onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab + eventListener.onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab else: - if not onlyIfEventsourceIsOpenhab is None: - onlyIfEventsourceIsOpenhab=True - eventListener=Item.EventListener(listeningTypes=types,listener=listener,onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab) + eventListener=Item.EventListener(listeningTypes=types,listener=listener,onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab,alsoGetMyEchosFromOpenHAB=alsoGetMyEchosFromOpenHAB) self.eventListeners[listener]=eventListener def removeEventListener(self,types:typing.List[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None]): @@ -254,8 +292,10 @@ def _update(self, value: typing.Any) -> None: on the item type and is checked accordingly. """ # noinspection PyTypeChecker + self.lastCommandSent = datetime.now() self.openhab.req_put('/items/{}/state'.format(self.name), data=value) + def update(self, value: typing.Any) -> None: """Updates the state of an item. @@ -267,14 +307,25 @@ def update(self, value: typing.Any) -> None: self._validate_value(value) v = self._rest_format(value) - + self._state=value self._update(v) if oldstate == self._state: - event = openhab.events.ItemStateEvent(itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_, newValue=self._state, asUpdate=True) + event = openhab.events.ItemStateEvent(itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_, newValue=self._state, newValueRaw=None, unitOfMeasure=self._unitOfMeasure, asUpdate=True) else: - event = openhab.events.ItemStateChangedEvent(itemname=self.name, source=openhab.events.EventSourceInternal,remoteDatatype=self.type_, newValue=self._state, oldRemoteDatatype=self.type_, oldValue=oldstate, asUpdate=True) - self._processEvent(event) + event = openhab.events.ItemStateChangedEvent(itemname=self.name, + source=openhab.events.EventSourceInternal, + remoteDatatype=self.type_, + newValue=self._state, + newValueRaw=None, + unitOfMeasure=self._unitOfMeasure, + oldRemoteDatatype=self.type_, + oldValue=oldstate, + oldValueRaw="", + oldUnitOfMeasure="", + asUpdate=True, + ) + self._processInternalEvent(event) # noinspection PyTypeChecker def command(self, value: typing.Any) -> None: @@ -288,12 +339,15 @@ def command(self, value: typing.Any) -> None: self._validate_value(value) v = self._rest_format(value) - + self._state = value + self.lastCommandSent = datetime.now() self.openhab.req_post('/items/{}'.format(self.name), data=v) - - event = openhab.events.ItemCommandEvent(itemname=self.name, source=openhab.events.EventSourceInternal,remoteDatatype=self.type_, newValue=self._state) - self._processEvent(event) + uoM="" + if hasattr(self,"_unitOfMeasure"): + uoM=self._unitOfMeasure + event = openhab.events.ItemCommandEvent(itemname=self.name, source=openhab.events.EventSourceInternal,remoteDatatype=self.type_, newValue=value, newValueRaw=None, unitOfMeasure=uoM) + self._processInternalEvent(event) def update_state_null(self) -> None: @@ -441,7 +495,7 @@ def _parse_rest(self, value: str) -> float: value=m.group(1) unitOfMeasure = m.group(2) - logging.getLogger().debug("original value:{}, myvalue:{}, my UoM:{}".format(m,value,unitOfMeasure)) + #logging.getLogger().debug("original value:{}, myvalue:{}, my UoM:{}".format(m,value,unitOfMeasure)) return (float(value),unitOfMeasure) except Exception as e: self.logger.error("error in parsing new value '{}' for '{}'".format(value,self.name),e) diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index d0cb9a7..3fcc40a 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -6,7 +6,7 @@ import openhab.items as items import logging log=logging.getLogger() -logging.basicConfig(level=0) +logging.basicConfig(level=20,format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") log.error("xx") log.warning("www") @@ -19,53 +19,31 @@ -testdata:Dict[str,Tuple[str,str]]={'OnOff' : ('ItemStateEvent','{"type":"OnOff","value":"ON"}'), - 'Decimal' : ('ItemStateEvent','{"type":"Decimal","value":"170.0"}'), - 'DateTime' : ('ItemStateEvent','{"type":"DateTime","value":"2020-12-04T15:53:33.968+0100"}'), - 'UnDef' : ('ItemStateEvent','{"type":"UnDef","value":"UNDEF"}'), - 'String' : ('ItemStateEvent','{"type":"String","value":"WANING_GIBBOUS"}'), - 'Quantitykm' : ('ItemStateEvent','{"type":"Quantity","value":"389073.99674024084 km"}'), - 'Quantitykm grad' : ('ItemStateEvent', '{"type":"Quantity","value":"233.32567712620255 °"}'), - 'Quantitywm2' : ('ItemStateEvent', '{"type":"Quantity","value":"0.0 W/m²"}'), - 'Percent' : ('ItemStateEvent', '{"type":"Percent","value":"52"}'), - 'UpDown' : ('ItemStateEvent', '{"type":"UpDown","value":"DOWN"}'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # 'XXX ': ('ItemStateEvent', 'XXXXXXXXXXXXXXXXXXXX'), - # - - 'OnOffChange' : ('ItemStateChangedEvent','{"type":"OnOff","value":"OFF","oldType":"OnOff","oldValue":"ON"}'), - 'DecimalChange' : ('ItemStateChangedEvent','{"type":"Decimal","value":"170.0","oldType":"Decimal","oldValue":"186.0"}'), - 'QuantityChange' : ('ItemStateChangedEvent','{"type":"Quantity","value":"389073.99674024084 km","oldType":"Quantity","oldValue":"389076.56223012594 km"}'), - 'QuantityGradChange' : ('ItemStateChangedEvent', '{"type":"Quantity","value":"233.32567712620255 °","oldType":"Quantity","oldValue":"233.1365666436372 °"}'), - 'DecimalChangeFromNull' : ('ItemStateChangedEvent', '{"type":"Decimal","value":"0.5","oldType":"UnDef","oldValue":"NULL"}'), - 'DecimalChangeFromNullToUNDEF' : ('ItemStateChangedEvent', '{"type":"Decimal","value":"15","oldType":"UnDef","oldValue":"NULL"}'), - 'PercentChange' : ('ItemStateChangedEvent', '{"type":"Percent","value":"52","oldType":"UnDef","oldValue":"NULL"}'), +testdata:Dict[str,Tuple[str,str,str]]={'OnOff' : ('ItemCommandEvent','testroom1_LampOnOff','{"type":"OnOff","value":"ON"}'), + 'Decimal' : ('ItemCommandEvent','xx','{"type":"Decimal","value":"170.0"}'), + 'DateTime' : ('ItemCommandEvent','xx','{"type":"DateTime","value":"2020-12-04T15:53:33.968+0100"}'), + 'UnDef' : ('ItemCommandEvent','xx','{"type":"UnDef","value":"UNDEF"}'), + 'String' : ('ItemCommandEvent','xx','{"type":"String","value":"WANING_GIBBOUS"}'), + 'Quantitykm' : ('ItemCommandEvent','xx','{"type":"Quantity","value":"389073.99674024084 km"}'), + 'Quantitykm grad' : ('ItemCommandEvent','xx', '{"type":"Quantity","value":"233.32567712620255 °"}'), + 'Quantitywm2' : ('ItemCommandEvent','xx', '{"type":"Quantity","value":"0.0 W/m²"}'), + 'Percent' : ('ItemCommandEvent','xx', '{"type":"Percent","value":"52"}'), + 'UpDown' : ('ItemCommandEvent','xx', '{"type":"UpDown","value":"DOWN"}'), + + + 'OnOffChange' : ('ItemStateChangedEvent','xx', '{"type":"OnOff","value":"OFF","oldType":"OnOff","oldValueRaw":"ON"}'), + 'DecimalChange' : ('ItemStateChangedEvent','xx', '{"type":"Decimal","value":"170.0","oldType":"Decimal","oldValueRaw":"186.0"}'), + 'QuantityChange' : ('ItemStateChangedEvent','xx', '{"type":"Quantity","value":"389073.99674024084 km","oldType":"Quantity","oldValueRaw":"389076.56223012594 km"}'), + 'QuantityGradChange' : ('ItemStateChangedEvent','xx', '{"type":"Quantity","value":"233.32567712620255 °","oldType":"Quantity","oldValueRaw":"233.1365666436372 °"}'), + 'DecimalChangeFromNull' : ('ItemStateChangedEvent','xx', '{"type":"Decimal","value":"0.5","oldType":"UnDef","oldValueRaw":"NULL"}'), + 'DecimalChangeFromNullToUNDEF' : ('ItemStateChangedEvent','xx', '{"type":"Decimal","value":"15","oldType":"UnDef","oldValueRaw":"NULL"}'), + 'PercentChange' : ('ItemStateChangedEvent','xx', '{"type":"Percent","value":"52","oldType":"UnDef","oldValueRaw":"NULL"}'), + # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - 'Datatypechange' : ('ItemStateChangedEvent', '{"type":"OnOff","value":"ON","oldType":"UnDef","oldValue":"NULL"}') + 'Datatypechange' : ('ItemStateChangedEvent','xx', '{"type":"OnOff","value":"ON","oldType":"UnDef","oldValueRaw":"NULL"}') } +testitems:Dict[str,openhab.items.Item] = {} def executeParseCheck(): for testkey in testdata: @@ -76,39 +54,98 @@ def executeParseCheck(): if True: myopenhab = openhab.OpenHAB(base_url,autoUpdate=True) - # allitems:List[items.Item] = openhab.fetch_all_items() - # for aItem in allitems: - # - # print(aItem) - itemDimmer=myopenhab.get_item("testroom1_LampDimmer") print(itemDimmer) itemAzimuth=myopenhab.get_item("testworld_Azimuth") print(itemAzimuth) + itemAzimuth.state=44.0 itemClock=myopenhab.get_item("myClock") - itemDimmer.command(12.5) + + expectClock = None + expectedValue=None def onAzimuthChange(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): - log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname,event.newValue, item.state)) + log.info("########################### UPDATE of {itemname} to eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state}) from OPENHAB ONLY".format( + itemname=event.itemname,eventvalue=event.newValue,eventvalueraw=event.newValueRaw, item_state=item._state,itemstate=item.state)) itemAzimuth.addEventListener(openhab.events.ItemCommandEventType,onAzimuthChange,onlyIfEventsourceIsOpenhab=True) + def onClockChange(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): + log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname, event.newValueRaw, item.state)) + if not expectClock is None: + assert item.state == expectClock + + itemClock.addEventListener(openhab.events.ItemCommandEventType,onClockChange,onlyIfEventsourceIsOpenhab=True) + + + def onAzimuthChangeAll(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): + if event.source == openhab.events.EventSourceInternal: + log.info("########################### INTERNAL UPDATE of {} to {} (itemsvalue:{}) from internal".format(event.itemname,event.newValueRaw, item.state)) + else: + log.info("########################### EXTERNAL UPDATE of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValueRaw, item.state)) - # def onAzimuthChangeAll(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): - # if event.source == openhab.events.EventSourceInternal: - # log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from internal".format(event.itemname,event.newValue, item.state)) - # else: - # log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValue, item.state)) - # - # itemAzimuth.addEventListener(openhab.events.ItemCommandEventType,onAzimuthChangeAll,onlyIfEventsourceIsOpenhab=False) + itemAzimuth.addEventListener(openhab.events.ItemCommandEventType,onAzimuthChangeAll,onlyIfEventsourceIsOpenhab=False) #print(itemClock) + + time.sleep(2) + log.info("###################################### starting test 'internal Event'") + + expectClock=2 + itemClock.state=2 + time.sleep(0.1) + expectClock = None + testname="OnOff" + log.info("###################################### starting test '{}'".format(testname)) + + def createEventData(type,itemname,payload): + result={} + result["type"]=type + result["topic"]="smarthome/items/{itemname}/state".format(itemname=itemname) + result["payload"]=payload + return result + + + def onAnyItemCommand(item: openhab.items.Item, event: openhab.events.ItemStateEvent): + log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname, event.newValueRaw, item.state)) + if not expectedValue is None: + assert event.newValue==expectedValue + + + testname="OnOff" + expectedValue="ON" + type='ItemCommandEvent' + itemname='testroom1_LampOnOff' + payload='{"type":"OnOff","value":"ON"}' + eventData=createEventData(type,itemname,payload) + testroom1_LampOnOff:openhab.items.SwitchItem = myopenhab.get_item(itemname) + testroom1_LampOnOff.off() + testroom1_LampOnOff.addEventListener(types=openhab.events.ItemCommandEventType,listener=onAnyItemCommand,onlyIfEventsourceIsOpenhab=False) + myopenhab.parseEvent(eventData) + + + #itemDimmer = myopenhab.get_item("testroom1_LampDimmer") + + + + + + + + + #myopenhab.parseEvent(testdata[testname]) + t=0 while True: time.sleep(10) + t=t+1 x=0 - x=2 + if x==1: itemAzimuth=None elif x==2: - itemAzimuth.command(55.1) \ No newline at end of file + azimuthvalue= 55.1 + t + log.info("-------------------------setting azimuth to {}".format(azimuthvalue)) + itemAzimuth.command(azimuthvalue) + log.info("-------------------------did set azimuth to {}".format(itemAzimuth.state)) + #we receive an update from openhab immediately. \ No newline at end of file From 1ac15fb5486c3e759e09b7ebf78f48e5c3ae35dd Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Fri, 11 Dec 2020 00:59:13 +0100 Subject: [PATCH 03/25] ItemFactory to create new items create item test --- README.rst | 26 +++++- openhab/client.py | 41 ++++++++-- openhab/items.py | 136 +++++++++++++++++++++++++++++++- tests/test_eventsubscription.py | 116 ++++++++++++++++++++++++++- tests/testutil.py | 7 ++ 5 files changed, 310 insertions(+), 16 deletions(-) create mode 100644 tests/testutil.py diff --git a/README.rst b/README.rst index 84d02d4..8ddd779 100644 --- a/README.rst +++ b/README.rst @@ -21,12 +21,19 @@ python library for accessing the openHAB REST API This library allows for easily accessing the openHAB REST API. A number of features are implemented but not all, this is work in progress. +currently you can + - retrieve current state of items + - send updates and commands to items + - receive commands, updates and changes from openhab + + Requirements ------------ - python >= 3.5 - python :: dateutil - python :: requests + - python :: sseclient - openHAB version 2 Installation @@ -49,7 +56,7 @@ Example usage of the library: from openhab import OpenHAB base_url = 'http://localhost:8080/rest' - openhab = OpenHAB(base_url) + openhab = OpenHAB(base_url,autoUpdate=True) # fetch all items items = openhab.fetch_all_items() @@ -87,6 +94,23 @@ Example usage of the library: for v in lights_group.members.values(): v.update('OFF') + # receive updates from openhab: + + # fetch a item and keep it + testroom1_LampOnOff = openhab.get_item('light_switch') + + #define a callback function to receive events + def onLight_switchCommand(item: openhab.items.Item, event: openhab.events.ItemCommandEvent): + log.info("########################### COMMAND of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValueRaw, item.state)) + if event.source == openhab.events.EventSourceOpenhab: + log.info("this change came from openhab") + # install listener for evetns + testroom1_LampOnOff.addEventListener(types=openhab.events.ItemCommandEventType, listener=onLight_switchCommand, onlyIfEventsourceIsOpenhab=False) + # switch you will receive update also for your changes in the code. (see + testroom1_LampOnOff.off() + + #Events stop to be delivered + testroom1_LampOnOff=None Note on NULL and UNDEF ---------------------- diff --git a/openhab/client.py b/openhab/client.py index 55dd77a..9d26859 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -104,11 +104,19 @@ def _check_req_return(req: requests.Response) -> None: REST request. """ if not (200 <= req.status_code < 300): + logging.getLogger().error(req.content) req.raise_for_status() - def parseItem(self, event:openhab.events.ItemEvent): + def _parseItem(self, event:openhab.events.ItemEvent)->None: + """method to parse an ItemEvent from openhab. + it interprets the received ItemEvent data. + in case the item was previously registered it will then delegate further parsing of the event to item iteself through a call of the items _processExternalEvent method + + Args: + event:openhab.events.ItemEvent holding the eventdata + """ if event.itemname in self.registered_items: item=self.registered_items[event.itemname] if item is None: @@ -126,7 +134,14 @@ def parseItem(self, event:openhab.events.ItemEvent): - def parseEvent(self,eventData:typing.Dict): + def _parseEvent(self, eventData:typing.Dict)->None: + """method to parse a event from openhab. + it interprets the received event dictionary and populates an openhab.events.event Object. + for Itemevents it then calls _parseItem for a more detailed interpretation of the received data + then it informs all registered listeners of openhab events + Args: + eventData send by openhab in a Dict + """ log=logging.getLogger() eventreason=eventData["type"] @@ -156,7 +171,7 @@ def parseEvent(self,eventData:typing.Dict): else: log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=itemname, datatype=remoteDatatype, newValueRaw=newValue)) - self.parseItem(event) + self._parseItem(event) self.informEventListeners(event) else: log.info("received unknown Event-type in Openhab Event stream: {}".format(eventData)) @@ -182,7 +197,10 @@ def removeEventListener(self, listener:typing.Optional[typing.Callable[[openhab. - def sseDaemonThread(self): + def _sseDaemonThread(self): + """internal method to receice events from openhab. + This method blocks and therefore should be started as separate thread. + """ self.logger.info("starting Openhab - Event Deamon") next_waittime=initial_waittime=0.1 while self.__keep_event_deamon_running__: @@ -194,7 +212,7 @@ def sseDaemonThread(self): next_waittime = initial_waittime for event in messages: eventData = json.loads(event.data) - self.parseEvent(eventData) + self._parseEvent(eventData) if not self.__keep_event_deamon_running__: return @@ -206,18 +224,22 @@ def sseDaemonThread(self): - def register_item(self, item: openhab.items.Item): + def register_item(self, item: openhab.items.Item)->None: + """method to register an instantiated item. registered items can receive commands an updated from openhab. + Args: + an Item object + """ if not item is None and not item.name is None: if not item.name in self.registered_items: #self.registered_items[item.name]=weakref.ref(item) self.registered_items[item.name] = item - def __installSSEClient__(self): + def __installSSEClient__(self)->None: """ installs an event Stream to receive all Item events""" #now start readerThread self.__keep_event_deamon_running__=True - self.sseDaemon = threading.Thread(target=self.sseDaemonThread, args=(), daemon=True) + self.sseDaemon = threading.Thread(target=self._sseDaemonThread, args=(), daemon=True) self.sseDaemon.start() def req_get(self, uri_path: str) -> typing.Any: @@ -253,6 +275,9 @@ def req_post(self, uri_path: str, data: typing.Optional[dict] = None) -> None: self._check_req_return(r) return None + def req_json_put(self, uri_path: str, jasonData: str = None) -> None: + r = self.session.put(self.base_url + uri_path, data=jasonData, headers={'Content-Type': 'application/json',"Accept": "application/json"}, timeout=self.timeout) + self._check_req_return(r) def req_put(self, uri_path: str, data: typing.Optional[dict] = None) -> None: """Helper method for initiating a HTTP PUT request. diff --git a/openhab/items.py b/openhab/items.py index cf176f0..40ef5ff 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -21,9 +21,11 @@ # pylint: disable=bad-indentation from __future__ import annotations import logging +import inspect import re import typing - +import json +import time import dateutil.parser import openhab.types @@ -33,11 +35,109 @@ __author__ = 'Georges Toth ' __license__ = 'AGPLv3+' +class ItemFactory: + def __init__(self,openhabClient:openhab.client.OpenHAB): + self.openHABClient=openhabClient + + def createOrUpdateItem(self, + name: str, + type: typing.Union[str, typing.Type[Item]], + quantityType: typing.Optional[str] = None, + label: typing.Optional[str] = None, + category: typing.Optional[str] = None, + tags: typing.Optional[typing.List[str]] = None, + groupNames: typing.Optional[typing.List[str]] = None, + grouptype: typing.Optional[str] = None, + functionname: typing.Optional[str] = None, + functionparams: typing.Optional[typing.List[str]] = None + ) -> Item: + self.createOrUpdateItemAsync(name=name, + type=type, + quantityType=quantityType, + label=label, + category=category, + tags=tags, + groupNames=groupNames, + grouptype=grouptype, + functionname=functionname, + functionparams=functionparams) + + time.sleep(0.05) + result = None + retrycounter = 10 + while True: + try: + result = self.getItem(name) + return result + except Exception as e: + retrycounter -= 1 + if retrycounter < 0: + raise e + else: + time.sleep(0.05) + + + + def createOrUpdateItemAsync(self, + name:str, + type:typing.Union[str, typing.Type[Item]], + quantityType:typing.Optional[str]=None, + label:typing.Optional[str]=None, + category:typing.Optional[str]=None, + tags: typing.Optional[typing.List[str]]=None, + groupNames: typing.Optional[typing.List[str]]=None, + grouptype:typing.Optional[str]=None, + functionname:typing.Optional[str]=None, + functionparams: typing.Optional[typing.List[str]]=None + )->None: + + paramdict: typing.Dict[str, typing.Union[str, typing.List[str], typing.Dict[str, typing.Union[str, typing.List]]]] = {} + + if isinstance(type, str): + itemtypename=type + elif inspect.isclass(type): + if issubclass(type, Item): + itemtypename=type.TYPENAME + if quantityType is None: + paramdict["type"]=itemtypename + else: + paramdict["type"] = "{}:{}".format(itemtypename, quantityType) + + paramdict["name"]=name + + if not label is None: + paramdict["label"]=label + + if not category is None: + paramdict["category"] = category + + if not tags is None: + paramdict["tags"] = tags + + if not groupNames is None: + paramdict["groupNames"] = groupNames + + if not grouptype is None: + paramdict["groupType"] = grouptype + + if not functionname is None and not functionparams is None: + paramdict["function"] = {"name":functionname,"params":functionparams} + + + jsonBody=json.dumps(paramdict) + logging.getLogger().debug("about to create item with PUT request:{}".format(jsonBody)) + self.openHABClient.req_json_put('/items/{}'.format(name), jasonData=jsonBody) + + + def getItem(self,itemname): + return self.openHABClient.get_item(itemname) + class Item: """Base item class.""" types = [] # type: typing.List[typing.Type[openhab.types.CommandType]] + TYPENAME = "unknown" def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict) -> None: """Constructor. @@ -50,6 +150,8 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict) -> N self.openhab = openhab_conn self.autoUpdate = self.openhab.autoUpdate self.type_ = None + self.quantityType = None + self._unitOfMeasure = "" self.group = False self.name = '' self._state = None # type: typing.Optional[typing.Any] @@ -67,6 +169,11 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict) -> N self.eventListeners: typing.Dict[typing.Callable[[openhab.events.ItemEvent],None],Item.EventListener]={} #typing.List[typing.Callable] = [] + + + + + def init_from_json(self, json_data: dict): """Initialize this object from a json configuration as fetched from openHAB. @@ -86,6 +193,20 @@ def init_from_json(self, json_data: dict): else: self.type_ = json_data['type'] + parts=self.type_.split(":") + if len(parts)==2: + self.quantityType=parts[1] + if "editable" in json_data: + self.editable=json_data['editable'] + if "label" in json_data: + self.label=json_data['label'] + if "category" in json_data: + self.category=json_data['category'] + if "tags" in json_data: + self.tags=json_data['tags'] + if "groupNames" in json_data: + self.groupNames=json_data['groupNames'] + self.__set_state(json_data['state']) @property @@ -106,9 +227,9 @@ def state(self, value: typing.Any): oldstate= self._state self.update(value) # if oldstate==self._state: - # event=openhab.events.ItemStateEvent( itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_,newValueRaw=self._state, asUpdate=False) + # event=openhab.events.ItemStateEvent( name=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_,newValueRaw=self._state, asUpdate=False) # else: - # event = openhab.events.ItemStateChangedEvent(itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_, newValueRaw=self._state, oldRemoteDatatype=self.type_,oldValueRaw=oldstate, asUpdate=False) + # event = openhab.events.ItemStateChangedEvent(name=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_, newValueRaw=self._state, oldRemoteDatatype=self.type_,oldValueRaw=oldstate, asUpdate=False) # self._processEvent(event) @property @@ -389,6 +510,7 @@ class DateTimeItem(Item): """DateTime item type.""" types = [openhab.types.DateTimeType] + TYPENAME = "DateTime" def __gt__(self, other): return self._state > other @@ -430,6 +552,7 @@ def _rest_format(self, value): class PlayerItem(Item): """PlayerItem item type.""" + TYPENAME = "Player" types = [openhab.types.PlayerType] @@ -453,7 +576,9 @@ def previous(self) -> None: class SwitchItem(Item): """SwitchItem item type.""" + types = [openhab.types.OnOffType] + TYPENAME = "Switch" def on(self) -> None: """Set the state of the switch to ON.""" @@ -475,6 +600,7 @@ class NumberItem(Item): """NumberItem item type.""" types = [openhab.types.DecimalType] + TYPENAME = "Number" def _parse_rest(self, value: str) -> float: """Parse a REST result into a native object. @@ -518,6 +644,7 @@ class ContactItem(Item): """Contact item type.""" types = [openhab.types.OpenCloseType] + TYPENAME = "Contact" def command(self, *args, **kwargs) -> None: """This overrides the `Item` command method. @@ -539,6 +666,7 @@ class DimmerItem(Item): """DimmerItem item type.""" types = [openhab.types.OnOffType, openhab.types.PercentType, openhab.types.IncreaseDecreaseType] + TYPENAME = "Dimmer" def _parse_rest(self, value: str) -> int: """Parse a REST result into a native object. @@ -587,6 +715,7 @@ class ColorItem(Item): types = [openhab.types.OnOffType, openhab.types.PercentType, openhab.types.IncreaseDecreaseType, openhab.types.ColorType] + TYPENAME = "Color" def _parse_rest(self, value: str) -> str: """Parse a REST result into a native object. @@ -634,6 +763,7 @@ class RollershutterItem(Item): """RollershutterItem item type.""" types = [openhab.types.UpDownType, openhab.types.PercentType, openhab.types.StopType] + TYPENAME = "Rollershutter" def _parse_rest(self, value: str) -> int: """Parse a REST result into a native object. diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index 3fcc40a..e399f72 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -5,8 +5,11 @@ import time import openhab.items as items import logging +import json +import random +import tests.testutil as testutil log=logging.getLogger() -logging.basicConfig(level=20,format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") +logging.basicConfig(level=10,format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") log.error("xx") log.warning("www") @@ -51,7 +54,105 @@ def executeParseCheck(): stringToParse=testdata[testkey] -if True: + +def testCreateItem(myopenhab:openhab.OpenHAB): + myitemFactory = openhab.items.ItemFactory(myopenhab) + random.seed() + testprefix = "x2_{}".format(random.randint(1,1000)) + itemname = "{}CreateItemTest".format(testprefix) + itemQuantityType = "Angle" # "Length",Temperature,,Pressure,Speed,Intensity,Dimensionless,Angle + itemtype = "Number" + itemtype = openhab.items.NumberItem + + labeltext = "das ist eine testzahl:" + itemlabel = "[{labeltext}%.1f °]".format(labeltext=labeltext) + itemcategory = "{}TestCategory".format(testprefix) + itemtags: List[str] = ["{}testtag1".format(testprefix), "{}testtag2".format(testprefix)] + itemgroupNames: List[str] = ["{}testgroup1".format(testprefix), "{}testgroup2".format(testprefix)] + grouptype = "{}testgrouptype".format(testprefix) + functionname = "{}testfunctionname".format(testprefix) + functionparams: List[str] = ["{}testfunctionnameParam1".format(testprefix), "{}testfunctionnameParam2".format(testprefix), "{}testfunctionnameParam3".format(testprefix)] + + x2=myitemFactory.createOrUpdateItem(name=itemname, type=itemtype, quantityType=itemQuantityType, label=itemlabel, category=itemcategory, tags=itemtags, groupNames=itemgroupNames, grouptype=grouptype, functionname=functionname, functionparams=functionparams) + x2.state=123.45 + testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert(itemtype.TYPENAME+":"+itemQuantityType, x2.type_, "type") + testutil.doassert(123.45, x2.state, "state") + testutil.doassert(itemQuantityType, x2.quantityType, "quantityType") + testutil.doassert(itemlabel, x2.label, "label") + testutil.doassert(itemcategory,x2.category,"category") + for aExpectedTag in itemtags: + testutil.doassert(aExpectedTag in x2.tags,True,"tag {}".format(aExpectedTag)) + + for aExpectedGroupname in itemgroupNames: + testutil.doassert(aExpectedGroupname in x2.groupNames ,True,"tag {}".format(aExpectedGroupname)) + + + + + + + + + + + + +myopenhab = openhab.OpenHAB(base_url,autoUpdate=False) +testCreateItem(myopenhab) + + +if False: + myopenhab = openhab.OpenHAB(base_url, autoUpdate=False) + + testprefix="x1" + itemname="{}CreateItemTest".format(testprefix) + itemQuantityType="Angle" # "Length",Temperature,,Pressure,Speed,Intensity,Dimensionless,Angle + itemtype="Number" + + labeltext="das ist eine testzahl:" + itemlabel="[{labeltext}%.1f °]".format(labeltext=labeltext) + itemcategory="{}TestCategory".format(testprefix) + itemtags:List[str]=["{}testtag1".format(testprefix),"{}testtag2".format(testprefix)] + itemgroupNames:List[str]=["{}testgroup1".format(testprefix),"{}testgroup2".format(testprefix)] + grouptype= "{}testgrouptype".format(testprefix) + functionname="{}testfunctionname".format(testprefix) + functionparams:List[str]=["{}testfunctionnameParam1".format(testprefix),"{}testfunctionnameParam2".format(testprefix),"{}testfunctionnameParam3".format(testprefix)] + + + + #paramdict:Dict[str,Union[str,List[str],Dict[str,Union[str,List]]]]={} + + if itemQuantityType is None: + paramdict["type"]=itemtype + else: + paramdict["type"] = "{}:{}".format(itemtype,itemQuantityType) + + paramdict["name"]=itemname + + if not itemlabel is None: + paramdict["label"]=itemlabel + + if not itemcategory is None: + paramdict["category"] = itemcategory + + if not itemtags is None: + paramdict["tags"] = itemtags + + if not itemgroupNames is None: + paramdict["groupNames"] = itemgroupNames + + if not grouptype is None: + paramdict["groupType"] = grouptype + + if not functionname is None: + paramdict["function"] = {"name":functionname,"params":functionparams} + + + jsonBody=json.dumps(paramdict) + print(jsonBody) + myopenhab.req_json_put('/items/{}'.format(itemname), jasonData=jsonBody) +if False: myopenhab = openhab.OpenHAB(base_url,autoUpdate=True) itemDimmer=myopenhab.get_item("testroom1_LampDimmer") @@ -107,10 +208,14 @@ def createEventData(type,itemname,payload): return result + def onLight_switchCommand(item: openhab.items.Item, event: openhab.events.ItemCommandEvent): + log.info("########################### COMMAND of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValueRaw, item.state)) + def onAnyItemCommand(item: openhab.items.Item, event: openhab.events.ItemStateEvent): log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname, event.newValueRaw, item.state)) if not expectedValue is None: - assert event.newValue==expectedValue + actualValue=event.newValue + assert actualValue==expectedValue, "expected value to be {}, but it was {}".format(expectedValue,actualValue) testname="OnOff" @@ -121,8 +226,11 @@ def onAnyItemCommand(item: openhab.items.Item, event: openhab.events.ItemStateEv eventData=createEventData(type,itemname,payload) testroom1_LampOnOff:openhab.items.SwitchItem = myopenhab.get_item(itemname) testroom1_LampOnOff.off() + time.sleep(0.5) testroom1_LampOnOff.addEventListener(types=openhab.events.ItemCommandEventType,listener=onAnyItemCommand,onlyIfEventsourceIsOpenhab=False) - myopenhab.parseEvent(eventData) + testroom1_LampOnOff.addEventListener(types=openhab.events.ItemCommandEventType, listener=onLight_switchCommand, onlyIfEventsourceIsOpenhab=False) + # testroom1_LampOnOff=None + myopenhab._parseEvent(eventData) #itemDimmer = myopenhab.get_item("testroom1_LampDimmer") diff --git a/tests/testutil.py b/tests/testutil.py new file mode 100644 index 0000000..6c09f17 --- /dev/null +++ b/tests/testutil.py @@ -0,0 +1,7 @@ +import logging +from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable + +log=logging.getLogger() + +def doassert(expect:Any,actual:Any,label:Optional[str]=""): + assert actual==expect, f"expected {label}:'{expect}', but got '{actual}'".format(label=label,actual=actual,expect=expect) \ No newline at end of file From ac20567cffd5129fad535625fd08b5003c9544e2 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Sun, 13 Dec 2020 01:22:46 +0100 Subject: [PATCH 04/25] added testcases for creation of all itemtypes basic tests for setting states and commands added a method to delete items --- openhab/client.py | 37 ++++- openhab/items.py | 75 ++++++++- tests/test_create_delete_items.py | 261 ++++++++++++++++++++++++++++++ tests/test_eventsubscription.py | 42 +---- tests/testutil.py | 2 +- 5 files changed, 373 insertions(+), 44 deletions(-) create mode 100644 tests/test_create_delete_items.py diff --git a/openhab/client.py b/openhab/client.py index 9d26859..e63c2ea 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -223,6 +223,8 @@ def _sseDaemonThread(self): + def get_registered_items(self)->weakref.WeakValueDictionary: + return self.registered_items def register_item(self, item: openhab.items.Item)->None: """method to register an instantiated item. registered items can receive commands an updated from openhab. @@ -275,8 +277,39 @@ def req_post(self, uri_path: str, data: typing.Optional[dict] = None) -> None: self._check_req_return(r) return None - def req_json_put(self, uri_path: str, jasonData: str = None) -> None: - r = self.session.put(self.base_url + uri_path, data=jasonData, headers={'Content-Type': 'application/json',"Accept": "application/json"}, timeout=self.timeout) + + def req_json_put(self, uri_path: str, jsonData: str = None) -> None: + """Helper method for initiating a HTTP PUT request. + + Besides doing the actual request, it also checks the return value and returns the resulting decoded + JSON data. + + Args: + uri_path (str): The path to be used in the PUT request. + jsondata (str): the request data as jason + + + Returns: + None: No data is returned. + """ + + r = self.session.put(self.base_url + uri_path, data=jsonData, headers={'Content-Type': 'application/json', "Accept": "application/json"}, timeout=self.timeout) + self._check_req_return(r) + + def req_del(self, uri_path: str)->None: + """Helper method for initiating a HTTP DELETE request. + + Besides doing the actual request, it also checks the return value and returns the resulting decoded + JSON data. + + Args: + uri_path (str): The path to be used in the DELETE request. + + + Returns: + None: No data is returned. + """ + r= self.session.delete(self.base_url + uri_path,headers={"Accept": "application/json"}) self._check_req_return(r) def req_put(self, uri_path: str, data: typing.Optional[dict] = None) -> None: diff --git a/openhab/items.py b/openhab/items.py index 40ef5ff..acb8c95 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -19,6 +19,8 @@ # # pylint: disable=bad-indentation + + from __future__ import annotations import logging import inspect @@ -36,7 +38,15 @@ __license__ = 'AGPLv3+' class ItemFactory: + """A factory to get an Item from Openhab or create new items in openHAB""" + def __init__(self,openhabClient:openhab.client.OpenHAB): + """Constructor. + + Args: + openhab_conn (openhab.OpenHAB): openHAB object. + + """ self.openHABClient=openhabClient def createOrUpdateItem(self, @@ -51,6 +61,31 @@ def createOrUpdateItem(self, functionname: typing.Optional[str] = None, functionparams: typing.Optional[typing.List[str]] = None ) -> Item: + """creates a new item in openhab if there is no item with name 'name' yet. + if there is an item with 'name' already in openhab, the item gets updated with the infos provided. be aware that not provided fields will be deleted in openhab. + consider to get the existing item via 'getItem' and then read out existing fields to populate the parameters here. + + This function blocks until the item is created. + + + Args: + name (str): unique name of the item + type ( str or any Itemclass): the type used in openhab (like Group, Number, Contact, DateTime, Rollershutter, Color, Dimmer, Switch, Player) + server. + To create groups use the itemtype 'Group'! + quantityType (str): optional quantityType ( like Angle, Temperature, Illuminance (see https://www.openhab.org/docs/concepts/units-of-measurement.html)) + label (str): optional openhab label (see https://www.openhab.org/docs/configuration/items.html#label) + category (str): optional category. no documentation found + tags (List of str): optional list of tags (see https://www.openhab.org/docs/configuration/items.html#tags) + groupNames (List of str): optional list of groups this item belongs to. + grouptype (str): optional grouptype. no documentation found + functionname (str): optional functionname. no documentation found + functionparams (List of str): optional list of function Params. no documentation found + + Returns: + the created Item + """ + self.createOrUpdateItemAsync(name=name, type=type, quantityType=quantityType, @@ -62,6 +97,8 @@ def createOrUpdateItem(self, functionname=functionname, functionparams=functionparams) + + time.sleep(0.05) result = None retrycounter = 10 @@ -90,7 +127,29 @@ def createOrUpdateItemAsync(self, functionname:typing.Optional[str]=None, functionparams: typing.Optional[typing.List[str]]=None )->None: + """creates a new item in openhab if there is no item with name 'name' yet. + if there is an item with 'name' already in openhab, the item gets updated with the infos provided. be aware that not provided fields will be deleted in openhab. + consider to get the existing item via 'getItem' and then read out existing fields to populate the parameters here. + + This function does not wait for openhab to create the item. Use this function if you need to create many items quickly. + + + Args: + name (str): unique name of the item + type ( str or any Itemclass): the type used in openhab (like Group, Number, Contact, DateTime, Rollershutter, Color, Dimmer, Switch, Player) + server. + To create groups use the itemtype 'Group'! + quantityType (str): optional quantityType ( like Angle, Temperature, Illuminance (see https://www.openhab.org/docs/concepts/units-of-measurement.html)) + label (str): optional openhab label (see https://www.openhab.org/docs/configuration/items.html#label) + category (str): optional category. no documentation found + tags (List of str): optional list of tags (see https://www.openhab.org/docs/configuration/items.html#tags) + groupNames (List of str): optional list of groups this item belongs to. + grouptype (str): optional grouptype. no documentation found + functionname (str): optional functionname. no documentation found + functionparams (List of str): optional list of function Params. no documentation found + + """ paramdict: typing.Dict[str, typing.Union[str, typing.List[str], typing.Dict[str, typing.Union[str, typing.List]]]] = {} if isinstance(type, str): @@ -126,7 +185,7 @@ def createOrUpdateItemAsync(self, jsonBody=json.dumps(paramdict) logging.getLogger().debug("about to create item with PUT request:{}".format(jsonBody)) - self.openHABClient.req_json_put('/items/{}'.format(name), jasonData=jsonBody) + self.openHABClient.req_json_put('/items/{}'.format(name), jsonData=jsonBody) def getItem(self,itemname): @@ -139,7 +198,7 @@ class Item: types = [] # type: typing.List[typing.Type[openhab.types.CommandType]] TYPENAME = "unknown" - def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict) -> None: + def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto_update:typing.Optional[bool]=True) -> None: """Constructor. Args: @@ -148,7 +207,7 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict) -> N server. """ self.openhab = openhab_conn - self.autoUpdate = self.openhab.autoUpdate + self.autoUpdate = auto_update self.type_ = None self.quantityType = None self._unitOfMeasure = "" @@ -294,9 +353,15 @@ def _isMyOwnChange(self, event): else: return True + def delete(self): + """deletes the item from openhab """ + self.openhab.req_del('/items/{}'.format(self.name)) + self._state=None + self.removeAllEventListener() def _processExternalEvent(self, event:openhab.events.ItemEvent): + if not self.autoUpdate: return self.logger.info("processing external event") newValue,uom=self._parse_rest(event.newValueRaw) event.newValue=newValue @@ -382,6 +447,9 @@ def addEventListener(self,types:typing.List[openhab.events.EventType],listener:t eventListener=Item.EventListener(listeningTypes=types,listener=listener,onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab,alsoGetMyEchosFromOpenHAB=alsoGetMyEchosFromOpenHAB) self.eventListeners[listener]=eventListener + def removeAllEventListener(self): + self.eventListeners=[] + def removeEventListener(self,types:typing.List[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None]): if listener in self.eventListeners: eventListener = self.eventListeners[listener] @@ -506,6 +574,7 @@ def is_state_undef(self) -> bool: return False + class DateTimeItem(Item): """DateTime item type.""" diff --git a/tests/test_create_delete_items.py b/tests/test_create_delete_items.py new file mode 100644 index 0000000..5dadd2b --- /dev/null +++ b/tests/test_create_delete_items.py @@ -0,0 +1,261 @@ +from __future__ import annotations +from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable + + +import openhab +import openhab.events +import time +import openhab.items as items +import logging +import json +import random +import tests.testutil as testutil +from datetime import datetime,timedelta +log=logging.getLogger() +logging.basicConfig(level=10,format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") + +log.error("xx") +log.warning("www") +log.info("iii") +log.debug("ddddd") + + + +base_url = 'http://10.10.20.81:8080/rest' + + + + + +def testCreateItems(myopenhab:openhab.OpenHAB): + myitemFactory = openhab.items.ItemFactory(myopenhab) + random.seed() + nameprefix = "x2_{}".format(random.randint(1, 1000)) + knxshuttergroup=myopenhab.get_item("gKNXImport_shutter") + log.info("knxshuttergroup:{}".format(knxshuttergroup)) + + aGroupItem:openhab.items.Item =testGroup(myitemFactory,nameprefix) + log.info("the new group:{}".format(aGroupItem)) + aNumberItem=testNumberItem(myitemFactory,nameprefix) + + aContactItem:openhab.items.ContactItem=testContactItem(myitemFactory,nameprefix) + log.info("the new aContactItem:{}".format(aContactItem)) + + aDatetimeItem:openhab.items.DateTimeItem=testDateTimeItem(myitemFactory,nameprefix) + log.info("the new aDatetimeItem:{}".format(aDatetimeItem)) + + aRollershutterItem:openhab.items.RollershutterItem = testRollershutterItem(myitemFactory, nameprefix) + log.info("the new aRollershutterItem:{}".format(aRollershutterItem)) + + aColorItem:openhab.items.ColorItem = testColorItem(myitemFactory, nameprefix) + log.info("the new aColorItem:{}".format(aColorItem)) + + aDimmerItem:openhab.items.DimmerItem = testDimmerItem(myitemFactory, nameprefix) + log.info("the new aDimmerItem:{}".format(aDimmerItem)) + + aSwitchItem:openhab.items.SwitchItem = testSwitchItem(myitemFactory, nameprefix) + log.info("the new Switch:{}".format(aSwitchItem)) + + aPlayerItem:openhab.items.PlayerItem = testPlayerItem(myitemFactory, nameprefix) + log.info("the new Player:{}".format(aPlayerItem)) + + coloritemname=aColorItem.name + aColorItem.delete() + try: + shouldNotWork=myitemFactory.getItem(coloritemname) + testutil.doassert(False,True,"this lookup should raise a exception because the item should have been removed.") + except: + pass + + + #Group, Number, Contact, DateTime, Rollershutter, Color, Dimmer, Switch, Player + +def testNumberItem(itemFactory,nameprefix): + + itemname = "{}CreateItemTest".format(nameprefix) + itemQuantityType = "Angle" # "Length",Temperature,,Pressure,Speed,Intensity,Dimensionless,Angle + itemtype = "Number" + itemtype = openhab.items.NumberItem + + labeltext = "das ist eine testzahl:" + itemlabel = "[{labeltext}%.1f °]".format(labeltext=labeltext) + itemcategory = "{}TestCategory".format(nameprefix) + itemtags: List[str] = ["{}testtag1".format(nameprefix), "{}testtag2".format(nameprefix)] + itemgroupNames: List[str] = ["{}testgroup1".format(nameprefix), "{}testgroup2".format(nameprefix)] + grouptype = "{}testgrouptype".format(nameprefix) + functionname = "{}testfunctionname".format(nameprefix) + functionparams: List[str] = ["{}testfunctionnameParam1".format(nameprefix), "{}testfunctionnameParam2".format(nameprefix), "{}testfunctionnameParam3".format(nameprefix)] + + x2=itemFactory.createOrUpdateItem(name=itemname, type=itemtype, quantityType=itemQuantityType, label=itemlabel, category=itemcategory, tags=itemtags, groupNames=itemgroupNames, grouptype=grouptype, functionname=functionname, functionparams=functionparams) + x2.state=123.45 + testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert(itemtype.TYPENAME+":"+itemQuantityType, x2.type_, "type") + testutil.doassert(123.45, x2.state, "state") + testutil.doassert(itemQuantityType, x2.quantityType, "quantityType") + testutil.doassert(itemlabel, x2.label, "label") + testutil.doassert(itemcategory,x2.category,"category") + for aExpectedTag in itemtags: + testutil.doassert(aExpectedTag in x2.tags,True,"tag {}".format(aExpectedTag)) + + for aExpectedGroupname in itemgroupNames: + testutil.doassert(aExpectedGroupname in x2.groupNames ,True,"tag {}".format(aExpectedGroupname)) + + return x2 + +def testGroup(itemFactory,nameprefix)->openhab.items.Item: + itemtype = "Group" + itemname = "{}TestGroup".format(nameprefix) + testgroupItem = itemFactory.createOrUpdateItem(name=itemname, type=itemtype) + return testgroupItem + +def testContactItem(itemFactory,nameprefix): + + itemname = "{}CreateContactItemTest".format(nameprefix) + itemtype = openhab.items.ContactItem + + x2=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) + x2.state="OPEN" + testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert("OPEN", x2.state, "itemstate") + x2.state = "CLOSED" + testutil.doassert("CLOSED", x2.state, "itemstate") + try: + x2.state = "SEPP" + testutil.doassert(False, True, "this should have caused a exception!") + except: + pass + + return x2 + +def testDateTimeItem(itemFactory,nameprefix): + + itemname = "{}CreateDateTimeItemTest".format(nameprefix) + itemtype = openhab.items.DateTimeItem + + x2=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) + log.info("current datetime in item:{}".format(x2.state)) + now=datetime.now() + x2.state=now + testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert(now, x2.state, "itemstate") + return x2 + +def testRollershutterItem(itemFactory,nameprefix): + + itemname = "{}CreateRollershutterItemTest".format(nameprefix) + itemtype = openhab.items.RollershutterItem + + x2:openhab.items.RollershutterItem=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) + + + x2.up() + testutil.doassert(itemname,x2.name,"itemname") + + testutil.doassert("UP", x2.state, "itemstate") + x2.state=53 + testutil.doassert(53, x2.state, "itemstate") + return x2 + +def testColorItem(itemFactory,nameprefix): + + itemname = "{}CreateColorItemTest".format(nameprefix) + itemtype = openhab.items.ColorItem + + x2:openhab.items.ColorItem=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) + + + x2.on() + testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert("ON", x2.state, "itemstate") + newValue="51,52,53" + x2.state=newValue + + log.info("itemsate:{}".format(x2.state)) + testutil.doassert(newValue, x2.state, "itemstate") + return x2 + + + +def testDimmerItem(itemFactory,nameprefix): + + itemname = "{}CreateDimmerItemTest".format(nameprefix) + itemtype = openhab.items.DimmerItem + + x2:openhab.items.DimmerItem=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) + + + x2.on() + testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert("ON", x2.state, "itemstate") + + x2.off() + testutil.doassert("OFF", x2.state, "itemstate") + + + newValue=51 + x2.state=newValue + + log.info("itemsate:{}".format(x2.state)) + testutil.doassert(newValue, x2.state, "itemstate") + return x2 + + + +def testSwitchItem(itemFactory,nameprefix): + + itemname = "{}CreateSwitchItemTest".format(nameprefix) + itemtype = openhab.items.SwitchItem + + x2:openhab.items.SwitchItem=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) + + + x2.on() + testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert("ON", x2.state, "itemstate") + + x2.off() + testutil.doassert("OFF", x2.state, "itemstate") + + x2.toggle() + testutil.doassert("ON", x2.state, "itemstate") + + newValue = "OFF" + x2.state = newValue + + log.info("itemsate:{}".format(x2.state)) + testutil.doassert(newValue, x2.state, "itemstate") + return x2 + + + + +def testPlayerItem(itemFactory,nameprefix): + itemname = "{}CreatePlayerItemTest".format(nameprefix) + itemtype = openhab.items.PlayerItem + + x2: openhab.items.PlayerItem = itemFactory.createOrUpdateItem(name=itemname, type=itemtype) + x2.play() + + testutil.doassert(itemname, x2.name, "itemname") + testutil.doassert("PLAY", x2.state, "itemstate") + + x2.pause() + testutil.doassert("PAUSE", x2.state, "itemstate") + return x2 + + + + + + + +myopenhab = openhab.OpenHAB(base_url,autoUpdate=False) +keeprunning=True +testCreateItems(myopenhab) + + +while keeprunning: + time.sleep(10) + + x=0 + diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index e399f72..a2c74e8 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -1,5 +1,7 @@ +from __future__ import annotations from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable + import openhab import openhab.events import time @@ -55,42 +57,6 @@ def executeParseCheck(): -def testCreateItem(myopenhab:openhab.OpenHAB): - myitemFactory = openhab.items.ItemFactory(myopenhab) - random.seed() - testprefix = "x2_{}".format(random.randint(1,1000)) - itemname = "{}CreateItemTest".format(testprefix) - itemQuantityType = "Angle" # "Length",Temperature,,Pressure,Speed,Intensity,Dimensionless,Angle - itemtype = "Number" - itemtype = openhab.items.NumberItem - - labeltext = "das ist eine testzahl:" - itemlabel = "[{labeltext}%.1f °]".format(labeltext=labeltext) - itemcategory = "{}TestCategory".format(testprefix) - itemtags: List[str] = ["{}testtag1".format(testprefix), "{}testtag2".format(testprefix)] - itemgroupNames: List[str] = ["{}testgroup1".format(testprefix), "{}testgroup2".format(testprefix)] - grouptype = "{}testgrouptype".format(testprefix) - functionname = "{}testfunctionname".format(testprefix) - functionparams: List[str] = ["{}testfunctionnameParam1".format(testprefix), "{}testfunctionnameParam2".format(testprefix), "{}testfunctionnameParam3".format(testprefix)] - - x2=myitemFactory.createOrUpdateItem(name=itemname, type=itemtype, quantityType=itemQuantityType, label=itemlabel, category=itemcategory, tags=itemtags, groupNames=itemgroupNames, grouptype=grouptype, functionname=functionname, functionparams=functionparams) - x2.state=123.45 - testutil.doassert(itemname,x2.name,"itemname") - testutil.doassert(itemtype.TYPENAME+":"+itemQuantityType, x2.type_, "type") - testutil.doassert(123.45, x2.state, "state") - testutil.doassert(itemQuantityType, x2.quantityType, "quantityType") - testutil.doassert(itemlabel, x2.label, "label") - testutil.doassert(itemcategory,x2.category,"category") - for aExpectedTag in itemtags: - testutil.doassert(aExpectedTag in x2.tags,True,"tag {}".format(aExpectedTag)) - - for aExpectedGroupname in itemgroupNames: - testutil.doassert(aExpectedGroupname in x2.groupNames ,True,"tag {}".format(aExpectedGroupname)) - - - - - @@ -99,7 +65,7 @@ def testCreateItem(myopenhab:openhab.OpenHAB): myopenhab = openhab.OpenHAB(base_url,autoUpdate=False) -testCreateItem(myopenhab) + if False: @@ -151,7 +117,7 @@ def testCreateItem(myopenhab:openhab.OpenHAB): jsonBody=json.dumps(paramdict) print(jsonBody) - myopenhab.req_json_put('/items/{}'.format(itemname), jasonData=jsonBody) + myopenhab.req_json_put('/items/{}'.format(itemname), jsonData=jsonBody) if False: myopenhab = openhab.OpenHAB(base_url,autoUpdate=True) diff --git a/tests/testutil.py b/tests/testutil.py index 6c09f17..eeb8dfd 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -4,4 +4,4 @@ log=logging.getLogger() def doassert(expect:Any,actual:Any,label:Optional[str]=""): - assert actual==expect, f"expected {label}:'{expect}', but got '{actual}'".format(label=label,actual=actual,expect=expect) \ No newline at end of file + assert actual==expect, f"expected {label}:'{expect}', but it actually has '{actual}'".format(label=label,actual=actual,expect=expect) \ No newline at end of file From deae6a0d1b6001351a92959d75d74397770419b1 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Sun, 13 Dec 2020 13:51:21 +0100 Subject: [PATCH 05/25] added testcases finished documentation --- README.rst | 35 ++++- openhab/client.py | 27 +++- openhab/events.py | 4 + openhab/items.py | 64 +++++++-- tests/test_create_delete_items.py | 123 ++++++++++++----- tests/test_eventsubscription.py | 220 ++++++++++++++---------------- tests/testutil.py | 20 +++ 7 files changed, 322 insertions(+), 171 deletions(-) diff --git a/README.rst b/README.rst index 8ddd779..aa5c2a1 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,9 @@ A number of features are implemented but not all, this is work in progress. currently you can - retrieve current state of items - send updates and commands to items - - receive commands, updates and changes from openhab + - receive commands, updates and changes triggered by openhab + - create new items and groups + - delete items and groups Requirements @@ -105,13 +107,42 @@ Example usage of the library: if event.source == openhab.events.EventSourceOpenhab: log.info("this change came from openhab") # install listener for evetns - testroom1_LampOnOff.addEventListener(types=openhab.events.ItemCommandEventType, listener=onLight_switchCommand, onlyIfEventsourceIsOpenhab=False) + testroom1_LampOnOff.addEventListener(listeningTypes=openhab.events.ItemCommandEventType, listener=onLight_switchCommand, onlyIfEventsourceIsOpenhab=False) # switch you will receive update also for your changes in the code. (see testroom1_LampOnOff.off() #Events stop to be delivered testroom1_LampOnOff=None + + #create or delete items: + # first instantiate a Factory: + itemFactory = openhab.items.ItemFactory(openhab) + #create the item + testDimmer = itemFactory.createOrUpdateItem(name="the_testDimmer", type=openhab.items.DimmerItem) + #use item + testDimmer.state=95 + + + + # you can set change many item attributes: + nameprefix="testcase_1_" + itemname = "{}CreateItemTest".format(nameprefix) + itemQuantityType = "Angle" + itemtype = "Number" + itemtype = openhab.items.NumberItem + + labeltext = "this is a test azimuth:" + itemlabel = "[{labeltext}%.1f °]".format(labeltext=labeltext) + itemcategory = "{}TestCategory".format(nameprefix) + itemtags: List[str] = ["{}testtag1".format(nameprefix), "{}testtag2".format(nameprefix)] + itemgroupNames: List[str] = ["{}testgroup1".format(nameprefix), "{}testgroup2".format(nameprefix)] + grouptype = "{}testgrouptype".format(nameprefix) + functionname = "{}testfunctionname".format(nameprefix) + functionparams: List[str] = ["{}testfunctionnameParam1".format(nameprefix), "{}testfunctionnameParam2".format(nameprefix), "{}testfunctionnameParam3".format(nameprefix)] + + testazimuth=itemFactory.createOrUpdateItem(name=itemname, type=itemtype, quantityType=itemQuantityType, label=itemlabel, category=itemcategory, tags=itemtags, groupNames=itemgroupNames, grouptype=grouptype, functionname=functionname, functionparams=functionparams) + Note on NULL and UNDEF ---------------------- diff --git a/openhab/client.py b/openhab/client.py index e63c2ea..0eeec48 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -61,8 +61,8 @@ def __init__(self, base_url: str, http_auth (AuthBase, optional): An alternative to username/password pair, is to specify a custom http authentication object of type :class:`requests.auth.AuthBase`. timeout (float, optional): An optional timeout for REST transactions - autoUpdate (bool, optional): Register for Openhab Item Events to actively get informed about changes. - maxEchoToOpenHAB_ms (int, optional): interpret Events from openHAB with same statevalue as we have coming within maxEchoToOpenhabMS millisends since our update/command as echos of our update//command + autoUpdate (bool, optional): True: receive Openhab Item Events to actively get informed about changes. + maxEchoToOpenHAB_ms (int, optional): interpret Events from openHAB which hold a state-value equal to items current state-value which are coming in within maxEchoToOpenhabMS millisends since our update/command as echos of our own update//command Returns: OpenHAB: openHAB class instance. """ @@ -172,11 +172,15 @@ def _parseEvent(self, eventData:typing.Dict)->None: log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=itemname, datatype=remoteDatatype, newValueRaw=newValue)) self._parseItem(event) - self.informEventListeners(event) + self._informEventListeners(event) else: log.info("received unknown Event-type in Openhab Event stream: {}".format(eventData)) - def informEventListeners(self,event:openhab.events.ItemEvent): + def _informEventListeners(self, event:openhab.events.ItemEvent): + """internal method to send itemevents to listeners. + Args: + event:openhab.events.ItemEvent to be sent to listeners + """ for aListener in self.eventListeners: try: aListener(event) @@ -184,9 +188,17 @@ def informEventListeners(self,event:openhab.events.ItemEvent): self.logger.error("error executing Eventlistener for event:{}.".format(event.itemname),e) def addEventListener(self, listener:typing.Callable[[openhab.events.ItemEvent],None]): + """method to register a callback function to get informed about all Item-Events received from openhab. + Args: + listener:typing.Callable[[openhab.events.ItemEvent] a method with one parameter of type openhab.events.ItemEvent which will be called for every event + """ self.eventListeners.append(listener) def removeEventListener(self, listener:typing.Optional[typing.Callable[[openhab.events.ItemEvent],None]]=None): + """method to unregister a callback function to stop getting informed about all Item-Events received from openhab. + Args: + listener:typing.Callable[[openhab.events.ItemEvent] the method to be removed. + """ if listener is None: self.eventListeners.clear() elif listener in self.eventListeners: @@ -198,7 +210,7 @@ def removeEventListener(self, listener:typing.Optional[typing.Callable[[openhab. def _sseDaemonThread(self): - """internal method to receice events from openhab. + """internal method to receive events from openhab. This method blocks and therefore should be started as separate thread. """ self.logger.info("starting Openhab - Event Deamon") @@ -224,10 +236,15 @@ def _sseDaemonThread(self): def get_registered_items(self)->weakref.WeakValueDictionary: + """get a Dict of weak references to registered items. + Args: + an Item object + """ return self.registered_items def register_item(self, item: openhab.items.Item)->None: """method to register an instantiated item. registered items can receive commands an updated from openhab. + Usually you don´t need to register as Items register themself. Args: an Item object """ diff --git a/openhab/events.py b/openhab/events.py index 204a65a..bb4579a 100644 --- a/openhab/events.py +++ b/openhab/events.py @@ -37,12 +37,14 @@ @dataclass class ItemEvent(object): + """The base class for all ItemEvents""" type = ItemEventType itemname: str source: EventSource @dataclass class ItemStateEvent(ItemEvent): + """a Event representing a state event on a Item""" type = ItemStateEventType remoteDatatype: str newValue: typing.Any @@ -53,6 +55,7 @@ class ItemStateEvent(ItemEvent): @dataclass class ItemCommandEvent(ItemEvent): + """a Event representing a command event on a Item""" type = ItemCommandEventType remoteDatatype: str newValue: typing.Any @@ -62,6 +65,7 @@ class ItemCommandEvent(ItemEvent): @dataclass class ItemStateChangedEvent(ItemStateEvent): + """a Event representing a state change event on a Item""" type = ItemStateChangedEventType oldRemoteDatatype: str oldValueRaw: str diff --git a/openhab/items.py b/openhab/items.py index acb8c95..0527656 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -38,7 +38,7 @@ __license__ = 'AGPLv3+' class ItemFactory: - """A factory to get an Item from Openhab or create new items in openHAB""" + """A factory to get an Item from Openhab, create new or delete existing items in openHAB""" def __init__(self,openhabClient:openhab.client.OpenHAB): """Constructor. @@ -283,13 +283,8 @@ def state(self,fetchFromOpenhab=False) -> typing.Any: @state.setter def state(self, value: typing.Any): - oldstate= self._state self.update(value) - # if oldstate==self._state: - # event=openhab.events.ItemStateEvent( name=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_,newValueRaw=self._state, asUpdate=False) - # else: - # event = openhab.events.ItemStateChangedEvent(name=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_, newValueRaw=self._state, oldRemoteDatatype=self.type_,oldValueRaw=oldstate, asUpdate=False) - # self._processEvent(event) + @property def members(self): @@ -341,6 +336,7 @@ def _rest_format(self, value: str) -> typing.Union[str, bytes]: return _value def _isMyOwnChange(self, event): + """find out if the incoming event is actually just a echo of my previous command or change""" now = datetime.now() self.logger.debug("_isMyOwnChange:event.source:{}, event.type{}, self._state:{}, event.newValue:{},self.lastCommandSent:{}, self.lastUpdateSent:{} , now:{}".format(event.source,event.type,self._state,event.newValue ,self.lastCommandSent,self.lastUpdateSent,now)) if event.source == openhab.events.EventSourceOpenhab: @@ -367,9 +363,11 @@ def _processExternalEvent(self, event:openhab.events.ItemEvent): event.newValue=newValue event.unitOfMeasure=uom if event.type==openhab.events.ItemStateChangedEventType: - oldValue,ouom=self._parse_rest(event.oldValueRaw) - event.oldValue=oldValue - event.oldUnitOfMeasure=ouom + try: + oldValue,ouom=self._parse_rest(event.oldValueRaw) + except: + event.oldValue=None + event.oldUnitOfMeasure=None isMyOwnChange=self._isMyOwnChange(event) self.logger.info("external event:{}".format(event)) if not isMyOwnChange: @@ -397,7 +395,16 @@ def _processInternalEvent(self,event:openhab.events.ItemEvent): class EventListener(object): + """EventListener Objects hold data about a registered event listener""" def __init__(self,listeningTypes:typing.Set[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab,alsoGetMyEchosFromOpenHAB): + """Constructor of an EventListener Object + Args: + listeningTypes (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is interested in. + onlyIfEventsourceIsOpenhab (bool): the listener only wants events that are coming from openhab. + alsoGetMyEchosFromOpenHAB (bool): the listener also wants to receive events coming from openhab that originally were triggered by commands or changes by our item itself. + + + """ allTypes = {openhab.events.ItemStateEvent.type, openhab.events.ItemCommandEvent.type, openhab.events.ItemStateChangedEvent.type} if listeningTypes is None: self.listeningTypes = allTypes @@ -413,6 +420,11 @@ def __init__(self,listeningTypes:typing.Set[openhab.events.EventType],listener:t self.alsoGetMyEchosFromOpenHAB=alsoGetMyEchosFromOpenHAB def addTypes(self,listeningTypes:typing.Set[openhab.events.EventType]): + """add aditional listening types + Args: + listeningTypes (openhab.events.EventType or set of openhab.events.EventType): the additional eventTypes the listener is interested in. + + """ if listeningTypes is None: return elif not hasattr(listeningTypes, '__iter__'): self.listeningTypes.add(listeningTypes) @@ -422,6 +434,11 @@ def addTypes(self,listeningTypes:typing.Set[openhab.events.EventType]): self.listeningTypes.update(listeningTypes) def removeTypes(self,listeningTypes:typing.Set[openhab.events.EventType]): + """remove listening types + Args: + listeningTypes (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is not interested in anymore + + """ if listeningTypes is None: self.listeningTypes.clear() elif not hasattr(listeningTypes, '__iter__'): @@ -436,21 +453,40 @@ def removeTypes(self,listeningTypes:typing.Set[openhab.events.EventType]): - def addEventListener(self,types:typing.List[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab=True,alsoGetMyEchosFromOpenHAB=False): + def addEventListener(self,listeningTypes:typing.Set[openhab.events.EventType],listener:typing.Callable[[openhab.items.Item,openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab=True,alsoGetMyEchosFromOpenHAB=False): + """add a Listener interested in changes of items happening in openhab + Args: + Args: + listeningTypes (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is interested in. + listener (Callable[[openhab.items.Item,openhab.events.ItemEvent],None]: a method with 2 parameters: + item (openhab.items.Item): the item that received a command, change or update + event (openhab.events.ItemEvent): the item Event holding the actual change + onlyIfEventsourceIsOpenhab (bool): the listener only wants events that are coming from openhab. + alsoGetMyEchosFromOpenHAB (bool): the listener also wants to receive events coming from openhab that originally were triggered by commands or changes by our item itself. + + """ if listener in self.eventListeners: eventListener= self.eventListeners[listener] - eventListener.addTypes(types) + eventListener.addTypes(listeningTypes) eventListener.onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab else: - eventListener=Item.EventListener(listeningTypes=types,listener=listener,onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab,alsoGetMyEchosFromOpenHAB=alsoGetMyEchosFromOpenHAB) + eventListener=Item.EventListener(listeningTypes=listeningTypes,listener=listener,onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab,alsoGetMyEchosFromOpenHAB=alsoGetMyEchosFromOpenHAB) self.eventListeners[listener]=eventListener def removeAllEventListener(self): self.eventListeners=[] def removeEventListener(self,types:typing.List[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None]): + """removes a previously registered Listener interested in changes of items happening in openhab + Args: + Args: + listeningTypes (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is interested in. + listener: the previously registered listener method. + + + """ if listener in self.eventListeners: eventListener = self.eventListeners[listener] eventListener.removeTypes(types) @@ -692,6 +728,8 @@ def _parse_rest(self, value: str) -> float: #logging.getLogger().debug("original value:{}, myvalue:{}, my UoM:{}".format(m,value,unitOfMeasure)) return (float(value),unitOfMeasure) + else: + return value except Exception as e: self.logger.error("error in parsing new value '{}' for '{}'".format(value,self.name),e) diff --git a/tests/test_create_delete_items.py b/tests/test_create_delete_items.py index 5dadd2b..cf8199c 100644 --- a/tests/test_create_delete_items.py +++ b/tests/test_create_delete_items.py @@ -1,3 +1,24 @@ +# -*- coding: utf-8 -*- +"""tests for creating and deletion of items """ + +# +# Alexey Grubauer (c) 2020-present +# +# python-openhab is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# python-openhab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with python-openhab. If not, see . +# +# pylint: disable=bad-indentation + from __future__ import annotations from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable @@ -27,48 +48,80 @@ -def testCreateItems(myopenhab:openhab.OpenHAB): +def test_create_and_delete_items(myopenhab:openhab.OpenHAB, nameprefix): + log.info("starting tests 'create and delete items'") myitemFactory = openhab.items.ItemFactory(myopenhab) - random.seed() - nameprefix = "x2_{}".format(random.randint(1, 1000)) - knxshuttergroup=myopenhab.get_item("gKNXImport_shutter") - log.info("knxshuttergroup:{}".format(knxshuttergroup)) - aGroupItem:openhab.items.Item =testGroup(myitemFactory,nameprefix) - log.info("the new group:{}".format(aGroupItem)) - aNumberItem=testNumberItem(myitemFactory,nameprefix) + try: + aGroupItem:openhab.items.Item =testGroup(myitemFactory,nameprefix) + log.info("the new group:{}".format(aGroupItem)) + aNumberItem=testNumberItem(myitemFactory,nameprefix) - aContactItem:openhab.items.ContactItem=testContactItem(myitemFactory,nameprefix) - log.info("the new aContactItem:{}".format(aContactItem)) + aContactItem:openhab.items.ContactItem=testContactItem(myitemFactory,nameprefix) + log.info("the new aContactItem:{}".format(aContactItem)) - aDatetimeItem:openhab.items.DateTimeItem=testDateTimeItem(myitemFactory,nameprefix) - log.info("the new aDatetimeItem:{}".format(aDatetimeItem)) + aDatetimeItem:openhab.items.DateTimeItem=testDateTimeItem(myitemFactory,nameprefix) + log.info("the new aDatetimeItem:{}".format(aDatetimeItem)) - aRollershutterItem:openhab.items.RollershutterItem = testRollershutterItem(myitemFactory, nameprefix) - log.info("the new aRollershutterItem:{}".format(aRollershutterItem)) + aRollershutterItem:openhab.items.RollershutterItem = testRollershutterItem(myitemFactory, nameprefix) + log.info("the new aRollershutterItem:{}".format(aRollershutterItem)) - aColorItem:openhab.items.ColorItem = testColorItem(myitemFactory, nameprefix) - log.info("the new aColorItem:{}".format(aColorItem)) + aColorItem:openhab.items.ColorItem = testColorItem(myitemFactory, nameprefix) + log.info("the new aColorItem:{}".format(aColorItem)) - aDimmerItem:openhab.items.DimmerItem = testDimmerItem(myitemFactory, nameprefix) - log.info("the new aDimmerItem:{}".format(aDimmerItem)) + aDimmerItem:openhab.items.DimmerItem = testDimmerItem(myitemFactory, nameprefix) + log.info("the new aDimmerItem:{}".format(aDimmerItem)) - aSwitchItem:openhab.items.SwitchItem = testSwitchItem(myitemFactory, nameprefix) - log.info("the new Switch:{}".format(aSwitchItem)) + aSwitchItem:openhab.items.SwitchItem = testSwitchItem(myitemFactory, nameprefix) + log.info("the new Switch:{}".format(aSwitchItem)) + + aPlayerItem:openhab.items.PlayerItem = testPlayerItem(myitemFactory, nameprefix) + log.info("the new Player:{}".format(aPlayerItem)) + + log.info("creation tests worked") + finally: + aGroupItem.delete() + aNumberItem.delete() + aContactItem.delete() + aDatetimeItem.delete() + aRollershutterItem.delete() + + coloritemname=aColorItem.name + aColorItem.delete() + try: + shouldNotWork=myitemFactory.getItem(coloritemname) + testutil.doassert(False,True,"this getItem should raise a exception because the item should have been removed.") + except: + pass + log.info("deletion of coloritem worked") + aDimmerItem.delete() + aSwitchItem.delete() + aPlayerItem.delete() - aPlayerItem:openhab.items.PlayerItem = testPlayerItem(myitemFactory, nameprefix) - log.info("the new Player:{}".format(aPlayerItem)) - coloritemname=aColorItem.name - aColorItem.delete() - try: - shouldNotWork=myitemFactory.getItem(coloritemname) - testutil.doassert(False,True,"this lookup should raise a exception because the item should have been removed.") - except: - pass - #Group, Number, Contact, DateTime, Rollershutter, Color, Dimmer, Switch, Player + + + + log.info("test 'create and delete items' finished successfully") + + +def delete_all_items_starting_with(myopenhab:openhab.OpenHAB, nameprefix): + log.info("starting to delete all items starting with '{}'".format(nameprefix)) + if nameprefix is None: return + if len(nameprefix) < 3: + log.warning("don´t think that you really want to do this") + return + allItems=myopenhab.fetch_all_items() + count=0 + for aItemname in allItems: + if aItemname.startswith(nameprefix): + aItem=allItems[aItemname] + aItem.delete() + count+=1 + log.info("finished to delete all items starting with '{}'. deleted {} items".format(nameprefix,count)) + def testNumberItem(itemFactory,nameprefix): @@ -251,11 +304,9 @@ def testPlayerItem(itemFactory,nameprefix): myopenhab = openhab.OpenHAB(base_url,autoUpdate=False) keeprunning=True -testCreateItems(myopenhab) - - -while keeprunning: - time.sleep(10) +random.seed() +nameprefix = "x2_{}".format(random.randint(1, 1000)) - x=0 +test_create_and_delete_items(myopenhab, nameprefix) +#delete_all_items_starting_with(myopenhab,"x2_") diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index a2c74e8..08cbf5a 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +"""tests the subscription for events from openhab """ + +# +# Alexey Grubauer (c) 2020-present +# +# python-openhab is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# python-openhab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with python-openhab. If not, see . +# +# pylint: disable=bad-indentation from __future__ import annotations from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable @@ -13,10 +33,10 @@ log=logging.getLogger() logging.basicConfig(level=10,format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") -log.error("xx") -log.warning("www") -log.info("iii") -log.debug("ddddd") +log.error("errormessage") +log.warning("waringingmessage") +log.info("infomessage") +log.debug("debugmessage") @@ -51,9 +71,11 @@ testitems:Dict[str,openhab.items.Item] = {} def executeParseCheck(): + log.info("executing parse checks") for testkey in testdata: log.info("testing:{}".format(testkey)) stringToParse=testdata[testkey] + log.info("parse checks finished successfully") @@ -64,142 +86,117 @@ def executeParseCheck(): -myopenhab = openhab.OpenHAB(base_url,autoUpdate=False) -if False: - myopenhab = openhab.OpenHAB(base_url, autoUpdate=False) +if True: + myopenhab = openhab.OpenHAB(base_url,autoUpdate=True) + myItemfactory = openhab.items.ItemFactory(myopenhab) + random.seed() + namesuffix = "_{}".format(random.randint(1, 1000)) + testnumberA:openhab.items.NumberItem = myItemfactory.createOrUpdateItem(name="test_eventSubscription_numberitem_A{}".format(namesuffix),type=openhab.items.NumberItem) + testnumberB:openhab.items.NumberItem = myItemfactory.createOrUpdateItem(name="test_eventSubscription_numberitem_B{}".format(namesuffix),type=openhab.items.NumberItem) + itemname = "test_eventSubscription_switchitem_A{}".format(namesuffix) + switchItem:openhab.items.SwitchItem = myItemfactory.createOrUpdateItem(name=itemname, type=openhab.items.SwitchItem) + try: + testnumberA.state = 44.0 + testnumberB.state = 66.0 - testprefix="x1" - itemname="{}CreateItemTest".format(testprefix) - itemQuantityType="Angle" # "Length",Temperature,,Pressure,Speed,Intensity,Dimensionless,Angle - itemtype="Number" - labeltext="das ist eine testzahl:" - itemlabel="[{labeltext}%.1f °]".format(labeltext=labeltext) - itemcategory="{}TestCategory".format(testprefix) - itemtags:List[str]=["{}testtag1".format(testprefix),"{}testtag2".format(testprefix)] - itemgroupNames:List[str]=["{}testgroup1".format(testprefix),"{}testgroup2".format(testprefix)] - grouptype= "{}testgrouptype".format(testprefix) - functionname="{}testfunctionname".format(testprefix) - functionparams:List[str]=["{}testfunctionnameParam1".format(testprefix),"{}testfunctionnameParam2".format(testprefix),"{}testfunctionnameParam3".format(testprefix)] + expect_A = None + expect_B = None + def on_A_Change(item:openhab.items.NumberItem ,event:openhab.events.ItemStateEvent): + log.info("########################### UPDATE of {itemname} to eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state}) from OPENHAB ONLY".format( + itemname=event.itemname,eventvalue=event.newValue,eventvalueraw=event.newValueRaw, item_state=item._state,itemstate=item.state)) - #paramdict:Dict[str,Union[str,List[str],Dict[str,Union[str,List]]]]={} + testnumberA.addEventListener(openhab.events.ItemCommandEventType,on_A_Change,onlyIfEventsourceIsOpenhab=True) - if itemQuantityType is None: - paramdict["type"]=itemtype - else: - paramdict["type"] = "{}:{}".format(itemtype,itemQuantityType) + def ontestnumberBChange(item:openhab.items.NumberItem ,event:openhab.events.ItemStateEvent): + log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname, event.newValueRaw, item.state)) + if not expect_B is None: + assert item.state == expect_B - paramdict["name"]=itemname + testnumberB.addEventListener(openhab.events.ItemCommandEventType,ontestnumberBChange,onlyIfEventsourceIsOpenhab=True) - if not itemlabel is None: - paramdict["label"]=itemlabel - if not itemcategory is None: - paramdict["category"] = itemcategory + def ontestnumberAChangeAll(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): + if event.source == openhab.events.EventSourceInternal: + log.info("########################### INTERNAL UPDATE of {} to {} (itemsvalue:{}) from internal".format(event.itemname,event.newValueRaw, item.state)) + else: + log.info("########################### EXTERNAL UPDATE of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValueRaw, item.state)) - if not itemtags is None: - paramdict["tags"] = itemtags + testnumberA.addEventListener(openhab.events.ItemCommandEventType,ontestnumberAChangeAll,onlyIfEventsourceIsOpenhab=False) - if not itemgroupNames is None: - paramdict["groupNames"] = itemgroupNames + #print(itemClock) - if not grouptype is None: - paramdict["groupType"] = grouptype + time.sleep(2) + log.info("###################################### starting test 'internal Event'") - if not functionname is None: - paramdict["function"] = {"name":functionname,"params":functionparams} + expect_B=2 + testnumberB.state=2 + time.sleep(0.1) + expect_B = None + checkcount = 0 - jsonBody=json.dumps(paramdict) - print(jsonBody) - myopenhab.req_json_put('/items/{}'.format(itemname), jsonData=jsonBody) -if False: - myopenhab = openhab.OpenHAB(base_url,autoUpdate=True) - itemDimmer=myopenhab.get_item("testroom1_LampDimmer") - print(itemDimmer) - itemAzimuth=myopenhab.get_item("testworld_Azimuth") - print(itemAzimuth) - itemAzimuth.state=44.0 - itemClock=myopenhab.get_item("myClock") - expectClock = None - expectedValue=None + def createEventData(type,itemname,payload): + result={} + result["type"]=type + result["topic"]="smarthome/items/{itemname}/state".format(itemname=itemname) + result["payload"]=payload + return result + + + def onLight_switchCommand(item: openhab.items.Item, event: openhab.events.ItemCommandEvent): + log.info("########################### COMMAND of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValueRaw, item.state)) + def onAnyItemCommand(item: openhab.items.Item, event: openhab.events.ItemStateEvent): + log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname, event.newValueRaw, item.state)) + if not expected_switch_Value is None: + global checkcount + actualValue=event.newValue + assert actualValue == expected_switch_Value, "expected value to be {}, but it was {}".format(expected_switch_Value, actualValue) + checkcount+=1 - def onAzimuthChange(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): - log.info("########################### UPDATE of {itemname} to eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state}) from OPENHAB ONLY".format( - itemname=event.itemname,eventvalue=event.newValue,eventvalueraw=event.newValueRaw, item_state=item._state,itemstate=item.state)) - itemAzimuth.addEventListener(openhab.events.ItemCommandEventType,onAzimuthChange,onlyIfEventsourceIsOpenhab=True) - def onClockChange(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): - log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname, event.newValueRaw, item.state)) - if not expectClock is None: - assert item.state == expectClock + testname="OnOff" + expected_switch_Value= "ON" + type='ItemCommandEvent' - itemClock.addEventListener(openhab.events.ItemCommandEventType,onClockChange,onlyIfEventsourceIsOpenhab=True) + payload='{"type":"OnOff","value":"ON"}' + eventData=createEventData(type,itemname,payload) - def onAzimuthChangeAll(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): - if event.source == openhab.events.EventSourceInternal: - log.info("########################### INTERNAL UPDATE of {} to {} (itemsvalue:{}) from internal".format(event.itemname,event.newValueRaw, item.state)) - else: - log.info("########################### EXTERNAL UPDATE of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValueRaw, item.state)) - itemAzimuth.addEventListener(openhab.events.ItemCommandEventType,onAzimuthChangeAll,onlyIfEventsourceIsOpenhab=False) + switchItem.addEventListener(listeningTypes=openhab.events.ItemCommandEventType,listener=onAnyItemCommand,onlyIfEventsourceIsOpenhab=False) + switchItem.addEventListener(listeningTypes=openhab.events.ItemCommandEventType, listener=onLight_switchCommand, onlyIfEventsourceIsOpenhab=False) - #print(itemClock) + myopenhab._parseEvent(eventData) - time.sleep(2) - log.info("###################################### starting test 'internal Event'") + expected_switch_Value = "OFF" + switchItem.off() - expectClock=2 - itemClock.state=2 - time.sleep(0.1) - expectClock = None - testname="OnOff" - log.info("###################################### starting test '{}'".format(testname)) + expected_switch_Value = "ON" + switchItem.on() - def createEventData(type,itemname,payload): - result={} - result["type"]=type - result["topic"]="smarthome/items/{itemname}/state".format(itemname=itemname) - result["payload"]=payload - return result + assert checkcount==3, "not all events got processed successfully" - def onLight_switchCommand(item: openhab.items.Item, event: openhab.events.ItemCommandEvent): - log.info("########################### COMMAND of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValueRaw, item.state)) - def onAnyItemCommand(item: openhab.items.Item, event: openhab.events.ItemStateEvent): - log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname, event.newValueRaw, item.state)) - if not expectedValue is None: - actualValue=event.newValue - assert actualValue==expectedValue, "expected value to be {}, but it was {}".format(expectedValue,actualValue) + log.info("###################################### test 'internal Event' finished successfully") - testname="OnOff" - expectedValue="ON" - type='ItemCommandEvent' - itemname='testroom1_LampOnOff' - payload='{"type":"OnOff","value":"ON"}' - eventData=createEventData(type,itemname,payload) - testroom1_LampOnOff:openhab.items.SwitchItem = myopenhab.get_item(itemname) - testroom1_LampOnOff.off() - time.sleep(0.5) - testroom1_LampOnOff.addEventListener(types=openhab.events.ItemCommandEventType,listener=onAnyItemCommand,onlyIfEventsourceIsOpenhab=False) - testroom1_LampOnOff.addEventListener(types=openhab.events.ItemCommandEventType, listener=onLight_switchCommand, onlyIfEventsourceIsOpenhab=False) - # testroom1_LampOnOff=None - myopenhab._parseEvent(eventData) + finally: + testnumberA.delete() + testnumberB.delete() + switchItem.delete() - #itemDimmer = myopenhab.get_item("testroom1_LampDimmer") @@ -208,18 +205,11 @@ def onAnyItemCommand(item: openhab.items.Item, event: openhab.events.ItemStateEv - #myopenhab.parseEvent(testdata[testname]) - t=0 - while True: + + + + keep_going=True + while keep_going: + #waiting for events from openhab time.sleep(10) - t=t+1 - x=0 - - if x==1: - itemAzimuth=None - elif x==2: - azimuthvalue= 55.1 + t - log.info("-------------------------setting azimuth to {}".format(azimuthvalue)) - itemAzimuth.command(azimuthvalue) - log.info("-------------------------did set azimuth to {}".format(itemAzimuth.state)) - #we receive an update from openhab immediately. \ No newline at end of file + diff --git a/tests/testutil.py b/tests/testutil.py index eeb8dfd..d3502a9 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +"""utils for testing """ + +# +# Alexey Grubauer (c) 2020-present +# +# python-openhab is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# python-openhab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with python-openhab. If not, see . +# +# pylint: disable=bad-indentation import logging from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable From 92864c11f437fce96fa11e8fce4d8ef67ae89271 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Mon, 14 Dec 2020 15:10:29 +0100 Subject: [PATCH 06/25] cleaning code (PEPs) renaming of literals to python style --- openhab/client.py | 212 ++++++------- openhab/events.py | 42 ++- openhab/items.py | 486 +++++++++++++++--------------- openhab/types.py | 36 +-- tests/test_create_delete_items.py | 250 ++++++++------- tests/test_eventsubscription.py | 188 +++++------- tests/testutil.py | 7 +- 7 files changed, 576 insertions(+), 645 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index 0eeec48..317fe1f 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -31,7 +31,7 @@ import json import time from requests.auth import HTTPBasicAuth -from dataclasses import dataclass + import openhab.items import openhab.events @@ -48,8 +48,8 @@ def __init__(self, base_url: str, password: typing.Optional[str] = None, http_auth: typing.Optional[requests.auth.AuthBase] = None, timeout: typing.Optional[float] = None, - autoUpdate: typing.Optional[bool] = False, - maxEchoToOpenHAB_ms: typing.Optional[int]=800) -> None: + auto_update: typing.Optional[bool] = False, + max_echo_to_openhab_ms: typing.Optional[int] = 800) -> None: """Constructor. Args: @@ -59,33 +59,31 @@ def __init__(self, base_url: str, password (str, optional): A optional password, used in conjunction with a optional provided username, in case openHAB requires authentication. http_auth (AuthBase, optional): An alternative to username/password pair, is to - specify a custom http authentication object of type :class:`requests.auth.AuthBase`. + specify a custom http authentication object of data_type :class:`requests.auth.AuthBase`. timeout (float, optional): An optional timeout for REST transactions - autoUpdate (bool, optional): True: receive Openhab Item Events to actively get informed about changes. - maxEchoToOpenHAB_ms (int, optional): interpret Events from openHAB which hold a state-value equal to items current state-value which are coming in within maxEchoToOpenhabMS millisends since our update/command as echos of our own update//command + auto_update (bool, optional): True: receive Openhab Item Events to actively get informed about changes. + max_echo_to_openhab_ms (int, optional): interpret Events from openHAB which hold a state-value equal to items current state-value which are coming in within maxEchoToOpenhabMS milliseconds since our update/command as echos of our own update//command Returns: OpenHAB: openHAB class instance. """ self.base_url = base_url self.events_url = "{}/events?topics=smarthome/items".format(base_url.strip('/')) - self.autoUpdate=autoUpdate + self.autoUpdate = auto_update self.session = requests.Session() self.session.headers['accept'] = 'application/json' - #self.registered_items:typing.Dict[str,openhab.items.Item]= {} self.registered_items = weakref.WeakValueDictionary() - if http_auth is not None: self.session.auth = http_auth elif not (username is None or password is None): self.session.auth = HTTPBasicAuth(username, password) self.timeout = timeout - self.maxEchoToOpenhabMS=maxEchoToOpenHAB_ms + self.maxEchoToOpenhabMS = max_echo_to_openhab_ms self.logger = logging.getLogger(__name__) - self.__keep_event_deamon_running__ = False - self.eventListeners:typing.List[typing.Callable] = [] + self.__keep_event_daemon_running__ = False + self.eventListeners: typing.List[typing.Callable] = [] if self.autoUpdate: self.__installSSEClient__() @@ -97,7 +95,7 @@ def _check_req_return(req: requests.Response) -> None: req (requests.Response): A requests Response object. Returns: - None: Returns None if no error occured; else raises an exception. + None: Returns None if no error occurred; else raises an exception. Raises: ValueError: Raises a ValueError exception in case of a non-successful @@ -107,76 +105,87 @@ def _check_req_return(req: requests.Response) -> None: logging.getLogger().error(req.content) req.raise_for_status() - - - def _parseItem(self, event:openhab.events.ItemEvent)->None: + def _parse_item(self, event: openhab.events.ItemEvent) -> None: """method to parse an ItemEvent from openhab. it interprets the received ItemEvent data. - in case the item was previously registered it will then delegate further parsing of the event to item iteself through a call of the items _processExternalEvent method + in case the item was previously registered it will then delegate further parsing of the event to item itself through a call of the items _processExternalEvent method Args: - event:openhab.events.ItemEvent holding the eventdata + event:openhab.events.ItemEvent holding the event data """ - if event.itemname in self.registered_items: - item=self.registered_items[event.itemname] + if event.item_name in self.registered_items: + item = self.registered_items[event.item_name] if item is None: - self.logger.warning("item '{}' was removed in all scopes. Ignoring the events coming in for it.".format(event.itemname)) + self.logger.warning("item '{}' was removed in all scopes. Ignoring the events coming in for it.".format(event.item_name)) else: - item._processExternalEvent(event) + item.process_external_event(event) else: - self.logger.debug("item '{}' not registered. ignoring the arrived event.".format(event.itemname)) - - - - - - - + self.logger.debug("item '{}' not registered. ignoring the arrived event.".format(event.item_name)) - - def _parseEvent(self, eventData:typing.Dict)->None: + def _parse_event(self, event_data: typing.Dict) -> None: """method to parse a event from openhab. it interprets the received event dictionary and populates an openhab.events.event Object. - for Itemevents it then calls _parseItem for a more detailed interpretation of the received data + for Item events it then calls _parseItem for a more detailed interpretation of the received data then it informs all registered listeners of openhab events Args: - eventData send by openhab in a Dict + event_data send by openhab in a Dict """ - log=logging.getLogger() - eventreason=eventData["type"] - - - - if eventreason in ["ItemCommandEvent","ItemStateEvent","ItemStateChangedEvent"]: - itemname = eventData["topic"].split("/")[-2] - event=None - payloadData = json.loads(eventData["payload"]) - remoteDatatype = payloadData["type"] - newValue = payloadData["value"] - log.debug("####### new Event arrived:") - log.debug("item name:{}".format(itemname)) - log.debug("type:{}".format(eventreason)) - log.debug("payloadData:{}".format(eventData["payload"])) - - if eventreason =="ItemStateEvent": - event = openhab.events.ItemStateEvent(itemname=itemname, source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype,newValueRaw=newValue,unitOfMeasure="",newValue="",asUpdate=False) - elif eventreason =="ItemCommandEvent": - event = openhab.events.ItemCommandEvent(itemname=itemname, source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype, newValueRaw=newValue,unitOfMeasure="", newValue="") - elif eventreason in ["ItemStateChangedEvent"]: - oldremoteDatatype = payloadData["oldType"] - oldValue = payloadData["oldValue"] - event=openhab.events.ItemStateChangedEvent(itemname=itemname,source=openhab.events.EventSourceOpenhab, remoteDatatype=remoteDatatype,newValueRaw=newValue,newValue="",unitOfMeasure="",oldRemoteDatatype=oldremoteDatatype,oldValueRaw=oldValue, oldValue="", oldUnitOfMeasure="",asUpdate=False) - log.debug("received ItemStateChanged for '{itemname}'[{olddatatype}->{datatype}]:{oldState}->{newValue}".format(itemname=itemname, olddatatype=oldremoteDatatype, datatype=remoteDatatype, oldState=oldValue, newValue=newValue)) - - else: - log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=itemname, datatype=remoteDatatype, newValueRaw=newValue)) - - self._parseItem(event) - self._informEventListeners(event) + log = logging.getLogger() + if "data_type" in event_data: + + event_reason = event_data["data_type"] + if event_reason in ["ItemCommandEvent", "ItemStateEvent", "ItemStateChangedEvent"]: + item_name = event_data["topic"].split("/")[-2] + event = None + payload_data = json.loads(event_data["payload"]) + remote_datatype = payload_data["data_type"] + new_value = payload_data["value"] + log.debug("####### new Event arrived:") + log.debug("item name:{}".format(item_name)) + log.debug("Event-type:{}".format(event_reason)) + log.debug("payloadData:{}".format(event_data["payload"])) + + if event_reason == "ItemStateEvent": + event = openhab.events.ItemStateEvent(item_name=item_name, + source=openhab.events.EventSourceOpenhab, + remote_datatype=remote_datatype, + new_value_raw=new_value, + unit_of_measure="", + new_value="", + as_update=False) + elif event_reason == "ItemCommandEvent": + event = openhab.events.ItemCommandEvent(item_name=item_name, + source=openhab.events.EventSourceOpenhab, + remote_datatype=remote_datatype, + new_value_raw=new_value, + unit_of_measure="", + new_value="") + + elif event_reason in ["ItemStateChangedEvent"]: + old_remote_datatype = payload_data["oldType"] + old_value = payload_data["oldValue"] + event = openhab.events.ItemStateChangedEvent(item_name=item_name, + source=openhab.events.EventSourceOpenhab, + remote_datatype=remote_datatype, + new_value_raw=new_value, + new_value="", + unit_of_measure="", + old_remote_datatype=old_remote_datatype, + old_value_raw=old_value, + old_value="", + old_unit_of_measure="", + as_update=False) + log.debug("received ItemStateChanged for '{itemname}'[{olddatatype}->{datatype}]:{oldState}->{newValue}".format(itemname=item_name, olddatatype=old_remote_datatype, datatype=remote_datatype, oldState=old_value, newValue=new_value)) + + else: + log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=item_name, datatype=remote_datatype, newValue=new_value)) + + self._parse_item(event) + self._inform_event_listeners(event) else: - log.info("received unknown Event-type in Openhab Event stream: {}".format(eventData)) + log.debug("received unknown Event-data_type in Openhab Event stream: {}".format(event_data)) - def _informEventListeners(self, event:openhab.events.ItemEvent): + def _inform_event_listeners(self, event: openhab.events.ItemEvent): """internal method to send itemevents to listeners. Args: event:openhab.events.ItemEvent to be sent to listeners @@ -185,16 +194,16 @@ def _informEventListeners(self, event:openhab.events.ItemEvent): try: aListener(event) except Exception as e: - self.logger.error("error executing Eventlistener for event:{}.".format(event.itemname),e) + self.logger.error("error executing Eventlistener for event:{}.".format(event.item_name), e) - def addEventListener(self, listener:typing.Callable[[openhab.events.ItemEvent],None]): + def add_event_listener(self, listener: typing.Callable[[openhab.events.ItemEvent], None]): """method to register a callback function to get informed about all Item-Events received from openhab. Args: - listener:typing.Callable[[openhab.events.ItemEvent] a method with one parameter of type openhab.events.ItemEvent which will be called for every event + listener:typing.Callable[[openhab.events.ItemEvent] a method with one parameter of data_type openhab.events.ItemEvent which will be called for every event """ self.eventListeners.append(listener) - def removeEventListener(self, listener:typing.Optional[typing.Callable[[openhab.events.ItemEvent],None]]=None): + def remove_event_listener(self, listener: typing.Optional[typing.Callable[[openhab.events.ItemEvent], None]] = None): """method to unregister a callback function to stop getting informed about all Item-Events received from openhab. Args: listener:typing.Callable[[openhab.events.ItemEvent] the method to be removed. @@ -204,18 +213,13 @@ def removeEventListener(self, listener:typing.Optional[typing.Callable[[openhab. elif listener in self.eventListeners: self.eventListeners.remove(listener) - - - - - - def _sseDaemonThread(self): + def _sse_daemon_thread(self): """internal method to receive events from openhab. This method blocks and therefore should be started as separate thread. """ - self.logger.info("starting Openhab - Event Deamon") - next_waittime=initial_waittime=0.1 - while self.__keep_event_deamon_running__: + self.logger.info("starting Openhab - Event Daemon") + next_waittime = initial_waittime = 0.1 + while self.__keep_event_daemon_running__: try: self.logger.info("about to connect to Openhab Events-Stream.") @@ -223,42 +227,39 @@ def _sseDaemonThread(self): next_waittime = initial_waittime for event in messages: - eventData = json.loads(event.data) - self._parseEvent(eventData) - if not self.__keep_event_deamon_running__: + event_data = json.loads(event.data) + self._parse_event(event_data) + if not self.__keep_event_daemon_running__: return except Exception as e: - self.logger.warning("Lost connection to Openhab Events-Stream.",e) - time.sleep(next_waittime) # aleep a bit and then retry - next_waittime=min(10,next_waittime+0.5) # increase waittime over time up to 10 seconds - - + self.logger.warning("Lost connection to Openhab Events-Stream.", e) + time.sleep(next_waittime) # sleep a bit and then retry + next_waittime = min(10, next_waittime+0.5) # increase waittime over time up to 10 seconds - def get_registered_items(self)->weakref.WeakValueDictionary: + def get_registered_items(self) -> weakref.WeakValueDictionary: """get a Dict of weak references to registered items. Args: an Item object """ return self.registered_items - def register_item(self, item: openhab.items.Item)->None: + def register_item(self, item: openhab.items.Item) -> None: """method to register an instantiated item. registered items can receive commands an updated from openhab. Usually you don´t need to register as Items register themself. Args: an Item object """ - if not item is None and not item.name is None: - if not item.name in self.registered_items: - #self.registered_items[item.name]=weakref.ref(item) + if item is not None and item.name is not None: + if item.name not in self.registered_items: self.registered_items[item.name] = item - def __installSSEClient__(self)->None: + def __installSSEClient__(self) -> None: """ installs an event Stream to receive all Item events""" - #now start readerThread - self.__keep_event_deamon_running__=True - self.sseDaemon = threading.Thread(target=self._sseDaemonThread, args=(), daemon=True) + # now start readerThread + self.__keep_event_daemon_running__ = True + self.sseDaemon = threading.Thread(target=self._sse_daemon_thread, args=(), daemon=True) self.sseDaemon.start() def req_get(self, uri_path: str) -> typing.Any: @@ -295,7 +296,7 @@ def req_post(self, uri_path: str, data: typing.Optional[dict] = None) -> None: return None - def req_json_put(self, uri_path: str, jsonData: str = None) -> None: + def req_json_put(self, uri_path: str, json_data: str = None) -> None: """Helper method for initiating a HTTP PUT request. Besides doing the actual request, it also checks the return value and returns the resulting decoded @@ -303,17 +304,17 @@ def req_json_put(self, uri_path: str, jsonData: str = None) -> None: Args: uri_path (str): The path to be used in the PUT request. - jsondata (str): the request data as jason + json_data (str): the request data as jason Returns: None: No data is returned. """ - r = self.session.put(self.base_url + uri_path, data=jsonData, headers={'Content-Type': 'application/json', "Accept": "application/json"}, timeout=self.timeout) + r = self.session.put(self.base_url + uri_path, data=json_data, headers={'Content-Type': 'application/json', "Accept": "application/json"}, timeout=self.timeout) self._check_req_return(r) - def req_del(self, uri_path: str)->None: + def req_del(self, uri_path: str) -> None: """Helper method for initiating a HTTP DELETE request. Besides doing the actual request, it also checks the return value and returns the resulting decoded @@ -326,7 +327,7 @@ def req_del(self, uri_path: str)->None: Returns: None: No data is returned. """ - r= self.session.delete(self.base_url + uri_path,headers={"Accept": "application/json"}) + r = self.session.delete(self.base_url + uri_path, headers={"Accept": "application/json"}) self._check_req_return(r) def req_put(self, uri_path: str, data: typing.Optional[dict] = None) -> None: @@ -347,14 +348,13 @@ def req_put(self, uri_path: str, data: typing.Optional[dict] = None) -> None: return None - # fetch all items def fetch_all_items(self) -> typing.Dict[str, openhab.items.Item]: """Returns all items defined in openHAB. Returns: dict: Returns a dict with item names as key and item class instances as value. """ - items = {} # type: dict + items = {} # data_type: dict res = self.req_get('/items/') for i in res: @@ -364,7 +364,7 @@ def fetch_all_items(self) -> typing.Dict[str, openhab.items.Item]: return items def get_item(self, name: str) -> openhab.items.Item: - """Returns an item with its state and type as fetched from openHAB. + """Returns an item with its state and data_type as fetched from openHAB. Args: name (str): The name of the item to fetch from openHAB. @@ -379,7 +379,7 @@ def get_item(self, name: str) -> openhab.items.Item: def json_to_item(self, json_data: dict) -> openhab.items.Item: """This method takes as argument the RAW (JSON decoded) response for an openHAB item. - It checks of what type the item is and returns a class instance of the + It checks of what data_type the item is and returns a class instance of the specific item filled with the item's state. Args: diff --git a/openhab/events.py b/openhab/events.py index bb4579a..d1cdd34 100644 --- a/openhab/events.py +++ b/openhab/events.py @@ -24,50 +24,48 @@ from dataclasses import dataclass -EventType= typing.NewType('EventType', str) -ItemEventType : EventType = EventType("Item") -ItemStateEventType : EventType = EventType("ItemState") -ItemCommandEventType : EventType = EventType("ItemCommand") -ItemStateChangedEventType : EventType = EventType("ItemStateChanged") +EventType = typing.NewType('EventType', str) +ItemEventType: EventType = EventType("Item") +ItemStateEventType: EventType = EventType("ItemState") +ItemCommandEventType: EventType = EventType("ItemCommand") +ItemStateChangedEventType: EventType = EventType("ItemStateChanged") -EventSource= typing.NewType('EventSource', str) -EventSourceInternal : EventSource = EventSource("Internal") -EventSourceOpenhab : EventSource = EventSource("Openhab") +EventSource = typing.NewType('EventSource', str) +EventSourceInternal: EventSource = EventSource("Internal") +EventSourceOpenhab: EventSource = EventSource("Openhab") + @dataclass class ItemEvent(object): """The base class for all ItemEvents""" type = ItemEventType - itemname: str + item_name: str source: EventSource + remote_datatype: str + new_value: typing.Any + new_value_raw: typing.Any + unit_of_measure: str + @dataclass class ItemStateEvent(ItemEvent): """a Event representing a state event on a Item""" type = ItemStateEventType - remoteDatatype: str - newValue: typing.Any - newValueRaw: str - asUpdate:bool - unitOfMeasure: str + as_update: bool @dataclass class ItemCommandEvent(ItemEvent): """a Event representing a command event on a Item""" type = ItemCommandEventType - remoteDatatype: str - newValue: typing.Any - newValueRaw: typing.Any - unitOfMeasure: str @dataclass class ItemStateChangedEvent(ItemStateEvent): """a Event representing a state change event on a Item""" type = ItemStateChangedEventType - oldRemoteDatatype: str - oldValueRaw: str - oldValue: typing.Any - oldUnitOfMeasure: str + old_remote_datatype: str + old_value_raw: str + old_value: typing.Any + old_unit_of_measure: str diff --git a/openhab/items.py b/openhab/items.py index 0527656..c043050 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -37,30 +37,31 @@ __author__ = 'Georges Toth ' __license__ = 'AGPLv3+' + class ItemFactory: """A factory to get an Item from Openhab, create new or delete existing items in openHAB""" - def __init__(self,openhabClient:openhab.client.OpenHAB): + def __init__(self, openhab_client: openhab.client.OpenHAB): """Constructor. Args: - openhab_conn (openhab.OpenHAB): openHAB object. + openhab_client (openhab.OpenHAB): openHAB object. """ - self.openHABClient=openhabClient - - def createOrUpdateItem(self, - name: str, - type: typing.Union[str, typing.Type[Item]], - quantityType: typing.Optional[str] = None, - label: typing.Optional[str] = None, - category: typing.Optional[str] = None, - tags: typing.Optional[typing.List[str]] = None, - groupNames: typing.Optional[typing.List[str]] = None, - grouptype: typing.Optional[str] = None, - functionname: typing.Optional[str] = None, - functionparams: typing.Optional[typing.List[str]] = None - ) -> Item: + self.openHABClient = openhab_client + + def create_or_update_item(self, + name: str, + data_type: typing.Union[str, typing.Type[Item]], + quantity_type: typing.Optional[str] = None, + label: typing.Optional[str] = None, + category: typing.Optional[str] = None, + tags: typing.Optional[typing.List[str]] = None, + group_names: typing.Optional[typing.List[str]] = None, + group_type: typing.Optional[str] = None, + function_name: typing.Optional[str] = None, + function_params: typing.Optional[typing.List[str]] = None + ) -> Item: """creates a new item in openhab if there is no item with name 'name' yet. if there is an item with 'name' already in openhab, the item gets updated with the infos provided. be aware that not provided fields will be deleted in openhab. consider to get the existing item via 'getItem' and then read out existing fields to populate the parameters here. @@ -70,41 +71,38 @@ def createOrUpdateItem(self, Args: name (str): unique name of the item - type ( str or any Itemclass): the type used in openhab (like Group, Number, Contact, DateTime, Rollershutter, Color, Dimmer, Switch, Player) + data_type ( str or any Itemclass): the type used in openhab (like Group, Number, Contact, DateTime, Rollershutter, Color, Dimmer, Switch, Player) server. To create groups use the itemtype 'Group'! - quantityType (str): optional quantityType ( like Angle, Temperature, Illuminance (see https://www.openhab.org/docs/concepts/units-of-measurement.html)) + quantity_type (str): optional quantity_type ( like Angle, Temperature, Illuminance (see https://www.openhab.org/docs/concepts/units-of-measurement.html)) label (str): optional openhab label (see https://www.openhab.org/docs/configuration/items.html#label) category (str): optional category. no documentation found tags (List of str): optional list of tags (see https://www.openhab.org/docs/configuration/items.html#tags) - groupNames (List of str): optional list of groups this item belongs to. - grouptype (str): optional grouptype. no documentation found - functionname (str): optional functionname. no documentation found - functionparams (List of str): optional list of function Params. no documentation found + group_names (List of str): optional list of groups this item belongs to. + group_type (str): optional group_type. no documentation found + function_name (str): optional function_name. no documentation found + function_params (List of str): optional list of function Params. no documentation found Returns: the created Item """ - self.createOrUpdateItemAsync(name=name, - type=type, - quantityType=quantityType, - label=label, - category=category, - tags=tags, - groupNames=groupNames, - grouptype=grouptype, - functionname=functionname, - functionparams=functionparams) - - + self.create_or_update_item_async(name=name, + type=data_type, + quantity_type=quantity_type, + label=label, + category=category, + tags=tags, + group_names=group_names, + group_type=group_type, + function_name=function_name, + function_params=function_params) time.sleep(0.05) - result = None retrycounter = 10 while True: try: - result = self.getItem(name) + result = self.get_item(name) return result except Exception as e: retrycounter -= 1 @@ -113,20 +111,18 @@ def createOrUpdateItem(self, else: time.sleep(0.05) - - - def createOrUpdateItemAsync(self, - name:str, - type:typing.Union[str, typing.Type[Item]], - quantityType:typing.Optional[str]=None, - label:typing.Optional[str]=None, - category:typing.Optional[str]=None, - tags: typing.Optional[typing.List[str]]=None, - groupNames: typing.Optional[typing.List[str]]=None, - grouptype:typing.Optional[str]=None, - functionname:typing.Optional[str]=None, - functionparams: typing.Optional[typing.List[str]]=None - )->None: + def create_or_update_item_async(self, + name: str, + type: typing.Union[str, typing.Type[Item]], + quantity_type: typing.Optional[str] = None, + label: typing.Optional[str] = None, + category: typing.Optional[str] = None, + tags: typing.Optional[typing.List[str]] = None, + group_names: typing.Optional[typing.List[str]] = None, + group_type: typing.Optional[str] = None, + function_name: typing.Optional[str] = None, + function_params: typing.Optional[typing.List[str]] = None + ) -> None: """creates a new item in openhab if there is no item with name 'name' yet. if there is an item with 'name' already in openhab, the item gets updated with the infos provided. be aware that not provided fields will be deleted in openhab. consider to get the existing item via 'getItem' and then read out existing fields to populate the parameters here. @@ -136,69 +132,66 @@ def createOrUpdateItemAsync(self, Args: name (str): unique name of the item - type ( str or any Itemclass): the type used in openhab (like Group, Number, Contact, DateTime, Rollershutter, Color, Dimmer, Switch, Player) + type ( str or any Itemclass): the data_type used in openhab (like Group, Number, Contact, DateTime, Rollershutter, Color, Dimmer, Switch, Player) server. To create groups use the itemtype 'Group'! - quantityType (str): optional quantityType ( like Angle, Temperature, Illuminance (see https://www.openhab.org/docs/concepts/units-of-measurement.html)) + quantity_type (str): optional quantity_type ( like Angle, Temperature, Illuminance (see https://www.openhab.org/docs/concepts/units-of-measurement.html)) label (str): optional openhab label (see https://www.openhab.org/docs/configuration/items.html#label) category (str): optional category. no documentation found tags (List of str): optional list of tags (see https://www.openhab.org/docs/configuration/items.html#tags) - groupNames (List of str): optional list of groups this item belongs to. - grouptype (str): optional grouptype. no documentation found - functionname (str): optional functionname. no documentation found - functionparams (List of str): optional list of function Params. no documentation found + group_names (List of str): optional list of groups this item belongs to. + group_type (str): optional group_type. no documentation found + function_name (str): optional function_name. no documentation found + function_params (List of str): optional list of function Params. no documentation found """ paramdict: typing.Dict[str, typing.Union[str, typing.List[str], typing.Dict[str, typing.Union[str, typing.List]]]] = {} - - if isinstance(type, str): - itemtypename=type - elif inspect.isclass(type): + itemtypename = type + if inspect.isclass(type): if issubclass(type, Item): - itemtypename=type.TYPENAME - if quantityType is None: - paramdict["type"]=itemtypename + itemtypename = type.TYPENAME + + if quantity_type is None: + paramdict["type"] = itemtypename else: - paramdict["type"] = "{}:{}".format(itemtypename, quantityType) + paramdict["type"] = "{}:{}".format(itemtypename, quantity_type) - paramdict["name"]=name + paramdict["name"] = name - if not label is None: - paramdict["label"]=label + if label is not None: + paramdict["label"] = label - if not category is None: + if category is not None: paramdict["category"] = category - if not tags is None: + if tags is not None: paramdict["tags"] = tags - if not groupNames is None: - paramdict["groupNames"] = groupNames - - if not grouptype is None: - paramdict["groupType"] = grouptype - - if not functionname is None and not functionparams is None: - paramdict["function"] = {"name":functionname,"params":functionparams} + if group_names is not None: + paramdict["groupNames"] = group_names + if group_type is not None: + paramdict["groupType"] = group_type - jsonBody=json.dumps(paramdict) - logging.getLogger().debug("about to create item with PUT request:{}".format(jsonBody)) - self.openHABClient.req_json_put('/items/{}'.format(name), jsonData=jsonBody) + if function_name is not None and function_params is not None: + paramdict["function"] = {"name": function_name, "params": function_params} + json_body = json.dumps(paramdict) + logging.getLogger().debug("about to create item with PUT request:{}".format(json_body)) + self.openHABClient.req_json_put('/items/{}'.format(name), json_data=json_body) - def getItem(self,itemname): + def get_item(self, itemname): return self.openHABClient.get_item(itemname) class Item: """Base item class.""" - types = [] # type: typing.List[typing.Type[openhab.types.CommandType]] + types = [] # data_type: typing.List[typing.Type[openhab.types.CommandType]] TYPENAME = "unknown" - def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto_update:typing.Optional[bool]=True) -> None: + def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto_update: typing.Optional[bool] = True) -> None: """Constructor. Args: @@ -210,13 +203,18 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto self.autoUpdate = auto_update self.type_ = None self.quantityType = None + self.editable = None + self.label = "" + self.category = "" + self.tags = "" + self.groupNames = "" self._unitOfMeasure = "" self.group = False self.name = '' - self._state = None # type: typing.Optional[typing.Any] - self._raw_state = None # type: typing.Optional[typing.Any] # raw state as returned by the server - self._raw_state_event = None # type: typing.str # raw state as received from Serverevent - self._members = {} # type: typing.Dict[str, typing.Any] # group members (key = item name), for none-group items it's empty + self._state = None # data_type: typing.Optional[typing.Any] + self._raw_state = None # data_type: typing.Optional[typing.Any] # raw state as returned by the server + self._raw_state_event = None # data_type: str # raw state as received from Serverevent + self._members = {} # data_type: typing.Dict[str, typing.Any] # group members (key = item name), for none-group items it's empty self.logger = logging.getLogger(__name__) @@ -225,13 +223,7 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto self.lastUpdateSent = datetime.fromtimestamp(0) self.openhab.register_item(self) - self.eventListeners: typing.Dict[typing.Callable[[openhab.events.ItemEvent],None],Item.EventListener]={} - #typing.List[typing.Callable] = [] - - - - - + self.event_listeners: typing.Dict[typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], Item.EventListener] = {} def init_from_json(self, json_data: dict): """Initialize this object from a json configuration as fetched from openHAB. @@ -252,30 +244,30 @@ def init_from_json(self, json_data: dict): else: self.type_ = json_data['type'] - parts=self.type_.split(":") - if len(parts)==2: - self.quantityType=parts[1] + parts = self.type_.split(":") + if len(parts) == 2: + self.quantityType = parts[1] if "editable" in json_data: - self.editable=json_data['editable'] + self.editable = json_data['editable'] if "label" in json_data: - self.label=json_data['label'] + self.label = json_data['label'] if "category" in json_data: - self.category=json_data['category'] + self.category = json_data['category'] if "tags" in json_data: - self.tags=json_data['tags'] + self.tags = json_data['tags'] if "groupNames" in json_data: - self.groupNames=json_data['groupNames'] + self.groupNames = json_data['groupNames'] self.__set_state(json_data['state']) @property - def state(self,fetchFromOpenhab=False) -> typing.Any: + def state(self, fetch_from_openhab=False) -> typing.Any: """The state property represents the current state of the item. The state is automatically refreshed from openHAB on reading it. Updating the value via this property send an update to the event bus. """ - if not self.autoUpdate or fetchFromOpenhab: + if not self.autoUpdate or fetch_from_openhab: json_data = self.openhab.get_item_raw(self.name) self.init_from_json(json_data) @@ -285,10 +277,9 @@ def state(self,fetchFromOpenhab=False) -> typing.Any: def state(self, value: typing.Any): self.update(value) - @property def members(self): - """If item is a type of Group, it will return all member items for this group. + """If item is a data_type of Group, it will return all member items for this group. For none group item empty dictionary will be returned. @@ -319,13 +310,13 @@ def _validate_value(self, value: typing.Union[str, typing.Type[openhab.types.Com else: raise ValueError() - def _parse_rest(self, value: str) -> str: + def _parse_rest(self, value: str) -> typing.Tuple[str, str]: """Parse a REST result into a native object.""" - return (value,"") + return value, "" def _rest_format(self, value: str) -> typing.Union[str, bytes]: """Format a value before submitting to openHAB.""" - _value = value # type: typing.Union[str, bytes] + _value = value # data_type: typing.Union[str, bytes] # Only latin-1 encoding is supported by default. If non-latin-1 characters were provided, convert them to bytes. try: @@ -335,13 +326,13 @@ def _rest_format(self, value: str) -> typing.Union[str, bytes]: return _value - def _isMyOwnChange(self, event): + def _is_my_own_change(self, event): """find out if the incoming event is actually just a echo of my previous command or change""" now = datetime.now() - self.logger.debug("_isMyOwnChange:event.source:{}, event.type{}, self._state:{}, event.newValue:{},self.lastCommandSent:{}, self.lastUpdateSent:{} , now:{}".format(event.source,event.type,self._state,event.newValue ,self.lastCommandSent,self.lastUpdateSent,now)) + self.logger.debug("_isMyOwnChange:event.source:{}, event.data_type{}, self._state:{}, event.new_value:{},self.lastCommandSent:{}, self.lastUpdateSent:{} , now:{}".format(event.source, event.type, self._state, event.new_value, self.lastCommandSent, self.lastUpdateSent, now)) if event.source == openhab.events.EventSourceOpenhab: if event.type in [openhab.events.ItemCommandEventType, openhab.events.ItemStateChangedEventType, openhab.events.ItemStateEventType]: - if self._state == event.newValue: + if self._state == event.new_value: if max(self.lastCommandSent, self.lastUpdateSent) + timedelta(milliseconds=self.openhab.maxEchoToOpenhabMS) > now: # this is the echo of the command we just sent to openHAB. return True @@ -352,150 +343,144 @@ def _isMyOwnChange(self, event): def delete(self): """deletes the item from openhab """ self.openhab.req_del('/items/{}'.format(self.name)) - self._state=None - self.removeAllEventListener() - + self._state = None + self.remove_all_event_listeners() - def _processExternalEvent(self, event:openhab.events.ItemEvent): - if not self.autoUpdate: return + def process_external_event(self, event: openhab.events.ItemEvent): + if not self.autoUpdate: + return self.logger.info("processing external event") - newValue,uom=self._parse_rest(event.newValueRaw) - event.newValue=newValue - event.unitOfMeasure=uom - if event.type==openhab.events.ItemStateChangedEventType: + new_value, uom = self._parse_rest(event.new_value_raw) + event.new_value = new_value + event.unit_of_measure = uom + if event.type == openhab.events.ItemStateChangedEventType: try: - oldValue,ouom=self._parse_rest(event.oldValueRaw) + event: openhab.events.ItemStateChangedEvent + old_value, ouom = self._parse_rest(event.old_value_raw) + event.old_value = old_value + event.old_unit_of_measure = ouom except: - event.oldValue=None - event.oldUnitOfMeasure=None - isMyOwnChange=self._isMyOwnChange(event) + event.old_value = None + event.old_unit_of_measure = None + is_my_own_change = self._is_my_own_change(event) self.logger.info("external event:{}".format(event)) - if not isMyOwnChange: - self.__set_state(value=event.newValueRaw) - event.newValue=self._state - for aListener in self.eventListeners.values(): + if not is_my_own_change: + self.__set_state(value=event.new_value_raw) + event.new_value = self._state + for aListener in self.event_listeners.values(): if event.type in aListener.listeningTypes: - if not isMyOwnChange or (isMyOwnChange and aListener.alsoGetMyEchosFromOpenHAB): + if not is_my_own_change or (is_my_own_change and aListener.alsoGetMyEchosFromOpenHAB): try: - aListener.callbackfunction(self,event) + aListener.callbackfunction(self, event) except Exception as e: - self.logger.error("error executing Eventlistener for item:{}.".format(event.itemname),e) + self.logger.error("error executing Eventlistener for item:{}.".format(event.item_name), e) - def _processInternalEvent(self,event:openhab.events.ItemEvent): + def _process_internal_event(self, event: openhab.events.ItemEvent): self.logger.info("processing internal event") - for aListener in self.eventListeners.values(): + for aListener in self.event_listeners.values(): if event.type in aListener.listeningTypes: if aListener.onlyIfEventsourceIsOpenhab: continue else: try: - aListener.callbackfunction(self,event) + aListener.callbackfunction(self, event) except Exception as e: - self.logger.error("error executing Eventlistener for item:{}.".format(event.itemname),e) - + self.logger.error("error executing Eventlistener for item:{}.".format(event.item_name), e) class EventListener(object): """EventListener Objects hold data about a registered event listener""" - def __init__(self,listeningTypes:typing.Set[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab,alsoGetMyEchosFromOpenHAB): + def __init__(self, listening_types: typing.Set[openhab.events.EventType], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], only_if_eventsource_is_openhab: bool, also_get_my_echos_from_openhab: bool): """Constructor of an EventListener Object Args: - listeningTypes (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is interested in. - onlyIfEventsourceIsOpenhab (bool): the listener only wants events that are coming from openhab. - alsoGetMyEchosFromOpenHAB (bool): the listener also wants to receive events coming from openhab that originally were triggered by commands or changes by our item itself. + listening_types (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is interested in. + only_if_eventsource_is_openhab (bool): the listener only wants events that are coming from openhab. + also_get_my_echos_from_openhab (bool): the listener also wants to receive events coming from openhab that originally were triggered by commands or changes by our item itself. """ - allTypes = {openhab.events.ItemStateEvent.type, openhab.events.ItemCommandEvent.type, openhab.events.ItemStateChangedEvent.type} - if listeningTypes is None: - self.listeningTypes = allTypes - elif not hasattr(listeningTypes, '__iter__'): - self.listeningTypes = set([listeningTypes]) - elif not listeningTypes: - self.listeningTypes = allTypes + all_types = {openhab.events.ItemStateEvent.type, openhab.events.ItemCommandEvent.type, openhab.events.ItemStateChangedEvent.type} + if listening_types is None: + self.listeningTypes = all_types + elif not hasattr(listening_types, '__iter__'): + self.listeningTypes = {listening_types} + elif not listening_types: + self.listeningTypes = all_types else: - self.listeningTypes = listeningTypes + self.listeningTypes = listening_types - self.callbackfunction:typing.Callable[[openhab.events.ItemEvent],None]=listener - self.onlyIfEventsourceIsOpenhab = onlyIfEventsourceIsOpenhab - self.alsoGetMyEchosFromOpenHAB=alsoGetMyEchosFromOpenHAB + self.callbackfunction: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None] = listener + self.onlyIfEventsourceIsOpenhab = only_if_eventsource_is_openhab + self.alsoGetMyEchosFromOpenHAB = also_get_my_echos_from_openhab - def addTypes(self,listeningTypes:typing.Set[openhab.events.EventType]): + def add_types(self, listening_types: typing.Set[openhab.events.EventType]): """add aditional listening types Args: - listeningTypes (openhab.events.EventType or set of openhab.events.EventType): the additional eventTypes the listener is interested in. + listening_types (openhab.events.EventType or set of openhab.events.EventType): the additional eventTypes the listener is interested in. """ - if listeningTypes is None: return - elif not hasattr(listeningTypes, '__iter__'): - self.listeningTypes.add(listeningTypes) - elif not listeningTypes: + if listening_types is None: + return + elif not hasattr(listening_types, '__iter__'): + self.listeningTypes.add(listening_types) + elif not listening_types: return else: - self.listeningTypes.update(listeningTypes) + self.listeningTypes.update(listening_types) - def removeTypes(self,listeningTypes:typing.Set[openhab.events.EventType]): + def remove_types(self, listening_types: typing.Set[openhab.events.EventType]): """remove listening types Args: - listeningTypes (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is not interested in anymore + listening_types (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is not interested in anymore """ - if listeningTypes is None: + if listening_types is None: self.listeningTypes.clear() - elif not hasattr(listeningTypes, '__iter__'): - self.listeningTypes.remove(listeningTypes) - elif not listeningTypes: + elif not hasattr(listening_types, '__iter__'): + self.listeningTypes.remove(listening_types) + elif not listening_types: self.listeningTypes.clear() else: - self.listeningTypes.difference_update(listeningTypes) + self.listeningTypes.difference_update(listening_types) - - - - - - def addEventListener(self,listeningTypes:typing.Set[openhab.events.EventType],listener:typing.Callable[[openhab.items.Item,openhab.events.ItemEvent],None],onlyIfEventsourceIsOpenhab=True,alsoGetMyEchosFromOpenHAB=False): + def add_event_listener(self, listening_types: typing.Set[openhab.events.EventType], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], only_if_eventsource_is_openhab=True, also_get_my_echos_from_openhab=False): """add a Listener interested in changes of items happening in openhab Args: Args: - listeningTypes (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is interested in. + listening_types (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is interested in. listener (Callable[[openhab.items.Item,openhab.events.ItemEvent],None]: a method with 2 parameters: item (openhab.items.Item): the item that received a command, change or update event (openhab.events.ItemEvent): the item Event holding the actual change - onlyIfEventsourceIsOpenhab (bool): the listener only wants events that are coming from openhab. - alsoGetMyEchosFromOpenHAB (bool): the listener also wants to receive events coming from openhab that originally were triggered by commands or changes by our item itself. + only_if_eventsource_is_openhab (bool): the listener only wants events that are coming from openhab. + also_get_my_echos_from_openhab (bool): the listener also wants to receive events coming from openhab that originally were triggered by commands or changes by our item itself. """ - if listener in self.eventListeners: - eventListener= self.eventListeners[listener] - eventListener.addTypes(listeningTypes) - eventListener.onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab + if listener in self.event_listeners: + event_listener = self.event_listeners[listener] + event_listener.add_types(listening_types) + event_listener.onlyIfEventsourceIsOpenhab = only_if_eventsource_is_openhab else: - eventListener=Item.EventListener(listeningTypes=listeningTypes,listener=listener,onlyIfEventsourceIsOpenhab=onlyIfEventsourceIsOpenhab,alsoGetMyEchosFromOpenHAB=alsoGetMyEchosFromOpenHAB) - self.eventListeners[listener]=eventListener + event_listener = Item.EventListener(listening_types=listening_types, listener=listener, only_if_eventsource_is_openhab=only_if_eventsource_is_openhab, also_get_my_echos_from_openhab=also_get_my_echos_from_openhab) + self.event_listeners[listener] = event_listener - def removeAllEventListener(self): - self.eventListeners=[] + def remove_all_event_listeners(self): + self.event_listeners = [] - def removeEventListener(self,types:typing.List[openhab.events.EventType],listener:typing.Callable[[openhab.events.ItemEvent],None]): + def remove_event_listener(self, types: typing.List[openhab.events.EventType], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None]): """removes a previously registered Listener interested in changes of items happening in openhab Args: Args: - listeningTypes (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is interested in. + listening_types (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is interested in. listener: the previously registered listener method. """ - if listener in self.eventListeners: - eventListener = self.eventListeners[listener] - eventListener.removeTypes(types) - if not eventListener.listeningTypes: - self.eventListeners.pop(listener) - - - - + if listener in self.event_listeners: + event_listener = self.event_listeners[listener] + event_listener.remove_types(types) + if not event_listener.listeningTypes: + self.event_listeners.pop(listener) def __set_state(self, value: str) -> None: """Private method for setting the internal state.""" @@ -513,52 +498,57 @@ def _update(self, value: typing.Any) -> None: """Updates the state of an item, input validation is expected to be already done. Args: - value (object): The value to update the item with. The type of the value depends - on the item type and is checked accordingly. + value (object): The value to update the item with. The data_type of the value depends + on the item data_type and is checked accordingly. """ # noinspection PyTypeChecker self.lastCommandSent = datetime.now() self.openhab.req_put('/items/{}/state'.format(self.name), data=value) - def update(self, value: typing.Any) -> None: """Updates the state of an item. Args: - value (object): The value to update the item with. The type of the value depends - on the item type and is checked accordingly. + value (object): The value to update the item with. The data_type of the value depends + on the item data_type and is checked accordingly. """ oldstate = self._state self._validate_value(value) v = self._rest_format(value) - self._state=value + self._state = value self._update(v) if oldstate == self._state: - event = openhab.events.ItemStateEvent(itemname=self.name,source=openhab.events.EventSourceInternal, remoteDatatype=self.type_, newValue=self._state, newValueRaw=None, unitOfMeasure=self._unitOfMeasure, asUpdate=True) + event = openhab.events.ItemStateEvent(item_name=self.name, + source=openhab.events.EventSourceInternal, + remote_datatype=self.type_, + new_value=self._state, + new_value_raw=None, + unit_of_measure=self._unitOfMeasure, + as_update=True) else: - event = openhab.events.ItemStateChangedEvent(itemname=self.name, + event = openhab.events.ItemStateChangedEvent(item_name=self.name, source=openhab.events.EventSourceInternal, - remoteDatatype=self.type_, - newValue=self._state, - newValueRaw=None, - unitOfMeasure=self._unitOfMeasure, - oldRemoteDatatype=self.type_, - oldValue=oldstate, - oldValueRaw="", - oldUnitOfMeasure="", - asUpdate=True, + remote_datatype=self.type_, + new_value=self._state, + new_value_raw=None, + unit_of_measure=self._unitOfMeasure, + old_remote_datatype=self.type_, + old_value=oldstate, + old_value_raw="", + old_unit_of_measure="", + as_update=True, ) - self._processInternalEvent(event) + self._process_internal_event(event) # noinspection PyTypeChecker def command(self, value: typing.Any) -> None: """Sends the given value as command to the event bus. Args: - value (object): The value to send as command to the event bus. The type of the - value depends on the item type and is checked accordingly. + value (object): The value to send as command to the event bus. The data_type of the + value depends on the item data_type and is checked accordingly. """ self._validate_value(value) @@ -568,12 +558,11 @@ def command(self, value: typing.Any) -> None: self.lastCommandSent = datetime.now() self.openhab.req_post('/items/{}'.format(self.name), data=v) - uoM="" - if hasattr(self,"_unitOfMeasure"): - uoM=self._unitOfMeasure - event = openhab.events.ItemCommandEvent(itemname=self.name, source=openhab.events.EventSourceInternal,remoteDatatype=self.type_, newValue=value, newValueRaw=None, unitOfMeasure=uoM) - self._processInternalEvent(event) - + unit_of_measure = "" + if hasattr(self, "_unitOfMeasure"): + unit_of_measure = self._unitOfMeasure + event = openhab.events.ItemCommandEvent(item_name=self.name, source=openhab.events.EventSourceInternal, remote_datatype=self.type_, new_value=value, new_value_raw=None, unit_of_measure=unit_of_measure) + self._process_internal_event(event) def update_state_null(self) -> None: """Update the state of the item to *NULL*.""" @@ -610,9 +599,8 @@ def is_state_undef(self) -> bool: return False - class DateTimeItem(Item): - """DateTime item type.""" + """DateTime item data_type.""" types = [openhab.types.DateTimeType] TYPENAME = "DateTime" @@ -629,7 +617,7 @@ def __eq__(self, other): def __ne__(self, other): return not self.__eq__(other) - def _parse_rest(self, value): + def _parse_rest(self, value) -> typing.Tuple[datetime, str]: """Parse a REST result into a native object. Args: @@ -639,7 +627,7 @@ def _parse_rest(self, value): datetime.datetime: The datetime.datetime object as converted from the string parameter. """ - return (dateutil.parser.parse(value),"") + return dateutil.parser.parse(value), "" def _rest_format(self, value): """Format a value before submitting to openHAB. @@ -656,7 +644,7 @@ def _rest_format(self, value): class PlayerItem(Item): - """PlayerItem item type.""" + """PlayerItem item data_type.""" TYPENAME = "Player" types = [openhab.types.PlayerType] @@ -679,9 +667,7 @@ def previous(self) -> None: class SwitchItem(Item): - """SwitchItem item type.""" - - + """SwitchItem item data_type.""" types = [openhab.types.OnOffType] TYPENAME = "Switch" @@ -702,12 +688,12 @@ def toggle(self) -> None: class NumberItem(Item): - """NumberItem item type.""" + """NumberItem item data_type.""" types = [openhab.types.DecimalType] TYPENAME = "Number" - def _parse_rest(self, value: str) -> float: + def _parse_rest(self, value: str) -> typing.Tuple[float, str]: """Parse a REST result into a native object. Args: @@ -717,21 +703,21 @@ def _parse_rest(self, value: str) -> float: float: The float object as converted from the string parameter. str: The unit Of Measure or empty string """ - - #m = re.match(r'''^(-?[0-9.]+)''', value) + if value in ('UNDEF', 'NULL'): + return value + # m = re.match(r'''^(-?[0-9.]+)''', value) try: - m= re.match("(-?[0-9.]+)\s?(.*)?$", value) + m = re.match("(-?[0-9.]+)\s?(.*)?$", value) if m: - value=m.group(1) - unitOfMeasure = m.group(2) + value = m.group(1) + unit_of_measure = m.group(2) - #logging.getLogger().debug("original value:{}, myvalue:{}, my UoM:{}".format(m,value,unitOfMeasure)) - return (float(value),unitOfMeasure) + return float(value), unit_of_measure else: - return value + return float(value), "" except Exception as e: - self.logger.error("error in parsing new value '{}' for '{}'".format(value,self.name),e) + self.logger.error("error in parsing new value '{}' for '{}'".format(value, self.name), e) raise ValueError('{}: unable to parse value "{}"'.format(self.__class__, value)) @@ -748,7 +734,7 @@ def _rest_format(self, value: float) -> str: class ContactItem(Item): - """Contact item type.""" + """Contact item data_type.""" types = [openhab.types.OpenCloseType] TYPENAME = "Contact" @@ -756,7 +742,7 @@ class ContactItem(Item): def command(self, *args, **kwargs) -> None: """This overrides the `Item` command method. - Note: Commands are not accepted for items of type contact. + Note: Commands are not accepted for items of data_type contact. """ raise ValueError('This item ({}) only supports updates, not commands!'.format(self.__class__)) @@ -770,12 +756,12 @@ def closed(self) -> None: class DimmerItem(Item): - """DimmerItem item type.""" + """DimmerItem item data_type.""" types = [openhab.types.OnOffType, openhab.types.PercentType, openhab.types.IncreaseDecreaseType] TYPENAME = "Dimmer" - def _parse_rest(self, value: str) -> int: + def _parse_rest(self, value: str) -> typing.Tuple[int, str]: """Parse a REST result into a native object. Args: @@ -784,7 +770,7 @@ def _parse_rest(self, value: str) -> int: Returns: int: The int object as converted from the string parameter. """ - return (int(float(value)),"") + return int(float(value)), "" def _rest_format(self, value: typing.Union[str, int]) -> str: """Format a value before submitting to OpenHAB. @@ -818,13 +804,13 @@ def decrease(self) -> None: class ColorItem(Item): - """ColorItem item type.""" + """ColorItem item data_type.""" types = [openhab.types.OnOffType, openhab.types.PercentType, openhab.types.IncreaseDecreaseType, openhab.types.ColorType] TYPENAME = "Color" - def _parse_rest(self, value: str) -> str: + def _parse_rest(self, value: str) -> typing.Tuple[str, str]: """Parse a REST result into a native object. Args: @@ -833,7 +819,7 @@ def _parse_rest(self, value: str) -> str: Returns: str: The str object as converted from the string parameter. """ - return (str(value),"") + return str(value), "" def _rest_format(self, value: typing.Union[str, int]) -> str: """Format a value before submitting to openHAB. @@ -867,12 +853,12 @@ def decrease(self) -> None: class RollershutterItem(Item): - """RollershutterItem item type.""" + """RollershutterItem item data_type.""" types = [openhab.types.UpDownType, openhab.types.PercentType, openhab.types.StopType] TYPENAME = "Rollershutter" - def _parse_rest(self, value: str) -> int: + def _parse_rest(self, value: str) -> typing.Tuple[int, str]: """Parse a REST result into a native object. Args: @@ -881,7 +867,7 @@ def _parse_rest(self, value: str) -> int: Returns: int: The int object as converted from the string parameter. """ - return (int(float(value)),"") + return int(float(value)), "" def _rest_format(self, value: typing.Union[str, int]) -> str: """Format a value before submitting to openHAB. diff --git a/openhab/types.py b/openhab/types.py index 05d44c1..cd32192 100644 --- a/openhab/types.py +++ b/openhab/types.py @@ -30,7 +30,7 @@ class CommandType(metaclass=abc.ABCMeta): - """Base command type class.""" + """Base command data_type class.""" @classmethod @abc.abstractmethod @@ -39,8 +39,8 @@ def validate(cls, value: typing.Any): directly, we throw a NotImplementedError exception. Args: - value (Object): The value to validate. The type of the value depends on the item - type and is checked accordingly. + value (Object): The value to validate. The data_type of the value depends on the item + data_type and is checked accordingly. Raises: NotImplementedError: Raises NotImplementedError as the base class should never @@ -50,13 +50,13 @@ def validate(cls, value: typing.Any): class StringType(CommandType): - """StringType type class.""" + """StringType data_type class.""" @classmethod def validate(cls, value: str) -> None: """Value validation method. - Valid values are andy of type string. + Valid values are andy of data_type string. Args: value (str): The value to validate. @@ -69,7 +69,7 @@ def validate(cls, value: str) -> None: class OnOffType(StringType): - """OnOffType type class.""" + """OnOffType data_type class.""" @classmethod def validate(cls, value: str) -> None: @@ -90,7 +90,7 @@ def validate(cls, value: str) -> None: class OpenCloseType(StringType): - """OpenCloseType type class.""" + """OpenCloseType data_type class.""" @classmethod def validate(cls, value: str) -> None: @@ -111,7 +111,7 @@ def validate(cls, value: str) -> None: class ColorType(StringType): - """ColorType type class.""" + """ColorType data_type class.""" @classmethod def validate(cls, value: str) -> None: @@ -136,13 +136,13 @@ def validate(cls, value: str) -> None: class DecimalType(CommandType): - """DecimalType type class.""" + """DecimalType data_type class.""" @classmethod def validate(cls, value: typing.Union[float, int]) -> None: """Value validation method. - Valid values are any of type ``float`` or ``int``. + Valid values are any of data_type ``float`` or ``int``. Args: value (float): The value to validate. @@ -155,13 +155,13 @@ def validate(cls, value: typing.Union[float, int]) -> None: class PercentType(DecimalType): - """PercentType type class.""" + """PercentType data_type class.""" @classmethod def validate(cls, value: typing.Union[float, int]) -> None: """Value validation method. - Valid values are any of type ``float`` or ``int`` and must be greater of equal to 0 + Valid values are any of data_type ``float`` or ``int`` and must be greater of equal to 0 and smaller or equal to 100. Args: @@ -177,7 +177,7 @@ def validate(cls, value: typing.Union[float, int]) -> None: class IncreaseDecreaseType(StringType): - """IncreaseDecreaseType type class.""" + """IncreaseDecreaseType data_type class.""" @classmethod def validate(cls, value: str) -> None: @@ -198,13 +198,13 @@ def validate(cls, value: str) -> None: class DateTimeType(CommandType): - """DateTimeType type class.""" + """DateTimeType data_type class.""" @classmethod def validate(cls, value: datetime.datetime) -> None: """Value validation method. - Valid values are any of type ``datetime.datetime``. + Valid values are any of data_type ``datetime.datetime``. Args: value (datetime.datetime): The value to validate. @@ -217,7 +217,7 @@ def validate(cls, value: datetime.datetime) -> None: class UpDownType(StringType): - """UpDownType type class.""" + """UpDownType data_type class.""" @classmethod def validate(cls, value: str) -> None: @@ -238,7 +238,7 @@ def validate(cls, value: str) -> None: class StopType(StringType): - """UpDownType type class.""" + """UpDownType data_type class.""" @classmethod def validate(cls, value: str) -> None: @@ -259,7 +259,7 @@ def validate(cls, value: str) -> None: class PlayerType(StringType): - """PlayerType type class.""" + """PlayerType data_type class.""" @classmethod def validate(cls, value: str) -> None: diff --git a/tests/test_create_delete_items.py b/tests/test_create_delete_items.py index cf8199c..96cd067 100644 --- a/tests/test_create_delete_items.py +++ b/tests/test_create_delete_items.py @@ -31,144 +31,151 @@ import json import random import tests.testutil as testutil -from datetime import datetime,timedelta -log=logging.getLogger() -logging.basicConfig(level=10,format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") +from datetime import datetime + +log = logging.getLogger() +logging.basicConfig(level=10, format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") log.error("xx") log.warning("www") log.info("iii") log.debug("ddddd") - - base_url = 'http://10.10.20.81:8080/rest' - - - -def test_create_and_delete_items(myopenhab:openhab.OpenHAB, nameprefix): +def test_create_and_delete_items(myopenhab: openhab.OpenHAB, nameprefix): log.info("starting tests 'create and delete items'") - myitemFactory = openhab.items.ItemFactory(myopenhab) - + my_item_factory = openhab.items.ItemFactory(myopenhab) + a_group_item = None + a_number_item = None + a_contact_item = None + a_datetime_item = None + a_rollershutter_item = None + a_color_item = None + a_dimmer_item = None + a_switch_item = None + a_player_item = None try: - aGroupItem:openhab.items.Item =testGroup(myitemFactory,nameprefix) - log.info("the new group:{}".format(aGroupItem)) - aNumberItem=testNumberItem(myitemFactory,nameprefix) + a_group_item: openhab.items.Item = testGroup(my_item_factory, nameprefix) + log.info("the new group:{}".format(a_group_item)) + + a_number_item = test_NumberItem(my_item_factory, nameprefix) - aContactItem:openhab.items.ContactItem=testContactItem(myitemFactory,nameprefix) - log.info("the new aContactItem:{}".format(aContactItem)) + a_contact_item: openhab.items.ContactItem = test_ContactItem(my_item_factory, nameprefix) + log.info("the new aContactItem:{}".format(a_contact_item)) - aDatetimeItem:openhab.items.DateTimeItem=testDateTimeItem(myitemFactory,nameprefix) - log.info("the new aDatetimeItem:{}".format(aDatetimeItem)) + a_datetime_item: openhab.items.DateTimeItem = test_DateTimeItem(my_item_factory, nameprefix) + log.info("the new aDatetimeItem:{}".format(a_datetime_item)) - aRollershutterItem:openhab.items.RollershutterItem = testRollershutterItem(myitemFactory, nameprefix) - log.info("the new aRollershutterItem:{}".format(aRollershutterItem)) + a_rollershutter_item: openhab.items.RollershutterItem = test_RollershutterItem(my_item_factory, nameprefix) + log.info("the new aRollershutterItem:{}".format(a_rollershutter_item)) - aColorItem:openhab.items.ColorItem = testColorItem(myitemFactory, nameprefix) - log.info("the new aColorItem:{}".format(aColorItem)) + a_color_item: openhab.items.ColorItem = test_ColorItem(my_item_factory, nameprefix) + log.info("the new aColorItem:{}".format(a_color_item)) - aDimmerItem:openhab.items.DimmerItem = testDimmerItem(myitemFactory, nameprefix) - log.info("the new aDimmerItem:{}".format(aDimmerItem)) + a_dimmer_item: openhab.items.DimmerItem = test_DimmerItem(my_item_factory, nameprefix) + log.info("the new aDimmerItem:{}".format(a_dimmer_item)) - aSwitchItem:openhab.items.SwitchItem = testSwitchItem(myitemFactory, nameprefix) - log.info("the new Switch:{}".format(aSwitchItem)) + a_switch_item: openhab.items.SwitchItem = test_SwitchItem(my_item_factory, nameprefix) + log.info("the new Switch:{}".format(a_switch_item)) - aPlayerItem:openhab.items.PlayerItem = testPlayerItem(myitemFactory, nameprefix) - log.info("the new Player:{}".format(aPlayerItem)) + a_player_item: openhab.items.PlayerItem = test_PlayerItem(my_item_factory, nameprefix) + log.info("the new Player:{}".format(a_player_item)) log.info("creation tests worked") finally: - aGroupItem.delete() - aNumberItem.delete() - aContactItem.delete() - aDatetimeItem.delete() - aRollershutterItem.delete() - - coloritemname=aColorItem.name - aColorItem.delete() - try: - shouldNotWork=myitemFactory.getItem(coloritemname) - testutil.doassert(False,True,"this getItem should raise a exception because the item should have been removed.") - except: - pass - log.info("deletion of coloritem worked") - aDimmerItem.delete() - aSwitchItem.delete() - aPlayerItem.delete() - - - - - - + if a_group_item is not None: + a_group_item.delete() + if a_number_item is not None: + a_number_item.delete() + if a_contact_item is not None: + a_contact_item.delete() + if a_datetime_item is not None: + a_datetime_item.delete() + if a_rollershutter_item is not None: + a_rollershutter_item.delete() + + if a_color_item is not None: + coloritemname = a_color_item.name + a_color_item.delete() + try: + should_not_work = my_item_factory.get_item(coloritemname) + testutil.doassert(False, True, "this getItem should raise a exception because the item should have been removed.") + except: + pass + if a_dimmer_item is not None: + a_dimmer_item.delete() + if a_switch_item is not None: + a_switch_item.delete() + if a_player_item is not None: + a_player_item.delete() log.info("test 'create and delete items' finished successfully") -def delete_all_items_starting_with(myopenhab:openhab.OpenHAB, nameprefix): +def delete_all_items_starting_with(myopenhab: openhab.OpenHAB, nameprefix): log.info("starting to delete all items starting with '{}'".format(nameprefix)) - if nameprefix is None: return + if nameprefix is None: + return if len(nameprefix) < 3: log.warning("don´t think that you really want to do this") return - allItems=myopenhab.fetch_all_items() - count=0 - for aItemname in allItems: + all_items = myopenhab.fetch_all_items() + count = 0 + for aItemname in all_items: if aItemname.startswith(nameprefix): - aItem=allItems[aItemname] - aItem.delete() - count+=1 - log.info("finished to delete all items starting with '{}'. deleted {} items".format(nameprefix,count)) - + a_item = all_items[aItemname] + a_item.delete() + count += 1 + log.info("finished to delete all items starting with '{}'. deleted {} items".format(nameprefix, count)) -def testNumberItem(itemFactory,nameprefix): +def test_NumberItem(item_factory, nameprefix): itemname = "{}CreateItemTest".format(nameprefix) - itemQuantityType = "Angle" # "Length",Temperature,,Pressure,Speed,Intensity,Dimensionless,Angle + item_quantity_type = "Angle" # "Length",Temperature,,Pressure,Speed,Intensity,Dimensionless,Angle itemtype = "Number" - itemtype = openhab.items.NumberItem labeltext = "das ist eine testzahl:" itemlabel = "[{labeltext}%.1f °]".format(labeltext=labeltext) itemcategory = "{}TestCategory".format(nameprefix) itemtags: List[str] = ["{}testtag1".format(nameprefix), "{}testtag2".format(nameprefix)] - itemgroupNames: List[str] = ["{}testgroup1".format(nameprefix), "{}testgroup2".format(nameprefix)] + itemgroup_names: List[str] = ["{}testgroup1".format(nameprefix), "{}testgroup2".format(nameprefix)] grouptype = "{}testgrouptype".format(nameprefix) functionname = "{}testfunctionname".format(nameprefix) functionparams: List[str] = ["{}testfunctionnameParam1".format(nameprefix), "{}testfunctionnameParam2".format(nameprefix), "{}testfunctionnameParam3".format(nameprefix)] - x2=itemFactory.createOrUpdateItem(name=itemname, type=itemtype, quantityType=itemQuantityType, label=itemlabel, category=itemcategory, tags=itemtags, groupNames=itemgroupNames, grouptype=grouptype, functionname=functionname, functionparams=functionparams) - x2.state=123.45 - testutil.doassert(itemname,x2.name,"itemname") - testutil.doassert(itemtype.TYPENAME+":"+itemQuantityType, x2.type_, "type") + x2 = item_factory.create_or_update_item(name=itemname, data_type=itemtype, quantity_type=item_quantity_type, label=itemlabel, category=itemcategory, tags=itemtags, group_names=itemgroup_names, group_type=grouptype, function_name=functionname, function_params=functionparams) + x2.state = 123.45 + testutil.doassert(itemname, x2.name, "item_name") + testutil.doassert(itemtype+":"+item_quantity_type, x2.type_, "data_type") testutil.doassert(123.45, x2.state, "state") - testutil.doassert(itemQuantityType, x2.quantityType, "quantityType") + testutil.doassert(item_quantity_type, x2.quantityType, "quantity_type") testutil.doassert(itemlabel, x2.label, "label") - testutil.doassert(itemcategory,x2.category,"category") + testutil.doassert(itemcategory, x2.category, "category") for aExpectedTag in itemtags: - testutil.doassert(aExpectedTag in x2.tags,True,"tag {}".format(aExpectedTag)) + testutil.doassert(True, aExpectedTag in x2.tags, "tag {}".format(aExpectedTag)) - for aExpectedGroupname in itemgroupNames: - testutil.doassert(aExpectedGroupname in x2.groupNames ,True,"tag {}".format(aExpectedGroupname)) + for aExpectedGroupname in itemgroup_names: + testutil.doassert(True, aExpectedGroupname in x2.groupNames, "tag {}".format(aExpectedGroupname)) return x2 -def testGroup(itemFactory,nameprefix)->openhab.items.Item: +def testGroup(itemFactory, nameprefix) -> openhab.items.Item: itemtype = "Group" itemname = "{}TestGroup".format(nameprefix) - testgroupItem = itemFactory.createOrUpdateItem(name=itemname, type=itemtype) - return testgroupItem + testgroup_item = itemFactory.create_or_update_item(name=itemname, data_type=itemtype) + return testgroup_item + -def testContactItem(itemFactory,nameprefix): +def test_ContactItem(item_factory, nameprefix): itemname = "{}CreateContactItemTest".format(nameprefix) itemtype = openhab.items.ContactItem - x2=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) - x2.state="OPEN" - testutil.doassert(itemname,x2.name,"itemname") + x2 = item_factory.create_or_update_item(name=itemname, data_type=itemtype) + x2.state = "OPEN" + testutil.doassert(itemname, x2.name, "item_name") testutil.doassert("OPEN", x2.state, "itemstate") x2.state = "CLOSED" testutil.doassert("CLOSED", x2.state, "itemstate") @@ -180,90 +187,86 @@ def testContactItem(itemFactory,nameprefix): return x2 -def testDateTimeItem(itemFactory,nameprefix): + +def test_DateTimeItem(item_factory, nameprefix): itemname = "{}CreateDateTimeItemTest".format(nameprefix) itemtype = openhab.items.DateTimeItem - x2=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) + x2 = item_factory.create_or_update_item(name=itemname, data_type=itemtype) log.info("current datetime in item:{}".format(x2.state)) - now=datetime.now() - x2.state=now - testutil.doassert(itemname,x2.name,"itemname") + now = datetime.now() + x2.state = now + testutil.doassert(itemname, x2.name, "item_name") testutil.doassert(now, x2.state, "itemstate") return x2 -def testRollershutterItem(itemFactory,nameprefix): + +def test_RollershutterItem(item_factory, nameprefix): itemname = "{}CreateRollershutterItemTest".format(nameprefix) itemtype = openhab.items.RollershutterItem - x2:openhab.items.RollershutterItem=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) - + x2: openhab.items.RollershutterItem = item_factory.create_or_update_item(name=itemname, data_type=itemtype) x2.up() - testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert(itemname, x2.name, "item_name") testutil.doassert("UP", x2.state, "itemstate") - x2.state=53 + x2.state = 53 testutil.doassert(53, x2.state, "itemstate") return x2 -def testColorItem(itemFactory,nameprefix): + +def test_ColorItem(item_factory, nameprefix): itemname = "{}CreateColorItemTest".format(nameprefix) itemtype = openhab.items.ColorItem - x2:openhab.items.ColorItem=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) - + x2: openhab.items.ColorItem=item_factory.create_or_update_item(name=itemname, data_type=itemtype) x2.on() - testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert(itemname, x2.name, "item_name") testutil.doassert("ON", x2.state, "itemstate") - newValue="51,52,53" - x2.state=newValue + newValue = "51,52,53" + x2.state = newValue log.info("itemsate:{}".format(x2.state)) testutil.doassert(newValue, x2.state, "itemstate") return x2 - -def testDimmerItem(itemFactory,nameprefix): +def test_DimmerItem(item_factory, nameprefix): itemname = "{}CreateDimmerItemTest".format(nameprefix) itemtype = openhab.items.DimmerItem - x2:openhab.items.DimmerItem=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) - + x2: openhab.items.DimmerItem = item_factory.create_or_update_item(name=itemname, data_type=itemtype) x2.on() - testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert(itemname, x2.name, "item_name") testutil.doassert("ON", x2.state, "itemstate") x2.off() testutil.doassert("OFF", x2.state, "itemstate") - - newValue=51 - x2.state=newValue + new_value = 51 + x2.state = new_value log.info("itemsate:{}".format(x2.state)) - testutil.doassert(newValue, x2.state, "itemstate") + testutil.doassert(new_value, x2.state, "itemstate") return x2 - -def testSwitchItem(itemFactory,nameprefix): +def test_SwitchItem(item_factory, nameprefix): itemname = "{}CreateSwitchItemTest".format(nameprefix) itemtype = openhab.items.SwitchItem - x2:openhab.items.SwitchItem=itemFactory.createOrUpdateItem(name=itemname, type=itemtype) - + x2: openhab.items.SwitchItem = item_factory.create_or_update_item(name=itemname, data_type=itemtype) x2.on() - testutil.doassert(itemname,x2.name,"itemname") + testutil.doassert(itemname, x2.name, "item_name") testutil.doassert("ON", x2.state, "itemstate") x2.off() @@ -272,24 +275,22 @@ def testSwitchItem(itemFactory,nameprefix): x2.toggle() testutil.doassert("ON", x2.state, "itemstate") - newValue = "OFF" - x2.state = newValue + new_value = "OFF" + x2.state = new_value log.info("itemsate:{}".format(x2.state)) - testutil.doassert(newValue, x2.state, "itemstate") + testutil.doassert(new_value, x2.state, "itemstate") return x2 - - -def testPlayerItem(itemFactory,nameprefix): +def test_PlayerItem(item_factory, nameprefix): itemname = "{}CreatePlayerItemTest".format(nameprefix) itemtype = openhab.items.PlayerItem - x2: openhab.items.PlayerItem = itemFactory.createOrUpdateItem(name=itemname, type=itemtype) + x2: openhab.items.PlayerItem = item_factory.create_or_update_item(name=itemname, data_type=itemtype) x2.play() - testutil.doassert(itemname, x2.name, "itemname") + testutil.doassert(itemname, x2.name, "item_name") testutil.doassert("PLAY", x2.state, "itemstate") x2.pause() @@ -297,16 +298,11 @@ def testPlayerItem(itemFactory,nameprefix): return x2 - - - - - -myopenhab = openhab.OpenHAB(base_url,autoUpdate=False) -keeprunning=True +myopenhab = openhab.OpenHAB(base_url, auto_update=False) +keeprunning = True random.seed() nameprefix = "x2_{}".format(random.randint(1, 1000)) test_create_and_delete_items(myopenhab, nameprefix) -#delete_all_items_starting_with(myopenhab,"x2_") +# delete_all_items_starting_with(myopenhab,"x2_") diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index 08cbf5a..5b43264 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -20,163 +20,128 @@ # pylint: disable=bad-indentation from __future__ import annotations from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable - - import openhab import openhab.events import time import openhab.items as items import logging -import json import random -import tests.testutil as testutil -log=logging.getLogger() -logging.basicConfig(level=10,format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") + +log = logging.getLogger() +logging.basicConfig(level=10, format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") log.error("errormessage") log.warning("waringingmessage") log.info("infomessage") log.debug("debugmessage") - - base_url = 'http://10.10.20.81:8080/rest' - - -testdata:Dict[str,Tuple[str,str,str]]={'OnOff' : ('ItemCommandEvent','testroom1_LampOnOff','{"type":"OnOff","value":"ON"}'), - 'Decimal' : ('ItemCommandEvent','xx','{"type":"Decimal","value":"170.0"}'), - 'DateTime' : ('ItemCommandEvent','xx','{"type":"DateTime","value":"2020-12-04T15:53:33.968+0100"}'), - 'UnDef' : ('ItemCommandEvent','xx','{"type":"UnDef","value":"UNDEF"}'), - 'String' : ('ItemCommandEvent','xx','{"type":"String","value":"WANING_GIBBOUS"}'), - 'Quantitykm' : ('ItemCommandEvent','xx','{"type":"Quantity","value":"389073.99674024084 km"}'), - 'Quantitykm grad' : ('ItemCommandEvent','xx', '{"type":"Quantity","value":"233.32567712620255 °"}'), - 'Quantitywm2' : ('ItemCommandEvent','xx', '{"type":"Quantity","value":"0.0 W/m²"}'), - 'Percent' : ('ItemCommandEvent','xx', '{"type":"Percent","value":"52"}'), - 'UpDown' : ('ItemCommandEvent','xx', '{"type":"UpDown","value":"DOWN"}'), - - - 'OnOffChange' : ('ItemStateChangedEvent','xx', '{"type":"OnOff","value":"OFF","oldType":"OnOff","oldValueRaw":"ON"}'), - 'DecimalChange' : ('ItemStateChangedEvent','xx', '{"type":"Decimal","value":"170.0","oldType":"Decimal","oldValueRaw":"186.0"}'), - 'QuantityChange' : ('ItemStateChangedEvent','xx', '{"type":"Quantity","value":"389073.99674024084 km","oldType":"Quantity","oldValueRaw":"389076.56223012594 km"}'), - 'QuantityGradChange' : ('ItemStateChangedEvent','xx', '{"type":"Quantity","value":"233.32567712620255 °","oldType":"Quantity","oldValueRaw":"233.1365666436372 °"}'), - 'DecimalChangeFromNull' : ('ItemStateChangedEvent','xx', '{"type":"Decimal","value":"0.5","oldType":"UnDef","oldValueRaw":"NULL"}'), - 'DecimalChangeFromNullToUNDEF' : ('ItemStateChangedEvent','xx', '{"type":"Decimal","value":"15","oldType":"UnDef","oldValueRaw":"NULL"}'), - 'PercentChange' : ('ItemStateChangedEvent','xx', '{"type":"Percent","value":"52","oldType":"UnDef","oldValueRaw":"NULL"}'), - - - # 'XXX': ('ItemStateChangedEvent', 'XXXXXXXXXX'), - 'Datatypechange' : ('ItemStateChangedEvent','xx', '{"type":"OnOff","value":"ON","oldType":"UnDef","oldValueRaw":"NULL"}') - } -testitems:Dict[str,openhab.items.Item] = {} - -def executeParseCheck(): - log.info("executing parse checks") - for testkey in testdata: - log.info("testing:{}".format(testkey)) - stringToParse=testdata[testkey] - log.info("parse checks finished successfully") - - - - - - - - - - - - +testdata: Dict[str, Tuple[str, str, str]] = {'OnOff': ('ItemCommandEvent', 'testroom1_LampOnOff', '{"data_type":"OnOff","value":"ON"}'), + 'Decimal': ('ItemCommandEvent', 'xx', '{"data_type":"Decimal","value":"170.0"}'), + 'DateTime': ('ItemCommandEvent', 'xx', '{"data_type":"DateTime","value":"2020-12-04T15:53:33.968+0100"}'), + 'UnDef': ('ItemCommandEvent', 'xx', '{"data_type":"UnDef","value":"UNDEF"}'), + 'String': ('ItemCommandEvent', 'xx', '{"data_type":"String","value":"WANING_GIBBOUS"}'), + 'Quantitykm': ('ItemCommandEvent', 'xx', '{"data_type":"Quantity","value":"389073.99674024084 km"}'), + 'Quantitykm grad': ('ItemCommandEvent', 'xx', '{"data_type":"Quantity","value":"233.32567712620255 °"}'), + 'Quantitywm2': ('ItemCommandEvent', 'xx', '{"data_type":"Quantity","value":"0.0 W/m²"}'), + 'Percent': ('ItemCommandEvent', 'xx', '{"data_type":"Percent","value":"52"}'), + 'UpDown': ('ItemCommandEvent', 'xx', '{"data_type":"UpDown","value":"DOWN"}'), + 'OnOffChange': ('ItemStateChangedEvent', 'xx', '{"data_type":"OnOff","value":"OFF","oldType":"OnOff","old_value_raw":"ON"}'), + 'DecimalChange': ('ItemStateChangedEvent', 'xx', '{"data_type":"Decimal","value":"170.0","oldType":"Decimal","old_value_raw":"186.0"}'), + 'QuantityChange': ('ItemStateChangedEvent', 'xx', '{"data_type":"Quantity","value":"389073.99674024084 km","oldType":"Quantity","old_value_raw":"389076.56223012594 km"}'), + 'QuantityGradChange': ('ItemStateChangedEvent', 'xx', '{"data_type":"Quantity","value":"233.32567712620255 °","oldType":"Quantity","old_value_raw":"233.1365666436372 °"}'), + 'DecimalChangeFromNull': ('ItemStateChangedEvent', 'xx', '{"data_type":"Decimal","value":"0.5","oldType":"UnDef","old_value_raw":"NULL"}'), + 'DecimalChangeFromNullToUNDEF': ('ItemStateChangedEvent', 'xx', '{"data_type":"Decimal","value":"15","oldType":"UnDef","old_value_raw":"NULL"}'), + 'PercentChange': ('ItemStateChangedEvent', 'xx', '{"data_type":"Percent","value":"52","oldType":"UnDef","old_value_raw":"NULL"}'), + 'Datatypechange': ('ItemStateChangedEvent', 'xx', '{"data_type":"OnOff","value":"ON","oldType":"UnDef","old_value_raw":"NULL"}') + } +testitems: Dict[str, openhab.items.Item] = {} if True: - myopenhab = openhab.OpenHAB(base_url,autoUpdate=True) + myopenhab = openhab.OpenHAB(base_url, auto_update=True) myItemfactory = openhab.items.ItemFactory(myopenhab) random.seed() namesuffix = "_{}".format(random.randint(1, 1000)) - testnumberA:openhab.items.NumberItem = myItemfactory.createOrUpdateItem(name="test_eventSubscription_numberitem_A{}".format(namesuffix),type=openhab.items.NumberItem) - testnumberB:openhab.items.NumberItem = myItemfactory.createOrUpdateItem(name="test_eventSubscription_numberitem_B{}".format(namesuffix),type=openhab.items.NumberItem) + testnumberA: openhab.items.NumberItem = myItemfactory.create_or_update_item(name="test_eventSubscription_numberitem_A{}".format(namesuffix), data_type=openhab.items.NumberItem) + testnumberB: openhab.items.NumberItem = myItemfactory.create_or_update_item(name="test_eventSubscription_numberitem_B{}".format(namesuffix), data_type=openhab.items.NumberItem) itemname = "test_eventSubscription_switchitem_A{}".format(namesuffix) - switchItem:openhab.items.SwitchItem = myItemfactory.createOrUpdateItem(name=itemname, type=openhab.items.SwitchItem) + switchItem: openhab.items.SwitchItem = myItemfactory.create_or_update_item(name=itemname, data_type=openhab.items.SwitchItem) try: testnumberA.state = 44.0 testnumberB.state = 66.0 - - expect_A = None expect_B = None - def on_A_Change(item:openhab.items.NumberItem ,event:openhab.events.ItemStateEvent): + def on_a_change(item: openhab.items.NumberItem, event: openhab.events.ItemStateEvent): log.info("########################### UPDATE of {itemname} to eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state}) from OPENHAB ONLY".format( - itemname=event.itemname,eventvalue=event.newValue,eventvalueraw=event.newValueRaw, item_state=item._state,itemstate=item.state)) + itemname=event.item_name, eventvalue=event.new_value, eventvalueraw=event.new_value_raw, item_state=item._state, itemstate=item.state)) - testnumberA.addEventListener(openhab.events.ItemCommandEventType,on_A_Change,onlyIfEventsourceIsOpenhab=True) - def ontestnumberBChange(item:openhab.items.NumberItem ,event:openhab.events.ItemStateEvent): - log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname, event.newValueRaw, item.state)) - if not expect_B is None: + testnumberA.add_event_listener(openhab.events.ItemCommandEventType, on_a_change, only_if_eventsource_is_openhab=True) + + + def on_testnumber_b_change(item: openhab.items.NumberItem, event: openhab.events.ItemStateEvent): + log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.item_name, event.new_value_raw, item.state)) + if expect_B is not None: assert item.state == expect_B - testnumberB.addEventListener(openhab.events.ItemCommandEventType,ontestnumberBChange,onlyIfEventsourceIsOpenhab=True) + testnumberB.add_event_listener(openhab.events.ItemCommandEventType, on_testnumber_b_change, only_if_eventsource_is_openhab=True) - def ontestnumberAChangeAll(item:openhab.items.Item ,event:openhab.events.ItemStateEvent): + + def on_testnumber_a_change_all(item: openhab.items.Item, event: openhab.events.ItemStateEvent): if event.source == openhab.events.EventSourceInternal: - log.info("########################### INTERNAL UPDATE of {} to {} (itemsvalue:{}) from internal".format(event.itemname,event.newValueRaw, item.state)) + log.info("########################### INTERNAL UPDATE of {} to {} (itemsvalue:{}) from internal".format(event.item_name, event.new_value_raw, item.state)) else: - log.info("########################### EXTERNAL UPDATE of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValueRaw, item.state)) + log.info("########################### EXTERNAL UPDATE of {} to {} (itemsvalue:{}) from OPENHAB".format(event.item_name, event.new_value_raw, item.state)) - testnumberA.addEventListener(openhab.events.ItemCommandEventType,ontestnumberAChangeAll,onlyIfEventsourceIsOpenhab=False) - #print(itemClock) + testnumberA.add_event_listener(openhab.events.ItemCommandEventType, on_testnumber_a_change_all, only_if_eventsource_is_openhab=False) time.sleep(2) log.info("###################################### starting test 'internal Event'") - expect_B=2 - testnumberB.state=2 + expect_B = 2 + testnumberB.state = 2 time.sleep(0.1) expect_B = None checkcount = 0 - - def createEventData(type,itemname,payload): - result={} - result["type"]=type - result["topic"]="smarthome/items/{itemname}/state".format(itemname=itemname) - result["payload"]=payload + def create_event_data(type, itemname, payload): + result = {} + result["data_type"] = type + result["topic"] = "smarthome/items/{itemname}/state".format(itemname=itemname) + result["payload"] = payload return result - def onLight_switchCommand(item: openhab.items.Item, event: openhab.events.ItemCommandEvent): - log.info("########################### COMMAND of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValueRaw, item.state)) - - def onAnyItemCommand(item: openhab.items.Item, event: openhab.events.ItemStateEvent): - log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.itemname, event.newValueRaw, item.state)) - if not expected_switch_Value is None: - global checkcount - actualValue=event.newValue - assert actualValue == expected_switch_Value, "expected value to be {}, but it was {}".format(expected_switch_Value, actualValue) - checkcount+=1 - + def on_light_switch_command(item: openhab.items.Item, event: openhab.events.ItemCommandEvent): + log.info("########################### COMMAND of {} to {} (itemsvalue:{}) from OPENHAB".format(event.item_name, event.new_value_raw, item.state)) - testname="OnOff" - expected_switch_Value= "ON" - type='ItemCommandEvent' - - payload='{"type":"OnOff","value":"ON"}' - eventData=createEventData(type,itemname,payload) + def on_any_item_command(item: openhab.items.Item, event: openhab.events.ItemStateEvent): + log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.item_name, event.new_value_raw, item.state)) + if expected_switch_Value is not None: + global checkcount + actual_value = event.new_value + assert actual_value == expected_switch_Value, "expected value to be {}, but it was {}".format(expected_switch_Value, actual_value) + checkcount += 1 + testname = "OnOff" + expected_switch_Value = "ON" + type = 'ItemCommandEvent' - switchItem.addEventListener(listeningTypes=openhab.events.ItemCommandEventType,listener=onAnyItemCommand,onlyIfEventsourceIsOpenhab=False) - switchItem.addEventListener(listeningTypes=openhab.events.ItemCommandEventType, listener=onLight_switchCommand, onlyIfEventsourceIsOpenhab=False) + payload = '{"data_type":"OnOff","value":"ON"}' + eventData = create_event_data(type, itemname, payload) + switchItem.add_event_listener(listening_types=openhab.events.ItemCommandEventType, listener=on_any_item_command, only_if_eventsource_is_openhab=False) + switchItem.add_event_listener(listening_types=openhab.events.ItemCommandEventType, listener=on_light_switch_command, only_if_eventsource_is_openhab=False) - myopenhab._parseEvent(eventData) + myopenhab._parse_event(eventData) expected_switch_Value = "OFF" switchItem.off() @@ -184,32 +149,17 @@ def onAnyItemCommand(item: openhab.items.Item, event: openhab.events.ItemStateEv expected_switch_Value = "ON" switchItem.on() - assert checkcount==3, "not all events got processed successfully" - - + assert checkcount == 3, "not all events got processed successfully" log.info("###################################### test 'internal Event' finished successfully") - - finally: testnumberA.delete() testnumberB.delete() switchItem.delete() - - - - - - - - - - - - keep_going=True + keep_going = True + log.info("###################################### tests finished successfully") while keep_going: - #waiting for events from openhab + # waiting for events from openhab time.sleep(10) - diff --git a/tests/testutil.py b/tests/testutil.py index d3502a9..3c98404 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -21,7 +21,8 @@ import logging from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable -log=logging.getLogger() +log = logging.getLogger() -def doassert(expect:Any,actual:Any,label:Optional[str]=""): - assert actual==expect, f"expected {label}:'{expect}', but it actually has '{actual}'".format(label=label,actual=actual,expect=expect) \ No newline at end of file + +def doassert(expect: Any, actual: Any, label: Optional[str] = ""): + assert actual == expect, f"expected {label}:'{expect}', but it actually has '{actual}'".format(label=label, actual=actual, expect=expect) From 50f7504b702b83f9f4f1c32d53d41622dbc82865 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Mon, 14 Dec 2020 15:43:58 +0100 Subject: [PATCH 07/25] merged with upstream master cleaning code regarding new codestyles --- openhab/client.py | 11 ++++++++--- openhab/items.py | 14 ++++++++------ tests/test_create_delete_items.py | 30 ++++++++++++++++++++---------- tests/test_eventsubscription.py | 6 +----- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index b16a661..406c4b3 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -62,7 +62,8 @@ def __init__(self, base_url: str, specify a custom http authentication object of data_type :class:`requests.auth.AuthBase`. timeout (float, optional): An optional timeout for REST transactions auto_update (bool, optional): True: receive Openhab Item Events to actively get informed about changes. - max_echo_to_openhab_ms (int, optional): interpret Events from openHAB which hold a state-value equal to items current state-value which are coming in within maxEchoToOpenhabMS milliseconds since our update/command as echos of our own update//command + max_echo_to_openhab_ms (int, optional): interpret Events from openHAB which hold a state-value equal to items current state-value + which are coming in within maxEchoToOpenhabMS milliseconds since our update/command as echos of our own update//command Returns: OpenHAB: openHAB class instance. """ @@ -174,7 +175,12 @@ def _parse_event(self, event_data: typing.Dict) -> None: old_value="", old_unit_of_measure="", as_update=False) - log.debug("received ItemStateChanged for '{itemname}'[{olddatatype}->{datatype}]:{oldState}->{newValue}".format(itemname=item_name, olddatatype=old_remote_datatype, datatype=remote_datatype, oldState=old_value, newValue=new_value)) + log.debug("received ItemStateChanged for '{itemname}'[{olddatatype}->{datatype}]:{oldState}->{newValue}".format( + itemname=item_name, + olddatatype=old_remote_datatype, + datatype=remote_datatype, + oldState=old_value, + newValue=new_value)) else: log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=item_name, datatype=remote_datatype, newValue=new_value)) @@ -293,7 +299,6 @@ def req_post(self, uri_path: str, data: typing.Optional[dict] = None) -> None: r = self.session.post(self.base_url + uri_path, data=data, headers={'Content-Type': 'text/plain'}, timeout=self.timeout) self._check_req_return(r) - def req_json_put(self, uri_path: str, json_data: str = None) -> None: """Helper method for initiating a HTTP PUT request. diff --git a/openhab/items.py b/openhab/items.py index e4de480..34ad930 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -19,9 +19,8 @@ # # pylint: disable=bad-indentation - -import datetime from __future__ import annotations +import datetime import logging import inspect import re @@ -329,7 +328,8 @@ def _rest_format(self, value: str) -> typing.Union[str, bytes]: def _is_my_own_change(self, event): """find out if the incoming event is actually just a echo of my previous command or change""" now = datetime.now() - self.logger.debug("_isMyOwnChange:event.source:{}, event.data_type{}, self._state:{}, event.new_value:{},self.lastCommandSent:{}, self.lastUpdateSent:{} , now:{}".format(event.source, event.type, self._state, event.new_value, self.lastCommandSent, self.lastUpdateSent, now)) + self.logger.debug("_isMyOwnChange:event.source:{}, event.data_type{}, self._state:{}, event.new_value:{},self.lastCommandSent:{}, self.lastUpdateSent:{} , now:{}".format( + event.source, event.type, self._state, event.new_value, self.lastCommandSent, self.lastUpdateSent, now)) if event.source == openhab.events.EventSourceOpenhab: if event.type in [openhab.events.ItemCommandEventType, openhab.events.ItemStateChangedEventType, openhab.events.ItemStateEventType]: if self._state == event.new_value: @@ -442,7 +442,9 @@ def remove_types(self, listening_types: typing.Set[openhab.events.EventType]): else: self.listeningTypes.difference_update(listening_types) - def add_event_listener(self, listening_types: typing.Set[openhab.events.EventType], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], only_if_eventsource_is_openhab=True, also_get_my_echos_from_openhab=False): + def add_event_listener(self, listening_types: typing.Set[openhab.events.EventType], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], + only_if_eventsource_is_openhab=True, + also_get_my_echos_from_openhab=False): """add a Listener interested in changes of items happening in openhab Args: Args: @@ -467,7 +469,7 @@ def add_event_listener(self, listening_types: typing.Set[openhab.events.EventTyp def remove_all_event_listeners(self): self.event_listeners = [] - def remove_event_listener(self, types: typing.List[openhab.events.EventType], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None]): + def remove_event_listener(self, types: typing.Set[openhab.events.EventType], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None]): """removes a previously registered Listener interested in changes of items happening in openhab Args: Args: @@ -721,7 +723,7 @@ def _parse_rest(self, value: str) -> typing.Tuple[float, str]: str: The unit Of Measure or empty string """ if value in ('UNDEF', 'NULL'): - return value + return value, "" # m = re.match(r'''^(-?[0-9.]+)''', value) try: m = re.match("(-?[0-9.]+)\s?(.*)?$", value) diff --git a/tests/test_create_delete_items.py b/tests/test_create_delete_items.py index 96cd067..ff27a37 100644 --- a/tests/test_create_delete_items.py +++ b/tests/test_create_delete_items.py @@ -44,9 +44,9 @@ base_url = 'http://10.10.20.81:8080/rest' -def test_create_and_delete_items(myopenhab: openhab.OpenHAB, nameprefix): +def test_create_and_delete_items(openhab: openhab.OpenHAB, nameprefix): log.info("starting tests 'create and delete items'") - my_item_factory = openhab.items.ItemFactory(myopenhab) + my_item_factory = openhab.items.ItemFactory(openhab) a_group_item = None a_number_item = None a_contact_item = None @@ -145,7 +145,16 @@ def test_NumberItem(item_factory, nameprefix): functionname = "{}testfunctionname".format(nameprefix) functionparams: List[str] = ["{}testfunctionnameParam1".format(nameprefix), "{}testfunctionnameParam2".format(nameprefix), "{}testfunctionnameParam3".format(nameprefix)] - x2 = item_factory.create_or_update_item(name=itemname, data_type=itemtype, quantity_type=item_quantity_type, label=itemlabel, category=itemcategory, tags=itemtags, group_names=itemgroup_names, group_type=grouptype, function_name=functionname, function_params=functionparams) + x2 = item_factory.create_or_update_item(name=itemname, + data_type=itemtype, + quantity_type=item_quantity_type, + label=itemlabel, + category=itemcategory, + tags=itemtags, + group_names=itemgroup_names, + group_type=grouptype, + function_name=functionname, + function_params=functionparams) x2.state = 123.45 testutil.doassert(itemname, x2.name, "item_name") testutil.doassert(itemtype+":"+item_quantity_type, x2.type_, "data_type") @@ -161,6 +170,7 @@ def test_NumberItem(item_factory, nameprefix): return x2 + def testGroup(itemFactory, nameprefix) -> openhab.items.Item: itemtype = "Group" itemname = "{}TestGroup".format(nameprefix) @@ -223,16 +233,16 @@ def test_ColorItem(item_factory, nameprefix): itemname = "{}CreateColorItemTest".format(nameprefix) itemtype = openhab.items.ColorItem - x2: openhab.items.ColorItem=item_factory.create_or_update_item(name=itemname, data_type=itemtype) + x2: openhab.items.ColorItem = item_factory.create_or_update_item(name=itemname, data_type=itemtype) x2.on() testutil.doassert(itemname, x2.name, "item_name") testutil.doassert("ON", x2.state, "itemstate") - newValue = "51,52,53" - x2.state = newValue + new_value = "51,52,53" + x2.state = new_value log.info("itemsate:{}".format(x2.state)) - testutil.doassert(newValue, x2.state, "itemstate") + testutil.doassert(new_value, x2.state, "itemstate") return x2 @@ -301,8 +311,8 @@ def test_PlayerItem(item_factory, nameprefix): myopenhab = openhab.OpenHAB(base_url, auto_update=False) keeprunning = True random.seed() -nameprefix = "x2_{}".format(random.randint(1, 1000)) +mynameprefix = "x2_{}".format(random.randint(1, 1000)) -test_create_and_delete_items(myopenhab, nameprefix) -# delete_all_items_starting_with(myopenhab,"x2_") +test_create_and_delete_items(myopenhab, mynameprefix) +# delete_all_items_starting_with(openhab,"x2_") diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index 5b43264..dd80f02 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -18,7 +18,6 @@ # along with python-openhab. If not, see . # # pylint: disable=bad-indentation -from __future__ import annotations from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable import openhab import openhab.events @@ -112,10 +111,7 @@ def on_testnumber_a_change_all(item: openhab.items.Item, event: openhab.events.I def create_event_data(type, itemname, payload): - result = {} - result["data_type"] = type - result["topic"] = "smarthome/items/{itemname}/state".format(itemname=itemname) - result["payload"] = payload + result = {"data_type": type, "topic": "smarthome/items/{itemname}/state".format(itemname=itemname), "payload": payload} return result From 47c1a9e1a30aeefa70324c382b2336630bc41b41 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Mon, 14 Dec 2020 17:21:57 +0100 Subject: [PATCH 08/25] updated samples in readme --- README.rst | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index aa5c2a1..4eb55e3 100644 --- a/README.rst +++ b/README.rst @@ -107,7 +107,7 @@ Example usage of the library: if event.source == openhab.events.EventSourceOpenhab: log.info("this change came from openhab") # install listener for evetns - testroom1_LampOnOff.addEventListener(listeningTypes=openhab.events.ItemCommandEventType, listener=onLight_switchCommand, onlyIfEventsourceIsOpenhab=False) + testroom1_LampOnOff.add_event_listener(listening_types=openhab.events.ItemCommandEventType, listener=onLight_switchCommand, only_if_eventsource_is_openhab=False) # switch you will receive update also for your changes in the code. (see testroom1_LampOnOff.off() @@ -119,7 +119,7 @@ Example usage of the library: # first instantiate a Factory: itemFactory = openhab.items.ItemFactory(openhab) #create the item - testDimmer = itemFactory.createOrUpdateItem(name="the_testDimmer", type=openhab.items.DimmerItem) + testDimmer = itemFactory.create_or_update_item(name="the_testDimmer", data_type=openhab.items.DimmerItem) #use item testDimmer.state=95 @@ -128,20 +128,28 @@ Example usage of the library: # you can set change many item attributes: nameprefix="testcase_1_" itemname = "{}CreateItemTest".format(nameprefix) - itemQuantityType = "Angle" + item_quantity_type = "Angle" # "Length",Temperature,,Pressure,Speed,Intensity,Dimensionless,Angle itemtype = "Number" - itemtype = openhab.items.NumberItem - labeltext = "this is a test azimuth:" + labeltext = "das ist eine testzahl:" itemlabel = "[{labeltext}%.1f °]".format(labeltext=labeltext) itemcategory = "{}TestCategory".format(nameprefix) itemtags: List[str] = ["{}testtag1".format(nameprefix), "{}testtag2".format(nameprefix)] - itemgroupNames: List[str] = ["{}testgroup1".format(nameprefix), "{}testgroup2".format(nameprefix)] + itemgroup_names: List[str] = ["{}testgroup1".format(nameprefix), "{}testgroup2".format(nameprefix)] grouptype = "{}testgrouptype".format(nameprefix) functionname = "{}testfunctionname".format(nameprefix) functionparams: List[str] = ["{}testfunctionnameParam1".format(nameprefix), "{}testfunctionnameParam2".format(nameprefix), "{}testfunctionnameParam3".format(nameprefix)] - testazimuth=itemFactory.createOrUpdateItem(name=itemname, type=itemtype, quantityType=itemQuantityType, label=itemlabel, category=itemcategory, tags=itemtags, groupNames=itemgroupNames, grouptype=grouptype, functionname=functionname, functionparams=functionparams) + x2 = item_factory.create_or_update_item(name=itemname, + data_type=itemtype, + quantity_type=item_quantity_type, + label=itemlabel, + category=itemcategory, + tags=itemtags, + group_names=itemgroup_names, + group_type=grouptype, + function_name=functionname, + function_params=functionparams) Note on NULL and UNDEF ---------------------- From 10e6973ba38679c34012d8c2e07881df12646549 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Mon, 14 Dec 2020 22:47:58 +0100 Subject: [PATCH 09/25] fixed bug in event-parsing and testcases --- openhab/client.py | 6 ++--- tests/test_eventsubscription.py | 40 ++++++++++++++++----------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index 406c4b3..7706cc9 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -131,14 +131,14 @@ def _parse_event(self, event_data: typing.Dict) -> None: event_data send by openhab in a Dict """ log = logging.getLogger() - if "data_type" in event_data: + if "type" in event_data: + event_reason = event_data["type"] - event_reason = event_data["data_type"] if event_reason in ["ItemCommandEvent", "ItemStateEvent", "ItemStateChangedEvent"]: item_name = event_data["topic"].split("/")[-2] event = None payload_data = json.loads(event_data["payload"]) - remote_datatype = payload_data["data_type"] + remote_datatype = payload_data["type"] new_value = payload_data["value"] log.debug("####### new Event arrived:") log.debug("item name:{}".format(item_name)) diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index dd80f02..e8fc593 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -36,24 +36,24 @@ base_url = 'http://10.10.20.81:8080/rest' -testdata: Dict[str, Tuple[str, str, str]] = {'OnOff': ('ItemCommandEvent', 'testroom1_LampOnOff', '{"data_type":"OnOff","value":"ON"}'), - 'Decimal': ('ItemCommandEvent', 'xx', '{"data_type":"Decimal","value":"170.0"}'), - 'DateTime': ('ItemCommandEvent', 'xx', '{"data_type":"DateTime","value":"2020-12-04T15:53:33.968+0100"}'), - 'UnDef': ('ItemCommandEvent', 'xx', '{"data_type":"UnDef","value":"UNDEF"}'), - 'String': ('ItemCommandEvent', 'xx', '{"data_type":"String","value":"WANING_GIBBOUS"}'), - 'Quantitykm': ('ItemCommandEvent', 'xx', '{"data_type":"Quantity","value":"389073.99674024084 km"}'), - 'Quantitykm grad': ('ItemCommandEvent', 'xx', '{"data_type":"Quantity","value":"233.32567712620255 °"}'), - 'Quantitywm2': ('ItemCommandEvent', 'xx', '{"data_type":"Quantity","value":"0.0 W/m²"}'), - 'Percent': ('ItemCommandEvent', 'xx', '{"data_type":"Percent","value":"52"}'), - 'UpDown': ('ItemCommandEvent', 'xx', '{"data_type":"UpDown","value":"DOWN"}'), - 'OnOffChange': ('ItemStateChangedEvent', 'xx', '{"data_type":"OnOff","value":"OFF","oldType":"OnOff","old_value_raw":"ON"}'), - 'DecimalChange': ('ItemStateChangedEvent', 'xx', '{"data_type":"Decimal","value":"170.0","oldType":"Decimal","old_value_raw":"186.0"}'), - 'QuantityChange': ('ItemStateChangedEvent', 'xx', '{"data_type":"Quantity","value":"389073.99674024084 km","oldType":"Quantity","old_value_raw":"389076.56223012594 km"}'), - 'QuantityGradChange': ('ItemStateChangedEvent', 'xx', '{"data_type":"Quantity","value":"233.32567712620255 °","oldType":"Quantity","old_value_raw":"233.1365666436372 °"}'), - 'DecimalChangeFromNull': ('ItemStateChangedEvent', 'xx', '{"data_type":"Decimal","value":"0.5","oldType":"UnDef","old_value_raw":"NULL"}'), - 'DecimalChangeFromNullToUNDEF': ('ItemStateChangedEvent', 'xx', '{"data_type":"Decimal","value":"15","oldType":"UnDef","old_value_raw":"NULL"}'), - 'PercentChange': ('ItemStateChangedEvent', 'xx', '{"data_type":"Percent","value":"52","oldType":"UnDef","old_value_raw":"NULL"}'), - 'Datatypechange': ('ItemStateChangedEvent', 'xx', '{"data_type":"OnOff","value":"ON","oldType":"UnDef","old_value_raw":"NULL"}') +testdata: Dict[str, Tuple[str, str, str]] = {'OnOff': ('ItemCommandEvent', 'testroom1_LampOnOff', '{"type":"OnOff","value":"ON"}'), + 'Decimal': ('ItemCommandEvent', 'xx', '{"type":"Decimal","value":"170.0"}'), + 'DateTime': ('ItemCommandEvent', 'xx', '{"type":"DateTime","value":"2020-12-04T15:53:33.968+0100"}'), + 'UnDef': ('ItemCommandEvent', 'xx', '{"type":"UnDef","value":"UNDEF"}'), + 'String': ('ItemCommandEvent', 'xx', '{"type":"String","value":"WANING_GIBBOUS"}'), + 'Quantitykm': ('ItemCommandEvent', 'xx', '{"type":"Quantity","value":"389073.99674024084 km"}'), + 'Quantitykm grad': ('ItemCommandEvent', 'xx', '{"type":"Quantity","value":"233.32567712620255 °"}'), + 'Quantitywm2': ('ItemCommandEvent', 'xx', '{"type":"Quantity","value":"0.0 W/m²"}'), + 'Percent': ('ItemCommandEvent', 'xx', '{"type":"Percent","value":"52"}'), + 'UpDown': ('ItemCommandEvent', 'xx', '{"type":"UpDown","value":"DOWN"}'), + 'OnOffChange': ('ItemStateChangedEvent', 'xx', '{"type":"OnOff","value":"OFF","oldType":"OnOff","old_value_raw":"ON"}'), + 'DecimalChange': ('ItemStateChangedEvent', 'xx', '{"type":"Decimal","value":"170.0","oldType":"Decimal","old_value_raw":"186.0"}'), + 'QuantityChange': ('ItemStateChangedEvent', 'xx', '{"type":"Quantity","value":"389073.99674024084 km","oldType":"Quantity","old_value_raw":"389076.56223012594 km"}'), + 'QuantityGradChange': ('ItemStateChangedEvent', 'xx', '{"type":"Quantity","value":"233.32567712620255 °","oldType":"Quantity","old_value_raw":"233.1365666436372 °"}'), + 'DecimalChangeFromNull': ('ItemStateChangedEvent', 'xx', '{"type":"Decimal","value":"0.5","oldType":"UnDef","old_value_raw":"NULL"}'), + 'DecimalChangeFromNullToUNDEF': ('ItemStateChangedEvent', 'xx', '{"type":"Decimal","value":"15","oldType":"UnDef","old_value_raw":"NULL"}'), + 'PercentChange': ('ItemStateChangedEvent', 'xx', '{"type":"Percent","value":"52","oldType":"UnDef","old_value_raw":"NULL"}'), + 'Datatypechange': ('ItemStateChangedEvent', 'xx', '{"type":"OnOff","value":"ON","oldType":"UnDef","old_value_raw":"NULL"}') } testitems: Dict[str, openhab.items.Item] = {} @@ -111,7 +111,7 @@ def on_testnumber_a_change_all(item: openhab.items.Item, event: openhab.events.I def create_event_data(type, itemname, payload): - result = {"data_type": type, "topic": "smarthome/items/{itemname}/state".format(itemname=itemname), "payload": payload} + result = {"type": type, "topic": "smarthome/items/{itemname}/state".format(itemname=itemname), "payload": payload} return result @@ -132,7 +132,7 @@ def on_any_item_command(item: openhab.items.Item, event: openhab.events.ItemStat expected_switch_Value = "ON" type = 'ItemCommandEvent' - payload = '{"data_type":"OnOff","value":"ON"}' + payload = '{"type":"OnOff","value":"ON"}' eventData = create_event_data(type, itemname, payload) switchItem.add_event_listener(listening_types=openhab.events.ItemCommandEventType, listener=on_any_item_command, only_if_eventsource_is_openhab=False) switchItem.add_event_listener(listening_types=openhab.events.ItemCommandEventType, listener=on_light_switch_command, only_if_eventsource_is_openhab=False) From d442ec627c5d0c54f48ac7b18bb7dcd8b9fd1880 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Sun, 20 Dec 2020 17:09:48 +0100 Subject: [PATCH 10/25] major refactor to better reflect incoming states/statechange and commands. changed handling of incoming values wrote quite a lot of testcases for creation/deletion of items wrote quite a lot of testcases for event handling --- openhab/client.py | 140 ++-- openhab/events.py | 33 +- openhab/items.py | 391 ++++++++--- openhab/types.py | 333 ++++++++- tests/test_create_delete_items.py | 56 +- tests/test_eventsubscription.py | 1074 +++++++++++++++++++++++++++-- tests/testutil.py | 2 +- 7 files changed, 1752 insertions(+), 277 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index 7706cc9..17c1565 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -35,6 +35,7 @@ import openhab.items import openhab.events +import openhab.types __author__ = 'Georges Toth ' __license__ = 'AGPLv3+' @@ -113,14 +114,8 @@ def _parse_item(self, event: openhab.events.ItemEvent) -> None: Args: event:openhab.events.ItemEvent holding the event data """ - if event.item_name in self.registered_items: - item = self.registered_items[event.item_name] - if item is None: - self.logger.warning("item '{}' was removed in all scopes. Ignoring the events coming in for it.".format(event.item_name)) - else: - item.process_external_event(event) - else: - self.logger.debug("item '{}' not registered. ignoring the arrived event.".format(event.item_name)) + + def _parse_event(self, event_data: typing.Dict) -> None: """method to parse a event from openhab. @@ -136,61 +131,85 @@ def _parse_event(self, event_data: typing.Dict) -> None: if event_reason in ["ItemCommandEvent", "ItemStateEvent", "ItemStateChangedEvent"]: item_name = event_data["topic"].split("/")[-2] - event = None - payload_data = json.loads(event_data["payload"]) - remote_datatype = payload_data["type"] - new_value = payload_data["value"] - log.debug("####### new Event arrived:") - log.debug("item name:{}".format(item_name)) - log.debug("Event-type:{}".format(event_reason)) - log.debug("payloadData:{}".format(event_data["payload"])) - - if event_reason == "ItemStateEvent": - event = openhab.events.ItemStateEvent(item_name=item_name, - source=openhab.events.EventSourceOpenhab, - remote_datatype=remote_datatype, - new_value_raw=new_value, - unit_of_measure="", - new_value="", - as_update=False) - elif event_reason == "ItemCommandEvent": - event = openhab.events.ItemCommandEvent(item_name=item_name, - source=openhab.events.EventSourceOpenhab, - remote_datatype=remote_datatype, - new_value_raw=new_value, - unit_of_measure="", - new_value="") - - elif event_reason in ["ItemStateChangedEvent"]: - old_remote_datatype = payload_data["oldType"] - old_value = payload_data["oldValue"] - event = openhab.events.ItemStateChangedEvent(item_name=item_name, - source=openhab.events.EventSourceOpenhab, - remote_datatype=remote_datatype, - new_value_raw=new_value, - new_value="", - unit_of_measure="", - old_remote_datatype=old_remote_datatype, - old_value_raw=old_value, - old_value="", - old_unit_of_measure="", - as_update=False) - log.debug("received ItemStateChanged for '{itemname}'[{olddatatype}->{datatype}]:{oldState}->{newValue}".format( - itemname=item_name, - olddatatype=old_remote_datatype, - datatype=remote_datatype, - oldState=old_value, - newValue=new_value)) + event_data = json.loads(event_data["payload"]) + + raw_event=openhab.events.RawItemEvent(item_name=item_name,event_type=event_reason,content=event_data) + self._inform_event_listeners(raw_event) + if item_name in self.registered_items: + item = self.registered_items[item_name] + if item is None: + self.logger.warning("item '{}' was removed in all scopes. Ignoring the events coming in for it.".format(item_name)) + else: + item.process_external_event(raw_event) else: - log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=item_name, datatype=remote_datatype, newValue=new_value)) + self.logger.debug("item '{}' not registered. ignoring the arrived event.".format(item_name)) + + + + # event = None + # payload_data = json.loads(event_data["payload"]) + # remote_datatype = payload_data["type"] + # new_value = payload_data["value"] + # log.debug("####### new Event arrived:") + # log.debug("item name:{}".format(item_name)) + # log.debug("Event-type:{}".format(event_reason)) + # log.debug("payloadData:{}".format(event_data["payload"])) + # + # if event_reason == "ItemStateEvent": + # event = openhab.events.ItemStateEvent(item_name=item_name, + # source=openhab.events.EventSourceOpenhab, + # remote_datatype=remote_datatype, + # new_value_raw=new_value, + # unit_of_measure="", + # new_value="", + # as_update=False, + # is_value=None, + # command=None) + # elif event_reason == "ItemCommandEvent": + # event = openhab.events.ItemCommandEvent(item_name=item_name, + # source=openhab.events.EventSourceOpenhab, + # remote_datatype=remote_datatype, + # new_value_raw=new_value, + # unit_of_measure="", + # new_value="", + # is_value = None, + # command = None + # ) + # + # elif event_reason in ["ItemStateChangedEvent"]: + # old_remote_datatype = payload_data["oldType"] + # old_value = payload_data["oldValue"] + # event = openhab.events.ItemStateChangedEvent(item_name=item_name, + # source=openhab.events.EventSourceOpenhab, + # remote_datatype=remote_datatype, + # new_value_raw=new_value, + # new_value="", + # unit_of_measure="", + # old_remote_datatype=old_remote_datatype, + # old_value_raw=old_value, + # old_value="", + # old_unit_of_measure="", + # as_update=False, + # is_value=None, + # command=None + # ) + # log.debug("received ItemStateChanged for '{itemname}'[{olddatatype}->{datatype}]:{oldState}->{newValue}".format( + # itemname=item_name, + # olddatatype=old_remote_datatype, + # datatype=remote_datatype, + # oldState=old_value, + # newValue=new_value)) + # + # else: + # log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=item_name, datatype=remote_datatype, newValue=new_value)) + + - self._parse_item(event) - self._inform_event_listeners(event) else: log.debug("received unknown Event-data_type in Openhab Event stream: {}".format(event_data)) - def _inform_event_listeners(self, event: openhab.events.ItemEvent): + def _inform_event_listeners(self, event: openhab.events.RawItemEvent): """internal method to send itemevents to listeners. Args: event:openhab.events.ItemEvent to be sent to listeners @@ -201,14 +220,14 @@ def _inform_event_listeners(self, event: openhab.events.ItemEvent): except Exception as e: self.logger.error("error executing Eventlistener for event:{}.".format(event.item_name), e) - def add_event_listener(self, listener: typing.Callable[[openhab.events.ItemEvent], None]): + def add_event_listener(self, listener: typing.Callable[[openhab.events.RawItemEvent], None]): """method to register a callback function to get informed about all Item-Events received from openhab. Args: listener:typing.Callable[[openhab.events.ItemEvent] a method with one parameter of data_type openhab.events.ItemEvent which will be called for every event """ self.eventListeners.append(listener) - def remove_event_listener(self, listener: typing.Optional[typing.Callable[[openhab.events.ItemEvent], None]] = None): + def remove_event_listener(self, listener: typing.Optional[typing.Callable[[openhab.events.RawItemEvent], None]] = None): """method to unregister a callback function to stop getting informed about all Item-Events received from openhab. Args: listener:typing.Callable[[openhab.events.ItemEvent] the method to be removed. @@ -394,6 +413,9 @@ def json_to_item(self, json_data: dict) -> openhab.items.Item: if _type == 'Group' and 'groupType' in json_data: _type = json_data["groupType"] + if _type == 'String': + return openhab.items.StringItem(self, json_data) + if _type == 'Switch': return openhab.items.SwitchItem(self, json_data) diff --git a/openhab/events.py b/openhab/events.py index d1cdd34..cb3a377 100644 --- a/openhab/events.py +++ b/openhab/events.py @@ -22,19 +22,33 @@ import typing from dataclasses import dataclass +import openhab.types EventType = typing.NewType('EventType', str) + +RawItemEventType: EventType = EventType("RawItem") + ItemEventType: EventType = EventType("Item") -ItemStateEventType: EventType = EventType("ItemState") -ItemCommandEventType: EventType = EventType("ItemCommand") -ItemStateChangedEventType: EventType = EventType("ItemStateChanged") +ItemStateEventType: EventType = EventType("ItemStateEvent") +ItemCommandEventType: EventType = EventType("ItemCommandEvent") +ItemStateChangedEventType: EventType = EventType("ItemStateChangedEvent") EventSource = typing.NewType('EventSource', str) EventSourceInternal: EventSource = EventSource("Internal") EventSourceOpenhab: EventSource = EventSource("Openhab") +@dataclass +class RawItemEvent(object): + + item_name: str + source = EventSourceOpenhab + event_type: EventType + content: typing.Dict + + + @dataclass class ItemEvent(object): @@ -42,17 +56,18 @@ class ItemEvent(object): type = ItemEventType item_name: str source: EventSource - remote_datatype: str - new_value: typing.Any - new_value_raw: typing.Any + value_datatype: typing.Type[openhab.types.CommandType] + value: typing.Any + value_raw: typing.Any unit_of_measure: str + is_my_own_echo:bool @dataclass class ItemStateEvent(ItemEvent): """a Event representing a state event on a Item""" type = ItemStateEventType - as_update: bool + @dataclass @@ -61,11 +76,13 @@ class ItemCommandEvent(ItemEvent): type = ItemCommandEventType + + @dataclass class ItemStateChangedEvent(ItemStateEvent): """a Event representing a state change event on a Item""" type = ItemStateChangedEventType - old_remote_datatype: str + old_value_datatype: typing.Type[openhab.types.CommandType] old_value_raw: str old_value: typing.Any old_unit_of_measure: str diff --git a/openhab/items.py b/openhab/items.py index 34ad930..3f8b9a5 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -60,7 +60,7 @@ def create_or_update_item(self, group_type: typing.Optional[str] = None, function_name: typing.Optional[str] = None, function_params: typing.Optional[typing.List[str]] = None - ) -> Item: + ) -> typing.Type[Item]: """creates a new item in openhab if there is no item with name 'name' yet. if there is an item with 'name' already in openhab, the item gets updated with the infos provided. be aware that not provided fields will be deleted in openhab. consider to get the existing item via 'getItem' and then read out existing fields to populate the parameters here. @@ -180,7 +180,7 @@ def create_or_update_item_async(self, logging.getLogger().debug("about to create item with PUT request:{}".format(json_body)) self.openHABClient.req_json_put('/items/{}'.format(name), json_data=json_body) - def get_item(self, itemname): + def get_item(self, itemname) -> typing.Type[Item]: return self.openHABClient.get_item(itemname) @@ -188,6 +188,11 @@ class Item: """Base item class.""" types = [] # data_type: typing.List[typing.Type[openhab.types.CommandType]] + state_types = [] + command_event_types = [] + state_event_types = [] + state_changed_event_types = [] + TYPENAME = "unknown" def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto_update: typing.Optional[bool] = True) -> None: @@ -201,6 +206,7 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto self.openhab = openhab_conn self.autoUpdate = auto_update self.type_ = None + self.quantityType = None self.editable = None self.label = "" @@ -218,8 +224,10 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto self.logger = logging.getLogger(__name__) self.init_from_json(json_data) - self.lastCommandSent = datetime.fromtimestamp(0) - self.lastUpdateSent = datetime.fromtimestamp(0) + self.last_command_sent_time = datetime.fromtimestamp(0) + self.last_command_sent = "" + + self.openhab.register_item(self) self.event_listeners: typing.Dict[typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], Item.EventListener] = {} @@ -237,15 +245,18 @@ def init_from_json(self, json_data: dict) -> None: if 'groupType' in json_data: self.type_ = json_data['groupType'] + # init members for i in json_data['members']: self.members[i['name']] = self.openhab.json_to_item(i) else: self.type_ = json_data['type'] + parts = self.type_.split(":") if len(parts) == 2: self.quantityType = parts[1] + if "editable" in json_data: self.editable = json_data['editable'] if "label" in json_data: @@ -257,7 +268,16 @@ def init_from_json(self, json_data: dict) -> None: if "groupNames" in json_data: self.groupNames = json_data['groupNames'] - self.__set_state(json_data['state']) + #self.__set_state(json_data['state']) + self._raw_state = json_data['state'] + + if self.is_undefined(self._raw_state): + self._state = None + else: + self._state, self._unitOfMeasure = self._parse_rest(self._raw_state) + + + @property def state(self, fetch_from_openhab=False) -> typing.Any: @@ -309,6 +329,9 @@ def _validate_value(self, value: typing.Union[str, typing.Type[openhab.types.Com else: raise ValueError() + + + def _parse_rest(self, value: str) -> typing.Tuple[str, str]: """Parse a REST result into a native object.""" return value, "" @@ -325,20 +348,24 @@ def _rest_format(self, value: str) -> typing.Union[str, bytes]: return _value - def _is_my_own_change(self, event): + def _is_my_own_echo(self, event:openhab.events.ItemEvent): """find out if the incoming event is actually just a echo of my previous command or change""" now = datetime.now() - self.logger.debug("_isMyOwnChange:event.source:{}, event.data_type{}, self._state:{}, event.new_value:{},self.lastCommandSent:{}, self.lastUpdateSent:{} , now:{}".format( - event.source, event.type, self._state, event.new_value, self.lastCommandSent, self.lastUpdateSent, now)) - if event.source == openhab.events.EventSourceOpenhab: - if event.type in [openhab.events.ItemCommandEventType, openhab.events.ItemStateChangedEventType, openhab.events.ItemStateEventType]: - if self._state == event.new_value: - if max(self.lastCommandSent, self.lastUpdateSent) + timedelta(milliseconds=self.openhab.maxEchoToOpenhabMS) > now: - # this is the echo of the command we just sent to openHAB. - return True - return False - else: + self.logger.debug("_isMyOwnChange:event.source:{}, event.data_type{}, self._state:{}, event.new_value:{},self.last_command_sent_time:{}, now:{}".format( + event.source, event.type, self._state, event.value, self.last_command_sent_time, now)) + if event.source != openhab.events.EventSourceOpenhab: return True + if self.last_command_sent_time + timedelta(milliseconds=self.openhab.maxEchoToOpenhabMS) > now: + if event.type == openhab.events.ItemCommandEventType: + if self.last_command_sent == event.value: + # this is the echo of the command we just sent to openHAB. + return True + elif event.type in [openhab.events.ItemStateChangedEventType, openhab.events.ItemStateEventType]: + if self._state == event.value: + # this is the echo of the command we just sent to openHAB. + return True + return False + def delete(self): """deletes the item from openhab """ @@ -346,34 +373,133 @@ def delete(self): self._state = None self.remove_all_event_listeners() - def process_external_event(self, event: openhab.events.ItemEvent): + + def get_value_unitofmeasure(self, parsed_value:str): + if isinstance(parsed_value,tuple): + value_result = parsed_value[0] + uom = parsed_value[1] + return value_result,uom + else: + return parsed_value, "" + + def digest_external_command_event(self, command_type_class:typing.Type[openhab.types.CommandType], command:str)->openhab.events.ItemCommandEvent: + parsed_value = command_type_class.parse(command) + value_result,uom = self.get_value_unitofmeasure(parsed_value) + item_command_event = openhab.events.ItemCommandEvent(item_name=self.name, source=openhab.events.EventSourceOpenhab, value_datatype=command_type_class, value=value_result, unit_of_measure=uom, value_raw=command, is_my_own_echo=False) + item_command_event.is_my_own_echo = self._is_my_own_echo(item_command_event) + if command_type_class in self.state_types: + if not item_command_event.is_my_own_echo: + self.__set_state(value_result) + self._unitOfMeasure = uom + return item_command_event + + + def digest_external_state_event(self, state_type_class:typing.Type[openhab.types.CommandType], value:str)->openhab.events.ItemStateEvent: + parsed_value = state_type_class.parse(value) + value_result,uom = self.get_value_unitofmeasure(parsed_value) + item_state_event = openhab.events.ItemStateEvent(item_name=self.name, source=openhab.events.EventSourceOpenhab, value_datatype=state_type_class, value=value_result, unit_of_measure=uom, value_raw=value, is_my_own_echo=False) + item_state_event.is_my_own_echo = self._is_my_own_echo(item_state_event) + if item_state_event in self.state_types: + if not item_state_event.is_my_own_echo: + self.__set_state(value_result) + self._unitOfMeasure = uom + return item_state_event + + + + + def digest_external_state_changed_event(self, state_type_class:typing.Type[openhab.types.CommandType], value:str, old_state_type_class:typing.Type[openhab.types.CommandType], old_value:str)->openhab.events.ItemStateChangedEvent: + parsed_value = state_type_class.parse(value) + value_result,uom = self.get_value_unitofmeasure(parsed_value) + + parsed_old_value=old_value + if old_state_type_class is not None: + parsed_old_value = old_state_type_class.parse(old_value) + old_value_result, old_uom = self.get_value_unitofmeasure(parsed_old_value) + + + + + item_state_changed_event = openhab.events.ItemStateChangedEvent(item_name=self.name, + source=openhab.events.EventSourceOpenhab, + value_datatype=state_type_class, + value=value_result, + unit_of_measure=uom, + value_raw=value, + old_value_datatype=old_state_type_class, + old_value=old_value_result, + old_unit_of_measure=old_uom, + old_value_raw=old_value, + is_my_own_echo=False) + + item_state_changed_event.is_my_own_echo = self._is_my_own_echo(item_state_changed_event) + if item_state_changed_event in self.state_types: + if not item_state_changed_event.is_my_own_echo: + self._state=value_result + return item_state_changed_event + + + def _parse_external_command_event(self, raw_Event: openhab.events.RawItemEvent) -> openhab.events.ItemCommandEvent: + command_type = raw_Event.content["type"] + command_type_class = openhab.types.CommandType.getTypeFor(command_type) + # if self.typeclass is None: + # self.typeclass = command_type_class + command=raw_Event.content["value"] + if command_type_class in self.command_event_types: + item_command_event=self.digest_external_command_event(command_type_class,command) + return item_command_event + raise Exception("unknown command event type") + + def _parse_external_state_event(self, raw_Event: openhab.events.RawItemEvent) -> openhab.events.ItemStateEvent: + state_type = raw_Event.content["type"] + state_type_class = openhab.types.CommandType.getTypeFor(state_type) + # if self.typeclass is None: + # self.typeclass = state_type_class + + value = raw_Event.content["value"] + if state_type_class in self.state_event_types: + item_state_event = self.digest_external_state_event(state_type_class, value) + return item_state_event + raise Exception("unknown state event type") + + def _parse_external_state_changed_event(self, raw_Event: openhab.events.RawItemEvent) -> openhab.events.ItemStateEvent: + state_changed_type = raw_Event.content["type"] + state_changed_type_class = openhab.types.CommandType.getTypeFor(state_changed_type) + state_changed_old_type = raw_Event.content["oldType"] + state_changed_old_type_class = openhab.types.CommandType.getTypeFor(state_changed_old_type) + # if self.typeclass is None: + # self.typeclass = state_changed_type_class + value = raw_Event.content["value"] + old_value = raw_Event.content["oldValue"] + if state_changed_type_class in self.state_changed_event_types: + item_state_changed_event = self.digest_external_state_changed_event(state_type_class=state_changed_type_class, value=value, old_state_type_class=state_changed_old_type_class, old_value=old_value) + return item_state_changed_event + raise Exception("unknown statechanged event type:{}".format(state_changed_type_class)) + + + + + + def process_external_event(self, raw_Event: openhab.events.RawItemEvent): if not self.autoUpdate: return self.logger.info("processing external event") - new_value, uom = self._parse_rest(event.new_value_raw) - event.new_value = new_value - event.unit_of_measure = uom - if event.type == openhab.events.ItemStateChangedEventType: - try: - event: openhab.events.ItemStateChangedEvent - old_value, ouom = self._parse_rest(event.old_value_raw) - event.old_value = old_value - event.old_unit_of_measure = ouom - except: - event.old_value = None - event.old_unit_of_measure = None - is_my_own_change = self._is_my_own_change(event) - self.logger.info("external event:{}".format(event)) - if not is_my_own_change: - self.__set_state(value=event.new_value_raw) - event.new_value = self._state + + if raw_Event.event_type == openhab.events.ItemCommandEvent.type: + event=self._parse_external_command_event(raw_Event) + elif raw_Event.event_type == openhab.events.ItemStateChangedEvent.type: + event=self._parse_external_state_changed_event(raw_Event) + elif raw_Event.event_type == openhab.events.ItemStateEvent.type: + event=self._parse_external_state_event(raw_Event) + + for aListener in self.event_listeners.values(): if event.type in aListener.listeningTypes: - if not is_my_own_change or (is_my_own_change and aListener.alsoGetMyEchosFromOpenHAB): + if not event.is_my_own_echo or aListener.alsoGetMyEchosFromOpenHAB: try: aListener.callbackfunction(self, event) except Exception as e: - self.logger.error("error executing Eventlistener for item:{}.".format(event.item_name), e) + self.logger.exception("error executing Eventlistener for item:{}.".format(event.item_name)) def _process_internal_event(self, event: openhab.events.ItemEvent): self.logger.info("processing internal event") @@ -385,7 +511,8 @@ def _process_internal_event(self, event: openhab.events.ItemEvent): try: aListener.callbackfunction(self, event) except Exception as e: - self.logger.error("error executing Eventlistener for item:{}.".format(event.item_name), e) + self.logger.exception("error executing Eventlistener for item:{}.".format(event.item_name)) + class EventListener(object): """EventListener Objects hold data about a registered event listener""" @@ -467,7 +594,7 @@ def add_event_listener(self, listening_types: typing.Set[openhab.events.EventTyp self.event_listeners[listener] = event_listener def remove_all_event_listeners(self): - self.event_listeners = [] + self.event_listeners = {} def remove_event_listener(self, types: typing.Set[openhab.events.EventType], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None]): """removes a previously registered Listener interested in changes of items happening in openhab @@ -484,14 +611,20 @@ def remove_event_listener(self, types: typing.Set[openhab.events.EventType], lis if not event_listener.listeningTypes: self.event_listeners.pop(listener) + def is_undefined(self, value:str) -> bool: + for aStateType in self.state_types: + if not aStateType.is_undefined(self._raw_state): + return False + return True + + def __set_state(self, value: str) -> None: """Private method for setting the internal state.""" - self._raw_state = value - - if value in ('UNDEF', 'NULL'): + if self.is_undefined(value): self._state = None else: - self._state, self._unitOfMeasure = self._parse_rest(value) + self._state = value + def __str__(self) -> str: """String representation.""" @@ -505,7 +638,8 @@ def _update(self, value: typing.Any) -> None: on the item data_type and is checked accordingly. """ # noinspection PyTypeChecker - self.lastCommandSent = datetime.now() + self.last_command_sent_time = datetime.now() + self.last_command_sent=value self.openhab.req_put('/items/{}/state'.format(self.name), data=value) def update(self, value: typing.Any) -> None: @@ -525,23 +659,26 @@ def update(self, value: typing.Any) -> None: if oldstate == self._state: event = openhab.events.ItemStateEvent(item_name=self.name, source=openhab.events.EventSourceInternal, - remote_datatype=self.type_, - new_value=self._state, - new_value_raw=None, + value_datatype=self.type_, + value=self._state, + value_raw=None, unit_of_measure=self._unitOfMeasure, - as_update=True) + is_my_own_echo=False + ) else: + + event = openhab.events.ItemStateChangedEvent(item_name=self.name, source=openhab.events.EventSourceInternal, - remote_datatype=self.type_, - new_value=self._state, - new_value_raw=None, + value_datatype=self.type_, + value=self._state, + value_raw=None, unit_of_measure=self._unitOfMeasure, - old_remote_datatype=self.type_, + old_value_datatype=self.type_, old_value=oldstate, old_value_raw="", old_unit_of_measure="", - as_update=True, + is_my_own_echo=False ) self._process_internal_event(event) @@ -556,15 +693,24 @@ def command(self, value: typing.Any) -> None: self._validate_value(value) + + v = self._rest_format(value) self._state = value - self.lastCommandSent = datetime.now() + self.last_command_sent_time = datetime.now() self.openhab.req_post('/items/{}'.format(self.name), data=v) unit_of_measure = "" if hasattr(self, "_unitOfMeasure"): unit_of_measure = self._unitOfMeasure - event = openhab.events.ItemCommandEvent(item_name=self.name, source=openhab.events.EventSourceInternal, remote_datatype=self.type_, new_value=value, new_value_raw=None, unit_of_measure=unit_of_measure) + event = openhab.events.ItemCommandEvent(item_name=self.name, + source=openhab.events.EventSourceInternal, + value_datatype=self.type_, + value=value, + value_raw=None, + unit_of_measure=unit_of_measure, + is_my_own_echo=True + ) self._process_internal_event(event) def update_state_null(self) -> None: @@ -602,10 +748,27 @@ def is_state_undef(self) -> bool: return False +class StringItem(Item): + """DateTime item data_type.""" + + types = [openhab.types.StringType] + state_types = types + command_event_types = types + state_event_types = types + state_changed_event_types = types + + TYPENAME = "String" + + class DateTimeItem(Item): """DateTime item data_type.""" types = [openhab.types.DateTimeType] + state_types = types + command_event_types = types + state_event_types = types + state_changed_event_types = types + TYPENAME = "DateTime" def __gt__(self, other: datetime.datetime) -> bool: @@ -666,52 +829,74 @@ class PlayerItem(Item): """PlayerItem item data_type.""" TYPENAME = "Player" - types = [openhab.types.PlayerType] + types = [openhab.types.PlayPauseType, openhab.types.NextPrevious, openhab.types.RewindFastforward] + state_types = [openhab.types.PlayPauseType, openhab.types.RewindFastforward] + command_event_types = [openhab.types.PlayPauseType, openhab.types.NextPrevious, openhab.types.RewindFastforward] + state_event_types = [openhab.types.PlayPauseType, openhab.types.RewindFastforward] + state_changed_event_types = [openhab.types.PlayPauseType, openhab.types.RewindFastforward] def play(self) -> None: - """Set the state of the player to PLAY.""" - self.command('PLAY') + """send the command PLAY.""" + self.command(openhab.types.PlayPauseType.PLAY) def pause(self) -> None: - """Set the state of the player to PAUSE.""" - self.command('PAUSE') + """send the command PAUSE.""" + self.command(openhab.types.PlayPauseType.PAUSE) def next(self) -> None: - """Set the state of the player to NEXT.""" - self.command('NEXT') + """send the command NEXT.""" + self.command(openhab.types.NextPrevious.NEXT) def previous(self) -> None: - """Set the state of the player to PREVIOUS.""" - self.command('PREVIOUS') + """send the command PREVIOUS.""" + self.command(openhab.types.NextPrevious.PREVIOUS) + def fastforward(self) -> None: + """send the command FASTFORWARD""" + self.command(openhab.types.RewindFastforward.FASTFORWARD) + + def rewind(self) -> None: + """send the command REWIND.""" + self.command(openhab.types.RewindFastforward.REWIND) class SwitchItem(Item): """SwitchItem item data_type.""" types = [openhab.types.OnOffType] + state_types = types + command_event_types = types + state_event_types = types + state_changed_event_types = types TYPENAME = "Switch" def on(self) -> None: """Set the state of the switch to ON.""" - self.command('ON') + self.command(openhab.types.OnOffType.ON) def off(self) -> None: """Set the state of the switch to OFF.""" - self.command('OFF') + self.command(openhab.types.OnOffType.OFF) def toggle(self) -> None: """Toggle the state of the switch to OFF to ON and vice versa.""" - if self.state == 'ON': + if self.state == openhab.types.OnOffType.ON: self.off() - else: + elif self.state == openhab.types.OnOffType.OFF: self.on() + + class NumberItem(Item): """NumberItem item data_type.""" types = [openhab.types.DecimalType] + state_types = types + command_event_types = types + state_event_types = types + state_changed_event_types = types TYPENAME = "Number" + def _parse_rest(self, value: str) -> typing.Tuple[float, str]: """Parse a REST result into a native object. @@ -723,7 +908,7 @@ def _parse_rest(self, value: str) -> typing.Tuple[float, str]: str: The unit Of Measure or empty string """ if value in ('UNDEF', 'NULL'): - return value, "" + return None, "" # m = re.match(r'''^(-?[0-9.]+)''', value) try: m = re.match("(-?[0-9.]+)\s?(.*)?$", value) @@ -756,6 +941,10 @@ class ContactItem(Item): """Contact item data_type.""" types = [openhab.types.OpenCloseType] + state_types = types + command_event_types = types + state_event_types = types + state_changed_event_types = types TYPENAME = "Contact" def command(self, *args, **kwargs) -> None: @@ -767,20 +956,24 @@ def command(self, *args, **kwargs) -> None: def open(self) -> None: """Set the state of the contact item to OPEN.""" - self.state = 'OPEN' + self.state = openhab.types.OpenCloseType.OPEN def closed(self) -> None: """Set the state of the contact item to CLOSED.""" - self.state = 'CLOSED' + self.state = openhab.types.OpenCloseType.CLOSED class DimmerItem(Item): """DimmerItem item data_type.""" types = [openhab.types.OnOffType, openhab.types.PercentType, openhab.types.IncreaseDecreaseType] + state_types = [openhab.types.PercentType] + command_event_types = [openhab.types.OnOffType, openhab.types.PercentType, openhab.types.IncreaseDecreaseType] + state_event_types = [openhab.types.OnOffType, openhab.types.PercentType] + state_changed_event_types = [openhab.types.PercentType] TYPENAME = "Dimmer" - def _parse_rest(self, value: str) -> typing.Tuple[int, str]: + def _parse_rest(self, value: str) -> typing.Tuple[float, str]: """Parse a REST result into a native object. Args: @@ -789,7 +982,7 @@ def _parse_rest(self, value: str) -> typing.Tuple[int, str]: Returns: int: The int object as converted from the string parameter. """ - return int(float(value)), "" + return float(value), "" def _rest_format(self, value: typing.Union[str, int]) -> str: """Format a value before submitting to OpenHAB. @@ -807,28 +1000,32 @@ def _rest_format(self, value: typing.Union[str, int]) -> str: def on(self) -> None: """Set the state of the dimmer to ON.""" - self.command('ON') + self.command(openhab.types.OnOffType.ON) def off(self) -> None: """Set the state of the dimmer to OFF.""" - self.command('OFF') + self.command(openhab.types.OnOffType.OFF) def increase(self) -> None: """Increase the state of the dimmer.""" - self.command('INCREASE') + self.command(openhab.types.IncreaseDecreaseType.INCREASE) def decrease(self) -> None: """Decrease the state of the dimmer.""" - self.command('DECREASE') + self.command(openhab.types.IncreaseDecreaseType.DECREASE) -class ColorItem(Item): +class ColorItem(DimmerItem): """ColorItem item data_type.""" - types = [openhab.types.OnOffType, openhab.types.PercentType, openhab.types.IncreaseDecreaseType, - openhab.types.ColorType] + types = [openhab.types.OnOffType, openhab.types.PercentType, openhab.types.IncreaseDecreaseType, openhab.types.ColorType] + state_types = [openhab.types.ColorType] + command_event_types = [openhab.types.OnOffType, openhab.types.PercentType, openhab.types.IncreaseDecreaseType, openhab.types.ColorType] + state_event_types = [openhab.types.OnOffType, openhab.types.PercentType, openhab.types.ColorType] + state_changed_event_types = [openhab.types.ColorType] TYPENAME = "Color" + def _parse_rest(self, value: str) -> typing.Tuple[str, str]: """Parse a REST result into a native object. @@ -838,7 +1035,8 @@ def _parse_rest(self, value: str) -> typing.Tuple[str, str]: Returns: str: The str object as converted from the string parameter. """ - return str(value), "" + result = openhab.types.ColorType.parse(value) + return result, "" def _rest_format(self, value: typing.Union[str, int]) -> str: """Format a value before submitting to openHAB. @@ -849,32 +1047,29 @@ def _rest_format(self, value: typing.Union[str, int]) -> str: Returns: str: The string as possibly converted from the parameter. """ + if isinstance(value,tuple): + if len(value) == 3: + return "{},{},{}".format(value[0],value[1],value[2]) if not isinstance(value, str): return str(value) return value - def on(self) -> None: - """Set the state of the color to ON.""" - self.command('ON') + def get_value_unitofmeasure(self, parsed_value:str): + return parsed_value, "" - def off(self) -> None: - """Set the state of the color to OFF.""" - self.command('OFF') - def increase(self) -> None: - """Increase the state of the color.""" - self.command('INCREASE') +class RollershutterItem(Item): + """RollershutterItem item data_type.""" - def decrease(self) -> None: - """Decrease the state of the color.""" - self.command('DECREASE') + types = [openhab.types.UpDownType, openhab.types.PercentType, openhab.types.StopMoveType] + state_types = [openhab.types.PercentType] + command_event_types = [openhab.types.UpDownType, openhab.types.StopMoveType, openhab.types.PercentType] + state_event_types = [openhab.types.UpDownType, openhab.types.PercentType] + state_changed_event_types = [openhab.types.PercentType] -class RollershutterItem(Item): - """RollershutterItem item data_type.""" - types = [openhab.types.UpDownType, openhab.types.PercentType, openhab.types.StopType] TYPENAME = "Rollershutter" def _parse_rest(self, value: str) -> typing.Tuple[int, str]: @@ -904,12 +1099,12 @@ def _rest_format(self, value: typing.Union[str, int]) -> str: def up(self) -> None: """Set the state of the dimmer to ON.""" - self.command('UP') + self.command(openhab.types.UpDownType.UP) def down(self) -> None: """Set the state of the dimmer to OFF.""" - self.command('DOWN') + self.command(openhab.types.UpDownType.DOWN) def stop(self) -> None: """Set the state of the dimmer to OFF.""" - self.command('STOP') + self.command(openhab.types.StopMoveType.STOP) diff --git a/openhab/types.py b/openhab/types.py index 02fc9e9..cd3e072 100644 --- a/openhab/types.py +++ b/openhab/types.py @@ -19,11 +19,12 @@ # # pylint: disable=bad-indentation - +from __future__ import annotations import abc import datetime import re import typing +import dateutil.parser __author__ = 'Georges Toth ' __license__ = 'AGPLv3+' @@ -32,6 +33,35 @@ class CommandType(metaclass=abc.ABCMeta): """Base command data_type class.""" + TYPENAME = "" + SUPPORTED_TYPENAMES = [] + UNDEF = 'UNDEF' + NULL = 'NULL' + UNDEFINED_STATES = [UNDEF,NULL] + + @classmethod + def is_undefined(cls, value: typing.Any) -> None: + return value in CommandType.UNDEFINED_STATES + + + @classmethod + def getTypeFor(cls, typename:str, parent_cls:typing.Optional[typing.Type[CommandType]]=None)->typing.Type[CommandType]: + if parent_cls is None: parent_cls=CommandType + for a_type in parent_cls.__subclasses__(): + if typename in a_type.SUPPORTED_TYPENAMES: + return a_type + else: + #mayba a subclass of a subclass + result=a_type.getTypeFor(typename,a_type) + if result is not None: + return result + return None + + @classmethod + @abc.abstractmethod + def parse(cls, value: str) -> str: + raise NotImplementedError() + @classmethod @abc.abstractmethod def validate(cls, value: typing.Any) -> None: @@ -48,15 +78,56 @@ def validate(cls, value: typing.Any) -> None: """ raise NotImplementedError() +class UndefType(CommandType): + TYPENAME = "UnDef" + SUPPORTED_TYPENAMES = [TYPENAME] + + + @classmethod + def parse(cls, value: str) -> str: + if value in UndefType.UNDEFINED_STATES: + return None + return value + + @classmethod + def validate(cls, value: str) -> None: + pass + + +class GroupType(CommandType): + TYPENAME = "Group" + SUPPORTED_TYPENAMES = [TYPENAME] + + @classmethod + def parse(cls, value: str) -> str: + if value in GroupType.UNDEFINED_STATES: + return None + return value + + @classmethod + def validate(cls, value: str) -> None: + pass + class StringType(CommandType): """StringType data_type class.""" + TYPENAME = "String" + SUPPORTED_TYPENAMES = [TYPENAME] + + @classmethod + def parse(cls, value: str) -> str: + if value in StringType.UNDEFINED_STATES: + return None + if not isinstance(value, str): + raise ValueError() + return value + @classmethod def validate(cls, value: str) -> None: """Value validation method. - Valid values are andy of data_type string. + Valid values are any of data_type string. Args: value (str): The value to validate. @@ -64,12 +135,25 @@ def validate(cls, value: str) -> None: Raises: ValueError: Raises ValueError if an invalid value has been specified. """ - if not isinstance(value, str): - raise ValueError() + StringType.parse(value) class OnOffType(StringType): """OnOffType data_type class.""" + TYPENAME = "OnOff" + SUPPORTED_TYPENAMES = [TYPENAME] + ON = "ON" + OFF = "OFF" + POSSIBLE_VALUES = [ON, OFF] + + @classmethod + def parse(cls, value: str) -> str: + if value in OnOffType.UNDEFINED_STATES: + return None + if value not in OnOffType.POSSIBLE_VALUES: + raise ValueError() + return value + @classmethod def validate(cls, value: str) -> None: @@ -84,13 +168,28 @@ def validate(cls, value: str) -> None: ValueError: Raises ValueError if an invalid value has been specified. """ super().validate(value) + OnOffType.parse(value) + - if value not in ['ON', 'OFF']: - raise ValueError() class OpenCloseType(StringType): """OpenCloseType data_type class.""" + TYPENAME = "OpenClosed" + SUPPORTED_TYPENAMES = [TYPENAME] + OPEN = "OPEN" + CLOSED = "CLOSED" + POSSIBLE_VALUES = [OPEN,CLOSED] + + + @classmethod + def parse(cls, value: str) -> str: + if value in OpenCloseType.UNDEFINED_STATES: + return None + if value not in OpenCloseType.POSSIBLE_VALUES: + raise ValueError() + return value + @classmethod def validate(cls, value: str) -> None: @@ -105,16 +204,30 @@ def validate(cls, value: str) -> None: ValueError: Raises ValueError if an invalid value has been specified. """ super().validate(value) + OpenCloseType.parse(value) + - if value not in ['OPEN', 'CLOSED']: - raise ValueError() class ColorType(StringType): """ColorType data_type class.""" + TYPENAME = "HSB" + SUPPORTED_TYPENAMES = [TYPENAME] @classmethod - def validate(cls, value: str) -> None: + def parse(cls, value:str) -> typing.Tuple[int,int,float]: + if value in ColorType.UNDEFINED_STATES: + return None + hs, ss, bs = re.split(',', value) + h=int(hs) + s=int(ss) + b=float(bs) + if not ((0 <= h <= 360) and (0 <= s <= 100) and (0 <= b <= 100)): + raise ValueError() + return h, s, b + + @classmethod + def validate(cls, value: typing.Union[str, typing.Tuple[int,int,float]]) -> None: """Value validation method. Valid values are in format H,S,B. @@ -129,14 +242,43 @@ def validate(cls, value: str) -> None: Raises: ValueError: Raises ValueError if an invalid value has been specified. """ - super().validate(value) - h, s, b = re.split(',', value) - if not ((0 <= int(h) <= 360) and (0 <= int(s) <= 100) and (0 <= int(b) <= 100)): - raise ValueError() + if isinstance(value,tuple): + if len(value) == 3: + strvalue = "{},{},{}".format(value[0],value[1],value[2]) + super().validate(strvalue) + ColorType.parse(strvalue) + else: + strvalue = str(value) + + super().validate(strvalue) + ColorType.parse(strvalue) + class DecimalType(CommandType): """DecimalType data_type class.""" + TYPENAME = "Decimal" + SUPPORTED_TYPENAMES = [TYPENAME,"Quantity"] + + @classmethod + def parse(cls, value: str) -> typing.Tuple[typing.Union[int,float],str]: + if value in DecimalType.UNDEFINED_STATES: + return None + + m = re.match("(-?[0-9.]+)\s?(.*)?$", value) + if m: + value_value = m.group(1) + value_unit_of_measure = m.group(2) + try: + return_value = int(value_value) + except: + try: + return_value = float(value_value) + except Exception as e: + raise ValueError(e) + return return_value, value_unit_of_measure + raise ValueError() + @classmethod def validate(cls, value: typing.Union[float, int]) -> None: @@ -154,8 +296,25 @@ def validate(cls, value: typing.Union[float, int]) -> None: raise ValueError() + + + class PercentType(DecimalType): """PercentType data_type class.""" + TYPENAME = "Percent" + SUPPORTED_TYPENAMES = [TYPENAME] + + @classmethod + def parse(cls, value: str) -> float: + if value in PercentType.UNDEFINED_STATES: + return None + try: + f = float(value) + if not 0 <= f <= 100: + raise ValueError() + return f + except Exception as e: + raise ValueError(e) @classmethod def validate(cls, value: typing.Union[float, int]) -> None: @@ -178,6 +337,21 @@ def validate(cls, value: typing.Union[float, int]) -> None: class IncreaseDecreaseType(StringType): """IncreaseDecreaseType data_type class.""" + TYPENAME = "IncreaseDecrease" + SUPPORTED_TYPENAMES = [TYPENAME] + + INCREASE = "INCREASE" + DECREASE = "DECREASE" + + POSSIBLE_VALUES = [INCREASE,DECREASE] + + @classmethod + def parse(cls, value: str) -> str: + if value in IncreaseDecreaseType.UNDEFINED_STATES: + return None + if value not in IncreaseDecreaseType.POSSIBLE_VALUES: + raise ValueError() + return value @classmethod def validate(cls, value: str) -> None: @@ -192,13 +366,19 @@ def validate(cls, value: str) -> None: ValueError: Raises ValueError if an invalid value has been specified. """ super().validate(value) - - if value not in ['INCREASE', 'DECREASE']: - raise ValueError() + IncreaseDecreaseType.parse(value) class DateTimeType(CommandType): """DateTimeType data_type class.""" + TYPENAME = "DateTime" + SUPPORTED_TYPENAMES = [TYPENAME] + + @classmethod + def parse(cls, value: str) -> str: + if value in DateTimeType.UNDEFINED_STATES: + return None + return dateutil.parser.parse(value) @classmethod def validate(cls, value: datetime.datetime) -> None: @@ -219,6 +399,20 @@ def validate(cls, value: datetime.datetime) -> None: class UpDownType(StringType): """UpDownType data_type class.""" + TYPENAME = "UpDown" + SUPPORTED_TYPENAMES = [TYPENAME] + UP = "UP" + DOWN = "DOWN" + POSSIBLE_VALUES = [UP,DOWN] + + @classmethod + def parse(cls, value: str) -> str: + if value in UpDownType.UNDEFINED_STATES: + return None + if value not in UpDownType.POSSIBLE_VALUES: + raise ValueError() + return value + @classmethod def validate(cls, value: str) -> None: """Value validation method. @@ -233,13 +427,25 @@ def validate(cls, value: str) -> None: """ super().validate(value) - if value not in ['UP', 'DOWN']: - raise ValueError() + UpDownType.parse(value) -class StopType(StringType): +class StopMoveType(StringType): """UpDownType data_type class.""" + TYPENAME = "StopMove" + SUPPORTED_TYPENAMES = [TYPENAME] + STOP = "STOP" + POSSIBLE_VALUES = [STOP] + + @classmethod + def parse(cls, value: str) -> str: + if value in StopMoveType.UNDEFINED_STATES: + return None + if value not in StopMoveType.POSSIBLE_VALUES: + raise ValueError() + return value + @classmethod def validate(cls, value: str) -> None: """Value validation method. @@ -254,18 +460,64 @@ def validate(cls, value: str) -> None: """ super().validate(value) - if value not in ['STOP']: + StopMoveType.parse(value) + +class PlayPauseType(StringType): + """PlayPauseType data_type class.""" + + TYPENAME = "PlayPause" + SUPPORTED_TYPENAMES = [TYPENAME] + PLAY = "PLAY" + PAUSE = "PAUSE" + POSSIBLE_VALUES = [PLAY,PAUSE] + + @classmethod + def parse(cls, value: str) -> str: + if value in PlayPauseType.UNDEFINED_STATES: + return None + if value not in PlayPauseType.POSSIBLE_VALUES: raise ValueError() + return value + + @classmethod + def validate(cls, value: str) -> None: + """Value validation method. + + Valid values are ``PLAY``, ``PAUSE`` + + Args: + value (str): The value to validate. + + Raises: + ValueError: Raises ValueError if an invalid value has been specified. + """ + super().validate(value) + PlayPauseType.parse(value) + +class NextPrevious(StringType): + """NextPrevious data_type class.""" + + TYPENAME = "NextPrevious" + SUPPORTED_TYPENAMES = [TYPENAME] + NEXT = "NEXT" + PREVIOUS = "PREVIOUS" + POSSIBLE_VALUES = [NEXT, PREVIOUS] + + @classmethod + def parse(cls, value: str) -> str: + if value in NextPrevious.UNDEFINED_STATES: + return None + if value not in NextPrevious.POSSIBLE_VALUES: + raise ValueError() + return value -class PlayerType(StringType): - """PlayerType data_type class.""" @classmethod def validate(cls, value: str) -> None: """Value validation method. - Valid values are ``PLAY``, ``PAUSE``, ``NEXT``, ``PREVIOUS``, ``REWIND``, and ``FASTFORWARD``. + Valid values are ``PLAY``, ``PAUSE`` Args: value (str): The value to validate. @@ -275,5 +527,38 @@ def validate(cls, value: str) -> None: """ super().validate(value) - if value not in ['PLAY', 'PAUSE', 'NEXT', 'PREVIOUS', 'REWIND', 'FASTFORWARD']: + NextPrevious.parse(value) + +class RewindFastforward(StringType): + """RewindFastforward data_type class.""" + + TYPENAME = "RewindFastforward" + SUPPORTED_TYPENAMES = [TYPENAME] + REWIND = "REWIND" + FASTFORWARD = "FASTFORWARD" + POSSIBLE_VALUES = [REWIND, FASTFORWARD] + + @classmethod + def parse(cls, value: str) -> str: + if value in RewindFastforward.UNDEFINED_STATES: + return None + if value not in RewindFastforward.POSSIBLE_VALUES: raise ValueError() + return value + + + @classmethod + def validate(cls, value: str) -> None: + """Value validation method. + + Valid values are ``REWIND``, ``FASTFORWARD`` + + Args: + value (str): The value to validate. + + Raises: + ValueError: Raises ValueError if an invalid value has been specified. + """ + super().validate(value) + + RewindFastforward.parse(value) \ No newline at end of file diff --git a/tests/test_create_delete_items.py b/tests/test_create_delete_items.py index ff27a37..c17fce0 100644 --- a/tests/test_create_delete_items.py +++ b/tests/test_create_delete_items.py @@ -44,10 +44,12 @@ base_url = 'http://10.10.20.81:8080/rest' -def test_create_and_delete_items(openhab: openhab.OpenHAB, nameprefix): +def test_create_and_delete_items(myopenhab: openhab.OpenHAB, nameprefix): log.info("starting tests 'create and delete items'") - my_item_factory = openhab.items.ItemFactory(openhab) + + my_item_factory = openhab.items.ItemFactory(myopenhab) a_group_item = None + a_string_item = None a_number_item = None a_contact_item = None a_datetime_item = None @@ -60,6 +62,9 @@ def test_create_and_delete_items(openhab: openhab.OpenHAB, nameprefix): a_group_item: openhab.items.Item = testGroup(my_item_factory, nameprefix) log.info("the new group:{}".format(a_group_item)) + a_string_item: openhab.items.Item = test_StringItem(my_item_factory, nameprefix) + log.info("the new stringItem:{}".format(a_string_item)) + a_number_item = test_NumberItem(my_item_factory, nameprefix) a_contact_item: openhab.items.ContactItem = test_ContactItem(my_item_factory, nameprefix) @@ -86,24 +91,26 @@ def test_create_and_delete_items(openhab: openhab.OpenHAB, nameprefix): log.info("creation tests worked") finally: if a_group_item is not None: - a_group_item.delete() + a_group_item.delete() + if a_string_item is not None: + a_string_item.delete() if a_number_item is not None: - a_number_item.delete() + a_number_item.delete() if a_contact_item is not None: - a_contact_item.delete() + a_contact_item.delete() if a_datetime_item is not None: - a_datetime_item.delete() + a_datetime_item.delete() if a_rollershutter_item is not None: - a_rollershutter_item.delete() + a_rollershutter_item.delete() if a_color_item is not None: - coloritemname = a_color_item.name - a_color_item.delete() - try: - should_not_work = my_item_factory.get_item(coloritemname) - testutil.doassert(False, True, "this getItem should raise a exception because the item should have been removed.") - except: - pass + coloritemname = a_color_item.name + a_color_item.delete() + try: + should_not_work = my_item_factory.get_item(coloritemname) + testutil.doassert(False, True, "this getItem should raise a exception because the item should have been removed.") + except: + pass if a_dimmer_item is not None: a_dimmer_item.delete() if a_switch_item is not None: @@ -178,6 +185,23 @@ def testGroup(itemFactory, nameprefix) -> openhab.items.Item: return testgroup_item +def test_StringItem(item_factory, nameprefix): + + itemname = "{}CreateStringItemTest".format(nameprefix) + itemtype = openhab.items.StringItem + + x2 = item_factory.create_or_update_item(name=itemname, data_type=itemtype) + x2.state = "test string value 1" + testutil.doassert(itemname, x2.name, "item_name") + testutil.doassert("test string value 1", x2.state, "itemstate") + x2.state = "test string value 2" + testutil.doassert("test string value 2", x2.state, "itemstate") + + + + return x2 + + def test_ContactItem(item_factory, nameprefix): itemname = "{}CreateContactItemTest".format(nameprefix) @@ -238,11 +262,11 @@ def test_ColorItem(item_factory, nameprefix): x2.on() testutil.doassert(itemname, x2.name, "item_name") testutil.doassert("ON", x2.state, "itemstate") - new_value = "51,52,53" + new_value = 51,52,53 x2.state = new_value log.info("itemsate:{}".format(x2.state)) - testutil.doassert(new_value, x2.state, "itemstate") + testutil.doassert((51,52,53), x2.state, "itemstate") return x2 diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index e8fc593..34b2fc2 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -21,10 +21,15 @@ from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable import openhab import openhab.events +import openhab.types import time import openhab.items as items import logging import random +import testutil + +from datetime import datetime,timezone +import pytz log = logging.getLogger() logging.basicConfig(level=10, format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") @@ -57,105 +62,1032 @@ } testitems: Dict[str, openhab.items.Item] = {} -if True: - myopenhab = openhab.OpenHAB(base_url, auto_update=True) - myItemfactory = openhab.items.ItemFactory(myopenhab) - random.seed() - namesuffix = "_{}".format(random.randint(1, 1000)) - testnumberA: openhab.items.NumberItem = myItemfactory.create_or_update_item(name="test_eventSubscription_numberitem_A{}".format(namesuffix), data_type=openhab.items.NumberItem) - testnumberB: openhab.items.NumberItem = myItemfactory.create_or_update_item(name="test_eventSubscription_numberitem_B{}".format(namesuffix), data_type=openhab.items.NumberItem) - itemname = "test_eventSubscription_switchitem_A{}".format(namesuffix) - switchItem: openhab.items.SwitchItem = myItemfactory.create_or_update_item(name=itemname, data_type=openhab.items.SwitchItem) - try: - testnumberA.state = 44.0 - testnumberB.state = 66.0 - expect_A = None - expect_B = None - def on_a_change(item: openhab.items.NumberItem, event: openhab.events.ItemStateEvent): - log.info("########################### UPDATE of {itemname} to eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state}) from OPENHAB ONLY".format( - itemname=event.item_name, eventvalue=event.new_value, eventvalueraw=event.new_value_raw, item_state=item._state, itemstate=item.state)) +expected_state = None +state_correct_count=0 + +expected_command = None +command_correct_count=0 + +expected_new_state = None +expected_old_state = None +state_changed_correct_count=0 + +do_breakpoint=False + +def on_item_state(item: openhab.items.Item, event: openhab.events.ItemStateEvent): + global state_correct_count + log.info("########################### STATE arrived for {itemname} : eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state})".format( + itemname=event.item_name, eventvalue=event.value, eventvalueraw=event.value_raw, item_state=item._state, itemstate=item.state)) + if expected_state is not None: + if isinstance(event.value,datetime): + testutil.doassert(expected_state, event.value.replace(tzinfo=None), "stateEvent item {} ".format(item.name)) + else: + testutil.doassert(expected_state,event.value,"stateEvent item {} ".format(item.name)) + state_correct_count += 1 + + +def on_item_statechange(item: openhab.items.Item, event: openhab.events.ItemStateChangedEvent): + global state_changed_correct_count + log.info("########################### STATE of {itemname} CHANGED from {oldvalue} to {newvalue} (items state: {new_value_item}.".format(itemname=event.item_name,oldvalue=event.old_value,newvalue=event.value, new_value_item=item.state)) + if expected_new_state is not None: + if isinstance(event.value, datetime): + testutil.doassert(expected_new_state, event.value.replace(tzinfo=None), "state changed event item {} value".format(item.name)) + else: + testutil.doassert(expected_new_state, event.value, "state changed event item {} new value".format(item.name)) + + + if expected_old_state is not None: + if isinstance(event.old_value, datetime): + testutil.doassert(expected_old_state, event.old_value.replace(tzinfo=None), "state changed event item {} old value".format(item.name)) + else: + testutil.doassert(expected_old_state, event.old_value, "OLD state changed event item {} old value".format(item.name)) + + state_changed_correct_count +=1 + + + +def on_item_command(item: openhab.items.Item, event: openhab.events.ItemCommandEvent): + global command_correct_count + log.info("########################### COMMAND arrived for {itemname} : eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state})".format( + itemname=event.item_name, eventvalue=event.value, eventvalueraw=event.value_raw, item_state=item._state, itemstate=item.state)) + if expected_command is not None: + if isinstance(event.value, datetime): + testutil.doassert(expected_command, event.value.replace(tzinfo=None), "command event item {}".format(item.name)) + else: + testutil.doassert(expected_command, event.value, "command event item {}".format(item.name)) + command_correct_count +=1 + + + +myopenhab = openhab.OpenHAB(base_url, auto_update=True) +myItemfactory = openhab.items.ItemFactory(myopenhab) + +random.seed() +namesuffix = "_{}".format(random.randint(1, 1000)) + +test_azimuth=myItemfactory.get_item("testworld_Azimuth") +test_azimuth.add_event_listener(listening_types=openhab.events.ItemStateEventType, listener=on_item_state) +test_azimuth.add_event_listener(listening_types=openhab.events.ItemCommandEventType, listener=on_item_command) +test_azimuth.add_event_listener(listening_types=openhab.events.ItemStateChangedEventType, listener=on_item_statechange) + + + + + + +def create_event_data(event_type:openhab.events.ItemEventType, itemname, payload): + result= {} + if event_type== openhab.events.ItemStateEventType: + event_type_topic_path="statechanged" + elif event_type== openhab.events.ItemStateChangedEventType: + event_type_topic_path="state" + elif event_type== openhab.events.ItemCommandEventType: + event_type_topic_path="command" + result = {"type": event_type, "topic": "smarthome/items/{itemname}/{event_type_topic_path}".format(itemname=itemname,event_type_topic_path=event_type_topic_path), "payload": payload} + return result + +def create_event_payload(type:str,value:str,oldType:str=None,oldValue:str=None): + result='{"type":"'+type+'","value":"'+str(value)+'"' + if oldType is None: + oldType = type + if oldValue is not None: + result = result + ', "oldType":"'+oldType+'","oldValue":"'+str(oldValue)+'"' + result = result + '}' + return result + +def test_number_item(): + global expected_state + global state_correct_count + + global expected_command + global command_correct_count + + global expected_new_state + global expected_old_state + global state_changed_correct_count + + global do_breakpoint + + state_correct_count = 0 + command_correct_count = 0 + state_changed_correct_count = 0 + + + try: + testitem: openhab.items.NumberItem = myItemfactory.create_or_update_item(name="test_eventSubscription_numberitem_A{}".format(namesuffix), data_type=openhab.items.NumberItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) + + expected_new_state = expected_state = sending_state = 170.3 + #eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, '{"type":"Decimal","value":"'+str(sending_state)+'"}') + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Decimal",str(sending_state))) + myopenhab._parse_event(eventData) + testutil.doassert(1,state_correct_count) + + + sending_state = openhab.types.UndefType.UNDEF + expected_new_state = expected_state = None + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Decimal",str(sending_state))) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + + expected_new_state = expected_state = sending_state = -4 + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Decimal",str(sending_state))) + myopenhab._parse_event(eventData) + testutil.doassert(2, state_correct_count) + + expected_new_state = expected_state = sending_state = 170.3 + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Quantity",str(sending_state))) + myopenhab._parse_event(eventData) + testutil.doassert(3, state_correct_count) + + expected_new_state = expected_state = 180 + sending_state = "180 °" + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Quantity",str(sending_state))) + myopenhab._parse_event(eventData) + testutil.doassert(4, state_correct_count) + + expected_old_state = 180 + expected_new_state = expected_state = 190 + sending_state = "190 °" + eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("Quantity",str(sending_state),oldValue=expected_old_state)) + myopenhab._parse_event(eventData) + testutil.doassert(4, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + + expected_old_state = 190 + expected_new_state = expected_state = 200 + sending_state = "200 °" + eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("Quantity",str(sending_state),oldValue=expected_old_state)) + myopenhab._parse_event(eventData) + testutil.doassert(4, state_correct_count) + testutil.doassert(2, state_changed_correct_count) + + expected_old_state = None + expected_command = 200.1 + sending_command = 200.1 + + eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("Quantity", str(sending_command))) + myopenhab._parse_event(eventData) + testutil.doassert(4, state_correct_count) + testutil.doassert(2, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + + log.info("################## starting tests with real item on openhab") + + sending_state = 123.4 + expected_new_state = expected_state = sending_state + expected_command = sending_state + expected_old_state = None + + testitem.state = sending_state + time.sleep(0.5) + testutil.doassert(5, state_correct_count) + testutil.doassert(3, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + + expected_old_state = sending_state + expected_new_state = expected_state = sending_state = expected_command = 567.8 + + testitem.state = sending_state + time.sleep(0.5) + testutil.doassert(6, state_correct_count) + testutil.doassert(4, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + + expected_old_state = sending_state + expected_new_state = expected_state = sending_state = expected_command = 999.99 + + testitem.command(sending_state) + time.sleep(0.5) + testutil.doassert(7, state_correct_count) + testutil.doassert(5, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + + + + finally: + pass + testitem.delete() + + +def test_string_item(): + global expected_state + global state_correct_count + + global expected_command + global command_correct_count + + global expected_new_state + global expected_old_state + global state_changed_correct_count + + global do_breakpoint + + state_correct_count = 0 + command_correct_count = 0 + state_changed_correct_count = 0 + + try: + testitem: openhab.items.StringItem = myItemfactory.create_or_update_item(name="test_eventSubscription_stringitem_A{}".format(namesuffix), data_type=openhab.items.StringItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) + + expected_new_state = expected_state = sending_state = "test value 1" + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("String", str(sending_state))) + myopenhab._parse_event(eventData) + + testutil.doassert(1, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + + sending_state = openhab.types.UndefType.UNDEF + expected_new_state = expected_state = None + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("String", str(sending_state))) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + expected_new_state = expected_state = sending_state = "äöü°" + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("String", str(sending_state))) + myopenhab._parse_event(eventData) + testutil.doassert(2, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + expected_old_state = expected_state + expected_new_state = expected_state = sending_state = "test value 2" + eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("String", str(sending_state), oldValue=expected_old_state)) + myopenhab._parse_event(eventData) + testutil.doassert(2, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + expected_old_state = None + sending_command = "test value 3" + expected_new_state = expected_state = sending_command + expected_command = sending_command + + eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("String", str(sending_command))) + myopenhab._parse_event(eventData) + testutil.doassert(2, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + log.info("################## starting tests with real item on openhab") + + sending_state = "test value 4" + expected_new_state = expected_state = sending_state + expected_command = sending_state + expected_old_state = None + + testitem.state = sending_state + time.sleep(0.5) + testutil.doassert(3, state_correct_count) + testutil.doassert(2, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + + expected_old_state = sending_state + expected_new_state = expected_state = sending_state = expected_command = "test value 5" + testitem.command(sending_state) + time.sleep(0.5) + testutil.doassert(4, state_correct_count) + testutil.doassert(3, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + + + + finally: + pass + testitem.delete() + +def test_dateTime_item(): + global expected_state + global state_correct_count + + global expected_command + global command_correct_count + + global expected_new_state + global expected_old_state + global state_changed_correct_count + + global do_breakpoint + + state_correct_count = 0 + command_correct_count = 0 + state_changed_correct_count = 0 + + try: + testitem: openhab.items.DateTimeItem = myItemfactory.create_or_update_item(name="test_eventSubscription_datetimeitem_A{}".format(namesuffix), data_type=openhab.items.DateTimeItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) + + log.info("starting step 1") + expected_new_state = expected_state = sending_state = datetime.now() + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("DateTime", str(sending_state))) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + log.info("starting step 2") + expected_old_state = expected_state + expected_new_state = expected_state = sending_state = datetime.now() + eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("DateTime", str(sending_state), oldValue=expected_old_state)) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + log.info("################## starting tests with real item on openhab") + log.info("starting step 3") + + sending_state = datetime(2001,2,3,4,5,6,microsecond=7000) + expected_new_state = expected_state = sending_state + expected_command = sending_state + expected_old_state = None + + + testitem.state = sending_state + time.sleep(0.5) + testutil.doassert(2, state_correct_count) + testutil.doassert(2, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + log.info("starting step 4") + expected_new_state = expected_old_state = expected_state + expected_new_state = expected_command = expected_state = sending_state = sending_command = sending_state = datetime(2001,2,3,4,5,6,microsecond=8000) + testitem.command(sending_command) + time.sleep(0.5) + testutil.doassert(3, state_correct_count) + testutil.doassert(3, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + + finally: + pass + testitem.delete() + + +def test_player_item(): + global expected_state + global state_correct_count + + global expected_command + global command_correct_count + + global expected_new_state + global expected_old_state + global state_changed_correct_count + + global do_breakpoint + + state_correct_count = 0 + command_correct_count = 0 + state_changed_correct_count = 0 + + try: + testitem: openhab.items.PlayerItem = myItemfactory.create_or_update_item(name="test_eventSubscription_playeritem_A{}".format(namesuffix), data_type=openhab.items.PlayerItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) + + expected_new_state = expected_state = sending_state = openhab.types.PlayPauseType.PLAY + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("PlayPause", str(sending_state))) + myopenhab._parse_event(eventData) + + testutil.doassert(1, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + sending_state = openhab.types.UndefType.UNDEF + expected_new_state = expected_state = None + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("PlayPause", str(sending_state))) + myopenhab._parse_event(eventData) + + testutil.doassert(1, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + expected_new_state = expected_state = sending_state = openhab.types.RewindFastforward.FASTFORWARD + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("RewindFastforward", str(sending_state))) + myopenhab._parse_event(eventData) + + testutil.doassert(2, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + expected_old_state = expected_state + expected_new_state = expected_state = sending_state = openhab.types.PlayPauseType.PAUSE + eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("PlayPause", str(sending_state), oldType="RewindFastforward" ,oldValue=expected_old_state)) + myopenhab._parse_event(eventData) + testutil.doassert(2, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + + + sending_command = openhab.types.NextPrevious.NEXT + expected_command = openhab.types.NextPrevious.NEXT + + eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("NextPrevious", str(sending_command))) + myopenhab._parse_event(eventData) + testutil.doassert(2, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + log.info("################## starting tests with real item on openhab") + + + sending_command = expected_command = openhab.types.PlayPauseType.PAUSE + expected_new_state = expected_state = sending_command + expected_old_state = None + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(3, state_correct_count) + testutil.doassert(2, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + expected_old_state = expected_state + sending_command = expected_command = openhab.types.PlayPauseType.PLAY + expected_new_state = expected_state = sending_command + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(4, state_correct_count) + testutil.doassert(3, state_changed_correct_count) + testutil.doassert(3, command_correct_count) + + expected_old_state = openhab.types.PlayPauseType.PLAY + sending_command = expected_command = openhab.types.NextPrevious.NEXT + expected_new_state = expected_state = openhab.types.PlayPauseType.PLAY + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(4, state_correct_count) # NEXT is not a state! + testutil.doassert(3, state_changed_correct_count) # NEXT is not a state! + testutil.doassert(4, command_correct_count) + + + finally: + pass + testitem.delete() + + +def test_switch_item(): + global expected_state + global state_correct_count + + global expected_command + global command_correct_count + + global expected_new_state + global expected_old_state + global state_changed_correct_count + + global do_breakpoint + + state_correct_count = 0 + command_correct_count = 0 + state_changed_correct_count = 0 + + try: + testitem: openhab.items.SwitchItem = myItemfactory.create_or_update_item(name="test_eventSubscription_switchitem_A{}".format(namesuffix), data_type=openhab.items.SwitchItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) + + expected_new_state = expected_state = sending_state = openhab.types.OnOffType.ON + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("OnOff", str(sending_state))) + myopenhab._parse_event(eventData) + + testutil.doassert(1, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + expected_old_state = expected_state + expected_new_state = expected_state = sending_state = openhab.types.OnOffType.OFF + eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("OnOff", str(sending_state), oldValue=expected_old_state)) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + + + expected_command = sending_command = openhab.types.OnOffType.ON + + + eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("OnOff", str(sending_command))) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + log.info("################## starting tests with real item on openhab") + + + sending_state = sending_command = expected_command = openhab.types.OnOffType.ON + expected_new_state = expected_state = sending_command + expected_old_state = None + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(2, state_correct_count) + testutil.doassert(2, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + expected_old_state = sending_state + sending_state = openhab.types.OnOffType.OFF + expected_new_state = expected_state = sending_state + + testitem.state =sending_state + + time.sleep(0.5) + testutil.doassert(3, state_correct_count) + testutil.doassert(3, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + + + finally: + pass + testitem.delete() + +def test_contact_item(): + global expected_state + global state_correct_count + + global expected_command + global command_correct_count + + global expected_new_state + global expected_old_state + global state_changed_correct_count + + global do_breakpoint + + state_correct_count = 0 + command_correct_count = 0 + state_changed_correct_count = 0 + + try: + testitem: openhab.items.ContactItem = myItemfactory.create_or_update_item(name="test_eventSubscription_contactitem_A{}".format(namesuffix), data_type=openhab.items.ContactItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) + + expected_new_state = expected_state = sending_state = openhab.types.OpenCloseType.OPEN + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("OpenClosed", str(sending_state))) + myopenhab._parse_event(eventData) + + testutil.doassert(1, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + expected_old_state = expected_state + expected_new_state = expected_state = sending_state = openhab.types.OpenCloseType.CLOSED + eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("OpenClosed", str(sending_state), oldValue=expected_old_state)) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + + + expected_command = sending_command = openhab.types.OpenCloseType.OPEN + + + eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("OpenClosed", str(sending_command))) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + log.info("################## starting tests with real item on openhab") + + + sending_state = sending_command = expected_command = openhab.types.OpenCloseType.OPEN + expected_new_state = expected_state = sending_command + expected_old_state = None + testitem.state = sending_state + + time.sleep(0.5) + testutil.doassert(2, state_correct_count) + testutil.doassert(2, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + expected_old_state = sending_state + sending_state = openhab.types.OpenCloseType.CLOSED + expected_new_state = expected_state = sending_state + + testitem.state =sending_state + + time.sleep(0.5) + testutil.doassert(3, state_correct_count) + testutil.doassert(3, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + + + finally: + pass + testitem.delete() + +def test_dimmer_item(): + global expected_state + global state_correct_count + + global expected_command + global command_correct_count + + global expected_new_state + global expected_old_state + global state_changed_correct_count + + global do_breakpoint + + state_correct_count = 0 + command_correct_count = 0 + state_changed_correct_count = 0 + + try: + testitem: openhab.items.DimmerItem = myItemfactory.create_or_update_item(name="test_eventSubscription_dimmeritem_A{}".format(namesuffix), data_type=openhab.items.DimmerItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) + + expected_new_state = expected_state = sending_state = 45.67 + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Percent", str(sending_state))) + myopenhab._parse_event(eventData) + + testutil.doassert(1, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + expected_old_state = expected_state + expected_new_state = expected_state = sending_state = 12.12 + eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("Percent", str(sending_state), oldValue=expected_old_state)) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + + + expected_command = sending_command = 44.44 + + + eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("Percent", str(sending_command))) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + log.info("################## starting tests with real item on openhab") + + + sending_state = sending_command = expected_command = 66.77 + expected_new_state = expected_state = sending_command + expected_old_state = None + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(2, state_correct_count) + testutil.doassert(2, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + expected_old_state = sending_state + sending_state = 99.5 + expected_new_state = expected_state = sending_state + + testitem.state =sending_state + + time.sleep(0.5) + testutil.doassert(3, state_correct_count) + testutil.doassert(3, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + + + expected_old_state = sending_state + expected_state= sending_state = openhab.types.OnOffType.OFF + expected_new_state = 0 + testitem.state = sending_state + + time.sleep(0.5) + testutil.doassert(4, state_correct_count) + testutil.doassert(4, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + + + expected_old_state = 0 + expected_state= sending_state = openhab.types.OnOffType.ON + expected_new_state = 100 + testitem.state = sending_state + + time.sleep(0.5) + testutil.doassert(5, state_correct_count) + testutil.doassert(5, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + + + expected_old_state = 100 + expected_command = sending_command = expected_state = sending_state = openhab.types.IncreaseDecreaseType.DECREASE + expected_new_state = 99 + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state + testutil.doassert(5, state_changed_correct_count) # openhab does not automatically increase the value + testutil.doassert(3, command_correct_count) + + expected_old_state = 99 + expected_command = sending_command = expected_state = sending_state = openhab.types.IncreaseDecreaseType.DECREASE + expected_new_state = 98 + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state + testutil.doassert(5, state_changed_correct_count) # openhab does not automatically increase the value + testutil.doassert(4, command_correct_count) + + + finally: + pass + testitem.delete() + +def test_color_item(): + global expected_state + global state_correct_count + + global expected_command + global command_correct_count + + global expected_new_state + global expected_old_state + global state_changed_correct_count + + global do_breakpoint + + state_correct_count = 0 + command_correct_count = 0 + state_changed_correct_count = 0 + + try: + testitem: openhab.items.ColorItem = myItemfactory.create_or_update_item(name="test_eventSubscription_coloritem_A{}".format(namesuffix), data_type=openhab.items.ColorItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) + + expected_new_state = expected_state = sending_state = 45.67 + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Percent", str(sending_state))) + myopenhab._parse_event(eventData) + + testutil.doassert(1, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + sending_state = "1,2,3" + expected_new_state = expected_state = 1,2,3 + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("HSB", str(sending_state))) + myopenhab._parse_event(eventData) + + testutil.doassert(2, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + + expected_old_state = 1,2,3 + sending_old_state = "1,2,3" + expected_new_state = expected_state = 4,5,6 + sending_state = "4,5,6" + + eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("HSB", str(sending_state), oldType="HSB",oldValue=sending_old_state)) + myopenhab._parse_event(eventData) + testutil.doassert(2, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(0, command_correct_count) + + + log.info("################## starting tests with real item on openhab") + expected_new_state = expected_old_state = None + expected_state = expected_command = sending_command = (1,2,3) + + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(3, state_correct_count) + testutil.doassert(2, state_changed_correct_count) + testutil.doassert(1, command_correct_count) + + + sending_state = sending_command = expected_command = 66.77 + expected_new_state = 1,2,66.77 + expected_state = sending_state + expected_old_state = 1,2,3 + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(4, state_correct_count) + testutil.doassert(3, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + + + expected_old_state = expected_new_state + expected_state = sending_state = 99.5 + expected_new_state = 1,2,99.5 + + testitem.state =sending_state + + time.sleep(0.5) + testutil.doassert(5, state_correct_count) + testutil.doassert(4, state_changed_correct_count) + testutil.doassert(2, command_correct_count) + + + expected_old_state = expected_new_state + expected_command = sending_command = openhab.types.IncreaseDecreaseType.DECREASE + + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state + testutil.doassert(4, state_changed_correct_count) # openhab does not automatically increase the value + testutil.doassert(3, command_correct_count) + + expected_old_state = expected_new_state + expected_state = openhab.types.OnOffType.OFF + expected_new_state = 1,2,0 + + + expected_command = sending_command = openhab.types.OnOffType.OFF + + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(6, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state + testutil.doassert(5, state_changed_correct_count) # openhab does not automatically increase the value + testutil.doassert(4, command_correct_count) + + finally: + pass + testitem.delete() + + +def test_rollershutter_item(): + global expected_state + global state_correct_count + + global expected_command + global command_correct_count + + global expected_new_state + global expected_old_state + global state_changed_correct_count + + global do_breakpoint - testnumberA.add_event_listener(openhab.events.ItemCommandEventType, on_a_change, only_if_eventsource_is_openhab=True) + state_correct_count = 0 + command_correct_count = 0 + state_changed_correct_count = 0 + try: + testitem: openhab.items.RollershutterItem = myItemfactory.create_or_update_item(name="test_eventSubscription_rollershutteritem_A{}".format(namesuffix), data_type=openhab.items.RollershutterItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=True) + testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) - def on_testnumber_b_change(item: openhab.items.NumberItem, event: openhab.events.ItemStateEvent): - log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.item_name, event.new_value_raw, item.state)) - if expect_B is not None: - assert item.state == expect_B + expected_new_state = expected_state = sending_state = 45.67 + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Percent", str(sending_state))) + myopenhab._parse_event(eventData) + testutil.doassert(1, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) - testnumberB.add_event_listener(openhab.events.ItemCommandEventType, on_testnumber_b_change, only_if_eventsource_is_openhab=True) + sending_state = openhab.types.UpDownType.UP + expected_state = sending_state + eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("UpDown", str(sending_state))) + myopenhab._parse_event(eventData) + testutil.doassert(2, state_correct_count) + testutil.doassert(0, state_changed_correct_count) + testutil.doassert(0, command_correct_count) - def on_testnumber_a_change_all(item: openhab.items.Item, event: openhab.events.ItemStateEvent): - if event.source == openhab.events.EventSourceInternal: - log.info("########################### INTERNAL UPDATE of {} to {} (itemsvalue:{}) from internal".format(event.item_name, event.new_value_raw, item.state)) - else: - log.info("########################### EXTERNAL UPDATE of {} to {} (itemsvalue:{}) from OPENHAB".format(event.item_name, event.new_value_raw, item.state)) + log.info("################## starting tests with real item on openhab") + expected_new_state = expected_old_state = None + expected_state = expected_command = sending_command = 55.66 - testnumberA.add_event_listener(openhab.events.ItemCommandEventType, on_testnumber_a_change_all, only_if_eventsource_is_openhab=False) + testitem.command(sending_command) - time.sleep(2) - log.info("###################################### starting test 'internal Event'") + time.sleep(0.5) + testutil.doassert(3, state_correct_count) + testutil.doassert(1, state_changed_correct_count) + testutil.doassert(1, command_correct_count) - expect_B = 2 - testnumberB.state = 2 - time.sleep(0.1) - expect_B = None - checkcount = 0 + sending_state = sending_command = expected_command = 66.77 + expected_new_state = 66.77 + expected_state = sending_state + expected_old_state = 55.66 + testitem.command(sending_command) + time.sleep(0.5) + testutil.doassert(4, state_correct_count) + testutil.doassert(2, state_changed_correct_count) + testutil.doassert(2, command_correct_count) - def create_event_data(type, itemname, payload): - result = {"type": type, "topic": "smarthome/items/{itemname}/state".format(itemname=itemname), "payload": payload} - return result + log.info("xx") + expected_old_state = expected_new_state + expected_state = expected_command = sending_command = openhab.types.UpDownType.UP + expected_new_state = 0 + testitem.command(sending_command) - def on_light_switch_command(item: openhab.items.Item, event: openhab.events.ItemCommandEvent): - log.info("########################### COMMAND of {} to {} (itemsvalue:{}) from OPENHAB".format(event.item_name, event.new_value_raw, item.state)) + time.sleep(0.5) + testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state + testutil.doassert(3, state_changed_correct_count) # openhab does not automatically increase the value + testutil.doassert(3, command_correct_count) + expected_old_state = expected_new_state + expected_state = 0 + expected_new_state = 0 - def on_any_item_command(item: openhab.items.Item, event: openhab.events.ItemStateEvent): - log.info("########################### UPDATE of {} to {} (itemsvalue:{}) from OPENHAB ONLY".format(event.item_name, event.new_value_raw, item.state)) - if expected_switch_Value is not None: - global checkcount - actual_value = event.new_value - assert actual_value == expected_switch_Value, "expected value to be {}, but it was {}".format(expected_switch_Value, actual_value) - checkcount += 1 + expected_command = sending_command = openhab.types.StopMoveType.STOP - testname = "OnOff" - expected_switch_Value = "ON" - type = 'ItemCommandEvent' + testitem.command(sending_command) - payload = '{"type":"OnOff","value":"ON"}' - eventData = create_event_data(type, itemname, payload) - switchItem.add_event_listener(listening_types=openhab.events.ItemCommandEventType, listener=on_any_item_command, only_if_eventsource_is_openhab=False) - switchItem.add_event_listener(listening_types=openhab.events.ItemCommandEventType, listener=on_light_switch_command, only_if_eventsource_is_openhab=False) + time.sleep(0.5) + testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state + testutil.doassert(3, state_changed_correct_count) # openhab does not automatically increase the value + testutil.doassert(4, command_correct_count) - myopenhab._parse_event(eventData) + finally: + pass + testitem.delete() - expected_switch_Value = "OFF" - switchItem.off() - expected_switch_Value = "ON" - switchItem.on() +test_number_item() +test_string_item() +test_dateTime_item() +test_player_item() +test_switch_item() +test_contact_item() +test_dimmer_item() +test_rollershutter_item() - assert checkcount == 3, "not all events got processed successfully" - log.info("###################################### test 'internal Event' finished successfully") +keep_going = True +log.info("###################################### tests finished successfully") +while keep_going: + # waiting for events from openhab + time.sleep(10) - finally: - testnumberA.delete() - testnumberB.delete() - switchItem.delete() - keep_going = True - log.info("###################################### tests finished successfully") - while keep_going: - # waiting for events from openhab - time.sleep(10) diff --git a/tests/testutil.py b/tests/testutil.py index 3c98404..8139510 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -25,4 +25,4 @@ def doassert(expect: Any, actual: Any, label: Optional[str] = ""): - assert actual == expect, f"expected {label}:'{expect}', but it actually has '{actual}'".format(label=label, actual=actual, expect=expect) + assert actual == expect, "expected {label}:'{expect}', but it actually has '{actual}'".format(label=label, actual=actual, expect=expect) From 628a9af0beb520f7978b79b35dac74984a8fac28 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Wed, 30 Dec 2020 10:25:03 +0100 Subject: [PATCH 11/25] changed Server Side Events client to aiohttp_sse_client as suggested by Georges some additions around event content bugfixes cleaned code to fix linting issues --- README.rst | 33 ++- openhab/client.py | 162 +++++------ openhab/events.py | 9 +- openhab/items.py | 333 +++++++++++++--------- openhab/types.py | 91 +++--- test.py | 11 +- tests/test_create_delete_items.py | 19 +- tests/test_eventsubscription.py | 457 +++++++++++++----------------- 8 files changed, 552 insertions(+), 563 deletions(-) diff --git a/README.rst b/README.rst index 4eb55e3..4e089c2 100644 --- a/README.rst +++ b/README.rst @@ -35,7 +35,7 @@ Requirements - python >= 3.5 - python :: dateutil - python :: requests - - python :: sseclient + - python :: aiohttp_sse_client - openHAB version 2 Installation @@ -106,9 +106,11 @@ Example usage of the library: log.info("########################### COMMAND of {} to {} (itemsvalue:{}) from OPENHAB".format(event.itemname, event.newValueRaw, item.state)) if event.source == openhab.events.EventSourceOpenhab: log.info("this change came from openhab") - # install listener for evetns + + # install listener for events to receive all events (from internal and openhab) testroom1_LampOnOff.add_event_listener(listening_types=openhab.events.ItemCommandEventType, listener=onLight_switchCommand, only_if_eventsource_is_openhab=False) - # switch you will receive update also for your changes in the code. (see + + # if you switch the item yourself you will also get update / state / command events. (with event.source == openhab.events.EventSourceInternal) testroom1_LampOnOff.off() #Events stop to be delivered @@ -121,24 +123,29 @@ Example usage of the library: #create the item testDimmer = itemFactory.create_or_update_item(name="the_testDimmer", data_type=openhab.items.DimmerItem) #use item - testDimmer.state=95 + testDimmer.state = 95 + testDimmer.off() + testDimmer.command("ON") + #or better: + testDimmer.command(openhab.types.OnOffType.OFF) + + + # you can set or change many item attributes: - # you can set change many item attributes: - nameprefix="testcase_1_" - itemname = "{}CreateItemTest".format(nameprefix) + itemname = "CreateItemTest" item_quantity_type = "Angle" # "Length",Temperature,,Pressure,Speed,Intensity,Dimensionless,Angle itemtype = "Number" labeltext = "das ist eine testzahl:" itemlabel = "[{labeltext}%.1f °]".format(labeltext=labeltext) - itemcategory = "{}TestCategory".format(nameprefix) - itemtags: List[str] = ["{}testtag1".format(nameprefix), "{}testtag2".format(nameprefix)] - itemgroup_names: List[str] = ["{}testgroup1".format(nameprefix), "{}testgroup2".format(nameprefix)] - grouptype = "{}testgrouptype".format(nameprefix) - functionname = "{}testfunctionname".format(nameprefix) - functionparams: List[str] = ["{}testfunctionnameParam1".format(nameprefix), "{}testfunctionnameParam2".format(nameprefix), "{}testfunctionnameParam3".format(nameprefix)] + itemcategory = "TestCategory" + itemtags: List[str] = ["testtag1", "testtag2"] + itemgroup_names: List[str] = ["testgroup1", "testgroup2"] + grouptype = "testgrouptype" + functionname = "testfunctionname" + functionparams: List[str] = ["testfunctionnameParam1", "testfunctionnameParam2", "testfunctionnameParam3"] x2 = item_factory.create_or_update_item(name=itemname, data_type=itemtype, diff --git a/openhab/client.py b/openhab/client.py index 17c1565..fff92d8 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -24,12 +24,14 @@ import typing import warnings -from sseclient import SSEClient + +from aiohttp_sse_client import client as sse_client +import asyncio import threading import requests import weakref import json -import time + from requests.auth import HTTPBasicAuth @@ -84,13 +86,14 @@ def __init__(self, base_url: str, self.maxEchoToOpenhabMS = max_echo_to_openhab_ms self.logger = logging.getLogger(__name__) + self.sseDaemon = None self.__keep_event_daemon_running__ = False + self.__wait_while_looping = threading.Event() self.eventListeners: typing.List[typing.Callable] = [] if self.autoUpdate: self.__installSSEClient__() - @staticmethod - def _check_req_return(req: requests.Response) -> None: + def _check_req_return(self, req: requests.Response) -> None: """Internal method for checking the return value of a REST HTTP request. Args: @@ -104,6 +107,8 @@ def _check_req_return(req: requests.Response) -> None: REST request. """ if not 200 <= req.status_code < 300: + self.logger.error("HTTP error: {} caused by request '{}' with content '{}' ".format(req.status_code, req.url, req.content)) + req.raise_for_status() def _parse_item(self, event: openhab.events.ItemEvent) -> None: @@ -115,8 +120,6 @@ def _parse_item(self, event: openhab.events.ItemEvent) -> None: event:openhab.events.ItemEvent holding the event data """ - - def _parse_event(self, event_data: typing.Dict) -> None: """method to parse a event from openhab. it interprets the received event dictionary and populates an openhab.events.event Object. @@ -133,7 +136,7 @@ def _parse_event(self, event_data: typing.Dict) -> None: item_name = event_data["topic"].split("/")[-2] event_data = json.loads(event_data["payload"]) - raw_event=openhab.events.RawItemEvent(item_name=item_name,event_type=event_reason,content=event_data) + raw_event = openhab.events.RawItemEvent(item_name=item_name, event_type=event_reason, content=event_data) self._inform_event_listeners(raw_event) if item_name in self.registered_items: @@ -141,71 +144,10 @@ def _parse_event(self, event_data: typing.Dict) -> None: if item is None: self.logger.warning("item '{}' was removed in all scopes. Ignoring the events coming in for it.".format(item_name)) else: - item.process_external_event(raw_event) + item._process_external_event(raw_event) else: self.logger.debug("item '{}' not registered. ignoring the arrived event.".format(item_name)) - - - # event = None - # payload_data = json.loads(event_data["payload"]) - # remote_datatype = payload_data["type"] - # new_value = payload_data["value"] - # log.debug("####### new Event arrived:") - # log.debug("item name:{}".format(item_name)) - # log.debug("Event-type:{}".format(event_reason)) - # log.debug("payloadData:{}".format(event_data["payload"])) - # - # if event_reason == "ItemStateEvent": - # event = openhab.events.ItemStateEvent(item_name=item_name, - # source=openhab.events.EventSourceOpenhab, - # remote_datatype=remote_datatype, - # new_value_raw=new_value, - # unit_of_measure="", - # new_value="", - # as_update=False, - # is_value=None, - # command=None) - # elif event_reason == "ItemCommandEvent": - # event = openhab.events.ItemCommandEvent(item_name=item_name, - # source=openhab.events.EventSourceOpenhab, - # remote_datatype=remote_datatype, - # new_value_raw=new_value, - # unit_of_measure="", - # new_value="", - # is_value = None, - # command = None - # ) - # - # elif event_reason in ["ItemStateChangedEvent"]: - # old_remote_datatype = payload_data["oldType"] - # old_value = payload_data["oldValue"] - # event = openhab.events.ItemStateChangedEvent(item_name=item_name, - # source=openhab.events.EventSourceOpenhab, - # remote_datatype=remote_datatype, - # new_value_raw=new_value, - # new_value="", - # unit_of_measure="", - # old_remote_datatype=old_remote_datatype, - # old_value_raw=old_value, - # old_value="", - # old_unit_of_measure="", - # as_update=False, - # is_value=None, - # command=None - # ) - # log.debug("received ItemStateChanged for '{itemname}'[{olddatatype}->{datatype}]:{oldState}->{newValue}".format( - # itemname=item_name, - # olddatatype=old_remote_datatype, - # datatype=remote_datatype, - # oldState=old_value, - # newValue=new_value)) - # - # else: - # log.debug("received command for '{itemname}'[{datatype}]:{newValue}".format(itemname=item_name, datatype=remote_datatype, newValue=new_value)) - - - else: log.debug("received unknown Event-data_type in Openhab Event stream: {}".format(event_data)) @@ -237,29 +179,35 @@ def remove_event_listener(self, listener: typing.Optional[typing.Callable[[openh elif listener in self.eventListeners: self.eventListeners.remove(listener) - def _sse_daemon_thread(self): - """internal method to receive events from openhab. - This method blocks and therefore should be started as separate thread. - """ - self.logger.info("starting Openhab - Event Daemon") - next_waittime = initial_waittime = 0.1 + def sse_client_handler(self): + """the actual handler to receive Events from openhab + """ + self.logger.info("about to connect to Openhab Events-Stream.") + + async def run_loop(): + async with sse_client.EventSource(self.events_url) as event_source: + try: + self.logger.info("starting Openhab - Event Daemon") + async for event in event_source: + if not self.__keep_event_daemon_running__: + self.sseDaemon = None + return + event_data = json.loads(event.data) + self._parse_event(event_data) + + except ConnectionError as exception: + self.logger.error("connection error") + self.logger.exception(exception) while self.__keep_event_daemon_running__: + # keep restarting the handler after timeouts or connection issues try: - self.logger.info("about to connect to Openhab Events-Stream.") - - messages = SSEClient(self.events_url) - - next_waittime = initial_waittime - for event in messages: - event_data = json.loads(event.data) - self._parse_event(event_data) - if not self.__keep_event_daemon_running__: - return - + asyncio.run(run_loop()) + except asyncio.TimeoutError: + self.logger.info("reconnecting after timeout") except Exception as e: - self.logger.warning("Lost connection to Openhab Events-Stream.", e) - time.sleep(next_waittime) # sleep a bit and then retry - next_waittime = min(10, next_waittime+0.5) # increase waittime over time up to 10 seconds + self.logger.exception(e) + + self.sseDaemon = None def get_registered_items(self) -> weakref.WeakValueDictionary: """get a Dict of weak references to registered items. @@ -278,14 +226,44 @@ def register_item(self, item: openhab.items.Item) -> None: if item.name not in self.registered_items: self.registered_items[item.name] = item + def stop_receiving_events(self): + """stop to receive events from openhab. + """ + self.__keep_event_daemon_running__ = False + + def start_receiving_events(self): + """start to receive events from openhab. + """ + if not self.__keep_event_daemon_running__: + if self.sseDaemon is not None: + if self.sseDaemon.is_alive(): + # we are running already and did not stop yet. + # so we simply keep running + self.__keep_event_daemon_running__ = True + return + self.__installSSEClient__() + def __installSSEClient__(self) -> None: """ installs an event Stream to receive all Item events""" - # now start readerThread self.__keep_event_daemon_running__ = True - self.sseDaemon = threading.Thread(target=self._sse_daemon_thread, args=(), daemon=True) + self.keep_running = True + self.sseDaemon = threading.Thread(target=self.sse_client_handler, args=(), daemon=True) + + self.logger.info("about to connect to Openhab Events-Stream.") self.sseDaemon.start() + def stop_looping(self): + """ method to reactivate the thread which went into the loop_for_events loop. + """ + self.__wait_while_looping.set() + + def loop_for_events(self): + """ method to keep waiting for events from openhab + you can use this method after your program finished initialization and all other work and now wants to keep waiting for events. + """ + self.__wait_while_looping.wait() + def req_get(self, uri_path: str) -> typing.Any: """Helper method for initiating a HTTP GET request. @@ -375,7 +353,7 @@ def fetch_all_items(self) -> typing.Dict[str, openhab.items.Item]: Returns: dict: Returns a dict with item names as key and item class instances as value. """ - items = {} # data_type: dict + items = {} # type: dict res = self.req_get('/items/') for i in res: diff --git a/openhab/events.py b/openhab/events.py index cb3a377..5ccaa11 100644 --- a/openhab/events.py +++ b/openhab/events.py @@ -39,6 +39,7 @@ EventSourceInternal: EventSource = EventSource("Internal") EventSourceOpenhab: EventSource = EventSource("Openhab") + @dataclass class RawItemEvent(object): @@ -48,8 +49,6 @@ class RawItemEvent(object): content: typing.Dict - - @dataclass class ItemEvent(object): """The base class for all ItemEvents""" @@ -60,7 +59,8 @@ class ItemEvent(object): value: typing.Any value_raw: typing.Any unit_of_measure: str - is_my_own_echo:bool + is_my_own_echo: bool + is_non_value_command: bool # these are events that will not be rpresented as state or statechanged values. E.g. Rollershutters UP or Players NEXT. @dataclass @@ -69,15 +69,12 @@ class ItemStateEvent(ItemEvent): type = ItemStateEventType - @dataclass class ItemCommandEvent(ItemEvent): """a Event representing a command event on a Item""" type = ItemCommandEventType - - @dataclass class ItemStateChangedEvent(ItemStateEvent): """a Event representing a state change event on a Item""" diff --git a/openhab/items.py b/openhab/items.py index 3f8b9a5..5d0e8d4 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -60,7 +60,7 @@ def create_or_update_item(self, group_type: typing.Optional[str] = None, function_name: typing.Optional[str] = None, function_params: typing.Optional[typing.List[str]] = None - ) -> typing.Type[Item]: + ) -> Item: """creates a new item in openhab if there is no item with name 'name' yet. if there is an item with 'name' already in openhab, the item gets updated with the infos provided. be aware that not provided fields will be deleted in openhab. consider to get the existing item via 'getItem' and then read out existing fields to populate the parameters here. @@ -180,14 +180,20 @@ def create_or_update_item_async(self, logging.getLogger().debug("about to create item with PUT request:{}".format(json_body)) self.openHABClient.req_json_put('/items/{}'.format(name), json_data=json_body) - def get_item(self, itemname) -> typing.Type[Item]: + def get_item(self, itemname) -> Item: + """get a existing openhab item + Args: + itemname (str): unique name of the item + Returns: + Item: the Item + """ return self.openHABClient.get_item(itemname) class Item: """Base item class.""" - types = [] # data_type: typing.List[typing.Type[openhab.types.CommandType]] + types = [] # type: typing.List[typing.Type[openhab.types.CommandType]] state_types = [] command_event_types = [] state_event_types = [] @@ -216,19 +222,16 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto self._unitOfMeasure = "" self.group = False self.name = '' - self._state = None # data_type: typing.Optional[typing.Any] - self._raw_state = None # data_type: typing.Optional[typing.Any] # raw state as returned by the server - self._raw_state_event = None # data_type: str # raw state as received from Serverevent - self._members = {} # data_type: typing.Dict[str, typing.Any] # group members (key = item name), for none-group items it's empty + self._state = None # type: typing.Optional[typing.Any] + self._raw_state = None # type: typing.Optional[typing.Any] # raw state as returned by the server + self._raw_state_event = None # type: str # raw state as received from Serverevent + self._members = {} # type: typing.Dict[str, typing.Any] # group members (key = item name), for none-group items it's empty self.logger = logging.getLogger(__name__) self.init_from_json(json_data) self.last_command_sent_time = datetime.fromtimestamp(0) self.last_command_sent = "" - - - self.openhab.register_item(self) self.event_listeners: typing.Dict[typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], Item.EventListener] = {} @@ -244,8 +247,6 @@ def init_from_json(self, json_data: dict) -> None: self.group = True if 'groupType' in json_data: self.type_ = json_data['groupType'] - - # init members for i in json_data['members']: self.members[i['name']] = self.openhab.json_to_item(i) @@ -268,7 +269,6 @@ def init_from_json(self, json_data: dict) -> None: if "groupNames" in json_data: self.groupNames = json_data['groupNames'] - #self.__set_state(json_data['state']) self._raw_state = json_data['state'] if self.is_undefined(self._raw_state): @@ -276,15 +276,15 @@ def init_from_json(self, json_data: dict) -> None: else: self._state, self._unitOfMeasure = self._parse_rest(self._raw_state) - - - @property def state(self, fetch_from_openhab=False) -> typing.Any: """The state property represents the current state of the item. + Args: + fetch_from_openhab (bool) : override auto_update setting and refresh the state now. - The state is automatically refreshed from openHAB on reading it. - Updating the value via this property send an update to the event bus. + The state is automatically refreshed from openHAB through incoming events if auto_update is turned on. + If auto_update is not turned on, the state gets refreshed now. + Updating the value via this property sends an update to the event bus. """ if not self.autoUpdate or fetch_from_openhab: json_data = self.openhab.get_item_raw(self.name) @@ -329,16 +329,13 @@ def _validate_value(self, value: typing.Union[str, typing.Type[openhab.types.Com else: raise ValueError() - - - def _parse_rest(self, value: str) -> typing.Tuple[str, str]: """Parse a REST result into a native object.""" return value, "" def _rest_format(self, value: str) -> typing.Union[str, bytes]: """Format a value before submitting to openHAB.""" - _value = value # data_type: typing.Union[str, bytes] + _value = value # type: typing.Union[str, bytes] # Only latin-1 encoding is supported by default. If non-latin-1 characters were provided, convert them to bytes. try: @@ -348,24 +345,41 @@ def _rest_format(self, value: str) -> typing.Union[str, bytes]: return _value - def _is_my_own_echo(self, event:openhab.events.ItemEvent): - """find out if the incoming event is actually just a echo of my previous command or change""" + def _is_my_own_echo(self, event: openhab.events.ItemEvent): + """find out if the incoming event is actually just a echo of my previous command or update""" now = datetime.now() - self.logger.debug("_isMyOwnChange:event.source:{}, event.data_type{}, self._state:{}, event.new_value:{},self.last_command_sent_time:{}, now:{}".format( - event.source, event.type, self._state, event.value, self.last_command_sent_time, now)) - if event.source != openhab.events.EventSourceOpenhab: - return True - if self.last_command_sent_time + timedelta(milliseconds=self.openhab.maxEchoToOpenhabMS) > now: - if event.type == openhab.events.ItemCommandEventType: - if self.last_command_sent == event.value: - # this is the echo of the command we just sent to openHAB. - return True - elif event.type in [openhab.events.ItemStateChangedEventType, openhab.events.ItemStateEventType]: - if self._state == event.value: - # this is the echo of the command we just sent to openHAB. - return True - return False - + result = None + try: + if event.source != openhab.events.EventSourceOpenhab: + result = True + return result + if self.last_command_sent_time + timedelta(milliseconds=self.openhab.maxEchoToOpenhabMS) > now: + if event.type == openhab.events.ItemCommandEventType: + if self.last_command_sent == event.value: + # this is the echo of the command we just sent to openHAB. + result = True + return result + else: + self.logger.debug("it is not an echo. last command sent:{}, eventvalue:{}".format(self.last_command_sent, event.value)) + elif event.type in [openhab.events.ItemStateChangedEventType, openhab.events.ItemStateEventType]: + if self._state == event.value: + # this is the echo of the command we just sent to openHAB. + result = True + return result + else: + self.logger.debug("it is not an echo. previous state:{}, eventvalue:{}".format(self._state, event.value)) + result = False + return result + finally: + self.logger.debug("checking if it is my own echo result:{result} for item:{itemname}, event.source:{source}, event.data_type{datatype}, self._state:{state}, event.new_value:{value}, self.last_command_sent_time:{last_command_sent_time}, now:{now}".format( + result=result, + itemname=event.item_name, + source=event.source, + datatype=event.type, + state=self._state, + value=event.value, + last_command_sent_time=self.last_command_sent_time, + now=now)) def delete(self): """deletes the item from openhab """ @@ -373,19 +387,45 @@ def delete(self): self._state = None self.remove_all_event_listeners() + def __extract_value_and_unitofmeasure(self, value: str) -> typing.Tuple[str, str]: + """Private method to extract value and unit of measue + + Args: + value (str): the parsed value - def get_value_unitofmeasure(self, parsed_value:str): - if isinstance(parsed_value,tuple): - value_result = parsed_value[0] - uom = parsed_value[1] - return value_result,uom + Returns: + tuple[str,str] : 2 strings containing the value and the unit of measure + """ + if isinstance(value, tuple): + value_result = value[0] + uom = value[1] + return value_result, uom else: - return parsed_value, "" + return value, "" - def digest_external_command_event(self, command_type_class:typing.Type[openhab.types.CommandType], command:str)->openhab.events.ItemCommandEvent: + def _digest_external_command_event(self, command_type_class: typing.Type[openhab.types.CommandType], command: str) -> openhab.events.ItemCommandEvent: + """Private method to process a command event coming from openhab + + Args: + command_type_class (openhab.types.CommandType): the fitting CommandType to correctly process the command + command (str): the received command + + Returns: + openhab.events.ItemCommandEvent : the populated event + """ parsed_value = command_type_class.parse(command) - value_result,uom = self.get_value_unitofmeasure(parsed_value) - item_command_event = openhab.events.ItemCommandEvent(item_name=self.name, source=openhab.events.EventSourceOpenhab, value_datatype=command_type_class, value=value_result, unit_of_measure=uom, value_raw=command, is_my_own_echo=False) + value_result, uom = self.__extract_value_and_unitofmeasure(parsed_value) + is_non_value_command = False + if command_type_class not in self.state_changed_event_types + self.state_event_types: + is_non_value_command = True + item_command_event = openhab.events.ItemCommandEvent(item_name=self.name, + source=openhab.events.EventSourceOpenhab, + value_datatype=command_type_class, + value=value_result, + unit_of_measure=uom, + value_raw=command, + is_my_own_echo=False, + is_non_value_command=is_non_value_command) item_command_event.is_my_own_echo = self._is_my_own_echo(item_command_event) if command_type_class in self.state_types: if not item_command_event.is_my_own_echo: @@ -393,11 +433,30 @@ def digest_external_command_event(self, command_type_class:typing.Type[openhab.t self._unitOfMeasure = uom return item_command_event + def digest_external_state_event(self, state_type_class: typing.Type[openhab.types.CommandType], value: str) -> openhab.events.ItemStateEvent: + """Private method to process a state event coming from openhab - def digest_external_state_event(self, state_type_class:typing.Type[openhab.types.CommandType], value:str)->openhab.events.ItemStateEvent: + Args: + state_type_class (openhab.types.CommandType): the fitting CommandType to correctly process the value + value (str): the received new state + + Returns: + openhab.events.ItemStateEvent : the populated event + """ parsed_value = state_type_class.parse(value) - value_result,uom = self.get_value_unitofmeasure(parsed_value) - item_state_event = openhab.events.ItemStateEvent(item_name=self.name, source=openhab.events.EventSourceOpenhab, value_datatype=state_type_class, value=value_result, unit_of_measure=uom, value_raw=value, is_my_own_echo=False) + value_result, uom = self.__extract_value_and_unitofmeasure(parsed_value) + is_non_value_command = False + if state_type_class not in self.state_changed_event_types + self.state_event_types: + is_non_value_command = True + item_state_event = openhab.events.ItemStateEvent(item_name=self.name, + source=openhab.events.EventSourceOpenhab, + value_datatype=state_type_class, + value=value_result, + unit_of_measure=uom, + value_raw=value, + is_my_own_echo=False, + is_non_value_command=is_non_value_command) + item_state_event.is_my_own_echo = self._is_my_own_echo(item_state_event) if item_state_event in self.state_types: if not item_state_event.is_my_own_echo: @@ -405,21 +464,28 @@ def digest_external_state_event(self, state_type_class:typing.Type[openhab.types self._unitOfMeasure = uom return item_state_event + def digest_external_state_changed_event(self, state_type_class: typing.Type[openhab.types.CommandType], value: str, old_state_type_class: typing.Type[openhab.types.CommandType], old_value: str) -> openhab.events.ItemStateChangedEvent: + """Private method to process a state changed event coming from openhab + Args: + state_type_class (openhab.types.CommandType): the fitting CommandType to correctly process the value + value (str): the new state + old_state_type_class (openhab.types.CommandType): the fitting CommandType to correctly process the old_value + old_value (str): the old state - def digest_external_state_changed_event(self, state_type_class:typing.Type[openhab.types.CommandType], value:str, old_state_type_class:typing.Type[openhab.types.CommandType], old_value:str)->openhab.events.ItemStateChangedEvent: + Returns: + openhab.events.ItemStateChangedEvent : the populated event + """ parsed_value = state_type_class.parse(value) - value_result,uom = self.get_value_unitofmeasure(parsed_value) - - parsed_old_value=old_value + value_result, uom = self.__extract_value_and_unitofmeasure(parsed_value) + old_value_result = old_uom = "" if old_state_type_class is not None: parsed_old_value = old_state_type_class.parse(old_value) - old_value_result, old_uom = self.get_value_unitofmeasure(parsed_old_value) - - - - + old_value_result, old_uom = self.__extract_value_and_unitofmeasure(parsed_old_value) + is_non_value_command = False + if state_type_class not in self.state_changed_event_types + self.state_event_types: + is_non_value_command = True item_state_changed_event = openhab.events.ItemStateChangedEvent(item_name=self.name, source=openhab.events.EventSourceOpenhab, value_datatype=state_type_class, @@ -430,68 +496,81 @@ def digest_external_state_changed_event(self, state_type_class:typing.Type[openh old_value=old_value_result, old_unit_of_measure=old_uom, old_value_raw=old_value, - is_my_own_echo=False) + is_my_own_echo=False, + is_non_value_command=is_non_value_command) item_state_changed_event.is_my_own_echo = self._is_my_own_echo(item_state_changed_event) if item_state_changed_event in self.state_types: if not item_state_changed_event.is_my_own_echo: - self._state=value_result + self._state = value_result return item_state_changed_event - - def _parse_external_command_event(self, raw_Event: openhab.events.RawItemEvent) -> openhab.events.ItemCommandEvent: - command_type = raw_Event.content["type"] - command_type_class = openhab.types.CommandType.getTypeFor(command_type) - # if self.typeclass is None: - # self.typeclass = command_type_class - command=raw_Event.content["value"] + def _parse_external_command_event(self, raw_event: openhab.events.RawItemEvent) -> openhab.events.ItemCommandEvent: + """Private method to process a command event coming from openhab + Args: + raw_event (openhab.events.RawItemEvent): the raw event + Returns: + openhab.events.ItemCommandEvent : the populated event + """ + command_type = raw_event.content["type"] + command_type_class = openhab.types.CommandType.get_type_for(command_type) + command = raw_event.content["value"] if command_type_class in self.command_event_types: - item_command_event=self.digest_external_command_event(command_type_class,command) + item_command_event = self._digest_external_command_event(command_type_class, command) return item_command_event raise Exception("unknown command event type") - def _parse_external_state_event(self, raw_Event: openhab.events.RawItemEvent) -> openhab.events.ItemStateEvent: - state_type = raw_Event.content["type"] - state_type_class = openhab.types.CommandType.getTypeFor(state_type) - # if self.typeclass is None: - # self.typeclass = state_type_class - - value = raw_Event.content["value"] + def _parse_external_state_event(self, raw_event: openhab.events.RawItemEvent) -> openhab.events.ItemStateEvent: + """Private method to process a state event coming from openhab + Args: + raw_event (openhab.events.RawItemEvent): the raw event + Returns: + openhab.events.ItemStateEvent : the populated event + """ + state_type = raw_event.content["type"] + state_type_class = openhab.types.CommandType.get_type_for(state_type) + value = raw_event.content["value"] if state_type_class in self.state_event_types: item_state_event = self.digest_external_state_event(state_type_class, value) return item_state_event raise Exception("unknown state event type") - def _parse_external_state_changed_event(self, raw_Event: openhab.events.RawItemEvent) -> openhab.events.ItemStateEvent: - state_changed_type = raw_Event.content["type"] - state_changed_type_class = openhab.types.CommandType.getTypeFor(state_changed_type) - state_changed_old_type = raw_Event.content["oldType"] - state_changed_old_type_class = openhab.types.CommandType.getTypeFor(state_changed_old_type) - # if self.typeclass is None: - # self.typeclass = state_changed_type_class - value = raw_Event.content["value"] - old_value = raw_Event.content["oldValue"] + def _parse_external_state_changed_event(self, raw_event: openhab.events.RawItemEvent) -> openhab.events.ItemStateEvent: + """Private method to process a state changed event coming from openhab + Args: + raw_event (openhab.events.RawItemEvent): the raw event + Returns: + openhab.events.ItemStateEvent : the populated event + """ + state_changed_type = raw_event.content["type"] + state_changed_type_class = openhab.types.CommandType.get_type_for(state_changed_type) + state_changed_old_type = raw_event.content["oldType"] + state_changed_old_type_class = openhab.types.CommandType.get_type_for(state_changed_old_type) + value = raw_event.content["value"] + old_value = raw_event.content["oldValue"] if state_changed_type_class in self.state_changed_event_types: item_state_changed_event = self.digest_external_state_changed_event(state_type_class=state_changed_type_class, value=value, old_state_type_class=state_changed_old_type_class, old_value=old_value) return item_state_changed_event raise Exception("unknown statechanged event type:{}".format(state_changed_type_class)) + def _process_external_event(self, raw_event: openhab.events.RawItemEvent): + """Private method to process a event coming from openhab and inform all Listeners about the event + Args: + raw_event (openhab.events.RawItemEvent): the raw event - - - - def process_external_event(self, raw_Event: openhab.events.RawItemEvent): + """ if not self.autoUpdate: return self.logger.info("processing external event") - if raw_Event.event_type == openhab.events.ItemCommandEvent.type: - event=self._parse_external_command_event(raw_Event) - elif raw_Event.event_type == openhab.events.ItemStateChangedEvent.type: - event=self._parse_external_state_changed_event(raw_Event) - elif raw_Event.event_type == openhab.events.ItemStateEvent.type: - event=self._parse_external_state_event(raw_Event) - + if raw_event.event_type == openhab.events.ItemCommandEvent.type: + event = self._parse_external_command_event(raw_event) + elif raw_event.event_type == openhab.events.ItemStateChangedEvent.type: + event = self._parse_external_state_changed_event(raw_event) + elif raw_event.event_type == openhab.events.ItemStateEvent.type: + event = self._parse_external_state_event(raw_event) + else: + raise NotImplementedError("Event type:{} is not implemented.".format(raw_event.event_type)) for aListener in self.event_listeners.values(): if event.type in aListener.listeningTypes: @@ -499,9 +578,14 @@ def process_external_event(self, raw_Event: openhab.events.RawItemEvent): try: aListener.callbackfunction(self, event) except Exception as e: - self.logger.exception("error executing Eventlistener for item:{}.".format(event.item_name)) + self.logger.exception("error executing Eventlistener for item:{}.".format(event.item_name), e) def _process_internal_event(self, event: openhab.events.ItemEvent): + """Private method to process a event coming from local changes and inform all Listeners about the event + Args: + event (openhab.events.ItemEvent): the internal event + + """ self.logger.info("processing internal event") for aListener in self.event_listeners.values(): if event.type in aListener.listeningTypes: @@ -511,8 +595,7 @@ def _process_internal_event(self, event: openhab.events.ItemEvent): try: aListener.callbackfunction(self, event) except Exception as e: - self.logger.exception("error executing Eventlistener for item:{}.".format(event.item_name)) - + self.logger.exception("error executing Eventlistener for item:{}.".format(event.item_name), e) class EventListener(object): """EventListener Objects hold data about a registered event listener""" @@ -539,7 +622,7 @@ def __init__(self, listening_types: typing.Set[openhab.events.EventType], listen self.onlyIfEventsourceIsOpenhab = only_if_eventsource_is_openhab self.alsoGetMyEchosFromOpenHAB = also_get_my_echos_from_openhab - def add_types(self, listening_types: typing.Set[openhab.events.EventType]): + def add_types(self, listening_types: typing.Union[openhab.events.EventType, typing.Set[openhab.events.EventType]]): """add aditional listening types Args: listening_types (openhab.events.EventType or set of openhab.events.EventType): the additional eventTypes the listener is interested in. @@ -554,7 +637,7 @@ def add_types(self, listening_types: typing.Set[openhab.events.EventType]): else: self.listeningTypes.update(listening_types) - def remove_types(self, listening_types: typing.Set[openhab.events.EventType]): + def remove_types(self, listening_types: typing.Union[openhab.events.EventType, typing.Set[openhab.events.EventType]]): """remove listening types Args: listening_types (openhab.events.EventType or set of openhab.events.EventType): the eventTypes the listener is not interested in anymore @@ -569,7 +652,7 @@ def remove_types(self, listening_types: typing.Set[openhab.events.EventType]): else: self.listeningTypes.difference_update(listening_types) - def add_event_listener(self, listening_types: typing.Set[openhab.events.EventType], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], + def add_event_listener(self, listening_types: typing.Union[openhab.events.EventType, typing.Set[openhab.events.EventType]], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], only_if_eventsource_is_openhab=True, also_get_my_echos_from_openhab=False): """add a Listener interested in changes of items happening in openhab @@ -596,7 +679,7 @@ def add_event_listener(self, listening_types: typing.Set[openhab.events.EventTyp def remove_all_event_listeners(self): self.event_listeners = {} - def remove_event_listener(self, types: typing.Set[openhab.events.EventType], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None]): + def remove_event_listener(self, types: typing.Union[openhab.events.EventType, typing.Set[openhab.events.EventType]], listener: typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None]): """removes a previously registered Listener interested in changes of items happening in openhab Args: Args: @@ -611,13 +694,12 @@ def remove_event_listener(self, types: typing.Set[openhab.events.EventType], lis if not event_listener.listeningTypes: self.event_listeners.pop(listener) - def is_undefined(self, value:str) -> bool: + def is_undefined(self, value: str) -> bool: for aStateType in self.state_types: - if not aStateType.is_undefined(self._raw_state): + if not aStateType.is_undefined(value): return False return True - def __set_state(self, value: str) -> None: """Private method for setting the internal state.""" if self.is_undefined(value): @@ -625,7 +707,6 @@ def __set_state(self, value: str) -> None: else: self._state = value - def __str__(self) -> str: """String representation.""" return '<{0} - {1} : {2}>'.format(self.type_, self.name, self._state) @@ -639,7 +720,7 @@ def _update(self, value: typing.Any) -> None: """ # noinspection PyTypeChecker self.last_command_sent_time = datetime.now() - self.last_command_sent=value + self.last_command_sent = value self.openhab.req_put('/items/{}/state'.format(self.name), data=value) def update(self, value: typing.Any) -> None: @@ -663,11 +744,10 @@ def update(self, value: typing.Any) -> None: value=self._state, value_raw=None, unit_of_measure=self._unitOfMeasure, - is_my_own_echo=False + is_my_own_echo=False, + is_non_value_command=False ) else: - - event = openhab.events.ItemStateChangedEvent(item_name=self.name, source=openhab.events.EventSourceInternal, value_datatype=self.type_, @@ -678,7 +758,8 @@ def update(self, value: typing.Any) -> None: old_value=oldstate, old_value_raw="", old_unit_of_measure="", - is_my_own_echo=False + is_my_own_echo=False, + is_non_value_command=False ) self._process_internal_event(event) @@ -692,9 +773,6 @@ def command(self, value: typing.Any) -> None: """ self._validate_value(value) - - - v = self._rest_format(value) self._state = value self.last_command_sent_time = datetime.now() @@ -709,7 +787,8 @@ def command(self, value: typing.Any) -> None: value=value, value_raw=None, unit_of_measure=unit_of_measure, - is_my_own_echo=True + is_my_own_echo=True, + is_non_value_command=False ) self._process_internal_event(event) @@ -859,6 +938,7 @@ def rewind(self) -> None: """send the command REWIND.""" self.command(openhab.types.RewindFastforward.REWIND) + class SwitchItem(Item): """SwitchItem item data_type.""" types = [openhab.types.OnOffType] @@ -884,8 +964,6 @@ def toggle(self) -> None: self.on() - - class NumberItem(Item): """NumberItem item data_type.""" @@ -896,8 +974,7 @@ class NumberItem(Item): state_changed_event_types = types TYPENAME = "Number" - - def _parse_rest(self, value: str) -> typing.Tuple[float, str]: + def _parse_rest(self, value: str) -> typing.Tuple[typing.Union[float, None], str]: """Parse a REST result into a native object. Args: @@ -1025,8 +1102,7 @@ class ColorItem(DimmerItem): state_changed_event_types = [openhab.types.ColorType] TYPENAME = "Color" - - def _parse_rest(self, value: str) -> typing.Tuple[str, str]: + def _parse_rest(self, value: str) -> typing.Tuple[typing.Tuple[int, int, float], str]: """Parse a REST result into a native object. Args: @@ -1047,16 +1123,16 @@ def _rest_format(self, value: typing.Union[str, int]) -> str: Returns: str: The string as possibly converted from the parameter. """ - if isinstance(value,tuple): + if isinstance(value, tuple): if len(value) == 3: - return "{},{},{}".format(value[0],value[1],value[2]) + return "{},{},{}".format(value[0], value[1], value[2]) if not isinstance(value, str): return str(value) return value - def get_value_unitofmeasure(self, parsed_value:str): - return parsed_value, "" + def __extract_value_and_unitofmeasure(self, value: str): + return value, "" class RollershutterItem(Item): @@ -1067,9 +1143,6 @@ class RollershutterItem(Item): command_event_types = [openhab.types.UpDownType, openhab.types.StopMoveType, openhab.types.PercentType] state_event_types = [openhab.types.UpDownType, openhab.types.PercentType] state_changed_event_types = [openhab.types.PercentType] - - - TYPENAME = "Rollershutter" def _parse_rest(self, value: str) -> typing.Tuple[int, str]: diff --git a/openhab/types.py b/openhab/types.py index cd3e072..dade788 100644 --- a/openhab/types.py +++ b/openhab/types.py @@ -34,25 +34,26 @@ class CommandType(metaclass=abc.ABCMeta): """Base command data_type class.""" TYPENAME = "" + SUPPORTED_TYPENAMES = [] UNDEF = 'UNDEF' NULL = 'NULL' - UNDEFINED_STATES = [UNDEF,NULL] + UNDEFINED_STATES = [UNDEF, NULL] @classmethod - def is_undefined(cls, value: typing.Any) -> None: + def is_undefined(cls, value: typing.Any) -> bool: return value in CommandType.UNDEFINED_STATES - @classmethod - def getTypeFor(cls, typename:str, parent_cls:typing.Optional[typing.Type[CommandType]]=None)->typing.Type[CommandType]: - if parent_cls is None: parent_cls=CommandType + def get_type_for(cls, typename: str, parent_cls: typing.Optional[typing.Type[CommandType]] = None) -> typing.Union[typing.Type[CommandType], None]: + if parent_cls is None: + parent_cls = CommandType for a_type in parent_cls.__subclasses__(): if typename in a_type.SUPPORTED_TYPENAMES: return a_type else: - #mayba a subclass of a subclass - result=a_type.getTypeFor(typename,a_type) + # mayba a subclass of a subclass + result = a_type.get_type_for(typename, a_type) if result is not None: return result return None @@ -78,13 +79,13 @@ def validate(cls, value: typing.Any) -> None: """ raise NotImplementedError() + class UndefType(CommandType): TYPENAME = "UnDef" SUPPORTED_TYPENAMES = [TYPENAME] - @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in UndefType.UNDEFINED_STATES: return None return value @@ -99,7 +100,7 @@ class GroupType(CommandType): SUPPORTED_TYPENAMES = [TYPENAME] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in GroupType.UNDEFINED_STATES: return None return value @@ -116,7 +117,7 @@ class StringType(CommandType): SUPPORTED_TYPENAMES = [TYPENAME] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in StringType.UNDEFINED_STATES: return None if not isinstance(value, str): @@ -147,14 +148,13 @@ class OnOffType(StringType): POSSIBLE_VALUES = [ON, OFF] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in OnOffType.UNDEFINED_STATES: return None if value not in OnOffType.POSSIBLE_VALUES: raise ValueError() return value - @classmethod def validate(cls, value: str) -> None: """Value validation method. @@ -171,26 +171,22 @@ def validate(cls, value: str) -> None: OnOffType.parse(value) - - class OpenCloseType(StringType): """OpenCloseType data_type class.""" TYPENAME = "OpenClosed" SUPPORTED_TYPENAMES = [TYPENAME] OPEN = "OPEN" CLOSED = "CLOSED" - POSSIBLE_VALUES = [OPEN,CLOSED] - + POSSIBLE_VALUES = [OPEN, CLOSED] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in OpenCloseType.UNDEFINED_STATES: return None if value not in OpenCloseType.POSSIBLE_VALUES: raise ValueError() return value - @classmethod def validate(cls, value: str) -> None: """Value validation method. @@ -207,27 +203,25 @@ def validate(cls, value: str) -> None: OpenCloseType.parse(value) - - class ColorType(StringType): """ColorType data_type class.""" TYPENAME = "HSB" SUPPORTED_TYPENAMES = [TYPENAME] @classmethod - def parse(cls, value:str) -> typing.Tuple[int,int,float]: + def parse(cls, value: str) -> typing.Union[typing.Tuple[int, int, float], None]: if value in ColorType.UNDEFINED_STATES: return None hs, ss, bs = re.split(',', value) - h=int(hs) - s=int(ss) - b=float(bs) + h = int(hs) + s = int(ss) + b = float(bs) if not ((0 <= h <= 360) and (0 <= s <= 100) and (0 <= b <= 100)): raise ValueError() return h, s, b @classmethod - def validate(cls, value: typing.Union[str, typing.Tuple[int,int,float]]) -> None: + def validate(cls, value: typing.Union[str, typing.Tuple[int, int, float]]) -> None: """Value validation method. Valid values are in format H,S,B. @@ -242,26 +236,24 @@ def validate(cls, value: typing.Union[str, typing.Tuple[int,int,float]]) -> None Raises: ValueError: Raises ValueError if an invalid value has been specified. """ - if isinstance(value,tuple): + strvalue = str(value) + if isinstance(value, tuple): if len(value) == 3: - strvalue = "{},{},{}".format(value[0],value[1],value[2]) + strvalue = "{},{},{}".format(value[0], value[1], value[2]) super().validate(strvalue) ColorType.parse(strvalue) - else: - strvalue = str(value) super().validate(strvalue) ColorType.parse(strvalue) - class DecimalType(CommandType): """DecimalType data_type class.""" TYPENAME = "Decimal" - SUPPORTED_TYPENAMES = [TYPENAME,"Quantity"] + SUPPORTED_TYPENAMES = [TYPENAME, "Quantity"] @classmethod - def parse(cls, value: str) -> typing.Tuple[typing.Union[int,float],str]: + def parse(cls, value: str) -> typing.Union[None, typing.Tuple[typing.Union[int, float], str]]: if value in DecimalType.UNDEFINED_STATES: return None @@ -279,7 +271,6 @@ def parse(cls, value: str) -> typing.Tuple[typing.Union[int,float],str]: return return_value, value_unit_of_measure raise ValueError() - @classmethod def validate(cls, value: typing.Union[float, int]) -> None: """Value validation method. @@ -296,16 +287,13 @@ def validate(cls, value: typing.Union[float, int]) -> None: raise ValueError() - - - class PercentType(DecimalType): """PercentType data_type class.""" TYPENAME = "Percent" SUPPORTED_TYPENAMES = [TYPENAME] @classmethod - def parse(cls, value: str) -> float: + def parse(cls, value: str) -> typing.Union[float, None]: if value in PercentType.UNDEFINED_STATES: return None try: @@ -343,10 +331,10 @@ class IncreaseDecreaseType(StringType): INCREASE = "INCREASE" DECREASE = "DECREASE" - POSSIBLE_VALUES = [INCREASE,DECREASE] + POSSIBLE_VALUES = [INCREASE, DECREASE] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in IncreaseDecreaseType.UNDEFINED_STATES: return None if value not in IncreaseDecreaseType.POSSIBLE_VALUES: @@ -375,7 +363,7 @@ class DateTimeType(CommandType): SUPPORTED_TYPENAMES = [TYPENAME] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[datetime, None]: if value in DateTimeType.UNDEFINED_STATES: return None return dateutil.parser.parse(value) @@ -403,10 +391,10 @@ class UpDownType(StringType): SUPPORTED_TYPENAMES = [TYPENAME] UP = "UP" DOWN = "DOWN" - POSSIBLE_VALUES = [UP,DOWN] + POSSIBLE_VALUES = [UP, DOWN] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in UpDownType.UNDEFINED_STATES: return None if value not in UpDownType.POSSIBLE_VALUES: @@ -439,7 +427,7 @@ class StopMoveType(StringType): POSSIBLE_VALUES = [STOP] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in StopMoveType.UNDEFINED_STATES: return None if value not in StopMoveType.POSSIBLE_VALUES: @@ -462,6 +450,7 @@ def validate(cls, value: str) -> None: StopMoveType.parse(value) + class PlayPauseType(StringType): """PlayPauseType data_type class.""" @@ -469,10 +458,10 @@ class PlayPauseType(StringType): SUPPORTED_TYPENAMES = [TYPENAME] PLAY = "PLAY" PAUSE = "PAUSE" - POSSIBLE_VALUES = [PLAY,PAUSE] + POSSIBLE_VALUES = [PLAY, PAUSE] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in PlayPauseType.UNDEFINED_STATES: return None if value not in PlayPauseType.POSSIBLE_VALUES: @@ -495,6 +484,7 @@ def validate(cls, value: str) -> None: PlayPauseType.parse(value) + class NextPrevious(StringType): """NextPrevious data_type class.""" @@ -505,14 +495,13 @@ class NextPrevious(StringType): POSSIBLE_VALUES = [NEXT, PREVIOUS] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in NextPrevious.UNDEFINED_STATES: return None if value not in NextPrevious.POSSIBLE_VALUES: raise ValueError() return value - @classmethod def validate(cls, value: str) -> None: """Value validation method. @@ -529,6 +518,7 @@ def validate(cls, value: str) -> None: NextPrevious.parse(value) + class RewindFastforward(StringType): """RewindFastforward data_type class.""" @@ -539,14 +529,13 @@ class RewindFastforward(StringType): POSSIBLE_VALUES = [REWIND, FASTFORWARD] @classmethod - def parse(cls, value: str) -> str: + def parse(cls, value: str) -> typing.Union[str, None]: if value in RewindFastforward.UNDEFINED_STATES: return None if value not in RewindFastforward.POSSIBLE_VALUES: raise ValueError() return value - @classmethod def validate(cls, value: str) -> None: """Value validation method. @@ -561,4 +550,4 @@ def validate(cls, value: str) -> None: """ super().validate(value) - RewindFastforward.parse(value) \ No newline at end of file + RewindFastforward.parse(value) diff --git a/test.py b/test.py index 5c80710..a8ed34c 100644 --- a/test.py +++ b/test.py @@ -21,18 +21,19 @@ import datetime import openhab +import openhab.items -#base_url = 'http://localhost:8080/rest' -base_url = 'http://10.10.20.81:8080/rest' -openhab = openhab.OpenHAB(base_url) +base_url = 'http://localhost:8080/rest' + +oh = openhab.OpenHAB(base_url) # fetch all items -items = openhab.fetch_all_items() +items = oh.fetch_all_items() # fetch other items, show how to toggle a switch sunset = items.get('Sunset') sunrise = items.get('Sunrise') -knx_day_night = items.get('KNX_day_night') +knx_day_night: openhab.items.SwitchItem = items.get('KNX_day_night') now = datetime.datetime.now(datetime.timezone.utc) diff --git a/tests/test_create_delete_items.py b/tests/test_create_delete_items.py index c17fce0..0413c55 100644 --- a/tests/test_create_delete_items.py +++ b/tests/test_create_delete_items.py @@ -21,14 +21,11 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable - - import openhab import openhab.events -import time import openhab.items as items +import openhab.types import logging -import json import random import tests.testutil as testutil from datetime import datetime @@ -41,7 +38,7 @@ log.info("iii") log.debug("ddddd") -base_url = 'http://10.10.20.81:8080/rest' +base_url = 'http://localhost:8080/rest' def test_create_and_delete_items(myopenhab: openhab.OpenHAB, nameprefix): @@ -196,9 +193,6 @@ def test_StringItem(item_factory, nameprefix): testutil.doassert("test string value 1", x2.state, "itemstate") x2.state = "test string value 2" testutil.doassert("test string value 2", x2.state, "itemstate") - - - return x2 @@ -262,11 +256,11 @@ def test_ColorItem(item_factory, nameprefix): x2.on() testutil.doassert(itemname, x2.name, "item_name") testutil.doassert("ON", x2.state, "itemstate") - new_value = 51,52,53 + new_value = 51, 52, 53 x2.state = new_value log.info("itemsate:{}".format(x2.state)) - testutil.doassert((51,52,53), x2.state, "itemstate") + testutil.doassert((51, 52, 53), x2.state, "itemstate") return x2 @@ -300,6 +294,7 @@ def test_SwitchItem(item_factory, nameprefix): x2: openhab.items.SwitchItem = item_factory.create_or_update_item(name=itemname, data_type=itemtype) x2.on() + testutil.doassert(itemname, x2.name, "item_name") testutil.doassert("ON", x2.state, "itemstate") @@ -309,7 +304,7 @@ def test_SwitchItem(item_factory, nameprefix): x2.toggle() testutil.doassert("ON", x2.state, "itemstate") - new_value = "OFF" + new_value = openhab.types.OnOffType.OFF x2.state = new_value log.info("itemsate:{}".format(x2.state)) @@ -338,5 +333,3 @@ def test_PlayerItem(item_factory, nameprefix): mynameprefix = "x2_{}".format(random.randint(1, 1000)) test_create_and_delete_items(myopenhab, mynameprefix) -# delete_all_items_starting_with(openhab,"x2_") - diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index 34b2fc2..d2924a8 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -18,7 +18,7 @@ # along with python-openhab. If not, see . # # pylint: disable=bad-indentation -from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable + import openhab import openhab.events import openhab.types @@ -28,8 +28,7 @@ import random import testutil -from datetime import datetime,timezone -import pytz +from datetime import datetime log = logging.getLogger() logging.basicConfig(level=10, format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") @@ -39,74 +38,51 @@ log.info("infomessage") log.debug("debugmessage") -base_url = 'http://10.10.20.81:8080/rest' - -testdata: Dict[str, Tuple[str, str, str]] = {'OnOff': ('ItemCommandEvent', 'testroom1_LampOnOff', '{"type":"OnOff","value":"ON"}'), - 'Decimal': ('ItemCommandEvent', 'xx', '{"type":"Decimal","value":"170.0"}'), - 'DateTime': ('ItemCommandEvent', 'xx', '{"type":"DateTime","value":"2020-12-04T15:53:33.968+0100"}'), - 'UnDef': ('ItemCommandEvent', 'xx', '{"type":"UnDef","value":"UNDEF"}'), - 'String': ('ItemCommandEvent', 'xx', '{"type":"String","value":"WANING_GIBBOUS"}'), - 'Quantitykm': ('ItemCommandEvent', 'xx', '{"type":"Quantity","value":"389073.99674024084 km"}'), - 'Quantitykm grad': ('ItemCommandEvent', 'xx', '{"type":"Quantity","value":"233.32567712620255 °"}'), - 'Quantitywm2': ('ItemCommandEvent', 'xx', '{"type":"Quantity","value":"0.0 W/m²"}'), - 'Percent': ('ItemCommandEvent', 'xx', '{"type":"Percent","value":"52"}'), - 'UpDown': ('ItemCommandEvent', 'xx', '{"type":"UpDown","value":"DOWN"}'), - 'OnOffChange': ('ItemStateChangedEvent', 'xx', '{"type":"OnOff","value":"OFF","oldType":"OnOff","old_value_raw":"ON"}'), - 'DecimalChange': ('ItemStateChangedEvent', 'xx', '{"type":"Decimal","value":"170.0","oldType":"Decimal","old_value_raw":"186.0"}'), - 'QuantityChange': ('ItemStateChangedEvent', 'xx', '{"type":"Quantity","value":"389073.99674024084 km","oldType":"Quantity","old_value_raw":"389076.56223012594 km"}'), - 'QuantityGradChange': ('ItemStateChangedEvent', 'xx', '{"type":"Quantity","value":"233.32567712620255 °","oldType":"Quantity","old_value_raw":"233.1365666436372 °"}'), - 'DecimalChangeFromNull': ('ItemStateChangedEvent', 'xx', '{"type":"Decimal","value":"0.5","oldType":"UnDef","old_value_raw":"NULL"}'), - 'DecimalChangeFromNullToUNDEF': ('ItemStateChangedEvent', 'xx', '{"type":"Decimal","value":"15","oldType":"UnDef","old_value_raw":"NULL"}'), - 'PercentChange': ('ItemStateChangedEvent', 'xx', '{"type":"Percent","value":"52","oldType":"UnDef","old_value_raw":"NULL"}'), - 'Datatypechange': ('ItemStateChangedEvent', 'xx', '{"type":"OnOff","value":"ON","oldType":"UnDef","old_value_raw":"NULL"}') - } -testitems: Dict[str, openhab.items.Item] = {} - - - +base_url = 'http://localhost:8080/rest' expected_state = None -state_correct_count=0 +state_correct_count = 0 expected_command = None -command_correct_count=0 +command_correct_count = 0 expected_new_state = None expected_old_state = None -state_changed_correct_count=0 +state_changed_correct_count = 0 + +count = 0 + +do_breakpoint = False -do_breakpoint=False def on_item_state(item: openhab.items.Item, event: openhab.events.ItemStateEvent): - global state_correct_count + global state_correct_count log.info("########################### STATE arrived for {itemname} : eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state})".format( itemname=event.item_name, eventvalue=event.value, eventvalueraw=event.value_raw, item_state=item._state, itemstate=item.state)) if expected_state is not None: - if isinstance(event.value,datetime): + if isinstance(event.value, datetime): testutil.doassert(expected_state, event.value.replace(tzinfo=None), "stateEvent item {} ".format(item.name)) else: - testutil.doassert(expected_state,event.value,"stateEvent item {} ".format(item.name)) + testutil.doassert(expected_state, event.value, "stateEvent item {} ".format(item.name)) state_correct_count += 1 def on_item_statechange(item: openhab.items.Item, event: openhab.events.ItemStateChangedEvent): global state_changed_correct_count - log.info("########################### STATE of {itemname} CHANGED from {oldvalue} to {newvalue} (items state: {new_value_item}.".format(itemname=event.item_name,oldvalue=event.old_value,newvalue=event.value, new_value_item=item.state)) + log.info("########################### STATE of {itemname} CHANGED from {oldvalue} to {newvalue} (items state: {new_value_item}.".format(itemname=event.item_name, oldvalue=event.old_value, newvalue=event.value, new_value_item=item.state)) if expected_new_state is not None: if isinstance(event.value, datetime): testutil.doassert(expected_new_state, event.value.replace(tzinfo=None), "state changed event item {} value".format(item.name)) else: testutil.doassert(expected_new_state, event.value, "state changed event item {} new value".format(item.name)) - if expected_old_state is not None: if isinstance(event.old_value, datetime): testutil.doassert(expected_old_state, event.old_value.replace(tzinfo=None), "state changed event item {} old value".format(item.name)) else: testutil.doassert(expected_old_state, event.old_value, "OLD state changed event item {} old value".format(item.name)) - state_changed_correct_count +=1 - + state_changed_correct_count += 1 def on_item_command(item: openhab.items.Item, event: openhab.events.ItemCommandEvent): @@ -118,8 +94,7 @@ def on_item_command(item: openhab.items.Item, event: openhab.events.ItemCommandE testutil.doassert(expected_command, event.value.replace(tzinfo=None), "command event item {}".format(item.name)) else: testutil.doassert(expected_command, event.value, "command event item {}".format(item.name)) - command_correct_count +=1 - + command_correct_count += 1 myopenhab = openhab.OpenHAB(base_url, auto_update=True) @@ -128,36 +103,30 @@ def on_item_command(item: openhab.items.Item, event: openhab.events.ItemCommandE random.seed() namesuffix = "_{}".format(random.randint(1, 1000)) -test_azimuth=myItemfactory.get_item("testworld_Azimuth") -test_azimuth.add_event_listener(listening_types=openhab.events.ItemStateEventType, listener=on_item_state) -test_azimuth.add_event_listener(listening_types=openhab.events.ItemCommandEventType, listener=on_item_command) -test_azimuth.add_event_listener(listening_types=openhab.events.ItemStateChangedEventType, listener=on_item_statechange) - - - - - -def create_event_data(event_type:openhab.events.ItemEventType, itemname, payload): - result= {} - if event_type== openhab.events.ItemStateEventType: - event_type_topic_path="statechanged" - elif event_type== openhab.events.ItemStateChangedEventType: - event_type_topic_path="state" - elif event_type== openhab.events.ItemCommandEventType: - event_type_topic_path="command" - result = {"type": event_type, "topic": "smarthome/items/{itemname}/{event_type_topic_path}".format(itemname=itemname,event_type_topic_path=event_type_topic_path), "payload": payload} +def create_event_data(event_type: openhab.events.ItemEventType, itemname, payload): + if event_type == openhab.events.ItemStateEventType: + event_type_topic_path = "statechanged" + elif event_type == openhab.events.ItemStateChangedEventType: + event_type_topic_path = "state" + elif event_type == openhab.events.ItemCommandEventType: + event_type_topic_path = "command" + else: + raise NotImplementedError("Event type {} not implemented".format(event_type)) + result = {"type": event_type, "topic": "smarthome/items/{itemname}/{event_type_topic_path}".format(itemname=itemname, event_type_topic_path=event_type_topic_path), "payload": payload} return result -def create_event_payload(type:str,value:str,oldType:str=None,oldValue:str=None): - result='{"type":"'+type+'","value":"'+str(value)+'"' - if oldType is None: - oldType = type - if oldValue is not None: - result = result + ', "oldType":"'+oldType+'","oldValue":"'+str(oldValue)+'"' + +def create_event_payload(type: str, value: str, old_type: str = None, old_value: str = None): + result = '{"type":"' + type + '","value":"' + str(value) + '"' + if old_type is None: + old_type = type + if old_value is not None: + result = result + ', "oldType":"' + old_type + '","oldValue":"' + str(old_value) + '"' result = result + '}' return result + def test_number_item(): global expected_state global state_correct_count @@ -175,7 +144,6 @@ def test_number_item(): command_correct_count = 0 state_changed_correct_count = 0 - try: testitem: openhab.items.NumberItem = myItemfactory.create_or_update_item(name="test_eventSubscription_numberitem_A{}".format(namesuffix), data_type=openhab.items.NumberItem) testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=True) @@ -183,47 +151,46 @@ def test_number_item(): testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) expected_new_state = expected_state = sending_state = 170.3 - #eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, '{"type":"Decimal","value":"'+str(sending_state)+'"}') - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Decimal",str(sending_state))) - myopenhab._parse_event(eventData) - testutil.doassert(1,state_correct_count) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Decimal", str(sending_state))) + myopenhab._parse_event(event_data) + testutil.doassert(1, state_correct_count) sending_state = openhab.types.UndefType.UNDEF expected_new_state = expected_state = None - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Decimal",str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Decimal", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) expected_new_state = expected_state = sending_state = -4 - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Decimal",str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Decimal", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(2, state_correct_count) expected_new_state = expected_state = sending_state = 170.3 - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Quantity",str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Quantity", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(3, state_correct_count) expected_new_state = expected_state = 180 sending_state = "180 °" - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Quantity",str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Quantity", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(4, state_correct_count) expected_old_state = 180 expected_new_state = expected_state = 190 sending_state = "190 °" - eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("Quantity",str(sending_state),oldValue=expected_old_state)) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("Quantity", str(sending_state), old_value=expected_old_state)) + myopenhab._parse_event(event_data) testutil.doassert(4, state_correct_count) testutil.doassert(1, state_changed_correct_count) expected_old_state = 190 expected_new_state = expected_state = 200 sending_state = "200 °" - eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("Quantity",str(sending_state),oldValue=expected_old_state)) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("Quantity", str(sending_state), old_value=expected_old_state)) + myopenhab._parse_event(event_data) testutil.doassert(4, state_correct_count) testutil.doassert(2, state_changed_correct_count) @@ -231,13 +198,12 @@ def test_number_item(): expected_command = 200.1 sending_command = 200.1 - eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("Quantity", str(sending_command))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("Quantity", str(sending_command))) + myopenhab._parse_event(event_data) testutil.doassert(4, state_correct_count) testutil.doassert(2, state_changed_correct_count) testutil.doassert(1, command_correct_count) - log.info("################## starting tests with real item on openhab") sending_state = 123.4 @@ -251,7 +217,6 @@ def test_number_item(): testutil.doassert(3, state_changed_correct_count) testutil.doassert(1, command_correct_count) - expected_old_state = sending_state expected_new_state = expected_state = sending_state = expected_command = 567.8 @@ -261,7 +226,6 @@ def test_number_item(): testutil.doassert(4, state_changed_correct_count) testutil.doassert(1, command_correct_count) - expected_old_state = sending_state expected_new_state = expected_state = sending_state = expected_command = 999.99 @@ -271,9 +235,6 @@ def test_number_item(): testutil.doassert(5, state_changed_correct_count) testutil.doassert(2, command_correct_count) - - - finally: pass testitem.delete() @@ -303,37 +264,32 @@ def test_string_item(): testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) expected_new_state = expected_state = sending_state = "test value 1" - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("String", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("String", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - - sending_state = openhab.types.UndefType.UNDEF expected_new_state = expected_state = None - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("String", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("String", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) expected_new_state = expected_state = sending_state = "äöü°" - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("String", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("String", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(2, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - expected_old_state = expected_state expected_new_state = expected_state = sending_state = "test value 2" - eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("String", str(sending_state), oldValue=expected_old_state)) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("String", str(sending_state), old_value=expected_old_state)) + myopenhab._parse_event(event_data) testutil.doassert(2, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(0, command_correct_count) @@ -343,8 +299,8 @@ def test_string_item(): expected_new_state = expected_state = sending_command expected_command = sending_command - eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("String", str(sending_command))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("String", str(sending_command))) + myopenhab._parse_event(event_data) testutil.doassert(2, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(1, command_correct_count) @@ -362,7 +318,6 @@ def test_string_item(): testutil.doassert(2, state_changed_correct_count) testutil.doassert(1, command_correct_count) - expected_old_state = sending_state expected_new_state = expected_state = sending_state = expected_command = "test value 5" testitem.command(sending_state) @@ -371,14 +326,12 @@ def test_string_item(): testutil.doassert(3, state_changed_correct_count) testutil.doassert(2, command_correct_count) - - - finally: pass testitem.delete() -def test_dateTime_item(): + +def test_datetime_item(): global expected_state global state_correct_count @@ -403,8 +356,8 @@ def test_dateTime_item(): log.info("starting step 1") expected_new_state = expected_state = sending_state = datetime.now() - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("DateTime", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("DateTime", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) @@ -412,8 +365,8 @@ def test_dateTime_item(): log.info("starting step 2") expected_old_state = expected_state expected_new_state = expected_state = sending_state = datetime.now() - eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("DateTime", str(sending_state), oldValue=expected_old_state)) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("DateTime", str(sending_state), old_value=expected_old_state)) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(0, command_correct_count) @@ -421,12 +374,11 @@ def test_dateTime_item(): log.info("################## starting tests with real item on openhab") log.info("starting step 3") - sending_state = datetime(2001,2,3,4,5,6,microsecond=7000) + sending_state = datetime(2001, 2, 3, 4, 5, 6, microsecond=7000) expected_new_state = expected_state = sending_state expected_command = sending_state expected_old_state = None - testitem.state = sending_state time.sleep(0.5) testutil.doassert(2, state_correct_count) @@ -435,14 +387,13 @@ def test_dateTime_item(): log.info("starting step 4") expected_new_state = expected_old_state = expected_state - expected_new_state = expected_command = expected_state = sending_state = sending_command = sending_state = datetime(2001,2,3,4,5,6,microsecond=8000) + expected_new_state = expected_command = expected_state = sending_command = datetime(2001, 2, 3, 4, 5, 6, microsecond=8000) testitem.command(sending_command) time.sleep(0.5) testutil.doassert(3, state_correct_count) testutil.doassert(3, state_changed_correct_count) testutil.doassert(1, command_correct_count) - finally: pass testitem.delete() @@ -472,60 +423,49 @@ def test_player_item(): testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) expected_new_state = expected_state = sending_state = openhab.types.PlayPauseType.PLAY - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("PlayPause", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("PlayPause", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - sending_state = openhab.types.UndefType.UNDEF expected_new_state = expected_state = None - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("PlayPause", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("PlayPause", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - expected_new_state = expected_state = sending_state = openhab.types.RewindFastforward.FASTFORWARD - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("RewindFastforward", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("RewindFastforward", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(2, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - expected_old_state = expected_state expected_new_state = expected_state = sending_state = openhab.types.PlayPauseType.PAUSE - eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("PlayPause", str(sending_state), oldType="RewindFastforward" ,oldValue=expected_old_state)) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("PlayPause", str(sending_state), old_type="RewindFastforward", old_value=expected_old_state)) + myopenhab._parse_event(event_data) testutil.doassert(2, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - - - sending_command = openhab.types.NextPrevious.NEXT expected_command = openhab.types.NextPrevious.NEXT - eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("NextPrevious", str(sending_command))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("NextPrevious", str(sending_command))) + myopenhab._parse_event(event_data) testutil.doassert(2, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(1, command_correct_count) log.info("################## starting tests with real item on openhab") - sending_command = expected_command = openhab.types.PlayPauseType.PAUSE expected_new_state = expected_state = sending_command expected_old_state = None @@ -552,11 +492,10 @@ def test_player_item(): testitem.command(sending_command) time.sleep(0.5) - testutil.doassert(4, state_correct_count) # NEXT is not a state! - testutil.doassert(3, state_changed_correct_count) # NEXT is not a state! + testutil.doassert(4, state_correct_count) # NEXT is not a state! + testutil.doassert(3, state_changed_correct_count) # NEXT is not a state! testutil.doassert(4, command_correct_count) - finally: pass testitem.delete() @@ -586,39 +525,31 @@ def test_switch_item(): testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) expected_new_state = expected_state = sending_state = openhab.types.OnOffType.ON - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("OnOff", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("OnOff", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - expected_old_state = expected_state expected_new_state = expected_state = sending_state = openhab.types.OnOffType.OFF - eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("OnOff", str(sending_state), oldValue=expected_old_state)) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("OnOff", str(sending_state), old_value=expected_old_state)) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - - - expected_command = sending_command = openhab.types.OnOffType.ON - - eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("OnOff", str(sending_command))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("OnOff", str(sending_command))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(1, command_correct_count) log.info("################## starting tests with real item on openhab") - sending_state = sending_command = expected_command = openhab.types.OnOffType.ON expected_new_state = expected_state = sending_command expected_old_state = None @@ -633,19 +564,18 @@ def test_switch_item(): sending_state = openhab.types.OnOffType.OFF expected_new_state = expected_state = sending_state - testitem.state =sending_state + testitem.state = sending_state time.sleep(0.5) testutil.doassert(3, state_correct_count) testutil.doassert(3, state_changed_correct_count) testutil.doassert(2, command_correct_count) - - finally: pass testitem.delete() + def test_contact_item(): global expected_state global state_correct_count @@ -670,39 +600,31 @@ def test_contact_item(): testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) expected_new_state = expected_state = sending_state = openhab.types.OpenCloseType.OPEN - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("OpenClosed", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("OpenClosed", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - expected_old_state = expected_state expected_new_state = expected_state = sending_state = openhab.types.OpenCloseType.CLOSED - eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("OpenClosed", str(sending_state), oldValue=expected_old_state)) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("OpenClosed", str(sending_state), old_value=expected_old_state)) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - - - expected_command = sending_command = openhab.types.OpenCloseType.OPEN - - eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("OpenClosed", str(sending_command))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("OpenClosed", str(sending_command))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(1, command_correct_count) log.info("################## starting tests with real item on openhab") - sending_state = sending_command = expected_command = openhab.types.OpenCloseType.OPEN expected_new_state = expected_state = sending_command expected_old_state = None @@ -717,19 +639,18 @@ def test_contact_item(): sending_state = openhab.types.OpenCloseType.CLOSED expected_new_state = expected_state = sending_state - testitem.state =sending_state + testitem.state = sending_state time.sleep(0.5) testutil.doassert(3, state_correct_count) testutil.doassert(3, state_changed_correct_count) testutil.doassert(1, command_correct_count) - - finally: pass testitem.delete() + def test_dimmer_item(): global expected_state global state_correct_count @@ -754,39 +675,31 @@ def test_dimmer_item(): testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) expected_new_state = expected_state = sending_state = 45.67 - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Percent", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Percent", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - expected_old_state = expected_state expected_new_state = expected_state = sending_state = 12.12 - eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("Percent", str(sending_state), oldValue=expected_old_state)) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("Percent", str(sending_state), old_value=expected_old_state)) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - - - expected_command = sending_command = 44.44 - - eventData = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("Percent", str(sending_command))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemCommandEventType, testitem.name, create_event_payload("Percent", str(sending_command))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(1, command_correct_count) log.info("################## starting tests with real item on openhab") - sending_state = sending_command = expected_command = 66.77 expected_new_state = expected_state = sending_command expected_old_state = None @@ -801,17 +714,15 @@ def test_dimmer_item(): sending_state = 99.5 expected_new_state = expected_state = sending_state - testitem.state =sending_state + testitem.state = sending_state time.sleep(0.5) testutil.doassert(3, state_correct_count) testutil.doassert(3, state_changed_correct_count) testutil.doassert(2, command_correct_count) - - expected_old_state = sending_state - expected_state= sending_state = openhab.types.OnOffType.OFF + expected_state = sending_state = openhab.types.OnOffType.OFF expected_new_state = 0 testitem.state = sending_state @@ -820,10 +731,8 @@ def test_dimmer_item(): testutil.doassert(4, state_changed_correct_count) testutil.doassert(2, command_correct_count) - - expected_old_state = 0 - expected_state= sending_state = openhab.types.OnOffType.ON + expected_state = sending_state = openhab.types.OnOffType.ON expected_new_state = 100 testitem.state = sending_state @@ -832,33 +741,31 @@ def test_dimmer_item(): testutil.doassert(5, state_changed_correct_count) testutil.doassert(2, command_correct_count) - - expected_old_state = 100 - expected_command = sending_command = expected_state = sending_state = openhab.types.IncreaseDecreaseType.DECREASE + expected_command = sending_command = expected_state = openhab.types.IncreaseDecreaseType.DECREASE expected_new_state = 99 testitem.command(sending_command) time.sleep(0.5) - testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state - testutil.doassert(5, state_changed_correct_count) # openhab does not automatically increase the value + testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state + testutil.doassert(5, state_changed_correct_count) # openhab does not automatically increase the value testutil.doassert(3, command_correct_count) expected_old_state = 99 - expected_command = sending_command = expected_state = sending_state = openhab.types.IncreaseDecreaseType.DECREASE + expected_command = sending_command = expected_state = openhab.types.IncreaseDecreaseType.DECREASE expected_new_state = 98 testitem.command(sending_command) time.sleep(0.5) - testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state - testutil.doassert(5, state_changed_correct_count) # openhab does not automatically increase the value + testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state + testutil.doassert(5, state_changed_correct_count) # openhab does not automatically increase the value testutil.doassert(4, command_correct_count) - finally: pass testitem.delete() + def test_color_item(): global expected_state global state_correct_count @@ -883,39 +790,36 @@ def test_color_item(): testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) expected_new_state = expected_state = sending_state = 45.67 - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Percent", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Percent", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) sending_state = "1,2,3" - expected_new_state = expected_state = 1,2,3 - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("HSB", str(sending_state))) - myopenhab._parse_event(eventData) + expected_new_state = expected_state = 1, 2, 3 + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("HSB", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(2, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) - - - expected_old_state = 1,2,3 + expected_old_state = 1, 2, 3 sending_old_state = "1,2,3" - expected_new_state = expected_state = 4,5,6 + expected_new_state = expected_state = 4, 5, 6 sending_state = "4,5,6" - eventData = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("HSB", str(sending_state), oldType="HSB",oldValue=sending_old_state)) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateChangedEventType, testitem.name, create_event_payload("HSB", str(sending_state), old_type="HSB", old_value=sending_old_state)) + myopenhab._parse_event(event_data) testutil.doassert(2, state_correct_count) testutil.doassert(1, state_changed_correct_count) testutil.doassert(0, command_correct_count) - log.info("################## starting tests with real item on openhab") expected_new_state = expected_old_state = None - expected_state = expected_command = sending_command = (1,2,3) + expected_state = expected_command = sending_command = (1, 2, 3) testitem.command(sending_command) @@ -924,11 +828,10 @@ def test_color_item(): testutil.doassert(2, state_changed_correct_count) testutil.doassert(1, command_correct_count) - sending_state = sending_command = expected_command = 66.77 - expected_new_state = 1,2,66.77 + expected_new_state = 1, 2, 66.77 expected_state = sending_state - expected_old_state = 1,2,3 + expected_old_state = 1, 2, 3 testitem.command(sending_command) time.sleep(0.5) @@ -936,34 +839,30 @@ def test_color_item(): testutil.doassert(3, state_changed_correct_count) testutil.doassert(2, command_correct_count) - - expected_old_state = expected_new_state expected_state = sending_state = 99.5 - expected_new_state = 1,2,99.5 + expected_new_state = 1, 2, 99.5 - testitem.state =sending_state + testitem.state = sending_state time.sleep(0.5) testutil.doassert(5, state_correct_count) testutil.doassert(4, state_changed_correct_count) testutil.doassert(2, command_correct_count) - expected_old_state = expected_new_state expected_command = sending_command = openhab.types.IncreaseDecreaseType.DECREASE testitem.command(sending_command) time.sleep(0.5) - testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state - testutil.doassert(4, state_changed_correct_count) # openhab does not automatically increase the value + testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state + testutil.doassert(4, state_changed_correct_count) # openhab does not automatically increase the value testutil.doassert(3, command_correct_count) expected_old_state = expected_new_state - expected_state = openhab.types.OnOffType.OFF - expected_new_state = 1,2,0 - + expected_state = openhab.types.OnOffType.OFF + expected_new_state = 1, 2, 0 expected_command = sending_command = openhab.types.OnOffType.OFF @@ -1003,8 +902,8 @@ def test_rollershutter_item(): testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=True) expected_new_state = expected_state = sending_state = 45.67 - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Percent", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("Percent", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(1, state_correct_count) testutil.doassert(0, state_changed_correct_count) @@ -1012,17 +911,16 @@ def test_rollershutter_item(): sending_state = openhab.types.UpDownType.UP expected_state = sending_state - eventData = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("UpDown", str(sending_state))) - myopenhab._parse_event(eventData) + event_data = create_event_data(openhab.events.ItemStateEventType, testitem.name, create_event_payload("UpDown", str(sending_state))) + myopenhab._parse_event(event_data) testutil.doassert(2, state_correct_count) testutil.doassert(0, state_changed_correct_count) testutil.doassert(0, command_correct_count) - log.info("################## starting tests with real item on openhab") expected_new_state = expected_old_state = None - expected_state = expected_command = sending_command = 55.66 + expected_state = expected_command = sending_command = 55.66 testitem.command(sending_command) @@ -1031,7 +929,6 @@ def test_rollershutter_item(): testutil.doassert(1, state_changed_correct_count) testutil.doassert(1, command_correct_count) - sending_state = sending_command = expected_command = 66.77 expected_new_state = 66.77 expected_state = sending_state @@ -1043,7 +940,6 @@ def test_rollershutter_item(): testutil.doassert(2, state_changed_correct_count) testutil.doassert(2, command_correct_count) - log.info("xx") expected_old_state = expected_new_state expected_state = expected_command = sending_command = openhab.types.UpDownType.UP @@ -1051,15 +947,14 @@ def test_rollershutter_item(): testitem.command(sending_command) time.sleep(0.5) - testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state - testutil.doassert(3, state_changed_correct_count) # openhab does not automatically increase the value + testutil.doassert(5, state_correct_count) # openhab.types.IncreaseDecreaseType.DECREASE is not sent as state + testutil.doassert(3, state_changed_correct_count) # openhab does not automatically increase the value testutil.doassert(3, command_correct_count) expected_old_state = expected_new_state - expected_state = 0 + expected_state = 0 expected_new_state = 0 - expected_command = sending_command = openhab.types.StopMoveType.STOP testitem.command(sending_command) @@ -1074,20 +969,76 @@ def test_rollershutter_item(): testitem.delete() +def test_echos_for_rollershutter_item(): + + global do_breakpoint + count=0 + try: + log.info("testing echos for rollershutter") + + def on_item_command(item: openhab.items.Item, event: openhab.events.ItemCommandEvent): + global count + count +=1 + + def on_item_statechange(item: openhab.items.Item, event: openhab.events.ItemStateChangedEvent): + global count + count += 1 + def on_item_state(item: openhab.items.Item, event: openhab.events.ItemStateEvent): + global count + count += 1 + + testitem: openhab.items.RollershutterItem = myItemfactory.create_or_update_item(name="test_eventSubscription_rollershutteritem_A{}".format(namesuffix), data_type=openhab.items.RollershutterItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state, also_get_my_echos_from_openhab=False) + testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=False) + testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=False) + + testitem.command(75.66) + + time.sleep(0.5) + testutil.doassert(0, count) + + testitem.command(55.55) + time.sleep(0.5) + testutil.doassert(0, count) + + testitem.state = 33.22 + time.sleep(0.5) + testutil.doassert(0, count) + + testitem.update(11.12) + time.sleep(0.5) + testutil.doassert(0, count) + + log.info("########## testing for echos for rollershutter finished seccessfully") + + finally: + pass + testitem.delete() + + +time.sleep(3) +log.info("stopping daemon") +myopenhab.stop_receiving_events() +log.info("stopped daemon") +time.sleep(1) +testitem: openhab.items.RollershutterItem = myItemfactory.create_or_update_item(name="dummy_test_item_{}".format(namesuffix), data_type=openhab.items.RollershutterItem) +time.sleep(1) +testitem.delete() +time.sleep(1) +log.info("restarting daemon") +myopenhab.start_receiving_events() +log.info("restarted daemon") + test_number_item() test_string_item() -test_dateTime_item() +test_datetime_item() test_player_item() test_switch_item() test_contact_item() test_dimmer_item() test_rollershutter_item() +test_echos_for_rollershutter_item() +log.info("tests for events finished successfully") - -keep_going = True -log.info("###################################### tests finished successfully") -while keep_going: - # waiting for events from openhab - time.sleep(10) - - +myopenhab.loop_for_events() +log.info("stopping program") From 0b1e91e3ac793e8c71e742411f353fab8c634d77 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Wed, 30 Dec 2020 17:37:01 +0100 Subject: [PATCH 12/25] added http_buffersize to avoid "line too long" Exception when reading images from OH --- openhab/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openhab/client.py b/openhab/client.py index fff92d8..6dc7d13 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -76,6 +76,7 @@ def __init__(self, base_url: str, self.session = requests.Session() self.session.headers['accept'] = 'application/json' self.registered_items = weakref.WeakValueDictionary() + self.http_buffersize = 4*1024*1024 if http_auth is not None: self.session.auth = http_auth @@ -185,7 +186,7 @@ def sse_client_handler(self): self.logger.info("about to connect to Openhab Events-Stream.") async def run_loop(): - async with sse_client.EventSource(self.events_url) as event_source: + async with sse_client.EventSource(self.events_url,read_bufsize=self.http_buffersize) as event_source: try: self.logger.info("starting Openhab - Event Daemon") async for event in event_source: From 77da438a66d6a0d13f460e246bc772e4844b0973 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Sun, 3 Jan 2021 19:13:11 +0100 Subject: [PATCH 13/25] added audiosinks added voices added voice interpreter added cache for items --- README.rst | 28 +++++-- openhab/audio.py | 88 ++++++++++++++++++++ openhab/client.py | 130 ++++++++++++++++++++++++++++-- openhab/items.py | 18 +++-- tests/test_audio.py | 98 ++++++++++++++++++++++ tests/test_create_delete_items.py | 25 +++++- tests/test_eventsubscription.py | 3 +- 7 files changed, 372 insertions(+), 18 deletions(-) create mode 100644 openhab/audio.py create mode 100644 tests/test_audio.py diff --git a/README.rst b/README.rst index 4e089c2..e090935 100644 --- a/README.rst +++ b/README.rst @@ -22,11 +22,19 @@ This library allows for easily accessing the openHAB REST API. A number of features are implemented but not all, this is work in progress. currently you can - - retrieve current state of items - - send updates and commands to items - - receive commands, updates and changes triggered by openhab - - create new items and groups - - delete items and groups + items: + - retrieve current state of items + - send updates and commands to items + - receive commands, updates and changes triggered by openhab + - create new items and groups + - delete items and groups + + audio: + - retrieve audiosinks + - retrieve voices + - say text on audiosink using a voice + - retrieve voice interpreter + - send commands to voice interpreter Requirements @@ -158,6 +166,16 @@ Example usage of the library: function_name=functionname, function_params=functionparams) + + #you can "say" something on the default audio sink with the default voice + dv=openhab.get_audio_defaultvoice() + ds=openhab.get_audio_defaultsink() + dv.say("this is a test",ds) + + # you can send a textual "voice" command to openhab for interpretation/execution: + vi = openhab.get_voicesinterpreter("system") + vi.interpret("switch on the_testDimmer") + Note on NULL and UNDEF ---------------------- diff --git a/openhab/audio.py b/openhab/audio.py new file mode 100644 index 0000000..5ebfbe8 --- /dev/null +++ b/openhab/audio.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +"""python classes for accessing audio functions""" + +# +# Alexey Grubauer (c) 2020 +# +# python-openhab is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# python-openhab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with python-openhab. If not, see . +# + +# pylint: disable=bad-indentation +from __future__ import annotations +import typing +from dataclasses import dataclass +import openhab.types + + +class Audiosink: + + @staticmethod + def json_to_audiosink(raw_json:typing.Dict, openhab:openhab.OpenHAB) -> Audiosink: + new_audiosink = Audiosink(openhab=openhab, id=raw_json["id"], label=raw_json["label"]) + return new_audiosink + + + def __init__(self, openhab:openhab.OpenHAB, id:str, label:str): + self.openhab=openhab + self.id = id + self.label = label + + def say(self,text: str, voice:Voice): + self.openhab.say(text=text,audiosinkid=self.id, voiceid=voice.id) + + def __str__(self): + return "id:'{}', label:'{}'".format(self.id,self.label) + + +class Voice: + @staticmethod + def json_to_voice(raw_json: typing.Dict, openhab:openhab.OpenHAB) -> Voice: + new_voice = Voice(openhab=openhab, id=raw_json["id"], label=raw_json["label"], locale=raw_json["locale"]) + return new_voice + + def __init__(self, openhab:openhab.OpenHAB, id: str, label: str, locale: str): + self.openhab = openhab + self.id = id + self.label = label + self.locale = locale + + def say(self,text: str, audiosink:Audiosink): + audiosink.say(text=text,voice=self) + + def __str__(self): + return "id:'{}', label:'{}', locale:'{}'".format(self.id, self.label,self.locale) + + +class Voiceinterpreter: + @staticmethod + def json_to_voiceinterpreter(raw_json: typing.Dict, openhab:openhab.OpenHAB) -> Voiceinterpreter: + id: str = raw_json["id"] + label: str = raw_json["label"] + locales: typing.List[str] = [] + if "locales" in raw_json: + locales = raw_json["locales"] + new_voiceinterpreter = Voiceinterpreter(openhab=openhab, id=id, label=label, locales=locales) + return new_voiceinterpreter + + def __init__(self, openhab:openhab.OpenHAB, id: str, label: str, locales: typing.List[str]): + self.openhab = openhab + self.id = id + self.label = label + self.locales = locales + + def interpret(self, text:str): + self.openhab.interpret(text=text,voiceinterpreterid=self.id) + + def __str__(self): + return "id:'{}', label:'{}', locales:'{}'".format(self.id, self.label,self.locales) \ No newline at end of file diff --git a/openhab/client.py b/openhab/client.py index 6dc7d13..ecc41a3 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -32,12 +32,14 @@ import weakref import json + from requests.auth import HTTPBasicAuth import openhab.items import openhab.events import openhab.types +import openhab.audio __author__ = 'Georges Toth ' __license__ = 'AGPLv3+' @@ -76,6 +78,7 @@ def __init__(self, base_url: str, self.session = requests.Session() self.session.headers['accept'] = 'application/json' self.registered_items = weakref.WeakValueDictionary() + self.all_items:typing.Dict[str,openhab.items.Item] = {} self.http_buffersize = 4*1024*1024 if http_auth is not None: @@ -129,7 +132,7 @@ def _parse_event(self, event_data: typing.Dict) -> None: Args: event_data send by openhab in a Dict """ - log = logging.getLogger() + log = logging.getLogger(__name__) if "type" in event_data: event_reason = event_data["type"] @@ -186,7 +189,7 @@ def sse_client_handler(self): self.logger.info("about to connect to Openhab Events-Stream.") async def run_loop(): - async with sse_client.EventSource(self.events_url,read_bufsize=self.http_buffersize) as event_source: + async with sse_client.EventSource(self.events_url, read_bufsize=self.http_buffersize) as event_source: try: self.logger.info("starting Openhab - Event Daemon") async for event in event_source: @@ -217,16 +220,30 @@ def get_registered_items(self) -> weakref.WeakValueDictionary: """ return self.registered_items + def register_all_items(self) -> None: + """fetches all items from openhab and caches them in all_items. + subsequent calls to get_item will use this cache. + subsequent calls to register_all_items will rebuld the chache + """ + self.all_items = self.fetch_all_items() + for item in self.all_items.values(): + self.register_item(item) + def register_item(self, item: openhab.items.Item) -> None: - """method to register an instantiated item. registered items can receive commands an updated from openhab. + """method to register an instantiated item. registered items can receive commands and updates from openhab. Usually you don´t need to register as Items register themself. Args: an Item object """ if item is not None and item.name is not None: if item.name not in self.registered_items: + self.logger.debug("registered item:{}".format(item.name)) self.registered_items[item.name] = item + def unregister_item(self,name): + if name in self.all_items: + self.all_items.pop(name) + def stop_receiving_events(self): """stop to receive events from openhab. """ @@ -363,7 +380,7 @@ def fetch_all_items(self) -> typing.Dict[str, openhab.items.Item]: return items - def get_item(self, name: str) -> openhab.items.Item: + def get_item(self, name: str, force_request_to_openhab:typing.Optional[bool]=False) -> openhab.items.Item: """Returns an item with its state and data_type as fetched from openHAB. Args: @@ -372,8 +389,11 @@ def get_item(self, name: str) -> openhab.items.Item: Returns: Item: A corresponding Item class instance with the state of the requested item. """ - json_data = self.get_item_raw(name) + if name in self.all_items and not force_request_to_openhab: + return self.all_items[name] + + json_data = self.get_item_raw(name) return self.json_to_item(json_data) def json_to_item(self, json_data: dict) -> openhab.items.Item: @@ -438,6 +458,106 @@ def get_item_raw(self, name: str) -> typing.Any: """ return self.req_get('/items/{}'.format(name)) + # audio + def get_audio_defaultsink(self) -> openhab.audio.Audiosink: + """returns openhabs default audio sink + + + Returns: + openhab.audio.Audiosink: the audio sink + """ + defaultsink = self._get_audio_defaultsink_raw() + return openhab.audio.Audiosink.json_to_audiosink(defaultsink,self) + + def _get_audio_defaultsink_raw(self) -> typing.Dict: + return self.req_get('/audio/defaultsink') + + def get_all_audiosinks(self) -> typing.List[openhab.audio.Audiosink]: + """returns openhabs audio sinks + + + Returns: + List[openhab.audio.Audiosink]: a list of audio sinks + """ + result: typing.List[openhab.audio.Audiosink] = [] + sinks = self._get_all_audiosinks_raw() + for sink in sinks: + result.append(openhab.audio.Audiosink.json_to_audiosink(sink,self)) + return result + + def _get_all_audiosinks_raw(self) -> typing.Any: + return self.req_get('/audio/sinks') + + # voices + def get_audio_defaultvoice(self) -> openhab.audio.Voice: + """returns openhabs default voice + + + Returns: + openhab.audio.Voice: the voice + """ + defaultvoice = self._get_audio_defaultvoice_raw() + return openhab.audio.Voice.json_to_voice(defaultvoice,self) + + def _get_audio_defaultvoice_raw(self) -> typing.Dict: + return self.req_get('/voice/defaultvoice') + + def get_all_voices(self) -> typing.List[openhab.audio.Voice]: + """returns openhabs voices + + + Returns: + List[openhab.audio.Voice]: a list of voices + """ + result: typing.List[openhab.audio.Voice] = [] + voices = self._get_all_voices_raw() + for voice in voices: + result.append(openhab.audio.Voice.json_to_voice(voice,self)) + return result + + def _get_all_voices_raw(self) -> typing.Dict: + return self.req_get('/voice/voices') + + # voiceinterpreters + def get_voicesinterpreter(self, id:str) -> openhab.audio.Voiceinterpreter: + """returns a openhab voiceinterpreter + Args: + id (str): The id of the voiceinterpreter to be fetched. + Returns: + openhab.audio.Voiceinterpreter: the Voiceinterpreter + """ + voiceinterpreter = self._get_voicesinterpreter_raw(id) + return openhab.audio.Voiceinterpreter.json_to_voiceinterpreter(voiceinterpreter,self) + + + def _get_voicesinterpreter_raw(self, id:str) -> typing.Dict: + return self.req_get('/voice/interpreters/{}'.format(id)) + + def get_all_voicesinterpreters(self) -> typing.List[openhab.audio.Voiceinterpreter]: + """returns openhabs voiceinterpreters + Returns: + List[openhab.audio.Voiceinterpreter]: a list of voiceinterpreters + """ + result: typing.List[openhab.audio.Voiceinterpreter] = [] + voiceinterpreters = self._get_all_voiceinterpreters_raw() + for voiceinterpreter in voiceinterpreters: + result.append(openhab.audio.Voiceinterpreter.json_to_voiceinterpreter(voiceinterpreter,self)) + return result + + def _get_all_voiceinterpreters_raw(self) -> typing.Dict: + return self.req_get('/voice/interpreters') + + def say(self, text: str, audiosinkid: str, voiceid: str): + url=self.base_url + "/voice/say/?voiceid={voiceid}&sinkid={sinkid}".format(voiceid=requests.utils.quote(voiceid), sinkid=requests.utils.quote(audiosinkid)) + r = self.session.post(url, data=text, headers={'Accept': 'application/json'}, timeout=self.timeout) + self._check_req_return(r) + + def interpret(self, text: str, voiceinterpreterid: str): + url=self.base_url + "/voice/interpreters/{interpreterid}".format(interpreterid=requests.utils.quote(voiceinterpreterid)) + r = self.session.post(url, data=text, headers={'Accept': 'application/json'}, timeout=self.timeout) + self._check_req_return(r) + + # noinspection PyPep8Naming class openHAB(OpenHAB): diff --git a/openhab/items.py b/openhab/items.py index 5d0e8d4..87d8a05 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -177,18 +177,25 @@ def create_or_update_item_async(self, paramdict["function"] = {"name": function_name, "params": function_params} json_body = json.dumps(paramdict) - logging.getLogger().debug("about to create item with PUT request:{}".format(json_body)) + logging.getLogger(__name__).debug("about to create item with PUT request:{}".format(json_body)) self.openHABClient.req_json_put('/items/{}'.format(name), json_data=json_body) - def get_item(self, itemname) -> Item: + def get_item(self, itemname,force_request_to_openhab:typing.Optional[bool]=False) -> Item: """get a existing openhab item Args: itemname (str): unique name of the item Returns: Item: the Item """ - return self.openHABClient.get_item(itemname) + return self.openHABClient.get_item(name=itemname,force_request_to_openhab=force_request_to_openhab) + def fetch_all_items(self) -> typing.Dict[str, openhab.items.Item]: + """Returns all items defined in openHAB. + + Returns: + dict: Returns a dict with item names as key and item class instances as value. + """ + return self.openHABClient.fetch_all_items() class Item: """Base item class.""" @@ -384,6 +391,7 @@ def _is_my_own_echo(self, event: openhab.events.ItemEvent): def delete(self): """deletes the item from openhab """ self.openhab.req_del('/items/{}'.format(self.name)) + self.openhab.unregister_item(self.name) self._state = None self.remove_all_event_listeners() @@ -561,7 +569,7 @@ def _process_external_event(self, raw_event: openhab.events.RawItemEvent): """ if not self.autoUpdate: return - self.logger.info("processing external event") + self.logger.debug("processing external event:{}".format(raw_event)) if raw_event.event_type == openhab.events.ItemCommandEvent.type: event = self._parse_external_command_event(raw_event) @@ -586,7 +594,7 @@ def _process_internal_event(self, event: openhab.events.ItemEvent): event (openhab.events.ItemEvent): the internal event """ - self.logger.info("processing internal event") + self.logger.debug("processing internal event:{}".format(event)) for aListener in self.event_listeners.values(): if event.type in aListener.listeningTypes: if aListener.onlyIfEventsourceIsOpenhab: diff --git a/tests/test_audio.py b/tests/test_audio.py new file mode 100644 index 0000000..8ffa802 --- /dev/null +++ b/tests/test_audio.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +"""tests for creating and deletion of items """ + +# +# Alexey Grubauer (c) 2020-present +# +# python-openhab is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# python-openhab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with python-openhab. If not, see . +# +# pylint: disable=bad-indentation + +from __future__ import annotations +import openhab +import openhab.events +import openhab.items as items +import openhab.types +import logging +import time + +log = logging.getLogger(__name__) +logging.basicConfig(level=10, format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") + +log.error("xx") +log.warning("www") +log.info("iii") +log.debug("ddddd") + +base_url = 'http://10.10.20.80:8080/rest' + + +def test_sinks(myopenhab: openhab.OpenHAB): + log.info("starting tests 'test sinks'") + ds=myopenhab.get_audio_defaultsink() + log.info("defaultsink:{}".format(ds)) + allSinks=myopenhab.get_all_audiosinks() + for aSink in allSinks: + log.info("sink:{}".format(aSink)) + + +def test_voices(myopenhab: openhab.OpenHAB): + log.info("starting tests 'test voices'") + dv=myopenhab.get_audio_defaultvoice() + log.info("defaultvoice:{}".format(dv)) + allVoices=myopenhab.get_all_voices() + for aVoice in allVoices: + log.info("voice:{}".format(aVoice)) + +def test_voiceinterpreters(myopenhab: openhab.OpenHAB): + log.info("starting tests 'test test_voiceinterpreters'") + allVoice_interpreters = myopenhab.get_all_voicesinterpreters() + for aVoiceinterpreter in allVoice_interpreters: + log.info("voiceinterpreter:{}".format(aVoiceinterpreter)) + log.info("now get the 'system' voiceinterpreter") + vi = myopenhab.get_voicesinterpreter("system") + log.info("system voiceinterpreter:{}".format(vi)) + +def test_say(myopenhab: openhab.OpenHAB): + myopenhab.get_audio_defaultvoice().say("das ist ein test",myopenhab.get_audio_defaultsink()) + +def test_interpret(myopenhab: openhab.OpenHAB): + my_item_factory = openhab.items.ItemFactory(myopenhab) + switchitem: openhab.items.SwitchItem = my_item_factory.create_or_update_item(name="test_interpret", label="test_interpret", data_type=openhab.items.SwitchItem) + try: + + switchitem.off() + time.sleep(0.3) + vi = myopenhab.get_voicesinterpreter("system") + text="schalte {} ein".format(switchitem.name) + log.info("interpreting text:'{text}'".format(text=text)) + vi.interpret(text=text) + time.sleep(0.3) + switchitem.state == openhab.types.OnOffType.ON + finally: + switchitem.delete() + + + + + + +myopenhab = openhab.OpenHAB(base_url, auto_update=False) + + +# test_sinks(myopenhab) +# test_voices(myopenhab) +# test_voiceinterpreters(myopenhab) +# test_say(myopenhab) +test_interpret(myopenhab) \ No newline at end of file diff --git a/tests/test_create_delete_items.py b/tests/test_create_delete_items.py index 0413c55..b20a453 100644 --- a/tests/test_create_delete_items.py +++ b/tests/test_create_delete_items.py @@ -30,7 +30,7 @@ import tests.testutil as testutil from datetime import datetime -log = logging.getLogger() +log = logging.getLogger(__name__) logging.basicConfig(level=10, format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") log.error("xx") @@ -38,7 +38,8 @@ log.info("iii") log.debug("ddddd") -base_url = 'http://localhost:8080/rest' +base_url = 'http://10.10.20.81:8080/rest' +#base_url = 'http://localhost:8080/rest' def test_create_and_delete_items(myopenhab: openhab.OpenHAB, nameprefix): @@ -86,6 +87,8 @@ def test_create_and_delete_items(myopenhab: openhab.OpenHAB, nameprefix): log.info("the new Player:{}".format(a_player_item)) log.info("creation tests worked") + + finally: if a_group_item is not None: a_group_item.delete() @@ -326,6 +329,22 @@ def test_PlayerItem(item_factory, nameprefix): testutil.doassert("PAUSE", x2.state, "itemstate") return x2 +def test_register_all_items(item_factory:openhab.items.ItemFactory ,myopenhab:openhab.OpenHAB): + + itemname = "CreateSwitchItemTest_register_all_items" + itemtype = openhab.items.SwitchItem + x2: openhab.items.SwitchItem = item_factory.create_or_update_item(name=itemname, data_type=itemtype) + try: + myopenhab.register_all_items() + for aItem in myopenhab.all_items.items(): + log.info("found item:{}".format(aItem)) + item_factory.get_item(itemname) + + + finally: + x2.delete() + + myopenhab = openhab.OpenHAB(base_url, auto_update=False) keeprunning = True @@ -333,3 +352,5 @@ def test_PlayerItem(item_factory, nameprefix): mynameprefix = "x2_{}".format(random.randint(1, 1000)) test_create_and_delete_items(myopenhab, mynameprefix) +my_item_factory = openhab.items.ItemFactory(myopenhab) +test_register_all_items(item_factory=my_item_factory, myopenhab=myopenhab) diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index d2924a8..2e3f6e6 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -30,7 +30,7 @@ from datetime import datetime -log = logging.getLogger() +log = logging.getLogger(__name__) logging.basicConfig(level=10, format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") log.error("errormessage") @@ -39,6 +39,7 @@ log.debug("debugmessage") base_url = 'http://localhost:8080/rest' +base_url = 'http://10.10.20.81:8080/rest' expected_state = None state_correct_count = 0 From 70154e76933c9a3f15562962ed079bda199a4ba0 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Wed, 20 Jan 2021 15:59:40 +0100 Subject: [PATCH 14/25] fix for _is_my_own_echo and Eventtypes --- openhab/client.py | 2 ++ openhab/items.py | 28 ++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index ecc41a3..bdbc491 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -133,6 +133,7 @@ def _parse_event(self, event_data: typing.Dict) -> None: event_data send by openhab in a Dict """ log = logging.getLogger(__name__) + if "type" in event_data: event_reason = event_data["type"] @@ -548,6 +549,7 @@ def _get_all_voiceinterpreters_raw(self) -> typing.Dict: return self.req_get('/voice/interpreters') def say(self, text: str, audiosinkid: str, voiceid: str): + log.info("sending say command to OH for voiceid:'{}', audiosinkid:'{}'".format(voiceid,audiosinkid)) url=self.base_url + "/voice/say/?voiceid={voiceid}&sinkid={sinkid}".format(voiceid=requests.utils.quote(voiceid), sinkid=requests.utils.quote(audiosinkid)) r = self.session.post(url, data=text, headers={'Accept': 'application/json'}, timeout=self.timeout) self._check_req_return(r) diff --git a/openhab/items.py b/openhab/items.py index 87d8a05..813391f 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -239,6 +239,8 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto self.init_from_json(json_data) self.last_command_sent_time = datetime.fromtimestamp(0) self.last_command_sent = "" + self.last_change_sent_time = datetime.fromtimestamp(0) + self.last_change_sent = None self.openhab.register_item(self) self.event_listeners: typing.Dict[typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], Item.EventListener] = {} @@ -317,6 +319,8 @@ def members(self) -> typing.Dict[str, typing.Any]: def _validate_value(self, value: typing.Union[str, typing.Type[openhab.types.CommandType]]) -> None: """Private method for verifying the new value before modifying the state of the item.""" + if value == openhab.types.CommandType.UNDEF: + return if self.type_ == 'String': if not isinstance(value, (str, bytes)): raise ValueError() @@ -360,9 +364,9 @@ def _is_my_own_echo(self, event: openhab.events.ItemEvent): if event.source != openhab.events.EventSourceOpenhab: result = True return result - if self.last_command_sent_time + timedelta(milliseconds=self.openhab.maxEchoToOpenhabMS) > now: + if self.last_change_sent_time + timedelta(milliseconds=self.openhab.maxEchoToOpenhabMS) > now: if event.type == openhab.events.ItemCommandEventType: - if self.last_command_sent == event.value: + if self.last_change_sent == event.value: # this is the echo of the command we just sent to openHAB. result = True return result @@ -526,7 +530,7 @@ def _parse_external_command_event(self, raw_event: openhab.events.RawItemEvent) if command_type_class in self.command_event_types: item_command_event = self._digest_external_command_event(command_type_class, command) return item_command_event - raise Exception("unknown command event type") + raise Exception("unknown command event type:'{}'".format(command_type_class)) def _parse_external_state_event(self, raw_event: openhab.events.RawItemEvent) -> openhab.events.ItemStateEvent: """Private method to process a state event coming from openhab @@ -537,11 +541,13 @@ def _parse_external_state_event(self, raw_event: openhab.events.RawItemEvent) -> """ state_type = raw_event.content["type"] state_type_class = openhab.types.CommandType.get_type_for(state_type) + if state_type_class is None: + raise Exception("unknown statetype:'{}'".format(state_type)) value = raw_event.content["value"] - if state_type_class in self.state_event_types: + if state_type_class in self.state_event_types+[openhab.types.UndefType]: item_state_event = self.digest_external_state_event(state_type_class, value) return item_state_event - raise Exception("unknown state event type") + raise Exception("unknown state event type:'{}'".format(state_type_class)) def _parse_external_state_changed_event(self, raw_event: openhab.events.RawItemEvent) -> openhab.events.ItemStateEvent: """Private method to process a state changed event coming from openhab @@ -727,8 +733,9 @@ def _update(self, value: typing.Any) -> None: on the item data_type and is checked accordingly. """ # noinspection PyTypeChecker - self.last_command_sent_time = datetime.now() - self.last_command_sent = value + self.last_change_sent_time = datetime.now() + self.last_change_sent = value + self.openhab.req_put('/items/{}/state'.format(self.name), data=value) def update(self, value: typing.Any) -> None: @@ -783,7 +790,12 @@ def command(self, value: typing.Any) -> None: self._validate_value(value) v = self._rest_format(value) self._state = value - self.last_command_sent_time = datetime.now() + now=datetime.now() + self.last_command_sent_time = now + self.last_change_sent_time = now + + self.last_command_sent = value + self.last_change_sent = value self.openhab.req_post('/items/{}'.format(self.name), data=v) unit_of_measure = "" From 7ac50f1f7f8554e7f89e2e607e836c202fbd2d78 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Thu, 28 Jan 2021 22:05:28 +0100 Subject: [PATCH 15/25] introduced a historylist of sent itemvalues for better echo checking --- openhab/history.py | 80 +++++++++++++++++++++++++++++++++ openhab/items.py | 48 +++++++------------- tests/test_eventsubscription.py | 2 + tests/test_history.py | 75 +++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 31 deletions(-) create mode 100644 openhab/history.py create mode 100644 tests/test_history.py diff --git a/openhab/history.py b/openhab/history.py new file mode 100644 index 0000000..ded16e1 --- /dev/null +++ b/openhab/history.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""python class to store historic data with a defined lifetime. Items older than seconds_of_history will be removed""" + +# +# Alexey Grubauer (c) 2020 +# + +# pylint: disable=bad-indentation +from __future__ import annotations +from typing import TYPE_CHECKING, List, Set, Dict, Tuple, Union, Any, Optional, NewType, Callable, Generic, TypeVar +from datetime import datetime, timedelta + + +T = TypeVar("T") + + +class History(Generic[T]): + def __init__(self, seconds_of_history: float) -> None: + self.entries: List[Tuple[datetime, T]] = [] + self.history_lenght = timedelta(seconds=seconds_of_history) + + def add(self, item: T, when: datetime = None) -> None: + if when is None: + when = datetime.now() + self.__clean_history__() + self.entries.append((when, item)) + + def __clean_history__(self) -> None: + now = datetime.now() + valid_until = now - self.history_lenght + remove_from_idx = 0 + + for entry in self.entries: + if entry[0] < valid_until: + remove_from_idx += 1 + else: + break + + self.entries = self.entries[remove_from_idx:] + + def __contains__(self, item: T) -> bool: + time,entry = self.get(item) + if time is None: + return False + else: + return True + + + def clear(self): + self.entries.clear() + + def get(self, item:T) -> (datetime,T): + self.__clean_history__() + for entry in reversed(self.entries): + if entry[1] == item: + return entry + return None,None + + + + def get_entries(self) -> List[T]: + self.__clean_history__() + return [entry[1] for entry in self.entries] + + def remove(self, item: T = None, when: datetime = None) -> None: + if item is None and when is None: + return + to_remove = [] + for entry in self.entries: + if when is not None: + if entry[0] == when: + to_remove.append(entry) + if item is not None: + if entry[1] == item: + to_remove.append(entry) + for to_remove_entry in to_remove: + try: + self.entries.remove(to_remove_entry) + except: + pass diff --git a/openhab/items.py b/openhab/items.py index 813391f..d548536 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -31,6 +31,7 @@ import openhab.types import openhab.events +import openhab.history from datetime import datetime, timedelta __author__ = 'Georges Toth ' @@ -208,7 +209,7 @@ class Item: TYPENAME = "unknown" - def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto_update: typing.Optional[bool] = True) -> None: + def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto_update: typing.Optional[bool] = True, maxEchoToOpenhabMS = None) -> None: """Constructor. Args: @@ -237,12 +238,13 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto self.logger = logging.getLogger(__name__) self.init_from_json(json_data) - self.last_command_sent_time = datetime.fromtimestamp(0) - self.last_command_sent = "" - self.last_change_sent_time = datetime.fromtimestamp(0) - self.last_change_sent = None self.openhab.register_item(self) self.event_listeners: typing.Dict[typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], Item.EventListener] = {} + self.maxEchoToOpenhabMS = maxEchoToOpenhabMS + if self.maxEchoToOpenhabMS is None: + self.maxEchoToOpenhabMS = self.openhab.maxEchoToOpenhabMS + self.change_sent_history = openhab.history.History(self.maxEchoToOpenhabMS/1000) #this History hold all recent changes sent to OH. We need this for subsequent events coming in to check if these incoming events are our own echo or not. + # this is necessary as the we might send MANY changes out to OH and receive back events of those changes with some delay and they would all be recognized as not an echo (although they are). The history stores the last values sent within a given time period. def init_from_json(self, json_data: dict) -> None: """Initialize this object from a json configuration as fetched from openHAB. @@ -360,36 +362,23 @@ def _is_my_own_echo(self, event: openhab.events.ItemEvent): """find out if the incoming event is actually just a echo of my previous command or update""" now = datetime.now() result = None + time_sent = None try: if event.source != openhab.events.EventSourceOpenhab: result = True - return result - if self.last_change_sent_time + timedelta(milliseconds=self.openhab.maxEchoToOpenhabMS) > now: - if event.type == openhab.events.ItemCommandEventType: - if self.last_change_sent == event.value: - # this is the echo of the command we just sent to openHAB. - result = True - return result - else: - self.logger.debug("it is not an echo. last command sent:{}, eventvalue:{}".format(self.last_command_sent, event.value)) - elif event.type in [openhab.events.ItemStateChangedEventType, openhab.events.ItemStateEventType]: - if self._state == event.value: - # this is the echo of the command we just sent to openHAB. - result = True - return result - else: - self.logger.debug("it is not an echo. previous state:{}, eventvalue:{}".format(self._state, event.value)) - result = False + else: + time_sent,item = self.change_sent_history.get(event.value) + result = time_sent is not None return result finally: - self.logger.debug("checking if it is my own echo result:{result} for item:{itemname}, event.source:{source}, event.data_type{datatype}, self._state:{state}, event.new_value:{value}, self.last_command_sent_time:{last_command_sent_time}, now:{now}".format( + self.logger.debug("check if it is my own echo result:{result} for item:{itemname}, event.source:{source}, event.data_type{datatype}, self._state:{state}, event.new_value:{value}, time sent:{time_sent}, now:{now}".format( result=result, itemname=event.item_name, source=event.source, datatype=event.type, state=self._state, value=event.value, - last_command_sent_time=self.last_command_sent_time, + time_sent=time_sent, now=now)) def delete(self): @@ -733,8 +722,9 @@ def _update(self, value: typing.Any) -> None: on the item data_type and is checked accordingly. """ # noinspection PyTypeChecker - self.last_change_sent_time = datetime.now() - self.last_change_sent = value + + self.change_sent_history.add(value) + self.logger.debug("sending update to OH for item {} with new value:{}".format(self.name,value)) self.openhab.req_put('/items/{}/state'.format(self.name), data=value) @@ -790,12 +780,8 @@ def command(self, value: typing.Any) -> None: self._validate_value(value) v = self._rest_format(value) self._state = value - now=datetime.now() - self.last_command_sent_time = now - self.last_change_sent_time = now - self.last_command_sent = value - self.last_change_sent = value + self.change_sent_history.add(value) self.openhab.req_post('/items/{}'.format(self.name), data=v) unit_of_measure = "" diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index 2e3f6e6..2847ad5 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -994,6 +994,8 @@ def on_item_state(item: openhab.items.Item, event: openhab.events.ItemStateEvent testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=False) testitem.command(75.66) + testitem.command(75.65) + testitem.command(75.64) time.sleep(0.5) testutil.doassert(0, count) diff --git a/tests/test_history.py b/tests/test_history.py new file mode 100644 index 0000000..1d696d5 --- /dev/null +++ b/tests/test_history.py @@ -0,0 +1,75 @@ +import unittest +from openhab.history import History +import time + + +class Item(object): + def __init__(self, name: str): + self.name = name + def __str__(self): + return "item:'{}'".format(self.name) + + +class TestHistory(unittest.TestCase): + def setUp(self): + self.item_history: History[Item] = History[Item](0.3) + + + def test_add_remove(self): + a = Item("a") + b = Item("b") + + self.item_history.add(a) + self.item_history.add(b) + + self.assertIn(a, self.item_history) + self.assertIn(b, self.item_history) + self.item_history.remove(a) + self.assertNotIn(a, self.item_history) + self.assertIn(b, self.item_history) + + entries = self.item_history.get_entries() + + self.assertNotIn(a, entries) + self.assertIn(b, entries) + + self.item_history.remove(a) + self.assertNotIn(a, self.item_history) + self.assertIn(b, self.item_history) + self.item_history.remove(b) + self.assertNotIn(a, self.item_history) + self.assertNotIn(b, self.item_history) + + def test_stale(self): + a = Item("a") + b = Item("b") + c = Item("c") + + self.item_history.clear() + self.item_history.add(a) + time.sleep(0.2) + self.item_history.add(b) + self.assertIn(a, self.item_history) + self.assertIn(b, self.item_history) + time.sleep(0.11) + self.assertNotIn(a, self.item_history) + self.assertIn(b, self.item_history) + time.sleep(0.21) + self.assertNotIn(a, self.item_history) + self.assertNotIn(b, self.item_history) + + self.item_history.add(c) + time.sleep(0.4) + self.assertNotIn(a, self.item_history) + self.assertNotIn(b, self.item_history) + self.assertNotIn(c, self.item_history) + + entries = self.item_history.get_entries() + self.assertEqual(len(entries), 0) + + def tearDown(self) -> None: + self.item_history = None + + +if __name__ == '__main__': + unittest.main() From 64f1799a419e5c0612f49e3110cc3171414791e9 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Mon, 1 Feb 2021 13:20:39 +0100 Subject: [PATCH 16/25] changed behaviour for not yet implemented OH types --- openhab/client.py | 2 ++ openhab/items.py | 2 +- openhab/types.py | 9 ++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index bdbc491..49f17a1 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -209,6 +209,8 @@ async def run_loop(): asyncio.run(run_loop()) except asyncio.TimeoutError: self.logger.info("reconnecting after timeout") + except openhab.types.TypeNotImplementedError as e: + self.logger.warning("received unknown datatye '{}' for item '{}'".format(e.datatype,e.itemname)) except Exception as e: self.logger.exception(e) diff --git a/openhab/items.py b/openhab/items.py index d548536..2ab225e 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -531,7 +531,7 @@ def _parse_external_state_event(self, raw_event: openhab.events.RawItemEvent) -> state_type = raw_event.content["type"] state_type_class = openhab.types.CommandType.get_type_for(state_type) if state_type_class is None: - raise Exception("unknown statetype:'{}'".format(state_type)) + raise openhab.types.TypeNotImplementedError(itemname=self.name,datatype=state_type) value = raw_event.content["value"] if state_type_class in self.state_event_types+[openhab.types.UndefType]: item_state_event = self.digest_external_state_event(state_type_class, value) diff --git a/openhab/types.py b/openhab/types.py index dade788..be4e782 100644 --- a/openhab/types.py +++ b/openhab/types.py @@ -30,6 +30,13 @@ __license__ = 'AGPLv3+' +class TypeNotImplementedError(NotImplementedError): + """Exception raised for incoming OH events containing not implemented datatypes""" + def __init__(self, itemname: str, datatype: str): + self.itemname = itemname + self.datatype = datatype + + class CommandType(metaclass=abc.ABCMeta): """Base command data_type class.""" @@ -263,7 +270,7 @@ def parse(cls, value: str) -> typing.Union[None, typing.Tuple[typing.Union[int, value_unit_of_measure = m.group(2) try: return_value = int(value_value) - except: + except ValueError: try: return_value = float(value_value) except Exception as e: From 7ddd650ec642c3d16fe1ce830c5b5eb2118fe578 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Tue, 2 Feb 2021 11:08:08 +0100 Subject: [PATCH 17/25] introduced slotted sending of changes --- openhab/client.py | 9 +++++- openhab/items.py | 48 +++++++++++++++++++++++++--- tests/test_create_delete_items.py | 52 +++++++++++++++++++++++++++++-- 3 files changed, 101 insertions(+), 8 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index 49f17a1..0221841 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -31,6 +31,8 @@ import requests import weakref import json +from datetime import datetime,timedelta,timezone + from requests.auth import HTTPBasicAuth @@ -54,7 +56,8 @@ def __init__(self, base_url: str, http_auth: typing.Optional[requests.auth.AuthBase] = None, timeout: typing.Optional[float] = None, auto_update: typing.Optional[bool] = False, - max_echo_to_openhab_ms: typing.Optional[int] = 800) -> None: + max_echo_to_openhab_ms: typing.Optional[int] = 800, + min_time_between_slotted_changes_ms: typing.Optional[float]=0) -> None: """Class Constructor. Args: @@ -69,6 +72,7 @@ def __init__(self, base_url: str, auto_update (bool, optional): True: receive Openhab Item Events to actively get informed about changes. max_echo_to_openhab_ms (int, optional): interpret Events from openHAB which hold a state-value equal to items current state-value which are coming in within maxEchoToOpenhabMS milliseconds since our update/command as echos of our own update//command + min_time_between_slotted_changes_ms: the minimum time between 2 changes of items.Item which have turned on use_slotted_sending. (see description of items.Item) Returns: OpenHAB: openHAB class instance. """ @@ -94,6 +98,9 @@ def __init__(self, base_url: str, self.__keep_event_daemon_running__ = False self.__wait_while_looping = threading.Event() self.eventListeners: typing.List[typing.Callable] = [] + self._last_slotted_modification_sent = datetime.fromtimestamp(0) + self._slotted_modification_lock = threading.RLock() + self.min_time_between_slotted_changes_ms = min_time_between_slotted_changes_ms if self.autoUpdate: self.__installSSEClient__() diff --git a/openhab/items.py b/openhab/items.py index 2ab225e..57f16a2 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -60,7 +60,10 @@ def create_or_update_item(self, group_names: typing.Optional[typing.List[str]] = None, group_type: typing.Optional[str] = None, function_name: typing.Optional[str] = None, - function_params: typing.Optional[typing.List[str]] = None + function_params: typing.Optional[typing.List[str]] = None, + auto_update: typing.Optional[bool] = True, + maxEchoToOpenhabMS=None, + use_slotted_sending: bool = False ) -> Item: """creates a new item in openhab if there is no item with name 'name' yet. if there is an item with 'name' already in openhab, the item gets updated with the infos provided. be aware that not provided fields will be deleted in openhab. @@ -82,6 +85,10 @@ def create_or_update_item(self, group_type (str): optional group_type. no documentation found function_name (str): optional function_name. no documentation found function_params (List of str): optional list of function Params. no documentation found + auto_update (bool): if True you will receive changes of the item from openhab. + maxEchoToOpenhabMS: when you change an item openhab gets informed about that change. Then openhab will inform all listeners (including ourself) about that change. The item will protect you from those echos if they happen within this timespan. + use_slotted_sending: when you send many consecutive changes very rapidly to openhab it might happen that openhab things or other bindings can not digest this that quickly and will therefore lose some of the changes. + if you set use_slotted_sending to True, the item will make sure that no more than min_time_between_slotted_changes_ms milliseconds specified at client.openHAB. Returns: the created Item @@ -103,6 +110,9 @@ def create_or_update_item(self, while True: try: result = self.get_item(name) + result.autoUpdate = auto_update + result.maxEchoToOpenhabMS = maxEchoToOpenhabMS + result.use_slotted_sending = use_slotted_sending return result except Exception as e: retrycounter -= 1 @@ -121,8 +131,7 @@ def create_or_update_item_async(self, group_names: typing.Optional[typing.List[str]] = None, group_type: typing.Optional[str] = None, function_name: typing.Optional[str] = None, - function_params: typing.Optional[typing.List[str]] = None - ) -> None: + function_params: typing.Optional[typing.List[str]] = None) -> None: """creates a new item in openhab if there is no item with name 'name' yet. if there is an item with 'name' already in openhab, the item gets updated with the infos provided. be aware that not provided fields will be deleted in openhab. consider to get the existing item via 'getItem' and then read out existing fields to populate the parameters here. @@ -143,6 +152,9 @@ def create_or_update_item_async(self, group_type (str): optional group_type. no documentation found function_name (str): optional function_name. no documentation found function_params (List of str): optional list of function Params. no documentation found + maxEchoToOpenhabMS: when you change an item openhab gets informed about that change. Then openhab will inform all listeners (including ourself) about that change. The item will protect you from those echos if they happen within this timespan. + use_slotted_sending: when you send many consecutive changes very rapidly to openhab it might happen that openhab things or other bindings can not digest this that quickly and will therefore lose some of the changes. + if you set use_slotted_sending to True, the item will make sure that no more than min_time_between_slotted_changes_ms milliseconds specified at client.openHAB. """ @@ -179,6 +191,7 @@ def create_or_update_item_async(self, json_body = json.dumps(paramdict) logging.getLogger(__name__).debug("about to create item with PUT request:{}".format(json_body)) + self.openHABClient.req_json_put('/items/{}'.format(name), json_data=json_body) def get_item(self, itemname,force_request_to_openhab:typing.Optional[bool]=False) -> Item: @@ -209,13 +222,17 @@ class Item: TYPENAME = "unknown" - def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto_update: typing.Optional[bool] = True, maxEchoToOpenhabMS = None) -> None: + def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto_update: typing.Optional[bool] = True, maxEchoToOpenhabMS = None, use_slotted_sending:bool = False) -> None: """Constructor. Args: openhab_conn (openhab.OpenHAB): openHAB object. json_data (dic): A dict converted from the JSON data returned by the openHAB server. + auto_update (bool): if True you will receive changes of the item from openhab. + maxEchoToOpenhabMS: when you change an item openhab gets informed about that change. Then openhab will inform all listeners (including ourself) about that change. The item will protect you from those echos if they happen within this timespan. + use_slotted_sending: when you send many consecutive changes very rapidly to openhab it might happen that openhab things or other bindings can not digest this that quickly and will therefore lose some of the changes. + if you set use_slotted_sending to True, the item will make sure that no more than min_time_between_slotted_changes_ms milliseconds specified at client.openHAB. """ self.openhab = openhab_conn self.autoUpdate = auto_update @@ -241,6 +258,7 @@ def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict, auto self.openhab.register_item(self) self.event_listeners: typing.Dict[typing.Callable[[openhab.items.Item, openhab.events.ItemEvent], None], Item.EventListener] = {} self.maxEchoToOpenhabMS = maxEchoToOpenhabMS + self.use_slotted_sending = use_slotted_sending if self.maxEchoToOpenhabMS is None: self.maxEchoToOpenhabMS = self.openhab.maxEchoToOpenhabMS self.change_sent_history = openhab.history.History(self.maxEchoToOpenhabMS/1000) #this History hold all recent changes sent to OH. We need this for subsequent events coming in to check if these incoming events are our own echo or not. @@ -714,6 +732,21 @@ def __str__(self) -> str: """String representation.""" return '<{0} - {1} : {2}>'.format(self.type_, self.name, self._state) + def wait_for_sending_slot(self): + try: + self.openhab._slotted_modification_lock.acquire() + now = datetime.utcnow() + wait_until = self.openhab._last_slotted_modification_sent + timedelta(milliseconds=self.openhab.min_time_between_slotted_changes_ms) + if wait_until > now: + wait_time = wait_until - now + wait_time_seconds = wait_time.seconds + wait_time.microseconds / 1000000 + self.logger.debug("waiting for {} seconds for sending slotted update to OH for item {}.".format(wait_time_seconds, self.name)) + time.sleep(wait_time_seconds) + self.openhab._last_slotted_modification_sent = datetime.utcnow() + + finally: + self.openhab._slotted_modification_lock.release() + def _update(self, value: typing.Any) -> None: """Updates the state of an item, input validation is expected to be already done. @@ -724,8 +757,10 @@ def _update(self, value: typing.Any) -> None: # noinspection PyTypeChecker self.change_sent_history.add(value) - self.logger.debug("sending update to OH for item {} with new value:{}".format(self.name,value)) + if self.use_slotted_sending: + self.wait_for_sending_slot() + self.logger.debug("sending update to OH for item {} with new value:{}".format(self.name, value)) self.openhab.req_put('/items/{}/state'.format(self.name), data=value) def update(self, value: typing.Any) -> None: @@ -782,8 +817,11 @@ def command(self, value: typing.Any) -> None: self._state = value self.change_sent_history.add(value) + if self.use_slotted_sending: + self.wait_for_sending_slot() self.openhab.req_post('/items/{}'.format(self.name), data=v) + unit_of_measure = "" if hasattr(self, "_unitOfMeasure"): unit_of_measure = self._unitOfMeasure diff --git a/tests/test_create_delete_items.py b/tests/test_create_delete_items.py index b20a453..e8cfd8f 100644 --- a/tests/test_create_delete_items.py +++ b/tests/test_create_delete_items.py @@ -27,6 +27,7 @@ import openhab.types import logging import random +import time import tests.testutil as testutil from datetime import datetime @@ -344,6 +345,52 @@ def test_register_all_items(item_factory:openhab.items.ItemFactory ,myopenhab:op finally: x2.delete() +def test_slotted_sending(item_factory:openhab.items.ItemFactory ,myopenhab:openhab.OpenHAB): + try: + myopenhab.min_time_between_slotted_changes_ms = 500 + testnumber_slotted: openhab.items.NumberItem = item_factory.create_or_update_item(name="test_slotted", data_type="Number",use_slotted_sending=True) + testnumber_not_slotted: openhab.items.NumberItem = item_factory.create_or_update_item(name="test_not_slotted", data_type="Number", use_slotted_sending=False) + testnumber_slotted.state=0.1 + started = datetime.now() + number_of_changes = 10 + log.info("starting to test if slotted items really wait and unslotted items do not wait.") + for i in range(1,number_of_changes+1): + log.info("about to send value {}".format(i)) + testnumber_slotted.state = i + log.info("did send value {}".format(i)) + testnumber_not_slotted.state = i + finished = datetime.now() + duration = finished-started + duration_in_seconds = duration.seconds+duration.microseconds/1000000 + min_expected_duration = number_of_changes * myopenhab.min_time_between_slotted_changes_ms / 1000 + log.info("the run took {} seconds".format(duration_in_seconds)) + log.info("we expect it should have taken at least {} seconds".format(min_expected_duration)) + log.info("we expect it should have taken less than {} seconds".format(2 * min_expected_duration)) + testutil.doassert(True,duration_in_seconds >= min_expected_duration,"duration") + testutil.doassert(True, duration_in_seconds < 2 * min_expected_duration, "duration") + + # now we test that it should not wait because there is enough time passed since last change + time.sleep(myopenhab.min_time_between_slotted_changes_ms/1000) + log.info("starting to test if slotted items do not wait if the last send was long enough back in time.") + started = datetime.now() + testnumber_slotted.state = 0 + finished = datetime.now() + duration = finished - started + duration_in_seconds = duration.seconds + duration.microseconds / 1000000 + max_expected_duration = myopenhab.min_time_between_slotted_changes_ms / 1000 + log.info("the run took {} seconds".format(duration_in_seconds)) + log.info("we expect it should have taken less than {} seconds".format(max_expected_duration)) + testutil.doassert(True, duration_in_seconds < myopenhab.min_time_between_slotted_changes_ms / 1000, "duration") + + + + finally: + myopenhab.min_time_between_slotted_changes_ms = 0 + + + + + myopenhab = openhab.OpenHAB(base_url, auto_update=False) @@ -351,6 +398,7 @@ def test_register_all_items(item_factory:openhab.items.ItemFactory ,myopenhab:op random.seed() mynameprefix = "x2_{}".format(random.randint(1, 1000)) -test_create_and_delete_items(myopenhab, mynameprefix) +#test_create_and_delete_items(myopenhab, mynameprefix) my_item_factory = openhab.items.ItemFactory(myopenhab) -test_register_all_items(item_factory=my_item_factory, myopenhab=myopenhab) +#test_register_all_items(item_factory=my_item_factory, myopenhab=myopenhab) +test_slotted_sending(item_factory=my_item_factory, myopenhab=myopenhab) From 3f1ff0cc8ad4b71f55737e136de595a03bd5cd88 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Wed, 3 Feb 2021 17:28:50 +0100 Subject: [PATCH 18/25] changed item Factory and item creation methods for slotted sending of changes --- openhab/client.py | 20 +++++++++++++++----- openhab/items.py | 10 ++++++++-- tests/test_create_delete_items.py | 2 ++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index 0221841..5835289 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -390,21 +390,31 @@ def fetch_all_items(self) -> typing.Dict[str, openhab.items.Item]: return items - def get_item(self, name: str, force_request_to_openhab:typing.Optional[bool]=False) -> openhab.items.Item: + def get_item(self, name: str, force_request_to_openhab:typing.Optional[bool]=False,auto_update: typing.Optional[bool] = True,maxEchoToOpenhabMS=None, use_slotted_sending: bool = False) -> openhab.items.Item: """Returns an item with its state and data_type as fetched from openHAB. Args: name (str): The name of the item to fetch from openHAB. + force_request_to_openhab (bool): ignore cached items and ask openhab + auto_update (bool): if True you will receive changes of the item from openhab. + maxEchoToOpenhabMS: when you change an item openhab gets informed about that change. Then openhab will inform all listeners (including ourself) about that change. The item will protect you from those echos if they happen within this timespan. + use_slotted_sending: when you send many consecutive changes very rapidly to openhab it might happen that openhab things or other bindings can not digest this that quickly and will therefore lose some of the changes. + if you set use_slotted_sending to True, the item will make sure that no more than min_time_between_slotted_changes_ms milliseconds specified at client.openHAB. Returns: Item: A corresponding Item class instance with the state of the requested item. """ if name in self.all_items and not force_request_to_openhab: - return self.all_items[name] - + item= self.all_items[name] + else: + json_data = self.get_item_raw(name) + item= self.json_to_item(json_data) + item.autoUpdate = auto_update + if maxEchoToOpenhabMS is not None: + item.maxEchoToOpenhabMS = maxEchoToOpenhabMS + item.use_slotted_sending = use_slotted_sending + return item - json_data = self.get_item_raw(name) - return self.json_to_item(json_data) def json_to_item(self, json_data: dict) -> openhab.items.Item: """This method takes as argument the RAW (JSON decoded) response for an openHAB item. diff --git a/openhab/items.py b/openhab/items.py index 57f16a2..3d5c12d 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -194,14 +194,20 @@ def create_or_update_item_async(self, self.openHABClient.req_json_put('/items/{}'.format(name), json_data=json_body) - def get_item(self, itemname,force_request_to_openhab:typing.Optional[bool]=False) -> Item: + def get_item(self, itemname,force_request_to_openhab:typing.Optional[bool]=False,auto_update: typing.Optional[bool] = True,maxEchoToOpenhabMS=None, use_slotted_sending: bool = False) -> Item: """get a existing openhab item Args: itemname (str): unique name of the item + force_request_to_openhab (bool): ignore cached items and ask openhab + auto_update (bool): if True you will receive changes of the item from openhab. + maxEchoToOpenhabMS: when you change an item openhab gets informed about that change. Then openhab will inform all listeners (including ourself) about that change. The item will protect you from those echos if they happen within this timespan. + use_slotted_sending: when you send many consecutive changes very rapidly to openhab it might happen that openhab things or other bindings can not digest this that quickly and will therefore lose some of the changes. + if you set use_slotted_sending to True, the item will make sure that no more than min_time_between_slotted_changes_ms milliseconds specified at client.openHAB. Returns: Item: the Item """ - return self.openHABClient.get_item(name=itemname,force_request_to_openhab=force_request_to_openhab) + item = self.openHABClient.get_item(name=itemname,force_request_to_openhab=force_request_to_openhab,auto_update=auto_update,maxEchoToOpenhabMS=maxEchoToOpenhabMS,use_slotted_sending=use_slotted_sending) + return item def fetch_all_items(self) -> typing.Dict[str, openhab.items.Item]: """Returns all items defined in openHAB. diff --git a/tests/test_create_delete_items.py b/tests/test_create_delete_items.py index e8cfd8f..cf39e69 100644 --- a/tests/test_create_delete_items.py +++ b/tests/test_create_delete_items.py @@ -386,6 +386,8 @@ def test_slotted_sending(item_factory:openhab.items.ItemFactory ,myopenhab:openh finally: myopenhab.min_time_between_slotted_changes_ms = 0 + testnumber_slotted.delete() + testnumber_not_slotted.delete() From 55477f3233246d07e6cc7fd09372dffecea20ee2 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Sat, 13 Feb 2021 11:11:25 +0100 Subject: [PATCH 19/25] openHAB3 added authentication to SSE through API token --- openhab/client.py | 36 ++++++++++++++++++++++++++++--- tests/test_create_delete_items.py | 23 ++++++++++++++------ tests/test_eventsubscription.py | 28 ++++++++++++++++++------ 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index 5835289..670bc26 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -32,10 +32,12 @@ import weakref import json from datetime import datetime,timedelta,timezone - +from enum import Enum from requests.auth import HTTPBasicAuth +from aiohttp.helpers import BasicAuth +from aiohttp import ClientSession import openhab.items @@ -43,12 +45,16 @@ import openhab.types import openhab.audio + __author__ = 'Georges Toth ' __license__ = 'AGPLv3+' class OpenHAB: """openHAB REST API client.""" + class Version(Enum): + OH2 = 2 + OH3 = 3 def __init__(self, base_url: str, username: typing.Optional[str] = None, @@ -56,6 +62,8 @@ def __init__(self, base_url: str, http_auth: typing.Optional[requests.auth.AuthBase] = None, timeout: typing.Optional[float] = None, auto_update: typing.Optional[bool] = False, + openhab_version:typing.Optional[Version] = Version.OH2, + http_headers_for_autoupdate: typing.Optional[typing.Dict[str,str]] = None, max_echo_to_openhab_ms: typing.Optional[int] = 800, min_time_between_slotted_changes_ms: typing.Optional[float]=0) -> None: """Class Constructor. @@ -77,13 +85,23 @@ def __init__(self, base_url: str, OpenHAB: openHAB class instance. """ self.base_url = base_url - self.events_url = "{}/events?topics=smarthome/items".format(base_url.strip('/')) + self.openhab_version = openhab_version + if self.openhab_version == OpenHAB.Version.OH2: + self.events_url = "{}/events?topics=smarthome/items".format(base_url.strip('/')) + elif self.openhab_version == OpenHAB.Version.OH3: + self.events_url = "{}/events".format(base_url.strip('/')) + else: + raise ValueError("Unknown Openhab Version specified") self.autoUpdate = auto_update self.session = requests.Session() self.session.headers['accept'] = 'application/json' self.registered_items = weakref.WeakValueDictionary() self.all_items:typing.Dict[str,openhab.items.Item] = {} self.http_buffersize = 4*1024*1024 + self.http_auth=http_auth + if http_headers_for_autoupdate is None: + http_headers_for_autoupdate = {} + self.sse_headers = http_headers_for_autoupdate if http_auth is not None: self.session.auth = http_auth @@ -97,6 +115,7 @@ def __init__(self, base_url: str, self.sseDaemon = None self.__keep_event_daemon_running__ = False self.__wait_while_looping = threading.Event() + #self.sse_session = ClientSession() self.eventListeners: typing.List[typing.Callable] = [] self._last_slotted_modification_sent = datetime.fromtimestamp(0) self._slotted_modification_lock = threading.RLock() @@ -195,10 +214,21 @@ def sse_client_handler(self): """the actual handler to receive Events from openhab """ self.logger.info("about to connect to Openhab Events-Stream.") + # this curls works: + # iobroker @ iobrokerserver: ~$ curl - X + # GET + # "http://10.10.20.85:8080/rest/events" - H + # "accept: */*" - H + # "Authorization: oh.ingenioushome.vFACRDQPY0Pf7JwgXZcqUz9rjrJYt0IZaeVobkrkLNfVx3mzhiWAdTqApWt3B2hL21z82eFj1VFbHqOMAAhQ" async def run_loop(): - async with sse_client.EventSource(self.events_url, read_bufsize=self.http_buffersize) as event_source: + + + + + async with sse_client.EventSource(self.events_url, headers=self.sse_headers, read_bufsize=self.http_buffersize) as event_source: try: + self.logger.info("starting Openhab - Event Daemon") async for event in event_source: if not self.__keep_event_daemon_running__: diff --git a/tests/test_create_delete_items.py b/tests/test_create_delete_items.py index cf39e69..3bd20f4 100644 --- a/tests/test_create_delete_items.py +++ b/tests/test_create_delete_items.py @@ -30,6 +30,7 @@ import time import tests.testutil as testutil from datetime import datetime +from requests.auth import HTTPBasicAuth log = logging.getLogger(__name__) logging.basicConfig(level=10, format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") @@ -39,9 +40,10 @@ log.info("iii") log.debug("ddddd") -base_url = 'http://10.10.20.81:8080/rest' -#base_url = 'http://localhost:8080/rest' - +TOKEN="" +base_url_oh2 = 'http://10.10.20.80:8080/rest' +base_url_oh3 = "http://10.10.20.85:8080/rest" +token = "in openhab admin web interface klick your created user (lower left corner). then create new API toker and copy it here" def test_create_and_delete_items(myopenhab: openhab.OpenHAB, nameprefix): log.info("starting tests 'create and delete items'") @@ -392,15 +394,24 @@ def test_slotted_sending(item_factory:openhab.items.ItemFactory ,myopenhab:openh +target_oh_version = 3 +myopenhab = None +if target_oh_version==2: + myopenhab = openhab.OpenHAB(base_url_oh2, openhab_version= openhab.OpenHAB.Version.OH2 ,auto_update=False) +elif target_oh_version==3: + http_auth = HTTPBasicAuth(token, "") + myopenhab = openhab.OpenHAB(base_url_oh3, openhab_version= openhab.OpenHAB.Version.OH3 , auto_update=False, http_auth=http_auth) + +myItemfactory = openhab.items.ItemFactory(myopenhab) +#myopenhab = openhab.OpenHAB(base_url, auto_update=False,http_auth=HTTPBasicAuth(TOKEN,"")) -myopenhab = openhab.OpenHAB(base_url, auto_update=False) keeprunning = True random.seed() mynameprefix = "x2_{}".format(random.randint(1, 1000)) -#test_create_and_delete_items(myopenhab, mynameprefix) +test_create_and_delete_items(myopenhab, mynameprefix) my_item_factory = openhab.items.ItemFactory(myopenhab) #test_register_all_items(item_factory=my_item_factory, myopenhab=myopenhab) -test_slotted_sending(item_factory=my_item_factory, myopenhab=myopenhab) +#test_slotted_sending(item_factory=my_item_factory, myopenhab=myopenhab) diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index 2847ad5..d819bc5 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -27,6 +27,8 @@ import logging import random import testutil +from requests.auth import HTTPBasicAuth + from datetime import datetime @@ -38,8 +40,13 @@ log.info("infomessage") log.debug("debugmessage") -base_url = 'http://localhost:8080/rest' -base_url = 'http://10.10.20.81:8080/rest' + +base_url_oh2 = 'http://10.10.20.80:8080/rest' +base_url_oh3 = "http://10.10.20.85:8080/rest" +token = "in openhab admin web interface klick your created user (lower left corner). then create new API toker and copy it here" +target_oh_version = 3 + + expected_state = None state_correct_count = 0 @@ -56,6 +63,18 @@ do_breakpoint = False +if target_oh_version==2: + headers = {"Authorization": token} + myopenhab = openhab.OpenHAB(base_url_oh2, openhab_version= openhab.OpenHAB.Version.OH2 ,auto_update=True,http_auth=HTTPBasicAuth(token,""),http_headers_for_autoupdate=headers) +elif target_oh_version==3: + headers = {"Authorization": "{}".format(token)} + myopenhab = openhab.OpenHAB(base_url_oh3, openhab_version= openhab.OpenHAB.Version.OH3 , auto_update=True, http_auth=HTTPBasicAuth(token, ""), http_headers_for_autoupdate=headers) +myItemfactory = openhab.items.ItemFactory(myopenhab) + +random.seed() +namesuffix = "_{}".format(random.randint(1, 1000)) + + def on_item_state(item: openhab.items.Item, event: openhab.events.ItemStateEvent): global state_correct_count log.info("########################### STATE arrived for {itemname} : eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state})".format( @@ -98,11 +117,6 @@ def on_item_command(item: openhab.items.Item, event: openhab.events.ItemCommandE command_correct_count += 1 -myopenhab = openhab.OpenHAB(base_url, auto_update=True) -myItemfactory = openhab.items.ItemFactory(myopenhab) - -random.seed() -namesuffix = "_{}".format(random.randint(1, 1000)) def create_event_data(event_type: openhab.events.ItemEventType, itemname, payload): From 2072edea9c1415b262fcc3ca4982b67d6e989b4a Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Wed, 10 Mar 2021 00:54:27 +0100 Subject: [PATCH 20/25] added widgets more detailed logging again --- openhab/client.py | 43 ++++++++---- openhab/items.py | 2 +- openhab/ui.py | 113 ++++++++++++++++++++++++++++++++ tests/test_eventsubscription.py | 104 ++++++++++++++++++++++------- tests/test_widgets.py | 72 ++++++++++++++++++++ 5 files changed, 299 insertions(+), 35 deletions(-) create mode 100644 openhab/ui.py create mode 100644 tests/test_widgets.py diff --git a/openhab/client.py b/openhab/client.py index 670bc26..f92ae8a 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -162,12 +162,14 @@ def _parse_event(self, event_data: typing.Dict) -> None: if "type" in event_data: event_reason = event_data["type"] + log.debug("received Event: {}".format(event_data)) if event_reason in ["ItemCommandEvent", "ItemStateEvent", "ItemStateChangedEvent"]: item_name = event_data["topic"].split("/")[-2] event_data = json.loads(event_data["payload"]) raw_event = openhab.events.RawItemEvent(item_name=item_name, event_type=event_reason, content=event_data) + log.debug("about to inform listeners") self._inform_event_listeners(raw_event) if item_name in self.registered_items: @@ -214,6 +216,8 @@ def sse_client_handler(self): """the actual handler to receive Events from openhab """ self.logger.info("about to connect to Openhab Events-Stream.") + ct = threading.currentThread() + ct.name = "sse_client_handler started at {}".format(datetime.now()) # this curls works: # iobroker @ iobrokerserver: ~$ curl - X # GET @@ -249,7 +253,8 @@ async def run_loop(): except openhab.types.TypeNotImplementedError as e: self.logger.warning("received unknown datatye '{}' for item '{}'".format(e.datatype,e.itemname)) except Exception as e: - self.logger.exception(e) + self.logger.warning("problem receiving event: '{}' ".format(e)) + self.sseDaemon = None @@ -338,7 +343,7 @@ def req_get(self, uri_path: str) -> typing.Any: self._check_req_return(r) return r.json() - def req_post(self, uri_path: str, data: typing.Optional[dict] = None) -> None: + def req_post(self, uri_path: str, data: typing.Optional[dict] = None, headers: typing.Optional[dict]=None) -> None: """Helper method for initiating a HTTP POST request. Besides doing the actual request, it also checks the return value and returns the resulting decoded @@ -351,10 +356,12 @@ def req_post(self, uri_path: str, data: typing.Optional[dict] = None) -> None: Returns: None: No data is returned. """ - r = self.session.post(self.base_url + uri_path, data=data, headers={'Content-Type': 'text/plain'}, timeout=self.timeout) + if headers is None: + headers = {'Content-Type': 'text/plain'} + r = self.session.post(self.base_url + uri_path, data=data, headers=headers, timeout=self.timeout) self._check_req_return(r) - def req_json_put(self, uri_path: str, json_data: str = None) -> None: + def req_json_put(self, uri_path: str, json_data: str = None, headers: typing.Optional[dict]=None) -> None: """Helper method for initiating a HTTP PUT request. Besides doing the actual request, it also checks the return value and returns the resulting decoded @@ -368,11 +375,12 @@ def req_json_put(self, uri_path: str, json_data: str = None) -> None: Returns: None: No data is returned. """ - - r = self.session.put(self.base_url + uri_path, data=json_data, headers={'Content-Type': 'application/json', "Accept": "application/json"}, timeout=self.timeout) + if headers is None: + headers = {'Content-Type': 'application/json', "Accept": "application/json"} + r = self.session.put(self.base_url + uri_path, data=json_data, headers=headers, timeout=self.timeout) self._check_req_return(r) - def req_del(self, uri_path: str) -> None: + def req_del(self, uri_path: str, headers: typing.Optional[dict]=None) -> None: """Helper method for initiating a HTTP DELETE request. Besides doing the actual request, it also checks the return value and returns the resulting decoded @@ -385,10 +393,12 @@ def req_del(self, uri_path: str) -> None: Returns: None: No data is returned. """ - r = self.session.delete(self.base_url + uri_path, headers={"Accept": "application/json"}) + if headers is None: + headers = {"Accept": "application/json"} + r = self.session.delete(self.base_url + uri_path, headers=headers) self._check_req_return(r) - def req_put(self, uri_path: str, data: typing.Optional[dict] = None) -> None: + def req_put(self, uri_path: str, data: typing.Optional[dict] = None, headers: typing.Optional[dict]=None) -> None: """Helper method for initiating a HTTP PUT request. Besides doing the actual request, it also checks the return value and returns the resulting decoded @@ -401,7 +411,9 @@ def req_put(self, uri_path: str, data: typing.Optional[dict] = None) -> None: Returns: None: No data is returned. """ - r = self.session.put(self.base_url + uri_path, data=data, headers={'Content-Type': 'text/plain'}, timeout=self.timeout) + if headers is None: + headers = {'Content-Type': 'text/plain'} + r = self.session.put(self.base_url + uri_path, data=data, headers=headers, timeout=self.timeout) self._check_req_return(r) # fetch all items @@ -598,7 +610,7 @@ def _get_all_voiceinterpreters_raw(self) -> typing.Dict: return self.req_get('/voice/interpreters') def say(self, text: str, audiosinkid: str, voiceid: str): - log.info("sending say command to OH for voiceid:'{}', audiosinkid:'{}'".format(voiceid,audiosinkid)) + self.logger.info("sending say command to OH for voiceid:'{}', audiosinkid:'{}'".format(voiceid,audiosinkid)) url=self.base_url + "/voice/say/?voiceid={voiceid}&sinkid={sinkid}".format(voiceid=requests.utils.quote(voiceid), sinkid=requests.utils.quote(audiosinkid)) r = self.session.post(url, data=text, headers={'Accept': 'application/json'}, timeout=self.timeout) self._check_req_return(r) @@ -608,6 +620,15 @@ def interpret(self, text: str, voiceinterpreterid: str): r = self.session.post(url, data=text, headers={'Accept': 'application/json'}, timeout=self.timeout) self._check_req_return(r) +# UI + def _get_all_widgets_raw(self): + url = "/ui/components/ui%3Awidget" + return self.req_get(url) + + def _get_widget_raw(self, component_UID:str): + url = "/ui/components/ui%3Awidget/{componentUID}".format(componentUID=component_UID) + return self.req_get(url) + # noinspection PyPep8Naming diff --git a/openhab/items.py b/openhab/items.py index 3d5c12d..fc9bb18 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -588,7 +588,7 @@ def _process_external_event(self, raw_event: openhab.events.RawItemEvent): """ if not self.autoUpdate: return - self.logger.debug("processing external event:{}".format(raw_event)) + self.logger.debug("processing external event:{}".format(raw_event[:200])) if raw_event.event_type == openhab.events.ItemCommandEvent.type: event = self._parse_external_command_event(raw_event) diff --git a/openhab/ui.py b/openhab/ui.py new file mode 100644 index 0000000..5fb1bf5 --- /dev/null +++ b/openhab/ui.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +"""python classes for manipulating ui elements in openhab""" + +# +# Alexey Grubauer (c) 2021 +# +# python-openhab is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# python-openhab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with python-openhab. If not, see . +# + +# pylint: disable=bad-indentation +from __future__ import annotations +import typing +from dataclasses import dataclass +import openhab.types +import dateutil.parser +import datetime + +class WidgetFactory: + """A factory to get UI widgets from Openhab, create new or delete existing widgets in openHAB""" + + def __init__(self, openhab_client: openhab.client.OpenHAB): + """Constructor. + + Args: + openhab_client (openhab.OpenHAB): openHAB object. + + """ + self.openHABClient = openhab_client + + def get_widget(self, uid:str) -> Widget: + url = "/ui/components/ui%3Awidget/{componentUID}".format(componentUID=uid) + result_dict = self.openHABClient.req_get(url) + widget = Widget(self.openHABClient, result_dict) + return widget + + def exists_widget(self, uid:str) -> bool: + try: + existing_widget = self.get_widget(uid) + return True + except: + return False + + def create_widget(self, uid:str, widget_code: typing.Optional[typing.Dict]=None): + if self.exists_widget(uid): + raise KeyError("UID '{}' already exists".format(uid)) + + if widget_code is None: + widget_code={"uid":uid, "timestamp":datetime.datetime.now().strftime("%b %d, %Y, %I:%M:%S %p")} + + result = Widget(openhab_conn=self.openHABClient, widget=widget_code, loaded=False) + result.uid = uid + return result + + def delete_widget(self, uid:str) -> None: + try: + self.openHABClient.req_del("/ui/components/ui%3Awidget/{componentUID}".format(componentUID=uid)) + except: + raise KeyError("UID '{}' does not exist".format(uid)) + + + +class Widget(): + def __init__(self, openhab_conn: openhab.client.OpenHAB, widget:typing.Dict, loaded:typing.Optional[bool]=True): + self.openhab = openhab_conn + self._uid: str = widget["uid"] + self._timestamp: datetime = dateutil.parser.parse(widget["timestamp"]) + self.code: typing.Dict = widget + self._loaded = loaded + + @property + def uid(self) -> str: + return self._uid + + @uid.setter + def uid(self, uid: str) -> None: + if uid != self._uid: + self._changed_uid = True + self._uid = uid + self.code["uid"] = uid + + + @property + def timestamp(self) -> datetime: + return self._timestamp + + @timestamp.setter + def timestamp(self, timestamp: datetime) -> None: + self._timestamp = timestamp + self.code["timestamp"] = timestamp.strftime("%b %d, %Y, %I:%M:%S %p") + + def __str__(self): + return str(self.code) + + + def delete(self): + self.openhab.req_del("/ui/components/ui%3Awidget/{componentUID}".format(componentUID=self.uid), headers = {'Content-Type': '*/*'}) + + def save(self): + if self._loaded: + self.openhab.req_put("/ui/components/ui%3Awidget/{componentUID}".format(componentUID=self.uid), data=str(self.code), headers = {'Content-Type': 'application/json'}) + else: + self.openhab.req_post("/ui/components/ui%3Awidget".format(componentUID=self.uid), data=str(self.code), headers = {'Content-Type': 'application/json'}) diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index d819bc5..412832a 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -43,13 +43,17 @@ base_url_oh2 = 'http://10.10.20.80:8080/rest' base_url_oh3 = "http://10.10.20.85:8080/rest" + token = "in openhab admin web interface klick your created user (lower left corner). then create new API toker and copy it here" +token=OPENHAB_AUTH_TOKEN_PRODUCTION + target_oh_version = 3 expected_state = None state_correct_count = 0 +state_count = 0 expected_command = None command_correct_count = 0 @@ -57,7 +61,7 @@ expected_new_state = None expected_old_state = None state_changed_correct_count = 0 - +sleeptime_in_event_listener = 0 count = 0 do_breakpoint = False @@ -79,6 +83,7 @@ def on_item_state(item: openhab.items.Item, event: openhab.events.ItemStateEvent global state_correct_count log.info("########################### STATE arrived for {itemname} : eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state})".format( itemname=event.item_name, eventvalue=event.value, eventvalueraw=event.value_raw, item_state=item._state, itemstate=item.state)) + time.sleep(sleeptime_in_event_listener) if expected_state is not None: if isinstance(event.value, datetime): testutil.doassert(expected_state, event.value.replace(tzinfo=None), "stateEvent item {} ".format(item.name)) @@ -90,6 +95,7 @@ def on_item_state(item: openhab.items.Item, event: openhab.events.ItemStateEvent def on_item_statechange(item: openhab.items.Item, event: openhab.events.ItemStateChangedEvent): global state_changed_correct_count log.info("########################### STATE of {itemname} CHANGED from {oldvalue} to {newvalue} (items state: {new_value_item}.".format(itemname=event.item_name, oldvalue=event.old_value, newvalue=event.value, new_value_item=item.state)) + time.sleep(sleeptime_in_event_listener) if expected_new_state is not None: if isinstance(event.value, datetime): testutil.doassert(expected_new_state, event.value.replace(tzinfo=None), "state changed event item {} value".format(item.name)) @@ -109,6 +115,7 @@ def on_item_command(item: openhab.items.Item, event: openhab.events.ItemCommandE global command_correct_count log.info("########################### COMMAND arrived for {itemname} : eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state})".format( itemname=event.item_name, eventvalue=event.value, eventvalueraw=event.value_raw, item_state=item._state, itemstate=item.state)) + time.sleep(sleeptime_in_event_listener) if expected_command is not None: if isinstance(event.value, datetime): testutil.doassert(expected_command, event.value.replace(tzinfo=None), "command event item {}".format(item.name)) @@ -254,6 +261,56 @@ def test_number_item(): pass testitem.delete() +def test_events_stress(): + created_itemname = "test_eventSubscription_numberitem_A{}".format(namesuffix) + def on_item_state_stress(item: openhab.items.Item, event: openhab.events.ItemStateEvent): + global state_correct_count + global state_count + + log.info("########################### STATE arrived for {itemname} : eventvalue:{eventvalue}(event value raraw:{eventvalueraw}) (itemstate:{itemstate},item_state:{item_state})".format( + itemname=event.item_name, eventvalue=event.value, eventvalueraw=event.value_raw, item_state=item._state, itemstate=item.state)) + if item.name == created_itemname: + state_count += 1 + time.sleep(sleeptime_in_event_listener) + if expected_state is not None: + if isinstance(event.value, datetime): + testutil.doassert(expected_state, event.value.replace(tzinfo=None), "stateEvent item {} ".format(item.name)) + else: + testutil.doassert(expected_state, event.value, "stateEvent item {} ".format(item.name)) + state_correct_count += 1 + + try: + testitem: openhab.items.NumberItem = myItemfactory.create_or_update_item(name=created_itemname, data_type=openhab.items.NumberItem) + testitem.add_event_listener(openhab.events.ItemStateEventType, on_item_state_stress, also_get_my_echos_from_openhab=True) + #testitem.add_event_listener(openhab.events.ItemCommandEventType, on_item_command, also_get_my_echos_from_openhab=False) + #testitem.add_event_listener(openhab.events.ItemStateChangedEventType, on_item_statechange, also_get_my_echos_from_openhab=False) + + expected_state = None + sleeptime_in_event_listener = 0.5 + state_correct_count = 0 + global state_count + state_count = 0 + number_of_messages = 30 + for i in range(1,number_of_messages+1): + log.info("sending state:{}".format(i)) + testitem.state = i + time.sleep(sleeptime_in_event_listener / 2) + + # + # + # + # sending_state = 123.4 + # expected_new_state = expected_state = sending_state + # expected_command = sending_state + # expected_old_state = None + # + # testitem.state = sending_state + time.sleep(((number_of_messages /2)+3)*sleeptime_in_event_listener) + testutil.doassert(number_of_messages, state_count) + + finally: + pass + testitem.delete() def test_string_item(): global expected_state @@ -1033,28 +1090,29 @@ def on_item_state(item: openhab.items.Item, event: openhab.events.ItemStateEvent testitem.delete() -time.sleep(3) -log.info("stopping daemon") -myopenhab.stop_receiving_events() -log.info("stopped daemon") -time.sleep(1) -testitem: openhab.items.RollershutterItem = myItemfactory.create_or_update_item(name="dummy_test_item_{}".format(namesuffix), data_type=openhab.items.RollershutterItem) -time.sleep(1) -testitem.delete() -time.sleep(1) -log.info("restarting daemon") -myopenhab.start_receiving_events() -log.info("restarted daemon") - -test_number_item() -test_string_item() -test_datetime_item() -test_player_item() -test_switch_item() -test_contact_item() -test_dimmer_item() -test_rollershutter_item() -test_echos_for_rollershutter_item() +# time.sleep(3) +# log.info("stopping daemon") +# myopenhab.stop_receiving_events() +# log.info("stopped daemon") +# time.sleep(1) +# testitem: openhab.items.RollershutterItem = myItemfactory.create_or_update_item(name="dummy_test_item_{}".format(namesuffix), data_type=openhab.items.RollershutterItem) +# time.sleep(1) +# testitem.delete() +# time.sleep(1) +# log.info("restarting daemon") +# myopenhab.start_receiving_events() +# log.info("restarted daemon") + +# test_number_item() +# test_string_item() +# test_datetime_item() +# test_player_item() +# test_switch_item() +# test_contact_item() +# test_dimmer_item() +# test_rollershutter_item() +# test_echos_for_rollershutter_item() +test_events_stress() log.info("tests for events finished successfully") myopenhab.loop_for_events() diff --git a/tests/test_widgets.py b/tests/test_widgets.py new file mode 100644 index 0000000..57ccb3a --- /dev/null +++ b/tests/test_widgets.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +"""tests for creating and deletion of items """ + +# +# Alexey Grubauer (c) 2020-present +# +# python-openhab is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# python-openhab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with python-openhab. If not, see . +# +# pylint: disable=bad-indentation + +from __future__ import annotations +import openhab +import openhab.events +import openhab.items as items +import openhab.ui as ui +import openhab.types +import logging +import time +from requests.auth import HTTPBasicAuth + +log = logging.getLogger(__name__) +logging.basicConfig(level=10, format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") + +log.error("xx") +log.warning("www") +log.info("iii") +log.debug("ddddd") + +base_url_oh3 = 'http://10.10.20.85:8080/rest' +token = "in openhab admin web interface klick your created user (lower left corner). then create new API toker and copy it here" + +def test_widgets(myopenhab: openhab.OpenHAB): + log.info("starting tests 'test widgets'") + # all_widgets=myopenhab._get_all_widgets_raw() + # log.info("all widgets:{}".format(all_widgets)) + wf = ui.WidgetFactory(myopenhab) + awidget = wf.get_widget("test_widget1") + + log.info("test_widget1 widgets:{}".format(awidget)) + awidget.code["tags"].append("testtag") + log.info("test_widget1.1 widgets:{}".format(awidget)) + awidget.save() + time.sleep(0.2) + + + aw2=wf.create_widget("test_widget2",awidget.code) + aw2.save() + time.sleep(0.2) + wf.delete_widget("test_widget2") + + + + + + +headers = {"Authorization": "{}".format(token)} +myopenhab = openhab.OpenHAB(base_url_oh3, openhab_version= openhab.OpenHAB.Version.OH3 , auto_update=True, http_auth=HTTPBasicAuth(token, ""), http_headers_for_autoupdate=headers) + + +test_widgets(myopenhab) + From 9f3a21f20ff4f9736e2c62852e9fd96731f12ab0 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Wed, 10 Mar 2021 00:55:21 +0100 Subject: [PATCH 21/25] added widgets more detailed logging again --- tests/test_eventsubscription.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index 412832a..a0e8948 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -43,9 +43,8 @@ base_url_oh2 = 'http://10.10.20.80:8080/rest' base_url_oh3 = "http://10.10.20.85:8080/rest" - token = "in openhab admin web interface klick your created user (lower left corner). then create new API toker and copy it here" -token=OPENHAB_AUTH_TOKEN_PRODUCTION + target_oh_version = 3 From c3a331b13d2b98743f5585946aa0400c95ac0207 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Wed, 10 Mar 2021 14:04:43 +0100 Subject: [PATCH 22/25] decoupled receiving of events from processing of events through an additional thread for processing(dispatching) of events. --- openhab/client.py | 49 +++++++++++++++++++++++++++++++-- openhab/items.py | 2 +- tests/test_eventsubscription.py | 47 +++++++++++++++---------------- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index f92ae8a..a12f8d7 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -113,6 +113,7 @@ def __init__(self, base_url: str, self.logger = logging.getLogger(__name__) self.sseDaemon = None + self.sse_event_dispatcher_daemon = None self.__keep_event_daemon_running__ = False self.__wait_while_looping = threading.Event() #self.sse_session = ClientSession() @@ -120,6 +121,11 @@ def __init__(self, base_url: str, self._last_slotted_modification_sent = datetime.fromtimestamp(0) self._slotted_modification_lock = threading.RLock() self.min_time_between_slotted_changes_ms = min_time_between_slotted_changes_ms + self.incoming_events = [] + self.incoming_events_rlock = threading.RLock() + self.__keep_event_dispatcher_running__ = False + self.__dispatcher_is_running = False + self.__sse_event_received = threading.Event() if self.autoUpdate: self.__installSSEClient__() @@ -162,7 +168,7 @@ def _parse_event(self, event_data: typing.Dict) -> None: if "type" in event_data: event_reason = event_data["type"] - log.debug("received Event: {}".format(event_data)) + if event_reason in ["ItemCommandEvent", "ItemStateEvent", "ItemStateChangedEvent"]: item_name = event_data["topic"].split("/")[-2] @@ -212,6 +218,33 @@ def remove_event_listener(self, listener: typing.Optional[typing.Callable[[openh elif listener in self.eventListeners: self.eventListeners.remove(listener) + def event_dispatcher_thread(self): + ct = threading.currentThread() + ct.name = "sse_event_dispatcher (started at {})".format(datetime.now()) + self.__dispatcher_is_running = True + while True: + if not self.__keep_event_dispatcher_running__: + self.__dispatcher_is_running = False + return + event_data = None + try: + self.incoming_events_rlock.acquire() + try: + if len(self.incoming_events) > 0: + event_data = self.incoming_events.pop(0) + finally: + self.incoming_events_rlock.release() + if event_data is None: + #no data available in Q, so we wait 10seconds for a wakeup event + self.__sse_event_received.wait(10) + self.__sse_event_received.clear() + else: + self.logger.debug("dispatching Event: {}...".format(str(event_data)[:300])) + self._parse_event(event_data) + except Exception as e: + self.logger.warning("problem dispatching event: '{}' ".format(e)) + + def sse_client_handler(self): """the actual handler to receive Events from openhab """ @@ -239,7 +272,14 @@ async def run_loop(): self.sseDaemon = None return event_data = json.loads(event.data) - self._parse_event(event_data) + self.logger.debug("received Event: {}...".format(str(event_data)[:300])) + self.incoming_events_rlock.acquire() + try: + self.incoming_events.append(event_data) + finally: + self.incoming_events_rlock.release() + self.__sse_event_received.set() # inform dispatcher thread about the new event + #self._parse_event(event_data) except ConnectionError as exception: self.logger.error("connection error") @@ -308,13 +348,16 @@ def start_receiving_events(self): def __installSSEClient__(self) -> None: """ installs an event Stream to receive all Item events""" - + self.__keep_event_dispatcher_running__ = True self.__keep_event_daemon_running__ = True self.keep_running = True + self.sse_event_dispatcher_daemon = threading.Thread(target=self.event_dispatcher_thread, args=(), daemon=True) self.sseDaemon = threading.Thread(target=self.sse_client_handler, args=(), daemon=True) self.logger.info("about to connect to Openhab Events-Stream.") + self.sse_event_dispatcher_daemon.start() self.sseDaemon.start() + self.logger.info("connected to Openhab Events-Stream.") def stop_looping(self): """ method to reactivate the thread which went into the loop_for_events loop. diff --git a/openhab/items.py b/openhab/items.py index fc9bb18..938ddf4 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -588,7 +588,7 @@ def _process_external_event(self, raw_event: openhab.events.RawItemEvent): """ if not self.autoUpdate: return - self.logger.debug("processing external event:{}".format(raw_event[:200])) + self.logger.debug("processing external event:{}".format(str(raw_event)[:300])) if raw_event.event_type == openhab.events.ItemCommandEvent.type: event = self._parse_external_command_event(raw_event) diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py index a0e8948..1610595 100644 --- a/tests/test_eventsubscription.py +++ b/tests/test_eventsubscription.py @@ -44,7 +44,8 @@ base_url_oh2 = 'http://10.10.20.80:8080/rest' base_url_oh3 = "http://10.10.20.85:8080/rest" token = "in openhab admin web interface klick your created user (lower left corner). then create new API toker and copy it here" - +OPENHAB_AUTH_TOKEN_PRODUCTION = "oh.ingenioushome.vFACRDQPY0Pf7JwgXZcqUz9rjrJYt0IZaeVobkrkLNfVx3mzhiWAdTqApWt3B2hL21z82eFj1VFbHqOMAAhQ" +token=OPENHAB_AUTH_TOKEN_PRODUCTION target_oh_version = 3 @@ -1089,28 +1090,28 @@ def on_item_state(item: openhab.items.Item, event: openhab.events.ItemStateEvent testitem.delete() -# time.sleep(3) -# log.info("stopping daemon") -# myopenhab.stop_receiving_events() -# log.info("stopped daemon") -# time.sleep(1) -# testitem: openhab.items.RollershutterItem = myItemfactory.create_or_update_item(name="dummy_test_item_{}".format(namesuffix), data_type=openhab.items.RollershutterItem) -# time.sleep(1) -# testitem.delete() -# time.sleep(1) -# log.info("restarting daemon") -# myopenhab.start_receiving_events() -# log.info("restarted daemon") - -# test_number_item() -# test_string_item() -# test_datetime_item() -# test_player_item() -# test_switch_item() -# test_contact_item() -# test_dimmer_item() -# test_rollershutter_item() -# test_echos_for_rollershutter_item() +time.sleep(3) +log.info("stopping daemon") +myopenhab.stop_receiving_events() +log.info("stopped daemon") +time.sleep(1) +testitem: openhab.items.RollershutterItem = myItemfactory.create_or_update_item(name="dummy_test_item_{}".format(namesuffix), data_type=openhab.items.RollershutterItem) +time.sleep(1) +testitem.delete() +time.sleep(1) +log.info("restarting daemon") +myopenhab.start_receiving_events() +log.info("restarted daemon") + +test_number_item() +test_string_item() +test_datetime_item() +test_player_item() +test_switch_item() +test_contact_item() +test_dimmer_item() +test_rollershutter_item() +test_echos_for_rollershutter_item() test_events_stress() log.info("tests for events finished successfully") From 614b3478893d5d030708d790f1454c9ed49e9963 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Wed, 10 Mar 2021 17:23:50 +0100 Subject: [PATCH 23/25] changes widget behaviour --- openhab/ui.py | 26 +++++++++++------- tests/test_widgets.py | 62 +++++++++++++++++++++++++++++++++---------- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/openhab/ui.py b/openhab/ui.py index 5fb1bf5..9dc1d27 100644 --- a/openhab/ui.py +++ b/openhab/ui.py @@ -25,6 +25,7 @@ import openhab.types import dateutil.parser import datetime +import json class WidgetFactory: """A factory to get UI widgets from Openhab, create new or delete existing widgets in openHAB""" @@ -73,41 +74,46 @@ def delete_widget(self, uid:str) -> None: class Widget(): def __init__(self, openhab_conn: openhab.client.OpenHAB, widget:typing.Dict, loaded:typing.Optional[bool]=True): self.openhab = openhab_conn - self._uid: str = widget["uid"] - self._timestamp: datetime = dateutil.parser.parse(widget["timestamp"]) self.code: typing.Dict = widget self._loaded = loaded + self._changed_uid = False @property def uid(self) -> str: - return self._uid + return self.code["uid"] + @uid.setter def uid(self, uid: str) -> None: - if uid != self._uid: + if uid != self.uid: self._changed_uid = True - self._uid = uid self.code["uid"] = uid + @property def timestamp(self) -> datetime: - return self._timestamp + return dateutil.parser.parse(self.code["timestamp"]) @timestamp.setter def timestamp(self, timestamp: datetime) -> None: - self._timestamp = timestamp + self.code["timestamp"] = timestamp.strftime("%b %d, %Y, %I:%M:%S %p") def __str__(self): return str(self.code) + def set_code(self, code:typing.Dict): + self.code = code def delete(self): self.openhab.req_del("/ui/components/ui%3Awidget/{componentUID}".format(componentUID=self.uid), headers = {'Content-Type': '*/*'}) def save(self): - if self._loaded: - self.openhab.req_put("/ui/components/ui%3Awidget/{componentUID}".format(componentUID=self.uid), data=str(self.code), headers = {'Content-Type': 'application/json'}) + code_str = json.dumps(self.code) + if self._loaded and not self._changed_uid: + #self.openhab.req_put("/ui/components/ui%3Awidget/{componentUID}".format(componentUID=self.uid), data=str(self.code), headers = {'Content-Type': 'application/json'}) + self.openhab.req_put("/ui/components/ui%3Awidget/{componentUID}".format(componentUID=self.uid), data=code_str, headers={'Content-Type': 'application/json'}) else: - self.openhab.req_post("/ui/components/ui%3Awidget".format(componentUID=self.uid), data=str(self.code), headers = {'Content-Type': 'application/json'}) + #self.openhab.req_post("/ui/components/ui%3Awidget".format(componentUID=self.uid), data=str(self.code), headers = {'Content-Type': 'application/json'}) + self.openhab.req_post("/ui/components/ui%3Awidget".format(componentUID=self.uid), data=code_str, headers={'Content-Type': 'application/json'}) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 57ccb3a..6dfe0ac 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -28,6 +28,7 @@ import logging import time from requests.auth import HTTPBasicAuth +import yaml log = logging.getLogger(__name__) logging.basicConfig(level=10, format="%(levelno)s:%(asctime)s - %(message)s - %(name)s - PID:%(process)d - THREADID:%(thread)d - %(levelname)s - MODULE:%(module)s, -FN:%(filename)s -FUNC:%(funcName)s:%(lineno)d") @@ -38,26 +39,59 @@ log.debug("ddddd") base_url_oh3 = 'http://10.10.20.85:8080/rest' -token = "in openhab admin web interface klick your created user (lower left corner). then create new API toker and copy it here" +#token = "in openhab admin web interface klick your created user (lower left corner). then create new API toker and copy it here" +token=OPENHAB_AUTH_TOKEN_PRODUCTION = "oh.ingenioushome.vFACRDQPY0Pf7JwgXZcqUz9rjrJYt0IZaeVobkrkLNfVx3mzhiWAdTqApWt3B2hL21z82eFj1VFbHqOMAAhQ" def test_widgets(myopenhab: openhab.OpenHAB): log.info("starting tests 'test widgets'") + wf = ui.WidgetFactory(myopenhab) # all_widgets=myopenhab._get_all_widgets_raw() # log.info("all widgets:{}".format(all_widgets)) - wf = ui.WidgetFactory(myopenhab) - awidget = wf.get_widget("test_widget1") - - log.info("test_widget1 widgets:{}".format(awidget)) - awidget.code["tags"].append("testtag") - log.info("test_widget1.1 widgets:{}".format(awidget)) - awidget.save() - time.sleep(0.2) - - aw2=wf.create_widget("test_widget2",awidget.code) - aw2.save() - time.sleep(0.2) - wf.delete_widget("test_widget2") + # awidget = wf.get_widget("test_widget1") + # + # log.info("test_widget1 widgets:{}".format(awidget)) + # awidget.code["tags"].append("testtag") + # log.info("test_widget1.1 widgets:{}".format(awidget)) + # awidget.save() + # time.sleep(0.2) + # + # + # aw2=wf.create_widget("test_widget2",awidget.code) + # aw2.save() + # time.sleep(0.2) + # wf.delete_widget("test_widget2") + + ######### create new widget + widget_code="""uid: test_widget_created +tags: + - wwww + - testtag xx +props: + parameters: + - description: A text prop + label: Prop 1 + name: prop1 + required: false + type: TEXT + - context: item + description: An item to control + label: Item + name: item + required: false + type: TEXT + parameterGroups: [] +timestamp: Mar 10, 2021, 3:14:22 PM +component: f7-card +config: + title: '=(props.item) ? "State of " + props.item : "Set props to test!"' + footer: =props.prop1 + content: =items[props.item].displayState || items[props.item].state +""" + + code = yaml.unsafe_load(widget_code) + widget = wf.create_widget("created_widget_for_test",code) + widget.save() From 7ab6e9c45930ca182328e2c7be8948dc2c5729f6 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Sun, 14 Mar 2021 18:33:05 +0100 Subject: [PATCH 24/25] logging Q len --- openhab/client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openhab/client.py b/openhab/client.py index a12f8d7..c47a530 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -227,10 +227,12 @@ def event_dispatcher_thread(self): self.__dispatcher_is_running = False return event_data = None + qlen = 0 try: self.incoming_events_rlock.acquire() try: - if len(self.incoming_events) > 0: + qlen=len(self.incoming_events) + if qlen > 0: event_data = self.incoming_events.pop(0) finally: self.incoming_events_rlock.release() @@ -239,7 +241,7 @@ def event_dispatcher_thread(self): self.__sse_event_received.wait(10) self.__sse_event_received.clear() else: - self.logger.debug("dispatching Event: {}...".format(str(event_data)[:300])) + self.logger.debug("dispatching Event (items in eventQ:{}), Event: {}...".format(qlen,str(event_data)[:300])) self._parse_event(event_data) except Exception as e: self.logger.warning("problem dispatching event: '{}' ".format(e)) @@ -272,7 +274,8 @@ async def run_loop(): self.sseDaemon = None return event_data = json.loads(event.data) - self.logger.debug("received Event: {}...".format(str(event_data)[:300])) + qlen = len(self.incoming_events) + self.logger.debug("received Event.qlen:{}. event: {}...".format(qlen,str(event_data)[:300])) self.incoming_events_rlock.acquire() try: self.incoming_events.append(event_data) From 3b7cfe72c60f01e5a68dcca6c47d3806489240b7 Mon Sep 17 00:00:00 2001 From: galexey <12891955+galexey@users.noreply.github.com> Date: Wed, 17 Mar 2021 12:01:12 +0100 Subject: [PATCH 25/25] token store --- ... openhab event messages through rest api.ods | Bin 0 -> 17065 bytes tests/test_create_delete_items.py | 3 ++- tests/test_eventsubscription.py | 6 ++---- tests/test_widgets.py | 5 +++-- tests/token_store.py | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 openhab/documentation/possible openhab event messages through rest api.ods create mode 100644 tests/token_store.py diff --git a/openhab/documentation/possible openhab event messages through rest api.ods b/openhab/documentation/possible openhab event messages through rest api.ods new file mode 100644 index 0000000000000000000000000000000000000000..73cc286cf4c9489fbc5bff2b534c885b23d7d30d GIT binary patch literal 17065 zcma*O1AHah(mos;6Wf!CIk7XbolI=owr$(CZ6_1kw(WoBzVEr`-us?&zpwVMe_g$I z*Ry(6udcnSS3S~Vz#zx~01yBG^a}j4{${Kp)Bpegzqik?0L+ce_3fOk^tG+5%#3xl z?TjrgXdEo`s4cZ^jcutdt@JJQEOqV8^)2kEZLMtdwe@U`^!4qe{|)mK=AVN3sqtA_ z7#JJc+x!E~mY&AS(#qcI6T+V6@9oh4!tS?;j<(HzvBLffD=QmILmPct+y8F)SMAwZ zTAKZLtH0`J%%^6qZDDMnZ)-Pn(GwA^^S;GMVY*kFi5p}6& z+So&(HAUU>47y~*Bf>`YM-*(W)Xn)TY|D!c6nH5(S0=vsndB{w7UZm&3I0^fFlH0A zIiyZ0Hyl!Jgxzy2*1|9YbrBGdxZyvH?{?iVS$}Oq8Ys!won%EhKj3qQuCWzo)j(-C z$hHSI;75D&ghPst_}Telw@8Zhe^0oPVeQU7+aAA%__iqlG?KbJagWq1BKc&DEDz}T*=bKp1Q`IT$<9Cc84ji(?~ zDJ=#LQTJdp4GIVV;0p`@@IOz$U%3zJ(|xvf&Sv_y)K2DRXKE)AI}IrB8-=uOMo|Fq z_SJ*B?B6xaW(pvqFhE+7kl*Szh2zEq5kW(6Cr&50q%umRrumMk#pvB1#^$l~lyIJpc&VGf5(&B1%euwwb=P{IluOu76Dl=4u7 z$xU|dy4zvg%79cx&8U=y*yAav&d4vZEgMtRtB6g_BN(B$D{RBtB3xoJwQ~wJu*f7b zUw;-AXolV6lJCk&VOC?J+YW_I;cl(y5yyN8W;NtlcJMjH)c)5|yp;k2^fU*kiPU_T zSdXM_m1O0-E~*wKO7KhY=hXC_|{un+YaS@Z(eh=PSeO zS5g|1DNy)V2Sk+mMWJjGv|5sF2))%1 zP>HcwDKC3WkPya2VzEb6+!F?pi9KaUgjl9<4R1~!u`nlXO^IruxRx(?=)4{mjZt0v zVXlsu&zxBu;!ODOqvju2$WxCmEn2e67YcEqU@3uo!kqX%P6FFam5x}Kz}Le>M6hqD zjIIt>(15schj(1rv=;W&-wIE)l?e$~17vRf0H}F+Q^i%6`*^X&$o1sKU4mKR?yHX? z7Q+T+_u4(PGp*$G5Wkz)fY(Y`j_%ZPD1R~FVp$Xr<5vG>kRr#vN13&>`0$eK?{!h=~iDQqh|CPSJKGN8p!FOG;B72MsnE}ya-V@93+M0%Srdbs&T`8=jy`prYf5h2rBwxo?LqkJpX7R0d>X$ z%Ugu$n(P#5y}1IAdun23BJ0QlLoY?>7k()c$I0%e8c3L+>+BFz7xigGd}1mFeFD>G zGlii?ruW5+(E58?uZPGX@pr~{)q|PIQePG$Fj_C-jU1&CWx`G?dERbl!LnUJ?P?#S z+;_6G&96q)J0(@Ryor<*cPysf5q@HDPpud)3A+N`sG!8D(7-5WxOhwZsGxMH!yamd ztwy^p-z_^TP!gThGif%K5k&{g@cfYP4Js-3V>!R|GAH7?o==JUb1qY;Qb@q_LZY=l zF4-#SV^~t{Cs^x4P zZmz^+J&9vYlSQS!tTt@DM7m|+_kFZ1ilh33hzF^<<>)6X-8IDEyoZUjAp@xB8;sGzGXSATk|B>dPW7Vnz@677pITMC!mU zv=PI2kdjL9_G$56{m8NHC{B9GP+kB^%?bRanC`WUvE)_WO#sA)U$j$dHBpbTgx}A1 zlU&8|2r3-~|2a&^-Rl}XmcY2~D6$ZxBqwNU`d-I|`;%IIw_j*(aKYN20T3lPqdE2h z!-DiwRFYxM0{OU==L43Y>6WK&SC8#ZYSMNEVpF|@zsO>{z8Wek4?pT z`naV*ogcxO0i?t2mho4KTFQcT(XSIf#S!c)m3+FU0EC6mVyU$C)?k8nQkgU9_jw;9 zY`P$B*^%B}ueEa}DDMzWKVYF1aP$-!Eum|;TLc^=UAUccAjN6OBNFqzE-P8n6u4bb+p=`g+urV(g`~DKB-JuqGRdntYnWLCZlIcP zH1s!Lgj~6ttzx|{JRcImcHAIjW5m*hWtX(kf5OF0P-=sb@;oEsI`0I>D- znX{*E%zq8zRUtgS$+TIRfEH~2)o7Yt~g2Vt|^#H zt3omBK~>yL(bQOp$g@fla*tWD|I=Jwl^aD5W>`UgdC)XZNK~ww25bt(g28;5w!dBw zH!D9eBqD&CDP-;b`N>d`80Z(&XBgIMYVcd>%&dq$2L>;H757RWW{hZH_mS%k2Xk$g+r1W=2cf*;7-ds+0GZjHd ze<|#W`P4}p9kUfsL>2@v@&WyN6{IvVka>1&>tcz{==A(}7pz1pAeNy*X6yoWnCmxl zNGn(BjemGAy(DfYjq$KBc#9vI7ax*W;JIMn)$FxKs>7S9#B#tlEhk7-qdN`elhU4S z&>{hSeqGtsOtb2wOi}Y}1QX656pbF+NI}`|zl9;Y3U0a{dBt@wZI=E4NBf4JP>hja z5cX`#H^u-=2bix2(fy?9lHA>GFn7(srC_d7TY5Ey30px$l)jJ^n*2)R*g^(d`kqe1 zTpM(>2X>7me{)o~iciKaMh)%B6?`(GbbbJS3z7PL)uuG}OZg}xoVT_)gjtvKN)Rce zd1S)+SiQ^5f!o0c@b9^5h{(4+yU(d8+|U0uSN%uksBfqJd!|}1KWec;`=#xH0;WMJ zIFC{+L5qSE5t%j5$eYF75>TQ7xq*R9S|0nuy*M$J0c;s8$YM1;IbFS?iOqGfc!_gS z5VKv7;j0l?3KoHVy&!X{R5xNe%7tP9C2^n{`uaIfs}^{~IEMnWv%bHysojBEx^COX zMeiF2LqiV&fvuW$H#YP+@##ix^i=dhQ9T=TS+qpO5?spa)#_|kA>tDe(}g+gLHAH+ zjnInJ(*Sn6gC8K>Q}~rt#w5Jxv1Xx5u5!W_7dtN~_xAC82`(`U7-)JX*i4iT0~RbN z#v?Y$j9sfnsKQ3|?6gP$kcBs^E_A^rARL7oJ*)I{EE?=17 z=vkf$9^bQY&Eax?8uuF(SP}HVslbbA&Fzlnw`y^VSf_gs8&%aCq^27UQA8x+X@NRq z4D|vIk%lRbasFli)WNUexs(!Ux#5E@6XQ3)N|VU9@Qu1oUASl+)*b!>B3vgs5#}f$ zt98pyV*GqjzOsWqvBPC*&%~vyeJ)AYn6!Wa^JnqF^csgvZ>uXhVNblFQvCGR!#_ZG zXb8~)G=VM4?EieYSvocz!84^}MBQg7G`D87))HV%+#t-BA)1Al1l zl*D!xGw=7%tYRH%+I}#ntIBoy-{aJ727a&^zn&6w72>XKGyh|}6`8cM0RaHifd04f z2K|{E=~`OY{eBJcJ2_H6ky~Ly{pj!%_~vN9y|pia*kBh(WoKciss-5|DdmW)Dp^-g z6`A*CvD1UNbK8%F<*Hhj=M358MELpWuwcTdkYc%3E8~(4o7M6HLa3xu;^y9cH$qzua_ue}U(V1RU38Ddmf*EkQS1(uTXLgEZJI&B+{q>Fz#gIXlhm816*9p;wJ^0#HF^?pDvrJYY%7<;> zZ9B{TP@Gh5Bf3+$Z-`vE#_#G-K{K|_(2gO>76t^~8Rm6yb9<87>hy%PV$rGW!xFnm z6!v44A&7F0;bg7xMypUkF;ua&6<-s^5qETE{7Dw5U}YJ&J#OiUN+4Nz8*A$knD(4V zLL^)BjdWZBaRCVEe-tCk!ytA`mcb04ss?7yS~c_L5$n%#i`_}W9}v<_R59evJ1(KW zKE9jl!zZUYLZh6ekf($_HqVjfY3y*>H}b~2ZWxO|0dteaCZfwQa!wphn){DK(lyct zzVEy=2D1)Qt02TC1{3cz^Q?~)l|JF#n5U`0)Sr&GuV@|Ys5MD8Bubal9fc{+E6N5p zFT~*TEd%a&Z|>0qKU=LMt+=di)XS%4i@R+ry9G1@(Eu@LT6)g7_vi~yiugmf?-#N# zLG*h!#$YvsA;4fIFIH6@T}t8c@g=NU_$@&7IN$gn>@s*b@eklOUAa%23G3p`wDdW% zS-#^u#eL|;(zdObmBCi9p?^Oe=8P?fq=7sbpSJ+WjOHr{GfQ@yleG%5#pPl`6=LqYnK@F%_m~j^Q zkJftC(NdiD?M#x6vF@&y$+1&kkedkQ9%)|2eW3a_D75g_5@>}gPSaG?#s<7vCArlZ ze6GcX9kI(d77MsB;c>Wu=6@4s>-3fLO=rqMmQ|wh_Z-)U&&59;)LV zSXVx}zRx~QLv+(?psJhhUWQE#zFv+4>DmHapv%9m9t%SC0D58Y#;XwSo8mvxiyAt? z?lm8bLU6zxndXyJG=C;kK|J8xB4>vVDE0$ykHaVpEF4G#V(7F4V((DWC3tP4;?XJa z!B7)ECN2s>3z>y9pVpD>4!yGKl5;AF@+>*uBsyG@;Hu?w281mdW^^C<6i??!;O-~} zxQE4D=bQb28|+S?#<=!PmkSOTg0;svXVgz1Uv!!kbJ9Zy7>}Aopu|WE7>@C7OD_3R zpUF(BcEWeXO0+J;C*Avvf=7GBSEZ4RnCzH2|3^UDxZqD)VU>^8O`k=nX#8(7^?Kh^ zh84z`e>QDLEz{5xA%iaIE!J?Q9B&db5oK&?sUCUDdYi}O_*pYL)6PD(U3n~ZXV$;v zKyQ74x1{7Yl6`=sOH}1b4J54n)p@|bJ-m?xN%$DfAO!?vVSCN(+&c3haIC=r8*8#E z?lf>PBpcsoI3s4cMvH5KvE)T&U&bhGV!^k+H57`FhXs5Wq@qh&d~Y?Iw7Ils z(#%^u+a0-LyQp>VN2vRr$BD)J15sOVxcuEjP6kOXT1mRbrt2D{AG5TR zomn6LRoPg_`SGmc8QPzlBDM_b169DC=NAxo!Mn6OXNgNoUyVGA5)Wso-ID5$vSYRT zo9WAd3k#*q*2bpJu3o`&N3kci-tk80<|j(@V0aIVp^+WSz?#UxNa1^0VrBo#g=AbK zw8~n*bl5IEU}c4cg=AQxQ2>-zLzK4B*ih}(1;Lgqc{JWBYW~(Jw*tbGD>>%^ok=`< zS738;6CH9J?z|xvrWuLV$ z^Cj;lz(>Rp>QWalynjJx(fdf=MXryCL)IAsi~^K+7O>VJ)QG23bTEk&zems2S;8sZ zUG@|!-o@KPZ)L0jtMc3tZ{C3(s>vLLuqnA=HS63lZqvVYINV5Ebf~(EES^iT=c-Ez zg8S9S#T}bo+z+J@njTb~)_VOuK!z`qs{)nRyj0cnf`hQPj?4jA} zOv~))C&jrq|7e#OI|ZmsMV$W(m2JK=L$^4JzIIIE*I@1_fn)|arvx!Nt5BZuQfxqR z!=F*4Em~eU$E(2AJG5;Z)?XO81->;WVgpU0hd1~`0^L(^onK8*XK@E{pqIP_21^NP znSUf8m&s9_C~~T}_qAsytH%_L6VY)C z6E)g^2dX}%;T{|V-VtbCkA@7F_7MXX)#Qhb4v!skIq@;A&uq5Fm;%<=>g{%M^Cnrf zVtB^|2L|fCC~;3abgHT;eoic9!x&~-GBS?bmlETmq?KFdkEAuHu}k!Upg64T0G*Sz zB`WQC4M^R4F?3t$Z-an?98}t4+K{>tqUg42JxRy*i`RZ!a&utdqhPtCRJJ9yCQO^d zS4;?_swBvM@b}0yhZFH{NjPEvI zWetNJX6w0;z-oY5EgQHkrV~g@;_lf0`O_Kw-TiIQE~3gbzaWpwK3K%>Pa{J6=Kx%h-|0!ZzR<>9#XOt+G2@hN~caTp(L zmzTK{#4eH=5D;EZLmxrTi(b3V7jo&o+7bI@k=+gpk*lONunEctn^)BuKixMTDTxSx zC+nw;3R(g(I8a~AdABwKa0uaH`eArxfzw)m4?Hikbb&H=K#cOzU*O1M_Nd4NaKwIfDe zDWC>Ju`1ZxzGmEkT5HQS@KZ_|$gvgCpvAt>Fgtc?uBvA+oL6{!xVS4rvnvAC0!YXA zmTx-$bf{9?-<_ssNm@IYm$7F0K;qt#~x}@`Sf62 z3^bg0?>_JDk0z=(eIYFXzz5WU;;94A;Q?UbVLYr)+R}C2W*dU}%!f)<)ylOWWwF7_ zsM`l0597)AsSJ)eA6rNbsp0@5Nbp07QLYupzOHKZ=O2_B(uc8h^le?J1l=y2OgJi` z#g@j7mR@qJbZu06^`0%eB75hQIEkn=exotuc~XlS5vS@Qz)5M3oh+l~@Gc(4u11yn z;&%J%U4+QV{^hHSC(iv@+xbcmpoy%~T>rjJ6aZ@KhPwFuw;r2xM-xEL8KD2|Xvwyo zh#zYuUk&)K9iSHvC?Ol$%#^alr~-Te55BjKsQFh=pkYAqVqQ4)FF%5pUZUZWC!^yc z@@lwpz5tZ-0T9`8ZW9*5Nt*@cxXg}sCjohZhEVjsAz~|q=mKs-@jw6%pa4)xt4+nf zwhRJ+vc6@4UV|V=E%(ItCp7A9Z!TVgnW^uld)Nhw^r4 zPIG$;*VF5nfeom&1y)xpn%AnglQBdbXO=zqyl!JyoA8EM<}X$ zg{=Fd;3{jM%I-}zwh=6(aP#xXQb>uD2vYf_ z<^Iv&(f|A^{vX*0fsE6S_xD@R-|16JbGjiIEC9g54SCyFXNFD+|M5X-OeiD9qohPp~4w{IZ|>*U#+(;&ZVIV0%mK1pol( zO5fl*@5e^y$1r`Yr0TBfQ6%!X76&Vv96&ni+ z3zrZLpA3iKE6z7sd~9^euQ)_RM8qUyROH_%C@6?&SijP7($IZpq$Xjbr(k4aWME=p z=is1Z6Xg<=U>8>4VWQ?`rQ_jd=i%WH{lU)5%PS}(AtJylDI_K;Cdn@KaBcZIMB&?{Vq^hs3ZlkJgsGx77Xyl@5V5e^E zpl<1*r6i@VCa0&VW}&EPswQizsivi=WvHX(WTdRCtE;1LY+!6=tfy;ktZ!;+W^86- zX=SEw?QU*uZ)WS_U}j)rZe?p_=3%YxW^3x`=w#&JZ{iW{;O6V$=i(D==M(J^ zoa*Ku;}#U>5}N87p5+;x<7KAkYpv#GVHDtG;$?5=>tf^W?i%7@8{**^+2BX?;0EIo*rzU66O{d5EvL1;}@M48Wk5Bn-ZCv85imw7ag7!?Uxc8mYA3n znwT4sUY(GZmzk9jl3kaWRgjQfl9rv9l2aO+Tc27`ol#ViUfPnE5R#J+mzx@wlb%wP z9#fK$QJWr;lb)5El~$G=Uz(d*SC~>*SeRc{TTxS5Qqx>pnp<5_(o&K0snyoj*3`Gu zH8s{Xx3$#Qv^LjubabRtcBNPK9N82>9Luand$kZg~f%rrIn@mnZd%S&qdAz@Ue!PEwe=kD-yZ`_|91!8>m3LY` zONW)lQu63JQs1A}JGYrJO)WUPt7u-&Lwo3Le~w{=w@%B5wR*1rKfd1UFCt_(NQ?HT zs*tRuXM;o)lbgi&+GQQ4Lg{4~m*=Ak_p|Q6k9L=slA)6}`niYI3+$u{w^OMSzOGIoylWiK z`_G@*UrHl6V(yCG#|LZPZ_{h3HbzEVhOf;Om2WaRVYEM9!mHn;^f^^%x+-Vz8 zTr!j4UlIZdPc^N+95`~^7d0GL+mBD^xj%4grZY=Ds!e!2b;G|@da~Qwa6jqIGiSWo z%qPA-nkq+fKeZ}rzTFgNI%6~0oTELi3MF`AN+M;Da8~GN@}yj<$v^H{Y{8;CP;G$5 zHaG*W-_WLN?i)i;_P@FzjNWf;k-MD2RFvoAzEUW z6ljTTyJ3VFm|uH+RNN_CZF`jn<0tp#0r6M}g9TLi+42BEFA8&$is2*khr}jzaz2g7 z?3Faiexa%gI@EhbgRLQ~9=%ukvHxdUV0mK|auo2Gn`-67gz5xNui-v0;h57I*e z92?j3dBXD7GgV8Db2TJwwC_hfwpz_3sF%!J%@a`Ei2#jhGi3lYZP2TM=Mxaz&^mSu z`;cp-;L;3K_tk(YIT+Na2R|{b7=$MovUGb;B^P-hPkPur!&6*AX^ne3WfEp?)+OP- z8nR@dY$qeFM}Tfs1~MO@rNDIN`11kb&v(>F4^P^;apm|)^z7&NJP@hot&3re5G5+C zQ68Hl#3R-aGT^t_IGdg;>U5@VxpZzMN7*;x=F9|kPQEO=vQrh=U499)0h4EqR%*n5 z9TV1~UYl5oVO=eeIdF%$b$|2{}R{cyxcAkRME4R6^Hv(_nZhdNZ#!N$tE(OU4n?!Wq8jgEv`(-URPOuLscn}&_ z3p!}ML`_B|`eL@QfL1|bl%%<2&7pEN*i@GM7KwUT``zU|5_ZX}kf=W1!D@^9&#G5qpBRaWUrG z`5o@-$l~gfg=pSaP2-7NS_Bkb6vIj1pwJwfC0TA@5g9HhXI9XQi4y=kDbk;Y4O?cb zZ}h*K-8C*u6@&KDiB+Cm$nwYReZ%6zs|>a(ZjBcdMDr|vIUCgf>Jz=^nl4r8{{WCz zw`Is}?cH_p!;&%b-E5LKcIt7^yv`3ca-*IJ>zdlh z43c~F4k1!1swZ;Qs zhiC3+PV|t@0p1`}b1E%kWL^gJY>e zc1vFbeM&Zsi&AmPxKFNE?=0KW+fbVmOI>GaTmP|h{4CQqfqF|zcEd^>?Vb;Q_!bef zhVyir@Uwr3>SNySY7eB9jaqS0>3()r!AOF4WxfX6uJ0!a{CtMItMzO{XgCH!-7nww z)nYrS%WtJJm0M+|At9yiJMYD&+bJ!J z?1-}_j#5lSP&YnQZM-wA4cV!twaoHlZ6a&5fv~n}sXr>h>s)e6y2y+-lNe^tBQ)jf z)02J-myibszZ=H1y5Z|wmgI|W_nKYgE4g_2RWx^1Ts&)0g@AY!Ej;a#VCkKf`*tQ9 za?M%jqRz0W7KJCZQ4yU6OF$)yxZN6vMYY#{(FUei&W||Y>V9q}xwlY_sqzQk`VKL{ zck=}|fRh1!OMCIJRZqIh&37H}Mi-llp%ADEP`+qZ{L}Au(_k2EENrKd)3}wH!ys;y zaoghje)>r|7JAAbtLb0pQV_s9aORR9Z}ieuSSTo;tejk|*A#TSLw;e8v8RO-<|cc= z!y$NZb9y-gqTN3Gn{f*Q{7S&I<;A*!AcA^@kLB=$oFj|HqJa`KSYcm4)Ah*98C97K zb|UHkDs^K3K3?*;(O})K*~Ja9b$9T`KC)0QB`0J<5heMTi&ZHK$^lg8?f{Ci!4%#zr=<*V0-TsTd8 zW|1>{vH;j~19xHUo7f)Ki4uDk4O`f`tpzltGwbSxtEy@~V-8kXe{GCr9E!#4JLfjK z+~WD!gK4mrHkq+92W$c>PnxZ62|Wp-x@td>qpR>}u| zhxwj;D!)K;S0mh>-m1WT0uGq}s$SBbW;;W`_u{4m{)PH(8vx&On)}*yYp5r6o2-_Z zaSYej*9y)mUN9mh?KO=5+VPFTWi5$=85>^kk>3ve5uJ+;+mNr1^I)LFY|?HTmj>b` zcvG5#X}9o=i_mN~R1nV&fs1CX@CXRQ&r>&y=A4&AtQ?BnO8%&wqWAB?{9xn)flI zRv@<$<{i2-`sFC8o3lE0#mSmDAid+EZIB3sQ>h0*)hBZ9j(t--_n z7Ik9$y)!sJ$3fs{8H`WQTC$&^5U&Z3_Hp<1DdhxYGDb?W{<(X;ShyLm3_0U^WCTPr zL{{>w*+mY{@v8ilm)K;opA?P0+xqfY6uq3e&zQNm)dZ-N~^xm>=FdOgaBeh%NHe(57<|Q zpR70taoyq<`iY%Wq{LS~lrRsFZj);ceQw@0GEGdYOkEI>A$u^9DhObS7Zt4wJJb8D zpao|M-+MI-JczTp->zmGoi4GCC)6uiUx{${rn)=fZ%wb>(aK6d$79)n{^* z5#dYjUc?xm2xPWa>b=pACwCdn?dewO%!A17hwVY4jv(-HU$amEO(o0ORD1M29|;#1 zkht2@S6lGgxD$0>qVDNq$CL*oFi%oX%w(-EX6QC`(|Rfx;x5Y1 z;c_g(YtO(7$gY1fBFH=K-9r%3O%bLvZow|ReB7x_JyJ5|-hLmtM^N-Dp@!=7(*z4oV+Trq z>)FEdVuHgq1{~*eLcgEn@C(%b91@=@e9DvGrO?_Lo~pV74H&LpAou&`H>SST`|i}A ze45@8xy1CaABvD0E4Ek*eNw2o@2{yYxyN86IBlRDc?SGBnUg6D=cyFWS_YfUMCz6w z$2v%n*ZoN#v6`&yP;%o;Cv~fEYmb}(v5NOjC9Cyw&s8dOSq_9ype6a~r2C5$(;we; zmTj+W2DRmn9xz7J1@g1Qx;}0@8yg(jAJ{rw_ML6qr6d-T&)$8affBXok3p%!hC2kf~OF^Nc2_kziG3P&sVGG0=g5U!T_dxJt9NY1K}!Xcp;OiioS_r zu#APLd$oB3chBkSO-0IbHu{Xx``LfFdzwJ=7{N;hr}wbhMqS-fmBX(cZcPFw>dhX* zU4cND;CSsA2&q7xu=~|_(#Vyy>JE%cx+KTxNQYN3JF=EZPP2cCc;-8f9)DcOVh*K` z_#Xa2hpne?^kjFRoL(s1e~4SB8W8^eQ^js;)?aK7m?Ew+fqy+Q>zs3B5(g)qv%J*| z;<$*cU$1CbF^K7$TAl?;R3J6CapEh*h;5yDS)?9f$8BXr8k?{s`lyHHfo;sGBXgLf zerhu8LBv~5{edqc8?ylmFB*}s$kpNPnJ+9;BDs2hb8|jppxGXRrnSZt+tL!MyJ_st zHBmG649`BT^WGqF$=kO9{}e|loZW@UP8$Qm`}k6=UKY>y@d}?^PaL<_lXk=?<{O8F zxy(BNn}=H*T{Ci^-0qQ#q>W971YjwB__I~4`a>#-930h3Tx;y_5BlflVNV*%VtWhs z!dNU%{Vfj2SPD{}MR7v2GrlC?2>BtU#aQxc=={~^%PU*WKX1?z4;8fFpm|5);Zi}q zW8@C9yXNQkzj^U7&aOxp!|BtXF3q|PDgh4qDqDc%ed9u%_akN=ABrR zc#zk5Yw3?h$N&I?g_ICID;zrO1zqhE*9?aE!x;u=BJb&?Avb2&>ZvLbb=?o*KOb<~ z)4!XY{V``7_mm8l!g}`>hG%^J03=Q-45D-Kr}})Q3m_sO$zSn9%jfTj3BP$Aw)%E< z#ukSEOHEL9%o#%U;O-L)rT#xFJgF4S{SUbGsmwU0ujtJ`+5Z zVgGB(wW>+p-L=+KX^uJ}wbFCn+q3qE_4d|fcF!xXl7+O9_6l@*;Kw5}DNX?t*n7tJu4+C7;}*%Y3HW;34QRBD&W@ zj2n&q`}&8FcJilmW0UwnUmUo|kH@?E;f}}cd{R;~+Z&hWFZa>}QjVS1lh3JhSLiY?_iS2yJGdXGVzL;-8au`TE=$q$+zk?oVeq1=TI<&bg#M{5S?V zOW5Q(OWRXF!~k+>G>mewmn`+O@2)hK>j`j?EZ1}CRK)JF7-Jc}QA;~hV*5Nr6@VUL zal#5yUQ-Bon)tZk(Oq2fa)lycQ?Ojg(t~6~F1Na97`_X0vWmJp;@%-ev{S*!9iBvA zM7b^l1jF4#En1ohC$UxzwdWJG5p8M7_ez`kw2oVCxI zlN_hW+-*Na_R!k1O4;5-w?5<8CL8eP{Yia11zHUGc1m^P5jAtL48T^Dr1=q@&Kaxb zc-4aBYFT)WF~H@cy+;@R-EMyqf=WkR4~tYP@M}ZTi_@AATpd%P-gtIq+82l2?T68s z;+0nW-S!=)#-6mFJ#b>fg@$Yt10CGHSHo4_#toSHVxIH)(16&GnW&;+A(uNaiLRgY zjgTwJkKOB?!cDx*Tb0=bj9XBONOx?!$gD+``eiMe$#Nyy5hU@TedRAEPH%o$5)OPy z2sy0~`FdQogSwx~KPS9Dg6+iKx-iwY$0w%AfBe`T zjOSjkzrV$QKy>HxiAE1xzpqPtcNAi)bbZ-DL5%D8xEp`}f!f=X%5{x<&|PU&$@LbVT%&Z0r%v~q_vcz|j8I(+a^^)$H-s17inlC{I_gQg2mURc}R%X{y5He(i z)<3aONk>N?8GdLuIPsg*$hgIL(-B{eKq3fM+t_T#rc5hF{JEVa)M^73-b(MoRI1-6r$K=3faM%S)Q4fMdTcfYlHj)C|Hh;IRVJFC8D6T*Y#sg47 zf}IirtVAQ4`Z^TG#IVqM!imqK1>yw^f-yF)2E-MN>n>#4quj6v zAKV;ch=M47e?hZ3*1mT<|d_xv?5#;3jnH@eGbZo44||(5Pu*EaU%t^ zdQe;O$ke6?)yVi=8R@Hg5h}Or6%z8Gy!Tx-Q(9IeX2Gr(Bwh*q=rDr$s{aMj!mB>Y zFIMZ^stGCl_Ifb$>Ciz7k6gFj8{H>UE6>CpNKVmL%O8*S@koaNFkUR^NkCk*BCli{ z)G|5z!HC47aisrH@OmfdhM3w-Q_{Xx40F8=*Pa)It{Ua@#2v(K9p!2`u8xAYl^(=~ zV<#moO;}?p_GdI{+-qU)B2upZ>H2G*aOd+MB%GvvsZM!3 zIe^r`uPfwPwvn1I+T9p)P8#=4n>}^>Ok%@i_%6*`T9ib=|4a%?bU}Shrc{x{k=aT) z9$j%;VeM3cX-mx}gDhOVTN1l!IW$>rSxpT_MpxF=BmxbwEYyrk2ORL=uu(+?XTHTi zQ{`u4*HF%SXw4<1Lv>zaGAVt`Y51`g2O^0C-4$I$M3Gi+qs7iZZHiM@JaYi6)0QSn zmG*wd;VqAxecM1){(y%bU5dVxM9~xWSZN8y5g5fN+e+2<5^sYVzR`%ry?QyBX!l(C2JM)jr%iY+}R>?Phy5f$HV60 zV26^ieVqJe2A5fby?$}3%m}lTYxYJsi~0kXT?NxxVLki&Cq(i7^_mp9hD8n0pa(yH zWczvt90vbxdBw4B8OF8iF)F_lahT-FR7_XlT(eVu9l*Ar%+kB2$sw$1Fe96;#F02^ zcTQ#XD2DsUwOyYRO28$LN#iW+v1A;u_W=22pzA}2s=TfcmHq4Z`sYCVR31A6Hm%Hk z4dmx$_fw2Y0<)g!$%v zJaq7LF{=^uj{7J>aDH5SCLXAx^M!H4(!R)_(m+Xytu(k>Bi*t{V zn?eDD%Awe!e&o`)aOns&FGy)p};KgNz9qyjxc^!YRH`qs^LgOP>5yhE+I5 z?#?QusX7r!_$4eoXWAXXN6yPAfJi@oVYv?9&`-v=wZuYda^Q#h^3Ib1kD=*F*5M?r z^}+%b$YEH?5q6d~If&`k)@PM2Q$UfR0XZyj0}!Eo;Q>?Kj-IDEjLd zfQf)c+aCHW9=W*Cy;QYxK|9h0*^yWXM=TrSZ zSMzWD@xMp{H~e!PozKj5stiujK3BnarOe z^@;S)=<&%{|6>sUVfn9AnBV0we@et>ZOork?f+x