#!/usr/bin/perl

#
# L4000-daemon v2.0
#
# This program records the serial data transmitted by a Buderus Logamatic 4000 series heating controller
# for recording in files and via an online service.   It assumes a Raspberry Pi serial interface attached
# to the Logamatic's room sensor interface via a level shifter (10 KOhm, 12 V Z diode, transistor, pull-up).
#

#
# Copyright 2015-2022 Peter G. Holzleitner. All rights reserved.
# 
# Redistribution and use, with or without modification, are permitted provided that the following conditions are met:
#
# Redistributions must retain the above copyright notice, this list of conditions and the following disclaimer.
# 
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR 
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
# OF THE POSSIBILITY OF SUCH DAMAGE.
#

# changelog
# v1.0 initial release
# v1.1 add FM443 solar module support
# v1.5 add MQTT support
# v2.0 implement block checksum (thank you Andrey G. for reverse engineereing the algorithm!)

use strict;

use POSIX qw(strftime);
use Net::SMTP;
use Switch;             # Raspbian: apt-get install libswitch-perl
use Proc::Daemon;       # Raspbian: apt-get install libproc-daemon-perl
use Net::MQTT::Simple;  # for posting to local MQTT server without authentication


sub Decode;
sub DecodeZone;
sub DecodeWater;
sub DecodeBoiler;
sub DecodeConfig;
sub DecodeErrlog;
sub DecodeEnergy;
sub DecodeSolar;
sub ReporError;
sub SendMail;
sub RecLen;
sub CheckLogFile;
sub SendData;
sub checksum;


my $BUFSIZ  = 16;
my $PORT    = "/dev/ttyAMA0";
my $WGET    = "wget -O /dev/null --quiet";
my $URL     = "http://emoncms.org/input/post.json?apikey=YOURWRITEKEYHERE";  # JSON data will be appended in SendData
    # set $URL = "" to disable posting to EMONCMS server, or use your own server URL

my $MQTTHOST = "localhost";               # set $MQTTHOST = "" to disable posting data to MQTT server
my $MQTTROOT = "MYHOUSE/heating";

my $RUNFILE = "/run/heating.log";
my $LOGFILE = "/var/log/buderus/%Y%m%d-%H0000.log";
my $lastlog = "";

# +++ CONFIGURE THESE OPTIONS +++

my $SMTP_HOST = 'your.smtphost.com';
my $SMTP_PORT = 587;                       # port 587 for Submit, or use 25 for SMTP
my $SMTP_USER = 'SMTP-SENDING-USERNAME';   # if authentication is required
my $SMTP_PASS = 'SMTP-SENDING-PASSWORD';
my $SMTP_FROM = 'Heating Monitor <heating@yourdomain.com>';
my $SMTP_TO   = 'you@yourdomain.com';

my $MAIL_REPEAT = 3600 * 4;   # repeat alarm mails every 4 hours

# --- END CONFIGURATION SECTION +++

# debugging options

my $rawlog  = 0;    # raw serial data (only useful in special cases)
my $blklog  = 0;    # set to 1 to debug received data blocks 
my $reclog  = 0;    # records (1-n blocks)
my $errlog  = 0;    # error reporting
my $weblog  = 1;    # web requests
my $datalog = 1;    # data records
my $mailog  = 0;    # outgoing emails

my $daemon  = 1 ;   # if true, detach and write stdout to hourly log files (set to false for debugging)

my $mqtt;           # MQTT server connection object


my %RUN;
my %ERR;

# become a daemon
Proc::Daemon::Init if $daemon;

# trap sigterm for clean exit
my $continue = 1; $SIG{TERM} = sub { $continue = 0 };

# set up MQTT connection
$mqtt = Net::MQTT::Simple->new($MQTTHOST) if $MQTTHOST;

# set serial port to binary mode, 1200 bps, 8N1

system("stty -F $PORT 1200 cs8 -cstopb -parity raw pass8");

# open serial port

open(SERIAL,  "<" . $PORT) || die "$0: cannot open $PORT for reading: $!";
binmode(SERIAL)            || die "binmode failed";

CheckLogFile if $daemon;

my $lastrec = 0; 
my $recbuf = '';
my $line;

for(my $ofs = 0; $continue; )
  {
  $ofs += sysread SERIAL, $line, 132, $ofs;   # add next snippet to buffer

#  Sample data received on the bus:
#
#  81 00 04 02 28 28 2a 29 59 af 82
#  81 06 00 00 64 02 80 00 d0 af 82
#  81 0c 2c 38 43 00 00 00 a4 af 82
#  82 00 04 02 19 19 26 6e 9c af 82
#  82 06 00 00 64 00 80 00 93 af 82
#  82 0c 19 20 27 00 00 00 a5 af 82
# exception:
#  89 18 01 af 00 de 00 00 00 ed af 82


  # find block end marker 0xaf 0x82 or 0xaf 0x02

  my $be = index($line, "\xaf\x82");   # standard case - block ends in 0xaf 0x82
  my $be2 = index($line, "\xaf\x02");  # alternative   - block ends in 0xaf 0x02
  $be = $be2 if $be < 0 && $be2 >= 0;

  my $be3 = index($line, "\x89\x18");
  if(($be == 9 || $be == 10) && $be3 == 0) {
    # unexplained exception in protocol - documented length for block 0x89 is 24, here we get 7 bytes @ offset 24; ignore
    # print "... exception 89 18 ...";
    $be = -1;  $line = substr $line, $be3+2, 100;  # ignore the extra data
    }

  if($rawlog) { print "* index = $be, line=";  print unpack("H*", $line); print "\n"; }

  next if $be < 0;
  if($be >= 9) { 
    # ignore bytes before curent block
    my $subblock = substr $line, $be-9, 9; $line = substr $line, $be+1, 100;

    # unpack received string into array of byte values for easier processing
    my @bblock = unpack 'C*', $subblock;  my $payload = substr $subblock, 2, 6;   # blknum offset payload[6] checksum end-marker

    # verify block checksum over the first 8 bytes == ninth byte
    my $cs = checksum(@bblock);
    if($cs != @bblock[8]) {
      printf "  checksum error: %s rx=%02x calc=%02x\n", unpack("H*", $subblock), @bblock[8], $cs;
      next;
      }

    # is this a continuation of a previous block, or a new one?
    my $recnum = @bblock[0]; my $payofs = @bblock[1];
    printf "  block 0x%02x:%02d = %s\n", $recnum, $payofs, unpack("H*", $subblock) if $blklog;

    # payload offset 0 identifies a new block
    if( $payofs == 0 || $be2 >= 0 || ($recnum == 0x89 && $payofs == 0x18) ) {
      if($lastrec && length $recbuf) {

        CheckLogFile if $daemon;
        Decode($lastrec, $recbuf);
        }
      # start new
      $recbuf = $payload;

      } else {

      $recbuf .= $payload if length $recbuf;
      }
    $lastrec = $recnum;
    }

  $line = substr $line, $be+2, 200;  $ofs = length $line;
  }

close(SERIAL);


exit(0);



sub Decode {
  my $recnum = shift;
  my $record = shift;

  my $reclen = length($record);

  if($reclog) { printf ">> record %02x (%d) = ", $recnum, $reclen; print unpack("H*", $record); print "\n"; }

  switch($recnum) {
    case 0x80 { DecodeZone(1, $record); }
    case 0x81 { DecodeZone(2, $record); }
    case 0x82 { DecodeZone(3, $record); }
    case 0x83 { DecodeZone(4, $record); }
    case 0x8a { DecodeZone(5, $record); }
    case 0x8b { DecodeZone(6, $record); }
    case 0x8c { DecodeZone(7, $record); }
    case 0x8d { DecodeZone(8, $record); }
    case 0x8e { DecodeZone(9, $record); }

    case 0x84 { DecodeWater($record);   }

    case 0x87 { DecodeErrlog($record);  }
    case 0x88 { DecodeBoiler($record);  }
    case 0x89 { DecodeConfig($record);  }

    case 0x9B { DecodeEnergy($record);  }

    case 0x9E { DecodeSolar($record);   }  # FM443 module
    }

  }


