From 39d948a03a80b80642ce9ad0edf443b69d650791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 08:41:25 +0200 Subject: [PATCH 01/36] Remove python 2 support, version 0.7.0 tag --- fhem/fhem/__init__.py | 512 +++++++++++++++++++++++++----------------- fhem/setup.py | 34 +-- selftest/selftest.py | 265 ++++++++++++---------- 3 files changed, 463 insertions(+), 348 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 765909f..50e5b53 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -1,4 +1,4 @@ -'''API for FHEM homeautomation server, supporting telnet or HTTP/HTTPS connections with authentication and CSRF-token support.''' +"""API for FHEM homeautomation server, supporting telnet or HTTP/HTTPS connections with authentication and CSRF-token support.""" import datetime import json import logging @@ -9,44 +9,37 @@ import threading import time -try: - # Python 3.x - from urllib.parse import quote - from urllib.parse import urlencode - from urllib.request import urlopen - from urllib.error import URLError - from urllib.request import HTTPSHandler - from urllib.request import HTTPPasswordMgrWithDefaultRealm - from urllib.request import HTTPBasicAuthHandler - from urllib.request import build_opener - from urllib.request import install_opener -except ImportError: - # Python 2.x - from urllib import urlencode - from urllib2 import quote - from urllib2 import urlopen - from urllib2 import URLError - from urllib2 import HTTPSHandler - from urllib2 import HTTPPasswordMgrWithDefaultRealm - from urllib2 import HTTPBasicAuthHandler - from urllib2 import build_opener - from urllib2 import install_opener +from urllib.parse import quote +from urllib.parse import urlencode +from urllib.request import urlopen +from urllib.error import URLError +from urllib.request import HTTPSHandler +from urllib.request import HTTPPasswordMgrWithDefaultRealm +from urllib.request import HTTPBasicAuthHandler +from urllib.request import build_opener +from urllib.request import install_opener # needs to be in sync with setup.py and documentation (conf.py, branch gh-pages) -__version__ = '0.6.5' - -# create logger with 'python_fhem' -# logger = logging.getLogger(__name__) +__version__ = "0.7.0" class Fhem: - '''Connects to FHEM via socket communication with optional SSL and password - support''' - - def __init__(self, server, port=7072, - use_ssl=False, protocol="telnet", username="", password="", csrf=True, - cafile="", loglevel=1): - ''' + """Connects to FHEM via socket communication with optional SSL and password + support""" + + def __init__( + self, + server, + port=7072, + use_ssl=False, + protocol="telnet", + username="", + password="", + csrf=True, + cafile="", + loglevel=1, + ): + """ Instantiate connector object. :param server: address of FHEM server @@ -58,15 +51,15 @@ def __init__(self, server, port=7072, :param csrf: (http(s)) use csrf token (FHEM 5.8 and newer), default True :param cafile: path to public certificate of your root authority, if left empty, https protocol will ignore certificate checks. :param loglevel: deprecated, will be removed. Please use standard python logging API with logger 'Fhem'. - ''' + """ self.log = logging.getLogger("Fhem") - validprots = ['http', 'https', 'telnet'] + validprots = ["http", "https", "telnet"] self.server = server self.port = port self.ssl = use_ssl self.csrf = csrf - self.csrftoken = '' + self.csrftoken = "" self.username = username self.password = password self.loglevel = loglevel @@ -101,30 +94,32 @@ def __init__(self, server, port=7072, self._install_opener() def connect(self): - '''create socket connection to server (telnet protocol only)''' - if self.protocol == 'telnet': + """create socket connection to server (telnet protocol only)""" + if self.protocol == "telnet": try: self.log.debug("Creating socket...") if self.ssl: - self.bsock = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) + self.bsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock = ssl.wrap_socket(self.bsock) - self.log.info("Connecting to {}:{} with SSL (TLS)".format( - self.server, self.port)) + self.log.info( + "Connecting to {}:{} with SSL (TLS)".format( + self.server, self.port + ) + ) else: - self.sock = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) - self.log.info("Connecting to {}:{} without SSL".format( - self.server, self.port)) + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.log.info( + "Connecting to {}:{} without SSL".format(self.server, self.port) + ) self.sock.connect((self.server, self.port)) self.connection = True - self.log.info("Connected to {}:{}".format( - self.server, self.port)) + self.log.info("Connected to {}:{}".format(self.server, self.port)) except socket.error: self.connection = False - self.log.error("Failed to connect to {}:{}".format( - self.server, self.port)) + self.log.error( + "Failed to connect to {}:{}".format(self.server, self.port) + ) return if self.password != "": @@ -155,29 +150,30 @@ def connect(self): stp = dat.find("csrf_") if stp != -1: token = dat[stp:] - token = token[:token.find("'")] + token = token[: token.find("'")] self.csrftoken = token self.connection = True else: self.log.error( - "CSRF token requested for server that doesn't know CSRF") + "CSRF token requested for server that doesn't know CSRF" + ) else: - self.log.error( - "No valid answer on send when expecting csrf.") + self.log.error("No valid answer on send when expecting csrf.") else: self.connection = True def connected(self): - '''Returns True if socket/http(s) session is connected to server.''' + """Returns True if socket/http(s) session is connected to server.""" return self.connection def set_loglevel(self, level): - '''Set logging level. [Deprecated, will be removed, use python logging.setLevel] + """Set logging level. [Deprecated, will be removed, use python logging.setLevel] :param level: 0: critical, 1: errors, 2: info, 3: debug - ''' + """ self.log.warning( - "Deprecation: please set logging levels using python's standard logging for logger 'Fhem'") + "Deprecation: please set logging levels using python's standard logging for logger 'Fhem'" + ) if level == 0: self.log.setLevel(logging.CRITICAL) elif level == 1: @@ -188,8 +184,8 @@ def set_loglevel(self, level): self.log.setLevel(logging.DEBUG) def close(self): - '''Closes socket connection. (telnet only)''' - if self.protocol == 'telnet': + """Closes socket connection. (telnet only)""" + if self.protocol == "telnet": if self.connected(): time.sleep(0.2) self.sock.close() @@ -204,8 +200,9 @@ def _install_opener(self): self.opener = None if self.username != "": self.password_mgr = HTTPPasswordMgrWithDefaultRealm() - self.password_mgr.add_password(None, self.baseurlauth, - self.username, self.password) + self.password_mgr.add_password( + None, self.baseurlauth, self.username, self.password + ) self.auth_handler = HTTPBasicAuthHandler(self.password_mgr) if self.ssl is True: if self.cafile == "": @@ -218,8 +215,7 @@ def _install_opener(self): self.context.verify_mode = ssl.CERT_REQUIRED self.https_handler = HTTPSHandler(context=self.context) if self.username != "": - self.opener = build_opener(self.https_handler, - self.auth_handler) + self.opener = build_opener(self.https_handler, self.auth_handler) else: self.opener = build_opener(self.https_handler) else: @@ -230,14 +226,14 @@ def _install_opener(self): install_opener(self.opener) def send(self, buf, timeout=10): - '''Sends a buffer to server + """Sends a buffer to server - :param buf: binary buffer''' + :param buf: binary buffer""" if len(buf) > 0: if not self.connected(): self.log.debug("Not connected, trying to connect...") self.connect() - if self.protocol == 'telnet': + if self.protocol == "telnet": if self.connected(): self.log.debug("Connected, sending...") try: @@ -246,12 +242,16 @@ def send(self, buf, timeout=10): return None except OSError as err: self.log.error( - "Failed to send msg, len={}. Exception raised: {}".format(len(buf), err)) + "Failed to send msg, len={}. Exception raised: {}".format( + len(buf), err + ) + ) self.connection = None return None else: self.log.error( - "Failed to send msg, len={}. Not connected.".format(len(buf))) + "Failed to send msg, len={}. Not connected.".format(len(buf)) + ) return None else: # HTTP(S) paramdata = None @@ -260,8 +260,8 @@ def send(self, buf, timeout=10): self.log.error("CSRF token not available!") self.connection = False else: - datas = {'fwcsrf': self.csrftoken} - paramdata = urlencode(datas).encode('UTF-8') + datas = {"fwcsrf": self.csrftoken} + paramdata = urlencode(datas).encode("UTF-8") try: self.log.debug("Cmd: {}".format(buf)) @@ -274,43 +274,45 @@ def send(self, buf, timeout=10): ccmd = self.baseurltoken self.log.info("Request: {}".format(ccmd)) - if ccmd.lower().startswith('http'): + if ccmd.lower().startswith("http"): ans = urlopen(ccmd, paramdata, timeout=timeout) else: self.log.error( - "Invalid URL {}, Failed to send msg, len={}, {}".format(ccmd, len(buf), err)) + "Invalid URL {}, Failed to send msg, len={}, {}".format( + ccmd, len(buf), err + ) + ) return None data = ans.read() return data except URLError as err: self.connection = False - self.log.error( - "Failed to send msg, len={}, {}".format(len(buf), err)) + self.log.error("Failed to send msg, len={}, {}".format(len(buf), err)) return None except socket.timeout as err: # Python 2.7 fix - self.log.error( - "Failed to send msg, len={}, {}".format(len(buf), err)) + self.log.error("Failed to send msg, len={}, {}".format(len(buf), err)) return None def send_cmd(self, msg, timeout=10.0): - '''Sends a command to server. + """Sends a command to server. :param msg: string with FHEM command, e.g. 'set lamp on' :param timeout: timeout on send (sec). - ''' + """ if not self.connected(): self.connect() if not self.nolog: self.log.debug("Sending: {}".format(msg)) - if self.protocol == 'telnet': + if self.protocol == "telnet": if self.connection: msg = "{}\n".format(msg) - cmd = msg.encode('utf-8') + cmd = msg.encode("utf-8") return self.send(cmd) else: self.log.error( - "Failed to send msg, len={}. Not connected.".format(len(msg))) + "Failed to send msg, len={}. Not connected.".format(len(msg)) + ) return None else: return self.send(msg, timeout=timeout) @@ -318,23 +320,24 @@ def send_cmd(self, msg, timeout=10.0): def _recv_nonblocking(self, timeout=0.1): if not self.connected(): self.connect() - data = b'' + data = b"" if self.connection: self.sock.setblocking(False) - data = b'' + data = b"" try: data = self.sock.recv(32000) except socket.error as err: # Resource temporarily unavailable, operation did not complete are expected - if err.errno != errno.EAGAIN and err.errno!= errno.ENOENT: + if err.errno != errno.EAGAIN and err.errno != errno.ENOENT: self.log.debug( - "Exception in non-blocking (1). Error: {}".format(err)) + "Exception in non-blocking (1). Error: {}".format(err) + ) time.sleep(timeout) wok = 1 while len(data) > 0 and wok > 0: time.sleep(timeout) - datai = b'' + datai = b"" try: datai = self.sock.recv(32000) if len(datai) == 0: @@ -343,25 +346,26 @@ def _recv_nonblocking(self, timeout=0.1): data += datai except socket.error as err: # Resource temporarily unavailable, operation did not complete are expected - if err.errno != errno.EAGAIN and err.errno!= errno.ENOENT: + if err.errno != errno.EAGAIN and err.errno != errno.ENOENT: self.log.debug( - "Exception in non-blocking (2). Error: {}".format(err)) + "Exception in non-blocking (2). Error: {}".format(err) + ) wok = 0 self.sock.setblocking(True) return data def send_recv_cmd(self, msg, timeout=0.1, blocking=False): - ''' + """ Sends a command to the server and waits for an immediate reply. :param msg: FHEM command (e.g. 'set lamp on') :param timeout: waiting time for reply :param blocking: (telnet only) on True: use blocking socket communication (bool) - ''' - data = b'' + """ + data = b"" if not self.connected(): self.connect() - if self.protocol == 'telnet': + if self.protocol == "telnet": if self.connection: self.send_cmd(msg) time.sleep(timeout) @@ -379,7 +383,8 @@ def send_recv_cmd(self, msg, timeout=0.1, blocking=False): self.sock.setblocking(True) else: self.log.error( - "Failed to send msg, len={}. Not connected.".format(len(msg))) + "Failed to send msg, len={}. Not connected.".format(len(msg)) + ) else: data = self.send_cmd(msg) if data is None: @@ -389,13 +394,14 @@ def send_recv_cmd(self, msg, timeout=0.1, blocking=False): return {} try: - sdata = data.decode('utf-8') + sdata = data.decode("utf-8") jdata = json.loads(sdata) except Exception as err: self.log.error( - "Failed to decode json, exception raised. {} {}".format(data, err)) + "Failed to decode json, exception raised. {} {}".format(data, err) + ) return {} - if len(jdata[u'Results']) == 0: + if len(jdata["Results"]) == 0: self.log.error("Query had no result.") return {} else: @@ -404,42 +410,54 @@ def send_recv_cmd(self, msg, timeout=0.1, blocking=False): def get_dev_state(self, dev, timeout=0.1): self.log.warning( - "Deprecation: use get_device('device') instead of get_dev_state") + "Deprecation: use get_device('device') instead of get_dev_state" + ) return self.get_device(dev, timeout=timeout, raw_result=True) def get_dev_reading(self, dev, reading, timeout=0.1): self.log.warning( - "Deprecation: use get_device_reading('device', 'reading') instead of get_dev_reading") + "Deprecation: use get_device_reading('device', 'reading') instead of get_dev_reading" + ) return self.get_device_reading(dev, reading, value_only=True, timeout=timeout) def getDevReadings(self, dev, reading, timeout=0.1): self.log.warning( - "Deprecation: use get_device_reading('device', ['reading']) instead of getDevReadings") - return self.get_device_reading(dev, timeout=timeout, value_only=True, raw_result=True) + "Deprecation: use get_device_reading('device', ['reading']) instead of getDevReadings" + ) + return self.get_device_reading( + dev, timeout=timeout, value_only=True, raw_result=True + ) def get_dev_readings(self, dev, readings, timeout=0.1): self.log.warning( - "Deprecation: use get_device_reading('device', ['reading']) instead of get_dev_readings") - return self.get_device_reading(dev, readings, timeout=timeout, value_only=True, raw_result=True) + "Deprecation: use get_device_reading('device', ['reading']) instead of get_dev_readings" + ) + return self.get_device_reading( + dev, readings, timeout=timeout, value_only=True, raw_result=True + ) def get_dev_reading_time(self, dev, reading, timeout=0.1): self.log.warning( - "Deprecation: use get_device_reading('device', 'reading', time_only=True) instead of get_dev_reading_time") + "Deprecation: use get_device_reading('device', 'reading', time_only=True) instead of get_dev_reading_time" + ) return self.get_device_reading(dev, reading, timeout=timeout, time_only=True) def get_dev_readings_time(self, dev, readings, timeout=0.1): self.log.warning( - "Deprecation: use get_device_reading('device', ['reading'], time_only=True) instead of get_dev_reading_time") + "Deprecation: use get_device_reading('device', ['reading'], time_only=True) instead of get_dev_reading_time" + ) return self.get_device_reading(dev, readings, timeout=timeout, time_only=True) def getFhemState(self, timeout=0.1): self.log.warning( - "Deprecation: use get() without parameters instead of getFhemState") + "Deprecation: use get() without parameters instead of getFhemState" + ) return self.get(timeout=timeout, raw_result=True) def get_fhem_state(self, timeout=0.1): self.log.warning( - "Deprecation: use get() without parameters instead of get_fhem_state") + "Deprecation: use get() without parameters instead of get_fhem_state" + ) return self.get(timeout=timeout, raw_result=True) @staticmethod @@ -457,21 +475,32 @@ def _response_filter(self, response, arg, value, value_only=None, time_only=None self.log.error("Too many positional arguments") return {} result = {} - for r in response if 'totalResultsReturned' not in response else response['Results']: + for r in ( + response if "totalResultsReturned" not in response else response["Results"] + ): arg = [arg[0]] if len(arg) and isinstance(arg[0], str) else arg if value_only: - result[r['Name']] = {k: v['Value'] for k, v in r[value].items() if - 'Value' in v and (not len(arg) or (len(arg) and k == arg[0]))} # k in arg[0]))} fixes #14 + result[r["Name"]] = { + k: v["Value"] + for k, v in r[value].items() + if "Value" in v and (not len(arg) or (len(arg) and k == arg[0])) + } # k in arg[0]))} fixes #14 elif time_only: - result[r['Name']] = {k: v['Time'] for k, v in r[value].items() if - 'Time' in v and (not len(arg) or (len(arg) and k == arg[0]))} # k in arg[0]))} + result[r["Name"]] = { + k: v["Time"] + for k, v in r[value].items() + if "Time" in v and (not len(arg) or (len(arg) and k == arg[0])) + } # k in arg[0]))} else: - result[r['Name']] = {k: v for k, v in r[value].items() if - (not len(arg) or (len(arg) and k == arg[0]))} # k in arg[0]))} - if not result[r['Name']]: - result.pop(r['Name'], None) - elif len(result[r['Name']].values()) == 1: - result[r['Name']] = list(result[r['Name']].values())[0] + result[r["Name"]] = { + k: v + for k, v in r[value].items() + if (not len(arg) or (len(arg) and k == arg[0])) + } # k in arg[0]))} + if not result[r["Name"]]: + result.pop(r["Name"], None) + elif len(result[r["Name"]].values()) == 1: + result[r["Name"]] = list(result[r["Name"]].values())[0] return result def _parse_filters(self, name, value, not_value, filter_list, case_sensitive): @@ -479,8 +508,7 @@ def _parse_filters(self, name, value, not_value, filter_list, case_sensitive): if value: self._append_filter(name, value, compare, "{}{}{}", filter_list) elif not_value: - self._append_filter(name, not_value, compare, - "{}!{}{}", filter_list) + self._append_filter(name, not_value, compare, "{}!{}{}", filter_list) def _convert_data(self, response, k, v): try: @@ -492,9 +520,10 @@ def _convert_data(self, response, k, v): response[k] = int(v) elif re.findall(r"^[0-9]+\.[0-9]+$", v): response[k] = float(v) - elif re.findall("^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", v): - response[k] = datetime.datetime.strptime( - v, '%Y-%m-%d %H:%M:%S') + elif re.findall( + "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", v + ): + response[k] = datetime.datetime.strptime(v, "%Y-%m-%d %H:%M:%S") if isinstance(v, dict): self._parse_data_types(response[k]) if isinstance(v, list): @@ -508,8 +537,24 @@ def _parse_data_types(self, response): for i, v in enumerate(response): self._convert_data(response, i, v) - def get(self, name=None, state=None, group=None, room=None, device_type=None, not_name=None, not_state=None, not_group=None, - not_room=None, not_device_type=None, case_sensitive=None, filters=None, timeout=0.1, blocking=False, raw_result=None): + def get( + self, + name=None, + state=None, + group=None, + room=None, + device_type=None, + not_name=None, + not_state=None, + not_group=None, + not_room=None, + not_device_type=None, + case_sensitive=None, + filters=None, + timeout=0.1, + blocking=False, + raw_result=None, + ): """ Get FHEM data of devices, can filter by parameters or custom defined filters. All filters use regular expressions (except full match), so don't forget escaping. @@ -537,30 +582,26 @@ def get(self, name=None, state=None, group=None, room=None, device_type=None, no self.connect() if self.connected(): filter_list = [] - self._parse_filters("NAME", name, not_name, - filter_list, case_sensitive) - self._parse_filters("STATE", state, not_state, - filter_list, case_sensitive) - self._parse_filters("group", group, not_group, - filter_list, case_sensitive) - self._parse_filters("room", room, not_room, - filter_list, case_sensitive) - self._parse_filters("TYPE", device_type, - not_device_type, filter_list, case_sensitive) + self._parse_filters("NAME", name, not_name, filter_list, case_sensitive) + self._parse_filters("STATE", state, not_state, filter_list, case_sensitive) + self._parse_filters("group", group, not_group, filter_list, case_sensitive) + self._parse_filters("room", room, not_room, filter_list, case_sensitive) + self._parse_filters( + "TYPE", device_type, not_device_type, filter_list, case_sensitive + ) if filters: for key, value in filters.items(): - filter_list.append("{}{}{}".format( - key, "=" if case_sensitive else "~", value)) + filter_list.append( + "{}{}{}".format(key, "=" if case_sensitive else "~", value) + ) cmd = "jsonlist2 {}".format(":FILTER=".join(filter_list)) - if self.protocol == 'telnet': - result = self.send_recv_cmd( - cmd, blocking=blocking, timeout=timeout) + if self.protocol == "telnet": + result = self.send_recv_cmd(cmd, blocking=blocking, timeout=timeout) else: - result = self.send_recv_cmd( - cmd, blocking=False, timeout=timeout) + result = self.send_recv_cmd(cmd, blocking=False, timeout=timeout) if not result or raw_result: return result - result = result['Results'] + result = result["Results"] self._parse_data_types(result) return result else: @@ -577,7 +618,11 @@ def get_states(self, **kwargs): response = self.get(**kwargs) if not response: return response - return {r['Name']: r['Readings']['state']['Value'] for r in response if 'state' in r['Readings']} + return { + r["Name"]: r["Readings"]["state"]["Value"] + for r in response + if "state" in r["Readings"] + } def get_readings(self, *arg, **kwargs): """ @@ -589,12 +634,14 @@ def get_readings(self, *arg, **kwargs): :param kwargs: use keyword arguments from :py:meth:`Fhem.get` function :return: dict of FHEM devices with readings """ - value_only = kwargs['value_only'] if 'value_only' in kwargs else None - time_only = kwargs['time_only'] if 'time_only' in kwargs else None - kwargs.pop('value_only', None) - kwargs.pop('time_only', None) + value_only = kwargs["value_only"] if "value_only" in kwargs else None + time_only = kwargs["time_only"] if "time_only" in kwargs else None + kwargs.pop("value_only", None) + kwargs.pop("time_only", None) response = self.get(**kwargs) - return self._response_filter(response, arg, 'Readings', value_only=value_only, time_only=time_only) + return self._response_filter( + response, arg, "Readings", value_only=value_only, time_only=time_only + ) def get_attributes(self, *arg, **kwargs): """ @@ -605,7 +652,7 @@ def get_attributes(self, *arg, **kwargs): :return: dict of FHEM devices with attributes """ response = self.get(**kwargs) - return self._response_filter(response, arg, 'Attributes') + return self._response_filter(response, arg, "Attributes") def get_internals(self, *arg, **kwargs): """ @@ -616,7 +663,7 @@ def get_internals(self, *arg, **kwargs): :return: dict of FHEM devices with internals """ response = self.get(**kwargs) - return self._response_filter(response, arg, 'Internals') + return self._response_filter(response, arg, "Internals") def get_device(self, device, **kwargs): """ @@ -677,14 +724,28 @@ def get_device_internal(self, device, *arg, **kwargs): class FhemEventQueue: - '''Creates a thread that listens to FHEM events and dispatches them to - a Python queue.''' - - def __init__(self, server, que, port=7072, protocol='telnet', - use_ssl=False, username="", password="", csrf=True, cafile="", - filterlist=None, timeout=0.1, - eventtimeout=60, serverregex=None, loglevel=1, raw_value=False): - ''' + """Creates a thread that listens to FHEM events and dispatches them to + a Python queue.""" + + def __init__( + self, + server, + que, + port=7072, + protocol="telnet", + use_ssl=False, + username="", + password="", + csrf=True, + cafile="", + filterlist=None, + timeout=0.1, + eventtimeout=60, + serverregex=None, + loglevel=1, + raw_value=False, + ): + """ Construct an event queue object, FHEM events will be queued into the queue given at initialization. :param server: FHEM server address @@ -702,34 +763,43 @@ def __init__(self, server, que, port=7072, protocol='telnet', :param serverregex: FHEM regex to restrict event messages on server side. :param loglevel: deprecated, will be removed. Use standard python logging function for logger 'FhemEventQueue', old: 0: no log, 1: errors, 2: info, 3: debug :param raw_value: default False. On True, the value of a reading is not parsed for units, and returned as-is. - ''' + """ # self.set_loglevel(loglevel) - self.log = logging.getLogger('FhemEventQueue') + self.log = logging.getLogger("FhemEventQueue") self.informcmd = "inform timer" self.timeout = timeout if serverregex is not None: self.informcmd += " " + serverregex - if protocol != 'telnet': + if protocol != "telnet": self.log.error("ONLY TELNET is currently supported for EventQueue") return - self.fhem = Fhem(server=server, port=port, use_ssl=use_ssl, username=username, - password=password, cafile=cafile, loglevel=loglevel) + self.fhem = Fhem( + server=server, + port=port, + use_ssl=use_ssl, + username=username, + password=password, + cafile=cafile, + loglevel=loglevel, + ) self.fhem.connect() time.sleep(timeout) - self.EventThread = threading.Thread(target=self._event_worker_thread, - args=(que, filterlist, - timeout, eventtimeout, raw_value)) + self.EventThread = threading.Thread( + target=self._event_worker_thread, + args=(que, filterlist, timeout, eventtimeout, raw_value), + ) self.EventThread.setDaemon(True) self.EventThread.start() def set_loglevel(self, level): - ''' + """ Set logging level, [Deprecated, will be removed, use python's logging.setLevel] :param level: 0: critical, 1: errors, 2: info, 3: debug - ''' + """ self.log.warning( - "Deprecation: please set logging levels using python's standard logging for logger 'FhemEventQueue'") + "Deprecation: please set logging levels using python's standard logging for logger 'FhemEventQueue'" + ) if level == 0: self.log.setLevel(logging.CRITICAL) elif level == 1: @@ -739,8 +809,9 @@ def set_loglevel(self, level): elif level == 3: self.log.setLevel(logging.DEBUG) - def _event_worker_thread(self, que, filterlist, timeout=0.1, - eventtimeout=120, raw_value=False): + def _event_worker_thread( + self, que, filterlist, timeout=0.1, eventtimeout=120, raw_value=False + ): self.log.debug("FhemEventQueue worker thread starting...") if self.fhem.connected() is not True: self.log.warning("EventQueueThread: Fhem is not connected!") @@ -758,7 +829,9 @@ def _event_worker_thread(self, que, filterlist, timeout=0.1, lastreceive = time.time() self.fhem.send_cmd(self.informcmd) else: - self.log.warning("Fhem is not connected in EventQueue thread, retrying!") + self.log.warning( + "Fhem is not connected in EventQueue thread, retrying!" + ) time.sleep(5.0) if first is True: first = False @@ -772,53 +845,70 @@ def _event_worker_thread(self, que, filterlist, timeout=0.1, if self.fhem.connected() is True: data = self.fhem._recv_nonblocking(timeout) - lines = data.decode('utf-8').split('\n') + lines = data.decode("utf-8").split("\n") for l in lines: if len(l) > 0: lastreceive = time.time() - li = l.split(' ') + li = l.split(" ") if len(li) > 4: - dd = li[0].split('-') - tt = li[1].split(':') + dd = li[0].split("-") + tt = li[1].split(":") try: - if '.' in tt[2]: + if "." in tt[2]: secs = float(tt[2]) tt[2] = str(int(secs)) - tt.append(str(int((secs-int(secs))*1000000))) + tt.append(str(int((secs - int(secs)) * 1000000))) except Exception as e: - self.log.warning("EventQueue: us-Bugfix failed with {}".format(e)) + self.log.warning( + "EventQueue: us-Bugfix failed with {}".format(e) + ) try: if len(tt) == 3: - dt = datetime.datetime(int(dd[0]), int(dd[1]), - int(dd[2]), int(tt[0]), - int(tt[1]), int(tt[2])) + dt = datetime.datetime( + int(dd[0]), + int(dd[1]), + int(dd[2]), + int(tt[0]), + int(tt[1]), + int(tt[2]), + ) else: - dt = datetime.datetime(int(dd[0]), int(dd[1]), - int(dd[2]), int(tt[0]), - int(tt[1]), int(tt[2]), int(tt[3])) + dt = datetime.datetime( + int(dd[0]), + int(dd[1]), + int(dd[2]), + int(tt[0]), + int(tt[1]), + int(tt[2]), + int(tt[3]), + ) except Exception as e: - self.log.debug("EventQueue: invalid date format in date={} time={}, event {} ignored: {}".format(li[0], li[1], l, e)) + self.log.debug( + "EventQueue: invalid date format in date={} time={}, event {} ignored: {}".format( + li[0], li[1], l, e + ) + ) continue devtype = li[2] dev = li[3] - val = '' + val = "" for i in range(4, len(li)): val += li[i] if i < len(li) - 1: val += " " full_val = val vl = val.split(" ") - val = '' - unit = '' + val = "" + unit = "" if len(vl) > 0: - if len(vl[0])>0 and vl[0][-1] == ':': + if len(vl[0]) > 0 and vl[0][-1] == ":": read = vl[0][:-1] if len(vl) > 1: val = vl[1] if len(vl) > 2: unit = vl[2] else: - read = 'STATE' + read = "STATE" if len(vl) > 0: val = vl[0] if len(vl) > 1: @@ -830,13 +920,13 @@ def _event_worker_thread(self, que, filterlist, timeout=0.1, for f in filterlist: adQt = True for c in f: - if c == 'devtype': + if c == "devtype": if devtype != f[c]: adQt = False - if c == 'device': + if c == "device": if dev != f[c]: adQt = False - if c == 'reading': + if c == "reading": if read != f[c]: adQt = False if adQt: @@ -844,21 +934,21 @@ def _event_worker_thread(self, que, filterlist, timeout=0.1, if adQ: if raw_value is False: ev = { - 'timestamp': dt, - 'devicetype': devtype, - 'device': dev, - 'reading': read, - 'value': val, - 'unit': unit + "timestamp": dt, + "devicetype": devtype, + "device": dev, + "reading": read, + "value": val, + "unit": unit, } else: - ev = { - 'timestamp': dt, - 'devicetype': devtype, - 'device': dev, - 'reading': read, - 'value': full_val, - 'unit': None + ev = { + "timestamp": dt, + "devicetype": devtype, + "device": dev, + "reading": read, + "value": full_val, + "unit": None, } que.put(ev) # self.log.debug("Event queued for {}".format(ev['device'])) @@ -868,6 +958,6 @@ def _event_worker_thread(self, que, filterlist, timeout=0.1, return def close(self): - '''Stop event thread and close socket.''' + """Stop event thread and close socket.""" self.eventThreadActive = False - time.sleep(0.5+self.timeout) + time.sleep(0.5 + self.timeout) diff --git a/fhem/setup.py b/fhem/setup.py index 08eb31c..ab39158 100644 --- a/fhem/setup.py +++ b/fhem/setup.py @@ -3,19 +3,21 @@ with open("README.md", "r") as fh: long_description = fh.read() -setup(name='fhem', - version='0.6.6', - description='Python API for FHEM home automation server', - long_description=long_description, - long_description_content_type="text/markdown", - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: MIT License', - ], - keywords='fhem home automation', - url='http://github.com/domschl/python-fhem', - author='Dominik Schloesser', - author_email='dsc@dosc.net', - license='MIT', - packages=['fhem'], - zip_safe=False) +setup( + name="fhem", + version="0.7.0", + description="Python API for FHEM home automation server", + long_description=long_description, + long_description_content_type="text/markdown", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + ], + keywords="fhem home automation", + url="http://github.com/domschl/python-fhem", + author="Dominik Schloesser", + author_email="dsc@dosc.net", + license="MIT", + packages=["fhem"], + zip_safe=False, +) diff --git a/selftest/selftest.py b/selftest/selftest.py index b9dc46d..3e2951f 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -3,37 +3,17 @@ import shutil import logging import time - -try: - # Python 3.x - import queue -except: - # Python 2.x - import Queue as queue - -try: - # Python 3.x - from urllib.parse import quote - from urllib.parse import urlencode - from urllib.request import urlopen - from urllib.error import URLError - from urllib.request import HTTPSHandler - from urllib.request import HTTPPasswordMgrWithDefaultRealm - from urllib.request import HTTPBasicAuthHandler - from urllib.request import build_opener - from urllib.request import install_opener -except ImportError: - # Python 2.x - from urllib import urlencode - from urllib2 import quote - from urllib2 import urlopen - from urllib2 import URLError - from urllib2 import HTTPSHandler - from urllib2 import HTTPPasswordMgrWithDefaultRealm - from urllib2 import HTTPBasicAuthHandler - from urllib2 import build_opener - from urllib2 import install_opener - +import queue + +from urllib.parse import quote +from urllib.parse import urlencode +from urllib.request import urlopen +from urllib.error import URLError +from urllib.request import HTTPSHandler +from urllib.request import HTTPPasswordMgrWithDefaultRealm +from urllib.request import HTTPBasicAuthHandler +from urllib.request import build_opener +from urllib.request import install_opener import tarfile import fhem @@ -47,7 +27,7 @@ class FhemSelfTester: def __init__(self): - self.log = logging.getLogger('SelfTester') + self.log = logging.getLogger("SelfTester") def download(self, filename, urlpath): """ @@ -61,7 +41,7 @@ def download(self, filename, urlpath): self.log.error("Failed to download {}, {}".format(urlpath, e)) return False try: - with open(filename, 'wb') as f: + with open(filename, "wb") as f: f.write(dat) except Exception as e: self.log.error("Failed to write {}, {}".format(filename, e)) @@ -77,23 +57,33 @@ def install(self, archivename, destination, sanity_check_file): """ if not archivename.endswith("tar.gz"): self.log.error( - "Archive needs to be of type *.tar.gz: {}".format(archivename)) + "Archive needs to be of type *.tar.gz: {}".format(archivename) + ) return False if not os.path.exists(archivename): self.log.error("Archive {} not found.".format(archivename)) return False - if "fhem" not in destination or (os.path.exists(destination) and not os.path.exists(sanity_check_file)): + if "fhem" not in destination or ( + os.path.exists(destination) and not os.path.exists(sanity_check_file) + ): self.log.error( - "Dangerous or inconsistent fhem install-path: {}, need destination with 'fhem' in name.".format(destination)) + "Dangerous or inconsistent fhem install-path: {}, need destination with 'fhem' in name.".format( + destination + ) + ) self.log.error( - "Or {} exists and sanity-check-file {} doesn't exist.".format(destination, sanity_check_file)) + "Or {} exists and sanity-check-file {} doesn't exist.".format( + destination, sanity_check_file + ) + ) return False if os.path.exists(destination): try: shutil.rmtree(destination) except Exception as e: self.log.error( - "Failed to remove existing installation at {}".format(destination)) + "Failed to remove existing installation at {}".format(destination) + ) return False try: tar = tarfile.open(archivename, "r:gz") @@ -103,18 +93,18 @@ def install(self, archivename, destination, sanity_check_file): self.log.error("Failed to extract {}, {}".format(archivename, e)) return True - def is_running(self, fhem_url='localhost', protocol='http', port=8083): + def is_running(self, fhem_url="localhost", protocol="http", port=8083): """ Check if an fhem server is already running. """ fh = fhem.Fhem(fhem_url, protocol=protocol, port=port) - ver = fh.send_cmd('version') + ver = fh.send_cmd("version") if ver is not None: fh.close() return ver return None - def shutdown(self, fhem_url='localhost', protocol='http', port=8083): + def shutdown(self, fhem_url="localhost", protocol="http", port=8083): """ Shutdown a running FHEM server """ @@ -131,6 +121,7 @@ def shutdown(self, fhem_url='localhost', protocol='http', port=8083): def set_reading(fhi, name, reading, value): fhi.send_cmd("setreading {} {} {}".format(name, reading, value)) + def create_device(fhi, name, readings): fhi.send_cmd("define {} dummy".format(name)) fhi.send_cmd("attr {} setList state:on,off".format(name)) @@ -142,78 +133,94 @@ def create_device(fhi, name, readings): readingList += rd fhi.send_cmd("attr {} readingList {}".format(name, readingList)) for rd in readings: - set_reading(fhi,name,rd,readings[rd]) + set_reading(fhi, name, rd, readings[rd]) -if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG,format='%(asctime)s.%(msecs)03d %(levelname)s %(message)s', - datefmt='%Y-%m-%d %H:%M:%S') +if __name__ == "__main__": + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s.%(msecs)03d %(levelname)s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) print("Start FhemSelfTest") st = FhemSelfTester() print("State 1: Object created.") config = { - 'archivename': "./fhem-5.9.tar.gz", - 'urlpath': "https://fhem.de/fhem-5.9.tar.gz", - 'destination': "./fhem", - 'fhem_file': "./fhem/fhem-5.9/fhem.pl", - 'config_file': "./fhem/fhem-5.9/fhem.cfg", - 'fhem_dir': "./fhem/fhem-5.9/", - 'exec': "cd fhem/fhem-5.9/ && perl fhem.pl fhem.cfg", - 'testhost': 'localhost', + "archivename": "./fhem-5.9.tar.gz", + "urlpath": "https://fhem.de/fhem-5.9.tar.gz", + "destination": "./fhem", + "fhem_file": "./fhem/fhem-5.9/fhem.pl", + "config_file": "./fhem/fhem-5.9/fhem.cfg", + "fhem_dir": "./fhem/fhem-5.9/", + "exec": "cd fhem/fhem-5.9/ && perl fhem.pl fhem.cfg", + "testhost": "localhost", } - if st.is_running(fhem_url=config['testhost'], protocol='http', port=8083) is not None: + if ( + st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) + is not None + ): print("Fhem is already running!") - st.shutdown(fhem_url=config['testhost'], protocol='http', port=8083) + st.shutdown(fhem_url=config["testhost"], protocol="http", port=8083) time.sleep(1) - if st.is_running(fhem_url=config['testhost'], protocol='http', port=8083) is not None: + if ( + st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) + is not None + ): print("Shutdown failed!") sys.exit(-3) print("--------------------") print("Reinstalling FHEM...") - if not st.download(config['archivename'], config['urlpath']): + if not st.download(config["archivename"], config["urlpath"]): print("Download failed.") sys.exit(-1) print("Starting fhem installation") -# WARNING! THIS DELETES ANY EXISTING FHEM SERVER at 'destination'! -# All configuration files, databases, logs etc. are DELETED to allow a fresh test install! - if not st.install(config['archivename'], config['destination'], config['fhem_file']): + # WARNING! THIS DELETES ANY EXISTING FHEM SERVER at 'destination'! + # All configuration files, databases, logs etc. are DELETED to allow a fresh test install! + if not st.install( + config["archivename"], config["destination"], config["fhem_file"] + ): print("Install failed") sys.exit(-2) - os.system('cat fhem-config-addon.cfg >> {}'.format(config['config_file'])) + os.system("cat fhem-config-addon.cfg >> {}".format(config["config_file"])) - certs_dir = os.path.join(config['fhem_dir'], 'certs') - os.system('mkdir {}'.format(certs_dir)) - os.system('cd {} && openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 36500 -out server-cert.pem -subj "/C=DE/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=www.example.com/emailAddress=email@example.com"'.format(certs_dir)) + certs_dir = os.path.join(config["fhem_dir"], "certs") + os.system("mkdir {}".format(certs_dir)) + os.system( + 'cd {} && openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 36500 -out server-cert.pem -subj "/C=DE/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=www.example.com/emailAddress=email@example.com"'.format( + certs_dir + ) + ) - os.system(config['exec']) + os.system(config["exec"]) time.sleep(1) - if st.is_running(fhem_url=config['testhost'], protocol='http', port=8083) is None: + if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: print("Fhem is NOT running after install and start!") sys.exit(-4) print("Install should be ok, Fhem running.") connections = [ - {'protocol': 'http', - 'port': 8083}, - {'protocol': 'telnet', - 'port': 7073, - 'use_ssl': True, - 'password': 'secretsauce'}, - {'protocol': 'telnet', - 'port': 7072}, - {'protocol': 'https', - 'port': 8084}, - {'protocol': 'https', - 'port': 8085, - 'username': 'test', - 'password': 'secretsauce'}, + {"protocol": "http", "port": 8083}, + { + "protocol": "telnet", + "port": 7073, + "use_ssl": True, + "password": "secretsauce", + }, + {"protocol": "telnet", "port": 7072}, + {"protocol": "https", "port": 8084}, + { + "protocol": "https", + "port": 8085, + "username": "test", + "password": "secretsauce", + }, ] first = True @@ -221,53 +228,65 @@ def create_device(fhi, name, readings): print("----------------- Fhem ------------") print("Testing python-fhem Fhem():") for connection in connections: - print('Testing connection to {} via {}'.format( - config['testhost'], connection)) - fh = fhem.Fhem(config['testhost'], **connection) + print("Testing connection to {} via {}".format(config["testhost"], connection)) + fh = fhem.Fhem(config["testhost"], **connection) devs = [ - {'name': 'clima_sensor1', - 'readings': {'temperature': 18.2, - 'humidity': 88.2}}, - {'name': 'clima_sensor2', - 'readings': {'temperature': 19.1, - 'humidity': 85.7}} + { + "name": "clima_sensor1", + "readings": {"temperature": 18.2, "humidity": 88.2}, + }, + { + "name": "clima_sensor2", + "readings": {"temperature": 19.1, "humidity": 85.7}, + }, ] if first is True: for dev in devs: - create_device(fh, dev['name'], dev['readings']) + create_device(fh, dev["name"], dev["readings"]) first = False for dev in devs: for i in range(10): - print("Repetion: {}".format(i+1)) - for rd in dev['readings']: - dict_value = fh.get_device_reading( - dev['name'], rd, blocking=False) + print("Repetion: {}".format(i + 1)) + for rd in dev["readings"]: + dict_value = fh.get_device_reading(dev["name"], rd, blocking=False) try: - value = dict_value['Value'] + value = dict_value["Value"] except: print( - 'Bad reply reading {} {} -> {}'.format(dev['name'], rd, dict_value)) + "Bad reply reading {} {} -> {}".format( + dev["name"], rd, dict_value + ) + ) sys.exit(-7) - if value == dev['readings'][rd]: + if value == dev["readings"][rd]: print( - "Reading-test {},{}={} ok.".format(dev['name'], rd, dev['readings'][rd])) + "Reading-test {},{}={} ok.".format( + dev["name"], rd, dev["readings"][rd] + ) + ) else: - print("Failed to set and read reading! {},{} {} != {}".format( - dev['name'], rd, value, dev['readings'][rd])) + print( + "Failed to set and read reading! {},{} {} != {}".format( + dev["name"], rd, value, dev["readings"][rd] + ) + ) sys.exit(-5) num_temps = 0 for dev in devs: - if 'temperature' in dev['readings']: + if "temperature" in dev["readings"]: num_temps += 1 temps = fh.get_readings("temperature", timeout=0.1, blocking=False) if len(temps) != num_temps: - print("There should have been {} devices with temperature reading, but we got {}. Ans: {}".format( - num_temps, len(temps), temps)) + print( + "There should have been {} devices with temperature reading, but we got {}. Ans: {}".format( + num_temps, len(temps), temps + ) + ) sys.exit(-6) else: print("Multiread of all devices with 'temperature' reading: ok.") @@ -285,34 +304,34 @@ def create_device(fhi, name, readings): print("---------------Queues--------------------------") print("Testing python-fhem telnet FhemEventQueues():") for connection in connections: - if connection['protocol'] != 'telnet': + if connection["protocol"] != "telnet": continue - print('Testing connection to {} via {}'.format( - config['testhost'], connection)) - fh = fhem.Fhem(config['testhost'], **connections[0]) + print("Testing connection to {} via {}".format(config["testhost"], connection)) + fh = fhem.Fhem(config["testhost"], **connections[0]) que = queue.Queue() - que_events=0 - fq = fhem.FhemEventQueue(config['testhost'], que, **connection) - + que_events = 0 + fq = fhem.FhemEventQueue(config["testhost"], que, **connection) + devs = [ - {'name': 'clima_sensor1', - 'readings': {'temperature': 18.2, - 'humidity': 88.2}}, - {'name': 'clima_sensor2', - 'readings': {'temperature': 19.1, - 'humidity': 85.7}} + { + "name": "clima_sensor1", + "readings": {"temperature": 18.2, "humidity": 88.2}, + }, + { + "name": "clima_sensor2", + "readings": {"temperature": 19.1, "humidity": 85.7}, + }, ] time.sleep(1.0) for dev in devs: for i in range(10): - print("Repetion: {}".format(i+1)) - for rd in dev['readings']: - set_reading(fh,dev['name'],rd,18.0+i/0.2) + print("Repetion: {}".format(i + 1)) + for rd in dev["readings"]: + set_reading(fh, dev["name"], rd, 18.0 + i / 0.2) que_events += 1 time.sleep(0.05) - time.sleep(3) # This is crucial due to python's "thread"-handling. ql = 0 has_data = True @@ -327,7 +346,11 @@ def create_device(fhi, name, readings): print("Queue length: {}".format(ql)) if ql != que_events: - print("FhemEventQueue contains {} entries, expected {} entries, failure.".format(ql,que_events)) + print( + "FhemEventQueue contains {} entries, expected {} entries, failure.".format( + ql, que_events + ) + ) sys.exit(-8) else: print("Queue test success, Ok.") From cb967324dabf2761ef9db76aeffea5955693cab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 09:03:15 +0200 Subject: [PATCH 02/36] Selftest updated to use FHEM 6.0 --- README.md | 2 +- selftest/README.md | 14 ++++++++++++-- selftest/selftest.py | 12 ++++++------ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e9576ad..efc1df9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ [![PyPI version](https://badge.fury.io/py/fhem.svg)](https://badge.fury.io/py/fhem) -[![TravisCI Test Status](https://travis-ci.org/domschl/python-fhem.svg?branch=master)](https://travis-ci.org/domschl/python-fhem) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/116e9e988d934aaa9cfbfa5b8aef7f78)](https://www.codacy.com/app/dominik.schloesser/python-fhem?utm_source=github.com&utm_medium=referral&utm_content=domschl/python-fhem&utm_campaign=Badge_Grade) [![License](http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE) [![Docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://domschl.github.io/python-fhem/index.html) @@ -46,6 +45,7 @@ pip install [-U] -e . ## History +* 0.7.0 (2023-08-17): [unpublished] Ongoing: move Travis CI -> Github actions, Python 2.x support removed, modernize python packaging. * 0.6.6 (2022-11-09): [unpublished] Fix for new option that produces fractional seconds in event data. * 0.6.5 (2020-03-24): New option `raw_value` for `FhemEventQueue`. Default `False` (old behavior), on `True`, the full, unparsed reading is returned, without looking for a unit. * 0.6.4 (2020-03-24): Bug fix for [#21](https://github.com/domschl/python-fhem/issues/21), Index out-of-range in event loop background thread for non-standard event formats. diff --git a/selftest/README.md b/selftest/README.md index 2b5db51..c8a4067 100644 --- a/selftest/README.md +++ b/selftest/README.md @@ -1,6 +1,6 @@ ## Automatic FHEM installation and python-fhem API self-test for CI. -The selftest tree is only used for continous integration with TravisCI. +The selftest can be used manually, but are targeted for use with github action CI. The scripts automatically download the latest FHEM release, install, configure and run it and then use the Python API to perform self-tests. @@ -24,4 +24,14 @@ It needs to be installed with either: * or `apt-get install libio-socket-ssl-perl` * or `pacman -S perl-io-socket-ssl` -If selftests fails on the first SSL connection, it is usually a sign, that the fhem-perl requirements for SSL are not installed. \ No newline at end of file +If selftests fails on the first SSL connection, it is usually a sign, that the fhem-perl requirements for SSL are not installed. + +## Manual test run + +- Install `python-fhem`, e.g. by `pip install -e .` in the fhem source directory. +- Make sure that Perl's `socke::ssl` is installed (s.a.) +- Run `python selftest.py` + +## History + +- 2023-08-17: Updated for FHEM 6.0, python 2.x support removed. Prepared move from Travis CI to github actions. diff --git a/selftest/selftest.py b/selftest/selftest.py index 3e2951f..87347f0 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -146,13 +146,13 @@ def create_device(fhi, name, readings): st = FhemSelfTester() print("State 1: Object created.") config = { - "archivename": "./fhem-5.9.tar.gz", - "urlpath": "https://fhem.de/fhem-5.9.tar.gz", + "archivename": "./fhem-6.0.tar.gz", + "urlpath": "https://fhem.de/fhem-6.0.tar.gz", "destination": "./fhem", - "fhem_file": "./fhem/fhem-5.9/fhem.pl", - "config_file": "./fhem/fhem-5.9/fhem.cfg", - "fhem_dir": "./fhem/fhem-5.9/", - "exec": "cd fhem/fhem-5.9/ && perl fhem.pl fhem.cfg", + "fhem_file": "./fhem/fhem-6.0/fhem.pl", + "config_file": "./fhem/fhem-6.0/fhem.cfg", + "fhem_dir": "./fhem/fhem-6.0/", + "exec": "cd fhem/fhem-6.0/ && perl fhem.pl fhem.cfg", "testhost": "localhost", } From 381267c922335874d3a885f6b7d3803c146daf87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 14:55:23 +0200 Subject: [PATCH 03/36] Infra updates, preps for 0.7.0 --- .github/workflows/python-fhem-test.yaml | 43 +++++++++++++++++++++++++ LICENSE | 2 +- fhem/MANIFEST.in | 1 - fhem/setup.cfg | 5 --- fhem/setup.py | 20 ++++++------ publish.sh | 13 ++++++++ 6 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/python-fhem-test.yaml delete mode 100644 fhem/MANIFEST.in delete mode 100644 fhem/setup.cfg create mode 100755 publish.sh diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml new file mode 100644 index 0000000..53de1a4 --- /dev/null +++ b/.github/workflows/python-fhem-test.yaml @@ -0,0 +1,43 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python package + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.6", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Get perl stuff + sudo apt install perl libio-socket-ssl-perl + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install twine build + - name: Build python-fhem package + run | + cd fhem + cp ../README.md . + python -m build + -m name: Install python-fhem + python -m pip install ./fhem/dist/*.gz + - name: Test with selftest.py + run: | + cd selftest + python selftest.py diff --git a/LICENSE b/LICENSE index 70f4c6d..0deb158 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016-2018 Dominik Schlösser, dsc@dosc.net +Copyright (c) 2016-2023 Dominik Schlösser, dsc@dosc.net Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/fhem/MANIFEST.in b/fhem/MANIFEST.in deleted file mode 100644 index e45f46a..0000000 --- a/fhem/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include ../README.md diff --git a/fhem/setup.cfg b/fhem/setup.cfg deleted file mode 100644 index 79bc678..0000000 --- a/fhem/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[bdist_wheel] -# This flag says that the code is written to work on both Python 2 and Python -# 3. If at all possible, it is good practice to do this. If you cannot, you -# will need to generate wheels for each Python version that you support. -universal=1 diff --git a/fhem/setup.py b/fhem/setup.py index ab39158..d2fa230 100644 --- a/fhem/setup.py +++ b/fhem/setup.py @@ -1,23 +1,25 @@ -from setuptools import setup +import setuptools with open("README.md", "r") as fh: long_description = fh.read() -setup( +setuptools.setup( name="fhem", version="0.7.0", + author="Dominik Schloesser", + author_email="dsc@dosc.net", description="Python API for FHEM home automation server", long_description=long_description, long_description_content_type="text/markdown", + url="http://github.com/domschl/python-fhem", + project_urls={"Bug Tracker": "https://github.com/domschl/python-fhem/issues"}, classifiers=[ + "Programming Language :: Python :: 3", "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", ], - keywords="fhem home automation", - url="http://github.com/domschl/python-fhem", - author="Dominik Schloesser", - author_email="dsc@dosc.net", - license="MIT", - packages=["fhem"], - zip_safe=False, + package_dir={"": "."}, + packages=setuptools.find_packages(where="."), + python_requires=">=3.6", ) diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..148b906 --- /dev/null +++ b/publish.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +if [ ! -f ~/.pypirc ]; then + echo "Please configure .pypirc for pypi access first" + exit -2 +fi +cd fhem +cp ../README.md . +rm dist/* +export PIP_USER= +python -m build +# twine upload dist/* + From d44b927a1d6d72eae6e14f060a0b9a6b9089bff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 14:58:01 +0200 Subject: [PATCH 04/36] workfl --- .github/workflows/python-fhem-test.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index 53de1a4..f82ae47 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -25,17 +25,19 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Get perl stuff + run: | sudo apt install perl libio-socket-ssl-perl - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install twine build - name: Build python-fhem package - run | + run: | cd fhem cp ../README.md . python -m build -m name: Install python-fhem + run: | python -m pip install ./fhem/dist/*.gz - name: Test with selftest.py run: | From 4e375db9bfc9b412cc4c4df26e7c99c9193b4227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 14:58:45 +0200 Subject: [PATCH 05/36] workfl --- .github/workflows/python-fhem-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index f82ae47..af0574f 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -36,7 +36,7 @@ jobs: cd fhem cp ../README.md . python -m build - -m name: Install python-fhem + - name: Install python-fhem run: | python -m pip install ./fhem/dist/*.gz - name: Test with selftest.py From 71192a1beb3c074c5243c26983f488601da2a85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 15:04:33 +0200 Subject: [PATCH 06/36] x07 --- .github/workflows/python-fhem-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index af0574f..ac741ed 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -5,7 +5,7 @@ name: Python package on: push: - branches: [ "master" ] + branches: [ "master", "dev_0.7.0" ] pull_request: branches: [ "master" ] From 05dc5c5c2228a092dcccffee96f216f5dc339b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 15:06:06 +0200 Subject: [PATCH 07/36] py3.6.15 --- .github/workflows/python-fhem-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index ac741ed..6f839ec 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.10", "3.11"] + python-version: ["3.6.15", "3.10", "3.11"] steps: - uses: actions/checkout@v3 From 803c6b3c4cab02a47b8fc5190ff1439c69b28262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 15:09:04 +0200 Subject: [PATCH 08/36] py38maybe --- .github/workflows/python-fhem-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index 6f839ec..d14316d 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6.15", "3.10", "3.11"] + python-version: ["3.8", "3.10", "3.11"] steps: - uses: actions/checkout@v3 From c4e2c46ab88333d2d8f321c0f3a677c672953a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 15:20:05 +0200 Subject: [PATCH 09/36] module name in logs --- selftest/selftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index 87347f0..324e8da 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -139,7 +139,7 @@ def create_device(fhi, name, readings): if __name__ == "__main__": logging.basicConfig( level=logging.DEBUG, - format="%(asctime)s.%(msecs)03d %(levelname)s %(message)s", + format="%(asctime)s.%(msecs)03d %(name)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) print("Start FhemSelfTest") From ce7376ba81d5c481fc0e04966a850573bd013bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 18:14:53 +0200 Subject: [PATCH 10/36] Ongoing tests --- fhem/fhem/__init__.py | 109 +++++++++++++++++++-------------- selftest/fhem-config-addon.cfg | 3 +- selftest/selftest.py | 88 +++++++++++++++----------- 3 files changed, 119 insertions(+), 81 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 50e5b53..9479d9a 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -96,31 +96,44 @@ def __init__( def connect(self): """create socket connection to server (telnet protocol only)""" if self.protocol == "telnet": - try: - self.log.debug("Creating socket...") - if self.ssl: - self.bsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock = ssl.wrap_socket(self.bsock) - self.log.info( - "Connecting to {}:{} with SSL (TLS)".format( - self.server, self.port - ) - ) - else: - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.log.info( - "Connecting to {}:{} without SSL".format(self.server, self.port) + # try: + self.log.debug("Creating socket...") + if self.ssl: + self.bsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + self.sock = context.wrap_socket(self.bsock) + self.log.info( + "Connecting to {}:{} with SSL (TLS)".format( + self.server, self.port ) - - self.sock.connect((self.server, self.port)) - self.connection = True - self.log.info("Connected to {}:{}".format(self.server, self.port)) - except socket.error: - self.connection = False - self.log.error( - "Failed to connect to {}:{}".format(self.server, self.port) ) - return + else: + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.log.info( + "Connecting to {}:{} without SSL".format(self.server, self.port) + ) + # except Exception as e: + # self.connection = False + # self.log.error( + # "Failed to create socket to {}:{}: {}".format(self.server, self.port, e) + # ) + # return + + self.log.debug("pre-connect (no try/except)") + # try: + # self.sock.timeout = 5.0 + self.sock.connect((self.server, self.port)) + self.log.debug("post-connect") + # except Exception as e: + # self.connection = False + # self.log.error( + # "Failed to connect to {}:{}: {}".format(self.server, self.port, e) + # ) + # return + self.connection = True + self.log.info("Connected to {}:{}".format(self.server, self.port)) if self.password != "": # time.sleep(1.0) @@ -219,6 +232,7 @@ def _install_opener(self): else: self.opener = build_opener(self.https_handler) else: + self.context = None if self.username != "": self.opener = build_opener(self.auth_handler) if self.opener is not None: @@ -263,36 +277,41 @@ def send(self, buf, timeout=10): datas = {"fwcsrf": self.csrftoken} paramdata = urlencode(datas).encode("UTF-8") - try: + # try: + if len(buf) > 0: self.log.debug("Cmd: {}".format(buf)) cmd = quote(buf) self.log.debug("Cmd-enc: {}".format(cmd)) + else: + cmd = "" + if len(cmd) > 0: + ccmd = self.baseurl + cmd + else: + ccmd = self.baseurltoken - if len(cmd) > 0: - ccmd = self.baseurl + cmd - else: - ccmd = self.baseurltoken - - self.log.info("Request: {}".format(ccmd)) - if ccmd.lower().startswith("http"): + self.log.info("Request: {}".format(ccmd)) + if ccmd.lower().startswith("http"): + if self.context is None: ans = urlopen(ccmd, paramdata, timeout=timeout) else: - self.log.error( - "Invalid URL {}, Failed to send msg, len={}, {}".format( - ccmd, len(buf), err - ) + ans = urlopen(ccmd, paramdata, timeout=timeout, context=self.context) + else: + self.log.error( + "Invalid URL {}, Failed to send msg, len={}, {}".format( + ccmd, len(buf), err ) - return None - data = ans.read() - return data - except URLError as err: - self.connection = False - self.log.error("Failed to send msg, len={}, {}".format(len(buf), err)) - return None - except socket.timeout as err: - # Python 2.7 fix - self.log.error("Failed to send msg, len={}, {}".format(len(buf), err)) + ) return None + data = ans.read() + return data + # except URLError as err: + # self.connection = False + # self.log.error("Failed to send msg, len={}, {}".format(len(buf), err)) + # return None + # except socket.timeout as err: + # # Python 2.7 fix + # self.log.error("Failed to send msg, len={}, {}".format(len(buf), err)) + # return None def send_cmd(self, msg, timeout=10.0): """Sends a command to server. diff --git a/selftest/fhem-config-addon.cfg b/selftest/fhem-config-addon.cfg index 932418d..f1f4df0 100644 --- a/selftest/fhem-config-addon.cfg +++ b/selftest/fhem-config-addon.cfg @@ -8,6 +8,7 @@ define telnetPort telnet 7072 global define telnetPort2 telnet 7073 global attr telnetPort2 SSL 1 +attr telnetPort2 sslVersion TLSv12:!SSLv3 define allowTelPort2 allowed attr allowTelPort2 password secretsauce attr allowTelPort2 validFor telnetPort2 @@ -22,7 +23,7 @@ attr WEBS longpoll 1 define WebPwd FHEMWEB 8085 global attr WebPwd HTTPS 1 -attr WEBS sslVersion TLSv12:!SSLv3 +attr WebPwd sslVersion TLSv12:!SSLv3 attr WebPwd longpoll 1 define allowWebPwd allowed # test:secretsauce NOTE: do not reuse those values for actual installations! diff --git a/selftest/selftest.py b/selftest/selftest.py index 324e8da..aa85f89 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -45,6 +45,8 @@ def download(self, filename, urlpath): f.write(dat) except Exception as e: self.log.error("Failed to write {}, {}".format(filename, e)) + return False + self.log.debug("Downloaded {} to {}".format(urlpath, filename)) return True def install(self, archivename, destination, sanity_check_file): @@ -91,17 +93,25 @@ def install(self, archivename, destination, sanity_check_file): tar.close() except Exception as e: self.log.error("Failed to extract {}, {}".format(archivename, e)) + return False + self.log.debug("Extracted {} to {}".format(archivename, destination)) return True def is_running(self, fhem_url="localhost", protocol="http", port=8083): """ Check if an fhem server is already running. """ - fh = fhem.Fhem(fhem_url, protocol=protocol, port=port) - ver = fh.send_cmd("version") + try: + fh = fhem.Fhem(fhem_url, protocol=protocol, port=port) + ver = fh.send_cmd("version") + except Exception as e: + ver = None if ver is not None: fh.close() + self.log.warning("Fhem already running at {}".format(fhem_url)) return ver + fh.close() + self.log.debug("Fhem not running at {}".format(fhem_url)) return None def shutdown(self, fhem_url="localhost", protocol="http", port=8083): @@ -142,9 +152,10 @@ def create_device(fhi, name, readings): format="%(asctime)s.%(msecs)03d %(name)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) - print("Start FhemSelfTest") + log = logging.getLogger("SelfTesterMainApp") + log.info("Start FhemSelfTest") st = FhemSelfTester() - print("State 1: Object created.") + log.info("State 1: Object created.") config = { "archivename": "./fhem-6.0.tar.gz", "urlpath": "https://fhem.de/fhem-6.0.tar.gz", @@ -160,30 +171,30 @@ def create_device(fhi, name, readings): st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is not None ): - print("Fhem is already running!") + log.info("Fhem is already running!") st.shutdown(fhem_url=config["testhost"], protocol="http", port=8083) time.sleep(1) if ( st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is not None ): - print("Shutdown failed!") + log.error("Shutdown failed!") sys.exit(-3) - print("--------------------") - print("Reinstalling FHEM...") + log.info("--------------------") + log.info("Reinstalling FHEM...") if not st.download(config["archivename"], config["urlpath"]): - print("Download failed.") + log.error("Download failed.") sys.exit(-1) - print("Starting fhem installation") + log.info("Starting fhem installation") # WARNING! THIS DELETES ANY EXISTING FHEM SERVER at 'destination'! # All configuration files, databases, logs etc. are DELETED to allow a fresh test install! if not st.install( config["archivename"], config["destination"], config["fhem_file"] ): - print("Install failed") + log.info("Install failed") sys.exit(-2) os.system("cat fhem-config-addon.cfg >> {}".format(config["config_file"])) @@ -196,14 +207,20 @@ def create_device(fhi, name, readings): ) ) + cert_file = os.path.join(certs_dir, "server-cert.pem") + key_file = os.path.join(certs_dir, "server-key.pem") + if not os.path.exists(cert_file) or not os.path.exists(key_file): + log.error("Failed to create certificate files!") + sys.exit(-2) + os.system(config["exec"]) time.sleep(1) if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: - print("Fhem is NOT running after install and start!") + log.error("Fhem is NOT running after install and start!") sys.exit(-4) - print("Install should be ok, Fhem running.") + log.info("Install should be ok, Fhem running.") connections = [ {"protocol": "http", "port": 8083}, @@ -224,11 +241,11 @@ def create_device(fhi, name, readings): ] first = True - print("") - print("----------------- Fhem ------------") - print("Testing python-fhem Fhem():") + log.info("") + log.info("----------------- Fhem ------------") + log.info("Testing python-fhem Fhem():") for connection in connections: - print("Testing connection to {} via {}".format(config["testhost"], connection)) + log.info("Testing connection to {} via {}".format(config["testhost"], connection)) fh = fhem.Fhem(config["testhost"], **connection) devs = [ @@ -249,13 +266,16 @@ def create_device(fhi, name, readings): for dev in devs: for i in range(10): - print("Repetion: {}".format(i + 1)) + log.debug("Repetion: {}, connection: {}".format(i + 1, fh.connection)) + if fh.connected() is False: + log.info("Connecting...") + fh.connect() for rd in dev["readings"]: dict_value = fh.get_device_reading(dev["name"], rd, blocking=False) try: value = dict_value["Value"] except: - print( + log.error( "Bad reply reading {} {} -> {}".format( dev["name"], rd, dict_value ) @@ -263,13 +283,13 @@ def create_device(fhi, name, readings): sys.exit(-7) if value == dev["readings"][rd]: - print( + log.debug( "Reading-test {},{}={} ok.".format( dev["name"], rd, dev["readings"][rd] ) ) else: - print( + log.error( "Failed to set and read reading! {},{} {} != {}".format( dev["name"], rd, value, dev["readings"][rd] ) @@ -282,31 +302,29 @@ def create_device(fhi, name, readings): num_temps += 1 temps = fh.get_readings("temperature", timeout=0.1, blocking=False) if len(temps) != num_temps: - print( + log.error( "There should have been {} devices with temperature reading, but we got {}. Ans: {}".format( num_temps, len(temps), temps ) ) sys.exit(-6) else: - print("Multiread of all devices with 'temperature' reading: ok.") + log.info("Multiread of all devices with 'temperature' reading: ok.") states = fh.get_states() if len(states) < 5: - print("Iconsistent number of states: {}".format(len(states))) + log.error("Iconsistent number of states: {}".format(len(states))) sys.exit(-7) else: - print("states received: {}, ok.".format(len(states))) + log.info("states received: {}, ok.".format(len(states))) fh.close() - print("") - print("") - print("---------------Queues--------------------------") - print("Testing python-fhem telnet FhemEventQueues():") + log.info("---------------Queues--------------------------") + log.info("Testing python-fhem telnet FhemEventQueues():") for connection in connections: if connection["protocol"] != "telnet": continue - print("Testing connection to {} via {}".format(config["testhost"], connection)) + log.info("Testing connection to {} via {}".format(config["testhost"], connection)) fh = fhem.Fhem(config["testhost"], **connections[0]) que = queue.Queue() @@ -326,7 +344,7 @@ def create_device(fhi, name, readings): time.sleep(1.0) for dev in devs: for i in range(10): - print("Repetion: {}".format(i + 1)) + log.debug("Repetion: {}".format(i + 1)) for rd in dev["readings"]: set_reading(fh, dev["name"], rd, 18.0 + i / 0.2) que_events += 1 @@ -344,19 +362,19 @@ def create_device(fhi, name, readings): que.task_done() ql += 1 - print("Queue length: {}".format(ql)) + log.debug("Queue length: {}".format(ql)) if ql != que_events: - print( + log.error( "FhemEventQueue contains {} entries, expected {} entries, failure.".format( ql, que_events ) ) sys.exit(-8) else: - print("Queue test success, Ok.") + log.info("Queue test success, Ok.") fh.close() fq.close() time.sleep(0.5) - print("") + log.info("All tests successfull.") sys.exit(0) From 0def70883486d7dd98ddac1dbaa96cc25780e6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 18:32:39 +0200 Subject: [PATCH 11/36] All tests ok --- fhem/fhem/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 9479d9a..bf6cd30 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -69,6 +69,7 @@ def __init__( self.bsock = None self.sock = None self.https_handler = None + self.opener = None # Set LogLevel # self.set_loglevel(loglevel) @@ -269,6 +270,9 @@ def send(self, buf, timeout=10): return None else: # HTTP(S) paramdata = None + if self.opener is not None: + install_opener(self.opener) + if self.csrf and len(buf) > 0: if len(self.csrftoken) == 0: self.log.error("CSRF token not available!") @@ -294,7 +298,7 @@ def send(self, buf, timeout=10): if self.context is None: ans = urlopen(ccmd, paramdata, timeout=timeout) else: - ans = urlopen(ccmd, paramdata, timeout=timeout, context=self.context) + ans = urlopen(ccmd, paramdata, timeout=timeout) # , context=self.context) else: self.log.error( "Invalid URL {}, Failed to send msg, len={}, {}".format( From 219db37d2c4b41ecbbfb492beb8194431559f666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 18:35:52 +0200 Subject: [PATCH 12/36] More time to start fhem --- selftest/selftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index aa85f89..aac432c 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -214,7 +214,7 @@ def create_device(fhi, name, readings): sys.exit(-2) os.system(config["exec"]) - time.sleep(1) + time.sleep(2) if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: log.error("Fhem is NOT running after install and start!") From b24e28ec2c644018b7537c880153dc4abe1d17cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 18:42:21 +0200 Subject: [PATCH 13/36] Test fhem start --- .github/workflows/python-fhem-test.yaml | 1 + selftest/selftest.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index d14316d..cc8dc13 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -39,6 +39,7 @@ jobs: - name: Install python-fhem run: | python -m pip install ./fhem/dist/*.gz + rm ./fhem/dist/* - name: Test with selftest.py run: | cd selftest diff --git a/selftest/selftest.py b/selftest/selftest.py index aac432c..a79d0f9 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -213,7 +213,8 @@ def create_device(fhi, name, readings): log.error("Failed to create certificate files!") sys.exit(-2) - os.system(config["exec"]) + ret = os.system(config["exec"]) + log.info("Fhem startup at {} returned: {}".format(config['exec'], ret)) time.sleep(2) if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: From 9d1d1e89323bb390ccd884e2fcdebbacb3877870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 18:52:16 +0200 Subject: [PATCH 14/36] Test fhem start, slower --- selftest/selftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index a79d0f9..5921a05 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -215,7 +215,7 @@ def create_device(fhi, name, readings): ret = os.system(config["exec"]) log.info("Fhem startup at {} returned: {}".format(config['exec'], ret)) - time.sleep(2) + time.sleep(5) if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: log.error("Fhem is NOT running after install and start!") From 2b077c73d904071c6ac82791344f8d9e022c984f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 18:56:29 +0200 Subject: [PATCH 15/36] Test fhem start, opo --- selftest/selftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index 5921a05..be49b4e 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -219,12 +219,12 @@ def create_device(fhi, name, readings): if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: log.error("Fhem is NOT running after install and start!") - sys.exit(-4) + # sys.exit(-4) log.info("Install should be ok, Fhem running.") connections = [ - {"protocol": "http", "port": 8083}, + # {"protocol": "http", "port": 8083}, { "protocol": "telnet", "port": 7073, From 941d25c901863195091bbd175328c9cbe24b4b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 18:59:51 +0200 Subject: [PATCH 16/36] github actions has trouble starting fhem. --- selftest/selftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index be49b4e..8a40f40 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -215,16 +215,16 @@ def create_device(fhi, name, readings): ret = os.system(config["exec"]) log.info("Fhem startup at {} returned: {}".format(config['exec'], ret)) - time.sleep(5) + time.sleep(1) if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: log.error("Fhem is NOT running after install and start!") - # sys.exit(-4) + sys.exit(-4) log.info("Install should be ok, Fhem running.") connections = [ - # {"protocol": "http", "port": 8083}, + {"protocol": "http", "port": 8083}, { "protocol": "telnet", "port": 7073, From 7de2136fa3a6cb4e92fb23a3ea055efd903458be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 19:05:26 +0200 Subject: [PATCH 17/36] bg --- selftest/selftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index 8a40f40..472ffd6 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -163,7 +163,7 @@ def create_device(fhi, name, readings): "fhem_file": "./fhem/fhem-6.0/fhem.pl", "config_file": "./fhem/fhem-6.0/fhem.cfg", "fhem_dir": "./fhem/fhem-6.0/", - "exec": "cd fhem/fhem-6.0/ && perl fhem.pl fhem.cfg", + "exec": "cd fhem/fhem-6.0/ && perl fhem.pl fhem.cfg &", "testhost": "localhost", } From f93c264617295974b5e47082d3dd562f4fae802c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 19:17:12 +0200 Subject: [PATCH 18/36] Use subprocess --- selftest/selftest.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index 472ffd6..67a1d38 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -4,6 +4,7 @@ import logging import time import queue +import subprocess from urllib.parse import quote from urllib.parse import urlencode @@ -163,7 +164,8 @@ def create_device(fhi, name, readings): "fhem_file": "./fhem/fhem-6.0/fhem.pl", "config_file": "./fhem/fhem-6.0/fhem.cfg", "fhem_dir": "./fhem/fhem-6.0/", - "exec": "cd fhem/fhem-6.0/ && perl fhem.pl fhem.cfg &", + "exec": "cd fhem/fhem-6.0/ && perl fhem.pl fhem.cfg", + "cmds": ["perl", "fhem.pl", "fhem.cfg"], "testhost": "localhost", } @@ -199,6 +201,7 @@ def create_device(fhi, name, readings): os.system("cat fhem-config-addon.cfg >> {}".format(config["config_file"])) + certs_dir = os.path.join(config["fhem_dir"], "certs") os.system("mkdir {}".format(certs_dir)) os.system( @@ -213,8 +216,9 @@ def create_device(fhi, name, readings): log.error("Failed to create certificate files!") sys.exit(-2) - ret = os.system(config["exec"]) - log.info("Fhem startup at {} returned: {}".format(config['exec'], ret)) + # os.system(config["exec"]) + subprocess.Popen(config["cmds"], cwd=config['fhem_dir'], close_fds=True) + log.info("Fhem startup at {}".format(config['exec'])) time.sleep(1) if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: From b2bceaf9ef311620a439eb0d317d19e6889d1d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 19:24:48 +0200 Subject: [PATCH 19/36] trials --- selftest/selftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selftest/selftest.py b/selftest/selftest.py index 67a1d38..41027eb 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -201,6 +201,10 @@ def create_device(fhi, name, readings): os.system("cat fhem-config-addon.cfg >> {}".format(config["config_file"])) + if not os.path.exists(config["config_file"]): + log.error("Failed to create config file!") + sys.exit(-2) + certs_dir = os.path.join(config["fhem_dir"], "certs") os.system("mkdir {}".format(certs_dir)) From 88f990dcbf34e7c305ea46517f2a0a2b35c5c2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 19:29:01 +0200 Subject: [PATCH 20/36] slow? --- selftest/selftest.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index 41027eb..fdaafc7 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -205,7 +205,6 @@ def create_device(fhi, name, readings): log.error("Failed to create config file!") sys.exit(-2) - certs_dir = os.path.join(config["fhem_dir"], "certs") os.system("mkdir {}".format(certs_dir)) os.system( @@ -223,11 +222,18 @@ def create_device(fhi, name, readings): # os.system(config["exec"]) subprocess.Popen(config["cmds"], cwd=config['fhem_dir'], close_fds=True) log.info("Fhem startup at {}".format(config['exec'])) - time.sleep(1) - if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: - log.error("Fhem is NOT running after install and start!") - sys.exit(-4) + retry_cnt = 10 + for i in range(retry_cnt): + time.sleep(1) + + if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: + log.warning("Fhem is NOT (yet) running after install and start!") + if i == retry_cnt - 1: + log.error("Giving up.") + sys.exit(-4) + else: + break log.info("Install should be ok, Fhem running.") From ddb2dde056d3bf32357f5c8f36acd66cde64b872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 19:33:46 +0200 Subject: [PATCH 21/36] ret --- selftest/selftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index fdaafc7..c0d29cd 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -220,8 +220,8 @@ def create_device(fhi, name, readings): sys.exit(-2) # os.system(config["exec"]) - subprocess.Popen(config["cmds"], cwd=config['fhem_dir'], close_fds=True) - log.info("Fhem startup at {}".format(config['exec'])) + ret = subprocess.Popen(config["cmds"], cwd=config['fhem_dir'], close_fds=True) + log.info("Fhem startup at {}: {}".format(config['exec'], ret)) retry_cnt = 10 for i in range(retry_cnt): From 7d2d37fc09564da8fe5dc47b105a3856694ce94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 19:38:51 +0200 Subject: [PATCH 22/36] Checkoutput --- selftest/selftest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index c0d29cd..97e490c 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -220,8 +220,12 @@ def create_device(fhi, name, readings): sys.exit(-2) # os.system(config["exec"]) - ret = subprocess.Popen(config["cmds"], cwd=config['fhem_dir'], close_fds=True) - log.info("Fhem startup at {}: {}".format(config['exec'], ret)) + process = subprocess.Popen(config["cmds"], cwd=config['fhem_dir'],stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + output, error = process.communicate() + if process.returncode != 0: + raise Exception("Process fhem failed %d %s %s" % (process.returncode, output, error)) + log.info("Fhem startup at {}: {}".format(config['exec'], output.decode('utf-8'))) retry_cnt = 10 for i in range(retry_cnt): From 7d7e8e2a3914fbfdba15c04dad60db512312fe5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 19:59:52 +0200 Subject: [PATCH 23/36] more sub --- selftest/selftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selftest/selftest.py b/selftest/selftest.py index 97e490c..34f1e6b 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -221,11 +221,11 @@ def create_device(fhi, name, readings): # os.system(config["exec"]) process = subprocess.Popen(config["cmds"], cwd=config['fhem_dir'],stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.PIPE, start_new_session=True) output, error = process.communicate() if process.returncode != 0: raise Exception("Process fhem failed %d %s %s" % (process.returncode, output, error)) - log.info("Fhem startup at {}: {}".format(config['exec'], output.decode('utf-8'))) + log.info("Fhem startup at {}: {}".format(config['cmds'], output.decode('utf-8'))) retry_cnt = 10 for i in range(retry_cnt): From 46c63b25344c8bfee3b14b25d7bd5318492de434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 20:37:36 +0200 Subject: [PATCH 24/36] Manual fhem.pl start and reuse by tester --- .github/workflows/python-fhem-test.yaml | 15 ++- selftest/selftest.py | 135 +++++++++++++----------- 2 files changed, 90 insertions(+), 60 deletions(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index cc8dc13..b6f34d1 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -40,7 +40,20 @@ jobs: run: | python -m pip install ./fhem/dist/*.gz rm ./fhem/dist/* + - name: Install fhem server + run: | + wget https://fhem.de/fhem-6.0.tar.gz + mkdir fhem + cd fhem + tar xvf - ../fhem-6.0.tar.gz + cd fhem-6.0 + mkdir certs + cd certs + openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 36500 -out server-cert.pem -subj "/C=DE/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=www.example.com/emailAddress=email@example.com + cd .. + cp ../../selftest/fhem.cfg . + perl fhem.pl fhem.cfg - name: Test with selftest.py run: | cd selftest - python selftest.py + python selftest.py --reuse diff --git a/selftest/selftest.py b/selftest/selftest.py index 34f1e6b..505f5a9 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -5,6 +5,7 @@ import time import queue import subprocess +import argparse from urllib.parse import quote from urllib.parse import urlencode @@ -148,6 +149,17 @@ def create_device(fhi, name, readings): if __name__ == "__main__": + # check args for reuse (-r) + parser = argparse.ArgumentParser(description="Fhem self-tester") + parser.add_argument( + "-r", + "--reuse", + action="store_true", + help="Reuse existing FHEM installation", + ) + args = parser.parse_args() + reuse = args.reuse + logging.basicConfig( level=logging.DEBUG, format="%(asctime)s.%(msecs)03d %(name)s %(levelname)s %(message)s", @@ -169,77 +181,82 @@ def create_device(fhi, name, readings): "testhost": "localhost", } + installed = False if ( st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is not None ): log.info("Fhem is already running!") - st.shutdown(fhem_url=config["testhost"], protocol="http", port=8083) - time.sleep(1) - if ( - st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) - is not None + if reuse is True: + installed = True + else: + st.shutdown(fhem_url=config["testhost"], protocol="http", port=8083) + time.sleep(1) + if ( + st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) + is not None + ): + log.error("Shutdown failed!") + sys.exit(-3) + log.info("--------------------") + log.info("Reinstalling FHEM...") + + if installed is False: + if not st.download(config["archivename"], config["urlpath"]): + log.error("Download failed.") + sys.exit(-1) + + log.info("Starting fhem installation") + + # WARNING! THIS DELETES ANY EXISTING FHEM SERVER at 'destination'! + # All configuration files, databases, logs etc. are DELETED to allow a fresh test install! + if not st.install( + config["archivename"], config["destination"], config["fhem_file"] ): - log.error("Shutdown failed!") - sys.exit(-3) - log.info("--------------------") - log.info("Reinstalling FHEM...") + log.info("Install failed") + sys.exit(-2) - if not st.download(config["archivename"], config["urlpath"]): - log.error("Download failed.") - sys.exit(-1) - - log.info("Starting fhem installation") - - # WARNING! THIS DELETES ANY EXISTING FHEM SERVER at 'destination'! - # All configuration files, databases, logs etc. are DELETED to allow a fresh test install! - if not st.install( - config["archivename"], config["destination"], config["fhem_file"] - ): - log.info("Install failed") - sys.exit(-2) + os.system("cat fhem-config-addon.cfg >> {}".format(config["config_file"])) - os.system("cat fhem-config-addon.cfg >> {}".format(config["config_file"])) + if not os.path.exists(config["config_file"]): + log.error("Failed to create config file!") + sys.exit(-2) - if not os.path.exists(config["config_file"]): - log.error("Failed to create config file!") - sys.exit(-2) - - certs_dir = os.path.join(config["fhem_dir"], "certs") - os.system("mkdir {}".format(certs_dir)) - os.system( - 'cd {} && openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 36500 -out server-cert.pem -subj "/C=DE/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=www.example.com/emailAddress=email@example.com"'.format( - certs_dir + certs_dir = os.path.join(config["fhem_dir"], "certs") + os.system("mkdir {}".format(certs_dir)) + os.system( + 'cd {} && openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 36500 -out server-cert.pem -subj "/C=DE/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=www.example.com/emailAddress=email@example.com"'.format( + certs_dir + ) ) - ) - cert_file = os.path.join(certs_dir, "server-cert.pem") - key_file = os.path.join(certs_dir, "server-key.pem") - if not os.path.exists(cert_file) or not os.path.exists(key_file): - log.error("Failed to create certificate files!") - sys.exit(-2) - - # os.system(config["exec"]) - process = subprocess.Popen(config["cmds"], cwd=config['fhem_dir'],stdout=subprocess.PIPE, - stderr=subprocess.PIPE, start_new_session=True) - output, error = process.communicate() - if process.returncode != 0: - raise Exception("Process fhem failed %d %s %s" % (process.returncode, output, error)) - log.info("Fhem startup at {}: {}".format(config['cmds'], output.decode('utf-8'))) - - retry_cnt = 10 - for i in range(retry_cnt): - time.sleep(1) - - if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: - log.warning("Fhem is NOT (yet) running after install and start!") - if i == retry_cnt - 1: - log.error("Giving up.") - sys.exit(-4) - else: - break + cert_file = os.path.join(certs_dir, "server-cert.pem") + key_file = os.path.join(certs_dir, "server-key.pem") + if not os.path.exists(cert_file) or not os.path.exists(key_file): + log.error("Failed to create certificate files!") + sys.exit(-2) + + # os.system(config["exec"]) + process = subprocess.Popen(config["cmds"], cwd=config['fhem_dir'],stdout=subprocess.PIPE, + stderr=subprocess.PIPE, start_new_session=True) + output, error = process.communicate() + if process.returncode != 0: + raise Exception("Process fhem failed %d %s %s" % (process.returncode, output, error)) + log.info("Fhem startup at {}: {}".format(config['cmds'], output.decode('utf-8'))) + + retry_cnt = 2 + for i in range(retry_cnt): + time.sleep(1) + + if st.is_running(fhem_url=config["testhost"], protocol="http", port=8083) is None: + log.warning("Fhem is NOT (yet) running after install and start!") + if i == retry_cnt - 1: + log.error("Giving up.") + sys.exit(-4) + else: + break - log.info("Install should be ok, Fhem running.") + log.info("Install should be ok, Fhem running.") connections = [ {"protocol": "http", "port": 8083}, From 6851d834757ca7250cae6d2a6e332729cea7755e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 20:41:16 +0200 Subject: [PATCH 25/36] paths fix --- .github/workflows/python-fhem-test.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index b6f34d1..c2e5c9d 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.10", "3.11"] + python-version: ["3.11"] steps: - uses: actions/checkout@v3 @@ -42,6 +42,7 @@ jobs: rm ./fhem/dist/* - name: Install fhem server run: | + cd selftest wget https://fhem.de/fhem-6.0.tar.gz mkdir fhem cd fhem @@ -51,7 +52,7 @@ jobs: cd certs openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 36500 -out server-cert.pem -subj "/C=DE/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=www.example.com/emailAddress=email@example.com cd .. - cp ../../selftest/fhem.cfg . + cp ../../fhem-config-addon.cfg fhem.cfg perl fhem.pl fhem.cfg - name: Test with selftest.py run: | From 058376886910e77d7239755199c43a8a59424462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 20:44:47 +0200 Subject: [PATCH 26/36] untar --- .github/workflows/python-fhem-test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index c2e5c9d..9b8b731 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -43,10 +43,10 @@ jobs: - name: Install fhem server run: | cd selftest - wget https://fhem.de/fhem-6.0.tar.gz + wget -nv https://fhem.de/fhem-6.0.tar.gz mkdir fhem cd fhem - tar xvf - ../fhem-6.0.tar.gz + tar -xvzf ../fhem-6.0.tar.gz cd fhem-6.0 mkdir certs cd certs From 13ed563d90d7c8c439844f9de6212f4db566a3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 20:47:25 +0200 Subject: [PATCH 27/36] fix cert printer --- .github/workflows/python-fhem-test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index 9b8b731..72ec570 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -46,11 +46,11 @@ jobs: wget -nv https://fhem.de/fhem-6.0.tar.gz mkdir fhem cd fhem - tar -xvzf ../fhem-6.0.tar.gz + tar -xzf ../fhem-6.0.tar.gz cd fhem-6.0 mkdir certs cd certs - openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 36500 -out server-cert.pem -subj "/C=DE/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=www.example.com/emailAddress=email@example.com + openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 36500 -out server-cert.pem -subj "/C=DE/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=www.example.com/emailAddress=email@example.com" cd .. cp ../../fhem-config-addon.cfg fhem.cfg perl fhem.pl fhem.cfg From 56814e60554b466b027b470b851b6819abe963aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 20:53:53 +0200 Subject: [PATCH 28/36] perl module path mess --- selftest/fhem-config-addon.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selftest/fhem-config-addon.cfg b/selftest/fhem-config-addon.cfg index f1f4df0..e915961 100644 --- a/selftest/fhem-config-addon.cfg +++ b/selftest/fhem-config-addon.cfg @@ -4,6 +4,8 @@ ### telnet: 7072, 7073 (secured) ### https: 8084, 8085 (with password. user: test, pwd: secretsauce) +attr global modpath . + define telnetPort telnet 7072 global define telnetPort2 telnet 7073 global From 01f333d68fde5c93f3e3d03fbeda000b4bd799ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 20:58:18 +0200 Subject: [PATCH 29/36] port conflict --- selftest/fhem-config-addon.cfg | 2 +- selftest/selftest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selftest/fhem-config-addon.cfg b/selftest/fhem-config-addon.cfg index e915961..0ebc789 100644 --- a/selftest/fhem-config-addon.cfg +++ b/selftest/fhem-config-addon.cfg @@ -18,7 +18,7 @@ attr allowTelPort2 validFor telnetPort2 # HTTPS requires IO::Socket::SSL, to be installed with cpan -i IO::Socket::SSL # or apt-get install libio-socket-ssl-perl # or pacman -S perl-io-socket-ssl -define WEBS FHEMWEB 8084 global +define WEBS FHEMWEB 8086 global attr WEBS HTTPS 1 attr WEBS sslVersion TLSv12:!SSLv3 attr WEBS longpoll 1 diff --git a/selftest/selftest.py b/selftest/selftest.py index 505f5a9..e9358ec 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -267,7 +267,7 @@ def create_device(fhi, name, readings): "password": "secretsauce", }, {"protocol": "telnet", "port": 7072}, - {"protocol": "https", "port": 8084}, + {"protocol": "https", "port": 8086}, { "protocol": "https", "port": 8085, From 90f4bba6785e07eae787b29e0ca8988c674098e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 21:01:09 +0200 Subject: [PATCH 30/36] on --- .github/workflows/python-fhem-test.yaml | 31 +++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index 72ec570..b2c4982 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -40,21 +40,22 @@ jobs: run: | python -m pip install ./fhem/dist/*.gz rm ./fhem/dist/* - - name: Install fhem server - run: | - cd selftest - wget -nv https://fhem.de/fhem-6.0.tar.gz - mkdir fhem - cd fhem - tar -xzf ../fhem-6.0.tar.gz - cd fhem-6.0 - mkdir certs - cd certs - openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 36500 -out server-cert.pem -subj "/C=DE/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=www.example.com/emailAddress=email@example.com" - cd .. - cp ../../fhem-config-addon.cfg fhem.cfg - perl fhem.pl fhem.cfg +# - name: Install fhem server +# run: | +# cd selftest +# wget -nv https://fhem.de/fhem-6.0.tar.gz +# mkdir fhem +# cd fhem +# tar -xzf ../fhem-6.0.tar.gz +# cd fhem-6.0 +# mkdir certs +# cd certs +# openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 36500 -out server-cert.pem -subj "/C=DE/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=www.example.com/emailAddress=email@example.com" +# cd .. +# cp ../../fhem-config-addon.cfg fhem.cfg +# perl fhem.pl fhem.cfg - name: Test with selftest.py run: | cd selftest - python selftest.py --reuse + python selftest.py +# python selftest.py --reuse From 87c8402f10c23acc8e89b6b9e802ee5ee3c78685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 21:05:05 +0200 Subject: [PATCH 31/36] Badges! --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index efc1df9..87114be 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![PyPI version](https://badge.fury.io/py/fhem.svg)](https://badge.fury.io/py/fhem) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/116e9e988d934aaa9cfbfa5b8aef7f78)](https://www.codacy.com/app/dominik.schloesser/python-fhem?utm_source=github.com&utm_medium=referral&utm_content=domschl/python-fhem&utm_campaign=Badge_Grade) +[![Python package](https://github.com/domschl/python-fhem/actions/workflows/python-fhem-test.yaml/badge.svg)](https://github.com/domschl/python-fhem/actions/workflows/python-fhem-test.yaml) [![License](http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE) [![Docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://domschl.github.io/python-fhem/index.html) From 36c43173467bbbec84abac5b4a969bd7af056fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Thu, 17 Aug 2023 21:07:02 +0200 Subject: [PATCH 32/36] Test all python versions 3.8, 3.10, 3.11 --- .github/workflows/python-fhem-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-fhem-test.yaml b/.github/workflows/python-fhem-test.yaml index b2c4982..a97820c 100644 --- a/.github/workflows/python-fhem-test.yaml +++ b/.github/workflows/python-fhem-test.yaml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11"] + python-version: ["3.8", "3.10", "3.11"] steps: - uses: actions/checkout@v3 From 0df812087524c35fae3bc1e343fff4af589b5989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Fri, 18 Aug 2023 10:17:23 +0200 Subject: [PATCH 33/36] Remove all global states from authenticator and contex sessions --- fhem/fhem/__init__.py | 31 ++++++++++++++++++------------- publish.sh | 3 +++ test_mod.sh | 8 ++++++++ 3 files changed, 29 insertions(+), 13 deletions(-) create mode 100755 test_mod.sh diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index bf6cd30..d3d7232 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -106,9 +106,7 @@ def connect(self): context.verify_mode = ssl.CERT_NONE self.sock = context.wrap_socket(self.bsock) self.log.info( - "Connecting to {}:{} with SSL (TLS)".format( - self.server, self.port - ) + "Connecting to {}:{} with SSL (TLS)".format(self.server, self.port) ) else: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -124,7 +122,7 @@ def connect(self): self.log.debug("pre-connect (no try/except)") # try: - # self.sock.timeout = 5.0 + # self.sock.timeout = 5.0 self.sock.connect((self.server, self.port)) self.log.debug("post-connect") # except Exception as e: @@ -212,6 +210,9 @@ def close(self): def _install_opener(self): self.opener = None + self.auth_handler = None + self.password_mgr = None + self.context = None if self.username != "": self.password_mgr = HTTPPasswordMgrWithDefaultRealm() self.password_mgr.add_password( @@ -233,12 +234,11 @@ def _install_opener(self): else: self.opener = build_opener(self.https_handler) else: - self.context = None if self.username != "": self.opener = build_opener(self.auth_handler) - if self.opener is not None: - self.log.debug("Setting up opener on: {}".format(self.baseurlauth)) - install_opener(self.opener) + # if self.opener is not None: + # self.log.debug("Setting up opener on: {}".format(self.baseurlauth)) + # install_opener(self.opener) def send(self, buf, timeout=10): """Sends a buffer to server @@ -270,8 +270,8 @@ def send(self, buf, timeout=10): return None else: # HTTP(S) paramdata = None - if self.opener is not None: - install_opener(self.opener) + # if self.opener is not None: + # install_opener(self.opener) if self.csrf and len(buf) > 0: if len(self.csrftoken) == 0: @@ -295,10 +295,15 @@ def send(self, buf, timeout=10): self.log.info("Request: {}".format(ccmd)) if ccmd.lower().startswith("http"): - if self.context is None: - ans = urlopen(ccmd, paramdata, timeout=timeout) + if self.opener is not None: + ans = self.opener.open(ccmd, paramdata, timeout=timeout) else: - ans = urlopen(ccmd, paramdata, timeout=timeout) # , context=self.context) + if self.context is None: + ans = urlopen(ccmd, paramdata, timeout=timeout) + else: + ans = urlopen( + ccmd, paramdata, timeout=timeout, context=self.context + ) else: self.log.error( "Invalid URL {}, Failed to send msg, len={}, {}".format( diff --git a/publish.sh b/publish.sh index 148b906..1cb8188 100755 --- a/publish.sh +++ b/publish.sh @@ -4,6 +4,9 @@ if [ ! -f ~/.pypirc ]; then echo "Please configure .pypirc for pypi access first" exit -2 fi +if [[ ! -d fhem/dist ]]; then + mkdir fhem/dist +fi cd fhem cp ../README.md . rm dist/* diff --git a/test_mod.sh b/test_mod.sh new file mode 100755 index 0000000..4a32622 --- /dev/null +++ b/test_mod.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +pip uninstall fhem +./publish.sh +pip install fhem/dist/fhem-0.7.0.tar.gz +cd selftest +python selftest.py + From ac159a8d0408bf3157d33edec339afa92c072704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Fri, 18 Aug 2023 10:41:07 +0200 Subject: [PATCH 34/36] Arch Linux special --- test_mod.sh | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/test_mod.sh b/test_mod.sh index 4a32622..a6b2bc0 100755 --- a/test_mod.sh +++ b/test_mod.sh @@ -1,8 +1,36 @@ #!/bin/bash +if [ -f /etc/os-release ]; then + # freedesktop.org and systemd + . /etc/os-release 2>/dev/null + OS=$NAME + VER=$VERSION_ID + if uname -a | grep -Fq "WSL" 2> /dev/null; then + SUB_SYSTEM="WSL" + fi +elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + OS=$(lsb_release -si) + VER=$(lsb_release -sr) +elif [ -f /etc/lsb-release ]; then + # For some versions of Debian/Ubuntu without lsb_release command + . /etc/lsb-release + OS=$DISTRIB_ID + VER=$DISTRIB_RELEASE +fi -pip uninstall fhem +if [[ "$OS" == "Arch Linux" ]]; then + echo "Arch Linux" + pip uninstall fhem --break-system-packages +else + echo "OS: $OS" + pip uninstall fhem +fi ./publish.sh -pip install fhem/dist/fhem-0.7.0.tar.gz +if [[ "$OS" == "Arch Linux" ]]; then + pip install fhem/dist/fhem-0.7.0.tar.gz --break-system-packages +else + pip install fhem/dist/fhem-0.7.0.tar.gz +fi cd selftest python selftest.py From f7b523450598191a427b8123870a30a2ec638489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Fri, 18 Aug 2023 10:54:15 +0200 Subject: [PATCH 35/36] Documentation updates for 0.7.0 --- README.md | 35 ++++++++++++++--------------------- selftest/README.md | 16 ++++++++++++---- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 87114be..86c21b8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Python FHEM (home automation server) API Simple API to connect to the [FHEM home automation server](https://fhem.de/) via sockets or http(s), using the telnet or web port on FHEM with optional SSL (TLS) and password or basicAuth support. -**Note:** Python 2.x deprecation warning. `python-fhem` versions 0.6.x will be the last versions supporting Python 2.x. +**Note:** Starting with verson 0.7.0, Python 2.x is no longer supported with `python-fhem`. If you still require support for Python 2, use versions 0.6.5. ## Installation @@ -23,29 +23,22 @@ pip install [-U] fhem ### From source -In `python-fhem/fhem`: - -Get a copy of README for the install (required by setup.py): - -```bash -cp ../README.md . -``` - -then: +To build your own package, install `python-build` and run: ```bash -pip install [-U] . +cd fhem +python -m build ``` -or, as developer installation, allowing inplace editing: +This will create a `dist` directory with the package. Install with: ```bash -pip install [-U] -e . +pip install [-U] dist/fhem-.tar.gz ``` - + ## History -* 0.7.0 (2023-08-17): [unpublished] Ongoing: move Travis CI -> Github actions, Python 2.x support removed, modernize python packaging. +* 0.7.0 (2023-08-17): [unpublished] Ongoing: move Travis CI -> Github actions, Python 2.x support removed, modernize python packaging, global states for SSL and authentication removed (support for multiple sessions). * 0.6.6 (2022-11-09): [unpublished] Fix for new option that produces fractional seconds in event data. * 0.6.5 (2020-03-24): New option `raw_value` for `FhemEventQueue`. Default `False` (old behavior), on `True`, the full, unparsed reading is returned, without looking for a unit. * 0.6.4 (2020-03-24): Bug fix for [#21](https://github.com/domschl/python-fhem/issues/21), Index out-of-range in event loop background thread for non-standard event formats. @@ -142,12 +135,7 @@ The library can create an event queue that uses a background thread to receive and dispatch FHEM events: ```python -try: - # Python 3.x - import queue -except: - # Python 2.x - import Queue as queue +import queue import fhem que = queue.Queue() @@ -160,6 +148,11 @@ while True: que.task_done() ``` +## Selftest + +For a more complete example, you can look at [`selftest/selftest.py`](https://github.com/domschl/python-fhem/tree/master/selftest). This automatically installs an FHEM server, and runs a number of tests, +creating devices and checking their state using the various different transports. + # Documentation see: [python-fhem documentation](https://domschl.github.io/python-fhem/index.html) diff --git a/selftest/README.md b/selftest/README.md index c8a4067..667ab1a 100644 --- a/selftest/README.md +++ b/selftest/README.md @@ -6,10 +6,10 @@ The scripts automatically download the latest FHEM release, install, configure a perform self-tests. Tests performed: -* All tests are run with both python 2.7 and python 3.x * FHEM connections via sockets, secure sockets, HTTP and HTTPS with password. * Automatic creation of devices on Fhem (using all connection variants) * Aquiring readings from Fhem using all different connection types and python versions +* Automatic testing of the FhemEventQueue **WARNING**: Be careful when using this script, e.g. the install-class ***completely erases*** the existing FHEM installation within the selftest tree (and all configuration files) to allow clean tests. @@ -24,14 +24,22 @@ It needs to be installed with either: * or `apt-get install libio-socket-ssl-perl` * or `pacman -S perl-io-socket-ssl` -If selftests fails on the first SSL connection, it is usually a sign, that the fhem-perl requirements for SSL are not installed. +If selftests fails on the first SSL connection, it is usually a sign that the fhem-perl requirements for SSL are not installed. ## Manual test run -- Install `python-fhem`, e.g. by `pip install -e .` in the fhem source directory. -- Make sure that Perl's `socke::ssl` is installed (s.a.) +- Make sure `python-fhem` is installed (e.g. `pip install fhem`) +- Make sure that Perl's `socket::ssl` is installed (s.a.) - Run `python selftest.py` +You can run the selftest with option `--reuse` to reuse an existing and running FHEM installation. The selftest requires a number of +ports and passwords to be configured. Check out `fhem-config-addon.cfg` for details. + +## CI notes + +The selftest can be used for CI testing. It is currently used with github actions. Be aware that port `8084` is not available on github actions. +See `.github/workflows/python-fhem-test.yaml` for details. + ## History - 2023-08-17: Updated for FHEM 6.0, python 2.x support removed. Prepared move from Travis CI to github actions. From 90937b4b8e45d969bdbe4610d866d59df5d73d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schl=C3=B6sser?= Date: Fri, 18 Aug 2023 11:47:46 +0200 Subject: [PATCH 36/36] Test multiple concurrent connections with different authentications --- selftest/fhem-config-addon.cfg | 11 +++++ selftest/selftest.py | 90 ++++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/selftest/fhem-config-addon.cfg b/selftest/fhem-config-addon.cfg index 0ebc789..e4ab70d 100644 --- a/selftest/fhem-config-addon.cfg +++ b/selftest/fhem-config-addon.cfg @@ -31,4 +31,15 @@ define allowWebPwd allowed # test:secretsauce NOTE: do not reuse those values for actual installations! attr allowWebPwd basicAuth dGVzdDpzZWNyZXRzYXVjZQ== attr allowWebPwd validFor WebPwd + +define MultiWebPwd FHEMWEB 8087 global +attr MultiWebPwd HTTPS 1 +attr MultiWebPwd sslVersion TLSv12:!SSLv3 +attr MultiWebPwd longpoll 1 +define allowMultiWebPwd allowed +# echo -n "toast:salad" | base64 +# dG9hc3Q6c2FsYWQ= +attr allowMultiWebPwd basicAuth dG9hc3Q6c2FsYWQ= +attr allowMultiWebPwd validFor WebPwd + ################################################################################# diff --git a/selftest/selftest.py b/selftest/selftest.py index e9358ec..470a0b3 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -274,9 +274,25 @@ def create_device(fhi, name, readings): "username": "test", "password": "secretsauce", }, + { + "protocol": "https", + "port": 8087, + "username": "toast", + "password": "salad", + }, ] first = True + devs = [ + { + "name": "clima_sensor1", + "readings": {"temperature": 18.2, "humidity": 88.2}, + }, + { + "name": "clima_sensor2", + "readings": {"temperature": 19.1, "humidity": 85.7}, + }, + ] log.info("") log.info("----------------- Fhem ------------") log.info("Testing python-fhem Fhem():") @@ -284,16 +300,6 @@ def create_device(fhi, name, readings): log.info("Testing connection to {} via {}".format(config["testhost"], connection)) fh = fhem.Fhem(config["testhost"], **connection) - devs = [ - { - "name": "clima_sensor1", - "readings": {"temperature": 18.2, "humidity": 88.2}, - }, - { - "name": "clima_sensor2", - "readings": {"temperature": 19.1, "humidity": 85.7}, - }, - ] if first is True: for dev in devs: @@ -355,6 +361,70 @@ def create_device(fhi, name, readings): log.info("states received: {}, ok.".format(len(states))) fh.close() + log.info("---------------MultiConnect--------------------") + fhm = [] + for connection in connections[-2:]: + log.info("Testing multi-connection to {} via {}".format(config["testhost"], connection)) + fhm.append(fhem.Fhem(config["testhost"], **connection)) + + for dev in devs: + for i in range(10): + for fh in fhm: + log.debug("Repetion: {}, connection: {}".format(i + 1, fh.connection)) + if fh.connected() is False: + log.info("Connecting...") + fh.connect() + for rd in dev["readings"]: + dict_value = fh.get_device_reading(dev["name"], rd, blocking=False) + try: + value = dict_value["Value"] + except: + log.error( + "Bad reply reading {} {} -> {}".format( + dev["name"], rd, dict_value + ) + ) + sys.exit(-7) + + if value == dev["readings"][rd]: + log.debug( + "Reading-test {},{}={} ok.".format( + dev["name"], rd, dev["readings"][rd] + ) + ) + else: + log.error( + "Failed to set and read reading! {},{} {} != {}".format( + dev["name"], rd, value, dev["readings"][rd] + ) + ) + sys.exit(-5) + + num_temps = 0 + for dev in devs: + if "temperature" in dev["readings"]: + num_temps += 1 + for fh in fhm: + temps = fh.get_readings("temperature", timeout=0.1, blocking=False) + if len(temps) != num_temps: + log.error( + "There should have been {} devices with temperature reading, but we got {}. Ans: {}".format( + num_temps, len(temps), temps + ) + ) + sys.exit(-6) + else: + log.info("Multiread of all devices with 'temperature' reading: ok.") + + for fh in fhm: + states = fh.get_states() + if len(states) < 5: + log.error("Iconsistent number of states: {}".format(len(states))) + sys.exit(-7) + else: + log.info("states received: {}, ok.".format(len(states))) + fh.close() + log.info("---------------Queues--------------------------") log.info("Testing python-fhem telnet FhemEventQueues():") for connection in connections: