Skip to content

Instantly share code, notes, and snippets.

@mqu
Last active December 30, 2018 10:14
Show Gist options
  • Save mqu/9519e39ccc474f111ffb to your computer and use it in GitHub Desktop.
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/
#!/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
@leifnel
Copy link

leifnel commented Mar 18, 2017

I found one on eBay ;-)

@mqu
Copy link
Author

mqu commented Mar 31, 2017

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.

@mqu
Copy link
Author

mqu commented Mar 31, 2017

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.

@mqu
Copy link
Author

mqu commented Apr 1, 2017

you may have a look at this repository : https://github.com/mqu/viessmann-mqtt/ (0x20CB).

@mqu
Copy link
Author

mqu commented Apr 4, 2017

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