# $Id: 73_km200.pm 0035 2015-01-06 20:00:00Z Matthias_Deeke $ ######################################################################################################################## # # 73_km200.pm # Creates the possibility to access the Buderus central heating system via # Buderus KM200 or KM50 communication module. It uses HttpUtils_NonblockingGet # from Rudolf Koenig to avoid a full blockage of the fhem main system during # polling procedure. # # Author : Matthias Deeke # Contributions : Olaf Droegehorn, Andreas Hahn, Rudolf Koenig, Markus Bloch, Stefan M., Furban # e-mail : matthias.deeke(AT)deeke(PUNKT)eu # Fhem Forum : http://forum.fhem.de/index.php/topic,25540.0.html # Fhem Wiki : http://www.fhemwiki.de/wiki/Buderus_Web_Gateway # # 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. # # Fhem 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 . # # fhem.cfg: define km200 # # Example 1 - Bare Passwords: # define myKm200 km200 192.168.178.200 GatewayGeheim PrivateGeheim # # Example 2 - base64 encoded passwords: Both passwords may be pre-encode with base64 # define myKm200 km200 192.168.178.200 R2F0ZXdheUdlaGVpbQ== UHJpdmF0ZUdlaGVpbQ== # ######################################################################################################################## # CHANGELOG # # Version Date Programmer Subroutine Description of Change # 1.00 28.08.2014 Sailor All Initial Release for collaborative programming work # 1.01 13.10.2014 Furban km200_Define Correcting "if (int(@a) == 6))" into "if (int(@a) == 6)" # 1.01 13.10.2014 Furban km200_Define Changing "if ($url =~ m/^((\d\d\d[01]\d\d2[0-4]\d25[0-5])\.){3}(\d\d\d[01]\d\d2[0-4]\d25[0-5])$/)" into "if ($url =~ m/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/)" # 1.02 20.10.2014 Sailor km200_Encrypt Swapping over to Crypt::Rijndael # 1.02 20.10.2014 Sailor km200_Decrypt Swapping over to Crypt::Rijndael # 1.03 21.10.2014 Sailor All Improving log3 functions for debugging # 1.04 22.10.2014 Sailor All New function for get status and implementing export to Readings # 1.05 23.10.2014 Sailor km200_Define Minimum interval changed to 20s since polling procedure lasts about 10s # 1.05 23.10.2014 Sailor km200_Get New subroutine to receive individual data adhoc # 1.06 25.10.2014 Sailor km200_Set First try # 1.07 26.10.2014 Nobody0472 ALL Add FailSafe & Error Handling + KM50 + Interval in Definition # 1.08 27.10.2014 Sailor km200_GetData Trying out whether "my $options = HTTP::Headers->new("Accept" => "application/json","User-Agent" => "TeleHeater/2.2.3", "agent" => "TeleHeater/2.2.3");" is the same as "$ua->agent('TeleHeater/2.2.3');" # 1.08 27.10.2014 Sailor km200_Define Lot's of commenting in order to improve readability :-) # 1.08 27.10.2014 Sailor km200_CompleteDataInit Improvement of console output for easier debugging # 1.08 27.10.2014 Sailor =pod First Issue of description added # 1.09 27.10.2014 Sailor km200_GetData Try-out Failed and original code re enabled for: "Trying out whether "my $options = HTTP::Headers->new("Accept" => "application/json","User-Agent" => "TeleHeater/2.2.3", "agent" => "TeleHeater/2.2.3");" is the same as "$ua->agent('TeleHeater/2.2.3');"" # 1.09 27.10.2014 Nobody0472 km200_Attr First Issue # 1.09 27.10.2014 Sailor km200_Attr Adapted to double interval attributes "IntervalDynVal" and "IntervalStatVal" # 1.09 27.10.2014 Sailor km200_Define Adapted to double interval attributes "IntervalDynVal" and "IntervalStatVal" and deleted interval of being imported from the define line. # 1.09 27.10.2014 Sailor km200_Define Created list of known static services # 1.09 27.10.2014 Sailor km200_Define Calculated a list of responding services which are not static = responding dynamic services # 1.09 27.10.2014 Sailor km200_CompleteDynData Subroutine km200_CompleteData renamed to "km200_CompleteDynData" and only downloading responding dynamic services # 1.09 27.10.2014 Sailor km200_CompleteStatData Subroutine "km200_CompleteStatData" created only downloading responding static services # 1.10 28.10.2014 Sailor km200_Define Attribute check moved to km200_Attr # 1.10 28.10.2014 Sailor km200_Attr Attribute check included # 1.10 28.10.2014 Sailor All Clear-ups for comments and unused debug - print commands # 1.10 28.10.2014 Sailor km200_Define Decoding of passwords with base64 implemented to avoid bare passwords in fhem.cfg # 1.11 31.10.2014 Sailor km200_Define Added "/heatingCircuits/hc2" and subordinates # 1.11 31.10.2014 Sailor km200_CompleteDynData First try-outs with fork() command - FAILED! All fork() commands deleted # 1.12 04.11.2014 Sailor ----------- Nearly all subroutines renamed! ----------------------------------------------------------------------------------------------------------------------- # 1.12 04.11.2014 Sailor All Integration of HttpUtils_NonblockingGet(). Nearly all Subroutines renamed due to new functionality # 1.13 05.11.2014 Sailor All Clearing up "print" and "log" command # 1.13 05.11.2014 Sailor km200_ParseHttpResponseInit encode_utf8() command added to avoid crashes with "/heatSources/flameCurrent" or "/system/appliance/flameCurrent" # 1.13 05.11.2014 Sailor km200_ParseHttpResponseDyn encode_utf8() command added to avoid crashes with "/heatSources/flameCurrent" or "/system/appliance/flameCurrent" # 1.13 05.11.2014 Sailor km200_ParseHttpResponseStat encode_utf8() command added to avoid crashes with "/heatSources/flameCurrent" or "/system/appliance/flameCurrent" # 1.13 05.11.2014 Sailor km200_GetSingleService New function used to obtain single value: HttpUtils_BlockingGet() # 1.13 05.11.2014 Sailor km200_PostSingleService New function used to write single value: HttpUtils_BlockingGet() (Yes, even called "get" in the end, it allows to write (POST) as well # 1.14 06.11.2014 Sailor km200_PostSingleService Set value works! But only if no background download of dynamic or static values is active the same time # 1.15 06.11.2014 Sailor km200_Get Creating proper hash out of array of responding services # 1.15 07.11.2014 Sailor km200_Set Creating proper hash out of array of writeable services # 1.15 07.11.2014 Sailor km200_Initialize Adding DbLog_splitFn # 1.15 07.11.2014 Sailor km200_DbLog_splitFn First try-out. print function temporarily disabled # 1.15 07.11.2014 Sailor All Adding some print-commands for debugging # 1.15 07.11.2014 Sailor km200_Set Starting with blocking flags # 1.16 09.11.2014 Sailor km200_GetDynService Additional Log for debugging added # 1.16 09.11.2014 Sailor km200_GetStatService Additional Log for debugging added # 1.17 10.11.2014 Sailor km200_Initialize Corrected DbLog entry to $hash->{DbLog_splitFn} = "km200_DbLog_splitFn"; # 1.17 10.11.2014 Sailor km200_Define Password logging deleted. Too dangerous as soon a user posts its log seeking for help since gateway password cannot be changed # 1.17 10.11.2014 Sailor km200_Define Adding $hash->{DELAYDYNVAL} and $hash->{DELAYSTATVAL} to delay start of timer # 1.17 10.11.2014 Sailor km200_Attr Adding $hash->{DELAYDYNVAL} and $hash->{DELAYSTATVAL} to delay start of timer # 1.17 10.11.2014 Sailor km200_PostSingleService Error handling # 1.17 10.11.2014 Sailor km200_GetSingleService Error handling # 1.17 10.11.2014 Sailor km200_Define Added additional STATE information # 1.17 10.11.2014 Sailor km200_GetDynService Added additional STATE information # 1.17 10.11.2014 Sailor km200_GetStatService Added additional STATE information # 1.18 10.11.2014 Sailor km200_Initialize Implementing polling time-out attribute in $hash # 1.18 10.11.2014 Sailor km200_Define Implementing polling time-out attribute in $hash # 1.18 10.11.2014 Sailor km200_Attr Implementing polling time-out attribute in $hash # 1.18 10.11.2014 Sailor km200_PostSingleService Implementing polling time-out attribute in $hash # 1.18 10.11.2014 Sailor km200_GetSingleService Implementing polling time-out attribute in $hash # 1.18 10.11.2014 Sailor km200_GetInitService Implementing polling time-out attribute in $hash # 1.18 10.11.2014 Sailor km200_GetDynService Implementing polling time-out attribute in $hash # 1.18 10.11.2014 Sailor km200_GetStatService Implementing polling time-out attribute in $hash # 1.19 14.11.2014 Sailor km200_GetInitService Log Level for data not being available downgraded to Level 4 # 1.19 14.11.2014 Sailor km200_GetSingleService Log Level for data not being available downgraded to Level 4 # 1.19 14.11.2014 Sailor =pod Description updated # 1.20 14.11.2014 Sailor All Implement Console-Printouts only if attribute "ConsoleMessage" is set # 1.21 09.12.2014 Sailor km200_GetDynService Catching JSON parsing errors in order to prevent fhem crashes # 1.21 09.12.2014 Sailor km200_GetStatService Catching JSON parsing errors in order to prevent fhem crashes # 1.21 09.12.2014 Sailor km200_GetInitService Catching JSON parsing errors in order to prevent fhem crashes # 1.21 09.12.2014 Sailor km200_GetSingleService Catching JSON parsing errors in order to prevent fhem crashes # 1.22 09.12.2014 Sailor All Small format corrections # 1.23 12.12.2014 Sailor km200_Define Swapping service for test of communication from "/system/brand" to "/gateway/DateTime" # 1.24 12.12.2014 Sailor km200_Initialize Deactivating $hash->{DbLog_splitFn} # 1.25 07.01.2015 Sailor Comments Updating comments based on created WIKI # 1.25 07.01.2015 Sailor km200_Attr BugFix around ConsoleMessage Attribute # 1.25 07.01.2015 Sailor All Log Message of verbose level 2 improved # 1.25 07.01.2015 Sailor =pod Bugfix to be joined in commandref ######################################################################################################################## ######################################################################################################################## # List of open Problems: # # *Many values in readings make no sense at all. # SOLUTION: Manual disable values by user attribute that are of no use. # # *DbLog: X_DbLog_splitFn not completely implemented in order to hand over the units of the readings to DbLog database # ######################################################################################################################## package main; use strict; use warnings; use Blocking; use Time::HiRes qw(gettimeofday sleep); use Digest::MD5 qw(md5 md5_hex md5_base64); use base qw( Exporter ); use List::MoreUtils qw(first_index); use MIME::Base64; use LWP::UserAgent; use JSON; use Crypt::Rijndael; use HttpUtils; use Encode; use constant false => 0; use constant true => 1; sub km200_Define($$); sub km200_Undefine($$); ###START###### Initialize module ##############################################################################START#### sub km200_Initialize($) { my ($hash) = @_; $hash->{STATE} = "Init"; $hash->{DefFn} = "km200_Define"; $hash->{UndefFn} = "km200_Undefine"; $hash->{SetFn} = "km200_Set"; $hash->{GetFn} = "km200_Get"; $hash->{AttrFn} = "km200_Attr"; # $hash->{DbLog_splitFn} = "km200_DbLog_split"; $hash->{AttrList} = "do_not_notify:1,0 " . "loglevel:0,1,2,3,4,5,6 " . "IntervalDynVal " . "IntervalStatVal " . "PollingTimeout " . "ConsoleMessage " . $readingFnAttributes; } ####END####### Initialize module ###############################################################################END##### ###START###### Activate module after module has been used via fhem command "define" ##########################START#### sub km200_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $name = $a[0]; #$a[1] just contains the "km200" module name and we already know that! :-) my $url = $a[2]; my $km200_gateway_password = $a[3]; my $km200_private_password = $a[4]; my $ModuleVersion = "v1.25"; $hash->{NAME} = $name; $hash->{STATE} = "define"; Log3 $name, 5, $name. " : km200 - Starting to define module with version: " . $ModuleVersion; ###START###### Define provided services of gateway ########################################################START#### my @KM200_AllServices = ( "/", "/gateway", "/gateway/DateTime", "/gateway/instAccess", "/gateway/instWriteAccess", "/gateway/uuid", "/gateway/versionFirmware", "/gateway/versionHardware", "/heatingCircuits", "/heatingCircuits/hc1", "/heatingCircuits/hc1/activeSwitchProgram", "/heatingCircuits/hc1/actualSupplyTemperature", "/heatingCircuits/hc1/controlType", "/heatingCircuits/hc1/currentOpModeInfo", "/heatingCircuits/hc1/currentRoomSetpoint", "/heatingCircuits/hc1/designTemp", "/heatingCircuits/hc1/fastHeatupFactor", "/heatingCircuits/hc1/heatCurveMax", "/heatingCircuits/hc1/heatCurveMin", "/heatingCircuits/hc1/manualRoomSetpoint", "/heatingCircuits/hc1/operationMode", "/heatingCircuits/hc1/pumpModulation", "/heatingCircuits/hc1/roomInfluence", "/heatingCircuits/hc1/roomtemperature", "/heatingCircuits/hc1/roomTempOffset", "/heatingCircuits/hc1/setpointOptimization", "/heatingCircuits/hc1/solarInfluence", "/heatingCircuits/hc1/status", "/heatingCircuits/hc1/suWiSwitchMode", "/heatingCircuits/hc1/suWiThreshold", "/heatingCircuits/hc1/switchPrograms", "/heatingCircuits/hc1/temperatureLevels", "/heatingCircuits/hc1/temperatureRoomSetpoint", "/heatingCircuits/hc1/temporaryRoomSetpoint", "/heatingCircuits/hc2", "/heatingCircuits/hc2/activeSwitchProgram", "/heatingCircuits/hc2/actualSupplyTemperature", "/heatingCircuits/hc2/controlType", "/heatingCircuits/hc2/currentOpModeInfo", "/heatingCircuits/hc2/currentRoomSetpoint", "/heatingCircuits/hc2/designTemp", "/heatingCircuits/hc2/fastHeatupFactor", "/heatingCircuits/hc2/heatCurveMax", "/heatingCircuits/hc2/heatCurveMin", "/heatingCircuits/hc2/manualRoomSetpoint", "/heatingCircuits/hc2/operationMode", "/heatingCircuits/hc2/pumpModulation", "/heatingCircuits/hc2/roomInfluence", "/heatingCircuits/hc2/roomtemperature", "/heatingCircuits/hc2/roomTempOffset", "/heatingCircuits/hc2/setpointOptimization", "/heatingCircuits/hc2/solarInfluence", "/heatingCircuits/hc2/status", "/heatingCircuits/hc2/suWiSwitchMode", "/heatingCircuits/hc2/suWiThreshold", "/heatingCircuits/hc2/switchPrograms", "/heatingCircuits/hc2/temperatureLevels", "/heatingCircuits/hc2/temperatureRoomSetpoint", "/heatingCircuits/hc2/temporaryRoomSetpoint", "/heatSources", "/heatSources/actualCHPower", "/heatSources/actualDHWPower", "/heatSources/actualPower", "/heatSources/actualsupplytemperature", "/heatSources/ChimneySweeper", "/heatSources/CHpumpModulation", "/heatSources/flameCurrent", "/heatSources/flameStatus", "/heatSources/gasAirPressure", "/heatSources/nominalCHPower", "/heatSources/nominalDHWPower", "/heatSources/numberOfStarts", "/heatSources/powerSetpoint", "/heatSources/powerSetpoint", "/heatSources/returnTemperature", "/heatSources/systemPressure", "/heatSources/workingTime", "/heatSources/hs1/energyReservoir", "/heatSources/hs1/reservoirAlert", "/heatSources/hs1/nominalFuelConsumption", "/heatSources/hs1/fuelConsmptCorrFactor", "/heatSources/hs1/actualModulation", "/heatSources/hs1/actualPower", "/heatSources/hs1/fuel", "/notifications", "/recordings", "/recordings/heatingCircuits", "/recordings/heatingCircuits/hc1", "/recordings/heatingCircuits/hc1/roomtemperature", "/recordings/heatSources", "/recordings/heatSources/actualCHPower", "/recordings/heatSources/actualDHWPower", "/recordings/heatSources/actualPower", "/recordings/system", "/recordings/system/heatSources", "/recordings/system/heatSources/hs1", "/recordings/system/heatSources/hs1/actualPower", "/recordings/system/sensors", "/recordings/system/sensors/temperatures", "/recordings/system/sensors/temperatures/outdoor_t1", "/solarCircuits", "/solarCircuits/sc1/", "/solarCircuits/sc1/collectorTemperature", "/solarCircuits/sc1/pumpModulation", "/solarCircuits/sc1/solarYield", "/solarCircuits/sc1/status", "/system", "/system/appliance", "/system/appliance/actualPower", "/system/appliance/actualSupplyTemperature", "/system/appliance/ChimneySweeper", "/system/appliance/CHpumpModulation", "/system/appliance/flameCurrent", "/system/appliance/gasAirPressure", "/system/appliance/nominalBurnerLoad", "/system/appliance/numberOfStarts", "/system/appliance/powerSetpoint", "/system/appliance/systemPressure", "/system/appliance/workingTime", "/system/brand", "/system/bus", "/system/healthStatus", "/system/heatSources/", "/system/heatSources/hs1", "/system/heatSources/hs1/actualModulation", "/system/heatSources/hs1/actualPower", "/system/heatSources/hs1/energyReservoir", "/system/heatSources/hs1/fuel", "/system/heatSources/hs1/fuel/density", "/system/heatSources/hs1/fuelConsmptCorrFactor", "/system/heatSources/hs1/nominalFuelConsumption", "/system/heatSources/hs1/reservoirAlert", "/system/info", "/system/minOutdoorTemp", "/system/sensors", "/system/sensors/temperatures", "/system/sensors/temperatures/chimney", "/system/sensors/temperatures/hotWater_t1", "/system/sensors/temperatures/hotWater_t2", "/system/sensors/temperatures/outdoor_t1", "/system/sensors/temperatures/return", "/system/sensors/temperatures/supply_t1", "/system/sensors/temperatures/supply_t1_setpoint", "/system/sensors/temperatures/switch", "/system/systemType" ); my @KM200_StatServices = ( "/gateway/uuid", "/gateway/instAccess", "/gateway/versionFirmware", "/gateway/versionHardware", "/system/brand", "/system/bus", "/system/info", "/system/systemType" ); ####END####### Define provided services of gateway #########################################################END##### ###START### Check whether all variables are available #####################################################START#### if (int(@a) == 5) { ###START### Check whether IPv4 address is valid if ($url =~ m/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/) { Log3 $name, 5, $name. " : km200 - IPv4-address is valid : " . $url; } else { return $name .": Error - IPv4 address is not valid \n Please use \"define km200 \" instead"; } ####END#### Check whether IPv4 address is valid ###START### Check whether gateway password is base64 encoded or bare, has the right length and delete "-" if required my $PasswordEncrypted = false; my $EvalPassWord = $km200_gateway_password; $EvalPassWord =~ tr/-//d; if ( length($EvalPassWord) == 16) { $km200_gateway_password = $EvalPassWord; Log3 $name, 5, $name. " : km200 - Provided GatewayPassword provided as bareword has the correct length at least."; } else # Check whether the password is eventually base64 encoded { # Remove additional encoding with base64 my $decryptData = decode_base64($km200_gateway_password); $decryptData =~ tr/-//d; $decryptData =~ s/\r|\n//g; if ( length($decryptData) == 16) { $km200_gateway_password = $decryptData; $PasswordEncrypted = true; Log3 $name, 5, $name. " : km200 - Provided GatewayPassword encoded with base64 has the correct length at least."; } else { return $name .": Error - GatewayPassword does not have the correct length.\n". " Please enter gateway password in the format of \"aaaabbbbccccdddd\" or \"aaaa-bbbb-cccc-dddd\"\n". " You may encode your password with base64 first, in order to prevent bare passwords in fhem.cfg.\n". " If you choose to encrypt your gateway password with base64, you also must encrypt your private password the same way\n"; Log3 $name, 3, $name. " : km200 - Provided Gateway Password does not follow the specifications"; } } ####END#### Check whether gateway password has the right length and delete "-" if required ###START### Check whether private password is available and decode it with base64 if encoding is used if ($PasswordEncrypted == true) { my $decryptData = decode_base64($km200_private_password); $decryptData =~ s/\r|\n//g; if (length($decryptData) > 0) { $km200_private_password = $decryptData; Log3 $name, 5, $name. " : km200 - Provided PrivatePassword exists at least"; } else { return $name .": Error - PrivatePassword does not have the minimum length.\n". " You may encode your password with base64 first, in order to prevent bare passwords in fhem.cfg.\n". " If you choose to encrypt your private password with base64, you also must encrypt your gateway password the same way\n"; } } else # If private password is provided as bare word { if (length($km200_private_password) > 0) { Log3 $name, 5, $name. " : km200 - Provided PrivatePassword exists at least"; } else { return $name .": Error - PrivatePassword has not been provided.\n". " You may encode your password with base64 first, in order to prevent bare passwords in fhem.cfg.\n". " If you choose to encrypt your private password with base64, you also must encrypt your gateway password the same way\n"; Log3 $name, 3, $name. " : km200 - Provided Private Password does not follow the specifications"; } } ####END#### Check whether private password is available and decode it with base64 if encoding is used } else { return $name .": km200 - Error - Not enough parameter provided." . "\n" . "Gateway IPv4 address, Gateway and Private Passwords must be provided" ."\n". "Please use \"define km200 \" instead"; } ####END#### Check whether all variables are available ######################################################END##### ###START###### Create the secret SALT of the MD5-Hash for AES-encoding ####################################START#### my $Buderus_MD5Salt = pack( 'C*', 0x86, 0x78, 0x45, 0xe9, 0x7c, 0x4e, 0x29, 0xdc, 0xe5, 0x22, 0xb9, 0xa7, 0xd3, 0xa3, 0xe0, 0x7b, 0x15, 0x2b, 0xff, 0xad, 0xdd, 0xbe, 0xd7, 0xf5, 0xff, 0xd8, 0x42, 0xe9, 0x89, 0x5a, 0xd1, 0xe4 ); ####END####### Create the secret SALT of the MD5-Hash for AES-encoding #####################################END##### ###START###### Create keys with MD5 #######################################################################START#### # Copy Salt my $km200_crypt_md5_salt = $Buderus_MD5Salt; # First half of the key: MD5 of (km200GatePassword . Salt) my $key_1 = md5($km200_gateway_password . $km200_crypt_md5_salt); # Second half of the key: - Initial: MD5 of ( Salt) my $key_2_initial = md5($km200_crypt_md5_salt); # Second half of the key: - private: MD5 of ( Salt . km200PrivatePassword) my $key_2_private = md5($km200_crypt_md5_salt . $km200_private_password); # Create keys my $km200_crypt_key_initial = ($key_1 . $key_2_initial); my $km200_crypt_key_private = ($key_1 . $key_2_private); ####END####### Create keys with MD5 #########################################################################END##### ###START###### Writing values to global hash ###############################################################START#### $hash->{NAME} = $name; $hash->{URL} = $url; $hash->{VERSION} = $ModuleVersion; $hash->{INTERVALDYNVAL} = 60; $hash->{DELAYDYNVAL} = 60; $hash->{INTERVALSTATVAL} = 3600; $hash->{DELAYSTATVAL} = 120; $hash->{DISABLESTATVALPOLLING} = false; $hash->{POLLINGTIMEOUT} = 5; $hash->{CONSOLEMESSAGE} = false; $hash->{temp}{ServiceCounterInit} = 0; $hash->{temp}{ServiceCounterDyn} = 0; $hash->{temp}{ServiceCounterStat} = 0; $hash->{status}{FlagInitRequest} = false; $hash->{status}{FlagGetRequest} = false; $hash->{status}{FlagSetRequest} = false; $hash->{status}{FlagDynRequest} = false; $hash->{status}{FlagStatRequest} = false; $hash->{Secret}{CRYPTKEYPRIVATE} = $km200_crypt_key_private; $hash->{Secret}{CRYPTKEYINITIAL} = $km200_crypt_key_initial; @{$hash->{Secret}{KM200ALLSERVICES}} = @KM200_AllServices; @{$hash->{Secret}{KM200STATSERVICES}} = @KM200_StatServices; @{$hash->{Secret}{KM200RESPONDINGSERVICES}} = (); @{$hash->{Secret}{KM200WRITEABLESERVICES}} = (); ####END####### Writing values to global hash ################################################################END##### ###START###### For Debugging purpose only ##################################################################START#### Log3 $name, 5, $name. " : km200 - Define H : " .$hash; Log3 $name, 5, $name. " : km200 - Define D : " .$def; Log3 $name, 5, $name. " : km200 - Define A : " .@a; Log3 $name, 5, $name. " : km200 - Define Name : " .$name; Log3 $name, 5, $name. " : km200 - Define Adr : " .$url; ####END####### For Debugging purpose only ###################################################################END##### ###START###### Check whether communication to the physical unit is possible ################################START#### my $Km200Info =""; $hash->{temp}{service} = "/gateway/DateTime"; $Km200Info = km200_GetSingleService($hash); if ($Km200Info eq "ERROR") { $Km200Info = $hash->{temp}{TransferValue}; $hash->{temp}{TransferValue} = ""; } if ($Km200Info eq "ERROR") { ## Communication with Gateway WRONG !! ## $hash->{STATE}="Error - No Communication"; return ($name .": km200 - ERROR - The communication between fhem and the Buderus KM200 failed! \n". " Please check physical connection, IP-address and passwords! \n"); } elsif ($Km200Info eq "SERVICE NOT AVAILABLE") { Log3 $name, 5, $name. " : km200 - /gateway/DateTime : NOT AVAILABLE"; ## Communication OK but service not available ## } else { Log3 $name, 5, $name. " : km200 - /gateway/DateTime : " .$Km200Info->{value}; ## Communication OK and service is available ## } ####END####### Check whether communication to the physical unit is possible ################################END##### ###START###### Get complete set of responding services and first time readings ############################START#### if ($hash->{CONSOLEMESSAGE} == true) {print("\n");} km200_GetInitService($hash); ####END####### Get complete set of responding services and first time readings #############################END##### ###START###### Initiate the timer for continuous polling of dynamical values from KM200 but wait 60s ######START#### InternalTimer(gettimeofday()+$hash->{DELAYDYNVAL}, "km200_GetDynService", $hash, 0); Log3 $name, 5, $name. " : km200 - Internal timer for dynamic values prepared with default interval."; ####END####### Initiate the timer for continuous polling of dynamical values from KM200 ####################END##### ###START###### Initiate the timer for continuous polling of static values from KM200 but wait 120s ########START#### InternalTimer(gettimeofday()+$hash->{DELAYSTATVAL}, "km200_GetStatService", $hash, 0); Log3 $name, 5, $name. " : km200 - Internal timer for static values prepared with default interval."; ####END####### Initiate the timer for continuous polling of static values from KM200 #######################END##### ###START###### Set status of km200 fhem module to Initialized #############################################START#### $hash->{STATE}="Initialized"; ####END####### Set status of km200 fhem module to Initialized ##############################################END##### return undef; } ####END####### Activate module after module has been used via fhem command "define" ############################END##### ###START###### To bind unit of value to DbLog entries #########################################################START#### ### This Function needs to be completed later sub km200_DbLog_split($) { my ($event) = @_; my ($reading, $value, $unit); # print("km200_DbLog_splitFn - Test for event:" . $event ."\n"); return ($reading, $value, $unit); } ####END####### To bind unit of value to DbLog entries ##########################################################END##### ###START###### Deactivate module module after "undefine" command by fhem ######################################START#### sub km200_Undefine($$) { my ($hash, $def) = @_; my $name = $hash->{NAME}; my $url = $hash->{URL}; # Stop the internal timer for this module RemoveInternalTimer($hash); Log3 $name, 3, $name. " - km200 has been undefined. The KM unit at $url will no longer polled."; return undef; } ####END####### Deactivate module module after "undefine" command by fhem #######################################END##### ###START###### Handle attributes after changes via fhem GUI ###################################################START#### sub km200_Attr(@) { my @a = @_; my $name = $a[1]; my $hash = $defs{$name}; my $DisableStatValPolling = $hash->{DISABLESTATVALPOLLING}; my $IntervalDynVal = $hash->{INTERVALDYNVAL}; my $IntervalStatVal = $hash->{INTERVALSTATVAL}; my $DelayDynVal = $hash->{DELAYDYNVAL}; my $DelayStatVal = $hash->{DELAYSTATVAL}; ### Check whether dynamic interval attribute has been provided if($a[2] eq "IntervalDynVal") { $IntervalDynVal = $a[3]; ###START### Check whether polling interval is not too short if ($IntervalDynVal > 19) { $hash->{INTERVALDYNVAL} = $IntervalDynVal; Log3 $name, 5, $name. " : km200 - IntervalDynVal set to attribute value:" . $IntervalDynVal ." s"; } else { return $name .": Error - Gateway interval for IntervalDynVal too small - server response time longer than defined interval, please use something >=20, default is 90"; } ####END#### Check whether polling interval is not too short } ### Check whether static interval attribute has been provided elsif($a[2] eq "IntervalStatVal") { $IntervalStatVal = $a[3]; if ($IntervalStatVal == 0) ### Check whether statical values supposed to be polled at all. The attribute "IntervalStatVal" set to "0" means no polling for statical values. { $DisableStatValPolling = true; $hash->{DISABLESTATVALPOLLING} = $DisableStatValPolling; Log3 $name, 5, $name. " : km200 - Polling for static values diabled"; } else { ###START### Check whether polling interval is not too short if ($IntervalStatVal > 19) { $DisableStatValPolling = false; $hash->{INTERVALSTATVAL} = $IntervalStatVal; Log3 $name, 5, $name. " : km200 - IntervalStatVal set to attribute value:" . $IntervalStatVal ." s"; } else { return $name .": Error - Gateway interval for IntervalStatVal too small - server response time longer than defined interval, please use something >=20, default is 3600"; } ####END#### Check whether polling interval is not too short } } ### Check whether room attribute have been provided elsif($a[2] eq "room") { ### Do nothing } ### Check whether polling timeout attribute has been provided elsif($a[2] eq "PollingTimeout") { ###START### Check whether timeout is not too short if ($a[3] >= 5) { $hash->{POLLINGTIMEOUT} = $a[3]; Log3 $name, 5, $name. " : km200 - Polling timeout set to attribute value:" . $a[3] ." s"; } else { Log3 $name, 5, $name. " : km200 - Error - Gateway polling timeout attribute too small: " . $a[3] ." s"; return $name .": Error - Gateway polling timeout attribute is too small - server response time is 5s minimum, default is 5"; } ####END#### Check whether timeout is not too short } ### Check whether console printout attribute has been provided elsif($a[2] eq "ConsoleMessage") { ### If messages on console shall be visible if ($a[3] == 1) { $hash->{CONSOLEMESSAGE} = true; Log3 $name, 5, $name. " : km200 - Console printouts enabled"; } ### If messages on console shall NOT be visible else { $hash->{CONSOLEMESSAGE} = false; Log3 $name, 5, $name. " : km200 - Console printouts disabled"; } } ### If no attributes of the above known ones have been selected else { Log3 $name, 5, $name. " : km200 - Unknown attribute: $a[2]"; } ###START### Stop the current timer RemoveInternalTimer($hash); ####END#### Stop the current timer ###START###### Initiate the timer for continuous polling of dynamical values from KM200 ###################START#### InternalTimer(gettimeofday()+$DelayDynVal, "km200_GetDynService", $hash, 0); Log3 $name, 4, $name. " : km200 - Define: InternalTimer for dynamic values started with interval of: ".$IntervalDynVal; ####END####### Initiate the timer for continuous polling of dynamical values from KM200 ####################END##### ###START###### Initiate the timer for continuous polling of static values from KM200 ######################START#### if ($DisableStatValPolling == false) { InternalTimer(gettimeofday()+$DelayStatVal, "km200_GetStatService", $hash, 0); Log3 $name, 4, $name. " : km200 - Define: InternalTimer for static values started with interval of: ".$IntervalStatVal; } else { Log3 $name, 4, $name. " : km200 - Define: No InternalTimer for static values since polling disabled by \"attr IntervalStatVal 0\" in fhem.cfg "; } ####END####### Initiate the timer for continuous polling of static values from KM200 #######################END##### return undef; } ####END####### Handle attributes after changes via fhem GUI ####################################################END##### ###START###### Obtain value after "get" command by fhem #######################################################START#### sub km200_Get($@) { my ( $hash, @a ) = @_; ### If not enough arguments have been provided if ( @a < 2 ) { return "\"get km200\" needs at least one argument"; } my $name = shift @a; my $opt = shift @a; my %km200_gets; my $ReturnValue; ### Get the list of possible services and create a hash out of it my @RespondingServices = @{$hash->{Secret}{KM200RESPONDINGSERVICES}}; foreach my $item(@RespondingServices) { $km200_gets{$item} = ("1"); } ### If service chosen in GUI does not exist if(!$km200_gets{$opt}) { my @cList = keys %km200_gets; return "Unknown argument $opt, choose one of " . join(" ", @cList); } ### Save chosen service into hash $hash->{temp}{service} = $opt; ### Block other scheduled and unscheduled routines $hash->{status}{FlagGetRequest} = true; ### Console outputs for debugging purposes if ($hash->{CONSOLEMESSAGE} == true) {print("Obtaining value of $opt \n");} ### Read service-hash my $ReturnHash = km200_GetSingleService($hash); ### De-block other scheduled and unscheduled routines $hash->{status}{FlagGetRequest} = false; ### Extract value $ReturnValue = $ReturnHash->{value}; ### Delete temporary value $hash->{temp}{service} = ""; ### Return value return($ReturnValue); } ####END####### Obtain value after "get" command by fhem ########################################################END##### ###START###### Manipulate service after "set" command by fhem #################################################START#### sub km200_Set($@) { my ( $hash, @a ) = @_; ### If not enough arguments have been provided if ( @a < 2 ) { return "\"set km200\" needs at least one argument"; } my $name = shift @a; my $opt = shift @a; my $value = join("", @a); my %km200_sets; ### Get the list of possible services and create a hash out of it my @WriteableServices = @{$hash->{Secret}{KM200WRITEABLESERVICES}}; foreach my $item(@WriteableServices) { $km200_sets{$item} = ("1"); } ### If service chosen in GUI does not exist if(!$km200_sets{$opt}) { my @cList = keys %km200_sets; return "Unknown argument $opt, choose one of " . join(" ", @cList); } ### Save chosen service and value into hash $hash->{temp}{service} = $opt; $hash->{temp}{postdata} = $value; ### Console outputs for debugging purposes if ($hash->{CONSOLEMESSAGE} == true) {print("Writing $opt with value: $value\n");} ### Call set sub km200_PostSingleService($hash); km200_PostSingleService($hash); ### Return value my $ReturnValue = "The service " . $opt . " has been changed to: " . $value; return($ReturnValue); } ####END####### Manipulate service after "Set" command by fhem ##################################################END##### ###START####### Repeats "string" for "count" times ############################################################START#### sub str_repeat($$) { my $string = $_[0]; my $count = $_[1]; return(${string}x${count}); } ####END######## Repeats "string" for "count" times #############################################################END##### ###START###### Subroutine Encrypt Data ########################################################################START#### sub km200_Encrypt($) { my ($hash, $def) = @_; my $km200_crypt_key_private = $hash->{Secret}{CRYPTKEYPRIVATE}; my $name = $hash->{NAME}; my $encryptData = $hash->{temp}{jsoncontent}; # Create Rijndael encryption object my $cipher = Crypt::Rijndael->new($km200_crypt_key_private, Crypt::Rijndael::MODE_ECB() ); # Get blocksize and add PKCS #7 padding my $blocksize = $cipher->blocksize(); my $encrypt_padchar = $blocksize - ( length( $encryptData ) % $blocksize ); $encryptData .= str_repeat( chr( $encrypt_padchar ), $encrypt_padchar ); # Do the encryption my $ciphertext = $cipher->encrypt( $encryptData ); # Do additional encoding with base64 $ciphertext = encode_base64($ciphertext); # Return the encoded text return($ciphertext); } ####END####### Subroutine Encrypt Data #########################################################################END##### ###START###### Subroutine Decrypt Data ########################################################################START#### sub km200_Decrypt($) { my ($hash, $def) = @_; my $km200_crypt_key_private = $hash->{Secret}{CRYPTKEYPRIVATE}; my $name = $hash->{NAME}; my $decryptData = $hash->{temp}{decodedcontent}; # Remove additional encoding with base64 $decryptData = decode_base64($decryptData); # Create Rijndael decryption object and do the decryption my $cipher = Crypt::Rijndael->new($km200_crypt_key_private, Crypt::Rijndael::MODE_ECB() ); my $deciphertext = $cipher->decrypt( $decryptData ); # Remove zero padding $deciphertext =~ s/\x00+$//; # Remove PKCS #7 padding my $decipher_len = length($deciphertext); my $decipher_padchar = ord(substr($deciphertext,($decipher_len - 1),1)); my $i = 0; for ( $i = 0; $i < $decipher_padchar ; $i++ ) { if ( $decipher_padchar != ord( substr($deciphertext,($decipher_len - $i - 1),1))) { last; } } # Return decrypted text if ( $i != $decipher_padchar ) { ### Log entries for debugging purposes Log3 $name, 5, $name. " : km200 - decryptData1 - decipher_len : " .$decipher_len; $deciphertext =~ s/\x00+$//; Log3 $name, 5, $name. " : km200 - decryptData1 - deciphertext : " .$deciphertext; ### Log entries for debugging purposes return $deciphertext; } else { $deciphertext = substr($deciphertext,0,$decipher_len - $decipher_padchar); ### Log entries for debugging purposes Log3 $name, 5, $name. " : km200 - decryptData2 - decipher_len : " .$decipher_len; $deciphertext =~ s/\x00+$//; Log3 $name, 5, $name. " : km200 - decryptData2 - deciphertext : " .$deciphertext; ### Log entries for debugging purposes return $deciphertext; } } ####END####### Subroutine Decrypt Data #########################################################################END##### ###START###### Subroutine set individual data value ###########################################################START#### sub km200_PostSingleService($) { my ($hash, $def) = @_; my $Service = $hash->{temp}{service}; my $km200_gateway_host = $hash->{URL} ; my $name = $hash->{NAME} ; my $PollingTimeout = $hash->{POLLINGTIMEOUT}; my $err; my $data; my $json; my $JsonContent; my $url ="http://" . $km200_gateway_host . $Service; ### Manipulate the value $json->{value} = $hash->{temp}{postdata}; ### Encode as json $JsonContent = encode_json($json); ### Encrypt $hash->{temp}{jsoncontent} = $JsonContent; $data = km200_Encrypt($hash); ### Create parameter set for HttpUtils_BlockingGet my $param = { url => $url, timeout => $PollingTimeout, data => $data, method => "POST", header => "agent: TeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json", }; ### Write value with HttpUtils_BlockingGet ($err, $data) = HttpUtils_BlockingGet($param); ### If error messsage has been returned if($err ne "") { Log3 $name, 2, $name . " - ERROR: $err"; if ($hash->{CONSOLEMESSAGE} == true) {print("km200_PostSingleService - Error: $err\n");} return "ERROR"; } else { ### ReRead the whole json hash by read-out the value $json = km200_GetSingleService($hash); ### Log entries for debugging purposes Log3 $name, 5, $name. " : km200_PostSingleService : " .$json; ### Log entries for debugging purposes $hash->{status}{FlagSetRequest} = false; return $json; } } ####END####### Subroutine set individual data value ############################################################END##### ###START###### Subroutine get individual data value ###########################################################START#### sub km200_GetSingleService($) { my ($hash, $def) = @_; my $Service = $hash->{temp}{service}; my $km200_gateway_host = $hash->{URL}; my $name = $hash->{NAME}; my $PollingTimeout = $hash->{POLLINGTIMEOUT}; my $err; my $data; my $json; my $url ="http://" . $km200_gateway_host . $Service; my $param = { url => $url, timeout => $PollingTimeout, method => "GET", header => "agent: TeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json", }; ($err, $data) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 2, $name . " : ERROR: Service: ".$Service. ": No proper Communication with Gateway: " .$err; if ($hash->{CONSOLEMESSAGE} == true) {print("km200_GetSingleService ERROR: $err\n");} return "ERROR"; } else { $hash->{temp}{decodedcontent} = $data; my $decodedContent = km200_Decrypt($hash); if ($decodedContent ne "") { eval { $json = decode_json(encode_utf8($decodedContent)); 1; } or do { Log3 $name, 5, $name. " : km200_GetSingleService - Data cannot be parsed by JSON on km200 for http://" . $param->{url}; if ($hash->{CONSOLEMESSAGE} == true) {print("Data not parseable on km200 for " . $param->{url} . "\n");} }; if( exists $json -> {"value"}) { my $JsonId = $json->{id}; my $JsonType = $json->{type}; my $JsonValue = $json->{value}; ### Write reading for fhem readingsSingleUpdate( $hash, $JsonId, $JsonValue, 1); ### Write reading for fhem } else { ### Log entries for debugging purposes Log3 $name, 4, $name. " : km200_GetSingleService - value not found for:" .$Service; ### Log entries for debugging purposes } } else { Log3 $name, 4, $name. " : km200_GetSingleService: ". $Service . " NOT available"; if ($hash->{CONSOLEMESSAGE} == true) {print "The following Service CANNOT be read : $Service \n";} } ### Log entries for debugging purposes Log3 $name, 5, $name. " : km200_GetSingleService : " .$json; ### Log entries for debugging purposes $hash->{status}{FlagGetRequest} = false; return $json; } } ####END####### Subroutine get individual data value ############################################################END##### ###START###### Subroutine initial contact of services via HttpUtils ###########################################START#### sub km200_GetInitService($) { my ($hash, $def) = @_; my $km200_gateway_host = $hash->{URL} ; my $name = $hash->{NAME} ; $hash->{status}{FlagInitRequest} = true; my @KM200_InitServices = @{$hash->{Secret}{KM200ALLSERVICES}}; my $ServiceCounterInit = $hash->{temp}{ServiceCounterInit}; my $PollingTimeout = $hash->{POLLINGTIMEOUT}; my $Service = $KM200_InitServices[$ServiceCounterInit]; my $url ="http://" . $km200_gateway_host . $Service; my $param = { url => $url, timeout => $PollingTimeout, hash => $hash, method => "GET", header => "agent: TeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json", callback => \&km200_ParseHttpResponseInit }; HttpUtils_NonblockingGet($param); } ####END####### Subroutine initial contact of services via HttpUtils ############################################END##### ###START###### Subroutine to download complete initial data set from gateway ##################################START#### # For all existing known services try reading the respective values from gateway sub km200_ParseHttpResponseInit($) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash ->{NAME}; my $ServiceCounterInit = $hash ->{temp}{ServiceCounterInit}; my @KM200_RespondingServices = @{$hash ->{Secret}{KM200RESPONDINGSERVICES}}; my @KM200_WriteableServices = @{$hash ->{Secret}{KM200WRITEABLESERVICES}}; my @KM200_InitServices = @{$hash ->{Secret}{KM200ALLSERVICES}}; my $NumberInitServices = @KM200_InitServices; my $Service = $KM200_InitServices[$ServiceCounterInit]; my $json; ### Log entries for debugging purposes Log3 $name, 5, $name. " : km200_ParseHttpResponseInit: Try to parse : " .$Service; ### Log entries for debugging purposes if($err ne "") { Log3 $name, 2, $name . " : ERROR: Service: ".$Service. ": No proper Communication with Gateway: " .$err; if ($hash->{CONSOLEMESSAGE} == true) {print("km200_ParseHttpResponseInit ERROR: $err\n");} return "ERROR"; } $hash->{temp}{decodedcontent} = $data; my $decodedContent = km200_Decrypt($hash); if ($decodedContent ne "") { eval { $json = decode_json(encode_utf8($decodedContent)); 1; } or do { Log3 $name, 4, $name. " : km200_ParseHttpResponseInit: ". $Service . " CANNOT be parsed"; if ($hash->{CONSOLEMESSAGE} == true) {print "The following Service CANNOT be parsed by JSON : $Service \n";} }; if( exists $json -> {"value"}) { my $JsonId = $json->{id}; my $JsonType = $json->{type}; my $JsonValue = $json->{value}; ### Log entries for debugging purposes Log3 $name, 4, $name. " : km200_ParseHttpResponseInit: value found for : " .$Service; Log3 $name, 5, $name. " : km200_ParseHttpResponseInit: id : " .$JsonId; Log3 $name, 5, $name. " : km200_ParseHttpResponseInit: value : " .$JsonValue; ### Log entries for debugging purposes ### Add service to the list of responding services push (@KM200_RespondingServices, $Service); ### Add service to the list of responding services ### Write reading for fhem readingsSingleUpdate( $hash, $JsonId, $JsonValue, 1); ### Write reading for fhem if ($hash->{CONSOLEMESSAGE} == true) {print "The following Service can be read";} ### Check whether service is writeable and write name of service in array if ($json->{writeable} == 1) { if ($hash->{CONSOLEMESSAGE} == true) {print " and is writeable";} push (@KM200_WriteableServices, $Service); } else { # Do nothing if ($hash->{CONSOLEMESSAGE} == true) {print " ";} } ### Check whether service is writeable and write name of service in array if ($hash->{CONSOLEMESSAGE} == true) {print ": $JsonId \n";} } else { ### Log entries for debugging purposes Log3 $name, 4, $name. " : km200_ParseHttpResponseInit - value not found for:" .$Service; ### Log entries for debugging purposes } } else { Log3 $name, 4, $name. " : km200_ParseHttpResponseInit: ". $Service . " NOT available"; if ($hash->{CONSOLEMESSAGE} == true) {print "The following Service CANNOT be read : $Service \n";} } ### Log entries for debugging purposes Log3 $name, 5, $name. " : km200_ParseHttpResponseInit : response : " .$data; ### Log entries for debugging purposes if ($ServiceCounterInit < ($NumberInitServices-1)) ### If the list of KM200ALLSERVICES has not been finished yet { ++$ServiceCounterInit; $hash->{temp}{ServiceCounterInit} = $ServiceCounterInit; @{$hash->{Secret}{KM200RESPONDINGSERVICES}} = @KM200_RespondingServices; @{$hash->{Secret}{KM200WRITEABLESERVICES}} = @KM200_WriteableServices; km200_GetInitService($hash); } else ### If the list of KM200ALLSERVICES is finished { $hash->{temp}{ServiceCounterInit} = 0; ###START###### Filter all static services out of responsive services = responsive dynamic services ########START#### my @KM200_DynServices = @KM200_RespondingServices; foreach my $SearchWord(@{$hash->{Secret}{KM200STATSERVICES}}) { my $FoundPosition = first_index{ $_ eq $SearchWord }@KM200_DynServices; if ($FoundPosition >= 0) { splice(@KM200_DynServices, $FoundPosition, 1); } } ####END####### Filter all static services out of responsive services = responsive dynamic services #########END##### ### Save arrays of services in hash @{$hash->{Secret}{KM200RESPONDINGSERVICES}} = @KM200_RespondingServices; @{$hash->{Secret}{KM200WRITEABLESERVICES}} = @KM200_WriteableServices; @{$hash->{Secret}{KM200DYNSERVICES}} = @KM200_DynServices; ### Save arrays of services in hash $hash->{status}{FlagInitRequest} = false; } ### Clear up temporary variables $hash->{temp}{decodedcontent} = ""; ### Clear up temporary variables return; } ####END####### Subroutine to download complete initial data set from gateway ###################################END##### ###START###### Subroutine obtaining dynamic services via HttpUtils ##############################################################START#### sub km200_GetDynService($) { my ($hash, $def) = @_; my $km200_gateway_host = $hash->{URL}; my $name = $hash->{NAME}; $hash->{status}{FlagDynRequest} = true; $hash->{STATE} = "Polling"; my @KM200_DynServices = @{$hash->{Secret}{KM200DYNSERVICES}}; my $ServiceCounterDyn = $hash->{temp}{ServiceCounterDyn}; my $PollingTimeout = $hash->{POLLINGTIMEOUT}; if (@KM200_DynServices != 0) { my $Service = $KM200_DynServices[$ServiceCounterDyn]; ### Console outputs for debugging purposes if ($ServiceCounterDyn == 0) { if ($hash->{CONSOLEMESSAGE} == true) {print("Starting download of dynamic services\n");} } if ($hash->{CONSOLEMESSAGE} == true) {print("$Service\n");} ### Console outputs for debugging purposes my $url = "http://" . $km200_gateway_host . $Service; my $param = { url => $url, timeout => $PollingTimeout, hash => $hash, method => "GET", header => "agent: TeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json", callback => \&km200_ParseHttpResponseDyn }; HttpUtils_NonblockingGet($param); } else { Log3 $name, 5, $name . " : No dynamic values available to be read. Skipping download."; if ($hash->{CONSOLEMESSAGE} == true) {print("No dynamic values available to be read. Skipping download.\n")} } } ####END####### Subroutine get dynamic data value ###############################################################END##### ###START###### Subroutine to download complete dynamic data set from gateway ##################################START#### # For all responding dynamic services read the respective values from gateway sub km200_ParseHttpResponseDyn($) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash ->{NAME}; my $ServiceCounterDyn = $hash ->{temp}{ServiceCounterDyn}; my @KM200_DynServices = @{$hash ->{Secret}{KM200DYNSERVICES}}; my $NumberDynServices = @KM200_DynServices; my $Service = $KM200_DynServices[$ServiceCounterDyn]; my $json; Log3 $name, 5, $name. " : Parsing response of dynamic service received for: " . $Service; if($err ne "") { Log3 $name, 2, $name . " : ERROR: Service: ".$Service. ": No proper Communication with Gateway: " .$err; readingsSingleUpdate($hash, "fullResponse", "ERROR", 1); if ($hash->{CONSOLEMESSAGE} == true) {print("km200_ParseHttpResponseDyn ERROR: $err\n");} } $hash->{temp}{decodedcontent} = $data; my $decodedContent = km200_Decrypt($hash); if ($decodedContent ne "") { eval { $json = decode_json(encode_utf8($decodedContent)); 1; } or do { Log3 $name, 5, $name. " : km200_parseHttpResponseDyn - Data cannot be parsed by JSON on km200 for http://" . $param->{url}; if ($hash->{CONSOLEMESSAGE} == true) {print("Data not parseable on km200 for " . $param->{url} . "\n");} }; if( exists $json -> {"value"}) { my $JsonId = $json->{id}; my $JsonType = $json->{type}; my $JsonValue = $json->{value}; ### Log entries for debugging purposes Log3 $name, 4, $name. " : km200_parseHttpResponseDyn: value found for : " .$Service; Log3 $name, 5, $name. " : km200_parseHttpResponseDyn: id : " .$JsonId; Log3 $name, 5, $name. " : km200_parseHttpResponseDyn: value : " .$JsonValue; ### Log entries for debugging purposes ### Write reading readingsSingleUpdate( $hash, $JsonId, $JsonValue, 1); ### Write reading } else { ### Log entries for debugging purposes Log3 $name, 5, $name. " : km200_parseHttpResponseDyn - value not found for:" .$Service; ### Log entries for debugging purposes } } else { Log3 $name, 5, $name. " : km200_parseHttpResponseDyn - Data not available on km200 for http://" . $param->{url}; if ($hash->{CONSOLEMESSAGE} == true) {print("Data not available on km200 for " . $param->{url} . "\n");} } ### Clear up temporary variables $hash->{temp}{decodedcontent} = ""; $hash->{temp}{service} = ""; ### Clear up temporary variables if ($ServiceCounterDyn < ($NumberDynServices-1)) { ++$ServiceCounterDyn; $hash->{temp}{ServiceCounterDyn} = $ServiceCounterDyn; km200_GetDynService($hash); } else { $hash->{STATE} = "Initialized"; $hash->{temp}{ServiceCounterDyn} = 0; if ($hash->{CONSOLEMESSAGE} == true) {print ("Finished\n");} ###START###### Re-Start the timer #####################################START#### InternalTimer(gettimeofday()+$hash->{INTERVALDYNVAL}, "km200_GetDynService", $hash, 1); ####END####### Re-Start the timer ######################################END##### $hash->{status}{FlagDynRequest} = false; } return undef; } ####END####### Subroutine to download complete dynamic data set from gateway ###################################END##### ###START###### Subroutine obtaining static services via HttpUtils #############################################START#### sub km200_GetStatService($) { my ($hash, $def) = @_; my $km200_gateway_host = $hash->{URL}; my $name = $hash->{NAME}; $hash->{status}{FlagStatRequest} = true; $hash->{STATE} = "Polling"; my @KM200_StatServices = @{$hash->{Secret}{KM200STATSERVICES}}; my $ServiceCounterStat = $hash->{temp}{ServiceCounterStat}; my $PollingTimeout = $hash->{POLLINGTIMEOUT}; if (@KM200_StatServices != 0) { my $Service = $KM200_StatServices[$ServiceCounterStat]; ### Console outputs for debugging purposes if ($ServiceCounterStat == 0) { if ($hash->{CONSOLEMESSAGE} == true) {print("Starting download of static services\n");} } if ($hash->{CONSOLEMESSAGE} == true) {print("$Service\n");} ### Console outputs for debugging purposes my $url = "http://" . $km200_gateway_host . $Service; my $param = { url => $url, timeout => $PollingTimeout, hash => $hash, method => "GET", header => "agent: TeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json", callback => \&km200_ParseHttpResponseStat }; HttpUtils_NonblockingGet($param); } else { Log3 $name, 5, $name . " : No static values available to be read. Skipping download."; if ($hash->{CONSOLEMESSAGE} == true) {print("No static values available to be read. Skipping download.\n")} } } ####END####### Subroutine get static data value ################################################################END##### ###START###### Subroutine to download complete static data set from gateway ###################################START#### # For all responding static services read the respective values from gateway sub km200_ParseHttpResponseStat($) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash ->{NAME}; my $ServiceCounterStat = $hash ->{temp}{ServiceCounterStat}; my @KM200_StatServices = @{$hash ->{Secret}{KM200STATSERVICES}}; my $NumberStatServices = @KM200_StatServices; my $Service = $KM200_StatServices[$ServiceCounterStat]; my $json; Log3 $name, 5, $name. " : Parsing response of static service received for: " . $Service; if($err ne "") { Log3 $name, 2, $name . " : ERROR: Service: ".$Service. ": No proper Communication with Gateway: " .$err; readingsSingleUpdate($hash, "fullResponse", "ERROR", 1); if ($hash->{CONSOLEMESSAGE} == true) {print("km200_ParseHttpResponseStat ERROR: $err\n");} } $hash->{temp}{decodedcontent} = $data; my $decodedContent = km200_Decrypt($hash); if ($decodedContent ne "") { eval { $json = decode_json(encode_utf8($decodedContent)); 1; } or do { Log3 $name, 5, $name. " : km200_parseHttpResponseStat - Data cannot be parsed by JSON on km200 for http://" . $param->{url}; if ($hash->{CONSOLEMESSAGE} == true) {print("Data not parseable on km200 for " . $param->{url} . "\n");} }; if( exists $json -> {"value"}) { my $JsonId = $json->{id}; my $JsonType = $json->{type}; my $JsonValue = $json->{value}; ### Log entries for debugging purposes Log3 $name, 4, $name. " : km200_parseHttpResponseStat: value found for : " .$Service; Log3 $name, 5, $name. " : km200_parseHttpResponseStat: id : " .$JsonId; Log3 $name, 5, $name. " : km200_parseHttpResponseStat: value : " .$JsonValue; ### Log entries for debugging purposes ### Write reading readingsSingleUpdate( $hash, $JsonId, $JsonValue, 1); ### Write reading } else { ### Log entries for debugging purposes Log3 $name, 5, $name. " : km200_parseHttpResponseStat - value not found for:" .$Service; ### Log entries for debugging purposes } } else { Log3 $name, 5, $name. " : km200_parseHttpResponseStat - Data not available on km200 for http://" . $param->{url}; } ### Clear up temporary variables $hash->{temp}{decodedcontent} = ""; $hash->{temp}{service} = ""; ### Clear up temporary variables if ($ServiceCounterStat < ($NumberStatServices-1)) { ++$ServiceCounterStat; $hash->{temp}{ServiceCounterStat} = $ServiceCounterStat; km200_GetStatService($hash); } else { $hash->{STATE} = "Initialized"; $hash->{temp}{ServiceCounterStat} = 0; if ($hash->{CONSOLEMESSAGE} == true) {print ("Finished\n");} ###START###### Re-Start the timer #####################################START#### InternalTimer(gettimeofday()+$hash->{INTERVALSTATVAL}, "km200_GetStatService", $hash, 1); ####END####### Re-Start the timer ######################################END##### $hash->{status}{FlagStatRequest} = false; } return undef; } ####END####### Subroutine to download complete static data set from gateway ####################################END##### 1; ###START###### Description for fhem commandref ################################################################START#### =pod =begin html

