From fb51fdaa24f335e0537bfda865ddfae007301b88 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:38:20 +0100 Subject: [PATCH 1/6] modified some plugins for parameter checks, restartability and code cleanness --- comfoair/__init__.py | 196 ++++++++++++------------- comfoair/plugin.yaml | 17 ++- helios/__init__.py | 318 ++++++++++++++++++++-------------------- helios/plugin.yaml | 11 +- intercom_2n/__init__.py | 153 +++++++++---------- intercom_2n/plugin.yaml | 13 +- jvcproj/__init__.py | 35 ++--- jvcproj/plugin.yaml | 8 +- kostal/__init__.py | 112 ++++++-------- kostal/plugin.yaml | 10 +- luxtronic2/__init__.py | 88 ++++------- luxtronic2/plugin.yaml | 13 +- plex/__init__.py | 44 +++--- plex/plugin.yaml | 8 +- rcswitch/__init__.py | 189 ++++++++++++------------ rcswitch/plugin.yaml | 6 +- 16 files changed, 599 insertions(+), 622 deletions(-) diff --git a/comfoair/__init__.py b/comfoair/__init__.py index 1bfcf970f..5196fff66 100755 --- a/comfoair/__init__.py +++ b/comfoair/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +# !/usr/bin/env python ######################################################################### # Copyright 2013 Stefan Kals ######################################################################### @@ -22,7 +22,6 @@ import socket import time import serial -import re import threading from . import commands @@ -32,33 +31,30 @@ class ComfoAir(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = '1.3.0' + PLUGIN_VERSION = '1.3.1' - def __init__(self, smarthome, host=None, port=0, serialport=None, kwltype='comfoair350'): + def __init__(self): self.logger = logging.getLogger('ComfoAir') self.connected = False - self._sh = smarthome self._params = {} self._init_cmds = [] self._cyclic_cmds = {} self._lock = threading.Lock() - self._host = host - self._port = int(port) - self._serialport = serialport + self._host = self.get_parameter_value('host') + self._port = self.get_parameter_value('port') + self._serialport = self.get_parameter_value('serialport') + self._kwltype = self.get_parameter_value('kwltype') self._connection_attempts = 0 self._connection_errorlog = 60 self._initread = False - # automatically (re)connect - smarthome.connections.monitor(self) - # Load controlset and commandset - if kwltype in commands.controlset and kwltype in commands.commandset: - self._controlset = commands.controlset[kwltype] - self._commandset = commands.commandset[kwltype] - self.log_info('Loaded commands for KWL type \'{}\''.format(kwltype)) + if self._kwltype in commands.controlset and self._kwltype in commands.commandset: + self._controlset = commands.controlset[self._kwltype] + self._commandset = commands.commandset[self._kwltype] + self.log_info('Loaded commands for KWL type \'{}\''.format(self._kwltype)) else: - self.log_err('Commands for KWL type \'{}\' could not be found!'.format(kwltype)) + self.log_err('Commands for KWL type \'{}\' could not be found!'.format(self._kwltype)) return None # Remember packet config @@ -69,13 +65,13 @@ def __init__(self, smarthome, host=None, port=0, serialport=None, kwltype='comfo self._reponsecommandinc = self._controlset['ResponseCommandIncrement'] self._commandlength = 2 self._checksumlength = 1 - + def connect(self): - if self._serialport is not None: + if self._serialport: self.connect_serial() else: self.connect_tcp() - + def connect_tcp(self): self._lock.acquire() try: @@ -94,12 +90,12 @@ def connect_tcp(self): self.log_info('connected to {}:{}'.format(self._host, self._port)) self._connection_attempts = 0 self._lock.release() - + def connect_serial(self): self._lock.acquire() try: self._serialconnection = serial.Serial( - self._serialport, 9600, serial.EIGHTBITS, serial.PARITY_NONE, serial.STOPBITS_ONE, timeout=2) + self._serialport, 9600, serial.EIGHTBITS, serial.PARITY_NONE, serial.STOPBITS_ONE, timeout=2) except Exception as e: self._connection_attempts -= 1 if self._connection_attempts <= 0: @@ -118,8 +114,8 @@ def disconnect(self): self.disconnect_serial() else: self.disconnect_tcp() - - def disconnect_tcp(self): + + def disconnect_tcp(self): self.connected = False try: self._sock.shutdown(socket.SHUT_RDWR) @@ -130,54 +126,54 @@ def disconnect_tcp(self): except: pass - def disconnect_serial(self): + def disconnect_serial(self): self.connected = False try: self._serialconnection.close() self._serialconnection = None except: pass - + def send_bytes(self, packet): if self._serialport is not None: self.send_bytes_serial(packet) else: self.send_bytes_tcp(packet) - + def send_bytes_tcp(self, packet): self._sock.sendall(packet) def send_bytes_serial(self, packet): self._serialconnection.write(packet) - + def read_bytes(self, length): if self._serialport is not None: return self.read_bytes_serial(length) else: return self.read_bytes_tcp(length) - + def read_bytes_tcp(self, length): return self._sock.recv(length) def read_bytes_serial(self, length): return self._serialconnection.read(length) - + def parse_item(self, item): # Process the read config if self.has_iattr(item.conf, 'comfoair_read'): commandname = self.get_iattr_value(item.conf, 'comfoair_read') - if (commandname == None or commandname not in self._commandset): + if (commandname is None or commandname not in self._commandset): self.log_err('Item {} contains invalid read command \'{}\'!'.format(item, commandname)) return None - + # Remember the read config to later update this item if the configured response comes in self.log_info('Item {} reads by using command \'{}\'.'.format(item, commandname)) commandconf = self._commandset[commandname] commandcode = commandconf['Command'] - if not commandcode in self._params: + if commandcode not in self._params: self._params[commandcode] = {'commandname': [commandname], 'items': [item]} - elif not item in self._params[commandcode]['items']: + elif item not in self._params[commandcode]['items']: self._params[commandcode]['commandname'].append(commandname) self._params[commandcode]['items'].append(item) @@ -185,7 +181,7 @@ def parse_item(self, item): if (self.has_iattr(item.conf, 'comfoair_init') and self.get_iattr_value(item.conf, 'comfoair_init') == 'true'): self.log_info('Item {} is initialized on startup.'.format(item)) # Only add the item to the initial commands if it is not cyclic. Cyclic commands get called on init because this is the first cycle... - if not commandcode in self._init_cmds and not self.has_iattr(item.conf, 'comfoair_read_cycle'): + if commandcode not in self._init_cmds and not self.has_iattr(item.conf, 'comfoair_read_cycle'): self._init_cmds.append(commandcode) # Allow items to be cyclically updated @@ -193,7 +189,7 @@ def parse_item(self, item): cycle = int(self.get_iattr_value(item.conf, 'comfoair_read_cycle')) self.log_info('Item {} should read cyclic every {} seconds.'.format(item, cycle)) - if not commandcode in self._cyclic_cmds: + if commandcode not in self._cyclic_cmds: self._cyclic_cmds[commandcode] = {'cycle': cycle, 'nexttime': 0} else: # If another item requested this command already with a longer cycle, use the shorter cycle now @@ -203,12 +199,12 @@ def parse_item(self, item): # Process the send config if self.has_iattr(item.conf, 'comfoair_send'): commandname = self.get_iattr_value(item.conf, 'comfoair_send') - if commandname == None: + if commandname is None: return None elif commandname not in self._commandset: self.log_err('Item {} contains invalid write command \'{}\'!'.format(item, commandname)) return None - + self.log_info('Item {} writes by using command \'{}\''.format(item, commandname)) return self.update_item else: @@ -221,7 +217,7 @@ def update_item(self, item, caller=None, source=None, dest=None): if caller != 'ComfoAir' and self.has_iattr(item.conf, 'comfoair_send'): commandname = self.get_iattr_value(item.conf, 'comfoair_send') - if type(item) != int: + if type(item) is not int: value = int(item()) else: value = item() @@ -238,18 +234,18 @@ def update_item(self, item, caller=None, source=None, dest=None): aw = float(readafterwrite) time.sleep(aw) self.send_command(readcommandname) - - # If commands should be triggered after this write + + # If commands should be triggered after this write if self.has_iattr(item.conf, 'comfoair_trigger'): trigger = self.get_iattr_value(item.conf, 'comfoair_trigger') - if trigger == None: + if trigger is None: self.log_err('Item {} contains invalid trigger command list \'{}\'!'.format(item, trigger)) else: - tdelay = 5 # default delay + tdelay = 5 # default delay if self.has_iattr(item.conf, 'comfoair_trigger_afterwrite'): tdelay = float(self.get_iattr_value(item.conf, 'comfoair_trigger_afterwrite')) - if type(trigger) != list: - trigger = [trigger] + if type(trigger) is not list: + trigger = [trigger] for triggername in trigger: triggername = triggername.strip() if triggername is not None and readafterwrite is not None: @@ -268,32 +264,32 @@ def handle_cyclic_cmds(self): self.log_debug('Triggering cyclic read command: {}'.format(commandname)) self.send_command(commandname) entry['nexttime'] = currenttime + entry['cycle'] - + def send_command(self, commandname, value=None): try: - #self.log_debug('Got a new send job: Command {} with value {}'.format(commandname, value)) - + # self.log_debug('Got a new send job: Command {} with value {}'.format(commandname, value)) + # Get command config commandconf = self._commandset[commandname] commandcode = int(commandconf['Command']) commandcodebytecount = commandconf['CommandBytes'] commandtype = commandconf['Type'] commandvaluebytes = commandconf['ValueBytes'] - #self.log_debug('Command config: {}'.format(commandconf)) - + # self.log_debug('Command config: {}'.format(commandconf)) + # Transform value for write commands - #self.log_debug('Got value: {}'.format(value)) + # self.log_debug('Got value: {}'.format(value)) if 'ValueTransform' in commandconf and value is not None and value != '' and commandtype == 'Write': commandtransform = commandconf['ValueTransform'] value = self.value_transform(value, commandtype, commandtransform) - #self.log_debug('Transformed value using method {} to {}'.format(commandtransform, value)) - + # self.log_debug('Transformed value using method {} to {}'.format(commandtransform, value)) + # Build value byte array valuebytes = bytearray() if value is not None and commandvaluebytes > 0: valuebytes = self.int2bytes(value, commandvaluebytes) - #self.log_debug('Created value bytes: {}'.format(valuebytes)) - + # self.log_debug('Created value bytes: {}'.format(valuebytes)) + # Calculate the checksum commandbytes = self.int2bytes(commandcode, commandcodebytecount) payload = bytearray() @@ -301,7 +297,7 @@ def send_command(self, commandname, value=None): if len(valuebytes) > 0: payload.extend(valuebytes) checksum = self.calc_checksum(payload) - + # Build packet packet = bytearray() packet.extend(self.int2bytes(self._controlset['PacketStart'], 2)) @@ -311,22 +307,22 @@ def send_command(self, commandname, value=None): packet.extend(self.int2bytes(checksum, 1)) packet.extend(self.int2bytes(self._controlset['PacketEnd'], 2)) self.log_debug('Preparing command {} with value {} (transformed to value byte \'{}\') to be sent.'.format(commandname, value, self.bytes2hexstring(valuebytes))) - + # Use a lock to allow only one sender at a time self._lock.acquire() if not self.connected: raise Exception("No connection to ComfoAir.") - + try: self.send_bytes(packet) self.log_debug('Successfully sent packet: {}'.format(self.bytes2hexstring(packet))) except Exception as e: raise Exception('Exception while sending: {}'.format(e)) - + if commandtype == 'Read': packet = bytearray() - + # Try to receive a packet start, a command and a data length byte firstpartlen = len(self._packetstart) + self._commandlength + 1 while self.alive and len(packet) < firstpartlen: @@ -335,9 +331,9 @@ def send_command(self, commandname, value=None): self.log_debug('Trying to receive {} bytes for the first part of the response.'.format(bytestoreceive)) chunk = self.read_bytes(bytestoreceive) self.log_debug('Received {} bytes chunk of response part 1: {}'.format(len(chunk), self.bytes2hexstring(chunk))) - if len(chunk) == 0: + if len(chunk) == 0: raise Exception('Received 0 bytes chunk - ignoring packet!') - + # Cut away old ACK (but only if the telegram wasn't started already) if len(packet) == 0: chunk = self.remove_ack_begin(chunk) @@ -346,11 +342,11 @@ def send_command(self, commandname, value=None): raise Exception("error receiving first part of packet: timeout") except Exception as e: raise Exception("error receiving first part of packet: {}".format(e)) - + datalen = packet[firstpartlen - 1] - #self.log_info('Got a data length of: {}'.format(datalen)) + # self.log_info('Got a data length of: {}'.format(datalen)) packetlen = firstpartlen + datalen + self._checksumlength + len(self._packetend) - + # Try to receive the second part of the packet while self.alive and len(packet) < packetlen or packet[-2:] != self._packetend: try: @@ -358,42 +354,42 @@ def send_command(self, commandname, value=None): if len(packet) >= packetlen and packet[-2:] != self._packetend: packetlen = len(packet) + 1 self.log_debug('Extended packet length because of encoded characters.'.format(self.bytes2hexstring(chunk))) - + # Receive next chunk bytestoreceive = packetlen - len(packet) self.log_debug('Trying to receive {} bytes for the second part of the response.'.format(bytestoreceive)) chunk = self.read_bytes(bytestoreceive) self.log_debug('Received {} bytes chunk of response part 2: {}'.format(len(chunk), self.bytes2hexstring(chunk))) packet.extend(chunk) - if len(chunk) == 0: + if len(chunk) == 0: raise Exception('Received 0 bytes chunk - ignoring packet!') except socket.timeout: raise Exception("error receiving second part of packet: timeout") except Exception as e: raise Exception("error receiving second part of packet: {}".format(e)) - + # Send ACK self.send_bytes(self._acknowledge) - + # Parse response self.parse_response(packet) - + except Exception as e: self.disconnect() self.log_err("send_command failed: {}".format(e)) - finally: + finally: # At the end, release the lock self._lock.release() def parse_response(self, response): - #resph = self.bytes2int(response) + # resph = self.bytes2int(response) self.log_debug('Successfully received response: {}'.format(self.bytes2hexstring(response))) # A telegram looks like this: start sequence (2 bytes), command (2 bytes), data length (1 byte), data, checksum (1 byte), end sequence (2 bytes, already cut away) commandcodebytes = response[2:4] - commandcodebytes[1] -= self._reponsecommandinc # The request command of this response is -1 (for comfoair 350) - commandcodebytes.append(0) # Add a data length byte of 0 (always true for read commands) + commandcodebytes[1] -= self._reponsecommandinc # The request command of this response is -1 (for comfoair 350) + commandcodebytes.append(0) # Add a data length byte of 0 (always true for read commands) commandcode = self.bytes2int(commandcodebytes) # Remove begin and checksum to get the data @@ -405,8 +401,8 @@ def parse_response(self, response): # Validate checksum packetpart = bytearray() - packetpart.extend(response[2:5]) # Command and data length - packetpart.extend(databytes) # Decoded data bytes + packetpart.extend(response[2:5]) # Command and data length + packetpart.extend(databytes) # Decoded data bytes checksum = self.calc_checksum(packetpart) receivedchecksum = response[len(response) - len(self._packetend) - 1] if (receivedchecksum != checksum): @@ -437,7 +433,7 @@ def parse_response(self, response): # Extract value valuebytes = databytes[index:index + commandvaluebytes] rawvalue = self.bytes2int(valuebytes) - + # Tranform value value = self.value_transform(rawvalue, commandtype, commandtransform) self.log_debug('Matched command {} and read transformed value {} (raw value was {}) from byte position {} and byte length {}.'.format(commandname, value, rawvalue, commandresppos, commandvaluebytes)) @@ -448,20 +444,24 @@ def parse_response(self, response): self.log_err('Telegram did not contain enough data bytes for the configured command {} to extract a value!'.format(commandname)) def run(self): + # automatically (re)connect + self._sh.connections.monitor(self) + self.alive = True - self._sh.scheduler.add('ComfoAir-init', self.send_init_commands, prio=5, cycle=600, offset=2) + self.scheduler_add('ComfoAir-init', self.send_init_commands, prio=5, cycle=600, offset=2) maxloops = 20 - loops = 0 + loops = 0 while self.alive and not self._initread and loops < maxloops: # wait for init read to finish time.sleep(0.5) loops += 1 - self._sh.scheduler.remove('ComfoAir-init') - + self.scheduler_remove('ComfoAir-init') + def stop(self): - self._sh.scheduler.remove('ComfoAir-cyclic') self.alive = False + self._sh.connections.remove(self) + self.scheduler_remove('ComfoAir-cyclic') self.disconnect() - + def send_init_commands(self): try: # Do the init read commands @@ -471,19 +471,19 @@ def send_init_commands(self): for commandcode in self._init_cmds: commandname = self.commandname_by_commandcode(commandcode) self.send_command(commandname) - + # Find the shortest cycle shortestcycle = -1 for commandname in list(self._cyclic_cmds.keys()): entry = self._cyclic_cmds[commandname] if shortestcycle == -1 or entry['cycle'] < shortestcycle: shortestcycle = entry['cycle'] - + # Start the worker thread if shortestcycle != -1: # Balance unnecessary calls and precision workercycle = int(shortestcycle / 2) - self._sh.scheduler.add('ComfoAir-cyclic', self.handle_cyclic_cmds, cycle=workercycle, prio=5, offset=0) + self.scheduler_add('ComfoAir-cyclic', self.handle_cyclic_cmds, cycle=workercycle, prio=5, offset=0) self.log_info('Added cyclic worker thread ({} sec cycle). Shortest item update cycle found: {} sec.'.format(workercycle, shortestcycle)) finally: self._initread = True @@ -494,30 +494,30 @@ def remove_ack_begin(self, packet): while len(packet) >= acklen and packet[0:acklen] == self._acknowledge: packet = packet[acklen:] return packet - + def calc_checksum(self, packetpart): return (sum(packetpart) + 173) % 256 - - def log_debug(self, text): + + def log_debug(self, text): self.logger.debug('ComfoAir: {}'.format(text)) - def log_info(self, text): + def log_info(self, text): self.logger.info('ComfoAir: {}'.format(text)) - def log_err(self, text): + def log_err(self, text): self.logger.error('ComfoAir: {}'.format(text)) - + def int2bytes(self, value, length): # Limit value to the passed byte length value = value % (2 ** (length * 8)) return value.to_bytes(length, byteorder='big') - + def bytes2int(self, bytesvalue): return int.from_bytes(bytesvalue, byteorder='big', signed=False) - + def bytes2hexstring(self, bytesvalue): return ":".join("{:02x}".format(c) for c in bytesvalue) - + def encode_specialchars(self, packet): specialchar = self._controlset['SpecialCharacter'] encodedpacket = bytearray() @@ -528,9 +528,9 @@ def encode_specialchars(self, packet): # Encoding works by doubling the special char self.log_debug('Encoded special char at position {} of data bytes {}.'.format(count, self.bytes2hexstring(packet))) encodedpacket.append(char) - #self.log_debug('Encoded data bytes: {}.'.format(encodedpacket)) + # self.log_debug('Encoded data bytes: {}.'.format(encodedpacket)) return encodedpacket - + def decode_specialchars(self, packet): specialchar = self._controlset['SpecialCharacter'] decodedpacket = bytearray() @@ -549,7 +549,7 @@ def decode_specialchars(self, packet): # Reset dropping marker specialcharremoved = 0 return decodedpacket - + def value_transform(self, value, commandtype, transformmethod): if transformmethod == 'Temperature': if commandtype == 'Read': @@ -562,7 +562,7 @@ def value_transform(self, value, commandtype, transformmethod): elif commandtype == 'Write': return int(1875000 / value) return value - + def commandname_by_commandcode(self, commandcode): for commandname in self._commandset.keys(): if self._commandset[commandname]['Command'] == commandcode: diff --git a/comfoair/plugin.yaml b/comfoair/plugin.yaml index 806000e79..7438bad8e 100755 --- a/comfoair/plugin.yaml +++ b/comfoair/plugin.yaml @@ -12,11 +12,11 @@ plugin: documentation: https://github.com/smarthomeNG/smarthome/wiki/Comfoair-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/31291-neues-plugin-comfoair-kwl-wohnraumlüftung-zehnder-paul-wernig - version: 1.3.0 # Plugin version + version: 1.3.1 # Plugin version sh_minversion: 1.3 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False - restartable: unknown + multi_instance: false + restartable: true classname: ComfoAir # class containing the plugin parameters: @@ -30,16 +30,19 @@ parameters: valid_list: - comfoair350 - comfoair500 + host: type: ip description: de: 'Netzwerverbindung: Hostname/IP des KWL Systems' en: 'Network connection: Hostname/IP of KWL system' + port: type: int description: de: 'Netzwerkverbindung: Port des KWL Systems' en: 'Network connection: Port of KWL system' + serialport: type: str description: @@ -61,22 +64,22 @@ item_attributes: comfoair_read_afterwrite: type: num description: - de: 'Konfiguriert eine Verzögerung in Sekunden nachdem ein Lesekommando nach einem Schreibkommando an das KWL System geschickt wird.' + de: 'Konfiguriert eine Verzögerung in Sekunden, nachdem ein Lesekommando nach einem Schreibkommando an das KWL System geschickt wird.' en: 'Configures delay in seconds to issue a read command after write command.' comfoair_read_cycle: type: num description: - de: 'Konfiguriert ein Interval in Sekunden für das Lesekommando.' + de: 'Konfiguriert ein Intervall in Sekunden für das Lesekommando.' en: 'Configures a interval in seconds for the read command.' comfoair_init: type: bool description: - de: 'Konfiguriert ob der Wert aus dem KWL System initialisiert werden soll.' + de: 'Konfiguriert, ob der Wert aus dem KWL System initialisiert werden soll.' en: 'Configures to initialize the item value with the value from the KWL system.' comfoair_trigger: type: list(str) description: - de: 'Konfiguriert Lesekommandos die nach einem Schreibvorgang auf das Item aufgerufen werden.' + de: 'Konfiguriert Lesekommandos, die nach einem Schreibvorgang auf das Item aufgerufen werden.' en: 'Configures read commands after an update to the item.' comfoair_trigger_afterwrite: type: num diff --git a/helios/__init__.py b/helios/__init__.py index ff7e14bd7..f8c97d53d 100755 --- a/helios/__init__.py +++ b/helios/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +# !/usr/bin/env python ######################################################################### # Copyright 2014 Marcel Tiews marcel.tiews@gmail.com # Modified 2014-2017 by René Jahncke aka Tom-Bom-badil @ github.com @@ -22,11 +22,8 @@ import sys import serial import logging -import socket import threading -import struct import time -import datetime import array from lib.model.smartplugin import SmartPlugin @@ -34,10 +31,10 @@ # old / removed: logger = logging.getLogger("") # Old CONST's - previous definitions -#CONST_BUSMEMBER__MAINBOARD = 0x11 -#CONST_BUSMEMBER__SLAVEBOARDS = 0x10 -#CONST_BUSMEMBER__CONTROLBOARDS = 0x20 -#CONST_BUSMEMBER__ME = 0x2F +# CONST_BUSMEMBER__MAINBOARD = 0x11 +# CONST_BUSMEMBER__SLAVEBOARDS = 0x10 +# CONST_BUSMEMBER__CONTROLBOARDS = 0x20 +# CONST_BUSMEMBER__ME = 0x2F # Broadcast addresses - no way to address slave boards in the units directly (according to Vallox) @@ -45,10 +42,10 @@ CONST_BUS_ALL_REMOTES = 0x20 # Individual addresses -CONST_BUS_MAINBOARD1 = 0x11 # 1st of max 15 ventilation units (mainboards 1-F) -CONST_BUS_REMOTE1 = 0x21 # 1st of max 15 remote controls (remotes 1-F, default jumper = 1) -CONST_BUS_LON = 0x28 # default for LON bus module (just for information --> expensive) -CONST_BUS_ME = 0x2F # stealth mode - we are behaving like a regular remote control +CONST_BUS_MAINBOARD1 = 0x11 # 1st of max 15 ventilation units (mainboards 1-F) +CONST_BUS_REMOTE1 = 0x21 # 1st of max 15 remote controls (remotes 1-F, default jumper = 1) +CONST_BUS_LON = 0x28 # default for LON bus module (just for information --> expensive) +CONST_BUS_ME = 0x2F # stealth mode - we are behaving like a regular remote control CONST_MAP_VARIABLES_TO_ID = { "power_state" : {"varid" : 0xA3, 'type': 'bit', 'bitposition': 0, 'read': True, 'write': True }, @@ -70,9 +67,9 @@ "boost_status" : {"varid" : 0x71, 'type': 'bit', 'bitposition': 6, 'read': True, 'write': False }, "boost_remaining" : {"varid" : 0x79, 'type': 'dec', 'bitposition': -1, 'read': True, 'write': False }, "fan_in_on_off" : {"varid" : 0x08, 'type': 'bit', 'bitposition': 3, 'read': True, 'write': True }, - "fan_in_percent" : {"varid" : 0xB0, 'type': 'dec', 'bitposition': -1, 'read': True, 'write': True }, + "fan_in_percent" : {"varid" : 0xB0, 'type': 'dec', 'bitposition': -1, 'read': True, 'write': True }, "fan_out_on_off" : {"varid" : 0x08, 'type': 'bit', 'bitposition': 5, 'read': True, 'write': True }, - "fan_out_percent" : {"varid" : 0xB1, 'type': 'dec', 'bitposition': -1, 'read': True, 'write': True }, + "fan_out_percent" : {"varid" : 0xB1, 'type': 'dec', 'bitposition': -1, 'read': True, 'write': True }, "clean_filter" : {"varid" : 0xAB, 'type': 'dec', 'bitposition': -1, 'read': True, 'write': True }, "device_error" : {"varid" : 0x36, 'type': 'dec', 'bitposition': -1, 'read': True, 'write': False } } @@ -96,54 +93,56 @@ class HeliosException(Exception): class HeliosBase(SmartPlugin): - PLUGIN_VERSION = "1.4.2" + PLUGIN_VERSION = "1.4.3" ALLOW_MULTIINSTANCE = False - def __init__(self, tty='/dev/ttyUSB0'): + def __init__(self, **kwargs): self.logger = logging.getLogger(__name__) - self._tty = tty self._is_connected = False - self._port = False self._lock = threading.Lock() - + if 'tty' in kwargs: + self._tty = kwargs['tty'] + if 'port' in kwargs: + self._port = kwargs['port'] + def connect(self): if self._is_connected and self._port: return True - + try: self.logger.debug("Helios: Connecting...") self._port = serial.Serial( - self._tty, - baudrate=9600, - bytesize=serial.EIGHTBITS, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE, + self._tty, + baudrate=9600, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, timeout=1) self._is_connected = True return True except: self.logger.error("Helios: Could not open {0}.".format(self._tty)) return False - + def disconnect(self): if self._is_connected and self._port: self.logger.debug("HeliosBase: Disconnecting...") self._port.close() self._is_connected = False - + def _createTelegram(self, sender, receiver, function, value): - telegram = [1,sender,receiver,function,value,0] + telegram = [1, sender, receiver, function, value, 0] telegram[5] = self._calculateCRC(telegram) return telegram - + def _waitForSilence(self): # Modbus RTU only allows one master (client which controls communication). # So lets try to wait a bit and jump in when nobody's speaking. # Modbus defines a waittime of 3,5 Characters between telegrams: - # (1/9600baud * (1 Start bit + 8 Data bits + 1 Parity bit + 1 Stop bit) + # (1/9600baud * (1 Start bit + 8 Data bits + 1 Parity bit + 1 Stop bit) # => about 4ms # Lets go with 7ms! ;O) - + gotSlot = False backupTimeout = self._port.timeout end = time.time() + 3 @@ -155,128 +154,127 @@ def _waitForSilence(self): gotSlot = True break self._port.timeout = backupTimeout - return gotSlot + return gotSlot def _sendTelegram(self, telegram): if not self._is_connected: return False - + self.logger.debug("Helios: Sending telegram '{0}'".format(self._telegramToString(telegram))) self._port.write(bytearray(telegram)) return True - + def _readTelegram(self, sender, receiver, datapoint): # sometimes a lot of garbage is received...so lets get a bit robust # and read a bit of this junk and see whether we are getting something # useful out of it! # How long does it take until something useful is received??? timeout = time.time() + 1 - telegram = [0,0,0,0,0,0] + telegram = [0, 0, 0, 0, 0, 0] while self._is_connected and timeout > time.time(): char = self._port.read(1) - if(len(char) > 0): + if (len(char) > 0): byte = bytearray(char)[0] telegram.pop(0) telegram.append(byte) # Telegrams always start with a 0x01, is the CRC valid?, ... - if (telegram[0] == 0x01 and - telegram[1] == sender and - telegram[2] == receiver and - telegram[3] == datapoint and - telegram[5] == self._calculateCRC(telegram)): + if (telegram[0] == 0x01 and + telegram[1] == sender and + telegram[2] == receiver and + telegram[3] == datapoint and + telegram[5] == self._calculateCRC(telegram)): self.logger.debug("Telegram received '{0}'".format(self._telegramToString(telegram))) return telegram[4] - return None - + def _calculateCRC(self, telegram): sum = 0 for c in telegram[:-1]: sum = sum + c return sum % 256 - + def _telegramToString(self, telegram): str = "" for c in telegram: # str = str + hex(c) + " " 0x01 was showing as 0x1, 0x1A was showing as 0x1a - str = str + '0x%0*X' % (2,c) + " " - str = str[:-1] # remove trailing space + str = str + '0x%0*X' % (2, c) + " " + str = str[:-1] # remove trailing space return str - + def _convertFromRawValue(self, varname, rawvalue): value = None vardef = CONST_MAP_VARIABLES_TO_ID[varname] - + if vardef["type"] == "temperature": value = CONST_TEMPERATURE[rawvalue] elif vardef["type"] == "fanspeed": if rawvalue == 0x01: value = 1 - elif rawvalue == 0x03: + elif rawvalue == 0x03: value = 2 - elif rawvalue == 0x07: + elif rawvalue == 0x07: value = 3 - elif rawvalue == 0x0F: + elif rawvalue == 0x0F: value = 4 - elif rawvalue == 0x1F: + elif rawvalue == 0x1F: value = 5 - elif rawvalue == 0x3F: + elif rawvalue == 0x3F: value = 6 - elif rawvalue == 0x7F: + elif rawvalue == 0x7F: value = 7 - elif rawvalue == 0xFF: + elif rawvalue == 0xFF: value = 8 else: value = None elif vardef["type"] == "bit": value = rawvalue >> vardef["bitposition"] & 0x01 - elif vardef["type"] == "dec": # decimal value + elif vardef["type"] == "dec": # decimal value value = rawvalue - - return value + + return value def _convertFromValue(self, varname, value, prevvalue): rawvalue = None vardef = CONST_MAP_VARIABLES_TO_ID[varname] - + if vardef['type'] == "temperature": rawvalue = CONST_TEMPERATURE.index(int(value)) elif vardef["type"] == "fanspeed": value = int(value) if value == 1: rawvalue = 0x01 - elif value == 2: + elif value == 2: rawvalue = 0x03 - elif value == 3: + elif value == 3: rawvalue = 0x07 - elif value == 4: + elif value == 4: rawvalue = 0x0F - elif value == 5: + elif value == 5: rawvalue = 0x1F - elif value == 6: + elif value == 6: rawvalue = 0x3F - elif value == 7: + elif value == 7: rawvalue = 0x7F - elif value == 8: + elif value == 8: rawvalue = 0xFF else: rawvalue = None elif vardef["type"] == "bit": # for bits we have to keep the other bits of the byte (previous value) - if value in (True,1,"true","True","1","On","on"): + if value in (True, 1, "true", "True", "1", "On", "on"): rawvalue = prevvalue | (1 << vardef["bitposition"]) else: rawvalue = prevvalue & ~(1 << vardef["bitposition"]) - elif vardef["type"] == "dec": # decimal value + elif vardef["type"] == "dec": # decimal value rawvalue = int(value) - - return rawvalue - - def writeValue(self,varname, value): - if CONST_MAP_VARIABLES_TO_ID[varname]["write"] != True: + + return rawvalue + + def writeValue(self, varname, value): + if CONST_MAP_VARIABLES_TO_ID[varname]["write"] is not True: self.logger.error("Helios: Variable {0} may not be written!".format(varname)) - return False + return False success = False - + self._lock.acquire() try: # if we have got to write a single bit, we need the current (byte) value to @@ -287,43 +285,42 @@ def writeValue(self,varname, value): # Send poll request telegram = self._createTelegram( CONST_BUS_ME, - CONST_BUS_MAINBOARD1, - 0, + CONST_BUS_MAINBOARD1, + 0, CONST_MAP_VARIABLES_TO_ID[varname]["varid"] ) self._sendTelegram(telegram) # Read response currentval = self._readTelegram( - CONST_BUS_MAINBOARD1, - CONST_BUS_ME, + CONST_BUS_MAINBOARD1, + CONST_BUS_ME, CONST_MAP_VARIABLES_TO_ID[varname]["varid"] ) - if currentval == None: - self.logger.error("Helios: Sending value to ventilation system failed. Can not read current variable value '{0}'." - .format(varname)) + if currentval is None: + self.logger.error("Helios: Sending value to ventilation system failed. Can not read current variable value '{0}'.".format(varname)) return False rawvalue = self._convertFromValue(varname, value, currentval) - else: + else: rawvalue = self._convertFromValue(varname, value, None) - - # send the new value + + # send the new value if self._waitForSilence(): - if rawvalue != None: + if rawvalue is not None: # Broadcasting value to all remote control boards telegram = self._createTelegram( CONST_BUS_ME, - CONST_BUS_ALL_REMOTES, - CONST_MAP_VARIABLES_TO_ID[varname]["varid"], + CONST_BUS_ALL_REMOTES, + CONST_MAP_VARIABLES_TO_ID[varname]["varid"], rawvalue ) self._sendTelegram(telegram) - + # Broadcasting value to all mainboards telegram = self._createTelegram( CONST_BUS_ME, - CONST_BUS_ALL_MAINBOARDS, - CONST_MAP_VARIABLES_TO_ID[varname]["varid"], + CONST_BUS_ALL_MAINBOARDS, + CONST_MAP_VARIABLES_TO_ID[varname]["varid"], rawvalue ) self._sendTelegram(telegram) @@ -331,75 +328,75 @@ def writeValue(self,varname, value): # Writing value to 1st mainboard telegram = self._createTelegram( CONST_BUS_ME, - CONST_BUS_MAINBOARD1, - CONST_MAP_VARIABLES_TO_ID[varname]["varid"], - rawvalue + CONST_BUS_MAINBOARD1, + CONST_MAP_VARIABLES_TO_ID[varname]["varid"], + rawvalue ) self._sendTelegram(telegram) - + # Send checksum a second time self._sendTelegram([telegram[5]]) -#################### Special treatment to switch on remote controls after off state: + # Special treatment to switch on remote controls after off state: + # TODO: doesn't work so far if CONST_MAP_VARIABLES_TO_ID[varname]["varid"] == 0xA3 and CONST_MAP_VARIABLES_TO_ID[varname]["bitposition"] == 0: - self.logger.debug("On/off command - special treatment for the remote controls") + self.logger.debug("On/off command - special treatment for the remote controls") telegram = self._createTelegram( CONST_BUS_ME, - CONST_BUS_ALL_REMOTES, - CONST_MAP_VARIABLES_TO_ID[varname]["varid"], - rawvalue + CONST_BUS_ALL_REMOTES, + CONST_MAP_VARIABLES_TO_ID[varname]["varid"], + rawvalue ) self._sendTelegram(telegram) telegram = self._createTelegram( CONST_BUS_ME, - CONST_BUS_REMOTE1, - CONST_MAP_VARIABLES_TO_ID[varname]["varid"], - rawvalue + CONST_BUS_REMOTE1, + CONST_MAP_VARIABLES_TO_ID[varname]["varid"], + rawvalue ) self._sendTelegram(telegram) self._sendTelegram([telegram[5]]) -#################### Doesn't work so far + # Doesn't work so far success = True - + else: - self.logger.error("Helios: Sending value to ventilation system failed. Can not convert value '{0}' for variable '{1}'." - .format(value,varname)) + self.logger.error("Helios: Sending value to ventilation system failed. Can not convert value '{0}' for variable '{1}'.".format(value,varname)) success = False else: self.logger.error("Helios: Sending value to ventilation system failed. No free slot for sending telegrams available.") success = False except Exception as e: - self.logger.error("Helios: Exception in writeValue() occurred: {0}".format(e)) + self.logger.error("Helios: Exception in writeValue() occurred: {0}".format(e)) finally: self._lock.release() - + return success - - def readValue(self,varname): - if CONST_MAP_VARIABLES_TO_ID[varname]["read"] != True: + + def readValue(self, varname): + if CONST_MAP_VARIABLES_TO_ID[varname]["read"] is not True: self.logger.error("Variable {0} may not be read!".format(varname)) return False value = None - + self._lock.acquire() try: - self.logger.debug("Helios: Reading value: {0}".format(varname)) + self.logger.debug("Helios: Reading value: {0}".format(varname)) if self._waitForSilence(): # Send poll request telegram = self._createTelegram( CONST_BUS_ME, - CONST_BUS_MAINBOARD1, - 0, + CONST_BUS_MAINBOARD1, + 0, CONST_MAP_VARIABLES_TO_ID[varname]["varid"] ) self._sendTelegram(telegram) # Read response value = self._readTelegram( - CONST_BUS_MAINBOARD1, - CONST_BUS_ME, + CONST_BUS_MAINBOARD1, + CONST_BUS_ME, CONST_MAP_VARIABLES_TO_ID[varname]["varid"] ) if value is not None: @@ -408,36 +405,37 @@ def readValue(self,varname): self.logger.debug("Value for {0} ({1}) received: {2}|{3}|{4} --> converted = {5}" .format(varname, '0x%0*X' % (2, CONST_MAP_VARIABLES_TO_ID[varname]["varid"]), '0x%0*X' % (2,raw_value), "{0:08b}".format(raw_value), raw_value, value) - ) + ) else: # logging as info only, so we stop spamming log file as some noise on the bus seems to be normal - self.logger.info("Helios: No valid value for '{0}' from ventilation system received." - .format(varname) - ) + self.logger.info("Helios: No valid value for '{0}' from ventilation system received.".format(varname) + ) else: self.logger.warning("Helios: Reading value from ventilation system failed. No free slot to send poll request available.") except Exception as e: - self.logger.error("Helios: Exception in readValue() occurred: {0}".format(e)) + self.logger.error("Helios: Exception in readValue() occurred: {0}".format(e)) finally: self._lock.release() - + return value - -class Helios(HeliosBase): + +class Helios(HeliosBase): _items = {} - - def __init__(self, smarthome, tty, cycle=300): - HeliosBase.__init__(self, tty) - self._sh = smarthome - self._cycle = int(cycle) + + def __init__(self): + self._tty = self.get_parameter_value('tty') + self._cycle = self.get_parameter_value('cycle') + self._port = None + super().__init__() self._alive = False - + def run(self): self.connect() self._alive = True self._sh.scheduler.add('Helios', self._update, cycle=self._cycle) def stop(self): + self._sh.scheduler.remove('Helios') self.disconnect() self._alive = False @@ -449,62 +447,65 @@ def parse_item(self, item): return self.update_item else: self.logger.warning("Helios: Ignoring unknown variable '{0}'".format(varname)) - + def update_item(self, item, caller=None, source=None, dest=None): if caller != 'Helios': - self.writeValue(item.conf['helios_var'], item()) - + self.writeValue(item.conf['helios_var'], item()) + def _update(self): self.logger.debug("Helios: Updating values") for var in self._items.keys(): val = self.readValue(var) - if val != None: - self._items[var](val,"Helios") + if val is not None: + self._items[var](val, "Helios") + - def main(): - import argparse - + import argparse + parser = argparse.ArgumentParser( - description="Helios ventilation system commandline interface.", - epilog="Without arguments all readable values using default tty will be retrieved.", - argument_default=argparse.SUPPRESS) - parser.add_argument("-t", "--tty", dest="port", default="/dev/ttyUSB0", help="Serial device to use") + description="Helios ventilation system commandline interface.", + epilog="Without arguments all readable values using default tty will be retrieved.", + argument_default=argparse.SUPPRESS + ) + parser.add_argument("-t", "--tty", dest="tty", default="/dev/ttyUSB0", help="Serial device to use") parser.add_argument("-r", "--read", dest="read_var", help="Read variables from ventilation system") parser.add_argument("-w", "--write", dest="write_var", help="Write variable to ventilation system") parser.add_argument("-v", "--value", dest="value", help="Value to write (required with option -v)") parser.add_argument("-d", "--debug", dest="enable_debug", action="store_true", help="Prints debug statements.") args = vars(parser.parse_args()) - + if "write_var" in args.keys() and "value" not in args.keys(): parser.print_usage() return + logger = logging.getLogger() logger.setLevel(logging.DEBUG) # old log version -# ch = logging.StreamHandler() -# if "enable_debug" in args.keys(): -# ch.setLevel(logging.DEBUG) -# else: -# ch.setLevel(logging.INFO) -# formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") -# ch.setFormatter(formatter) -# logger.addHandler(ch) +# ch = logging.StreamHandler() +# if "enable_debug" in args.keys(): +# ch.setLevel(logging.DEBUG) +# else: +# ch.setLevel(logging.INFO) +# formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +# ch.setFormatter(formatter) +# logger.addHandler(ch) + helios = None try: - helios = HeliosBase(args["port"]) + helios = HeliosBase(tty=args["tty"]) helios.connect() if not helios._is_connected: raise Exception("Not connected") - + if "read_var" in args.keys(): - print("{0} = {1}".format(args["read_var"],helios.readValue(args["read_var"]))) + print("{0} = {1}".format(args["read_var"], helios.readValue(args["read_var"]))) elif "write_var" in args.keys(): helios.writeValue(args["write_var"],args["value"]) else: for var in CONST_MAP_VARIABLES_TO_ID.keys(): - print("{0} = {1}".format(var,helios.readValue(var))) + print("{0} = {1}".format(var, helios.readValue(var))) except Exception as e: print("Exception: {0}".format(e)) return 1 @@ -512,5 +513,6 @@ def main(): if helios: helios.disconnect() + if __name__ == "__main__": - sys.exit(main()) + sys.exit(main()) diff --git a/helios/plugin.yaml b/helios/plugin.yaml index 457c49c97..2a643b9ed 100755 --- a/helios/plugin.yaml +++ b/helios/plugin.yaml @@ -1,3 +1,6 @@ +%YAML 1.1 +--- + plugin: type: interface description: @@ -9,10 +12,10 @@ plugin: keywords: 'helios vallox ventilation' documentation: https://github.com/Tom-Bom-badil/helios/wiki support: https://knx-user-forum.de/forum/supportforen/smarthome-py/40092-erweiterung-helios-vallox-plugin - version: 1.4.2 - sh_minversion: 1.1 - multi_instance: False - restartable: unknown + version: 1.4.3 + sh_minversion: 1.6 + multi_instance: false + restartable: true classname: 'Helios' parameters: diff --git a/intercom_2n/__init__.py b/intercom_2n/__init__.py index 37fba4268..a859fe667 100755 --- a/intercom_2n/__init__.py +++ b/intercom_2n/__init__.py @@ -31,17 +31,22 @@ requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + class Intercom2n(SmartPlugin): - PLUGIN_VERSION = "1.3.0.1" + PLUGIN_VERSION = "1.3.1" ALLOW_MULTIINSTANCE = False - def __init__(self, sh, intercom_ip, ssl=False, auth_type=0, username=None, password=None): - self._sh = sh + def __init__(self): + self._intercom_ip = self.get_parameter_value('intercom_ip') + self._ssl = self.get_parameter_value('ssl') + self._auth_type = self.get_parameter_value('auth_type') + self._username = self.get_parameter_value('username') + self._password = self.get_parameter_value('password') self.is_stopped = False self.sid = None self._logger = logging.getLogger(__name__) self.event_timeout = 30 - self.ip_cam = IPCam(intercom_ip, ssl=ssl, auth_type=auth_type, user=username, password=password) + self.ip_cam = IPCam(self._intercom_ip, ssl=self._ssl, auth_type=self._auth_type, user=self._username, password=self._password) # item dictionaries for events self.possible_events = [ @@ -104,7 +109,7 @@ def get_events(self): def parse_event_data(self, raw_data): try: - raw_data =json.loads(raw_data) + raw_data = json.loads(raw_data) except Exception: self._logger.warning("Unknown 2n_event: '{event}' not in dictionary format.".format(event=raw_data)) return @@ -231,8 +236,8 @@ def update_item(self, item, caller=None, source=None, dest=None): elif command == 'firmware_upload': # check for child item firmware_filepath child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "firmware_file": if os.path.exists(child_item()): parent_item(self.ip_cam.commands.firmware_upload(child_item())) @@ -241,16 +246,16 @@ def update_item(self, item, caller=None, source=None, dest=None): elif command == 'config_get': # check for child item config_file child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "config_file": parent_item(self.ip_cam.commands.config_get(filename=child_item())) break elif command == 'config_upload': # check for child item firmware_filepath child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "config_file": if os.path.exists(child_item()): parent_item(self.ip_cam.commands.config_upload(child_item())) @@ -261,8 +266,8 @@ def update_item(self, item, caller=None, source=None, dest=None): elif command == 'switch_status': switch = None child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "switch": switch = child_item() break @@ -272,8 +277,8 @@ def update_item(self, item, caller=None, source=None, dest=None): action = None response = None child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "switch": switch = child_item() elif path == "action": @@ -286,8 +291,8 @@ def update_item(self, item, caller=None, source=None, dest=None): elif command == 'io_caps': port = None child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "port": port = child_item() break @@ -295,8 +300,8 @@ def update_item(self, item, caller=None, source=None, dest=None): elif command == 'io_status': port = None child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "port": port = child_item() break @@ -306,8 +311,8 @@ def update_item(self, item, caller=None, source=None, dest=None): action = None response = None child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "port": port = child_item() elif path == "action": @@ -320,50 +325,50 @@ def update_item(self, item, caller=None, source=None, dest=None): elif command == 'phone_status': account = None child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "account": account = child_item() break parent_item(self.ip_cam.commands.phone_status(account)) elif command == 'call_status': - session = None - child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') - if path == "session": - session = child_item() - break - parent_item(self.ip_cam.commands.call_status(session)) + session = None + child_items = parent_item.return_children() + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') + if path == "session": + session = child_item() + break + parent_item(self.ip_cam.commands.call_status(session)) elif command == 'call_dial': - number = None - child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') - if path == "number": - number = child_item() - break - parent_item(self.ip_cam.commands.call_dial(number)) + number = None + child_items = parent_item.return_children() + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') + if path == "number": + number = child_item() + break + parent_item(self.ip_cam.commands.call_dial(number)) elif command == 'call_answer': - session = None - child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') - if path == "session": - session = child_item() - break - parent_item(self.ip_cam.commands.call_answer(session)) + session = None + child_items = parent_item.return_children() + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') + if path == "session": + session = child_item() + break + parent_item(self.ip_cam.commands.call_answer(session)) elif command == 'call_hangup': - session = None - reason = None - child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') - if path == "session": - session = child_item() - if path == "reason": - reason = child_item() - parent_item(self.ip_cam.commands.call_hangup(session, reason)) + session = None + reason = None + child_items = parent_item.return_children() + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') + if path == "session": + session = child_item() + if path == "reason": + reason = child_item() + parent_item(self.ip_cam.commands.call_hangup(session, reason)) elif command == 'camera_caps': parent_item(self.ip_cam.commands.camera_caps()) elif command == 'camera_snapshot': @@ -373,8 +378,8 @@ def update_item(self, item, caller=None, source=None, dest=None): source = None time = None child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "snapshot_file": snapshot_file = child_item() if path == "width": @@ -384,7 +389,7 @@ def update_item(self, item, caller=None, source=None, dest=None): if path == "source": source = child_item() if path == "time": - time == child_item() + time = child_item() parent_item(self.ip_cam.commands.camera_snapshot(width, height, snapshot_file, source, time)) elif command == 'display_caps': parent_item(self.ip_cam.commands.display_caps()) @@ -392,22 +397,22 @@ def update_item(self, item, caller=None, source=None, dest=None): gif_file = None display = None child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "gif_file": gif_file = child_item() if path == "display": display = child_item() parent_item(self.ip_cam.commands.display_upload_image(display, gif_file)) elif command == 'display_delete_image': - display = None - child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') - if path == "display": - display = child_item() - break - parent_item(self.ip_cam.commands.display_delete_image(display)) + display = None + child_items = parent_item.return_children() + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') + if path == "display": + display = child_item() + break + parent_item(self.ip_cam.commands.display_delete_image(display)) elif command == 'log_caps': parent_item(self.ip_cam.commands.log_caps()) elif command == 'audio_test': @@ -421,8 +426,8 @@ def update_item(self, item, caller=None, source=None, dest=None): picture_count = None timespan = None child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "to": to = child_item() if path == "width": @@ -441,8 +446,8 @@ def update_item(self, item, caller=None, source=None, dest=None): elif command == 'pcap': pcap_file = None child_items = parent_item.return_children() - for child_item in child_items: - path = child_item._name.replace(parent_item._name,'').lstrip('.') + for child_item in child_items: + path = child_item._name.replace(parent_item._name, '').lstrip('.') if path == "pcap_file": pcap_file = child_item() break diff --git a/intercom_2n/plugin.yaml b/intercom_2n/plugin.yaml index 469911e88..996c54c90 100755 --- a/intercom_2n/plugin.yaml +++ b/intercom_2n/plugin.yaml @@ -12,27 +12,28 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1030539-plugin-2n-intercom - version: 1.3.0.1 # Plugin version - sh_minversion: 1.3 # minimum shNG version to use this plugin + version: 1.3.1 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False # plugin supports multi instance - restartable: unknown + multi_instance: false # plugin supports multi instance + restartable: false classname: Intercom2n # class containing the plugin parameters: # Definition of parameters to be configured in etc/plugin.yaml -# def __init__(self, sh, intercom_ip, ssl=False, auth_type=0, username=None, password=None): intercom_ip: type: ip description: de: "Intercom IP Adresse" en: "IP address of Intercom" + ssl: type: bool default: False description: de: "ssl Verschlüsselung verwenden" en: "Use ssl encryption" + auth_type: type: int default: 0 @@ -41,12 +42,14 @@ parameters: description: de: "Zu verwendender auth_type (0: no authentication, 1: Basic Authentication, 2: Digest Authentication)" en: "auth_type to use (0: no authentication, 1: Basic Authentication, 2: Digest Authentication)" + username: type: str default: None* description: de: "Benutzername, falls auth_type > 0" en: "Usernam, falls auth_type > 0" + password: type: str default: None* diff --git a/jvcproj/__init__.py b/jvcproj/__init__.py index f7f3d5268..c907cc622 100755 --- a/jvcproj/__init__.py +++ b/jvcproj/__init__.py @@ -97,10 +97,9 @@ class JVC_DILA_Control(SmartPlugin): the update functions for the items """ ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION='1.0.1' + PLUGIN_VERSION = '1.0.2' - - def __init__(self, smarthome, host='0.0.0.0', gammaconf_dir='/usr/local/smarthome/etc/jvcproj/'): + def __init__(self): """ Initalizes the plugin. The parameters describe for this method are pulled from the entry in plugin.conf. :param host: JVC DILA Projectors IP address @@ -108,10 +107,9 @@ def __init__(self, smarthome, host='0.0.0.0', gammaconf_dir='/usr/local/smarthom :port is fixed to 20554 """ super().__init__() - self.logger = logging.getLogger(__name__) - self._sh=smarthome - self.host_port = (host, 20554) - self.gammaconf_dir = gammaconf_dir + self.host = self.get_parameter_value('host') + self.gammaconf_dir = self.get_parameter_value('gammaconf_dir') + self.host_port = (self.host, 20554) self.logger.debug("Plugin '{}': configured for host: '{}'".format(self.get_fullname(), self.host_port)) def run(self): @@ -164,7 +162,7 @@ def update_item(self, item, caller=None, source=None, dest=None): :param source: if given it represents the source :param dest: if given it represents the dest """ - if item(): + if item() and self.alive: if self.has_iattr(item.conf, 'jvcproj_cmd'): if self.get_iattr_value(item.conf, 'jvcproj_cmd') == 'None': self.logger.debug("Plugin '{}': no command given for update_item '{}'. Please check jvcproj_cmd!" @@ -184,17 +182,16 @@ def update_item(self, item, caller=None, source=None, dest=None): .format(self.get_fullname(), item, caller, source, dest)) self.check_gamma_cmd(item) - def check_gamma_cmd(self, item): """check gamma options to import new gammatable""" self.logger.debug("Plugin '{}': checking for gamma.conf an correct gamma input (must a custom gammatable) in '{}' : '{}'." .format(self.get_fullname(), item, self.get_iattr_value(item.conf, 'jvcproj_gamma'))) - _checklist = (self.get_iattr_value(item.conf, 'jvcproj_gamma').replace(' ','')).split('|') + _checklist = (self.get_iattr_value(item.conf, 'jvcproj_gamma').replace(' ', '')).split('|') if len(_checklist) != 2: self.logger.debug("Plugin '{}': ERROR! Item:'{}': exactly two arguments (file and custom gamma table) must be given!" .format(self.get_fullname(), item)) return - _cmdlist=[] + _cmdlist = [] if self.gammaconf_dir[-1] != '/': self.gammaconf_dir = self.gammaconf_dir + '/' if os_path.isfile(self.gammaconf_dir + _checklist[0]) is False: @@ -243,7 +240,7 @@ def load_table(self, data): def check_gammadata(self, table): """Check gamma data from file""" gammadata = table - if gammadata is None : + if gammadata is None: return None if len(gammadata) == 256: gammadata = [gammadata, gammadata, gammadata] @@ -268,12 +265,11 @@ def le16_split(self, colortable): yield val % 256 yield int(val / 256) - def check_cmd(self, item): """create command list and execute low level string validation for each command""" self.logger.debug("Plugin '{}': create commandlist for item '{}' : '{}' and check command(s)." .format(self.get_fullname(), item, self.get_iattr_value(item.conf, 'jvcproj_cmd'))) - _checklist = (self.get_iattr_value(item.conf, 'jvcproj_cmd').replace(' ','')).split('|') + _checklist = (self.get_iattr_value(item.conf, 'jvcproj_cmd').replace(' ', '')).split('|') _cmdlist = [] for _cmd in _checklist: if _cmd.upper()[2:6] == UNIT_ID and _cmd.upper()[-2:] == END: @@ -297,7 +293,7 @@ def handleconn_op(self, cmdlist): """handle connection and sending commands for jvcproj_cmd""" self.connect() for cmd in cmdlist: - if cmd[:2] == REQ: ##maybe in the future?? + if cmd[:2] == REQ: # maybe in the future?? self.logger.debug("Plugin '{}': WARNING! A request is not yet supported!" .format(self.get_fullname())) elif cmd[:2] == OPE: @@ -308,7 +304,6 @@ def handleconn_op(self, cmdlist): .format(self.get_fullname(), cmd)) self.disconnect('finished! Now disconnecting!') - def connect(self): """Open network connection to projector and perform handshake""" self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -330,13 +325,13 @@ def set(self, cmd): self.send(binascii.a2b_hex(cmd)) self.expect(binascii.a2b_hex(ACK + cmd[2:10] + END)) except Timeout: - self.disconnect('ERROR! Command not acknowledged! Aborting!') - raise CommandNack('Command not acknowledged', cmd) + self.disconnect('ERROR! Command not acknowledged! Aborting!') + raise CommandNack('Command not acknowledged', cmd) def get(self): pass - def disconnect(self, message ='disconnecting...'): + def disconnect(self, message='disconnecting...'): """Close socket""" self.logger.debug("Plugin '{}': {}" .format(self.get_fullname(), message)) @@ -344,6 +339,8 @@ def disconnect(self, message ='disconnecting...'): def send(self, data): """Send data with optional""" + if not self.alive: + return try: self.socket.send(data) except ConnectionAbortedError as err: diff --git a/jvcproj/plugin.yaml b/jvcproj/plugin.yaml index b584191e5..9305f9815 100755 --- a/jvcproj/plugin.yaml +++ b/jvcproj/plugin.yaml @@ -10,11 +10,11 @@ plugin: state: ready support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1188479-plugin-steuerung-von-jvc-d-ila-projektoren - version: 1.0.1 # Plugin version - sh_minversion: 1.3 # minimum shNG version to use this plugin + version: 1.0.2 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False # plugin supports multi instance - restartable: unknown + multi_instance: false # plugin supports multi instance + restartable: true classname: JVC_DILA_Control # class containing the plugin parameters: diff --git a/kostal/__init__.py b/kostal/__init__.py index b2d00cfcc..f503e0104 100755 --- a/kostal/__init__.py +++ b/kostal/__init__.py @@ -21,13 +21,13 @@ # ######################################################################### -import logging from lib.model.smartplugin import SmartPlugin -from lib.utils import Utils -import urllib.request, json +import urllib.request +import json import time import re + class Kostal(SmartPlugin): """ Since UI-version 6 the inverter can answere requests with json. @@ -37,36 +37,36 @@ class Kostal(SmartPlugin): """ ALLOW_MULTIINSTANCE = True - PLUGIN_VERSION = "1.3.2" + PLUGIN_VERSION = "1.3.3" _key2json = { - 'operation_status' : 16780032, - 'dctot_w' : 33556736, - 'dc1_v' : 33555202, - 'dc1_a' : 33555201, - 'dc1_w' : 33555203, - 'dc2_v' : 33555458, - 'dc2_a' : 33555457, - 'dc2_w' : 33555459, - 'dc3_v' : 33555714, - 'dc3_a' : 33555713, - 'dc3_w' : 33555715, - 'actot_w' : 67109120, - 'actot_Hz' : 67110400, - 'actot_cos' : 67110656, - 'actot_limitation' : 67110144, - 'ac1_v' : 67109378, - 'ac1_a' : 67109377, - 'ac1_w' : 67109379, - 'ac2_v' : 67109634, - 'ac2_a' : 67109633, - 'ac2_w' : 67109635, - 'ac3_v' : 67109890, - 'ac3_a' : 67109889, - 'ac3_w' : 67109891, - 'yield_day_kwh' : 251658754, - 'yield_tot_kwh' : 251658753, - 'operationtime_h' : 251658496 + 'operation_status': 16780032, + 'dctot_w': 33556736, + 'dc1_v': 33555202, + 'dc1_a': 33555201, + 'dc1_w': 33555203, + 'dc2_v': 33555458, + 'dc2_a': 33555457, + 'dc2_w': 33555459, + 'dc3_v': 33555714, + 'dc3_a': 33555713, + 'dc3_w': 33555715, + 'actot_w': 67109120, + 'actot_Hz': 67110400, + 'actot_cos': 67110656, + 'actot_limitation': 67110144, + 'ac1_v': 67109378, + 'ac1_a': 67109377, + 'ac1_w': 67109379, + 'ac2_v': 67109634, + 'ac2_a': 67109633, + 'ac2_w': 67109635, + 'ac3_v': 67109890, + 'ac3_a': 67109889, + 'ac3_w': 67109891, + 'yield_day_kwh': 251658754, + 'yield_tot_kwh': 251658753, + 'operationtime_h': 251658496 } _key2td = { 'actot_w': 9, @@ -105,25 +105,19 @@ class Kostal(SmartPlugin): 'l3_watt': 'ac3_w' } - def __init__(self, sh, ip, user="pvserver", passwd="pvwr",cycle=300, datastructure="html"): - self._sh = sh - self.logger = logging.getLogger(__name__) + def __init__(self): + self.ip = self.get_parameter_value('ip') + self.user = self.get_parameter_value('user') + self.passwd = self.get_parameter_value('passwd') + self.cycle = self.get_parameter_value('cycle') + self.datastructure_param = self.get_parameter_value('datastructure') self.logger.info('Init Kostal plugin') - self.user = user - self.passwd = passwd - self.cycle = int(cycle) self._items = {} - if Utils.is_ip(ip): - self.ip = ip - else: - self.logger.error(str(ip) + " is not a valid IP") - if datastructure == "html": + if self.datastructure_param == "html": self._keytable = self._key2td - #self.datastructure = "html" self.datastructure = self._html else: self._keytable = self._key2json - #self.datastructure = "json" self.datastructure = self._json def run(self): @@ -132,13 +126,14 @@ def run(self): """ self.logger.debug("run method Kostal called") self.alive = True - self._sh.scheduler.add('Kostal', self._refresh, cycle=self.cycle) + self.scheduler_add('Kostal', self._refresh, cycle=self.cycle) def stop(self): """ Stop method for the plugin """ self.logger.debug("stop method Kostal called") + self.scheduler_remove('Kostal') self.alive = False def parse_item(self, item): @@ -155,21 +150,8 @@ def parse_item(self, item): self._items[setting] = item return self.update_item - def parse_logic(self, logic): - pass - - def update_item(self, item, caller=None, source=None, dest=None): - """ - Write items values - :param item: item to be updated towards the plugin - :param caller: if given it represents the callers name - :param source: if given it represents the source - :param dest: if given it represents the dest - """ - pass - def _html(self): - #HTML-OLD-Coding + # HTML-OLD-Coding try: data = self._sh.tools.fetch_url( 'http://' + self.ip + '/', self.user, self.passwd, timeout=2).decode() @@ -189,18 +171,18 @@ def _html(self): return def _json(self): - #NEW-JSON-Coding + # NEW-JSON-Coding try: # generate url; fetching only needed elements kostalurl = 'http://' + self.ip + '/api/dxs.json?sessionid=SmartHomeNG' for item in self._items: value = self._keytable[item] - kostalurl +='&dxsEntries=' + str(value) + kostalurl += '&dxsEntries=' + str(value) with urllib.request.urlopen(kostalurl) as url: data = json.loads(url.read().decode()) for values in data['dxsEntries']: kostal_key = str(list(self._keytable.keys())[list(self._keytable.values()).index(values['dxsId'])]) - value=values['value'] + value = values['value'] if kostal_key == "operation_status": self.logger.debug("operation_status" + str(value)) if str(value) == "0": @@ -214,16 +196,18 @@ def _json(self): else: value = "unknown" if kostal_key == "yield_day_kwh": - value = value / 1000 + value = float(value) / 1000 if kostal_key in self._items: self._items[kostal_key](value) - self.logger.debug("items[" + str(kostal_key) +"] = " +str(value)) + self.logger.debug("items[" + str(kostal_key) + "] = " + str(value)) except Exception as e: self.logger.error( 'could not retrieve data from {0}: {1}'.format(self.ip, e)) return def _refresh(self): + if not self.alive: + return start = time.time() # run the working methods self.datastructure() diff --git a/kostal/plugin.yaml b/kostal/plugin.yaml index 24d43000e..4e0d5ea70 100755 --- a/kostal/plugin.yaml +++ b/kostal/plugin.yaml @@ -27,11 +27,11 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1109697-kostal-plugin-piko-wechselrichter - version: 1.3.2 # Plugin version - sh_minversion: 1.3 # minimum shNG version to use this plugin + version: 1.3.3 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: True # plugin supports multi instance - restartable: unknown + multi_instance: true # plugin supports multi instance + restartable: true classname: Kostal # class containing the plugin parameters: @@ -41,7 +41,7 @@ parameters: ip: type: ip - mandatory: True + mandatory: true description: de: "IP Adresse des Konverters" en: "IP address of converter" diff --git a/luxtronic2/__init__.py b/luxtronic2/__init__.py index 13cf7b5e5..3116f4e3f 100755 --- a/luxtronic2/__init__.py +++ b/luxtronic2/__init__.py @@ -30,6 +30,25 @@ from lib.model.smartplugin import SmartPlugin +MODES = { + 0: 'Heizbetrieb', + 1: 'Keine Anforderung', + 2: 'Netz- Einschaltverzoegerung', + 3: 'SSP Zeit', + 4: 'Sperrzeit', + 5: 'Brauchwasser', + 6: 'Estrich Programm', + 7: 'Abtauen', + 8: 'Pumpenvorlauf', + 9: 'Thermische Desinfektion', + 10: 'Kuehlbetrieb', + 12: 'Schwimmbad', + 13: 'Heizen Ext.', + 14: 'Brauchwasser Ext.', + 16: 'Durchflussueberwachung', + 17: 'ZWE Betrieb' +} + class luxex(Exception): pass @@ -233,7 +252,7 @@ def refresh_calculated(self): class Luxtronic2(LuxBase): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = '1.3.2' + PLUGIN_VERSION = '1.3.3' _parameter = {} _attribute = {} @@ -241,21 +260,22 @@ class Luxtronic2(LuxBase): _decoded = {} alive = True - def __init__(self, smarthome, host, port=8888, cycle=300): - LuxBase.__init__(self, host, port) - self._sh = smarthome - self._cycle = int(cycle) + def __init__(self): + self._is_connected = False + self._cycle = self.get_parameter_value('cycle') + LuxBase.__init__(self, self.get_parameter_value('host'), self.get_parameter_value('port')) self.connect() def run(self): self.alive = True - self._sh.scheduler.add('Luxtronic2', self._refresh, cycle=self._cycle) + self.scheduler_add('Luxtronic2', self._refresh, cycle=self._cycle) def stop(self): self.alive = False + self.scheduler_remove('Luxtronic2') def _refresh(self): - if not self.is_connected: + if not self.is_connected or not self.alive: return start = time.time() if len(self._parameter) > 0: @@ -285,54 +305,8 @@ def _refresh(self): def _decode(self, identifier, value): if identifier == 119: - if value == 0: - return 'Heizbetrieb' - if value == 1: - return 'Keine Anforderung' - if value == 2: - return 'Netz- Einschaltverzoegerung' - if value == 3: - return 'SSP Zeit' - if value == 4: - return 'Sperrzeit' - if value == 5: - return 'Brauchwasser' - if value == 6: - return 'Estrich Programm' - if value == 7: - return 'Abtauen' - if value == 8: - return 'Pumpenvorlauf' - if value == 9: - return 'Thermische Desinfektion' - if value == 10: - return 'Kuehlbetrieb' - if value == 12: - return 'Schwimmbad' - if value == 13: - return 'Heizen Ext.' - if value == 14: - return 'Brauchwasser Ext.' - if value == 16: - return 'Durchflussueberwachung' - if value == 17: - return 'ZWE Betrieb' - return '???' - if identifier == 10: - return float(value) / 10 - if identifier == 11: - return float(value) / 10 - if identifier == 12: - return float(value) / 10 - if identifier == 15: - return float(value) / 10 - if identifier == 19: - return float(value) / 10 - if identifier == 20: - return float(value) / 10 - if identifier == 151: - return float(value) / 10 - if identifier == 152: + return MODES.get(value, '???') + if identifier in (10, 11, 12, 15, 19, 20, 151, 152): return float(value) / 10 return value @@ -356,11 +330,12 @@ def parse_item(self, item): return self.update_item def update_item(self, item, caller=None, source=None, dest=None): - if caller != 'Luxtronic2': + if caller != 'Luxtronic2' and self.alive: self.set_param(self.get_iattr_value(item.conf, 'lux2_p'), item()) def main(): + lux = None try: lux = LuxBase('192.168.178.25') lux.connect() @@ -389,5 +364,6 @@ def main(): if lux: lux.close() + if __name__ == "__main__": sys.exit(main()) diff --git a/luxtronic2/plugin.yaml b/luxtronic2/plugin.yaml index 1a4349c3d..e98af2d62 100755 --- a/luxtronic2/plugin.yaml +++ b/luxtronic2/plugin.yaml @@ -11,11 +11,11 @@ plugin: # documentation: https://github.com/smarthomeNG/plugins/blob/develop/mqtt/README.md # url of documentation (wiki) page # support: https://knx-user-forum.de/forum/supportforen/smarthome-py - version: 1.3.2 # Plugin version + version: 1.3.3 # Plugin version sh_minversion: 1.3 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False - restartable: unknown + multi_instance: false + restartable: true classname: Luxtronic2 # class containing the plugin parameters: @@ -32,6 +32,13 @@ parameters: de: 'Gibt den Port des Gerätes an' en: 'Specifies the port of the devices' + cycle: + type: int + default: 300 + description: + de: 'Zeitintervall zur Datenabfrage' + en: 'Interval for retrieving data' + item_attributes: # Definition of item attributes defined by this plugin lux2: diff --git a/plex/__init__.py b/plex/__init__.py index 37bce2f96..243166afe 100755 --- a/plex/__init__.py +++ b/plex/__init__.py @@ -19,7 +19,6 @@ # along with SmartHomeNG. If not, see . ######################################################################### -import logging import json import requests import random @@ -27,41 +26,40 @@ class Plex(SmartPlugin): - PLUGIN_VERSION = "1.0.0" + PLUGIN_VERSION = "1.0.1" ALLOW_MULTIINSTANCE = False - def __init__(self, smarthome, displaytime=6000): - self.logger = logging.getLogger(__name__) + def __init__(self): + self._displayTime = self.get_parameter_value('displaytime') self.logger.info("Init Plex notifications") - self._sh = smarthome - self._displayTime = int(displaytime) self._images = ["info", "error", "warning"] self._clients = [] def run(self): - pass + self.alive = True def stop(self): - pass + self.alive = False def _push(self, host, data): - try: - res = requests.post(host, - headers={ - "User-Agent": "sh.py", - "Content-Type": "application/json"}, - timeout=4, - data=json.dumps(data), - ) - self.logger.debug(res) - response = res.text - del res - self.logger.debug(response) - except Exception as e: - self.logger.exception(e) + if self.alive: + try: + res = requests.post(host, + headers={ + "User-Agent": "sh.py", + "Content-Type": "application/json"}, + timeout=4, + data=json.dumps(data), + ) + self.logger.debug(res) + response = res.text + del res + self.logger.debug(response) + except Exception as e: + self.logger.exception(e) def notify(self, title, message, image="info"): - if not image in self._images: + if image not in self._images: self.logger.warn("Plex image must be: {}".format(", ".join(self._images))) else: data = {"jsonrpc": "2.0", diff --git a/plex/plugin.yaml b/plex/plugin.yaml index 7882ce0c8..302dd0894 100755 --- a/plex/plugin.yaml +++ b/plex/plugin.yaml @@ -12,11 +12,11 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page # support: https://knx-user-forum.de/forum/supportforen/smarthome-py - version: 1.0.0 # Plugin version - sh_minversion: 1.1 # minimum shNG version to use this plugin + version: 1.0.1 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False # plugin supports multi instance - restartable: unknown + multi_instance: false # plugin supports multi instance + restartable: true classname: Plex # class containing the plugin parameters: diff --git a/rcswitch/__init__.py b/rcswitch/__init__.py index 0226ecc3c..389df1fec 100755 --- a/rcswitch/__init__.py +++ b/rcswitch/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab ######################################################################### -# Copyright 2017 Daniel Frank knx-user-forum.de:dafra +# Copyright 2017 Daniel Frank knx-user-forum.de:dafra ######################################################################### # This file is part of SmartHomeNG. https://github.com/smarthomeNG// # @@ -21,7 +21,6 @@ import logging from lib.model.smartplugin import SmartPlugin -from datetime import datetime, timedelta from socket import gethostname import time import threading @@ -30,105 +29,105 @@ from subprocess import DEVNULL import shlex + class RCswitch(SmartPlugin): - ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "1.2.2" + ALLOW_MULTIINSTANCE = False + PLUGIN_VERSION = "1.2.2" + + def __init__(self): + self.logger = logging.getLogger(__name__) + self.setupOK = True + self.mapping = {'a': 1, 'A': 1, 'b': 2, 'B': 2, 'c': 3, 'C': 3, 'd': 4, 'D': 4, 'e': 5, 'E': 5} + self.rcswitch_dir = self.get_parameter_value('rcswitch_dir') + self.rcswitch_sendDuration = self.get_parameter_value('rcswitch_sendDuration') + self.rcswitch_host = self.get_parameter_value('rcswitch_host') + self.rcswitch_user = self.get_parameter_value('rcswitch_user') + self.rcswitch_password = self.get_parameter_value('rcswitch_password') + + # format path: cut possible '/' at end of rcswitch_dir parameter + if self.rcswitch_dir.endswith('/'): + self.rcswitch_dir = self.rcswitch_dir[:-1] + + # Handle host, check if anything is defined in self.rcswitch_host parameter and if is valid hostname or valid IPv4 adress + if self.rcswitch_host: + # then check if user defined its own local host -> error + if ((self.rcswitch_host == gethostname()) or (self.rcswitch_host == '127.0.0.1')): + self.logger.error('RCswitch: rcswitch_host is defined as your own machine, not the remote address! Please check the parameter rcswitch_host, >>{}<< it seems to be not correct!'.format(self.rcswitch_host)) + + # check connection to remote host and accept fingerprint + user = None + try: + # following line shall raise an error in case connection is not possible. + user = subprocess.check_output(shlex.split('sshpass -p {} ssh -o StrictHostKeyChecking=no {}@{} grep {} /etc/passwd'.format(self.rcswitch_password, self.rcswitch_user, self.rcswitch_host, self.rcswitch_user)), stderr=DEVNULL).decode('utf8')[:len(self.rcswitch_user)] + # check if rc switch is installed at the specified path on remote host + self.fileStat = subprocess.check_output(shlex.split('sshpass -p {} ssh {}@{} stat -c %a {}'.format(self.rcswitch_password, self.rcswitch_user, self.rcswitch_host, self.rcswitch_dir)), stderr=DEVNULL).decode('utf8') + self.rcswitch_dir = 'sshpass -p {} ssh {}@{} {}'.format(self.rcswitch_password, self.rcswitch_user, self.rcswitch_host, self.rcswitch_dir) + self.logger.info('RCswitch: Using {} as host.'.format(self.rcswitch_host)) + except subprocess.CalledProcessError as e: + self.setupOK = False + # give user hint where the problem is located + try: + if (user == self.rcswitch_user): + self.logger.error('RCswitch: send file of RCswitch not found at {} on {}. Check if RCswitch is installed correctly at specifed path on {}. System returned: {}'.format(self.rcswitch_dir, self.rcswitch_host, self.rcswitch_host, e)) + else: + self.logger.error('RCswitch: send file of RCswitch not found at {} on {}. Additional problem with user authentication possible. System returned: {}'.format(self.rcswitch_dir, self.rcswitch_host, e)) + except UnboundLocalError as e: + self.logger.error('RCswitch: Cannot connect to {}. Check rcswitch_host, rcswitch_user and rcswitch_password are set (correctly). Ensure SSH server is running on {}. System returned: {}'.format(self.rcswitch_host, self.rcswitch_host, e)) + else: + # check if rc switch is installed at the specified path + if not os.path.isfile('{}/send'.format(self.rcswitch_dir)): + self.logger.error('RCswitch: send file of RCswitch not found at {} on localhost. Check path, if RCswitch is installed correctly on target, and correct format of v4 IP adress (in case rcswitch_host is defined).'.format(self.rcswitch_dir)) + self.setupOK = False + else: + self.logger.info('RCswitch: setup on localhost OK') - def __init__(self, smarthome, rcswitch_dir='/usr/local/bin/rcswitch-pi', rcswitch_sendDuration='0.5', rcswitch_host=None, rcswitch_user=None, rcswitch_password=None): - self.logger = logging.getLogger(__name__) - self.setupOK = True - self.mapping = {'a':1,'A':1,'b':2,'B':2,'c':3,'C':3,'d':4,'D':4,'e':5,'E':5} - self._sh = smarthome - - # format path: cut possible '/' at end of rcswitch_dir parameter - if rcswitch_dir[len(rcswitch_dir)-1] == '/': - self.rcswitch_dir = rcswitch_dir[0:len(rcswitch_dir)-1] - else: - self.rcswitch_dir = rcswitch_dir + # setup semaphore + self.lock = threading.Lock() - # Check optional Parameters: check sendDuration - try: - self.sendDuration = float(rcswitch_sendDuration) - except Exception as e: - self.sendDuration = float(0.5) - self.logger.warning('RCswitch: Argument {} for rcswitch_sendDuration is not a valid number. Using default value instead.'.format(rcswitch_sendDuration)) - - # Handle host, check if anything is defined in rcswitch_host parameter and if is valid hostname or valid IPv4 adress - if (rcswitch_host and (self.is_hostname(rcswitch_host) or self.is_ipv4(rcswitch_host))): - # then check if user defined its own local host -> error - if ((rcswitch_host == gethostname()) or (rcswitch_host == '127.0.0.1')): - self.logger.error('RCswitch: rcswitch_host is defined as your own machine, not the remote address! Please check the parameter rcswitch_host, >>{}<< it seems to be not correct!'.format(rcswitch_host)) + # don't load plugin if init didn't work + if not self.setupOK: + self._init_complete = False - #check connection to remote host and accept fingerprint - try: - # following line shall raise an error in case connection is not possible. - self.user = subprocess.check_output(shlex.split('sshpass -p {} ssh -o StrictHostKeyChecking=no {}@{} grep {} /etc/passwd'.format(rcswitch_password, rcswitch_user, rcswitch_host, rcswitch_user)), stderr=DEVNULL).decode('utf8')[0:len(rcswitch_user)] - # check if rc switch is installed at the specified path on remote host - self.fileStat = subprocess.check_output(shlex.split('sshpass -p {} ssh {}@{} stat -c %a {}'.format(rcswitch_password, rcswitch_user, rcswitch_host, self.rcswitch_dir)), stderr=DEVNULL).decode('utf8') - self.rcswitch_dir = ('sshpass -p {} ssh {}@{} {}'.format(rcswitch_password, rcswitch_user, rcswitch_host, self.rcswitch_dir)) - self.logger.info('RCswitch: Using {} as host.'.format(rcswitch_host)) - except subprocess.CalledProcessError as e: - self.setupOK = False - # give user hint where the problem is located - try: - if (user == rcswitch_user): - self.logger.error('RCswitch: send file of RCswitch not found at {} on {}. Check if RCswitch is installed correctly at specifed path on {}. System returned: {}'.format(self.rcswitch_dir, rcswitch_host, rcswitch_host, e)) - else: - self.logger.error('RCswitch: send file of RCswitch not found at {} on {}. Additional problem with user authentication possible. System returned: {}'.format(self.rcswitch_dir, rcswitch_host, e)) - except UnboundLocalError as e: - self.logger.error('RCswitch: Cannot connect to {}. Check rcswitch_host, rcswitch_user and rcswitch_password are set (correctly). Ensure SSH server is running on {}. System returned: {}'.format(rcswitch_host, rcswitch_host, e)) - else: - # check if rc switch is installed at the specified path - if not os.path.isfile('{}/send'.format(self.rcswitch_dir)): - self.logger.error('RCswitch: send file of RCswitch not found at {} on localhost. Check path, if RCswitch is installed correctly on target, and correct format of v4 IP adress (in case rcswitch_host is defined).'.format(self.rcswitch_dir)) - self.setupOK = False - else: - self.logger.info('RCswitch: setup on localhost OK') - - # setup semaphore - self.lock = threading.Lock() - - def run(self): - self.alive = True + def run(self): + self.alive = True - def stop(self): - self.alive = False + def stop(self): + self.alive = False - def parse_item(self, item): - # generate warnings for incomplete configured itemns - if self.has_iattr(item.conf, 'rc_device'): - if self.has_iattr(item.conf, 'rc_code'): - return self.update_item - else: - self.logger.warning('RC Switch: attribute rc_code for {} missing. Item will be ignored by RCswitch plugin'.format(item)) - return None - elif self.has_iattr(item.conf, 'rc_code'): - self.logger.warning('RC Switch: attribute rc_device for {} missing. Item will be ignored by RCswitch plugin'.format(item)) - return None - else: - return None + def parse_item(self, item): + # generate warnings for incomplete configured itemns + if self.has_iattr(item.conf, 'rc_device'): + if self.has_iattr(item.conf, 'rc_code'): + return self.update_item + else: + self.logger.warning('RC Switch: attribute rc_code for {} missing. Item will be ignored by RCswitch plugin'.format(item)) + return None + elif self.has_iattr(item.conf, 'rc_code'): + self.logger.warning('RC Switch: attribute rc_device for {} missing. Item will be ignored by RCswitch plugin'.format(item)) + return None + else: + return None + def update_item(self, item, caller=None, source=None, dest=None): + # send commands to devices + if self.has_iattr(item.conf, 'rc_code') and self.has_iattr(item.conf, 'rc_device') and self.alive: + # prepare parameters + value = item() + rcCode = self.get_iattr_value(item.conf, 'rc_code') + rcDevice = self.get_iattr_value(item.conf, 'rc_device') - def update_item(self, item, caller=None, source=None, dest=None): - # send commands to devices - if self.has_iattr(item.conf, 'rc_code') and self.has_iattr(item.conf, 'rc_device') and self.setupOK: #if 'rc_device' in item.conf and 'rc_code' in item.conf and self.setupOK: - # prepare parameters - value = item() - rcCode = self.get_iattr_value(item.conf, 'rc_code') - rcDevice = self.get_iattr_value(item.conf, 'rc_device') - - # avoid parallel access by use of semaphore - self.lock.acquire() - # sending commands - if(rcDevice in self.mapping):#handling of device encoded with a,A,b,... - subprocess.call(shlex.split('{}/send {} {} {}'.format(self.rcswitch_dir, rcCode, self.mapping[rcDevice], int(value))), stdout=DEVNULL, stderr=DEVNULL) - self.logger.info('RC Switch: setting device {} with system code {} to {}'.format(rcDevice, rcCode, value)) - else: - try:#handling of devices encoded with 1,2,3 - subprocess.call(shlex.split('{}/send {} {} {}'.format(self.rcswitch_dir, rcCode, int(rcDevice), int(value))), stdout=DEVNULL, stderr=DEVNULL) - self.logger.info('RC Switch: setting device {} with system code {} to {}'.format(rcDevice, rcCode, value)) - except Exception as e:#invalid encoding of device - self.logger.error('RC Switch: requesting invalid device {} with system code {} '.format(rcDevice, rcCode)) - time.sleep(min(self.sendDuration,10))# give the transmitter time to complete sending of the command (but not more than 10s) - self.lock.release() \ No newline at end of file + # avoid parallel access by use of semaphore + self.lock.acquire() + # sending commands + if (rcDevice in self.mapping): # handling of device encoded with a,A,b,... + subprocess.call(shlex.split('{}/send {} {} {}'.format(self.rcswitch_dir, rcCode, self.mapping[rcDevice], int(value))), stdout=DEVNULL, stderr=DEVNULL) + self.logger.info('RC Switch: setting device {} with system code {} to {}'.format(rcDevice, rcCode, value)) + else: + try: # handling of devices encoded with 1,2,3 + subprocess.call(shlex.split('{}/send {} {} {}'.format(self.rcswitch_dir, rcCode, int(rcDevice), int(value))), stdout=DEVNULL, stderr=DEVNULL) + self.logger.info('RC Switch: setting device {} with system code {} to {}'.format(rcDevice, rcCode, value)) + except Exception: # invalid encoding of device + self.logger.error('RC Switch: requesting invalid device {} with system code {} '.format(rcDevice, rcCode)) + time.sleep(min(self.rcswitch_sendDuration, 10)) # give the transmitter time to complete sending of the command (but not more than 10s) + self.lock.release() diff --git a/rcswitch/plugin.yaml b/rcswitch/plugin.yaml index 65fe7b22b..42b301887 100755 --- a/rcswitch/plugin.yaml +++ b/rcswitch/plugin.yaml @@ -13,10 +13,10 @@ plugin: support: https://knx-user-forum.de/forum/supportforen/smarthome-py/39094-logic-und-howto-für-433mhz-steckdosen version: 1.2.2 # Plugin version - sh_minversion: 1.2 # minimum shNG version to use this plugin + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False # plugin supports multi instance - restartable: unknown + multi_instance: false # plugin supports multi instance + restartable: true classname: RCswitch # class containing the plugin parameters: From 36d417a1f17b864343c2a86044beffbced6b3a3a Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:38:54 +0100 Subject: [PATCH 2/6] fixes --- helios/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helios/__init__.py b/helios/__init__.py index f8c97d53d..b08b0f9f2 100755 --- a/helios/__init__.py +++ b/helios/__init__.py @@ -432,10 +432,10 @@ def __init__(self): def run(self): self.connect() self._alive = True - self._sh.scheduler.add('Helios', self._update, cycle=self._cycle) + self.scheduler_add('Helios', self._update, cycle=self._cycle) def stop(self): - self._sh.scheduler.remove('Helios') + self.scheduler_remove('Helios') self.disconnect() self._alive = False From 39dbee8daa57ebb36ceb176121ff9cbf3dcc34e2 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Fri, 24 Nov 2023 17:39:09 +0100 Subject: [PATCH 3/6] rest of the plugins --- roomba_980/__init__.py | 55 +++++++++--------- roomba_980/plugin.yaml | 18 +++--- sma/__init__.py | 81 +++++++++++++------------- sma/plugin.yaml | 18 +++--- smarttv/__init__.py | 24 ++++---- smarttv/plugin.yaml | 18 +++--- sml/__init__.py | 125 +++++++++++++++++++---------------------- sml/plugin.yaml | 8 +-- systemair/__init__.py | 65 ++++++++++----------- systemair/plugin.yaml | 8 +-- 10 files changed, 207 insertions(+), 213 deletions(-) diff --git a/roomba_980/__init__.py b/roomba_980/__init__.py index ac2ab99e3..036d1a035 100755 --- a/roomba_980/__init__.py +++ b/roomba_980/__init__.py @@ -19,23 +19,23 @@ # along with SmartHome.py. If not, see . ######################################################################### -import logging +# TODO: das Modul ist im Sourcetree nicht vorhanden... woher soll das kommen? from plugins.roomba_980.roomba import Roomba from lib.model.smartplugin import SmartPlugin -from lib.item import Items + class ROOMBA_980(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "1.0.1" + PLUGIN_VERSION = "1.0.2" myroomba = None - def __init__(self, sh, adress=None, blid=None, roombaPassword=None, cycle=900): - self._address = adress - self._blid = blid - self._roombaPassword = roombaPassword - self._cycle = cycle + def __init__(self): + self._address = self.get_parameter_value('adress') + self._blid = self.get_parameter_value('blid') + self._roombaPassword = self.get_parameter_value('roombaPassword') + self._cycle = self.get_parameter_value('cycle') self._status_batterie = None self._status_items = {} @@ -61,7 +61,7 @@ def run(self): self.alive = True def stop(self): - self.scheduler.remove('get_status') + self.scheduler_remove('get_status') self.myroomba.disconnect() self.alive = False @@ -69,36 +69,35 @@ def __call__(self): pass def update_item(self, item, caller=None, source=None, dest=None): - if caller != __name__: + if caller != __name__ and self.alive: self.logger.debug('item_update {} '.format(item)) if self.get_iattr_value(item.conf, 'roomba_980') == "start": - if item() == True: - self.send_command("start") + if item() is True: + self.send_command("start") elif self.get_iattr_value(item.conf, 'roomba_980') == "stop": - if item() == True: - self.send_command("stop") + if item() is True: + self.send_command("stop") elif self.get_iattr_value(item.conf, 'roomba_980') == "dock": - if item() == True: - self.send_command("dock") + if item() is True: + self.send_command("dock") def get_status(self): status = self.myroomba.master_state for status_item in self._status_items: - if status_item == "status_batterie": - self._status_items[status_item](status['state']['reported']['batPct'],__name__) - elif status_item == "status_bin_full": - self._status_items[status_item](status['state']['reported']['bin']['full'],__name__) - elif status_item == "status_cleanMissionStatus_phase": - self._status_items[status_item](status['state']['reported']['cleanMissionStatus']['phase'],__name__) - elif status_item == "status_cleanMissionStatus_error": - self._status_items[status_item](status['state']['reported']['cleanMissionStatus']['error'],__name__) + if status_item == "status_batterie": + self._status_items[status_item](status['state']['reported']['batPct'], __name__) + elif status_item == "status_bin_full": + self._status_items[status_item](status['state']['reported']['bin']['full'], __name__) + elif status_item == "status_cleanMissionStatus_phase": + self._status_items[status_item](status['state']['reported']['cleanMissionStatus']['phase'], __name__) + elif status_item == "status_cleanMissionStatus_error": + self._status_items[status_item](status['state']['reported']['cleanMissionStatus']['error'], __name__) self.logger.debug('Status update') def send_command(self, command): - if self.myroomba != None: - self.myroomba.send_command(command) - self.logger.debug('send command: {} to Roomba'.format(command)) - + if self.myroomba is not None: + self.myroomba.send_command(command) + self.logger.debug('send command: {} to Roomba'.format(command)) diff --git a/roomba_980/plugin.yaml b/roomba_980/plugin.yaml index 8b9ea3e8e..1da819cd6 100755 --- a/roomba_980/plugin.yaml +++ b/roomba_980/plugin.yaml @@ -7,17 +7,17 @@ plugin: en: 'integration of the iRobot Roomba vacuum cleaner series 900' maintainer: 'Zapfen83' tester: '?' - state: ready + state: development keywords: irobot roomba # keywords, where applicable # documentation: https://github.com/smarthomeNG/plugins/blob/develop/mqtt/README.md # url of documentation (wiki) page # support: https://knx-user-forum.de/forum/supportforen/smarthome-py # Following entries are for Smart-Plugins: - version: 1.0.1 # Plugin version - sh_minversion: 1.5 # minimum shNG version to use this plugin + version: 1.0.2 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False - restartable: unknown + multi_instance: false + restartable: true classname: ROOMBA_980 # class containing the plugin parameters: @@ -25,28 +25,28 @@ parameters: adress: type: str - default: True + default: true description: de: "Die IP Adresse des roomba Staubsaugers" en: "The IP address of the roomba vacuum cleaner" blid: type: str - default: True + default: true description: de: "Die blid des roomba Staubsaugers -> kann mit der getpassword.py ausgelesen werden" en: "The blid of the roomba vacuum cleaner -> use getpassword.py to get it" roombaPassword: type: str - default: True + default: true description: de: "Das Passwort des roomba Staubsaugers -> kann mit der getpassword.py ausgelesen werden" en: "The password of the roomba vacuum cleaner -> use getpassword.py to get it" cycle: type: num - default: False + default: false description: de: "update des items alle x Sekunden, default wert 900" en: "update the state item every x secounds, default is 900" diff --git a/sma/__init__.py b/sma/__init__.py index 2a453c865..82d224a9e 100755 --- a/sma/__init__.py +++ b/sma/__init__.py @@ -11,29 +11,29 @@ # # SMA-Plugin for SmartHomeNG. https://github.com/smarthomeNG// # -# License: Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0) -# http://creativecommons.org/licenses/by-nc-sa/3.0/ +# License: Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0) +# http://creativecommons.org/licenses/by-nc-sa/3.0/ # -# You are free: -# to Share — to copy, distribute and transmit the work -# to Remix — to adapt the work -# Under the following conditions: -# Attribution: -# You must attribute the work in the manner specified by the author or licensor -# (but not in any way that suggests that they endorse you or your use of the work). -# Noncommercial: -# You may not use this work for commercial purposes. -# Share Alike: -# If you alter, transform, or build upon this work, you may distribute the resulting work -# only under the same or similar license to this one. +# You are free: +# to Share — to copy, distribute and transmit the work +# to Remix — to adapt the work +# Under the following conditions: +# Attribution: +# You must attribute the work in the manner specified by the author or licensor +# (but not in any way that suggests that they endorse you or your use of the work). +# Noncommercial: +# You may not use this work for commercial purposes. +# Share Alike: +# If you alter, transform, or build upon this work, you may distribute the resulting work +# only under the same or similar license to this one. # # DISCLAIMER: -# A user of this plugin acknowledges that he or she is receiving this -# software on an "as is" basis and the user is not relying on the accuracy -# or functionality of the software for any purpose. The user further -# acknowledges that any use of this software will be at his own risk -# and the copyright owner accepts no responsibility whatsoever arising from -# the use or application of the software. +# A user of this plugin acknowledges that he or she is receiving this +# software on an "as is" basis and the user is not relying on the accuracy +# or functionality of the software for any purpose. The user further +# acknowledges that any use of this software will be at his own risk +# and the copyright owner accepts no responsibility whatsoever arising from +# the use or application of the software. ######################################################################### import logging @@ -42,7 +42,6 @@ import socket from datetime import datetime from dateutil import tz -import itertools from lib.model.smartplugin import SmartPlugin @@ -185,19 +184,19 @@ class SMA(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "1.3.1" + PLUGIN_VERSION = "1.3.2" - def __init__(self, smarthome, bt_addr, password="0000", update_cycle="60", allowed_timedelta="10"): + def __init__(self): + # TODO: self._own_bt_addr setzen self.logger = logging.getLogger(__name__) - self._sh = smarthome - self._update_cycle = int(update_cycle) + self._inv_bt_addr = self.get_parameter_value('bt_addr') + self._inv_password = self.get_parameter_value('password') + self._update_cycle = self.get_parameter_value('update_cycle') + self._allowed_timedelta = self.get_parameter_value('allowed_timedelta') self._fields = {} self._requests = [] self._cmd_lock = threading.Lock() self._reply_lock = threading.Condition() - self._inv_bt_addr = bt_addr - self._inv_password = password - self._allowed_timedelta = int(allowed_timedelta) self._inv_last_read_timestamp_utc = 0 self._inv_serial = 0 self._own_bt_addr_le = bytearray(BCAST_ADDR) @@ -209,7 +208,7 @@ def __init__(self, smarthome, bt_addr, password="0000", update_cycle="60", allow raise Exception("Python socket module does not support Bluetooth - see README.md how to install") def _update_values(self): - #logger.warning("sma: signal strength = {}%%".format(self._inv_get_bt_signal_strength())) + # logger.warning("sma: signal strength = {}%%".format(self._inv_get_bt_signal_strength())) self._cmd_lock.acquire() try: for request in self._requests: @@ -224,7 +223,7 @@ def _update_values(self): self._reply_lock.release() if ('LAST_UPDATE' in self._fields) and not (self._inv_last_read_timestamp_utc == 0): self._inv_last_read_datetime = datetime.fromtimestamp(self._inv_last_read_timestamp_utc, tz.tzlocal()) - #self._inv_last_read_str = self._inv_last_read_datetime.strftime("%d.%m.%Y %H:%M:%S") + # self._inv_last_read_str = self._inv_last_read_datetime.strftime("%d.%m.%Y %H:%M:%S") self._inv_last_read_str = self._inv_last_read_datetime.strftime("%d.%m. %H:%M ") for item in self._fields['LAST_UPDATE']['items']: item(self._inv_last_read_str, 'SMA', self._inv_serial) @@ -241,7 +240,7 @@ def run(self): self._plugin_active = self._plugin_active_item() # "or self._is_connected" ensures the connection will be closed before terminating while self.alive or self._is_connected: - #self.logger.warning("sma: state self._is_connected = {} / self._plugin_active = {} / self.alive = {}".format(self._is_connected, self._plugin_active, self.alive)) + # self.logger.warning("sma: state self._is_connected = {} / self._plugin_active = {} / self.alive = {}".format(self._is_connected, self._plugin_active, self.alive)) # connect to inverter if active but not connected if self._plugin_active and not self._is_connected: @@ -318,7 +317,7 @@ def run(self): if not self.alive: break if msg is None: - #self.logger.debug("sma: no msg...") + # self.logger.debug("sma: no msg...") continue if len(msg) >= 60: i = 41 @@ -331,16 +330,16 @@ def run(self): if lri not in lris: self.logger.info("sma: unknown lri={:#06x} / cls={:#02x} / dataType={:#02x} - trying to continue".format(lri, cls, dataType)) if (dataType == 0x00) or (dataType == 0x40): - i += 28 + i += 28 elif (dataType == 0x08) or (dataType == 0x10): - i += 40 + i += 40 else: self.logger.error("sma: rx - unknown datatype {:#02x}".format(dataType)) raise continue else: timestamp_utc = int.from_bytes(msg[i + 4:i + 8], byteorder='little') - value = eval(lri_evals[lris[lri][0]], dict(msg=msg,i=i,attribute_to_text=attribute_to_text)) + value = eval(lri_evals[lris[lri][0]], dict(msg=msg, i=i, attribute_to_text=attribute_to_text)) i += lris[lri][1] self.logger.debug("sma: lri={:#06x} / cls={:#02x} / timestamp={} / value={}".format(lri, cls, timestamp_utc, value)) if full_id in self._fields: @@ -477,6 +476,7 @@ def _recv_smanet2_msg(self, no_timeout_warning=False): # remove escape characters i = 0 while True: + # TODO: if this works - fine, seems not to be standard Python 3? if smanet2_msg[i] == 0x7d: smanet2_msg[i + 1] ^= 0x20 del(smanet2_msg[i]) @@ -493,6 +493,7 @@ def _recv_smanet2_msg(self, no_timeout_warning=False): def _recv_smanet1_msg_with_cmdcode(self, cmdcodes_expected=[0x0001]): retries = 3 + msg = None while self.alive: retries -= 1 if retries == 0: @@ -522,7 +523,7 @@ def _send_msg(self, msg): # set length fields msg[1:3] = len(msg).to_bytes(2, byteorder='little') msg[3] = msg[1] ^ msg[2] ^ 0x7e - #print("tx: len={} / data=[{}]".format(len(msg), ' '.join(['0x%02x' % b for b in msg]))) + # print("tx: len={} / data=[{}]".format(len(msg), ' '.join(['0x%02x' % b for b in msg]))) self._btsocket.send(msg) def _calc_crc16(self, msg): @@ -530,7 +531,7 @@ def _calc_crc16(self, msg): for i in msg: crc = (crc >> 8) ^ FCSTAB[(crc ^ i) & 0xFF] crc ^= 0xFFFF - #print("crc16 = {:x}".format(crc)) + # print("crc16 = {:x}".format(crc)) return crc def _inv_connect(self): @@ -653,7 +654,7 @@ def _inv_send_request(self, request_set): msg += SMANET2_HDR + bytes([0x09, 0xA0]) + BCAST_ADDR + bytes([0x00, 0x00]) + self._inv_bt_addr_le + bytes([0x00] + [0x00] + [0, 0, 0, 0]) + (self._send_count | 0x8000).to_bytes(2, byteorder='little') msg += request_set[0].to_bytes(4, byteorder='little') + request_set[1].to_bytes(4, byteorder='little') + request_set[2].to_bytes(4, byteorder='little') # send msg to inverter - #self.logger.debug("sma: requesting {:#06x}-{:#06x}...".format(request_set[1], request_set[2])) + # self.logger.debug("sma: requesting {:#06x}-{:#06x}...".format(request_set[1], request_set[2])) self._send_msg(msg) def _inv_set_time(self): @@ -666,7 +667,7 @@ def _inv_set_time(self): msg += SMANET2_HDR + bytes([0x10, 0xA0]) + BCAST_ADDR + bytes([0x00, 0x00]) + self._inv_bt_addr_le + bytes([0x00] + [0x00] + [0, 0, 0, 0]) + (self._send_count | 0x8000).to_bytes(2, byteorder='little') msg += int(0xF000020A).to_bytes(4, byteorder='little') + int(0x00236D00).to_bytes(4, byteorder='little') + int(0x00236D00).to_bytes(4, byteorder='little') + int(0x00236D00).to_bytes(4, byteorder='little') local_time = int(time.time()).to_bytes(4, byteorder='little') - msg += local_time + local_time + local_time + round((datetime.now()-datetime.utcnow()).total_seconds()).to_bytes(4, byteorder='little') + local_time + bytes([0x01, 0x00, 0x00, 0x00]) + msg += local_time + local_time + local_time + round((datetime.now() - datetime.utcnow()).total_seconds()).to_bytes(4, byteorder='little') + local_time + bytes([0x01, 0x00, 0x00, 0x00]) # msg += local_time + local_time + local_time + time.localtime().tm_gmtoff.to_bytes(4, byteorder='little') + local_time + bytes([0x01, 0x00, 0x00, 0x00]) # send msg to inverter - self._send_msg(msg) \ No newline at end of file + self._send_msg(msg) diff --git a/sma/plugin.yaml b/sma/plugin.yaml index 3bc6bc912..f18e18f74 100755 --- a/sma/plugin.yaml +++ b/sma/plugin.yaml @@ -12,18 +12,18 @@ plugin: documentation: https://smarthomeng.de/user/plugins_doc/config/sma.html # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/27997-beitrag-plugin-zum-lesen-von-sma-wechselrichtern-sunnyboy-5000tl-21-getestet - version: 1.3.1 # Plugin version - sh_minversion: 1.3 # minimum shNG version to use this plugin + version: 1.3.2 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False # plugin supports multi instance - restartable: unknown + multi_instance: false # plugin supports multi instance + restartable: true classname: SMA # class containing the plugin parameters: # Definition of parameters to be configured in etc/plugin.yaml bt_addr: type: str - mandatory: True + mandatory: true description: de: 'Bluetooth-Adresse des SMA Wechselrichters (mit "hcitool scan" herauszufinden).' en: 'Bluetooth address of SMA inverter (find out with "hcitool scan").' @@ -31,14 +31,14 @@ parameters: cycle: type: int default: 60 - mandatory: False + mandatory: false description: de: '(optional) Dieser Parameter muss normalerweise nicht angegeben werden. Er erlaubt es die Update-Frquenz anzupassen (Standard: alle 60 Sekunden).' en: "(optional) This parameter usually doesn't have to be specified. It allows to change the update frequency (cycle every 60 seconds)." password: type: str - mandatory: False + mandatory: false description: de: '(optional) Passwort des Wechselrichters im User-Mode. Default: 0000.' en: '(optional) Password of the inverter in user mode. Default: 0000.' @@ -46,7 +46,7 @@ parameters: allowed_timedelta: type: int default: 10 - mandatory: False + mandatory: false description: de: '(optional) Erlaubter Zeitunterschied zwischen Systemzeit und Zeit des Wechselrichters. Falls der Inverter auf Systemzeit steht, kann die Prüfung mit -1 deaktiviert werden. Default: 10.' en: '(optional) Allowed difference of inverter to system time - if above, inverter is set to system time - set to -1 to disable. Default: 10.' @@ -55,7 +55,7 @@ item_attributes: # Definition of item attributes defined by this plugin sma: type: str - mandatory: True + mandatory: true description: de: 'Auszulesender Wert, möglich sind: PLUGIN_ACTIVE, AC_P_TOTAL, E_DAY, E_TOTAL, INV_SERIAL, INV_ADDRESS, LAST_UPDATE, DC_STRING1_P, DC_STRING2_P, DC_STRING1_U, DC_STRING2_U, DC_STRING1_I, DC_STRING2_I, OPERATING_TIME, FEEDING_TIME, GRID_FREQUENCY, STATUS, GRID_RELAY, SW_VERSION.' en: 'Value to read. Possible: PLUGIN_ACTIVE, AC_P_TOTAL, E_DAY, E_TOTAL, INV_SERIAL, INV_ADDRESS, LAST_UPDATE, DC_STRING1_P, DC_STRING2_P, DC_STRING1_U, DC_STRING2_U, DC_STRING1_I, DC_STRING2_I, OPERATING_TIME, FEEDING_TIME, GRID_FREQUENCY, STATUS, GRID_RELAY, SW_VERSION.' diff --git a/smarttv/__init__.py b/smarttv/__init__.py index 07dde2fc5..654f13475 100755 --- a/smarttv/__init__.py +++ b/smarttv/__init__.py @@ -27,21 +27,23 @@ from lib.model.smartplugin import SmartPlugin from uuid import getnode as getmac + class SmartTV(SmartPlugin): ALLOW_MULTIINSTANCE = True - PLUGIN_VERSION = "1.3.2" + PLUGIN_VERSION = "1.3.3" - def __init__(self, smarthome, host, port=55000, tv_version='classic', delay=1): + def __init__(self): self.logger = logging.getLogger(__name__) - self._sh = smarthome - self._host = host - self._port = int(port) - self._delay = delay - if tv_version not in ['samsung_m_series', 'classic']: + self._tv_version = self.get_parameter_value('tv_version') + self._host = self.get_parameter_value('host') + self._port = self.get_parameter_value('port') + self._delay = self.get_parameter_value('delay') + if self._tv_version not in ['samsung_m_series', 'classic']: self.logger.error('No valid tv_version attribute specified to plugin') - self._tv_version = tv_version - self.logger.debug("Smart TV plugin for {0} SmartTV device initalized".format(tv_version)) + self._init_complete = False + else: + self.logger.debug("Smart TV plugin for {0} SmartTV device initalized".format(self._tv_version)) def push_samsung_m_series(self, key): """ @@ -68,7 +70,7 @@ def push_classic(self, key): self.logger.debug("Connected to {0}:{1}".format(self._host, self._port)) except Exception: self.logger.warning("Could not connect to %s:%s, to send key: %s." % - (self._host, self._port, key)) + (self._host, self._port, key)) return src = s.getsockname()[0] # ip of remote @@ -140,6 +142,8 @@ def parse_item(self, item): return None def update_item(self, item, caller=None, source=None, dest=None): + if not self.alive: + return val = item() if isinstance(val, str): if val.startswith('KEY_'): diff --git a/smarttv/plugin.yaml b/smarttv/plugin.yaml index ef43b482d..8127d8b16 100755 --- a/smarttv/plugin.yaml +++ b/smarttv/plugin.yaml @@ -12,25 +12,25 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page # support: https://knx-user-forum.de/forum/supportforen/smarthome-py - version: 1.3.2 # Plugin version - sh_minversion: 1.3 # minimum shNG version to use this plugin + version: 1.3.3 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: True # plugin supports multi instance - restartable: unknown + multi_instance: true # plugin supports multi instance + restartable: true classname: SmartTV # class containing the plugin parameters: # Definition of parameters to be configured in etc/plugin.yaml host: type: str - mandatory: True + mandatory: true description: de: 'IP Adresse des SmartTV Geräts.' en: 'Specifies the ip address of your SmartTV device.' port: type: int - mandatory: False + mandatory: false default: 55000 description: de: '(optional) Im Fall eines Ports zu setzen, der nicht dem Standard entspricht (Standardports siehe tv_version). Default: 55000.' @@ -38,7 +38,7 @@ parameters: tv_version: type: str - mandatory: False + mandatory: false default: classic description: de: '(optional) Erlaubte Werte sind "classic" (Port 55000) oder "samsung_m_series" (Standardport: 8001). Default: "classic".' @@ -46,7 +46,7 @@ parameters: delay: type: int - mandatory: False + mandatory: false default: 1 description: de: '(optional) Verzögerung in Sekunden, falls mehr als eine Taste gesendet wird. Reduziert Probleme, wenn bspw. TV-Programme wie "135" aufgerufen werden. Default: 1' @@ -57,7 +57,7 @@ item_attributes: # Definition of item attributes defined by this plugin smarttv: type: str - mandatory: True + mandatory: true description: de: 'Es gibt zwei Wege das Attribut zu nutzen. * Auf einem Item des Typs `str` mit dem Wert "true": Jeder String der in das Item geschrieben wird, wird an den SmartTV gesendet. diff --git a/sml/__init__.py b/sml/__init__.py index aabb788d9..2e93df094 100755 --- a/sml/__init__.py +++ b/sml/__init__.py @@ -32,31 +32,32 @@ from lib.model.smartplugin import SmartPlugin + class Sml(SmartPlugin): ALLOW_MULTIINSTANCE = True - PLUGIN_VERSION = '1.0.0' + PLUGIN_VERSION = '1.0.1' _v1_start = b'\x1b\x1b\x1b\x1b\x01\x01\x01\x01' _v1_end = b'\x1b\x1b\x1b\x1b\x1a' _units = { # Blue book @ http://www.dlms.com/documentation/overviewexcerptsofthedlmsuacolouredbooks/index.html - 1 : 'a', 2 : 'mo', 3 : 'wk', 4 : 'd', 5 : 'h', 6 : 'min.', 7 : 's', 8 : '°', 9 : '°C', 10 : 'currency', - 11 : 'm', 12 : 'm/s', 13 : 'm³', 14 : 'm³', 15 : 'm³/h', 16 : 'm³/h', 17 : 'm³/d', 18 : 'm³/d', 19 : 'l', 20 : 'kg', - 21 : 'N', 22 : 'Nm', 23 : 'Pa', 24 : 'bar', 25 : 'J', 26 : 'J/h', 27 : 'W', 28 : 'VA', 29 : 'var', 30 : 'Wh', - 31 : 'WAh', 32 : 'varh', 33 : 'A', 34 : 'C', 35 : 'V', 36 : 'V/m', 37 : 'F', 38 : 'Ω', 39 : 'Ωm²/h', 40 : 'Wb', - 41 : 'T', 42 : 'A/m', 43 : 'H', 44 : 'Hz', 45 : 'Rac', 46 : 'Rre', 47 : 'Rap', 48 : 'V²h', 49 : 'A²h', 50 : 'kg/s', - 51 : 'Smho' + 1: 'a', 2: 'mo', 3: 'wk', 4: 'd', 5: 'h', 6: 'min.', 7: 's', 8: '°', 9: '°C', 10: 'currency', + 11: 'm', 12: 'm/s', 13: 'm³', 14: 'm³', 15: 'm³/h', 16: 'm³/h', 17: 'm³/d', 18: 'm³/d', 19: 'l', 20: 'kg', + 21: 'N', 22: 'Nm', 23: 'Pa', 24: 'bar', 25: 'J', 26: 'J/h', 27: 'W', 28: 'VA', 29: 'var', 30: 'Wh', + 31: 'WAh', 32: 'varh', 33: 'A', 34: 'C', 35: 'V', 36: 'V/m', 37: 'F', 38: 'Ω', 39: 'Ωm²/h', 40: 'Wb', + 41: 'T', 42: 'A/m', 43: 'H', 44: 'Hz', 45: 'Rac', 46: 'Rre', 47: 'Rap', 48: 'V²h', 49: 'A²h', 50: 'kg/s', + 51: 'Smho' } _devices = { - 'smart-meter-gateway-com-1' : 'hex' + 'smart-meter-gateway-com-1': 'hex' } - def __init__(self, smarthome, host=None, port=0, serialport=None, device="raw", cycle=300): - self._sh = smarthome - self.host = host - self.port = int(port) - self.serialport = serialport - self.cycle = cycle + def __init__(self): + self.host = self.get_parameter_value('host') + self.port = self.get_parameter_value('port') + self.serialport = self.get_parameter_value('serialport') + self.cycle = self.get_parameter_value('cycle') + self.device = self.get_parameter_value('device') self.connected = False self._serial = None self._sock = None @@ -66,23 +67,24 @@ def __init__(self, smarthome, host=None, port=0, serialport=None, device="raw", self._lock = threading.Lock() self.logger = logging.getLogger(__name__) - if device in self._devices: - device = self._devices[device] + if self._device in self._devices: + self._device = self._devices[self._device] - if device == "hex": + if self._device == "hex": self._prepare = self._prepareHex - elif device == "raw": + elif self._device == "raw": self._prepare = self._prepareRaw else: - self.logger.warning("Device type \"{}\" not supported - defaulting to \"raw\"".format(device)) + self.logger.warning("Device type \"{}\" not supported - defaulting to \"raw\"".format(self._device)) self._prepare = self._prepareRaw def run(self): self.alive = True - self._sh.scheduler.add('Sml', self._refresh, cycle=self.cycle) + self.scheduler_add('Sml', self._refresh, cycle=self.cycle) def stop(self): self.alive = False + self.scheduler_remove('Sml') self.disconnect() def parse_item(self, item): @@ -98,16 +100,8 @@ def parse_item(self, item): return self.update_item return None - def parse_logic(self, logic): - pass - - def update_item(self, item, caller=None, source=None, dest=None): - if caller != 'Sml': - pass - def connect(self): self._lock.acquire() - target = None try: if self.serialport is not None: self._target = 'serial://{}'.format(self.serialport) @@ -164,9 +158,9 @@ def _read(self, length): raise e return b''.join(total) - + def _refresh(self): - if self.connected: + if self.connected and self.alive: start = time.time() retry = 5 data = None @@ -183,8 +177,8 @@ def _refresh(self): if start_pos != -1 and end_pos == -1: data = data[:start_pos] elif start_pos != -1 and end_pos != -1: - chunk = data[start_pos:end_pos+len(self._v1_end)+3] - self.logger.debug('Found chunk at {} - {} ({} bytes):{}'.format(start_pos, end_pos, end_pos-start_pos, ''.join(' {:02x}'.format(x) for x in chunk))) + chunk = data[start_pos:end_pos + len(self._v1_end) + 3] + self.logger.debug('Found chunk at {} - {} ({} bytes):{}'.format(start_pos, end_pos, end_pos - start_pos, ''.join(' {:02x}'.format(x) for x in chunk))) chunk_crc_str = '{:02X}{:02X}'.format(chunk[-2], chunk[-1]) chunk_crc_calc = self._crc16(chunk[:-2]) chunk_crc_calc_str = '{:02X}{:02X}'.format((chunk_crc_calc >> 8) & 0xff, chunk_crc_calc & 0xff) @@ -236,27 +230,27 @@ def _parse(self, data): packetsize = 7 self.logger.debug('Data ({} bytes):{}'.format(len(data), ''.join(' {:02x}'.format(x) for x in data))) self._dataoffset = 0 - while self._dataoffset < builtins.len(data)-packetsize: + while self._dataoffset < builtins.len(data) - packetsize: # Find SML_ListEntry starting with 0x77 0x07 and OBIS code end with 0xFF - if data[self._dataoffset] == 0x77 and data[self._dataoffset+1] == 0x07 and data[self._dataoffset+packetsize] == 0xff: + if data[self._dataoffset] == 0x77 and data[self._dataoffset + 1] == 0x07 and data[self._dataoffset + packetsize] == 0xff: packetstart = self._dataoffset self._dataoffset += 1 try: entry = { - 'objName' : self._read_entity(data), - 'status' : self._read_entity(data), - 'valTime' : self._read_entity(data), - 'unit' : self._read_entity(data), - 'scaler' : self._read_entity(data), - 'value' : self._read_entity(data), - 'signature' : self._read_entity(data) + 'objName': self._read_entity(data), + 'status': self._read_entity(data), + 'valTime': self._read_entity(data), + 'unit': self._read_entity(data), + 'scaler': self._read_entity(data), + 'value': self._read_entity(data), + 'signature': self._read_entity(data) } # add additional calculated fields entry['obis'] = '{}-{}:{}.{}.{}*{}'.format(entry['objName'][0], entry['objName'][1], entry['objName'][2], entry['objName'][3], entry['objName'][4], entry['objName'][5]) entry['valueReal'] = entry['value'] * 10 ** entry['scaler'] if entry['scaler'] is not None else entry['value'] - entry['unitName'] = self._units[entry['unit']] if entry['unit'] != None and entry['unit'] in self._units else None + entry['unitName'] = self._units[entry['unit']] if entry['unit'] is not None and entry['unit'] in self._units else None values[entry['obis']] = entry except Exception as e: @@ -269,8 +263,8 @@ def _parse(self, data): def _read_entity(self, data): upack = { - 5 : { 1 : '>b', 2 : '>h', 4 : '>i', 8 : '>q' }, # int - 6 : { 1 : '>B', 2 : '>H', 4 : '>I', 8 : '>Q' } # uint + 5: {1: '>b', 2: '>h', 4: '>i', 8: '>q'}, # int + 6: {1: '>B', 2: '>H', 4: '>I', 8: '>Q'} # uint } result = None @@ -296,23 +290,23 @@ def _read_entity(self, data): self._parse_error('Tried to read {} bytes, but only have {}', [len, builtins.len(data) - self._dataoffset], data, self._dataoffset, packetstart) elif type == 0: # octet string - result = data[self._dataoffset:self._dataoffset+len] + result = data[self._dataoffset:self._dataoffset + len] elif type == 5 or type == 6: # int or uint - d = data[self._dataoffset:self._dataoffset+len] + d = data[self._dataoffset:self._dataoffset + len] ulen = len if ulen not in upack[type]: # extend to next greather unpack unit - while ulen not in upack[type]: - d = b'\x00' + d - ulen += 1 + while ulen not in upack[type]: + d = b'\x00' + d + ulen += 1 result = struct.unpack(upack[type][ulen], d)[0] elif type == 7: # list result = [] self._dataoffset += 1 - for i in range(0, len + 1): + for _ in range(0, len + 1): result.append(self._read_entity(data)) return result @@ -326,7 +320,7 @@ def _read_entity(self, data): def _parse_error(self, msg, msgargs, data, dataoffset, packetstart): position = dataoffset - packetstart databytes = '' - for i, b in enumerate(data[packetstart:packetstart+64]): + for i, b in enumerate(data[packetstart:packetstart + 64]): databytes = databytes + ' {}{:02x}{}'.format( '' if i != position - 1 else '<', b, @@ -348,24 +342,23 @@ def _prepareHex(self, data): data = re.sub("[^a-f0-9]", " ", data) data = re.sub("( +[a-f0-9]|[a-f0-9] +)", "", data) data = data.encode() - return bytes(''.join(chr(int(data[i:i+2], 16)) for i in range(0, len(data), 2)), "iso8859-1") + return bytes(''.join(chr(int(data[i:i + 2], 16)) for i in range(0, len(data), 2)), "iso8859-1") def _crc16(self, data): - crc = 0xffff - - p = 0; - while p < len(data): - c = 0xff & data[p] - p = p + 1 + crc = 0xffff - for i in range(0, 8): - if ((crc & 0x0001) ^ (c & 0x0001)): - crc = (crc >> 1) ^ 0x8408 - else: - crc = crc >> 1 - c = c >> 1 + p = 0 + while p < len(data): + c = 0xff & data[p] + p = p + 1 - crc = ~crc & 0xffff + for i in range(0, 8): + if ((crc & 0x0001) ^ (c & 0x0001)): + crc = (crc >> 1) ^ 0x8408 + else: + crc = crc >> 1 + c = c >> 1 - return ((crc << 8) | ((crc >> 8) & 0xff)) & 0xffff + crc = ~crc & 0xffff + return ((crc << 8) | ((crc >> 8) & 0xff)) & 0xffff diff --git a/sml/plugin.yaml b/sml/plugin.yaml index 91360c5b2..c031f5e10 100755 --- a/sml/plugin.yaml +++ b/sml/plugin.yaml @@ -12,11 +12,11 @@ plugin: documentation: http://smarthomeng.de/user/plugins_doc/config/sml.html # support: https://knx-user-forum.de/forum/supportforen/smarthome-py - version: 1.0.0 # Plugin version - sh_minversion: 1.1 # minimum shNG version to use this plugin + version: 1.0.1 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: True # plugin supports multi instance - restartable: unknown + multi_instance: true # plugin supports multi instance + restartable: true classname: Sml # class containing the plugin parameters: diff --git a/systemair/__init__.py b/systemair/__init__.py index dc467c6ee..24a40b8e5 100755 --- a/systemair/__init__.py +++ b/systemair/__init__.py @@ -18,50 +18,47 @@ # You should have received a copy of the GNU General Public License # along with this plugin. If not, see . ######################################################################### -import logging from time import sleep import minimalmodbus -from serial import SerialException import serial import threading from ctypes import c_short from lib.model.smartplugin import SmartPlugin + class Systemair(SmartPlugin): - PLUGIN_VERSION = "1.3.0.1" + PLUGIN_VERSION = "1.3.1" ALLOW_MULTIINSTANCE = False - def __init__(self, smarthome, serialport, slave_address="1", update_cycle="30"): - self._sh = smarthome - self.logger = logging.getLogger(__name__) + def __init__(self): self.instrument = None - self.slave_address = int(slave_address) + self.serialport = self.get_parameter_value('serialport') + self.slave_address = self.get_parameter_value('slave_address') + self._update_cycle = self.get_parameter_value('update_cycle') self._update_coil = {} - self.serialport = serialport - self.slave_address = slave_address minimalmodbus.TIMEOUT = 3 - minimalmodbus.CLOSE_PORT_AFTER_EACH_CALL=True - self._sh.scheduler.add(__name__, self._read_modbus, prio=5, cycle=int(update_cycle)) + minimalmodbus.CLOSE_PORT_AFTER_EACH_CALL = True + self.scheduler_add(__name__, self._read_modbus, prio=5, cycle=self._update_cycle) self.my_reg_items = [] self.mod_write_repeat = 20 # if port is already open, e.g on auto-update, # repeat mod_write attempt x times a 1 seconds self._lockmb = threading.Lock() # modbus serial port lock self.init_serial_connection(self.serialport, self.slave_address) - self._reg_sets = [{'name':'fan', 'range':range(101, 138+1)}, - {'name':'heater', 'range':range(201, 221+1), 'scaled_signed':range(208, 218+1)}, - {'name':'damper', 'range':range(301, 301+1)}, - {'name':'rotor', 'range':range(351, 352+1)}, - {'name':'week', 'range':range(401, 459+1)}, - {'name':'system', 'range':range(501, 507+1)}, - {'name':'clock', 'range':range(551, 557+1)}, - {'name':'filter', 'range':range(601, 602+1)}, - {'name':'VTC_defr', 'range':range(651, 654+1)}, - {'name':'VTR_defr', 'range':range(671, 672+1)}, - {'name':'dig_in', 'range':range(701, 709+1)}, - {'name':'PCU_PB', 'range':range(751, 751+1)}, - {'name':'alarms', 'range':range(801, 802+1)}, - {'name':'demand', 'range':range(851, 859+1)}, - {'name':'wireless', 'range':range(901, 1020+1)},] + self._reg_sets = [{'name': 'fan', 'range': range(101, 138 + 1)}, + {'name': 'heater', 'range': range(201, 221 + 1), 'scaled_signed': range(208, 218 + 1)}, + {'name': 'damper', 'range': range(301, 301 + 1)}, + {'name': 'rotor', 'range': range(351, 352 + 1)}, + {'name': 'week', 'range': range(401, 459 + 1)}, + {'name': 'system', 'range': range(501, 507 + 1)}, + {'name': 'clock', 'range': range(551, 557 + 1)}, + {'name': 'filter', 'range': range(601, 602 + 1)}, + {'name': 'VTC_defr', 'range': range(651, 654 + 1)}, + {'name': 'VTR_defr', 'range': range(671, 672 + 1)}, + {'name': 'dig_in', 'range': range(701, 709 + 1)}, + {'name': 'PCU_PB', 'range': range(751, 751 + 1)}, + {'name': 'alarms', 'range': range(801, 802 + 1)}, + {'name': 'demand', 'range': range(851, 859 + 1)}, + {'name': 'wireless', 'range': range(901, 1020 + 1)},] def init_serial_connection(self, serialport, slave_address): try: @@ -84,9 +81,9 @@ def _read_modbus(self): for reg_set in self._reg_sets: if 'range_used' in reg_set: read_regs = dict(zip(reg_set['range_used'], self.instrument.read_registers( - reg_set['range_used'].start -1, - reg_set['range_used'].stop - reg_set['range_used'].start, - functioncode = 3))) + reg_set['range_used'].start - 1, + reg_set['range_used'].stop - reg_set['range_used'].start, + functioncode=3))) if 'scaled_signed' in reg_set: for scaled_reg in reg_set['scaled_signed']: read_regs[scaled_reg] = c_short(read_regs[scaled_reg]).value / 10 @@ -100,7 +97,7 @@ def _read_modbus(self): # get coils for coil_addr in self._update_coil: - value = self.instrument.read_bit(coil_addr-1, functioncode=2) + value = self.instrument.read_bit(coil_addr - 1, functioncode=2) if value is not None: for item in self._update_coil[coil_addr]: item(value, 'systemair_value_from_bus', "Coil {}".format(coil_addr)) @@ -123,7 +120,7 @@ def connect(self): def update_item(self, item, caller=None, source=None, dest=None): # ignore values from bus - if caller == 'systemair_value_from_bus': + if caller == 'systemair_value_from_bus' or not self.alive: return if item in self.my_reg_items: if self.has_iattr(item.conf, 'mod_write'): @@ -138,9 +135,9 @@ def parse_item(self, item): for reg_set in self._reg_sets: if modbus_regaddr in reg_set['range']: - if not 'regs_used' in reg_set: + if 'regs_used' not in reg_set: reg_set['regs_used'] = dict() - if not modbus_regaddr in reg_set['regs_used']: + if modbus_regaddr not in reg_set['regs_used']: reg_set['regs_used'][modbus_regaddr] = set() reg_set['regs_used'][modbus_regaddr].add(item) @@ -155,7 +152,7 @@ def parse_item(self, item): if self.has_iattr(item.conf, 'systemair_coiladdr'): modbus_coiladdr = int(self.get_iattr_value(item.conf, 'systemair_coiladdr')) self.logger.debug("systemair_value_from_bus: {0} connected to coil register {1:#04x}".format(item, modbus_coiladdr)) - if not modbus_coiladdr in self._update_coil: + if modbus_coiladdr not in self._update_coil: self._update_coil[modbus_coiladdr] = set() self._update_coil[modbus_coiladdr].add(item) diff --git a/systemair/plugin.yaml b/systemair/plugin.yaml index b1979bc82..98e015e4d 100755 --- a/systemair/plugin.yaml +++ b/systemair/plugin.yaml @@ -12,11 +12,11 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/939623-systemair-modbus-plugin-zentrale-lüftungsanlage - version: 1.3.0.1 # Plugin version - sh_minversion: 1.3 # minimum shNG version to use this plugin + version: 1.3.1 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False # plugin supports multi instance - restartable: unknown + multi_instance: false # plugin supports multi instance + restartable: true classname: Systemair # class containing the plugin parameters: From 3fcbe8329a98cc7d545e32d5947c6a57113da61f Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:04:19 +0100 Subject: [PATCH 4/6] fix init parameters --- comfoair/__init__.py | 2 +- intercom_2n/__init__.py | 2 +- jvcproj/__init__.py | 2 +- kostal/__init__.py | 2 +- luxtronic2/__init__.py | 4 ++-- plex/__init__.py | 2 +- rcswitch/__init__.py | 2 +- roomba_980/__init__.py | 2 +- sma/__init__.py | 2 +- smarttv/__init__.py | 2 +- sml/__init__.py | 2 +- systemair/__init__.py | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/comfoair/__init__.py b/comfoair/__init__.py index 5196fff66..4e72f9eb2 100755 --- a/comfoair/__init__.py +++ b/comfoair/__init__.py @@ -33,7 +33,7 @@ class ComfoAir(SmartPlugin): ALLOW_MULTIINSTANCE = False PLUGIN_VERSION = '1.3.1' - def __init__(self): + def __init__(self, **kwargs): self.logger = logging.getLogger('ComfoAir') self.connected = False self._params = {} diff --git a/intercom_2n/__init__.py b/intercom_2n/__init__.py index a859fe667..1c785e053 100755 --- a/intercom_2n/__init__.py +++ b/intercom_2n/__init__.py @@ -36,7 +36,7 @@ class Intercom2n(SmartPlugin): PLUGIN_VERSION = "1.3.1" ALLOW_MULTIINSTANCE = False - def __init__(self): + def __init__(self, **kwargs): self._intercom_ip = self.get_parameter_value('intercom_ip') self._ssl = self.get_parameter_value('ssl') self._auth_type = self.get_parameter_value('auth_type') diff --git a/jvcproj/__init__.py b/jvcproj/__init__.py index c907cc622..649cea506 100755 --- a/jvcproj/__init__.py +++ b/jvcproj/__init__.py @@ -99,7 +99,7 @@ class JVC_DILA_Control(SmartPlugin): ALLOW_MULTIINSTANCE = False PLUGIN_VERSION = '1.0.2' - def __init__(self): + def __init__(self, **kwargs): """ Initalizes the plugin. The parameters describe for this method are pulled from the entry in plugin.conf. :param host: JVC DILA Projectors IP address diff --git a/kostal/__init__.py b/kostal/__init__.py index f503e0104..ffc2ab3bf 100755 --- a/kostal/__init__.py +++ b/kostal/__init__.py @@ -105,7 +105,7 @@ class Kostal(SmartPlugin): 'l3_watt': 'ac3_w' } - def __init__(self): + def __init__(self, **kwargs): self.ip = self.get_parameter_value('ip') self.user = self.get_parameter_value('user') self.passwd = self.get_parameter_value('passwd') diff --git a/luxtronic2/__init__.py b/luxtronic2/__init__.py index 3116f4e3f..1d0dee433 100755 --- a/luxtronic2/__init__.py +++ b/luxtronic2/__init__.py @@ -58,7 +58,7 @@ class LuxBase(SmartPlugin): # ATTENTION: This is NOT the SmartPlugin class of the plugin!!! - def __init__(self, host, port=8888): + def __init__(self, host, port=8888, **kwargs): self.logger = logging.getLogger(__name__) self.host = host self.port = int(port) @@ -260,7 +260,7 @@ class Luxtronic2(LuxBase): _decoded = {} alive = True - def __init__(self): + def __init__(self, **kwargs): self._is_connected = False self._cycle = self.get_parameter_value('cycle') LuxBase.__init__(self, self.get_parameter_value('host'), self.get_parameter_value('port')) diff --git a/plex/__init__.py b/plex/__init__.py index 243166afe..b21159f38 100755 --- a/plex/__init__.py +++ b/plex/__init__.py @@ -29,7 +29,7 @@ class Plex(SmartPlugin): PLUGIN_VERSION = "1.0.1" ALLOW_MULTIINSTANCE = False - def __init__(self): + def __init__(self, **kwargs): self._displayTime = self.get_parameter_value('displaytime') self.logger.info("Init Plex notifications") self._images = ["info", "error", "warning"] diff --git a/rcswitch/__init__.py b/rcswitch/__init__.py index 389df1fec..0198b4321 100755 --- a/rcswitch/__init__.py +++ b/rcswitch/__init__.py @@ -35,7 +35,7 @@ class RCswitch(SmartPlugin): ALLOW_MULTIINSTANCE = False PLUGIN_VERSION = "1.2.2" - def __init__(self): + def __init__(self, **kwargs): self.logger = logging.getLogger(__name__) self.setupOK = True self.mapping = {'a': 1, 'A': 1, 'b': 2, 'B': 2, 'c': 3, 'C': 3, 'd': 4, 'D': 4, 'e': 5, 'E': 5} diff --git a/roomba_980/__init__.py b/roomba_980/__init__.py index 036d1a035..b43738a7f 100755 --- a/roomba_980/__init__.py +++ b/roomba_980/__init__.py @@ -31,7 +31,7 @@ class ROOMBA_980(SmartPlugin): myroomba = None - def __init__(self): + def __init__(self, **kwargs): self._address = self.get_parameter_value('adress') self._blid = self.get_parameter_value('blid') self._roombaPassword = self.get_parameter_value('roombaPassword') diff --git a/sma/__init__.py b/sma/__init__.py index 82d224a9e..a70879e32 100755 --- a/sma/__init__.py +++ b/sma/__init__.py @@ -186,7 +186,7 @@ class SMA(SmartPlugin): ALLOW_MULTIINSTANCE = False PLUGIN_VERSION = "1.3.2" - def __init__(self): + def __init__(self, **kwargs): # TODO: self._own_bt_addr setzen self.logger = logging.getLogger(__name__) self._inv_bt_addr = self.get_parameter_value('bt_addr') diff --git a/smarttv/__init__.py b/smarttv/__init__.py index 654f13475..bb90f1a44 100755 --- a/smarttv/__init__.py +++ b/smarttv/__init__.py @@ -33,7 +33,7 @@ class SmartTV(SmartPlugin): ALLOW_MULTIINSTANCE = True PLUGIN_VERSION = "1.3.3" - def __init__(self): + def __init__(self, **kwargs): self.logger = logging.getLogger(__name__) self._tv_version = self.get_parameter_value('tv_version') self._host = self.get_parameter_value('host') diff --git a/sml/__init__.py b/sml/__init__.py index 2e93df094..b1eb80af4 100755 --- a/sml/__init__.py +++ b/sml/__init__.py @@ -52,7 +52,7 @@ class Sml(SmartPlugin): 'smart-meter-gateway-com-1': 'hex' } - def __init__(self): + def __init__(self, **kwargs): self.host = self.get_parameter_value('host') self.port = self.get_parameter_value('port') self.serialport = self.get_parameter_value('serialport') diff --git a/systemair/__init__.py b/systemair/__init__.py index 24a40b8e5..df353ccea 100755 --- a/systemair/__init__.py +++ b/systemair/__init__.py @@ -30,7 +30,7 @@ class Systemair(SmartPlugin): PLUGIN_VERSION = "1.3.1" ALLOW_MULTIINSTANCE = False - def __init__(self): + def __init__(self, **kwargs): self.instrument = None self.serialport = self.get_parameter_value('serialport') self.slave_address = self.get_parameter_value('slave_address') From 6fb4ee30a950c54f7327be07e8fbd29b5b143531 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:55:57 +0100 Subject: [PATCH 5/6] remove unnecessary logging setup --- comfoair/__init__.py | 2 -- sma/__init__.py | 2 -- sml/__init__.py | 2 -- 3 files changed, 6 deletions(-) diff --git a/comfoair/__init__.py b/comfoair/__init__.py index 4e72f9eb2..382495e72 100755 --- a/comfoair/__init__.py +++ b/comfoair/__init__.py @@ -18,7 +18,6 @@ # along with this plugin. If not, see . ######################################################################### -import logging import socket import time import serial @@ -34,7 +33,6 @@ class ComfoAir(SmartPlugin): PLUGIN_VERSION = '1.3.1' def __init__(self, **kwargs): - self.logger = logging.getLogger('ComfoAir') self.connected = False self._params = {} self._init_cmds = [] diff --git a/sma/__init__.py b/sma/__init__.py index a70879e32..7b6cd32d6 100755 --- a/sma/__init__.py +++ b/sma/__init__.py @@ -36,7 +36,6 @@ # the use or application of the software. ######################################################################### -import logging import threading import time import socket @@ -188,7 +187,6 @@ class SMA(SmartPlugin): def __init__(self, **kwargs): # TODO: self._own_bt_addr setzen - self.logger = logging.getLogger(__name__) self._inv_bt_addr = self.get_parameter_value('bt_addr') self._inv_password = self.get_parameter_value('password') self._update_cycle = self.get_parameter_value('update_cycle') diff --git a/sml/__init__.py b/sml/__init__.py index b1eb80af4..bf08ad475 100755 --- a/sml/__init__.py +++ b/sml/__init__.py @@ -20,7 +20,6 @@ # along with SmartHomeNG. If not, see . ######################################################################### -import logging import time import re import serial @@ -65,7 +64,6 @@ def __init__(self, **kwargs): self._dataoffset = 0 self._items = {} self._lock = threading.Lock() - self.logger = logging.getLogger(__name__) if self._device in self._devices: self._device = self._devices[self._device] From bc8af66e9699d56b768f93856b659280d69b802a Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:56:56 +0100 Subject: [PATCH 6/6] more logging cleanup --- intercom_2n/__init__.py | 2 -- rcswitch/__init__.py | 2 -- smarttv/__init__.py | 2 -- 3 files changed, 6 deletions(-) diff --git a/intercom_2n/__init__.py b/intercom_2n/__init__.py index 1c785e053..5956279c8 100755 --- a/intercom_2n/__init__.py +++ b/intercom_2n/__init__.py @@ -20,7 +20,6 @@ ######################################################################### import json -import logging import os from time import sleep import requests @@ -44,7 +43,6 @@ def __init__(self, **kwargs): self._password = self.get_parameter_value('password') self.is_stopped = False self.sid = None - self._logger = logging.getLogger(__name__) self.event_timeout = 30 self.ip_cam = IPCam(self._intercom_ip, ssl=self._ssl, auth_type=self._auth_type, user=self._username, password=self._password) diff --git a/rcswitch/__init__.py b/rcswitch/__init__.py index 0198b4321..8446f8303 100755 --- a/rcswitch/__init__.py +++ b/rcswitch/__init__.py @@ -19,7 +19,6 @@ # along with SmartHomeNG. If not, see . ######################################################################### -import logging from lib.model.smartplugin import SmartPlugin from socket import gethostname import time @@ -36,7 +35,6 @@ class RCswitch(SmartPlugin): PLUGIN_VERSION = "1.2.2" def __init__(self, **kwargs): - self.logger = logging.getLogger(__name__) self.setupOK = True self.mapping = {'a': 1, 'A': 1, 'b': 2, 'B': 2, 'c': 3, 'C': 3, 'd': 4, 'D': 4, 'e': 5, 'E': 5} self.rcswitch_dir = self.get_parameter_value('rcswitch_dir') diff --git a/smarttv/__init__.py b/smarttv/__init__.py index bb90f1a44..8b3afd9c9 100755 --- a/smarttv/__init__.py +++ b/smarttv/__init__.py @@ -19,7 +19,6 @@ # along with SmartHomeNG. If not, see . # -import logging import socket import time import base64 @@ -34,7 +33,6 @@ class SmartTV(SmartPlugin): PLUGIN_VERSION = "1.3.3" def __init__(self, **kwargs): - self.logger = logging.getLogger(__name__) self._tv_version = self.get_parameter_value('tv_version') self._host = self.get_parameter_value('host') self._port = self.get_parameter_value('port')