From 7c6887eaaf812b63bab6c5e134f80a2ef36aeb31 Mon Sep 17 00:00:00 2001 From: fishsoupisgood Date: Tue, 12 Jan 2021 16:58:31 +0000 Subject: works --- heating-cgi/heating.cgi | 707 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 707 insertions(+) create mode 100755 heating-cgi/heating.cgi (limited to 'heating-cgi/heating.cgi') 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 "\n"; + print +"\n"; + for my $r ( sort( keys(%$mqtt_data) ) ) { + my $rd = $mqtt_data->{$r}; + + print ""; + print +""; + print ""; + print ""; + print ""; + print ""; + print ""; + print ""; + + if ( exists $rd->{failed_reads} and ( $rd->{failed_reads} > 5 ) ) { + print ""; + } + else { + print ""; + } + + for my $t (qw(10 15 18 19 20 21 22 23 24 25)) { + my $s = ""; + $s = "color: red" if $t == $rd->{var1}; + print ""; + } + + print "\n"; + } + + print "
RadiatorsLowTempHighHumidDeltaValveSensorSet target
", $r, "", $rd->{var1}, "", + $rd->{TEMPERATURE}, "", $rd->{var2}, "", $rd->{HUMIDITY}, "", $rd->{DELTA}, "", + $rd->{state}, "FailedOk"; + print submit( -name => $r, -value => $t, -style => $s ); + print "
\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 "\n"; + print "\n"; + my $pump = $boiler->{0.1}->{values}->[0]; + my $pump_colour = '#c0c0ff'; + $pump_colour = '#ffc0c0' if $pump == 1; + print "\n"; + print "\n"; + print "\n"; + print "\n"; + print "\n"; + print "\n"; + print ""; + print +"\n"; + print "
Boiler
CH Pump running:", $pump, + "
Target water temp:", + fmt( $boiler->{257}->{values}->[0] ), + "
Current water temp:", + fmt( $boiler->{25}->{values}->[0] ), + "
Return water temp:", + fmt( $boiler->{28}->{values}->[0] ), + "
Modulation level:", + fmt( $boiler->{17}->{values}->[0] ), + "
CH Pressure:", + fmt2( $boiler->{18}->{values}->[0] ), + "
Fault:", + $boiler->{0.0}->{values}->[0], + "
Outside temp:", + sprintf( "%.1f", $outside_temp ), "
\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 => "" +); + +print start_form(); +do_radiators(); +print end_form(); + +print "
Refresh
\n"; + +if ( scalar( keys %$all_params ) > 0 ) { + print "

Updates

\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 ), "
"; + + } + print ""; +} + +print ""; +do_boiler($outside_temp); +print end_html(); -- cgit v1.2.3