# $Id$ #################################################################################################### # # 98_MaxScanner.pm # The MaxScanner enables FHEM to capture temperature and valve-position of thermostats # in regular intervals # # This module is written by john. # # 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 . # # # 25.10.15 - 1.0.0.0 # initial build # # Task-list # * define minimal Scan-Interval # * define credit threshold # * multiple shutters # * notify for shutter contacts and implicated thermostats # * check text off/on as desi-Temp # # 06.01.16 # * RestartTimer # 09.01.16 # * change: using of instead of NotifyFn explicit notify # * fixed : erreous initial scenario # * new : get associatedDevices # * change. scanTemp substitues scnEnabled # 11.01.16 - 1.0.0.0 # * change: limit logging, when window open detected # 13.01.16 - 1.0.0.1 # * change: FIND, minor changes # 15.01.16 - 1.0.0.2 # * fixed : Work- check of external change of desired was incorrect # 07.03.16 - 1.0.0.3 # * fixed : startup after initialization of fhem #################################################################################################### package main; use strict; use warnings; use Data::Dumper; use vars qw(%defs); use vars qw($readingFnAttributes); use vars qw(%attr); use vars qw(%modules); my $MaxScanner_Version = "1.0.0.3 - 07.03.2016"; my $MaxScanner_ModulName = "MaxScanner"; # minimal poll-rate for thermostat in minutes given by firmware my $MaxScanner_BaseIntervall = 3; my $MaxScanner_DefaultCreditThreshold = 300; # attributes for thermostat instance my $MaxScanner_TXPerMinutes = 32; # transmissions per hour my $MaxScanner_AttrEnabled = 'scanTemp'; my $MaxScanner_AttrShutterList = 'scnShutterList'; my $MaxScanner_AttrProcessByDesiChange = 'scnProcessByDesiChange'; my $MaxScanner_AttrModeHandling = 'scnModeHandling'; # attributes for module instance my $MaxScanner_AttrCreditThreshold = 'scnCreditThreshold'; my $MaxScanner_AttrMinInterval = 'scnMinInterval'; # define user defined attributes my @MaxScanner_AttrForMax = ( #$MaxScanner_AttrEnabled . ':0,1', $MaxScanner_AttrProcessByDesiChange . ':0,1', $MaxScanner_AttrShutterList, $MaxScanner_AttrModeHandling . ':NOCHANGE,AUTO,MANUAL' ); # ########################## # output format: . sub MaxScanner_Log($$$) { my ( $hash, $loglevel, $text ) = @_; my $xline = ( caller(0) )[2]; my $xsubroutine = ( caller(1) )[3]; my $sub = ( split( ':', $xsubroutine ) )[2]; my $ss = $MaxScanner_ModulName . "_"; $sub =~ s/$ss//; my $instName = ( ref($hash) eq "HASH" ) ? $hash->{NAME} : $MaxScanner_ModulName; Log3 $hash, $loglevel, "$MaxScanner_ModulName $instName $sub.$xline " . $text; } ########################## sub MaxScanner_Initialize($) { my ($hash) = @_; $hash->{DefFn} = $MaxScanner_ModulName . '_Define'; $hash->{UndefFn} = $MaxScanner_ModulName . '_Undef'; $hash->{SetFn} = $MaxScanner_ModulName . '_Set'; $hash->{GetFn} = $MaxScanner_ModulName . '_Get'; $hash->{AttrFn} = $MaxScanner_ModulName . '_Attr'; $hash->{NotifyFn} = $MaxScanner_ModulName . '_Notify'; $hash->{AttrList} = $MaxScanner_AttrCreditThreshold . ':150,200,250,300,350,400 ' . $MaxScanner_AttrMinInterval . ':3,6,9,12,15,18,21,24,27,30 ' . 'disable:0,1 ' . $readingFnAttributes; MaxScanner_Log '', 3, "Init Done with Version $MaxScanner_Version"; } ########################## sub MaxScanner_RestartTimer($$) { my ( $hash, $seconds ) = @_; my $name = $hash->{NAME}; $seconds = 1 if ( $seconds <= 0 ); RemoveInternalTimer($name); my $sdNextScan = gettimeofday() + $seconds; InternalTimer( $sdNextScan, $MaxScanner_ModulName . '_Timer', $name, 1 ); } ########################## sub MaxScanner_Define($$$) { my ( $hash, $def ) = @_; my @a = split( "[ \t][ \t]*", $def ); my $name = $a[0]; MaxScanner_Log $hash, 4, "parameters: @a"; if ( @a < 2 ) { return 'wrong syntax: define ' . $MaxScanner_ModulName; } # only one scanner instance is allowed # get the count of instances my @scanners = keys %{ $modules{$MaxScanner_ModulName}{defptr} }; my $scannerCount = @scanners; if ($scannerCount > 0) { return 'only one scanner instance is allowed'; } #. $hash->{helper}{thermostats} = (); $hash->{helper}{initDone} = ''; $hash->{VERSION} = $MaxScanner_Version; # register modul $modules{$MaxScanner_ModulName}{defptr}{$name} = $hash; # create timer RemoveInternalTimer($name); #my $xsub = $MaxScanner_ModulName . "_Timer"; #InternalTimer( gettimeofday() + 20, $xsub, $name, 0 ); # MaxScanner_RestartTimer($hash,20); # MaxScanner_Log $hash, 2, 'timer started'; return undef; } ########################## sub MaxScanner_Undef($$) { my ( $hash, $arg ) = @_; RemoveInternalTimer( $hash->{NAME} ); MaxScanner_Log $hash, 2, "done"; return undef; } ########################### sub MaxScanner_Get($@) { my ( $hash, @a ) = @_; my $name = $hash->{NAME}; my $ret = "Unknown argument $a[1], choose one of associatedDevices:noArg"; my $cmd = lc( $a[1] ); my @carr; MaxScanner_Log $hash, 4, 'cmd:' . $cmd; # check the commands if ( $cmd eq 'associateddevices' ) { if ( defined( $hash->{helper}{associatedDevices} ) ) { @carr = @{ $hash->{helper}{associatedDevices} }; $ret = join( '
', @carr ); } else { $ret = 'no devices'; } } return $ret; } ########################### sub MaxScanner_Set($@) { my ( $hash, @a ) = @_; my $name = $hash->{NAME}; my $reINT = '^([\\+,\\-]?\\d+$)'; # int # standard commands with no parameter my @cmdPara = (); my @cmdNoPara = ('run'); my @allCommands = ( @cmdPara, @cmdNoPara ); my $strAllCommands = join( " ", (@cmdPara) ) . ' ' . join( ":noArg ", @cmdNoPara ) . ':noArg '; my $usage = "Unknown argument $a[1], choose one of " . $strAllCommands; # we need at least one argument return $usage if ( @a < 2 ); my $cmd = $a[1]; if ( $cmd eq "?" ) { return $usage; } my $value = $a[2]; # is command defined ? if ( ( grep { /$cmd/ } @allCommands ) <= 0 ) { MaxScanner_Log $hash, 2, "cmd:$cmd no match for : @allCommands"; return return "unknown command : $cmd"; } # need we a parameter ? my $hits = scalar grep { /$cmd/ } @cmdNoPara; my $needPara = ( $hits > 0 ) ? '' : 1; MaxScanner_Log $hash, 4, "hits: $hits needPara:$needPara"; # if parameter needed, it must be an integer return "Value must be an integer" if ( $needPara && !( $value =~ m/$reINT/ ) ); # command run if ( $cmd eq "run" ) { MaxScanner_Timer($name) if ( $hash->{helper}{initDone} ); } return undef; } ########################## # handling of notifies sub MaxScanner_Notify($$$) { my ( $hash, $dev ) = @_; my $name = $hash->{NAME}; my $disable = AttrVal( $name, 'disable', '0' ); if ( grep( m/^(INITIALIZED)$/, @{ $dev->{CHANGED} } ) ) { MaxScanner_Log( $hash, 4, 'INITIALIZED' ); MaxScanner_RestartTimer($hash,20); return; } # no action if not initialized return if ( !$hash->{helper}{initDone} ); # no action if disabled return if ( $disable eq '1' ); my $devName = $dev->{NAME}; #MaxScanner_Log $hash, 5, 'start: '.$devName; # get associated devices my @associated = @{ $hash->{helper}{associatedDevices} }; # if not found return if ( !grep( /^$devName/, @associated ) ) { return; } # get the event of the device my $devReadings = int( @{ $dev->{CHANGED} } ); MaxScanner_Log $hash, 5, 'is associated: ' . $devName . ' check readings:' . $devReadings; my $found = ''; my $xevent = ''; for ( my $i = 0 ; $i < $devReadings ; $i++ ) { # , $xevent = $dev->{CHANGED}[$i]; $xevent = '' if ( !defined($xevent) ); #MaxScanner_Log $hash, 4, 'check event:<'.$xevent.'>'; if ( $xevent =~ m/^(onoff|desiredTemperature|temperature):.*/ ) { MaxScanner_Log $hash, 4, 'matching event:<' . $xevent . '>'; $found = '1'; last; } } # return if no matching with intersting properties return if ( !$found ); # loop over all instances of scanner foreach my $instName ( sort keys %{ $modules{$MaxScanner_ModulName}{defptr} } ) { my $instHash = $defs{$instName}; MaxScanner_Log $instHash, 3, 'will start <' . $instName . '> triggerd by ' . $devName . ' ' . $xevent; MaxScanner_Timer($instName); } } ########################## # Gets the summary value of associated shutter contacts sub MaxScanner_GetShutterValue($) { my ($thermHash) = @_; my $retval = 0; # if no shutters exist if ( !defined( $thermHash->{helper}{shutterContacts} ) ) { return $retval; } # get the array my @shuttersTemp = @{ $thermHash->{helper}{shutterContacts} }; # loop over all shutters foreach my $shutterName (@shuttersTemp) { my $windowIsOpen = ReadingsVal( $shutterName, "onoff", 0 ); MaxScanner_Log $thermHash, 5, $shutterName . ' onoff:' . $windowIsOpen; if ( $windowIsOpen > 0 ) { $retval = 1; last; } } MaxScanner_Log $thermHash, 5, 'retval:' . $retval; return $retval; } ########################## # looks for shutterContacts for the given thermostat sub MaxScanner_ShutterCheck($$) { my ( $modHash, $thermHash ) = @_; my $thermName = $thermHash->{NAME}; # get the list of associated shutter contacts my $strShutterNameList = AttrVal( $thermName, $MaxScanner_AttrShutterList, "?" ); if ( $strShutterNameList eq '?' ) { MaxScanner_Log $thermHash, 5, $thermName . ': found no definition for ' . $MaxScanner_AttrShutterList . ' got ' . $strShutterNameList; return; } #MaxScanner_Log $thermHash, 5, "found shutter definition list : ".$strShutterNameList; my @shutters; my @shuttersTemp = split( /,/, $strShutterNameList ); #MaxScanner_Log $thermHash, 5, "shuttersTemp : ".join(',', @shuttersTemp); # validate each shutter contact foreach my $shutterName (@shuttersTemp) { #MaxScanner_Log $thermHash, 5, 'check shuttersTemp : '.$shutterName; # ignore empty strings if ( $shutterName eq '' ) { next; } # ignore duplicated names if ( grep( /^$shutterName/, @shutters ) ) { next; } # ignore unknown devices my $hash = $defs{$shutterName}; if ( !$hash ) { MaxScanner_Log $thermHash, 4, "unknown device : " . $shutterName; next; } # device is not a shutter contact if ( $hash->{type} ne 'ShutterContact' ) { MaxScanner_Log $thermHash, 2, "device is not a shutter contact : " . $shutterName; next; } #MaxScanner_Log $thermHash, 5, 'accept shuttersTemp : '.$shutterName; push @shutters, $shutterName; } MaxScanner_Log $thermHash, 4, "accepted following shutters : " . join( ",", @shutters ); $thermHash->{helper}{shutterContacts} = [@shutters]; } ########################## # looks for MAX components # called by Run sub MaxScanner_Find($) { my ($modHash) = @_; my $modName = $modHash->{NAME}; my $numValidThermos = 0; my @shutterContacts = (); #------------------ look for all max-thermostats $modHash->{helper}{thermostats} = (); # loop over all max thermostats foreach my $aaa ( sort keys %{ $modules{MAX}{defptr} } ) { my $hash = $modules{MAX}{defptr}{$aaa}; # type must exist # it seems, that maxlan environment holds some foreign entries # see http://forum.fhem.de/index.php/topic,11624.msg390975.html#msg390975 if ( !defined( $hash->{type} ) ) { MaxScanner_Log $modHash, 5, 'missing property type for device: ' . $aaa; next; } # exit if it is not a HeatingThermostat next if $hash->{type} !~ m/^HeatingThermostat.*/; # basic properties are reqired if ( !defined( $hash->{IODev} ) || !defined( $hash->{NAME} ) ) { MaxScanner_Log $modHash, 1, 'missing property IODEV or NAME for device: ' . $aaa; next; } #. # name of the max device my $name = $hash->{NAME}; # MaxScanner_Log $modHash, 5, $name . " is HeatingThermostat"; # thermostat must be enabled for the scanner if ( AttrVal( $name, $MaxScanner_AttrEnabled, '?' ) ne '1' ) { MaxScanner_Log $modHash, 5, $name . ' ' . $MaxScanner_AttrEnabled . ' is not active, therefore this device is ignored'; next; } # MaxScanner_Log $modHash, 5, $name . ' is enabled for scanner'; # check special user attributes, if not exists, create them my $xattr = AttrVal( $name, 'userattr', '' ); if ( !( $xattr =~ m/$MaxScanner_AttrShutterList/ ) ) { # extend user attributes for scanner module my $scnCommands = $xattr . " " . join( " ", @MaxScanner_AttrForMax ); my $fhemCmd = "attr $name userattr $scnCommands"; fhem($fhemCmd); MaxScanner_Log $modHash, 4, $name . " initialized userAttributes"; } # with keepAuto=1 Scanner cannot cooperate if ( AttrVal( $name, 'keepAuto', '0' ) ne '0' && AttrVal( $name, 'scnProcessByDesiChange', '0' ) eq '0' ) { MaxScanner_Log $modHash, 0, $name . 'don\'t use keepAuto in conjunction with changeMode processing !!!'; next; } MaxScanner_Log $modHash, 5, $name . " is accepted"; $numValidThermos++; # check for shutter contacts MaxScanner_ShutterCheck( $modHash, $hash ); # if there exist shuttercontacts if ( defined( $hash->{helper}{shutterContacts} ) ) { # build sum of all sc's push( @shutterContacts, @{ $hash->{helper}{shutterContacts} } ); MaxScanner_Log $modHash, 5, "shutterContacts : " . join( ",", @shutterContacts ); } # create helper reading or thermostat $hash->{helper}{NextScan} = int( gettimeofday() ) if ( !defined( $hash->{helper}{NextScan} ) ); # this is needed for sorting later $modHash->{helper}{thermostats}{$name} = $hash->{helper}{NextScan}; } # remove duplicates my %shutterHash = map { $_ => 1 } @shutterContacts; @shutterContacts = keys %shutterHash; # $modHash->{helper}{shutterContacts} = [@shutterContacts]; my @thermos = keys %{ $modHash->{helper}{thermostats} }; my @allAssociatedDevices = ( @shutterContacts, @thermos ); $modHash->{helper}{associatedDevices} = [@allAssociatedDevices]; } ########################################################## # return a hash with useful infos relating to weekprofile sub MaxScanner_WeekProfileInfo($) { my ($name) = @_; my %result = (); my $loopCount = 0; $result{desired} = undef; # return if ($name ne 'HT.JOHN'); # !!! my $hash = $defs{$name}; if ( !$hash ) { return undef; } my %dayNames = ( 0 => "Sat", 1 => "Sun", 2 => "Mon", 3 => "Tue", 4 => "Wed", 5 => "Thu", 6 => "Fri" ); MaxScanner_Log $hash, 5, "----- Start ---------"; my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime(time); # determine the current weekday my $maxWday = $wday + 1; # 7 equals 0 for max $maxWday = 0 if ( $maxWday == 7 ); # determine next weekday my $WdayNext = $maxWday + 1; $WdayNext = 0 if ( $WdayNext == 7 ); # get the reading names for wanted readings my $profileTime = "weekprofile-$maxWday-$dayNames{$maxWday}-time"; my $profileTemp = "weekprofile-$maxWday-$dayNames{$maxWday}-temp"; my $profileTempNext = "weekprofile-$WdayNext-$dayNames{$WdayNext}-temp"; # get desired-profile for the current day # example:weekprofile-0-Sat-temp 20.0 °C / 21.0 °C / 21.0 °C / 21.0 °C / 20.0 °C / 20.0 °C my $ltemp = ReadingsVal( $name, $profileTemp, '' ); # get profile for the next day my $ltempNext = ReadingsVal( $name, $profileTempNext, '' ); MaxScanner_Log $hash, 5, "$profileTemp:$ltemp $profileTempNext:$ltempNext"; # current and next must be defined if ( !$ltemp || !$ltempNext ) { return undef; } # read profileTime # example: weekprofile-0-Sat-time 00:00-06:00 / 06:00-08:00 / 08:00-16:00 / 16:00-22:00 / 22:00-23:55 / 23:55-00:00 my $lProfileTime = ReadingsVal( $name, $profileTime, '' ); # must be defined if ( !$lProfileTime ) { return undef; } MaxScanner_Log $hash, 5, "$profileTime:$lProfileTime"; # split desired-value via slash my @tempArr = split( '/', $ltemp ); # the same for the next profile-step my @tempArrNext = split( '/', $ltempNext ); # prepare array for desired-values ${ result { tempArr } } = (); # store all desired values of the day to an array $loopCount = 1; for my $ss (@tempArr) { # extract temperature by looking for number my ($xval) = ( $ss =~ /(\d+\.\d+)/ ); MaxScanner_Log $hash, 5, "desi-Temp No. $loopCount:$xval"; push( @{ $result{tempArr} }, $xval ); $loopCount++; } # extract first temperature of the next day my ($xval) = ( $tempArrNext[0] =~ /(\d+\.\d+)/ ); push( @{ $result{tempArr} }, $xval ); MaxScanner_Log $hash, 5, "temp next day:$xval"; # analyze the time-periods of the profile #00:00-08:00 / 08:00-22:00 / 22:00-00:00 my @atime = split( '/', $lProfileTime ); # create serial form of current date my $xDate = mktime( 0, 0, 0, $mday, $mon, $year ); my @t = localtime($xDate); $xDate = sprintf( "%02d:%02d", $t[2], $t[1] ); # split profile-time using slash as splitter # and remove supernummery spaces ==> 00:00-23:55 23:55-00:00 my @btime = split( '\s*/\s*', $lProfileTime ); # MaxScanner_Log $hash,5,"profile-time:@atime xDate:$xDate"; MaxScanner_Log $hash, 5, "profile-time:@btime"; my $curTime = gettimeofday(); my $count = 0; # prepeare array @{ $result{timeArr} } = (); # loop over all time-slots $result{tempFound} = 0; for my $ss (@btime) { # extract start-time and stop-time my ( $a1, $a2 ) = ( $ss =~ /(\d+:\d+)\-(\d+:\d+)/ ); if ( !defined($a2) ) { MaxScanner_Log $hash, 2, "$name a2 not defined for $ss"; next; } # adjust stop-time when special time 24:00 $a2 = '24:00' if ( $a2 eq "00:00" ); # ende anpassen # extract hour and minute of stop-time my ( $xhour, $xmin ) = ( $a2 =~ /(\d+):(\d+)/ ); # create serial date $xDate = mktime( 0, $xmin, $xhour, $mday, $mon, $year ); # create string form my $sDate = FmtDateTime($xDate); MaxScanner_Log $hash, 5, "stopDate:$sDate segment-count:$count"; # store the stop time to result-container push( @{ $result{timeArr} }, $xDate ); # if current time > found stop-date if ( $curTime > $xDate ) { # mark the last found segment $result{tempFound} = $count + 1; MaxScanner_Log $hash, 5, "segment-count:$count found with " . FmtDateTime($xDate); } $count = $count + 1; } # prepare the hash # stop-time for the current time-slot $result{nextSwitchDate} = @{ $result{timeArr} }[ $result{tempFound} ]; # desired for the next time slot $result{nextDesired} = @{ $result{tempArr} }[ $result{tempFound} + 1 ]; # desired for the current time slot $result{desired} = @{ $result{tempArr} }[ $result{tempFound} ]; # desired must be defined if ( !defined( $result{desired} ) ) { MaxScanner_Log $hash, 2, "$name: desired not defined"; return undef; } MaxScanner_Log $hash, 4, "tempFound-Idx :" . $result{tempFound}; MaxScanner_Log $hash, 4, "nextSwitchDate:" . FmtDateTime( $result{nextSwitchDate} ); MaxScanner_Log $hash, 4, "desired :" . $result{desired}; MaxScanner_Log $hash, 4, "nextDesired :$result{nextDesired}"; return \%result; } ###################################################### # loop over all thermostats and check what is to do sub MaxScanner_Work($$$) { my $reUINT = '^([\\+]?\\d+)$'; # uint without whitespaces my ( $modHash, $thermi_sort, $numWorkIntervall ) = @_; my $isCul = ''; my $settingDone = ''; # end loop if a set command was performed my @scan_time; my $modName = $modHash->{NAME}; my $boolSimulateJohn = ''; # loop the sorted list over enabled thermostats foreach my $therm (@$thermi_sort) { #MaxScanner_Log $modHash, 3, Dumper($therm); my $hash = $defs{$therm}; my $sdCurTime = gettimeofday(); #serial date of current date my $strCurTime = FmtDateTime($sdCurTime); my $boolDesiChange = AttrVal( $therm, $MaxScanner_AttrProcessByDesiChange, '0' ) eq '1'; my $strModeHandling = uc AttrVal( $therm, $MaxScanner_AttrModeHandling, 'AUTO' ); my $dontChangeMe = ''; #. check timestamp of the reading temperature my $strTempTime = ReadingsTimestamp( $therm, 'temperature', '' ); if ( $strTempTime eq "" ) { MaxScanner_Log $hash, 1, '!! READING:temperature is not defined !!'; next; } # get desired timestamp my $strDesiTime = ReadingsTimestamp( $therm, 'desiredTemperature', '' ); # get next scan serial date my $sdNextScan = $hash->{helper}{NextScan}; MaxScanner_Log $hash, 4, 'ns:' . FmtDateTime($sdNextScan) . ' strDesiTime:' . $strDesiTime . ' Is Mode DesicChange:' . $boolDesiChange; # convert temperature time into serial format my $sdTempTime = time_str2num($strTempTime); # convert desired timestamp into serial format if possible, otherwise use current time my $sdDesiTime = ($strDesiTime) ? time_str2num($strDesiTime) : gettimeofday(); #. check Cul my $strCulName; my $strCreditTime = ''; my $numCulCredits; my $numDutyCycle = '?'; my $strIOHash = $defs{$therm}{IODev}; # CULMAX0, hash of IO-Devices my $strIOName = $strIOHash->{NAME}; my $strIOType = $strIOHash->{TYPE}; # CUL_MAX,MAXLAN type des IO-Devices #. MaxScanner_Log $hash, 4, 'TYPE:'.$strIOType.' IOName:'.$strIOName.' simCube:'.$boolSimulateJohn; # if com-device is a MAXLAN if ( $strIOType eq "MAXLAN" ) { # determine name of IO devices $strCulName = $strIOName; # get dutycycle my $strDutyCycle = ReadingsVal( $strCulName, 'dutycycle', '?' ); # if not a number try to get it via internal value $strDutyCycle = InternalVal( $strCulName, 'dutycycle', 0 ) if ( $strDutyCycle eq "?" ); # get the timestamp of reading dutycycle $strCreditTime = ReadingsTimestamp( $strCulName, 'dutycycle', '' ); # take the middle term of ... my ( $a1, $a2, $a3 ) = ( $strDutyCycle =~ /([\s]*)(\d+)(.*)/ ); if ( defined($a2) ) { $numDutyCycle = $a2; } else { $numDutyCycle = 100; MaxScanner_Log $hash, 2, '!! dutycyle not a number: <' . $strDutyCycle . '>; force to 100'; } # transform dutycycle to CulCredits $numCulCredits = ( 100 - $numDutyCycle ) * 10; $isCul = ''; } # we got a CUL else { # determine name of IO devices $strCulName = $strIOHash->{IODev}{NAME}; # get the credit's timestamp $strCreditTime = ReadingsTimestamp( $strCulName, 'credit10ms', '' ); # get the credits $numCulCredits = ReadingsVal( $strCulName, 'credit10ms', 0 ); # force dynamic scanning for CUL $isCul = '1'; } # simulate cube if ($boolSimulateJohn && $therm eq 'HT.JOHN') { $isCul = ''; MaxScanner_Log $hash, 4, '!! Simulate cube with HT.JOHN isCul:'.$isCul; } # because cube not knows msgcnt, we fix the timestamp my $strLastTransmit = ($isCul) ? ReadingsTimestamp( $therm, 'msgcnt', '' ) : FmtDateTime( gettimeofday() - 20 ); # msgcnt must exist if ( $strLastTransmit eq '' ) { MaxScanner_Log $hash, 1, '!! Reading:msgcnt is not defined'; next; } # convert timestamp lastTransmit to serial date my $sdLastTransmit = time_str2num($strLastTransmit); MaxScanner_Log $hash, 4, "CulName:$strCulName CulCredits:$numCulCredits " . "CreditTime:$strCreditTime dutyCycle:$numDutyCycle"; # somtimes we get "no answer" instead of a number if ( !( $numCulCredits =~ m/$reUINT/ ) ) { MaxScanner_Log $hash, 1, '!! credit10ms/dutycycle must be a number'; next; } # creditTime must exist if ( $strCreditTime eq '' ) { MaxScanner_Log $hash, 1, '!! READINGS:credit10ms is not defined'; next; } # convert credit time to serial date my $sdCreditTime = time_str2num($strCreditTime); # get current desired temperature my $numDesiTemp = ReadingsVal( $therm, 'desiredTemperature', '' ); if ( $numDesiTemp eq 'on' || $numDesiTemp eq 'off' ) #Hint by MrHeat { MaxScanner_Log $hash, 3, 'reading desiredTemperature: thermostat is forced on/off. Skipping thermostat'; next; } # desi temp must be a number elsif ( $numDesiTemp eq '' ) { MaxScanner_Log $hash, 1, '!! reading desiredTemperature is not available'; next; } # get current mode my $strMode = ReadingsVal( $therm, 'mode', '' ); # current mode must be defined if ( $strMode eq "" ) { MaxScanner_Log $hash, 1, '!! reading mode is not available'; next; } # get weekprofile-Info my $weekProfile = MaxScanner_WeekProfileInfo($therm); # must be defined if ( !defined($weekProfile) ) { MaxScanner_Log $hash, 1, '!! weekprofile is not available'; next; } # don't change mode if the latency is active; only cul is affected if ( $sdLastTransmit + 5 >= $sdCurTime && $isCul ) { MaxScanner_Log $hash, 4, 'no action due transmission latency'; next; } # get desired of weekprofile my $normDesiTemp = $weekProfile->{desired}; # get window-open temperature my $numWinOpenTemp = ReadingsVal( $therm, 'windowOpenTemperature', '-1' ); # get the additional credits calculated from the elapsed time my $numCreditDiff = ( $sdCurTime - $sdCreditTime ); my $numCreditThreshold = AttrVal( $modName, $MaxScanner_AttrCreditThreshold, $MaxScanner_DefaultCreditThreshold ); # calculate resulting credits my $numCredit = $numCulCredits + $numCreditDiff; # limit the result $numCredit = 900 if ( $numCredit > 900 ); MaxScanner_Log $hash, 4, 'CulCredits:' . $numCulCredits . ' Credits:' . int($numCredit) . ' isCul:' . $isCul . ' CreditThreshold:' . $numCreditThreshold; # determine next scan time depending on the time of last scan my $sdNextScanOld = $sdNextScan; # preset the minimal timestamp: my $nextPlan = $sdNextScan; # if dynamic scanning if ($isCul) { # 17 secs before next scan time $nextPlan = $sdTempTime + $numWorkIntervall * 60 - 17; } # static scanning (CUBE) else { $nextPlan = $sdNextScan + $numWorkIntervall * 60; } # adjust the next scantime until it is in future $nextPlan = $nextPlan + ( 60 * $MaxScanner_BaseIntervall ) while ( $sdCurTime > $nextPlan ); $sdNextScan = $nextPlan; MaxScanner_Log $hash, 4, 'ns:' . FmtTime($sdNextScan) . ' nsOld:' . FmtTime($sdNextScanOld); # basic inits if thermostat if not not already done if ( !defined( $hash->{helper}{TemperatureTime} ) ) { MaxScanner_Log $hash, 4, 'create helpers with ns:' . FmtDateTime($sdNextScan); $hash->{helper}{TemperatureTime} = $sdTempTime; # timestamp of the last receive of temperature $hash->{helper}{DesiTime} = $sdDesiTime; # timestamp of the last receive of desired $hash->{helper}{WinWasOpen} = 0; $hash->{helper}{TempBeforeWindOpen} = $numDesiTemp; # $hash->{helper}{LastWasAutoReset} = ''; $hash->{helper}{leadDesiTemp} = ($boolDesiChange) ? $normDesiTemp : $numDesiTemp; $hash->{helper}{desiredOffset} = ($boolDesiChange) ? $numDesiTemp - $normDesiTemp : 0; $hash->{helper}{switchDate} = undef; $hash->{helper}{LastCmdDate} = $sdCurTime; $hash->{helper}{gotTempTS} = ''; } # gather the timestamp for next profile switch my $switchDate = ( defined($weekProfile) ) ? $weekProfile->{nextSwitchDate} : $sdDesiTime; # create a helper if not already done $hash->{helper}{switchDate} = $switchDate if ( !defined( $hash->{helper}{switchDate} ) ); # if switchDate is changed, then adjust leading desired if ( $hash->{helper}{switchDate} != $switchDate ) { $hash->{helper}{gotTempTS} = ''; $hash->{helper}{switchDate} = $switchDate; $hash->{helper}{leadDesiTemp} = $normDesiTemp; $hash->{helper}{TempBeforeWindOpen} = $normDesiTemp; # MrHeat $hash->{helper}{desiredOffset} = 0; MaxScanner_Log $hash, 3, "reset leadDesiTemp:" . $hash->{helper}{leadDesiTemp}.' Mode:'.$strMode; # when triggermode ModeChange and mode is manual, we must switch to auto to force the new setpoint/desired if ( !$boolDesiChange && ( $strMode eq 'manual' ) && ( $normDesiTemp != $numDesiTemp ) ) { my $cmd = "set $therm desiredTemperature auto"; fhem($cmd); $hash->{helper}{LastCmdDate} = $sdCurTime; $settingDone = 1; MaxScanner_Log $hash, 3, "switchTime: <<$cmd>>"; } # force wait time for Cube-devices, after switch date is changed if (! $isCul) { $sdNextScan = $sdCurTime + $numWorkIntervall * 60; $hash->{helper}{NextScan} = int($sdNextScan); MaxScanner_Log $hash, 3, 'forward NextScan for Cube-Devices ns:'.FmtDateTime($sdNextScan); } # now stop further actions with this thermostat, and wait for activation by the weekprofile # next; } # if mode switch is active, then offset must be 0 if ( !$boolDesiChange && $hash->{helper}{desiredOffset} != 0 ) { $hash->{helper}{desiredOffset} = 0; MaxScanner_Log $hash, 4, 'force desiredOffset to 0'; } # determine nextScan for CUL-like devices if ($isCul) { # if temperature time is younger than old time, then determine nextScan if ( $sdTempTime != $hash->{helper}{TemperatureTime} ) { $hash->{helper}{gotTempTS} = 1; # remember timerstamp $hash->{helper}{TemperatureTime} = $sdTempTime; $hash->{helper}{NextScan} = int($sdNextScan); $hash->{helper}{NextScanTimestamp} = FmtDateTime( $hash->{helper}{NextScan} ); MaxScanner_Log $hash, 3, 'TEMPERATURE received at ' . $strTempTime . ', ==> new ns:' . FmtDateTime($sdNextScan); } } else { # no wait time for cube devices if ($sdCurTime >= $hash->{helper}{NextScan} && !$hash->{helper}{gotTempTS}) { $hash->{helper}{gotTempTS} = 1; MaxScanner_Log $hash, 3, 'TEMPERATURE received is assumed (Cube)'; } } # get shutter's state my $boolWinIsOpenByFK = MaxScanner_GetShutterValue($hash) > 0; # opened window can also be detected by temperature fall # Don't change mode, if WindowOpen is recognized by temperature fall # then desiredTemp=WidowOpenTemp my $boolWinIsOpenByTempFall = $numDesiTemp == $numWinOpenTemp; # don't touch the thermostat, if windowOpen is recognized if ( $boolWinIsOpenByFK || $boolWinIsOpenByTempFall ) { MaxScanner_Log $hash, 3, '<> no action due open window; desi-temp before window open:' . $hash->{helper}{TempBeforeWindOpen} if ($hash->{helper}{WinWasOpen} == 0); $hash->{helper}{WinWasOpen} = 1; $dontChangeMe = 1; #next; } # window is closed else { # now window is closed and it was open before if ( $hash->{helper}{WinWasOpen} > 0 ) { # ----------- <> it was just closed --------- if ( $hash->{helper}{WinWasOpen} == 1 ) { # switch to state 2: we are waiting for desi-temp $hash->{helper}{WinWasOpen} = 2; MaxScanner_Log $hash, 3, "strMode:$strMode DesiTemp:$numDesiTemp TempBeforeWindOpen:" . $hash->{helper}{TempBeforeWindOpen}; # now set in each case desired temperature, # we expect desired temperature receive and than procede with scanner # therefore we will get no problem, even there is a delay by command queue $numCredit -= 110; # therfore our credit counter must be reduced my $cmd = "set $therm desiredTemperature " . ( $strMode eq 'auto' ? 'auto' : '' ) . ' ' . $hash->{helper}{TempBeforeWindOpen}; #MrHeat fhem($cmd); $hash->{helper}{LastCmdDate} = $sdCurTime; MaxScanner_Log $hash, 3, '<>due window is closed: ' . $cmd; $hash->{helper}{DesiTime} = $sdDesiTime; # remember timestamp of desiTemp # no further action after changing desired # abort, due we waiting for feedback of desiTemp next; } # -------- <> we are waiting for desitemp ----------------- elsif ( $hash->{helper}{WinWasOpen} == 2 ) { # forward to next step only, if timestamp of desiredTemp is changed if ( $hash->{helper}{DesiTime} == $sdDesiTime ) { next; } MaxScanner_Log $hash, 3, '<> received new desiredTemperature after opened window: continue scanning now'; # window open statemachine closed $hash->{helper}{WinWasOpen} = 0; } } else { # <> ----------------- window is closed and was closed before # only notice, if after window was closed desiTemp is received. $hash->{helper}{TempBeforeWindOpen} = $numDesiTemp; # calculate expected desiTemp my $expectedDesiTemp = $hash->{helper}{leadDesiTemp} + $hash->{helper}{desiredOffset}; MaxScanner_Log $hash, 4, "numDesiTemp:$numDesiTemp expectedDesiTemp:$expectedDesiTemp leadDesiTemp:" . $hash->{helper}{leadDesiTemp}; MaxScanner_Log $hash, 4, "normDesiTemp:$normDesiTemp desiredOffset:" . $hash->{helper}{desiredOffset}; # if the expected value does not match, than desired was changed outside # but when CUL than only, if we got temperature after a desired change by w-profile if ( $expectedDesiTemp != $numDesiTemp && $hash->{helper}{gotTempTS} ) { $hash->{helper}{leadDesiTemp} = $numDesiTemp; $hash->{helper}{desiredOffset} = 0; MaxScanner_Log $hash, 3, "change leadDesiTemp due manipulation:" . $hash->{helper}{leadDesiTemp}; } } } # if mode equals boost, the don't change anything if ( $strMode eq 'boost' ) { MaxScanner_Log $hash, 3, 'no action due boost'; $dontChangeMe = 1; #next; } # if we perform modeChange and are in auto mode and next scan is near to the profile switch date # then do not perform switch, because the profile should change the desired just in time if (!$boolDesiChange && $strMode eq 'auto' && $sdNextScan >= $weekProfile->{nextSwitchDate} - 60 ) { if ($isCul) { $hash->{helper}{NextScan} = $weekProfile->{nextSwitchDate} + 60; } else { $hash->{helper}{NextScan} = $weekProfile->{nextSwitchDate} + 60*3+10; } my $ss = FmtDateTime( $hash->{helper}{NextScan} ); $hash->{helper}{NextScanTimestamp} = $ss; MaxScanner_Log $hash, 3, 'no action due soon a week-profile switch point is reached ns:' . $ss; $dontChangeMe = 1; } #--------------- # next; # !!! #--------------- MaxScanner_Log $hash, 4, "Trigger Mode Desi-Change:$boolDesiChange "; # if scan time is exceeded and no other setting was done, # we check to trigger the thermostat if ( !$dontChangeMe && !$settingDone && ( $sdCurTime >= $hash->{helper}{NextScan} ) ) { # in each case store NextScan, this is the preliminary scan time, # if there are not enough credits # if we can transmit, the timestamp for NextScan will be again set , # after receiving of temperature $hash->{helper}{NextScan} = int($sdNextScan); # if we got enough credits, so we can trigger the thermostat if ( $numCredit >= $numCreditThreshold ) { # the estimated reduction of credits after execution of a trigger $numCredit -= 110; my $cmd; my $leadDesiTemp = $hash->{helper}{leadDesiTemp}; my $desiOffset = $hash->{helper}{desiredOffset}; # trigger thermostat by changing the desired temperature if ($boolDesiChange) { # perform trigger with offest and determin it if ( $desiOffset == 0 ) { # calc the difference between current and desired temperature my $currentTemp = ReadingsVal( $therm, 'temperature', $normDesiTemp ); my $diff = $normDesiTemp - $currentTemp; # calc the offset if ( $diff >= 0 ) # soll > ist { $desiOffset = 0.5; } else { # soll < ist $desiOffset = -0.5; } } # perform trigger without offset else { # force to zero $desiOffset = 0; } # calc the target desi temp my $newTemp = $leadDesiTemp + $desiOffset; # use current mode for default my $setMode = ( $strMode eq 'manual' ) ? '' : 'auto'; if ( $strModeHandling eq 'AUTO' ) { $setMode = 'auto'; } elsif ( $strModeHandling eq 'MANUAL' ) { $setMode = ''; } $cmd = "set $therm desiredTemperature $setMode $newTemp"; } # trigger thermostat by changing of mode else { my $modeCommand = ( $strMode eq 'manual' ) ? 'auto' : ''; $cmd = "set $therm desiredTemperature " . $modeCommand . " $leadDesiTemp"; # MaxScanner_Log $hash, 5, 'cmd:'.$cmd.' modeCommand:'.$modeCommand.' strMode:'.$strMode } # exec command, at least 180 seconds after last command send if ( $sdCurTime > $hash->{helper}{LastCmdDate} + 180 ) { fhem($cmd); MaxScanner_Log $hash, 3, "<<$cmd>>"; $hash->{helper}{LastCmdDate} = $sdCurTime; $hash->{helper}{desiredOffset} = $desiOffset; # mark execution of a command, to shortcut the loop later $settingDone = 1; } else { MaxScanner_Log $hash, 3, ' Wait at least 180 sec . after last command'; } # if we are using CUL, then dynamic scanning if ($isCul) { $hash->{helper}{NextScan} = int( $sdCurTime + 60 ); } else # if CUBE { $hash->{helper}{NextScan} = int( $sdCurTime + $numWorkIntervall * 60 ); } } # there are to less credits or other preventing reasons, so we have to wait else { # determine the waiting time my $numDiffCredit = $numCreditThreshold - $numCredit; my $numDiffTime = 0; # the waiting time must be greater then the needed credits # and must be a multiple of the baseinterval while ( $numDiffCredit > $numDiffTime ) { $numDiffTime += ( 60 * $MaxScanner_BaseIntervall ); } # adjust, so the check is called, before the calculated scan time is running out $sdNextScan += $numDiffTime - ( 60 * $MaxScanner_BaseIntervall ); $hash->{helper}{NextScan} = int($sdNextScan); MaxScanner_Log $hash, 3, ' not enough credits( ' . int($numCredit) . ' ) need ' . int($numDiffCredit) . "/$numDiffTime ns:" . FmtDateTime($sdNextScan); # move the timestamp of all thermostats, which follows on the current this ensures the round robin rule foreach my $thAdjust (@$thermi_sort) { # if the timestamp is younger then the timestamp of the current thermostat, move it if ( $defs{$thAdjust}{helper}{NextScan} < $hash->{helper}{NextScan} ) { # adjust the timestamp $defs{$thAdjust}{helper}{NextScan} += int($numDiffTime); # string representation of nextScan my $ss = FmtDateTime( $defs{$thAdjust}{helper}{NextScan} ); $defs{$thAdjust}{helper}{NextScanTimestamp} = $ss; MaxScanner_Log $hash, 3, "adjust $thAdjust to $ss"; } } } } # nothing is to do, so we wait else { MaxScanner_Log $hash, 4, ' WAITING ... ns : ' . FmtTime( $hash->{helper}{NextScan} ); } # store NextScan in an array, for optimized timer setup push( @scan_time, $hash->{helper}{NextScan} ); MaxScanner_Log $hash, 5, '++++++++ '; # foreach thermostat } # calculate the value for the timer # sort the trigger times of the thermostats my @scan_time_sort = sort @scan_time; # minimal time difference my $numDiffTime = 5; my $numCurTime = int( gettimeofday() ); # if we got at least one thermostat if ( @scan_time_sort >= 1 ) { # use the scanTime with the smallest value my $diff = $scan_time_sort[0] - $numCurTime; # minimal difference $diff = 2 if ( $diff < 2 ); if ( $diff > 2 ) { $numDiffTime = int($diff); MaxScanner_Log $modHash, 3, ' next scan in seconds : ' . $numDiffTime; } } # return the waiting time in seconds return $numDiffTime; } ########################## sub MaxScanner_Run($) { my ($name) = @_; my $hash = $defs{$name}; my $reUINT = '^([\\+]?\\d+)$'; my $numValidThermos = 0; my $nn = $MaxScanner_BaseIntervall; my $numMinInterval = ( AttrVal( $name, 'scnMinInterval', $nn ) =~ m/$reUINT/ ) ? $1 : $nn; #. my $retVal = 5; # loop forever while (1) { # find all thermostats MaxScanner_Find($hash); my $thermos = $hash->{helper}{thermostats}; if ( !$hash->{helper}{initDone} ) { $hash->{helper}{initDone} = 1; MaxScanner_Log $hash, 4, "init done"; } # sort the thermostats concering the nextScan timestamp my @thermi_sort = sort { $thermos->{$a} <=> $thermos->{$b} } keys %{$thermos}; MaxScanner_Log $hash, 4, "found " . scalar(@thermi_sort) . " thermostats"; # number of valid thermostats $numValidThermos = scalar(@thermi_sort); # stop, if we got no thermostat last if ( $numValidThermos <= 0 ); # a maximum of 32 thermostats is allowed $numValidThermos = $MaxScanner_TXPerMinutes if ( $numValidThermos > $MaxScanner_TXPerMinutes ); # calculate the optimal scan interval my $numWorkIntervall = int( 60 / int( $MaxScanner_TXPerMinutes / $numValidThermos ) ); $numWorkIntervall = $numMinInterval if ( $numWorkIntervall < $numMinInterval ); # adjust the intervall, so it is a multiple of the BaseIntervall $numWorkIntervall += ( $MaxScanner_BaseIntervall - ( $numWorkIntervall % $MaxScanner_BaseIntervall ) ) if ( $numWorkIntervall % $MaxScanner_BaseIntervall != 0 ); $hash->{helper}{workInterval} = $numWorkIntervall; #. MaxScanner_Log $hash, 4, "optimal scan intervall:$numWorkIntervall"; $retVal = MaxScanner_Work( $hash, \@thermi_sort, $numWorkIntervall ); # exit loop last; } return $retVal; } ########################## # called by internal timer sub MaxScanner_Timer($) { my ($name) = @_; my $hash = $defs{$name}; my $re01 = '^([0,1])$'; # only 0,1 my $stateStr = "processing"; my $numValidThermos = 0; my $isDisabled = ( AttrVal( $name, 'disable', 0 ) =~ m/$re01/ ) ? $1 : ''; my $numDiffTime = 5; my $sdNextScan; MaxScanner_Log $hash, 3, '------------started ---------------- instance:' . $name; # loop while (1) { # no further action if disabled if ($isDisabled) { MaxScanner_Log $hash, 4, "is disabled"; $stateStr = "disabled"; last; } # remove the timer of the script version RemoveInternalTimer('MaxScanRun'); # call runner $numDiffTime = MaxScanner_Run($name); last; } # update state readingsSingleUpdate( $hash, 'state', $stateStr, 0 ); MaxScanner_RestartTimer( $hash, $numDiffTime ); $sdNextScan = gettimeofday() + $numDiffTime; $hash->{helper}{nextWorkTime} = FmtDateTime($sdNextScan); } ########################## # attribute handling sub MaxScanner_Attr($$$$) { my ( $command, $name, $attribute, $value ) = @_; my $msg = undef; my $hash = $defs{$name}; my $reUINT = '^([\\+]?\\d+)$'; MaxScanner_Log $hash, 4, 'name:' . $name . ' attribute:' . $attribute . ' value:' . $value . ' command:' . $command; if ( $attribute eq 'disable' ) { # call timer delayed MaxScanner_RestartTimer( $hash, 1 ) if ( $hash->{helper}{initDone} ); } #. threshold elsif ( $attribute eq $MaxScanner_AttrCreditThreshold ) { my $isInt = ( $value =~ m/$reUINT/ ) ? $1 : ''; if ( !$isInt ) { $msg = 'value must be a number:' . $value; return $msg; } if ( $value < 150 || $value > 600 ) { $msg = 'value out of range [150..600] ' . $value; return $msg; } } #. scnMinInterval elsif ( $attribute eq $MaxScanner_AttrMinInterval ) { my $isInt = ( $value =~ m/$reUINT/ ) ? $1 : ''; if ( !$isInt ) { $msg = 'value must be a number:' . $value; return $msg; } if ( $value < 3 || $value > 60 ) { $msg = 'value out of range [3..60] ' . $value; return $msg; } } return $msg; } 1; =pod =begin html