sub DecodeZone {
  my $zone = shift;
  my $record = shift;

  my $run = $RUN{"zone$zone"}++;


  my @rbytes = unpack 'C*', $record;
  my $reclen = length $record;

#  printf ">> zone %d = ", $zone; print unpack("H*", $record); print "\n";

  return unless RecLen("zone $zone", 18, $reclen);

  my $vs = @rbytes[2]; my $vi = @rbytes[3]; 
  my $rs = @rbytes[4] / 2.0; my $ri = @rbytes[5] / 2.0; my $rss = sprintf "%.1f", $rs; my $ris = sprintf "%.1f", $rs; 
  my $ris = '----' if $ri == 55;
  my $o1 = @rbytes[6];  my $o0 = @rbytes[7];
  my $pu = @rbytes[8];  my $sg = @rbytes[9];  $sg = -(256-$sg) if $sg > 127;
  my $k1 = @rbytes[12]; my $k2 = @rbytes[13]; my $k3 = @rbytes[14];

  my $st = '';  my $err = '';

  my $ae = @rbytes[0] & 0x01;  $st .= 'Aus-Opt '     if $ae;
  my $ee = @rbytes[0] & 0x02;  $st .= 'Ein-Opt '     if $ee;
  my $au = @rbytes[0] & 0x04;  $st .= 'Auto '        if $au;
  my $wv = @rbytes[0] & 0x08;  $st .= 'WW-Vorrang '  if $wv;
  my $et = @rbytes[0] & 0x10;  $st .= 'Estrich-Tr '  if $et;
  my $fe = @rbytes[0] & 0x20;  $st .= 'Ferien '      if $fe;
  my $fr = @rbytes[0] & 0x40;  $st .= 'Frostschutz ' if $fr;
  my $ma = @rbytes[0] & 0x80;  $st .= 'Manuell '     if $ma;

  my $so = @rbytes[1] & 0x01;  $st .= 'Sommer '      if $so;
  my $ta = @rbytes[1] & 0x02;  $st .= 'Tag '         if $ta;
  my $fk = @rbytes[1] & 0x04;  $err.= 'Fernbedienungs-Kommunikation gestoert; '     if $fk;
  my $ff = @rbytes[1] & 0x08;  $err.= 'Fernbedienungs-Fehler; '   if $ff;
  my $vf = @rbytes[1] & 0x10;  $err.= 'Fehler Vorlauffuehler; ' if $vf;
  my $mv = @rbytes[1] & 0x20;  $err.= 'Maximale Vorlauftemperatur; ' if $mv;
  my $ex = @rbytes[1] & 0x40;  $err.= 'Externe Fehlermeldung; '  if $ex;

  my $w2 = @rbytes[10] & 0x01; $st .= 'WF2-EIN '     if $w2;
  my $w3 = @rbytes[10] & 0x02; $st .= 'WF3-EIN '     if $w3;
 
  my $s0 = @rbytes[10] & 0x20; $err.= 'Betriebsartschalter: AUS; '    if $s0;
  my $sh = @rbytes[10] & 0x40; $err.= 'Betriebsartschalter: MANUELL; ' if $sh;

  ReportError("Heizkreis $zone",  $err);
  
  printf "Heizkreis %d: Raum Soll/Ist = %s/%s C, Vorlauf Soll/Ist = %d/%d  %s  %s\n", $zone, $rss, $ris, $vs, $vi, $st, $err;
  printf "             Pumpe %d%%  Stellglied %d%%  Ein-Opt. %d min  Aus-Opt. %d min\n", $pu, $sg, $o1, $o0;
  printf "             Kennlinie: AT -10/0/+10 AT -> VL %d/%d/%d\n", $k3, $k2, $k1;

  if($run % 2 == ($zone % 2)) {
    if($ri == 55) {  # measured value invalid
      SendData( { "hk".$zone."_s" => $rs, "hk".$zone."_sg" => $sg, "hk".$zone."_v" => $vi, "hk".$zone."_vs" => $vs } );
      }
    else {
      SendData( { "hk".$zone => $ri, "hk".$zone."_s" => $rs, "hk".$zone."_sg" => $sg, "hk".$zone."_v" => $vi, "hk".$zone."_vs" => $vs , "hk".$zone."_pu" => $pu } );
      }
    }

  print "\n";
  }


sub DecodeWater {
  my $record = shift;

  my $run = $RUN{'water'}++;

  my @rbytes = unpack 'C*', $record;
  my $reclen = length $record;

#  printf ">> zone %d = ", $zone; print unpack("H*", $record); print "\n";

  return unless RecLen("water", 12, $reclen);

  my $ws = @rbytes[2]; my $wi = @rbytes[3]; 

  my $st = ''; my $err = '';

  my $au = @rbytes[0] & 0x01;  $st .= 'Auto '         if $au;
  my $di = @rbytes[0] & 0x02;  $st .= 'Desinfektion ' if $di;
  my $nl = @rbytes[0] & 0x04;  $st .= 'Nachladung '   if $nl;
  my $fe = @rbytes[0] & 0x08;  $st .= 'Ferien '       if $fe;
  my $fd = @rbytes[0] & 0x10;  $err.= 'Fehler bei Desinfektion; '  if $fd;
  my $ff = @rbytes[0] & 0x20;  $err.= 'Fehler in Temperatursensor; '     if $ff;
  my $fk = @rbytes[0] & 0x40;  $err.= 'Fehler - Warmwasser bleibt kalt; '       if $fk;
  my $fa = @rbytes[0] & 0x80;  $err.= 'Fehler in Inertanode; '      if $fa;

  my $la = @rbytes[1] & 0x01;  $st .= 'Laden '        if $la;
  my $ma = @rbytes[1] & 0x02;  $st .= 'Manuell '      if $ma;
  my $nn = @rbytes[1] & 0x04;  $st .= 'Nachladen '    if $nn;
  my $o0 = @rbytes[1] & 0x08;  $st .= 'A-Opt '        if $o0;
  my $o1 = @rbytes[1] & 0x10;  $st .= 'E-Opt '        if $o1;
  my $ta = @rbytes[1] & 0x20;  $st .= 'Tag '          if $ta;
  my $wa = @rbytes[1] & 0x40;  $st .= 'Warm '         if $wa;
  my $vo = @rbytes[1] & 0x80;  $st .= 'Vorrang '      if $vo;

  my $p0 = @rbytes[5] & 0x01;  $st .= 'Ladepumpe '    if $p0;
  my $p1 = @rbytes[5] & 0x02;  $st .= 'Zirk.-Pumpe '  if $p1;
  my $p2 = @rbytes[5] & 0x04;  $st .= 'Abs.-Solar '   if $p2;

  my $w2 = @rbytes[6] & 0x01;  $st .= 'WF2-EIN '      if $w2;
  my $w3 = @rbytes[6] & 0x02;  $st .= 'WF3-EIN '      if $w3;

  my $s0 = @rbytes[6] & 0x20;  $err.= 'Betriebsartschalter: AUS; '     if $s0;
  my $sh = @rbytes[6] & 0x40;  $err.= 'Betriebsartschalter: MANUELL; '  if $sh;
#  $sa = @rbytes[6] & 0x80;  $st .= 'SCHALT-AUT '   if $sa;

  my $ee = @rbytes[7] & 0x01;  $err.= 'Externe Fehlermeldung; '   if $w3;

  ReportError("Warmwasser",  $err);

  printf "Warmwasser:  Soll/Ist = %s/%s C  %s %s\n", $ws, $wi, $st, $err;

  if($run % 2 == 0) {
    SendData( { ww => $wi, ww_s => $ws, ww_l => $la, ww_laden => $p0, ww_zirk => $p1 });
    }

  print "\n";
  }


sub DecodeErrlog {
  my $record = shift;

  my $run = $RUN{'errlog'}++;

  my @rbytes = unpack 'C*', $record;
  my $reclen = length $record;

#  print ">> errlog = "; print unpack("H*", $record); print "\n";

  return unless RecLen("error log", 42, $reclen);

  # TBI
  }


