diff --git a/fhem/FHEM/79_BDKM.pm b/fhem/FHEM/79_BDKM.pm new file mode 100644 index 000000000..681158982 --- /dev/null +++ b/fhem/FHEM/79_BDKM.pm @@ -0,0 +1,1297 @@ +# $Id$ +############################################################################## +# +# 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 . +# +# 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 which is 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->{AttrList} = + "BaseInterval " . + "PollIds:textField-long " . + "HttpTimeout " . + $readingFnAttributes; + return undef; +} + +sub BDKM_Define($$) +{ + my ($hash, $def) = @_; + my @a = split(/\s+/, $def); + my $name = $a[0]; + + # salt will be removed in future versions and must be set by user in fhem.cfg + my $salt = ""; + my $cryptkey=""; + my $usage="usage: \"define BDKM \" or\n". + "\"define BDKM \""; + + (@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}(?{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$'; + # init attrs to defaults: + map {BDKM_Attr("del",$name,$_)} qw(BaseInterval ReadBackDelay HttpTimeout); + + BDKM_reInit($hash); + 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; + BDKM_reInit($hash); + } + } 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_reInit($) +{ + my ($hash) = @_; + BDKM_RemoveTimer($hash); + $hash->{UPDATES} = []; + if($hash->{ISPOLLING}) { + # let sequence finish and try again + BDKM_Timer($hash,29,"BDKM_reInit"); + return; + } + if(!$hash->{SEQUENCE}) { # init + # delay start to have a chance that all attrs are set + BDKM_Timer($hash,5,"BDKM_doSequence"); + } else { + BDKM_Timer($hash,$hash->{BASEINTERVAL},"BDKM_doSequence"); + } +} + +sub BDKM_doSequence($) +{ + my ($hash) = @_; + + # 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) + $hash->{ISPOLLING} and 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; + + my $hth= $param->{httpheader}; + $hth =~ s/[\r\n]//g; + Log3 $name, 5, "$name HTTP done @{$hash->{JOBQUEUE}}[0],$hth"; + 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; + } + # 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; + } + + Log3 $hash, 4, "$name $id - no JSON data available - raw data: $data"; + } + } + BDKM_JobQueueNextId($hash); # get next id + + 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} "; + + 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 + } 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 +=begin html + + +

BDKM

+
    + BDKM is a module supporting Buderus Logamatic KM gateways similar + to the km200 module. For installation of the + gateway see fhem km200 internet wiki
    + + 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).
    + + It determines value ranges, allowed values and writeability from + the gateway supporting FHEMWEB and readingsGroup when setting + Values (drop down value menues).
    + + 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:
    + get myBDKM INFO
    + + These IDs can be used with the PollIds attribute to define if and + how the IDs are read during the poll cycle.
    All IDs can be + mapped to own short readings. +

    + + + Define +
      + define <name> BDKM <IP-address|hostname> <GatewayPassword> + <PrivatePassword> <MD5-Salt>
      + or
      + define <name> BDKM <IP-address|hostname> <AES-Key>
      +

      + <name> : + Name of device
      + <IP-address> : + The IP adress of your Buderus gateway
      + <GatewayPassword> : + The gateway password as printed on case of the gateway s.th. + of the form: xxxx-xxxx-xxxx-xxxx
      + <PrivatePassword> : The private password as + set with the buderus App
      + <MD5-Salt> : MD5 salt for the crypt + algorithm you want to use (hex string like 867845e9.....). Have a look for km200 salt 86 ...
      + AES-Key can be generated here:
      + https://ssl-account.com/km200.andreashahn.info
      +
      +
    + + Set +
      + set <name> <ID> <value> ... +

      + where ID is a valid writeable gateway ID (See list command, + or "get myBDKM INFO")
      + 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.
      + + Examples: +
        + set myBDKM /heatingCircuits/hc1/temporaryRoomSetpoint 22.0
        + or the aliased version of it (if + /heatingCircuits/hc1/temporaryRoomSetpointee is aliased to + RoomTemporaryDesiredTemp):
        + set myBDKM RoomTemporaryDesiredTemp 22.0
        + special to set time of gateway to the hosts date:
        + set myBDKM /gateway/DateTime now
        + aliased:
        + set myBDKM DateTime now
        +
      +
      +
    +
    + + Get +
      + get <name> <ID> <[raw|json]>...

      + where ID is a valid gateway ID or an alias to it. + (See list command)
      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.
      + With the raw option the whole original decoded data of the + ID (as read from the gateway) is returned as a string.
      With + the json option a perl hash reference pointing to the + JSON data is returned (take a look into the module if you want to + use that)
      +
      + + Examples: +
        + get myBDKM /heatingCircuits/hc1/temporaryRoomGetpoint
        + or the aliased version of it (see attr below):
        + get myBDKM RoomTemporaryDesiredTemp
        + get myBDKM DateTime
        + get myBDKM /gateway/instAccess
        + Spacial to get Infos about IDs known by the gateway and own + configurations:
        + get myBDKM INFO
        + Everything matching /temp/ + get myBDKM INFO temp
        + Everything matching /Heaven/ or /Hell/ + get myBDKM INFO Heaven Hell
        + Everything known: + get myBDKM INFO .*
        + Arguments to INFO are reqular expressions + which are matched against all IDs and all aliases. +
      +
      +
    +
    + + + Attributes +
      +
    • BaseInterval
      + 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. +

    • +
    • ReadBackDelay
      + 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. +

    • +
    • HttpTimeout
      + 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. +

    • +
    • PollIds
      + Without this attribute FHEM readings are NOT generated + automatically!
      + This attribute defines how and when IDs are polled within + a base interval (set by atrribute BaseInterval).
      + The attribute contains list of space separated IDs and options + written as
      + GatewayID:Modulo:Delta:Alias +
      + Where Gateway is the real gateway ID like "/gateway/DateTime".
      + 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.
      + Delta defines the minimum difference a polled value must have to the + previous reading, before a FHEM reading with the new value is generated.
      + 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.
      + In detail:
      + ID:1:0:Alias - poll every cycle, when difference >= 0 to previous reading (means always, also for strings) trigger FHEM reading to "Alias"
      + ID:1::Alias - poll every cycle, no Delta set => trigger FHEM reading to "Alias" on value change only
      + ID:0::Alias - update reading on startup once if reading changed (to the one prevously saved in fhem.save)
      + ID:1:0.5:Alias - poll every cycle, when difference => 0.5 trigger a FHEM reading to "Alias"
      + ID:15::Alias - poll every 15th cylce, update reading only if changed
      + ID:::Alias - update reading on (get/set) only and only if value changed
      + ID::0:Alias - update reading on (get/set) only and trigger reading always on get/set
      + ID - without colons ":", poll every cycle, update reading allways (same as ID:1:0:)
      + Also some usefull defaults can be set by the special keyword RC300DEFAULTS, RC35DEFAULTS, RC30DEFAULTS.
      + 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" :-)
      + Definitions set by the special keywords (see the module code for it) are overwritten by definitions later set in the attribute definition
      + Example: +
        + attr myBDKM PollIds \
        + RC300DEFAULTS \
        + /gateway/DateTime:0::Date \
        + /system/info:0:0:\
        + /dhwCircuits/dhw1/actualTemp:1:0.2:WaterTemp +

        +
      + 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. +
      +

    • +
    +
+ +=end html