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: