Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fhem 0.6.2, fix for reading parser, #14 #15

Merged
merged 13 commits into from
Jul 22, 2019
46 changes: 38 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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] .
```
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -78,24 +85,47 @@ 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
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
Expand Down
18 changes: 10 additions & 8 deletions fhem/fhem/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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.

Expand All @@ -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 {}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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():
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion fhem/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
52 changes: 28 additions & 24 deletions selftest/selftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)