summaryrefslogtreecommitdiffstats
path: root/heating-cgi/heating.cgi
diff options
context:
space:
mode:
authorfishsoupisgood <github@madingley.org>2021-01-12 16:58:31 +0000
committerfishsoupisgood <github@madingley.org>2021-01-12 16:58:31 +0000
commit7c6887eaaf812b63bab6c5e134f80a2ef36aeb31 (patch)
tree4b4b0d371107ae1b8540ca1618cb9aa796b72616 /heating-cgi/heating.cgi
parentf4b573fe337a436d5e2b20be4be031d77376d609 (diff)
downloadheating-7c6887eaaf812b63bab6c5e134f80a2ef36aeb31.tar.gz
heating-7c6887eaaf812b63bab6c5e134f80a2ef36aeb31.tar.bz2
heating-7c6887eaaf812b63bab6c5e134f80a2ef36aeb31.zip
works
Diffstat (limited to 'heating-cgi/heating.cgi')
-rwxr-xr-xheating-cgi/heating.cgi707
1 files changed, 707 insertions, 0 deletions
diff --git a/heating-cgi/heating.cgi b/heating-cgi/heating.cgi
new file mode 100755
index 0000000..88474dd
--- /dev/null
+++ b/heating-cgi/heating.cgi
@@ -0,0 +1,707 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use Net::MQTT::Simple;
+use JSON::Parse;
+
+use Data::Dumper;
+
+use CGI qw/:standard :cgi-lib/;
+use CGI::Carp qw(fatalsToBrowser);
+
+package Boiler {
+
+ use IO::File;
+
+ #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',
+ graphs => { '1' => '', '3' => '' }
+ },
+ 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',
+ graphs => { '3' => '' }
+ },
+ 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',
+ graphs => { '3' => '' }
+ },
+ 25 => {
+ format => ["f8.8"],
+ name => "Boiler water temperature",
+ units => 'C',
+ graphs => { '1' => '', '2' => '' }
+ },
+ 26 => {
+ format => ["f8.8"],
+ name => "DHW temperature",
+ units => 'C',
+ graphs => { '2' => '' }
+ },
+ 27 => { format => ["f8.8"], name => "Outside temperature" },
+ 28 => {
+ format => ["f8.8"],
+ name => "Return water temperature",
+ units => 'C',
+ graphs => { '1' => '' }
+ },
+ 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',
+ graphs => { '2' => '' }
+ },
+ 57 => {
+ format => ["f8.8"],
+ name => "Max CH water setpoint",
+ units => 'C',
+ graphs => { '1' => '' }
+ },
+ 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',
+ graphs => { '2' => '' }
+ },
+ 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 fetch_file($) {
+ my ($name) = @_;
+
+ my $fh = new IO::File "<" . $name;
+ return undef unless defined $fh;
+ local $/;
+ my $guts = $fh->getline;
+ $fh->close;
+ undef $fh;
+
+ return $guts;
+ }
+
+ 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;
+ }
+ }
+
+ }
+
+ 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;
+ }
+ }
+
+ 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 load($) {
+ my $host = shift;
+ my $path = "/var/run/boiler_" . $host;
+
+ return undef unless -d $path;
+
+ my $wanted = { map { my $q = $_; $q =~ s/\..*$//; $q => 1 } @$report };
+
+ my $data = {};
+
+ for my $t ( keys %$wanted ) {
+ if ( $t < 0x100 ) {
+ my $fn = sprintf( "%s/B4x%02X", $path, $t );
+ my $guts = fetch_file($fn);
+ decode( $data, substr( $guts, 1 ), 0x0 ) if defined $guts;
+ }
+ else {
+ my $fn = sprintf( "%s/T9x%02X", $path, $t - 0x100 );
+ my $guts = fetch_file($fn);
+ decode( $data, substr( $guts, 1 ), 0x100 ) if defined $guts;
+ }
+ }
+
+ return $data;
+ }
+
+ 1;
+}
+
+my $mqtt_host = "10.32.139.1";
+my $mqtt_data = {};
+my $outside_temp = "";
+
+sub fmt($) {
+ my $v = shift;
+ return sprintf( "%.1f", $v );
+}
+
+sub fmt2($) {
+ my $v = shift;
+ return sprintf( "%.2f", $v );
+}
+
+sub mqtt_msg($$) {
+ my ( $topic, $message ) = @_;
+
+ $outside_temp = $message if $topic eq 'tele/weather/tempc';
+
+ return unless $topic =~ /^[^\/]+\/[^\/]+_radiator/;
+ return unless $topic =~ /^[^\/]+\/[^\/]+_radiator/;
+ return unless $topic =~ /^[^\/]+\/([^\/]+)\/(.+)$/;
+
+ $mqtt_data->{$1}->{$2} = $message;
+}
+
+sub do_radiators() {
+ my $mqtt = Net::MQTT::Simple->new($mqtt_host);
+
+ $mqtt->subscribe( 'stat/+/+', \&mqtt_msg );
+ $mqtt->subscribe( 'tele/+/SENSOR', \&mqtt_msg );
+ $mqtt->subscribe( 'tele/weather/tempc', \&mqtt_msg );
+
+ my $then = time;
+ $mqtt->tick(1) while ( time - $then ) < 3;
+
+ $mqtt->disconnect();
+
+ for my $r ( keys(%$mqtt_data) ) {
+ if ( exists $mqtt_data->{$r}->{SENSOR} ) {
+ my $pj = JSON::Parse::parse_json( $mqtt_data->{$r}->{SENSOR} );
+
+ if ( exists $pj->{'SI7021'} ) {
+ $mqtt_data->{$r}->{'TEMPERATURE'} =
+ $pj->{'SI7021'}->{'Temperature'}
+ if exists $pj->{'SI7021'}->{'Temperature'};
+ $mqtt_data->{$r}->{'HUMIDITY'} = $pj->{'SI7021'}->{'Humidity'}
+ if exists $pj->{'SI7021'}->{'Humidity'};
+ }
+ }
+
+ if ( exists $mqtt_data->{$r}->{POWER} ) {
+ if ( $mqtt_data->{$r}->{POWER} =~ /off/i ) {
+ $mqtt_data->{$r}->{POWER} = 0;
+ }
+ else {
+ $mqtt_data->{$r}->{POWER} = 1;
+ }
+ }
+
+ if ( ( $mqtt_data->{$r}->{POWER} == 0 )
+ && ( $mqtt_data->{$r}->{OPEN} == 0 ) )
+ {
+ $mqtt_data->{$r}->{state} = "closed";
+ $mqtt_data->{$r}->{state_colour} = "#c0c0ff";
+ }
+ elsif (( $mqtt_data->{$r}->{POWER} == 1 )
+ && ( $mqtt_data->{$r}->{OPEN} == 0 ) )
+ {
+ $mqtt_data->{$r}->{state} = "opening";
+ $mqtt_data->{$r}->{state_colour} = "#ffe0e0";
+ }
+ elsif (( $mqtt_data->{$r}->{POWER} == 1 )
+ && ( $mqtt_data->{$r}->{OPEN} == 1 ) )
+ {
+ $mqtt_data->{$r}->{state} = "open";
+ $mqtt_data->{$r}->{state_colour} = "#ffc0c0";
+ }
+ elsif (( $mqtt_data->{$r}->{POWER} == 0 )
+ && ( $mqtt_data->{$r}->{OPEN} == 1 ) )
+ {
+ $mqtt_data->{$r}->{state} = "closing";
+ $mqtt_data->{$r}->{state_colour} = "#e0e0ff";
+ }
+ else {
+ $mqtt_data->{$r}->{state} = "unknown";
+ $mqtt_data->{$r}->{state_colour} = "#ffffff";
+ }
+
+ if ( $mqtt_data->{$r}->{TEMPERATURE} < $mqtt_data->{$r}->{var1} ) {
+ $mqtt_data->{$r}->{temp_colour} = "#c0c0ff";
+ }
+ elsif ( $mqtt_data->{$r}->{TEMPERATURE} > $mqtt_data->{$r}->{var2} ) {
+ $mqtt_data->{$r}->{temp_colour} = "#ffc0c0";
+ }
+ else {
+ $mqtt_data->{$r}->{temp_colour} = "#ffffff";
+ }
+
+ for my $t (qw(var1 var2 TEMPERATURE HUMIDITY DELTA)) {
+ next unless exists $mqtt_data->{$r}->{$t};
+
+ $mqtt_data->{$r}->{$t} = fmt( $mqtt_data->{$r}->{$t} );
+ }
+
+ }
+
+ print "<table>\n";
+ print
+"<tr><th align=left><a href='http://munin.backdown.james.local/prometheus.james.local/prometheus.prometheus.james.local/index.html'>Radiators</a></th><th>Low</th><th>Temp</th><th>High</th><th>Humid</th><th>Delta</th><th>Valve</th><th>Sensor</th><th colspan=8 align=left>Set target</th></tr>\n";
+ for my $r ( sort( keys(%$mqtt_data) ) ) {
+ my $rd = $mqtt_data->{$r};
+
+ print "<tr>";
+ print
+"<td><a href='http://munin.backdown.james.local/prometheus.james.local/prometheus.prometheus.james.local/radiator_"
+ . $r
+ . "_temp.html'>", $r, "</a></td>";
+ print "<td>", $rd->{var1}, "</td>";
+ print "<td style='background-color: " . ${rd}->{temp_colour} . "' >",
+ $rd->{TEMPERATURE}, "</td>";
+ print "<td>", $rd->{var2}, "</td>";
+ print "<td>", $rd->{HUMIDITY}, "</td>";
+ print "<td>", $rd->{DELTA}, "</td>";
+ print "<td style='background-color: " . ${rd}->{state_colour} . "' >",
+ $rd->{state}, "</td>";
+
+ if ( exists $rd->{failed_reads} and ( $rd->{failed_reads} > 5 ) ) {
+ print "<td><font color='red'>Failed</font></td>";
+ }
+ else {
+ print "<td>Ok</td>";
+ }
+
+ for my $t (qw(10 15 18 19 20 21 22 23 24 25)) {
+ my $s = "";
+ $s = "color: red" if $t == $rd->{var1};
+ print "<td>";
+ print submit( -name => $r, -value => $t, -style => $s );
+ print "</td>";
+ }
+
+ print "</tr>\n";
+ }
+
+ print "</table>\n";
+}
+
+sub do_boiler($) {
+ my $outside_temp = shift;
+
+ my $boiler = Boiler::load("boiler-monster.prometheus.james.local");
+
+ return unless defined $boiler;
+
+ my $url =
+'http://munin.backdown.james.local/prometheus.james.local/boilermonster.prometheus.james.local/';
+ print "<table>\n";
+ print "<tr><th colspan=2 align=left><a href='" . $url
+ . "index.html'>Boiler</a></th></tr>\n";
+ my $pump = $boiler->{0.1}->{values}->[0];
+ my $pump_colour = '#c0c0ff';
+ $pump_colour = '#ffc0c0' if $pump == 1;
+ print "<tr><td><a href='"
+ . $url
+ . "boiler_ch_active_boilermonster.html'>CH Pump running:</a></td><td style='background-color: "
+ . $pump_colour
+ . "' >", $pump,
+ "</td></tr>\n";
+ print "<tr><td><a href='"
+ . $url
+ . "ch_temperatures_boilermonster.html'>Target water temp:</a></td><td>",
+ fmt( $boiler->{257}->{values}->[0] ),
+ "</td></tr>\n";
+ print "<tr><td><a href='"
+ . $url
+ . "ch_temperatures_boilermonster.html'>Current water temp:</a></td><td>",
+ fmt( $boiler->{25}->{values}->[0] ),
+ "</td></tr>\n";
+ print "<tr><td><a href='"
+ . $url
+ . "ch_temperatures_boilermonster.html'>Return water temp:</a></td><td>",
+ fmt( $boiler->{28}->{values}->[0] ),
+ "</td></tr>\n";
+ print "<tr><td><a href='"
+ . $url
+ . "modulation_boilermonster.html'>Modulation level:</a></td><td>",
+ fmt( $boiler->{17}->{values}->[0] ),
+ "</td></tr>\n";
+ print "<tr><td><a href='"
+ . $url
+ . "pressures_boilermonster.html'>CH Pressure:</a></td><td>",
+ fmt2( $boiler->{18}->{values}->[0] ),
+ "</td></tr>\n";
+ print "<tr><td><a href='"
+ . $url
+ . "boiler_fault_boilermonster.html'>Fault:</a></td><td>",
+ $boiler->{0.0}->{values}->[0],
+ "</td></tr>";
+ print
+"<tr><td><a href='http://weather.ourano.org/daytempdew.png'>Outside temp:</a></td><td>",
+ sprintf( "%.1f", $outside_temp ), "</td></tr>\n";
+ print "</table>\n";
+}
+
+my $css = <<'EOF';
+tr:nth-child(odd) td {
+ background-color: #ffffff
+}
+tr:nth-child(even) td {
+ background-color: #c0ffc0
+}
+EOF
+
+my $all_params = Vars();
+
+my $here = $ENV{SCRIPT_NAME};
+
+print header(
+ -type => 'text/html',
+ -charset => 'utf-8',
+ -refresh => '30; url=' . $here
+);
+
+print start_html(
+ -title => "heating",
+ -head => "<style>\n" . $css . "\n</style>"
+);
+
+print start_form();
+do_radiators();
+print end_form();
+
+print "<br/> <a href='" . $here . "'>Refresh</a></br>\n";
+
+if ( scalar( keys %$all_params ) > 0 ) {
+ print "<h2>Updates</h2>\n";
+ for my $param ( keys %$all_params ) {
+
+ my $mqtt = Net::MQTT::Simple->new($mqtt_host);
+ my $slop = 1;
+
+ $slop = 2 if $param =~ /bathroom/;
+
+ $mqtt->publish( "cmnd/" . $param . "/var1", $all_params->{$param} );
+ $mqtt->publish( "cmnd/" . $param . "/var2",
+ $all_params->{$param} + $slop );
+ $mqtt->tick(1);
+ $mqtt->disconnect();
+
+ print "Set $param to ", fmt( $all_params->{$param} ), "-",
+ fmt( $all_params->{$param} + $slop ), "<br/>";
+
+ }
+ print "<br\>";
+}
+
+print "<br\>";
+do_boiler($outside_temp);
+print end_html();