Skip to content

Commit cb9aa1b

Browse files
committed
viessmann: update
1 parent b1c7933 commit cb9aa1b

4 files changed

Lines changed: 97 additions & 66 deletions

File tree

viessmann/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,11 @@ def write_addr(self, addr, value):
187187
"""
188188
addr = addr.lower()
189189

190-
commandname = self._commands.get_command_from_reply(addr)
191-
if commandname is None:
190+
results = self._commands.get_commands_from_reply(addr)
191+
if results is None:
192192
self.logger.debug(f'Address {addr} not defined in commandset, aborting')
193193
return
194+
commandname = results[0]
194195

195196
self.logger.debug(f'Attempting to write address {addr} with value {value} for command {commandname}')
196197

viessmann/datatypes.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ def get_send_data(self, data, **kwargs):
6666
raise ValueError(f'incorrect data format, YYYY-MM-DD expected. Error was: {e}')
6767

6868
def get_shng_data(self, data, type=None, **kwargs):
69-
return datetime.strptime(data.hex(), '%Y%m%d%W%H%M%S').isoformat()
69+
return datetime.datetime.strptime(data.hex(), '%Y%m%d%W%H%M%S').isoformat()
7070

7171

7272
# D = date
7373
class DT_Date(DT_Time):
7474
def get_shng_data(self, data, type=None, **kwargs):
75-
return datetime.strptime(data.hex(), '%Y%m%d%W%H%M%S').date().isoformat()
75+
return datetime.datetime.strptime(data.hex(), '%Y%m%d%W%H%M%S').date().isoformat()
7676

7777

7878
# C = control timer (?)
@@ -86,12 +86,15 @@ def get_send_data(self, data, **kwargs):
8686
times += f'{an:02x}{aus:02x}'
8787
valuebytes = bytes.fromhex(times)
8888
self.logger.debug(f'created value bytes as hexstring: {bytes2hexstring(valuebytes)} and as bytes: {valuebytes}')
89+
return valuebytes
90+
except ValueError:
91+
raise
8992
except Exception as e:
9093
raise ValueError(f'incorrect data format, (An: hh:mm Aus: hh:mm) expected. Error was: {e}')
9194

9295
def get_shng_data(self, data, type=None, **kwargs):
93-
timer = self._decode_timer(data.hex())
94-
return [{'An': on_time, 'Aus': off_time} for on_time, off_time in zip(timer, timer)]
96+
timer = list(decode_timer(data.hex()))
97+
return [{'An': on_time, 'Aus': off_time} for on_time, off_time in zip(timer[::2], timer[1::2])]
9598

9699

97100
# H = hex

viessmann/plugin.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,32 @@ parameters:
4848
de: Item-Pfad für das Suspend-Item
4949
en: item path for suspend switch item
5050

51+
loop_guard_count:
52+
type: num
53+
default: 5
54+
description:
55+
de: Anzahl identischer Schreibversuche im Zeitfenster, die den Loop-Guard auslösen (0 = deaktiviert)
56+
en: number of identical write attempts within the time window that trigger the loop guard (0 = disabled)
57+
58+
loop_guard_window:
59+
type: num
60+
default: 5.0
61+
description:
62+
de: Zeitfenster in Sekunden für den Loop-Guard
63+
en: time window in seconds for the loop guard
64+
65+
loop_guard_source:
66+
type: str
67+
default: ''
68+
description:
69+
de: Präfix des aufrufenden Systems, auf das der Loop-Guard beschränkt wird (leer = alle externen Aufrufer)
70+
en: caller prefix to restrict loop guard to (empty = all external callers)
71+
description_long:
72+
de: 'Wenn gesetzt, zählt der Loop-Guard nur Schreibversuche, deren "caller"-String mit diesem Präfix
73+
beginnt (z.B. "MQTT"). Schreibvorgänge anderer Aufrufer werden immer durchgelassen.'
74+
en: 'If set, the loop guard only counts write attempts whose "caller" string starts with this prefix
75+
(e.g. "MQTT"). Writes from all other callers always pass through freely.'
76+
5177
command_class:
5278
type: str
5379
default: SDPCommandViessmann

viessmann/protocol.py

Lines changed: 61 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626

2727
import logging
2828

29-
from lib.model.sdp.globals import (CONN_SER_DIR, PLUGIN_ATTR_CB_ON_CONNECT, PLUGIN_ATTR_CB_ON_DISCONNECT, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_CONN_AUTO_CONN, PLUGIN_ATTR_CONN_BINARY, PLUGIN_ATTR_CONN_CYCLE, PLUGIN_ATTR_CONN_RETRIES, PLUGIN_ATTR_CONN_TIMEOUT, PLUGIN_ATTR_SERIAL_BAUD, PLUGIN_ATTR_SERIAL_BSIZE, PLUGIN_ATTR_SERIAL_PARITY, PLUGIN_ATTR_SERIAL_PORT, PLUGIN_ATTR_SERIAL_STOP)
29+
from lib.model.sdp.globals import (SDPError, SDPConnectionError, SDPProtocolError,
30+
CONN_SER_DIR, PLUGIN_ATTR_CB_ON_CONNECT, PLUGIN_ATTR_CB_ON_DISCONNECT, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_CONN_AUTO_CONN, PLUGIN_ATTR_CONN_BINARY, PLUGIN_ATTR_CONN_CYCLE, PLUGIN_ATTR_CONN_RETRIES, PLUGIN_ATTR_CONN_TIMEOUT, PLUGIN_ATTR_SERIAL_BAUD, PLUGIN_ATTR_SERIAL_BSIZE, PLUGIN_ATTR_SERIAL_PARITY, PLUGIN_ATTR_SERIAL_PORT, PLUGIN_ATTR_SERIAL_STOP)
3031
from lib.model.sdp.protocol import SDPProtocol
3132

3233
from time import sleep
@@ -223,8 +224,10 @@ def _send_init_on_send(self):
223224
self.__syncsent = False
224225
empty_replies = 0
225226

226-
self.logger.debug(f'communication initialized: {self._is_initialized}')
227-
return self._is_initialized
227+
if not self._is_initialized:
228+
raise SDPProtocolError('P300 protocol initialization failed after 10 attempts')
229+
self.logger.debug('P300 communication initialized successfully')
230+
return True
228231

229232
elif self._viess_proto == 'KW':
230233

@@ -249,80 +252,79 @@ def _send_init_on_send(self):
249252
return True
250253
sleep(.8)
251254
attempt = attempt + 1
252-
self.logger.error(f'sync not acquired after {attempt} attempts')
253-
self._close()
254-
return False
255+
self.logger.error(f'KW sync not acquired after {attempt} attempts')
256+
raise SDPProtocolError(f'KW protocol sync failed after {attempt} attempts')
255257

256258
return True
257259

258260
def _send(self, data_dict, **kwargs):
259261
"""
260-
send data. data_dict needs to contain the following information:
262+
Send payload and return parsed response, or raise on any failure.
261263
262264
data_dict['payload']: address from/to which to read/write (hex, str)
263265
data_dict['data']['len']: length of command to send
264266
data_dict['data']['value']: value bytes to write, None if reading
265267
266268
:param data_dict: send data
267-
:param read_response: KW only: read response value (True) or only return status byte
268269
:type data_dict: dict
269-
:type read_response: bool
270-
:return: Response packet (bytearray) if no error occured, None otherwise
270+
:return: Response bytes if read command, None if write command
271+
:raises SDPConnectionError: serial I/O failure or no response from device
272+
:raises SDPProtocolError: unexpected or invalid device response
271273
"""
272274
if kwargs:
273275
self.logger.debug(f'got additional kw args {kwargs}')
274276

275277
(packet, responselen) = self._build_payload(data_dict)
276278

277-
# send payload
278-
self._lock.acquire()
279279
try:
280-
self._send_bytes(packet)
281-
self.logger.debug(f'successfully sent packet {self._bytes2hexstring(packet)}')
282-
283-
# receive response
284-
response_packet = bytearray()
285-
self.logger.debug(f'trying to receive {responselen} bytes of the response')
286-
chunk = self._read_bytes(responselen)
287-
if self._viess_proto == 'P300':
288-
self.logger.debug(f'received {len(chunk)} bytes chunk of response as hexstring {self._bytes2hexstring(chunk)} and as bytes {chunk}')
289-
if len(chunk) != 0:
280+
with self._lock:
281+
self._send_bytes(packet)
282+
self.logger.debug(f'sent packet {self._bytes2hexstring(packet)}')
283+
284+
chunk = self._read_bytes(responselen)
285+
self.logger.debug(
286+
f'received {len(chunk)} bytes: '
287+
f'{self._bytes2hexstring(chunk) if chunk else "empty"}'
288+
)
289+
290+
if self._viess_proto == 'P300':
291+
if len(chunk) == 0:
292+
raise SDPConnectionError('no response from device after P300 command')
290293
if chunk[:1] == self._int2bytes(self._controlset['error'], 1):
291-
self.logger.error(f'interface returned error, response was {chunk}')
292-
elif len(chunk) == 1 and chunk[:1] == self._int2bytes(self._controlset['not_initiated'], 1):
293-
self.logger.error('received invalid chunk, connection not initialized, forcing re-initialize...')
294-
self._initialized = False
295-
elif chunk[:1] != self._int2bytes(self._controlset['acknowledge'], 1):
296-
self.logger.error(f'received invalid chunk, not starting with ACK, response was {chunk}')
297-
self.logger.warning('encountered invalid chunk, maybe communication was lost, forcing re-initialize')
298-
self._close()
299-
sleep(1)
300-
self._open()
301-
else:
302-
response_packet.extend(chunk)
303-
return self._parse_response(response_packet)
304-
else:
305-
self.logger.debug(f'received 0 bytes chunk - ignoring response_packet, chunk was {chunk}')
306-
elif self._viess_proto == 'KW':
307-
self.logger.debug(f'received {len(chunk)} bytes chunk of response as hexstring {self._bytes2hexstring(chunk)} and as bytes {chunk}')
308-
if len(chunk) != 0:
309-
response_packet.extend(chunk)
310-
return self._parse_response(response_packet, data_dict['data']['value'] is None)
311-
else:
312-
self.logger.error('received 0 bytes chunk - this probably is a communication error, possibly a wrong datapoint address?')
313-
except IOError as e:
314-
self.logger.error(f'send_command_packet failed with IO error, trying to reconnect. Error was: {e}')
315-
self._close()
294+
raise SDPProtocolError(
295+
f'device reported protocol error, response: {self._bytes2hexstring(chunk)}'
296+
)
297+
if (len(chunk) == 1 and
298+
chunk[:1] == self._int2bytes(self._controlset['not_initiated'], 1)):
299+
self._is_initialized = False
300+
raise SDPProtocolError(
301+
'device reports not initialized; will re-initialize on next send'
302+
)
303+
if chunk[:1] != self._int2bytes(self._controlset['acknowledge'], 1):
304+
raise SDPProtocolError(
305+
f'unexpected P300 response (no ACK): {self._bytes2hexstring(chunk)}'
306+
)
307+
return self._parse_response(bytearray(chunk))
308+
309+
elif self._viess_proto == 'KW':
310+
if len(chunk) == 0:
311+
raise SDPConnectionError('no response from device after KW command')
312+
return self._parse_response(bytearray(chunk), data_dict['data']['value'] is None)
313+
314+
except SDPError:
315+
self._is_initialized = False
316+
try:
317+
self._close()
318+
except Exception:
319+
pass
320+
raise
316321
except Exception as e:
317-
self.logger.error(f'send_command_packet failed with error: {e}')
318-
finally:
322+
self._is_initialized = False
319323
try:
320-
self._lock.release()
321-
except RuntimeError:
324+
self._close()
325+
except Exception:
322326
pass
323-
324-
# if we didn't return with data earlier, we hit an error. Act accordingly
325-
return None
327+
raise SDPConnectionError(f'unexpected error during send: {e}') from e
326328

327329
def _parse_response(self, response, read_response=True):
328330
"""
@@ -343,11 +345,10 @@ def _parse_response(self, response, read_response=True):
343345
checksum = self._calc_checksum(response[1:len(response) - 1]) # first, cut first byte (ACK) and last byte (checksum) and then calculate checksum
344346
received_checksum = response[len(response) - 1]
345347
if received_checksum != checksum:
346-
self.logger.error(f'calculated checksum {checksum} does not match received checksum of {received_checksum}! Ignoring reponse, cycling connection')
347-
self._close()
348-
sleep(1)
349-
self._open()
350-
return None
348+
raise SDPProtocolError(
349+
f'P300 checksum mismatch: expected {checksum:#04x}, '
350+
f'got {received_checksum:#04x}'
351+
)
351352

352353
# Extract command/address, valuebytes and valuebytecount out of response
353354
responsetypecode = response[3] # 0x00 = query, 0x01 = reply, 0x03 = error
@@ -357,7 +358,7 @@ def _parse_response(self, response, read_response=True):
357358
# Extract databytes out of response
358359
rawdatabytes = bytearray()
359360
rawdatabytes.extend(response[8:8 + (valuebytecount)])
360-
elif self._protocol == 'KW':
361+
elif self._viess_proto == 'KW':
361362

362363
# imitate P300 response code data for easier combined handling afterwards
363364
# a read_response telegram consists only of the value bytes

0 commit comments

Comments
 (0)