From da25708c59a041cd44cd65ecf87387f7675d87ca Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 29 Nov 2018 23:54:08 +0100 Subject: [PATCH 01/16] Add get function with filters --- fhem/fhem/__init__.py | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 0f46065..ca4c7a5 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -513,6 +513,52 @@ def get_fhem_state(self, timeout=0.1): return {} + def get(self, name=None, state=None, group=None, room=None, type=None, nname=None, nstate=None, ngroup=None, + nroom=None, ntype=None, case_sensitive=None, filter=None, timeout=0.1): + """ + Get FHEM state of devices, filter by parameters. + + :param name: regex for name + :param state: regex for state + :param group: regex for group + :param room: regex for room + :param type: regex for type + :param nname: not name + :param nstate: not state + :param ngroup: not group + :param nroom: not room + :param ntype: not type + :param case_sensitive: use case_sensitive instead of + :param filter: dict of filter - key=attribute/internal/reading, value=regex for value + :param timeout: timeout for reply + :return: dict of fhem devices + """ + text = [] + if not self.connected(): + self.connect() + if self.connected(): + cs = "=" if case_sensitive else "~" + if name or nname: + text.append("NAME{}{}{}".format("!" if nname else "", cs, name if name else nname)) + if state or nstate: + text.append("STATE{}{}{}".format("!" if nstate else "", cs, state if state else nstate)) + if group or ngroup: + text.append("group{}{}{}".format("!" if ngroup else "", cs, group if group else ngroup)) + if room or nroom: + text.append("room{}{}{}".format("!" if nroom else "", cs, room if room else nroom)) + if type or ntype: + text.append("TYPE{}{}{}".format("!" if ntype else "", cs, type if type else ntype)) + if filter: + for key, value in filter.items(): + text.append("{}{}{}".format(key, cs, value)) + result = self.send_recv_cmd("jsonlist2 {}".format(":FILTER=".join(text)), blocking=False, + timeout=timeout) + return result + else: + logger.error("Failed to get fhem state. Not connected.") + return {} + + class FhemEventQueue: '''Creates a thread that listens to FHEM events and dispatches them to a Python queue.''' From b035e32fa225789b7dc18d4a8fc8865971423025 Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 30 Nov 2018 00:12:03 +0100 Subject: [PATCH 02/16] Reformated code for PEP8 --- fhem/fhem/__init__.py | 72 ++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 45 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index ca4c7a5..638687c 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -6,6 +6,7 @@ import ssl import threading import logging + try: # Python 3.x from urllib.parse import quote @@ -29,17 +30,16 @@ from urllib2 import build_opener from urllib2 import install_opener - -__version__ = '0.5.4' # needs to be in sync with setup.py and documentation (conf.py, branch gh-pages) +__version__ = '0.5.4' # needs to be in sync with setup.py and documentation (conf.py, branch gh-pages) # create logger with 'python_fhem' logger = logging.getLogger(__name__) 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): @@ -95,7 +95,6 @@ def __init__(self, server, port=7072, self._install_opener() - def connect(self): '''create socket connection to server (telnet protocol only)''' if self.protocol == 'telnet': @@ -143,7 +142,7 @@ def connect(self): if self.csrf: dat = self.send("") if dat is not None: - dat=dat.decode("UTF-8") + dat = dat.decode("UTF-8") stp = dat.find("csrf_") if stp != -1: token = dat[stp:] @@ -157,12 +156,10 @@ def connect(self): else: self.connection = True - def connected(self): '''Returns True if socket/http(s) session is connected to server.''' return self.connection - def set_loglevel(self, level): '''Set logging level. @@ -177,7 +174,6 @@ def set_loglevel(self, level): elif level == 3: logger.setLevel(logging.DEBUG) - def close(self): '''Closes socket connection. (telnet only)''' if self.protocol == 'telnet': @@ -191,7 +187,6 @@ def close(self): else: self.connection = False - def _install_opener(self): self.opener = None if self.username != "": @@ -221,12 +216,11 @@ def _install_opener(self): logger.debug("Setting up opener on: {}".format(self.baseurlauth)) install_opener(self.opener) - def send(self, buf): '''Sends a buffer to server :param buf: binary buffer''' - if len(buf)>0: + if len(buf) > 0: if not self.connected(): logger.debug("Not connected, trying to connect...") self.connect() @@ -265,15 +259,14 @@ def send(self, buf): ccmd = self.baseurltoken logger.info("Request: {}".format(ccmd)) - ans = urlopen(ccmd, paramdata) # , data, 10) # XXX timeout + ans = urlopen(ccmd, paramdata) # , data, 10) # XXX timeout data = ans.read() return data except URLError as err: - self.connection=False + self.connection = False logger.error("Failed to send msg, len={}, {}".format(len(buf), err)) return None - def send_cmd(self, msg): '''Sends a command to server. @@ -294,7 +287,6 @@ def send_cmd(self, msg): else: return self.send(msg) - def _recv_nonblocking(self, timeout=0.1): if not self.connected(): self.connect() @@ -325,7 +317,6 @@ def _recv_nonblocking(self, timeout=0.1): self.sock.setblocking(True) return data - def send_recv_cmd(self, msg, timeout=0.1, blocking=True): ''' Sends a command to the server and waits for an immediate reply. @@ -375,7 +366,6 @@ def send_recv_cmd(self, msg, timeout=0.1, blocking=True): logger.info("JSON answer received.") return jdata - def get_dev_state(self, dev, timeout=0.1): ''' Get all FHEM device properties as JSON object @@ -392,7 +382,6 @@ def get_dev_state(self, dev, timeout=0.1): logger.error("Failed to get dev state for {}. Not connected.".format(dev)) return {} - def get_dev_reading(self, dev, reading, timeout=0.1): ''' Get a specific reading from a FHEM device @@ -413,12 +402,10 @@ def get_dev_reading(self, dev, reading, timeout=0.1): return read return read - def getDevReadings(self, dev, reading, timeout=0.1): logger.critical("Deprecation: use get_dev_readings instead of getDevReadings") self.get_dev_readings(dev, reading, timeout) - def get_dev_readings(self, dev, readings, timeout=0.1): ''' Get a list of readings for one FHEM device @@ -439,7 +426,6 @@ def get_dev_readings(self, dev, readings, timeout=0.1): logger.error("Reading not defined: {}, {}, {}".format(dev, reading, err)) return reads - def get_dev_reading_time(self, dev, reading, timeout=0.1): ''' Get the datetime of a specific reading from a FHEM device @@ -464,7 +450,6 @@ def get_dev_reading_time(self, dev, reading, timeout=0.1): return None return time - def get_dev_readings_time(self, dev, readings, timeout=0.1): ''' Get a list of datetimes of readings for one FHEM device @@ -490,12 +475,10 @@ def get_dev_readings_time(self, dev, readings, timeout=0.1): logger.error("Reading not defined: {} {} {}".format(dev, reading, err)) return reads - def getFhemState(self, timeout=0.1): logger.critical("Deprecation: use get_fhem_state instead of getFhemState") self.get_fhem_state(timeout) - def get_fhem_state(self, timeout=0.1): ''' Get FHEM state of all devices, returns a large JSON object with @@ -512,9 +495,8 @@ def get_fhem_state(self, timeout=0.1): logger.error("Failed to get fhem state. Not connected.") return {} - - def get(self, name=None, state=None, group=None, room=None, type=None, nname=None, nstate=None, ngroup=None, - nroom=None, ntype=None, case_sensitive=None, filter=None, timeout=0.1): + def get(self, name=None, state=None, group=None, room=None, device_type=None, nname=None, nstate=None, ngroup=None, + nroom=None, ndevice_type=None, case_sensitive=None, filters=None, timeout=0.1): """ Get FHEM state of devices, filter by parameters. @@ -522,14 +504,14 @@ def get(self, name=None, state=None, group=None, room=None, type=None, nname=Non :param state: regex for state :param group: regex for group :param room: regex for room - :param type: regex for type + :param device_type: regex for type :param nname: not name :param nstate: not state :param ngroup: not group :param nroom: not room - :param ntype: not type + :param ndevice_type: not type :param case_sensitive: use case_sensitive instead of - :param filter: dict of filter - key=attribute/internal/reading, value=regex for value + :param filters: dict of filter - key=attribute/internal/reading, value=regex for value :param timeout: timeout for reply :return: dict of fhem devices """ @@ -546,10 +528,11 @@ def get(self, name=None, state=None, group=None, room=None, type=None, nname=Non text.append("group{}{}{}".format("!" if ngroup else "", cs, group if group else ngroup)) if room or nroom: text.append("room{}{}{}".format("!" if nroom else "", cs, room if room else nroom)) - if type or ntype: - text.append("TYPE{}{}{}".format("!" if ntype else "", cs, type if type else ntype)) + if device_type or ndevice_type: + text.append( + "TYPE{}{}{}".format("!" if ndevice_type else "", cs, device_type if device_type else ndevice_type)) if filter: - for key, value in filter.items(): + for key, value in filters.items(): text.append("{}{}{}".format(key, cs, value)) result = self.send_recv_cmd("jsonlist2 {}".format(":FILTER=".join(text)), blocking=False, timeout=timeout) @@ -562,7 +545,8 @@ def get(self, name=None, state=None, group=None, room=None, type=None, nname=Non 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', + + 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): @@ -600,7 +584,6 @@ def __init__(self, server, que, port=7072, protocol='telnet', self.EventThread.setDaemon(True) self.EventThread.start() - def set_loglevel(self, level): ''' Set logging level, @@ -616,7 +599,6 @@ def set_loglevel(self, level): elif level == 3: logger.setLevel(logging.DEBUG) - def _event_worker_thread(self, que, filterlist, timeout=0.1, eventtimeout=120): self.fhem.send_cmd(self.informcmd) @@ -655,7 +637,7 @@ def _event_worker_thread(self, que, filterlist, timeout=0.1, val = '' for i in range(4, len(li)): val += li[i] - if i < len(li)-1: + if i < len(li) - 1: val += " " vl = val.split(" ") val = '' @@ -693,19 +675,19 @@ def _event_worker_thread(self, que, filterlist, timeout=0.1, adQ = True if adQ: 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 + } que.put(ev) time.sleep(timeout) self.fhem.close() return - def close(self): '''Stop event thread and close socket.''' self.eventThreadActive = False + From 8bd072fe886c9dea27096a41de3f68730d4c8753 Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 30 Nov 2018 00:26:10 +0100 Subject: [PATCH 03/16] Refactored get function --- fhem/fhem/__init__.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 638687c..943c43e 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -495,6 +495,13 @@ def get_fhem_state(self, timeout=0.1): logger.error("Failed to get fhem state. Not connected.") return {} + def _parse_filters(self, name, value, not_value, filter_list, case_sensitive): + compare = "=" if case_sensitive else "~" + if value: + filter_list.append("{}{}{}".format(name, compare, value)) + elif not_value: + filter_list.append("{}!{}{}".format(name, compare, not_value)) + def get(self, name=None, state=None, group=None, room=None, device_type=None, nname=None, nstate=None, ngroup=None, nroom=None, ndevice_type=None, case_sensitive=None, filters=None, timeout=0.1): """ @@ -515,27 +522,20 @@ def get(self, name=None, state=None, group=None, room=None, device_type=None, nn :param timeout: timeout for reply :return: dict of fhem devices """ - text = [] if not self.connected(): self.connect() if self.connected(): - cs = "=" if case_sensitive else "~" - if name or nname: - text.append("NAME{}{}{}".format("!" if nname else "", cs, name if name else nname)) - if state or nstate: - text.append("STATE{}{}{}".format("!" if nstate else "", cs, state if state else nstate)) - if group or ngroup: - text.append("group{}{}{}".format("!" if ngroup else "", cs, group if group else ngroup)) - if room or nroom: - text.append("room{}{}{}".format("!" if nroom else "", cs, room if room else nroom)) - if device_type or ndevice_type: - text.append( - "TYPE{}{}{}".format("!" if ndevice_type else "", cs, device_type if device_type else ndevice_type)) - if filter: + filter_list = [] + self._parse_filters("NAME", name, nname, filter_list, case_sensitive) + self._parse_filters("STATE", state, nstate, filter_list, case_sensitive) + self._parse_filters("group", group, ngroup, filter_list, case_sensitive) + self._parse_filters("room", room, nroom, filter_list, case_sensitive) + self._parse_filters("TYPE", device_type, ndevice_type, filter_list, case_sensitive) + if filters: for key, value in filters.items(): - text.append("{}{}{}".format(key, cs, value)) - result = self.send_recv_cmd("jsonlist2 {}".format(":FILTER=".join(text)), blocking=False, - timeout=timeout) + filter_list.append("{}{}{}".format(key, "=" if case_sensitive else "~", value)) + cmd = "jsonlist2 {}".format(":FILTER=".join(filter_list)) + result = self.send_recv_cmd(cmd, blocking=False, timeout=timeout) return result else: logger.error("Failed to get fhem state. Not connected.") From 973afcdbe275bc3eb011c931dfd4e1d408ba9a4e Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 30 Nov 2018 00:51:30 +0100 Subject: [PATCH 04/16] Support list for get filters --- fhem/fhem/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 943c43e..4507b43 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -495,12 +495,17 @@ def get_fhem_state(self, timeout=0.1): logger.error("Failed to get fhem state. Not connected.") return {} + def _append_filter(self, name, value, compare, string, filter_list): + value_list = [value] if isinstance(value, str) else value + values = ",".join(value_list) + filter_list.append(string.format(name, compare, values)) + def _parse_filters(self, name, value, not_value, filter_list, case_sensitive): compare = "=" if case_sensitive else "~" if value: - filter_list.append("{}{}{}".format(name, compare, value)) + self._append_filter(name, value, compare, "{}{}{}", filter_list) elif not_value: - filter_list.append("{}!{}{}".format(name, compare, not_value)) + self._append_filter(name, not_value, compare, "{}!{}{}", filter_list) def get(self, name=None, state=None, group=None, room=None, device_type=None, nname=None, nstate=None, ngroup=None, nroom=None, ndevice_type=None, case_sensitive=None, filters=None, timeout=0.1): From c4a6127d9b197239544cbfd4fb5fc03ec25a2b1b Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 30 Nov 2018 01:34:01 +0100 Subject: [PATCH 05/16] Add functions to filter results --- fhem/fhem/__init__.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 4507b43..1eab19b 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -507,6 +507,24 @@ def _parse_filters(self, name, value, not_value, filter_list, case_sensitive): elif not_value: self._append_filter(name, not_value, compare, "{}!{}{}", filter_list) + def _response_filter(self, response, arg, value, only_value=None): + result = {} + if len(arg) == 1: + for r in response['Results']: + if only_value: + if value in r and arg[0] in r[value] and 'Value' in r[value][arg[0]]: + result[r['Name']] = r[value][arg[0]]['Value'] + else: + if value in r and arg[0] in r[value]: + result[r['Name']] = r[value][arg[0]] + elif not len(arg): + for r in response['Results']: + result[r['Name']] = r[value] + else: + logger.error("Only one positional argument allowed") + return {} + return result + def get(self, name=None, state=None, group=None, room=None, device_type=None, nname=None, nstate=None, ngroup=None, nroom=None, ndevice_type=None, case_sensitive=None, filters=None, timeout=0.1): """ @@ -546,6 +564,21 @@ def get(self, name=None, state=None, group=None, room=None, device_type=None, nn logger.error("Failed to get fhem state. Not connected.") return {} + def get_states(self, **kwargs): + response = self.get(**kwargs) + return {r['Name']: r['Readings']['state']['Value'] for r in response['Results'] if 'state' in r['Readings']} + + def get_readings(self, arg, only_value=False, **kwargs): + response = self.get(**kwargs) + return self._response_filter(response, [arg], 'Readings', only_value=only_value) + + def get_attributes(self, *arg, **kwargs): + response = self.get(**kwargs) + return self._response_filter(response, arg, 'Attributes') + + def get_internals(self, *arg, **kwargs): + response = self.get(**kwargs) + return self._response_filter(response, arg, 'Internals') class FhemEventQueue: '''Creates a thread that listens to FHEM events and dispatches them to From 54a99fae0340f0f2ef6fcd997c3506d0fde12e8c Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 30 Nov 2018 01:56:30 +0100 Subject: [PATCH 06/16] Add documentation strings --- fhem/fhem/__init__.py | 48 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 1eab19b..6643e71 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -528,20 +528,21 @@ def _response_filter(self, response, arg, value, only_value=None): def get(self, name=None, state=None, group=None, room=None, device_type=None, nname=None, nstate=None, ngroup=None, nroom=None, ndevice_type=None, case_sensitive=None, filters=None, timeout=0.1): """ - Get FHEM state of devices, filter by parameters. - - :param name: regex for name - :param state: regex for state - :param group: regex for group - :param room: regex for room - :param device_type: regex for type + Get FHEM state of devices, filter by parameters. See https://fhem.de/commandref.html#devspec + This function abstracts often used filters and reduces transfered data size + + :param name: str or list, regex for device name in fhem + :param state: str or list, regex for device state in fhem + :param group: str or list, regex to filter fhem groups + :param room: str or list, regex to filter fhem room + :param device_type: str or list, regex to filter fhem device type :param nname: not name :param nstate: not state :param ngroup: not group :param nroom: not room :param ndevice_type: not type - :param case_sensitive: use case_sensitive instead of - :param filters: dict of filter - key=attribute/internal/reading, value=regex for value + :param case_sensitive: bool, use case_sensitivity for all filter functions + :param filters: dict of filters - key=attribute/internal/reading, value=regex for value, e.g. {"battery": "ok"} :param timeout: timeout for reply :return: dict of fhem devices """ @@ -565,21 +566,50 @@ def get(self, name=None, state=None, group=None, room=None, device_type=None, nn return {} def get_states(self, **kwargs): + """ + Return only device states, can use filters from get() + + :param kwargs: use keyword arguments from get function + :return: dict of fhem devices with states + """ response = self.get(**kwargs) return {r['Name']: r['Readings']['state']['Value'] for r in response['Results'] if 'state' in r['Readings']} def get_readings(self, arg, only_value=False, **kwargs): + """ + Return readings of a device, can use filters from get() + + :param arg: str, Get only specified reading, return all readings of device when parameter not given + :param only_value: return only value of reading, not timestamp + :param kwargs: use keyword arguments from get function + :return: dict of fhem devices with readings + """ response = self.get(**kwargs) return self._response_filter(response, [arg], 'Readings', only_value=only_value) def get_attributes(self, *arg, **kwargs): + """ + Return attributes of a device, can use filters from get() + + :param arg: str, Get only specified attribute, return all attributes of device when parameter not given + :param kwargs: use keyword arguments from get function + :return: dict of fhem devices with attributes + """ response = self.get(**kwargs) return self._response_filter(response, arg, 'Attributes') def get_internals(self, *arg, **kwargs): + """ + Return internals of a device, can use filters from get() + + :param arg: str, Get only specified internal, return all internals of device when parameter not given + :param kwargs: use keyword arguments from get function + :return: dict of fhem devices with internals + """ response = self.get(**kwargs) return self._response_filter(response, arg, 'Internals') + class FhemEventQueue: '''Creates a thread that listens to FHEM events and dispatches them to a Python queue.''' From 481ee7218e05759195f2022f95b7fbadd56f90e4 Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 30 Nov 2018 02:02:03 +0100 Subject: [PATCH 07/16] Made redundant functions deprecated --- fhem/fhem/__init__.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 6643e71..5297fc3 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -476,24 +476,12 @@ def get_dev_readings_time(self, dev, readings, timeout=0.1): return reads def getFhemState(self, timeout=0.1): - logger.critical("Deprecation: use get_fhem_state instead of getFhemState") - self.get_fhem_state(timeout) + logger.critical("Deprecation: use get() without parameters instead of getFhemState") + self.get(timeout) def get_fhem_state(self, timeout=0.1): - ''' - Get FHEM state of all devices, returns a large JSON object with - every single FHEM device and reading state - - :param timeout: timeout for reply - ''' - if not self.connected(): - self.connect() - if self.connected(): - return self.send_recv_cmd("jsonlist2", blocking=False, - timeout=timeout) - else: - logger.error("Failed to get fhem state. Not connected.") - return {} + logger.critical("Deprecation: use get() without parameters instead of get_fhem_state") + self.get(timeout) def _append_filter(self, name, value, compare, string, filter_list): value_list = [value] if isinstance(value, str) else value From d970bfa019bf1039d6258c0c5608e8fd0d019e8b Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 30 Nov 2018 16:34:49 +0100 Subject: [PATCH 08/16] Fixed wrong number of arguments --- fhem/fhem/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 5297fc3..535da71 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -563,7 +563,7 @@ def get_states(self, **kwargs): response = self.get(**kwargs) return {r['Name']: r['Readings']['state']['Value'] for r in response['Results'] if 'state' in r['Readings']} - def get_readings(self, arg, only_value=False, **kwargs): + def get_readings(self, *arg, **kwargs): """ Return readings of a device, can use filters from get() @@ -572,8 +572,10 @@ def get_readings(self, arg, only_value=False, **kwargs): :param kwargs: use keyword arguments from get function :return: dict of fhem devices with readings """ + only_value = kwargs['only_value'] if 'only_value' in kwargs else None + kwargs.pop('only_value', None) response = self.get(**kwargs) - return self._response_filter(response, [arg], 'Readings', only_value=only_value) + return self._response_filter(response, arg, 'Readings', only_value=only_value) def get_attributes(self, *arg, **kwargs): """ From 13be1efae0407688fca62bf596a826aa9150f046 Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 30 Nov 2018 16:48:42 +0100 Subject: [PATCH 09/16] Add only_value for empty args --- fhem/fhem/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 535da71..1bf63e2 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -507,7 +507,7 @@ def _response_filter(self, response, arg, value, only_value=None): result[r['Name']] = r[value][arg[0]] elif not len(arg): for r in response['Results']: - result[r['Name']] = r[value] + result[r['Name']] = r[value] if not only_value else {k: v['Value'] for k, v in r[value].items()} else: logger.error("Only one positional argument allowed") return {} From f6665dc3dcd0aac5c7f5949ff228ce04c49735b4 Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 30 Nov 2018 23:21:40 +0100 Subject: [PATCH 10/16] Make old get functions deprecated --- fhem/fhem/__init__.py | 226 ++++++++++++++++++++---------------------- 1 file changed, 106 insertions(+), 120 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 1bf63e2..ff2d44b 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -1,11 +1,11 @@ '''API for FHEM homeautomation server, supporting telnet or HTTP/HTTPS connections with authentication and CSRF-token support.''' -import time import datetime import json +import logging import socket import ssl import threading -import logging +import time try: # Python 3.x @@ -367,121 +367,38 @@ def send_recv_cmd(self, msg, timeout=0.1, blocking=True): return jdata def get_dev_state(self, dev, timeout=0.1): - ''' - Get all FHEM device properties as JSON object - - :param dev: FHEM device name - :param timeout: timeout for reply - ''' - if not self.connected(): - self.connect() - - if self.connected(): - return self.send_recv_cmd("jsonlist2 {}".format(dev), timeout=timeout) - else: - logger.error("Failed to get dev state for {}. Not connected.".format(dev)) - return {} + logger.critical("Deprecation: use get_device('device') instead of get_dev_state") + return self.get_device(dev, timeout=timeout) def get_dev_reading(self, dev, reading, timeout=0.1): - ''' - Get a specific reading from a FHEM device - - :param dev: FHEM device - :param reading: name of FHEM reading - :param timeout: timeout for reply - ''' - read = None - state = self.get_dev_state(dev, timeout=timeout) - if state == {}: - return None - - try: - read = state['Results'][0]['Readings'][reading]['Value'] - except Exception as err: - logger.error("Reading not defined: {}, {}, {}".format(dev, reading, err)) - return read - return read + logger.critical("Deprecation: use get_device_reading('device', 'reading') instead of get_dev_reading") + return self.get_device_reading(dev, reading, timeout=timeout) def getDevReadings(self, dev, reading, timeout=0.1): logger.critical("Deprecation: use get_dev_readings instead of getDevReadings") - self.get_dev_readings(dev, reading, timeout) + self.get_dev_readings(dev, reading, timeout=timeout) def get_dev_readings(self, dev, readings, timeout=0.1): - ''' - Get a list of readings for one FHEM device - - :param dev: FHEM device - :param readings: array of FHEM reading names - :param timeout: timeout for reply - ''' - reads = {} - state = self.get_dev_state(dev, timeout=timeout) - if state == {}: - return reads - for reading in readings: - try: - rr1 = state['Results'][0] - reads[reading] = rr1['Readings'][reading]['Value'] - except Exception as err: - logger.error("Reading not defined: {}, {}, {}".format(dev, reading, err)) - return reads + logger.critical("Deprecation: use get_device_reading('device', ['reading']) instead of get_dev_readings") + return self.get_device_reading(dev, readings, timeout=timeout) def get_dev_reading_time(self, dev, reading, timeout=0.1): - ''' - Get the datetime of a specific reading from a FHEM device - - :param dev: FHEM device - :param reading: name of FHEM reading - :param timeout: timeout for reply - ''' - read = None - state = self.get_dev_state(dev, timeout=timeout) - if state == {}: - return None - try: - read = state['Results'][0]['Readings'][reading]['Time'] - except: - logger.error("Reading not defined: {} {}".format(dev, reading)) - return None - try: - time = datetime.datetime.strptime(read, '%Y-%m-%d %H:%M:%S') - except (ValueError, TypeError) as err: - logger.error("Invalid time format: {}".format(err)) - return None - return time + logger.critical( + "Deprecation: use get_device_reading('device', 'reading', only_time=True) instead of get_dev_reading_time") + return self.get_device_reading(dev, reading, timeout=timeout) def get_dev_readings_time(self, dev, readings, timeout=0.1): - ''' - Get a list of datetimes of readings for one FHEM device - - :param dev: FHEM device - :param readings: array of FHEM reading names - :param timeout: timeout for reply - ''' - reads = {} - state = self.get_dev_state(dev, timeout=timeout) - if state == {}: - return reads - for reading in readings: - try: - rr1 = state['Results'][0] - read = rr1['Readings'][reading]['Time'] - try: - read_time = datetime.datetime.strptime(read, '%Y-%m-%d %H:%M:%S') - reads[reading] = read_time - except (ValueError, TypeError) as err: - logger.error("Invalid time format: {}".format(err)) - except Exception as err: - logger.error("Reading not defined: {} {} {}".format(dev, reading, err)) - return reads + logger.critical( + "Deprecation: use get_device_reading('device', ['reading'], only_time=True) instead of get_dev_reading_time") + return self.get_device_reading(dev, readings, timeout=timeout) def getFhemState(self, timeout=0.1): logger.critical("Deprecation: use get() without parameters instead of getFhemState") - self.get(timeout) + self.get(timeout=timeout) def get_fhem_state(self, timeout=0.1): logger.critical("Deprecation: use get() without parameters instead of get_fhem_state") - self.get(timeout) + self.get(timeout=timeout) def _append_filter(self, name, value, compare, string, filter_list): value_list = [value] if isinstance(value, str) else value @@ -495,22 +412,33 @@ def _parse_filters(self, name, value, not_value, filter_list, case_sensitive): elif not_value: self._append_filter(name, not_value, compare, "{}!{}{}", filter_list) - def _response_filter(self, response, arg, value, only_value=None): - result = {} - if len(arg) == 1: - for r in response['Results']: - if only_value: - if value in r and arg[0] in r[value] and 'Value' in r[value][arg[0]]: - result[r['Name']] = r[value][arg[0]]['Value'] - else: - if value in r and arg[0] in r[value]: - result[r['Name']] = r[value][arg[0]] - elif not len(arg): - for r in response['Results']: - result[r['Name']] = r[value] if not only_value else {k: v['Value'] for k, v in r[value].items()} - else: - logger.error("Only one positional argument allowed") + def _parse_timestamp(self, timestamp): + try: + return datetime.datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S') + except (ValueError, TypeError) as err: + logger.error("Invalid time format: {}".format(err)) + return None + + def _response_filter(self, response, arg, value, value_only=None, time_only=None): + if len(arg) > 2: + logger.error("Too many positional arguments") return {} + result = {} + for r in 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 in arg[0]))} + elif time_only: + result[r['Name']] = {k: self._parse_timestamp(v['Time']) for k, v in r[value].items() if + 'Time' in v and (not len(arg) or (len(arg) and 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 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 get(self, name=None, state=None, group=None, room=None, device_type=None, nname=None, nstate=None, ngroup=None, @@ -561,6 +489,8 @@ def get_states(self, **kwargs): :return: dict of fhem devices with states """ response = self.get(**kwargs) + if not response: + return response return {r['Name']: r['Readings']['state']['Value'] for r in response['Results'] if 'state' in r['Readings']} def get_readings(self, *arg, **kwargs): @@ -568,14 +498,17 @@ def get_readings(self, *arg, **kwargs): Return readings of a device, can use filters from get() :param arg: str, Get only specified reading, return all readings of device when parameter not given - :param only_value: return only value of reading, not timestamp + :param value_only: return only value of reading, not timestamp + :param time_only: return only timestamp of reading as datetime object :param kwargs: use keyword arguments from get function :return: dict of fhem devices with readings """ - only_value = kwargs['only_value'] if 'only_value' in kwargs else None - kwargs.pop('only_value', 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', only_value=only_value) + return self._response_filter(response, arg, 'Readings', value_only=value_only, time_only=time_only) def get_attributes(self, *arg, **kwargs): """ @@ -599,6 +532,60 @@ def get_internals(self, *arg, **kwargs): response = self.get(**kwargs) return self._response_filter(response, arg, 'Internals') + def get_device(self, device, **kwargs): + """ + Get all data from a device + + :param device: str or list, + :param kwargs: use keyword arguments from get function + :return: dict with data of fhem device + """ + return self.get(name=device, **kwargs) + + def get_device_state(self, device, **kwargs): + """ + Get state of one device + + :param device: str or list, + :param kwargs: use keyword arguments from get and get_states functions + :return: str when only one else dict + """ + result = self.get_states(name=device, **kwargs) + return result if len(result.values()) - 1 else list(result.values())[0] + + def get_device_reading(self, device, *arg, **kwargs): + """ + Get reading(s) of one device + + :param device: str or list, + :param arg: str for one reading, list for special readings, empty for all readings + :param kwargs: use keyword arguments from get and get_readings functions + :return: dict with readings + """ + return self.get_readings(*arg, name=device, **kwargs) + + def get_device_attribute(self, device, *arg, **kwargs): + """ + Get attribute(s) of one device + + :param device: str or list, + :param arg: str for one attribute, list for special attributes, empty for all attributes + :param kwargs: use keyword arguments from get and get_attributes functions + :return: dict with attributes + """ + return self.get_attributes(*arg, name=device, **kwargs) + + def get_device_internal(self, device, *arg, **kwargs): + """ + Get internal(s) of one device + + :param device: str or list, + :param arg: str for one internal value, list for special internal values, empty for all internal values + :param kwargs: use keyword arguments from get and get_internals functions + :return: dict with internals + """ + return self.get_internals(*arg, name=device, **kwargs) + class FhemEventQueue: '''Creates a thread that listens to FHEM events and dispatches them to @@ -748,4 +735,3 @@ def _event_worker_thread(self, que, filterlist, timeout=0.1, def close(self): '''Stop event thread and close socket.''' self.eventThreadActive = False - From adb22f6514eaa7755d384cc20ae99ffcc3031588 Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 30 Nov 2018 23:50:23 +0100 Subject: [PATCH 11/16] Convert to int, float and datetime --- fhem/fhem/__init__.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index ff2d44b..6ab10a5 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -2,6 +2,7 @@ import datetime import json import logging +import re import socket import ssl import threading @@ -412,25 +413,39 @@ def _parse_filters(self, name, value, not_value, filter_list, case_sensitive): elif not_value: self._append_filter(name, not_value, compare, "{}!{}{}", filter_list) - def _parse_timestamp(self, timestamp): - try: - return datetime.datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S') - except (ValueError, TypeError) as err: - logger.error("Invalid time format: {}".format(err)) - return None + def _convert_data(self, response, k, v): + if isinstance(v, str): + if re.findall("^[0-9]+$", v): + response[k] = int(v) + elif re.findall("^[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') + if isinstance(v, dict): + self._parse_data_types(response[k]) + if isinstance(v, list): + self._parse_data_types(response[k]) + + def _parse_data_types(self, response): + if isinstance(response, dict): + for k, v in response.items(): + self._convert_data(response, k, v) + if isinstance(response, list): + for i, v in enumerate(response): + self._convert_data(response, i, v) def _response_filter(self, response, arg, value, value_only=None, time_only=None): if len(arg) > 2: logger.error("Too many positional arguments") return {} result = {} - for r in response['Results']: + for r in response: 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 in arg[0]))} elif time_only: - result[r['Name']] = {k: self._parse_timestamp(v['Time']) for k, v in r[value].items() if + 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 in arg[0]))} else: result[r['Name']] = {k: v for k, v in r[value].items() if @@ -476,7 +491,10 @@ def get(self, name=None, state=None, group=None, room=None, device_type=None, nn filter_list.append("{}{}{}".format(key, "=" if case_sensitive else "~", value)) cmd = "jsonlist2 {}".format(":FILTER=".join(filter_list)) result = self.send_recv_cmd(cmd, blocking=False, timeout=timeout) - return result + if not result: + return result + self._parse_data_types(result) + return result['Results'] else: logger.error("Failed to get fhem state. Not connected.") return {} @@ -491,7 +509,7 @@ 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['Results'] 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): """ From eacadf9b28366337c624f18f48400312e0bfef80 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 2 Dec 2018 20:04:10 +0100 Subject: [PATCH 12/16] Fix false ouptput in deprecated functions --- fhem/fhem/__init__.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 6ab10a5..d7ffca5 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -369,37 +369,37 @@ def send_recv_cmd(self, msg, timeout=0.1, blocking=True): def get_dev_state(self, dev, timeout=0.1): logger.critical("Deprecation: use get_device('device') instead of get_dev_state") - return self.get_device(dev, timeout=timeout) + return self.get_device(dev, timeout=timeout, deprecated=True) def get_dev_reading(self, dev, reading, timeout=0.1): logger.critical("Deprecation: use get_device_reading('device', 'reading') instead of get_dev_reading") - return self.get_device_reading(dev, reading, timeout=timeout) + return self.get_device_reading(dev, reading, value_only=True, timeout=timeout) def getDevReadings(self, dev, reading, timeout=0.1): - logger.critical("Deprecation: use get_dev_readings instead of getDevReadings") - self.get_dev_readings(dev, reading, timeout=timeout) + logger.critical("Deprecation: use get_device_reading('device', ['reading']) instead of getDevReadings") + return self.get_device_reading(dev, timeout=timeout, value_only=True, deprecated=True) def get_dev_readings(self, dev, readings, timeout=0.1): logger.critical("Deprecation: use get_device_reading('device', ['reading']) instead of get_dev_readings") - return self.get_device_reading(dev, readings, timeout=timeout) + return self.get_device_reading(dev, readings, timeout=timeout, value_only=True, deprecated=True) def get_dev_reading_time(self, dev, reading, timeout=0.1): logger.critical( "Deprecation: use get_device_reading('device', 'reading', only_time=True) instead of get_dev_reading_time") - return self.get_device_reading(dev, reading, timeout=timeout) + return self.get_device_reading(dev, reading, timeout=timeout, time_only=True) def get_dev_readings_time(self, dev, readings, timeout=0.1): logger.critical( "Deprecation: use get_device_reading('device', ['reading'], only_time=True) instead of get_dev_reading_time") - return self.get_device_reading(dev, readings, timeout=timeout) + return self.get_device_reading(dev, readings, timeout=timeout, time_only=True) def getFhemState(self, timeout=0.1): logger.critical("Deprecation: use get() without parameters instead of getFhemState") - self.get(timeout=timeout) + return self.get(timeout=timeout, deprecated=True) def get_fhem_state(self, timeout=0.1): logger.critical("Deprecation: use get() without parameters instead of get_fhem_state") - self.get(timeout=timeout) + return self.get(timeout=timeout, deprecated=True) def _append_filter(self, name, value, compare, string, filter_list): value_list = [value] if isinstance(value, str) else value @@ -439,7 +439,7 @@ def _response_filter(self, response, arg, value, value_only=None, time_only=None logger.error("Too many positional arguments") return {} result = {} - for r in response: + 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 @@ -456,8 +456,11 @@ def _response_filter(self, response, arg, value, value_only=None, time_only=None result[r['Name']] = list(result[r['Name']].values())[0] return result + def _sand_down(self, value): + return value if len(value.values()) - 1 else list(value.values())[0] + def get(self, name=None, state=None, group=None, room=None, device_type=None, nname=None, nstate=None, ngroup=None, - nroom=None, ndevice_type=None, case_sensitive=None, filters=None, timeout=0.1): + nroom=None, ndevice_type=None, case_sensitive=None, filters=None, timeout=0.1, deprecated=None): """ Get FHEM state of devices, filter by parameters. See https://fhem.de/commandref.html#devspec This function abstracts often used filters and reduces transfered data size @@ -491,10 +494,10 @@ def get(self, name=None, state=None, group=None, room=None, device_type=None, nn filter_list.append("{}{}{}".format(key, "=" if case_sensitive else "~", value)) cmd = "jsonlist2 {}".format(":FILTER=".join(filter_list)) result = self.send_recv_cmd(cmd, blocking=False, timeout=timeout) - if not result: + if not result or deprecated: return result - self._parse_data_types(result) - return result['Results'] + self._parse_data_types(result['Results']) + return result else: logger.error("Failed to get fhem state. Not connected.") return {} @@ -569,7 +572,7 @@ def get_device_state(self, device, **kwargs): :return: str when only one else dict """ result = self.get_states(name=device, **kwargs) - return result if len(result.values()) - 1 else list(result.values())[0] + return self._sand_down(result) def get_device_reading(self, device, *arg, **kwargs): """ @@ -580,7 +583,8 @@ def get_device_reading(self, device, *arg, **kwargs): :param kwargs: use keyword arguments from get and get_readings functions :return: dict with readings """ - return self.get_readings(*arg, name=device, **kwargs) + result = self.get_readings(*arg, name=device, **kwargs) + return self._sand_down(result) def get_device_attribute(self, device, *arg, **kwargs): """ @@ -591,7 +595,8 @@ def get_device_attribute(self, device, *arg, **kwargs): :param kwargs: use keyword arguments from get and get_attributes functions :return: dict with attributes """ - return self.get_attributes(*arg, name=device, **kwargs) + result = self.get_attributes(*arg, name=device, **kwargs) + return self._sand_down(result) def get_device_internal(self, device, *arg, **kwargs): """ @@ -602,7 +607,8 @@ def get_device_internal(self, device, *arg, **kwargs): :param kwargs: use keyword arguments from get and get_internals functions :return: dict with internals """ - return self.get_internals(*arg, name=device, **kwargs) + result = self.get_internals(*arg, name=device, **kwargs) + return self._sand_down(result) class FhemEventQueue: From e528140f41626c594e6f6256549766ea4ca311d2 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 2 Dec 2018 20:07:05 +0100 Subject: [PATCH 13/16] Python2 compatibility for get functions --- 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 d7ffca5..856a690 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -414,7 +414,11 @@ def _parse_filters(self, name, value, not_value, filter_list, case_sensitive): self._append_filter(name, not_value, compare, "{}!{}{}", filter_list) def _convert_data(self, response, k, v): - if isinstance(v, str): + try: + test_type = unicode + except NameError: + test_type = str + if isinstance(v, test_type): if re.findall("^[0-9]+$", v): response[k] = int(v) elif re.findall("^[0-9]+\.[0-9]+$", v): From f9ff0c8f4cb384819218734706a1d41d6aef0d1f Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 2 Dec 2018 20:13:53 +0100 Subject: [PATCH 14/16] Add classmethods for Codacy --- fhem/fhem/__init__.py | 55 +++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 856a690..0ca4517 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -401,11 +401,39 @@ def get_fhem_state(self, timeout=0.1): logger.critical("Deprecation: use get() without parameters instead of get_fhem_state") return self.get(timeout=timeout, deprecated=True) - def _append_filter(self, name, value, compare, string, filter_list): + @classmethod + def _sand_down(cls, value): + return value if len(value.values()) - 1 else list(value.values())[0] + + @classmethod + def _append_filter(cls, name, value, compare, string, filter_list): value_list = [value] if isinstance(value, str) else value values = ",".join(value_list) filter_list.append(string.format(name, compare, values)) + @classmethod + def _response_filter(cls, response, arg, value, value_only=None, time_only=None): + if len(arg) > 2: + logger.error("Too many positional arguments") + return {} + result = {} + 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 in arg[0]))} + 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 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 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): compare = "=" if case_sensitive else "~" if value: @@ -438,31 +466,6 @@ def _parse_data_types(self, response): for i, v in enumerate(response): self._convert_data(response, i, v) - def _response_filter(self, response, arg, value, value_only=None, time_only=None): - if len(arg) > 2: - logger.error("Too many positional arguments") - return {} - result = {} - 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 in arg[0]))} - 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 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 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 _sand_down(self, value): - return value if len(value.values()) - 1 else list(value.values())[0] - def get(self, name=None, state=None, group=None, room=None, device_type=None, nname=None, nstate=None, ngroup=None, nroom=None, ndevice_type=None, case_sensitive=None, filters=None, timeout=0.1, deprecated=None): """ From 786f3a5b144dc6cc14507c856d5287e5bb0f56a5 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 2 Dec 2018 20:59:41 +0100 Subject: [PATCH 15/16] Improve documentation --- fhem/fhem/__init__.py | 58 +++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 0ca4517..c70dc53 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -469,23 +469,26 @@ def _parse_data_types(self, response): def get(self, name=None, state=None, group=None, room=None, device_type=None, nname=None, nstate=None, ngroup=None, nroom=None, ndevice_type=None, case_sensitive=None, filters=None, timeout=0.1, deprecated=None): """ - Get FHEM state of devices, filter by parameters. See https://fhem.de/commandref.html#devspec - This function abstracts often used filters and reduces transfered data size - - :param name: str or list, regex for device name in fhem - :param state: str or list, regex for device state in fhem - :param group: str or list, regex to filter fhem groups - :param room: str or list, regex to filter fhem room - :param device_type: str or list, regex to filter fhem device type + 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. + Filters can be used by all other get functions. + For more information about filters, see https://FHEM.de/commandref.html#devspec + + :param name: str or list, device name in FHEM + :param state: str or list, state in FHEM + :param group: str or list, filter FHEM groups + :param room: str or list, filter FHEM room + :param device_type: str or list, FHEM device type :param nname: not name :param nstate: not state :param ngroup: not group :param nroom: not room - :param ndevice_type: not type + :param ndevice_type: not device_type :param case_sensitive: bool, use case_sensitivity for all filter functions :param filters: dict of filters - key=attribute/internal/reading, value=regex for value, e.g. {"battery": "ok"} + :param deprecated: Don't convert to python types and send full FHEM response :param timeout: timeout for reply - :return: dict of fhem devices + :return: dict of FHEM devices """ if not self.connected(): self.connect() @@ -503,7 +506,8 @@ def get(self, name=None, state=None, group=None, room=None, device_type=None, nn result = self.send_recv_cmd(cmd, blocking=False, timeout=timeout) if not result or deprecated: return result - self._parse_data_types(result['Results']) + result = result['Results'] + self._parse_data_types(result) return result else: logger.error("Failed to get fhem state. Not connected.") @@ -511,10 +515,10 @@ def get(self, name=None, state=None, group=None, room=None, device_type=None, nn def get_states(self, **kwargs): """ - Return only device states, can use filters from get() + Return only device states, can use filters from get(). - :param kwargs: use keyword arguments from get function - :return: dict of fhem devices with states + :param kwargs: Use keyword arguments from get function + :return: dict of FHEM devices with states """ response = self.get(**kwargs) if not response: @@ -523,13 +527,13 @@ def get_states(self, **kwargs): def get_readings(self, *arg, **kwargs): """ - Return readings of a device, can use filters from get() + Return readings of a device, can use filters from get(). - :param arg: str, Get only specified reading, return all readings of device when parameter not given + :param arg: str, Get only a specified reading, return all readings of device when parameter not given :param value_only: return only value of reading, not timestamp - :param time_only: return only timestamp of reading as datetime object + :param time_only: return only timestamp of reading :param kwargs: use keyword arguments from get function - :return: dict of fhem devices with readings + :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 @@ -544,7 +548,7 @@ def get_attributes(self, *arg, **kwargs): :param arg: str, Get only specified attribute, return all attributes of device when parameter not given :param kwargs: use keyword arguments from get function - :return: dict of fhem devices with attributes + :return: dict of FHEM devices with attributes """ response = self.get(**kwargs) return self._response_filter(response, arg, 'Attributes') @@ -555,7 +559,7 @@ def get_internals(self, *arg, **kwargs): :param arg: str, Get only specified internal, return all internals of device when parameter not given :param kwargs: use keyword arguments from get function - :return: dict of fhem devices with internals + :return: dict of FHEM devices with internals """ response = self.get(**kwargs) return self._response_filter(response, arg, 'Internals') @@ -566,7 +570,7 @@ def get_device(self, device, **kwargs): :param device: str or list, :param kwargs: use keyword arguments from get function - :return: dict with data of fhem device + :return: dict with data of specific FHEM device """ return self.get(name=device, **kwargs) @@ -576,7 +580,7 @@ def get_device_state(self, device, **kwargs): :param device: str or list, :param kwargs: use keyword arguments from get and get_states functions - :return: str when only one else dict + :return: str, int, float when only specific value requested else dict """ result = self.get_states(name=device, **kwargs) return self._sand_down(result) @@ -588,7 +592,7 @@ def get_device_reading(self, device, *arg, **kwargs): :param device: str or list, :param arg: str for one reading, list for special readings, empty for all readings :param kwargs: use keyword arguments from get and get_readings functions - :return: dict with readings + :return: str, int, float when only specific value requested else dict """ result = self.get_readings(*arg, name=device, **kwargs) return self._sand_down(result) @@ -599,8 +603,8 @@ def get_device_attribute(self, device, *arg, **kwargs): :param device: str or list, :param arg: str for one attribute, list for special attributes, empty for all attributes - :param kwargs: use keyword arguments from get and get_attributes functions - :return: dict with attributes + :param kwargs: use keyword arguments from get function + :return: str, int, float when only specific value requested else dict """ result = self.get_attributes(*arg, name=device, **kwargs) return self._sand_down(result) @@ -611,8 +615,8 @@ def get_device_internal(self, device, *arg, **kwargs): :param device: str or list, :param arg: str for one internal value, list for special internal values, empty for all internal values - :param kwargs: use keyword arguments from get and get_internals functions - :return: dict with internals + :param kwargs: use keyword arguments from get function + :return: str, int, float when only specific value requested else dict """ result = self.get_internals(*arg, name=device, **kwargs) return self._sand_down(result) From 649caad45c9859fd724d8c841fb6813ad98286cb Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 2 Dec 2018 21:23:51 +0100 Subject: [PATCH 16/16] Update readme --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 52e6404..d370e16 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,14 @@ import fhem fh = fhem.Fhem("myserver.home.org") # Send a command to FHEM (this automatically connects() in case of telnet) fh.send_cmd("set lamp on") -# Get a specific reading from a device -temp = fh.get_dev_reading("LivingThermometer", "temperature") +# Get temperatur of LivingThermometer +temp = fh.get_device_reading("LivingThermometer", "temperature") +# Get a dict of kitchen lights with light on +lights = fh.get_states(group="Kitchen", state="on", device_type="light", value_only=True) +# Get all data of specific tvs +tvs = fh.get(device_type=["LGTV", "STV"]) +# Get indoor thermometers with low battery +low = fh.get_readings(name=".*Thermometer", nroom="outdoor", filter={"battery!": "ok"}) ``` To connect via telnet with SSL and password: