-
Notifications
You must be signed in to change notification settings - Fork 68
modbus_init, Luxtronic2 reconnect and pack/unpack and smartvisu type error #210
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
base: develop
Are you sure you want to change the base?
Changes from 9 commits
b1a4342
1a43438
f8aa391
01d63b6
fa17f52
3bb2327
ef9d73f
82d682c
e0dd9a7
86c80c1
f876695
8e29643
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,10 @@ | |
| # Requirements | ||
| This plugin has no requirements or dependencies. | ||
|
|
||
| # How it works | ||
| At init it tries to connect to the device. If successful and if only one parameter, attribute or calculated is configured it reads the entire block. E.g. if one parameter is configured for reading, it reads the entire parameter block every 'cycle', but not the other ones. That’s by design of the device. | ||
| If the connection is lost or if the max number of timeouts ('max_timeouts') is reached it tries to reconnect every 'reconnect_cycle'. | ||
|
|
||
| # Configuration | ||
|
|
||
| ## plugin.conf | ||
|
|
@@ -12,11 +16,17 @@ This plugin has no requirements or dependencies. | |
| class_path = plugins.luxtronic2 | ||
| host = 192.168.0.123 | ||
| # port = 8888 | ||
| # cycle = 300 | ||
| # reconnect_cycle = 60 | ||
| # max_timeouts = 10 | ||
| </pre> | ||
|
|
||
| ### Attributes | ||
| * `host`: specifies the hostname of your heating server. | ||
| * `port`: if you want to use a nonstandard port. | ||
| * `cycle`: read cycle time in seconds. | ||
| * `reconnect_cycle`: cycle time for reconnect attempts in seconds. | ||
| * `max_timeouts`: max number of timeouts before connection is considered lost. | ||
|
|
||
| ## items.conf | ||
|
|
||
|
|
@@ -46,6 +56,12 @@ Defines a mapping to a attribute (read-only). All attribute values are bytes (nu | |
| ### lux2_c | ||
| Defines a mapping to a calculated value (read-only). All calculated values are integer (numbers). | ||
|
|
||
| ### lux2_unpack | ||
| Python lambda function. Called before the value is sent towards the device. | ||
|
|
||
| ### lux2_pack | ||
| Called before the value is written to the smarthome item. | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could also be a common setting for items - something like
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see your point and I thought about it too. Something like eval_out would work most of the time, but it would fail for something like the example below. My thinking is that a plugin should provide you with a ready to use value and the other way around should take the unencoded value. E.g. for the knx plugin you have to provide knx_dpt and the plugin handles pack/unpack stuff.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ohinckel I looked a bit further into default values for modbus_pack (same for unpack and lux2). |
||
| <pre> | ||
| [heating] | ||
| [[temp_outside]] | ||
|
|
@@ -54,17 +70,16 @@ Defines a mapping to a calculated value (read-only). All calculated values are i | |
| [[state_numeric]] | ||
| type = num | ||
| lux2_c = 119 | ||
| lux2_unpack = lambda x: x/10.0 | ||
| [[state]] | ||
| type = str | ||
| lux2 = 119 | ||
| </pre> | ||
|
|
||
| ## logic.conf | ||
|
|
||
| Currently there is no logic configuration for this plugin. | ||
| #### Pack Unpack | ||
|
|
||
| # Functions | ||
| lux_unpack and lux_pack require a standard Python lambda function. Pack is called before the value is sent towards the device, unpack for the other way around (device to Smarthome). | ||
|
|
||
| Currently there are no functions offered from this plugin. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,8 +25,9 @@ | |
| import threading | ||
| import struct | ||
| import time | ||
| from collections import defaultdict | ||
|
|
||
| logger = logging.getLogger('') | ||
| logger = logging.getLogger('luxtronic2') | ||
|
|
||
|
|
||
| class luxex(Exception): | ||
|
|
@@ -35,12 +36,23 @@ class luxex(Exception): | |
|
|
||
| class LuxBase(): | ||
|
|
||
| def __init__(self, host, port=8888): | ||
| def __init__(self, host, port=8888, max_timeouts=10): | ||
| """Summary | ||
|
|
||
| Args: | ||
| host (TYPE): Description | ||
| port (int, optional): Description | ||
| max_timeouts (int, optional): after this number | ||
| of timeouts occured the connection is closed. | ||
| If None its not evaluated. | ||
| """ | ||
| self.host = host | ||
| self.port = int(port) | ||
| self._sock = False | ||
| self._lock = threading.Lock() | ||
| self.is_connected = False | ||
| self._max_timeouts = max_timeouts | ||
| self._timeouts = 0 # number of already occured timeouts | ||
| self._connection_attempts = 0 | ||
| self._connection_errorlog = 60 | ||
| self._params = [] | ||
|
|
@@ -93,6 +105,13 @@ def close(self): | |
| except: | ||
| pass | ||
|
|
||
| def __manage_timeouts(self): | ||
| self._timeouts += 1 | ||
| if self._max_timeouts and self._timeouts >= self._max_timeouts: | ||
| self._timeouts = 0 | ||
| self.close() | ||
| raise luxex("Max Nr timeouts reached: {}".format(self._max_timeouts)) | ||
|
|
||
| def _request(self, request, length): | ||
| if not self.is_connected: | ||
| raise luxex("no connection to luxtronic.") | ||
|
|
@@ -106,6 +125,7 @@ def _request(self, request, length): | |
| answer = self._sock.recv(length) | ||
| except socket.timeout: | ||
| self._lock.release() | ||
| self.__manage_timeouts() | ||
| raise luxex("error receiving answer: timeout") | ||
| except Exception as e: | ||
| self._lock.release() | ||
|
|
@@ -118,6 +138,7 @@ def _request_more(self, length): | |
| return self._sock.recv(length) | ||
| except socket.timeout: | ||
| self._lock.release() | ||
| self.__manage_timeouts() | ||
| raise luxex("error receiving payload: timeout") | ||
| except Exception as e: | ||
| self._lock.release() | ||
|
|
@@ -226,51 +247,101 @@ def refresh_calculated(self): | |
|
|
||
|
|
||
| class Luxtronic2(LuxBase): | ||
| _parameter = {} | ||
| _attribute = {} | ||
| _calculated = {} | ||
| _parameter = defaultdict(lambda: defaultdict(int)) | ||
| _attribute = defaultdict(lambda: defaultdict(int)) | ||
| _calculated = defaultdict(lambda: defaultdict()) | ||
| _decoded = {} | ||
| alive = True | ||
|
|
||
| def __init__(self, smarthome, host, port=8888, cycle=300): | ||
| LuxBase.__init__(self, host, port) | ||
| def __init__(self, smarthome, host, port=8888, | ||
| cycle=300, reconnect_cycle=60, max_timeouts=10): | ||
| LuxBase.__init__(self, host, port, max_timeouts) | ||
| self._sh = smarthome | ||
| self._cycle = int(cycle) | ||
| self.connect() | ||
| self._reconnect_cycle = int(reconnect_cycle) | ||
| try: | ||
| self.connect() | ||
| except Exception as e: | ||
| logger.error('luctronic2 connect faild: {}'.format(e)) | ||
|
|
||
| def run(self): | ||
| self.alive = True | ||
| self._sh.scheduler.add('Luxtronic2', self._refresh, cycle=self._cycle) | ||
| # Letting the scheduler recall the foo by handing it "cycle" could | ||
| # fail in this application, because it calles the foo without checking | ||
| # if the last call finished. This could clog up the system. | ||
| threading.Thread(name='Lux2_cycle', target=self.__refresh).start() | ||
| threading.Thread(name='Lux2_recon', target=self._reconnect).start() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there really cases were the update cylce needs too much time? Maybe using a short timeout for querying the device could help here.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See also my bug report for that. For now, it works with threading, but I could not get it working with the scheduler. I don't think it is about the socket timeout, because I also had problems with the scheduler in the modbus plugin too. |
||
|
|
||
| def stop(self): | ||
| self.alive = False | ||
|
|
||
| def _reconnect(self): | ||
| while self.alive: | ||
| start_time = time.time() | ||
| if not self.is_connected: | ||
| try: | ||
| logger.debug('lux2 trying reconnect') | ||
| self.connect() | ||
| except Exception as e: | ||
| logger.error('luctronic2 reconnect faild: {}'.format(e)) | ||
| cycle_time = time.time() - start_time | ||
| time_to_sleep = self._reconnect_cycle - cycle_time | ||
| while self.alive and time_to_sleep > 0: # make it more easly interuptable | ||
| time_to_sleep -= .1 | ||
| time.sleep(.1) | ||
|
|
||
| def __refresh(self): | ||
| while self.alive: | ||
| start_time = time.time() | ||
| self._refresh() | ||
| cycle_time = time.time() - start_time | ||
| time_to_sleep = self._cycle - cycle_time | ||
| while self.alive and time_to_sleep > 0: # make it more easly interuptable | ||
| time_to_sleep -= .1 | ||
| time.sleep(.1) | ||
|
|
||
| def _refresh(self): | ||
| if not self.is_connected: | ||
| return | ||
| start = time.time() | ||
| if len(self._parameter) > 0: | ||
| self.refresh_parameters() | ||
| try: | ||
| self.refresh_parameters() | ||
| except Exception as e: | ||
| logger.error('luctronic2 refresh parameters faild: {}' | ||
| .format(e)) | ||
| for p in self._parameter: | ||
| val = self.get_parameter(p) | ||
| if val: | ||
| self._parameter[p](val, 'Luxtronic2') | ||
| val = self._parameter[p]['unpack'](val) | ||
| self._parameter[p]['item'](val, 'Luxtronic2') | ||
| if len(self._attribute) > 0: | ||
| self.refresh_attributes() | ||
| try: | ||
| self.refresh_attributes() | ||
| except Exception as e: | ||
| logger.error('luctronic2 refresh attributes faild: {}' | ||
| .format(e)) | ||
| for a in self._attribute: | ||
| val = self.get_attribute(a) | ||
| if val: | ||
| self._attribute[a](val, 'Luxtronic2') | ||
| val = self._attribute[a]['unpack'](val) | ||
| self._attribute[a]['item'](val, 'Luxtronic2') | ||
| if len(self._calculated) > 0 or len(self._decoded) > 0: | ||
| self.refresh_calculated() | ||
| try: | ||
| self.refresh_calculated() | ||
| except Exception as e: | ||
| logger.error('luctronic2 refresh calculated faild: {}' | ||
| .format(e)) | ||
| for c in self._calculated: | ||
| val = self.get_calculated(c) | ||
| if val is not None: | ||
| self._calculated[c](val, 'Luxtronic2') | ||
| val = self._calculated[c]['unpack'](val) | ||
| self._calculated[c]['item'](val, 'Luxtronic2') | ||
| for d in self._decoded: | ||
| val = self.get_calculated(d) | ||
| if val is not None: | ||
| self._decoded[d](self._decode(d, val), 'Luxtronic2') | ||
| val = self._decoded[d]['unpack'](val) | ||
| self._decoded[d]['item'](self._decode(d, val), 'Luxtronic2') | ||
| cycletime = time.time() - start | ||
| logger.debug("cycle takes {0} seconds".format(cycletime)) | ||
|
|
||
|
|
@@ -328,22 +399,44 @@ def _decode(self, identifier, value): | |
| return value | ||
|
|
||
| def parse_item(self, item): | ||
| def __reverseListOp(param): | ||
| # reversing list building of smarthome.py config parser for the | ||
| # binary or opperator | | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think putting the value in quotes will fix this problem too. Usually you should be using quotes around the value to avoid escaping problem. Other setting may have this problem too, but we should not start to implement such an "unquoting mechanism" for each of these settings. A general solution, which already exists (using quotes), would be more clear and consistent.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did not know that quotes are supported, but if it works it's most definitely a better way to do it. I am going to fix it when there is time for it.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ohinckel I just check the config parser again and quotes are not supported right now. I also tried it and there is no magic gunk happening somewhere else. config.py line:89... attr, __, value = line.partition('=')
...
attr = attr.strip()
...
if '|' in value:
item[attr] = [strip_quotes(x) for x in value.split('|')]
else:
item[attr] = strip_quotes(value)So to allow list operators in combination with quotes, it would have to ignore pipes inside of quotes and split if there are pips outside of them. I did a quick hack, see below. If you have a better solution or if I missed something let me know. def split_pipe(s):
output = []
split_pos = []
quotes = ['"',"'"]
pipe = '|'
in_quoate = False
start_quote = None
for i in range(0, len(s)):
if not in_quoate and s[i] == pipe:
split_pos.append(i)
if not in_quoate and s[i] in quotes:
start_quote = s[i]
in_quoate = True
elif in_quoate and s[i] == start_quote:
in_quoate = False
if len(split_pos) > 0:
output.append(s[0:split_pos[0]])
for i in range(0,len(split_pos)):
if i+1 < len(split_pos):
output.append(s[split_pos[i]+1:split_pos[i+1]])
else:
output.append(s[split_pos[i]+1:])
output = [strip_quotes(x) for x in output]
return output
else:
return strip_quotes(s)
s = """'123|456'|"789'10'|1112"|131415 """
current = [strip_quotes(x) for x in s.split('|')]
for i in current:
print(i)
print('-'*10)
x = split_pipe(s)
for i in x:
print(i)
print('-'*10)
x = split_pipe('1')
print(x) |
||
| if isinstance(param, list): | ||
| return (' | '.join(param)) | ||
| else: | ||
| return param | ||
|
|
||
| def add_pack_unpack(dps): | ||
| if 'lux2_pack' in item.conf: | ||
| dps['pack'] = eval(__reverseListOp(item.conf['lux2_pack'])) | ||
| else: | ||
| dps['pack'] = lambda x: x | ||
| if 'lux2_unpack' in item.conf: | ||
| dps['unpack'] = eval(__reverseListOp(item.conf['lux2_unpack'])) | ||
| else: | ||
| dps['unpack'] = lambda x: x | ||
|
|
||
| if 'lux2' in item.conf: | ||
| d = item.conf['lux2'] | ||
| d = int(d) | ||
| self._decoded[d] = item | ||
| self._decoded[d]['item'] = item | ||
| add_pack_unpack(self._decoded[d]) | ||
| if 'lux2_a' in item.conf: | ||
| a = item.conf['lux2_a'] | ||
| a = int(a) | ||
| self._attribute[a] = item | ||
| self._attribute[a]['item'] = item | ||
| add_pack_unpack(self._attribute[a]) | ||
| if 'lux2_c' in item.conf: | ||
| c = item.conf['lux2_c'] | ||
| c = int(c) | ||
| self._calculated[c] = item | ||
| self._calculated[c]['item'] = item | ||
| add_pack_unpack(self._calculated[c]) | ||
| if 'lux2_p' in item.conf: | ||
| p = item.conf['lux2_p'] | ||
| p = int(p) | ||
| self._parameter[p] = item | ||
| self._parameter[p]['item'] = item | ||
| add_pack_unpack(self._calculated[p]) | ||
| return self.update_item | ||
|
|
||
| def update_item(self, item, caller=None, source=None, dest=None): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unpack option sounds like the common
evalsetting for items. It evaluates the value and puts the result into the item. IMHO, I prefer to use common settings instead of having special plugin settings for all the plugins.