Last active
December 30, 2018 10:14
-
-
Save mqu/9519e39ccc474f111ffb to your computer and use it in GitHub Desktop.
rvitalk : ruby P300 protocol implementation to handle IO to Viessmann heating systems - outdated : please have a look at : https://github.com/mqu/viessmann-mqtt/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/ruby | |
# encoding: utf-8 | |
# author : Marc Quinton, april 2015 | |
# name : rvitalk : ruby P300 protocol implementation to handle IO to Viessmann heating systems | |
# object : connect to a Viessmann heating system via Optolink adaptator to query internal values. | |
# version : 0.5 - added write mode for commands, P300 constants, | |
# requirements : ruby >= 2.1, ruby-serialport, a serial USB optolink adapter, a Viessman heating system. | |
# licence : MIT | |
# links : http://openv.wikispaces.com/vcontrold ; https://gist.github.com/mqu | |
# Viessman Optolink, serial line parameters are : 4800, 8 E 2, Even parity, 2 bits stop, without Handshake protocol | |
# @baud_rate = 4800 | |
# @data_bits = 8 | |
# @stop_bits = 2 | |
# @parity = SerialPort::EVEN | |
# ------------------------------------------------------------------------------------ | |
# P300 protocol packets details: | |
# ------------------------------------------------------------------------------------ | |
# When data is read: | |
# 41: Telegram start byte | |
# 05: Length of user data (number of bytes between the telegram start byte (0x41) and checksum) | |
# 00: 00 = request, response = 01, 03 = Error | |
# 01: 01 = Read Data, Write Data = 02, 07 = Function Call | |
# XX XX: 2 byte address of the data or procedure | |
# XX: number of bytes expected in the response | |
# XX: CRC=sum total of the values from the 2 bytes (excluding 41) | |
# | |
# If data is to be written: | |
# 41: Telegram start byte | |
# 06: Length of user data (number of bytes between the telegram start byte (0x41) and checksum) | |
# 00: 00 = request, response = 01, 03 = Error | |
# 02: 01 = Read Data, Write Data = 02, 07 = Function Call | |
# XXXX: 2 bytes address of the data or procedure | |
# XX: number of bytes to be written in the | |
# XX: content to be written | |
# XX: CRC=sum total of the values from the 2 bytes (excluding 41) modulo 256 | |
# ------------------------------------------------------------------------------------ | |
# protocol status (error) | |
# status : 0x15: error, 06: OK (ACK), 05: not init, 07=? | |
# - in KW mode, we receive periodicaly 05 (not init) | |
# - when toggleling in P300 protocol, we receive 06 (OK) | |
# - sending packets with CRC error, we receive 0x15 (error) | |
# - some time we receive 0x00 : don't know why. | |
# some variable need to be converted with a factor | |
# temperatures need to be divided by 10 | |
# some variables are formated in little endian (temperature, counters) | |
# address are coded in big endian format. | |
# ------------------------------------------------------------------------------------ | |
# some examples | |
# Example query outside temperature (Vitotronic 333) | |
# Send 41 05 00 01 55 25 02 82 | |
# Receive 06 41 07 01 01 55 25 02 07 01 8D | |
# Answer: 0x0107 = 263 = 26.3 ° | |
# getDeviceID (00F8, 2bytes)-> 20BC | |
# TX: Data: 0x41 0x05 0x00 0x01 0x00 0xf8 0x02 0x00 | |
# RX: Data: 0x06 0x41 0x07 0x01 0x01 0x00 0xf8 0x02 0x20 0xcb 0xee | |
require "serialport" | |
require 'timeout' | |
require "pp" | |
# global variable for debug output traces. | |
$debug=true | |
$debug_level=2 | |
# from vcontrold/framer.c : https://github.com/taupinfada/vcontrold/blob/master/framer.c#L33 | |
# general marker of P300 protocol | |
P300_LEADIN = 0x41 | |
P300_RESET = 0x04 | |
P300_ENABLE = [ 0x16, 0x00, 0x00 ] | |
# message type | |
P300_REQUEST = 0x00 | |
P300_RESPONSE = 0x01 | |
P300_ERROR_REPORT = 0x03 | |
P300X_LINK_MNT = 0x0f | |
# function | |
P300_READ_DATA = 0x01 | |
P300_WRITE_DATA = 0x02 | |
P300_FUNCT_CALL = 0x07 | |
# #define P300X_OPEN 0x01 | |
# #define P300X_CLOSE 0x00 | |
# #define P300X_ATTEMPTS 3 | |
# // response | |
# #define P300_ERROR 0x15 | |
# #define P300_NOT_INIT 0x05 | |
# #define P300_INIT_OK 0x06 | |
# new Exceptions | |
class CRCError < StandardError | |
end | |
class UnsupportedTypeError < StandardError | |
end | |
class APIError < StandardError | |
end | |
class ProtocolError < StandardError | |
end | |
class ApplicationError < StandardError | |
end | |
class UnknownCommand < StandardError | |
end | |
class TimeoutIOError < StandardError | |
end | |
class IOError < StandardError | |
end | |
class Timer | |
@@start = Time.now.to_f | |
def self.time | |
return (Time.now.to_f - @@start).round(4) | |
end | |
def self.reset | |
@@start = Time.now.to_f | |
end | |
end | |
# TODO should use logger facility : http://rubylearning.com/satishtalim/ruby_logging.html | |
class Debug | |
def self.printf level, *args | |
Kernel::printf(*args) if ($debug) && ($debug_level>=level) | |
end | |
end | |
class Array | |
def sum | |
self.inject{|sum,x| sum + x } | |
end | |
def to_hex | |
s=[] | |
self.each {|e| s << sprintf('0x%02x',e.ord)} | |
'[ ' +s.join(', ') + ' ]' | |
end | |
def to_s | |
to_hex | |
end | |
end | |
# manipulate adresses values (optolink format) | |
class Addr | |
# split 16 bits integrer (2 bytes len) as an array of 2 bytes | |
# addr=a1a2 as short -> [ a1, a2 ] as bytes. | |
# ex : 0x00F8 -> [0x00, 0xF8] | |
def self.split addr | |
a1 = (addr & 0xFF00) >> 8 | |
a2 = (addr & 0x00FF) | |
return [a1,a2] | |
end | |
# reverse of split | |
def self.unsplit a1, a2 | |
return a1*256 + a2 | |
end | |
end | |
# handle serial device IO | |
class TTy | |
def initialize _port=nil, _baud_rate=4800, _data_bits=8, _stop_bits=2, _parity=SerialPort::EVEN | |
@baud_rate = _baud_rate | |
@data_bits = _data_bits | |
@stop_bits = _stop_bits | |
@parity = _parity | |
@port=_port | |
raise ApplicationError, sprintf("serial port not found %s", _port) unless File.exists? _port | |
@sp=SerialPort.new(@port, @baud_rate, @data_bits, @stop_bits, @parity) unless @port==nil | |
end | |
def open port | |
@sp = SerialPort.new(port, @baud_rate, @data_bits, @stop_bits, @parity) | |
end | |
def close | |
@sp.close unless @sp==nil | |
@sp=nil | |
end | |
def sync= bool=false | |
@sp.sync=bool | |
end | |
def sync? | |
@sp.sync? | |
end | |
def flush | |
@sp.flush | |
end | |
# read 1 byte from serial | |
# can throw exception : 'write': Input/output error (Errno::EIO) | |
def read | |
begin | |
c=nil | |
# Debug::printf(5, "# TTy::reading ... \n") | |
while c==nil | |
self.flush() | |
c=@sp.read(1) | |
break if c!=nil | |
end | |
# Debug::printf(5, "# TTy::read : 0x%02x\n", c.ord) | |
return c | |
rescue | |
Debug::printf(2, "## TTy::read : IOerror\n") | |
raise IOError, "IO error reading serial" | |
end | |
end | |
# write 1 byte from serial | |
# can throw exception : 'write': Input/output error (Errno::EIO) | |
def write data, trace=false | |
begin | |
if data.is_a? Array | |
# Debug::printf(5, "# TTy::write (array) : %s\n", data.to_s) | |
data.each { |d| self.write(d,false) } | |
elsif data.is_a? String | |
self.write(data.to_i,false) # never tested ... take care | |
elsif data.is_a? Fixnum | |
@sp.putc(data) | |
else | |
raise "unsupported type : " + data.class | |
end | |
rescue | |
Debug::printf(2, "## TTy::write : IOerror\n") | |
raise IOError, "IO error writing serial" | |
end | |
end | |
end | |
# what is a data packet for OptoLink IO ? | |
class Packet | |
def initialize addr, len | |
@start=P300_LEADIN # start byte | |
@plen=5 # len of packet (different pour request and responses) | |
@cmd1=P300_REQUEST # 00=request (host -> Vitoden), 01=response, 03=Error | |
@cmd2=P300_READ_DATA # 01=Read Data, 02=Write Data = 02, 07=Function Call | |
# data | |
@data=nil # for read : address (high byte + low byte) + len (1byte) | |
@len=len # len of data | |
# checksum | |
@crc=nil # CRC from all bytes except start | |
@addr=addr | |
end | |
def size | |
raise "size() method should not be called in Packet class" | |
end | |
# CRC is made with the sum of packet len till data buffer except first byte. | |
def crc | |
sum=(@plen+@cmd1+@cmd2+@data.sum)%256 | |
end | |
# for debug only | |
def to_s | |
self.to_bytes.map {|e| e = sprintf("0x%02x",e)} | |
end | |
# return an array of byte ; will be sent to TTy device. | |
def to_bytes | |
return [@start, @plen, @cmd1, @cmd2, @data, @crc].flatten | |
end | |
end | |
# read request packet : use this class to build a request packet to read : an address with a len. | |
class ReadRequestPacket < Packet | |
def initialize addr, len | |
super addr, len | |
a=Addr::split addr | |
@data=[a[0], a[1], len] | |
@crc=self.crc | |
end | |
# return size of read request packet | |
def size | |
return 5 | |
end | |
# build packet for reading and return it a array of bytes | |
def pack | |
return self.to_bytes | |
end | |
end | |
# read request packet : use this class to build a request packet to read : an address with a len. | |
# samples packets : | |
# get mode (0x2323) -> 1 | |
# TX: Data: 0x41 0x05 0x00 0x01 0x23 0x23 0x01 0x4d | |
# RX: Data: 0x41 0x06 0x01 0x01 0x23 0x23 0x01 0x01 0x50 | |
# set mode 2 | |
# TX: Data: 0x41 0x06 0x00 0x02 0x23 0x23 0x01 0x02 0x51 | |
# RX: Data: 0x41 0x05 0x01 0x02 0x23 0x23 0x01 0x4f | |
# set mode 1 | |
# TX: Data: 0x41 0x06 0x00 0x02 0x23 0x23 0x01 0x01 0x50 | |
# RX: Data: 0x41 0x05 0x01 0x02 0x23 0x23 0x01 0x4f | |
# get reduce_room_temp (0x2307) -> 0x0F -> 15 | |
# TX: Data: 0x41 0x05 0x00 0x01 0x23 0x07 0x01 0x31 | |
# RX: Data: 0x41 0x06 0x01 0x01 0x23 0x07 0x01 0x0f 0x42 | |
# set reduce_room_temp (0x2307) -> 16 (0x10) | |
# TX: Data: 0x41 0x06 0x00 0x02 0x23 0x07 0x01 0x10 0x43 | |
# RX: Data: 0x41 0x05 0x01 0x02 0x23 0x07 0x01 0x33 | |
# [ 0x41, 0x06, 0x00, 0x02, 0x23, 0x02, 0x01, 0x01, 0x2f ] | |
class WriteRequestPacket < Packet | |
# all setting are with a packet size of 1 bytes (may be changed if needed). | |
def initialize addr, value, len=1 | |
super addr, len | |
@plen=6 # len of packet (different pour request and responses) | |
@cmd1=P300_REQUEST # 00=request (host -> Vitoden), 01=response, 03=Error | |
@cmd2=P300_WRITE_DATA # 01=Read Data, 02=Write Data = 02, 07=Function Call | |
a=Addr::split addr | |
@data=[a[0], a[1], len, value] | |
@crc=self.crc | |
end | |
# return size of write request packet | |
def size | |
return 6 | |
end | |
# return an array of byte ; will be sent to TTy device. | |
def to_bytes | |
return [@start, @plen, @cmd1, @cmd2, @data, @crc].flatten | |
end | |
# build packet for reading and return it a array of bytes | |
def pack | |
return self.to_bytes | |
end | |
end | |
# this class will help you decode a response packet from serial. | |
class ResponsePacket < Packet | |
def initialize addr, len | |
super addr, len | |
@plen=self.size # len of packet between 0x41 (P300_LEADIN) and CRC | |
@cmd1=1 # 00=request, 01=response, 03=Error | |
@cmd2=1 # 01=Read Data, 02=Write Data = 02, 07=Function Call | |
@received=nil # received packet | |
end | |
# given data (in bytes) decode response packet | |
def unpack data | |
# firt byte is 0x41 (P300_LEADIN) - packet start. | |
raise ProtocolError, "read error (start byte)" unless P300_LEADIN == data[0].ord | |
raise ProtocolError, "read error (packet len)" unless @plen == data[1].ord | |
raise ProtocolError, "read error (received addr)" unless @addr == Addr::unsplit(data[4].ord, data[5].ord) | |
@received=data | |
@plen=data[1] | |
raise CRCError, "## CRC error" unless check_crc | |
# store collected data | |
# data[6]=number of bytes | |
# data[7..] = read value | |
@data=data[7,@len] | |
case @len | |
when 1 | |
return @data[0].ord | |
when 2 | |
return @data.reverse.join.unpack('S>')[0] | |
when 4 | |
return @data.reverse.join.unpack('l>')[0] | |
end | |
end | |
# CRC is made with the sum of packet len till data buffer except first byte. | |
def crc | |
sum=0 | |
@received[1, @received.size-2].each {|e| sum=sum+e.ord } | |
return sum%256 | |
end | |
# return true is CRC is OK | |
def check_crc | |
self.crc == @received[-1].ord | |
end | |
def unpack_addr | |
d=self.data | |
return Addr::unsplit d[0].ord, d[1].ord | |
end | |
# return result as raw array | |
def raw | |
@received | |
end | |
# data packet | |
def data | |
@received[7,@len] | |
end | |
# return size of a request packet | |
def size | |
return @len+5 | |
end | |
end | |
class WriteResponsePacket < ResponsePacket | |
def initialize addr, len | |
super | |
@cmd2=2 # 01=Read Data, 02=Write Data = 02, 07=Function Call | |
end | |
def size | |
return 5 | |
end | |
# CRC is made with the sum of packet len till data buffer except first byte. | |
def crc | |
sum=0 | |
@received[1, @received.size-2].each {|e| sum=sum+e.ord } | |
return sum%256 | |
end | |
# given data (in bytes) decode response packet | |
def unpack data | |
# firt byte is 0x41 (P300_LEADIN) - packet start. | |
raise ProtocolError, "read error (start byte)" unless P300_LEADIN == data[0].ord | |
raise ProtocolError, "read error (packet len)" unless @plen == data[1].ord | |
raise ProtocolError, "read error (received addr)" unless @addr == Addr::unsplit(data[4].ord, data[5].ord) | |
@received=data | |
raise CRCError, "## CRC error" unless check_crc | |
return true | |
end | |
end | |
# want to send some "command" to Optolink. | |
# - a command store differents parameters (name, addr, size, factor, unit, description) | |
# - with command, you will talk to Optolink via TTy serial class | |
# - you will send and receive packets. | |
# - actualy only P300 protocol is supported. | |
# | |
class Command | |
def initialize addr, size, factor, unit, mode, type, name, descr, enum=nil | |
@addr=addr # address to read in Vito controler | |
@size=size # size in bytes | |
@factor=factor # temperature are read at 1/10 precision : need to divide by this factor to get real value. | |
@unit=unit # unit of data : °C, %, kwh, ... | |
@mode=mode # mode for commande :ro, :rw -> we can write values only for :rw commands. | |
@type=type # data type : :int4, :byte, :short, :addr, :bool, :enum | |
@name=name # name of ressource : ex power, inside_temp, ... | |
@descr=descr # long textual description of ressource. | |
@enum=enum # literal values for mode, and special values | |
end | |
# read some value à specified address (@addr) | |
# - specify size of read, factor for unit. | |
# returns an array of bytes. | |
def read tty | |
req=ReadRequestPacket.new @addr, @size | |
resp=ResponsePacket.new @addr, @size | |
# build packet and send it to tty line. | |
tty.write(req.pack,true) | |
# first byte should be 0x06 (ack) | |
c=tty.read | |
raise ProtocolError, "error : should received 0x06 (ack), but received " . c.ord unless c.ord == 6 | |
# read bytes from TTYusb and collect them in data array | |
data=[] | |
num=resp.size + 3 | |
num.times { data << tty.read } | |
# decode buffer and return as an array of bytes. | |
val = resp.unpack data | |
case @factor | |
when nil | |
# nope | |
else | |
val=val/(1.0*@factor) | |
end | |
# decode won't work for address values | |
case @type | |
when :addr | |
val=resp.unpack_addr | |
when :systime | |
data=resp.data | |
# sample packets : | |
# [ 0x20, 0x15, 0x04, 0x11, 0x06, 0x19, 0x21, 0x26 ] | |
# 0 1 2 3 4 5 6 7 | |
# 0,1 = 20.15 -> 2015 | |
# 2: month = 4=april | |
# 3: day=11 | |
# 4=week day : 0=sunday, 1:monday, ... | |
# 5=hour : 0x19 -> 19h | |
# 6:min : 0x21 -> 21mn | |
# 7:sec : 0x26 -> 26 seconds. | |
# see : https://github.com/taupinfada/vcontrold/blob/master/unit.c#L137 | |
val=sprintf("%02X/%02X/%02X%02X %02X:%02X:%02X ", data[3].ord, data[2].ord, data[0].ord, data[1].ord, data[5].ord, data[6].ord, data[7].ord) | |
when :float | |
val=sprintf("%0.2f", val) | |
when :error | |
# pp resp.data.to_s | |
# sample packet : [ 0xbc, 0x20, 0x15, 0x01, 0x05, 0x01, 0x09, 0x46, 0x27 ] | |
val=resp.data.to_s | |
when :enum | |
if @enum != nil | |
literal=@enum[val] | |
val=sprintf("%d;(%s)", val, literal) | |
end | |
end | |
return { | |
:addr => @addr, | |
:size => @size, | |
:factor=> @factor, | |
:type => @type, | |
:unit => @unit, | |
:name => @name, | |
:descr => @descr, | |
:raw => resp.raw, | |
:data => resp.data, | |
:value => val, | |
:literal => literal | |
} | |
end | |
# this method will allow to write a value at spécified address. | |
# example set mode to off | |
# | |
def write value, tty | |
req=WriteRequestPacket.new @addr, value, @size | |
resp=WriteResponsePacket.new @addr, @size | |
# build packet and send it to tty line. | |
tty.write(req.pack) | |
# first byte should be 0x06 (ack) | |
c=tty.read | |
raise ProtocolError, "Command::write() : error : should received 0x06 (ack), but received " . c.ord unless c.ord == 6 | |
# read bytes from TTYusb and collect them in data array | |
data=[] | |
num=resp.size + 3 | |
num.times { data << tty.read } | |
# decode buffer and return as an array of bytes. | |
return resp.unpack data | |
end | |
end | |
class Viessmann | |
def initialize | |
@tty=nil # tty class for RW (sub-class of SerialPort) | |
@port=nil # serial port (/dev/ttyUSB*) | |
# literals for enums values. | |
@enums = { | |
# may be : 0=only Water Heating; 1=Continuous reduced; 2=constant normal; 3=heat+WH; 4=heat + WH ; 5=off | |
# :mode => ['Standby mode', 'DHW only', 'Heating and hot water'] | |
:mode => ['water heating only', 'continuous reduced', 'constant normal', 'heating + hot water', 'heating + hot water', 'Off'], | |
:switching_valve => ['undefined', 'heating', 'middle position', 'hot water'], | |
} | |
# all supported commands for my device : Viessmann Vitodens 222-W, controler : vitotrol 200A, controler : vitotronic 200H01B (id 0x20CB) | |
# may be command be described in a separate file (vitotronic-20CB.rb) and include it. | |
# you can find an english version (xml) at this address for 20C8 device (share multiple addr with 20CB) : | |
# https://github.com/smbunn/Viessmann-Control/blob/master/vito_20c8_EN.xml | |
@commands = { | |
:deviceid => Command.new(0x00F8, 2, nil, '', :ro, :addr, 'deviceid', 'Device ID'), | |
:indoor_temp => Command.new(0x0896, 2, 10, '°C', :ro, :short, 'indoor_temp', 'Indoor temperature'), | |
:outdoor_temp => Command.new(0x0800, 2, 10, '°C', :ro, :short, 'outdoor_temp', 'Outdoor temperature'), | |
:outdoor_temp_lp => Command.new(0x5525, 2, 10, '°C', :ro, :short, 'outdoor_temp_lp', 'Outdoor temperature low-pass'), | |
:outdoor_temp_smooth => Command.new(0x5527, 2, 10, '°C', :ro, :short, 'outdoor_temp_smooth', 'Outdoor temperature smooth (attenuated)'), | |
:norm_room_temp => Command.new(0x2306, 1, nil, '°C', :rw, :byte, 'norm_room_temp', 'Normal room temperature'), | |
:reduce_room_temp => Command.new(0x2307, 1, nil, '°C', :rw, :byte, 'reduce_room_temp', 'Reduce room temperature'), | |
:boiler_temp => Command.new(0x0802, 2, 10, '°C', :ro, :short, 'boiler_temp', 'Boiler temperature'), | |
:boiler_temp_lp => Command.new(0x0810, 2, 10, '°C', :ro, :short, 'boiler_temp_lp', 'Boiler temperature low-pass'), | |
:boiler_temp_set => Command.new(0x555a, 2, 10, '°C', :ro, :short, 'boiler_temp_set', 'Boiler temperature setpoint'), | |
:hot_water_temp => Command.new(0x0804, 2, 10, '°C', :ro, :short, 'hot_water_temp', 'Hot water temperature'), | |
:hot_water_temp_lp => Command.new(0x0812, 2, 10, '°C', :ro, :short, 'hot_water_temp_lp', 'Hot water temperature low-pass'), | |
:hot_water_temp_set => Command.new(0x2544, 2, 10, '°C', :rw, :short, 'hot_water_temp_set', 'Hot water temperature target'), | |
:flow_temp => Command.new(0x080C, 2, 10, '°C', :ro, :short, 'flow_temp', 'Flow temperature'), | |
:return_temp => Command.new(0x080A, 2, 10, '°C', :ro, :short, 'return_temp', 'Return temperature'), | |
# circuit | |
:circuit_flow_temp => Command.new(0x2544, 2, 10, '°C', :ro, :short, 'circuit_flow_temp', 'Circuit flow temperature'), | |
:curve_level => Command.new(0x27d4, 1, nil, 'K', :ro, :byte, 'curve_level', 'heating curve level'), | |
:curve_slope => Command.new(0x27d3, 1, 10, '', :ro, :byte, 'curve_slope', 'heating curve slope'), | |
# :storage_charge_pump => Command.new(0x0845, 1, nil, '', :ro, :byte, 'storage_charge_pump', 'storage charge pump'), | |
# :circulation_pump => Command.new(0x0846, 1, nil, '', :ro, :byte, 'circulation_pump', 'circulation pump'), | |
# :mixer_position => Command.new(0x254C, 1, 2, '%', :ro, :byte, 'mixer_position', 'mixer position'), | |
:mode => Command.new(0x2301, 1, nil, '', :rw, :enum, 'mode', 'Operating mode', @enums[:mode]), | |
:eco_mode => Command.new(0x2331, 1, nil, '', :rw, :bool, 'eco_mode', 'Eco mode (bool)'), | |
:party_mode => Command.new(0x2330, 1, nil, '', :rw, :bool, 'party_mode', 'Party mode (bool)'), | |
:switching_valve => Command.new(0x0a10, 1, nil, '', :ro, :enum, 'switching_valve','switching valve', @enums[:switching_valve]), | |
:starts => Command.new(0x088a, 4, nil, '', :ro, :int4, 'starts', 'burner starts number'), | |
:runtime => Command.new(0x08A7, 4, nil, 's', :ro, :int4, 'runtime', 'burner runtime (s)'), | |
:runtime_h => Command.new(0x08A7, 4, 3600, 'h', :ro, :float, 'runtime_h', 'burner runtime (h)'), | |
:power_pump => Command.new(0x0a3c, 1, 1, '%', :ro, :byte, 'power_pump', 'power pump in %'), | |
:power => Command.new(0xa38f, 1, 2, '%', :ro, :byte, 'power', 'burner power in %'), | |
:flow => Command.new(0x0c24, 2, 1, 'l/h',:ro, :byte, 'flow', 'flow in l/h'), | |
:exhaust_gaz_temp => Command.new(0x0808, 2, 10, '°C', :ro, :short,'exhaust_gaz_temp', 'exhauts gaz temp in °C'), | |
:boiler_output => Command.new(0xa305, 1, 2, '%', :ro, :byte, 'boiler_output', 'boiler output in %'), # not working ... should see hot water flow ? | |
:frost_danger => Command.new(0x2510, 1, nil, '', :ro, :bool, 'frost_danger', 'frost danger'), | |
:system_time => Command.new(0x088E, 8, nil, '', :ro, :systime, 'system_time', 'System Time'), | |
# :error0 => Command.new(0x7507, 9, nil, '', :ro, :error, 'error0', 'error 0'), # errors : 0:, 1:7510, 2:7519, 3:7522, 4:752B, 5:7534, 6:753D, 7:7546, 8:754F, 9:7558 | |
# :error1 => Command.new(0x7510, 9, nil, '', :ro, :error, 'error1', 'error 1'), # errors : 0:, 1:7510, 2:7519, 3:7522, 4:752B, 5:7534, 6:753D, 7:7546, 8:754F, 9:7558 | |
# :conso => Command.new(0x7574, 4, nil, '', :ro, :long, 'conso', 'consomption'), | |
} | |
# from https://github.com/mqu/vitalk/blob/master/vito_parameter.c | |
# { 0x7507, 1, 1, "errors", "Error History (numerisch)", "", P_ERRORS, &read_errors, NULL }, | |
# { 0x7507, 1, 1, "errors_text", "Error History (text)", "", P_ERRORS, &read_errors_text, NULL }, | |
# { 0x6760, 1, 1, "ww_offset", "target Boiler Offset /WW", "K", P_HOT_WATER, &read_ww_offset, NULL }, // Offset Kessel/WW Soll | |
# | |
# { 0x27e6, 1, 1, "power_pump_max", "power pump Maximal", "%", P_CIRCUIT, &read_pp_max, &write_pp_max }, // Pumpenleistung Maximal | |
# { 0x27e7, 1, 1, "power_pump_min", "power pumpMinimal", "%", P_CIRCUIT, &read_pp_min, &write_pp_min }, // Pumpenleistung Minimal | |
# { 0x0a3c, 1, 1, "power_pump", "power pump", "%", P_HYDRAULIC, &read_pump_power, NULL }, // Pumpenleistung | |
end | |
# open serial device for IO. | |
def open port | |
@port=port | |
# close TTy if it was opened. | |
@tty.close unless @tty==nil | |
@tty = TTy.new port | |
@tty.sync=false | |
end | |
# return tu default protocol (KW) at exit | |
# call at exit, but also with interrupt (CTRL-C) | |
def shutdown reason=:exit | |
return if @tty==nil | |
return if reason==:int # from interrupt ; shutdown will be call at exit | |
# return to KW protocol | |
self.proto_toggle(:kw) | |
@tty.flush | |
@tty=nil | |
end | |
# for debug | |
def raw_read | |
@tty.read | |
end | |
# initialising mode and debug | |
# write data to serial ; you can pass [array], "string" or Fixnum as bytes (0x12) | |
def raw_write data | |
# Debug::printf(5, "# raw_write : %s\n", data) | |
@tty.write data | |
end | |
# toggle between 300 (P300) et KW protocol - see http://openv.wikispaces.com/Protokolle | |
# parameter : :p300 or :kw | |
def proto_toggle p | |
case p | |
when :p300 | |
Debug::printf(2, "# toggling to p300 protocol\n") | |
self.raw_write(P300_ENABLE) | |
when :kw | |
Debug::printf(2, "# toggling to kw protocol\n") | |
self.raw_write([P300_RESET]) | |
else | |
raise "proto_toggle() unsupported protocol" | |
end | |
end | |
# toggle to P300 protocol, and try to control responses. | |
# return true on success. | |
# else trigger exception : ProtocolError | |
def toggle p=:p300, _retry=10, timeout=15 | |
count=0 | |
while true | |
self.proto_toggle(:kw) | |
c=self.raw_read() # may be 0=error, 5:ok (ack) | |
break if c.ord==5 | |
printf("# received 0x%02x (waiting for 0x5)\n", c.ord) | |
sleep 2 | |
count+=1 | |
raise ProtocolError, "##!protocol connexion error toggling to KW" unless count < _retry | |
end | |
count=0 | |
self.proto_toggle(:p300) | |
while true | |
c=self.raw_read() | |
break if c.ord == 6 | |
printf("# received 0x%02x (waiting for 0x06)\n", c.ord) | |
sleep 0.1 unless c.ord==0x05 | |
self.proto_toggle(:p300) | |
count+=1 | |
raise ProtocolError, "##! protocol connexion error toggling to P300" unless count < _retry | |
end | |
Debug::printf(2, "# toggled to p300 protocol ...\n") | |
return true | |
end | |
# TTY device for IO | |
def tty | |
return @tty | |
end | |
# return a hash of available commands | |
def commands | |
return @commands | |
end | |
# send a read request to Optolink | |
# returns a hash with values | |
# may throw an Timeout::Error exception on timout. | |
def command_read cmd, timeout=(6.0/10) | |
# printf("\n\ncommand_read %s\n", cmd.to_s) | |
raise "hash key error" unless @commands.key? cmd | |
# setup a timout for reading serial. | |
# if a timeout occures, an exception will be thrown. | |
# time limit to read is about =~ 8/100 ; take biger value for safety | |
status = Timeout::timeout(timeout){ | |
return @commands[cmd].read @tty | |
} | |
end | |
# send a write request to Optolink | |
# may throw an Timeout::Error exception on timout. | |
def command_write cmd, value, timeout=(6.0/10) | |
raise UnknownCommand, "unknow command" unless @commands.key? cmd | |
# setup a timout for reading serial. | |
# if a timeout occures, an exception will be thrown. | |
# time limit to read is about =~ 8/100 ; take biger value for safety | |
status = Timeout::timeout(timeout){ | |
return @commands[cmd].write value, @tty | |
} | |
end | |
end | |
class Main | |
def initialize | |
# viessmann object | |
@v=Viessmann.new | |
end | |
def tty_open expr="/dev/ttyUSB*" | |
# serial port should be connected to /dev/ttyUSB* | |
@expr=expr # unless expr==nil | |
ports=Dir.glob(expr) | |
if ports.size == 0 | |
printf("##! did not found right %s serial\n", expr) | |
raise ApplicationError, "serial port not found" | |
end | |
@v.open(ports[0]) | |
return true | |
end | |
def lock | |
end | |
def unlock | |
end | |
def shutdown reason=:exit | |
Debug::printf(2, "## shutdown reason=%s\n", reason.to_s); | |
@v.shutdown | |
self.unlock | |
end | |
def init_shutdown | |
at_exit { self.shutdown :exit } | |
trap("INT") { self.shutdown :int ; exit} | |
end | |
def toggle mode=:p300 | |
@v.toggle(mode) | |
end | |
def mainloop | |
while true | |
result = main | |
# pp result | |
puts "# sleeping ..." | |
STDOUT.flush | |
sleep 10 | |
end | |
end | |
def mainloop2 | |
begin | |
count=0 | |
while true | |
res = @v.command_read :power | |
printf("%s=%s%s\n", res[:name], res[:value].to_s, res[:unit]) | |
sleep 0.3 | |
count+=1 | |
exit if count > 100 | |
end | |
rescue IOError | |
# could try to recover. | |
printf("##! IO error (%s)\n", Time.now.strftime("%H:%M:%S - %d/%m/%Y")) | |
exit 2 | |
rescue TimeoutIOError | |
printf("##! timeout IO error (%s)\n", Time.now.strftime("%H:%M:%S - %d/%m/%Y")) | |
exit 3 | |
end | |
end | |
def write cmd, value | |
cmd=cmd.to_sym | |
printf("write : %s=%s\n", cmd.to_s, value.to_s) | |
raise UnknownCommand, "unknown command " + cmd.to_s unless @v.commands.key? cmd | |
res=@v.command_write cmd, value | |
end | |
def main | |
begin | |
# time for debug purpose. | |
t=Time.now | |
printf("time=%d/%s\n", t.to_i, t.strftime("%H:%M:%S - %d/%m/%Y")) | |
results={} | |
# run all available commands and display "name"="result" to stdout. | |
@v.commands.keys.each do |cmd| | |
begin | |
tries ||= 5 | |
res=nil | |
t0=Time.now | |
res = @v.command_read cmd, timeout=(4.0/10) | |
time=Time.now - t0 | |
case res[:type] | |
when :addr | |
value=sprintf('0x%02x', res[:value]) | |
else | |
value=res[:value].to_s | |
end | |
printf("%s=%s%s\n", res[:name], value, res[:unit]) | |
# collect each results and return to main. | |
results[cmd] = value | |
STDOUT.flush | |
rescue Timeout::Error => e | |
Debug::printf(1, "##! timeout error in running command : %s (%0.f ms) ; retring ... (%s)\n", cmd.to_s, time.to_f*1000.0, Time.now.strftime("%H:%M:%S - %d/%m/%Y")) | |
pp e.backtrace | |
sleep 15 | |
# try to re-open TTY device and reinitialise protocol | |
self.tty_open @expr | |
self.toggle :p300 | |
retry unless (tries -= 1).zero? | |
Debug::printf(1, "##! did not retried this time\n") | |
raise e | |
rescue ProtocolError => e | |
Debug::printf(1, "##! protocol error in running command : %s (%0.f ms) ; retring ... (%s)\n", cmd.to_s, time.to_f*1000.0, Time.now.strftime("%H:%M:%S - %d/%m/%Y")) | |
pp e | |
retry unless (tries -= 1).zero? | |
raise e | |
rescue StandardError => e | |
Debug::printf(1, "##! error in running command : %s (%0.f ms) ; retring ... (%s)\n", cmd.to_s, time.to_f*1000.0, Time.now.strftime("%H:%M:%S - %d/%m/%Y")) | |
pp e | |
retry unless (tries -= 1).zero? | |
raise e | |
end | |
end | |
return results | |
rescue IOError => e | |
printf("##! IO error\n") | |
raise e | |
rescue TimeoutIOError => e | |
printf("##! timeout IO error\n") | |
raise e | |
rescue CRCError => e | |
printf("##! CRC error in packet received (%s)\n", Time.now.strftime("%H:%M:%S - %d/%m/%Y")) | |
raise e | |
rescue ProtocolError => e | |
printf("##! protocol error (%s)\n", Time.now.strftime("%H:%M:%S - %d/%m/%Y")) | |
raise e | |
#rescue UnsupportedTypeError | |
# printf("##! unsupported type exception\n", cmd.to_s) | |
# rescue APIError | |
# printf("##! timeout read error for value : %s\n", cmd.to_s) | |
#else | |
# printf("##! unknown exception\n") | |
# exit 10 | |
ensure | |
# @v.shutdown | |
end | |
end | |
end | |
main=Main.new | |
# initialize shutdown on interrup or normal exit. | |
main.init_shutdown | |
t0=Time.now | |
begin | |
tries ||= 10 | |
# try to open first serial device based on expr /dev/ttyUSB* | |
throw "## can't open TTY device" unless main.tty_open "/dev/ttyUSB*" | |
# toggle into P300 protocol mode | |
main.toggle :p300 | |
# run mainloop or main function | |
# main.main | |
# main.write :mode, 2 # -> error | |
main.write :reduce_room_temp, 13 # OK. | |
# main.write :eco_mode, 1 | |
# main.write "eco_mode", 1 | |
main.write :party_mode, 1 # ??? | |
main.mainloop | |
rescue StandardError => e | |
printf("## catch an exception ; retrying ... (%s)\n", Time.now.strftime("%H:%M:%S - %d/%m/%Y")) | |
pp e unless e==nil | |
puts e.backtrace | |
if (Time.now - t0).to_f > 200 | |
t0=Time.now | |
tries=0 | |
end | |
sleep 30 | |
retry unless (tries -= 1).zero? | |
end | |
exit 0 |
I found one on eBay ;-)
hi, my little script is not working right on raspberryPI3 ; so, I am writing an other version based on vitalk + ruby script + mqtt gateway. I will post full source code soon on my github account.
for shematic, it's based on this : http://openv.wikispaces.com/Bauanleitung+USB (http://openv.wikispaces.com/file/view/USB_optolink_v2_All.pdf) ; I use "TTL" output directly connected to an USB adaptator.
you may have a look at this repository : https://github.com/mqu/viessmann-mqtt/ (0x20CB).
I found an error in party-mode et eco-mode for my device : mqu/vitalk#1 ; fixed.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How do I get the actual optical connection device?
Can I buy the device without a vitoconnect100 for instance?
Or do I build it myself? Schematics?