diff --git a/README.rst b/README.rst index 84d02d4..e090935 100644 --- a/README.rst +++ b/README.rst @@ -21,12 +21,29 @@ 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 + 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 ------------ - python >= 3.5 - python :: dateutil - python :: requests + - python :: aiohttp_sse_client - openHAB version 2 Installation @@ -49,7 +66,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 +104,77 @@ 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 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) + + # 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 + testroom1_LampOnOff=None + + + #create or delete items: + # first instantiate a Factory: + itemFactory = openhab.items.ItemFactory(openhab) + #create the item + testDimmer = itemFactory.create_or_update_item(name="the_testDimmer", data_type=openhab.items.DimmerItem) + #use item + testDimmer.state = 95 + testDimmer.off() + testDimmer.command("ON") + #or better: + testDimmer.command(openhab.types.OnOffType.OFF) + + + + + # you can set or change many item attributes: + + 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" + 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, + quantity_type=item_quantity_type, + label=itemlabel, + category=itemcategory, + tags=itemtags, + group_names=itemgroup_names, + group_type=grouptype, + 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 bc4b092..c47a530 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,10 +24,27 @@ import typing import warnings + +from aiohttp_sse_client import client as sse_client +import asyncio +import threading import requests +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 +import openhab.events +import openhab.types +import openhab.audio + __author__ = 'Georges Toth ' __license__ = 'AGPLv3+' @@ -36,13 +52,21 @@ class OpenHAB: """openHAB REST API client.""" + class Version(Enum): + OH2 = 2 + OH3 = 3 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: - """Class constructor. + 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. Args: base_url (str): The openHAB REST URL, e.g. http://example.com/rest @@ -51,16 +75,33 @@ 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 - + 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. """ self.base_url = base_url - + 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 @@ -68,26 +109,270 @@ def __init__(self, base_url: str, self.session.auth = HTTPBasicAuth(username, password) self.timeout = timeout + self.maxEchoToOpenhabMS = max_echo_to_openhab_ms self.logger = logging.getLogger(__name__) - - @staticmethod - def _check_req_return(req: requests.Response) -> None: + 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() + 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 + 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__() + + def _check_req_return(self, req: requests.Response) -> None: """Internal method for checking the return value of a REST HTTP request. Args: 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 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: + """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 itself through a call of the items _processExternalEvent method + + Args: + 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. + 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: + event_data send by openhab in a Dict + """ + log = logging.getLogger(__name__) + + if "type" in event_data: + event_reason = event_data["type"] + + + 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: + 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: + self.logger.debug("item '{}' not registered. ignoring the arrived event.".format(item_name)) + + else: + log.debug("received unknown Event-data_type in Openhab Event stream: {}".format(event_data)) + + 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 + """ + for aListener in self.eventListeners: + try: + aListener(event) + 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.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.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. + """ + if listener is None: + self.eventListeners.clear() + 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 + qlen = 0 + try: + self.incoming_events_rlock.acquire() + try: + qlen=len(self.incoming_events) + if qlen > 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 (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)) + + + 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 + # "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, 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__: + self.sseDaemon = None + return + event_data = json.loads(event.data) + 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) + 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") + self.logger.exception(exception) + while self.__keep_event_daemon_running__: + # keep restarting the handler after timeouts or connection issues + try: + 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.warning("problem receiving event: '{}' ".format(e)) + + + self.sseDaemon = None + + 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_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 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. + """ + 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""" + 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. + """ + 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. @@ -104,7 +389,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 @@ -117,10 +402,49 @@ 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_put(self, uri_path: str, data: typing.Optional[dict] = 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 + JSON data. + + Args: + uri_path (str): The path to be used in the PUT request. + json_data (str): the request data as jason + + + Returns: + None: No data is returned. + """ + 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, 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 + JSON data. + + Args: + uri_path (str): The path to be used in the DELETE request. + + + Returns: + None: No data is returned. + """ + 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, 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 @@ -133,7 +457,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 @@ -152,23 +478,36 @@ 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. + 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. """ - json_data = self.get_item_raw(name) + if name in self.all_items and not force_request_to_openhab: + 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 - 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. - 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: @@ -181,6 +520,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) @@ -224,6 +566,116 @@ 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): + 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) + + 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) + +# 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 class openHAB(OpenHAB): 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 0000000..73cc286 Binary files /dev/null and b/openhab/documentation/possible openhab event messages through rest api.ods differ diff --git a/openhab/events.py b/openhab/events.py new file mode 100644 index 0000000..5ccaa11 --- /dev/null +++ b/openhab/events.py @@ -0,0 +1,85 @@ +# -*- 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 +import openhab.types + + +EventType = typing.NewType('EventType', str) + +RawItemEventType: EventType = EventType("RawItem") + +ItemEventType: EventType = EventType("Item") +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): + """The base class for all ItemEvents""" + type = ItemEventType + item_name: str + source: EventSource + value_datatype: typing.Type[openhab.types.CommandType] + value: typing.Any + value_raw: typing.Any + unit_of_measure: str + 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 +class ItemStateEvent(ItemEvent): + """a Event representing a state event on a Item""" + 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""" + type = ItemStateChangedEventType + 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/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 29ec56e..938ddf4 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -19,44 +19,256 @@ # # pylint: disable=bad-indentation - +from __future__ import annotations import datetime import logging +import inspect import re import typing - +import json +import time import dateutil.parser import openhab.types +import openhab.events +import openhab.history +from datetime import datetime, timedelta __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, openhab_client: openhab.client.OpenHAB): + """Constructor. + + Args: + openhab_client (openhab.OpenHAB): openHAB object. + + """ + 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, + 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. + 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 + 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'! + 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) + 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 + 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 + """ + + 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) + retrycounter = 10 + 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 + if retrycounter < 0: + raise e + else: + time.sleep(0.05) + + 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. + + 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 data_type used in openhab (like Group, Number, Contact, DateTime, Rollershutter, Color, Dimmer, Switch, Player) + server. + To create groups use the itemtype 'Group'! + 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) + 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 + 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. + + + """ + paramdict: typing.Dict[str, typing.Union[str, typing.List[str], typing.Dict[str, typing.Union[str, typing.List]]]] = {} + itemtypename = type + if inspect.isclass(type): + if issubclass(type, Item): + itemtypename = type.TYPENAME + + if quantity_type is None: + paramdict["type"] = itemtypename + else: + paramdict["type"] = "{}:{}".format(itemtypename, quantity_type) + + paramdict["name"] = name + + if label is not None: + paramdict["label"] = label + + if category is not None: + paramdict["category"] = category + + if tags is not None: + paramdict["tags"] = tags + + if group_names is not None: + paramdict["groupNames"] = group_names + + if group_type is not None: + paramdict["groupType"] = group_type + + 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(__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,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 + """ + 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. + + 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.""" types = [] # 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) -> 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 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: 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.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. + # 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. @@ -70,24 +282,48 @@ 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) else: self.type_ = json_data['type'] - self.__set_state(json_data['state']) + + 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._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) -> typing.Any: + 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. """ - json_data = self.openhab.get_item_raw(self.name) - self.init_from_json(json_data) + if not self.autoUpdate or fetch_from_openhab: + json_data = self.openhab.get_item_raw(self.name) + self.init_from_json(json_data) return self._state @@ -109,6 +345,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() @@ -128,9 +366,9 @@ def _validate_value(self, value: typing.Union[str, typing.Type[openhab.types.Com else: raise ValueError() - def _parse_rest(self, value: str) -> typing.Any: + 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.""" @@ -144,56 +382,466 @@ 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 update""" + now = datetime.now() + result = None + time_sent = None + try: + if event.source != openhab.events.EventSourceOpenhab: + result = True + else: + time_sent,item = self.change_sent_history.get(event.value) + result = time_sent is not None + return result + finally: + 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, + time_sent=time_sent, + now=now)) + + 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() + + 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 + + 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 value, "" + + 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.__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: + 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: + """Private method to process a state event coming from openhab + + 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.__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: + 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: + + """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 + + Returns: + openhab.events.ItemStateChangedEvent : the populated event + """ + parsed_value = state_type_class.parse(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.__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, + 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, + 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 + return item_state_changed_event + + 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) + return item_command_event + 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 + 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) + if state_type_class is None: + 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) + return item_state_event + 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 + 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 + + """ + if not self.autoUpdate: + return + 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) + 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: + if not event.is_my_own_echo or aListener.alsoGetMyEchosFromOpenHAB: + try: + aListener.callbackfunction(self, event) + except Exception as e: + 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.debug("processing internal event:{}".format(event)) + for aListener in self.event_listeners.values(): + if event.type in aListener.listeningTypes: + if aListener.onlyIfEventsourceIsOpenhab: + continue + else: + try: + aListener.callbackfunction(self, event) + except Exception as e: + 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""" + 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: + 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. + + + """ + 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 = listening_types + + 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 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. + + """ + 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(listening_types) + + 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 + + """ + if listening_types is None: + self.listeningTypes.clear() + elif not hasattr(listening_types, '__iter__'): + self.listeningTypes.remove(listening_types) + elif not listening_types: + self.listeningTypes.clear() + else: + self.listeningTypes.difference_update(listening_types) + + 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 + Args: + Args: + 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 + 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.event_listeners: + event_listener = self.event_listeners[listener] + event_listener.add_types(listening_types) + event_listener.onlyIfEventsourceIsOpenhab = only_if_eventsource_is_openhab + else: + 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 remove_all_event_listeners(self): + self.event_listeners = {} + + 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: + 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.event_listeners: + event_listener = self.event_listeners[listener] + event_listener.remove_types(types) + 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(value): + 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._parse_rest(value) + self._state = value 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. 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.change_sent_history.add(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: """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._update(v) + if oldstate == self._state: + event = openhab.events.ItemStateEvent(item_name=self.name, + source=openhab.events.EventSourceInternal, + value_datatype=self.type_, + value=self._state, + value_raw=None, + unit_of_measure=self._unitOfMeasure, + 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_, + value=self._state, + value_raw=None, + unit_of_measure=self._unitOfMeasure, + old_value_datatype=self.type_, + old_value=oldstate, + old_value_raw="", + old_unit_of_measure="", + is_my_own_echo=False, + is_non_value_command=False + ) + 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) + self._validate_value(value) v = self._rest_format(value) + 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 + 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, + is_non_value_command=False + ) + self._process_internal_event(event) + def update_state_null(self) -> None: """Update the state of the item to *NULL*.""" self._update('NULL') @@ -229,10 +877,28 @@ 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 type.""" + """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: """Greater than comparison.""" @@ -262,7 +928,7 @@ def __ne__(self, other: object) -> bool: return not self.__eq__(other) - def _parse_rest(self, value: str) -> datetime.datetime: + def _parse_rest(self, value) -> typing.Tuple[datetime, str]: """Parse a REST result into a native object. Args: @@ -272,7 +938,7 @@ def _parse_rest(self, value: str) -> datetime.datetime: 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: datetime.datetime) -> str: """Format a value before submitting to openHAB. @@ -289,54 +955,76 @@ def _rest_format(self, value: datetime.datetime) -> str: class PlayerItem(Item): - """PlayerItem item type.""" + """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) -class SwitchItem(Item): - """SwitchItem item type.""" + 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 type.""" + """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) -> float: + def _parse_rest(self, value: str) -> typing.Tuple[typing.Union[float, None], str]: """Parse a REST result into a native object. Args: @@ -344,13 +1032,23 @@ 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 value in ('UNDEF', 'NULL'): + return None, "" + # m = re.match(r'''^(-?[0-9.]+)''', value) + try: + m = re.match("(-?[0-9.]+)\s?(.*)?$", value) - if m: - return float(m.group(1)) + if m: + value = m.group(1) + unit_of_measure = m.group(2) + + return float(value), unit_of_measure + else: + return float(value), "" + 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)) @@ -367,32 +1065,42 @@ def _rest_format(self, value: float) -> str: class ContactItem(Item): - """Contact item type.""" + """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: """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__)) 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 type.""" + """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) -> int: + def _parse_rest(self, value: str) -> typing.Tuple[float, str]: """Parse a REST result into a native object. Args: @@ -401,7 +1109,7 @@ def _parse_rest(self, value: str) -> int: 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. @@ -419,28 +1127,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): - """ColorItem item type.""" +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) -> str: + def _parse_rest(self, value: str) -> typing.Tuple[typing.Tuple[int, int, float], str]: """Parse a REST result into a native object. Args: @@ -449,7 +1161,8 @@ def _parse_rest(self, value: 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. @@ -460,34 +1173,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 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') - - def decrease(self) -> None: - """Decrease the state of the color.""" - self.command('DECREASE') + def __extract_value_and_unitofmeasure(self, value: str): + return value, "" class RollershutterItem(Item): - """RollershutterItem item type.""" + """RollershutterItem item data_type.""" - types = [openhab.types.UpDownType, openhab.types.PercentType, openhab.types.StopType] + 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] + 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: @@ -496,7 +1204,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. @@ -514,12 +1222,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 6751c67..be4e782 100644 --- a/openhab/types.py +++ b/openhab/types.py @@ -19,18 +19,56 @@ # # pylint: disable=bad-indentation - +from __future__ import annotations import abc import datetime import re import typing +import dateutil.parser __author__ = 'Georges Toth ' __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 type class.""" + """Base command data_type class.""" + + TYPENAME = "" + + SUPPORTED_TYPENAMES = [] + UNDEF = 'UNDEF' + NULL = 'NULL' + UNDEFINED_STATES = [UNDEF, NULL] + + @classmethod + def is_undefined(cls, value: typing.Any) -> bool: + return value in CommandType.UNDEFINED_STATES + + @classmethod + 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.get_type_for(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 @@ -39,8 +77,8 @@ def validate(cls, value: typing.Any) -> None: 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 @@ -49,14 +87,55 @@ def validate(cls, value: typing.Any) -> None: raise NotImplementedError() +class UndefType(CommandType): + TYPENAME = "UnDef" + SUPPORTED_TYPENAMES = [TYPENAME] + + @classmethod + def parse(cls, value: str) -> typing.Union[str, None]: + 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) -> typing.Union[str, None]: + if value in GroupType.UNDEFINED_STATES: + return None + return value + + @classmethod + def validate(cls, value: str) -> None: + pass + + class StringType(CommandType): - """StringType type class.""" + """StringType data_type class.""" + + TYPENAME = "String" + SUPPORTED_TYPENAMES = [TYPENAME] + + @classmethod + def parse(cls, value: str) -> typing.Union[str, None]: + 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 type string. + Valid values are any of data_type string. Args: value (str): The value to validate. @@ -64,12 +143,24 @@ 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 type class.""" + """OnOffType data_type class.""" + TYPENAME = "OnOff" + SUPPORTED_TYPENAMES = [TYPENAME] + ON = "ON" + OFF = "OFF" + POSSIBLE_VALUES = [ON, OFF] + + @classmethod + 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: @@ -84,13 +175,24 @@ def validate(cls, value: str) -> None: ValueError: Raises ValueError if an invalid value has been specified. """ super().validate(value) - - if value not in ['ON', 'OFF']: - raise ValueError() + OnOffType.parse(value) class OpenCloseType(StringType): - """OpenCloseType type class.""" + """OpenCloseType data_type class.""" + TYPENAME = "OpenClosed" + SUPPORTED_TYPENAMES = [TYPENAME] + OPEN = "OPEN" + CLOSED = "CLOSED" + POSSIBLE_VALUES = [OPEN, CLOSED] + + @classmethod + 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: @@ -105,16 +207,28 @@ def validate(cls, value: str) -> None: ValueError: Raises ValueError if an invalid value has been specified. """ super().validate(value) - - if value not in ['OPEN', 'CLOSED']: - raise ValueError() + OpenCloseType.parse(value) class ColorType(StringType): - """ColorType type class.""" + """ColorType data_type class.""" + TYPENAME = "HSB" + SUPPORTED_TYPENAMES = [TYPENAME] @classmethod - def validate(cls, value: str) -> None: + 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) + 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,20 +243,46 @@ 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() + strvalue = str(value) + if isinstance(value, tuple): + if len(value) == 3: + strvalue = "{},{},{}".format(value[0], value[1], value[2]) + super().validate(strvalue) + ColorType.parse(strvalue) + + super().validate(strvalue) + ColorType.parse(strvalue) class DecimalType(CommandType): - """DecimalType type class.""" + """DecimalType data_type class.""" + TYPENAME = "Decimal" + SUPPORTED_TYPENAMES = [TYPENAME, "Quantity"] + + @classmethod + def parse(cls, value: str) -> typing.Union[None, 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 ValueError: + 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: """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 +295,27 @@ def validate(cls, value: typing.Union[float, int]) -> None: class PercentType(DecimalType): - """PercentType type class.""" + """PercentType data_type class.""" + TYPENAME = "Percent" + SUPPORTED_TYPENAMES = [TYPENAME] + + @classmethod + def parse(cls, value: str) -> typing.Union[float, None]: + 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: """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 +331,22 @@ def validate(cls, value: typing.Union[float, int]) -> None: class IncreaseDecreaseType(StringType): - """IncreaseDecreaseType type class.""" + """IncreaseDecreaseType data_type class.""" + TYPENAME = "IncreaseDecrease" + SUPPORTED_TYPENAMES = [TYPENAME] + + INCREASE = "INCREASE" + DECREASE = "DECREASE" + + POSSIBLE_VALUES = [INCREASE, DECREASE] + + @classmethod + def parse(cls, value: str) -> typing.Union[str, None]: + 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,19 +361,25 @@ 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 type class.""" + """DateTimeType data_type class.""" + TYPENAME = "DateTime" + SUPPORTED_TYPENAMES = [TYPENAME] + + @classmethod + def parse(cls, value: str) -> typing.Union[datetime, None]: + if value in DateTimeType.UNDEFINED_STATES: + return None + return dateutil.parser.parse(value) @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 +392,21 @@ def validate(cls, value: datetime.datetime) -> None: class UpDownType(StringType): - """UpDownType type class.""" + """UpDownType data_type class.""" + + TYPENAME = "UpDown" + SUPPORTED_TYPENAMES = [TYPENAME] + UP = "UP" + DOWN = "DOWN" + POSSIBLE_VALUES = [UP, DOWN] + + @classmethod + def parse(cls, value: str) -> typing.Union[str, None]: + 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: @@ -233,12 +422,24 @@ def validate(cls, value: str) -> None: """ super().validate(value) - if value not in ['UP', 'DOWN']: - raise ValueError() + UpDownType.parse(value) + +class StopMoveType(StringType): + """UpDownType data_type class.""" -class StopType(StringType): - """UpDownType type class.""" + TYPENAME = "StopMove" + SUPPORTED_TYPENAMES = [TYPENAME] + STOP = "STOP" + POSSIBLE_VALUES = [STOP] + + @classmethod + def parse(cls, value: str) -> typing.Union[str, None]: + 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: @@ -254,18 +455,65 @@ 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) -> typing.Union[str, None]: + 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`` -class PlayerType(StringType): - """PlayerType type class.""" + 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) -> 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. - Valid values are ``PLAY``, ``PAUSE``, ``NEXT``, ``PREVIOUS``, ``REWIND``, and ``FASTFORWARD``. + Valid values are ``PLAY``, ``PAUSE`` Args: value (str): The value to validate. @@ -275,5 +523,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) -> 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. + + 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) diff --git a/openhab/ui.py b/openhab/ui.py new file mode 100644 index 0000000..9dc1d27 --- /dev/null +++ b/openhab/ui.py @@ -0,0 +1,119 @@ +# -*- 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 +import json + +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.code: typing.Dict = widget + self._loaded = loaded + self._changed_uid = False + + @property + def uid(self) -> str: + return self.code["uid"] + + + @uid.setter + def uid(self, uid: str) -> None: + if uid != self.uid: + self._changed_uid = True + self.code["uid"] = uid + + + + @property + def timestamp(self) -> datetime: + return dateutil.parser.parse(self.code["timestamp"]) + + @timestamp.setter + def timestamp(self, timestamp: datetime) -> None: + + 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): + 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=code_str, headers={'Content-Type': 'application/json'}) diff --git a/test.py b/test.py index e25f4f4..a8ed34c 100644 --- a/test.py +++ b/test.py @@ -21,17 +21,19 @@ import datetime import openhab +import openhab.items base_url = 'http://localhost:8080/rest' -openhab = openhab.OpenHAB(base_url) + +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_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 new file mode 100644 index 0000000..86ec766 --- /dev/null +++ b/tests/test_create_delete_items.py @@ -0,0 +1,418 @@ +# -*- 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 +import openhab +import openhab.events +import openhab.items as items +import openhab.types +import logging +import random +import time +import tests.testutil as testutil +from datetime import datetime +from requests.auth import HTTPBasicAuth +from token_store import token + +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") + +TOKEN="" +base_url_oh2 = 'http://10.10.20.80:8080/rest' +base_url_oh3 = "http://10.10.20.85:8080/rest" + + +def test_create_and_delete_items(myopenhab: openhab.OpenHAB, nameprefix): + log.info("starting tests 'create and delete items'") + + 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 + a_rollershutter_item = None + a_color_item = None + a_dimmer_item = None + a_switch_item = None + a_player_item = None + try: + 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) + log.info("the new aContactItem:{}".format(a_contact_item)) + + a_datetime_item: openhab.items.DateTimeItem = test_DateTimeItem(my_item_factory, nameprefix) + log.info("the new aDatetimeItem:{}".format(a_datetime_item)) + + a_rollershutter_item: openhab.items.RollershutterItem = test_RollershutterItem(my_item_factory, nameprefix) + log.info("the new aRollershutterItem:{}".format(a_rollershutter_item)) + + a_color_item: openhab.items.ColorItem = test_ColorItem(my_item_factory, nameprefix) + log.info("the new aColorItem:{}".format(a_color_item)) + + a_dimmer_item: openhab.items.DimmerItem = test_DimmerItem(my_item_factory, nameprefix) + log.info("the new aDimmerItem:{}".format(a_dimmer_item)) + + a_switch_item: openhab.items.SwitchItem = test_SwitchItem(my_item_factory, nameprefix) + log.info("the new Switch:{}".format(a_switch_item)) + + 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: + if a_group_item is not None: + 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() + 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): + 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 + all_items = myopenhab.fetch_all_items() + count = 0 + for aItemname in all_items: + if aItemname.startswith(nameprefix): + 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 test_NumberItem(item_factory, nameprefix): + itemname = "{}CreateItemTest".format(nameprefix) + 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)] + + 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(item_quantity_type, x2.quantityType, "quantity_type") + testutil.doassert(itemlabel, x2.label, "label") + testutil.doassert(itemcategory, x2.category, "category") + for aExpectedTag in itemtags: + testutil.doassert(True, aExpectedTag in x2.tags, "tag {}".format(aExpectedTag)) + + for aExpectedGroupname in itemgroup_names: + testutil.doassert(True, aExpectedGroupname in x2.groupNames, "tag {}".format(aExpectedGroupname)) + + return x2 + + +def testGroup(itemFactory, nameprefix) -> openhab.items.Item: + itemtype = "Group" + itemname = "{}TestGroup".format(nameprefix) + testgroup_item = itemFactory.create_or_update_item(name=itemname, data_type=itemtype) + 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) + itemtype = openhab.items.ContactItem + + 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") + try: + x2.state = "SEPP" + testutil.doassert(False, True, "this should have caused a exception!") + except: + pass + + return x2 + + +def test_DateTimeItem(item_factory, nameprefix): + + itemname = "{}CreateDateTimeItemTest".format(nameprefix) + itemtype = openhab.items.DateTimeItem + + 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, "item_name") + testutil.doassert(now, x2.state, "itemstate") + return x2 + + +def test_RollershutterItem(item_factory, nameprefix): + + itemname = "{}CreateRollershutterItemTest".format(nameprefix) + itemtype = openhab.items.RollershutterItem + + x2: openhab.items.RollershutterItem = item_factory.create_or_update_item(name=itemname, data_type=itemtype) + + x2.up() + testutil.doassert(itemname, x2.name, "item_name") + + testutil.doassert("UP", x2.state, "itemstate") + x2.state = 53 + testutil.doassert(53, x2.state, "itemstate") + return x2 + + +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.on() + testutil.doassert(itemname, x2.name, "item_name") + testutil.doassert("ON", x2.state, "itemstate") + new_value = 51, 52, 53 + x2.state = new_value + + log.info("itemsate:{}".format(x2.state)) + testutil.doassert((51, 52, 53), x2.state, "itemstate") + return x2 + + +def test_DimmerItem(item_factory, nameprefix): + + itemname = "{}CreateDimmerItemTest".format(nameprefix) + itemtype = openhab.items.DimmerItem + + x2: openhab.items.DimmerItem = 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") + + x2.off() + testutil.doassert("OFF", x2.state, "itemstate") + + new_value = 51 + x2.state = new_value + + log.info("itemsate:{}".format(x2.state)) + testutil.doassert(new_value, x2.state, "itemstate") + return x2 + + +def test_SwitchItem(item_factory, nameprefix): + + itemname = "{}CreateSwitchItemTest".format(nameprefix) + itemtype = openhab.items.SwitchItem + + 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") + + x2.off() + testutil.doassert("OFF", x2.state, "itemstate") + + x2.toggle() + testutil.doassert("ON", x2.state, "itemstate") + + new_value = openhab.types.OnOffType.OFF + x2.state = new_value + + log.info("itemsate:{}".format(x2.state)) + testutil.doassert(new_value, x2.state, "itemstate") + return x2 + + +def test_PlayerItem(item_factory, nameprefix): + itemname = "{}CreatePlayerItemTest".format(nameprefix) + itemtype = openhab.items.PlayerItem + + x2: openhab.items.PlayerItem = item_factory.create_or_update_item(name=itemname, data_type=itemtype) + x2.play() + + testutil.doassert(itemname, x2.name, "item_name") + testutil.doassert("PLAY", x2.state, "itemstate") + + x2.pause() + 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() + +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 + testnumber_slotted.delete() + testnumber_not_slotted.delete() + + + + +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,"")) + +keeprunning = True +random.seed() +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) +#test_slotted_sending(item_factory=my_item_factory, myopenhab=myopenhab) diff --git a/tests/test_eventsubscription.py b/tests/test_eventsubscription.py new file mode 100644 index 0000000..cbab0b2 --- /dev/null +++ b/tests/test_eventsubscription.py @@ -0,0 +1,1117 @@ +# -*- 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 + +import openhab +import openhab.events +import openhab.types +import time +import openhab.items as items +import logging +import random +import testutil +from requests.auth import HTTPBasicAuth +from token_store import token + +from datetime import datetime + +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") +log.warning("waringingmessage") +log.info("infomessage") +log.debug("debugmessage") + + +base_url_oh2 = 'http://10.10.20.80:8080/rest' +base_url_oh3 = "http://10.10.20.85:8080/rest" + + +target_oh_version = 3 + + + +expected_state = None +state_correct_count = 0 +state_count = 0 + +expected_command = None +command_correct_count = 0 + +expected_new_state = None +expected_old_state = None +state_changed_correct_count = 0 +sleeptime_in_event_listener = 0 +count = 0 + +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( + 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)) + 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)) + 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)) + 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)) + 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)) + else: + testutil.doassert(expected_command, event.value, "command event item {}".format(item.name)) + command_correct_count += 1 + + + + +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, 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 + + 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 + + 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 + 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 + 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 + 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 °" + 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 °" + 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 °" + 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) + + expected_old_state = None + expected_command = 200.1 + sending_command = 200.1 + + 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 + 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_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 + 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" + 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 + 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 = "äöü°" + 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" + 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) + + expected_old_state = None + sending_command = "test value 3" + expected_new_state = expected_state = sending_command + expected_command = sending_command + + 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) + + 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() + 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) + + log.info("starting step 2") + expected_old_state = expected_state + expected_new_state = expected_state = sending_state = datetime.now() + 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) + + 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_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() + + +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 + 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 + 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 + 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 + 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 + + 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 + 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 + 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 + 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 + + 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 + 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 + 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 + 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 + + 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 + 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 + 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 + 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 + + 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 + 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 = 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 = 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 + 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 + 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 + sending_old_state = "1,2,3" + expected_new_state = expected_state = 4, 5, 6 + sending_state = "4,5,6" + + 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) + + 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 + + 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) + + expected_new_state = expected_state = sending_state = 45.67 + 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 = openhab.types.UpDownType.UP + expected_state = sending_state + 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 + + testitem.command(sending_command) + + time.sleep(0.5) + testutil.doassert(3, state_correct_count) + 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 + 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) + + 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) + + 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 + + expected_command = sending_command = openhab.types.StopMoveType.STOP + + 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(4, command_correct_count) + + finally: + pass + 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) + testitem.command(75.65) + testitem.command(75.64) + + 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_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() +log.info("stopping program") 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() diff --git a/tests/test_widgets.py b/tests/test_widgets.py new file mode 100644 index 0000000..75600ca --- /dev/null +++ b/tests/test_widgets.py @@ -0,0 +1,107 @@ +# -*- 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 +import yaml +from token_store import token + +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' + + + +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)) + + # 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() + + + + + + +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) + diff --git a/tests/testutil.py b/tests/testutil.py new file mode 100644 index 0000000..8139510 --- /dev/null +++ b/tests/testutil.py @@ -0,0 +1,28 @@ +# -*- 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 + +log = logging.getLogger() + + +def doassert(expect: Any, actual: Any, label: Optional[str] = ""): + assert actual == expect, "expected {label}:'{expect}', but it actually has '{actual}'".format(label=label, actual=actual, expect=expect) diff --git a/tests/token_store.py b/tests/token_store.py new file mode 100644 index 0000000..a59d35b --- /dev/null +++ b/tests/token_store.py @@ -0,0 +1 @@ +token = "in openhab admin web interface klick your created user (lower left corner). then create new API toker and copy it here"