├── graph.gv.pdf ├── graph.command ├── README.md ├── PrintDatapoints.py ├── VitosoftWLANServer.py ├── graph.gv ├── vcontrold_test.py ├── PrintEventTypes.py ├── VitosoftCommunication.md ├── VitosoftSoftware.md ├── PrintEventsForDatapoint.py ├── VitosoftXML.md └── Viessmann2MQTT.py /graph.gv.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarnau/InsideViessmannVitosoft/HEAD/graph.gv.pdf -------------------------------------------------------------------------------- /graph.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CMDPATH="`dirname \"$0\"`" 4 | cd "$CMDPATH" || exit 1 5 | 6 | dot graph.gv -Tpdf -O 7 | open graph.gv.pdf 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inside Viessmann Vitosoft 2 | 3 | ## Documentation 4 | 5 | - [Vitosoft Software](./VitosoftSoftware.md) 6 | - [Vitosoft Communication](./VitosoftCommunication.md) 7 | - [Vitosoft XML](./VitosoftXML.md) 8 | 9 | ## Sample code 10 | 11 | All Print-sample code requires XML files from the Vitosoft software inside the `data` folder! 12 | 13 | - [PrintDatapoints.py](PrintDatapoints.py) Prints all supported data points (heating units) 14 | - [PrintEventsForDatapoint.py](PrintEventsForDatapoint.py) Prints all events for a specific heating unit, sorted by groups 15 | - [PrintEventTypes.py](PrintEventTypes.py) Prints all event types in a readable form. Combined with the two scripts above you can get all information on how to read specific values from your heating system 16 | - [vcontrold_test.py](vcontrold_test.py) If you have vcontrold already installed on a Raspberry Pi, you can use this script to read specific events directly without adopting the `vito.xml` file in vcontrold to match your heating unit. 17 | - [Viessmann2MQTT.py](Viessmann2MQTT.py) A script to be run on e.g. a Raspberry Pi with Optolink. It polls a list of events (look at the source code – they need to be adopted to your heating unit!) and sends them via MQTT. 18 | - [VitosoftWLANServer.py](VitosoftWLANServer.py) A script to be run on e.g. a Raspberry Pi with Optolink. It implements a Vitosoft compatible WLAN server. This requires the routing tables on the Raspberry Pi to be set up for WLAN, etc. Complicated, the script is also a hack. Feel free to experiment with it, if you know what you are doing. 19 | -------------------------------------------------------------------------------- /PrintDatapoints.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from xml.etree import ElementTree as etree 5 | import pprint 6 | import sys 7 | 8 | pp = pprint.PrettyPrinter(width=200,compact=True) 9 | 10 | # This path needs to be adjusted to point to a directory for all XML files 11 | DATAPATH = "../data/" 12 | 13 | def print_allDataPoints(): 14 | # print supported Optolink systems (at least remove all MBus, LON, etc ones) 15 | evnDataPointTypes = {} 16 | def parse_ecnDataPointType(): 17 | for nodes in etree.parse(DATAPATH + "ecnDataPointType.xml").getroot(): 18 | dataPointType = {} 19 | for cell in nodes: 20 | value = cell.text 21 | if cell.tag in ['Description','EventOptimisationExceptionList','EventOptimisation','Options','ErrorType']: 22 | continue 23 | if cell.tag in ['ControllerType','ErrorType','EventOptimisation']: 24 | value = int(value) 25 | if cell.tag in ['EventOptimisation']: 26 | value = value != 0 27 | dataPointType[cell.tag] = value 28 | ID = dataPointType['ID'].strip() 29 | if len(ID)==0: 30 | continue 31 | del dataPointType['ID'] 32 | evnDataPointTypes[ID] = dataPointType 33 | parse_ecnDataPointType() 34 | 35 | # print it in a readable form 36 | lines = [] 37 | for ID,dataPointType in evnDataPointTypes.items(): 38 | if 'Identification' not in dataPointType: 39 | continue 40 | if not dataPointType['Identification'].startswith('20'): # ignore non-heating systems 41 | continue 42 | if 'IdentificationExtension' in dataPointType and len(dataPointType['IdentificationExtension'])!=4: 43 | continue 44 | idStr = 'ecnsysDeviceIdent:' + dataPointType['Identification'] 45 | if 'IdentificationExtension' in dataPointType: 46 | idStr += ' sysHardware/SoftwareIndexIdent:' + dataPointType['IdentificationExtension'] 47 | if 'IdentificationExtensionTill' in dataPointType: 48 | idStr += '-' + dataPointType['IdentificationExtensionTill'] 49 | if 'F0' in dataPointType: 50 | idStr += ' ecnsysDeviceIdentF0:' + dataPointType['F0'] 51 | if 'F0Till' in dataPointType: 52 | idStr += '-' + dataPointType['F0Till'] 53 | lines.append('%s : %s' % (idStr,ID)) 54 | lines.sort() 55 | for l in lines: 56 | print(l) 57 | 58 | if __name__ == "__main__": 59 | print_allDataPoints() 60 | -------------------------------------------------------------------------------- /VitosoftWLANServer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import socket 5 | import sys 6 | import serial 7 | import time 8 | import binascii 9 | 10 | # This script can run on e.g. a Raspberry Pi that is configured to be configured as a WLAN 11 | # access point, while the Ethernet port is configured as a started network port. 12 | 13 | # It is just an example and does not work reliably. Feel free to improve it. 14 | 15 | # Create a TCP/IP socket 16 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 | 18 | # Bind the socket to the port 19 | server_address = ('10.45.161.1', 45317) 20 | print >>sys.stderr, 'starting up on %s:%s' % server_address 21 | sock.bind(server_address) 22 | 23 | # Listen for incoming connections 24 | sock.listen(1) 25 | 26 | ser = serial.Serial( 27 | port='/dev/ttyUSB0', 28 | baudrate=4800, 29 | parity=serial.PARITY_EVEN, 30 | stopbits=serial.STOPBITS_TWO, 31 | bytesize=serial.EIGHTBITS 32 | ) 33 | 34 | print("SEND EOT") 35 | ser.write(binascii.unhexlify('04')) 36 | if ser.read(1)==binascii.unhexlify('05'): 37 | ser.write(binascii.unhexlify('160000')) 38 | print(binascii.hexlify(ser.read(1))) 39 | print("START") 40 | 41 | while True: 42 | # Wait for a connection 43 | print >>sys.stderr, 'waiting for a connection' 44 | connection, client_address = sock.accept() 45 | 46 | try: 47 | print >>sys.stderr, 'connection from', client_address 48 | 49 | while True: 50 | data = connection.recv(8) 51 | print >>sys.stderr, 'OptoLink > %s' % " ".join("{:02x}".format(ord(c)) for c in data) 52 | if data: 53 | # print >>sys.stderr, 'serial send' 54 | ser.write(data) 55 | # print >>sys.stderr, 'serial wait' 56 | time.sleep(1/(4800/12)*1.2) 57 | out = '' 58 | while ser.in_waiting > 0: 59 | out += ser.read(1) 60 | time.sleep(1/(4800/12)*1.2) 61 | if out != '': 62 | print >>sys.stderr, 'OptoLink < %s' % " ".join("{:02x}".format(ord(c)) for c in out) 63 | connection.sendall(out) 64 | else: 65 | print >>sys.stderr, 'no more data from', client_address 66 | break 67 | finally: 68 | # Clean up the connection 69 | connection.close() 70 | -------------------------------------------------------------------------------- /graph.gv: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=record, style=rounded]; 3 | rankdir=LR; 4 | 5 | ecnDeviceType [ label="{ ecnDeviceType|vendors}|{Id|54 – only used Id}|{Name|Viessmann Anlage}|{Description|-}|{Manufacturer|-}|StatusEventTypeId|{TechnicalIdentificationAddress|?}", style=bold ]; 6 | ecnDeviceTypeDataPointTypeLink [ label="{ ecnDeviceTypeDataPointTypeLink|1:n|vendor to systems}|DeviceTypeId|DataPointTypeId", style=dashed ]; 7 | ecnDatapointType [ label="{ ecnDatapointType|systems}|{Id|unique ID > 1}|{Name|localizable name}|{Description|localizable description}|StatusEventTypeId|{Address|ref to ecnDataPointType.xml}", style=bold ]; 8 | ecnDataPointTypeEventTypeLink [ label="{ ecnDataPointTypeEventTypeLink|1:n|system to events}|DataPointTypeId|EventTypeId", style=dashed ]; 9 | ecnEventType [ label="{ ecnEventType|individual events}|{Id|unique ID > 1}|{Name|localizable name}|{Description|localizable description}|{Address|ref to ecnEventType.xml}|{EnumType|true/false}|{Conversion|value conversion}|{Priority|50/100}|{Filtercriterion|always true}|{Reportingcriterion|always true}|{Type|1=Read-only, 2=Read/Write, 3=Write-only}|{URL|URL for custom editor}|DefaultValue", style=bold ]; 10 | ecnEventTypeEventValueTypeLink [ label="{ ecnEventTypeEventValueTypeLink|1:n|Events to values}|EventTypeId|EventValueId", style=dashed ]; 11 | ecnEventValueType [ label="{ ecnEventValueType|specific values for an event}|{Id|unique ID > 1}|{Name|localizable name}|{Description|localizable description}|{EnumAddressValue|matching enum value}|{EnumReplaceValue|localizable text replacement for an value}|StatusTypeId|{Unit|reference to Textresource_XX.xml}|{DataType|Binary,Bit,DateTime,Float,Int,NText,VarChar}|{Stepping|optional, default=1}|{LowerBorder|optional, default=0}|{UpperBorder|optional}", style=bold ]; 12 | ecnEventTypeEventTypeGroupLink [ label="{ ecnEventTypeEventTypeGroupLink|1:n|Events to groups}|EventTypeId|EventTypeGroupId|{EventTypeOrder|sorting order}", style=dashed ]; 13 | 14 | ecnEventTypeGroup [ label="{ ecnEventTypeGroup|group hierarchy of events}|{Id|unique ID > 1}|{Name|localizable name}|{ParentId|-1=no parent}|{EntrancePoint|true/false}|{Address|ref to ecnEventTypeGroup.xml}|{DeviceTypeId|ref to vendor}|{DataPointTypeId|ref to system}|{OrderIndex|sorting order}" ]; 15 | 16 | ecnDisplayConditionGroup [ label="{ ecnDisplayConditionGroup}|{Id|unique ID > 1}|{Name|localizable name}|{Description|localizable description}|{Type|1=AND,2=OR}|{ParentId|if dest is set: -1=no parent}|{EventTypeIdDest|-1 = unused}|{EventTypeGroupIdDest|-1 = unused}" ]; 17 | 18 | ecnDisplayCondition [ label="{ ecnDisplayCondition}|{Id|unique ID > 1}|{Name|localizable name}|{Description|localizable description}|{ConditionGroupId|group for this condition}|{EventTypeIdCondition|event for the condition}|{EventTypeValueCondition|value for the condition}|{Condition|0:=,1:≠,2:>3:≥,4:<,5:≤}|{ConditionValue|optional}" ]; 19 | 20 | 21 | ecnStatusType [ label="{ ecnStatusType}|{Id|unique ID > 1}|{Name|localizable name}|{ShowInEventTray|true/false}|Image|{SortOrder|sorting order}", style=diagonals ]; 22 | 23 | 24 | ecnDeviceType:Id -> ecnDeviceTypeDataPointTypeLink:DeviceTypeId; 25 | ecnDeviceType:StatusEventTypeId -> ecnStatusType:Id; 26 | 27 | ecnDeviceTypeDataPointTypeLink:DataPointTypeId -> ecnDatapointType:Id; 28 | ecnDatapointType:Id -> ecnDataPointTypeEventTypeLink:DataPointTypeId; 29 | ecnDatapointType:StatusEventTypeId -> ecnStatusType:Id; 30 | 31 | ecnDataPointTypeEventTypeLink:EventTypeId -> ecnEventType:Id; 32 | ecnEventType:Id -> ecnEventTypeEventValueTypeLink:EventTypeId; 33 | ecnEventType:Id -> ecnEventTypeEventTypeGroupLink:EventTypeId; 34 | 35 | ecnEventTypeEventValueTypeLink:EventValueId -> ecnEventValueType:Id; 36 | ecnEventValueType:StatusTypeId -> ecnStatusType:Id; 37 | 38 | ecnEventTypeGroup:ParentId -> ecnEventTypeGroup:Id; 39 | ecnEventTypeGroup:DeviceTypeId -> ecnDeviceType:Id; 40 | ecnEventTypeGroup:DataPointTypeId -> ecnDatapointType:Id; 41 | ecnEventTypeEventTypeGroupLink:EventTypeGroupId -> ecnEventTypeGroup:Id; 42 | 43 | ecnDisplayConditionGroup:ParentId -> ecnDisplayConditionGroup:Id; 44 | ecnDisplayConditionGroup:EventTypeIdDest -> ecnEventType:Id; 45 | ecnEventTypeGroup:Id -> ecnDisplayConditionGroup:EventTypeGroupIdDest; 46 | 47 | ecnDisplayConditionGroup:Id -> ecnDisplayCondition:ConditionGroupId; 48 | ecnEventType:Id -> ecnDisplayCondition:EventTypeIdCondition; 49 | ecnEventValueType:Id -> ecnDisplayCondition:EventTypeValueCondition; 50 | 51 | } -------------------------------------------------------------------------------- /vcontrold_test.py: -------------------------------------------------------------------------------- 1 | import telnetlib 2 | 3 | # This little script communicates with e.g. a Raspberry Pi running vcontrold, which 4 | # has a connection via an Optolink to the heating system. 5 | # It uses raw commands to execute various read functions, which removes the 6 | # need to create a custom vito.xml file for vcontrold. 7 | 8 | HOST = 'GatewayViessmann.local' # vcontrold telnet host 9 | PORT = '3002' # vcontrold port 10 | 11 | EOT = 4 12 | ENQ = 5 13 | VS2_ACK = 6 14 | NACK = 21 15 | VS2_NACK = 21 16 | VS2_START_VS2 = 22 17 | VS2_DAP_STANDARD = 65 # plus two 0x00 bytes 18 | 19 | VS1 = True 20 | VS2 = False 21 | 22 | if VS1: # VS1 23 | Virtual_Write = 244 24 | Virtual_READ = 247 25 | GFA_Read = 107 26 | GFA_Write = 104 27 | PROZESS_WRITE = 120 28 | PROZESS_READ = 123 29 | if VS2: # VS2 30 | Virtual_READ = 1 31 | Virtual_WRITE = 2 32 | Physical_READ = 3 33 | Physical_WRITE = 4 34 | EEPROM_READ = 5 35 | EEPROM_WRITE = 6 36 | Remote_Procedure_Call = 7 37 | Virtual_MBUS = 33 38 | Virtual_MarktManager_READ = 34 39 | Virtual_MarktManager_WRITE = 35 40 | Virtual_WILO_READ = 36 41 | Virtual_WILO_WRITE = 37 42 | XRAM_READ = 49 43 | XRAM_WRITE = 50 44 | Port_READ = 51 45 | Port_WRITE = 52 46 | BE_READ = 53 47 | BE_WRITE = 54 48 | KMBUS_RAM_READ = 65 49 | KMBUS_EEPROM_READ = 67 50 | KBUS_DATAELEMENT_READ = 81 51 | KBUS_DATAELEMENT_WRITE = 82 52 | KBUS_DATABLOCK_READ = 83 53 | KBUS_DATABLOCK_WRITE = 84 54 | KBUS_TRANSPARENT_READ = 85 55 | KBUS_TRANSPARENT_WRITE = 86 56 | KBUS_INITIALISATION_READ = 87 57 | KBUS_INITIALISATION_WRITE = 88 58 | KBUS_EEPROM_LT_READ = 89 59 | KBUS_EEPROM_LT_WRITE = 90 60 | KBUS_CONTROL_WRITE = 91 61 | KBUS_MEMBERLIST_READ = 93 62 | KBUS_MEMBERLIST_WRITE = 94 63 | KBUS_VIRTUAL_READ = 95 64 | KBUS_VIRTUAL_WRITE = 96 65 | KBUS_DIRECT_READ = 97 66 | KBUS_DIRECT_WRITE = 98 67 | KBUS_INDIRECT_READ = 99 68 | KBUS_INDIRECT_WRITE = 100 69 | KBUS_GATEWAY_READ = 101 70 | KBUS_GATEWAY_WRITE = 102 71 | PROZESS_WRITE = 120 72 | PROZESS_READ = 123 73 | OT_Physical_Read = 180 74 | OT_Virtual_Read = 181 75 | OT_Physical_Write = 182 76 | OT_Virtual_Write = 183 77 | GFA_READ = 201 78 | GFA_WRITE = 202 79 | 80 | 81 | def buildVS2Package(fc,addr,length): 82 | crc = (5 + 0 + fc + ((addr >> 8) & 0xFF) + (addr & 0xFF) + length) & 0xFF 83 | return ('%02X ' * 8) % (VS2_DAP_STANDARD,5,0,fc,(addr >> 8),(addr & 0xF8),length,crc) 84 | 85 | readCmds = [ 86 | { 'addr':0x00F8,'size':8,'cmd':Virtual_READ, 'name':'ID' }, 87 | # { 'addr':0xF000,'size':16,'cmd':Virtual_READ }, 88 | # { 'addr':0xF010,'size':16,'cmd':Virtual_READ }, 89 | # { 'addr':0x08E0,'size':7,'cmd':Virtual_READ }, 90 | 91 | # { 'addr':0x7700,'size':1,'cmd':Virtual_READ, 'name':'Heizkreis-Warmwasserschema' }, 92 | 93 | 94 | # { 'addr':0x47C5,'size':1,'cmd':Virtual_READ, 'name':'Vorlauf - Minimalbegrenzung M3' }, 95 | # { 'addr':0x47C6,'size':1,'cmd':Virtual_READ, 'name':'Vorlauf - Maximalbegrenzung M3' }, 96 | # 97 | # { 'addr':0x5525,'size':2,'cmd':Virtual_READ, 'name':'Aussentemperatur' }, 98 | # { 'addr':0x0810,'size':2,'cmd':Virtual_READ, 'name':'Kesseltemperatur' }, 99 | # { 'addr':0x555A,'size':2,'cmd':Virtual_READ, 'name':'Kesselsolltemperatur' }, 100 | # { 'addr':0x0816,'size':2,'cmd':Virtual_READ, 'name':'Abgastemperatur' }, 101 | # { 'addr':0x0810,'size':2,'cmd':Virtual_READ, 'name':'Vorlauftemperatur A1M1' }, 102 | # { 'addr':0x3900,'size':2,'cmd':Virtual_READ, 'name':'Vorlauftemperatur M2' }, 103 | # { 'addr':0x4900,'size':2,'cmd':Virtual_READ, 'name':'Vorlauftemperatur M3' }, 104 | # { 'addr':0x0812,'size':2,'cmd':Virtual_READ, 'name':'Temperatur Speicher Ladesensor Komfortsensor' }, 105 | # { 'addr':0x0814,'size':2,'cmd':Virtual_READ, 'name':'Auslauftemperatur' }, 106 | 107 | { 'addr':0xCF30,'size':32,'cmd':Virtual_READ, 'name':'Solarertrag' }, 108 | # { 'addr':0x6564,'size':2,'cmd':Virtual_READ, 'name':'Solar Kollektortemperatur' }, 109 | # { 'addr':0x6566,'size':2,'cmd':Virtual_READ, 'name':'Solar Speichertemperatur' }, 110 | { 'addr':0x6560,'size':2,'cmd':Virtual_READ, 'name':'Solar Wärmemenge' }, 111 | { 'addr':0x6568,'size':2,'cmd':Virtual_READ, 'name':'Solar Betriebsstunden' }, 112 | ] 113 | 114 | telnet_client = telnetlib.Telnet(HOST, PORT) 115 | for cmd in readCmds: 116 | #print(cmd) 117 | telnet_client.read_until(b"vctrld>") 118 | #print('raw\nSEND %02X\nWAIT %02X\nSEND %02X 00 00\nWAIT %02X\nSEND %s\nRECV %d\nEND\n' % (EOT, ENQ, VS2_START_VS2, VS2_ACK, buildPackage(cmd['cmd'], cmd['addr'], cmd['size']), 1+7+cmd['size']+1)) 119 | if VS1: 120 | telnet_client.write(('raw\nSEND 04\nWAIT 05\nSEND 01 %02X %02X %02X %02X\nRECV %d\nEND\n' % (cmd['cmd'], (cmd['addr'] >> 8), cmd['addr'] & 0xFF, cmd['size'], cmd['size'])).encode()) 121 | if VS2: 122 | telnet_client.write(('raw\nSEND %02X\nWAIT %02X\nSEND %02X 00 00\nWAIT %02X\nSEND %s\nRECV %d\nEND\n' % (EOT, ENQ, VS2_START_VS2, VS2_ACK, buildVS2Package(cmd['cmd'], cmd['addr'], cmd['size']), 1+7+cmd['size']+1)).encode()) 123 | out = telnet_client.read_until(b'\n').decode('utf-8').strip() 124 | out = out.replace('Result: ','') 125 | out = bytes.fromhex(out) 126 | if VS1: 127 | val = 0 128 | if cmd['size'] == 1: 129 | val = out[0] 130 | elif cmd['size'] == 2: 131 | val = (out[0] + (out[1] << 8)) * 0.1 132 | print('%04x : %.1f [%s] - %s' % (cmd['addr'],val,out.hex(),cmd['name'])) 133 | if VS2: 134 | if out[0] != VS2_ACK or out[1] != VS2_DAP_STANDARD: 135 | continue 136 | checksum = 0x00 137 | for val in out[2:-1]: 138 | checksum = (checksum + val) & 0xff 139 | if checksum != out[-1]: 140 | continue 141 | out = out[2:-1] 142 | print('%04x : %s - %s' % (cmd['addr'],out[6:6+cmd['size']].hex(),cmd['name'])) 143 | -------------------------------------------------------------------------------- /PrintEventTypes.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree as etree 2 | 3 | 4 | # This path needs to be adjusted to point to a directory for all XML files 5 | DATAPATH = "../data/" 6 | 7 | textList = {} 8 | def parse_Textresource(lang): 9 | root = etree.parse(DATAPATH + "Textresource_%s.xml" % lang).getroot() 10 | for rootNodes in root: 11 | if 'TextResources' in rootNodes.tag: 12 | for textNode in rootNodes: 13 | textList[textNode.attrib['Label']] = textNode.attrib['Value'] 14 | 15 | def parse_ecnEventType(): 16 | root = etree.parse(DATAPATH + "ecnEventType.xml").getroot() 17 | for xmlEvent in root.findall("EventType"): 18 | event = {} 19 | for cell in xmlEvent: 20 | event[cell.tag] = cell.text 21 | if 'Address' in event: 22 | IDstr = event['ID'].split('~')[0] 23 | if 'viessmann.eventtype.name.' + IDstr in textList: 24 | IDstr = textList['viessmann.eventtype.name.' + IDstr] 25 | border = '' 26 | if 'Conversion' in event and event['Conversion'] != 'NoConversion': 27 | if event['Conversion'] == 'Div10': 28 | border += '/10' 29 | elif event['Conversion'] == 'Div100': 30 | border += '/100' 31 | elif event['Conversion'] == 'Div1000': 32 | border += '/1000' 33 | elif event['Conversion'] == 'Div2': 34 | border += '/2' 35 | elif event['Conversion'] == 'Mult2': 36 | border += '*2' 37 | elif event['Conversion'] == 'Mult5': 38 | border += '*5' 39 | elif event['Conversion'] == 'Mult10': 40 | border += '*10' 41 | elif event['Conversion'] == 'Mult100': 42 | border += '*100' 43 | elif event['Conversion'] == 'MultOffset': 44 | if 'ConversionFactor' in event and event['ConversionFactor'] != '0': 45 | border += '*%s' % event['ConversionFactor'] 46 | if 'ConversionOffset' in event and event['ConversionOffset'] != '0': 47 | border += '+%s' % event['ConversionOffset'] 48 | elif event['Conversion'] == 'DateBCD': 49 | border += 'DateBCD(Y1Y2.MM.DD HH.MM.SS)' 50 | elif event['Conversion'] == 'DateMBus': 51 | border += 'NOTIMPL: %s' % event['Conversion'] 52 | elif event['Conversion'] == 'DatenpunktADDR': 53 | border += 'NOTIMPL: %s' % event['Conversion'] 54 | elif event['Conversion'] == 'DateTimeBCD': 55 | border += 'DateTimeBCD(Y1Y2.MM.DD HH.MM.SS)' 56 | elif event['Conversion'] == 'DateTimeMBus': 57 | border += 'NOTIMPL: %s' % event['Conversion'] 58 | elif event['Conversion'] == 'DateTimeVitocom': 59 | border += 'NOTIMPL: %s' % event['Conversion'] 60 | elif event['Conversion'] == 'Estrich': 61 | border += 'NOTIMPL: %s' % event['Conversion'] 62 | elif event['Conversion'] == 'HexByte2AsciiByte': 63 | border += 'HexByte2AsciiByte(..)' 64 | elif event['Conversion'] == 'HexByte2DecimalByte': 65 | border += 'HexByte2DecimalByte(..)' 66 | elif event['Conversion'] == 'HexToFloat': 67 | border += 'NOTIMPL: %s' % event['Conversion'] 68 | elif event['Conversion'] == 'HourDiffSec2Hour': 69 | border += 'NOTIMPL: %s' % event['Conversion'] 70 | elif event['Conversion'] == 'IPAddress': 71 | border += 'IPAddress(a,b,c,d)' 72 | elif event['Conversion'] == 'Kesselfolge': 73 | border += 'NOTIMPL: %s' % event['Conversion'] 74 | elif event['Conversion'] == 'MultOffsetBCD': 75 | border += 'BCD(11223344):' 76 | if 'ConversionFactor' in event and event['ConversionFactor'] != '0': 77 | border += '*%s' % event['ConversionFactor'] 78 | if 'ConversionOffset' in event and event['ConversionOffset'] != '0': 79 | border += '+%s' % event['ConversionOffset'] 80 | elif event['Conversion'] == 'MultOffsetFloat': 81 | border += 'NOTIMPL: %s' % event['Conversion'] 82 | elif event['Conversion'] == 'Phone2BCD': 83 | border += 'Phone2BCD(..)' 84 | elif event['Conversion'] == 'RotateBytes': 85 | border += 'RotateBytes(..)' 86 | elif event['Conversion'] == 'Sec2Hour': 87 | border += 'Sec2Hour(/3600.0)' 88 | elif event['Conversion'] == 'Sec2Minute': 89 | border += 'Sec2Minute(/60.0)' 90 | elif event['Conversion'] == 'Time53': 91 | border += 'Time53(hh:mm)' 92 | elif event['Conversion'] == 'UTCDiff2Month': 93 | border += 'NOTIMPL: %s' % event['Conversion'] 94 | elif event['Conversion'] == 'Vitocom300SGEinrichtenKanalLON': 95 | border += 'NOTIMPL: %s' % event['Conversion'] 96 | elif event['Conversion'] == 'Vitocom300SGEinrichtenKanalMBUS': 97 | border += 'NOTIMPL: %s' % event['Conversion'] 98 | elif event['Conversion'] == 'Vitocom300SGEinrichtenKanalWILO': 99 | border += 'NOTIMPL: %s' % event['Conversion'] 100 | elif event['Conversion'] == 'Vitocom3NV': 101 | border += 'NOTIMPL: %s' % event['Conversion'] 102 | elif event['Conversion'] == 'VitocomEingang': 103 | border += 'NOTIMPL: %s' % event['Conversion'] 104 | elif event['Conversion'] == 'VitocomNV': 105 | border += 'NOTIMPL: %s' % event['Conversion'] 106 | elif event['Conversion'] == 'FixedStringTerminalZeroes': 107 | border += 'FixedStringTerminalZeroes(str)' 108 | elif event['Conversion'] == 'HexByte2UTF16Byte': 109 | border += 'HexByte2UTF16Byte()' 110 | elif event['Conversion'] == 'HexByte2Version': 111 | border += 'HexByte2Version()' 112 | elif event['Conversion'] == 'BinaryToJson': 113 | border += 'BinaryToJson()' 114 | elif event['Conversion'] == 'DayMonthBCD': 115 | border += 'DayMonthBCD()' 116 | elif event['Conversion'] == 'LastBurnerCheck': 117 | border += 'LastBurnerCheck()' 118 | elif event['Conversion'] == 'LastCheckInterval': 119 | border += 'LastCheckInterval()' 120 | elif event['Conversion'] == 'DayToDate': 121 | border += 'DayToDate(..)' # up to 8 bytes offset to 1.1.1970 122 | else: 123 | border += '#### %s' % event['Conversion'] 124 | if len(border)==0: 125 | border = '*1' 126 | if 'LowerBorder' in event: 127 | border += ' %s' % (event['LowerBorder']) 128 | if 'UpperBorder' in event: 129 | border += '-%s' % (event['UpperBorder']) 130 | elif 'LowerBorder' in event: 131 | border += '-?' 132 | if 'Stepping' in event: 133 | border += ':%s' % event['Stepping'] 134 | parameter = '' 135 | if 'Parameter' in event: 136 | parameter += '%s/%s' % (event['Parameter'],event['BlockLength']) 137 | parameter += ' Byte:%s/%s' % (event['BytePosition'],event['ByteLength']) 138 | if event['BitLength'] != '0': 139 | parameter += ' Bit:%s/%s' % (event['BitPosition'],event['BitLength']) 140 | print('%6s %s %s %s --- %s' % (event['Address'],event['FCRead'],border,parameter,IDstr)) 141 | 142 | if __name__ == "__main__": 143 | parse_Textresource('de') # load english localization, 'de' is German 144 | parse_ecnEventType(); 145 | -------------------------------------------------------------------------------- /VitosoftCommunication.md: -------------------------------------------------------------------------------- 1 | # Viessmann Communication 2 | 3 | ## Hardware Layer 4 | 5 | ### Communication via OptoLink/USB Serial 6 | 7 | A simple serial port with a baud rate of 4800 8E2. 8 | 9 | ### Communication via WLAN 10 | 11 | Vitosoft is searching for Wireless Access Point `VIESSMANN-12345678`. `12345678` is the password to the WLAN Access Point. After connecting to the access point, it creates a TCP Connection to `10.45.161.1:45317` (10.x.x.x is a private Class-A network), which is treated very similar to the serial port, except sync is not necessary – it seems to be managed on the device side. Besides that it seems identical to the VS2 protocol. 12 | 13 | 14 | ## Software Layer 15 | 16 | Communication is a package based serial communication. In pseudo-code it works like this: 17 | ``` 18 | sendPackage() 19 | for retry=0 to RETRYCOUNT 20 | if hasReply() then 21 | receiveReplyPackage() 22 | return 23 | sleep(RETRYDELAY) 24 | ``` 25 | 26 | In the following section I write `RETRYCOUNT x RETRYDELAY ms`, e.g. `10x50ms` to define the delays. 27 | 28 | ### GWG 29 | 30 | The GWG protocol is only used for old "Gas Wand Geräte" (Gas Wall Units). It is detected, but no longer supported by the Vitosoft 300 application. It only supports 8-bit addresses with several function codes, allowing a slightly larger address space. 31 | 32 | The detection works as follows: wait up to 10x50ms for an `ENQ` (0x05). Then send `0xC7,0xF8,0x04` (Virtual Read 0xF8, 4 bytes). The reply should be `0x20,0x53` or `0x20,0x54` to detect the GWG hardware. `0x20` is the "Gruppenidentifikation" (group identification) and `0x53` or `0x54` are the "Regleridentifikation" (controller unit identification) 33 | 34 | ### VS1 35 | 36 | Version 1 of the Vitotronic protocol, supported by the KW units from Viessmann. It supports 16-bit addresses plus several function codes. 37 | 38 | Because the whole communication is timing based, a timer is called every 50ms to check the current state of the connection and does the following: 39 | 40 | 1. after opening the serial port or in case of an error, a connection is created by waiting for up to 30x100ms for an `ENQ` (0x05). It then immediately is confirmed by sending a `0x01`. In case an `VS2_ACK` (`0x06`) or `VS2_NACK` (`0x15`) was received, the VS2 protocol is also supported. 41 | 2. If there is a pending message to be send, the message is written out and for up to 20x50ms serial data is read. Once the number of expected bytes have been received, the data is then processed. If within the giving time, not enough data was received, a connection reset it triggered (see step #1) 42 | 3. If nothing needs to be send to the unit, every 500ms the connection is kept alive, by sending a check connection message (Virtual Read: 0xF8, 2 bytes), which expects a 2-byte reply within 20x50ms. 43 | 44 | #### VS1 Message Format 45 | 46 | The message is a simply list of bytes: 47 | 48 | 1. Command/Function Code (`Virtuell_Write` = `0xF4`, `Virtuell_Read` = `0xF7`, `GFA_Read` = `0x6B`, `GFA_Write` = `0x68`, `PROZESS_WRITE` = `0x78`, `PROZESS_READ` = `0x7B`) 49 | 2. Address High Byte 50 | 3. Address Low Byte 51 | 4. Block Length 52 | 5. For write functions: Block Length additional bytes 53 | 54 | Read functions send a reply containing of the number of bytes in the block. All write functions send a single byte `vs1_data`, which is `0x00` in case the write was successful, any other value is undefined and is a failure. 55 | 56 | ### VS2 57 | 58 | Version 2 of the Vitotronic protocol. It is supported by all modern Viessmann units. These modern units also seem to be backward compatible with the VS1 protocol. It is an extension of the VS1 protocol, mostly to be faster and more reliable – thanks to checksums. But it is also more complex, because of hand-shaking requirements. 59 | 60 | To initiate the connection, after the serial port read buffer is emptied, then an `EOT` (`0x04`) is send and for 30x100ms waited for an `ENQ` (`0x05`), after which a `VS2_START_VS2, 0, 0` (`0x16,0x00,0x00`) is send and within 30x100ms an `VS2_ACK` (`0x06`) is expected. If an `VS2_ACK` (`0x06`) is received, before the start was sent, the start is resent (and the timeout is reset). In case a `VS2_NACK` (`0x15`) is received, it also resents the start and resets the timeout. 61 | 62 | After message is send, for up 30x100ms serial data is read. An `VS2_ACK` (`0x06`) or `VS2_NACK` (`0x15`) is expected first. If more than one is received, the oldest ones are removed from the receive buffer. If it is not received as the first byte, the message was not send successfully. Further bytes are collected till a full message is received. If the checksum is valid, an `VS2_ACK` is send out to confirm the successful transmission. A `VS2_NACK` is never send! 63 | 64 | If no message was send within 5s, the connection is restarted, the serial port read buffer is emptied and then sending a `VS2_START_VS2, 0, 0` (`0x16,0x00,0x00`) and within 30x100ms expecting an `VS2_ACK` (`0x06`). 65 | 66 | In case a UNACKD Message was send, only a single `VS2_ACK` or `VS2_NACK` is expected. 67 | 68 | 69 | #### VS2 Message Format 70 | 71 | The message requires a simple checksum. 72 | 73 | 1. `VS2_DAP_STANDARD` (0x41) 74 | 2. Package length for the CRC 75 | 3. Protocol Identifier (0x00 = LDAP, 0x10 = RDAP, unused) | Message Identifier (0 = Request Message, 1 = Response Message, 2 = UNACKD Message, 3 = Error Message) 76 | 4. Message Sequenz Number (top 3 bits in the byte) | Function Code 77 | 5. Address High Byte 78 | 6. Address Low Byte 79 | 7. Block Length 80 | 8. Block Length additional bytes 81 | 9. CRC, a modulo-256 addition of bytes from Block Length and the additional bytes. CRC is technically the wrong name, it is more a checksum. But that is what Viessmann calls it. 82 | 83 | ##### Defined Commands/Function Codes: 84 | 85 | There are many commands defined, but only very few are actually used by Vitosoft, which seems to be `Virtual_READ`, `Virtual_WRITE`, `Remote_Procedure_Call`, `PROZESS_READ`, `PROZESS_WRITE`, `GFA_READ`, `GFA_WRITE`. 86 | 87 | Here is the complete list: 88 | - `undefined` = 0 89 | - `Virtual_READ` = 1 90 | - `Virtual_WRITE` = 2 91 | - `Physical_READ` = 3 92 | - `Physical_WRITE` = 4 93 | - `EEPROM_READ` = 5 94 | - `EEPROM_WRITE` = 6 95 | - `Remote_Procedure_Call` = 7 96 | - `Virtual_MBUS` = 33 97 | - `Virtual_MarktManager_READ` = 34 98 | - `Virtual_MarktManager_WRITE` = 35 99 | - `Virtual_WILO_READ` = 36 100 | - `Virtual_WILO_WRITE` = 37 101 | - `XRAM_READ` = 49 102 | - `XRAM_WRITE` = 50 103 | - `Port_READ` = 51 104 | - `Port_WRITE` = 52 105 | - `BE_READ` = 53 106 | - `BE_WRITE` = 54 107 | - `KMBUS_RAM_READ` = 65 108 | - `KMBUS_EEPROM_READ` = 67 109 | - `KBUS_DATAELEMENT_READ` = 81 110 | - `KBUS_DATAELEMENT_WRITE` = 82 111 | - `KBUS_DATABLOCK_READ` = 83 112 | - `KBUS_DATABLOCK_WRITE` = 84 113 | - `KBUS_TRANSPARENT_READ` = 85 114 | - `KBUS_TRANSPARENT_WRITE` = 86 115 | - `KBUS_INITIALISATION_READ` = 87 116 | - `KBUS_INITIALISATION_WRITE` = 88 117 | - `KBUS_EEPROM_LT_READ` = 89 118 | - `KBUS_EEPROM_LT_WRITE` = 90 119 | - `KBUS_CONTROL_WRITE` = 91 120 | - `KBUS_MEMBERLIST_READ` = 93 121 | - `KBUS_MEMBERLIST_WRITE` = 94 122 | - `KBUS_VIRTUAL_READ` = 95 123 | - `KBUS_VIRTUAL_WRITE` = 96 124 | - `KBUS_DIRECT_READ` = 97 125 | - `KBUS_DIRECT_WRITE` = 98 126 | - `KBUS_INDIRECT_READ` = 99 127 | - `KBUS_INDIRECT_WRITE` = 100 128 | - `KBUS_GATEWAY_READ` = 101 129 | - `KBUS_GATEWAY_WRITE` = 102 130 | - `PROZESS_WRITE` = 120 131 | - `PROZESS_READ` = 123 132 | - `OT_Physical_Read` = 180 133 | - `OT_Virtual_Read` = 181 134 | - `OT_Physical_Write` = 182 135 | - `OT_Virtual_Write` = 183 136 | - `GFA_READ` = 201 137 | - `GFA_WRITE` = 202 138 | -------------------------------------------------------------------------------- /VitosoftSoftware.md: -------------------------------------------------------------------------------- 1 | # Vitosoft Software 2 | 3 | [Vitosoft 300 Typ SID1](https://connectivity.viessmann.com/de/mp-fp/vitosoft.html) is available from Viessmann. It is only available for Windows. 4 | 5 | It requires an OptoLink/USB adapter or a system with an integrated WLAN interface. 6 | 7 | There is a 90 day [demo version](https://connectivity.viessmann.com/content/dam/public-micro/connectivity/vitosoft/de/Vitosoft-300-SID1-Setup.zip/_jcr_content/renditions/original.media_file.download_attachment.file/Vitosoft-300-SID1-Setup.zip) available. 8 | 9 | The version is outdated and requires updating directly after installation. 10 | 11 | These are the known versions: 12 | - Release 6.1.0.2 (15.12.2015) - available at [http://update.vitosoft.de/CurrentVersion/Vitosoft300SID1_Setup.exe](http://update.vitosoft.de/CurrentVersion/Vitosoft300SID1_Setup.exe) or inside [https://update-vitosoft.viessmann.com/CurrentVersion/Vitosoft300WithoutDocs.iso](https://update-vitosoft.viessmann.com/CurrentVersion/Vitosoft300WithoutDocs.iso) as the file `Vitosoft300SID1_Fallback_Setup.exe` 13 | - Release 7.1.4.8 (21.10.2016) 14 | - Release 8.0.5.0 (12.07.2017) - available at [https://connectivity.viessmann.com/](https://connectivity.viessmann.com/content/dam/vi-micro/CONNECTIVITY/Vitosoft/Vitosoft300SID1_Setup.exe/_jcr_content/renditions/original.media_file.download_attachment.file/Vitosoft300SID1_Setup.exe) 15 | - Release 8.0.6.2 (14.12.2017)- The demo link gets you this version. 16 | 17 | As you can see, there seem to be no new updates for 3.5 years. 18 | 19 | Even if you have no intention in running the software, you need to download it and extract several XML files from it, which contain all information about what data points are available for which system. To extract the `EXE` file, you need to use 7-Zip (7z on UNIX). On a Macintosh, it can be installed via `brew install p7zip`. The easiest is to copy the `EXE` file into a new directory, and then run the following terminal command inside the directory: 20 | 21 | 7z e Vitosoft300SID1_Setup.exe -r "*.xml" -y 22 | 23 | *WARNING*: This is only for testing! For get the real production XML files from Vitosoft, it is essential to run Vitosoft _once_ for this file to be generated correctly – otherwise you have a very outdated versions of the following files. All the other XML files are identical. 24 | 25 | - `C:\Program Files\Viessmann Vitosoft 300 SID1\ServiceTool\MobileClient\Config\ecnDataPointType.xml` 26 | - `C:\Program Files\Viessmann Vitosoft 300 SID1\ServiceTool\MobileClient\Config\ecnEventType.xml` 27 | - `C:\Program Files\Viessmann Vitosoft 300 SID1\ServiceTool\MobileClient\Config\ecnVersion.xml` 28 | 29 | 30 | ## Software update mechanism 31 | 32 | The software checks and downloads new versions at launch via 3 Soap requests. 33 | 34 | ### CheckSoftwareVersion 35 | 36 | **Endpoint**: POST - https://update-vitosoft.viessmann.com/vrimaster/VRIMasterWebService.asmx 37 | 38 | **Description**: Send the current version and check if a newer version is available. In this example the License info is from the trial version. 39 | 40 | #### Header 41 | ``` 42 | { 43 | User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; MS Web Services Client Protocol 4.0.30319.42000) 44 | Content-Type: text/xml; charset=utf-8 45 | SOAPAction: "http://www.e-controlnet.de/services/VRIMasterWebService/CheckSoftwareVersion" 46 | Host: update-vitosoft.viessmann.com 47 | Accept-Encoding: gzip, zlib, deflate, zstd, br 48 | } 49 | ``` 50 | 51 | 52 | #### Body 53 | ``` 54 | 90 Tage Testlizenz0001-01-01T00:00:001063B25913BD9D042609498C93AC6DA797D8008785F19C4F1A485EAFF715026860E26.1.0.2 55 | ``` 56 | 57 | ### RequestDownload 58 | 59 | **Endpoint**: POST - https://update-vitosoft.viessmann.com/vrimaster/VRIMasterWebService.asmx 60 | 61 | **Description**: Request the download of a new version. This response with a token UUID, which is valid for 24 hours to download the new version. 62 | 63 | #### Header 64 | ``` 65 | { 66 | content-type: text/xml; charset=utf-8 67 | Accept-Encoding: gzip, zlib, deflate, zstd, br 68 | } 69 | ``` 70 | 71 | #### Body 72 | ``` 73 | 90 Tage Testlizenz0001-01-01T00:00:001063B25913BD9D042609498C93AC6DA797D8008785F19C4F1A485EAFF715026860E2Software 74 | ``` 75 | 76 | #### Response 77 | ``` 78 | 79 | 80 | 81 | 82 | true 83 | token created 84 | 0 85 | 86 | d7d60ea5-47c2-4a9f-8358-ae06e7a50bca 87 | 2021-01-01T12:00:00.0000000+02:00 88 | 2021-01-02T12:00:00.0000000+02:00 89 | 90 | 91 | 92 | 93 | 94 | ``` 95 | 96 | ### DownloadSoftware 97 | 98 | **Endpoint**: POST - https://update-vitosoft.viessmann.com/vrimaster/VRIMasterWebService.asmx 99 | 100 | **Description**: With the token from the `RequestDownload` call, generate a URL to download the update. Strangely neither the UUID now the expiry is important. It seems the server completely ignore them. The UUID can be found again inside the generated URL, which points to an `EXE` file for a full installer of the software. 101 | 102 | #### Header 103 | ``` 104 | { 105 | x-aspnet-version: 2.0.50727 106 | content-type: text/xml; charset=utf-8 107 | date: Tue, 20 Jul 2021 13:43:35 GMT 108 | p3p: CP="NON CUR OTPi OUR NOR UNI" 109 | server: Microsoft-IIS/10.0 110 | cache-control: private, max-age=0 111 | x-powered-by: ASP.NET 112 | Accept-Encoding: gzip, zlib, deflate, zstd, br 113 | } 114 | ``` 115 | 116 | #### Body 117 | ``` 118 | d7d60ea5-47c2-4a9f-8358-ae06e7a50bca2021-01-01T12:00:00.0000000+02:002021-01-02T12:00:00.0000000+02:008.0.6.2 119 | ``` 120 | 121 | #### Response 122 | ``` 123 | 124 | 125 | 126 | 127 | true 128 | installer generated 129 | 0 130 | http://update.vitosoft.de/VRIMasterWebService/Software/CustomerSoftware/d7d60ea5-47c2-4a9f-8358-ae06e7a50bca/8.0.6.2%20637625575615728917.exe 131 | 132 | 133 | 134 | 135 | ``` 136 | -------------------------------------------------------------------------------- /PrintEventsForDatapoint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from xml.etree import ElementTree as etree 5 | import pprint 6 | import sys 7 | import binascii 8 | import re 9 | 10 | pp = pprint.PrettyPrinter(width=200,compact=True) 11 | 12 | # This path needs to be adjusted to point to a directory for all XML files 13 | DATAPATH = "../data/" 14 | 15 | MAX_STR_LENGTH = 1000 # certain strings (e.g. conditions) can be very long. This shortens them. 1000 matches "print everything" 16 | 17 | textList = {} 18 | def parse_Textresource(lang): 19 | root = etree.parse(DATAPATH + "Textresource_%s.xml" % lang).getroot() 20 | for rootNodes in root: 21 | if 'TextResources' in rootNodes.tag: 22 | for textNode in rootNodes: 23 | textList[textNode.attrib['Label']] = textNode.attrib['Value'].replace('##ecnnewline##','\\n').replace('##ecntab##','\\t') 24 | 25 | def translate(node,key): 26 | if key in node and node[key] and node[key].startswith('@@'): 27 | if node[key][2:] in textList and len(node[key][2:]): 28 | node[key] = textList[node[key][2:]] 29 | 30 | def parse_node(root,nodeName): 31 | elements = [] 32 | for xmlEvent in root.findall(".//" + nodeName): 33 | dp = {} 34 | for cell in xmlEvent: 35 | if cell.tag in ['Description','URL','DefaultValue','Filtercriterion','Reportingcriterion','Priority']: 36 | continue 37 | if cell.tag in ['Conversion','EnumType']: # these are not needed 38 | continue 39 | value = cell.text 40 | if value == 'true': 41 | value = True 42 | elif value == 'false': 43 | value = False 44 | if cell.tag in ['ConverterId','Id','Type','ConfigSetId','EventValueId','EnumAddressValue','StatusTypeId','EventTypeValueCondition','EventTypeIdCondition','ConditionGroupId','EventTypeGroupIdDest','EventTypeIdDest','EventTypeId','EventTypeGroupId','DataPointTypeId','DeviceTypeId','DataPointTypeId','OrderIndex','EventTypeOrder','ParentId','StatusDataPointTypeId','TechnicalIdentificationAddress','SortOrder']: 45 | value = int(value) 46 | if cell.tag == 'ParentId' and value == -1: 47 | continue 48 | dp[cell.tag] = value 49 | if cell.tag in ['Description','Name','EnumReplaceValue']: 50 | translate(dp,cell.tag) 51 | elements.append(dp) 52 | return elements 53 | 54 | ecnEventTypes = {} 55 | def parse_ecnEventTypes(): 56 | for nodes in etree.parse(DATAPATH + "ecnEventType.xml").getroot(): 57 | eventType = {} 58 | for cell in nodes: 59 | if cell.tag in []: 60 | continue 61 | value = cell.text 62 | if cell.tag == 'ValueList': 63 | valueDict = {} 64 | for valueEnum in value.split(';'): 65 | valueEnumVal,valueEnumStr = valueEnum.split('=', 1) 66 | if valueEnumStr.startswith('@@'): 67 | if valueEnumStr[2:] in textList and len(valueEnumStr[2:]): 68 | valueEnumStr = textList[valueEnumStr[2:]] 69 | try: 70 | valueEnumVal = int(valueEnumVal) 71 | except: 72 | pass 73 | valueDict[valueEnumVal] = valueEnumStr 74 | value = valueDict 75 | if value == 'true': 76 | value = True 77 | elif value == 'false': 78 | value = False 79 | elif cell.tag in ['BitLength', 'BitPosition', 'BlockLength', 'ByteLength', 'BytePosition', 'RPCHandler', 'MappingType']: 80 | try: 81 | value = int(value) 82 | except: 83 | pass 84 | elif cell.tag in ['ConversionFactor', 'Stepping', 'UpperBorder', 'LowerBorder']: 85 | try: 86 | value = float(value) 87 | except: 88 | pass 89 | # elif cell.tag in ['PrefixRead', 'PrefixWrite', 'ALZ'] and value: 90 | # if value.startswith('0x'): 91 | # value = value[2:] 92 | # value = binascii.unhexlify(value) 93 | eventType[cell.tag] = value 94 | if cell.tag in ['Description']: 95 | translate(eventType,cell.tag) 96 | if cell.tag in []:#['Unit']: 97 | if eventType[cell.tag] in textList and len(eventType[cell.tag]): 98 | eventType[cell.tag] = textList[eventType[cell.tag]] 99 | eventID = eventType['ID'] 100 | del eventType['ID'] 101 | if 'Conversion' in eventType: 102 | if eventType['Conversion'] == 'NoConversion': 103 | del eventType['Conversion'] 104 | if 'ConversionFactor' in eventType: 105 | del eventType['ConversionFactor'] 106 | if 'ConversionOffset' in eventType: 107 | del eventType['ConversionOffset'] 108 | if 'FCRead' in eventType and eventType['FCRead'] == 'undefined': 109 | eventType['FCRead'] = None 110 | if 'FCWrite' in eventType and eventType['FCWrite'] == 'undefined': 111 | eventType['FCWrite'] = None 112 | if 'OptionList' in eventType: 113 | eventType['OptionList'] = eventType['OptionList'].split(';') 114 | ecnEventTypes[eventID] = eventType 115 | 116 | usedEventTypes = set() 117 | def eventTypeDescr(eventID): 118 | if eventID not in ecnEventTypes: 119 | return eventID 120 | usedEventTypes.add(eventID) 121 | et = ecnEventTypes[eventID] 122 | result = eventID 123 | #cmd = et['FCRead'] 124 | #if cmd == 'Virtual_READ': 125 | # cmd = 'VR' 126 | #elif cmd == 'Virtual_WRITE': 127 | # cmd = 'VW' 128 | #result += ': ' + cmd 129 | #result += ':' + et['Address'] 130 | #if et['BlockLength'] == et['ByteLength']: 131 | # result += ':%d' % et['BlockLength'] 132 | #else: 133 | # result += ':%d' % et['ByteLength'] 134 | result += ' (%s)' % et['Parameter'] 135 | return result 136 | 137 | def parse_DPDefinitions(selectedDatapointTypeAddress): 138 | classes = set() 139 | root = etree.fromstring(re.sub(' xmlns="[^"]+"', '', open(DATAPATH + "DPDefinitions.xml",'r').read())) 140 | for xmlEvent in root.find("ECNDataSet/{urn:schemas-microsoft-com:xml-diffgram-v1}diffgram/ECNDataSet"): 141 | classes.add(xmlEvent.tag) 142 | 143 | nodeListe = {} 144 | for cl in classes: 145 | nodes = parse_node(root,cl) 146 | if True: 147 | if cl == 'ecnEventValueType': 148 | for dd in nodes: 149 | st = int(dd['StatusTypeId']) 150 | if st == 5: 151 | dd['StatusTypeId'] = '@@viessmann.vitodata.valuestatus.notevaluate' 152 | elif st == 4: 153 | dd['StatusTypeId'] = '@@viessmann.vitodata.valuestatus.outofinterval' 154 | elif st == 3: 155 | dd['StatusTypeId'] = 'Error' 156 | elif st == 2: 157 | dd['StatusTypeId'] = 'Warning' 158 | elif st == 1: 159 | dd['StatusTypeId'] = 'OK' 160 | elif st == 0: 161 | dd['StatusTypeId'] = 'Undefined' 162 | else: 163 | dd['StatusTypeId'] = '???' 164 | if len(nodes) > 0 and 'Id' in nodes[0]: 165 | dd = {} 166 | for e in nodes: 167 | foundId = e['Id'] 168 | del e['Id'] 169 | dd[foundId] = e 170 | nodes = dd 171 | nodeListe[cl] = nodes 172 | if cl in [ 'ecnVersion', 173 | 'ecnDeviceType', 174 | 'ecnStatusType', 175 | 'ecnDatapointType','ecnDeviceTypeDataPointTypeLink', 176 | 'ecnEventType','ecnDataPointTypeEventTypeLink', 177 | 'ecnTableExtension','ecnTableExtensionValue', 178 | 'ecnEventValueType','ecnEventTypeEventValueTypeLink', 179 | 'ecnEventTypeGroup','ecnEventTypeEventTypeGroupLink', 180 | 'ecnConverter','ecnConverterDeviceTypeLink','ecnCulture', 181 | ]: 182 | # 'ecnConfigSet','ecnStatusType','ecnConfigSetParameter']: 183 | # continue 184 | pass 185 | if False and cl == 'ecnDisplayConditionGroup': 186 | print(cl, len(nodeListe[cl]), type(nodeListe[cl])) 187 | for node in nodeListe: 188 | if isinstance(nodeListe[cl], list): 189 | pp.pprint(nodeListe[cl]) 190 | elif isinstance(nodeListe[cl], dict): 191 | pp.pprint(nodeListe[cl]) 192 | print('-' * 40) 193 | # break 194 | #sys.exit(0) 195 | pass 196 | 197 | usedConditionEventTypeIds = set() 198 | def getCondStr(groupId,groupOrEvent='EventTypeGroupIdDest'): 199 | condDict = { 0:'=', 1:'≠', 2:'>', 3:'≥', 4:'<', 5:'≤' } 200 | operDict = { 1:'AND', 2:'OR' } 201 | shortName = { # to avoid the conditions get incredibly long, we shorten some known common events 202 | '(00) Heizkreis-Warmwasserschema':'00_WWS', 203 | '(54) Solarregelung':'54_SR', 204 | '(7F) Unterscheidung Einfamilienhaus - Mehrparteienhaus':'7F_EFH', 205 | 'Software-Index des Gerätes':'SWIdx', 206 | 'SW-Index Solarmodul SM1':'SWIdxSolarSM1', 207 | 'HW-Index Solarmodul SM1':'HWIdxSolarSM1', 208 | '(00) Anlagen-Warmwasserschema':'00_AnlWWS', 209 | '(76) Konfiguration Kommunikationsmodul':'76_KonfKommMod', 210 | '(76) Kommunikationsmodul':'76_KommMod', 211 | '(A0) Kennung Fernbedienung A1M1':'A0_KennFBA1M1', 212 | '(A0) Kennung Fernbedienung M2':'A0_KennFBM2', 213 | '(A0) Kennung Fernbedienung M3':'A0_KennFBM3', 214 | 'Fernbedienung Heizkreis A1M1':'FBA1M1', 215 | 'Fernbedienung Heizkreis M2':'FBM2', 216 | 'Fernbedienung Heizkreis M3':'FBM3', 217 | '(E5) Kennung Pumpe Heizkreis A1':'E5_KennPumpHzkA1', 218 | '(E5) Kennung Pumpe Heizkreis M2':'E5_KennPumpHzkM2', 219 | '(E5) Kennung Pumpe Heizkreis M3':'E5_KennPumpHzkM3', 220 | 'Status Raumtemp.-Sensor HK1':'RaumTempSensHK1', 221 | 'Status Raumtemp.-Sensor HK2':'RaumTempSensHK2', 222 | 'Status Raumtemp.-Sensor HK3':'RaumTempSensHK3', 223 | '(30) Kennung Interne Umwälzpumpe':'30_KennIntUmwPumpe', 224 | '(91) Zuordnung externe Betriebsarten-umschaltung':'ZuordExtBetrUmsch', 225 | '(35) Kennung Anschlusserweiterung EA1':'35_KennAnschlErwEA1', 226 | '(5B) Kennung Anschlusserweiterung EA1':'5B_KennAnschlErwEA1', 227 | '(32) Kennung Anschlusserweiterung AM1':'32_KennAnschlErwAM1', 228 | } 229 | 230 | groupCond = '' 231 | for displayConditionGroupId,displayConditionGroup in nodeListe['ecnDisplayConditionGroup'].items(): 232 | if displayConditionGroup[groupOrEvent] != groupId: 233 | continue 234 | ll = [] 235 | for dispCondId,dispCond in nodeListe['ecnDisplayCondition'].items(): 236 | if dispCond['ConditionGroupId'] != displayConditionGroupId: 237 | continue 238 | eventTypeId = int(dispCond['EventTypeIdCondition']) 239 | usedConditionEventTypeIds.add(eventTypeId) 240 | eventType = nodeListe['ecnEventType'][eventTypeId] 241 | name = eventType['Name'].strip() 242 | if name in shortName: 243 | name = shortName[name] 244 | else: 245 | name = '"%s"' % name 246 | #name += ' (%d)' % (eventTypeId) 247 | eventValue = nodeListe['ecnEventValueType'][int(dispCond['EventTypeValueCondition'])] 248 | if 'EnumReplaceValue' in eventValue: 249 | val = '"%s"' % eventValue['EnumReplaceValue'] 250 | elif 'EnumAddressValue' in eventValue: 251 | val = '"%s"' % eventValue['EnumAddressValue'] 252 | else: 253 | val = '%s' % eventValue 254 | if 'EqualCondition' in dispCond: 255 | ll.append(name + '=' + val) 256 | else: 257 | ll.append(name + condDict[int(dispCond['Condition'])] + val) 258 | groupCond += (' ' + operDict[displayConditionGroup['Type']] + ' ').join(ll) 259 | if groupCond: 260 | return ' HIDDEN:(%s)' % groupCond 261 | return '' 262 | 263 | # find datapoint for a given address 264 | for datapointTypeId,datapointType in nodeListe['ecnDatapointType'].items(): 265 | if datapointType['Address'] != selectedDatapointTypeAddress: 266 | continue 267 | print(datapointType['Name']) 268 | print('=' * len(datapointType['Name'])) 269 | break 270 | 271 | # find all event IDs for a given data datapoint ID 272 | eventTypeIds = set() 273 | for dataPointTypeEventTypeLink in nodeListe['ecnDataPointTypeEventTypeLink']: 274 | if dataPointTypeEventTypeLink['DataPointTypeId'] != datapointTypeId: 275 | continue 276 | eventTypeIds.add(dataPointTypeEventTypeLink['EventTypeId']) 277 | 278 | usedGroups = {} 279 | for eventTypeId in eventTypeIds: 280 | eventType = nodeListe['ecnEventType'][eventTypeId].copy() 281 | eventType['Id'] = eventTypeId 282 | eventTypeAddress = eventType['Address'] 283 | #del eventType['Address'] 284 | if eventTypeAddress == 'DatabaseVersionForExport': 285 | continue 286 | if eventType['Name'] in ['ecnStatusEventType','ecnsysEventType~ErrorNotification']: 287 | continue 288 | if eventTypeAddress in ecnEventTypes and 'FCRead' in ecnEventTypes[eventTypeAddress]: 289 | eventType['FCRead'] = ecnEventTypes[eventTypeAddress]['FCRead'] 290 | #if eventType['FCRead'] == 'Remote_Procedure_Call': 291 | # continue 292 | pass 293 | else: 294 | continue # URL only event 295 | #if eventTypeAddress in ecnEventTypes and 'FCWrite' in ecnEventTypes[eventTypeAddress] and ecnEventTypes[eventTypeAddress]['FCWrite']: 296 | # eventType['FCWrite'] = ecnEventTypes[eventTypeAddress]['FCWrite'] 297 | del eventType['Type'] 298 | 299 | # build a list of value types for an event type 300 | if True: # for enums there is a list of possible values 301 | evalueDict = {} 302 | evalueList = [] 303 | for evl in nodeListe['ecnEventTypeEventValueTypeLink']: 304 | if evl['EventTypeId'] != eventTypeId: 305 | continue 306 | eventValueType = nodeListe['ecnEventValueType'][evl['EventValueId']] 307 | if 'Name' in eventValueType: 308 | del eventValueType['Name'] 309 | if 'StatusTypeId' in eventValueType and eventValueType['StatusTypeId'] == 'Undefined': 310 | del eventValueType['StatusTypeId'] 311 | if 'Unit' in eventValueType: 312 | if eventValueType['Unit'] == None: 313 | del eventValueType['Unit'] 314 | elif eventValueType['Unit'].startswith('ecnUnit.'): 315 | eventValueType['Unit'] = eventValueType['Unit'][8:] 316 | if eventValueType['DataType'] == 'Int': 317 | if 'ValuePrecision' in eventValueType: 318 | eventValueType['ValuePrecision'] = int(eventValueType['ValuePrecision']) 319 | if 'LowerBorder' in eventValueType: 320 | eventValueType['LowerBorder'] = int(eventValueType['LowerBorder']) 321 | if 'UpperBorder' in eventValueType: 322 | eventValueType['UpperBorder'] = int(eventValueType['UpperBorder']) 323 | if 'Stepping' in eventValueType: 324 | eventValueType['Stepping'] = int(eventValueType['Stepping']) 325 | if eventValueType['Stepping'] == 1: 326 | del eventValueType['Stepping'] 327 | elif eventValueType['DataType'] == 'Float': 328 | if 'LowerBorder' in eventValueType: 329 | eventValueType['LowerBorder'] = float(eventValueType['LowerBorder']) 330 | if 'UpperBorder' in eventValueType: 331 | eventValueType['UpperBorder'] = float(eventValueType['UpperBorder']) 332 | if 'Stepping' in eventValueType: 333 | eventValueType['Stepping'] = float(eventValueType['Stepping']) 334 | if eventValueType['Stepping'] == 1.0: 335 | del eventValueType['Stepping'] 336 | elif eventValueType['DataType'] == 'DateTime': 337 | if 'ValuePrecision' in eventValueType: 338 | eventValueType['ValuePrecision'] = int(eventValueType['ValuePrecision']) 339 | elif eventValueType['DataType'] == 'Binary': 340 | if 'LowerBorder' in eventValueType: 341 | eventValueType['LowerBorder'] = float(eventValueType['LowerBorder']) 342 | if 'UpperBorder' in eventValueType: 343 | eventValueType['UpperBorder'] = float(eventValueType['UpperBorder']) 344 | if 'Stepping' in eventValueType: 345 | eventValueType['Stepping'] = float(eventValueType['Stepping']) 346 | if eventValueType['Stepping'] == 1.0: 347 | del eventValueType['Stepping'] 348 | # else: 349 | # pp.pprint(eventValueType) 350 | if 'EnumAddressValue' in eventValueType and 'EnumReplaceValue' in eventValueType: 351 | evalueDict[int(eventValueType['EnumAddressValue'])] = eventValueType['EnumReplaceValue'] 352 | else: 353 | if 'EnumAddressValue' in eventValueType: 354 | del eventValueType['EnumAddressValue'] 355 | if 'EnumReplaceValue' in eventValueType: 356 | del eventValueType['EnumReplaceValue'] 357 | evalueList.append(eventValueType) 358 | if len(evalueList): 359 | if len(evalueList)==1: 360 | eventType['_VALUE_'] = evalueList[0] 361 | else: 362 | eventType['_VALUE_'] = evalueList 363 | else: 364 | eventType['_VALUE_'] = evalueDict 365 | 366 | if True: # events can be in several groups 367 | foundGroup = False 368 | for eventTypeEventTypeGroupLink in nodeListe['ecnEventTypeEventTypeGroupLink']: 369 | if eventTypeEventTypeGroupLink['EventTypeId'] != eventTypeId: 370 | continue 371 | if eventTypeEventTypeGroupLink['EventTypeGroupId'] not in nodeListe['ecnEventTypeGroup']: # the DP is missing entries! 372 | continue 373 | etgOrder = eventTypeEventTypeGroupLink['EventTypeOrder'] 374 | eventTypeGroup = nodeListe['ecnEventTypeGroup'][eventTypeEventTypeGroupLink['EventTypeGroupId']] 375 | if eventTypeGroup['DataPointTypeId'] == datapointTypeId: 376 | group = eventTypeGroup.copy() 377 | del group['EntrancePoint'] # always true 378 | if 'ParentId' in group: 379 | parent = nodeListe['ecnEventTypeGroup'][group['ParentId']].copy() 380 | del group['ParentId'] 381 | del parent['DataPointTypeId'] 382 | del parent['DeviceTypeId'] 383 | pname = parent['Name'] 384 | if pname in textList and len(pname): 385 | pname = textList[pname] 386 | parent['Name'] = pname 387 | group['Name'] = '%s - %s' % (pname, group['Name']) 388 | del group['DataPointTypeId'] 389 | del group['DeviceTypeId'] 390 | del group['OrderIndex'] 391 | if eventTypeEventTypeGroupLink['EventTypeGroupId'] not in usedGroups: 392 | usedGroups[eventTypeEventTypeGroupLink['EventTypeGroupId']] = {} 393 | etgOrder *= 10 394 | while etgOrder in usedGroups[eventTypeEventTypeGroupLink['EventTypeGroupId']]: 395 | etgOrder += 1 396 | usedGroups[eventTypeEventTypeGroupLink['EventTypeGroupId']][etgOrder] = eventType 397 | foundGroup = True 398 | if not foundGroup: 399 | if '_NO_GROUP_' in usedGroups: 400 | usedGroups[0].append(eventType) 401 | else: 402 | usedGroups[0] = [eventType] 403 | 404 | eventTypeGroups = {} 405 | for eventTypeGroupId,eventTypeGroup in nodeListe['ecnEventTypeGroup'].items(): 406 | if eventTypeGroup['DataPointTypeId'] == datapointTypeId: 407 | del eventTypeGroup['DataPointTypeId'] 408 | del eventTypeGroup['DeviceTypeId'] 409 | eventTypeGroups[eventTypeGroupId] = eventTypeGroup 410 | sortedEventTypeGroups = sorted(eventTypeGroups,key=lambda x:eventTypeGroups[x]['OrderIndex']) 411 | for eventTypeGroupId in sortedEventTypeGroups: 412 | eventTypeGroup = eventTypeGroups[eventTypeGroupId] 413 | if 'ParentId' not in eventTypeGroup: 414 | eventTypeGroupName = eventTypeGroup['Name'] 415 | if eventTypeGroupName in textList and len(eventTypeGroupName): 416 | eventTypeGroupName= textList[eventTypeGroupName] 417 | if eventTypeGroupName.startswith('ecnsys'): 418 | continue 419 | if eventTypeGroupName == 'Feuerungsautomat': 420 | break 421 | # find all children of this parent 422 | eventTypeGroupChildren = {} 423 | for eventTypeGroupChildId in sortedEventTypeGroups: 424 | eventTypeGroupChild = eventTypeGroups[eventTypeGroupChildId] 425 | if 'ParentId' not in eventTypeGroupChild: 426 | continue 427 | if eventTypeGroupChild['ParentId'] == eventTypeGroupId: 428 | del eventTypeGroupChild['ParentId'] 429 | eventTypeGroupChildren[eventTypeGroupChildId] = eventTypeGroupChild 430 | if len(eventTypeGroupChildren): # no children, parent is not needed 431 | def groupStr(groupId,groupName): 432 | result = groupName.strip() 433 | result += ' (%d)' % groupId 434 | condStr = getCondStr(groupId) 435 | if len(condStr)>MAX_STR_LENGTH: 436 | condStr = condStr[:MAX_STR_LENGTH]+'…)' 437 | result += condStr 438 | return result 439 | print() 440 | print("# " + groupStr(eventTypeGroupId,eventTypeGroupName)) 441 | for eventTypeGroupChildId in sorted(eventTypeGroupChildren,key=lambda x:eventTypeGroupChildren[x]['OrderIndex']): 442 | eventTypeGroupChild = eventTypeGroupChildren[eventTypeGroupChildId] 443 | eventTypeGroupChildName = eventTypeGroupChild['Name'] 444 | if eventTypeGroupChildName in textList and len(eventTypeGroupChildName): 445 | eventTypeGroupChildName= textList[eventTypeGroupChildName] 446 | if eventTypeGroupChildId in usedGroups and len(usedGroups[eventTypeGroupChildId]): 447 | print("- " + groupStr(eventTypeGroupChildId,eventTypeGroupChildName)) 448 | if eventTypeGroupChildId in usedGroups: 449 | for kk in sorted(usedGroups[eventTypeGroupChildId]): 450 | eventType = usedGroups[eventTypeGroupChildId][kk] 451 | def eventTypeStr(eventType): 452 | result = eventType['Name'].strip() 453 | result += ' (%d)' % eventType['Id'] 454 | valStr = '%s' % eventType['_VALUE_'] 455 | if len(valStr)>MAX_STR_LENGTH: 456 | valStr = valStr[:MAX_STR_LENGTH]+'…}' 457 | #result += ' ' + valStr 458 | result += ' [' + eventTypeDescr(eventType['Address']) + ']' 459 | condStr = getCondStr(eventType['Id'],'EventTypeIdDest') 460 | if len(condStr)>MAX_STR_LENGTH: 461 | condStr = condStr[:MAX_STR_LENGTH]+'…)' 462 | result += condStr 463 | return result 464 | print(' - ' + eventTypeStr(eventType)) 465 | 466 | if False: 467 | print() 468 | for eventTypeId in sorted(usedConditionEventTypeIds): 469 | eventType = nodeListe['ecnEventType'][eventTypeId] 470 | name = eventType['Name'].strip() 471 | print('%s (%d) %s' % (name,eventTypeId,eventType['Address'])) 472 | 473 | 474 | if __name__ == "__main__": 475 | # print all events with their conditions, sorted by groups for a specific data point 476 | parse_Textresource('de') # load english localization, 'de' is German 477 | parse_ecnEventTypes() 478 | parse_DPDefinitions('VScotHO1_72') 479 | # 20cb 0351 0000 0146 480 | -------------------------------------------------------------------------------- /VitosoftXML.md: -------------------------------------------------------------------------------- 1 | # Vitosoft data structures 2 | 3 | Vitosoft uses a large Microsoft SQL Server database `ecnViessmann.mdf/ldf` to keep track of all heating systems, devices and all their parameters. Besides that database, the information is also available as XML files, which is what we will use to find out all events, which are supported by our system. 4 | 5 | ## Definition of terms 6 | 7 | - Device is a vendor. Only one exists: Viessmann 8 | - Datapoints are heating units or devices. Typically there is only one active at any given time. There are about 400 datapoints known. 9 | - Events are readable and/or writeable values or callable functions to the heating unit. 10 | - Event Values are specific values for specific events. This allows naming certain values or creating named enums. 11 | - Event Groups allow grouping of events, e.g. all solar parameters in one group. This is only used for a nicer presentation within the Vitosoft application. 12 | - Display Condition and Display Condition Group are used to hide individual events, e.g. if not solar system is installed, then it will not show up in the UI. It allows relatively complex terms. 13 | 14 | 15 | ## XML files 16 | 17 | There are several XML files of interest. *Warning*: You actually need to launch Vitosoft _once_ for it to generate the `ecnDataPointType.xml` and `ecnEventType.xml` files from `ecnViessmann.mdf`. The outdated files from the EXE will be moved to a folder named `configbackup`. 18 | 19 | - `DPDefinitions.xml` - a massive almost 200MB large file, which contains the database of all data points in XML format 20 | - `ecnVersion.xml` – contains the `DataPointDefinitionVersion`. It is regenerated on first launch from the SQL database, which bumps the version number to `0.0.26.4683`. 21 | - `ecnDataPointType.xml` - The file contains all information needed to identify the Viessmann heating unit. The `ID` of the unit is used as `Address` in the `ecnDatapointType` inside `DPDefinitions.xml`. 22 | - `ecnEventType.xml` - Events are readable and/or writeable values or callable functions to the heating unit. The `ID` of the event is used as `Address` in the `ecnEventType` inside `DPDefinitions.xml`. 23 | - `ecnEventTypeGroup.xml` - this XML allows to combines events into groups, like all solar events together. The `ID` of the event is used as `Address` in the `ecnEventTypeGroup` inside `DPDefinitions.xml`. 24 | - `Textresource_de.xml` (for German) and `Textresource_en.xml` (for English) - translate `Labels` into a `Value`, which is a readable label for a field in the databases if the `Name` starts with `@@`. 25 | - `sysDeviceIdent.xml` - additional common events, which are needed to identify a specific heating system. 26 | 27 | 28 | ## `sysDeviceIdent.xml` 29 | 30 | Eight addresses to be used via `Virtual Read` are defined for all devices. 31 | 32 | | Viessmann internal name | Address | in XML files | 33 | | -------------------------- | ------- | ----------- | 34 | | `ProtocolIdentifierOffset` | `0xF0` | `ecnsysDeviceIdentF0` | 35 | | `GRUPPENIDENTIFIKATION` | `0xF8` | `sysDeviceGroupIdent` | 36 | | `REGLERIDENTIFIKATION` | `0xF9` | `sysDeviceIdent` | 37 | | `HARDWAREINDEX` | `0xFA` | `sysHardwareIndexIdent` | 38 | | `SOFTWAREINDEX` | `0xFB` | `sysSoftwareIndexIdent` | 39 | | `VERSION_LDA` | `0xFC` | `sysProtokollversionLDAIdent` | 40 | | `VERSION_RDA` | `0xFD` | `sysProtokollversionRDAIdent` | 41 | | `VERSION_SW1` | `0xFE` | `sysHighByteDeveloperVersionIdent` | 42 | | `VERSION_SW2` | `0xFF` | `sysLowByteDeveloperVersionIdent` | 43 | 44 | 45 | ## `ecnDataPointType.xml` 46 | 47 | `ecnDataPointType.xml` contains all information to identify supported systems. It is essential to run Vitosoft _once_ for this file to be generated correctly – otherwise you have a very outdated version. 48 | 49 | - `ID` is the global id, which is referenced by other databases as `Address`. 50 | - `Description` is an internal description of the system 51 | - `Identification` must match `sysDeviceGroupIdent` and `sysDeviceIdent`, which are combined as 4-digit hex-values. This is the main way to identify the actual system. 52 | - `IdentificationExtension` allows to identify specific hardware and software revisions, which might have different limitations. It must match the combined hex-values of `sysHardwareIndexIdent` and `sysSoftwareIndexIdent`. If this key is larger than 4 hex-characters, it does not seem to match with the Vitosoft app, it might be used for an internal app only. 53 | - `IdentificationExtensionTill` if this one exists, then `IdentificationExtension` till `IdentificationExtensionTill` is a range of systems, which match. 54 | - `EventOptimisation` can be 0 (allowed) or 1 (not allowed). It allows Vitosoft to combine certain read requests into one for better performance. 55 | - `EventOptimisationExceptionList` disables event optimization for specific events. 56 | - `Options` additional options for the system. Only `undefined`, `trending` and `appointeddate` are defined and i think none of them is used by Vitosoft. 57 | - `ControllerType` can have the following values: `GWG` = 0, `WP` = 1, `NR` = 2, `VC300Old` = 3, `BHKW` = 4, `WO1A` = 5, `VC100Old` = 6, `VC200Old` = 7, `VC100New` = 8, `VC200New` = 9, `VC300New` = 10, `WPR3` = 11, `VitotwinGW_BATI` = 12, `NRP` = 14 – it seems that only VC100–VC300 are supported by Vitosoft Optolink. 58 | - `ErrorType` can have the following values: 0 = Unknown, 1 = Default, 2 = VCom, 3 = WP, 4 = BHKW, 5 = DefaultRPC, 6 = WPR3, 7 = VitotwinGW_BATI. It seems to be ignored by Vitosoft. 59 | - `F0` match the `ProtocolIdentifierOffset` value. 60 | - `F0Till` similar to `IdentificationExtensionTill`, it allows defining a range. 61 | 62 | There are some special cases for identification of systems, which are hard-coded in the Vitosoft application, be aware. 63 | 64 | ## DPDefinitions 65 | 66 | `ecnConverter` → `ecnDeviceType` (Vendor, always Viessmann) → `ecnDatapointType` (Heating units, devices) → `ecnEventType` (parameter) → `ecnEventValueType` (individual values for a specific parameter) 67 | 68 | `ecnEventTypeGroup` (groups of parameters) → `ecnEventType` (individual parameter) 69 | 70 | ![Graph of the dependencies](graph.gv.pdf) 71 | 72 | ### ecnConverter 73 | Base class, only exist once. Can be ignored. 74 | 75 | `Id` unique ID of the converters 76 | 77 | 1 78 | vsm.Dispatcher.Notification 79 | vsm.Dispatcher, Version=1.2.0.0, Culture=neutral, PublicKeyToken=6ffa2c8f99f9eb6d 80 | 81 | ### ecnConverterDeviceTypeLink 82 | Defines a relationship between the converter and the devices. There is only one devices: Viessmann. Can be ignored. 83 | 84 | `ConverterId` is a reference to `Id` in `ecnConverter` 85 | `DeviceTypeTechnicalIdentificationAddress` is a reference to `TechnicalIdentificationAddress` in `ecnDeviceType` 86 | 87 | 1 88 | 7 89 | 90 | 91 | ### ecnDeviceType 92 | Sits below the converter, only exists once (ID 54). Unimportant when communicating with the unit. The "device" is always Viessmann! Can be ignored. 93 | 94 | `Id` unique ID of the device 95 | `Name` name of the device 96 | `Manufacturer` manufacturer of the device 97 | `Description` internal description 98 | `StatusEventTypeId` `ecnEventType` with the status 99 | `TechnicalIdentificationAddress` a reference to `ecnConverterDeviceTypeLink` 100 | 101 | 54 102 | Viessmann Anlage 103 | 104 | 105 | 1 106 | 7 107 | 108 | #### ecnDeviceTypeDataPointTypeLink 109 | Defines a relationship between a device and multiple data points. A device in this context is "Viessmann", while data points are the heating units. Can be ignored. 110 | 111 | `DeviceTypeId` is a reference to `Id` in `ecnDeviceType` 112 | `DataPointTypeId` is a reference to `Id` in `ecnDatapointType` 113 | 114 | 54 115 | 1 116 | 117 | 118 | ### ecnDatapointType 119 | A data point is an actual heating unit. There are about 400 defined. This is the first important type! 120 | 121 | `Id` unique ID of the data point 122 | `Name` name of the data point. '@@' as a prefix points into `Textresource_xx.xml` for translation. 123 | `Description` internal info about this datapoint. Always in German. 124 | `StatusEventTypeId` a `ecnEventType` with the status 125 | `Address` a reference to the `ID` in `ecnDataPointType.xml`, which describes how the heating unit can be identified. 126 | 127 | 350 128 | @@viessmann.datapointtype.name.VScotHO1_72 129 | Technische Produktbeschreibung: ab Softwareindex 72 (Projekt Neptun) 130 | 1 131 |
VScotHO1_72
132 | 133 | #### ecnDataPointTypeEventTypeLink 134 | Defines a relationship between a data point and multiple events. 135 | 136 | `DataPointTypeId` is a reference to `Id` in `ecnDatapointType` 137 | `EventTypeId` is a reference to `Id` in `ecnEventType` 138 | 139 | 10 140 | 2 141 | 142 | 143 | ### ecnEventType 144 | Events are individual parameter in the heating unit. 145 | 146 | `Id` unique ID of the event 147 | `EnumType` Is this event an enum value? Only `true` or `false` are valid. 148 | `Name` name of the events. '@@' as a prefix points into `Textresource_xx.xml` for translation. 149 | `Address` a reference to the `ID` in `ecnEventType.xml`, which has information on how to access the value in the heating unit as well as value ranges or lists (for enums) 150 | `Conversion` How to convert the binary value, see conversion 151 | `Description` internal info about this event. Always in German. 152 | `Priority` The default is 100, sometimes (enums, adressen 0xF8…0xFF=1…8) it is 50. Probably used to sort the order of requests. 153 | `Filtercriterion` always seems to be `true`? 154 | `Reportingcriterion` always seems to be `true`? 155 | `Type` allowed access to this event: 1=Read-only, 2=Read/Write, 3=Write-only 156 | `URL` Vitosoft internal web server URL to visualize the datapoint, e.g. an editor the time/date scheduling. 157 | `DefaultValue` default value for this event, if not read from the heating unit. Can be an integer or float. 158 | 159 | 329 160 | false 161 | @@viessmann.eventtype.name.GWG_Aussentemperatur 162 |
GWG_Aussentemperatur~0x006F
163 | Div2 164 | @@viessmann.eventtype.GWG_Aussentemperatur.description 165 | 100 166 | true 167 | true 168 | 1 169 | 170 | 171 | 172 | #### ecnEventTypeEventValueTypeLink 173 | Defines a relationship between an event and multiple event values. 174 | 175 | `EventTypeId` is a reference to `Id` in `ecnEventType` 176 | `EventValueId` is a reference to `Id` in `ecnEventValueType` 177 | 178 | 1 179 | 13287 180 | 181 | ### ecnEventValueType 182 | Value of an event, used to define conditions. 183 | 184 | `Id` unique ID of the event value 185 | `Name` name of this event value. '@@' as a prefix points into `Textresource_xx.xml` for translation. 186 | 187 | 2 188 | @@viessmann.eventvaluetype.name.Absenkzeit_gelerntA1M1 189 | 190 | 0 191 | ecnUnit.Minuten 192 | Int 193 | 10 194 | 0 195 | 250 196 | 197 | 198 | 199 | ### ecnEventTypeGroup 200 | Allows to group events together, like all solar events. Used for a more pleasant visualization. 201 | 202 | `Id` unique ID of the group 203 | `Name` name of the group. '@@' as a prefix points into `Textresource_xx.xml` for translation. 204 | `ParentId` is a reference to `Id` in `ecnEventTypeGroup`. -1 = no parent. 205 | `EntrancePoint` is the group an entrance or only a subgroup of another one? Only `true` or `false` are valid. 206 | `Address` a reference to `ecnEventTypeGroup.xml` 207 | `DeviceTypeId` is a reference to `Id` in `ecnDeviceType`. There is only one: 54 (Viessmann) 208 | `DataPointTypeId` is a reference to `Id` in `ecnDatapointType`. 209 | `OrderIndex` Sorting order for the group within the `DeviceTypeId`/`DataPointTypeId` selection. 210 | 211 | 3 212 | @@viessmann.eventtypegroup.name.Dekamatik_E~10_Bedienung 213 | 214 | -1 215 | true 216 |
Dekamatik_E~10_Bedienung
217 | 54 218 | 144 219 | 1 220 | 221 | #### ecnEventTypeEventTypeGroupLink 222 | Defines a relationship between a group and multiple events including an order value. 223 | 224 | `EventTypeId` is a reference to `Id` in `ecnEventType` 225 | `EventTypeGroupId` is a reference to `Id` in `ecnEventTypeGroup` 226 | `EventTypeOrder` ist die Sortierreihenfolge des Event Type innerhalb der Gruppe. Dubletten sind möglich! 227 | 228 | 1134 229 | 4 230 | 17 231 | 232 | 233 | ### ecnDisplayCondition 234 | A single condition to hide an event in the UI, because e.g. the hardware does not exist or other settings prohibit this event. As you can see: it allows to compare the event with a specific value for this event. This is extremely common for events, which are enums. But it is also possible to compare the event with a specific value, which could be a temperature limit. 235 | 236 | `Id` unique ID of the display condition 237 | `Name` name of the display condition - never set 238 | `ConditionGroupId` is a reference to `Id` in `ecnDisplayConditionGroup` 239 | `EventTypeIdCondition` is a reference to `Id` in `ecnEventType` 240 | `EventTypeValueCondition` is a reference to `Id` in `ecnEventValueType` 241 | `Description` internal info for this display condition 242 | `Condition` compare operator: 0=Equal, 1=NotEqual, 2=GreaterThan, 3=GreaterThanOrEqualTo, 4=LessThan, 5=LessThanOrEqualTo 243 | `ConditionValue` defined, if `Condition` >= 2, contains the value for the comparism 244 | 245 | 19922 246 | 247 | 5993 248 | 5276 249 | 8403 250 | 251 | 2 252 | 300 253 | 254 | ### ecnDisplayConditionGroup 255 | A display condition group allows combining multiple display conditions via AND or OR to a complex term. Best to look at the Python sample code for more details. 256 | 257 | `Id` unique ID of the condition group 258 | `Name` name of the condition group 259 | `Type` 1=AND or 2=OR all conditions 260 | `ParentId` is a reference to `Id` in `ecnDisplayConditionGroup`. -1 = no parent. 261 | `Description` internal info for this condition group 262 | `EventTypeIdDest` is a reference to `Id` in `ecnEventType`. -1 = not defined 263 | `EventTypeGroupIdDest` is a reference to `Id` in `ecnEventTypeGroup`. -1 = not defined 264 | 265 | 1 266 | VDensHC2 Überblick Solar 267 | 1 268 | -1 269 | 270 | -1 271 | 5604 272 | 273 | ## Conversion 274 | 275 | If a `ConversionFactor` is not defined, it is assumed to be `1`. If a `ConversionOffset` is not defined, it is assumed to be `0`. 276 | 277 | | conversion name | result type | conversion of the byte buffer `buf` | 278 | |---------------------------|-----|---------------| 279 | | NoConversion | – | no conversion | 280 | | MultOffset | Double | `V * ConversionFactor + ConversionOffset` | 281 | | Mult2 | Double | `V * 2` | 282 | | Mult5 | Double | `V * 5` | 283 | | Mult10 | Double | `V * 10` | 284 | | Mult100 | Double | `V * 100` | 285 | | Div2 | Double | `V / 2` | 286 | | Div10 | Double | `V / 10` | 287 | | Div100 | Double | `V / 100` | 288 | | Div1000 | Double | `V / 1000` | 289 | | Time53 | String | `HH:MM` with `HH=buf[0] >> 3; MM=(buf[0] & 7) * 10;` or an empty string, if `buffer[0]==0xFF` | 290 | | DateBCD | DateTime | BCD in `buf` with 8 bytes `YYYYMMDDxxHHMMSS` | 291 | | DateTimeBCD | DateTime | identical to DateTimeBCD | 292 | | DateTimeVitocom | ? | *NOT IMPLEMENTED* | 293 | | Sec2Minute | Double | `Round(V / 60, 2)` | 294 | | Sec2Hour | Double | `Round(V / (60*60)), 2)` | 295 | | Sec2Day | Double | `Round(V / (24*60*60), 2)` | 296 | | Sec2Week | Double | `Round(V / (7*24*60*60), 2)` | 297 | | Sec2Month | Double | *NOT IMPLEMENTED* | 298 | | Estrich | ? | *NOT IMPLEMENTED* | 299 | | Kesselfolge | ? | *NOT IMPLEMENTED* | 300 | | IPAddress | String | IP4 buf[3]+'.'+buf[2]+'.'+buf[1]+'.'+buf[0] | 301 | | Steuerzeichen | ? | *NOT IMPLEMENTED* | 302 | | UTCDiff2Hour | ? | *NOT IMPLEMENTED* | 303 | | UTCDiff2Day | ? | *NOT IMPLEMENTED* | 304 | | UTCDiff2Month | ? | *NOT IMPLEMENTED* | 305 | | HourDiffSec2Hour | ? | *NOT IMPLEMENTED* | 306 | | VitocomEingang | ? | *NOT IMPLEMENTED* | 307 | | Vitocom300SGEinrichtenKanalLON | ? | *NOT IMPLEMENTED* | 308 | | Vitocom300SGEinrichtenKanalMBUS | ? | *NOT IMPLEMENTED* | 309 | | Vitocom300SGEinrichtenKanalWILO | ? | *NOT IMPLEMENTED* | 310 | | VitocomNV | ? | *NOT IMPLEMENTED* | 311 | | Vitocom3NV | ? | *NOT IMPLEMENTED* | 312 | | HexByte2DecimalByte | ByteArray | standard byte array | 313 | | RotateBytes | ByteArray | reverse all bytes in the array | 314 | | Phone2BCD | ByteArray | converts BCD data (0-9) in a ByteArray. Assumes the BCD value 15 to be ignored. | 315 | | ImpulszaehlerV300FA2 | ? | *NOT IMPLEMENTED* | 316 | | DatenpunktADDR | ? | *NOT IMPLEMENTED* | 317 | | DateTimeMBus | ? | *NOT IMPLEMENTED* | 318 | | DateMBus | ? | *NOT IMPLEMENTED* | 319 | | MultOffsetBCD | Double | Up to 0-4 byte LSB value `V`, then applies `V * ConversionFactor + ConversionOffset` | 320 | | HexToFloat | ? | *NOT IMPLEMENTED* | 321 | | MultOffsetFloat | Double | *NOT IMPLEMENTED* | 322 | | HexByte2AsciiByte | ByteArray | standard byte array – same as `HexByte2DecimalByte` | 323 | | DayToDate | DateTime | `buf` of up to 8 bytes will be interpreted as a 64-bit LSB value, which represents the number of days starting 1.1.1970 – UNIX timestamp | 324 | | FixedStringTerminalZeroes | | zero terminated string | 325 | | LastBurnerCheck | ? | *NOT IMPLEMENTED* | 326 | | LastCheckInterval | ? | *NOT IMPLEMENTED* | 327 | | Convert4BytesToFloat | | Converts 4 IEEE bytes into a float | 328 | 329 | 330 | ## Unnecessary or unknown 331 | 332 | ### ecnConfigSet 333 | 334 | `Id` unique ID of the config set 335 | 336 | 1 337 | IO_VC_DE1 338 | vsmCommInterface.businesslogic.IOConfigSetProcessorBL, vsmCommInterface 339 | 340 | ### ecnConfigSetParameter 341 | 342 | `Id` unique ID of the config set parameter 343 | 344 | 1 345 | 1 346 | Evaluate 347 | true 348 | 349 | ### ecnCulture 350 | 351 | Used for translations of the interface in different languages. Not necessary to communicate with the system. 352 | 353 | `Id` unique ID of the culture 354 | `Name` ISO-Sprachcode 355 | 356 | 2 357 | en 358 | 359 | ### ecnStatusType 360 | 361 | Error status (ecnEventType #1). The following `Id` are defined: 362 | 363 | 0 364 | : Undefined (SortOrder = 100, ShowInEventTray = false) 365 | 366 | 1 367 | : OK (SortOrder = 3, ShowInEventTray = false) 368 | 369 | 2 370 | : Warning (SortOrder = 2, ShowInEventTray = true) 371 | 372 | 3 373 | : Error (SortOrder = 1, ShowInEventTray = true) 374 | 375 | 4 376 | : illegal value (SortOrder = 4, ShowInEventTray = true) 377 | 378 | 5 379 | : --- (SortOrder = 5, ShowInEventTray = true) 380 | 381 | `Id` unique ID of the status (0-5 are possible) 382 | `Name` name of the status. '@@' as a prefix points into `Textresource_xx.xml` for translation. 383 | `ShowInEventTray` Is the status value shown in the event tray? 384 | `Image` Base64 of a GIF representing the status 385 | `SortOrder` sort oder for the status 386 | 387 | 5 388 | @@viessmann.vitodata.valuestatus.notevaluate 389 | true 390 | ……… 391 | 5 392 | -------------------------------------------------------------------------------- /Viessmann2MQTT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import time 6 | import os 7 | import serial 8 | import binascii 9 | import enum 10 | from datetime import datetime 11 | import struct 12 | import paho.mqtt.client as mqtt 13 | import logging 14 | import logging.handlers 15 | 16 | logging.getLogger().setLevel('DEBUG') 17 | logging.info('Starting Viessmann2mqtt') 18 | 19 | MQTT_USER = "mqtt username" 20 | MQTT_PASSWORD = "mqtt password" 21 | MQTT_SERVER = 'mqtt server name or IP' 22 | MQTT_TOPIC = 'Viessmann' 23 | 24 | if not MQTT_TOPIC.endswith("/"): 25 | MQTT_TOPIC+="/" 26 | 27 | scriptPathAndName = None 28 | lastModDate = None 29 | def wait100ms(): 30 | global scriptPathAndName,lastModDate 31 | if not scriptPathAndName: 32 | scriptPathAndName = os.path.realpath(__file__) 33 | if not lastModDate: 34 | lastModDate = time.ctime(os.path.getmtime(scriptPathAndName)) 35 | if lastModDate != time.ctime(os.path.getmtime(scriptPathAndName)): 36 | print ('#### RELAUNCH ####') 37 | os.execv(sys.argv[0], sys.argv) 38 | sys.exit(0) 39 | time.sleep(0.1) 40 | 41 | ser = serial.Serial(port='/dev/ttyUSB0', baudrate=4800, parity=serial.PARITY_EVEN, stopbits=serial.STOPBITS_TWO, bytesize=serial.EIGHTBITS) 42 | 43 | def connecthandler(mqc,userdata,flags,rc): 44 | logging.info("Connected to MQTT broker with rc=%d" % (rc)) 45 | mqc.publish(MQTT_TOPIC+"connected",True,qos=1,retain=True) 46 | 47 | def disconnecthandler(mqc,userdata,rc): 48 | logging.warning("Disconnected from MQTT broker with rc=%d" % (rc)) 49 | 50 | 51 | def startCommunication(): 52 | #print("SEND EOT") 53 | ser.flush() 54 | ser.write(binascii.unhexlify('04')) 55 | sendStart = False 56 | t = 0 57 | while t < 3000: 58 | #print("... %.1fms" % (t*0.001)) 59 | if ser.in_waiting: 60 | buf = ser.read() 61 | if len(buf) == 1: 62 | if buf[0] == 0x05: 63 | #print("RECEIVED ENQ") 64 | #print("SEND VS2_START_VS2") 65 | ser.write(binascii.unhexlify('160000')) 66 | sendStart = True 67 | t = 0 68 | elif sendStart: 69 | if buf[0] == 0x06: # VS2_ACK 70 | #print("RECEIVED VS2_ACK") 71 | return True 72 | elif buf[0] == 0x15: # VS2_NACK 73 | #print("RECEIVED VS2_NACK") 74 | #print("SEND VS2_START_VS2") 75 | ser.write(binascii.unhexlify('160000')) 76 | sendStart = True 77 | t = 0 78 | else: 79 | #print("RECEIVED %s" % binascii.hexlify(buf)) 80 | pass 81 | else: 82 | #print("RECEIVED %s" % binascii.hexlify(buf)) 83 | pass 84 | wait100ms() 85 | t += 100 86 | return False 87 | 88 | class ReceiveState(enum.IntEnum): 89 | unknown = 0 90 | ENQ = 1 91 | ACK = 2 92 | NACK = 3 93 | class ProtocolIdentifier(enum.IntEnum): 94 | LDAP = 0 95 | RDAP = 0x10 96 | class MessageIdentifier(enum.IntEnum): 97 | RequestMessage = 0 98 | ResponseMessage = 1 99 | UNACKDMessage = 2 100 | ErrorMessage = 3 101 | class FunctionCodes(enum.IntEnum): 102 | undefined = 0 103 | Virtual_READ = 1 104 | Virtual_WRITE = 2 105 | Physical_READ = 3 106 | Physical_WRITE = 4 107 | EEPROM_READ = 5 108 | EEPROM_WRITE = 6 109 | Remote_Procedure_Call = 7 110 | Virtual_MBUS = 33 111 | Virtual_MarktManager_READ = 34 112 | Virtual_MarktManager_WRITE = 35 113 | Virtual_WILO_READ = 36 114 | Virtual_WILO_WRITE = 37 115 | XRAM_READ = 49 116 | XRAM_WRITE = 50 117 | Port_READ = 51 118 | Port_WRITE = 52 119 | BE_READ = 53 120 | BE_WRITE = 54 121 | KMBUS_RAM_READ = 65 122 | KMBUS_EEPROM_READ = 67 123 | KBUS_DATAELEMENT_READ = 81 124 | KBUS_DATAELEMENT_WRITE = 82 125 | KBUS_DATABLOCK_READ = 83 126 | KBUS_DATABLOCK_WRITE = 84 127 | KBUS_TRANSPARENT_READ = 85 128 | KBUS_TRANSPARENT_WRITE = 86 129 | KBUS_INITIALISATION_READ = 87 130 | KBUS_INITIALISATION_WRITE = 88 131 | KBUS_EEPROM_LT_READ = 89 132 | KBUS_EEPROM_LT_WRITE = 90 133 | KBUS_CONTROL_WRITE = 91 134 | KBUS_MEMBERLIST_READ = 93 135 | KBUS_MEMBERLIST_WRITE = 94 136 | KBUS_VIRTUAL_READ = 95 137 | KBUS_VIRTUAL_WRITE = 96 138 | KBUS_DIRECT_READ = 97 139 | KBUS_DIRECT_WRITE = 98 140 | KBUS_INDIRECT_READ = 99 141 | KBUS_INDIRECT_WRITE = 100 142 | KBUS_GATEWAY_READ = 101 143 | KBUS_GATEWAY_WRITE = 102 144 | PROZESS_WRITE = 120 145 | PROZESS_READ = 123 146 | OT_Physical_Read = 180 147 | OT_Virtual_Read = 181 148 | OT_Physical_Write = 182 149 | OT_Virtual_Write = 183 150 | GFA_READ = 201 151 | GFA_WRITE = 202 152 | 153 | class VS2Message(): 154 | protocol = ProtocolIdentifier.LDAP 155 | identifier = MessageIdentifier.RequestMessage 156 | Command = FunctionCodes.undefined 157 | ADDR = 0 158 | Data = bytes() 159 | msgBytes = bytes() 160 | 161 | def __init__(self, *args, **kwargs): 162 | if len(args) == 1: 163 | self.msgBytes = args[0] 164 | #print("VS2Message(%s)" % binascii.hexlify(self.msgBytes)) 165 | self.protocol = ProtocolIdentifier(self.msgBytes[0] & 0xF0) 166 | self.identifier = MessageIdentifier(self.msgBytes[0] & 0x0F) 167 | self.Command = FunctionCodes(self.msgBytes[1]) 168 | self.ADDR = (self.msgBytes[2] << 8) + self.msgBytes[3] 169 | self.BlockSize = self.msgBytes[4] 170 | self.Data = self.msgBytes[5:] 171 | else: 172 | self.protocol = args[0] 173 | self.identifier = args[1] 174 | self.Command = args[2] 175 | self.ADDR = args[3] 176 | self.BlockSize = args[4] 177 | if len(args) > 5: 178 | self.Data = args[5] 179 | else: 180 | self.Data = None 181 | 182 | buf = bytearray([self.protocol.value | self.identifier.value, self.Command.value, self.ADDR >> 8, self.ADDR & 0xFF, self.BlockSize]) 183 | if self.Data: 184 | buf += bytearray(self.Data) 185 | buf = bytearray([0x41, len(buf)]) + buf 186 | buf = buf + bytearray([sum(buf[1:]) & 0xFF]) 187 | self.msgBytes = bytes(buf) 188 | #print("VS2Message(%s)" % binascii.hexlify(self.msgBytes)) 189 | 190 | def __str__(self): 191 | if self.Data: 192 | str = '%s' % self.Data.hex() 193 | else: 194 | str = '' 195 | return f'%s %s %s 0x%04x %d:%s' % (self.protocol, self.identifier, self.Command, self.ADDR, self.BlockSize, str) 196 | 197 | def sendVS2Message(message): 198 | #print("SEND %s" % binascii.hexlify(message.msgBytes)) 199 | ser.write(message.msgBytes) 200 | t = 0 201 | buf = None 202 | receiveStatus = ReceiveState.unknown 203 | while t < 3000: 204 | #print("... %.1fms" % (t*0.001)) 205 | if ser.in_waiting: 206 | if buf == None: 207 | buf = ser.read(ser.in_waiting) 208 | else: 209 | buf += ser.read(ser.in_waiting) 210 | if len(buf): 211 | if buf[0] == 0x06: # VS2_ACK 212 | if receiveStatus != ReceiveState.ACK: 213 | #print("RECEIVED VS2_ACK") 214 | pass 215 | receiveStatus = ReceiveState.ACK 216 | elif buf[0] == 0x15: # VS2_NACK 217 | if receiveStatus != ReceiveState.NACK: 218 | #print("RECEIVED VS2_NACK") 219 | pass 220 | receiveStatus = ReceiveState.NACK 221 | if len(buf) > 1: 222 | if receiveStatus == ReceiveState.NACK: 223 | if buf[1] == 0x06: # VS2_ACK 224 | buf = buf[1:] 225 | receiveStatus = ReceiveState.ACK 226 | elif buf[1] == 0x15: # VS2_NACK 227 | buf = buf[1:] 228 | if len(buf) > 1: 229 | if receiveStatus != ReceiveState.ACK and receiveStatus != ReceiveState.NACK: 230 | break # unknown state 231 | #print("RECEIVED %s" % binascii.hexlify(buf)) 232 | if len(buf) > 3 and buf[1] == 0x41 and len(buf) == 1 + buf[2] + 3 and (sum(buf[2:-1]) & 0xff) == buf[-1]: 233 | msg = None 234 | if buf[0] == 0x06: # VS2_ACK 235 | msg = VS2Message(buf[3:-1]) 236 | ser.write(binascii.unhexlify('06')) # VS2_ACK 237 | return msg 238 | wait100ms() 239 | t += 100 240 | return None 241 | 242 | commStarted = False 243 | 244 | def errorcode(errorcode): 245 | errorcode_VScotHO1_72 = { 246 | 0x00: "Anlage ohne Fehler", 247 | 0x0F: "Wartung durchführen", 248 | 0x10: "Kurzschluss Außentemperatursensor", 249 | 0x18: "Unterbrechung Außentemperatursensor", 250 | 0x19: "Fehler externer Außentemperatursensor (Anschlusserweiterung ATS1)", 251 | 0x1D: "Störung Volumenstromsensor (STRS1)", 252 | 0x1E: "Störung Volumenstromsensor (STRS1)", 253 | 0x1F: "Störung Volumenstromsensor (STRS1)", 254 | 0x20: "Kurzschluss Vorlaufsensor Anlage", 255 | 0x28: "Unterbrechung Vorlaufsensor Anlage", 256 | 0x30: "Kurzschluss Kesseltemperatursensor", 257 | 0x38: "Unterbrechung Kesseltemperatursensor", 258 | 0x40: "Kurzschluss Vorlaufsensor HK2", 259 | 0x41: "Rücklauftemperatur HK2 Kurzschluss", 260 | 0x44: "Kurzschluss Vorlaufsensor HK3", 261 | 0x45: "Rücklauftemperatur HK3 Kurzschluss", 262 | 0x48: "Unterbrechung Vorlaufsensor HK2", 263 | 0x49: "Rücklauftemperatur HK2 Unterbrechung", 264 | 0x4C: "Unterbrechung Vorlaufsensor HK3", 265 | 0x4D: "Rücklauftemperatur HK3 Unterbrechung", 266 | 0x50: "Kurzschluss Speichertemperatursensor / Komfortsensor / Ladesensor", 267 | 0x51: "Kurzschluss Auslauftemperatursensor", 268 | 0x58: "Unterbrechung Speichertemperatursensor/ Komfortsensor/ Ladesensor", 269 | 0x59: "Unterbrechung Auslauftemperatursensor", 270 | 0x90: "Solarmodul: Kurzschluss Sensor 7", 271 | 0x91: "Solarmodul: Kurzschluss Sensor 10", 272 | 0x92: "Solarregelung: Kurzschluss Kollektortemp.Sensor", 273 | 0x93: "Solarregelung: Kurzschluss Kollektorrücklauf-Sensor", 274 | 0x94: "Solarregelung: Kurzschluss Speichertemp.Sensor", 275 | 0x98: "Solarmodul: Unterbrechung Sensor 7", 276 | 0x99: "Solarmodul: Unterbrechung Sensor 10", 277 | 0x9A: "Solar: Unterbrech. Kollektortemp.Sensor", 278 | 0x9B: "Vitosolic: Unterbrech. Kollektorrücklauf", 279 | 0x9C: "Solar: Unterbrech. Speichertemp.Sensor", 280 | 0x9E: "Solarmodul: Delta-T Überwachung", 281 | 0x9F: "Solarregelung: allgemeiner Fehler ", 282 | 0xA2: "Fehler niedriger Wasserdruck Regelung", 283 | 0xA3: "Abgastemperatursensor gesteckt", 284 | 0xA4: "Überschreitung Anlagenmaximaldruck", 285 | 0xA6: "Fehler Fremdstromanode nicht in Ordnung", 286 | 0xA7: "Fehler Uhrenbaustein Bedienteil ", 287 | 0xA8: "Interne Pumpe meldet Luft", 288 | 0xA9: "Interne Pumpe blockiert", 289 | 0xB0: "Kurzschluss Abgastemperatursensor", 290 | 0xB1: "Fehler Bedienteil", 291 | 0xB4: "interner Fehler Temperaturmessung", 292 | 0xB5: "interner Fehler EEPROM", 293 | 0xB7: "Kesselcodierkarte falsch/fehlerhaft", 294 | 0xB8: "Unterbrechung Abgastemperatursensor", 295 | 0xB9: "Fehlerhafte Übertragung Codiersteckerdaten", 296 | 0xBA: "Kommunikationsfehler Mischer HK2", 297 | 0xBB: "Kommunikationsfehler Mischer HK3", 298 | 0xBC: "Fehler Fernbedienung HK1", 299 | 0xBD: "Fehler Fernbedienung HK2", 300 | 0xBE: "Fehler Fernbedienung HK3", 301 | 0xBF: "LON-Modul falsch/fehlerhaft", 302 | 0xC1: "Kommunikationsfehler Anschl.Erw. EA1", 303 | 0xC2: "Kommunikationsfehler Solarregelung", 304 | 0xC3: "Kommunikationsfehler Anschl.Erw. AM1", 305 | 0xC4: "Kommunikationsfehler Anschl.Erw. OT", 306 | 0xC5: "Fehler Drehzahlgeregelte Pumpe - Interne Pumpe", 307 | 0xC6: "Fehler Drehzahlgeregelte Pumpe Heizkreis 2", 308 | 0xC7: "Fehler Drehzahlgeregelte Pumpe Heizkreis 1", 309 | 0xC8: "Fehler Drehzahlgeregelte Pumpe Heizkreis 3", 310 | 0xC9: "Komm.-fehler KM-Bus Gerät DAP1", 311 | 0xCA: "Komm.-fehler KM-Bus Gerät DAP2", 312 | 0xCD: "Kommunikationsfehler Vitocom 100", 313 | 0xCE: "Kommunikationsfehler Anschlußerweiterung extern", 314 | 0xCF: "Kommunikationsfehler LON-Modul", 315 | 0xD1: "Brennerstörung", 316 | 0xD6: "Störung Digitaler Eingang 1", 317 | 0xD7: "Störung Digitaler Eingang 2", 318 | 0xD8: "Störung Digitaler Eingang 3", 319 | 0xDA: "Kurzschluss Raumtemperatursensor HK1", 320 | 0xDB: "Kurzschluss Raumtemperatursensor HK2", 321 | 0xDC: "Kurzschluss Raumtemperatursensor HK3", 322 | 0xDD: "Unterbrechung Raumtemperatursensor HK1", 323 | 0xDE: "Unterbrechung Raumtemperatursensor HK2", 324 | 0xDF: "Unterbrechung Raumtemperatursensor HK3", 325 | 0xE0: "Fehler externer Teilnehmer LON", 326 | 0xE1: "SCOT Kalibrationswert Grenzverletzung O", 327 | 0xE2: "Keine Kalibration wg mangelnder Strömung", 328 | 0xE3: "Kalibrationsfehler thermisch", 329 | 0xE4: "Fehler in Spannungsversorgung 24V - Feuerungsautomat", 330 | 0xE5: "Fehler Flammenverstärker - Feuerungsautomat", 331 | 0xE6: "Min. Luft-/Wasserdruck nicht erreicht", 332 | 0xE7: "SCOT Kalibrationswert Grenzverletzung U", 333 | 0xE8: "SCOT Ionisationssignal weicht ab", 334 | 0xEA: "SCOT Kalibrationswert abw. von Vorgänger", 335 | 0xEB: "SCOT Kalibration nicht ausgeführt", 336 | 0xEC: "SCOT Ionisationssollwert fehlerhaft", 337 | 0xED: "SCOT Systemfehler", 338 | 0xEE: "Keine Flammbildung", 339 | 0xEF: "Flammenausfall in Sicherheitszeit", 340 | 0xF0: "Kommunikationsfehler zum Feuerungsatomat", 341 | 0xF1: "Abgastemperaturbegrenzer ausgelöst", 342 | 0xF2: "TB ausgelöst - Übertemperatur", 343 | 0xF3: "Flammenvortäuschung", 344 | 0xF4: "Keine Flammenbildung", 345 | 0xF5: "Fehler Luftdruckwächter", 346 | 0xF6: "Fehler Gasdruckschalter", 347 | 0xF7: "Fehler Luftdruckschalter", 348 | 0xF8: "Fehler Gasventil", 349 | 0xF9: "Fehler Gebläse - Drehzahl nicht erreicht", 350 | 0xFA: "Fehler Gebläse - Stillstand nicht erreicht", 351 | 0xFB: "Flammenausfall im Betrieb", 352 | 0xFC: "Fehler in der elektrischen Ansteuerung der Gasarmatur", 353 | 0xFD: "Interner Fehler Feuerungsautomat", 354 | 0xFE: "Vorwarnung Wartung fällig (Warnung) ", 355 | 0xFF: "Fehler Feuerungsautomat ohne eigenen Fehlercode", 356 | } 357 | if errorcode in errorcode_VScotHO1_72: 358 | return errorcode_VScotHO1_72[errorcode] 359 | return 'errorcode_%02x' % errorcode 360 | 361 | def DateTimeFromBCD(data, offset): 362 | # data[4+offset] == weekday, 0 = Monday 363 | return datetime.strptime('%02x%02x-%02x-%02x %02x:%02x:%02x' % (data[0+offset],data[1+offset],data[2+offset],data[3+offset],data[5+offset],data[6+offset],data[7+offset]), '%Y-%m-%d %H:%M:%S') 364 | 365 | def PhaseDay(data): 366 | dayStrs = [] 367 | for dayOffset in range(0,7): 368 | phases = [] 369 | dateStr = '' 370 | for r in range(0,8): 371 | offs = dayOffset*8+r 372 | if offs >= len(data): # my Viessmann returns just 57 bytes on a 58 byte request 373 | bb = 0xff 374 | else: 375 | bb = data[dayOffset*8+r] 376 | hour = bb >> 3 377 | if hour >= 24: 378 | dateStr += '24:00' 379 | else: 380 | min = (bb & 7) * 10 381 | dateStr += '%02d:%02d' % (hour,min) 382 | if r & 1: 383 | phases.append(dateStr) 384 | dateStr = '' 385 | else: 386 | dateStr += '-' 387 | while phases[-1]=='24:00-24:00': 388 | del phases[-1] 389 | dayStrs.append(' '.join(phases)) 390 | result = '' 391 | lastStr = None 392 | firstDay = 0 393 | currentDay = 0 394 | weekDayList = ['Mo','Di','Mi','Do','Fr','Sa','So'] 395 | for weekDayStr in dayStrs: 396 | if lastStr == None: 397 | lastStr = weekDayStr 398 | elif lastStr != weekDayStr: 399 | result += '%s-%s:%s ' % (weekDayList[firstDay],weekDayList[currentDay],lastStr) 400 | firstDay = currentDay + 1 401 | currentDay += 1 402 | if currentDay != firstDay: 403 | result += '%s-%s:%s' % (weekDayList[firstDay],weekDayList[currentDay-1],lastStr) 404 | return result.strip() 405 | 406 | eventTypeConversionFunctions = { 407 | 'Mult2': (lambda data,offset: '%d' % (struct.unpack("