Hi!
Anbei ein Plgin für smarthome.py, welches über ein am PC angeschlossenes Bluetooth-Dongle einen SMA-Wechselrichter auslesen kann. Unterstützt werden erstmal nur
- Tagesertrag
- Gesamtertrag
- momentane Einspeiseleistung
- weitere (Neben-)Sachen wie Seriennummer, BT-Adresse, Signalstärke der Bluetooth-Verbindung ...
plugin.conf
bt_addr => Bluetooth-Adresse des Wechselrichters (per hcitool scan)
password => Wechselrichter-Passwort - Standard ist 0000
update_cycle => Wie häufig werden (alle!) Werte neu gelesen (60s ist ganz ok)
Beispielitems:
__init__.py (im Ordner /plugins/sma/)
Das Plugin sollte halbwegs stabil laufen, allerdings wären ein paar try-except-Blöcke ganz sinnvoll. Zudem gibt es noch mehr Datenfelder zu lesen - da hab ich bisher keine Muße für gehabt. Interessant wären wohl noch (zwei) Strings auf der DC-Seite.
Grüße
Robert
Anbei ein Plgin für smarthome.py, welches über ein am PC angeschlossenes Bluetooth-Dongle einen SMA-Wechselrichter auslesen kann. Unterstützt werden erstmal nur
- Tagesertrag
- Gesamtertrag
- momentane Einspeiseleistung
- weitere (Neben-)Sachen wie Seriennummer, BT-Adresse, Signalstärke der Bluetooth-Verbindung ...
plugin.conf
Code:
[sma] class_name = SMA class_path = plugins.sma bt_addr = 00:80:25:xx:xx:xx password = 0000 update_cycle = 60
password => Wechselrichter-Passwort - Standard ist 0000
update_cycle => Wie häufig werden (alle!) Werte neu gelesen (60s ist ganz ok)
Beispielitems:
Code:
[Wechselrichter] [[Einspeiseleistung]] type = num visu = yes history = true sma = "AC_POWER" knx_dpt = 12 knx_send = 5/4/50 knx_reply = 5/4/50 [[Tagesertrag]] type = num visu = yes history = true sma = "DAY_YIELD" knx_dpt = 12 knx_send = 5/4/51 knx_reply = 5/4/51 [[Gesamtertrag]] type = num visu = yes history = true sma = "TOTAL_YIELD" knx_dpt = 12 knx_send = 5/4/52 knx_reply = 5/4/52 [[Seriennummer]] type = num visu = yes sma = "INV_SERIAL" knx_dpt = 12 knx_send = 5/4/53 knx_reply = 5/4/53 [[MAC_Adresse]] type = str visu = yes sma = "INV_ADDRESS" knx_dpt = 16000 knx_send = 5/4/54 knx_reply = 5/4/54 [[Letzte_Aktualisierung]] type = str visu = yes sma = "LAST_UPDATE" knx_dpt = 16000 knx_send = 5/4/55 knx_reply = 5/4/55
Code:
#!/usr/bin/env python # vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab ######################################################################### # Copyright 2013 Robert Budde robert@projekt131.de ######################################################################### # This software is based on Stuart Pittaway's "NANODE SMA PV MONITOR" # https://github.com/stuartpittaway/nanodesmapvmonitor # # SMA-Plugin for SmartHome.py. http://smarthome.sourceforge.net/ # # This plugin is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This plugin is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this plugin. If not, see <http://www.gnu.org/licenses/>. ######################################################################### from bluetooth import * import struct import logging import time from datetime import datetime from dateutil import tz BCAST_ADDR = [0xFF,0xFF,0xFF,0xFF,0xFF,0xFF] ZERO_ADDR = [0x00,0x00,0x00,0x00,0x00,0x00] SMANET2_HDR = [0x7E,0xFF,0x03,0x60,0x65] FCSTAB = [ 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, \ 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, \ 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, \ 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, \ 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, \ 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, \ 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, \ 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, \ 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, \ 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, \ 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, \ 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, \ 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, \ 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, \ 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, \ 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, \ 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, \ 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, \ 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, \ 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, \ 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, \ 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, \ 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, \ 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, \ 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, \ 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, \ 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, \ 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, \ 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, \ 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, \ 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, \ 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 ] TOTAL_YIELD = (0x2601, 0xA009, 0x54) DAY_YIELD = (0x2622, 0xA009, 0x54) AC_POWER = (0x263F, 0xA109, 0x51) logger = logging.getLogger('') class SMA(): def __init__(self, smarthome, bt_addr, password="0000", update_cycle="60"): self._sh = smarthome self._val = {} self._inv_bt_addr = bt_addr self._inv_password = password self._inv_last_read_datetime = datetime.fromtimestamp(0, tz.tzlocal()) self._inv_last_read = self._inv_last_read_datetime.strftime("%d.%m.%Y %H:%M:%S") self._own_bt_addr_le = BCAST_ADDR self._btsocket = BluetoothSocket( RFCOMM ) if not (is_valid_address(self._inv_bt_addr)): logger.warning("sma: inverter bluetooth address is invalid: %s" % self._inv_bt_addr) return self._inv_bt_name = lookup_name(self._inv_bt_addr, timeout=5) if (self._inv_bt_name == None): logger.warning("sma: inverter bluetooth name could not be looked up") self._inv_bt_name = "unknown" self._btsocket.connect((self._inv_bt_addr, 1)) logger.debug("sma: via bluetooth connected to %s (%s)" % (self._inv_bt_name, self._inv_bt_addr)) self._inv_connect() self._inv_login() self._sh.scheduler.add('sma.update', self._update_values, prio=5, cycle=int(update_cycle)) def _update_values(self): #logger.warning("sma: signal strength = %d%%" % self._inv_get_bt_signal_strength()) value = self._inv_get_3byte_value(AC_POWER) if (value >= 0): logger.debug("sma: current AC power = %dW" % value) if 'AC_POWER' in self._val: for item in self._val['AC_POWER']['items']: item(value, 'SMA', self._inv_bt_addr) else: logger.warning("sma: could not read current AC power!") value = self._inv_get_3byte_value(DAY_YIELD) if (value >= 0): logger.debug("sma: day yield = %dWh" % value) if 'DAY_YIELD' in self._val: for item in self._val['DAY_YIELD']['items']: item(value, 'SMA', self._inv_bt_addr) else: logger.warning("sma: could not read day yield!") value = self._inv_get_3byte_value(TOTAL_YIELD) if (value >= 0): logger.debug("sma: total yield = %dWh" % value) if 'TOTAL_YIELD' in self._val: for item in self._val['TOTAL_YIELD']['items']: item(value, 'SMA', self._inv_bt_addr) else: logger.warning("sma: could not read total yield!") logger.debug("sma: last read = %s" % self._inv_last_read) if 'LAST_UPDATE' in self._val: for item in self._val['LAST_UPDATE']['items']: item(self._inv_last_read, 'SMA', self._inv_bt_addr) def run(self): self.alive = True def stop(self): self.alive = False try: self._btsocket.close() self._btsocket = False except: pass def parse_item(self, item): if 'sma' in item.conf: sma_value = item.conf['sma'] logger.debug("sma: {0} connected to value of {1}".format(item, sma_value)) if not sma_value in self._val: self._val[sma_value] = {'items': [item], 'logics': []} else: if not item in self._val[sma_value]['items']: self._val[sma_value]['items'].append(item) if (sma_value == 'OWN_ADDRESS'): item(self._own_bt_addr, 'SMA', self._inv_bt_addr) if (sma_value == 'INV_ADDRESS'): item(self._inv_bt_addr, 'SMA', self._inv_bt_addr) if (sma_value == 'INV_SERIAL'): item(self._inv_serial, 'SMA', self._inv_bt_addr) # return None to indicate "read-only" return None def parse_logic(self, logic): if 'sma' in item.conf: sma_value = item.conf['sma'] logger.debug("sma: {0} connected to value of {1}".format(item, sma_value)) if not sma_value in self._val: self._val[sma_value] = {'items': [], 'logics': [item]} else: if not item in self._val[sma_value]['logics']: self._val[sma_value]['logics'].append(item) # receive function both for single and multi-part messages def _recv_msg(self, timeout=1.0): # wait for sfd recv_char = 0 self._btsocket.settimeout(timeout) while (recv_char != chr(0x7E)): recv_char = self._btsocket.recv(1) if (recv_char == None): logger.warning("sma: rx: could not receive SFD within %ds" % timeout) return [] msg = [ord(recv_char)] # get level 1 length and validate while (len(msg) < 4): recv_char = self._btsocket.recv(1) if (recv_char == None): logger.warning("sma: rx: could not receive complete length field within %ds" % timeout) return [] msg += [ord(recv_char)] if ((msg[1] ^ msg[2] ^ msg[3]) != 0x7E): logger.warning("sma: rx: length fields invalid") return [] length = (msg[2] << 8) + msg[1] if (length < 18): logger.warning("sma: rx: length to small: %d" % length) return [] # get remaining characters while (len(msg) < length): # by reading as much as possible we are also receiving follow-up msgs (to clear stucked msgs - no better solution yet)! recv_char = self._btsocket.recv(1000) if (recv_char == None): logger.warning("sma: rx: could not receive msg body within %ds" % timeout) return [] msg += [ord(i) for i in recv_char]; #logger.debug("sma: rx: len=%d/%d" % (len(msg), length)) if (len(msg) > length): logger.warning("sma: rx: dismissing follow-up data: %s" % ' '.join(['0x%02x' % b for b in msg[length::]])) msg = msg[0:length] # check src and dst addr and check if (msg[4:10] != self._inv_bt_addr_le): logger.warning("sma: rx: unknown src addr") return [] if (msg[10:16] != self._own_bt_addr_le) and (msg[10:16] != ZERO_ADDR) and (msg[10:16] != BCAST_ADDR): logger.warning("sma: rx: wrong dst addr") return [] # 18 (SMANET1) + 4 (SMANET2+ HDR) + 2 + 6 (SRC) + 2 + 6 (DST) + 6 + 1 (CNT) + 2 (CRC) + 1 (TAIL) if (length >= 48) and (msg[18:23] == SMANET2_HDR): #logger.debug("sma: smanet2+ msg") # remove escape characters after SMANET2+ HDR i = 22 while (i < length): if (msg[i] == 0x7d): del msg[i] msg[i] ^= 0x20 length -= 1 i += 1 #logger.debug("sma: escape chars removed") crc = self._calc_crc16(msg[19:-3]) if (((crc>>8) != msg[-2]) or ((crc&0xFF) != msg[-3])): logger.warning("sma: crc: crc16 error - %04x" % crc) logger.warning("sma: crc: len=%d %s\n" % (len(msg), ' '.join(['0x%02x' % b for b in msg]))) return [] #print "rx: len=%d %s\n" % (len(msg), ' '.join(['0x%02x' % b for b in msg])) return msg def _recv_msg_with_cmdcode(self, cmdcodes_expected): retries = 10 while (retries > 0): retries -= 1 msg = self._recv_msg() # get cmdcode if (msg != []) and (((msg[17] << 8) + msg[16]) in cmdcodes_expected): break if (retries == 0): logger.warning("sma: recv msg with cmdcode - retries used up!") return [] return msg def _send_msg(self, msg): if (len(msg) >= 0x3a): # calculate crc starting with byte 19 and append with LE byte-oder crc = self._calc_crc16(msg[19::]) msg += [crc & 0xff, (crc >> 8) & 0xff] # add escape sequences starting with byte 19 msg = msg[0:19] + self._add_escapes(msg[19::]) # add msg delimiter msg += [0x7e] # set length fields - msg[1] is exact overall length, msg[3] = 0x73-msg[1] msg[1] = len(msg) & 0xff msg[2] = (len(msg) >> 8) & 0xff msg[3] = msg[1] ^ msg[2] ^ 0x7e #print "tx: len=%d %s\n" % (len(msg), ' '.join(['0x%02x' % b for b in msg])) send = '' for i in msg: send += chr(i) self._btsocket.send(send) def _calc_crc16(self, msg): crc = 0xFFFF for i in msg: crc = (crc>>8) ^ FCSTAB[(crc ^ i) & 0xFF] crc ^= 0xFFFF #print("crc16 = %x") % crc return crc def _check_crc(self, msg): crc = self._calc_crc16(msg[19:-3]) if (((crc>>8) != msg[-2]) or ((crc&0xFF) != msg[-3])): logger.debug("sma: crc: crc16 error - %04x" % crc) logger.debug("sma: crc: len=%d %s\n" % (len(msg), ' '.join(['0x%02x' % b for b in msg]))) return False return True def _add_escapes(self, msg): escaped = [] for i in msg: if (i == 0x7d) or (i == 0x7e) or (i == 0x11) or (i == 0x12) or (i == 0x13): escaped += [0x7d, i^0x20] else: escaped += [i]; return escaped def _inv_connect(self): self._send_count = 0 self._inv_bt_addr_le = [int(x, 16) for x in self._inv_bt_addr.split(':')] self._inv_bt_addr_le = self._inv_bt_addr_le[::-1] # receive broadcast-msg from inverter msg = self._recv_msg_with_cmdcode([0x0002]); # extract net-id from the 0x0002 msg self._net_id = msg[22] # reply with wildcard src addr msg[4:10] = ZERO_ADDR msg[10:16] = self._inv_bt_addr_le self._send_msg(msg) # receive msg from inverter msg = self._recv_msg_with_cmdcode([0x000a]); # receive msg from inverter msg = self._recv_msg_with_cmdcode([0x0005,0x000c]); # receive msg from inverter msg = self._recv_msg_with_cmdcode([0x0005]); # extract own bluetooth addr self._own_bt_addr_le = msg[26:32] logger.debug("sma: own bluetooth address: %s" % ':'.join(['%02x' % b for b in self._own_bt_addr_le[::-1]])) # first SMA net2 msg retries = 10 while (retries > 0): retries -= 1 # level1 cmdcode = 0x0001 msg = [0x7E,0,0,0] + self._own_bt_addr_le + self._inv_bt_addr_le + [cmdcode&0xFF,(cmdcode>>8)&0xFF] # sma-net2 level ctrl = 0xA009 self._send_count += 1 if (self._send_count > 75): self._send_count = 1 msg += SMANET2_HDR + [ctrl&0xFF,(ctrl>>8)&0xFF] + BCAST_ADDR + [0x00,0x00] + self._inv_bt_addr_le + [0x00] + [0x00] + [0,0,0,0] + [self._send_count] msg += [0x80,0x00,0x02,0x00] + [0x00] + [0x00,0x00,0x00,0x00] + [0x00,0x00,0x00,0x00] # send msg to inverter self._send_msg(msg) # receive msg from inverter msg = self._recv_msg_with_cmdcode([0x0001]); if (msg != []): break; if (retries == 0): logger.warning("sma: connect - retries used up!") return # second SMA net2 msg cmdcode = 0x0001 msg = [0x7E, 0x00, 0x00, 0x00] + self._own_bt_addr_le + self._inv_bt_addr_le + [cmdcode&0xFF,(cmdcode>>8)&0xFF] # sma-net2 level ctrl = 0xA008 self._send_count += 1 if (self._send_count > 75): self._send_count = 1 msg += SMANET2_HDR + [ctrl&0xFF,(ctrl>>8)&0xFF] + BCAST_ADDR + [0x00,0x03] + self._inv_bt_addr_le + [0x00] + [0x03] + [0,0,0,0] + [self._send_count] msg += [0x80,0x0E,0x01,0xFD,0xFF,0xFF,0xFF,0xFF,0xFF] # send msg self._send_msg(msg) def _inv_login(self): timestamp_utc = int(time.time()) password_pattern = [0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88] password_pattern[0:len(self._inv_password)] = [((0x88 + ord(char)) & 0xff) for char in self._inv_password] retries = 10 while (retries > 0): retries -= 1 # level1 cmdcode = 0x0001 msg = [0x7E,0,0,0] + self._own_bt_addr_le + self._inv_bt_addr_le + [cmdcode&0xFF,(cmdcode>>8)&0xFF] # sma-net2 level ctrl = 0xA00E self._send_count += 1 if (self._send_count > 75): self._send_count = 1 msg += SMANET2_HDR + [ctrl&0xFF,(ctrl>>8)&0xFF] + BCAST_ADDR + [0x00,0x01] + self._inv_bt_addr_le + [0x00] + [0x01] + [0,0,0,0] + [self._send_count] msg += [0x80, 0x0C, 0x04, 0xFD, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00] msg += [timestamp_utc & 0xff, (timestamp_utc >> 8) & 0xff, (timestamp_utc >> 16) & 0xff, (timestamp_utc >> 24) & 0xff] msg += [0x00, 0x00, 0x00, 0x00] + password_pattern # send msg to inverter self._send_msg(msg) # send msg to inverter self._send_msg(msg) # receive msg from inverter msg = self._recv_msg_with_cmdcode([0x0001]); if (msg != []): break; if (retries == 0): logger.warning("sma: login - retries used up!") return # extract serial self._inv_serial = (msg[38] << 24) + (msg[37] << 16) + (msg[36] << 8) + msg[35] logger.debug("sma: inverter serial = %d" % self._inv_serial) def _inv_get_bt_signal_strength(self): cmdcode = 0x0003 msg = [0x7E,0,0,0] + self._own_bt_addr_le + self._inv_bt_addr_le + [cmdcode&0xFF,(cmdcode>>8)&0xFF] msg += [0x05, 0x00] self._send_msg(msg) msg = self._recv_msg_with_cmdcode([0x0004]) # extract signal strength return ((msg[22] * 100.0)/0xff) def _inv_get_value(self, value_set): retries = 10 while (retries > 0): retries -= 1 # level1 cmdcode = 0x0001 msg = [0x7E,0,0,0] + self._own_bt_addr_le + self._inv_bt_addr_le + [cmdcode&0xFF,(cmdcode>>8)&0xFF] # sma-net2 level self._send_count += 1 if (self._send_count > 75): self._send_count = 1 msg += SMANET2_HDR + [value_set[1]&0xFF,(value_set[1]>>8)&0xFF] + BCAST_ADDR + [0x00,0x00] + self._inv_bt_addr_le + [0x00] + [0x00] + [0,0,0,0] + [self._send_count] msg += [0x80,0x00,0x02,0x00] + [value_set[2]] + [0x00] + [value_set[0]&0xFF,(value_set[0]>>8)&0xFF] + [0x00, 0xFF] + [value_set[0]&0xFF,(value_set[0]>>8)&0xFF] + [0x00] # send msg to inverter self._send_msg(msg) #print "tx: len=%d %s\n" % (len(msg), ' '.join(['0x%02x' % b for b in msg])) # receive msg from inverter msg = self._recv_msg_with_cmdcode([cmdcode]); if (len(msg) >= 63): # extract valuetype recv_value_code = (msg[61]<<8)+msg[60] if (len(msg) >= 63) and (recv_value_code == value_set[0]): break else: logger.warning("sma: recv unrequested msg!") logger.warning("sma: len=%d %s\n" % (len(msg), ' '.join(['0x%02x' % b for b in msg]))) if (retries == 0): logger.warning("sma: get value - retries used up!") return [] # extract time(utc) self._inv_last_read_timestamp_utc = (msg[66]<<24)+(msg[65]<<16)+(msg[64]<<8)+msg[63] self._inv_last_read_datetime = datetime.fromtimestamp(self._inv_last_read_timestamp_utc, tz.tzlocal()) self._inv_last_read = self._inv_last_read_datetime.strftime("%d.%m.%Y %H:%M:%S") return msg def _inv_get_3byte_value(self, value_set): msg = self._inv_get_value(value_set) if (msg): return (msg[69]<<16)+(msg[68]<<8)+msg[67] else: return -1 if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) myplugin = Plugin('SMA') myplugin.run()
Grüße
Robert
Kommentar