diff options
Diffstat (limited to 'munin/boiler')
-rwxr-xr-x | munin/boiler | 552 |
1 files changed, 552 insertions, 0 deletions
diff --git a/munin/boiler b/munin/boiler new file mode 100755 index 0000000..199893c --- /dev/null +++ b/munin/boiler @@ -0,0 +1,552 @@ +#!/usr/bin/env perl + +use Net::Telnet; +use Data::Dumper; + +#0 Status => 0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +#17 Relative modulation level => 0 +#18 CH water pressure => 1.14453125 +#25 Boiler water temperature => 53 +#26 DHW temperature => 49 +#28 Return water temperature => 48.375 +#57 Max CH water setpoint => 80 +#80 Supply inlet temperature => 21.25 +#256 Status => 0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +#257 Control setpoint => 7 +#270 Maximum relative modulation level => 100 +#272 Room setpoint => 18 +#280 Room temperature => 26.7265625 +#312 DHW setpoint => 60 + +my $ot_registers = { + 0 => { + format => ["flag16"], + name => "Status", + names => [ + '', '', + '', '', + '', '', + 'DHW enabled', 'CH enabled', + '', 'diagnostic indication', + '', '', + 'Flame', 'DHW active', + 'CH active', 'fault' + ], + warnings => [ + undef, undef, undef, undef, undef, undef, undef, undef, + undef, undef, undef, undef, undef, undef, undef, '1' + ] + }, + 1 => { format => ["f8.8"], name => "Control setpoint", units => 'C' }, + 2 => { format => [ "flag8", "u8" ], name => "Master configuration" }, + 3 => { format => [ "flag8", "u8" ], name => "Slave configuration" }, + 4 => { format => [ "u8", "u8" ], name => "Remote command" }, + 5 => { format => [ "flag8", "u8" ], name => "Application-specific flags" }, + 6 => { format => [ "flag8", "flag8" ], name => "Remote parameter flags" }, + 7 => { format => ["f8.8"], name => "Cooling control signal" }, + 8 => { format => ["f8.8"], name => "Control setpoint 2" }, + 9 => { format => ["f8.8"], name => "Remote override room setpoint" }, + 10 => { format => [ "u8", "nu" ], name => "Number of TSPs" }, + 11 => { format => [ "u8", "u8" ], name => "TSP setting" }, + 12 => { format => [ "u8", "nu" ], name => "Size of fault buffer" }, + 13 => { format => [ "u8", "u8" ], name => "Fault buffer entry" }, + 14 => { + format => ["f8.8"], + name => "Maximum relative modulation level", + units => '%' + }, + 15 => { + format => [ "u8", "u8" ], + name => "Boiler capacity and modulation limits" + }, + 16 => { format => ["f8.8"], name => "Room setpoint", units => 'C' }, + 17 => + { format => ["f8.8"], name => "Relative modulation level", units => '%' }, + 18 => { + format => ["f8.8"], + name => "CH water pressure", + units => 'bar', + warning => '0.9:2.6', + critical => '0.8:2.8' + }, + 19 => { format => ["f8.8"], name => "DHW flow rate" }, + 20 => { format => ["time"], name => "Day of week and time of day" }, + 21 => { format => ["date"], name => "Date" }, + 22 => { format => ["u16"], name => "Year" }, + 23 => { format => ["f8.8"], name => "Room Setpoint CH2" }, + 24 => { format => ["f8.8"], name => "Room temperature", units => 'C' }, + 25 => + { format => ["f8.8"], name => "Boiler water temperature", units => 'C' }, + 26 => { format => ["f8.8"], name => "DHW temperature", units => 'C' }, + 27 => { format => ["f8.8"], name => "Outside temperature" }, + 28 => + { format => ["f8.8"], name => "Return water temperature", units => 'C' }, + 29 => { format => ["f8.8"], name => "Solar storage temperature" }, + 30 => { format => ["f8.8"], name => "Solar collector temperature" }, + 31 => { format => ["f8.8"], name => "Flow temperature CH2" }, + 32 => { format => ["f8.8"], name => "DHW2 temperature" }, + 33 => { format => ["s16"], name => "Exhaust temperature" }, + 34 => { format => ["f8.8"], name => "Boiler heat exchanger temperature" }, + 35 => { format => [ "u8", "u8" ], name => "Boiler fan speed and setpoint" }, + 48 => { format => [ "s8", "s8" ], name => "DHW setpoint boundaries" }, + 49 => { format => [ "s8", "s8" ], name => "Max CH setpoint boundaries" }, + 50 => + { format => [ "s8", "s8" ], name => "OTC heat curve ratio boundaries" }, + 51 => { format => [ "s8", "s8" ], name => "Remote parameter 4 boundaries" }, + 52 => { format => [ "s8", "s8" ], name => "Remote parameter 5 boundaries" }, + 53 => { format => [ "s8", "s8" ], name => "Remote parameter 6 boundaries" }, + 54 => { format => [ "s8", "s8" ], name => "Remote parameter 7 boundaries" }, + 55 => { format => [ "s8", "s8" ], name => "Remote parameter 8 boundaries" }, + 56 => { format => ["f8.8"], name => "DHW setpoint", units => 'C' }, + 57 => { format => ["f8.8"], name => "Max CH water setpoint", units => 'C' }, + 58 => { format => ["f8.8"], name => "OTC heat curve ratio" }, + 59 => { format => ["f8.8"], name => "Remote parameter 4" }, + 60 => { format => ["f8.8"], name => "Remote parameter 5" }, + 61 => { format => ["f8.8"], name => "Remote parameter 6" }, + 62 => { format => ["f8.8"], name => "Remote parameter 7" }, + 63 => { format => ["f8.8"], name => "Remote parameter 8" }, + 70 => { format => [ "flag8", "flag8" ], name => "Status V/H" }, + 71 => { format => [ "nu", "u8" ], name => "Control setpoint V/H" }, + 72 => { format => [ "flag8", "u8" ], name => "Fault flags/code V/H" }, + 73 => { format => ["u16"], name => "OEM diagnostic code V/H" }, + 74 => { format => [ "flag8", "u8" ], name => "Configuration/memberid V/H" }, + 75 => { format => ["f8.8"], name => "OpenTherm version V/H" }, + 76 => { format => [ "u8", "u8" ], name => "Product version V/H" }, + 77 => { format => [ "nu", "u8" ], name => "Relative ventilation" }, + 78 => { format => [ "u8", "u8" ], name => "Relative humidity exhaust air" }, + 79 => { format => ["u16"], name => "CO2 level exhaust air" }, + 80 => + { format => ["f8.8"], name => "Supply inlet temperature", units => 'C' }, + 81 => { format => ["f8.8"], name => "Supply outlet temperature" }, + 82 => { format => ["f8.8"], name => "Exhaust inlet temperature" }, + 83 => { format => ["f8.8"], name => "Exhaust outlet temperature" }, + 84 => { format => ["u16"], name => "Exhaust fan speed" }, + 85 => { format => ["u16"], name => "Inlet fan speed" }, + 86 => { + format => [ "flag8", "flag8" ], + name => "Remote parameter settings V/H" + }, + 87 => { format => [ "u8", "nu" ], name => "Nominal ventilation value" }, + 88 => { format => [ "u8", "nu" ], name => "Number of TSPs V/H" }, + 89 => { format => [ "u8", "u8" ], name => "TSP setting V/H" }, + 90 => { format => [ "u8", "nu" ], name => "Size of fault buffer V/H" }, + 91 => { format => [ "u8", "u8" ], name => "Fault buffer entry V/H" }, + 100 => { format => [ "nu", "flag8" ], name => "Remote override function" }, + 101 => { + format => [ "flag8", "flag8" ], + name => "Solar storage mode and status" + }, + 102 => { format => [ "flag8", "u8" ], name => "Solar storage fault flags" }, + 103 => + { format => [ "flag8", "u8" ], name => "Solar storage config/memberid" }, + 104 => + { format => [ "u8", "u8" ], name => "Solar storage product version" }, + 105 => { format => [ "u8", "nu" ], name => "Number of TSPs solar storage" }, + 106 => { format => [ "u8", "u8" ], name => "TSP setting solar storage" }, + 107 => { + format => [ "u8", "u8" ], + name => "Size of fault buffer solar storage" + }, + 108 => + { format => [ "u8", "u8" ], name => "Fault buffer entry solar storage" }, + 113 => { format => ["u16"], name => "Unsuccessful burner starts" }, + 114 => { format => ["u16"], name => "Flame signal too low count" }, + 115 => { format => ["u16"], name => "OEM diagnostic code" }, + 116 => { format => ["u16"], name => "Burner starts" }, + 117 => { format => ["u16"], name => "CH pump starts" }, + 118 => { format => ["u16"], name => "DHW pump/valve starts" }, + 119 => { format => ["u16"], name => "DHW burner starts" }, + 120 => { format => ["u16"], name => "Burner operation hours" }, + 121 => { format => ["u16"], name => "CH pump operation hours" }, + 122 => { format => ["u16"], name => "DHW pump/valve operation hours" }, + 123 => { format => ["u16"], name => "DHW burner operation hours" }, + 124 => { format => ["f8.8"], name => "OpenTherm version Master" }, + 125 => { format => ["f8.8"], name => "OpenTherm version Slave" }, + 126 => { format => [ "u8", "u8" ], name => "Master product version" }, + 127 => { format => [ "u8", "u8" ], name => "Slave product version" }, +}; + +my $ot_types = { + 0 => "read data", + 1 => "write data", + 2 => "invalidate data", + 3 => "unknown 0x3", + 4 => "read ack", + 5 => "write ack", + 6 => "data invalid", + 7 => "unknown 0x7" +}; + +sub decode($$$) { + my ( $dataref, $string, $offset ) = @_; + my $b0 = hex( substr( $string, 0, 2 ) ); + + my $type = ( $b0 >> 4 ) & 7; + my $id = hex( substr( $string, 2, 2 ) ); + + my $data = pack( 'H4', substr( $string, 4, 4 ) ); + my $format = ["u16"]; + + my $ret = { + id => $id, + fake_id => $id + $offset, + type => $type, + type_str => $ot_types->{$type}, + }; + + my $bits = undef; + + if ( exists $ot_registers->{$id} ) { + $ret->{id_str} = $ot_registers->{$id}->{name}; + $ret->{units} = $ot_registers->{$id}->{units}; + $format = $ot_registers->{$id}->{format}; + $bits = $ot_registers->{$id}->{names}; + } + + $ret->{format} = join( ',', @$format ); + $ret->{raw_hex} = substr( $string, 4, 4 ); + $ret->{raw} = $data; + + $ret->{values} = []; + for my $f (@$format) { + + if ( $f eq "u16" ) { + push @{ $ret->{values} }, unpack( 'S>', $data ); + $data = substr( $data, 2 ); + } + elsif ( $f eq 's16' ) { + push @{ $ret->{values} }, unpack( 's>', $data ); + $data = substr( $data, 2 ); + } + elsif ( $f eq 'u8' ) { + push @{ $ret->{values} }, unpack( 'C', $data ); + $data = substr( $data, 1 ); + } + elsif ( $f eq 's8' ) { + push @{ $ret->{values} }, unpack( 'c', $data ); + $data = substr( $data, 1 ); + } + elsif ( $f eq 'nu' ) { + $data = substr( $data, 1 ); + } + elsif ( $f eq 'flag16' ) { + push @{ $ret->{values} }, + ( unpack( "(A)*", unpack( "B16", $data ) ) ); + $data = substr( $data, 2 ); + } + elsif ( $f eq 'flag8' ) { + push @{ $ret->{values} }, + ( unpack( "(A)*", unpack( "B8", $data ) ) ); + $data = substr( $data, 1 ); + } + elsif ( $f eq 'f8.8' ) { + push @{ $ret->{values} }, unpack( 's>', $data ) / 256.0; + } + + } + + $dataref->{ $id + $offset } = $ret; + + if ( defined $bits ) { + my @vs = ( @{ $ret->{values} } ); + my $bit = scalar( @{ $ret->{values} } ); + + for my $label (@$bits) { + $bit--; + my $v = shift(@vs); + next unless length($label) > 0; + my $key = sprintf( "%d.%d", $id + $offset, $bit ); + $ret = {}; + + $ret->{id} = sprintf( "%d.%d", $id, $bit ); + $ret->{fake_id} = $key; + $ret->{type} = $type; + $ret->{type_str} = $ot_types->{$type}; + $ret->{id_str} = $label; + $ret->{values} = [$v]; + + $dataref->{$key} = $ret; + } + } + + return $ret; +} + +sub thermostat($$) { + my ( $string, $data ) = @_; + + return decode( $data, $string, 0x100 ); + +} + +sub boiler($$) { + my ( $string, $data ) = @_; + + return decode( $data, $string, 0x0 ); + +} + +sub get_units($) { + my $id = shift; + + if ( $id =~ /^\d+$/ ) { + return '' unless exists $ot_registers->{$id}; + return '' unless exists $ot_registers->{$id}->{units}; + return $ot_registers->{$id}->{units}; + } + elsif ( $id =~ /^(\d+)\.(\d+)$/ ) { + my $base_id = $1; + my $bit = $2; + + return '' unless exists $ot_registers->{$base_id}; + return '' unless exists $ot_registers->{$base_id}->{unitss}; + + $bit = + ( scalar( @{ $ot_registers->{$base_id}->{unitss} } ) - 1 ) - $bit; + return $ot_registers->{$base_id}->{unitss}->[$bit]; + } + else { + return ''; + } +} + +sub get_name($) { + my $id = shift; + + if ( $id =~ /^\d+$/ ) { + return "Unknown $id" unless exists $ot_registers->{$id}; + return $ot_registers->{$id}->{name}; + } + elsif ( $id =~ /^(\d+)\.(\d+)$/ ) { + my $base_id = $1; + my $bit = $2; + + return "Unknown $id" unless exists $ot_registers->{$base_id}; + return "Unknown $id" unless exists $ot_registers->{$base_id}->{names}; + + $bit = ( scalar( @{ $ot_registers->{$base_id}->{names} } ) - 1 ) - $bit; + return $ot_registers->{$base_id}->{names}->[$bit]; + } + else { + return undef; + } +} + +sub get_warning($) { + my $id = shift; + + if ( $id =~ /^\d+$/ ) { + return "Unknown $id" unless exists $ot_registers->{$id}; + return $ot_registers->{$id}->{warning}; + } + elsif ( $id =~ /^(\d+)\.(\d+)$/ ) { + my $base_id = $1; + my $bit = $2; + + return "Unknown $id" unless exists $ot_registers->{$base_id}; + return "Unknown $id" unless exists $ot_registers->{$base_id}->{names}; + + $bit = + ( scalar( @{ $ot_registers->{$base_id}->{warnings} } ) - 1 ) - $bit; + return $ot_registers->{$base_id}->{warnings}->[$bit]; + } + else { + return undef; + } +} + +sub get_critical($) { + my $id = shift; + + if ( $id =~ /^\d+$/ ) { + return "Unknown $id" unless exists $ot_registers->{$id}; + return $ot_registers->{$id}->{crtitical}; + } + elsif ( $id =~ /^(\d+)\.(\d+)$/ ) { + my $base_id = $1; + my $bit = $2; + + return "Unknown $id" unless exists $ot_registers->{$base_id}; + return "Unknown $id" unless exists $ot_registers->{$base_id}->{names}; + + $bit = + ( scalar( @{ $ot_registers->{$base_id}->{crtiticals} } ) - 1 ) - $bit; + return $ot_registers->{$base_id}->{crtiticals}->[$bit]; + } + else { + return undef; + } +} + +sub maul($) { + my $n = shift; + + $n =~ s/\s/_/g; + return "boiler_" . lc($n); +} + +my $report = [ + '0.0', '0.1', '0.2', '0.3', '0.6', '0.8', '0.9', '17', '18', '25', + '26', '28', '57', '80', '257', '270', '272', '280', '312' +]; + +sub do_thing($) { + my $t = shift; + + my $name = get_name($t); + my $maul = maul($name); + my $warning = get_warning($t); + my $critical = get_critical($t); + + print "$maul.draw LINE2\n"; + print "$maul.type GAUGE\n"; + print "$maul.label $name\n"; + + print "$maul.warning ", $warning, "\n" if $warning; + print "$maul.critical ", $critical, "\n" if $critical; +} + +sub do_units($) { + my $want_unit = shift; + + for my $t (@$report) { + my $unit = get_units($t); + + next unless $want_unit eq $unit; + do_thing($t); + } +} + +sub do_values_units($$) { + my ( $data, $wanted_units ) = @_; + + for my $t (@$report) { + next unless exists $data->{$t}; + + my $units = get_units($t); + next unless $units eq $wanted_units; + + my $name = get_name($t); + my $maul = maul($name); + + print $maul, ".value ", $data->{$t}->{values}->[0], "\n"; + } +} + +my $wanted = { map { my $q = $_; $q =~ s/\..*$//; $q => 1 } @$report }; +my $n_wanted = scalar( keys %$wanted ); + +for my $t ( keys %$ot_registers ) { + $ot_registers->{ $t + 0x100 } = $ot_registers->{$t}; +} + +my @bits = split( /_+/, $0 ); + +exit 0 unless scalar(@bits) > 1; + +my $host = $bits[1]; + +$host =~ s/-//g; + +my $short_host = $host; + +$short_host =~ s/\..*$//; + +if ( $ARGV[0] eq 'config' ) { + + print "host_name ", $host, "\n"; + + print "multigraph temperatures_${short_host}\n"; + print "graph_title Temperatures\n"; + print "graph_category boiler\n"; + print "graph_vlabel centigrade\n"; + + do_units('C'); + + print "multigraph pressures_${short_host}\n"; + print "graph_title Pressures\n"; + print "graph_category boiler\n"; + print "graph_vlabel bar\n"; + + do_units('bar'); + + print "multigraph modulation_${short_host}\n"; + print "graph_title Modulation\n"; + print "graph_category boiler\n"; + print "graph_vlabel bar\n"; + + do_units('%'); + + for my $t (@$report) { + next if length( get_units($t) ) > 0; + + my $name = get_name($t); + my $maul = maul($name); + + print "multigraph ${maul}_${short_host}\n"; + print "graph_title $name\n"; + print "graph_category boiler\n"; + + do_thing($t); + + } + + exit(0); +} + +my $telnet = new Net::Telnet( + Timeout => 10, + Telnetmode => 0, + Binmode => 1, + Host => $bits[1], + Port => 2001 +); + +#$telnet->open($bits[1]); + +my $data = {}; +my $then = time; +my $timeout = 60; + +while ( ($n_wanted) and ( ( time - $then ) < $timeout ) ) { + my $line = $telnet->getline( Timeout => 1, Errmode => 'return' ); + my $id = -1; + + chomp $line; + chomp $line; + + if ( $line =~ /^T/ ) { + $id = thermostat( substr( $line, 1, 8 ), $data ); + } + elsif ( $line =~ /^B/ ) { + $id = boiler( substr( $line, 1, 8 ), $data ); + } + + if ( exists $wanted->{$id} ) { + $n_wanted--; + delete $wanted->{$id}; + } + +} + +print "multigraph temperatures_${short_host}\n"; +do_values_units( $data, 'C' ); +print "multigraph pressures_${short_host}\n"; +do_values_units( $data, 'bar' ); +print "multigraph modulation_${short_host}\n"; +do_values_units( $data, '%' ); + +for my $t (@$report) { + next unless exists $data->{$t}; + + my $units = get_units($t); + next unless $units eq ''; + + my $name = get_name($t); + my $maul = maul($name); + + print "multigraph ${maul}_${short_host}\n"; + print $maul, ".value ", $data->{$t}->{values}->[0], "\n"; +} + +exit(0); |