| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554 |
- # $Id: 98_MaxScanner.pm 11044 2016-03-11 19:18:03Z john99sr $
- ####################################################################################################
- #
- # 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 <http://www.gnu.org/licenses/>.
- #
- #
- # 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: <module name> <instance-name> <calling sub without prefix>.<line nr> <text>
- 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 <name> ' . $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( '<br/>', @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++ )
- {
- # <onoff: 0> , <desiredTemperature: 12.0>
- $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,
- '<<stage 1>> 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 )
- {
- # ----------- <<stage 1>> 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, '<<stage 2>>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;
- }
- # -------- <<stage 2 >> 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,
- '<<stage 3>> received new desiredTemperature after opened window: continue scanning now';
- # window open statemachine closed
- $hash->{helper}{WinWasOpen} = 0;
- }
- } else
- {
- # <<stage 0>> ----------------- 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
- <a name="MaxScanner"></a>
- <h3>MaxScanner</h3>
- <p>The MaxScanner-Module enables FHEM to capture temperature and valve-position of thermostats in regular intervals. <p/>
- <ul>
- <a name="MaxScannerdefine"></a>
- <b>Define</b>
- <ul>
- <br/>
- <code>define <name> MaxScanner </code>
- <br/>
- </ul>
- <br>
- <a name="MaxScannerset"></a>
- <b>Set-Commands</b>
- <ul>
- <code>set <name> run</code>
- <br/><br/>
- <ul>
- Runs the scanner loop immediately. (Is usually done by timer)
- </ul><br/>
- </ul>
-
- <a name="MaxScannerget"></a>
- <b>Get-Commands</b>
- <ul>
- <code>get <name> associatedDevices</code><br/><br/>
- <ul>Gets the asscociated devices (thermostats, shutterContacts)</ul><br/>
- </ul>
-
- <a name="MaxScannerattr"></a>
- <b>Attributes for the Scanner-Device</b><br/><br/>
-
- <ul>
- <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
- <li><p><b>disable</b><br/>When value=1, then the scanner device is disabled; possible values: 0,1; default: 0</p></li>
- <li><p><b>scnCreditThreshold</b><br/>the minimum value of available credits; when lower, the scanner will remain inactive; possible values: 150..600; default: 300</p></li>
- <li><p><b>scnMinInterval</b><br/>scan interval in minutes, when the calculated interval is lower,
- then scnMinintervall will be used instead;possible values: 3..60; default: 3</p></li>
- </ul>
- <br/>
-
- <a name="MaxScannerthermoattr"></a>
- <b>User-Attributes for the Thermostat-Device</b><br/>
- <ul>
- <li><p><b>scanTemp</b><br/>When value=1, then scanner will use the thermostat; possible values: 0,1; default: 0</p></li>
- <li><p><b>scnProcessByDesiChange</b><br/>When value=1, then scanner will use method "desired change" instead of "mode change"; possible values: 0,1; default: 0</p></li>
- <li><p><b>scnModeHandling</b><br/>When scnProcessByDesiChange is active, this attribute select the way of handling the mode of the thermostat; possible values: [NOCHANGE,AUTO,MANUAL];default: AUTO</p></li>
- <li><p><b>scnShutterList</b><br/>comma-separated list of shutterContacts associated with the thermostat</p></li>
- </ul>
- <br/>
-
- <b>Additional information</b><br/><br/>
- <ul>
- <li><a href="http://forum.fhem.de/index.php/topic,11624.0.html">Discussion in FHEM forum</a></li><br/>
- <li><a href="http://www.fhemwiki.de/wiki/MAX!_Temperatur-Scanner">WIKI information in FHEM Wiki</a></li><br/>
- </ul>
- </ul>
- =end html
- =cut
|