diff --git a/README.md b/README.md index aafae36..6f1d005 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Python FHEM (home automation server) API Simple API to connect to the FHEM home automation server 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 version 0.6.x will be the last versions supporting Python 2.x. + ## Installation ### PIP installation (PyPI) @@ -21,13 +23,16 @@ pip install [-U] fhem ### From source -In ```python-fhem/fhem```: +In `python-fhem/fhem`: Get a copy of README for the install (required by setup.py): + ```bash cp ../README.md . ``` + then: + ```bash pip install [-U] . ``` @@ -40,6 +45,8 @@ pip install [-U] -e . ## History +* 0.6.2 (2019-06-06): Bug fix, get_device_reading() could return additional unrelated readings. [#14](https://github.com/domschl/python-fhem/issues/14). Default blocking mode for telnet has been set to non-blocking. This can be changed with parameter `blocking=True` (telnet only). Use of HTTP(S) is recommended (superior +performance and faster) * [build environment] (2019-07-22): Initial support for TravisCI automated self-tests. * 0.6.1 (2018-12-26): New API used telnet non-blocking on get which caused problems (d1nd141, [#12](https://github.com/domschl/python-fhem/issues/12)), fixed by using blocking telnet i/o. @@ -66,8 +73,8 @@ import fhem logging.basicConfig(level=logging.DEBUG) -# Connect via default protocol telnet, default port 7072: -fh = fhem.Fhem("myserver.home.org") +## Connect via HTTP, port 8083: +fh = fhem.Fhem("myserver.home.org", protocol="http", port=8083) # Send a command to FHEM (this automatically connects() in case of telnet) fh.send_cmd("set lamp on") # Get temperatur of LivingThermometer @@ -78,17 +85,18 @@ lights = fh.get_states(group="Kitchen", state="on", device_type="light", value_o tvs = fh.get(device_type=["LGTV", "STV"]) # Get indoor thermometers with low battery low = fh.get_readings(name=".*Thermometer", not_room="outdoor", filter={"battery!": "ok"}) +# Get temperature readings from all devices that have a temperature reading: +all_temps = fh.get_readings('temperature') ``` -To connect via telnet with SSL and password: +HTTPS connection: ```python -fh = fhem.Fhem("myserver.home.org", port=7073, use_ssl=True, password='mysecret') -fh.connect() -if fh.connected(): - # Do things +fh = fhem.Fhem('myserver.home.org', port=8085, protocol='https') ``` +Self-signed certs are accepted (since no `cafile` option is given). + To connect via https with SSL and basicAuth: ```python @@ -96,6 +104,28 @@ fh = fhem.Fhem('myserver.home.org', port=8086, protocol='https', cafile=mycertfile, username="myuser", password="secretsauce") ``` +If no public certificate `cafile` is given, then self-signed certs are accepted. + +## Connect via default protocol telnet, default port 7072: (deprecated) + +*Note*: Connection via telnet is not reliable for large requests, which +includes everything that uses wildcard-funcionality. + +```python +fh = fhem.Fhem("myserver.home.org") +``` + +To connect via telnet with SSL and password: + +```python +fh = fhem.Fhem("myserver.home.org", port=7073, use_ssl=True, password='mysecret') +fh.connect() +if fh.connected(): + # Do things +``` + +It is recommended to use HTTP(S) to connect to Fhem instead. + ### Event queues (currently telnet only) The library can create an event queue that uses a background thread to receive diff --git a/fhem/fhem/__init__.py b/fhem/fhem/__init__.py index 9444739..65a8f13 100644 --- a/fhem/fhem/__init__.py +++ b/fhem/fhem/__init__.py @@ -32,7 +32,7 @@ from urllib2 import install_opener # needs to be in sync with setup.py and documentation (conf.py, branch gh-pages) -__version__ = '0.6.1' +__version__ = '0.6.2' # create logger with 'python_fhem' # logger = logging.getLogger(__name__) @@ -346,7 +346,7 @@ def _recv_nonblocking(self, timeout=0.1): self.sock.setblocking(True) return data - def send_recv_cmd(self, msg, timeout=0.1, blocking=True): + def send_recv_cmd(self, msg, timeout=0.1, blocking=False): ''' Sends a command to the server and waits for an immediate reply. @@ -364,7 +364,8 @@ def send_recv_cmd(self, msg, timeout=0.1, blocking=True): data = [] if blocking is True: try: - data = self.sock.recv(32000) + # This causes failures if reply is larger! + data = self.sock.recv(64000) except socket.error: self.log.error("Failed to recv msg. {}".format(data)) return {} @@ -456,13 +457,13 @@ def _response_filter(self, response, arg, value, value_only=None, time_only=None 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]))} + '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 in arg[0]))} + '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 in arg[0]))} + (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: @@ -504,7 +505,7 @@ def _parse_data_types(self, 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, raw_result=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. @@ -525,6 +526,7 @@ def get(self, name=None, state=None, group=None, room=None, device_type=None, no :param filters: dict of filters - key=attribute/internal/reading, value=regex for value, e.g. {"battery": "ok"} :param raw_result: On True: Don't convert to python types and send full FHEM response :param timeout: timeout for reply + :param blocking: telnet socket mode, default blocking=False :return: dict of FHEM devices """ if not self.connected(): @@ -548,7 +550,7 @@ def get(self, name=None, state=None, group=None, room=None, device_type=None, no cmd = "jsonlist2 {}".format(":FILTER=".join(filter_list)) if self.protocol == 'telnet': result = self.send_recv_cmd( - cmd, blocking=True, timeout=timeout) + cmd, blocking=blocking, timeout=timeout) else: result = self.send_recv_cmd( cmd, blocking=False, timeout=timeout) diff --git a/fhem/setup.py b/fhem/setup.py index cc47ce6..d94a943 100644 --- a/fhem/setup.py +++ b/fhem/setup.py @@ -4,7 +4,7 @@ long_description = fh.read() setup(name='fhem', - version='0.6.1', + version='0.6.2', description='Python API for FHEM home automation server', long_description=long_description, long_description_content_type="text/markdown", diff --git a/selftest/selftest.py b/selftest/selftest.py index 6340ad3..f43016d 100644 --- a/selftest/selftest.py +++ b/selftest/selftest.py @@ -226,41 +226,45 @@ def create_device(fhem, name, readings): first = False for dev in devs: - for rd in dev['readings']: - dict_value = fh.get_device_reading(dev['name'], rd) - try: - value = dict_value['Value'] - except: - print( - 'Bad reply reading {} {} -> {}'.format(dev['name'], rd, dict_value)) - sys.exit(-7) - - if value == dev['readings'][rd]: - print( - "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])) - sys.exit(-5) + 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) + try: + value = dict_value['Value'] + except: + print( + 'Bad reply reading {} {} -> {}'.format(dev['name'], rd, dict_value)) + sys.exit(-7) + + if value == dev['readings'][rd]: + print( + "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])) + sys.exit(-5) num_temps = 0 for dev in devs: if 'temperature' in dev['readings']: num_temps += 1 - temps = fh.get_readings("temperature", timeout=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)) - try: - if connection['protocol'] != 'telnet': - sys.exit(-6) - else: - print('Telnet protocol is deprecated for wildcard operations!') - except: - sys.exit(-6) + sys.exit(-6) else: print("Multiread of all devices with 'temperature' reading: ok.") + states = fh.get_states() + if len(states) < 5: + print("Iconsistent number of states: {}".format(len(states))) + sys.exit(-7) + else: + print("states received: {}, ok.".format(len(states))) + fh.close() sys.exit(0)