sub DecodeBoiler {
  my $record = shift;

  my $run = $RUN{'boiler'}++;

  my @rbytes = unpack 'C*', $record;
  my $reclen = length $record;


#  print ">> boiler = "; print unpack("H*", $record); print "\n";

  return unless RecLen("boiler", 42, $reclen);

  my $ks = @rbytes[0]; my $ki = @rbytes[1]; my $k1 = @rbytes[2]; my $k0 = @rbytes[3]; my $br = @rbytes[8];

  my $st = ''; my $err = '';

  my $fb = @rbytes[6] & 0x01;  $err.= 'BRENNERSTOERUNG; ' if $fb;
  my $ff = @rbytes[6] & 0x02;  $err.= 'Fehler: KESSELFUEHLER; '   if $ff;
  my $fz = @rbytes[6] & 0x04;  $err.= 'Fehler: ZUS.-FUEHLER; '    if $fz;
  my $fk = @rbytes[6] & 0x08;  $err.= 'Fehler: KESSEL KALT; '     if $fk;
  my $fa = @rbytes[6] & 0x10;  $err.= 'Fehler: ABGAS-FUEHLER; '   if $fa;
  my $fg = @rbytes[6] & 0x20;  $err.= 'Fehler: ABGAS-GRENZTEMPERATUR; '   if $fg;
  my $fs = @rbytes[6] & 0x40;  $err.= 'Fehler: Sicherheitskette hat abgeschaltetp ' if $fs;
  my $fe = @rbytes[6] & 0x80;  $err.= 'Externe Fehlermeldung; '     if $fe;

  my $bst = @rbytes[7] & 0x01;  $st .= 'Abgastest '      if $bst;
  my $bs1 = @rbytes[7] & 0x02;  $st .= 'Stufe 1 '        if $bs1;
  my $bss = @rbytes[7] & 0x04;  $st .= 'Kesselschutz '   if $bss;
  my $bsb = @rbytes[7] & 0x08;  $st .= 'Betrieb '        if $bsb;
  my $bsl = @rbytes[7] & 0x10;  $st .= 'Leistung frei '  if $bsl;
  my $bsh = @rbytes[7] & 0x20;  $st .= 'Leistung hoch '  if $bsh;
  my $bs2 = @rbytes[7] & 0x40;  $st .= 'Stufe 2 '        if $bs2;

  my $bbt = @rbytes[34] & 0x01;  $st .= 'Abgastest '     if $bbt;
  my $bb0 = @rbytes[34] & 0x02;  $st .= 'Brenner=0 '     if $bb0;
  my $bba = @rbytes[34] & 0x04;  $st .= 'Brenner=Auto '  if $bba;
  my $bb1 = @rbytes[34] & 0x08;  $st .= 'Brenner=1 '     if $bb1;
  my $bb2 = @rbytes[34] & 0x10;  $st .= 'Brenner=2 '     if $bb2;

  my $s0 = @rbytes[34] & 0x20;  $err.= 'Betriebsartschalter: AUS; '       if $s0;
  my $sh = @rbytes[34] & 0x40;  $err.= 'Betriebsartschalter: MANUELL; '    if $sh;

  ReportError("Kessel",  $err);

  printf "Kessel:      Soll/Ist = %d/%d C  Ein/Aus = %d/%d C  Brenner = %d  %s %s\n", $ks, $ki, $k1, $k0, $br, $st, $err ? "FEHLER: " . $err : '';

  if($run % 2 == 0) {
    SendData( { kessel => $ki, kessel_s => $ks, brenner => $br, k_ein => $k1, k_aus => $k0 } );
    }

  print "\n";
  }


sub DecodeConfig {
  my $record = shift;

  my $run = $RUN{'config'}++;


  my @rbytes = unpack 'C*', $record;
  my $reclen = length $record;

#  print ">> run=$run config = "; print unpack("H*", $record); print "\n";

  return unless RecLen("config", -18, $reclen);  # really only interested in external temp

  my $at1 = @rbytes[0];  $at1 = -(256-$at1) if $at1 > 127;
  my $at2 = @rbytes[1];  $at2 = -(256-$at2) if $at2 > 127;

  my $st = ''; my $err = '';
  if($at1 == 110) {
    $at1 = ''; 
    $err = "Aussentemperatur-Sensor defekt";
    }

  ReportError("Aussentemperatur",  $err);

  return if $at1 < -40 || $at1 > 50 || $at2 < -40 || $at2 > 50;  # plausibility check

  printf "Aussen:      %d C (Gedaempft %d C)  %s %s\n", $at1, $at2, $st, $err;
  SendMail('Monitoring gestartet',  "Aussentemperatur $at1 C") if $run == 1;

  if($run % 5 == 0) {
    SendData( { aussen => $at1, aussen_d => $at2 } );
    }

  print "\n";
  }


