98_MaxScanner.pm 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554
  1. # $Id: 98_MaxScanner.pm 11044 2016-03-11 19:18:03Z john99sr $
  2. ####################################################################################################
  3. #
  4. # 98_MaxScanner.pm
  5. # The MaxScanner enables FHEM to capture temperature and valve-position of thermostats
  6. # in regular intervals
  7. #
  8. # This module is written by john.
  9. #
  10. # This file is part of fhem.
  11. #
  12. # Fhem is free software: you can redistribute it and/or modify
  13. # it under the terms of the GNU General Public License as published by
  14. # the Free Software Foundation, either version 2 of the License, or
  15. # (at your option) any later version.
  16. #
  17. # Fhem is distributed in the hope that it will be useful,
  18. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. # GNU General Public License for more details.
  21. #
  22. # You should have received a copy of the GNU General Public License
  23. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  24. #
  25. #
  26. # 25.10.15 - 1.0.0.0
  27. # initial build
  28. #
  29. # Task-list
  30. # * define minimal Scan-Interval
  31. # * define credit threshold
  32. # * multiple shutters
  33. # * notify for shutter contacts and implicated thermostats
  34. # * check text off/on as desi-Temp
  35. #
  36. # 06.01.16
  37. # * RestartTimer
  38. # 09.01.16
  39. # * change: using of instead of NotifyFn explicit notify
  40. # * fixed : erreous initial scenario
  41. # * new : get associatedDevices
  42. # * change. scanTemp substitues scnEnabled
  43. # 11.01.16 - 1.0.0.0
  44. # * change: limit logging, when window open detected
  45. # 13.01.16 - 1.0.0.1
  46. # * change: FIND, minor changes
  47. # 15.01.16 - 1.0.0.2
  48. # * fixed : Work- check of external change of desired was incorrect
  49. # 07.03.16 - 1.0.0.3
  50. # * fixed : startup after initialization of fhem
  51. ####################################################################################################
  52. package main;
  53. use strict;
  54. use warnings;
  55. use Data::Dumper;
  56. use vars qw(%defs);
  57. use vars qw($readingFnAttributes);
  58. use vars qw(%attr);
  59. use vars qw(%modules);
  60. my $MaxScanner_Version = "1.0.0.3 - 07.03.2016";
  61. my $MaxScanner_ModulName = "MaxScanner";
  62. # minimal poll-rate for thermostat in minutes given by firmware
  63. my $MaxScanner_BaseIntervall = 3;
  64. my $MaxScanner_DefaultCreditThreshold = 300;
  65. # attributes for thermostat instance
  66. my $MaxScanner_TXPerMinutes = 32; # transmissions per hour
  67. my $MaxScanner_AttrEnabled = 'scanTemp';
  68. my $MaxScanner_AttrShutterList = 'scnShutterList';
  69. my $MaxScanner_AttrProcessByDesiChange = 'scnProcessByDesiChange';
  70. my $MaxScanner_AttrModeHandling = 'scnModeHandling';
  71. # attributes for module instance
  72. my $MaxScanner_AttrCreditThreshold = 'scnCreditThreshold';
  73. my $MaxScanner_AttrMinInterval = 'scnMinInterval';
  74. # define user defined attributes
  75. my @MaxScanner_AttrForMax = (
  76. #$MaxScanner_AttrEnabled . ':0,1',
  77. $MaxScanner_AttrProcessByDesiChange . ':0,1',
  78. $MaxScanner_AttrShutterList,
  79. $MaxScanner_AttrModeHandling . ':NOCHANGE,AUTO,MANUAL'
  80. );
  81. #
  82. ##########################
  83. # output format: <module name> <instance-name> <calling sub without prefix>.<line nr> <text>
  84. sub MaxScanner_Log($$$)
  85. {
  86. my ( $hash, $loglevel, $text ) = @_;
  87. my $xline = ( caller(0) )[2];
  88. my $xsubroutine = ( caller(1) )[3];
  89. my $sub = ( split( ':', $xsubroutine ) )[2];
  90. my $ss = $MaxScanner_ModulName . "_";
  91. $sub =~ s/$ss//;
  92. my $instName =
  93. ( ref($hash) eq "HASH" ) ? $hash->{NAME} : $MaxScanner_ModulName;
  94. Log3 $hash, $loglevel, "$MaxScanner_ModulName $instName $sub.$xline " . $text;
  95. }
  96. ##########################
  97. sub MaxScanner_Initialize($)
  98. {
  99. my ($hash) = @_;
  100. $hash->{DefFn} = $MaxScanner_ModulName . '_Define';
  101. $hash->{UndefFn} = $MaxScanner_ModulName . '_Undef';
  102. $hash->{SetFn} = $MaxScanner_ModulName . '_Set';
  103. $hash->{GetFn} = $MaxScanner_ModulName . '_Get';
  104. $hash->{AttrFn} = $MaxScanner_ModulName . '_Attr';
  105. $hash->{NotifyFn} = $MaxScanner_ModulName . '_Notify';
  106. $hash->{AttrList} =
  107. $MaxScanner_AttrCreditThreshold
  108. . ':150,200,250,300,350,400 '
  109. . $MaxScanner_AttrMinInterval
  110. . ':3,6,9,12,15,18,21,24,27,30 '
  111. . 'disable:0,1 '
  112. . $readingFnAttributes;
  113. MaxScanner_Log '', 3, "Init Done with Version $MaxScanner_Version";
  114. }
  115. ##########################
  116. sub MaxScanner_RestartTimer($$)
  117. {
  118. my ( $hash, $seconds ) = @_;
  119. my $name = $hash->{NAME};
  120. $seconds = 1 if ( $seconds <= 0 );
  121. RemoveInternalTimer($name);
  122. my $sdNextScan = gettimeofday() + $seconds;
  123. InternalTimer( $sdNextScan, $MaxScanner_ModulName . '_Timer', $name, 1 );
  124. }
  125. ##########################
  126. sub MaxScanner_Define($$$)
  127. {
  128. my ( $hash, $def ) = @_;
  129. my @a = split( "[ \t][ \t]*", $def );
  130. my $name = $a[0];
  131. MaxScanner_Log $hash, 4, "parameters: @a";
  132. if ( @a < 2 )
  133. {
  134. return 'wrong syntax: define <name> ' . $MaxScanner_ModulName;
  135. }
  136. # only one scanner instance is allowed
  137. # get the count of instances
  138. my @scanners = keys %{ $modules{$MaxScanner_ModulName}{defptr} };
  139. my $scannerCount = @scanners;
  140. if ($scannerCount > 0)
  141. {
  142. return 'only one scanner instance is allowed';
  143. }
  144. #.
  145. $hash->{helper}{thermostats} = ();
  146. $hash->{helper}{initDone} = '';
  147. $hash->{VERSION} = $MaxScanner_Version;
  148. # register modul
  149. $modules{$MaxScanner_ModulName}{defptr}{$name} = $hash;
  150. # create timer
  151. RemoveInternalTimer($name);
  152. #my $xsub = $MaxScanner_ModulName . "_Timer";
  153. #InternalTimer( gettimeofday() + 20, $xsub, $name, 0 );
  154. # MaxScanner_RestartTimer($hash,20);
  155. # MaxScanner_Log $hash, 2, 'timer started';
  156. return undef;
  157. }
  158. ##########################
  159. sub MaxScanner_Undef($$)
  160. {
  161. my ( $hash, $arg ) = @_;
  162. RemoveInternalTimer( $hash->{NAME} );
  163. MaxScanner_Log $hash, 2, "done";
  164. return undef;
  165. }
  166. ###########################
  167. sub MaxScanner_Get($@)
  168. {
  169. my ( $hash, @a ) = @_;
  170. my $name = $hash->{NAME};
  171. my $ret = "Unknown argument $a[1], choose one of associatedDevices:noArg";
  172. my $cmd = lc( $a[1] );
  173. my @carr;
  174. MaxScanner_Log $hash, 4, 'cmd:' . $cmd;
  175. # check the commands
  176. if ( $cmd eq 'associateddevices' )
  177. {
  178. if ( defined( $hash->{helper}{associatedDevices} ) )
  179. {
  180. @carr = @{ $hash->{helper}{associatedDevices} };
  181. $ret = join( '<br/>', @carr );
  182. } else
  183. {
  184. $ret = 'no devices';
  185. }
  186. }
  187. return $ret;
  188. }
  189. ###########################
  190. sub MaxScanner_Set($@)
  191. {
  192. my ( $hash, @a ) = @_;
  193. my $name = $hash->{NAME};
  194. my $reINT = '^([\\+,\\-]?\\d+$)'; # int
  195. # standard commands with no parameter
  196. my @cmdPara = ();
  197. my @cmdNoPara = ('run');
  198. my @allCommands = ( @cmdPara, @cmdNoPara );
  199. my $strAllCommands =
  200. join( " ", (@cmdPara) ) . ' ' . join( ":noArg ", @cmdNoPara ) . ':noArg ';
  201. my $usage = "Unknown argument $a[1], choose one of " . $strAllCommands;
  202. # we need at least one argument
  203. return $usage if ( @a < 2 );
  204. my $cmd = $a[1];
  205. if ( $cmd eq "?" )
  206. {
  207. return $usage;
  208. }
  209. my $value = $a[2];
  210. # is command defined ?
  211. if ( ( grep { /$cmd/ } @allCommands ) <= 0 )
  212. {
  213. MaxScanner_Log $hash, 2, "cmd:$cmd no match for : @allCommands";
  214. return return "unknown command : $cmd";
  215. }
  216. # need we a parameter ?
  217. my $hits = scalar grep { /$cmd/ } @cmdNoPara;
  218. my $needPara = ( $hits > 0 ) ? '' : 1;
  219. MaxScanner_Log $hash, 4, "hits: $hits needPara:$needPara";
  220. # if parameter needed, it must be an integer
  221. return "Value must be an integer"
  222. if ( $needPara && !( $value =~ m/$reINT/ ) );
  223. # command run
  224. if ( $cmd eq "run" )
  225. {
  226. MaxScanner_Timer($name) if ( $hash->{helper}{initDone} );
  227. }
  228. return undef;
  229. }
  230. ##########################
  231. # handling of notifies
  232. sub MaxScanner_Notify($$$)
  233. {
  234. my ( $hash, $dev ) = @_;
  235. my $name = $hash->{NAME};
  236. my $disable = AttrVal( $name, 'disable', '0' );
  237. if ( grep( m/^(INITIALIZED)$/, @{ $dev->{CHANGED} } ) )
  238. {
  239. MaxScanner_Log( $hash, 4, 'INITIALIZED' );
  240. MaxScanner_RestartTimer($hash,20);
  241. return;
  242. }
  243. # no action if not initialized
  244. return if ( !$hash->{helper}{initDone} );
  245. # no action if disabled
  246. return if ( $disable eq '1' );
  247. my $devName = $dev->{NAME};
  248. #MaxScanner_Log $hash, 5, 'start: '.$devName;
  249. # get associated devices
  250. my @associated = @{ $hash->{helper}{associatedDevices} };
  251. # if not found return
  252. if ( !grep( /^$devName/, @associated ) )
  253. {
  254. return;
  255. }
  256. # get the event of the device
  257. my $devReadings = int( @{ $dev->{CHANGED} } );
  258. MaxScanner_Log $hash, 5, 'is associated: ' . $devName . ' check readings:' . $devReadings;
  259. my $found = '';
  260. my $xevent = '';
  261. for ( my $i = 0 ; $i < $devReadings ; $i++ )
  262. {
  263. # <onoff: 0> , <desiredTemperature: 12.0>
  264. $xevent = $dev->{CHANGED}[$i];
  265. $xevent = '' if ( !defined($xevent) );
  266. #MaxScanner_Log $hash, 4, 'check event:<'.$xevent.'>';
  267. if ( $xevent =~ m/^(onoff|desiredTemperature|temperature):.*/ )
  268. {
  269. MaxScanner_Log $hash, 4, 'matching event:<' . $xevent . '>';
  270. $found = '1';
  271. last;
  272. }
  273. }
  274. # return if no matching with intersting properties
  275. return if ( !$found );
  276. # loop over all instances of scanner
  277. foreach my $instName ( sort keys %{ $modules{$MaxScanner_ModulName}{defptr} } )
  278. {
  279. my $instHash = $defs{$instName};
  280. MaxScanner_Log $instHash, 3, 'will start <' . $instName . '> triggerd by ' . $devName . ' ' . $xevent;
  281. MaxScanner_Timer($instName);
  282. }
  283. }
  284. ##########################
  285. # Gets the summary value of associated shutter contacts
  286. sub MaxScanner_GetShutterValue($)
  287. {
  288. my ($thermHash) = @_;
  289. my $retval = 0;
  290. # if no shutters exist
  291. if ( !defined( $thermHash->{helper}{shutterContacts} ) )
  292. {
  293. return $retval;
  294. }
  295. # get the array
  296. my @shuttersTemp = @{ $thermHash->{helper}{shutterContacts} };
  297. # loop over all shutters
  298. foreach my $shutterName (@shuttersTemp)
  299. {
  300. my $windowIsOpen = ReadingsVal( $shutterName, "onoff", 0 );
  301. MaxScanner_Log $thermHash, 5, $shutterName . ' onoff:' . $windowIsOpen;
  302. if ( $windowIsOpen > 0 )
  303. {
  304. $retval = 1;
  305. last;
  306. }
  307. }
  308. MaxScanner_Log $thermHash, 5, 'retval:' . $retval;
  309. return $retval;
  310. }
  311. ##########################
  312. # looks for shutterContacts for the given thermostat
  313. sub MaxScanner_ShutterCheck($$)
  314. {
  315. my ( $modHash, $thermHash ) = @_;
  316. my $thermName = $thermHash->{NAME};
  317. # get the list of associated shutter contacts
  318. my $strShutterNameList = AttrVal( $thermName, $MaxScanner_AttrShutterList, "?" );
  319. if ( $strShutterNameList eq '?' )
  320. {
  321. MaxScanner_Log $thermHash, 5,
  322. $thermName . ': found no definition for ' . $MaxScanner_AttrShutterList . ' got ' . $strShutterNameList;
  323. return;
  324. }
  325. #MaxScanner_Log $thermHash, 5, "found shutter definition list : ".$strShutterNameList;
  326. my @shutters;
  327. my @shuttersTemp = split( /,/, $strShutterNameList );
  328. #MaxScanner_Log $thermHash, 5, "shuttersTemp : ".join(',', @shuttersTemp);
  329. # validate each shutter contact
  330. foreach my $shutterName (@shuttersTemp)
  331. {
  332. #MaxScanner_Log $thermHash, 5, 'check shuttersTemp : '.$shutterName;
  333. # ignore empty strings
  334. if ( $shutterName eq '' )
  335. {
  336. next;
  337. }
  338. # ignore duplicated names
  339. if ( grep( /^$shutterName/, @shutters ) )
  340. {
  341. next;
  342. }
  343. # ignore unknown devices
  344. my $hash = $defs{$shutterName};
  345. if ( !$hash )
  346. {
  347. MaxScanner_Log $thermHash, 4, "unknown device : " . $shutterName;
  348. next;
  349. }
  350. # device is not a shutter contact
  351. if ( $hash->{type} ne 'ShutterContact' )
  352. {
  353. MaxScanner_Log $thermHash, 2, "device is not a shutter contact : " . $shutterName;
  354. next;
  355. }
  356. #MaxScanner_Log $thermHash, 5, 'accept shuttersTemp : '.$shutterName;
  357. push @shutters, $shutterName;
  358. }
  359. MaxScanner_Log $thermHash, 4, "accepted following shutters : " . join( ",", @shutters );
  360. $thermHash->{helper}{shutterContacts} = [@shutters];
  361. }
  362. ##########################
  363. # looks for MAX components
  364. # called by Run
  365. sub MaxScanner_Find($)
  366. {
  367. my ($modHash) = @_;
  368. my $modName = $modHash->{NAME};
  369. my $numValidThermos = 0;
  370. my @shutterContacts = ();
  371. #------------------ look for all max-thermostats
  372. $modHash->{helper}{thermostats} = ();
  373. # loop over all max thermostats
  374. foreach my $aaa ( sort keys %{ $modules{MAX}{defptr} } )
  375. {
  376. my $hash = $modules{MAX}{defptr}{$aaa};
  377. # type must exist
  378. # it seems, that maxlan environment holds some foreign entries
  379. # see http://forum.fhem.de/index.php/topic,11624.msg390975.html#msg390975
  380. if ( !defined( $hash->{type} ) )
  381. {
  382. MaxScanner_Log $modHash, 5, 'missing property type for device: ' . $aaa;
  383. next;
  384. }
  385. # exit if it is not a HeatingThermostat
  386. next if $hash->{type} !~ m/^HeatingThermostat.*/;
  387. # basic properties are reqired
  388. if ( !defined( $hash->{IODev} )
  389. || !defined( $hash->{NAME} ) )
  390. {
  391. MaxScanner_Log $modHash, 1, 'missing property IODEV or NAME for device: ' . $aaa;
  392. next;
  393. }
  394. #.
  395. # name of the max device
  396. my $name = $hash->{NAME};
  397. # MaxScanner_Log $modHash, 5, $name . " is HeatingThermostat";
  398. # thermostat must be enabled for the scanner
  399. if ( AttrVal( $name, $MaxScanner_AttrEnabled, '?' ) ne '1' )
  400. {
  401. MaxScanner_Log $modHash, 5,
  402. $name . ' ' . $MaxScanner_AttrEnabled . ' is not active, therefore this device is ignored';
  403. next;
  404. }
  405. # MaxScanner_Log $modHash, 5, $name . ' is enabled for scanner';
  406. # check special user attributes, if not exists, create them
  407. my $xattr = AttrVal( $name, 'userattr', '' );
  408. if ( !( $xattr =~ m/$MaxScanner_AttrShutterList/ ) )
  409. {
  410. # extend user attributes for scanner module
  411. my $scnCommands = $xattr . " " . join( " ", @MaxScanner_AttrForMax );
  412. my $fhemCmd = "attr $name userattr $scnCommands";
  413. fhem($fhemCmd);
  414. MaxScanner_Log $modHash, 4, $name . " initialized userAttributes";
  415. }
  416. # with keepAuto=1 Scanner cannot cooperate
  417. if ( AttrVal( $name, 'keepAuto', '0' ) ne '0'
  418. && AttrVal( $name, 'scnProcessByDesiChange', '0' ) eq '0' )
  419. {
  420. MaxScanner_Log $modHash, 0, $name . 'don\'t use keepAuto in conjunction with changeMode processing !!!';
  421. next;
  422. }
  423. MaxScanner_Log $modHash, 5, $name . " is accepted";
  424. $numValidThermos++;
  425. # check for shutter contacts
  426. MaxScanner_ShutterCheck( $modHash, $hash );
  427. # if there exist shuttercontacts
  428. if ( defined( $hash->{helper}{shutterContacts} ) )
  429. {
  430. # build sum of all sc's
  431. push( @shutterContacts, @{ $hash->{helper}{shutterContacts} } );
  432. MaxScanner_Log $modHash, 5, "shutterContacts : " . join( ",", @shutterContacts );
  433. }
  434. # create helper reading or thermostat
  435. $hash->{helper}{NextScan} = int( gettimeofday() )
  436. if ( !defined( $hash->{helper}{NextScan} ) );
  437. # this is needed for sorting later
  438. $modHash->{helper}{thermostats}{$name} = $hash->{helper}{NextScan};
  439. }
  440. # remove duplicates
  441. my %shutterHash = map { $_ => 1 } @shutterContacts;
  442. @shutterContacts = keys %shutterHash;
  443. # $modHash->{helper}{shutterContacts} = [@shutterContacts];
  444. my @thermos = keys %{ $modHash->{helper}{thermostats} };
  445. my @allAssociatedDevices = ( @shutterContacts, @thermos );
  446. $modHash->{helper}{associatedDevices} = [@allAssociatedDevices];
  447. }
  448. ##########################################################
  449. # return a hash with useful infos relating to weekprofile
  450. sub MaxScanner_WeekProfileInfo($)
  451. {
  452. my ($name) = @_;
  453. my %result = ();
  454. my $loopCount = 0;
  455. $result{desired} = undef;
  456. # return if ($name ne 'HT.JOHN'); # !!!
  457. my $hash = $defs{$name};
  458. if ( !$hash )
  459. {
  460. return undef;
  461. }
  462. my %dayNames = (
  463. 0 => "Sat",
  464. 1 => "Sun",
  465. 2 => "Mon",
  466. 3 => "Tue",
  467. 4 => "Wed",
  468. 5 => "Thu",
  469. 6 => "Fri"
  470. );
  471. MaxScanner_Log $hash, 5, "----- Start ---------";
  472. my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime(time);
  473. # determine the current weekday
  474. my $maxWday = $wday + 1;
  475. # 7 equals 0 for max
  476. $maxWday = 0 if ( $maxWday == 7 );
  477. # determine next weekday
  478. my $WdayNext = $maxWday + 1;
  479. $WdayNext = 0 if ( $WdayNext == 7 );
  480. # get the reading names for wanted readings
  481. my $profileTime = "weekprofile-$maxWday-$dayNames{$maxWday}-time";
  482. my $profileTemp = "weekprofile-$maxWday-$dayNames{$maxWday}-temp";
  483. my $profileTempNext = "weekprofile-$WdayNext-$dayNames{$WdayNext}-temp";
  484. # get desired-profile for the current day
  485. # example:weekprofile-0-Sat-temp 20.0 °C / 21.0 °C / 21.0 °C / 21.0 °C / 20.0 °C / 20.0 °C
  486. my $ltemp = ReadingsVal( $name, $profileTemp, '' );
  487. # get profile for the next day
  488. my $ltempNext = ReadingsVal( $name, $profileTempNext, '' );
  489. MaxScanner_Log $hash, 5, "$profileTemp:$ltemp $profileTempNext:$ltempNext";
  490. # current and next must be defined
  491. if ( !$ltemp || !$ltempNext )
  492. {
  493. return undef;
  494. }
  495. # read profileTime
  496. # 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
  497. my $lProfileTime = ReadingsVal( $name, $profileTime, '' );
  498. # must be defined
  499. if ( !$lProfileTime )
  500. {
  501. return undef;
  502. }
  503. MaxScanner_Log $hash, 5, "$profileTime:$lProfileTime";
  504. # split desired-value via slash
  505. my @tempArr = split( '/', $ltemp );
  506. # the same for the next profile-step
  507. my @tempArrNext = split( '/', $ltempNext );
  508. # prepare array for desired-values
  509. ${ result { tempArr } } = ();
  510. # store all desired values of the day to an array
  511. $loopCount = 1;
  512. for my $ss (@tempArr)
  513. {
  514. # extract temperature by looking for number
  515. my ($xval) = ( $ss =~ /(\d+\.\d+)/ );
  516. MaxScanner_Log $hash, 5, "desi-Temp No. $loopCount:$xval";
  517. push( @{ $result{tempArr} }, $xval );
  518. $loopCount++;
  519. }
  520. # extract first temperature of the next day
  521. my ($xval) = ( $tempArrNext[0] =~ /(\d+\.\d+)/ );
  522. push( @{ $result{tempArr} }, $xval );
  523. MaxScanner_Log $hash, 5, "temp next day:$xval";
  524. # analyze the time-periods of the profile
  525. #00:00-08:00 / 08:00-22:00 / 22:00-00:00
  526. my @atime = split( '/', $lProfileTime );
  527. # create serial form of current date
  528. my $xDate = mktime( 0, 0, 0, $mday, $mon, $year );
  529. my @t = localtime($xDate);
  530. $xDate = sprintf( "%02d:%02d", $t[2], $t[1] );
  531. # split profile-time using slash as splitter
  532. # and remove supernummery spaces ==> 00:00-23:55 23:55-00:00
  533. my @btime = split( '\s*/\s*', $lProfileTime );
  534. # MaxScanner_Log $hash,5,"profile-time:@atime xDate:$xDate";
  535. MaxScanner_Log $hash, 5, "profile-time:@btime";
  536. my $curTime = gettimeofday();
  537. my $count = 0;
  538. # prepeare array
  539. @{ $result{timeArr} } = ();
  540. # loop over all time-slots
  541. $result{tempFound} = 0;
  542. for my $ss (@btime)
  543. {
  544. # extract start-time and stop-time
  545. my ( $a1, $a2 ) = ( $ss =~ /(\d+:\d+)\-(\d+:\d+)/ );
  546. if ( !defined($a2) )
  547. {
  548. MaxScanner_Log $hash, 2, "$name a2 not defined for $ss";
  549. next;
  550. }
  551. # adjust stop-time when special time 24:00
  552. $a2 = '24:00' if ( $a2 eq "00:00" ); # ende anpassen
  553. # extract hour and minute of stop-time
  554. my ( $xhour, $xmin ) = ( $a2 =~ /(\d+):(\d+)/ );
  555. # create serial date
  556. $xDate = mktime( 0, $xmin, $xhour, $mday, $mon, $year );
  557. # create string form
  558. my $sDate = FmtDateTime($xDate);
  559. MaxScanner_Log $hash, 5, "stopDate:$sDate segment-count:$count";
  560. # store the stop time to result-container
  561. push( @{ $result{timeArr} }, $xDate );
  562. # if current time > found stop-date
  563. if ( $curTime > $xDate )
  564. { # mark the last found segment
  565. $result{tempFound} = $count + 1;
  566. MaxScanner_Log $hash, 5, "segment-count:$count found with " . FmtDateTime($xDate);
  567. }
  568. $count = $count + 1;
  569. }
  570. # prepare the hash
  571. # stop-time for the current time-slot
  572. $result{nextSwitchDate} = @{ $result{timeArr} }[ $result{tempFound} ];
  573. # desired for the next time slot
  574. $result{nextDesired} = @{ $result{tempArr} }[ $result{tempFound} + 1 ];
  575. # desired for the current time slot
  576. $result{desired} = @{ $result{tempArr} }[ $result{tempFound} ];
  577. # desired must be defined
  578. if ( !defined( $result{desired} ) )
  579. {
  580. MaxScanner_Log $hash, 2, "$name: desired not defined";
  581. return undef;
  582. }
  583. MaxScanner_Log $hash, 4, "tempFound-Idx :" . $result{tempFound};
  584. MaxScanner_Log $hash, 4, "nextSwitchDate:" . FmtDateTime( $result{nextSwitchDate} );
  585. MaxScanner_Log $hash, 4, "desired :" . $result{desired};
  586. MaxScanner_Log $hash, 4, "nextDesired :$result{nextDesired}";
  587. return \%result;
  588. }
  589. ######################################################
  590. # loop over all thermostats and check what is to do
  591. sub MaxScanner_Work($$$)
  592. {
  593. my $reUINT = '^([\\+]?\\d+)$'; # uint without whitespaces
  594. my ( $modHash, $thermi_sort, $numWorkIntervall ) = @_;
  595. my $isCul = '';
  596. my $settingDone = ''; # end loop if a set command was performed
  597. my @scan_time;
  598. my $modName = $modHash->{NAME};
  599. my $boolSimulateJohn = '';
  600. # loop the sorted list over enabled thermostats
  601. foreach my $therm (@$thermi_sort)
  602. {
  603. #MaxScanner_Log $modHash, 3, Dumper($therm);
  604. my $hash = $defs{$therm};
  605. my $sdCurTime = gettimeofday(); #serial date of current date
  606. my $strCurTime = FmtDateTime($sdCurTime);
  607. my $boolDesiChange = AttrVal( $therm, $MaxScanner_AttrProcessByDesiChange, '0' ) eq '1';
  608. my $strModeHandling = uc AttrVal( $therm, $MaxScanner_AttrModeHandling, 'AUTO' );
  609. my $dontChangeMe = '';
  610. #. check timestamp of the reading temperature
  611. my $strTempTime = ReadingsTimestamp( $therm, 'temperature', '' );
  612. if ( $strTempTime eq "" )
  613. {
  614. MaxScanner_Log $hash, 1, '!! READING:temperature is not defined !!';
  615. next;
  616. }
  617. # get desired timestamp
  618. my $strDesiTime = ReadingsTimestamp( $therm, 'desiredTemperature', '' );
  619. # get next scan serial date
  620. my $sdNextScan = $hash->{helper}{NextScan};
  621. MaxScanner_Log $hash, 4,
  622. 'ns:' . FmtDateTime($sdNextScan) . ' strDesiTime:' . $strDesiTime . ' Is Mode DesicChange:' . $boolDesiChange;
  623. # convert temperature time into serial format
  624. my $sdTempTime = time_str2num($strTempTime);
  625. # convert desired timestamp into serial format if possible, otherwise use current time
  626. my $sdDesiTime = ($strDesiTime) ? time_str2num($strDesiTime) : gettimeofday();
  627. #. check Cul
  628. my $strCulName;
  629. my $strCreditTime = '';
  630. my $numCulCredits;
  631. my $numDutyCycle = '?';
  632. my $strIOHash = $defs{$therm}{IODev}; # CULMAX0, hash of IO-Devices
  633. my $strIOName = $strIOHash->{NAME};
  634. my $strIOType = $strIOHash->{TYPE}; # CUL_MAX,MAXLAN type des IO-Devices
  635. #.
  636. MaxScanner_Log $hash, 4, 'TYPE:'.$strIOType.' IOName:'.$strIOName.' simCube:'.$boolSimulateJohn;
  637. # if com-device is a MAXLAN
  638. if ( $strIOType eq "MAXLAN" )
  639. {
  640. # determine name of IO devices
  641. $strCulName = $strIOName;
  642. # get dutycycle
  643. my $strDutyCycle = ReadingsVal( $strCulName, 'dutycycle', '?' );
  644. # if not a number try to get it via internal value
  645. $strDutyCycle = InternalVal( $strCulName, 'dutycycle', 0 )
  646. if ( $strDutyCycle eq "?" );
  647. # get the timestamp of reading dutycycle
  648. $strCreditTime = ReadingsTimestamp( $strCulName, 'dutycycle', '' );
  649. # take the middle term of ...
  650. my ( $a1, $a2, $a3 ) = ( $strDutyCycle =~ /([\s]*)(\d+)(.*)/ );
  651. if ( defined($a2) )
  652. {
  653. $numDutyCycle = $a2;
  654. } else
  655. {
  656. $numDutyCycle = 100;
  657. MaxScanner_Log $hash, 2, '!! dutycyle not a number: <' . $strDutyCycle . '>; force to 100';
  658. }
  659. # transform dutycycle to CulCredits
  660. $numCulCredits = ( 100 - $numDutyCycle ) * 10;
  661. $isCul = '';
  662. }
  663. # we got a CUL
  664. else
  665. {
  666. # determine name of IO devices
  667. $strCulName = $strIOHash->{IODev}{NAME};
  668. # get the credit's timestamp
  669. $strCreditTime = ReadingsTimestamp( $strCulName, 'credit10ms', '' );
  670. # get the credits
  671. $numCulCredits = ReadingsVal( $strCulName, 'credit10ms', 0 );
  672. # force dynamic scanning for CUL
  673. $isCul = '1';
  674. }
  675. # simulate cube
  676. if ($boolSimulateJohn && $therm eq 'HT.JOHN')
  677. {
  678. $isCul = '';
  679. MaxScanner_Log $hash, 4, '!! Simulate cube with HT.JOHN isCul:'.$isCul;
  680. }
  681. # because cube not knows msgcnt, we fix the timestamp
  682. my $strLastTransmit =
  683. ($isCul) ? ReadingsTimestamp( $therm, 'msgcnt', '' ) : FmtDateTime( gettimeofday() - 20 );
  684. # msgcnt must exist
  685. if ( $strLastTransmit eq '' )
  686. {
  687. MaxScanner_Log $hash, 1, '!! Reading:msgcnt is not defined';
  688. next;
  689. }
  690. # convert timestamp lastTransmit to serial date
  691. my $sdLastTransmit = time_str2num($strLastTransmit);
  692. MaxScanner_Log $hash, 4,
  693. "CulName:$strCulName CulCredits:$numCulCredits " . "CreditTime:$strCreditTime dutyCycle:$numDutyCycle";
  694. # somtimes we get "no answer" instead of a number
  695. if ( !( $numCulCredits =~ m/$reUINT/ ) )
  696. {
  697. MaxScanner_Log $hash, 1, '!! credit10ms/dutycycle must be a number';
  698. next;
  699. }
  700. # creditTime must exist
  701. if ( $strCreditTime eq '' )
  702. {
  703. MaxScanner_Log $hash, 1, '!! READINGS:credit10ms is not defined';
  704. next;
  705. }
  706. # convert credit time to serial date
  707. my $sdCreditTime = time_str2num($strCreditTime);
  708. # get current desired temperature
  709. my $numDesiTemp = ReadingsVal( $therm, 'desiredTemperature', '' );
  710. if ( $numDesiTemp eq 'on' || $numDesiTemp eq 'off' ) #Hint by MrHeat
  711. {
  712. MaxScanner_Log $hash, 3, 'reading desiredTemperature: thermostat is forced on/off. Skipping thermostat';
  713. next;
  714. }
  715. # desi temp must be a number
  716. elsif ( $numDesiTemp eq '' )
  717. {
  718. MaxScanner_Log $hash, 1, '!! reading desiredTemperature is not available';
  719. next;
  720. }
  721. # get current mode
  722. my $strMode = ReadingsVal( $therm, 'mode', '' );
  723. # current mode must be defined
  724. if ( $strMode eq "" )
  725. {
  726. MaxScanner_Log $hash, 1, '!! reading mode is not available';
  727. next;
  728. }
  729. # get weekprofile-Info
  730. my $weekProfile = MaxScanner_WeekProfileInfo($therm);
  731. # must be defined
  732. if ( !defined($weekProfile) )
  733. {
  734. MaxScanner_Log $hash, 1, '!! weekprofile is not available';
  735. next;
  736. }
  737. # don't change mode if the latency is active; only cul is affected
  738. if ( $sdLastTransmit + 5 >= $sdCurTime && $isCul )
  739. {
  740. MaxScanner_Log $hash, 4, 'no action due transmission latency';
  741. next;
  742. }
  743. # get desired of weekprofile
  744. my $normDesiTemp = $weekProfile->{desired};
  745. # get window-open temperature
  746. my $numWinOpenTemp = ReadingsVal( $therm, 'windowOpenTemperature', '-1' );
  747. # get the additional credits calculated from the elapsed time
  748. my $numCreditDiff = ( $sdCurTime - $sdCreditTime );
  749. my $numCreditThreshold = AttrVal( $modName, $MaxScanner_AttrCreditThreshold, $MaxScanner_DefaultCreditThreshold );
  750. # calculate resulting credits
  751. my $numCredit = $numCulCredits + $numCreditDiff;
  752. # limit the result
  753. $numCredit = 900 if ( $numCredit > 900 );
  754. MaxScanner_Log $hash, 4,
  755. 'CulCredits:'
  756. . $numCulCredits
  757. . ' Credits:'
  758. . int($numCredit)
  759. . ' isCul:'
  760. . $isCul
  761. . ' CreditThreshold:'
  762. . $numCreditThreshold;
  763. # determine next scan time depending on the time of last scan
  764. my $sdNextScanOld = $sdNextScan;
  765. # preset the minimal timestamp:
  766. my $nextPlan = $sdNextScan;
  767. # if dynamic scanning
  768. if ($isCul)
  769. {
  770. # 17 secs before next scan time
  771. $nextPlan = $sdTempTime + $numWorkIntervall * 60 - 17;
  772. }
  773. # static scanning (CUBE)
  774. else
  775. {
  776. $nextPlan = $sdNextScan + $numWorkIntervall * 60;
  777. }
  778. # adjust the next scantime until it is in future
  779. $nextPlan = $nextPlan + ( 60 * $MaxScanner_BaseIntervall ) while ( $sdCurTime > $nextPlan );
  780. $sdNextScan = $nextPlan;
  781. MaxScanner_Log $hash, 4, 'ns:' . FmtTime($sdNextScan) . ' nsOld:' . FmtTime($sdNextScanOld);
  782. # basic inits if thermostat if not not already done
  783. if ( !defined( $hash->{helper}{TemperatureTime} ) )
  784. {
  785. MaxScanner_Log $hash, 4, 'create helpers with ns:' . FmtDateTime($sdNextScan);
  786. $hash->{helper}{TemperatureTime} = $sdTempTime; # timestamp of the last receive of temperature
  787. $hash->{helper}{DesiTime} = $sdDesiTime; # timestamp of the last receive of desired
  788. $hash->{helper}{WinWasOpen} = 0;
  789. $hash->{helper}{TempBeforeWindOpen} = $numDesiTemp;
  790. # $hash->{helper}{LastWasAutoReset} = '';
  791. $hash->{helper}{leadDesiTemp} = ($boolDesiChange) ? $normDesiTemp : $numDesiTemp;
  792. $hash->{helper}{desiredOffset} = ($boolDesiChange) ? $numDesiTemp - $normDesiTemp : 0;
  793. $hash->{helper}{switchDate} = undef;
  794. $hash->{helper}{LastCmdDate} = $sdCurTime;
  795. $hash->{helper}{gotTempTS} = '';
  796. }
  797. # gather the timestamp for next profile switch
  798. my $switchDate = ( defined($weekProfile) ) ? $weekProfile->{nextSwitchDate} : $sdDesiTime;
  799. # create a helper if not already done
  800. $hash->{helper}{switchDate} = $switchDate
  801. if ( !defined( $hash->{helper}{switchDate} ) );
  802. # if switchDate is changed, then adjust leading desired
  803. if ( $hash->{helper}{switchDate} != $switchDate )
  804. {
  805. $hash->{helper}{gotTempTS} = '';
  806. $hash->{helper}{switchDate} = $switchDate;
  807. $hash->{helper}{leadDesiTemp} = $normDesiTemp;
  808. $hash->{helper}{TempBeforeWindOpen} = $normDesiTemp; # MrHeat
  809. $hash->{helper}{desiredOffset} = 0;
  810. MaxScanner_Log $hash, 3, "reset leadDesiTemp:" . $hash->{helper}{leadDesiTemp}.' Mode:'.$strMode;
  811. # when triggermode ModeChange and mode is manual, we must switch to auto to force the new setpoint/desired
  812. if ( !$boolDesiChange && ( $strMode eq 'manual' ) && ( $normDesiTemp != $numDesiTemp ) )
  813. {
  814. my $cmd = "set $therm desiredTemperature auto";
  815. fhem($cmd);
  816. $hash->{helper}{LastCmdDate} = $sdCurTime;
  817. $settingDone = 1;
  818. MaxScanner_Log $hash, 3, "switchTime: <<$cmd>>";
  819. }
  820. # force wait time for Cube-devices, after switch date is changed
  821. if (! $isCul)
  822. {
  823. $sdNextScan = $sdCurTime + $numWorkIntervall * 60;
  824. $hash->{helper}{NextScan} = int($sdNextScan);
  825. MaxScanner_Log $hash, 3, 'forward NextScan for Cube-Devices ns:'.FmtDateTime($sdNextScan);
  826. }
  827. # now stop further actions with this thermostat, and wait for activation by the weekprofile
  828. # next;
  829. }
  830. # if mode switch is active, then offset must be 0
  831. if ( !$boolDesiChange && $hash->{helper}{desiredOffset} != 0 )
  832. {
  833. $hash->{helper}{desiredOffset} = 0;
  834. MaxScanner_Log $hash, 4, 'force desiredOffset to 0';
  835. }
  836. # determine nextScan for CUL-like devices
  837. if ($isCul)
  838. {
  839. # if temperature time is younger than old time, then determine nextScan
  840. if ( $sdTempTime != $hash->{helper}{TemperatureTime} )
  841. {
  842. $hash->{helper}{gotTempTS} = 1;
  843. # remember timerstamp
  844. $hash->{helper}{TemperatureTime} = $sdTempTime;
  845. $hash->{helper}{NextScan} = int($sdNextScan);
  846. $hash->{helper}{NextScanTimestamp} =
  847. FmtDateTime( $hash->{helper}{NextScan} );
  848. MaxScanner_Log $hash, 3, 'TEMPERATURE received at ' . $strTempTime . ', ==> new ns:' . FmtDateTime($sdNextScan);
  849. }
  850. }
  851. else
  852. {
  853. # no wait time for cube devices
  854. if ($sdCurTime >= $hash->{helper}{NextScan} && !$hash->{helper}{gotTempTS})
  855. {
  856. $hash->{helper}{gotTempTS} = 1;
  857. MaxScanner_Log $hash, 3, 'TEMPERATURE received is assumed (Cube)';
  858. }
  859. }
  860. # get shutter's state
  861. my $boolWinIsOpenByFK = MaxScanner_GetShutterValue($hash) > 0;
  862. # opened window can also be detected by temperature fall
  863. # Don't change mode, if WindowOpen is recognized by temperature fall
  864. # then desiredTemp=WidowOpenTemp
  865. my $boolWinIsOpenByTempFall = $numDesiTemp == $numWinOpenTemp;
  866. # don't touch the thermostat, if windowOpen is recognized
  867. if ( $boolWinIsOpenByFK || $boolWinIsOpenByTempFall )
  868. {
  869. MaxScanner_Log $hash, 3,
  870. '<<stage 1>> no action due open window; desi-temp before window open:' . $hash->{helper}{TempBeforeWindOpen}
  871. if ($hash->{helper}{WinWasOpen} == 0);
  872. $hash->{helper}{WinWasOpen} = 1;
  873. $dontChangeMe = 1;
  874. #next;
  875. }
  876. # window is closed
  877. else
  878. {
  879. # now window is closed and it was open before
  880. if ( $hash->{helper}{WinWasOpen} > 0 )
  881. {
  882. # ----------- <<stage 1>> it was just closed ---------
  883. if ( $hash->{helper}{WinWasOpen} == 1 )
  884. {
  885. # switch to state 2: we are waiting for desi-temp
  886. $hash->{helper}{WinWasOpen} = 2;
  887. MaxScanner_Log $hash, 3,
  888. "strMode:$strMode DesiTemp:$numDesiTemp TempBeforeWindOpen:" . $hash->{helper}{TempBeforeWindOpen};
  889. # now set in each case desired temperature,
  890. # we expect desired temperature receive and than procede with scanner
  891. # therefore we will get no problem, even there is a delay by command queue
  892. $numCredit -= 110; # therfore our credit counter must be reduced
  893. my $cmd =
  894. "set $therm desiredTemperature "
  895. . ( $strMode eq 'auto' ? 'auto' : '' ) . ' '
  896. . $hash->{helper}{TempBeforeWindOpen}; #MrHeat
  897. fhem($cmd);
  898. $hash->{helper}{LastCmdDate} = $sdCurTime;
  899. MaxScanner_Log $hash, 3, '<<stage 2>>due window is closed: ' . $cmd;
  900. $hash->{helper}{DesiTime} = $sdDesiTime; # remember timestamp of desiTemp
  901. # no further action after changing desired
  902. # abort, due we waiting for feedback of desiTemp
  903. next;
  904. }
  905. # -------- <<stage 2 >> we are waiting for desitemp -----------------
  906. elsif ( $hash->{helper}{WinWasOpen} == 2 )
  907. {
  908. # forward to next step only, if timestamp of desiredTemp is changed
  909. if ( $hash->{helper}{DesiTime} == $sdDesiTime )
  910. {
  911. next;
  912. }
  913. MaxScanner_Log $hash, 3,
  914. '<<stage 3>> received new desiredTemperature after opened window: continue scanning now';
  915. # window open statemachine closed
  916. $hash->{helper}{WinWasOpen} = 0;
  917. }
  918. } else
  919. {
  920. # <<stage 0>> ----------------- window is closed and was closed before
  921. # only notice, if after window was closed desiTemp is received.
  922. $hash->{helper}{TempBeforeWindOpen} = $numDesiTemp;
  923. # calculate expected desiTemp
  924. my $expectedDesiTemp = $hash->{helper}{leadDesiTemp} + $hash->{helper}{desiredOffset};
  925. MaxScanner_Log $hash, 4,
  926. "numDesiTemp:$numDesiTemp expectedDesiTemp:$expectedDesiTemp leadDesiTemp:" . $hash->{helper}{leadDesiTemp};
  927. MaxScanner_Log $hash, 4, "normDesiTemp:$normDesiTemp desiredOffset:" . $hash->{helper}{desiredOffset};
  928. # if the expected value does not match, than desired was changed outside
  929. # but when CUL than only, if we got temperature after a desired change by w-profile
  930. if ( $expectedDesiTemp != $numDesiTemp && $hash->{helper}{gotTempTS} )
  931. {
  932. $hash->{helper}{leadDesiTemp} = $numDesiTemp;
  933. $hash->{helper}{desiredOffset} = 0;
  934. MaxScanner_Log $hash, 3, "change leadDesiTemp due manipulation:" . $hash->{helper}{leadDesiTemp};
  935. }
  936. }
  937. }
  938. # if mode equals boost, the don't change anything
  939. if ( $strMode eq 'boost' )
  940. {
  941. MaxScanner_Log $hash, 3, 'no action due boost';
  942. $dontChangeMe = 1;
  943. #next;
  944. }
  945. # if we perform modeChange and are in auto mode and next scan is near to the profile switch date
  946. # then do not perform switch, because the profile should change the desired just in time
  947. if (!$boolDesiChange
  948. && $strMode eq 'auto'
  949. && $sdNextScan >= $weekProfile->{nextSwitchDate} - 60 )
  950. {
  951. if ($isCul)
  952. {
  953. $hash->{helper}{NextScan} = $weekProfile->{nextSwitchDate} + 60;
  954. }
  955. else {
  956. $hash->{helper}{NextScan} = $weekProfile->{nextSwitchDate} + 60*3+10;
  957. }
  958. my $ss = FmtDateTime( $hash->{helper}{NextScan} );
  959. $hash->{helper}{NextScanTimestamp} = $ss;
  960. MaxScanner_Log $hash, 3, 'no action due soon a week-profile switch point is reached ns:' . $ss;
  961. $dontChangeMe = 1;
  962. }
  963. #---------------
  964. # next; # !!!
  965. #---------------
  966. MaxScanner_Log $hash, 4, "Trigger Mode Desi-Change:$boolDesiChange ";
  967. # if scan time is exceeded and no other setting was done,
  968. # we check to trigger the thermostat
  969. if ( !$dontChangeMe
  970. && !$settingDone
  971. && ( $sdCurTime >= $hash->{helper}{NextScan} ) )
  972. {
  973. # in each case store NextScan, this is the preliminary scan time,
  974. # if there are not enough credits
  975. # if we can transmit, the timestamp for NextScan will be again set ,
  976. # after receiving of temperature
  977. $hash->{helper}{NextScan} = int($sdNextScan);
  978. # if we got enough credits, so we can trigger the thermostat
  979. if ( $numCredit >= $numCreditThreshold )
  980. {
  981. # the estimated reduction of credits after execution of a trigger
  982. $numCredit -= 110;
  983. my $cmd;
  984. my $leadDesiTemp = $hash->{helper}{leadDesiTemp};
  985. my $desiOffset = $hash->{helper}{desiredOffset};
  986. # trigger thermostat by changing the desired temperature
  987. if ($boolDesiChange)
  988. {
  989. # perform trigger with offest and determin it
  990. if ( $desiOffset == 0 )
  991. {
  992. # calc the difference between current and desired temperature
  993. my $currentTemp = ReadingsVal( $therm, 'temperature', $normDesiTemp );
  994. my $diff = $normDesiTemp - $currentTemp;
  995. # calc the offset
  996. if ( $diff >= 0 ) # soll > ist
  997. {
  998. $desiOffset = 0.5;
  999. } else
  1000. { # soll < ist
  1001. $desiOffset = -0.5;
  1002. }
  1003. }
  1004. # perform trigger without offset
  1005. else
  1006. {
  1007. # force to zero
  1008. $desiOffset = 0;
  1009. }
  1010. # calc the target desi temp
  1011. my $newTemp = $leadDesiTemp + $desiOffset;
  1012. # use current mode for default
  1013. my $setMode = ( $strMode eq 'manual' ) ? '' : 'auto';
  1014. if ( $strModeHandling eq 'AUTO' )
  1015. {
  1016. $setMode = 'auto';
  1017. } elsif ( $strModeHandling eq 'MANUAL' )
  1018. {
  1019. $setMode = '';
  1020. }
  1021. $cmd = "set $therm desiredTemperature $setMode $newTemp";
  1022. }
  1023. # trigger thermostat by changing of mode
  1024. else
  1025. {
  1026. my $modeCommand = ( $strMode eq 'manual' ) ? 'auto' : '';
  1027. $cmd = "set $therm desiredTemperature " . $modeCommand . " $leadDesiTemp";
  1028. # MaxScanner_Log $hash, 5, 'cmd:'.$cmd.' modeCommand:'.$modeCommand.' strMode:'.$strMode
  1029. }
  1030. # exec command, at least 180 seconds after last command send
  1031. if ( $sdCurTime > $hash->{helper}{LastCmdDate} + 180 )
  1032. {
  1033. fhem($cmd);
  1034. MaxScanner_Log $hash, 3, "<<$cmd>>";
  1035. $hash->{helper}{LastCmdDate} = $sdCurTime;
  1036. $hash->{helper}{desiredOffset} = $desiOffset;
  1037. # mark execution of a command, to shortcut the loop later
  1038. $settingDone = 1;
  1039. } else
  1040. {
  1041. MaxScanner_Log $hash, 3, ' Wait at least 180 sec . after last command';
  1042. }
  1043. # if we are using CUL, then dynamic scanning
  1044. if ($isCul)
  1045. {
  1046. $hash->{helper}{NextScan} = int( $sdCurTime + 60 );
  1047. } else # if CUBE
  1048. {
  1049. $hash->{helper}{NextScan} = int( $sdCurTime + $numWorkIntervall * 60 );
  1050. }
  1051. }
  1052. # there are to less credits or other preventing reasons, so we have to wait
  1053. else
  1054. {
  1055. # determine the waiting time
  1056. my $numDiffCredit = $numCreditThreshold - $numCredit;
  1057. my $numDiffTime = 0;
  1058. # the waiting time must be greater then the needed credits
  1059. # and must be a multiple of the baseinterval
  1060. while ( $numDiffCredit > $numDiffTime )
  1061. {
  1062. $numDiffTime += ( 60 * $MaxScanner_BaseIntervall );
  1063. }
  1064. # adjust, so the check is called, before the calculated scan time is running out
  1065. $sdNextScan += $numDiffTime - ( 60 * $MaxScanner_BaseIntervall );
  1066. $hash->{helper}{NextScan} = int($sdNextScan);
  1067. MaxScanner_Log $hash, 3,
  1068. ' not enough credits( '
  1069. . int($numCredit)
  1070. . ' ) need '
  1071. . int($numDiffCredit)
  1072. . "/$numDiffTime ns:"
  1073. . FmtDateTime($sdNextScan);
  1074. # move the timestamp of all thermostats, which follows on the current this ensures the round robin rule
  1075. foreach my $thAdjust (@$thermi_sort)
  1076. {
  1077. # if the timestamp is younger then the timestamp of the current thermostat, move it
  1078. if ( $defs{$thAdjust}{helper}{NextScan} < $hash->{helper}{NextScan} )
  1079. {
  1080. # adjust the timestamp
  1081. $defs{$thAdjust}{helper}{NextScan} += int($numDiffTime);
  1082. # string representation of nextScan
  1083. my $ss = FmtDateTime( $defs{$thAdjust}{helper}{NextScan} );
  1084. $defs{$thAdjust}{helper}{NextScanTimestamp} = $ss;
  1085. MaxScanner_Log $hash, 3, "adjust $thAdjust to $ss";
  1086. }
  1087. }
  1088. }
  1089. }
  1090. # nothing is to do, so we wait
  1091. else
  1092. {
  1093. MaxScanner_Log $hash, 4, ' WAITING ... ns : ' . FmtTime( $hash->{helper}{NextScan} );
  1094. }
  1095. # store NextScan in an array, for optimized timer setup
  1096. push( @scan_time, $hash->{helper}{NextScan} );
  1097. MaxScanner_Log $hash, 5, '++++++++ ';
  1098. # foreach thermostat
  1099. }
  1100. # calculate the value for the timer
  1101. # sort the trigger times of the thermostats
  1102. my @scan_time_sort = sort @scan_time;
  1103. # minimal time difference
  1104. my $numDiffTime = 5;
  1105. my $numCurTime = int( gettimeofday() );
  1106. # if we got at least one thermostat
  1107. if ( @scan_time_sort >= 1 )
  1108. {
  1109. # use the scanTime with the smallest value
  1110. my $diff = $scan_time_sort[0] - $numCurTime;
  1111. # minimal difference
  1112. $diff = 2 if ( $diff < 2 );
  1113. if ( $diff > 2 )
  1114. {
  1115. $numDiffTime = int($diff);
  1116. MaxScanner_Log $modHash, 3, ' next scan in seconds : ' . $numDiffTime;
  1117. }
  1118. }
  1119. # return the waiting time in seconds
  1120. return $numDiffTime;
  1121. }
  1122. ##########################
  1123. sub MaxScanner_Run($)
  1124. {
  1125. my ($name) = @_;
  1126. my $hash = $defs{$name};
  1127. my $reUINT = '^([\\+]?\\d+)$';
  1128. my $numValidThermos = 0;
  1129. my $nn = $MaxScanner_BaseIntervall;
  1130. my $numMinInterval = ( AttrVal( $name, 'scnMinInterval', $nn ) =~ m/$reUINT/ ) ? $1 : $nn;
  1131. #.
  1132. my $retVal = 5;
  1133. # loop forever
  1134. while (1)
  1135. {
  1136. # find all thermostats
  1137. MaxScanner_Find($hash);
  1138. my $thermos = $hash->{helper}{thermostats};
  1139. if ( !$hash->{helper}{initDone} )
  1140. {
  1141. $hash->{helper}{initDone} = 1;
  1142. MaxScanner_Log $hash, 4, "init done";
  1143. }
  1144. # sort the thermostats concering the nextScan timestamp
  1145. my @thermi_sort = sort { $thermos->{$a} <=> $thermos->{$b} } keys %{$thermos};
  1146. MaxScanner_Log $hash, 4, "found " . scalar(@thermi_sort) . " thermostats";
  1147. # number of valid thermostats
  1148. $numValidThermos = scalar(@thermi_sort);
  1149. # stop, if we got no thermostat
  1150. last if ( $numValidThermos <= 0 );
  1151. # a maximum of 32 thermostats is allowed
  1152. $numValidThermos = $MaxScanner_TXPerMinutes if ( $numValidThermos > $MaxScanner_TXPerMinutes );
  1153. # calculate the optimal scan interval
  1154. my $numWorkIntervall = int( 60 / int( $MaxScanner_TXPerMinutes / $numValidThermos ) );
  1155. $numWorkIntervall = $numMinInterval if ( $numWorkIntervall < $numMinInterval );
  1156. # adjust the intervall, so it is a multiple of the BaseIntervall
  1157. $numWorkIntervall += ( $MaxScanner_BaseIntervall - ( $numWorkIntervall % $MaxScanner_BaseIntervall ) )
  1158. if ( $numWorkIntervall % $MaxScanner_BaseIntervall != 0 );
  1159. $hash->{helper}{workInterval} = $numWorkIntervall;
  1160. #.
  1161. MaxScanner_Log $hash, 4, "optimal scan intervall:$numWorkIntervall";
  1162. $retVal = MaxScanner_Work( $hash, \@thermi_sort, $numWorkIntervall );
  1163. # exit loop
  1164. last;
  1165. }
  1166. return $retVal;
  1167. }
  1168. ##########################
  1169. # called by internal timer
  1170. sub MaxScanner_Timer($)
  1171. {
  1172. my ($name) = @_;
  1173. my $hash = $defs{$name};
  1174. my $re01 = '^([0,1])$'; # only 0,1
  1175. my $stateStr = "processing";
  1176. my $numValidThermos = 0;
  1177. my $isDisabled = ( AttrVal( $name, 'disable', 0 ) =~ m/$re01/ ) ? $1 : '';
  1178. my $numDiffTime = 5;
  1179. my $sdNextScan;
  1180. MaxScanner_Log $hash, 3, '------------started ---------------- instance:' . $name;
  1181. # loop
  1182. while (1)
  1183. {
  1184. # no further action if disabled
  1185. if ($isDisabled)
  1186. {
  1187. MaxScanner_Log $hash, 4, "is disabled";
  1188. $stateStr = "disabled";
  1189. last;
  1190. }
  1191. # remove the timer of the script version
  1192. RemoveInternalTimer('MaxScanRun');
  1193. # call runner
  1194. $numDiffTime = MaxScanner_Run($name);
  1195. last;
  1196. }
  1197. # update state
  1198. readingsSingleUpdate( $hash, 'state', $stateStr, 0 );
  1199. MaxScanner_RestartTimer( $hash, $numDiffTime );
  1200. $sdNextScan = gettimeofday() + $numDiffTime;
  1201. $hash->{helper}{nextWorkTime} = FmtDateTime($sdNextScan);
  1202. }
  1203. ##########################
  1204. # attribute handling
  1205. sub MaxScanner_Attr($$$$)
  1206. {
  1207. my ( $command, $name, $attribute, $value ) = @_;
  1208. my $msg = undef;
  1209. my $hash = $defs{$name};
  1210. my $reUINT = '^([\\+]?\\d+)$';
  1211. MaxScanner_Log $hash, 4, 'name:' . $name . ' attribute:' . $attribute . ' value:' . $value . ' command:' . $command;
  1212. if ( $attribute eq 'disable' )
  1213. {
  1214. # call timer delayed
  1215. MaxScanner_RestartTimer( $hash, 1 ) if ( $hash->{helper}{initDone} );
  1216. }
  1217. #. threshold
  1218. elsif ( $attribute eq $MaxScanner_AttrCreditThreshold )
  1219. {
  1220. my $isInt = ( $value =~ m/$reUINT/ ) ? $1 : '';
  1221. if ( !$isInt )
  1222. {
  1223. $msg = 'value must be a number:' . $value;
  1224. return $msg;
  1225. }
  1226. if ( $value < 150 || $value > 600 )
  1227. {
  1228. $msg = 'value out of range [150..600] ' . $value;
  1229. return $msg;
  1230. }
  1231. }
  1232. #. scnMinInterval
  1233. elsif ( $attribute eq $MaxScanner_AttrMinInterval )
  1234. {
  1235. my $isInt = ( $value =~ m/$reUINT/ ) ? $1 : '';
  1236. if ( !$isInt )
  1237. {
  1238. $msg = 'value must be a number:' . $value;
  1239. return $msg;
  1240. }
  1241. if ( $value < 3 || $value > 60 )
  1242. {
  1243. $msg = 'value out of range [3..60] ' . $value;
  1244. return $msg;
  1245. }
  1246. }
  1247. return $msg;
  1248. }
  1249. 1;
  1250. =pod
  1251. =begin html
  1252. <a name="MaxScanner"></a>
  1253. <h3>MaxScanner</h3>
  1254. <p>The MaxScanner-Module enables FHEM to capture temperature and valve-position of thermostats in regular intervals. <p/>
  1255. <ul>
  1256. <a name="MaxScannerdefine"></a>
  1257. <b>Define</b>
  1258. <ul>
  1259. <br/>
  1260. <code>define &lt;name&gt; MaxScanner </code>
  1261. <br/>
  1262. </ul>
  1263. <br>
  1264. <a name="MaxScannerset"></a>
  1265. <b>Set-Commands</b>
  1266. <ul>
  1267. <code>set &lt;name&gt; run</code>
  1268. <br/><br/>
  1269. <ul>
  1270. Runs the scanner loop immediately. (Is usually done by timer)
  1271. </ul><br/>
  1272. </ul>
  1273. <a name="MaxScannerget"></a>
  1274. <b>Get-Commands</b>
  1275. <ul>
  1276. <code>get &lt;name&gt; associatedDevices</code><br/><br/>
  1277. <ul>Gets the asscociated devices (thermostats, shutterContacts)</ul><br/>
  1278. </ul>
  1279. <a name="MaxScannerattr"></a>
  1280. <b>Attributes for the Scanner-Device</b><br/><br/>
  1281. <ul>
  1282. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  1283. <li><p><b>disable</b><br/>When value=1, then the scanner device is disabled; possible values: 0,1; default: 0</p></li>
  1284. <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>
  1285. <li><p><b>scnMinInterval</b><br/>scan interval in minutes, when the calculated interval is lower,
  1286. then scnMinintervall will be used instead;possible values: 3..60; default: 3</p></li>
  1287. </ul>
  1288. <br/>
  1289. <a name="MaxScannerthermoattr"></a>
  1290. <b>User-Attributes for the Thermostat-Device</b><br/>
  1291. <ul>
  1292. <li><p><b>scanTemp</b><br/>When value=1, then scanner will use the thermostat; possible values: 0,1; default: 0</p></li>
  1293. <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>
  1294. <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>
  1295. <li><p><b>scnShutterList</b><br/>comma-separated list of shutterContacts associated with the thermostat</p></li>
  1296. </ul>
  1297. <br/>
  1298. <b>Additional information</b><br/><br/>
  1299. <ul>
  1300. <li><a href="http://forum.fhem.de/index.php/topic,11624.0.html">Discussion in FHEM forum</a></li><br/>
  1301. <li><a href="http://www.fhemwiki.de/wiki/MAX!_Temperatur-Scanner">WIKI information in FHEM Wiki</a></li><br/>
  1302. </ul>
  1303. </ul>
  1304. =end html
  1305. =cut