KM200

    The Buderus KM200 or KM50 is a communication device to establish a connection between the Buderus central heating control unit and the internet.
    It has been designed in order to allow the inhabitants accessing their heating system via his Buderus App EasyControl.
    Furthermore it allows the maintenance companies to access the central heating control system to read and change settings.
    The km200 module enables read/write access to these parameters.

    In order to use the KM200 or KM50 with fhem, you must define the private password with the Buderus App EasyControl first.

    Remark:
    Despite the instruction of the Buderus KM200 Installation guide, the ports 5222 and 5223 should not be opened and allow access to the KM200/KM50 module from outside.
    You should configure (or leave) your internet router with the respective settings.
    If you want to read or change settings on the heating system, you should access the central heating control system via your fhem system only.

    As soon the module has been defined within the fhem.cfg, the module is trying to obtain all known/possible services.
    After this initial contact, the module differs between a set of continuous (dynamically) changing values (e.g.: temperatures) and not changing static values (e.g.: Firmware version).
    This two different set of values can be bound to an individual polling interval. Refer to Attributes

    Define
      define <name> km200 <IPv4-address> <GatewayPassword> <PrivatePassword>
        <name> : The name of the device. Recommendation: "myKm200".
        <IPv4-address> : A valid IPv4 address of the KM200 router. You might look into your browser which DHCP address has been given to the KM200/KM50.
        <GatewayPassword> : The gateway password which is provided on the type sign of the KM200/KM50.
        <PrivatePassword> : The private password which has been defined by the user via EasyControl.

    Set
      The set function is able to change a value of a service which has the "writeable" - tag within the KM200/KM50 service structure.
      Most of those values have an additional list of allowed values which are the only ones to be set.
      Other floatable type values can be changed only within their range of minimum and maximum value.
      set <service> <value>
        <service> : The name of the service which value shall be set. E.g.: "/heatingCircuits/hc1/operationMode"
        <value> : A valid value for this service.

    Get
      The get function is able to obtain a value of a service within the KM200/KM50 service structure.
      The additional list of allowed values or their range of minimum and maximum value will not be handed back.
      get <service>
        <service> : The name of the service which value shall be obtained. E.g.: "/heatingCircuits/hc1/operationMode"
          It returns only the value but not the unit or the range or list of allowed values possible.

    Attributes
      The following user attributes can be used with the km200 module in addition to the global ones e.g. room.
      • IntervalDynVal :
      • A valid polling interval for the dynamically changing values of the KM200/KM50. The value must be >=20s to allow the km200 module to perform a full polling procedure.
        The default value is 90s.
      • IntervalStatVal :
      • A valid polling interval for the statical values of the KM200/KM50. The value must be >=20s to allow the km200 module to perform a full polling procedure.
        The default value is 3600s.
        The value of "0" will disable the polling of statical values until the next fhem restart or a reload of the fhem.cfg - file.
      • PollingTimeout :
      • A valid time in order to allow the module to wait for a response of the KM200/KM50. Usually this value does not need to be changed but might in case of slow network or slow response.
        The default and minimum value is 5s.
      • ConsoleMessage :
      • A valid boolean value whether the activity and error messages shall be displayed in the console window. "0" (deactivated) or "1" (activated)
        The default value 0 (deactivated).