summaryrefslogtreecommitdiffstats
path: root/munin/boiler
diff options
context:
space:
mode:
Diffstat (limited to 'munin/boiler')
-rwxr-xr-xmunin/boiler552
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);