MaxScanner

The MaxScanner-Module enables FHEM to capture temperature and valve-position of thermostats in regular intervals.

    Define

      define <name> MaxScanner

    Set-Commands
      set <name> run

        Runs the scanner loop immediately. (Is usually done by timer)

    Get-Commands
      get <name> associatedDevices

        Gets the asscociated devices (thermostats, shutterContacts)

    Attributes for the Scanner-Device

    • readingFnAttributes
    • disable
      When value=1, then the scanner device is disabled; possible values: 0,1; default: 0

    • scnCreditThreshold
      the minimum value of available credits; when lower, the scanner will remain inactive; possible values: 150..600; default: 300

    • scnMinInterval
      scan interval in minutes, when the calculated interval is lower, then scnMinintervall will be used instead;possible values: 3..60; default: 3


    User-Attributes for the Thermostat-Device
    • scanTemp
      When value=1, then scanner will use the thermostat; possible values: 0,1; default: 0

    • scnProcessByDesiChange
      When value=1, then scanner will use method "desired change" instead of "mode change"; possible values: 0,1; default: 0

    • scnModeHandling
      When scnProcessByDesiChange is active, this attribute select the way of handling the mode of the thermostat; possible values: [NOCHANGE,AUTO,MANUAL];default: AUTO

    • scnShutterList
      comma-separated list of shutterContacts associated with the thermostat


    Additional information

=end html =cut