diff --git a/fhem/CHANGED b/fhem/CHANGED index 902ba9ce8..ebfad445e 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it + - feature: 76_SolarForecast: consumer device may have specified an own alias - feature: 73_AutoShuttersControl: https://forum.fhem.de/index.php?topic=136510.0 - feature: 76_SolarForecast: add temporary Migrate Getter x_migrate, diff --git a/fhem/FHEM/76_SolarForecast.pm b/fhem/FHEM/76_SolarForecast.pm index 9857ea28d..590aa921a 100644 --- a/fhem/FHEM/76_SolarForecast.pm +++ b/fhem/FHEM/76_SolarForecast.pm @@ -104,6 +104,8 @@ BEGIN { getAllSets HttpUtils_NonblockingGet HttpUtils_BlockingGet + GetFileFromURL + GetHttpFile init_done InternalTimer InternalVal @@ -157,6 +159,8 @@ BEGIN { # Versions History intern my %vNotesIntern = ( + "1.44.1" => "20.01.2025 Notification system: minor fixes, integration of controls_solarforecast_messages_test/prod ". + "Define: random start of Timer subs, consumerXX: consumer device may have specified an own alias ", "1.44.0" => "19.01.2025 _listDataPoolCircular: may select a dedicated hour, add temporary Migrate funktion x_migrate ". "fix interruptable key check in consumer attr Forum:https://forum.fhem.de/index.php?msg=1331073 ". "set prdef to 1.0, Implementation of a Messaging System ", @@ -423,6 +427,7 @@ my $calcmaxd = 30; my @dweattrmust = qw(TTT Neff RR1c ww SunUp SunRise SunSet); # Werte die im Attr forecastProperties des Weather-DWD_Opendata Devices mindestens gesetzt sein müssen my @draattrmust = qw(Rad1h); # Werte die im Attr forecastProperties des Radiation-DWD_Opendata Devices mindestens gesetzt sein müssen my $whistrepeat = 851; # Wiederholungsintervall Cache File Daten schreiben +my $gmfblto = 30; # Timeout Aholen Message File aus contrib my $solapirepdef = 3600; # default Abrufintervall SolCast API (s) my $forapirepdef = 900; # default Abrufintervall ForecastSolar API (s) @@ -489,10 +494,14 @@ my $actcoldef = 'orange'; my $inactcoldef = 'grey'; # default Färbung Icon wenn inaktiv my $bPath = 'https://svn.fhem.de/trac/browser/trunk/fhem/contrib/SolarForecast/'; # Basispfad Abruf contrib SolarForecast Files -my $pPath = '?format=txt'; # Download Format -my $cfile = 'controls_solarforecast.txt'; # Name des Controlfiles - +my $cfile = 'controls_solarforecast.txt'; # Controlfile Update FTUI-Files +my $msgfiletest = 'controls_solarforecast_messages_test.txt'; # TEST Input-File Notification System +my $msgfileprod = 'controls_solarforecast_messages_prod.txt'; # PRODUKTIVES Input-File Notification System +my $pPath = '?format=txt'; # Download Format +my $gmfilerepeat = 3000; # Wiederholungsuntervall Aholen Message File aus contrib +my $idxlimit = 900000; # Notification System: Indexe > $idxlimit sind reserviert für Steuerungsaufgaben +my $messagefile = $msgfileprod; # mögliche Debug-Module my @dd = qw( aiProcess aiData @@ -803,12 +812,14 @@ my %hqtxt = ( # H DE => qq{heute} }, simsg => { EN => qq{Message}, DE => qq{Mitteilung} }, - msgsys => { EN => qq{Messaging system}, + msgsys => { EN => qq{Notification system}, DE => qq{Mitteilungssystem} }, msgimp => { EN => qq{Importance}, DE => qq{Wichtigkeit} }, number => { EN => qq{Number}, DE => qq{Nummer} }, + ludich => { EN => qq{last update Input channels}, + DE => qq{letzte Aktualisierung Eingangskanäle} }, ctnsly => { EN => qq{continuously}, DE => qq{fortlaufend} }, yday => { EN => qq{yesterday}, @@ -871,6 +882,8 @@ my %hqtxt = ( # H DE => qq{Oh nein 😢, die Anlagenkonfiguration ist fehlerhaft. Bitte überprüfen Sie die Einstellungen und Hinweise!} }, pstate => { EN => qq{Planning status: 
Info: 
Mode: 
On: 
Off: 
Remaining lock time:  seconds}, DE => qq{Planungsstatus: 
Info: 
Modus: 
Ein: 
Aus: 
verbleibende Sperrzeit:  Sekunden} }, + dmgsig => { EN => qq{Read messages are not signaled again until a FHEM restart, but are retained if they are relevant.}, + DE => qq{Gelesene Mitteilungen werden bis zu einem FHEM Neustart nicht wieder signalisiert, bleiben bei Relevanz jedoch erhalten.} }, ); my %htitles = ( # Hash Hilfetexte (Mouse Over) @@ -1301,7 +1314,9 @@ my %hfspvh = ( # $data{$name}{dwdcatalog} # temporärer Speicher DWD Stationskatalog # $data{$name}{strings} # temporärer Speicher Stringkonfiguration # $data{$name}{aidectree}{object} # AI Decision Tree Object -# $data{$name}{messages} # Speicher Mitteilungssystem +# $data{$name}{messages} # Mitteilungssystem - permanent erneuerter Speicher +# $data{$name}{filemessages} # Mitteilungssystem - Input vom Message File +# $data{$name}{preparedmessages} # Mitteilungssystem - vorbereitete Messages innerhalb des Code ################################################################ # Init Fn @@ -1479,9 +1494,10 @@ sub Define { reloadCacheFiles ($params); singleUpdateState ( {hash => $hash, state => 'initialized', evt => 1} ); - $readyfnlist{$name} = $hash; # Registrierung in Ready-Schleife - InternalTimer (gettimeofday()+$whistrepeat, "FHEM::SolarForecast::periodicWriteMemcache", $hash, 0); # Einstieg periodisches Schreiben historische Daten - + $readyfnlist{$name} = $hash; # Registrierung in Ready-Schleife + InternalTimer (gettimeofday() + $whistrepeat + int(rand(300)), "FHEM::SolarForecast::periodicWriteMemcache", $hash, 0); # Einstieg periodisches Schreiben historische Daten + InternalTimer (gettimeofday() + 120 + int(rand(300)), "FHEM::SolarForecast::getMessageFileNonBlocking", $hash, 0); + return; } @@ -4661,7 +4677,7 @@ sub _getoutputMessages { ## no critic "not used" my $out = outputMessages ($paref); $out = qq{$out}; - $data{$name}{messages}{9999}{RD} = 1; # Lesekennzeichen setzen + $data{$name}{messages}{999999}{RD} = 1; # Lesekennzeichen setzen ## asynchrone Ausgabe ####################### @@ -5400,21 +5416,10 @@ sub __updPreFile { $err .= "Please check whether the path $path is present and accessible.
"; $err .= "After installing FTUI, come back and execute the get command again."; return $err; - - #my $ok = mkdir $path; - - #if (!$ok) { - # $err = "MKDIR ERROR: $!"; - # Log3 ($name, 1, "$name - $err"); - # return $err; - #} - #else { - # Log3 ($name, 3, "$name - MKDIR $path"); - #} } } - ($err, my $remFile) = __updGetUrl ($name, $bPath.$file.$pPath); + ($err, my $remFile) = __httpBlockingGet ($name, $bPath.$file.$pPath); if ($err) { Log3 ($name, 1, "$name - $err"); @@ -5446,27 +5451,27 @@ return; ############################################################### # File von url holen ############################################################### -sub __updGetUrl { +sub __httpBlockingGet { my $name = shift; my $url = shift; $url =~ s/%/%25/g; - my %upd_connecthash; + my %connecthash; my $unicodeEncoding = 1; - $upd_connecthash{url} = $url; - $upd_connecthash{keepalive} = ($url =~ m/localUpdate/ ? 0 : 1); # Forum #49798 - $upd_connecthash{forceEncoding} = '' if($unicodeEncoding); + $connecthash{url} = $url; + $connecthash{keepalive} = ($url =~ m/localUpdate/ ? 0 : 1); # Forum #49798 + $connecthash{forceEncoding} = '' if($unicodeEncoding); - my ($err, $dat) = HttpUtils_BlockingGet (\%upd_connecthash); + my ($err, $dat) = HttpUtils_BlockingGet (\%connecthash); if ($err) { - $err = "update ERROR: $err"; + $err = "GetUrl ERROR: $err"; return ($err, ''); } if (!$dat) { - $err = 'update ERROR: empty file received'; + $err = 'WARNING - empty file received'; return ($err, ''); } @@ -7347,7 +7352,7 @@ sub runTask { } releaseCentralTask ($hash); - centralTask ($hash, 1); + centralTask ($hash, 1); } } else { @@ -7363,13 +7368,13 @@ sub runTask { } releaseCentralTask ($hash); - centralTask ($hash, 1); + centralTask ($hash, 1); } } else { delete $hash->{HELPER}{S03DONE}; } - + return; } @@ -7638,6 +7643,7 @@ sub centralTask { setTimeTracking ($hash, $cst, 'runTimeCentralTask'); # Zyklus-Laufzeit ermitteln createReadingsFromArray ($hash, $evt); # Readings erzeugen + _readSystemMessages ($centpars); # Notification System - System Messages zusammenstellen if ($evt) { $centpars->{evt} = $evt; @@ -7822,10 +7828,10 @@ sub _collectAllRegConsumers { for my $c (1..$maxconsumer) { $c = sprintf "%02d", $c; - my ($err, $consumer, $hc) = isDeviceValid ( { name => $name, obj => "consumer${c}", method => 'attr' } ); + my ($err, $consumer, $hc, $alias) = isDeviceValid ( { name => $name, obj => "consumer${c}", method => 'attr' } ); next if($err); - push @{$data{$name}{current}{consumerdevs}}, $consumer; # alle Consumerdevices in CurrentHash eintragen + push @{$data{$name}{current}{consumerdevs}}, $consumer; # alle Consumerdevices in CurrentHash eintragen my $dswitch = $hc->{switchdev}; # alternatives Schaltdevice @@ -7833,13 +7839,13 @@ sub _collectAllRegConsumers { my ($err) = isDeviceValid ( { name => $name, obj => $dswitch, method => 'string' } ); next if($err); - push @{$data{$name}{current}{consumerdevs}}, $dswitch if($dswitch ne $consumer); # Switchdevice zusätzlich in CurrentHash eintragen + push @{$data{$name}{current}{consumerdevs}}, $dswitch if($dswitch ne $consumer); # Switchdevice zusätzlich in CurrentHash eintragen } else { $dswitch = $consumer; } - my $alias = AttrVal ($consumer, 'alias', $consumer); + $alias = AttrVal ($consumer, 'alias', $consumer) if(!$alias); my ($rtot,$utot,$ethreshold); if (exists $hc->{etotal}) { @@ -9912,7 +9918,7 @@ return $sf; } ################################################################ -# Erstellung Batterie Ladefreigabe +# Erstellung Batterie Ladefreigabe + SoC Prognose ################################################################ sub _batChargeRecmd { my $paref = shift; @@ -10046,11 +10052,7 @@ sub _batChargeRecmd { $socwh = sprintf "%.0f", $socwh; my $progsoc = sprintf "%.1f", (100 * $socwh / $batinstcap); # Prognose SoC in % - - #$progsoc = $progsoc < $batoptsoc ? $batoptsoc : - # $progsoc < $lowSoc ? $lowSoc : - # $progsoc; - + __createNextHoursSFCReadings ( {name => $name, nhr => $nhr, bn => $bn, @@ -12626,65 +12628,6 @@ sub __calcFcQuality { return $hdv; } -################################################################ -# Berechnen Tag / Stunden Verschieber -# aus aktueller Stunde + lfd. Nummer -################################################################ -sub calcDayHourMove { - my $chour = shift; - my $num = shift; - - my $fh = $chour + $num; - my $fd = int ($fh / 24) ; - $fh = $fh - ($fd * 24); - -return ($fd, $fh); -} - -################################################################ -# Zeit gemäß DWD_OpenData-Format -# Berechnen Tag / Stunden Verschieber ab aktuellen Tag -# Input: YYYY-MM-DD HH:MM:SS -# Output: $fd - 0 (Heute), 1 (Morgen), 2 (Übermorgen), .... -# $fh - Stunde von $fd ohne führende Null -# Return: fc${fd}_${fh} -################################################################ -sub formatWeatherTimestrg { - my $date = shift // return; - - my $cdate = strftime "%Y-%m-%d", localtime(time); - my $refts = timestringToTimestamp ($cdate.' 00:00:00'); # Referenztimestring - my $datts = timestringToTimestamp ($date); - my $fd = int (($datts - $refts) / 86400); - my $fh = int ((split /[ :]/, $date)[1]); - -return "fc${fd}_${fh}"; -} - -################################################################ -# Spezialfall auflösen wenn Wert von $val2 dem -# Redingwert von $val1 entspricht sofern $val1 negativ ist -################################################################ -sub substSpecialCases { - my $paref = shift; - my $dev = $paref->{dev}; - my $rdg = $paref->{rdg}; - my $rdgf = $paref->{rdgf}; - - my $val1 = ReadingsNum ($dev, $rdg, 0) * $rdgf; - my $val2; - - if($val1 <= 0) { - $val2 = abs($val1); - $val1 = 0; - } - else { - $val2 = 0; - } - -return ($val1,$val2); -} - ################################################################ # Energieverbrauch des Hauses in History speichern ################################################################ @@ -12970,6 +12913,36 @@ sub _genSpecialReadings { return; } +################################################################################### +# Messagefile für Notification System lesen +# Filestruktur: +# 0|SV|1 +# 0|DE|Mitteilung .... +# 0|EN|Message... +# $data{$name}{preparedmessages}{999500}{TS}: Timestamp Stand prepared Messages +################################################################################### +sub _readSystemMessages { + my $paref = shift; + my $name = $paref->{name}; + + delete $data{$name}{preparedmessages}; + + my $midx = 0; + + if (!ReadingsVal ($name, '.migrated', 0)) { + $midx++; + $data{$name}{preparedmessages}{$midx}{SV} = 1; + $data{$name}{preparedmessages}{$midx}{DE} = 'Die gespeicherten PV Daten können mit "get ... x_migrate" in ein neues Format umgesetzt werden welches den Median Ansatz bei der PV Prognose aktiviert und nutzt.'; + $data{$name}{preparedmessages}{$midx}{DE} .= '
Mit einem späteren Update des Moduls erfolgt diese Umstellung automatisch.'; + $data{$name}{preparedmessages}{$midx}{EN} = 'The stored PV data can be converted with “get ... x_migrate” into a new format which activates and uses the median approach in the PV forecast.'; + $data{$name}{preparedmessages}{$midx}{EN} .= '
With a later update of the module, this changeover will take place automatically.'; + } + + $data{$name}{preparedmessages}{999500}{TS} = time; + +return; +} + ################################################################ # FHEMWEB Fn ################################################################ @@ -13483,7 +13456,7 @@ sub _graphicHeader { ## Message-Icon ################# - my ($micon, $midx) = __fillupMessages ($paref); + my ($micon, $midx) = fillupMessageSystem ($paref); $img = FW_makeImage ($micon); my $msgicon = $midx ? "$img" : $img; my $msgtitle = $midx ? $htitles{outpmsg}{$lang} : $htitles{nomsgfo}{$lang}; @@ -13766,47 +13739,6 @@ sub _graphicHeader { return $header; } -################################################################ -# Mitteilungssystem füllen -# Schweregrad SV: -# 0 - keine Mitteilung -# 1 - Mitteilung -# 2 - Warnung -# 3 - Fehler / Problem -################################################################ -sub __fillupMessages { - my $paref = shift; - my $name = $paref->{name}; - my $lang = $paref->{lang}; - - for my $idx (keys %{$data{$name}{messages}}) { - next if($idx == 9999); - delete $data{$name}{messages}{$idx}; - } - - my $midx = 0; - my $max_sv = 0; - - if (!ReadingsVal ($name, '.migrated', 0)) { - $midx++; - $data{$name}{messages}{$midx}{SV} = 1; - $data{$name}{messages}{$midx}{DE} = 'Die gespeicherten PV Daten können mit "get ... x_migrate" in ein neues Format umgesetzt werden welches den Median Ansatz bei der PV Prognose aktiviert und nutzt.'; - $data{$name}{messages}{$midx}{DE} .= '
Mit einem späteren Update des Moduls erfolgt diese Umstellung automatisch.'; - $data{$name}{messages}{$midx}{EN} = 'The stored PV data can be converted with “get ... x_migrate” into a new format which activates and uses the median approach in the PV forecast.'; - $data{$name}{messages}{$midx}{EN} .= '
With a later update of the module, this changeover will take place automatically.'; - } - - if ($midx && !defined $data{$name}{messages}{9999}{RD}) { # RD = Read-Bit - my @aidx = map { $_ } (1..$midx); # größte vorhandene Severity finden - my @values = map { $data{$name}{messages}{$_}{SV} } @aidx; - $max_sv = max(@values); - } - - my $max_icon = $svicons{$max_sv}; # ... und das dazugehörige Icon - -return ($max_icon, $midx); -} - ################################################################ # erstelle Update-Icon ################################################################ @@ -16501,6 +16433,313 @@ sub _addHourAiRawdata { return; } +############################################################### +# Abruf und Einlesen Messagefile nonBlocking +############################################################### +sub getMessageFileNonBlocking { + my $hash = shift; + my $name = $hash->{NAME}; + + RemoveInternalTimer ($hash, "FHEM::SolarForecast::getMessageFileNonBlocking"); + InternalTimer (gettimeofday() + $gmfilerepeat, "FHEM::SolarForecast::getMessageFileNonBlocking", $hash, 0); + + my (undef, $disabled, $inactive) = controller ($name); + return if($disabled || $inactive); + + delete $hash->{HELPER}{GMFRUNNING} if(defined $hash->{HELPER}{GMFRUNNING}{pid} && $hash->{HELPER}{GMFRUNNING}{pid} =~ /DEAD/xs); + + if (defined $hash->{HELPER}{GMFRUNNING}{pid}) { + Log3 ($name, 3, qq{$name - another Message File Process with PID "$hash->{HELPER}{GMFRUNNING}{pid}" is already running ... get Message File is aborted}); + return; + } + + Log3 ($name, 4, "$name - Notification System - Message file >$messagefile< is retrieved non blocking"); + + my $paref = { name => $name, + hash => $hash, + block => 1 + }; + + $hash->{HELPER}{GMFRUNNING} = BlockingCall ( "FHEM::SolarForecast::_retrieveMessageFile", + $paref, + "FHEM::SolarForecast::_processMessageFile", + $gmfblto, + "FHEM::SolarForecast::_abortGetMessageFile", + $hash + ); + + + if (defined $hash->{HELPER}{GMFRUNNING}) { + $hash->{HELPER}{GMFRUNNING}{loglevel} = 3; + } + +return; +} + +############################################################### +# Message File aus contrib abholen +############################################################### +sub _retrieveMessageFile { + my $paref = shift; + my $name = $paref->{name}; + my $block = $paref->{block} // 0; + + my $valid = 1; + my ($err, $remfile) = __httpBlockingGet ($name, $bPath.$messagefile.$pPath); + + $remfile = q{} if($remfile =~ /No\snode\strunk\/fhem\/contrib\/SolarForecast\//xs); + + if ($err) { + $valid = 0; + Log3 ($name, 4, "$name - Notification System - retrieve of remote Message File faulty: $err"); + } + + if (!$remfile) { + $valid = 0; + Log3 ($name, 4, "$name - Notification System - no remote Message File >$messagefile< found"); + } + + if ($valid) { + $err = __updWriteFile ("$root/FHEM/", $messagefile, $remfile); + + if ($err) { + $valid = 0; + Log3 ($name, 1, "$name - $err"); + } + else { + Log3 ($name, 4, "$name - Notification System - new Message File updated to $root/FHEM/$messagefile"); + } + } + + $paref->{valid} = $valid; + my $serial = encode_base64 (Serialize ( $paref ), ""); # Serialisierung + + $block ? return ($serial) : return \&_processMessageFile ($serial); + +return; +} + +############################################################### +# Folgeroutine nach Message File aus contrib abholen +############################################################### +sub _processMessageFile { + my $serial = decode_base64 (shift); + + my $paref = eval { thaw ($serial) }; # Deserialisierung + my $name = $paref->{name}; + my $valid = $paref->{valid}; + my $hash = $defs{$name}; + + if ($valid) { + __readFileMessages ($paref); + } + +return; +} + +###################################################################### +# Messagefile für Notification System lesen +# Filestruktur: +# 0|SV|1 +# 0|DE|Mitteilung .... +# 0|EN|Message... +# $data{$name}{messages}{999000}{TS}: Timestamp Stand Message File +###################################################################### +sub __readFileMessages { + my $paref = shift; + my $name = $paref->{name}; + + my $hash = $defs{$name}; + + open (FD, "$root/FHEM/$messagefile") or do { return $! }; + + delete $data{$name}{filemessages}; + + my @locList = map { $_ =~ s/[\r\n]//; $_ } ; + close (FD); + + Log3 ($name, 4, "$name - Notification System - read local Message File >$messagefile< with ".scalar @locList." entries."); + + for my $l (@locList) { + next if ($l =~ /^\#/xs); + my @l = split /\|/, $l, 3; + next if(!isNumeric ($l[0])); + next if($l[1] !~ /^(DE|EN|SV)$/xs); + + $data{$name}{filemessages}{$l[0]}{$l[1]} = $l[2]; + } + + $data{$name}{filemessages}{999000}{TS} = time; + +return; +} + +#################################################################################################### +# Abbruchroutine BlockingCall Timeout +#################################################################################################### +sub _abortGetMessageFile { + my $hash = shift; + my $cause = shift // "Timeout: process terminated"; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + Log3 ($name, 1, "$name -> BlockingCall $hash->{HELPER}{GMFRUNNING}{fn} pid:$hash->{HELPER}{AIBLOCKRUNNING}{pid} aborted: $cause"); + + delete $hash->{HELPER}{GMFRUNNING}; + +return; +} + +########################################################################## +# Mitteilungssystem füllen +# Schweregrad SV: +# 0 - keine Mitteilung +# 1 - Mitteilung +# 2 - Warnung +# 3 - Fehler / Problem +# +# Statusspeicher: +# $data{$name}{messages}{999999}{RD}: 1 - gelesen, 0 - ungelesen +# $data{$name}{messages}{999000}{TS}: Timestamp Stand Message File +# $data{$name}{messages}{999500}{TS}: Timestamp Stand prepared Messages +########################################################################## +sub fillupMessageSystem { + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $lang = $paref->{lang}; + + my $otxt = q{}; + my $ntxt = q{}; + my $midx = 0; + my $max_sv = 0; + + ## Aufnahme Stand für alt/neu Vergleich + Clear Messages + ########################################################## + for my $idx (sort keys %{$data{$name}{messages}}) { + next if($idx >= $idxlimit); + $otxt .= $data{$name}{messages}{$idx}{SV} if(defined $data{$name}{messages}{$idx}{SV}); + $otxt .= $data{$name}{messages}{$idx}{DE} if(defined $data{$name}{messages}{$idx}{DE}); + $otxt .= $data{$name}{messages}{$idx}{EN} if(defined $data{$name}{messages}{$idx}{EN}); + + delete $data{$name}{messages}{$idx}; + } + + ## Messages füllen + ######################################################################## + # Integration File Messages + for my $mfi (sort keys %{$data{$name}{filemessages}}) { + next if($mfi >= $idxlimit); + $midx++; + $data{$name}{messages}{$midx}{SV} = $data{$name}{filemessages}{$mfi}{SV}; + $data{$name}{messages}{$midx}{DE} = $data{$name}{filemessages}{$mfi}{DE}; + $data{$name}{messages}{$midx}{EN} = $data{$name}{filemessages}{$mfi}{EN}; + } + + # Integration prepared Messages + for my $smi (sort keys %{$data{$name}{preparedmessages}}) { + next if($smi >= $idxlimit); + $midx++; + $data{$name}{messages}{$midx}{SV} = $data{$name}{preparedmessages}{$smi}{SV}; + $data{$name}{messages}{$midx}{DE} = $data{$name}{preparedmessages}{$smi}{DE}; + $data{$name}{messages}{$midx}{EN} = $data{$name}{preparedmessages}{$smi}{EN}; + } + + $data{$name}{messages}{999000}{TS} = $data{$name}{filemessages}{999000}{TS} // 0; + $data{$name}{messages}{999500}{TS} = $data{$name}{preparedmessages}{999500}{TS} // 0; + + ######################################################################## + ## Ende Messages auffüllen + + + ## Vergleich auf geänderte Messages + ##################################### + for my $idx (sort keys %{$data{$name}{messages}}) { + next if($idx >= $idxlimit); + $ntxt .= $data{$name}{messages}{$idx}{SV} if(defined $data{$name}{messages}{$idx}{SV}); + $ntxt .= $data{$name}{messages}{$idx}{DE} if(defined $data{$name}{messages}{$idx}{DE}); + $ntxt .= $data{$name}{messages}{$idx}{EN} if(defined $data{$name}{messages}{$idx}{EN}); + } + + if ($ntxt ne $otxt) { # es gibt neue Post! bzw. Änderungen -> Read-Bit läschen + delete $data{$name}{messages}{999999}{RD}; + } + + + if ($midx && !defined $data{$name}{messages}{999999}{RD}) { # RD = Read-Bit (undef -> Messages nicht gelesen) + my @aidx = map { $_ } (1..$midx); # größte vorhandene Severity finden ... + my @values = map { $data{$name}{messages}{$_}{SV} } @aidx; + $max_sv = max(@values); + } + + my $max_icon = $svicons{$max_sv}; # ... und das dazugehörige Icon + +return ($max_icon, $midx); +} + +################################################################ +# Ausgabe des Mitteilungsystems +################################################################ +sub outputMessages { + my $paref = shift; + my $name = $paref->{name}; + my $lang = $paref->{lang}; + + my ($micon, $midx) = fillupMessageSystem ($paref); # Ergebnisse füllen (sind leer wenn Browser nicht refreshed) + my $tnf = $data{$name}{messages}{999000}{TS} ? + (timestampToTimestring ($data{$name}{messages}{999000}{TS}, $lang))[0] : + 'n.a.'; + my $tpm = $data{$name}{messages}{999500}{TS} ? + (timestampToTimestring ($data{$name}{messages}{999500}{TS}, $lang))[0] : + 'n.a.'; + ## Ausgabe + ############ + my $out = qq{}; + $out .= qq{$hqtxt{msgsys}{$lang}

}; + $out .= qq{$hqtxt{ludich}{$lang} - File: $tnf, System: $tpm
}; + $out .= qq{($hqtxt{dmgsig}{$lang})

}; + + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + + my $hc = 0; + + for my $key (sort keys %{$data{$name}{messages}}) { + next if($key >= $idxlimit); + + $hc++; + my $enmsg = encode ("utf8", $data{$name}{messages}{$key}{$lang}); + + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + + if ($hc < $midx) { # Zwischenzeile + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + } + } + + $out .= qq{
Pos.       $hqtxt{msgimp}{$lang}       $hqtxt{simsg}{$lang}
$key $data{$name}{messages}{$key}{SV} $enmsg
 
}; + $out .= qq{}; + + $out .= "
"; + +return $out; +} + ############################################################### # Eintritt in den KI Train Prozess normal/Blocking ############################################################### @@ -16688,7 +16927,6 @@ return; sub aiTrain { ## no critic "not used" my $paref = shift; my $name = $paref->{name}; - my $type = $paref->{type}; my $block = $paref->{block} // 0; my $hash = $defs{$name}; @@ -18188,63 +18426,6 @@ sub _writeAsCsv { return "The memory structure was written to the file $outfile"; } -################################################################ -# Ausgabe des Mitteilungsystems -################################################################ -sub outputMessages { - my $paref = shift; - my $name = $paref->{name}; - my $lang = $paref->{lang}; - - my ($micon, $midx) = __fillupMessages ($paref); # Ergebnisse füllen (sind leer wenn Browser nicht refreshed) - - ## Ausgabe - ############ - my $out = qq{}; - $out .= qq{}.$hqtxt{msgsys}{$lang}.qq{
}; - $out .= qq{Hinweis: Gelesene Mitteilungen werden bis zu einem FHEM Neustart nicht wieder signalisiert, bleiben jedoch bei Relevanz erhalten.

}; - - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - - my $hc = 0; - - for my $key (sort keys %{$data{$name}{messages}}) { - next if($key == 9999); - - $hc++; - my $enmsg = encode ("utf8", $data{$name}{messages}{$key}{$lang}); - - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - - if ($hc < $midx) { # Zwischenzeile - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - } - } - - $out .= qq{
$hqtxt{number}{$lang}       $hqtxt{msgimp}{$lang}       $hqtxt{simsg}{$lang}
$key $data{$name}{messages}{$key}{SV} $enmsg
 
}; - $out .= qq{}; - - $out .= "
"; - -return $out; -} - ################################################################ # validiert die aktuelle Anlagenkonfiguration ################################################################ @@ -18986,11 +19167,70 @@ sub medianArray { return; } +################################################################ +# Berechnen Tag / Stunden Verschieber +# aus aktueller Stunde + lfd. Nummer +################################################################ +sub calcDayHourMove { + my $chour = shift; + my $num = shift; + + my $fh = $chour + $num; + my $fd = int ($fh / 24) ; + $fh = $fh - ($fd * 24); + +return ($fd, $fh); +} + +################################################################ +# Zeit gemäß DWD_OpenData-Format +# Berechnen Tag / Stunden Verschieber ab aktuellen Tag +# Input: YYYY-MM-DD HH:MM:SS +# Output: $fd - 0 (Heute), 1 (Morgen), 2 (Übermorgen), .... +# $fh - Stunde von $fd ohne führende Null +# Return: fc${fd}_${fh} +################################################################ +sub formatWeatherTimestrg { + my $date = shift // return; + + my $cdate = strftime "%Y-%m-%d", localtime(time); + my $refts = timestringToTimestamp ($cdate.' 00:00:00'); # Referenztimestring + my $datts = timestringToTimestamp ($date); + my $fd = int (($datts - $refts) / 86400); + my $fh = int ((split /[ :]/, $date)[1]); + +return "fc${fd}_${fh}"; +} + +################################################################ +# Spezialfall auflösen wenn Wert von $val2 dem +# Redingwert von $val1 entspricht sofern $val1 negativ ist +################################################################ +sub substSpecialCases { + my $paref = shift; + my $dev = $paref->{dev}; + my $rdg = $paref->{rdg}; + my $rdgf = $paref->{rdgf}; + + my $val1 = ReadingsNum ($dev, $rdg, 0) * $rdgf; + my $val2; + + if($val1 <= 0) { + $val2 = abs($val1); + $val1 = 0; + } + else { + $val2 = 0; + } + +return ($val1,$val2); +} + ################################################################ # Timestrings berechnen # gibt Zeitstring in lokaler Zeit zurück ################################################################ -sub timestampToTimestring { +sub timestampToTimestring { my $epoch = shift; my $lang = shift // ''; @@ -20027,7 +20267,7 @@ return $is; # reading: Device ist im Reading Value enthalten # attr: Device ist im Attr Value enthalten # string: Device ist im Objekt-Inhalt enthalten -# return: $valid - ist die Angabe valide (1) +# return: $err - evtl. Fehler # $a->[0] - das extrahierte Device # $h - Hash der geparsten Entität ##################################################################### @@ -20054,24 +20294,27 @@ sub isDeviceValid { } my ($a, $h) = parseParams ($dev); + + my ($dv, $al) = !$a->[0] ? ('', '') : + $a->[0] =~ /:/xs ? (split ':', $a->[0]) : + ($a->[0], ''); # (optionalen) SF-spezifischen Alias abtrennen - if ($a->[0] && $a->[0] =~ /\@/xs ) { # Remote Device - $a->[0] = (split '@', $a->[0])[0]; - return ($err, $a->[0], $h); # ToDo: $h aus remote Werten anreichern - } - - if (!$a->[0] || !$defs{$a->[0]}) { - $a->[0] //= ''; - $err = qq{The device '$a->[0]' doesn't exist or is not a valid device.}; - $err = qq{There is no device set. Check the syntax with the command reference.} if(!$a->[0]); - $err = qq{The device '$a->[0]' doesn't exist anymore! Delete or change the attribute '$obj'.} if(!$defs{$a->[0]} && $method eq 'attr' && $obj =~ /consumer/); + if (!$dv || !$defs{$dv}) { + $dv //= ''; + $err = qq{The device '$dv' doesn't exist or is not a valid device.}; + $err = qq{There is no device set. Check the syntax with the command reference.} if(!$dv); + $err = qq{The device '$dv' doesn't exist anymore! Delete or change the attribute '$obj'.} if(!$defs{$dv} && $method eq 'attr' && $obj =~ /consumer/); } if ($err) { Log3 ($name, 1, "$name - ERROR - $err"); } + + if ($al) { # Leerzeichen im SF-Alias generieren + $al =~ s/\+/ /g; + } -return ($err, $a->[0], $h); +return ($err, $dv, $h, $al); } ##################################################################### @@ -22619,7 +22862,7 @@ to ensure that the system configuration is correct.
-
  • consumerXX <Device Name> type=<type> power=<power> [switchdev=<device>]
    +
  • consumerXX <Device>[:<Alias>] type=<type> power=<power> [switchdev=<device>]
    [mode=<mode>] [icon=<Icon>[@<Color>]] [mintime=<minutes> | SunPath[:<Offset_Sunrise>:<Offset_Sunset>]]
    [on=<command>] [off=<command>] [swstate=<Readingname>:<on-Regex>:<off-Regex>] [asynchron=<Option>]
    [notbefore=<Expression>] [notafter=<Expression>] [locktime=<offlt>[:<onlt>]]
    @@ -22628,8 +22871,8 @@ to ensure that the system configuration is correct. [surpmeth=<Option>] [interruptable=<Option>] [noshow=<Option>] [exconfc=<Option>]


    - Registers a consumer <Device Name> with the SolarForecast Device. In this case, <Device Name> - is a consumer device already created in FHEM, e.g. a switchable socket. + Registers a consumer <Device> with the SolarForecast Device. An optional alias can be specified.
    + In this case, <Device> is a consumer device already created in FHEM, e.g. a switchable socket. Most of the keys are optional, but are a prerequisite for certain functionalities and are filled with default values.
    If the dish is defined "auto", the automatic mode in the integrated consumer graphic can be switched with the @@ -22664,6 +22907,11 @@ to ensure that the system configuration is correct.
      + + + + + @@ -25104,7 +25352,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
      -
    • consumerXX <Device Name> type=<type> power=<power> [switchdev=<device>]
      +
    • consumerXX <Device>[:<Alias>] type=<type> power=<power> [switchdev=<device>]
      [mode=<mode>] [icon=<Icon>[@<Farbe>]] [mintime=<minutes> | SunPath[:<Offset_Sunrise>:<Offset_Sunset>]]
      [on=<Kommando>] [off=<Kommando>] [swstate=<Readingname>:<on-Regex>:<off-Regex>] [asynchron=<Option>]
      [notbefore=<Ausdruck>] [notafter=<Ausdruck>] [locktime=<offlt>[:<onlt>]]
      @@ -25113,8 +25361,8 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. [surpmeth=<Option>] [interruptable=<Option>] [noshow=<Option>] [exconfc=<Option>]


      - Registriert einen Verbraucher <Device Name> beim SolarForecast Device. Dabei ist <Device Name> - ein in FHEM bereits angelegtes Verbraucher Device, z.B. eine Schaltsteckdose. + Registriert einen Verbraucher <Device> beim SolarForecast Device. Ein optionaler Alias kann angegeben werden.
      + Dabei ist <Device> ein in FHEM bereits angelegtes Verbraucher Device, z.B. eine Schaltsteckdose. Die meisten Schlüssel sind optional, sind aber für bestimmte Funktionalitäten Voraussetzung und werden mit default-Werten besetzt.
      Ist der Schüssel "auto" definiert, kann der Automatikmodus in der integrierten Verbrauchergrafik mit den @@ -25148,6 +25396,11 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
    • Device Consumer device. In the simple case, the device works both as an energy meter and as a switch.
      In the optional alias, spaces must be replaced by '+' (e.g. 'Ein+toller+Alias').
      If the consumer consists of different devices/channels (e.g. Homematic), the energy meter is defined as a <Device>.
      The associated switching device is specified with the key 'switchdev'.
      type Type of consumer. The following types are allowed:
      dishwasher - Consumer is a dishwasher
      dryer - Consumer is a tumble dryer
      + + + + +
      Device Verbraucher-Gerät. Im einfachen Fall arbeitet das Gerät sowohl als Energiemesser als auch als Schalter.
      Im optionalen Alias sind Leerzeichen durch '+' zu ersetzen (z.B. 'Ein+toller+Alias').
      Besteht der Verbraucher aus verschiedenen Geräten/Kanäalen (z.B. Homematic), wird der Energiemesser als <Device> definiert.
      Das dazugehörige Schalt-Gerät wird mit dem Schlüssel 'switchdev' spezifiziert.
      type Typ des Verbrauchers. Folgende Typen sind erlaubt:
      dishwasher - Verbraucher ist eine Spülmaschine
      dryer - Verbraucher ist ein Wäschetrockner