# $Id: 79_BDKM.pm 12770 2016-12-14 08:39:57Z arnoaugustin $
##############################################################################
#
#     79_BDKM.pm
#
#     This file is part of FHEM.
#
#     Fhem is free software: you can redistribute it and/or modify
#     it under the terms of the GNU General Public License as published by
#     the Free Software Foundation, either version 2 of the License, or
#     (at your option) any later version.
#
#     BDKM is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU General Public License for more details.
#
#     You should have received a copy of the GNU General Public License
#     along with FHEM.  If not, see <http://www.gnu.org/licenses/>.
#
#     Written by Arno Augustin
##############################################################################

package main;

use strict;
use POSIX;
use warnings;
#use Blocking;
#use HttpUtils;
use Encode;
use JSON;
use Time::HiRes qw(gettimeofday);
use Digest::MD5 qw(md5 md5_hex md5_base64);
use base qw( Exporter );
use MIME::Base64;
use LWP::UserAgent;
use Crypt::Rijndael;


my @BaseDirs = qw(
    /
    /dhwCircuits
    /gateway
    /heatingCircuits
    /heatSources
    /notifications
    /recordings
    /solarCircuits
    /system
);
#@BaseDirs = qw(/system/sensors/temperatures /dhwCircuits);

my %WdToNum = qw(Mo 1 Tu 2 We 3 Th 4 Fr 5 Sa 6 Su 7);



my @RC300DEFAULTS = 

# ID:POLL EVERY x CYCLE:MINDELTA:READINGNAME
# all gateway IDs are polled (gathered) once on startup
#*:1:0:*   poll every cycle, difference 0 => update on difference 0 (allways)
#*:1::*    poll every cycle, no difference set => update on change only
#*:0::*    poll on startup only and update reading on change only
#*:1:0.5:* poll every cycle, difference set to 0.5 => update only if difference to last read is >= 0.5
#*:15::*   poll on startup and every 15th cylce, update reading if changed
#*:::*     update reading on (get/set) only if value changed
#*::0:*    update reading on (get/set) always
#*         ID only, no ":", poll every cycle, update reading allways (same as *:1:0:*)

qw(/dhwCircuits/dhw1/actualTemp:1:0.2:WaterTemp
   /dhwCircuits/dhw1/currentSetpoint:1::WaterDesiredTemp
   /dhwCircuits/dhw1/operationMode:1::WaterMode 
   /dhwCircuits/dhw1/status:0::WaterStatus
   /dhwCircuits/dhw1/switchPrograms/A/1-Mo:0:0:WaterProgram-1-Mo
   /dhwCircuits/dhw1/switchPrograms/A/2-Tu:0:0:WaterProgram-2-Tu
   /dhwCircuits/dhw1/switchPrograms/A/3-We:0:0:WaterProgram-3-We
   /dhwCircuits/dhw1/switchPrograms/A/4-Th:0:0:WaterProgram-4-Th
   /dhwCircuits/dhw1/switchPrograms/A/5-Fr:0:0:WaterProgram-5-Fr
   /dhwCircuits/dhw1/switchPrograms/A/6-Sa:0:0:WaterProgram-6-Sa
   /dhwCircuits/dhw1/switchPrograms/A/7-Su:0:0:WaterProgram-7-Su
   /dhwCircuits/dhw1/temperatureLevels/high:1::WaterDayTemp
   /dhwCircuits/dhw1/waterFlow:::waterFlow
   /dhwCircuits/dhw1/workingTime:::WaterWorkingTime
   /gateway/DateTime:0:0:DateTime
   /gateway/instAccess:0:0:InstAccess
   /gateway/uuid:::Uuid
   /gateway/versionFirmware:::FirmwareVersion
   /heatSources/ChimneySweeper:::ChimneySweeper
   /heatSources/flameCurrent:::FlameCurrent
   /heatSources/gasAirPressure:0:0:GasAirPressure
   /heatSources/hs1/energyReservoir:::EnergyReservoir
   /heatSources/hs1/flameStatus:::FlameStatus
   /heatSources/hs1/fuel/caloricValue:0:0:CaloricValue
   /heatSources/hs1/fuel/density:0:0:FuelDensity
   /heatSources/hs1/fuelConsmptCorrFactor:0:0:FuelConsmptCorrFactor
   /heatSources/hs1/info:::HeatSourceInfo
   /heatSources/hs1/nominalFuelConsumption:0:0:FuelConsumption
   /heatSources/hs1/reservoirAlert:0:0:ReservoirAlert
   /heatSources/hs1/supplyTemperatureSetpoint:0:0:SupplyTemperatureSetpoint
   /heatSources/hs1/type:::HeatSourceType
   /heatSources/info:::HeatSourceInfo
   /heatSources/numberOfStarts:0:0:NumberOfStarts
   /heatSources/systemPressure:20:0.2:SystemPressure
   /heatSources/workingTime/centralHeating:0:0:CentralHeatingWorkingTime
   /heatSources/workingTime/secondBurner:0:0:SecondBurnerWorkingTime
   /heatSources/workingTime/totalSystem:0:0:SystemWorkingTime
   /heatingCircuits/hc1/activeSwitchProgram:0:0:ActiveSwitchProgram
   /heatingCircuits/hc1/actualSupplyTemperature:0:0:HC1SupplyTemp
   /heatingCircuits/hc1/currentRoomSetpoint:1::RoomDesiredTemp
   /heatingCircuits/hc1/fastHeatupFactor:0:0:HeatupFactor
   /heatingCircuits/hc1/manualRoomSetpoint:10::RoomManualDesiredTemp
   /heatingCircuits/hc1/operationMode:10::HeatMode 
   /heatingCircuits/hc1/pumpModulation:1:10:PumpModulation
   /heatingCircuits/hc1/status:0:0:Status 
   /heatingCircuits/hc1/switchPrograms/A/1-Mo:0:0:ProgramA1-Mo
   /heatingCircuits/hc1/switchPrograms/A/2-Tu:0:0:ProgramA2-Tu
   /heatingCircuits/hc1/switchPrograms/A/3-We:0:0:ProgramA3-We
   /heatingCircuits/hc1/switchPrograms/A/4-Th:0:0:ProgramA4-Th
   /heatingCircuits/hc1/switchPrograms/A/5-Fr:0:0:ProgramA5-Fr
   /heatingCircuits/hc1/switchPrograms/A/6-Sa:0:0:ProgramA6-Sa
   /heatingCircuits/hc1/switchPrograms/A/7-Su:0:0:ProgramA7-Su
   /heatingCircuits/hc1/switchPrograms/B/1-Mo:0:0:ProgramB1-Mo
   /heatingCircuits/hc1/switchPrograms/B/2-Tu:0:0:ProgramB2-Tu
   /heatingCircuits/hc1/switchPrograms/B/3-We:0:0:ProgramB3-We
   /heatingCircuits/hc1/switchPrograms/B/4-Th:0:0:ProgramB4-Th
   /heatingCircuits/hc1/switchPrograms/B/5-Fr:0:0:ProgramB5-Fr
   /heatingCircuits/hc1/switchPrograms/B/6-Sa:0:0:ProgramB6-Sa
   /heatingCircuits/hc1/switchPrograms/B/7-Su:0:0:ProgramB7-Su
   /heatingCircuits/hc1/temperatureLevels/comfort2:10::ComfortTemp
   /heatingCircuits/hc1/temperatureLevels/eco:10::EcoTemp
   /heatingCircuits/hc1/temporaryRoomSetpoint:1::RoomTemporaryDesiredTemp
   /notifications:0:0:Notifications
   /system/brand:0:0:SystemBrand
   /system/bus:::BusType
   /system/healthStatus:10::Health
   /system/heatSources/hs1/actualModulation:1::PowerModulation
   /system/heatSources/hs1/actualPower:1::Power
   /system/holidayModes/hm1/assignedTo:0:0:Holiday1Assign
   /system/holidayModes/hm1/dhwMode:0:0:Holiday1WaterMode
   /system/holidayModes/hm1/hcMode:0:0:Holiday1HeatMode
   /system/holidayModes/hm1/startStop:0:0:Holiday1
   /system/holidayModes/hm2/assignedTo:0:0:Holiday2Assign
   /system/holidayModes/hm2/dhwMode:0:0:Holiday2WaterMode
   /system/holidayModes/hm2/hcMode:0:0:Holiday2HeatMode
   /system/holidayModes/hm2/startStop:0:0:Holiday2
   /system/holidayModes/hm3/assignedTo:0:0:Holiday3Assign
   /system/holidayModes/hm3/dhwMode:0:0:Holiday3WaterMode
   /system/holidayModes/hm3/hcMode:0:0:Holiday3HeatMode
   /system/holidayModes/hm3/startStop:0:0:Holiday3
   /system/holidayModes/hm4/assignedTo:0:0:Holiday4Assign
   /system/holidayModes/hm4/dhwMode:0:0:Holiday4WaterMode
   /system/holidayModes/hm4/hcMode:0:0:Holiday4HeatMode
   /system/holidayModes/hm4/startStop:0:0:Holiday4
   /system/holidayModes/hm5/assignedTo:0:0:Holiday5Assign
   /system/holidayModes/hm5/dhwMode:0:0:Holiday5WaterMode
   /system/holidayModes/hm5/hcMode:0:0:Holiday5HeatMode
   /system/holidayModes/hm5/startStop:0:0:Holiday5
   /system/info:::SystemInfo
   /system/minOutdoorTemp:0:0:MinOutdoorTemp
   /system/sensors/temperatures/outdoor_t1:1:0.5:OutdoorTemp
   /system/sensors/temperatures/return:1:0.5:ReturnTemp
   /system/sensors/temperatures/supply_t1:1:0.5:SupplyTemp
   /system/sensors/temperatures/supply_t1_setpoint:1:0.5:DesiredSupplyTemp
   /system/systemType:::SystemType
);
# I don't know anything about RC30 and RC35 - feel free to fill with knowledge:
my @RC30DEFAULTS = 
    qw(/gateway/DateTime:0:0:DateTime
);

my @RC35DEFAULTS = 
    qw(/gateway/DateTime:0:0:DateTime
);

# extra valid value not in range (set by gateway)
my %extra_value=
qw(/heatingCircuits/hc1/fastHeatupFactor 0
   /heatingCircuits/hc1/temporaryRoomSetpoint -1
   /gateway/DateTime now);

sub BDKM_Define($$);
sub BDKM_Undefine($$);

sub BDKM_Initialize($)
{
    my ($hash) = @_;
    
    $hash->{STATE}           = "Init";
    $hash->{DefFn}           = "BDKM_Define";
    $hash->{UndefFn}         = "BDKM_Undefine";
    $hash->{SetFn}           = "BDKM_Set";
    $hash->{GetFn}           = "BDKM_Get";
    $hash->{AttrFn}          = "BDKM_Attr";
    $hash->{DeleteFn}        = "BDKM_Undefine";

    $hash->{AttrList}        = 
        "BaseInterval " .
        "InterPollDelay " .
        "PollIds:textField-long  " .
        "HttpTimeout " .
        $readingFnAttributes;
    return undef;
}

sub BDKM_Define($$)
{
    my ($hash, $def) = @_;
    my @a            = split(/\s+/, $def);
    my $name                    = $a[0];

    my $salt = "";
    my $cryptkey="";
    my $usage="usage: \"define <devicename> BDKM <IPv4-address|hostname>  <GatewayPassword> <PrivatePassword> <md5salt>\" or\n".
        "\"define <devicename> BDKM <IPv4-address|hostname>  <AES-Key (see:https://ssl-account.com/km200.andreashahn.info)>\"";

    (@a == 4 or @a ==6) or return "$name $usage"; 

    $hash->{NAME}               = $name;
    $hash->{STATE}              = "define";

    my $ip                      = $a[2];
    ($ip =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ and 
     $1<256 and $2<256 and $3<256 and $4<256) or 
     ($ip =~ m/(?=^.{1,253}$)(^(((?!-)[a-zA-Z0-9-]{1,63}(?<!-))|((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63})$)/) or
     return "$name IP or hostname invalid, $usage";
    
    if(@a == 6) {
        my @passwd                  = ($a[3],$a[4]); # gateway,private passwd
        
        $salt=$a[5];
   
        if(!($passwd[0] =~ /-/)) { # must be base64
            my $i;
            foreach $i ((0,1)) {
                $_ = decode_base64($passwd[$i]);
                s/[\r\n]//g;
                $passwd[$i] = $_;
            }
        }
        $passwd[0] =~ tr/-//d;
        if(length($passwd[0]) != 16 or 
           length($passwd[1]) == 0) {
            Log3 $name, 1, "$name please check passwords";
            return "$name ERROR gateway password needs format ".
                "\"aaaa-bbbb-cccc-dddd\"\n".
                "password may be encoded base64 to make it less human readable";
        }
        $salt = pack('H*',$salt);
        $cryptkey =  md5($passwd[0].$salt).md5($salt.$passwd[1]);
    } elsif (@a == 4) { 
        # complete AES-Key from define. Can be generated here:
        # https://ssl-account.com/km200.andreashahn.info
        $cryptkey = pack('H*',$a[3]);
    } 
    
    Log3 $name, 3, "$name using AES-Key: ".unpack("H*",$cryptkey)."\n";

    $hash->{CRYPT} = 
        Crypt::Rijndael->new($cryptkey, Crypt::Rijndael::MODE_ECB() );

    $hash->{NAME}                             = $name;
    $hash->{IP}                               = $ip;
    $hash->{SEQUENCE}                         = 0;
    $hash->{POLLIDS}                          = {}; # from attr PollIds
    $hash->{UPDATES}                          = []; # Ids to check for update reading after polling
    $hash->{REALTOUSER}                       = {}; # Hash to transform real IDs to readings
    $hash->{USERTOREAL}                       = {}; # Hash to readings to real IDs
    $hash->{IDS}                              = {}; # Hash containing IDS of first full poll
    $hash->{VERSION}                          = '$Id: 79_BDKM.pm 12770 2016-12-14 08:39:57Z arnoaugustin $';    
    # init attrs to defaults:
    map {BDKM_Attr("del",$name,$_)} qw(BaseInterval InterPollDelay ReadBackDelay HttpTimeout);

    # delay start to have a chance that all attrs are set
    BDKM_Timer($hash,10,"BDKM_doSequence");
    return undef;
}

sub BDKM_Attr(@)
{
    my ($cmd,$name,$attr,$val)  = @_;
    my $hash                    = $defs{$name};
    my $error                   = "$name: ERROR attribute $attr ";
    my $del                     = $cmd =~ /del/;
    local $_;

    defined $val or $val="";
    
    if ($attr eq "BaseInterval") {
        $del and $val = 120; # default
        if($val !~ /^\d+$/ or $val < 30) {
            return $error."needs interger value >= 30";
        } else {
            $hash->{BASEINTERVAL} = $val;            
        } 
    } elsif ($attr eq "InterPollDelay") {
        $del and $val = 0; # default
        if($val !~ /^\d+$/) {
            return $error."needs interger value";
        } else {
            $hash->{INTERPOLLDELAY} = $val/1000;            
        } 
    } elsif($attr eq "ReadBackDelay") {
        $del and $val = 500;
        if($val !~ /^\d+$/ or $val < 100 or $val > 2000) {
            return $error."needs interger value (milliseconds) between 100 and 2000";
        } else {
            $hash->{READBACKDELAY} = $val;
        } 
    } elsif ($attr eq "HttpTimeout") {
        $del and $val = 10; # default
        $val =~ /^([0-9]+|[0-9]+\.?[0.9]+)$/ or return $error."needs numeric value";
        $hash->{HTTPTIMEOUT} = $val;
    } elsif($attr eq "PollIds") {
        $hash->{POLLIDS}      = {}; # no more polling
        $hash->{UPDATES}      = []; # no updates for possibly running poll
        $del and return undef;
        $hash->{REALTOUSER}   = {};
        $hash->{USERTOREAL}   = {};
        my @ids=();
        # add defaults if set 
        $val =~ s|RC300DEFAULTS||g and push(@ids, @RC300DEFAULTS);
        $val =~ s|RC35DEFAULTS||g  and push(@ids, @RC35DEFAULTS);
        $val =~ s|RC30DEFAULTS||g  and push(@ids, @RC30DEFAULTS);
        push(@ids,split(/\s+/s,$val));
        my $err = $error."needs space separated valid gateway IDs like\n".
            "/system/sensors/temperatures/return:2:0.5:ReturnTemp\nor just\n".
            "/system/sensors/temperatures/return:::\nor just\n".
            "/system/sensors/temperatures/return\n";

        foreach (@ids) {
            s|\s+||gs;
            /[A-z]/ or next;
            my($id,$modulo,$delta,$replace);

            if(m|(^/[A-z0-9\-_/]+[A-z0-9])$|) { # no ":"
                # poll id every cycle, no delta check (allways update), no replacement
                ($id,$modulo,$delta,$replace) = ($1,1,0,"");
            } else { # colon separated extras id:modulo:delta:replace
                unless(($id,$modulo,$delta,$replace) = 
                  m|(^/[A-z0-9\-_/]+[A-z0-9]):[-]*(\d*):([0-9]*\.?[0-9]*):(.*)$|) {
                    return $err;
                }
                ($modulo eq "" or $modulo =~ '-') and $modulo = -1;
            }
            # check pathes:
            my $ok = 0;
            foreach my $dir (@BaseDirs) {
                $dir eq "/" and next;
                !index($id,$dir,0) and $ok=1 and last;
            }
            $ok or return $error."$id is not a valid gateway ID";

            
            defined $hash->{POLLIDS}{$id} and
                Log3 $hash, 4, "$name attr PollIds - Overwritig definition of $id";
                
            $delta eq "" or $delta += 0.0;
            $hash->{POLLIDS}{$id}{MODULO} =int($modulo);
            $hash->{POLLIDS}{$id}{DELTA}  = $delta;
            if($replace) {
                $hash->{REALTOUSER}{$id}=$replace;
                $hash->{USERTOREAL}{$replace}=$id;
            } else {
                # remove replacements for IDs if overwritten.
                if(defined  $hash->{REALTOUSER}{$id}) {
                     delete $hash->{REALTOUSER}{$id};
                     map {
                         $hash->{USERTOREAL}{$_} eq $id and delete $hash->{USERTOREAL}{$_}
                     } keys %{$hash->{USERTOREAL}};
                }
            }
        }
    }
    
    return undef;
}



sub BDKM_doSequence($)
{
    my ($hash)                 = @_;

    # BDKM_doSequence is never called directly. It's only triggered by its own timer.

    # restart timer for next sequence
    BDKM_Timer($hash,$hash->{BASEINTERVAL},"BDKM_doSequence");
    # only start polling if we are not polling (e.g. due to network promlems)
    if($hash->{ISPOLLING}) {
        Log3 $hash, 3, $hash->{NAME}." ERROR: trying to start new sequence while previous not finished";
        Log3 $hash, 3, $hash->{NAME}." Gateway not responding? BaseInterval too short? InterPollDelay too high?";
        return;
    }
    $hash->{ISPOLLING}=1;
    my $seq = $hash->{SEQUENCE};
    my $h   = $hash->{POLLIDS};

    Log3 $hash, 4, "$hash->{NAME} starting polling sequence #".$seq;

    if(!$seq) { # do full poll and init $hash->{IDS}
        @{$hash->{JOBQUEUE}} = @BaseDirs;
        # update only modulos >= 0
        @{$hash->{UPDATES}}  =
            sort grep {$h->{$_}{MODULO} >= 0} keys(%$h);
    } else {
        $h or return; # no ids to poll
        # only poll known IDs which are in turn
        @{$hash->{UPDATES}} =
            sort grep {$h->{$_}{MODULO} > 0 and $seq % $h->{$_}{MODULO} == 0 and defined $hash->{IDS}{$_}} keys(%$h);
        # JOBQUEUE: remove special switchPrograms and transform to the real base reading:
        my %seen=(); 
        @{$hash->{JOBQUEUE}} = BDKM_MapSwitchPrograms($hash->{UPDATES});
    }
    Log3 $hash, 6, $hash->{NAME}." jobqueue is:".join(" ",@{$hash->{JOBQUEUE}})."\n";
    Log3 $hash, 6, $hash->{NAME}." elements to update after polling: ".join(" ",@{$hash->{UPDATES}})."\n";
    readingsSingleUpdate($hash, "state", "polling", 0);
    BDKM_JobQueueNextId($hash);
}

sub BDKM_JobQueueNextId($)
{
    my ($hash)                 = @_;
    
    if(@{$hash->{JOBQUEUE}}) { # still ids to poll
        my $id = (@{$hash->{JOBQUEUE}})[0];
        Log3 $hash, 5, "$hash->{NAME} reading $id";
        # get next type
        BDKM_HttpGET($hash,$id,\&BDKM_JobQueueNextIdHttpDone);
    } else { 
        BDKM_UpdateReadings($hash,$hash->{UPDATES});
        $hash->{SEQUENCE}++;
        $hash->{ISPOLLING}=0;
        Log3 $hash, 4, $hash->{NAME}." update  ".join(" ",@{$hash->{UPDATES}})."\n";
        readingsSingleUpdate($hash, "state", "idle", 0);
    }
}

sub BDKM_JobQueueNextIdHttpDone($)
{
    my ($param, $err, $data)     = @_;
    my $hash                     = $param->{hash};
    my $name                     = $hash ->{NAME};
    my $json;

    if($err) {
        readingsSingleUpdate($hash, "state", 
                             "reading ids ERROR - retrying every 60s", 1);
        Log3 $name, 2, "$name communication ERROR in state $hash->{STATE}: $err";
        # try again in 60s
        BDKM_Timer($hash,60,"BDKM_JobQueueNextId");
        return; 
    }
    my $hth= $param->{httpheader};
    $hth =~ s/[\r\n]//g;
    Log3 $name, 5, "$name HTTP done @{$hash->{JOBQUEUE}}[0],$hth";
    # did this type, remove from job queue:
    my $id = shift(@{$hash->{JOBQUEUE}});

    ($json,$data) = BDKM_decode_http_data($hash,$data);

    if($json) {
        if (!$hash->{SEQUENCE} and $json and $json->{type} eq "refEnum") { # init only
            # new type
            foreach my $item (@{$json->{references}}) {
                my $entry = $item->{id};
                #exists $hash->{IGNOREIDS}{$entry} and next; # ignore
                # push to job queue
                push(@{$hash->{JOBQUEUE}},$entry);
            }
        } else {
            BDKM_update_id_from_json($hash,$json);
        }
    } else {
        if(!$hash->{SEQUENCE}) {
            if($id ne "/") {
                $hash->{IDS}{$id}{RAWDATA} = 1;
                $hth =~ s|HTTP/...|HTTP|;
                $hth =~ s/\s+/_/g;
                $hth =~ /200/ or $hash->{IDS}{$id}{HTTPHEADER} = $hth;
                $hth =~ /200/ or $data = $hth;
            }

            Log3 $hash, 4, "$name $id: $data";
        }
    }
    if($hash->{INTERPOLLDELAY}) {
        BDKM_Timer($hash,$hash->{INTERPOLLDELAY},"BDKM_JobQueueNextId");
    } else {
        BDKM_JobQueueNextId($hash);
    }
    return;
}

sub BDKM_UpdateReadings($$)
{
    my ($hash,$listref)  = @_;

    readingsBeginUpdate($hash);
    foreach my $id (@$listref) {
        my $val = $hash->{IDS}{$id}{VALUE};
        defined $val or next;
        Log3 $hash, 5, "Check reading update for $id $val";

        my $reading = defined $hash->{REALTOUSER}{$id} ?  
            $hash->{REALTOUSER}{$id} : $id;
        my $rdval = $hash->{READINGS}{$reading}{VAL}; 
        if(defined($rdval) and defined $hash->{POLLIDS}{$id}) {
            my $delta = $hash->{POLLIDS}{$id}{DELTA};
            # same as last - skip
            $delta eq "" and $rdval eq $val and next;
            # difference too small - skip
            $delta and abs($rdval-$val) < $delta and next;
        }
        Log3 $hash, 4, "$hash->{NAME} update reading $reading $val";
        readingsBulkUpdate($hash,$reading,$val);
    }
    readingsEndUpdate($hash,1);

}

sub BDKM_Undefine($$)
{
    my ($hash, $def)  = @_;
    my $name = $hash->{NAME};   

    BDKM_RemoveTimer($hash);
    return undef;
}


sub BDKM_GetInfo
{
    my ($hash, $matches) = @_;

    no warnings 'uninitialized';
    my $fmt="%-50.50s %-25.25s %-23.23s %s %-30.30s %-10.10s %-10.10s\n";
    my $header =sprintf($fmt,
                        "Gateway ID", "FHEM Reading (Alias)", "Last Value Read", "TW", 
                        "Valid Values", "Poll", "Rd.Update");
    my $llll = ("-" x length($header))."\n";
    my @ids;
    if($matches) {
        # loop over all possible IDs and aliases and check if
        # they match given regexp inputs
        my %seen=();
        map { 
            my $regex = qr/$_/;
            map {
                if($_ =~ $regex) { # match on input to INFO
                    my $realid = defined $hash->{USERTOREAL}{$_} ?
                        $hash->{USERTOREAL}{$_} : $_;
                    !$seen{$realid}++ and push(@ids,$realid);
                } 
            } (keys %{$hash->{IDS}}, keys %{$hash->{USERTOREAL}});
        } split(/\s+/,$matches);
    } else {
        # use all IDs
        @ids = keys %{$hash->{IDS}};
    }

    my @lines= sort map {
        my $id=$_;
        my $h=$hash->{IDS}{$id};
        my $p=$hash->{POLLIDS}{$id};
        my $m = $p->{MODULO};
        my $d = $p->{DELTA};
        my $u = $h->{UNIT};
        my $type = substr($h->{TYPE},0,1);
        my $flags = ($type ? $type : " ").($h->{WRITEABLE} ? '+' : '-');
            
        my $a = defined $h->{RAWDATA} ? $h->{HTTPHEADER} : $h->{ALLOWED};
        $u =~ s/µ/u/g;
        $a =~ s/ /,/g;
        sprintf($fmt,
                $id,
                $hash->{REALTOUSER}{$id},
                $h->{VALUE}.($u ? " $u":""),
                $flags,
                defined $a                ? $a          : 
                $h->{MIN} ? "[$h->{MIN}:$h->{MAX}]"     : "",
                (!defined $m or $m  <  0) ? ""          :
                $m == 0                   ? "once"      :
                $m == 1                   ? "always"    :
                $m  >1                    ? "every $m"  : "",
                (!defined $d or $d eq "") ? "on change" : 
                $d  == 0                  ? "always"    : "Δ >= $d"
         );
    } @ids;

    my $footer= $matches ? "" :
q(* The table shows all known gateway IDs.  A "+" sign in the W column means the ID is writeable.
  Long entries may be cut due to formating.
  Ranges for Valid  Values ranges are shown as: [from:to] 
  When no JSON data can be fetched the HTTP error is shown.
  Temperatures are normaly allowed to set in 0.5 C steps only.
  On startup all IDs are gathered once but do not automatically generate a fhem reading.
  IDs which shoud generate readings not only with the set/get command need to be defined with the "PollIds" attribute.
  Poll:
        always  => ID is polled every cycle (PollIds setting *:1:*:*)
        every X => ID is only polled every Xth cycle (PollIds setting *:X:*:*)
        once    => After gathering process on startup this ID is checked for reading update (PollIds setting *:0:*:*)       
        ''      => update checks only on get/set command (PollIds setting *::*:* or not set)

  Redings Udate:
        always    => Reading Update is always done on value update (PollIds setting *:*:0:*)
        Δ >= X    => Reading Update is done when difference to last reading was at least X (PollIds setting *:*:X:*)
        on change => Reading Update is done when value has changed to last reading (PollIds setting *:*::*)
);

    return "\n".$header.$llll.join("",@lines).$llll.$footer;
}


sub BDKM_Set($@)
{
    my ( $hash, $name, $id, @values) = @_;
    
    if(!defined $hash->{IDS}{$id} and !defined $hash->{USERTOREAL}{$id}) {
        no warnings 'uninitialized';
        # only print aliased commands:
        my @writeable=sort grep {
            $hash->{IDS}{$hash->{USERTOREAL}{$_}}{WRITEABLE}
        } keys %{$hash->{USERTOREAL}};
        my @cmds=map {
            my @vals=();
            my $realid=$hash->{USERTOREAL}{$_};
            defined $extra_value{$realid} and push (@vals,$extra_value{$realid});
            my $h=$hash->{IDS}{$realid}; 
            if ($realid =~ /HeatupFactor/) {
                push(@vals,(10,20,30,40,50,60,70,80,90,100));
            } elsif(defined $h->{ALLOWED}) {
                $h->{TYPE} ne "arrayData" and push(@vals,split(/\s+/,$h->{ALLOWED}));
            } elsif (defined $h->{MAX}) {
                if($h->{UNIT} eq "C") {                    
                    for(my $i=$h->{MIN}; $i <= $h->{MAX}; $i+=0.5) {
                        push(@vals,$i);
                    }
                }
            }
            if (@vals) {
                $_.=":".join(',',@vals);
            } else {
                $_;
            }
        } @writeable;
        return "Unknown argument $id, choose one of ".join(" ",@cmds);
    }

    @values or
        return "usage: set $hash->{NAME} <ID> <value ...>";

    my $value=join(" ",@values);
    my $ret;
    $ret = BDKM_SetId($hash, $id, $value);
    $ret !~ /Unable to set/ and return $ret;
    BDKM_msleep(2000);
    $ret = BDKM_SetId($hash, $id, $value);
    return $ret;
}

sub BDKM_HttpTest
{
    my ($hash,$id,$method,$data)   = @_;
    my $param = {
        url           => "http://" . $hash->{IP} . $id,
        hash          => $hash,
        data          => $data,
        method        => $method,
        header        => "agent: PortalTeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json",
    };
   
    $param->{timeout} = 3;
    my @a= HttpUtils_BlockingGet($param); #returns ($err, $data)
    $param->{hash}=0;
  
}

sub BDKM_SetId($@)
{
    my ($hash,$id,$value)   = @_;
    my $name=$hash->{NAME};

    defined $hash->{USERTOREAL}{$id} and $id = $hash->{USERTOREAL}{$id};
    # set getway time to host time:
    $id eq "/gateway/DateTime" and $value eq "now" and
        $value=strftime("%Y-%m-%dT%H:%M:%S", localtime);

    my $data;
    my $err;

    if($value =~ /\s+test$/ or defined $hash->{IDS}{$id}{RAWDATA}) {
        # we dont know anything about that...yet
        # try raw data send
        $value =~ s/\s+test$//g;
        $id =~ /firmware/i and return; # better...if we don't know what we do.
        
        Log3 $name, 3, "$name set rawpost $id value $value";
        $data = BDKM_Encrypt($hash,$value);
        Log3 $name, 3, "$name http PUT $id encrypted data $data";
        my $a;
        ($data,$err,$a) = BDKM_HttpTest($hash,$id,"PUT",$data);
        return "+1+$data+2+$err+3+\n";
    } elsif($value =~ /\s+raw$/) {
        $value =~ s/\s+raw$//g;
        Log3 $name, 3, "$name set raw $id value $value";
        $data = BDKM_Encrypt($hash,$value);
        ($data,$err) = BDKM_HttpPUT($hash,$id,$data);
        return $data.$err;
    } else {
        Log3 $name, 3, "$name set raw $id value $value";
        my $rawdata = BDKM_GetId($hash,$id,"raw") or
            return "unable to set $id because the value can not be read";  
        
        defined $hash->{IDS}{$id}{VALUE} or return "ID $id is unknown";
        
        my $h=$hash->{IDS}{$id};
        my $type=$h->{TYPE};
        
        defined $h->{WRITEABLE} and $h->{WRITEABLE} or return "ID $id is not writeable";
        my $allowed=defined $h->{ALLOWED} ? $h->{ALLOWED} : "";
        
        my $json={};
        if($type eq "floatValue") {
            $value =~ s/\"//;
            $value =~ /^-?\d+\.?\d*$/ or return "$id needs a float/integer value";
            Log3 $name, 3, "$name $id set floatValue $value";
            my $ok = (defined $extra_value{$id} and $extra_value{$id} eq $value);
            !$ok and defined $h->{MIN} and defined $h->{MAX} and
                ($value < $h->{MIN} || $value > $h->{MAX}) and
                return "allowed values for $id are: interger/float in range $h->{MIN} to $h->{MAX}";
            $json->{value} =  ($value + 0.0); # make number from it!
        } elsif ($type eq "stringValue") {
            Log3 $name, 3, "$name $id set stringValue $value";
            $allowed and $allowed !~ /$value/ and 
                return "allowed values for $id are: one of $allowed";
            $json->{value} =  $value;
            Log3 $name, 3, "$name set $id float value $value";
        } elsif ($type eq "arrayData") { # RC300 only for /system/holidayModes/hm[1-5]/assignedTo
            Log3 $name, 3, "$name $id set arrayData $value";
            my @a=split(/\s+/,$value);
            foreach(@a) {
                $allowed and $allowed !~ /$_/ and 
                    return "allowed values for $id are: one or more of $allowed";
            }
            $json->{values} =  \@a;
        } 
        if ($type eq "switchProgram") {
            Log3 $name, 3, "$name $id set switchProgram $value";
            my $postid=$id;
            $postid =~ s|/\d-([A-z][A-z])$||;
            $data=BDKM_makeSwitchPointData($rawdata,$1, $value);
            $data =~ /setpoint/ or return $data;
            $data = BDKM_Encrypt($hash,$data);
            ($data,$err) = BDKM_HttpPUT($hash,$postid,$data);
        } else {
            $data = BDKM_encode_http_data($hash,$json);
            BDKM_msleep($hash->{READBACKDELAY});
            ($data,$err) = BDKM_HttpPUT($hash,$id,$data);
        }
        BDKM_msleep($hash->{READBACKDELAY});
        my $ret = BDKM_GetId($hash,$id);

        $ret ne $value and $id ne "/gateway/DateTimeteTime" and
            return "$name Unable to set +$value+ to $id (readback: +$ret+)";
        return $ret;
    }
}

sub BDKM_Get($@)
{
    my ( $hash, $name, $id, $opt) = @_;

    # specials
    $id eq "INFO" and return BDKM_GetInfo($hash, $opt);
    if(defined $opt and $opt eq "rawforce")  {
        $opt="raw";
    } else {
        if(!defined $hash->{IDS}{$id} and !defined $hash->{USERTOREAL}{$id}
           or (defined($opt) and $opt ne "raw" and $opt ne "json")) {
            # only print aliased and special commands (like INFO):
            my @getable=qw(INFO);
            push(@getable, keys %{$hash->{USERTOREAL}});
            return "Unknown argument $id, choose one of ".join(" ",@getable);
        }
    }  
    return BDKM_GetId($hash, $id, $opt);
}

sub BDKM_GetId($@)
{
    my ($hash,$id,$opt)   = @_;
    my $name=$hash->{NAME};
    
    defined $hash->{USERTOREAL}{$id} and $id = $hash->{USERTOREAL}{$id};
    my $json;
    
    defined $opt or $opt = "";
    
    my $realid=$id;
    if($id =~ m|/\d-[MTWTFS][ouehrau]$|) { 
        # one of our pseudo switch program id
        # map to a real gateway id
        ($realid) =  BDKM_MapSwitchPrograms([$id]);
    }
    # blocking http get:
    my($err,$data, $httpheader) = BDKM_HttpGET($hash,$realid);
    if($err) {
        Log3 $name, 2, "$name unable to fetch ID $id - $err";
        return "$name unable to fetch ID $id - $err";
    } else {   
        ($json,$data) = BDKM_decode_http_data($hash,$data);
        Log3 $name, 2, "$name get $id - HTTP: $httpheader, data: $data";
        if($json) {
            BDKM_update_id_from_json($hash,$json);
            # always check for reading update when id was read
            BDKM_UpdateReadings($hash,[$id]);
            $opt eq "json" and return $json;
        }
        if($opt eq "raw") {
            return $data;
        }
        defined $hash->{IDS}{$id}{VALUE} and return $hash->{IDS}{$id}{VALUE};
    }
    return "";
}

# this routine takes the raw http json data of a switch program,
# the week day and the setpointstring to be set like
# "0700 comfort2 2200 eco"
# It then patches the new setpoints for that day to the json data (sorted!).

sub BDKM_makeSwitchPointData
{
    my ($data, $weekday, $setpointstr) =@_;
    my @setpoints=();
    my $timeraster=0;
    map {
        s/\}.*//;
        /switchPointTimeRaster.*?(\d+)/ and $timeraster=$1;
        /setpoint[^A-z]/ and !/\"$weekday\"/ and push(@setpoints,'{'.$_.'}');
    } split(/{/,$data);
    
    my @a=split(/\s+/,$setpointstr);
    while(@a) {
        $_=shift(@a);
        s|:||;
        my ($hr,$min)=/^(\d\d)(\d\d)$/ or return "invalid time format - use: HHMM";
        ($hr > 23 or $min > 59) and return "$hr$min use a valid time between 0000 and 2359";
        $timeraster and $min % $timeraster and 
            return "switch point $_ not allowed: switchpoint raster is 15 minutes";
        @a or return "$hr$min missing set point type";
        $_=shift(@a);
        my $time = $hr*60+$min;
        push(@setpoints, '{"dayOfWeek":"'.$weekday.'","setpoint":"'.$_.'","time":'.$time.'}'); # add weekday
    }
    return '['.join(',',
                sort {
                    # by day num
                    $WdToNum{($a =~ /dayOfWeek[^A-z]+([A-z][A-z])/)[0]} <=> 
                        $WdToNum{($b =~/dayOfWeek[^A-z]+([A-z][A-z])/)[0]} ||
                        # and time
                        ($a =~ /time[^\d]+(\d+)/)[0] <=> 
                        ($b =~ /time[^\d]+(\d+)/)[0] 
                } @setpoints).']';
}

############################## Helpers ###################################
sub BDKM_HttpPostOrGet
{
    my ($hash,$id,$method,$data,$callback)   = @_;
    my $param = {
        url           => "http://" . $hash->{IP} . $id,
        hash          => $hash,
        data          => $data,
        method        => $method,
        header        => "agent: PortalTeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json",
    };
    if(defined($callback)) {
        Log3 $hash, 5, "$hash->{NAME} async $method $param->{url}";
        $param->{timeout} = $hash->{HTTPTIMEOUT};
        $param->{callback} = $callback;
        HttpUtils_NonblockingGet($param);
        return undef;
    } else {
        $param->{timeout} = 3;
        Log3 $hash, 5, "$hash->{NAME} sync $method $param->{url}";
        return (HttpUtils_BlockingGet($param),$param->{httpheader}); #returns ($err, $data)
    }
}

sub BDKM_HttpGET
{
    my ($hash,$id,$callback)   = @_;
    return BDKM_HttpPostOrGet($hash,$id,"GET",undef,$callback);
}

sub BDKM_HttpPOST
{
    my ($hash,$id,$data,$callback)   = @_;
    return BDKM_HttpPostOrGet($hash,$id,"POST",$data,$callback);
}

sub BDKM_HttpPUT
{
    my ($hash,$id,$data,$callback)   = @_;
    return BDKM_HttpPostOrGet($hash,$id,"PUT",$data,$callback);
}

sub BDKM_Timer
{
    my ($hash,$secs,$callback) = @_;
    InternalTimer(gettimeofday()+$secs, $callback, $hash, 0);
}
sub BDKM_RemoveTimer
{
    RemoveInternalTimer($_[0]);
}

sub BDKM_Decrypt($$)
{
    my ($hash, $data)  = @_;

    $data = decode_base64($data);
    length($data) & 0xF and return ""; # must be 16byte blocked. if not decryt calls exit()!!!

    $data = $hash->{CRYPT}->decrypt( $data );
    my $len = length($data);
    
    ($len & 0xF) and return $data;

    # 16 byte block, remove padding
    my $i;
    for($i=0; $i < $len && ord(substr($data,-1-$i,1)) == 0; $i++){};
    if($i) {
        return substr($data,0,$len-$i);
    } else {
        # 16byte blocks not zero padded
        # check if RFC PKCS #7 padded and remove padding
        my $padchar = substr($data,($len - 1),1); #last char
        my $num = ord($padchar);
        if($num <= 16) {
            substr($data,$len - $num, $num) eq ($padchar x $num) and
                return substr($data,0,$len - $num);
        }
    }
    return $data;
}

sub BDKM_Encrypt($$)
{
    my ($hash, $data)           = @_;
    my $crypt                   =  $hash->{CRYPT};
    my $blocksize               =  $crypt->blocksize();
    # pad data to block size before encrypting - see RFC 5652
    my $numpad =  $blocksize - length($data)%$blocksize;

    return
        encode_base64($crypt->encrypt($data.(chr($numpad) x $numpad)));
}
sub BDKM_msleep
{
    select(undef, undef, undef, $_[0]/1000);
}


sub BDKM_decode_http_data
{
    my ($hash, $data) = @_;
    my $json="";
    Log3 $hash, 6, "$hash->{NAME} raw crypted HTTP data: $data";
    $data =~ /^\s*$/s and return ("","");
    $data = BDKM_Decrypt($hash,$data);
    my $len = length($data);
    Log3 $hash, 4, "$hash->{NAME} deocded $len bytes HTTP data: $data";
    if($data) {
        eval {$json = decode_json(encode_utf8($data)); 1;  } or do {
            $json="";
        }
    }
    return ($json,$data);
}

sub BDKM_encode_http_data
{
    my ($hash, $json) = @_;
    my $data = encode_json($json);
    Log3 $hash, 3, "$hash->{NAME} raw HTTP data: $data";
    $data = BDKM_Encrypt($hash,$data);
    Log3 $hash, 6, "$hash->{NAME} encocded HTTP data: $data";
    return $data;
}



sub BDKM_update_id_from_json
{
    my ($hash,$json) = @_;
    
    if ($json) {
        my $id       = $json->{id};
        my $type     = $json->{type};

        Log3 $hash, 6, "$hash->{NAME} update JSON $id $type";

        defined ($hash->{IDS}{$id}) or $hash->{IDS}{$id}={};

        my $h = $hash->{IDS}{$id};
        if($type eq "stringValue" or $type eq "floatValue"){
            if(!defined($h->{WRITEABLE})) { # initial
                $h->{WRITEABLE} = $json->{writeable};     
                $h->{TYPE}=$type;
                defined($json->{unitOfMeasure}) and $h->{UNIT}    = $json->{unitOfMeasure};
                defined($json->{minValue})      and $h->{MIN}     = $json->{minValue};
                defined($json->{maxValue})      and $h->{MAX}     = $json->{maxValue};
                defined($json->{allowedValues}) and $h->{ALLOWED} = join(" ",@{$json->{allowedValues}});
            }
            $h->{VALUE}     = $json->{value};
        } elsif ($type eq "switchProgram") {
            my @prog=();
            my $weekday;         
            foreach my $sp (@{$json->{switchPoints}}) {
                $weekday = $sp->{dayOfWeek};
                my $t = $sp->{time};
                my $h = int($t/60);
                my $entry = sprintf("%02d%02d %s",$h,$t-($h*60),$sp->{setpoint});
                my $num = $WdToNum{$weekday};
                $prog[$num] = defined $prog[$num] ? $prog[$num]." ".$entry : $entry;
                Log3 $hash, 5, "$hash->{NAME} update switchProgram $weekday $entry $sp->{time}";
            }
            my $i=1;
            foreach $weekday (qw(Mo Tu We Th Fr Sa Su)) {
                my $newid = "$id/$i-".$weekday;
                if(!defined $hash->{IDS}{$newid}) {
                    $hash->{IDS}{$newid}={
                        ID         => $newid,
                        TYPE       => $type,
                        WRITEABLE  => 1
                    }
                }
                $hash->{IDS}{$newid}{VALUE}  = $prog[$i++];
            } 
        } elsif ($type eq "errorList") {
            ### Sort list by timestamps
            my $err="";
            if(defined $json->{values}) {
                foreach my $entry (sort ( @{$json->{values}} )) {
                    $err .= sprintf("%-20.20s %-3.3s %-4.4s %-2.2s\n",
                                    $entry->{t}, $entry->{dcd}, $entry->{ccd}, $entry->{cat});
                }
            }
            if(!defined($h->{WRITEABLE})) { # initial
                $h->{WRITEABLE} = $json->{writeable};
                $h->{TYPE}      = "arrayData"; #is also arraydata
            }
            $h->{VALUE}     = $err;
        } elsif ($type eq "systeminfo" or $type eq "arrayData") {
            my $info="";
            if(defined $json->{values}) {
                foreach my $val (@{$json->{values}}) {
                    if(ref($val) eq 'HASH') {
                        $info .=join(" ", map { $_.":".$val->{$_} } keys %{$val})." ";
                    } else {
                        $info .= $val." ";
                    }
                }
                $info =~ s/ $//;
            }
            if(!defined($h->{WRITEABLE})) { # initial
                defined($json->{allowedValues}) and $h->{ALLOWED} = join(" ",@{$json->{allowedValues}});
                $h->{WRITEABLE} = $json->{writeable};
                $h->{TYPE}= "arrayData"; # info is also arraydata
            }
            $h->{VALUE}     = $info;
        } elsif ($type eq "yRecording") {
            defined $h->{TYPE} or $h->{TYPE}="Recroding";
            # ignore recordings - fhem records :-)
        } elsif ($type eq "refEnum") { 
            # ignore directory entry
        } elsif ($type eq "eMonitoringList") { 
            # ignore eMonitoringList - I don't have infos about that
        } else {
            Log3 $hash, 2, "$hash->{NAME}: unknown type $type for $id";
        }
    } else {
        Log3 $hash, 5, "$hash->{NAME}: no JSON data available";
    }
}

sub BDKM_MapSwitchPrograms
{
    # translate all /dhwCircuits/dhw1/switchPrograms/A/\d-[A-z][A-z]$ forms to
    # one real reading like /dhwCircuits/dhw1/switchPrograms/A
    my $aref=$_[0];
    my %seen=();
    my $x;
    # substitution needs $x becaus because $_ would modify original array!!
    return grep {!$seen{$_}++} map {$x=$_; $x =~ s|/\d-[MTWTFS][ouehrau]$||;$x} @$aref;
}


1;

# perl ./contrib/commandref_join.pl FHEM/79_BDKM.pm
# perl ./contrib/commandref_join.pl 

=pod
=item device
=item summary support for Buderus KM-Gateways
=item summary_DE Unterst&uuml;tzung f&uuml;r Buderus KM-Gateways
=begin html

<a name="BDKM"></a>
<h3>BDKM</h3>
<ul>
    BDKM is a module supporting Buderus Logamatic KM gateways similar
    to the <a href="#km200">km200</a> module. For installation of the
    gateway see fhem km200 internet wiki<br> 

    Compared with the km200 module the code of the BDKM module is more
    compact and has some extra features.  It has the ablility to
    define how often a gateway ID is polled, which FHEM reading
    (alias) is generated for a gateway ID and which minimum difference
    to the last reading must exist to generate a new reading (see
    attributes).<br>  

    It determines value ranges, allowed values and writeability from
    the gateway supporting FHEMWEB and readingsGroup when setting
    Values (drop down value menues).<br>  

    On definition of a BDKM device the gateway is connected and a full
    poll collecting all IDs is done. This takes about 20 to 30
    seconds. After that the module knows all IDs reported
    by the gateway. To examine these IDs just type:<BR>
    <code>get myBDKM INFO</code><BR>

    These IDs can be used with the PollIds attribute to define if and
    how the IDs are read during the poll cycle. <br> All IDs can be
    mapped to own short readings. 
    <br><br>

  <a name="BDKMdefine"></a>
  <b>Define</b>
  <ul>
    <code>define &lt;name&gt; BDKM &lt;IP-address|hostname&gt; &lt;GatewayPassword&gt;
    &lt;PrivatePassword&gt; &lt;MD5-Salt&gt;</code><br> 
    or <br>
    <code>define &lt;name&gt; BDKM &lt;IP-address|hostname&gt;  &lt;AES-Key&gt;</code><br> 
    <br><br>
    <code>&lt;name&gt;</code> : 
        Name of device<br>
    <code>&lt;IP-address&gt;</code> : 
        The IP adress of your Buderus gateway<br>
    <code>&lt;GatewayPassword&gt;</code> : 
       The gateway password as printed on case of the gateway s.th. 
       of the form: xxxx-xxxx-xxxx-xxxx<br>
    <code>&lt;PrivatePassword&gt;</code>  : The private password as 
       set with the buderus App<br>
    <code>&lt;MD5-Salt&gt;</code>  : MD5 salt for the crypt 
    algorithm you want to use (hex string like 867845e9.....). Have a look for km200 salt 86 ... <br>
    AES-Key can be generated here:<br>
    https://ssl-account.com/km200.andreashahn.info<br>
    <br>
  </ul>
  <a name="BDKMset"></a>
  <b>Set </b>
  <ul>
    <code>set &lt;name&gt; &lt;ID&gt; &lt;value&gt; ...</code>
    <br><br>
    where <code>ID</code> is a valid writeable gateway ID (See list command, 
    or "<code>get myBDKM INFO</code>")<br>
    The set command first reads the the ID from the gateway and also
    triggers a FHEM readings if necessary.  After that it is checked if the
    value is valid. Then the ID and value(s) are transfered to to the
    gateway. After waiting (attr ReadBackDelay milliseconds) the value
    is read back and checked against value to be set. If necessary again
    a FHEM reading may be triggered. The read back value or an error is 
    returned by the command. <br>

    Examples:
    <ul>
      <code>set myBDKM /heatingCircuits/hc1/temporaryRoomSetpoint 22.0</code><br>
      or the aliased version of it (if 
      /heatingCircuits/hc1/temporaryRoomSetpointee is aliased to 
      RoomTemporaryDesiredTemp):<br>
      <code>set myBDKM RoomTemporaryDesiredTemp 22.0</code><br>
      special to set time of gateway to the hosts date:<br>
      <code>set myBDKM /gateway/DateTime now</code><br>
      aliased:<br>
      <code>set myBDKM DateTime now</code><br>
    </ul>
    <br>
  </ul>
  <br>
  <a name="BDKMget"></a>
  <b>Get </b>
  <ul>
    <code>get &lt;name&gt; &lt;ID&gt; &lt;[raw|json]&gt;...</code><br><br> 
    where <code>ID</code> is a valid gateway ID or an alias to it.
    (See list command)<br> The get command reads the the ID from the
    gateway, triggeres readings if necessarry, and returns the value
    or an error if s.th. went wrong.  While polling is done
    asychronously with a non blocking HTTP GET. The set and get
    functions use a blocking HTTP GET/POST to be able to return a
    value directly to the user. Normaly get and set are only used by
    command line or when setting values via web interface.<br>
    With the <code>raw</code> option the whole original decoded data of the
    ID (as read from the gateway) is returned as a string.<br>  With
    the <code>json</code> option a perl hash reference pointing to the
    JSON data is returned (take a look into the module if you want to
    use that)<br>
    <br>

    Examples:
    <ul>
      <code>get myBDKM /heatingCircuits/hc1/temporaryRoomGetpoint</code><br>
      or the aliased version of it (see attr below):<br>
      <code>get myBDKM RoomTemporaryDesiredTemp</code><br>
      <code>get myBDKM DateTime</code><br>
      <code>get myBDKM /gateway/instAccess</code><br>
      Spacial to get Infos about IDs known by the gateway and own
      configurations:<BR>
      <code>get myBDKM INFO</code><br>
      Everything matching /temp/
      <code>get myBDKM INFO temp</code><br>
      Everything matching /Heaven/ or /Hell/
      <code>get myBDKM INFO Heaven Hell</code><br>
      Everything known:
      <code>get myBDKM INFO .*</code><br>
      Arguments to <code>INFO</code> are reqular expressions
      which are matched against all IDs and all aliases.
    </ul>
    <br>
  </ul>
  <br>

  <a name="BDKMattr"></a>
  <b>Attributes</b>
  <ul>
    <li>BaseInterval<br>
      The interval time in seconds between poll cycles.
      It defaults to 120 seconds. Which means that every 120 seconds a
      new poll collects values of IDs which turn it is.
    </li><br>
    <li>InterPollDelay<br>
      The delay time in milliseconds between reading of two IDs from 
      the gateway. It defaults to 0 (read as fast as possible).
      Some gateways/heatings seem to stop answering after a while
      when you are reading a lot of IDs. (verbose 2 "communication ERROR").
      To avoid gateway hangups always try to read only as many IDs as 
      really required. If it doesn't help try to increase the 
      InterPollDelay value. E.g. start with 100.
    </li><br>
    <li>ReadBackDelay<br>
      Read back delay for the set command in milliseconds.  This value
      defaults to 500 (0.5s).  After setting a value, the gateway need
      some time before the value can be read back.  If this delay is
      too short after writing you will get back the old value and not
      the expected new one. The default should work in most cases.
    </li><br>
    <li>HttpTimeout<br>
      Timeout for all HTTP requests in seconds (polling, set,
      get). This defaults to 10s.  If there is no answer from the
      gateway for HttpTimeout time an error is returned. If a HTTP 
      request expires while polling an error log (level 2) is 
      generated and the request is automatically restarted after 60 
      seconds.
    </li><br>
    <li>PollIds<br>
      Without this attribute FHEM readings are NOT generated 
      automatically! <br>
      This attribute defines how and when IDs are polled within
      a base interval (set by atrribute <code>BaseInterval</code>).<br>
      The attribute contains list of space separated IDs and options 
      written as <br>
      <code>GatewayID:Modulo:Delta:Alias</code>
      <br>
      Where Gateway is the real gateway ID like "/gateway/DateTime".<br>
      Modulo is the value which defines how often the GatewayID is
      polled from the gateway and checked for FHEM readings update.
      E.g. a value of 4 means that the ID is polled only every 4th cycle.<br>
      Delta defines the minimum difference a polled value must have to the 
      previous reading, before a FHEM reading with the new value is generated.<br>
      Alias defines a short name for the GatewayID under which the gateway ID
      can be accessed. Also readings (Logfile entries) are generated with this
      short alias if set. If not set, the original ID is used.<br>
      In detail:<br>
      <code>ID:1:0:Alias</code> - poll every cycle, when difference >= 0 to previous reading (means always, also for strings) trigger FHEM reading to "Alias"<br>
      <code>ID:1::Alias</code> -  poll every cycle, no Delta set => trigger FHEM reading to "Alias" on value change only<br>
      <code>ID:0::Alias</code> -  update reading on startup once if reading changed (to the one prevously saved in fhem.save)<br>
      <code>ID:1:0.5:Alias</code> - poll every cycle, when difference => 0.5 trigger a FHEM reading to "Alias"<br>
      <code>ID:15::Alias</code> - poll every 15th cylce, update reading only if changed<br>
      <code>ID:::Alias</code> - update reading on (get/set) only and only if value changed<br>
      <code>ID::0:Alias</code> - update reading on (get/set) only and trigger reading always on get/set<br>
      <code>ID</code> - without colons ":", poll every cycle, update reading allways (same as <code>ID:1:0:</code>)<br>
      Also some usefull defaults can be set by the special keyword RC300DEFAULTS, RC35DEFAULTS, RC30DEFAULTS.<br>
      As I don't know anything about RC35 or RC30 the later keywords are currently empty (please send me some info with "get myBDKM INFO" :-)<br>
      Definitions set by the special keywords (see the module code for it) are overwritten by definitions later set in the attribute definition<br>
      Example:
      <ul>
        <code>attr myBDKM PollIds \<br>
                RC300DEFAULTS \<br>
                /gateway/DateTime:0::Date \<br>
                /system/info:0:0:\<br>
                /dhwCircuits/dhw1/actualTemp:1:0.2:WaterTemp
        </code><br>
      </ul>
        Which means: Use RC300DEFAULTS, trigger FHEM reading "Date" when date has changed on startup only. Trigger FHEM reading "/system/info" (no aliasing) always on startup, poll water temperature every cycle and trigger FHEM reading "WaterTemp" when difference to last reading was at least 0.2 degrees.
      <br>
    </li><br>
    </ul>
</ul>

=end html