sub DecodeEnergy {
  my $record = shift;

  my $run = $RUN{'energy'}++;

  my @rbytes = unpack 'C*', $record;
  my $reclen = length $record;


#  print ">> energy = "; print unpack("H*", $record); print "\n";

  return unless RecLen("energy", 36, $reclen);

  my $wm = ((@rbytes[30] * 256 + @rbytes[31]) * 256 + @rbytes[32]) * 256 + @rbytes[33];
  my $sy = @rbytes[2] + 1900; my $sm = @rbytes[1]; my $sd = @rbytes[0];

  printf "Energie:     %d Pulse seit %d-%02d-%02d\n", $wm, $sy, $sm, $sd;


  if($run % 13 == 0) {
    SendData( { energie => $wm } );
    }

  print "\n";

  }


sub DecodeSolar {     # NB: Proof of concept only, not tested as no hardware (FM443 module) available
  my $record = shift;

  my $run = $RUN{'solar'}++;

  my @rbytes = unpack 'C*', $record;
  my $reclen = length $record;


#  print ">> solar = "; print unpack("H*", $record); print "\n";
#  return unless RecLen("solar", 36, $reclen);
  my $ct  = (@rbytes[3] * 256 + @rbytes[4]) / 10.0;  # collector temp
  my $pu  = @rbytes[5];                              # pump status
  my $t1  = @rbytes[6];   my $t2    = @rbytes[8];    # Two tank temperatures

  my $st  = ''; my $err = '';
  my $e1  = @rbytes[0] & 0x01;  $err.= 'Hyst Error; '           if $e1;
  my $e2  = @rbytes[0] & 0x02;  $err.= 'Tank 2 Temp Limit; '    if $e2;
  my $e3  = @rbytes[0] & 0x04;  $err.= 'Tank 1 Temp Limit; '    if $e3;
  my $e4  = @rbytes[0] & 0x08;  $err.= 'Collector Temp Limit; ' if $e4;
  # etc. - please expand as necessary based on Buderus manual information

  my $s10 = @rbytes[7] & 0x01;  $st .= 'T1 Off '       if $s10;
  my $s11 = @rbytes[7] & 0x02;  $st .= 'T1 Low Solar ' if $s11;
  my $s12 = @rbytes[7] & 0x04;  $st .= 'T1 Low Flow '  if $s12;
  my $s13 = @rbytes[7] & 0x08;  $st .= 'T1 High Flow ' if $s13;
  my $s14 = @rbytes[7] & 0x10;  $st .= 'T1 Manual '    if $s14;
  my $s20 = @rbytes[9] & 0x01;  $st .= 'T2 Off '       if $s20;
  my $s21 = @rbytes[9] & 0x02;  $st .= 'T2 Low Solar ' if $s21;
  my $s22 = @rbytes[9] & 0x04;  $st .= 'T2 Low Flow '  if $s22;
  my $s23 = @rbytes[9] & 0x08;  $st .= 'T2 High Flow ' if $s23;
  my $s24 = @rbytes[9] & 0x10;  $st .= 'T2 Manual '    if $s24;
  # etc. - please expand as necessary based on Buderus manual information

  ReportError("Solar",  $err);

  printf "Solar: Collector=%.1f C  Pump=%d  Tank1=%d  Tank2=%d  %s %s\n", $ct, $pu, $t1, $t2, $st, $err ? "ERROR: " . $err : '';
  SendData( { sol_coll => $ct, sol_t1 => $t1, sol_t2 => $t2, sol_pump => $pu } );

  print "\n";
  }



sub ReportError {
  my $subject = shift;
  my $message = shift;


  my $lasterr  = $ERR{$subject}{'message'};
  my $firsttim = $ERR{$subject}{'time'};
  my $lasttim  = $ERR{$subject}{'lasttime'};

  print "  ReportError: subj = $subject, msg = $message, last = $lasterr, first $firsttim, last $lasttim\n" if $errlog;

  if(!$message) {

    if($lasterr) {
      SendMail('GUTMELDUNG: ' . $subject, "Der seit " . localtime($firsttim) . " anstehende Fehler wurde behoben!");
      print "  ReportError: END of error $subject, since " . localtime($firsttim) . "\n" if $errlog;
      }

    # clear error
    undef $ERR{$subject}{'message'};
    undef $ERR{$subject}{'time'};
    undef $ERR{$subject}{'lasttime'};
    return;
    }

  if($lasterr eq $message && (time() - $lasttim) < $MAIL_REPEAT) {
    print "  ReportError: same error as before, last reported " . localtime($lasttim) . "\n" if $errlog;
    return;
    }

  $ERR{$subject}{'message'}  = $message;
  $ERR{$subject}{'time'}     = time() unless $firsttim;  # first report time only, don't update
  $ERR{$subject}{'lasttime'} = time();

  if($firsttim) {
    SendMail("FEHLER: $subject (ERINNERUNG)", "$message\nFehler steht an seit " . localtime($firsttim) . "\n");
    }
  else {
    SendMail("FEHLER: $subject", "$message\n(Neuer Fehler)\n");
    }
  }




sub SendMail {
  my $subject = shift;
  my $message = shift;


  print "Sendmail: subj = $subject, msg = $message\n" if $mailog;

  my $smtp = Net::SMTP->new($SMTP_HOST, Port => $SMTP_PORT, Timeout => 10);
  $smtp->auth($SMTP_USER, $SMTP_PASS) or die $smtp->message();
  $smtp->mail($SMTP_FROM) or die $smtp->message();

  if ($smtp->to($SMTP_TO)) {
    $smtp->data();
    $smtp->datasend("From: $SMTP_FROM\n");
    $smtp->datasend("Subject: $subject\n");
    $smtp->datasend("X-Priority: 1 (Highest)\n");
    $smtp->datasend("X-MSMail-Priority: High\n");
    $smtp->datasend("\n");
    $smtp->datasend($message);
    $smtp->dataend();
    }
  else {
    print "Sendmail error: ", $smtp->message();
    }
  $smtp->quit;
  }


sub RecLen {
  my $r_type = shift;
  my $r_exp = shift;
  my $r_len = shift;

  if( $r_exp > 0 && ($r_len != $r_exp) ) {
    print "Invalid record length for $r_type - expected $r_exp, got $r_len\n";
    return 0;
    }
  $r_exp = -$r_exp;
  if($r_exp > 0 && ($r_len < $r_exp) ) {
    print "Invalid record length for $r_type - expected at least $r_exp, got $r_len\n";
    return 0;
    }
  return 1;
  }


sub CheckLogFile {

  my $newlog = strftime($LOGFILE, localtime);

  if($lastlog ne $newlog) {
    print "\n\nsaving $newlog\n";
    close LOG;

    system("gzip <$RUNFILE >$newlog.gz");
    $lastlog = $newlog;

    # send running log to file
    open LOG, ">$RUNFILE" or die "cannot write to $RUNFILE";
    select(LOG);

    print "$newlog saved\n\n";
    }

  my $timestamp = strftime("%Y-%m-%d %H:%M:%S ", localtime);
  print $timestamp;
  }


sub SendData {
  # send data to be logged - implementation for emoncms and MQTT

  my $params = shift;
  my $json = "";
  my $cmd;

  foreach my $param (keys %$params) {
    $json .= ',' if $json;
    $json .= $param . ':' .  $params->{$param};

    $mqtt->retain("$MQTTROOT/$param" => $params->{$param}) if $MQTTROOT && $MQTTHOST;
    }

  if($datalog) {
    my $dt = strftime("%Y-%m-%d %H:%M:%S", localtime);
    print "[DATA] $dt $json\n";
    }

  return unless $URL;

  $cmd = "$WGET '$URL&json={$json}'";
  print " calling $cmd\n" if $weblog;
  system("$cmd >/dev/null &");
  print "done\n" if $weblog;
  }


sub checksum {
  my @block = @_;
  my ($cs, $n, $i);

  for($cs = 0, $n = 0; $n < 8; $n++) {
    my $b = $block[$n];
    for($i = 0; $i < $n; $i++) {
      if($b < 0x80) { $b = $b << 1 } else { $b = $b & 0x7f; $b = $b << 1; $b = $b ^ 0x19; }
      }
    $cs = $cs ^ $b;
    }
  return $cs;
  }
