diff --git a/fhem/contrib/DS_Starter/76_SolarForecast.pm b/fhem/contrib/DS_Starter/76_SolarForecast.pm index b01236a59..eb7df4c24 100644 --- a/fhem/contrib/DS_Starter/76_SolarForecast.pm +++ b/fhem/contrib/DS_Starter/76_SolarForecast.pm @@ -1,10 +1,10 @@ ######################################################################################################################## -# $Id: 76_SolarForecast.pm 21735 2022-11-08 23:53:24Z DS_Starter $ +# $Id: 76_SolarForecast.pm 21735 2022-11-12 23:53:24Z DS_Starter $ ######################################################################################################################### # 76_SolarForecast.pm # # (c) 2020-2022 by Heiko Maaz e-mail: Heiko dot Maaz at t-online dot de -# +# # This script is part of fhem. # # Fhem is free software: you can redistribute it and/or modify @@ -21,6 +21,10 @@ # along with fhem. If not, see . # ######################################################################################################################### +# +# Leerzeichen entfernen: sed -i 's/[[:space:]]*$//' 76_SolarForecast.pm +# +######################################################################################################################### package FHEM::SolarForecast; ## no critic 'package' use strict; @@ -41,19 +45,19 @@ eval "use JSON;1;" or my $jsonabs = "JSON"; ## no use FHEM::SynoModules::SMUtils qw( evaljson getClHash - delClHash + delClHash moduleVersion trim ); # Hilfsroutinen Modul -use Data::Dumper; +use Data::Dumper; use Storable 'dclone'; no if $] >= 5.017011, warnings => 'experimental::smartmatch'; - + # Run before module compilation BEGIN { # Import from main:: - GP_Import( + GP_Import( qw( attr asyncOutput @@ -85,9 +89,9 @@ BEGIN { InternalTimer IsDisabled Log - Log3 + Log3 modules - parseParams + parseParams readingsSingleUpdate readingsBulkUpdate readingsBulkUpdateIfChanged @@ -100,17 +104,17 @@ BEGIN { RemoveInternalTimer readingFnAttributes setKeyValue - sortTopicNum + sortTopicNum FW_cmd FW_directNotify - FW_ME - FW_subdir - FW_room - FW_detail - FW_wname + FW_ME + FW_subdir + FW_room + FW_detail + FW_wname ) ); - + # Export to main context with different name # my $pkg = caller(0); # my $main = $pkg; @@ -124,14 +128,18 @@ BEGIN { pageAsHtml NexthoursVal ) - ); - + ); + } # Versions History intern my %vNotesIntern = ( + "0.73.0" => "12.11.2022 save Ehodpieces (___saveEhodpieces), use debug modules, revise comref,typos , maxconsumer 12 ". + "attr ctrlLanguage for local language support, bugfix MODE is set to Manual after restart if ". + "attr ctrlInterval is not set, new attr consumerLink, graphic tooltips and formatting, ". + " ", "0.72.5" => "08.11.2022 calculate percentile correction factor instead of best percentile, exploit all available API requests ". - "graphicBeamWidth more values, add moduleTiltAngle: 5,15,35,55,65,75,85 , ". + "graphicBeamWidth more values, add moduleTiltAngle: 5,15,35,55,65,75,85 , ". "fix _estConsumptionForecast, delete Setter pvSolCastPercentile_XX ", "0.72.4" => "06.11.2022 change __solCast_ApiResponse -> special processing first dataset of current hour ", "0.72.3" => "05.11.2022 new status bit CurrentVal allStringsFullfilled ", @@ -162,7 +170,7 @@ my %vNotesIntern = ( "0.70.0 "=> "13.10.2022 delete Attr solCastPercentile, new manual Setter pvSolCastPercentile_XX ", "0.69.0 "=> "12.10.2022 Autocorrection function for model SolCast-API, __solCast_ApiRequest: request only 48 hours ", "0.68.7 "=> "07.10.2022 new function _calcCAQwithSolCastPercentil, check missed modules in _getRoofTopData ", - "0.68.6 "=> "06.10.2022 new attribute solCastPercentile, change _calcMaxEstimateToday ", + "0.68.6 "=> "06.10.2022 new attribute solCastPercentile, change _calcMaxEstimateToday ", "0.68.5 "=> "03.10.2022 extent plant configuration check ", "0.68.4 "=> "03.10.2022 do ___setLastAPIcallKeyData if response_status, generate events of Today_MaxPVforecast.* in every cycle ". "add SolCast section in _graphicHeader, change default colors and settings, new reading Today_PVreal ". @@ -207,7 +215,7 @@ my %vNotesIntern = ( "0.58.0 "=> "20.04.2022 new setter consumerImmediatePlanning, functions isConsumerPhysOn isConsumerPhysOff ", "0.57.3 "=> "10.04.2022 some fixes (\$eavg in ___csmSpecificEpieces, useAutoCorrection switch to regex) ", "0.57.2 "=> "03.04.2022 area factor for 25° added ", - "0.57.1 "=> "28.02.2022 new attr flowGraphicShowConsumerPower and flowGraphicShowConsumerRemainTime (Consumer remainTime in flowGraphic)", + "0.57.1 "=> "28.02.2022 new attr flowGraphicShowConsumerPower and flowGraphicShowConsumerRemainTime (Consumer remainTime in flowGraphic)", "0.56.11"=> "01.12.2021 comment: 'next if(\$surplus <= 0);' to resolve consumer planning problem if 'mode = must' and the ". "current doesn't have suplus ", "0.56.10"=> "14.11.2021 change sub _flowGraphic (Max), https://forum.fhem.de/index.php/topic,117864.msg1186970.html#msg1186970, new reset consumerMaster ", @@ -324,7 +332,8 @@ my %vNotesIntern = ( ## Konstanten ############### -my @chours = (5..21); # Stunden des Tages mit möglichen Korrekturwerten +my $deflang = 'EN'; # default Sprache wenn nicht konfiguriert +my @chours = (5..21); # Stunden des Tages mit möglichen Korrekturwerten my $kJtokWh = 0.00027778; # Umrechnungsfaktor kJ in kWh my $defmaxvar = 0.5; # max. Varianz pro Tagesberechnung Autokorrekturfaktor my $definterval = 70; # Standard Abfrageintervall @@ -341,20 +350,20 @@ my @dweattrmust = qw(TTT Neff R101 ww SunUp SunRise SunSet); my @draattrmust = qw(Rad1h); # Werte die im Attr forecastProperties des Radiation-DWD_Opendata Devices mindestens gesetzt sein müssen my $whistrepeat = 900; # Wiederholungsintervall Cache File Daten schreiben -my $apirepetdef = 3600; # default Abrufintervall SolCast API -my $apimaxreqs = 50; # max. täglich mögliche Requests SolCast API +my $apirepetdef = 3600; # default Abrufintervall SolCast API +my $apimaxreqs = 50; # max. täglich mögliche Requests SolCast API my $prdef = 0.85; # default Performance Ratio (PR) my $tempcoeffdef = -0.45; # default Temperaturkoeffizient Pmpp (%/°C) lt. Datenblatt Solarzelle my $tempmodinc = 25; # default Temperaturerhöhung an Solarzellen gegenüber Umgebungstemperatur bei wolkenlosem Himmel my $tempbasedef = 25; # Temperatur Module bei Nominalleistung my $cldampdef = 35; # Gewichtung (%) des Korrekturfaktors bzgl. effektiver Bewölkung, siehe: https://www.energie-experten.org/erneuerbare-energien/photovoltaik/planung/sonnenstunden -my $cloud_base = 0; # Fußpunktverschiebung bzgl. effektiver Bewölkung +my $cloud_base = 0; # Fußpunktverschiebung bzgl. effektiver Bewölkung my $rdampdef = 10; # Gewichtung (%) des Korrekturfaktors bzgl. Niederschlag (R101) -my $rain_base = 0; # Fußpunktverschiebung bzgl. effektiver Bewölkung +my $rain_base = 0; # Fußpunktverschiebung bzgl. effektiver Bewölkung -my $maxconsumer = 9; # maximale Anzahl der möglichen Consumer (Attribut) +my $maxconsumer = 12; # maximale Anzahl der möglichen Consumer (Attribut) my $epiecHCounts = 10; # Anzahl Einschaltzyklen (Consumer) für verbraucherspezifische Energiestück Ermittlung my @ctypes = qw(dishwasher dryer washingmachine heater charger other); # erlaubte Consumer Typen my $defmintime = 60; # default min. Einschalt- bzw. Zykluszeit in Minuten @@ -371,7 +380,7 @@ my $b1fontcoldef = '0D0D0D'; my $b2coldef = 'C4C4A7'; # default Farbe Beam 2 my $b2fontcoldef = '000000'; # default Schriftfarbe Beam 2 my $fgCDdef = 130; # Abstand Verbrauchericons zueinander - + my $defpopercent = 0.5; # Standard % aktuelle Leistung an nominaler Leistung gemäß Typenschild my $defhyst = 0; # default Hysterese @@ -393,6 +402,18 @@ my $cssdef = qq{.flowg.text { stroke: none; fill: gray; font-siz qq{.flowg.active_bat_out { stroke: green; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; } \n} ; + # mögliche Debug-Module +my @dd = qw( none + collectData + consumerPlanning + consumerSwitching + consumption + graphic + pvCorrection + radiationProcess + saveData2Cache + solcastProcess + ); # Steuerhashes ############### @@ -462,7 +483,7 @@ my %htr = ( # H 1 => { cl => 'odd' }, ); -my %hff = ( # Flächenfaktoren +my %hff = ( # Flächenfaktoren "0" => { N => 100, NE => 100, E => 100, SE => 100, S => 100, SW => 100, W => 100, NW => 100 }, # http://www.ing-büro-junge.de/html/photovoltaik.html "5" => { N => 95, NE => 96, E => 100, SE => 103, S => 105, SW => 103, W => 100, NW => 96 }, "10" => { N => 90, NE => 93, E => 100, SE => 105, S => 107, SW => 105, W => 100, NW => 93 }, @@ -470,7 +491,7 @@ my %hff = ( "20" => { N => 80, NE => 84, E => 97, SE => 108, S => 114, SW => 108, W => 97, NW => 84 }, "25" => { N => 75, NE => 80, E => 95, SE => 109, S => 115, SW => 109, W => 95, NW => 80 }, "30" => { N => 69, NE => 76, E => 94, SE => 110, S => 117, SW => 110, W => 94, NW => 76 }, - "35" => { N => 65, NE => 71, E => 92, SE => 110, S => 118, SW => 110, W => 92, NW => 71 }, + "35" => { N => 65, NE => 71, E => 92, SE => 110, S => 118, SW => 110, W => 92, NW => 71 }, "40" => { N => 59, NE => 68, E => 90, SE => 109, S => 117, SW => 109, W => 90, NW => 68 }, "45" => { N => 55, NE => 65, E => 87, SE => 108, S => 115, SW => 108, W => 87, NW => 65 }, "50" => { N => 49, NE => 62, E => 85, SE => 107, S => 113, SW => 107, W => 85, NW => 62 }, @@ -482,10 +503,10 @@ my %hff = ( "80" => { N => 35, NE => 46, E => 67, SE => 86, S => 95, SW => 86, W => 67, NW => 46 }, "85" => { N => 34, NE => 44, E => 64, SE => 82, S => 90, SW => 82, W => 64, NW => 44 }, "90" => { N => 33, NE => 43, E => 62, SE => 78, S => 85, SW => 78, W => 62, NW => 43 }, -); +); my %hqtxt = ( # Hash (Setup) Texte - cfd => { EN => qq{Please select the Weather forecast device with "set LINK currentForecastDev"}, + cfd => { EN => qq{Please select the Weather forecast device with "set LINK currentForecastDev"}, DE => qq{Bitte geben sie das Wettervorhersage Device mit "set LINK currentForecastDev" an} }, crd => { EN => qq{Please select the radiation forecast service with "set LINK currentRadiationDev"}, DE => qq{Bitte geben sie den Strahlungsvorhersage Dienst mit "set LINK currentRadiationDev" an} }, @@ -502,9 +523,9 @@ my %hqtxt = ( mta => { EN => qq{Please specify the module tilt angle with "set LINK moduleTiltAngle"}, DE => qq{Bitte geben Sie den Modulneigungswinkel mit "set LINK moduleTiltAngle" an} }, rip => { EN => qq{Please specify at least one combination Rooftop-ID/SolCast-API with "set LINK roofIdentPair"}, - DE => qq{Bitte geben Sie mindestens eine Kombination Rooftop-ID/SolCast-API mit "set LINK roofIdentPair" an} }, + DE => qq{Bitte geben Sie mindestens eine Kombination Rooftop-ID/SolCast-API mit "set LINK roofIdentPair" an} }, mrt => { EN => qq{Please set the assignment String / Rooftop identification with "set LINK moduleRoofTops"}, - DE => qq{Bitte setzen Sie die Zuordnung String / Rooftop Identifikation mit "set LINK moduleRoofTops"} }, + DE => qq{Bitte setzen Sie die Zuordnung String / Rooftop Identifikation mit "set LINK moduleRoofTops"} }, cnsm => { EN => qq{Consumer}, DE => qq{Verbraucher} }, eiau => { EN => qq{Off/On}, @@ -523,38 +544,50 @@ my %hqtxt = ( DE => qq{Qualität:} }, lblPvh => { EN => qq{next 4h:}, DE => qq{nächste 4h:} }, - lblPRe => { EN => qq{remain today:}, + lblPRe => { EN => qq{rest today:}, DE => qq{Rest heute:} }, lblPTo => { EN => qq{tomorrow:}, DE => qq{morgen:} }, - lblPCu => { EN => qq{actual:}, + lblPCu => { EN => qq{currently:}, DE => qq{aktuell:} }, bnsas => { EN => qq{from the upcoming sunrise}, DE => qq{ab dem kommenden Sonnenaufgang} }, - dvtn => { EN => qq{Deviation (%):}, - DE => qq{Abweichung (%):} }, + dvtn => { EN => qq{Deviation}, + DE => qq{Abweichung} }, tday => { EN => qq{today}, DE => qq{heute} }, yday => { EN => qq{yesterday}, DE => qq{gestern} }, after => { EN => qq{after}, DE => qq{nach} }, + nxtscc => { EN => qq{next SolCast call}, + DE => qq{nächste SolCast Abfrage} }, pstate => { EN => qq{Planning status: 
On: 
Off: }, DE => qq{Planungsstatus: 
Ein: 
Aus: } }, - fullfd => { EN => qq{fullfilled}, + fulfd => { EN => qq{fulfilled}, DE => qq{erfüllt} }, + pmtp => { EN => qq{produced more than predicted :-D}, + DE => qq{mehr produziert als vorhergesagt :-D} }, + petp => { EN => qq{produced same as predicted :-)}, + DE => qq{produziert wie vorhergesagt :-)} }, + pltp => { EN => qq{produced less than predicted :-(}, + DE => qq{weniger produziert als vorhergesagt :-(} }, + wusond => { EN => qq{wait until sunset}, + DE => qq{bis zum Sonnenuntergang warten} }, + snbefb => { EN => qq{should not be empty - maybe you found a bug}, + DE => qq{sollte nicht leer sein, vielleicht haben Sie einen Bug gefunden} }, awd => { EN => qq{LINK is waiting for solar forecast data ...

(The configuration can be checked with "set LINK plantConfiguration check".) }, DE => qq{LINK wartet auf Solarvorhersagedaten ...

(Die Konfiguration kann mit "set LINK plantConfiguration check" geprüft werden.)} }, - strok => { EN => qq{Congratulations 😊, the system configuration is error-free. Please observe any notes ().}, - DE => qq{Herzlichen Glückwunsch 😊, die Anlagenkonfiguration ist fehlerfrei. Bitte eventuelle Hinweise () beachten. } }, - strwn => { EN => qq{Looks quite good 😐, the system configuration is basically OK. Please observe the warnings ().}, - DE => qq{Sieht ganz gut aus 😐, die Anlagenkonfiguration ist prinzipiell in Ordnung. Bitte beachten sie die Warnungen ().} }, - strnok => { EN => qq{Oh no 🙁, the system configuration is incorrect. Please check the settings and notes !}, - DE => qq{Oh nein 🙁, die Anlagenkonfiguration ist fehlerhaft. Bitte überprüfen Sie die Einstellungen und Hinweise !} }, + strok => { EN => qq{Congratulations 😊, the system configuration is error-free. Please note any information ().}, + DE => qq{Herzlichen Glückwunsch 😊, die Anlagenkonfiguration ist fehlerfrei. Bitte eventuelle Hinweise () beachten.} }, + strwn => { EN => qq{Looks quite good 😐, the system configuration is basically OK. Please note the warnings ().}, + DE => qq{Sieht ganz gut aus 😐, die Anlagenkonfiguration ist prinzipiell in Ordnung. Bitte beachten Sie die Warnungen ().} }, + strnok => { EN => qq{Oh no 🙁, the system configuration is incorrect. Please check the settings and notes!}, + DE => qq{Oh nein 😢, die Anlagenkonfiguration ist fehlerhaft. Bitte überprüfen Sie die Einstellungen und Hinweise!} }, ); my %htitles = ( # Hash Hilfetexte (Mouse Over) - iaaf => { EN => qq{Automatic mode off -> Enable automatic mode}, + iaaf => { EN => qq{Automatic mode off -> Enable automatic mode}, DE => qq{Automatikmodus aus -> Automatik freigeben} }, ieas => { EN => qq{Automatic mode on -> Lock automatic mode}, DE => qq{Automatikmodus ein -> Automatik sperren} }, @@ -565,7 +598,7 @@ my %htitles = ( ieva => { EN => qq{On -> Switch off consumer}, DE => qq{Ein -> Verbraucher ausschalten} }, iens => { EN => qq{On -> no off-command defined!}, - DE => qq{Ein -> kein off-Kommando definiert!} }, + DE => qq{Ein -> kein off-Kommando definiert!} }, natc => { EN => qq{automatic cycle:}, DE => qq{automatischer Zyklus:} }, upd => { EN => qq{Click for update}, @@ -592,6 +625,20 @@ my %htitles = ( DE => qq{PV-Überschuß unzureichend} }, plchk => { EN => qq{Configuration check of the plant}, DE => qq{Konfigurationsprüfung der Anlage} }, + scaresps => { EN => qq{SolCast API request successful}, + DE => qq{SolCast API Abfrage erfolgreich} }, + scarespf => { EN => qq{SolCast API request failed}, + DE => qq{SolCast API Abfrage fehlgeschlagen} }, + dapic => { EN => qq{done API calls}, + DE => qq{bisherige API Abfragen} }, + rapic => { EN => qq{remaining API calls}, + DE => qq{verfügbare API Abfragen} }, + yheyfdl => { EN => qq{You have exceeded your free daily limit!}, + DE => qq{Sie haben Ihr kostenloses Tageslimit überschritten!} }, + scakdne => { EN => qq{API key does not exist}, + DE => qq{API Schlüssel existiert nicht} }, + scrsdne => { EN => qq{Rooftop site does not exist or is not accessible}, + DE => qq{Rooftop ID existiert nicht oder ist nicht abrufbar} }, ); my %weather_ids = ( @@ -686,7 +733,7 @@ my %weather_ids = ( '79' => { s => '0', icon => 'weather_frost', txtd => 'Eiskörner (gefrorene Regentropfen)', txte => 'Ice grains (frozen raindrops)' }, '80' => { s => '1', icon => 'weather_rain_light', txtd => 'leichter Regenschauer', txte => 'light rain shower' }, - '81' => { s => '1', icon => 'weather_rain', txtd => 'mäßiger oder starker Regenschauer', txte => 'moderate or heavy rain shower' }, + '81' => { s => '1', icon => 'weather_rain', txtd => 'mäßiger oder starker Regenschauer', txte => 'moderate or heavy rain shower' }, '82' => { s => '1', icon => 'weather_rain_heavy', txtd => 'sehr starker Regenschauer', txte => 'very heavy rain shower' }, '83' => { s => '0', icon => 'weather_snow', txtd => 'mäßiger oder starker Schneeregenschauer', txte => 'moderate or heavy sleet shower' }, '84' => { s => '0', icon => 'weather_snow_light', txtd => 'leichter Schneeschauer', txte => 'light snow shower' }, @@ -710,13 +757,13 @@ my %weather_ids = ( ); my %hef = ( # Energiedaktoren für Verbrauchertypen - "heater" => { f => 1.00, m => 1.00, l => 1.00, mt => 240 }, + "heater" => { f => 1.00, m => 1.00, l => 1.00, mt => 240 }, "other" => { f => 1.00, m => 1.00, l => 1.00, mt => $defmintime }, # f = Faktor Energieverbrauch in erster Stunde (wichtig auch für Kalkulation in __calcEnergyPieces !) "charger" => { f => 1.00, m => 1.00, l => 1.00, mt => 120 }, # m = Faktor Energieverbrauch zwischen erster und letzter Stunde "dishwasher" => { f => 0.45, m => 0.10, l => 0.45, mt => 180 }, # l = Faktor Energieverbrauch in letzter Stunde "dryer" => { f => 0.40, m => 0.40, l => 0.20, mt => 90 }, # mt = default mintime (Minuten) - "washingmachine" => { f => 0.30, m => 0.40, l => 0.30, mt => 120 }, -); + "washingmachine" => { f => 0.30, m => 0.40, l => 0.30, mt => 120 }, +); my %hcsr = ( # Funktiontemplate zur Erstellung optionaler Statistikreadings currentAPIinterval => { fnr => 1, fn => \&SolCastAPIVal, def => 0 }, @@ -727,17 +774,17 @@ my %hcsr = ( todayDoneAPIcalls => { fnr => 1, fn => \&SolCastAPIVal, def => 0 }, todayDoneAPIrequests => { fnr => 1, fn => \&SolCastAPIVal, def => 0 }, todayRemainingAPIcalls => { fnr => 1, fn => \&SolCastAPIVal, def => $apimaxreqs }, - todayRemainingAPIrequests => { fnr => 1, fn => \&SolCastAPIVal, def => $apimaxreqs }, - runTimeCentralTask => { fnr => 2, fn => \&CurrentVal, def => '-' }, + todayRemainingAPIrequests => { fnr => 1, fn => \&SolCastAPIVal, def => $apimaxreqs }, + runTimeCentralTask => { fnr => 2, fn => \&CurrentVal, def => '-' }, runTimeLastAPIAnswer => { fnr => 2, fn => \&CurrentVal, def => '-' }, - runTimeLastAPIProc => { fnr => 2, fn => \&CurrentVal, def => '-' }, - allStringsFullfilled => { fnr => 2, fn => \&CurrentVal, def => 0 }, + runTimeLastAPIProc => { fnr => 2, fn => \&CurrentVal, def => '-' }, + allStringsFullfilled => { fnr => 2, fn => \&CurrentVal, def => 0 }, ); # Information zu verwendeten internen Datenhashes # $data{$type}{$name}{circular} # Ringspeicher # $data{$type}{$name}{current} # current values -# $data{$type}{$name}{pvhist} # historische Werte +# $data{$type}{$name}{pvhist} # historische Werte # $data{$type}{$name}{nexthours} # NextHours Werte # $data{$type}{$name}{consumers} # Consumer Hash # $data{$type}{$name}{strings} # Stringkonfiguration @@ -750,18 +797,19 @@ sub Initialize { my $hash = shift; my $fwd = join ",", devspec2array("TYPE=FHEMWEB:FILTER=STATE=Initialized"); - my $hod = join ",", map { sprintf "%02d", $_} (01..24); + my $hod = join ",", map { sprintf "%02d", $_} (01..24); my $srd = join ",", sort keys (%hcsr); - + my ($consumer,@allc); for my $c (1..$maxconsumer) { $c = sprintf "%02d", $c; $consumer .= "consumer${c}:textField-long "; push @allc, $c; } - - my $allcs = join ",", @allc; - + + my $allcs = join ",", @allc; + my $dm = join ",", @dd; + $hash->{DefFn} = \&Define; $hash->{UndefFn} = \&Undef; $hash->{GetFn} = \&Get; @@ -782,11 +830,13 @@ sub Initialize { "affectRainfactorDamping:slider,0,1,100 ". "consumerLegend:none,icon_top,icon_bottom,text_top,text_bottom ". "consumerAdviceIcon ". + "consumerLink:0,1 ". "ctrlAutoRefresh:selectnumbers,120,0.2,1800,0,log10 ". "ctrlAutoRefreshFW:$fwd ". "ctrlConsRecommendReadings:multiple-strict,$allcs ". - "ctrlDebug:1,0 ". + "ctrlDebug:multiple-strict,$dm ". "ctrlInterval ". + "ctrlLanguage:DE,EN ". "ctrlOptimizeSolCastInterval:1,0 ". "ctrlNextDayForecastReadings:multiple-strict,$hod ". "ctrlShowLink:1,0 ". @@ -796,12 +846,12 @@ sub Initialize { "flowGraphicAnimate:1,0 ". "flowGraphicConsumerDistance:slider,80,10,500 ". "flowGraphicShowConsumer:1,0 ". - "flowGraphicShowConsumerDummy:1,0 ". - "flowGraphicShowConsumerPower:0,1 ". + "flowGraphicShowConsumerDummy:1,0 ". + "flowGraphicShowConsumerPower:0,1 ". "flowGraphicShowConsumerRemainTime:0,1 ". - "flowGraphicCss:textField-long ". + "flowGraphicCss:textField-long ". "graphicBeamHeight ". - "graphicBeamWidth:slider,40,5,100 ". + "graphicBeamWidth:slider,20,5,100 ". "graphicBeam1Color:colorpicker,RGB ". "graphicBeam2Color:colorpicker,RGB ". "graphicBeam1Content:pvForecast,pvReal,gridconsumption,consumptionForecast ". @@ -825,14 +875,14 @@ sub Initialize { "graphicEndHtml ". "graphicWeatherColor:colorpicker,RGB ". "graphicWeatherColorNight:colorpicker,RGB ". - $consumer. + $consumer. $readingFnAttributes; $hash->{FW_hideDisplayName} = 1; # Forum 88667 # $hash->{FW_addDetailToSummary} = 1; # $hash->{FW_atPageEnd} = 1; # wenn 1 -> kein Longpoll ohne informid in HTML-Tag - + $hash->{AttrRenameMap} = { "beam1Color" => "graphicBeam1Color", "beam1Content" => "graphicBeam1Content", "beam1FontColor" => "graphicBeam1FontColor", @@ -877,8 +927,8 @@ sub Initialize { }; eval { FHEM::Meta::InitMod( __FILE__, $hash ) }; ## no critic 'eval' - -return; + +return; } ############################################################### @@ -888,13 +938,13 @@ sub Define { my ($hash, $def) = @_; my @a = split(/\s+/x, $def); - + return "Error: Perl module ".$jsonabs." is missing. Install it on Debian with: sudo apt-get install libjson-perl" if($jsonabs); my $name = $hash->{NAME}; my $type = $hash->{TYPE}; $hash->{HELPER}{MODMETAABSENT} = 1 if($modMetaAbsent); # Modul Meta.pm nicht vorhanden - + my $params = { hash => $hash, notes => \%vNotesIntern, @@ -906,48 +956,48 @@ sub Define { use version 0.77; our $VERSION = moduleVersion ($params); # Versionsinformationen setzen createAssociatedWith ($hash); - + $params->{file} = $pvhcache.$name; # Cache File PV History lesen wenn vorhanden $params->{cachename} = "pvhist"; _readCacheFile ($params); $params->{file} = $pvccache.$name; # Cache File PV Circular lesen wenn vorhanden $params->{cachename} = "circular"; - _readCacheFile ($params); - + _readCacheFile ($params); + $params->{file} = $csmcache.$name; # Cache File Consumer lesen wenn vorhanden $params->{cachename} = "consumers"; _readCacheFile ($params); - + $params->{file} = $scpicache.$name; # Cache File SolCast API Werte lesen wenn vorhanden $params->{cachename} = "solcastapi"; _readCacheFile ($params); - + singleUpdateState ( {hash => $hash, state => 'initialized', evt => 1} ); - centralTask ($hash); # Einstieg in Abfrage + centralTask ($hash); # Einstieg in Abfrage InternalTimer (gettimeofday()+$whistrepeat, "FHEM::SolarForecast::periodicWriteCachefiles", $hash, 0); # Einstieg periodisches Schreiben historische Daten - + return; } ################################################################ # Cachefile lesen ################################################################ -sub _readCacheFile { +sub _readCacheFile { my $paref = shift; my $hash = $paref->{hash}; my $file = $paref->{file}; my $cachename = $paref->{cachename}; - + my $name = $hash->{NAME}; - my ($error, @content) = FileRead ($file); - + my ($error, @content) = FileRead ($file); + if(!$error) { my $json = join "", @content; - my $success = evaljson ($hash, $json); - + my $success = evaljson ($hash, $json); + if($success) { $data{$hash->{TYPE}}{$name}{$cachename} = decode_json ($json); Log3($name, 3, qq{$name - SolarForecast cache "$cachename" restored}); @@ -956,14 +1006,14 @@ sub _readCacheFile { Log3($name, 2, qq{$name - WARNING - The content of file "$file" is not readable and may be corrupt}); } } - + return; } ############################################################### # SolarForecast Set ############################################################### -sub Set { +sub Set { my ($hash, @a) = @_; return "\"set X\" needs at least an argument" if ( @a < 2 ); my $name = shift @a; @@ -973,15 +1023,15 @@ sub Set { my $prop = shift @a; my $prop1 = shift @a; my $prop2 = shift @a; - + return if(IsDisabled($name)); - + my ($setlist,@fcdevs,@cfs,@scp,@condevs); my ($fcd,$ind,$med,$cf,$sp,$cons) = ("","","","","","noArg"); - + my @re = qw( ConsumerMaster consumerPlanning - currentBatteryDev + currentBatteryDev currentForecastDev currentInverterDev currentMeterDev @@ -993,25 +1043,25 @@ sub Set { roofIdentPair pvHistory ); - my $resets = join ",",@re; - + my $resets = join ",",@re; + @fcdevs = devspec2array("TYPE=DWD_OpenData"); $fcd = join ",", @fcdevs if(@fcdevs); - + push @fcdevs, 'SolCast-API'; my $rdd = join ",", @fcdevs; for my $h (@chours) { - push @cfs, 'pvCorrectionFactor_'. sprintf("%02d",$h); + push @cfs, 'pvCorrectionFactor_'. sprintf("%02d",$h); } $cf = join " ", @cfs; - + my $type = $hash->{TYPE}; - + for my $c (sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}) { push @condevs, $c if($c); } - $cons = join ",", @condevs if(@condevs); + $cons = join ",", @condevs if(@condevs); $setlist = "Unknown argument $opt, choose one of ". "consumerImmediatePlanning:$cons ". @@ -1034,7 +1084,7 @@ sub Set { "writeHistory:noArg ". $cf ; - + my $params = { hash => $hash, name => $name, @@ -1046,10 +1096,10 @@ sub Set { prop1 => $prop1, prop2 => $prop2 }; - + if($hset{$opt} && defined &{$hset{$opt}{fn}}) { my $ret = q{}; - $ret = &{$hset{$opt}{fn}} ($params); + $ret = &{$hset{$opt}{fn}} ($params); return $ret; } @@ -1067,31 +1117,32 @@ sub _setconsumerImmediatePlanning { ## no critic "not used" my $opt = $paref->{opt}; my $c = $paref->{prop}; my $evt = $paref->{prop1} // 1; - + return qq{no consumer number specified} if(!$c); - return qq{no valid consumer id "$c"} if(!ConsumerVal ($hash, $c, "name", "")); + return qq{no valid consumer id "$c"} if(!ConsumerVal ($hash, $c, "name", "")); my $startts = time; my $stopdiff = ceil(ConsumerVal ($hash, $c, "mintime", $defmintime) / 60) * 3600; - my $stopts = $startts + $stopdiff; - + my $stopts = $startts + $stopdiff; + $paref->{consumer} = $c; $paref->{ps} = "planned:"; $paref->{startts} = $startts; # Unix Timestamp für geplanten Switch on $paref->{stopts} = $stopts; # Unix Timestamp für geplanten Switch off ___setConsumerPlanningState ($paref); + ___saveEhodpieces ($paref); ___setPlanningDeleteMeth ($paref); - + my $planstate = ConsumerVal ($hash, $c, "planstate", ""); my $calias = ConsumerVal ($hash, $c, "alias", ""); - + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - + Log3 ($name, 3, qq{$name - Consumer "$calias" $planstate}) if($planstate); centralTask ($hash, $evt); - + return; } @@ -1127,13 +1178,13 @@ sub _setcurrentRadiationDev { ## no critic "not used" if($prop ne 'SolCast-API' && (!$defs{$prop} || $defs{$prop}{TYPE} ne "DWD_OpenData")) { return qq{The device "$prop" doesn't exist or has no TYPE "DWD_OpenData"}; } - - if ($prop eq 'SolCast-API') { - return "The library FHEM::Utility::CTZ is missed. Please update FHEM completely." if($ctzAbsent); - + + if ($prop eq 'SolCast-API') { + return "The library FHEM::Utility::CTZ is missing. Please update FHEM completely." if($ctzAbsent); + my $rmf = reqModFail(); return "You have to install the required perl module: ".$rmf if($rmf); - } + } readingsSingleUpdate ($hash, "currentRadiationDev", $prop, 1); createAssociatedWith ($hash); @@ -1156,28 +1207,28 @@ sub _setroofIdentPair { ## no critic "not used" if(!$arg) { return qq{The command "$opt" needs an argument !}; } - + my ($a,$h) = parseParams ($arg); my $pk = $a->[0] // ""; - + if(!$pk) { return qq{Every roofident pair needs a pairkey! Use: rtid= apikey=}; } - + if(!$h->{rtid} || !$h->{apikey}) { return qq{The syntax of "$opt" is not correct. Please consider the commandref.}; - } - + } + my $type = $hash->{TYPE}; - + $data{$type}{$name}{solcastapi}{'?IdPair'}{'?'.$pk}{rtid} = $h->{rtid}; $data{$type}{$name}{solcastapi}{'?IdPair'}{'?'.$pk}{apikey} = $h->{apikey}; - + writeDataToFile ($hash, "solcastapi", $scpicache.$name); # Cache File SolCast API Werte schreiben - + my $msg = qq{The Roof identification pair "$pk" has been saved. }. qq{Repeat the command if you want to save more Roof identification pairs.}; - + return $msg; } @@ -1189,31 +1240,31 @@ sub _setmoduleRoofTops { ## no critic "not used" my $hash = $paref->{hash}; my $name = $paref->{name}; my $arg = $paref->{arg} // return qq{no module RoofTop was provided}; - - my ($a,$h) = parseParams ($arg); - + + my ($a,$h) = parseParams ($arg); + if(!keys %$h) { return qq{The provided module RoofTop has wrong format}; } - + while (my ($is, $pk) = each %$h) { my $rtid = SolCastAPIVal ($hash, '?IdPair', '?'.$pk, 'rtid', ''); my $apikey = SolCastAPIVal ($hash, '?IdPair', '?'.$pk, 'apikey', ''); - + if(!$rtid || !$apikey) { return qq{The roofIdentPair "$pk" of String "$is" has no Rooftop-ID and/or SolCast-API key assigned ! \n}. qq{Set the roofIdentPair "$pk" previously with "set $name roofIdentPair".} ; - } + } } - readingsSingleUpdate ($hash, "moduleRoofTops", $arg, 1); + readingsSingleUpdate ($hash, "moduleRoofTops", $arg, 1); writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben - + return if(_checkSetupNotComplete ($hash)); # keine Stringkonfiguration wenn Setup noch nicht komplett my $ret = createStringConfig ($hash); return $ret if($ret); - + return; } @@ -1230,17 +1281,17 @@ sub _setinverterDevice { ## no critic "not used" if(!$arg) { return qq{The command "$opt" needs an argument !}; } - + my ($a,$h) = parseParams ($arg); my $indev = $a->[0] // ""; - + if(!$indev || !$defs{$indev}) { return qq{The device "$indev" doesn't exist!}; } - + if(!$h->{pv} || !$h->{etotal}) { return qq{The syntax of "$opt" is not correct. Please consider the commandref.}; - } + } readingsSingleUpdate ($hash, "currentInverterDev", $arg, 1); createAssociatedWith ($hash); @@ -1257,29 +1308,29 @@ sub _setinverterStrings { ## no critic "not used" my $hash = $paref->{hash}; my $name = $paref->{name}; my $prop = $paref->{prop} // return qq{no inverter strings specified}; - + if ($prop =~ /\?/xs) { return qq{The inverter string designation is wrong. An inverter string name must not contain a '?' character!}; } - + my $type = $hash->{TYPE}; - - my @istrings = split ",", $prop; + + my @istrings = split ",", $prop; for my $k (keys %{$data{$type}{$name}{solcastapi}}) { next if ($k =~ /\?/xs || $k ~~ @istrings); - delete $data{$type}{$name}{solcastapi}{$k}; + delete $data{$type}{$name}{solcastapi}{$k}; } readingsSingleUpdate ($hash, "inverterStrings", $prop, 1); writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben - + return if(_checkSetupNotComplete ($hash)); # keine Stringkonfiguration wenn Setup noch nicht komplett - + my $ret = qq{NOTE: After setting or changing "inverterStrings" please check }. qq{/ set all module parameter (e.g. moduleTiltAngle) again ! \n}. qq{Use "set $name plantConfiguration check" to validate your Setup.}; - + return $ret; } @@ -1296,21 +1347,21 @@ sub _setmeterDevice { ## no critic "not used" if(!$arg) { return qq{The command "$opt" needs an argument !}; } - + my ($a,$h) = parseParams ($arg); my $medev = $a->[0] // ""; - + if(!$medev || !$defs{$medev}) { return qq{The device "$medev" doesn't exist!}; } - + if(!$h->{gcon} || !$h->{contotal} || !$h->{gfeedin} || !$h->{feedtotal}) { return qq{The syntax of "$opt" is not correct. Please consider the commandref.}; } if($h->{gcon} eq "-gfeedin" && $h->{gfeedin} eq "-gcon") { return qq{Incorrect input. It is not allowed that the keys gcon and gfeedin refer to each other.}; - } + } readingsSingleUpdate ($hash, "currentMeterDev", $arg, 1); createAssociatedWith ($hash); @@ -1332,26 +1383,26 @@ sub _setbatteryDevice { ## no critic "not used" if(!$arg) { return qq{The command "$opt" needs an argument !}; } - + my ($a,$h) = parseParams ($arg); my $badev = $a->[0] // ""; - + if(!$badev || !$defs{$badev}) { return qq{The device "$badev" doesn't exist!}; } - + if(!$h->{pin} || !$h->{pout}) { return qq{The keys "pin" and/or "pout" are not set. Please note the command reference.}; } - - if(($h->{pin} !~ /-/xs && $h->{pin} !~ /:/xs) || + + if(($h->{pin} !~ /-/xs && $h->{pin} !~ /:/xs) || ($h->{pout} !~ /-/xs && $h->{pout} !~ /:/xs)) { return qq{The keys "pin" and/or "pout" are not set correctly. Please note the command reference.}; } if($h->{pin} eq "-pout" && $h->{pout} eq "-pin") { return qq{Incorrect input. It is not allowed that the keys pin and pout refer to each other.}; - } + } readingsSingleUpdate ($hash, "currentBatteryDev", $arg, 1); createAssociatedWith ($hash); @@ -1373,19 +1424,19 @@ sub _setpowerTrigger { ## no critic "not used" if(!$arg) { return qq{The command "$opt" needs an argument !}; } - + my ($a,$h) = parseParams ($arg); - + if(!$h) { return qq{The syntax of "$opt" is not correct. Please consider the commandref.}; } - + for my $key (keys %{$h}) { if($key !~ /^[0-9]+(?:on|off)$/x || $h->{$key} !~ /^[0-9]+$/x) { return qq{The key "$key" is invalid. Please consider the commandref.}; } } - + writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben readingsSingleUpdate($hash, "powerTrigger", $arg, 1); @@ -1406,19 +1457,19 @@ sub _setenergyH4Trigger { ## no critic "not used" if(!$arg) { return qq{The command "$opt" needs an argument !}; } - + my ($a,$h) = parseParams ($arg); - + if(!$h) { return qq{The syntax of "$opt" is not correct. Please consider the commandref.}; } - + for my $key (keys %{$h}) { if($key !~ /^[0-9]+(?:on|off)$/x || $h->{$key} !~ /^[0-9]+$/x) { return qq{The key "$key" is invalid. Please consider the commandref.}; } } - + writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben readingsSingleUpdate($hash, "energyH4Trigger", $arg, 1); @@ -1434,26 +1485,26 @@ sub _setmodulePeakString { ## no critic "not used" my $hash = $paref->{hash}; my $name = $paref->{name}; my $arg = $paref->{arg} // return qq{no PV module peak specified}; - + $arg =~ s/,/./xg; - - my ($a,$h) = parseParams ($arg); - + + my ($a,$h) = parseParams ($arg); + if(!keys %$h) { return qq{The provided PV module peak has wrong format}; } - + while (my ($key, $value) = each %$h) { if($value !~ /[0-9.]/x) { return qq{The module peak of "$key" must be specified by numbers and optionally with decimal places}; - } + } } - + readingsSingleUpdate ($hash, "modulePeakString", $arg, 1); writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben - + return if(_checkSetupNotComplete ($hash)); # keine Stringkonfiguration wenn Setup noch nicht komplett - + my $ret = createStringConfig ($hash); return $ret if($ret); @@ -1468,11 +1519,11 @@ sub _setmoduleTiltAngle { ## no critic "not used" my $hash = $paref->{hash}; my $name = $paref->{name}; my $arg = $paref->{arg} // return qq{no tilt angle was provided}; - + my $tilt = join "|", sort keys %hff; - my ($a,$h) = parseParams ($arg); - + my ($a,$h) = parseParams ($arg); + if(!keys %$h) { return qq{The provided tilt angle has wrong format}; } @@ -1480,14 +1531,14 @@ sub _setmoduleTiltAngle { ## no critic "not used" while (my ($key, $value) = each %$h) { if($value !~ /^(?:$tilt)$/x) { return qq{The tilt angle of "$key" is wrong}; - } + } } - + readingsSingleUpdate ($hash, "moduleTiltAngle", $arg, 1); - writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben - + writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben + return if(_checkSetupNotComplete ($hash)); # keine Stringkonfiguration wenn Setup noch nicht komplett - + my $ret = createStringConfig ($hash); return $ret if($ret); @@ -1504,22 +1555,22 @@ sub _setmoduleDirection { ## no critic "not used" my $arg = $paref->{arg} // return qq{no module direction was provided}; my $dirs = "N|NE|E|SE|S|SW|W|NW"; # mögliche Richtungsangaben - - my ($a,$h) = parseParams ($arg); - + + my ($a,$h) = parseParams ($arg); + if(!keys %$h) { return qq{The provided module direction has wrong format}; } - + while (my ($key, $value) = each %$h) { if($value !~ /^(?:$dirs)$/x) { return qq{The module direction of "$key" is wrong: $value}; - } + } } readingsSingleUpdate ($hash, "moduleDirection", $arg, 1); writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben - + return if(_checkSetupNotComplete ($hash)); # keine Stringkonfiguration wenn Setup noch nicht komplett my $ret = createStringConfig ($hash); @@ -1537,24 +1588,24 @@ sub _setplantConfiguration { ## no critic "not used" my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; - + my ($err,@pvconf); - + $arg = 'check' if (!$arg); - - if($arg eq "check") { + + if($arg eq "check") { my $out = checkPlantConfig ($hash); $out = qq{$out}; - + ## asynchrone Ausgabe ####################### #$err = getClHash($hash); #$paref->{out} = $out; #InternalTimer(gettimeofday()+3, "FHEM::SolarForecast::__plantCfgAsynchOut", $paref, 0); - + return $out; } - + if($arg eq "save") { $err = writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben if($err) { @@ -1564,10 +1615,10 @@ sub _setplantConfiguration { ## no critic "not used" return qq{Plant Configuration has been written to file "}.$plantcfg.$name.qq{"}; } } - + if($arg eq "restore") { - ($err, @pvconf) = FileRead ($plantcfg.$name); - + ($err, @pvconf) = FileRead ($plantcfg.$name); + if(!$err) { my $rbit = 0; for my $elem (@pvconf) { @@ -1575,8 +1626,8 @@ sub _setplantConfiguration { ## no critic "not used" next if(!$reading || !defined $val); CommandSetReading (undef,"$name $reading $val"); $rbit = 1; - } - + } + if($rbit) { return qq{Plant Configuration restored from file "}.$plantcfg.$name.qq{"}; } @@ -1586,16 +1637,16 @@ sub _setplantConfiguration { ## no critic "not used" } else { return $err; - } + } } - + return; } ################################################################ # asynchrone Ausgabe Ergbnis Plantconfig Check ################################################################ -sub __plantCfgAsynchOut { +sub __plantCfgAsynchOut { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; @@ -1603,7 +1654,7 @@ sub __plantCfgAsynchOut { asyncOutput($hash->{HELPER}{CL}{1}, $out); delClHash ($name); - + return; } @@ -1620,14 +1671,14 @@ sub _setpvCorrectionFactor { ## no critic "not used" if($prop !~ /[0-9,.]/x) { return qq{The correction value must be specified by numbers and optionally with decimal places}; } - + $prop =~ s/,/./x; - + readingsSingleUpdate($hash, $opt, $prop." (manual)", 1); - - my $cfnum = (split "_", $opt)[1]; + + my $cfnum = (split "_", $opt)[1]; deleteReadingspec ($hash, "pvCorrectionFactor_${cfnum}_autocalc"); - + centralTask ($hash, 0); return; @@ -1642,19 +1693,19 @@ sub _setpvCorrectionFactorAuto { ## no critic "not used" my $name = $paref->{name}; my $opt = $paref->{opt}; my $prop = $paref->{prop} // return qq{no correction value specified}; - + readingsSingleUpdate($hash, "pvCorrectionFactor_Auto", $prop, 1); - + if($prop eq "off") { for my $n (1..24) { $n = sprintf "%02d", $n; my $rv = ReadingsVal ($name, "pvCorrectionFactor_${n}", ""); deleteReadingspec ($hash, "pvCorrectionFactor_${n}.*") if($rv !~ /manual/xs); } - + deleteReadingspec ($hash, "pvCorrectionFactor_.*_autocalc"); } - + writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration sichern return; @@ -1668,9 +1719,9 @@ sub _setreset { ## no critic "not used" my $hash = $paref->{hash}; my $name = $paref->{name}; my $prop = $paref->{prop} // return qq{no source specified for reset}; - + my $type = $hash->{TYPE}; - + if($prop eq "pvHistory") { my $day = $paref->{prop1} // ""; # ein bestimmter Tag der pvHistory angegeben ? my $dhour = $paref->{prop2} // ""; # eine bestimmte Stunde eines Tages der pvHistory angegeben ? @@ -1678,7 +1729,7 @@ sub _setreset { ## no critic "not used" if ($day) { if($dhour) { delete $data{$type}{$name}{pvhist}{$day}{$dhour}; - Log3($name, 3, qq{$name - Hour "$dhour" of day "$day" deleted in pvHistory}); + Log3($name, 3, qq{$name - Hour "$dhour" of day "$day" deleted in pvHistory}); } else { delete $data{$type}{$name}{pvhist}{$day}; @@ -1688,19 +1739,19 @@ sub _setreset { ## no critic "not used" else { delete $data{$type}{$name}{pvhist}; Log3($name, 3, qq{$name - all days of pvHistory deleted}); - } + } return; } - + if($prop eq "pvCorrection") { for my $n (1..24) { $n = sprintf "%02d", $n; deleteReadingspec ($hash, "pvCorrectionFactor_${n}.*"); } - + my $circ = $paref->{prop1} // 'no'; # alle pvKorr-Werte aus Caches löschen ? my $circh = $paref->{prop2} // q{}; # pvKorr-Werte einer bestimmten Stunde aus Caches löschen ? - + if ($circ eq "cached") { if ($circh) { delete $data{$type}{$name}{circular}{$circh}{pvcorrf}; @@ -1708,41 +1759,41 @@ sub _setreset { ## no critic "not used" for my $hid (keys %{$data{$type}{$name}{pvhist}}) { delete $data{$type}{$name}{pvhist}{$hid}{$circh}{pvcorrf}; - } + } Log3($name, 3, qq{$name - stored PV correction factor / SolCast percentile of hour "$circh" from pvCircular and pvHistory deleted}); - return; + return; } - + for my $hod (keys %{$data{$type}{$name}{circular}}) { delete $data{$type}{$name}{circular}{$hod}{pvcorrf}; delete $data{$type}{$name}{circular}{$hod}{quality}; } - + for my $hid (keys %{$data{$type}{$name}{pvhist}}) { for my $hidh (keys %{$data{$type}{$name}{pvhist}{$hid}}) { delete $data{$type}{$name}{pvhist}{$hid}{$hidh}{pvcorrf}; } } - + Log3($name, 3, qq{$name - all stored PV correction factors / SolCast percentile from pvCircular and pvHistory deleted}); } - + return; } - + if($prop eq "powerTrigger") { deleteReadingspec ($hash, "powerTrigger.*"); writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben return; } - + if($prop eq "energyH4Trigger") { deleteReadingspec ($hash, "energyH4Trigger.*"); writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben return; } - + if($prop eq "moduleRoofTops") { deleteReadingspec ($hash, "moduleRoofTops"); writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben @@ -1750,10 +1801,10 @@ sub _setreset { ## no critic "not used" } readingsDelete($hash, $prop); - + if($prop eq "roofIdentPair") { my $pk = $paref->{prop1} // ""; # ein bestimmter PairKey angegeben ? - + if ($pk) { delete $data{$type}{$name}{solcastapi}{'?IdPair'}{'?'.$pk}; Log3 ($name, 3, qq{$name - roofIdentPair: pair key "$pk" deleted}); @@ -1761,12 +1812,12 @@ sub _setreset { ## no critic "not used" else { delete $data{$type}{$name}{solcastapi}{'?IdPair'}; Log3($name, 3, qq{$name - roofIdentPair: all pair keys deleted}); - } - + } + writeDataToFile ($hash, "solcastapi", $scpicache.$name); # Cache File SolCast API Werte schreiben return; } - + if($prop eq "currentMeterDev") { readingsDelete($hash, "Current_GridConsumption"); readingsDelete($hash, "Current_GridFeedIn"); @@ -1779,10 +1830,10 @@ sub _setreset { ## no critic "not used" delete $data{$type}{$name}{current}{autarkyrate}; delete $data{$type}{$name}{current}{selfconsumption}; delete $data{$type}{$name}{current}{selfconsumptionrate}; - + writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben } - + if($prop eq "currentBatteryDev") { readingsDelete($hash, "Current_PowerBatIn"); readingsDelete($hash, "Current_PowerBatOut"); @@ -1790,34 +1841,34 @@ sub _setreset { ## no critic "not used" delete $data{$type}{$name}{current}{powerbatout}; delete $data{$type}{$name}{current}{powerbatin}; delete $data{$type}{$name}{current}{batcharge}; - + writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben } - + if($prop eq "currentInverterDev") { readingsDelete ($hash, "Current_PV"); deleteReadingspec ($hash, ".*_PVreal" ); writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben } - + if($prop eq "consumerPlanning") { # Verbraucherplanung resetten my $c = $paref->{prop1} // ""; # bestimmten Verbraucher setzen falls angegeben - + if ($c) { deleteConsumerPlanning ($hash, $c); } else { for my $cs (keys %{$data{$type}{$name}{consumers}}) { deleteConsumerPlanning ($hash, $cs); - } + } } - + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben } if($prop eq "consumerMaster") { # Verbraucherhash löschen my $c = $paref->{prop1} // ""; # bestimmten Verbraucher setzen falls angegeben - + if ($c) { my $calias = ConsumerVal ($hash, $c, "alias", ""); delete $data{$type}{$name}{consumers}{$c}; @@ -1828,12 +1879,12 @@ sub _setreset { ## no critic "not used" my $calias = ConsumerVal ($hash, $cs, "alias", ""); delete $data{$type}{$name}{consumers}{$cs}; Log3($name, 3, qq{$name - Consumer "$calias" deleted from memory}); - } + } } - + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - } - + } + createAssociatedWith ($hash); return; @@ -1846,9 +1897,9 @@ sub _setwriteHistory { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; - + my $ret; - + $ret = writeDataToFile ($hash, "circular", $pvccache.$name); # Cache File für PV Circular schreiben $ret = writeDataToFile ($hash, "pvhist", $pvhcache.$name); # Cache File für PV History schreiben @@ -1865,41 +1916,41 @@ sub _setclientAction { ## no critic "not used" my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; - my $argsref = $paref->{argsref}; + my $argsref = $paref->{argsref}; if(!$arg) { return qq{The command "$opt" needs an argument !}; } - + my @args = @{$argsref}; - + my $evt = shift @args; # Readings Event (state nicht gesteuert) my $action = shift @args; # z.B. set, setreading my $cname = shift @args; # Consumername - my $tail = join " ", map { my $p = $_; $p =~ s/\s//xg; $p; } @args; ## no critic 'Map blocks' # restliche Befehlsargumente - + my $tail = join " ", map { my $p = $_; $p =~ s/\s//xg; $p; } @args; ## no critic 'Map blocks' # restliche Befehlsargumente + Log3($name, 4, qq{$name - Client Action received / execute: "$action $cname $tail"}); - + if($action eq "set") { CommandSet (undef, "$cname $tail"); } - + if($action eq "get") { if($tail eq 'data') { centralTask ($hash, $evt); return; } } - + if($action eq "setreading") { CommandSetReading (undef, "$cname $tail"); } - + if($action eq "consumerImmediatePlanning") { CommandSet (undef, "$name $action $cname $evt"); return; } - + centralTask ($hash, $evt); return; @@ -1914,7 +1965,7 @@ sub Get { my $name = shift @a; my $opt = shift @a; my $arg = join " ", map { my $p = $_; $p =~ s/\s//xg; $p; } @a; ## no critic 'Map blocks' - + my $getlist = "Unknown argument $opt, choose one of ". "valConsumerMaster:noArg ". "data:noArg ". @@ -1927,9 +1978,9 @@ sub Get { "solCastData:noArg ". "valCurrent:noArg " ; - + return if(IsDisabled($name)); - + my $params = { hash => $hash, name => $name, @@ -1937,20 +1988,20 @@ sub Get { opt => $opt, arg => $arg }; - + if($hget{$opt} && defined &{$hget{$opt}{fn}}) { my $ret = q{}; - - if (!$hash->{CREDENTIALS} && $hget{$opt}{needcred}) { + + if (!$hash->{CREDENTIALS} && $hget{$opt}{needcred}) { return qq{Credentials of $name are not set."}; } - + $params->{force} = 1 if($opt eq 'roofTopData'); - + $ret = &{$hget{$opt}{fn}} ($params); # forcierter (manueller) Abruf SolCast API return $ret; } - + return $getlist; } @@ -1963,77 +2014,79 @@ sub _getRoofTopData { my $name = $paref->{name}; my $force = $paref->{force} // 0; my $t = $paref->{t} // time; - + + my $lang = AttrVal ($name, 'ctrlLanguage', AttrVal ('global', 'language', $deflang)); + if (!$force) { # regulärer SolCast API Abruf my $trr = SolCastAPIVal($hash, '?All', '?All', 'todayRemainingAPIrequests', $apimaxreqs); - my $lang = AttrVal ('global', 'language', 'EN'); - + if ($trr <= 0) { # Test readingsSingleUpdate($hash, 'nextSolCastCall', $hqtxt{bnsas}{$lang}, 1); return qq{SolCast free daily limit is used up}; } - + my $date = strftime "%Y-%m-%d", localtime($t); - my $srtime = timestringToTimestamp ($date.' '.ReadingsVal($name, "Today_SunRise", '23:59').':59'); + my $srtime = timestringToTimestamp ($date.' '.ReadingsVal($name, "Today_SunRise", '23:59').':59'); my $sstime = timestringToTimestamp ($date.' '.ReadingsVal($name, "Today_SunSet", '00:00').':00'); if ($t < $srtime || $t > $sstime) { readingsSingleUpdate($hash, 'nextSolCastCall', $hqtxt{bnsas}{$lang}, 1); return qq{The current time is not between sunrise and sunset}; } - + my $lrt = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_timestamp', 0); my $apiitv = SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $apirepetdef); - + if ($lrt && $t < $lrt + $apiitv) { my $rt = $lrt + $apiitv - $t; return qq{The waiting time to the next SolCast API call has not expired yet. The remaining waiting time is $rt seconds}; } } - + my $msg; if($ctzAbsent) { - $msg = qq{The library FHEM::Utility::CTZ is missed. Please update FHEM completely.}; - Log3 ($name, 2, "$name - ERROR - $msg"); + $msg = qq{The library FHEM::Utility::CTZ is missing. Please update FHEM completely.}; + Log3 ($name, 2, "$name - ERROR - $msg"); return $msg; } - + my $rmf = reqModFail(); if($rmf) { $msg = "You have to install the required perl module: ".$rmf; - Log3 ($name, 2, "$name - ERROR - $msg"); + Log3 ($name, 2, "$name - ERROR - $msg"); return $msg; } - + my $type = $hash->{TYPE}; - + ## statische SolCast API Kennzahlen bereitstellen ################################################### my %seen; my @as = map { $data{$type}{$name}{solcastapi}{'?IdPair'}{$_}{apikey}; } keys %{$data{$type}{$name}{solcastapi}{'?IdPair'}}; - my @unique = grep { !$seen{$_}++ } @as; + my @unique = grep { !$seen{$_}++ } @as; my $upc = scalar @unique; # Anzahl unique API Keys - + my $asc = CurrentVal ($hash, 'allstringscount', 1); # Anzahl der Strings my $madr = sprintf "%.0f", (($apimaxreqs / $asc) * $upc); # max. tägliche Anzahl API Calls - my $mpk = sprintf "%.4f", ($apimaxreqs / $madr); # Requestmultiplikator - - $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{solCastAPIcallMultiplier} = $mpk; + my $mpk = sprintf "%.4f", ($apimaxreqs / $madr); # Requestmultiplikator + + $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{solCastAPIcallMultiplier} = $mpk; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayMaxAPIcalls} = $madr; - + ## - + $paref->{allstrings} = ReadingsVal($name, 'inverterStrings', ''); - - __solCast_ApiRequest ($paref); + $paref->{lang} = $lang; + __solCast_ApiRequest ($paref); + return; } ################################################################################################ # SolCast Api Request # -# noch testen und einbauen Abruf aktuelle Daten ohne Rooftops +# noch testen und einbauen Abruf aktuelle Daten ohne Rooftops # (aus https://www.solarquotes.com.au/blog/how-to-use-solcast/): # https://api.solcast.com.au/pv_power/estimated_actuals?longitude=12.067722&latitude=51.285272& # capacity=5130&azimuth=180&tilt=30&format=json&api_key=.... @@ -2047,20 +2100,21 @@ sub __solCast_ApiRequest { my $string; ($string, $allstrings) = split ",", $allstrings, 2; - + my $rft = ReadingsVal ($name, "moduleRoofTops", ""); my ($a,$h) = parseParams ($rft); - + my $pk = $h->{$string} // q{}; my $roofid = SolCastAPIVal ($hash, '?IdPair', '?'.$pk, 'rtid', ''); my $apikey = SolCastAPIVal ($hash, '?IdPair', '?'.$pk, 'apikey', ''); - + my $debug = AttrVal ($name, 'ctrlDebug', 'none'); + if(!$roofid || !$apikey) { my $err = qq{The roofIdentPair "$pk" of String "$string" has no Rooftop-ID and/or SolCast-API key assigned !}; singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); return $err; } - + my $url = "https://api.solcast.com.au/rooftop_sites/". $roofid. "/forecasts?format=json". @@ -2068,10 +2122,12 @@ sub __solCast_ApiRequest { "&api_key=". $apikey; - Log3 ($name, 4, qq{$name - Request SolCast API for string "$string": $url}); - + if($debug =~ /solcastProcess/x) { # nur für Debugging + Log (1, qq{$name DEBUG> Request SolCast API for string "$string": $url}); + } + my $caller = (caller(0))[3]; # Rücksprungmarke - + my $param = { url => $url, timeout => 30, @@ -2080,12 +2136,13 @@ sub __solCast_ApiRequest { stc => [gettimeofday], allstrings => $allstrings, string => $string, + lang => $paref->{lang}, method => "GET", callback => \&__solCast_ApiResponse }; - - HttpUtils_NonblockingGet ($param); - + + HttpUtils_NonblockingGet ($param); + return; } @@ -2096,56 +2153,57 @@ sub __solCast_ApiResponse { my $paref = shift; my $err = shift; my $myjson = shift; - + my $hash = $paref->{hash}; my $name = $hash->{NAME}; my $caller = $paref->{caller}; my $string = $paref->{string}; my $allstrings = $paref->{allstrings}; my $stc = $paref->{stc}; # Startzeit API Abruf + my $lang = $paref->{lang}; my $type = $hash->{TYPE}; my $t = time; - + my $msg; - + my $sta = [gettimeofday]; # Start Response Verarbeitung if ($err ne "") { - $msg = 'SolCast API server response: '.$err; - + $msg = 'SolCast API server response: '.$err; + Log3 ($name, 2, "$name - $msg"); - + $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $err; - + singleUpdateState ( {hash => $hash, state => $msg, evt => 1} ); $data{$type}{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln $data{$type}{$name}{current}{runTimeLastAPIAnswer} = sprintf "%.4f", (tv_interval($stc) - tv_interval($sta)); # API Laufzeit ermitteln - + return; - } + } elsif ($myjson ne "") { # Evaluiere ob Daten im JSON-Format empfangen wurden my ($success) = evaljson($hash, $myjson); - + if(!$success) { - $msg = 'ERROR - invalid SolCast API server response'; - + $msg = 'ERROR - invalid SolCast API server response'; + Log3 ($name, 2, "$name - $msg"); - + singleUpdateState ( {hash => $hash, state => $msg, evt => 1} ); $data{$type}{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln $data{$type}{$name}{current}{runTimeLastAPIAnswer} = sprintf "%.4f", (tv_interval($stc) - tv_interval($sta)); # API Laufzeit ermitteln return; } - + my $jdata = decode_json ($myjson); - my $debug = AttrVal ($name, "ctrlDebug", 0); - - if($debug) { # nur für Debugging - Log (1, qq{DEBUG> $name SolCast API server response for string "$string":\n}. Dumper $jdata); + my $debug = AttrVal ($name, 'ctrlDebug', 'none'); + + if($debug =~ /solcastProcess/x) { # nur für Debugging + Log (1, qq{$name DEBUG> SolCast API server response for string "$string":\n}. Dumper $jdata); } - + ## bei Überschreitung Limit kommt: #################################### # 'response_status' => { @@ -2153,106 +2211,108 @@ sub __solCast_ApiResponse { # 'errors' => [], # 'error_code' => 'TooManyRequests' # } - + if (defined $jdata->{'response_status'}) { $msg = 'SolCast API server response: '.$jdata->{'response_status'}{'message'}; - + Log3 ($name, 3, "$name - $msg"); - - ___setLastAPIcallKeyData ($hash, $t); - + + ___setLastAPIcallKeyData ($hash, $lang, $t); + $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $jdata->{'response_status'}{'message'}; - + singleUpdateState ( {hash => $hash, state => $msg, evt => 1} ); $data{$type}{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln $data{$type}{$name}{current}{runTimeLastAPIAnswer} = sprintf "%.4f", (tv_interval($stc) - tv_interval($sta)); # API Laufzeit ermitteln - + return; } - + my $k = 0; - my ($period,$starttmstr); - + my ($period,$starttmstr); + while ($jdata->{'forecasts'}[$k]) { # vorhandene Startzeiten Schlüssel im SolCast API Hash löschen my $petstr = $jdata->{'forecasts'}[$k]{'period_end'}; - ($err, $starttmstr) = ___convPendToPstart ($name, $petstr); - - if ($err) { + ($err, $starttmstr) = ___convPendToPstart ($name, $lang, $petstr); + + if ($err) { Log3 ($name, 2, "$name - $err"); - + singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); return; } - - if(!$k && $petstr =~ /T\d{2}:00/xs) { # spezielle Behandlung ersten Datensatz wenn period_end auf volle Stunde fällt (es fehlt dann der erste Teil der Stunde) - $period = $jdata->{'forecasts'}[$k]{'period'}; # -> dann bereits beim letzten Abruf gespeicherte Daten der aktuellen Stunde durch 2 teilen damit + + if(!$k && $petstr =~ /T\d{2}:00/xs) { # spezielle Behandlung ersten Datensatz wenn period_end auf volle Stunde fällt (es fehlt dann der erste Teil der Stunde) + $period = $jdata->{'forecasts'}[$k]{'period'}; # -> dann bereits beim letzten Abruf gespeicherte Daten der aktuellen Stunde durch 2 teilen damit $period =~ s/.*(\d\d).*/$1/; # -> die neuen Daten (in dem Fall nur die einer halben Stunde) im nächsten Schritt addiert werden - + my $est50 = SolCastAPIVal ($hash, $string, $starttmstr, 'pv_estimate50', 0) / (60/$period); - $data{$type}{$name}{solcastapi}{$string}{$starttmstr}{pv_estimate50} = $est50 if($est50); - + $data{$type}{$name}{solcastapi}{$string}{$starttmstr}{pv_estimate50} = $est50 if($est50); + $k++; - next; + next; } - + delete $data{$type}{$name}{solcastapi}{$string}{$starttmstr}; - $k++; - } - - $k = 0; + $k++; + } + + $k = 0; my $uac = ReadingsVal ($name, 'pvCorrectionFactor_Auto', 'off'); # Auto- oder manuelle Korrektur - + while ($jdata->{'forecasts'}[$k]) { if(!$jdata->{'forecasts'}[$k]{'pv_estimate'}) { # keine PV Prognose -> Datensatz überspringen -> Verarbeitungszeit sparen $k++; - next; + next; } - + my $petstr = $jdata->{'forecasts'}[$k]{'period_end'}; - ($err, $starttmstr) = ___convPendToPstart ($name, $petstr); - + ($err, $starttmstr) = ___convPendToPstart ($name, $lang, $petstr); + my $pvest50 = $jdata->{'forecasts'}[$k]{'pv_estimate'}; - - $period = $jdata->{'forecasts'}[$k]{'period'}; + + $period = $jdata->{'forecasts'}[$k]{'period'}; $period =~ s/.*(\d\d).*/$1/; - - if($debug) { # nur für Debugging + + if($debug =~ /solcastProcess/x) { # nur für Debugging if (exists $data{$type}{$name}{solcastapi}{$string}{$starttmstr}) { - Log (1, qq{DEBUG> $name SolCast API Hash - Start Date/Time: }. $starttmstr); - Log (1, qq{DEBUG> $name SolCast API Hash - pv_estimate50 add: }.(sprintf "%.0f", ($pvest50 * ($period/60) * 1000)).qq{, contains already: }.SolCastAPIVal ($hash, $string, $starttmstr, 'pv_estimate50', 0)); + Log (1, qq{$name DEBUG> SolCast API Hash - Start Date/Time: }. $starttmstr); + Log (1, qq{$name DEBUG> SolCast API Hash - pv_estimate50 add: }.(sprintf "%.0f", ($pvest50 * ($period/60) * 1000)).qq{, contains already: }.SolCastAPIVal ($hash, $string, $starttmstr, 'pv_estimate50', 0)); } } - + $data{$type}{$name}{solcastapi}{$string}{$starttmstr}{pv_estimate50} += sprintf "%.0f", ($pvest50 * ($period/60) * 1000); - - $k++; + + $k++; } } - + Log3 ($name, 4, qq{$name - SolCast API answer received for string "$string"}); - - ___setLastAPIcallKeyData ($hash, $t); - + + ___setLastAPIcallKeyData ($hash, $lang, $t); + $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = 'success'; - + my $param = { hash => $hash, name => $name, - allstrings => $allstrings + allstrings => $allstrings, + lang => $lang }; - + $data{$type}{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln $data{$type}{$name}{current}{runTimeLastAPIAnswer} = sprintf "%.4f", (tv_interval($stc) - tv_interval($sta)); # API Laufzeit ermitteln - + return &$caller($param); } ############################################################### -# SolCast API: berechne Startzeit aus 'period_end' +# SolCast API: berechne Startzeit aus 'period_end' ############################################################### sub ___convPendToPstart { my $name = shift; + my $lang = shift; my $petstr = shift; my $cpar = { @@ -2263,31 +2323,31 @@ sub ___convPendToPstart { tzconv => 'local', writelog => 0 }; - + my ($err, $cpets) = convertTimeZone ($cpar); - + if ($err) { $err = 'ERROR while converting time zone: '.$err; return $err; } - + my ($cdatest,$ctimestr) = split " ", $cpets; # Datumstring YYYY-MM-TT / Zeitstring hh:mm:ss my ($chrst,$cminutstr) = split ":", $ctimestr; $chrst = int ($chrst); if ($cminutstr eq '00') { # Zeit/Periodenkorrektur $chrst -= 1; - + if($chrst < 0) { my $nt = (timestringToTimestamp ($cdatest.' 00:00:00')) - 3600; - $nt = (timestampToTimestring ($nt))[1]; + $nt = (timestampToTimestring ($nt, $lang))[1]; ($cdatest) = split " ", $nt; - $chrst = 23; + $chrst = 23; } } - + my $starttmstr = $cdatest." ".(sprintf "%02d", $chrst).":00:00"; # Startzeit von pv_estimate - + return ($err, $starttmstr); } @@ -2297,53 +2357,53 @@ return ($err, $starttmstr); ################################################################ sub ___setLastAPIcallKeyData { my $hash = shift; + my $lang = shift; my $t = shift // time; - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - - $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t))[3]; # letzte Abrufzeit - $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_timestamp} = $t; # letzter Abrufzeitstempel - + + $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit + $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_timestamp} = $t; # letzter Abrufzeitstempel + $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIrequests} += 1; - + my $drr = $apimaxreqs - SolCastAPIVal($hash, '?All', '?All', 'todayDoneAPIrequests', 0); $drr = 0 if($drr < 0); - - my $ddc = SolCastAPIVal($hash, '?All', '?All', 'todayDoneAPIrequests', 0) / - SolCastAPIVal($hash, '?All', '?All', 'solCastAPIcallMultiplier', 0); # ausgeführte API Calls - my $drc = SolCastAPIVal($hash, '?All', '?All', 'todayMaxAPIcalls', $apimaxreqs) - $ddc; # verbleibende SolCast API Calls am aktuellen Tag + + my $ddc = SolCastAPIVal($hash, '?All', '?All', 'todayDoneAPIrequests', 0) / + SolCastAPIVal($hash, '?All', '?All', 'solCastAPIcallMultiplier', 0); # ausgeführte API Calls + my $drc = SolCastAPIVal($hash, '?All', '?All', 'todayMaxAPIcalls', $apimaxreqs) - $ddc; # verbleibende SolCast API Calls am aktuellen Tag $drc = 0 if($drc < 0); - - $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayRemainingAPIrequests} = $drr; + + $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayRemainingAPIrequests} = $drr; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayRemainingAPIcalls} = $drc; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIcalls} = $ddc; - + ## Berechnung des optimalen Request Intervalls - ################################################ - if (AttrVal($name, 'ctrlOptimizeSolCastInterval', 0)) { - my $date = strftime "%Y-%m-%d", localtime($t); + ################################################ + if (AttrVal($name, 'ctrlOptimizeSolCastInterval', 0)) { + my $date = strftime "%Y-%m-%d", localtime($t); my $sstime = timestringToTimestamp ($date.' '.ReadingsVal($name, "Today_SunSet", '00:00').':00'); my $dart = $sstime - $t; # verbleibende Sekunden bis Sonnenuntergang $dart = 0 if($dart < 0); $drc += 1; # Test - + $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = $apirepetdef; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = int ($dart / $drc) if($dart && $drc); - + # Log3 ($name, 1, qq{$name - madr: $madr, drc: $drc, dart: $dart, interval: }. SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', "")); } else { $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = $apirepetdef; } - + #### - - my $lang = AttrVal ('global', 'language', 'EN'); + my $apiitv = SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $apirepetdef); - - readingsSingleUpdate ($hash, 'nextSolCastCall', $hqtxt{after}{$lang}.' '.(timestampToTimestring ($t + $apiitv))[0], 1); - + + readingsSingleUpdate ($hash, 'nextSolCastCall', $hqtxt{after}{$lang}.' '.(timestampToTimestring ($t + $apiitv, $lang))[0], 1); + return; } @@ -2353,7 +2413,7 @@ return; sub _getdata { my $paref = shift; my $hash = $paref->{hash}; - + return centralTask ($hash); } @@ -2363,7 +2423,7 @@ return centralTask ($hash); sub _gethtml { my $paref = shift; my $name = $paref->{name}; - + return pageAsHtml ($name); } @@ -2374,7 +2434,7 @@ return pageAsHtml ($name); sub _getftui { my $paref = shift; my $name = $paref->{name}; - + return pageAsHtml ($name, "ftui"); } @@ -2384,9 +2444,9 @@ return pageAsHtml ($name, "ftui"); sub _getlistPVHistory { my $paref = shift; my $hash = $paref->{hash}; - + my $ret = listDataPool ($hash, "pvhist"); - + return $ret; } @@ -2396,9 +2456,9 @@ return $ret; sub _getlistPVCircular { my $paref = shift; my $hash = $paref->{hash}; - + my $ret = listDataPool ($hash, "circular"); - + return $ret; } @@ -2408,9 +2468,9 @@ return $ret; sub _getlistNextHours { my $paref = shift; my $hash = $paref->{hash}; - + my $ret = listDataPool ($hash, "nexthours"); - + return $ret; } @@ -2420,9 +2480,9 @@ return $ret; sub _getForecastQualities { my $paref = shift; my $hash = $paref->{hash}; - + my $ret = listDataPool ($hash, "qualities"); - + return $ret; } @@ -2432,9 +2492,9 @@ return $ret; sub _getlistCurrent { my $paref = shift; my $hash = $paref->{hash}; - + my $ret = listDataPool ($hash, "current"); - + return $ret; } @@ -2444,9 +2504,9 @@ return $ret; sub _getlistvalConsumerMaster { my $paref = shift; my $hash = $paref->{hash}; - + my $ret = listDataPool ($hash, "consumer"); - + return $ret; } @@ -2456,9 +2516,9 @@ return $ret; sub _getlistSolCastData { my $paref = shift; my $hash = $paref->{hash}; - + my $ret = listDataPool ($hash, "solcastdata"); - + return $ret; } @@ -2469,13 +2529,13 @@ sub Attr { my $aName = shift; my $aVal = shift; my $hash = $defs{$name}; - + my ($do,$val); - + # $cmd can be "del" or "set" # $name is device name # aName and aVal are Attribute name and value - + if($aName eq "disable") { if($cmd eq "set") { $do = ($aVal) ? 1 : 0; @@ -2484,33 +2544,33 @@ sub Attr { $val = ($do == 1 ? "disabled" : "initialized"); singleUpdateState ( {hash => $hash, state => $val, evt => 1} ); } - + if($aName eq "ctrlNextDayForecastReadings") { deleteReadingspec ($hash, "Tomorrow_Hour.*"); } - + if ($cmd eq "set") { if ($aName eq "ctrlInterval") { unless ($aVal =~ /^[0-9]+$/x) { return qq{The value for $aName is not valid. Use only figures 0-9 !}; } InternalTimer(gettimeofday()+1.0, "FHEM::SolarForecast::centralTask", $hash, 0); - } - + } + if ($aName eq "affectMaxDayVariance") { unless ($aVal =~ /^[0-9.]+$/x) { return qq{The value for $aName is not valid. Use only numbers with optional decimal places !}; } } - + if ($init_done == 1 && $aName eq "ctrlOptimizeSolCastInterval") { if (!isSolCastUsed ($hash)) { return qq{The attribute $aName is only valid for device model "SolCastAPI".}; } } - + } - + my $params = { hash => $hash, name => $name, @@ -2519,12 +2579,12 @@ sub Attr { aName => $aName, aVal => $aVal }; - + $aName = "consumer" if($aName =~ /consumer?(\d+)$/xs); - + if($hattr{$aName} && defined &{$hattr{$aName}{fn}}) { my $ret = q{}; - $ret = &{$hattr{$aName}{fn}} ($params); + $ret = &{$hattr{$aName}{fn}} ($params); return $ret; } @@ -2541,28 +2601,28 @@ sub _attrconsumer { ## no critic "not used" my $aName = $paref->{aName}; my $aVal = $paref->{aVal}; my $cmd = $paref->{cmd}; - + return if(!$init_done); # Forum: https://forum.fhem.de/index.php/topic,117864.msg1159959.html#msg1159959 - + my $err; - + if($cmd eq "set") { my ($a,$h) = parseParams ($aVal); my $codev = $a->[0] // ""; - + if(!$codev || !$defs{$codev}) { return qq{The device "$codev" doesn't exist!}; } - + if(!$h->{type} || !exists $h->{power}) { return qq{The syntax of "$aName" is not correct. Please consider the commandref.}; } - + my $alowt = $h->{type} ~~ @ctypes ? 1 : 0; if(!$alowt) { return qq{The type "$h->{type}" isn't allowed!}; } - + if($h->{power} !~ /^[0-9]+$/xs) { return qq{The key "power" must be specified only by numbers without decimal places}; } @@ -2570,13 +2630,13 @@ sub _attrconsumer { ## no critic "not used" if($h->{mode} && $h->{mode} !~ /^(?:can|must)$/xs) { return qq{The mode "$h->{mode}" isn't allowed!} } - + if($h->{interruptable}) { # Check Regex/Hysterese my (undef,undef,$regex,$hyst) = split ":", $h->{interruptable}; - + $err = checkRegex ($regex); return $err if($err); - + if ($hyst && !isNumeric ($hyst)) { return qq{The hysteresis of key "interruptable" must be a numeric value like "0.5" or "2"}; } @@ -2584,47 +2644,47 @@ sub _attrconsumer { ## no critic "not used" if($h->{swoncond}) { # Check Regex my (undef,undef,$regex) = split ":", $h->{swoncond}; - + $err = checkRegex ($regex); return $err if($err); - } + } if($h->{swoffcond}) { # Check Regex my (undef,undef,$regex) = split ":", $h->{swoffcond}; - + $err = checkRegex ($regex); return $err if($err); - } - + } + if($h->{swstate}) { # Check Regex my (undef,$onregex,$offregex) = split ":", $h->{swstate}; - + $err = checkRegex ($onregex); return $err if($err); - + $err = checkRegex ($offregex); return $err if($err); } - } - else { + } + else { my $day = strftime "%d", localtime(time); # aktueller Tag (range 01 to 31) my $type = $hash->{TYPE}; my ($c) = $aName =~ /consumer([0-9]+)/xs; - + deleteReadingspec ($hash, "consumer${c}.*"); - + for my $i (1..24) { # Consumer aus History löschen delete $data{$type}{$name}{pvhist}{$day}{sprintf("%02d",$i)}{"csmt${c}"}; delete $data{$type}{$name}{pvhist}{$day}{sprintf("%02d",$i)}{"csme${c}"}; } - + delete $data{$type}{$name}{pvhist}{$day}{99}{"csmt${c}"}; delete $data{$type}{$name}{pvhist}{$day}{99}{"csme${c}"}; delete $data{$type}{$name}{consumers}{$c}; # Consumer Hash Verbraucher löschen - } + } writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - + InternalTimer(gettimeofday()+5, "FHEM::SolarForecast::createAssociatedWith", $hash, 0); return; @@ -2637,7 +2697,7 @@ sub _attrcreateConsRecRdgs { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $aName = $paref->{aName}; - + if ($aName eq 'ctrlConsRecommendReadings') { deleteReadingspec ($hash, "consumer.*_ConsumptionRecommended"); } @@ -2652,7 +2712,7 @@ sub _attrcreateStatisticRdgs { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $aName = $paref->{aName}; - + deleteReadingspec ($hash, "statistic_.*"); return; @@ -2664,44 +2724,44 @@ return; ################################################################################### sub Notify { # Es werden nur die Events von Geräten verarbeitet die im Hash $hash->{NOTIFYDEV} gelistet sind (wenn definiert). - # Dadurch kann die Menge der Events verringert werden. In sub DbRep_Define angeben. - + # Dadurch kann die Menge der Events verringert werden. In sub DbRep_Define angeben. + return; # nicht genutzt zur Zeit - - + + my $myHash = shift; my $dev_hash = shift; my $myName = $myHash->{NAME}; # Name des eigenen Devices my $devName = $dev_hash->{NAME}; # Device welches Events erzeugt hat - - return if(IsDisabled($myName) || !$myHash->{NOTIFYDEV}); - + + return if(IsDisabled($myName) || !$myHash->{NOTIFYDEV}); + my $events = deviceEvents($dev_hash, 1); return if(!$events); - + my $cdref = CurrentVal ($myHash, "consumerdevs", ""); # alle registrierten Consumer my @consumers = (); @consumers = @{$cdref} if(ref $cdref eq "ARRAY"); - + return if(!@consumers); - + if($devName ~~ @consumers) { my $cindex; my $type = $myHash->{TYPE}; for my $c (sort{$a<=>$b} keys %{$data{$type}{$myName}{consumers}}) { my $cname = ConsumerVal ($myHash, $c, "name", ""); if($devName eq $cname) { - $cindex = $c; + $cindex = $c; last; - } + } } - - my $autoreading = ConsumerVal ($myHash, $cindex, "autoreading", ""); - + + my $autoreading = ConsumerVal ($myHash, $cindex, "autoreading", ""); + for my $event (@{$events}) { $event = "" if(!defined($event)); my @evl = split(/\s+/x, $event); - + my @parts = split(/: /x,$event, 2); my $reading = shift @parts; @@ -2709,10 +2769,10 @@ sub Notify { Log3 ($myName, 4, qq{$myName - start centralTask by Notify - $devName:$reading}); RemoveInternalTimer($myHash, "FHEM::SolarForecast::centralTask"); InternalTimer (gettimeofday()+0.5, "FHEM::SolarForecast::centralTask", $myHash, 0); - } + } } } - + return; } @@ -2730,7 +2790,7 @@ sub DbLogSplit { $reading =~ tr/://d; $value = $parts[1]; $unit = $parts[2]; - + # Log3 ($device, 1, qq{$device - Split for DbLog done -> Reading: $reading, Value: $value, Unit: $unit}); } @@ -2740,104 +2800,104 @@ return ($reading, $value, $unit); ################################################################ # Shutdown ################################################################ -sub Shutdown { +sub Shutdown { my $hash = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + writeDataToFile ($hash, "pvhist", $pvhcache.$name); # Cache File für PV History schreiben writeDataToFile ($hash, "circular", $pvccache.$name); # Cache File für PV Circular schreiben writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben writeDataToFile ($hash, "solcastapi", $scpicache.$name); # Cache File SolCast API Werte schreiben - -return; + +return; } ################################################################ -# Die Undef-Funktion wird aufgerufen wenn ein Gerät mit delete -# gelöscht wird oder bei der Abarbeitung des Befehls rereadcfg, -# der ebenfalls alle Geräte löscht und danach das -# Konfigurationsfile neu einliest. Entsprechend müssen in der -# Funktion typische Aufräumarbeiten durchgeführt werden wie das -# saubere Schließen von Verbindungen oder das Entfernen von +# Die Undef-Funktion wird aufgerufen wenn ein Gerät mit delete +# gelöscht wird oder bei der Abarbeitung des Befehls rereadcfg, +# der ebenfalls alle Geräte löscht und danach das +# Konfigurationsfile neu einliest. Entsprechend müssen in der +# Funktion typische Aufräumarbeiten durchgeführt werden wie das +# saubere Schließen von Verbindungen oder das Entfernen von # internen Timern. ################################################################ sub Undef { my $hash = shift; my $arg = shift; - + RemoveInternalTimer($hash); - + return; } ################################################################# -# Wenn ein Gerät in FHEM gelöscht wird, wird zuerst die Funktion -# X_Undef aufgerufen um offene Verbindungen zu schließen, -# anschließend wird die Funktion X_Delete aufgerufen. -# Funktion: Aufräumen von dauerhaften Daten, welche durch das -# Modul evtl. für dieses Gerät spezifisch erstellt worden sind. -# Es geht hier also eher darum, alle Spuren sowohl im laufenden -# FHEM-Prozess, als auch dauerhafte Daten bspw. im physikalischen -# Gerät zu löschen die mit dieser Gerätedefinition zu tun haben. +# Wenn ein Gerät in FHEM gelöscht wird, wird zuerst die Funktion +# X_Undef aufgerufen um offene Verbindungen zu schließen, +# anschließend wird die Funktion X_Delete aufgerufen. +# Funktion: Aufräumen von dauerhaften Daten, welche durch das +# Modul evtl. für dieses Gerät spezifisch erstellt worden sind. +# Es geht hier also eher darum, alle Spuren sowohl im laufenden +# FHEM-Prozess, als auch dauerhafte Daten bspw. im physikalischen +# Gerät zu löschen die mit dieser Gerätedefinition zu tun haben. ################################################################# sub Delete { my $hash = shift; my $arg = shift; my $name = $hash->{NAME}; - + my $file = $pvhcache.$name; # Cache File PV History löschen - my $error = FileDelete($file); + my $error = FileDelete($file); if ($error) { - Log3 ($name, 1, qq{$name - ERROR deleting file "$file": $error}); + Log3 ($name, 1, qq{$name - ERROR deleting file "$file": $error}); } - + $error = qq{}; $file = $pvccache.$name; # Cache File PV Circular löschen - $error = FileDelete($file); - + $error = FileDelete($file); + if ($error) { - Log3 ($name, 1, qq{$name - ERROR deleting file "$file": $error}); + Log3 ($name, 1, qq{$name - ERROR deleting file "$file": $error}); } - + $error = qq{}; $file = $plantcfg.$name; # File Anlagenkonfiguration löschen - $error = FileDelete($file); - + $error = FileDelete($file); + if ($error) { - Log3 ($name, 1, qq{$name - ERROR deleting file "$file": $error}); + Log3 ($name, 1, qq{$name - ERROR deleting file "$file": $error}); } - + $error = qq{}; $file = $csmcache.$name; # File Consumer löschen - $error = FileDelete($file); - + $error = FileDelete($file); + if ($error) { - Log3 ($name, 1, qq{$name - ERROR deleting file "$file": $error}); + Log3 ($name, 1, qq{$name - ERROR deleting file "$file": $error}); } - + $error = qq{}; $file = $scpicache.$name; # File SolCast API Werte löschen - $error = FileDelete($file); - + $error = FileDelete($file); + if ($error) { - Log3 ($name, 1, qq{$name - ERROR deleting file "$file": $error}); + Log3 ($name, 1, qq{$name - ERROR deleting file "$file": $error}); } - + my $type = $hash->{TYPE}; - + delete $data{$type}{$name}{circular}; # Ringspeicher delete $data{$type}{$name}{current}; # current values - delete $data{$type}{$name}{pvhist}; # historische Werte + delete $data{$type}{$name}{pvhist}; # historische Werte delete $data{$type}{$name}{nexthours}; # NextHours Werte delete $data{$type}{$name}{consumers}; # Consumer Hash delete $data{$type}{$name}{strings}; # Stringkonfiguration delete $data{$type}{$name}{solcastapi}; # Zwischenspeicher Vorhersagewerte SolCast API - + return; } @@ -2847,31 +2907,31 @@ return; sub periodicWriteCachefiles { my $hash = shift; my $name = $hash->{NAME}; - + RemoveInternalTimer($hash, "FHEM::SolarForecast::periodicWriteCachefiles"); InternalTimer (gettimeofday()+$whistrepeat, "FHEM::SolarForecast::periodicWriteCachefiles", $hash, 0); - + return if(IsDisabled($name)); - + writeDataToFile ($hash, "circular", $pvccache.$name); # Cache File für PV Circular schreiben writeDataToFile ($hash, "pvhist", $pvhcache.$name); # Cache File für PV History schreiben - + return; } ################################################################ # Daten in File wegschreiben ################################################################ -sub writeDataToFile { +sub writeDataToFile { my $hash = shift; my $cachename = shift; my $file = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + my @data; - + if($cachename eq "plantconfig") { @data = _savePlantConfig ($hash); return "Plant configuration is empty, no data has been written" if(!@data); @@ -2881,33 +2941,33 @@ sub writeDataToFile { my $json = encode_json ($data{$type}{$name}{$cachename}); push @data, $json; } - + my $error = FileWrite($file, @data); - + if ($error) { my $err = qq{ERROR writing cache file "$file": $error}; Log3 ($name, 1, "$name - $err"); singleUpdateState ( {hash => $hash, state => "ERROR writing cache file $file - $error", evt => 1} ); - return $err; + return $err; } else { - my $lw = gettimeofday(); + my $lw = gettimeofday(); $hash->{HISTFILE} = "last write time: ".FmtTime($lw)." File: $file" if($cachename eq "pvhist"); singleUpdateState ( {hash => $hash, state => "wrote cachefile $cachename successfully", evt => 1} ); } - -return; + +return; } ################################################################ # Anlagenkonfiguration sichern ################################################################ -sub _savePlantConfig { +sub _savePlantConfig { my $hash = shift; my $name = $hash->{NAME}; - + my @pvconf; - + my @aconfigs = qw( pvCorrectionFactor_Auto currentBatteryDev @@ -2927,10 +2987,10 @@ sub _savePlantConfig { for my $cfg (@aconfigs) { my $val = ReadingsVal($name, $cfg, ""); next if(!$val); - push @pvconf, $cfg."<>".$val; - } - -return @pvconf; + push @pvconf, $cfg."<>".$val; + } + +return @pvconf; } ################################################################ @@ -2939,77 +2999,76 @@ return @pvconf; sub centralTask { my $hash = shift; my $evt = shift // 1; # Readings Event generieren - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my $cst = [gettimeofday]; # Zyklus-Startzeit RemoveInternalTimer($hash, "FHEM::SolarForecast::centralTask"); RemoveInternalTimer($hash, "FHEM::SolarForecast::singleUpdateState"); - + ### nicht mehr benötigte Readings/Daten löschen - kann später wieder raus !! + ############################################################## #for my $i (keys %{$data{$type}{$name}{pvhist}}) { # delete $data{$type}{$name}{pvhist}{$i}{"00"}; # delete $data{$type}{$name}{pvhist}{$i} if(!$i); # evtl. vorhandene leere Schlüssel entfernen #} - + #for my $c (keys %{$data{$type}{$name}{consumers}}) { # delete $data{$type}{$name}{consumers}{$c}{OnOff}; #} - + #deleteReadingspec ($hash, "CurrentHourPVforecast"); - #deleteReadingspec ($hash, "NextHours_Sum00_PVforecast"); - #deleteReadingspec ($hash, "nextPolltime"); + #deleteReadingspec ($hash, "NextHours_Sum00_PVforecast"); + #deleteReadingspec ($hash, "nextPolltime"); #delete $data{$type}{$name}{solcastapi}{'All'}; #delete $data{$type}{$name}{solcastapi}{'#All'}; #delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todaySolCastAPIcalls}; - #delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIInterval}; - #delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIcalls}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayRemaingAPIcalls}; - + for my $n (1..24) { $n = sprintf "%02d", $n; deleteReadingspec ($hash, "pvSolCastPercentile_${n}.*"); } ############################################################### - my $interval = controlParams ($name); + setModel ($hash); # Model setzen - setModel ($hash); # Model setzen - if($init_done == 1) { + my $interval = controlParams ($name); my @da; - + if(!$interval) { $hash->{MODE} = "Manual"; push @da, "nextCycletime<>Manual"; - } + } else { - my $new = gettimeofday()+$interval; + my $new = gettimeofday()+$interval; InternalTimer($new, "FHEM::SolarForecast::centralTask", $hash, 0); # Wiederholungsintervall - + if(!IsDisabled($name)) { $hash->{MODE} = "Automatic - next Cycletime: ".FmtTime($new); push @da, "nextCycletime<>".FmtTime($new); } } - - return if(IsDisabled($name)); - - if(!CurrentVal ($hash, 'allStringsFullfilled', 0)) { # die String Konfiguration erstellen wenn noch nicht erfolgreich ausgeführt - my $ret = createStringConfig ($hash); + + return if(IsDisabled($name)); + + if(!CurrentVal ($hash, 'allStringsFullfilled', 0)) { # die String Konfiguration erstellen wenn noch nicht erfolgreich ausgeführt + my $ret = createStringConfig ($hash); if ($ret) { singleUpdateState ( {hash => $hash, state => $ret, evt => 1} ); return; } } - - my $t = time; # aktuelle Unix-Zeit + + my $t = time; # aktuelle Unix-Zeit my $date = strftime "%Y-%m-%d", localtime($t); # aktuelles Datum - my $chour = strftime "%H", localtime($t); # aktuelle Stunde + my $chour = strftime "%H", localtime($t); # aktuelle Stunde my $minute = strftime "%M", localtime($t); # aktuelle Minute my $day = strftime "%d", localtime($t); # aktueller Tag (range 01 to 31) my $dayname = strftime "%a", localtime($t); # aktueller Wochentagsname + my $debug = AttrVal ($name, 'ctrlDebug', 'none'); # Debug Module my $centpars = { hash => $hash, @@ -3021,60 +3080,64 @@ sub centralTask { chour => $chour, day => $day, dayname => $dayname, + debug => $debug, + lang => AttrVal ($name, 'ctrlLanguage', AttrVal ('global', 'language', $deflang)), state => 'running', evt => 0, daref => \@da }; - - Log3 ($name, 4, "$name - ################################################################"); - Log3 ($name, 4, "$name - ### New data collection cycle ###"); - Log3 ($name, 4, "$name - ################################################################"); - Log3 ($name, 4, "$name - current hour of day: ".($chour+1)); - + + if ($debug !~ /^none$/xs) { + Log3 ($name, 4, "$name DEBUG> ################################################################"); + Log3 ($name, 4, "$name DEBUG> ### New centralTask cycle ###"); + Log3 ($name, 4, "$name DEBUG> ################################################################"); + Log3 ($name, 4, "$name DEBUG> current hour of day: ".($chour+1)); + } + singleUpdateState ($centpars); $centpars->{state} = 'updated'; collectAllRegConsumers ($centpars); # alle Verbraucher Infos laden _specialActivities ($centpars); # zusätzliche Events generieren + Sonderaufgaben _transferWeatherValues ($centpars); # Wetterwerte übertragen - + createReadingsFromArray ($hash, \@da, $evt); # Readings erzeugen - + if (isSolCastUsed ($hash)) { - _getRoofTopData ($centpars); # SolCast API Strahlungswerte abrufen - _transferSolCastRadiationValues ($centpars); # SolCast API Strahlungswerte übertragen und Forecast erstellen + _getRoofTopData ($centpars); # SolCast API Strahlungswerte abrufen + _transferSolCastRadiationValues ($centpars); # SolCast API Strahlungswerte übertragen und Forecast erstellen } else { - _transferDWDRadiationValues ($centpars); # DWD Strahlungswerte übertragen und Forecast erstellen + _transferDWDRadiationValues ($centpars); # DWD Strahlungswerte übertragen und Forecast erstellen } - + _calcMaxEstimateToday ($centpars); # heutigen Max PV Estimate & dessen Tageszeit ermitteln _transferInverterValues ($centpars); # WR Werte übertragen - _transferMeterValues ($centpars); # Energy Meter auswerten + _transferMeterValues ($centpars); # Energy Meter auswerten _transferBatteryValues ($centpars); # Batteriewerte einsammeln - _manageConsumerData ($centpars); # Consumerdaten sammeln und planen + _manageConsumerData ($centpars); # Consumerdaten sammeln und planen _estConsumptionForecast ($centpars); # Verbrauchsprognose erstellen - _evaluateThresholds ($centpars); # Schwellenwerte bewerten und signalisieren + _evaluateThresholds ($centpars); # Schwellenwerte bewerten und signalisieren _calcReadingsTomorrowPVFc ($centpars); # zusätzliche Readings Tomorrow_HourXX_PVforecast berechnen _createSummaries ($centpars); # Zusammenfassungen erstellen _calcTodayPVdeviation ($centpars); # Vorhersageabweichung erstellen (nach Sonnenuntergang) - + createReadingsFromArray ($hash, \@da, $evt); # Readings erzeugen calcCorrAndQuality ($centpars); # neue Korrekturfaktor/Qualität berechnen und speichern - - createReadingsFromArray ($hash, \@da, $evt); # Readings erzeugen - - saveEnergyConsumption ($centpars); # Energie Hausverbrauch speichern - - setTimeTracking ($hash, $cst, 'runTimeCentralTask'); # Zyklus-Laufzeit ermitteln - genStatisticReadings ($centpars); # optionale Statistikreadings erstellen - + createReadingsFromArray ($hash, \@da, $evt); # Readings erzeugen + + saveEnergyConsumption ($centpars); # Energie Hausverbrauch speichern + + setTimeTracking ($hash, $cst, 'runTimeCentralTask'); # Zyklus-Laufzeit ermitteln + + genStatisticReadings ($centpars); # optionale Statistikreadings erstellen + createReadingsFromArray ($hash, \@da, $evt); # Readings erzeugen if ($evt) { - $centpars->{evt} = $evt; + $centpars->{evt} = $evt; InternalTimer(gettimeofday()+1, "FHEM::SolarForecast::singleUpdateState", $centpars, 0); } else { @@ -3085,7 +3148,7 @@ sub centralTask { else { InternalTimer(gettimeofday()+5, "FHEM::SolarForecast::centralTask", $hash, 0); } - + return; } @@ -3097,23 +3160,23 @@ sub createStringConfig { ## no critic "not used" my $hash = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + delete $data{$type}{$name}{strings}; # Stringhash zurücksetzen $data{$type}{$name}{current}{allStringsFullfilled} = 0; - + my @istrings = split ",", ReadingsVal ($name, 'inverterStrings', ''); # Stringbezeichner - $data{$type}{$name}{current}{allstringscount} = scalar @istrings; # Anzahl der Anlagenstrings - + $data{$type}{$name}{current}{allstringscount} = scalar @istrings; # Anzahl der Anlagenstrings + if(!@istrings) { return qq{Define all used strings with command "set $name inverterStrings" first.}; } - + my $peak = ReadingsVal ($name, 'modulePeakString', ''); # kWp für jeden Stringbezeichner return qq{Please complete command "set $name modulePeakString".} if(!$peak); - + my ($aa,$ha) = parseParams ($peak); delete $data{$type}{$name}{current}{allstringspeak}; - + while (my ($strg, $pp) = each %$ha) { if ($strg ~~ @istrings) { $data{$type}{$name}{strings}{$strg}{peak} = $pp; @@ -3123,13 +3186,13 @@ sub createStringConfig { ## no critic "not used" return qq{Check "modulePeakString" -> the stringname "$strg" is not defined as valid string in reading "inverterStrings"}; } } - + if (isSolCastUsed ($hash)) { # SolCast-API Strahlungsquelle my $mrt = ReadingsVal ($name, 'moduleRoofTops', ''); # RoofTop Konfiguration -> Zuordnung return qq{Please complete command "set $name moduleRoofTops".} if(!$mrt); - + my ($ad,$hd) = parseParams ($mrt); - + while (my ($is, $pk) = each %$hd) { if ($is ~~ @istrings) { $data{$type}{$name}{strings}{$is}{pk} = $pk; @@ -3142,9 +3205,9 @@ sub createStringConfig { ## no critic "not used" else { # DWD Strahlungsquelle my $tilt = ReadingsVal ($name, 'moduleTiltAngle', ''); # Modul Neigungswinkel für jeden Stringbezeichner return qq{Please complete command "set $name moduleTiltAngle".} if(!$tilt); - + my ($at,$ht) = parseParams ($tilt); - + while (my ($key, $value) = each %$ht) { if ($key ~~ @istrings) { $data{$type}{$name}{strings}{$key}{tilt} = $value; @@ -3153,12 +3216,12 @@ sub createStringConfig { ## no critic "not used" return qq{Check "moduleTiltAngle" -> the stringname "$key" is not defined as valid string in reading "inverterStrings"}; } } - + my $dir = ReadingsVal ($name, 'moduleDirection', ''); # Modul Ausrichtung für jeden Stringbezeichner return qq{Please complete command "set $name moduleDirection".} if(!$dir); - + my ($ad,$hd) = parseParams ($dir); - + while (my ($key, $value) = each %$hd) { if ($key ~~ @istrings) { $data{$type}{$name}{strings}{$key}{dir} = $value; @@ -3168,25 +3231,25 @@ sub createStringConfig { ## no critic "not used" } } } - + if(!keys %{$data{$type}{$name}{strings}}) { return qq{The string configuration seems to be incomplete. \n}. qq{Please check the settings of inverterStrings, modulePeakString, moduleDirection, moduleTiltAngle }. qq{and/or moduleRoofTops if SolCast-API is used.}; } - + my @sca = keys %{$data{$type}{$name}{strings}}; # Gegencheck ob nicht mehr Strings in inverterStrings enthalten sind als eigentlich verwendet my @tom; - + for my $sn (@istrings) { next if ($sn ~~ @sca); - push @tom, $sn; + push @tom, $sn; } - + if(@tom) { return qq{Some Strings are not used. Please delete this string names from "inverterStrings" :}.join ",",@tom; } - + $data{$type}{$name}{current}{allStringsFullfilled} = 1; return; } @@ -3216,32 +3279,32 @@ sub _specialActivities { my $daref = $paref->{daref}; my $t = $paref->{t}; # aktuelle Zeit my $day = $paref->{day}; - + my ($ts,$ts1,$pvfc,$pvrl,$gcon); - + $ts1 = $date." ".sprintf("%02d",$chour).":00:00"; - - $pvfc = ReadingsNum($name, "Today_Hour".sprintf("%02d",$chour)."_PVforecast", 0); + + $pvfc = ReadingsNum($name, "Today_Hour".sprintf("%02d",$chour)."_PVforecast", 0); push @$daref, "LastHourPVforecast<>".$pvfc." Wh<>".$ts1; - + $pvrl = ReadingsNum($name, "Today_Hour".sprintf("%02d",$chour)."_PVreal", 0); push @$daref, "LastHourPVreal<>".$pvrl." Wh<>".$ts1; - + $gcon = ReadingsNum($name, "Today_Hour".sprintf("%02d",$chour)."_GridConsumption", 0); - push @$daref, "LastHourGridconsumptionReal<>".$gcon." Wh<>".$ts1; - + push @$daref, "LastHourGridconsumptionReal<>".$gcon." Wh<>".$ts1; + ## Planungsdaten spezifisch löschen (Anfang und Ende nicht am selben Tag) ########################################################################## - - for my $c (keys %{$data{$type}{$name}{consumers}}) { + + for my $c (keys %{$data{$type}{$name}{consumers}}) { next if(ConsumerVal ($hash, $c, "plandelete", "regular") eq "regular"); - + my $planswitchoff = ConsumerVal ($hash, $c, "planswitchoff", $t); my $pstate = simplifyCstate (ConsumerVal ($hash, $c, "planstate", "")); - + if ($t > $planswitchoff && $pstate =~ /planned|finished|unknown/xs) { deleteConsumerPlanning ($hash, $c); - + $data{$type}{$name}{consumers}{$c}{minutesOn} = 0; $data{$type}{$name}{consumers}{$c}{numberDayStarts} = 0; $data{$type}{$name}{consumers}{$c}{onoff} = "off"; @@ -3251,71 +3314,71 @@ sub _specialActivities { ## bestimmte einmalige Aktionen ################################## - my $tlim = "00"; + my $tlim = "00"; if($chour =~ /^($tlim)$/x) { if(!exists $hash->{HELPER}{H00DONE}) { $date = strftime "%Y-%m-%d", localtime($t-7200); # Vortag (2 h Differenz reichen aus) $ts = $date." 23:59:59"; - - $pvfc = ReadingsNum($name, "Today_Hour24_PVforecast", 0); + + $pvfc = ReadingsNum($name, "Today_Hour24_PVforecast", 0); push @$daref, "LastHourPVforecast<>".$pvfc."<>".$ts; - + $pvrl = ReadingsNum($name, "Today_Hour24_PVreal", 0); push @$daref, "LastHourPVreal<>".$pvrl."<>".$ts; - + $gcon = ReadingsNum($name, "Today_Hour24_GridConsumption", 0); push @$daref, "LastHourGridconsumptionReal<>".$gcon."<>".$ts; - writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration sichern - + writeDataToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration sichern + deleteReadingspec ($hash, "Today_Hour.*_Grid.*"); deleteReadingspec ($hash, "Today_Hour.*_PV.*"); deleteReadingspec ($hash, "Today_Hour.*_Bat.*"); deleteReadingspec ($hash, "powerTrigger_.*"); deleteReadingspec ($hash, "Today_MaxPVforecast.*"); deleteReadingspec ($hash, "Today_PVdeviation"); - + if(ReadingsVal ($name, "pvCorrectionFactor_Auto", "off") eq "on") { for my $n (1..24) { $n = sprintf "%02d", $n; deleteReadingspec ($hash, "pvCorrectionFactor_${n}.*"); } } - + delete $hash->{HELPER}{INITCONTOTAL}; delete $hash->{HELPER}{INITFEEDTOTAL}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIrequests}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIcalls}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayRemainingAPIrequests}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayRemainingAPIcalls}; - + $data{$type}{$name}{circular}{99}{ydayDvtn} = CircularVal ($hash, 99, 'tdayDvtn', '-'); delete $data{$type}{$name}{circular}{99}{tdayDvtn}; - + delete $data{$type}{$name}{pvhist}{$day}; # den (alten) aktuellen Tag aus History löschen Log3 ($name, 3, qq{$name - history day "$day" deleted}); - + for my $c (keys %{$data{$type}{$name}{consumers}}) { # Planungsdaten regulär löschen - next if(ConsumerVal ($hash, $c, "plandelete", "regular") ne "regular"); - + next if(ConsumerVal ($hash, $c, "plandelete", "regular") ne "regular"); + deleteConsumerPlanning ($hash, $c); - + $data{$type}{$name}{consumers}{$c}{minutesOn} = 0; $data{$type}{$name}{consumers}{$c}{numberDayStarts} = 0; $data{$type}{$name}{consumers}{$c}{onoff} = "off"; - } + } writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - + __createAdditionalEvents ($paref); # zusätzliche Events erzeugen - PV Vorhersage bis Ende des kommenden Tages - __delSolCastObsoleteData ($paref); # Bereinigung obsoleter Daten im solcastapi Hash - + __delSolCastObsoleteData ($paref); # Bereinigung obsoleter Daten im solcastapi Hash + $hash->{HELPER}{H00DONE} = 1; } } else { delete $hash->{HELPER}{H00DONE}; - } + } return; } @@ -3329,16 +3392,16 @@ sub __createAdditionalEvents { my $name = $paref->{name}; my $type = $paref->{type}; my $daref = $paref->{daref}; - - for my $idx (sort keys %{$data{$type}{$name}{nexthours}}) { + + for my $idx (sort keys %{$data{$type}{$name}{nexthours}}) { my $nhts = NexthoursVal ($hash, $idx, "starttime", undef); my $nhfc = NexthoursVal ($hash, $idx, "pvforecast", undef); next if(!defined $nhts || !defined $nhfc); - + my ($dt, $h) = $nhts =~ /([\w-]+)\s(\d{2})/xs; push @$daref, "AllPVforecastsToEvent<>".$nhfc." Wh<>".$dt." ".$h.":59:59"; } - + return; } @@ -3351,11 +3414,11 @@ sub __delSolCastObsoleteData { my $name = $paref->{name}; my $type = $paref->{type}; my $date = $paref->{date}; # aktuelles Datum - + if (!keys %{$data{$type}{$name}{solcastapi}}) { return; - } - + } + my $refts = timestringToTimestamp ($date.' 00:00:00'); # Referenztimestring for my $idx (sort keys %{$data{$type}{$name}{solcastapi}}) { # alle Datumschlüssel kleiner aktueller Tag 00:00:00 selektieren @@ -3364,89 +3427,98 @@ sub __delSolCastObsoleteData { delete $data{$type}{$name}{solcastapi}{$idx}{$scd} if ($ds && $ds < $refts); } } - + return; } ################################################################ -# Strahlungsvorhersage Werte von DWD Device +# Strahlungsvorhersage Werte von DWD Device # ermitteln und übertragen ################################################################ -sub _transferDWDRadiationValues { +sub _transferDWDRadiationValues { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $t = $paref->{t}; # Epoche Zeit my $chour = $paref->{chour}; my $daref = $paref->{daref}; - + my $raname = ReadingsVal($name, "currentRadiationDev", ""); # Radiation Forecast Device return if(!$raname || !$defs{$raname}); - + my $type = $paref->{type}; + my $debug = $paref->{debug}; + my $lang = $paref->{lang}; my $err = checkdwdattr ($name,$raname,\@draattrmust); $paref->{state} = $err if($err); - - for my $num (0..47) { - my ($fd,$fh) = _calcDayHourMove ($chour, $num); + + if($debug =~ /radiationProcess/x) { + Log (1, qq{$name DEBUG> collect Radiation data - device: $raname =>}); + } - if($fd > 1) { # überhängende Werte löschen + for my $num (0..47) { + my ($fd,$fh) = _calcDayHourMove ($chour, $num); + + if($fd > 1) { # überhängende Werte löschen delete $data{$type}{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}; next; } - + my $fh1 = $fh+1; my $fh2 = $fh1 == 24 ? 23 : $fh1; my $rad = ReadingsVal($raname, "fc${fd}_${fh2}_Rad1h", 0); - + my $time_str = "NextHour".sprintf "%02d", $num; - my $wantts = $t + (3600 * $num); - my $wantdt = (timestampToTimestring ($wantts))[1]; - my ($hod) = $wantdt =~ /\s(\d{2}):/xs; + my $wantts = $t + (3600 * $num); + my $wantdt = (timestampToTimestring ($wantts, $lang))[1]; + my ($hod) = $wantdt =~ /\s(\d{2}):/xs; $hod = sprintf "%02d", int ($hod)+1; # Stunde des Tages - - Log3 ($name, 5, "$name - collect Radiation data: device=$raname, rad=fc${fd}_${fh2}_Rad1h, Rad1h=$rad"); - + + if($debug =~ /radiationProcess/x) { + Log (1, qq{$name DEBUG> date: $wantdt, rad: fc${fd}_${fh2}_Rad1h, Rad1h: $rad}); + } + my $params = { - hash => $hash, - name => $name, - type => $type, - rad => $rad, - t => $t, - hod => $hod, - num => $num, - fh1 => $fh1, - fd => $fd, - day => $paref->{day} + hash => $hash, + name => $name, + type => $type, + rad => $rad, + t => $t, + hod => $hod, + num => $num, + fh1 => $fh1, + fd => $fd, + day => $paref->{day}, + debug => $debug }; - + my $calcpv = __calcDWDforecast ($params); # Vorhersage gewichtet kalkulieren - + $data{$type}{$name}{nexthours}{$time_str}{pvforecast} = $calcpv; $data{$type}{$name}{nexthours}{$time_str}{starttime} = $wantdt; $data{$type}{$name}{nexthours}{$time_str}{hourofday} = $hod; $data{$type}{$name}{nexthours}{$time_str}{today} = $fd == 0 ? 1 : 0; $data{$type}{$name}{nexthours}{$time_str}{Rad1h} = $rad; # nur Info: original Vorhersage Strahlungsdaten - - if($num < 23 && $fh < 24) { # Ringspeicher PV forecast Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 + + if($num < 23 && $fh < 24) { # Ringspeicher PV forecast Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{pvfc} = $calcpv; - } - - if($fd == 0 && int $calcpv > 0) { # Vorhersagedaten des aktuellen Tages zum manuellen Vergleich in Reading speichern - push @$daref, "Today_Hour".sprintf("%02d",$fh1)."_PVforecast<>$calcpv Wh"; } - + + if($fd == 0 && int $calcpv > 0) { # Vorhersagedaten des aktuellen Tages zum manuellen Vergleich in Reading speichern + push @$daref, "Today_Hour".sprintf("%02d",$fh1)."_PVforecast<>$calcpv Wh"; + } + if($fd == 0 && $fh1) { $paref->{calcpv} = $calcpv; $paref->{histname} = "pvfc"; $paref->{nhour} = sprintf("%02d",$fh1); - setPVhistory ($paref); + setPVhistory ($paref); delete $paref->{histname}; } } - + push @$daref, ".lastupdateForecastValues<>".$t; # Statusreading letzter DWD update - + return; } @@ -3460,7 +3532,7 @@ return; # * Wirkungsgrad WR in % z.B.: 98,3 # * Korrekturwerte wegen Ausrichtung/Verschattung etc. # -# Die Formel wäre dann: +# Die Formel wäre dann: # Ertrag in Wh = Rad1h * 0.00027778 * 31,04 qm * 16,52% * 98,3% * 100% * 1000 # # Berechnung nach Formel 2 aus http://www.ing-büro-junge.de/html/photovoltaik.html: @@ -3475,16 +3547,16 @@ return; # pv (kWh) = G * f * 0.00027778 (kWh/m2) / 1 kW/m2 * Pnenn (kW) * PR * Korr # pv (Wh) = G * f * 0.00027778 (kWh/m2) / 1 kW/m2 * Pnenn (kW) * PR * Korr * 1000 # -# Die Abhängigkeit der Strahlungsleistung der Sonnenenergie nach Wetterlage und Jahreszeit ist -# hier beschrieben: +# Die Abhängigkeit der Strahlungsleistung der Sonnenenergie nach Wetterlage und Jahreszeit ist +# hier beschrieben: # https://www.energie-experten.org/erneuerbare-energien/photovoltaik/planung/sonnenstunden # # !!! PV Berechnungsgrundlagen !!! # https://www.energie-experten.org/erneuerbare-energien/photovoltaik/planung/ertrag # http://www.ing-büro-junge.de/html/photovoltaik.html -# +# ################################################################################################## -sub __calcDWDforecast { +sub __calcDWDforecast { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; @@ -3495,54 +3567,55 @@ sub __calcDWDforecast { my $hod = $paref->{hod}; # Stunde des Tages my $fh1 = $paref->{fh1}; my $fd = $paref->{fd}; - + my $stch = $data{$type}{$name}{strings}; # String Configuration Hash - + my $reld = $fd == 0 ? "today" : $fd == 1 ? "tomorrow" : "unknown"; - + my $clouddamp = AttrVal($name, "affectCloudfactorDamping", $cldampdef); # prozentuale Berücksichtigung des Bewölkungskorrekturfaktors my $raindamp = AttrVal($name, "affectRainfactorDamping", $rdampdef); # prozentuale Berücksichtigung des Regenkorrekturfaktors my @strings = sort keys %{$stch}; - + my $rainprob = NexthoursVal ($hash, "NextHour".sprintf("%02d",$num), "rainprob", 0); # Niederschlagswahrscheinlichkeit> 0,1 mm während der letzten Stunde my $rcf = 1 - ((($rainprob - $rain_base)/100) * $raindamp/100); # Rain Correction Faktor mit Steilheit my $cloudcover = NexthoursVal ($hash, "NextHour".sprintf("%02d",$num), "cloudcover", 0); # effektive Wolkendecke nächste Stunde X my $ccf = 1 - ((($cloudcover - $cloud_base)/100) * $clouddamp/100); # Cloud Correction Faktor mit Steilheit und Fußpunkt - + my $temp = NexthoursVal ($hash, "NextHour".sprintf("%02d",$num), "temp", $tempbasedef); # vorhergesagte Temperatur Stunde X + my $debug = $paref->{debug}; my $range = calcRange ($cloudcover); # Range errechnen $paref->{range} = $range; my ($hcfound, $hc, $hq) = ___readCorrfAndQuality ($paref); # liest den anzuwendenden Korrekturfaktor delete $paref->{range}; - - my $pvsum = 0; - my $peaksum = 0; - my ($lh,$sq); - + + my $pvsum = 0; + my $peaksum = 0; + my ($lh,$sq); + for my $st (@strings) { # für jeden String der Config .. my $peak = $stch->{$st}{peak}; # String Peak (kWp) - + $paref->{peak} = $peak; $paref->{cloudcover} = $cloudcover; $paref->{temp} = $temp; - + my ($peakloss, $modtemp) = ___calcPeaklossByTemp ($paref); # Reduktion Peakleistung durch Temperaturkoeffizienten der Module (vorzeichengehaftet) $peak += $peakloss; - + delete $paref->{peak}; delete $paref->{cloudcover}; delete $paref->{temp}; - + $peak *= 1000; # kWp in Wp umrechnen my $ta = $stch->{$st}{tilt}; # Neigungswinkel Solarmodule my $moddir = $stch->{$st}{dir}; # Ausrichtung der Solarmodule - + my $af = $hff{$ta}{$moddir} / 100; # Flächenfaktor: http://www.ing-büro-junge.de/html/photovoltaik.html - + my $pv = sprintf "%.1f", ($rad * $af * $kJtokWh * $peak * $prdef * $ccf * $rcf); - + if(AttrVal ($name, 'verbose', 3) == 4) { $lh = { # Log-Hash zur Ausgabe "moduleDirection" => $moddir, @@ -3552,37 +3625,39 @@ sub __calcDWDforecast { "Loss String Peak Power by Temp" => $peakloss." kWP", "Area factor" => $af, "Estimated PV generation (calc)" => $pv." Wh", - }; - + }; + $sq = q{}; for my $idx (sort keys %{$lh}) { - $sq .= $idx." => ".$lh->{$idx}."\n"; + $sq .= $idx." => ".$lh->{$idx}."\n"; } - Log3 ($name, 4, "$name - PV forecast calc (raw) for $reld Hour ".sprintf("%02d",$hod)." string $st ->\n$sq"); + if($debug =~ /radiationProcess/x) { + Log (1, "$name DEBUG> PV forecast calc (raw) for $reld Hour ".sprintf("%02d",$hod)." string $st ->\n$sq"); + } } - + $pvsum += $pv; - $peaksum += $peak; + $peaksum += $peak; } - + $data{$type}{$name}{current}{allstringspeak} = $peaksum; # temperaturbedingte Korrektur der installierten Peakleistung in W - + $pvsum *= $hc; # Korrekturfaktor anwenden $pvsum = $peaksum if($pvsum > $peaksum); # Vorhersage nicht größer als die Summe aller PV-Strings Peak - + my $invcapacity = CurrentVal ($hash, "invertercapacity", 0); # Max. Leistung des Invertrs - + if ($invcapacity && $pvsum > $invcapacity) { $pvsum = $invcapacity + ($invcapacity * 0.01); # PV Vorhersage auf WR Kapazität zzgl. 1% begrenzen Log3 ($name, 4, "$name - PV forecast limited to $pvsum Watt due to inverter capacity"); } - + my $logao = qq{}; $paref->{pvsum} = $pvsum; $paref->{peaksum} = $peaksum; - ($pvsum, $logao) = ___70percentRule ($paref); - + ($pvsum, $logao) = ___70percentRule ($paref); + if(AttrVal ($name, 'verbose', 3) == 4) { $lh = { # Log-Hash zur Ausgabe "Cloudcover" => $cloudcover, @@ -3600,49 +3675,51 @@ sub __calcDWDforecast { "PV correction quality" => $hq, "PV generation forecast" => $pvsum." Wh ".$logao, }; - + $sq = q{}; for my $idx (sort keys %{$lh}) { - $sq .= $idx." => ".$lh->{$idx}."\n"; + $sq .= $idx." => ".$lh->{$idx}."\n"; + } + + if($debug =~ /radiationProcess/x) { + Log (1, "$name DEBUG> PV forecast calc for $reld Hour ".sprintf("%02d",$hod)." summary: \n$sq"); } - - Log3 ($name, 4, "$name - PV forecast calc for $reld Hour ".sprintf("%02d",$hod)." summary: \n$sq"); } - + return $pvsum; } ###################################################################### -# Liest den anzuwendenden Korrekturfaktor (Qualität) und +# Liest den anzuwendenden Korrekturfaktor (Qualität) und # speichert die Werte im Nexthours / PVhistory Hash ###################################################################### -sub ___readCorrfAndQuality { +sub ___readCorrfAndQuality { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; - my $num = $paref->{num}; # Nexthour + my $num = $paref->{num}; # Nexthour my $fh1 = $paref->{fh1}; my $fd = $paref->{fd}; my $range = $paref->{range}; - + my $uac = ReadingsVal ($name, "pvCorrectionFactor_Auto", "off"); # Auto- oder manuelle Korrektur my $pvcorr = ReadingsNum ($name, "pvCorrectionFactor_".sprintf("%02d",$fh1), 1.00); # PV Korrekturfaktor (auto oder manuell) - my $hc = $pvcorr; # Voreinstellung RAW-Korrekturfaktor + my $hc = $pvcorr; # Voreinstellung RAW-Korrekturfaktor my $hcfound = "use manual correction factor"; my $hq = "m"; - + if ($uac eq 'on') { # Autokorrektur soll genutzt werden - $hcfound = "yes"; # Status ob Autokorrekturfaktor im Wertevorrat gefunden wurde + $hcfound = "yes"; # Status ob Autokorrekturfaktor im Wertevorrat gefunden wurde ($hc, $hq) = CircularAutokorrVal ($hash, sprintf("%02d",$fh1), $range, undef); # Korrekturfaktor/KF-Qualität der Stunde des Tages der entsprechenden Bewölkungsrange $hq //= 0; if (!defined $hc) { $hcfound = "no"; - $hc = 1; # keine Korrektur + $hc = 1; # keine Korrektur $hq = 0; } } - + $hc = sprintf "%.2f", $hc; $data{$type}{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}{pvcorrf} = $hc."/".$hq; @@ -3653,17 +3730,17 @@ sub ___readCorrfAndQuality { $paref->{nhour} = sprintf("%02d",$fh1); $paref->{histname} = "pvcorrfactor"; setPVhistory ($paref); - delete $paref->{histname}; + delete $paref->{histname}; } - + return ($hcfound, $hc, $hq); } ################################################################ -# SolCast-API Strahlungsvorhersage Werte aus solcastapi-Hash +# SolCast-API Strahlungsvorhersage Werte aus solcastapi-Hash # übertragen und ggf. manipulieren ################################################################ -sub _transferSolCastRadiationValues { +sub _transferSolCastRadiationValues { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; @@ -3672,28 +3749,30 @@ sub _transferSolCastRadiationValues { my $chour = $paref->{chour}; my $date = $paref->{date}; my $daref = $paref->{daref}; - - return if(!keys %{$data{$type}{$name}{solcastapi}}); - - my @strings = sort keys %{$data{$type}{$name}{strings}}; - return if(!@strings); - for my $num (0..47) { + return if(!keys %{$data{$type}{$name}{solcastapi}}); + + my @strings = sort keys %{$data{$type}{$name}{strings}}; + return if(!@strings); + + my $lang = $paref->{lang}; + + for my $num (0..47) { my ($fd,$fh) = _calcDayHourMove ($chour, $num); - - if($fd > 1) { # überhängende Werte löschen + + if($fd > 1) { # überhängende Werte löschen delete $data{$type}{$name}{nexthours}{"NextHour".sprintf "%02d", $num}; next; } - + my $fh1 = $fh+1; my $wantts = (timestringToTimestamp ($date.' '.$chour.':00:00')) + ($num * 3600); - my $wantdt = (timestampToTimestring ($wantts))[1]; - + my $wantdt = (timestampToTimestring ($wantts, $lang))[1]; + my $time_str = "NextHour".sprintf "%02d", $num; - my ($hod) = $wantdt =~ /\s(\d{2}):/xs; + my ($hod) = $wantdt =~ /\s(\d{2}):/xs; $hod = sprintf "%02d", int ($hod)+1; # Stunde des Tages - + my $params = { hash => $hash, name => $name, @@ -3703,36 +3782,37 @@ sub _transferSolCastRadiationValues { fh1 => $fh1, num => $num, fd => $fd, - day => $paref->{day} + day => $paref->{day}, + debug => $paref->{debug} }; - + my $est = __calcSolCastEstimates ($params); - + $data{$type}{$name}{nexthours}{$time_str}{pvforecast} = $est; $data{$type}{$name}{nexthours}{$time_str}{starttime} = $wantdt; $data{$type}{$name}{nexthours}{$time_str}{hourofday} = $hod; $data{$type}{$name}{nexthours}{$time_str}{today} = $fd == 0 ? 1 : 0; $data{$type}{$name}{nexthours}{$time_str}{Rad1h} = '-'; # nur Info (nicht bei SolCast API) - - if($num < 23 && $fh < 24) { # Ringspeicher PV forecast Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 + + if($num < 23 && $fh < 24) { # Ringspeicher PV forecast Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 $data{$type}{$name}{circular}{sprintf "%02d",$fh1}{pvfc} = $est; - } - - if($fd == 0 && int $est > 0) { # Vorhersagedaten des aktuellen Tages zum manuellen Vergleich in Reading speichern - push @$daref, "Today_Hour".sprintf ("%02d",$fh1)."_PVforecast<>$est Wh"; } - + + if($fd == 0 && int $est > 0) { # Vorhersagedaten des aktuellen Tages zum manuellen Vergleich in Reading speichern + push @$daref, "Today_Hour".sprintf ("%02d",$fh1)."_PVforecast<>$est Wh"; + } + if($fd == 0 && $fh1) { $paref->{calcpv} = $est; $paref->{histname} = 'pvfc'; $paref->{nhour} = sprintf "%02d", $fh1; - setPVhistory ($paref); + setPVhistory ($paref); delete $paref->{histname}; } } - + push @$daref, ".lastupdateForecastValues<>".$t; # Statusreading letzter update - + return; } @@ -3748,9 +3828,9 @@ sub __calcSolCastEstimates { my $hod = $paref->{hod}; my $fd = $paref->{fd}; my $num = $paref->{num}; - + my $reld = $fd == 0 ? "today" : $fd == 1 ? "tomorrow" : "unknown"; - + my $clouddamp = AttrVal($name, "affectCloudfactorDamping", $cldampdef); # prozentuale Berücksichtigung des Bewölkungskorrekturfaktors my $raindamp = AttrVal($name, "affectRainfactorDamping", $rdampdef); # prozentuale Berücksichtigung des Regenkorrekturfaktors @@ -3761,18 +3841,19 @@ sub __calcSolCastEstimates { my $ccf = 1 - ((($cloudcover - $cloud_base)/100) * $clouddamp/100); # Cloud Correction Faktor mit Steilheit und Fußpunkt my $temp = NexthoursVal ($hash, "NextHour".sprintf("%02d",$num), "temp", $tempbasedef); # vorhergesagte Temperatur Stunde X - + my $debug = $paref->{debug}; + my ($hcfound, $perc, $hq) = ___readPercAndQuality ($paref); # liest den anzuwendenden Korrekturfaktor - + my ($lh,$sq); my $pvsum = 0; my $peaksum = 0; - + for my $string (sort keys %{$data{$type}{$name}{strings}}) { my $peak = $data{$type}{$name}{strings}{$string}{peak}; # String Peak (kWp) - + $peak *= 1000; - + my $est = SolCastAPIVal ($hash, $string, $wantdt, 'pv_estimate50', 0) * $perc; my $pv = sprintf "%.1f", ($est * $ccf * $rcf); @@ -3782,35 +3863,38 @@ sub __calcSolCastEstimates { "Estimated PV generation (raw)" => $est." Wh", "Estimated PV generation (calc)" => $pv." Wh", }; - + $sq = q{}; for my $idx (sort keys %{$lh}) { - $sq .= $idx." => ".$lh->{$idx}."\n"; + $sq .= $idx." => ".$lh->{$idx}."\n"; } - - Log3 ($name, 4, "$name - PV estimate for $reld Hour ".sprintf ("%02d", $hod)." string $string ->\n$sq"); + + if($debug =~ /radiationProcess/x) { + Log (1, "$name DEBUG> PV estimate for $reld Hour ".sprintf ("%02d", $hod)." string $string ->\n$sq"); + } + } $pvsum += $pv; - $peaksum += $peak; - } - + $peaksum += $peak; + } + $data{$type}{$name}{current}{allstringspeak} = $peaksum; # temperaturbedingte Korrektur der installierten Peakleistung in W - + $pvsum = $peaksum if($pvsum > $peaksum); # Vorhersage nicht größer als die Summe aller PV-Strings Peak - + my $invcapacity = CurrentVal ($hash, 'invertercapacity', 0); # Max. Leistung des Invertrs - + if ($invcapacity && $pvsum > $invcapacity) { $pvsum = $invcapacity + ($invcapacity * 0.01); # PV Vorhersage auf WR Kapazität zzgl. 1% begrenzen Log3 ($name, 4, "$name - PV forecast limited to $pvsum Watt due to inverter capacity"); } - + my $logao = qq{}; $paref->{pvsum} = $pvsum; $paref->{peaksum} = $peaksum; - ($pvsum, $logao) = ___70percentRule ($paref); - + ($pvsum, $logao) = ___70percentRule ($paref); + if(AttrVal ($name, 'verbose', 3) == 4) { $lh = { # Log-Hash zur Ausgabe "Starttime" => $wantdt, @@ -3820,76 +3904,78 @@ sub __calcSolCastEstimates { "Cloudfactor" => $ccf, "Rainprob" => $rainprob, "Rainfactor" => $rcf, - "RainFactorDamping" => $raindamp." %", + "RainFactorDamping" => $raindamp." %", "CloudCorrFoundInStore" => $hcfound, "SolCast selected percentile" => $perc, "PV correction quality" => $hq, "PV generation forecast" => $pvsum." Wh ".$logao, }; - + $sq = q{}; for my $idx (sort keys %{$lh}) { - $sq .= $idx." => ".$lh->{$idx}."\n"; + $sq .= $idx." => ".$lh->{$idx}."\n"; + } + + if($debug =~ /radiationProcess/x) { + Log (1, "$name DEBUG> PV estimate for $reld Hour ".sprintf ("%02d", $hod)." summary: \n$sq"); } - - Log3 ($name, 4, "$name - PV estimate for $reld Hour ".sprintf ("%02d", $hod)." summary: \n$sq"); } - + return $pvsum; } ###################################################################### -# Liest das anzuwendende Percentil (Qualität) und +# Liest das anzuwendende Percentil (Qualität) und # speichert die Werte im Nexthours / PVhistory Hash ###################################################################### -sub ___readPercAndQuality { +sub ___readPercAndQuality { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; - my $num = $paref->{num}; # Nexthour + my $num = $paref->{num}; # Nexthour my $fh1 = $paref->{fh1}; my $fd = $paref->{fd}; - + my $uac = ReadingsVal ($name, "pvCorrectionFactor_Auto", "off"); # Auto- oder manuelle Korrektur my $perc = ReadingsNum ($name, "pvCorrectionFactor_".sprintf("%02d",$fh1), 1.0); # Estimate Percentilfaktor my $hcfound = "use manual percentile selection"; my $hq = "m"; - + if ($uac eq 'on') { # Autokorrektur soll genutzt werden - $hcfound = "yes"; # Status ob Autokorrekturfaktor im Wertevorrat gefunden wurde + $hcfound = "yes"; # Status ob Autokorrekturfaktor im Wertevorrat gefunden wurde ($perc, $hq) = CircularAutokorrVal ($hash, sprintf("%02d",$fh1), 'percentile', undef); # Korrekturfaktor/KF-Qualität der Stunde des Tages der entsprechenden Bewölkungsrange $hq //= 0; - + if (!$perc) { $hcfound = "no"; - $perc = 1.0; # keine Korrektur + $perc = 1.0; # keine Korrektur $hq = 0; } - + $perc = 1.0 if($perc >= 10); } - + $perc = sprintf "%.2f", $perc; $data{$type}{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}{pvcorrf} = $perc."/".$hq; - + return ($hcfound, $perc, $hq); } ################################################################### -# Zellen Leistungskorrektur Einfluss durch Wärmekoeffizienten +# Zellen Leistungskorrektur Einfluss durch Wärmekoeffizienten # berechnen # -# Die Nominalleistung der Module wird bei 25 Grad -# Umgebungstemperatur und bei 1.000 Watt Sonneneinstrahlung -# gemessen. -# Steigt die Temperatur um 1 Grad Celsius sinkt die Modulleistung +# Die Nominalleistung der Module wird bei 25 Grad +# Umgebungstemperatur und bei 1.000 Watt Sonneneinstrahlung +# gemessen. +# Steigt die Temperatur um 1 Grad Celsius sinkt die Modulleistung # typisch um 0,4 Prozent. Solartellen können im Sommer 70°C heiß # werden. # -# Das würde für eine 10 kWp Photovoltaikanlage folgenden -# Leistungsverlust bedeuten: +# Das würde für eine 10 kWp Photovoltaikanlage folgenden +# Leistungsverlust bedeuten: # # Leistungsverlust = -0,4%/K * 45K * 10 kWp = 1,8 kWp # @@ -3903,9 +3989,9 @@ sub ___calcPeaklossByTemp { my $peak = $paref->{peak} // return (0,0); my $cloudcover = $paref->{cloudcover} // return (0,0); # vorhergesagte Wolkendecke Stunde X my $temp = $paref->{temp} // return (0,0); # vorhergesagte Temperatur Stunde X - + my $modtemp = $temp + ($tempmodinc * (1 - ($cloudcover/100))); # kalkulierte Modultemperatur - + my $peakloss = sprintf "%.2f", $tempcoeffdef * ($temp - $tempbasedef) * $peak / 100; return ($peakloss, $modtemp); @@ -3920,29 +4006,29 @@ sub ___70percentRule { my $name = $paref->{name}; my $pvsum = $paref->{pvsum}; my $peaksum = $paref->{peaksum}; - my $num = $paref->{num}; # Nexthour - + my $num = $paref->{num}; # Nexthour + my $logao = qq{}; my $confc = NexthoursVal ($hash, "NextHour".sprintf("%02d",$num), "confc", 0); my $max70 = $peaksum/100 * 70; - - if(AttrVal ($name, "affect70percentRule", "0") eq "1" && $pvsum > $max70) { + + if(AttrVal ($name, "affect70percentRule", "0") eq "1" && $pvsum > $max70) { $pvsum = $max70; $logao = qq{(reduced by 70 percent rule)}; - } + } if(AttrVal ($name, "affect70percentRule", "0") eq "dynamic" && $pvsum > $max70 + $confc) { $pvsum = $max70 + $confc; $logao = qq{(reduced by 70 percent dynamic rule)}; - } - + } + $pvsum = int $pvsum; - + return ($pvsum, $logao); } ################################################################ -# den Maximalwert PV Vorhersage für Heute ermitteln +# den Maximalwert PV Vorhersage für Heute ermitteln ################################################################ sub _calcMaxEstimateToday { my $paref = shift; @@ -3951,95 +4037,99 @@ sub _calcMaxEstimateToday { my $type = $paref->{type}; my $daref = $paref->{daref}; my $date = $paref->{date}; - + my $maxest = 0; my $maxtim = '-'; - - for my $h (1..23) { + + for my $h (1..23) { my $pvfc = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$h)."_PVforecast", 0); next if($pvfc <= $maxest); - + $maxtim = $date.' '.sprintf("%02d",$h-1).':00:00'; $maxest = $pvfc; } - + return if(!$maxest); - + push @$daref, "Today_MaxPVforecast<>". $maxest." Wh"; - push @$daref, "Today_MaxPVforecastTime<>". $maxtim; - + push @$daref, "Today_MaxPVforecastTime<>". $maxtim; + return; } ################################################################ # Werte Inverter Device ermitteln und übertragen ################################################################ -sub _transferInverterValues { +sub _transferInverterValues { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $t = $paref->{t}; # aktuelle Unix-Zeit my $chour = $paref->{chour}; my $day = $paref->{day}; - my $daref = $paref->{daref}; + my $daref = $paref->{daref}; my $indev = ReadingsVal($name, "currentInverterDev", ""); my ($a,$h) = parseParams ($indev); $indev = $a->[0] // ""; return if(!$indev || !$defs{$indev}); - + my $type = $paref->{type}; - + my ($pvread,$pvunit) = split ":", $h->{pv}; # Readingname/Unit für aktuelle PV Erzeugung my ($edread,$etunit) = split ":", $h->{etotal}; # Readingname/Unit für Energie total (PV Erzeugung) - + $data{$type}{$name}{current}{invertercapacity} = $h->{capacity} if($h->{capacity}); # optionale Angabe max. WR-Leistung - + return if(!$pvread || !$edread); - - Log3 ($name, 5, "$name - collect Inverter data: device=$indev, pv=$pvread ($pvunit), etotal=$edread ($etunit)"); - + my $pvuf = $pvunit =~ /^kW$/xi ? 1000 : 1; - my $pv = ReadingsNum ($indev, $pvread, 0) * $pvuf; # aktuelle Erzeugung (W) + my $pv = ReadingsNum ($indev, $pvread, 0) * $pvuf; # aktuelle Erzeugung (W) $pv = $pv < 0 ? 0 : sprintf("%.0f", $pv); # Forum: https://forum.fhem.de/index.php/topic,117864.msg1159718.html#msg1159718, https://forum.fhem.de/index.php/topic,117864.msg1166201.html#msg1166201 - - push @$daref, "Current_PV<>". $pv." W"; + + push @$daref, "Current_PV<>". $pv." W"; $data{$type}{$name}{current}{generation} = $pv; # Hilfshash Wert current generation Forum: https://forum.fhem.de/index.php/topic,117864.msg1139251.html#msg1139251 - + push @{$data{$type}{$name}{current}{genslidereg}}, $pv; # Schieberegister PV Erzeugung limitArray ($data{$type}{$name}{current}{genslidereg}, $defslidenum); - + my $etuf = $etunit =~ /^kWh$/xi ? 1000 : 1; - my $etotal = ReadingsNum ($indev, $edread, 0) * $etuf; # Erzeugung total (Wh) + my $etotal = ReadingsNum ($indev, $edread, 0) * $etuf; # Erzeugung total (Wh) + my $debug = $paref->{debug}; + if($debug =~ /collectData/x) { + Log (1, "$name DEBUG> collect Inverter data - device: $indev =>"); + Log (1, "$name DEBUG> pv: $pv W, etotal: $etotal Wh"); + } + my $nhour = $chour+1; - + my $histetot = HistoryVal ($hash, $day, sprintf("%02d",$nhour), "etotal", 0); # etotal zu Beginn einer Stunde - + my $ethishour; - if(!$histetot) { # etotal der aktuelle Stunde gesetzt ? + if(!$histetot) { # etotal der aktuelle Stunde gesetzt ? $paref->{etotal} = $etotal; $paref->{nhour} = sprintf("%02d",$nhour); $paref->{histname} = "etotal"; setPVhistory ($paref); delete $paref->{histname}; - + my $etot = CurrentVal ($hash, "etotal", $etotal); $ethishour = int ($etotal - $etot); } else { $ethishour = int ($etotal - $histetot); } - + $data{$type}{$name}{current}{etotal} = $etotal; # aktuellen etotal des WR speichern - + if($ethishour < 0) { $ethishour = 0; } - - push @$daref, "Today_Hour".sprintf("%02d",$nhour)."_PVreal<>".$ethishour." Wh"; + + push @$daref, "Today_Hour".sprintf("%02d",$nhour)."_PVreal<>".$ethishour." Wh"; $data{$type}{$name}{circular}{sprintf("%02d",$nhour)}{pvrl} = $ethishour; # Ringspeicher PV real Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 - + $paref->{ethishour} = $ethishour; $paref->{nhour} = sprintf("%02d",$nhour); $paref->{histname} = "pvrl"; @@ -4052,39 +4142,44 @@ return; ################################################################ # Wetter Werte aus dem angebenen Wetterdevice extrahieren ################################################################ -sub _transferWeatherValues { +sub _transferWeatherValues { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $t = $paref->{t}; # Epoche Zeit my $chour = $paref->{chour}; my $daref = $paref->{daref}; - + my $fcname = ReadingsVal($name, "currentForecastDev", ""); # Weather Forecast Device return if(!$fcname || !$defs{$fcname}); - + my $err = checkdwdattr ($name,$fcname,\@dweattrmust); $paref->{state} = $err if($err); - - my $type = $paref->{type}; + + my $type = $paref->{type}; + my $debug = $paref->{debug}; my ($time_str); - - my $fc0_SunRise = ReadingsVal($fcname, "fc0_SunRise", "00:00"); # Sonnenaufgang heute - my $fc0_SunSet = ReadingsVal($fcname, "fc0_SunSet", "00:00"); # Sonnenuntergang heute - my $fc1_SunRise = ReadingsVal($fcname, "fc1_SunRise", "00:00"); # Sonnenaufgang morgen - my $fc1_SunSet = ReadingsVal($fcname, "fc1_SunSet", "00:00"); # Sonnenuntergang morgen - + + my $fc0_SunRise = ReadingsVal($fcname, "fc0_SunRise", "00:00"); # Sonnenaufgang heute + my $fc0_SunSet = ReadingsVal($fcname, "fc0_SunSet", "00:00"); # Sonnenuntergang heute + my $fc1_SunRise = ReadingsVal($fcname, "fc1_SunRise", "00:00"); # Sonnenaufgang morgen + my $fc1_SunSet = ReadingsVal($fcname, "fc1_SunSet", "00:00"); # Sonnenuntergang morgen + push @$daref, "Today_SunRise<>". $fc0_SunRise; push @$daref, "Today_SunSet<>". $fc0_SunSet; push @$daref, "Tomorrow_SunRise<>".$fc1_SunRise; push @$daref, "Tomorrow_SunSet<>". $fc1_SunSet; - + my $fc0_SunRise_round = sprintf "%02d", (split ":", $fc0_SunRise)[0]; my $fc0_SunSet_round = sprintf "%02d", (split ":", $fc0_SunSet)[0]; my $fc1_SunRise_round = sprintf "%02d", (split ":", $fc1_SunRise)[0]; my $fc1_SunSet_round = sprintf "%02d", (split ":", $fc1_SunSet)[0]; - for my $num (0..46) { + if($debug =~ /collectData/x) { + Log (1, "$name DEBUG> collect Weather data - device: $fcname =>"); + } + + for my $num (0..46) { my ($fd,$fh) = _calcDayHourMove ($chour, $num); last if($fd > 1); @@ -4094,143 +4189,149 @@ sub _transferWeatherValues { my $neff = ReadingsNum($fcname, "fc${fd}_${fh2}_Neff", 0); # Effektive Wolkendecke my $r101 = ReadingsNum($fcname, "fc${fd}_${fh2}_R101", 0); # Niederschlagswahrscheinlichkeit> 0,1 mm während der letzten Stunde my $temp = ReadingsNum($fcname, "fc${fd}_${fh2}_TTT", 0); # Außentemperatur - + my $fhstr = sprintf "%02d", $fh; # hier kann Tag/Nacht-Grenze verstellt werden - + if($fd == 0 && ($fhstr lt $fc0_SunRise_round || $fhstr gt $fc0_SunSet_round)) { # Zeit vor Sonnenaufgang oder nach Sonnenuntergang heute $wid += 100; # "1" der WeatherID voranstellen wenn Nacht } elsif ($fd == 1 && ($fhstr lt $fc1_SunRise_round || $fhstr gt $fc1_SunSet_round)) { # Zeit vor Sonnenaufgang oder nach Sonnenuntergang morgen $wid += 100; # "1" der WeatherID voranstellen wenn Nacht } - + my $txt = ReadingsVal($fcname, "fc${fd}_${fh2}_wwd", ''); - Log3 ($name, 5, "$name - collect Weather data: device=$fcname, wid=fc${fd}_${fh1}_ww, val=$wid, txt=$txt, cc=$neff, rp=$r101, t=$temp"); - - $time_str = "NextHour".sprintf "%02d", $num; + if($debug =~ /collectData/x) { + Log (1, "$name DEBUG> wid: fc${fd}_${fh1}_ww, val: $wid, txt: $txt, cc: $neff, rp: $r101, temp: $temp"); + } + + $time_str = "NextHour".sprintf "%02d", $num; $data{$type}{$name}{nexthours}{$time_str}{weatherid} = $wid; $data{$type}{$name}{nexthours}{$time_str}{cloudcover} = $neff; $data{$type}{$name}{nexthours}{$time_str}{rainprob} = $r101; $data{$type}{$name}{nexthours}{$time_str}{temp} = $temp; - - if($num < 23 && $fh < 24) { # Ringspeicher Weather Forum: https://forum.fhem.de/index.php/topic,117864.msg1139251.html#msg1139251 + + if($num < 23 && $fh < 24) { # Ringspeicher Weather Forum: https://forum.fhem.de/index.php/topic,117864.msg1139251.html#msg1139251 $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{weatherid} = $wid; $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{weathertxt} = $txt; $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{wcc} = $neff; $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{wrp} = $r101; $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{temp} = $temp; - + if($num == 0) { # aktuelle Außentemperatur - $data{$type}{$name}{current}{temp} = $temp; - } + $data{$type}{$name}{current}{temp} = $temp; + } } - + if($fd == 0 && $fh1) { # Weather in pvhistory speichern $paref->{wid} = $wid; $paref->{histname} = "weatherid"; $paref->{nhour} = sprintf("%02d",$fh1); - setPVhistory ($paref); - + setPVhistory ($paref); + $paref->{wcc} = $neff; $paref->{histname} = "weathercloudcover"; setPVhistory ($paref); - + $paref->{wrp} = $r101; $paref->{histname} = "weatherrainprob"; setPVhistory ($paref); - + $paref->{temp} = $temp; $paref->{histname} = "temperature"; setPVhistory ($paref); - + delete $paref->{histname}; } } - + return; } ################################################################ # Werte Meter Device ermitteln und übertragen ################################################################ -sub _transferMeterValues { +sub _transferMeterValues { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $t = $paref->{t}; my $chour = $paref->{chour}; - my $daref = $paref->{daref}; + my $daref = $paref->{daref}; my $medev = ReadingsVal($name, "currentMeterDev", ""); # aktuelles Meter device my ($a,$h) = parseParams ($medev); $medev = $a->[0] // ""; return if(!$medev || !$defs{$medev}); - + my $type = $paref->{type}; - + my ($gc,$gcunit) = split ":", $h->{gcon}; # Readingname/Unit für aktuellen Netzbezug my ($gf,$gfunit) = split ":", $h->{gfeedin}; # Readingname/Unit für aktuelle Netzeinspeisung my ($gt,$ctunit) = split ":", $h->{contotal}; # Readingname/Unit für Bezug total my ($ft,$ftunit) = split ":", $h->{feedtotal}; # Readingname/Unit für Einspeisung total - + return if(!$gc || !$gf || !$gt || !$ft); - + $gfunit //= $gcunit; $gcunit //= $gfunit; - - Log3 ($name, 5, "$name - collect Meter data: device=$medev, gcon=$gc ($gcunit), gfeedin=$gf ($gfunit) ,contotal=$gt ($ctunit), feedtotal=$ft ($ftunit)"); - + my ($gco,$gfin); - + my $gcuf = $gcunit =~ /^kW$/xi ? 1000 : 1; my $gfuf = $gfunit =~ /^kW$/xi ? 1000 : 1; - + $gco = ReadingsNum ($medev, $gc, 0) * $gcuf; # aktueller Bezug (W) $gfin = ReadingsNum ($medev, $gf, 0) * $gfuf; # aktuelle Einspeisung (W) - + my $params; - + if ($gc eq "-gfeedin") { # Spezialfall gcon bei neg. gfeedin # Spezialfall: bei negativen gfeedin -> $gco = abs($gf), $gf = 0 $params = { dev => $medev, rdg => $gf, rdgf => $gfuf - }; - + }; + ($gfin,$gco) = substSpecialCases ($params); } - + if ($gf eq "-gcon") { # Spezialfall gfeedin bei neg. gcon $params = { dev => $medev, rdg => $gc, rdgf => $gcuf - }; - + }; + ($gco,$gfin) = substSpecialCases ($params); } - + push @$daref, "Current_GridConsumption<>".(int $gco)." W"; - $data{$type}{$name}{current}{gridconsumption} = int $gco; # Hilfshash Wert current grid consumption Forum: https://forum.fhem.de/index.php/topic,117864.msg1139251.html#msg1139251 - + $data{$type}{$name}{current}{gridconsumption} = int $gco; # Hilfshash Wert current grid consumption Forum: https://forum.fhem.de/index.php/topic,117864.msg1139251.html#msg1139251 + push @$daref, "Current_GridFeedIn<>".(int $gfin)." W"; $data{$type}{$name}{current}{gridfeedin} = int $gfin; # Hilfshash Wert current grid Feed in - + my $ctuf = $ctunit =~ /^kWh$/xi ? 1000 : 1; - my $gctotal = ReadingsNum ($medev, $gt, 0) * $ctuf; # Bezug total (Wh) + my $gctotal = ReadingsNum ($medev, $gt, 0) * $ctuf; # Bezug total (Wh) my $ftuf = $ftunit =~ /^kWh$/xi ? 1000 : 1; - my $fitotal = ReadingsNum ($medev, $ft, 0) * $ftuf; # Einspeisung total (Wh) - + my $fitotal = ReadingsNum ($medev, $ft, 0) * $ftuf; # Einspeisung total (Wh) + + my $debug = $paref->{debug}; + if($debug =~ /collectData/x) { + Log (1, "$name DEBUG> collect Meter data - device: $medev =>"); + Log (1, "$name DEBUG> gcon: $gco W, gfeedin: $gfin W, contotal: $gctotal Wh, feedtotal: $fitotal Wh"); + } + my $gcdaypast = 0; my $gfdaypast = 0; - - for my $hour (0..int $chour) { # alle bisherigen Erzeugungen des Tages summieren + + for my $hour (0..int $chour) { # alle bisherigen Erzeugungen des Tages summieren $gcdaypast += ReadingsNum ($name, "Today_Hour".sprintf("%02d",$hour)."_GridConsumption", 0); $gfdaypast += ReadingsNum ($name, "Today_Hour".sprintf("%02d",$hour)."_GridFeedIn", 0); } - + my $docon = 0; if ($gcdaypast == 0) { # Management der Stundenberechnung auf Basis Totalwerte GridConsumtion if (defined $hash->{HELPER}{INITCONTOTAL}) { @@ -4246,25 +4347,25 @@ sub _transferMeterValues { else { $docon = 1; } - + if ($docon) { my $gctotthishour = int ($gctotal - ($gcdaypast + $hash->{HELPER}{INITCONTOTAL})); - + if($gctotthishour < 0) { $gctotthishour = 0; } - + my $nhour = $chour+1; push @$daref, "Today_Hour".sprintf("%02d",$nhour)."_GridConsumption<>".$gctotthishour." Wh"; $data{$type}{$name}{circular}{sprintf("%02d",$nhour)}{gcons} = $gctotthishour; # Hilfshash Wert Bezug (Wh) Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 - + $paref->{gctotthishour} = $gctotthishour; $paref->{nhour} = sprintf("%02d",$nhour); $paref->{histname} = "cons"; setPVhistory ($paref); delete $paref->{histname}; - } - + } + my $dofeed = 0; if ($gfdaypast == 0) { # Management der Stundenberechnung auf Basis Totalwerte GridFeedIn if (defined $hash->{HELPER}{INITFEEDTOTAL}) { @@ -4280,30 +4381,30 @@ sub _transferMeterValues { else { $dofeed = 1; } - + if ($dofeed) { my $gftotthishour = int ($fitotal - ($gfdaypast + $hash->{HELPER}{INITFEEDTOTAL})); - + if($gftotthishour < 0) { $gftotthishour = 0; } - + my $nhour = $chour+1; push @$daref, "Today_Hour".sprintf("%02d",$nhour)."_GridFeedIn<>".$gftotthishour." Wh"; $data{$type}{$name}{circular}{sprintf("%02d",$nhour)}{gfeedin} = $gftotthishour; - + $paref->{gftotthishour} = $gftotthishour; $paref->{nhour} = sprintf("%02d",$nhour); $paref->{histname} = "gfeedin"; setPVhistory ($paref); delete $paref->{histname}; } - + return; } ################################################################ -# Consumer - Energieverbrauch aufnehmen +# Consumer - Energieverbrauch aufnehmen # - Masterdata ergänzen # - Schaltzeiten planen ################################################################ @@ -4319,169 +4420,169 @@ sub _manageConsumerData { my $daref = $paref->{daref}; my $nhour = $chour+1; - $paref->{nhour} = sprintf("%02d",$nhour); + $paref->{nhour} = sprintf("%02d",$nhour); for my $c (sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}) { - my $consumer = ConsumerVal ($hash, $c, "name", ""); - my $alias = ConsumerVal ($hash, $c, "alias", ""); - - ## aktuelle Leistung auslesen + my $consumer = ConsumerVal ($hash, $c, "name", ""); + my $alias = ConsumerVal ($hash, $c, "alias", ""); + + ## aktuelle Leistung auslesen ############################## - my $paread = ConsumerVal ($hash, $c, "rpcurr", ""); - my $up = ConsumerVal ($hash, $c, "upcurr", ""); + my $paread = ConsumerVal ($hash, $c, "rpcurr", ""); + my $up = ConsumerVal ($hash, $c, "upcurr", ""); my $pcurr = 0; - + if($paread) { my $eup = $up =~ /^kW$/xi ? 1000 : 1; - $pcurr = ReadingsNum ($consumer, $paread, 0) * $eup; - - push @$daref, "consumer${c}_currentPower<>". $pcurr." W"; + $pcurr = ReadingsNum ($consumer, $paread, 0) * $eup; + + push @$daref, "consumer${c}_currentPower<>". $pcurr." W"; } ## Verbrauch auslesen + speichern ################################### my $ethreshold = 0; - my $etotread = ConsumerVal ($hash, $c, "retotal", ""); - my $u = ConsumerVal ($hash, $c, "uetotal", ""); - + my $etotread = ConsumerVal ($hash, $c, "retotal", ""); + my $u = ConsumerVal ($hash, $c, "uetotal", ""); + if($etotread) { my $eu = $u =~ /^kWh$/xi ? 1000 : 1; my $etot = ReadingsNum ($consumer, $etotread, 0) * $eu; # Summe Energieverbrauch des Verbrauchers my $ehist = HistoryVal ($hash, $day, sprintf("%02d",$nhour), "csmt${c}", undef); # gespeicherter Totalverbrauch - $ethreshold = ConsumerVal ($hash, $c, "energythreshold", 0); # Schwellenwert (Wh pro Stunde) ab der ein Verbraucher als aktiv gewertet wird - + $ethreshold = ConsumerVal ($hash, $c, "energythreshold", 0); # Schwellenwert (Wh pro Stunde) ab der ein Verbraucher als aktiv gewertet wird + ## aktuelle Leistung ermitteln wenn kein Reading d. aktuellen Leistung verfügbar ################################################################################## if(!$paread){ my $timespan = $t - ConsumerVal ($hash, $c, "old_etottime", $t); my $delta = $etot - ConsumerVal ($hash, $c, "old_etotal", $etot); $pcurr = sprintf("%.6f", $delta / (3600 * $timespan)) if($delta); # Einheitenformel beachten !!: W = Wh / (3600 * s) - + $data{$type}{$name}{consumers}{$c}{old_etotal} = $etot; $data{$type}{$name}{consumers}{$c}{old_etottime} = $t; - + push @$daref, "consumer${c}_currentPower<>". $pcurr." W"; } - + if(defined $ehist && $etot >= $ehist && ($etot - $ehist) >= $ethreshold) { my $consumerco = $etot - $ehist; $consumerco += HistoryVal ($hash, $day, sprintf("%02d",$nhour), "csme${c}", 0); - + $paref->{consumerco} = $consumerco; # Verbrauch des Consumers aktuelle Stunde $paref->{histname} = "csme${c}"; setPVhistory ($paref); - delete $paref->{histname}; - } + delete $paref->{histname}; + } $paref->{consumerco} = $etot; # Totalverbrauch des Verbrauchers $paref->{histname} = "csmt${c}"; setPVhistory ($paref); delete $paref->{histname}; } - + deleteReadingspec ($hash, "consumer${c}_currentPower") if(!$etotread && !$paread); - + ## Verbraucher - Laufzeit und Zyklen pro Tag ermitteln ## Laufzeit (in Minuten) wird pro Stunde erfasst ## bei Tageswechsel Rücksetzen in _specialActivities - ####################################################### + ####################################################### my $starthour; if(isConsumerLogOn ($hash, $c, $pcurr)) { # Verbraucher ist logisch "an" - if(ConsumerVal ($hash, $c, "onoff", "off") eq "off") { + if(ConsumerVal ($hash, $c, "onoff", "off") eq "off") { $data{$type}{$name}{consumers}{$c}{startTime} = $t; $data{$type}{$name}{consumers}{$c}{onoff} = "on"; my $stimes = ConsumerVal ($hash, $c, "numberDayStarts", 0); # Anzahl der On-Schaltungen am Tag $data{$type}{$name}{consumers}{$c}{numberDayStarts} = $stimes+1; - $data{$type}{$name}{consumers}{$c}{lastMinutesOn} = ConsumerVal ($hash, $c, "minutesOn", 0); + $data{$type}{$name}{consumers}{$c}{lastMinutesOn} = ConsumerVal ($hash, $c, "minutesOn", 0); } - - $starthour = strftime "%H", localtime(ConsumerVal ($hash, $c, "startTime", $t)); - - if($chour eq $starthour) { - my $runtime = (($t - ConsumerVal ($hash, $c, "startTime", $t)) / 60); # in Minuten ! (gettimeofday sind ms !) - $data{$type}{$name}{consumers}{$c}{minutesOn} = ConsumerVal ($hash, $c, "lastMinutesOn", 0) + $runtime; + + $starthour = strftime "%H", localtime(ConsumerVal ($hash, $c, "startTime", $t)); + + if($chour eq $starthour) { + my $runtime = (($t - ConsumerVal ($hash, $c, "startTime", $t)) / 60); # in Minuten ! (gettimeofday sind ms !) + $data{$type}{$name}{consumers}{$c}{minutesOn} = ConsumerVal ($hash, $c, "lastMinutesOn", 0) + $runtime; } else { # neue Stunde hat begonnen if(ConsumerVal ($hash, $c, "onoff", "off") eq "on") { $data{$type}{$name}{consumers}{$c}{startTime} = timestringToTimestamp ($date." ".sprintf("%02d",$chour).":00:00"); - $data{$type}{$name}{consumers}{$c}{minutesOn} = ($t - ConsumerVal ($hash, $c, "startTime", $t)) / 60; # in Minuten ! (gettimeofday sind ms !) + $data{$type}{$name}{consumers}{$c}{minutesOn} = ($t - ConsumerVal ($hash, $c, "startTime", $t)) / 60; # in Minuten ! (gettimeofday sind ms !) $data{$type}{$name}{consumers}{$c}{lastMinutesOn} = 0; } - } + } } else { # Verbraucher soll nicht aktiv sein $data{$type}{$name}{consumers}{$c}{onoff} = "off"; - $starthour = strftime "%H", localtime(ConsumerVal ($hash, $c, "startTime", $t)); - + $starthour = strftime "%H", localtime(ConsumerVal ($hash, $c, "startTime", $t)); + if($chour ne $starthour) { $data{$type}{$name}{consumers}{$c}{minutesOn} = 0; delete $data{$type}{$name}{consumers}{$c}{startTime}; } } - + $paref->{val} = ConsumerVal ($hash, $c, "numberDayStarts", 0); # Anzahl Tageszyklen des Verbrauchers speichern $paref->{histname} = "cyclescsm${c}"; setPVhistory ($paref); delete $paref->{histname}; - + $paref->{val} = ceil ConsumerVal ($hash, $c, "minutesOn", 0); # Verbrauchsminuten akt. Stunde des Consumers $paref->{histname} = "minutescsm${c}"; setPVhistory ($paref); delete $paref->{histname}; - + ## Durchschnittsverbrauch / Betriebszeit ermitteln + speichern ################################################################ my $consumerco = 0; my $runhours = 0; my $dnum = 0; - + for my $n (sort{$a<=>$b} keys %{$data{$type}{$name}{pvhist}}) { # Betriebszeit und gemessenen Verbrauch ermitteln my $csme = HistoryVal ($hash, $n, 99, "csme${c}", 0); my $hours = HistoryVal ($hash, $n, 99, "hourscsme${c}", 0); next if(!$hours); - + $consumerco += $csme; $runhours += $hours; - $dnum++; + $dnum++; } - - if ($dnum) { - if($consumerco) { + + if ($dnum) { + if($consumerco) { $data{$type}{$name}{consumers}{$c}{avgenergy} = ceil ($consumerco/$runhours); # Durchschnittsverbrauch pro Stunde in Wh } else { delete $data{$type}{$name}{consumers}{$c}{avgenergy}; } - - $data{$type}{$name}{consumers}{$c}{avgruntime} = (ceil($runhours/$dnum)) * 60; # Durchschnittslaufzeit am Tag in Minuten + + $data{$type}{$name}{consumers}{$c}{avgruntime} = (ceil($runhours/$dnum)) * 60; # Durchschnittslaufzeit am Tag in Minuten } - + $paref->{consumer} = $c; - + __calcEnergyPieces ($paref); # Energieverbrauch auf einzelne Stunden für Planungsgrundlage aufteilen __planSwitchTimes ($paref); # Consumer Switch Zeiten planen __setTimeframeState ($paref); # Timeframe Status ermitteln __setConsRcmdState ($paref); # Consumption Recommended Status setzen __switchConsumer ($paref); # Consumer schalten __remainConsumerTime ($paref); # Restlaufzeit Verbraucher ermitteln - - ## Consumer Schaltstatus und Schaltzeit für Readings ermitteln + + ## Consumer Schaltstatus und Schaltzeit für Readings ermitteln ################################################################ - my $costate = isConsumerPhysOn ($hash, $c) ? "on" : + my $costate = isConsumerPhysOn ($hash, $c) ? "on" : isConsumerPhysOff ($hash, $c) ? "off" : "unknown"; $data{$type}{$name}{consumers}{$c}{state} = $costate; - + my ($pstate,$starttime,$stoptime) = __getPlanningStateAndTimes ($paref); - - push @$daref, "consumer${c}<>" ."name='$alias' state='$costate' planningstate='$pstate' "; # Consumer Infos + + push @$daref, "consumer${c}<>" ."name='$alias' state='$costate' planningstate='$pstate' "; # Consumer Infos push @$daref, "consumer${c}_planned_start<>"."$starttime" if($starttime); # Consumer Start geplant - push @$daref, "consumer${c}_planned_stop<>". "$stoptime" if($stoptime); # Consumer Stop geplant + push @$daref, "consumer${c}_planned_stop<>". "$stoptime" if($stoptime); # Consumer Stop geplant } - + delete $paref->{consumer}; - + return; } @@ -4497,9 +4598,9 @@ sub __calcEnergyPieces { my $name = $paref->{name}; my $type = $paref->{type}; my $c = $paref->{consumer}; - + my $etot = HistoryVal ($hash, $paref->{day}, sprintf("%02d",$paref->{nhour}), "csmt${c}", 0); - + if($etot) { $paref->{etot} = $etot; ___csmSpecificEpieces ($paref); @@ -4511,42 +4612,42 @@ sub __calcEnergyPieces { delete $data{$type}{$name}{consumers}{$c}{epiecEstart}; delete $data{$type}{$name}{consumers}{$c}{epiecHist}; delete $data{$type}{$name}{consumers}{$c}{epiecHour}; - + for my $h (1..$epiecHCounts) { delete $data{$type}{$name}{consumers}{$c}{"epiecHist_".$h}; delete $data{$type}{$name}{consumers}{$c}{"epiecHist_".$h."_hours"}; } - } + } delete $data{$type}{$name}{consumers}{$c}{epieces}; - + my $cotype = ConsumerVal ($hash, $c, "type", $defctype ); my $mintime = ConsumerVal ($hash, $c, "mintime", $defmintime); my $hours = ceil ($mintime / 60); # Laufzeit in h - + my $ctote = ConsumerVal ($hash, $c, "avgenergy", undef); # gemessener durchschnittlicher Energieverbrauch pro Stunde (Wh) - $ctote = $ctote ? - $ctote : + $ctote = $ctote ? + $ctote : ConsumerVal ($hash, $c, "power", 0); # alternativer nominaler Energieverbrauch in W (bzw. Wh bezogen auf 1 h) - - if (int($hef{$cotype}{f}) == 1) { # bei linearen Verbrauchertypen die nominale Leistungsangabe verwenden statt Durchschnitt + + if (int($hef{$cotype}{f}) == 1) { # bei linearen Verbrauchertypen die nominale Leistungsangabe verwenden statt Durchschnitt $ctote = ConsumerVal ($hash, $c, "power", 0); } - + my $epiecef = $ctote * $hef{$cotype}{f}; # Gewichtung erste Laufstunde my $epiecel = $ctote * $hef{$cotype}{l}; # Gewichtung letzte Laufstunde - + my $epiecem = $ctote * $hef{$cotype}{m}; - + for my $h (1..$hours) { my $he; - $he = $epiecef if($h == 1 ); # kalk. Energieverbrauch Startstunde - $he = $epiecem if($h > 1 && $h < $hours); # kalk. Energieverbrauch Folgestunde(n) - $he = $epiecel if($h == $hours ); # kalk. Energieverbrauch letzte Stunde - - $data{$type}{$name}{consumers}{$c}{epieces}{${h}} = sprintf('%.2f', $he); + $he = $epiecef if($h == 1 ); # kalk. Energieverbrauch Startstunde + $he = $epiecem if($h > 1 && $h < $hours); # kalk. Energieverbrauch Folgestunde(n) + $he = $epiecel if($h == $hours ); # kalk. Energieverbrauch letzte Stunde + + $data{$type}{$name}{consumers}{$c}{epieces}{${h}} = sprintf('%.2f', $he); } - + return; } @@ -4555,14 +4656,14 @@ return; # # epiecHCounts = x gibt an wie viele Zyklen betrachtet werden # sollen -# epiecHist => x ist der Index des Speicherbereichs der aktuell +# epiecHist => x ist der Index des Speicherbereichs der aktuell # benutzt wird. # # epiecHist_x => 1=x 2=x 3=x 4=x epieces eines Index -# epiecHist_x_hours => x Stunden des Durchlauf bzw. wie viele +# epiecHist_x_hours => x Stunden des Durchlauf bzw. wie viele # Einträge epiecHist_x hat -# epiecAVG => 1=x 2=x und epiecAVG_hours => x enthalten die -# durchschnittlichen Werte der in epiecHCounts +# epiecAVG => 1=x 2=x und epiecAVG_hours => x enthalten die +# durchschnittlichen Werte der in epiecHCounts # vorgegebenen Durchläufe. # ################################################################### @@ -4571,86 +4672,86 @@ sub ___csmSpecificEpieces { my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; - my $c = $paref->{consumer}; + my $c = $paref->{consumer}; my $etot = $paref->{etot}; my $t = $paref->{t}; - + if(ConsumerVal ($hash, $c, "onoff", "off") eq "on") { # Status "Aus" verzögern um Pausen im Waschprogramm zu überbrücken $data{$type}{$name}{consumers}{$c}{lastOnTime} = $t; } - - my $offTime = defined $data{$type}{$name}{consumers}{$c}{lastOnTime} ? + + my $offTime = defined $data{$type}{$name}{consumers}{$c}{lastOnTime} ? $t - $data{$type}{$name}{consumers}{$c}{lastOnTime} : 99; if($offTime < 300) { # erst nach 60s ist das Gerät aus my $epiecHist = ""; my $epiecHist_hours = ""; - + if(ConsumerVal ($hash, $c, "epiecHour", -1) < 0) { # neue Aufzeichnung $data{$type}{$name}{consumers}{$c}{epiecStartTime} = $t; - $data{$type}{$name}{consumers}{$c}{epiecHist} += 1; + $data{$type}{$name}{consumers}{$c}{epiecHist} += 1; $data{$type}{$name}{consumers}{$c}{epiecHist} = 1 if(ConsumerVal ($hash, $c, "epiecHist", 0) > $epiecHCounts); - - $epiecHist = "epiecHist_".ConsumerVal ($hash, $c, "epiecHist", 0); + + $epiecHist = "epiecHist_".ConsumerVal ($hash, $c, "epiecHist", 0); delete $data{$type}{$name}{consumers}{$c}{$epiecHist}; # Löschen, wird neu erfasst } - + $epiecHist = "epiecHist_".ConsumerVal ($hash, $c, "epiecHist", 0); # Namen fürs Speichern - $epiecHist_hours = "epiecHist_".ConsumerVal ($hash, $c, "epiecHist", 0)."_hours"; + $epiecHist_hours = "epiecHist_".ConsumerVal ($hash, $c, "epiecHist", 0)."_hours"; my $epiecHour = floor (($t - ConsumerVal ($hash, $c, "epiecStartTime", $t)) / 60 / 60) + 1; # aktuelle Betriebsstunde ermitteln, ( / 60min) mögliche wäre auch durch 15min /Minute /Stunde - - if(ConsumerVal ($hash, $c, "epiecHour", 0) != $epiecHour) { # Stundenwechsel? Differenz von etot noch auf die vorherige Stunde anrechnen + + if(ConsumerVal ($hash, $c, "epiecHour", 0) != $epiecHour) { # Stundenwechsel? Differenz von etot noch auf die vorherige Stunde anrechnen my $epiecHour_last = $epiecHour - 1; - + $data{$type}{$name}{consumers}{$c}{$epiecHist}{$epiecHour_last} = $etot - ConsumerVal ($hash, $c, "epiecEstart", 0) if($epiecHour > 1); - $data{$type}{$name}{consumers}{$c}{epiecEstart} = $etot; + $data{$type}{$name}{consumers}{$c}{epiecEstart} = $etot; } - + my $ediff = $etot - ConsumerVal ($hash, $c, "epiecEstart", 0); $data{$type}{$name}{consumers}{$c}{$epiecHist}{$epiecHour} = sprintf '%.2f', $ediff; $data{$type}{$name}{consumers}{$c}{epiecHour} = $epiecHour; $data{$type}{$name}{consumers}{$c}{$epiecHist_hours} = $ediff ? $epiecHour : $epiecHour - 1; # wenn mehr als 1 Wh verbraucht wird die Stunde gezählt - } + } else { # Durchschnitt ermitteln if(ConsumerVal ($hash, $c, "epiecHour", 0) > 0) { # Durchschnittliche Stunden ermitteln my $hours = 0; - + for my $h (1..$epiecHCounts) { # durchschnittliche Stunden über alle epieces ermitteln und aufrunden - $hours += ConsumerVal ($hash, $c, "epiecHist_".$h."_hours", 0); + $hours += ConsumerVal ($hash, $c, "epiecHist_".$h."_hours", 0); } - + $hours = ceil ($hours / $epiecHCounts); $data{$type}{$name}{consumers}{$c}{epiecAVG_hours} = $hours; - - delete $data{$type}{$name}{consumers}{$c}{epiecAVG}; # Durchschnitt für epics ermitteln - + + delete $data{$type}{$name}{consumers}{$c}{epiecAVG}; # Durchschnitt für epics ermitteln + for my $hour (1..$hours) { # jede Stunde durchlaufen my $hoursE = 1; - + for my $h (1..$epiecHCounts) { # jedes epiec durchlaufen my $epiecHist = "epiecHist_".$h; - + if(defined $data{$type}{$name}{consumers}{$c}{$epiecHist}{$hour}) { if($data{$type}{$name}{consumers}{$c}{$epiecHist}{$hour} > 5) { - $data{$type}{$name}{consumers}{$c}{epiecAVG}{$hour} += $data{$type}{$name}{consumers}{$c}{$epiecHist}{$hour}; + $data{$type}{$name}{consumers}{$c}{epiecAVG}{$hour} += $data{$type}{$name}{consumers}{$c}{$epiecHist}{$hour}; $hoursE += 1; } } - + } - - my $eavg = defined $data{$type}{$name}{consumers}{$c}{epiecAVG}{$hour} ? + + my $eavg = defined $data{$type}{$name}{consumers}{$c}{epiecAVG}{$hour} ? $data{$type}{$name}{consumers}{$c}{epiecAVG}{$hour} : 0; - + $data{$type}{$name}{consumers}{$c}{epiecAVG}{$hour} = sprintf('%.2f', $eavg / $hoursE); # Durchschnitt ermittelt und in epiecAVG schreiben } } - + $data{$type}{$name}{consumers}{$c}{epiecHour} = -1; # epiecHour auf initialwert setzen für nächsten durchlauf } - + return; } @@ -4668,88 +4769,88 @@ sub __planSwitchTimes { my $hash = $paref->{hash}; my $name = $paref->{name}; my $c = $paref->{consumer}; - + return if(ConsumerVal ($hash, $c, "planstate", undef)); # Verbraucher ist schon geplant/gestartet/fertig - + my $type = $paref->{type}; - my $debug = AttrVal ($name, "ctrlDebug", 0); - + my $debug = $paref->{debug}; + my $nh = $data{$type}{$name}{nexthours}; my $maxkey = (scalar keys %{$data{$type}{$name}{nexthours}}) - 1; my %max; my %mtimes; - + ## max. Überschuß ermitteln ############################# for my $idx (sort keys %{$nh}) { my $pvfc = NexthoursVal ($hash, $idx, "pvforecast", 0 ); my $confcex = NexthoursVal ($hash, $idx, "confcEx", 0 ); # prognostizierter Verbrauch ohne registrierte Consumer - - my $spexp = $pvfc-$confcex; # prognostizierter Energieüberschuß (kann negativ sein) - + + my $spexp = $pvfc-$confcex; # prognostizierter Energieüberschuß (kann negativ sein) + my ($hour) = $idx =~ /NextHour(\d+)/xs; $max{$spexp}{starttime} = NexthoursVal ($hash, $idx, "starttime", ""); $max{$spexp}{today} = NexthoursVal ($hash, $idx, "today", 0); $max{$spexp}{nexthour} = int ($hour); } - + my $order = 1; for my $k (reverse sort{$a<=>$b} keys %max) { $max{$order}{spexp} = $k; $max{$order}{starttime} = $max{$k}{starttime}; $max{$order}{nexthour} = $max{$k}{nexthour}; $max{$order}{today} = $max{$k}{today}; - - my $ts = timestringToTimestamp ($max{$k}{starttime}); + + my $ts = timestringToTimestamp ($max{$k}{starttime}); $mtimes{$ts}{spexp} = $k; $mtimes{$ts}{starttime} = $max{$k}{starttime}; $mtimes{$ts}{nexthour} = $max{$k}{nexthour}; $mtimes{$ts}{today} = $max{$k}{today}; - + delete $max{$k}; - + $order++; } - + my $epiece1 = (~0 >> 1); my $epieces = ConsumerVal ($hash, $c, "epieces", ""); - + if(ref $epieces eq "HASH") { $epiece1 = $data{$type}{$name}{consumers}{$c}{epieces}{1}; } else { return; } - - if($debug) { # nur für Debugging - Log (1, qq{DEBUG> $name consumer "$c" - epiece1: $epiece1}); + + if($debug =~ /consumerPlanning/x) { + Log (1, qq{$name DEBUG> consumer "$c" - epiece1: $epiece1}); } - + my $mode = ConsumerVal ($hash, $c, "mode", "can"); my $calias = ConsumerVal ($hash, $c, "alias", ""); my $mintime = ConsumerVal ($hash, $c, "mintime", $defmintime); my $stopdiff = ceil($mintime / 60) * 3600; - + $paref->{maxref} = \%max; $paref->{mintime} = $mintime; $paref->{stopdiff} = $stopdiff; - + if($mode eq "can") { # Verbraucher kann geplant werden - if($debug) { # nur für Debugging - Log (1, qq{DEBUG> $name consumer "$c" - mode: $mode, relevant hash: mtimes}); - for my $m (sort{$a<=>$b} keys %mtimes) { - Log (1, qq{DEBUG> $name consumer "$c" - hash: mtimes, surplus expected: $mtimes{$m}{spexp}, starttime: $mtimes{$m}{starttime}, nexthour: $mtimes{$m}{nexthour}, today: $mtimes{$m}{today}}); + if($debug =~ /consumerPlanning/x) { # nur für Debugging + Log (1, qq{$name DEBUG> consumer "$c" - mode: $mode, relevant hash: mtimes}); + for my $m (sort{$a<=>$b} keys %mtimes) { + Log (1, qq{$name DEBUG> consumer "$c" - hash: mtimes, surplus expected: $mtimes{$m}{spexp}, starttime: $mtimes{$m}{starttime}, nexthour: $mtimes{$m}{nexthour}, today: $mtimes{$m}{today}}); } } - + for my $ts (sort{$a<=>$b} keys %mtimes) { - if($mtimes{$ts}{spexp} >= $epiece1) { # die früheste Startzeit sofern Überschuß größer als Bedarf + if($mtimes{$ts}{spexp} >= $epiece1) { # die früheste Startzeit sofern Überschuß größer als Bedarf my $starttime = $mtimes{$ts}{starttime}; $paref->{starttime} = $starttime; $starttime = ___switchonTimelimits ($paref); - + delete $paref->{starttime}; - + my $startts = timestringToTimestamp ($starttime); # Unix Timestamp für geplanten Switch on $paref->{ps} = "planned:"; @@ -4757,34 +4858,35 @@ sub __planSwitchTimes { $paref->{stopts} = $startts + $stopdiff; ___setConsumerPlanningState ($paref); + ___saveEhodpieces ($paref); delete $paref->{ps}; delete $paref->{startts}; - delete $paref->{stopts}; - + delete $paref->{stopts}; + last; } else { $paref->{ps} = "no planning: the max expected surplus is less $epiece1"; ___setConsumerPlanningState ($paref); - + delete $paref->{ps}; } - } + } } else { # Verbraucher _muß_ geplant werden if($debug) { # nur für Debugging - Log (1, qq{DEBUG> $name consumer "$c" - mode: $mode, relevant hash: max}); - for my $o (sort{$a<=>$b} keys %max) { - Log (1, qq{DEBUG> $name consumer "$c" - hash: max, surplus: $max{$o}{spexp}, starttime: $max{$o}{starttime}, nexthour: $max{$o}{nexthour}, today: $max{$o}{today}}); + Log (1, qq{$name DEBUG> consumer "$c" - mode: $mode, relevant hash: max}); + for my $o (sort{$a<=>$b} keys %max) { + Log (1, qq{$name DEBUG> consumer "$c" - hash: max, surplus: $max{$o}{spexp}, starttime: $max{$o}{starttime}, nexthour: $max{$o}{nexthour}, today: $max{$o}{today}}); } } - + for my $o (sort{$a<=>$b} keys %max) { next if(!$max{$o}{today}); # der max-Wert ist _nicht_ heute $paref->{elem} = $o; - ___planMust ($paref); + ___planMust ($paref); last; } @@ -4792,19 +4894,94 @@ sub __planSwitchTimes { my $p = (sort{$a<=>$b} keys %max)[0]; $paref->{elem} = $p; ___planMust ($paref); - } + } } - + my $planstate = ConsumerVal ($hash, $c, "planstate", ""); - + if($planstate) { Log3 ($name, 3, qq{$name - Consumer "$calias" $planstate}); } - + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - + ___setPlanningDeleteMeth ($paref); + +return; +} + +################################################################ +# die geplanten EIN-Stunden des Tages mit den dazu gehörigen +# Consumer spezifischen epieces im Consumer-Hash speichern +################################################################ +sub ___saveEhodpieces { + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $type = $paref->{type}; + my $c = $paref->{consumer}; + my $startts = $paref->{startts}; # Unix Timestamp für geplanten Switch on + my $stopts = $paref->{stopts}; # Unix Timestamp für geplanten Switch off + + my $p = 1; + delete $data{$type}{$name}{consumers}{$c}{ehodpieces}; + + for (my $i = $startts; $i <= $stopts; $i+=3600) { + my $chod = (strftime "%H", localtime($i)) + 1; + my $epieces = ConsumerVal ($hash, $c, 'epieces', ''); + + my $ep = 0; + if(ref $epieces eq "HASH") { + $ep = defined $data{$type}{$name}{consumers}{$c}{epieces}{$p} ? + $data{$type}{$name}{consumers}{$c}{epieces}{$p} : + 0; + } + else { + last; + } + + $chod = sprintf '%02d', $chod; + $data{$type}{$name}{consumers}{$c}{ehodpieces}{$chod} = sprintf '%.2f', $ep if($ep); + + $p++; + } + +return; +} + +################################################################ +# Planungsdaten bzw. aktuelle Planungszustände setzen +################################################################ +sub ___setConsumerPlanningState { + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $type = $paref->{type}; + my $c = $paref->{consumer}; + my $ps = $paref->{ps}; # Planstatus + my $startts = $paref->{startts}; # Unix Timestamp für geplanten Switch on + my $stopts = $paref->{stopts}; # Unix Timestamp für geplanten Switch off + my $lang = $paref->{lang}; + my ($starttime,$stoptime); + + if ($startts) { + (undef,undef,undef,$starttime) = timestampToTimestring ($startts, $lang); + $data{$type}{$name}{consumers}{$c}{planswitchon} = $startts; + } + + if ($stopts) { + (undef,undef,undef,$stoptime) = timestampToTimestring ($stopts, $lang); + $data{$type}{$name}{consumers}{$c}{planswitchoff} = $stopts; + } + + $ps .= " " if ($starttime || $stoptime); + $ps .= $starttime if ($starttime); + $ps .= $stoptime if (!$starttime && $stoptime); + $ps .= " - ".$stoptime if ($starttime && $stoptime); + + $data{$type}{$name}{consumers}{$c}{planstate} = $ps; + return; } @@ -4821,24 +4998,26 @@ sub ___planMust { my $elem = $paref->{elem}; my $mintime = $paref->{mintime}; my $stopdiff = $paref->{stopdiff}; + my $lang = $paref->{lang}; my $maxts = timestringToTimestamp ($maxref->{$elem}{starttime}); # Unix Timestamp des max. Überschusses heute - my $half = ceil ($mintime / 2 / 60); # die halbe Gesamtlaufzeit in h als Vorlaufzeit einkalkulieren - my $startts = $maxts - ($half * 3600); - my (undef,undef,undef,$starttime) = timestampToTimestring ($startts); - + my $half = ceil ($mintime / 2 / 60); # die halbe Gesamtlaufzeit in h als Vorlaufzeit einkalkulieren + my $startts = $maxts - ($half * 3600); + my (undef,undef,undef,$starttime) = timestampToTimestring ($startts, $lang); + $paref->{starttime} = $starttime; $starttime = ___switchonTimelimits ($paref); delete $paref->{starttime}; - + $startts = timestringToTimestamp ($starttime); my $stopts = $startts + $stopdiff; - + $paref->{ps} = "planned:"; - $paref->{startts} = $startts; # Unix Timestamp für geplanten Switch on + $paref->{startts} = $startts; # Unix Timestamp für geplanten Switch on $paref->{stopts} = $stopts; # Unix Timestamp für geplanten Switch off ___setConsumerPlanningState ($paref); + ___saveEhodpieces ($paref); delete $paref->{ps}; delete $paref->{startts}; @@ -4847,44 +5026,8 @@ sub ___planMust { return; } - ################################################################ -# Planungsdaten bzw. aktuelle Planungszustände setzen -################################################################ -sub ___setConsumerPlanningState { - my $paref = shift; - my $hash = $paref->{hash}; - my $name = $paref->{name}; - my $type = $paref->{type}; - my $c = $paref->{consumer}; - my $ps = $paref->{ps}; # Planstatus - my $startts = $paref->{startts}; # Unix Timestamp für geplanten Switch on - my $stopts = $paref->{stopts}; # Unix Timestamp für geplanten Switch off - - my ($starttime,$stoptime); - - if ($startts) { - (undef,undef,undef,$starttime) = timestampToTimestring ($startts); - $data{$type}{$name}{consumers}{$c}{planswitchon} = $startts; - } - - if ($stopts) { - (undef,undef,undef,$stoptime) = timestampToTimestring ($stopts); - $data{$type}{$name}{consumers}{$c}{planswitchoff} = $stopts; - } - - $ps .= " " if ($starttime || $stoptime); - $ps .= $starttime if ($starttime); - $ps .= $stoptime if (!$starttime && $stoptime); - $ps .= " - ".$stoptime if ($starttime && $stoptime); - - $data{$type}{$name}{consumers}{$c}{planstate} = $ps; - -return; -} - -################################################################ -# Einschaltgrenzen berücksichtigen und Korrektur +# Einschaltgrenzen berücksichtigen und Korrektur # zurück liefern ################################################################ sub ___switchonTimelimits { @@ -4898,22 +5041,22 @@ sub ___switchonTimelimits { my $notbefore = ConsumerVal ($hash, $c, "notbefore", 0); my $notafter = ConsumerVal ($hash, $c, "notafter", 0); my ($starthour) = $starttime =~ /\s(\d{2}):/xs; - + my $change = q{}; - + if($notbefore && int $starthour < int $notbefore) { $starthour = $notbefore; $change = "notbefore"; } - + if($notafter && int $starthour > int $notafter) { $starthour = $notafter; $change = "notafter"; - } - + } + $starthour = sprintf("%02d", $starthour); - $starttime =~ s/\s(\d{2}):/ $starthour:/x; - + $starttime =~ s/\s(\d{2}):/ $starthour:/x; + if($change) { my $cname = ConsumerVal ($hash, $c, "name", ""); Log3 ($name, 3, qq{$name - Planned starttime "$cname" changed from "$origtime" to "$starttime" due to $change condition}); @@ -4934,19 +5077,19 @@ sub ___setPlanningDeleteMeth { my $sonkey = ConsumerVal ($hash, $c, "planswitchon", ""); my $soffkey = ConsumerVal ($hash, $c, "planswitchoff", ""); - + if($sonkey && $soffkey) { my $onday = strftime "%d", localtime($sonkey); my $offday = strftime "%d", localtime($soffkey); - + if ($offday ne $onday) { # Planungsdaten spezifische Löschmethode $data{$type}{$name}{consumers}{$c}{plandelete} = "specific"; } else { # Planungsdaten Löschmethode jeden Tag in Stunde 0 (_specialActivities) $data{$type}{$name}{consumers}{$c}{plandelete} = "regular"; - } + } } - + return; } @@ -4962,15 +5105,15 @@ sub __setTimeframeState { my $t = $paref->{t}; # aktueller Unixtimestamp my $startts = ConsumerVal ($hash, $c, "planswitchon", undef); # geplante Unix Startzeit - my $stopts = ConsumerVal ($hash, $c, "planswitchoff", undef); # geplante Unix Stopzeit - + my $stopts = ConsumerVal ($hash, $c, "planswitchoff", undef); # geplante Unix Stopzeit + if ($startts && $t >= $startts && $stopts && $t <= $stopts) { # ist Zeit innerhalb der Planzeit ein/aus ? $data{$type}{$name}{consumers}{$c}{isIntimeframe} = 1; - } + } else { $data{$type}{$name}{consumers}{$c}{isIntimeframe} = 0; } - + return; } @@ -4989,18 +5132,18 @@ sub __setConsRcmdState { my $nompower = ConsumerVal ($hash, $c, "power", 0); # Consumer nominale Leistungsaufnahme (W) my $ccr = AttrVal ($name, 'ctrlConsRecommendReadings', ''); # Liste der Consumer für die ConsumptionRecommended-Readings erstellt werden sollen my $rescons = isConsumerPhysOn($hash, $c) ? 0 : $nompower; # resultierender Verbauch nach Einschaltung Consumer - + if (!$nompower || $surplus - $rescons > 0) { $data{$type}{$name}{consumers}{$c}{isConsumptionRecommended} = 1; # Einschalten des Consumers günstig } else { $data{$type}{$name}{consumers}{$c}{isConsumptionRecommended} = 0; } - + if ($ccr =~ /$c/xs) { - push @$daref, "consumer${c}_ConsumptionRecommended<>". ConsumerVal ($hash, $c, 'isConsumptionRecommended', 0); + push @$daref, "consumer${c}_ConsumptionRecommended<>". ConsumerVal ($hash, $c, 'isConsumptionRecommended', 0); } - + return; } @@ -5015,13 +5158,13 @@ sub __switchConsumer { my $c = $paref->{consumer}; my $t = $paref->{t}; # aktueller Unixtimestamp my $state = $paref->{state}; - - $state = ___switchConsumerOn ($paref); # Verbraucher Einschaltbedingung prüfen + auslösen + + $state = ___switchConsumerOn ($paref); # Verbraucher Einschaltbedingung prüfen + auslösen $state = ___switchConsumerOff ($paref); # Verbraucher Ausschaltbedingung prüfen + auslösen - $state = ___setConsumerSwitchingState ($paref); # Consumer aktuelle Schaltzustände ermitteln & setzen - + $state = ___setConsumerSwitchingState ($paref); # Consumer aktuelle Schaltzustände ermitteln & setzen + $paref->{state} = $state; - + return; } @@ -5029,51 +5172,52 @@ return; # Verbraucher einschalten ################################################################ sub ___switchConsumerOn { - my $paref = shift; + my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $c = $paref->{consumer}; my $t = $paref->{t}; # aktueller Unixtimestamp my $state = $paref->{state}; - - my $debug = AttrVal ($name, "ctrlDebug", 0); + my $debug = $paref->{debug}; + my $pstate = ConsumerVal ($hash, $c, "planstate", ""); my $startts = ConsumerVal ($hash, $c, "planswitchon", undef); # geplante Unix Startzeit my $oncom = ConsumerVal ($hash, $c, "oncom", ""); # Set Command für "on" my $auto = ConsumerVal ($hash, $c, "auto", 1); my $cname = ConsumerVal ($hash, $c, "name", ""); # Consumer Device Name my $calias = ConsumerVal ($hash, $c, "alias", ""); # Consumer Device Alias - + my ($swoncond,$swoffcond,$info,$err); ($swoncond,$info,$err) = isAddSwitchOnCond ($hash, $c); # zusätzliche Switch on Bedingung Log3 ($name, 1, "$name - $err") if($err); - + ($swoffcond,$info,$err) = isAddSwitchOffCond ($hash, $c); # zusätzliche Switch off Bedingung Log3 ($name, 1, "$name - $err") if($err); - - if ($debug) { # nur für Debugging + + if ($debug =~ /consumerSwitching/x) { # nur für Debugging my $cons = CurrentVal ($hash, 'consumption', 0); my $nompow = ConsumerVal ($hash, $c, 'power', '-'); my $sp = CurrentVal ($hash, 'surplus', 0); - - Log (1, qq{DEBUG> $name consumer "$c" - general switching parameters => }. + + Log (1, qq{$name DEBUG> consumer "$c" - general switching parameters => }. qq{auto mode: $auto, current Consumption: $cons W, nompower: $nompow, surplus: $sp W, }. - qq{planning state: $pstate, start timestamp: }.($startts ? $startts : "undef").", ". - qq{timestamp: $t} - ); - Log (1, qq{DEBUG> $name consumer "$c" - current Context is switching "on" => }. - qq{swoncond: $swoncond, on-command: $oncom } - ); + qq{planning state: $pstate, start timestamp: }.($startts ? $startts : "undef") + ); + Log (1, qq{$name DEBUG> consumer "$c" - current Context is switching "on" => }. + qq{swoncond: $swoncond, on-command: $oncom } + ); } - + if ($auto && $oncom && $swoncond && !$swoffcond && # kein Einschalten wenn zusätzliche Switch off Bedingung zutrifft - simplifyCstate($pstate) =~ /planned|priority|starting/xs && + simplifyCstate($pstate) =~ /planned|priority|starting/xs && isInTimeframe ($hash, $c)) { # Verbraucher Start ist geplant && Startzeit überschritten my $mode = ConsumerVal ($hash, $c, "mode", $defcmode); # Consumer Planungsmode my $enable = ___enableSwitchByBatPrioCharge ($paref); # Vorrangladung Batterie ? - - Log3 ($name, 4, "$name - Consumer switch enabled by battery: $enable"); - + + if($debug =~ /consumerSwitching/x) { + Log (1, qq{$name DEBUG> Consumer switch enabled by battery: $enable}); + } + if ($mode eq "can" && !$enable) { # Batterieladung - keine Verbraucher "Einschalten" Freigabe $paref->{ps} = "priority charging battery"; @@ -5084,39 +5228,39 @@ sub ___switchConsumerOn { elsif ($mode eq "must" || isConsRcmd($hash, $c)) { # "Muss"-Planung oder Überschuß > Leistungsaufnahme CommandSet(undef,"$cname $oncom"); my $stopdiff = ceil(ConsumerVal ($hash, $c, "mintime", $defmintime) / 60) * 3600; - + $paref->{ps} = "switching on:"; ___setConsumerPlanningState ($paref); delete $paref->{ps}; - - $state = qq{switching Consumer "$calias" to "$oncom"}; - + + $state = qq{switching Consumer '$calias' to '$oncom'}; + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - + Log3 ($name, 2, "$name - $state (Automatic = $auto)"); } } elsif (((isInterruptable($hash, $c) == 1 && isConsRcmd ($hash, $c)) || # unterbrochenen Consumer fortsetzen - (isInterruptable($hash, $c) == 3 && isConsRcmd ($hash, $c))) && - isInTimeframe ($hash, $c) && + (isInterruptable($hash, $c) == 3 && isConsRcmd ($hash, $c))) && + isInTimeframe ($hash, $c) && simplifyCstate ($pstate) =~ /interrupted|interrupting/xs && $auto && $oncom) { - + CommandSet(undef,"$cname $oncom"); - + $paref->{ps} = "continuing:"; ___setConsumerPlanningState ($paref); delete $paref->{ps}; - + my $caution = isInterruptable($hash, $c) == 3 ? 'interrupt condition no longer present' : 'existing surplus'; - $state = qq{switching Consumer "$calias" to "$oncom", caution: $caution}; - - writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - + $state = qq{switching Consumer '$calias' to '$oncom', caution: $caution}; + + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben + Log3 ($name, 2, "$name - $state"); } @@ -5127,14 +5271,14 @@ return $state; # Verbraucher ausschalten ################################################################ sub ___switchConsumerOff { - my $paref = shift; + my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $c = $paref->{consumer}; my $t = $paref->{t}; # aktueller Unixtimestamp my $state = $paref->{state}; - - my $debug = AttrVal ($name, "ctrlDebug", 0); + my $debug = $paref->{debug}; + my $pstate = ConsumerVal ($hash, $c, "planstate", ""); my $stopts = ConsumerVal ($hash, $c, "planswitchoff", undef); # geplante Unix Stopzeit my $auto = ConsumerVal ($hash, $c, "auto", 1); @@ -5142,67 +5286,67 @@ sub ___switchConsumerOff { my $calias = ConsumerVal ($hash, $c, "alias", ""); # Consumer Device Alias my $mode = ConsumerVal ($hash, $c, "mode", $defcmode); # Consumer Planungsmode my $hyst = ConsumerVal ($hash, $c, "hysteresis", $defhyst); # Hysterese - + my $offcom = ConsumerVal ($hash, $c, "offcom", ""); # Set Command für "off" my ($swoffcond,$info,$err) = isAddSwitchOffCond ($hash, $c); # zusätzliche Switch off Bedingung my $caution; - + Log3 ($name, 1, "$name - $err") if($err); - - if($debug) { # nur für Debugging - Log (1, qq{DEBUG> $name consumer "$c" - current Context is switching "off" => }. - qq{swoffcond: $swoffcond, off-command: $offcom } + + if($debug =~ /consumerSwitching/x) { # nur für Debugging + Log (1, qq{$name DEBUG> consumer "$c" - current Context is switching "off" => }. + qq{swoffcond: $swoffcond, off-command: $offcom } ); } - - if(($swoffcond || ($stopts && $t >= $stopts)) && - ($auto && $offcom && simplifyCstate($pstate) =~ /started|starting|stopping|interrupt|continu/xs)) { + + if(($swoffcond || ($stopts && $t >= $stopts)) && + ($auto && $offcom && simplifyCstate($pstate) =~ /started|starting|stopping|interrupt|continu/xs)) { CommandSet(undef,"$cname $offcom"); - + $paref->{ps} = "switching off:"; ___setConsumerPlanningState ($paref); - delete $paref->{ps}; - + delete $paref->{ps}; + $caution = $swoffcond ? "switch-off condition (key swoffcond) is true" : "planned switch-off time reached/exceeded"; - $state = qq{switching Consumer "$calias" to "$offcom", caution: $caution}; - + $state = qq{switching Consumer '$calias' to '$offcom', caution: $caution}; + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - + Log3 ($name, 2, "$name - $state (Automatic = $auto)"); } - elsif (((isInterruptable($hash, $c, $hyst) && !isConsRcmd ($hash, $c)) || isInterruptable($hash, $c, $hyst) == 2) && # Consumer unterbrechen + elsif (((isInterruptable($hash, $c, $hyst) && !isConsRcmd ($hash, $c)) || isInterruptable($hash, $c, $hyst) == 2) && # Consumer unterbrechen isInTimeframe ($hash, $c) && simplifyCstate ($pstate) =~ /started|continued|interrupting/xs && $auto && $offcom) { - + CommandSet(undef,"$cname $offcom"); - + $paref->{ps} = "interrupting:"; ___setConsumerPlanningState ($paref); delete $paref->{ps}; - + $caution = isInterruptable($hash, $c, $hyst) == 2 ? 'interrupt condition' : 'surplus shortage'; - $state = qq{switching Consumer "$calias" to "$offcom", caution: $caution}; - + $state = qq{switching Consumer '$calias' to '$offcom', caution: $caution}; + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - + Log3 ($name, 2, "$name - $state"); } - + return $state; } ################################################################ -# Consumer aktuelle Schaltzustände ermitteln & setzen -# Consumer "on" setzen wenn physisch ein und alter Status +# Consumer aktuelle Schaltzustände ermitteln & setzen +# Consumer "on" setzen wenn physisch ein und alter Status # "starting" -# Consumer "off" setzen wenn physisch aus und alter Status +# Consumer "off" setzen wenn physisch aus und alter Status # "stopping" ################################################################ -sub ___setConsumerSwitchingState { +sub ___setConsumerSwitchingState { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; @@ -5210,14 +5354,14 @@ sub ___setConsumerSwitchingState { my $c = $paref->{consumer}; my $t = $paref->{t}; my $state = $paref->{state}; - + my $pstate = simplifyCstate (ConsumerVal ($hash, $c, "planstate", "")); my $calias = ConsumerVal ($hash, $c, "alias", ""); # Consumer Device Alias my $auto = ConsumerVal ($hash, $c, "auto", 1); - + if ($pstate eq 'starting' && isConsumerPhysOn ($hash, $c)) { my $stopdiff = ceil(ConsumerVal ($hash, $c, "mintime", $defmintime) / 60) * 3600; - + $paref->{ps} = "switched on:"; $paref->{startts} = $t; $paref->{stopts} = $t + $stopdiff; @@ -5227,11 +5371,11 @@ sub ___setConsumerSwitchingState { delete $paref->{ps}; delete $paref->{startts}; delete $paref->{stopts}; - - $state = qq{Consumer "$calias" switched on}; - + + $state = qq{Consumer '$calias' switched on}; + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - + Log3 ($name, 2, "$name - $state"); } elsif ($pstate eq 'stopping' && isConsumerPhysOff ($hash, $c)) { @@ -5241,41 +5385,41 @@ sub ___setConsumerSwitchingState { ___setConsumerPlanningState ($paref); delete $paref->{ps}; - delete $paref->{stopts}; - - $state = qq{Consumer "$calias" switched off}; - + delete $paref->{stopts}; + + $state = qq{Consumer '$calias' switched off}; + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - - Log3 ($name, 2, "$name - $state"); + + Log3 ($name, 2, "$name - $state"); } elsif ($pstate eq 'continuing' && isConsumerPhysOn ($hash, $c)) { $paref->{ps} = "continued:"; ___setConsumerPlanningState ($paref); - delete $paref->{ps}; - - $state = qq{Consumer "$calias" switched on (continued)}; - + delete $paref->{ps}; + + $state = qq{Consumer '$calias' switched on (continued)}; + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - - Log3 ($name, 2, "$name - $state"); + + Log3 ($name, 2, "$name - $state"); } elsif ($pstate eq 'interrupting' && isConsumerPhysOff ($hash, $c)) { $paref->{ps} = "interrupted:"; ___setConsumerPlanningState ($paref); - delete $paref->{ps}; - - $state = qq{Consumer "$calias" switched off (interrupted)}; - + delete $paref->{ps}; + + $state = qq{Consumer '$calias' switched off (interrupted)}; + writeDataToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben - - Log3 ($name, 2, "$name - $state"); + + Log3 ($name, 2, "$name - $state"); } - + return $state; } @@ -5289,22 +5433,22 @@ sub __remainConsumerTime { my $type = $paref->{type}; my $c = $paref->{consumer}; my $t = $paref->{t}; # aktueller Unixtimestamp - + my ($planstate,$startstr,$stoptstr) = __getPlanningStateAndTimes ($paref); - my $stopts = ConsumerVal ($hash, $c, "planswitchoff", undef); # geplante Unix Stopzeit - + my $stopts = ConsumerVal ($hash, $c, "planswitchoff", undef); # geplante Unix Stopzeit + $data{$type}{$name}{consumers}{$c}{remainTime} = 0; - - if (isInTimeframe($hash, $c) && (($planstate =~ /started/xs && isConsumerPhysOn($hash, $c)) | $planstate =~ /interrupt|continu/xs)) { + + if (isInTimeframe($hash, $c) && (($planstate =~ /started/xs && isConsumerPhysOn($hash, $c)) | $planstate =~ /interrupt|continu/xs)) { my $remainTime = $stopts - $t ; $data{$type}{$name}{consumers}{$c}{remainTime} = sprintf "%.0f", ($remainTime / 60) if($remainTime > 0); } - + return; } ################################################################ -# Freigabe Einschalten Verbraucher durch Batterie Vorrangladung +# Freigabe Einschalten Verbraucher durch Batterie Vorrangladung # return 0 -> keine Einschaltfreigabe Verbraucher # return 1 -> Einschaltfreigabe Verbraucher ################################################################ @@ -5313,13 +5457,13 @@ sub ___enableSwitchByBatPrioCharge { my $hash = $paref->{hash}; my $name = $paref->{name}; my $c = $paref->{consumer}; - + my $ena = 1; my $pcb = AttrVal ($name, 'affectBatteryPreferredCharge', 0); # Vorrangladung Batterie zu X% my ($badev) = useBattery ($name); - + return $ena if(!$pcb || !$badev); # Freigabe Schalten Consumer wenn kein Prefered Battery/Soll-Ladung 0 oder keine Batterie installiert - + my $cbcharge = CurrentVal ($hash, "batcharge", 0); # aktuelle Batterieladung $ena = 0 if($cbcharge < $pcb); # keine Freigabe wenn Batterieladung kleiner Soll-Ladung @@ -5333,100 +5477,101 @@ sub __getPlanningStateAndTimes { my $paref = shift; my $hash = $paref->{hash}; my $c = $paref->{consumer}; - - my $pstate = ConsumerVal ($hash, $c, "planstate", ""); + my $lang = $paref->{lang}; + + my $pstate = ConsumerVal ($hash, $c, "planstate", ""); $pstate = simplifyCstate ($pstate); - + my $startts = ConsumerVal ($hash, $c, "planswitchon", ""); my $stopts = ConsumerVal ($hash, $c, "planswitchoff", ""); - + my $starttime = ''; my $stoptime = ''; - $starttime = (timestampToTimestring ($startts))[0] if($startts); - $stoptime = (timestampToTimestring ($stopts))[0] if($stopts); - + $starttime = (timestampToTimestring ($startts, $lang))[0] if($startts); + $stoptime = (timestampToTimestring ($stopts, $lang))[0] if($stopts); + return ($pstate, $starttime, $stoptime); } ################################################################ # Batteriewerte sammeln ################################################################ -sub _transferBatteryValues { +sub _transferBatteryValues { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $chour = $paref->{chour}; my $day = $paref->{day}; - my $daref = $paref->{daref}; + my $daref = $paref->{daref}; my ($badev,$a,$h) = useBattery ($name); return if(!$badev); - + my $type = $paref->{type}; - + my ($pin,$piunit) = split ":", $h->{pin}; # Readingname/Unit für aktuelle Batterieladung my ($pou,$pounit) = split ":", $h->{pout}; # Readingname/Unit für aktuelle Batterieentladung my ($bin,$binunit) = split ":", $h->{intotal} // "-:-"; # Readingname/Unit der total in die Batterie eingespeisten Energie (Zähler) my ($bout,$boutunit) = split ":", $h->{outtotal} // "-:-"; # Readingname/Unit der total aus der Batterie entnommenen Energie (Zähler) my $batchr = $h->{charge} // ""; # Readingname Ladezustand Batterie - + return if(!$pin || !$pou); - + $pounit //= $piunit; $piunit //= $pounit; $boutunit //= $binunit; $binunit //= $boutunit; - + Log3 ($name, 5, "$name - collect Battery data: device=$badev, pin=$pin ($piunit), pout=$pou ($pounit), totalin: $bin ($binunit), totalout: $bout ($boutunit), charge: $batchr"); - + my $piuf = $piunit =~ /^kW$/xi ? 1000 : 1; my $pouf = $pounit =~ /^kW$/xi ? 1000 : 1; my $binuf = $binunit =~ /^kWh$/xi ? 1000 : 1; - my $boutuf = $boutunit =~ /^kWh$/xi ? 1000 : 1; - + my $boutuf = $boutunit =~ /^kWh$/xi ? 1000 : 1; + my $pbo = ReadingsNum ($badev, $pou, 0) * $pouf; # aktuelle Batterieentladung (W) my $pbi = ReadingsNum ($badev, $pin, 0) * $piuf; # aktueller Batterieladung (W) my $btotout = ReadingsNum ($badev, $bout, 0) * $boutuf; # totale Batterieentladung (Wh) my $btotin = ReadingsNum ($badev, $bin, 0) * $binuf; # totale Batterieladung (Wh) my $batcharge = ReadingsNum ($badev, $batchr, 0); - + my $params; - + if ($pin eq "-pout") { # Spezialfall pin bei neg. pout $params = { dev => $badev, rdg => $pou, rdgf => $pouf - }; - - ($pbo,$pbi) = substSpecialCases ($params); + }; + + ($pbo,$pbi) = substSpecialCases ($params); } - + if ($pou eq "-pin") { # Spezialfall pout bei neg. pin $params = { dev => $badev, rdg => $pin, rdgf => $piuf - }; - + }; + ($pbi,$pbo) = substSpecialCases ($params); } - my $nhour = $chour+1; + my $nhour = $chour+1; ###### my $histbatintot = HistoryVal ($hash, $day, sprintf("%02d",$nhour), "batintotal", undef); # totale Betterieladung zu Beginn einer Stunde - + my $batinthishour; - if(!defined $histbatintot) { # totale Betterieladung der aktuelle Stunde gesetzt ? + if(!defined $histbatintot) { # totale Betterieladung der aktuelle Stunde gesetzt ? $paref->{batintotal} = $btotin; $paref->{nhour} = sprintf("%02d",$nhour); $paref->{histname} = "batintotal"; setPVhistory ($paref); delete $paref->{histname}; - - my $bitot = CurrentVal ($hash, "batintotal", $btotin); + + my $bitot = CurrentVal ($hash, "batintotal", $btotin); $batinthishour = int ($btotin - $bitot); } else { @@ -5436,9 +5581,9 @@ sub _transferBatteryValues { if($batinthishour < 0) { $batinthishour = 0; } - + $data{$type}{$name}{circular}{sprintf("%02d",$nhour)}{batin} = $batinthishour; # Ringspeicher Battery In Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 - + $paref->{batinthishour} = $batinthishour; $paref->{nhour} = sprintf("%02d",$nhour); $paref->{histname} = "batinthishour"; @@ -5448,16 +5593,16 @@ sub _transferBatteryValues { ###### my $histbatouttot = HistoryVal ($hash, $day, sprintf("%02d",$nhour), "batouttotal", undef); # totale Betterieladung zu Beginn einer Stunde - + my $batoutthishour; - if(!defined $histbatouttot) { # totale Betterieladung der aktuelle Stunde gesetzt ? + if(!defined $histbatouttot) { # totale Betterieladung der aktuelle Stunde gesetzt ? $paref->{batouttotal} = $btotout; $paref->{nhour} = sprintf("%02d",$nhour); $paref->{histname} = "batouttotal"; setPVhistory ($paref); delete $paref->{histname}; - - my $botot = CurrentVal ($hash, "batouttotal", $btotout); + + my $botot = CurrentVal ($hash, "batouttotal", $btotout); $batoutthishour = int ($btotout - $botot); } else { @@ -5467,57 +5612,59 @@ sub _transferBatteryValues { if($batoutthishour < 0) { $batoutthishour = 0; } - + $data{$type}{$name}{circular}{sprintf("%02d",$nhour)}{batout} = $batoutthishour; # Ringspeicher Battery In Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 - + $paref->{batoutthishour} = $batoutthishour; $paref->{nhour} = sprintf("%02d",$nhour); $paref->{histname} = "batoutthishour"; setPVhistory ($paref); delete $paref->{histname}; - + ###### - + push @$daref, "Today_Hour".sprintf("%02d",$nhour)."_BatIn<>". $batinthishour. " Wh"; - push @$daref, "Today_Hour".sprintf("%02d",$nhour)."_BatOut<>".$batoutthishour." Wh"; + push @$daref, "Today_Hour".sprintf("%02d",$nhour)."_BatOut<>".$batoutthishour." Wh"; push @$daref, "Current_PowerBatIn<>". (int $pbi)." W"; push @$daref, "Current_PowerBatOut<>".(int $pbo)." W"; push @$daref, "Current_BatCharge<>". $batcharge." %"; - + $data{$type}{$name}{current}{powerbatin} = int $pbi; # Hilfshash Wert aktuelle Batterieladung $data{$type}{$name}{current}{powerbatout} = int $pbo; # Hilfshash Wert aktuelle Batterieentladung $data{$type}{$name}{current}{batintotal} = int $btotin; # totale Batterieladung $data{$type}{$name}{current}{batouttotal} = int $btotout; # totale Batterieentladung $data{$type}{$name}{current}{batcharge} = $batcharge; # aktuelle Batterieladung - + return; } ################################################################ # Energieverbrauch Vorhersage kalkulieren -# -# Es werden nur gleiche Wochentage (Mo ... So) -# zusammengefasst und der Durchschnitt ermittelt als +# +# Es werden nur gleiche Wochentage (Mo ... So) +# zusammengefasst und der Durchschnitt ermittelt als # Vorhersage ################################################################ sub _estConsumptionForecast { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; - my $chour = $paref->{chour}; + my $chour = $paref->{chour}; my $t = $paref->{t}; my $day = $paref->{day}; # aktuelles Tagdatum (01...31) my $dayname = $paref->{dayname}; # aktueller Tagname - + my $medev = ReadingsVal ($name, "currentMeterDev", ""); # aktuelles Meter device my $swdfcfc = AttrVal ($name, "affectConsForecastIdentWeekdays", 0); # nutze nur gleiche Wochentage (Mo...So) für Verbrauchsvorhersage my ($am,$hm) = parseParams ($medev); + $medev = $am->[0] // ""; return if(!$medev || !$defs{$medev}); - + my $type = $paref->{type}; + my $debug = $paref->{debug}; my $acref = $data{$type}{$name}{consumers}; - + ## Verbrauchsvorhersage für den nächsten Tag ############################################## my $tomorrow = strftime "%a", localtime($t+86400); # Wochentagsname kommender Tag @@ -5526,44 +5673,46 @@ sub _estConsumptionForecast { my $consumerco = 0; #my $min = (~0 >> 1); #my $max = -(~0 >> 1); - + for my $n (sort{$a<=>$b} keys %{$data{$type}{$name}{pvhist}}) { next if ($n eq $day); # aktuellen (unvollständigen) Tag nicht berücksichtigen - + if ($swdfcfc) { # nur gleiche Tage (Mo...So) einbeziehen my $hdn = HistoryVal ($hash, $n, 99, 'dayname', undef); next if(!$hdn || $hdn ne $tomorrow); } - + my $dcon = HistoryVal ($hash, $n, 99, "con", 0); next if(!$dcon); #for my $c (sort{$a<=>$b} keys %{$acref}) { # historischen Verbrauch aller registrierten Verbraucher aufaddieren # $consumerco += HistoryVal ($hash, $n, 99, "csme${c}", 0); #} - + #$dcon -= $consumerco if($dcon >= $consumerco); # Verbrauch registrierter Verbraucher aus Verbrauchsvorhersage eliminieren - + #$min = $dcon if($dcon < $min); #$max = $dcon if($dcon > $max); - + $totcon += $dcon; - $dnum++; + $dnum++; } - + if ($dnum) { #my $ddiff = ($max - $min)/$dnum; # Glättungsdifferenz #my $tomavg = int (($totcon/$dnum)-$ddiff); my $tomavg = int ($totcon / $dnum); $data{$type}{$name}{current}{tomorrowconsumption} = $tomavg; # prognostizierter Durchschnittsverbrauch aller (gleicher) Wochentage - - Log3 ($name, 4, "$name - estimated Consumption for tomorrow: $tomavg, days for avg: $dnum, hist. consumption registered consumers: ".sprintf "%.2f", $consumerco); + + if($debug =~ /consumption/x) { + Log (1, "$name DEBUG> estimated Consumption for tomorrow: $tomavg, days for avg: $dnum, hist. consumption registered consumers: ".sprintf "%.2f", $consumerco); + } } else { - my $lang = AttrVal ("global", 'language', 'EN'); + my $lang = $paref->{lang}; $data{$type}{$name}{current}{tomorrowconsumption} = $hqtxt{wfmdcf}{$lang}; } - + ## Verbrauchsvorhersage für die nächsten Stunden ################################################## my $conh = { "01" => 0, "02" => 0, "03" => 0, "04" => 0, @@ -5573,7 +5722,7 @@ sub _estConsumptionForecast { "17" => 0, "18" => 0, "19" => 0, "20" => 0, "21" => 0, "22" => 0, "23" => 0, "24" => 0, }; - + my $conhex = { "01" => 0, "02" => 0, "03" => 0, "04" => 0, "05" => 0, "06" => 0, "07" => 0, "08" => 0, "09" => 0, "10" => 0, "11" => 0, "12" => 0, @@ -5581,65 +5730,67 @@ sub _estConsumptionForecast { "17" => 0, "18" => 0, "19" => 0, "20" => 0, "21" => 0, "22" => 0, "23" => 0, "24" => 0, }; - + for my $k (sort keys %{$data{$type}{$name}{nexthours}}) { my $nhtime = NexthoursVal ($hash, $k, "starttime", undef); # Startzeit next if(!$nhtime); - + $dnum = 0; $consumerco = 0; #$min = (~0 >> 1); #$max = -(~0 >> 1); my $utime = timestringToTimestamp ($nhtime); my $nhday = strftime "%a", localtime($utime); # Wochentagsname des NextHours Key - my $nhhr = sprintf("%02d", (int (strftime "%H", localtime($utime))) + 1); # Stunde des Tages vom NextHours Key (01,02,...24) - + my $nhhr = sprintf("%02d", (int (strftime "%H", localtime($utime))) + 1); # Stunde des Tages vom NextHours Key (01,02,...24) + for my $m (sort{$a<=>$b} keys %{$data{$type}{$name}{pvhist}}) { next if($m eq $day); # next wenn gleicher Tag (Datum) wie heute - + if ($swdfcfc) { # nur gleiche Tage (Mo...So) einbeziehen my $hdn = HistoryVal ($hash, $m, 99, "dayname", undef); next if(!$hdn || $hdn ne $nhday); } - - my $hcon = HistoryVal ($hash, $m, $nhhr, "con", 0); + + my $hcon = HistoryVal ($hash, $m, $nhhr, "con", 0); next if(!$hcon); - + for my $c (sort{$a<=>$b} keys %{$acref}) { # historischer Verbrauch aller registrierten Verbraucher aufaddieren $consumerco += HistoryVal ($hash, $m, $nhhr, "csme${c}", 0); } - + #$hcon -= $consumerco if($hcon >= $consumerco); # Verbrauch registrierter Verbraucher aus Verbrauch eliminieren $conhex->{$nhhr} += $hcon - $consumerco if($hcon >= $consumerco); # prognostizierter Verbrauch Ex registrierter Verbraucher #$min = $hcon if($hcon < $min); #$max = $hcon if($hcon > $max); - - $conh->{$nhhr} += $hcon; + + $conh->{$nhhr} += $hcon; $dnum++; } - + if ($dnum) { #my $hdiff = ($max - $min)/$dnum; # Glättungsdifferenz #my $conavg = int(($conh->{$nhhr}/$dnum)-$hdiff); $data{$type}{$name}{nexthours}{$k}{confcEx} = int ($conhex->{$nhhr} / $dnum); - + my $conavg = int ($conh->{$nhhr} / $dnum); $data{$type}{$name}{nexthours}{$k}{confc} = $conavg; # Durchschnittsverbrauch aller gleicher Wochentage pro Stunde - + if (NexthoursVal ($hash, $k, "today", 0)) { # nur Werte des aktuellen Tag speichern - $data{$type}{$name}{circular}{sprintf("%02d",$nhhr)}{confc} = $conavg; - + $data{$type}{$name}{circular}{sprintf("%02d",$nhhr)}{confc} = $conavg; + $paref->{confc} = $conavg; $paref->{nhour} = sprintf("%02d",$nhhr); $paref->{histname} = "confc"; setPVhistory ($paref); - delete $paref->{histname}; - } - - Log3 ($name, 4, "$name - estimated Consumption for $nhday -> starttime: $nhtime, con: $conavg, days for avg: $dnum, hist. consumption registered consumers: ".sprintf "%.2f", $consumerco); - } + delete $paref->{histname}; + } + + if($debug =~ /consumption/x) { + Log (1, "$name DEBUG> estimated Consumption for $nhday -> starttime: $nhtime, con: $conavg, days for avg: $dnum, hist. consumption registered consumers: ".sprintf "%.2f", $consumerco); + } + } } - + return; } @@ -5651,42 +5802,42 @@ sub _evaluateThresholds { my $hash = $paref->{hash}; my $name = $paref->{name}; my $daref = $paref->{daref}; - + my $pt = ReadingsVal($name, "powerTrigger", ""); my $eh4t = ReadingsVal($name, "energyH4Trigger", ""); - - if ($pt) { - my $aaref = CurrentVal ($hash, "genslidereg", ""); + + if ($pt) { + my $aaref = CurrentVal ($hash, "genslidereg", ""); my @aa = (); @aa = @{$aaref} if (ref $aaref eq "ARRAY"); - - if (scalar @aa >= $defslidenum) { + + if (scalar @aa >= $defslidenum) { $paref->{taref} = \@aa; $paref->{tname} = "powerTrigger"; $paref->{tholds} = $pt; - + __evaluateArray ($paref); } } - - if ($eh4t) { - my $aaref = CurrentVal ($hash, "h4fcslidereg", ""); + + if ($eh4t) { + my $aaref = CurrentVal ($hash, "h4fcslidereg", ""); my @aa = (); @aa = @{$aaref} if (ref $aaref eq "ARRAY"); - - if (scalar @aa >= $defslidenum) { + + if (scalar @aa >= $defslidenum) { $paref->{taref} = \@aa; $paref->{tname} = "energyH4Trigger"; $paref->{tholds} = $eh4t; - + __evaluateArray ($paref); } } - + delete $paref->{taref}; delete $paref->{tname}; delete $paref->{tholds}; - + return; } @@ -5700,29 +5851,29 @@ sub __evaluateArray { my $taref = $paref->{taref}; # Referenz zum Threshold-Array my $tname = $paref->{tname}; # Thresholdname, z.B. powerTrigger my $tholds = $paref->{tholds}; # Triggervorgaben, z.B. aus Reading powerTrigger - + my $gen1 = @$taref[0]; my $gen2 = @$taref[1]; my $gen3 = @$taref[2]; - + my ($a,$h) = parseParams ($tholds); - + for my $key (keys %{$h}) { my ($knum,$cond) = $key =~ /^([0-9]+)(on|off)$/x; - + if($cond eq "on" && $gen1 > $h->{$key}) { next if($gen2 < $h->{$key}); - next if($gen3 < $h->{$key}); + next if($gen3 < $h->{$key}); push @$daref, "${tname}_${knum}<>on" if(ReadingsVal($name, "${tname}_${knum}", "off") eq "off"); } - + if($cond eq "off" && $gen1 < $h->{$key}) { next if($gen2 > $h->{$key}); next if($gen3 > $h->{$key}); push @$daref, "${tname}_${knum}<>off" if(ReadingsVal($name, "${tname}_${knum}", "on") eq "on"); } } - + return; } @@ -5730,39 +5881,39 @@ return; # zusätzliche Readings Tomorrow_HourXX_PVforecast # berechnen ################################################################ -sub _calcReadingsTomorrowPVFc { +sub _calcReadingsTomorrowPVFc { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $daref = $paref->{daref}; - + my $h = $data{$type}{$name}{nexthours}; my $hods = AttrVal($name, 'ctrlNextDayForecastReadings', ''); return if(!keys %{$h} || !$hods); - - for my $idx (sort keys %{$h}) { - my $today = NexthoursVal ($hash, $idx, 'today', 1); - next if($today); # aktueller Tag wird nicht benötigt - my $h = NexthoursVal ($hash, $idx, 'hourofday', ''); + for my $idx (sort keys %{$h}) { + my $today = NexthoursVal ($hash, $idx, 'today', 1); + next if($today); # aktueller Tag wird nicht benötigt + + my $h = NexthoursVal ($hash, $idx, 'hourofday', ''); next if(!$h); - next if($hods !~ /$h/xs); # diese Stunde des Tages soll nicht erzeugt werden - + next if($hods !~ /$h/xs); # diese Stunde des Tages soll nicht erzeugt werden + my $st = NexthoursVal ($hash, $idx, 'starttime', 'XXXX-XX-XX XX:XX:XX'); # Starttime my $pvfc = NexthoursVal ($hash, $idx, 'pvforecast', 0); - + push @$daref, "Tomorrow_Hour".$h."_PVforecast<>".$pvfc." Wh"; } - + return; } ################################################################ # Zusammenfassungen erstellen ################################################################ -sub _createSummaries { +sub _createSummaries { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; @@ -5770,9 +5921,9 @@ sub _createSummaries { my $daref = $paref->{daref}; my $chour = $paref->{chour}; # aktuelle Stunde my $minute = $paref->{minute}; # aktuelle Minute - + $minute = (int $minute) + 1; # Minute Range umsetzen auf 1 bis 60 - + ## Initialisierung #################### my $next1HoursSum = { "PV" => 0, "Consumption" => 0 }; @@ -5783,100 +5934,100 @@ sub _createSummaries { my $tomorrowSum = { "PV" => 0, "Consumption" => 0 }; my $todaySumFc = { "PV" => 0, "Consumption" => 0 }; my $todaySumRe = { "PV" => 0, "Consumption" => 0 }; - + my $rdh = 24 - $chour - 1; # verbleibende Anzahl Stunden am Tag beginnend mit 00 (abzüglich aktuelle Stunde) my $remainminutes = 60 - $minute; # verbleibende Minuten der aktuellen Stunde - + my $restofhourpvfc = (NexthoursVal($hash, "NextHour00", "pvforecast", 0)) / 60 * $remainminutes; my $restofhourconfc = (NexthoursVal($hash, "NextHour00", "confc", 0)) / 60 * $remainminutes; - + $next1HoursSum->{PV} = $restofhourpvfc; $next2HoursSum->{PV} = $restofhourpvfc; $next3HoursSum->{PV} = $restofhourpvfc; $next4HoursSum->{PV} = $restofhourpvfc; $restOfDaySum->{PV} = $restofhourpvfc; - + $next1HoursSum->{Consumption} = $restofhourconfc; $next2HoursSum->{Consumption} = $restofhourconfc; $next3HoursSum->{Consumption} = $restofhourconfc; $next4HoursSum->{Consumption} = $restofhourconfc; $restOfDaySum->{Consumption} = $restofhourconfc; - + for my $h (1..47) { my $pvfc = NexthoursVal ($hash, "NextHour".sprintf("%02d",$h), "pvforecast", 0); my $confc = NexthoursVal ($hash, "NextHour".sprintf("%02d",$h), "confc", 0); my $istdy = NexthoursVal ($hash, "NextHour".sprintf("%02d",$h), "today", 1); - + if($h == 1) { $next1HoursSum->{PV} += $pvfc / 60 * $minute; $next1HoursSum->{Consumption} += $confc / 60 * $minute; } - + if($h <= 2) { $next2HoursSum->{PV} += $pvfc if($h < 2); $next2HoursSum->{PV} += $pvfc / 60 * $minute if($h == 2); $next2HoursSum->{Consumption} += $confc if($h < 2); - $next2HoursSum->{Consumption} += $confc / 60 * $minute if($h == 2); + $next2HoursSum->{Consumption} += $confc / 60 * $minute if($h == 2); } - + if($h <= 3) { $next3HoursSum->{PV} += $pvfc if($h < 3); - $next3HoursSum->{PV} += $pvfc / 60 * $minute if($h == 3); + $next3HoursSum->{PV} += $pvfc / 60 * $minute if($h == 3); $next3HoursSum->{Consumption} += $confc if($h < 3); - $next3HoursSum->{Consumption} += $confc / 60 * $minute if($h == 3); - } + $next3HoursSum->{Consumption} += $confc / 60 * $minute if($h == 3); + } if($h <= 4) { $next4HoursSum->{PV} += $pvfc if($h < 4); - $next4HoursSum->{PV} += $pvfc / 60 * $minute if($h == 4); + $next4HoursSum->{PV} += $pvfc / 60 * $minute if($h == 4); $next4HoursSum->{Consumption} += $confc if($h < 4); $next4HoursSum->{Consumption} += $confc / 60 * $minute if($h == 4); - } - + } + $restOfDaySum->{PV} += $pvfc if($h <= $rdh); $restOfDaySum->{Consumption} += $confc if($h <= $rdh); - + $tomorrowSum->{PV} += $pvfc if(!$istdy); } - + for my $th (1..24) { $todaySumFc->{PV} += ReadingsNum($name, "Today_Hour".sprintf("%02d",$th)."_PVforecast", 0); $todaySumRe->{PV} += ReadingsNum($name, "Today_Hour".sprintf("%02d",$th)."_PVreal", 0); } - + push @{$data{$type}{$name}{current}{h4fcslidereg}}, int $next4HoursSum->{PV}; # Schieberegister 4h Summe Forecast limitArray ($data{$type}{$name}{current}{h4fcslidereg}, $defslidenum); - + my $gcon = CurrentVal ($hash, "gridconsumption", 0); # aktueller Netzbezug my $tconsum = CurrentVal ($hash, "tomorrowconsumption", undef); # Verbrauchsprognose für folgenden Tag my $pvgen = CurrentVal ($hash, "generation", 0); my $gfeedin = CurrentVal ($hash, "gridfeedin", 0); my $batin = CurrentVal ($hash, "powerbatin", 0); # aktuelle Batterieladung my $batout = CurrentVal ($hash, "powerbatout", 0); # aktuelle Batterieentladung - + my $consumption = int ($pvgen - $gfeedin + $gcon - $batin + $batout); my $selfconsumption = int ($pvgen - $gfeedin - $batin + $batout); $selfconsumption = $selfconsumption < 0 ? 0 : $selfconsumption; - + my $surplus = int ($pvgen - $consumption); # aktueller Überschuß $surplus = 0 if($surplus < 0); # wegen Vergleich nompower vs. surplus - + my $selfconsumptionrate = 0; my $autarkyrate = 0; $selfconsumptionrate = sprintf("%.0f", $selfconsumption / $pvgen * 100) if($pvgen * 1 > 0); $autarkyrate = sprintf("%.0f", $selfconsumption / ($selfconsumption + $gcon) * 100) if($selfconsumption); - + $data{$type}{$name}{current}{consumption} = $consumption; $data{$type}{$name}{current}{selfconsumption} = $selfconsumption; $data{$type}{$name}{current}{selfconsumptionrate} = $selfconsumptionrate; $data{$type}{$name}{current}{autarkyrate} = $autarkyrate; $data{$type}{$name}{current}{surplus} = $surplus; - + push @$daref, "Current_Consumption<>". $consumption. " W"; push @$daref, "Current_SelfConsumption<>". $selfconsumption. " W"; push @$daref, "Current_SelfConsumptionRate<>". $selfconsumptionrate. " %"; push @$daref, "Current_AutarkyRate<>". $autarkyrate. " %"; - + push @$daref, "NextHours_Sum01_PVforecast<>". (int $next1HoursSum->{PV})." Wh"; push @$daref, "NextHours_Sum02_PVforecast<>". (int $next2HoursSum->{PV})." Wh"; push @$daref, "NextHours_Sum03_PVforecast<>". (int $next3HoursSum->{PV})." Wh"; @@ -5885,11 +6036,11 @@ sub _createSummaries { push @$daref, "Tomorrow_PVforecast<>". (int $tomorrowSum->{PV}). " Wh"; push @$daref, "Today_PVforecast<>". (int $todaySumFc->{PV}). " Wh"; push @$daref, "Today_PVreal<>". (int $todaySumRe->{PV}). " Wh"; - + push @$daref, "Tomorrow_ConsumptionForecast<>". $tconsum. " Wh" if(defined $tconsum); push @$daref, "NextHours_Sum04_ConsumptionForecast<>". (int $next4HoursSum->{Consumption})." Wh"; push @$daref, "RestOfDayConsumptionForecast<>". (int $restOfDaySum->{Consumption}). " Wh"; - + return; } @@ -5905,23 +6056,23 @@ sub _calcTodayPVdeviation { my $t = $paref->{t}; my $date = $paref->{date}; my $daref = $paref->{daref}; - + my $sstime = timestringToTimestamp ($date.' '.ReadingsVal($name, "Today_SunSet", '22:00').':00'); - + return if($t < $sstime); - + my $pvfc = ReadingsNum ($name, 'Today_PVforecast', 0); my $pvre = ReadingsNum ($name, 'Today_PVreal', 0); - + my $diff = $pvfc - $pvre; - + if($pvre) { - my $dp = sprintf "%.2f" , (100 * $diff / $pvre); - $data{$type}{$name}{circular}{99}{tdayDvtn} = $dp; - + my $dp = sprintf "%.2f" , (100 * $diff / $pvre); + $data{$type}{$name}{circular}{99}{tdayDvtn} = $dp; + push @$daref, "Today_PVdeviation<>". $dp." %"; } - + return; } @@ -5929,30 +6080,30 @@ return; # Berechnen Forecast Tag / Stunden Verschieber # aus aktueller Stunde + lfd. Nummer ################################################################ -sub _calcDayHourMove { +sub _calcDayHourMove { my $chour = shift; my $num = shift; - my $fh = $chour + $num; + my $fh = $chour + $num; my $fd = int ($fh / 24) ; - $fh = $fh - ($fd * 24); - + $fh = $fh - ($fd * 24); + return ($fd,$fh); } ################################################################ -# Spezialfall auflösen wenn Wert von $val2 dem +# Spezialfall auflösen wenn Wert von $val2 dem # Redingwert von $val1 entspricht sofern $val1 negativ ist ################################################################ sub substSpecialCases { - my $paref = shift; + 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; @@ -5960,7 +6111,7 @@ sub substSpecialCases { else { $val2 = 0; } - + return ($val1,$val2); } @@ -5970,48 +6121,48 @@ return ($val1,$val2); sub saveEnergyConsumption { my $paref = shift; my $name = $paref->{name}; - my $chour = $paref->{chour}; - + my $chour = $paref->{chour}; + my $pvrl = ReadingsNum($name, "Today_Hour".sprintf("%02d",$chour+1)."_PVreal", 0); my $gfeedin = ReadingsNum($name, "Today_Hour".sprintf("%02d",$chour+1)."_GridFeedIn", 0); my $gcon = ReadingsNum($name, "Today_Hour".sprintf("%02d",$chour+1)."_GridConsumption", 0); my $batin = ReadingsNum($name, "Today_Hour".sprintf("%02d",$chour+1)."_BatIn", 0); my $batout = ReadingsNum($name, "Today_Hour".sprintf("%02d",$chour+1)."_BatOut", 0); - + my $con = $pvrl - $gfeedin + $gcon - $batin + $batout; - + $paref->{con} = $con; $paref->{nhour} = sprintf("%02d",$chour+1); $paref->{histname} = "con"; - setPVhistory ($paref); + setPVhistory ($paref); delete $paref->{histname}; - + return; } ################################################################ -# optionale Statistikreadings erstellen +# optionale Statistikreadings erstellen ################################################################ sub genStatisticReadings { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $daref = $paref->{daref}; - + my @csr = split ',', AttrVal($name, 'ctrlStatisticReadings', ''); - + return if(!@csr); - + for my $kpi (@csr) { if ($hcsr{$kpi}{fnr} == 1) { push @$daref, 'statistic_'.$kpi.'<>'. &{$hcsr{$kpi}{fn}} ($hash, '?All', '?All', $kpi, $hcsr{$kpi}{def}); } - + if ($hcsr{$kpi}{fnr} == 2) { push @$daref, 'statistic_'.$kpi.'<>'. &{$hcsr{$kpi}{fn}} ($hash, $kpi, $hcsr{$kpi}{def}); - } + } } - + return; } @@ -6023,48 +6174,48 @@ sub collectAllRegConsumers { my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; - + delete $data{$type}{$name}{current}{consumerdevs}; - + for my $c (1..$maxconsumer) { $c = sprintf "%02d", $c; my $consumer = AttrVal ($name, "consumer${c}", ""); next if(!$consumer); - + my ($ac,$hc) = parseParams ($consumer); $consumer = $ac->[0] // ""; - + if(!$consumer || !$defs{$consumer}) { my $err = qq{ERROR - the device "$consumer" doesn't exist anymore! Delete or change the attribute "consumer${c}".}; Log3 ($name, 1, "$name - $err"); - next; + next; } - + push @{$data{$type}{$name}{current}{consumerdevs}}, $consumer; # alle Consumerdevices in CurrentHash eintragen - + my $alias = AttrVal ($consumer, "alias", $consumer); - + my ($rtot,$utot,$ethreshold); if(exists $hc->{etotal}) { my $etotal = $hc->{etotal}; ($rtot,$utot,$ethreshold) = split ":", $etotal; } - + my ($rpcurr,$upcurr,$pthreshold); - if(exists $hc->{pcurr}) { + if(exists $hc->{pcurr}) { my $pcurr = $hc->{pcurr}; ($rpcurr,$upcurr,$pthreshold) = split ":", $pcurr; } - + my ($rswstate,$onreg,$offreg); if(exists $hc->{swstate}) { ($rswstate,$onreg,$offreg) = split ":", $hc->{swstate}; } - + my ($dswoncond,$rswoncond,$swoncondregex); if(exists $hc->{swoncond}) { # zusätzliche Einschaltbedingung ($dswoncond,$rswoncond,$swoncondregex) = split ":", $hc->{swoncond}; - } + } my ($dswoffcond,$rswoffcond,$swoffcondregex); if(exists $hc->{swoffcond}) { # vorrangige Ausschaltbedingung @@ -6076,8 +6227,8 @@ sub collectAllRegConsumers { if(exists $hc->{interruptable} && $hc->{interruptable} ne '0') { $interruptable = $hc->{interruptable}; ($interruptable,$hyst) = $interruptable =~ /(.*):(.*)$/xs if($interruptable ne '1'); - } - + } + my $rauto = $hc->{auto} // q{}; my $ctype = $hc->{type} // $defctype; my $auto = 1; @@ -6091,16 +6242,16 @@ sub collectAllRegConsumers { $data{$type}{$name}{consumers}{$c}{mintime} = $hc->{mintime} // $hef{$ctype}{mt}; # Initialwert min. Einschalt- bzw. Zykluszeit (evtl. Überschreiben in manageConsumerData) $data{$type}{$name}{consumers}{$c}{mode} = $hc->{mode} // $defcmode; # Planungsmode des Verbrauchers $data{$type}{$name}{consumers}{$c}{icon} = $hc->{icon} // q{}; # Icon für den Verbraucher - $data{$type}{$name}{consumers}{$c}{oncom} = $hc->{on} // q{}; # Setter Einschaltkommando + $data{$type}{$name}{consumers}{$c}{oncom} = $hc->{on} // q{}; # Setter Einschaltkommando $data{$type}{$name}{consumers}{$c}{offcom} = $hc->{off} // q{}; # Setter Ausschaltkommando $data{$type}{$name}{consumers}{$c}{autoreading} = $rauto; # Readingname zur Automatiksteuerung - $data{$type}{$name}{consumers}{$c}{auto} = $auto; # Automaticsteuerung: 1 - Automatic ein, 0 - Automatic aus + $data{$type}{$name}{consumers}{$c}{auto} = $auto; # Automaticsteuerung: 1 - Automatic ein, 0 - Automatic aus $data{$type}{$name}{consumers}{$c}{retotal} = $rtot // q{}; # Reading der Leistungsmessung $data{$type}{$name}{consumers}{$c}{uetotal} = $utot // q{}; # Unit der Leistungsmessung - $data{$type}{$name}{consumers}{$c}{energythreshold} = $ethreshold // 0; # Schwellenwert (Wh pro Stunde) ab der ein Verbraucher als aktiv gewertet wird + $data{$type}{$name}{consumers}{$c}{energythreshold} = $ethreshold // 0; # Schwellenwert (Wh pro Stunde) ab der ein Verbraucher als aktiv gewertet wird $data{$type}{$name}{consumers}{$c}{rpcurr} = $rpcurr // q{}; # Reading der aktuellen Leistungsaufnahme $data{$type}{$name}{consumers}{$c}{upcurr} = $upcurr // q{}; # Unit der aktuellen Leistungsaufnahme - $data{$type}{$name}{consumers}{$c}{powerthreshold} = $pthreshold // 0; # Schwellenwert d. aktuellen Leistung(W) ab der ein Verbraucher als aktiv gewertet wird + $data{$type}{$name}{consumers}{$c}{powerthreshold} = $pthreshold // 0; # Schwellenwert d. aktuellen Leistung(W) ab der ein Verbraucher als aktiv gewertet wird $data{$type}{$name}{consumers}{$c}{notbefore} = $hc->{notbefore} // q{}; # nicht einschalten vor Stunde in 24h Format (00-23) $data{$type}{$name}{consumers}{$c}{notafter} = $hc->{notafter} // q{}; # nicht einschalten nach Stunde in 24h Format (00-23) $data{$type}{$name}{consumers}{$c}{rswstate} = $rswstate // 'state'; # Schaltstatus Reading @@ -6115,9 +6266,9 @@ sub collectAllRegConsumers { $data{$type}{$name}{consumers}{$c}{interruptable} = $interruptable; # Ein-Zustand des Verbrauchers ist unterbrechbar $data{$type}{$name}{consumers}{$c}{hysteresis} = $hyst // $defhyst; # Hysterese } - + # Log3 ($name, 5, "$name - all registered consumers:\n".Dumper $data{$type}{$name}{consumers}); - + return; } @@ -6127,17 +6278,17 @@ return; sub FwFn { my ($FW_wname, $name, $room, $pageHash) = @_; # pageHash is set for summaryFn. my $hash = $defs{$name}; - + RemoveInternalTimer($hash, \&pageRefresh); $hash->{HELPER}{FW} = $FW_wname; - + my $ret = ""; $ret .= entryGraphic ($name); $ret .= ""; - + # Autorefresh nur des aufrufenden FHEMWEB-Devices my $al = AttrVal($name, "ctrlAutoRefresh", 0); - if($al) { + if($al) { InternalTimer(gettimeofday()+$al, \&pageRefresh, $hash, 0); Log3 ($name, 5, "$name - next start of autoRefresh: ".FmtDateTime(gettimeofday()+$al)); } @@ -6146,55 +6297,55 @@ return $ret; } ################################################################ -sub pageRefresh { +sub pageRefresh { my $hash = shift; my $name = $hash->{NAME}; - + # Seitenrefresh festgelegt durch SolarForecast-Attribut "ctrlAutoRefresh" und "ctrlAutoRefreshFW" my $rd = AttrVal($name, "ctrlAutoRefreshFW", $hash->{HELPER}{FW}); { map { FW_directNotify("#FHEMWEB:$_", "location.reload('true')", "") } $rd } ## no critic 'Map blocks' - + my $al = AttrVal($name, "ctrlAutoRefresh", 0); - - if($al) { + + if($al) { InternalTimer(gettimeofday()+$al, \&pageRefresh, $hash, 0); Log3 ($name, 5, "$name - next start of autoRefresh: ".FmtDateTime(gettimeofday()+$al)); - } + } else { RemoveInternalTimer($hash, \&pageRefresh); } - + return; } ################################################################ # Grafik als HTML zurück liefern (z.B. für Widget) ################################################################ -sub pageAsHtml { +sub pageAsHtml { my $name = shift; my $ftui = shift; - + my $ret = ""; $ret .= entryGraphic ($name, $ftui); $ret .= ""; - + return $ret; } ################################################################ # Einstieg Grafikanzeige ################################################################ -sub entryGraphic { +sub entryGraphic { my $name = shift; my $ftui = shift // ""; - + my $hash = $defs{$name}; - + # Setup Vollständigkeit/disabled prüfen ######################################### my $incomplete = _checkSetupNotComplete ($hash); return $incomplete if($incomplete); - + # Kontext des SolarForecast-Devices speichern für Refresh ########################################################## $hash->{HELPER}{SPGDEV} = $name; # Name des aufrufenden SolarForecastSPG-Devices @@ -6202,18 +6353,18 @@ sub entryGraphic { $hash->{HELPER}{SPGDETAIL} = $FW_detail ? $FW_detail : ""; # Name des SolarForecastSPG-Devices (wenn Detailansicht) # Parameter f. Anzeige extrahieren - ################################### - my $width = AttrNum ($name, 'graphicBeamWidth', 20); # zu klein ist nicht problematisch + ################################### + my $width = AttrNum ($name, 'graphicBeamWidth', 20); # zu klein ist nicht problematisch my $maxhours = AttrNum ($name, 'graphicHourCount', 24); my $alias = AttrVal ($name, 'alias', $name); # Linktext als Aliasname oder Devicename setzen - my $gsel = AttrVal ($name, 'graphicSelect', 'both'); # Auswahl der anzuzeigenden Grafiken + my $gsel = AttrVal ($name, 'graphicSelect', 'both'); # Auswahl der anzuzeigenden Grafiken my $html_start = AttrVal ($name, 'graphicStartHtml', undef); # beliebige HTML Strings die vor der Grafik ausgegeben werden my $html_end = AttrVal ($name, 'graphicEndHtml', undef); # beliebige HTML Strings die nach der Grafik ausgegeben werden my $w = $width * $maxhours; # gesammte Breite der Ausgabe , WetterIcon braucht ca. 34px my $offset = -1 * AttrNum ($name, 'graphicHistoryHour', $histhourdef); - + my $dlink = qq{$alias}; - + my $paref = { hash => $hash, name => $name, @@ -6221,17 +6372,19 @@ sub entryGraphic { ftui => $ftui, maxhours => $maxhours, modulo => 1, - dstyle => qq{style='padding-left: 10px; padding-right: 10px; padding-top: 3px; padding-bottom: 3px;'}, # TD-Style + dstyle => qq{style='padding-left: 10px; padding-right: 10px; padding-top: 3px; padding-bottom: 3px; white-space:nowrap;'}, # TD-Style offset => $offset, hourstyle => AttrVal ($name, 'graphicHourStyle', ''), colorb1 => AttrVal ($name, 'graphicBeam1Color', $b1coldef), colorb2 => AttrVal ($name, 'graphicBeam2Color', $b2coldef), fcolor1 => AttrVal ($name, 'graphicBeam1FontColor', $b1fontcoldef), - fcolor2 => AttrVal ($name, 'graphicBeam2FontColor', $b2fontcoldef), + fcolor2 => AttrVal ($name, 'graphicBeam2FontColor', $b2fontcoldef), beam1cont => AttrVal ($name, 'graphicBeam1Content', 'pvReal'), - beam2cont => AttrVal ($name, 'graphicBeam2Content', 'pvForecast'), + beam2cont => AttrVal ($name, 'graphicBeam2Content', 'pvForecast'), caicon => AttrVal ($name, 'consumerAdviceIcon', $caicondef), # Consumer AdviceIcon clegend => AttrVal ($name, 'consumerLegend', 'icon_top'), # Lage und Art Cunsumer Legende + clink => AttrVal ($name, 'consumerLink' , 1), # Detail-Link zum Verbraucher + debug => AttrVal ($name, 'ctrlDebug', 'none'), # Debug Module lotype => AttrVal ($name, 'graphicLayoutType', 'double'), kw => AttrVal ($name, 'graphicEnergyUnit', 'Wh'), height => AttrNum ($name, 'graphicBeamHeight', 200), @@ -6246,100 +6399,100 @@ sub entryGraphic { wlalias => AttrVal ($name, 'alias', $name), sheader => AttrNum ($name, 'graphicHeaderShow', 1), # Anzeigen des Grafik Headers hdrDetail => AttrVal ($name, 'graphicHeaderDetail', 'all'), # ermöglicht den Inhalt zu begrenzen, um bspw. passgenau in ftui einzubetten - lang => AttrVal ("global", 'language', 'EN'), flowgsize => AttrVal ($name, 'flowGraphicSize', $flowGSizedef), # Größe Energieflußgrafik flowgani => AttrVal ($name, 'flowGraphicAnimate', 0), # Animation Energieflußgrafik flowgcons => AttrVal ($name, 'flowGraphicShowConsumer', 1), # Verbraucher in der Energieflußgrafik anzeigen - flowgconX => AttrVal ($name, 'flowGraphicShowConsumerDummy', 1), # Dummyverbraucher in der Energieflußgrafik anzeigen + flowgconX => AttrVal ($name, 'flowGraphicShowConsumerDummy', 1), # Dummyverbraucher in der Energieflußgrafik anzeigen flowgconsPower => AttrVal ($name, 'flowGraphicShowConsumerPower' , 1), # Verbraucher Leistung in der Energieflußgrafik anzeigen - flowgconsTime => AttrVal ($name, 'flowGraphicShowConsumerRemainTime', 1), # Verbraucher Restlaufeit in der Energieflußgrafik anzeigen + flowgconsTime => AttrVal ($name, 'flowGraphicShowConsumerRemainTime', 1), # Verbraucher Restlaufeit in der Energieflußgrafik anzeigen flowgconsDist => AttrVal ($name, 'flowGraphicConsumerDistance', $fgCDdef), # Abstand Verbrauchericons zueinander css => AttrVal ($name, 'flowGraphicCss', $cssdef), # flowGraphicCss Styles + lang => AttrVal ($name, 'ctrlLanguage', AttrVal ('global', 'language', $deflang)), }; - + my $ret = q{}; - + $ret .= "$dlink
" if(AttrVal($name, 'ctrlShowLink', 0)); - + $ret .= $html_start if (defined($html_start)); - $ret .= ""; + #$ret .= ""; + $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "
"; - - # Headerzeile generieren - ########################## + + # Headerzeile generieren + ########################## my $header = _graphicHeader ($paref); - + # Verbraucherlegende und Steuerung - ################################### + ################################### my $legendtxt = _graphicConsumerLegend ($paref); - + $ret .= "\n"; # das \n erleichtert das Lesen der debug Quelltextausgabe my $m = $paref->{modulo} % 2; - - if ($header) { # Header ausgeben - $ret .= ""; + + if ($header) { # Header ausgeben + $ret .= ""; $ret .= ""; $ret .= ""; - + $paref->{modulo}++; } - + my $clegend = $paref->{clegend}; $m = $paref->{modulo} % 2; - + if ($legendtxt && ($clegend eq 'top')) { $ret .= ""; - #$ret .= ""; $ret .= ""; - + $paref->{modulo}++; } - + $m = $paref->{modulo} % 2; - + if($gsel eq "both" || $gsel eq "forecast") { my %hfch; my $hfcg = \%hfch; #(hfcg = hash forecast graphic) - + # Werte aktuelle Stunde - ########################## + ########################## $paref->{hfcg} = $hfcg; $paref->{thishour} = _beamGraphicFirstHour ($paref); # get consumer list and display it in Graphics - ################################################ + ################################################ _showConsumerInGraphicBeam ($paref); # Werte restliche Stunden ########################### my $back = _beamGraphicRemainingHours ($paref); $paref->{maxVal} = $back->{maxVal}; # Startwert wenn kein Wert bereits via attr vorgegeben ist - $paref->{maxCon} = $back->{maxCon}; + $paref->{maxCon} = $back->{maxCon}; $paref->{maxDif} = $back->{maxDif}; # für Typ diff $paref->{minDif} = $back->{minDif}; # für Typ diff - + # Balkengrafik ################ - $ret .= _beamGraphic ($paref); + $ret .= _beamGraphic ($paref); } - + $m = $paref->{modulo} % 2; - + if($gsel eq "both" || $gsel eq "flow") { $ret .= ""; my $fg = _flowGraphic ($paref); $ret .= ""; $ret .= ""; - + $paref->{modulo}++; } - + $m = $paref->{modulo} % 2; - + # Legende unten ################# if ($legendtxt && ($clegend eq 'bottom')) { @@ -6349,117 +6502,117 @@ sub entryGraphic { $ret .= "$legendtxt"; $ret .= ""; } - + $ret .= "
$header
"; $ret .= ""; $ret .= "$legendtxt
"; $ret .= "$fg
"; - + $ret .= "
"; $ret .= $html_end if (defined($html_end)); - + return $ret; } ################################################################ # Vollständigkeit Setup prüfen ################################################################ -sub _checkSetupNotComplete { +sub _checkSetupNotComplete { my $hash = shift; my $ret = q{}; - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + my $is = ReadingsVal ($name, "inverterStrings", undef); # String Konfig my $fcdev = ReadingsVal ($name, "currentForecastDev", undef); # Device Vorhersage Wetterdaten (Bewölkung etc.) my $radev = ReadingsVal ($name, "currentRadiationDev", undef); # Device Strahlungsdaten Vorhersage my $indev = ReadingsVal ($name, "currentInverterDev", undef); # Inverter Device my $medev = ReadingsVal ($name, "currentMeterDev", undef); # Meter Device - + my $peaks = ReadingsVal ($name, "modulePeakString", undef); # String Peak my $dir = ReadingsVal ($name, "moduleDirection", undef); # Modulausrichtung Konfig my $ta = ReadingsVal ($name, "moduleTiltAngle", undef); # Modul Neigungswinkel Konfig my $mrt = ReadingsVal ($name, "moduleRoofTops", undef); # RoofTop Konfiguration (SolCast API) my $rip = 1 if(exists $data{$type}{$name}{solcastapi}{'?IdPair'}); # es existiert mindestens ein Paar RoofTop-ID / API-Key - + my $pv0 = NexthoursVal ($hash, "NextHour00", "pvforecast", undef); # der erste PV ForeCast Wert - - my $link = qq{$name}; - my $height = AttrNum ($name, 'graphicBeamHeight', 200); - my $lang = AttrVal ("global", 'language', 'EN'); - - if(IsDisabled($name)) { + + my $link = qq{$name}; + my $height = AttrNum ($name, 'graphicBeamHeight', 200); + my $lang = AttrVal ($name, 'ctrlLanguage', AttrVal ('global', 'language', $deflang)); + + if(IsDisabled($name)) { $ret .= ""; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "
"; - $ret .= qq{SolarForecast device $link is disabled}; + $ret .= qq{SolarForecast device $link is disabled}; $ret .= "
"; - + return $ret; - } - + } + if(!$is || !$fcdev || !$radev || !$indev || !$medev || !$peaks || - (isSolCastUsed ($hash) ? (!$rip || !$mrt) : (!$dir || !$ta )) || - !defined $pv0) { + (isSolCastUsed ($hash) ? (!$rip || !$mrt) : (!$dir || !$ta )) || + !defined $pv0) { $ret .= ""; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "
"; - + if(!$fcdev) { ## no critic 'Cascading' $ret .= $hqtxt{cfd}{$lang}; } elsif(!$radev) { - $ret .= $hqtxt{crd}{$lang}; + $ret .= $hqtxt{crd}{$lang}; } elsif(!$indev) { - $ret .= $hqtxt{cid}{$lang}; + $ret .= $hqtxt{cid}{$lang}; } elsif(!$medev) { - $ret .= $hqtxt{mid}{$lang}; + $ret .= $hqtxt{mid}{$lang}; } elsif(!$is) { - $ret .= $hqtxt{ist}{$lang}; + $ret .= $hqtxt{ist}{$lang}; } - elsif(!$peaks) { - $ret .= $hqtxt{mps}{$lang}; + elsif(!$peaks) { + $ret .= $hqtxt{mps}{$lang}; } elsif(!$rip && isSolCastUsed ($hash)) { # Verwendung SolCast API - $ret .= $hqtxt{rip}{$lang}; - } + $ret .= $hqtxt{rip}{$lang}; + } elsif(!$mrt && isSolCastUsed ($hash)) { # Verwendung SolCast API - $ret .= $hqtxt{mrt}{$lang}; + $ret .= $hqtxt{mrt}{$lang}; } elsif(!$dir && !isSolCastUsed ($hash)) { # Verwendung DWD Strahlungsdevice $ret .= $hqtxt{mdr}{$lang}; } elsif(!$ta && !isSolCastUsed ($hash)) { # Verwendung DWD Strahlungsdevice - $ret .= $hqtxt{mta}{$lang}; + $ret .= $hqtxt{mta}{$lang}; } elsif(!defined $pv0) { - $ret .= $hqtxt{awd}{$lang}; + $ret .= $hqtxt{awd}{$lang}; } - + $ret .= "
"; $ret =~ s/LINK/$link/gxs; - + return $ret; } - + return; } ################################################################ -# forecastGraphic Headerzeile generieren +# forecastGraphic Headerzeile generieren ################################################################ -sub _graphicHeader { +sub _graphicHeader { my $paref = shift; my $sheader = $paref->{sheader}; - + return if(!$sheader); - + my $hdrDetail = $paref->{hdrDetail}; # ermöglicht den Inhalt zu begrenzen, um bspw. passgenau in ftui einzubetten my $ftui = $paref->{ftui}; my $lang = $paref->{lang}; @@ -6467,19 +6620,19 @@ sub _graphicHeader { my $hash = $paref->{hash}; my $kw = $paref->{kw}; my $dstyle = $paref->{dstyle}; # TD-Style - + my $lup = ReadingsTimestamp ($name, ".lastupdateForecastValues", "0000-00-00 00:00:00"); # letzter Forecast Update - + my $pcfa = ReadingsVal ($name,"pvCorrectionFactor_Auto", "off"); my $co4h = ReadingsNum ($name,"NextHours_Sum04_ConsumptionForecast", 0); - my $coRe = ReadingsNum ($name,"RestOfDayConsumptionForecast", 0); + my $coRe = ReadingsNum ($name,"RestOfDayConsumptionForecast", 0); my $coTo = ReadingsNum ($name,"Tomorrow_ConsumptionForecast", 0); my $coCu = ReadingsNum ($name,"Current_Consumption", 0); my $pv4h = ReadingsNum ($name,"NextHours_Sum04_PVforecast", 0); - my $pvRe = ReadingsNum ($name,"RestOfDayPVforecast", 0); + my $pvRe = ReadingsNum ($name,"RestOfDayPVforecast", 0); my $pvTo = ReadingsNum ($name,"Tomorrow_PVforecast", 0); my $pvCu = ReadingsNum ($name,"Current_PV", 0); - + my $pvcorrf00 = NexthoursVal($hash, "NextHour00", "pvcorrf", "-/m"); my ($pcf,$pcq) = split "/", $pvcorrf00; my $pvcanz = qq{factor: $pcf / quality: $pcq}; # Test @@ -6495,7 +6648,7 @@ sub _graphicHeader { $pvRe = sprintf("%.1f" , $pvRe/1000)." kWh"; $pvTo = sprintf("%.1f" , $pvTo/1000)." kWh"; $pvCu = sprintf("%.1f" , $pvCu/1000)." kW"; - } + } else { $co4h .= " Wh"; $coRe .= " Wh"; @@ -6506,45 +6659,45 @@ sub _graphicHeader { $pvTo .= " Wh"; $pvCu .= " W"; } - + my $lupt = $hqtxt{lupt}{$lang}; my $autoct = $hqtxt{autoct}{$lang}; - my $lbpcq = $hqtxt{lbpcq}{$lang}; - my $lblPv4h = $hqtxt{lblPvh}{$lang}; + my $lbpcq = $hqtxt{lbpcq}{$lang}; + my $lblPv4h = $hqtxt{lblPvh}{$lang}; my $lblPvRe = $hqtxt{lblPRe}{$lang}; my $lblPvTo = $hqtxt{lblPTo}{$lang}; my $lblPvCu = $hqtxt{lblPCu}{$lang}; - + ## Header Start ################# - my $header = qq{}; + my $header = qq{
}; - # Header Link + Status + Update Button - ######################################### + # Header Link + Status + Update Button + ######################################### if($hdrDetail eq "all" || $hdrDetail eq "statusLink") { my ($upicon,$scicon,$img); - + my ($year, $month, $day, $time) = $lup =~ /(\d{4})-(\d{2})-(\d{2})\s+(.*)/x; $lup = "$year-$month-$day $time"; - + if($lang eq "DE") { - $lup = "$day.$month.$year $time"; + $lup = "$day.$month.$year $time"; } - my $cmdupdate = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction 0 get $name data')"}; # Update Button generieren + my $cmdupdate = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction 0 get $name data')"}; # Update Button generieren if ($ftui eq "ftui") { - $cmdupdate = qq{"ftui.setFhemStatus('set $name clientAction 0 get $name data')"}; - } - - my $cmdplchk = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=get $name plantConfigCheck', function(data){FW_okDialog(data)})"}; # Plant Check Button generieren + $cmdupdate = qq{"ftui.setFhemStatus('set $name clientAction 0 get $name data')"}; + } + + my $cmdplchk = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=get $name plantConfigCheck', function(data){FW_okDialog(data)})"}; # Plant Check Button generieren if ($ftui eq "ftui") { - $cmdplchk = qq{"ftui.setFhemStatus('get $name plantConfigCheck')"}; + $cmdplchk = qq{"ftui.setFhemStatus('get $name plantConfigCheck')"}; } my $upstate = ReadingsVal($name, 'state', ''); - + ## Anlagen Check-Icon ####################### $img = FW_makeImage('edit_settings@grey'); @@ -6555,18 +6708,18 @@ sub _graphicHeader { ################ my $naup = ReadingsVal ($name, 'nextCycletime', ''); if ($upstate =~ /updated|successfully|switched/ix) { - $img = FW_makeImage('10px-kreis-gruen.png', $htitles{upd}{$lang}.' ('.$htitles{natc}{$lang}.' '.$naup.')'); - #$img = FW_makeImage('10px-kreis-gruen.png', $htitles{upd}{$lang}); + $img = FW_makeImage('10px-kreis-gruen.png', $htitles{upd}{$lang}.' '.$htitles{natc}{$lang}.' '.$naup.''); + #$img = FW_makeImage('10px-kreis-gruen.png', $htitles{upd}{$lang}.' ('.$htitles{natc}{$lang}.' '.$naup.')'); $upicon = "$img"; - } + } elsif ($upstate =~ /running/ix) { $img = FW_makeImage('10px-kreis-gelb.png', 'running'); $upicon = "$img"; - } + } elsif ($upstate =~ /initialized/ix) { $img = FW_makeImage('1px-spacer.png', 'initialized'); $upicon = "$img"; - } + } else { $img = FW_makeImage('10px-kreis-rot.png', $htitles{upd}{$lang}.' ('.$htitles{natc}{$lang}.' '.$naup.')'); $upicon = "$img"; @@ -6577,129 +6730,171 @@ sub _graphicHeader { my $acicon; if ($pcfa eq "on") { $acicon = FW_makeImage('10px-kreis-gruen.png', $htitles{on}{$lang}); - } + } elsif ($pcfa eq "off") { - $htitles{akorron}{$lang} =~ s//$name/xs; + $htitles{akorron}{$lang} =~ s//$name/xs; $acicon = FW_makeImage('-', $htitles{akorron}{$lang}); - } + } elsif ($pcfa =~ /standby/ix) { my ($rtime) = $pcfa =~ /for (.*?) hours/x; $img = FW_makeImage('10px-kreis-gelb.png', $htitles{dela}{$lang}); $acicon = "$img (Start in ".$rtime." h)"; - } + } else { $acicon = FW_makeImage('10px-kreis-rot.png', $htitles{undef}{$lang}); } - + ## SolCast Sektion #################### - my $api = isSolCastUsed ($hash) ? 'SolCast:' : q{}; - + my $api = isSolCastUsed ($hash) ? 'SolCast:' : q{}; + if($api) { my $nscc = ReadingsVal ($name, 'nextSolCastCall', '?'); my $lrt = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_time', '-'); - + if ($lrt =~ /(\d{4})-(\d{2})-(\d{2})\s+(.*)/x) { my ($sly, $slmo, $sld, $slt) = $lrt =~ /(\d{4})-(\d{2})-(\d{2})\s+(.*)/x; $lrt = "$sly-$slmo-$sld $slt"; - + if($lang eq "DE") { - $lrt = "$sld.$slmo.$sly $slt"; + $lrt = "$sld.$slmo.$sly $slt"; } } - + $api .= ' '.$lrt; my $scrm = SolCastAPIVal ($hash, '?All', '?All', 'response_message', '-'); - + if ($scrm eq 'success') { - $img = FW_makeImage('10px-kreis-gruen.png', $scrm.' ('.$htitles{natc}{$lang}.' '.$nscc.')'); - $scicon = "$img"; - } + $img = FW_makeImage('10px-kreis-gruen.png', $htitles{scaresps}{$lang}.' '.$htitles{natc}{$lang}.' '.$nscc); + } + elsif ($scrm =~ /You have exceeded your free daily limit/i) { + $img = FW_makeImage('10px-kreis-rot.png', $htitles{scarespf}{$lang}.': '. $htitles{yheyfdl}{$lang}); + } + elsif ($scrm =~ /ApiKey does not exist/i) { + $img = FW_makeImage('10px-kreis-rot.png', $htitles{scarespf}{$lang}.': '. $htitles{scakdne}{$lang}); + } + elsif ($scrm =~ /Rooftop site does not exist or is not accessible/i) { + $img = FW_makeImage('10px-kreis-rot.png', $htitles{scarespf}{$lang}.': '. $htitles{scrsdne}{$lang}); + } else { - $img = FW_makeImage('10px-kreis-rot.png', $scrm.' ('.$htitles{natc}{$lang}.' '.$nscc.')'); - $scicon = "$img"; + $img = FW_makeImage('10px-kreis-rot.png', $htitles{scarespf}{$lang}.': '. $scrm); } + $scicon = "$img"; + $api .= '  '.$scicon; + $api .= ''; $api .= '  ('; $api .= SolCastAPIVal ($hash, '?All', '?All', 'todayDoneAPIrequests', 0); $api .= '/'; $api .= SolCastAPIVal ($hash, '?All', '?All', 'todayRemainingAPIrequests', 50); $api .= ')'; + $api .= ''; } - + ## Qualitäts-Icon ###################### my $pcqicon; - + if (isSolCastUsed ($hash)) { - $pcqicon = $pcq < 10 ? FW_makeImage('10px-kreis-rot.png', $pvcanz) : - $pcq < 20 ? FW_makeImage('10px-kreis-gelb.png', $pvcanz) : + $pcqicon = $pcq < 10 ? FW_makeImage('10px-kreis-rot.png', $pvcanz) : + $pcq < 20 ? FW_makeImage('10px-kreis-gelb.png', $pvcanz) : FW_makeImage('10px-kreis-gruen.png', $pvcanz); } else { - $pcqicon = $pcq < 3 ? FW_makeImage('10px-kreis-rot.png', $pvcanz) : - $pcq < 5 ? FW_makeImage('10px-kreis-gelb.png', $pvcanz) : - FW_makeImage('10px-kreis-gruen.png', $pvcanz); + $pcqicon = $pcq < 3 ? FW_makeImage('10px-kreis-rot.png', $pvcanz) : + $pcq < 5 ? FW_makeImage('10px-kreis-gelb.png', $pvcanz) : + FW_makeImage('10px-kreis-gruen.png', $pvcanz); } - + $pcqicon = "-" if(!$pvfc00 || $pcq == -1); - + ## Abweichung PV Prognose/Erzeugung ##################################### my $tdayDvtn = CircularVal ($hash, 99, 'tdayDvtn', '-'); my $ydayDvtn = CircularVal ($hash, 99, 'ydayDvtn', '-'); - $tdayDvtn = sprintf "%.0f", $tdayDvtn if(isNumeric($tdayDvtn)); - $ydayDvtn = sprintf "%.0f", $ydayDvtn if(isNumeric($ydayDvtn)); + $tdayDvtn = sprintf "%.1f %%", $tdayDvtn if(isNumeric($tdayDvtn)); + $ydayDvtn = sprintf "%.1f %%", $ydayDvtn if(isNumeric($ydayDvtn)); + $tdayDvtn =~ s/\./,/; + $tdayDvtn =~ s/\,0//; + $ydayDvtn =~ s/\./,/; + $ydayDvtn =~ s/,0//; + my $dvtntxt = $hqtxt{dvtn}{$lang}.' '; - my $tdaytxt = $hqtxt{tday}{$lang}.' '.$tdayDvtn; - my $ydaytxt = $hqtxt{yday}{$lang}.' '.$ydayDvtn; + my $tdaytxt = $hqtxt{tday}{$lang}.': '."".$tdayDvtn.""; + my $ydaytxt = $hqtxt{yday}{$lang}.': '."".$ydayDvtn.""; + my $text_tdayDvtn = $tdayDvtn =~ /^-[1-9]/? $hqtxt{pmtp}{$lang} : + $tdayDvtn =~ /^0/ ? $hqtxt{petp}{$lang} : + $tdayDvtn =~ /^[1-9]/ ? $hqtxt{pltp}{$lang} : + $hqtxt{wusond}{$lang}; + + my $text_ydayDvtn = $ydayDvtn =~ /^-[1-9]/? $hqtxt{pmtp}{$lang} : + $ydayDvtn =~ /^0/ ? $hqtxt{petp}{$lang} : + $ydayDvtn =~ /^[1-9]/ ? $hqtxt{pltp}{$lang} : + $hqtxt{snbefb}{$lang}; + + ## erste Header-Zeilen ####################### my $alias = AttrVal ($name, "alias", $name ); # Linktext als Aliasname - my $dlink = qq{$alias}; - + my $dlink = qq{$alias}; + $header .= qq{}; - $header .= qq{}; - $header .= qq{}; - $header .= qq{}; - $header .= qq{}; + $header .= qq{}; + $header .= qq{}; + $header .= qq{}; + $header .= qq{}; $header .= qq{}; $header .= qq{}; - $header .= qq{}; - $header .= qq{}; - $header .= qq{}; + $header .= qq{}; + $header .= qq{}; + $header .= qq{}; + $header .= qq{}; + $header .= qq{}; + $header .= qq{}; $header .= qq{}; } - - # Header Information pv + + # Header Information pv ######################## - if($hdrDetail eq "all" || $hdrDetail eq "pv" || $hdrDetail eq "pvco") { + if($hdrDetail eq "all" || $hdrDetail eq "pv" || $hdrDetail eq "pvco") { $header .= ""; $header .= ""; $header .= ""; $header .= ""; $header .= ""; - $header .= ""; + $header .= ""; $header .= ""; } - - + + # Header Information co - ######################## + ######################## if($hdrDetail eq "all" || $hdrDetail eq "co" || $hdrDetail eq "pvco") { $header .= ""; $header .= ""; - $header .= ""; + $header .= ""; $header .= ""; $header .= ""; - $header .= ""; - $header .= ""; + $header .= ""; + $header .= ""; } + + $header .= qq{}; + $header .= qq{}; + $header .= qq{}; $header .= qq{
$dlink $chkicon $lupt   $lup   $upicon $api $dlink $chkicon $lupt $lup   $upicon $api
$autoct  $acicon  $lbpcq  $pcqicon $dvtntxt $tdaytxt, $ydaytxt $autoct    $acicon       $lbpcq    $pcqicon $dvtntxt}; + $header .= qq{}; + $header .= qq{$tdaytxt}; + $header .= qq{}; + $header .= qq{, }; + $header .= qq{}; + $header .= qq{$ydaytxt}; + $header .= qq{}; + $header .= qq{

PV =>$lblPvCu $pvCu$lblPv4h $pv4h$lblPvRe $pvRe$lblPvTo $pvTo$lblPvTo $pvTo
CO =>$lblPvCu$coCu$lblPvCu$coCu$lblPv4h$co4h$lblPvRe$coRe$lblPvTo$coTo
$lblPvTo$coTo

}; - + return $header; } @@ -6713,27 +6908,28 @@ sub _showConsumerInGraphicBeam { my $name = $paref->{name}; my $type = $paref->{type}; my $hfcg = $paref->{hfcg}; + my $lang = $paref->{lang}; # get consumer list and display it in Graphics - ################################################ + ################################################ my @consumers = sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}; # definierte Verbraucher ermitteln - + for (@consumers) { next if(!$_); my ($itemName, undef) = split(':',$_); $itemName =~ s/^\s+|\s+$//gx; # trim it, if blanks were used $_ =~ s/^\s+|\s+$//gx; # trim it, if blanks were used - + # check if listed device is planned #################################### if (ReadingsVal($name, $itemName."_Planned", "no") eq "yes") { #get start and end hour my ($start, $end); # werden auf Balken Pos 0 - 23 umgerechnet, nicht auf Stunde !!, Pos = 24 -> ungültige Pos = keine Anzeige - if(AttrVal("global","language","EN") eq "DE") { + if($lang eq "DE") { (undef,undef,undef,$start) = ReadingsVal($name, $itemName."_PlannedOpTimeBegin", '00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; (undef,undef,undef,$end) = ReadingsVal($name, $itemName."_PlannedOpTimeEnd", '00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; - } + } else { (undef,undef,undef,$start) = ReadingsVal($name, $itemName."_PlannedOpTimeBegin", '0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; (undef,undef,undef,$end) = ReadingsVal($name, $itemName."_PlannedOpTimeEnd", '0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; @@ -6748,63 +6944,67 @@ sub _showConsumerInGraphicBeam { if ($start < $hfcg->{0}{time}) { # gridconsumption seems to be tomorrow $start = 24-$hfcg->{0}{time}+$start; $flag = 1; - } - else { - $start -= $hfcg->{0}{time}; + } + else { + $start -= $hfcg->{0}{time}; } if ($flag) { # gridconsumption seems to be tomorrow $end = 24-$hfcg->{0}{time}+$end; - } - else { - $end -= $hfcg->{0}{time}; + } + else { + $end -= $hfcg->{0}{time}; } $_ .= ":".$start.":".$end; - } - else { - $_ .= ":24:24"; + } + else { + $_ .= ":24:24"; } } - + return; } ################################################################ -# Verbraucherlegende und Steuerung +# Verbraucherlegende und Steuerung ################################################################ -sub _graphicConsumerLegend { +sub _graphicConsumerLegend { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; # Consumer AdviceIcon my ($clegendstyle, $clegend) = split('_', $paref->{clegend}); + my $clink = $paref->{clink}; my $type = $paref->{type}; my @consumers = sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}; # definierte Verbraucher ermitteln - + $clegend = '' if(($clegendstyle eq 'none') || (!int(@consumers))); $paref->{clegend} = $clegend; return if(!$clegend ); - + my $ftui = $paref->{ftui}; my $lang = $paref->{lang}; my $dstyle = $paref->{dstyle}; # TD-Style - + my $staticon; - + ## Tabelle Start ################# - my $ctable = qq{}; - $ctable .= qq{}; - + my $ctable = qq{
}; + $ctable .= qq{}; + $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; - - my $cnum = @consumers; + + $ctable .= qq{}; + $ctable .= qq{}; + + my $cnum = @consumers; if($cnum > 1) { $ctable .= qq{}; $ctable .= qq{}; @@ -6818,57 +7018,65 @@ sub _graphicConsumerLegend { $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; - $ctable .= qq{}; + $ctable .= qq{}; } - + $ctable .= qq{}; + if ($clegend ne 'top') { + $ctable .= qq{}; + } + my $modulo = 1; my $tro = 0; - - for my $c (@consumers) { + + for my $c (@consumers) { my $caicon = $paref->{caicon}; # Consumer AdviceIcon my $cname = ConsumerVal ($hash, $c, "name", ""); # Name des Consumerdevices my $calias = ConsumerVal ($hash, $c, "alias", $cname); # Alias des Consumerdevices - my $cicon = ConsumerVal ($hash, $c, "icon", ""); # Icon des Consumerdevices + my $cicon = ConsumerVal ($hash, $c, "icon", ""); # Icon des Consumerdevices my $oncom = ConsumerVal ($hash, $c, "oncom", ""); # Consumer Einschaltkommando my $offcom = ConsumerVal ($hash, $c, "offcom", ""); # Consumer Ausschaltkommando my $autord = ConsumerVal ($hash, $c, "autoreading", ""); # Readingname f. Automatiksteuerung my $auto = ConsumerVal ($hash, $c, "auto", 1); # Automatic Mode - + my $cmdon = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction 0 set $cname $oncom')"}; my $cmdoff = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction 0 set $cname $offcom')"}; my $cmdautoon = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction 0 setreading $cname $autord 1')"}; my $cmdautooff = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction 0 setreading $cname $autord 0')"}; my $implan = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction 0 consumerImmediatePlanning $c')"}; - + if ($ftui eq "ftui") { $cmdon = qq{"ftui.setFhemStatus('set $name clientAction 0 set $cname $oncom')"}; $cmdoff = qq{"ftui.setFhemStatus('set $name clientAction 0 set $cname $offcom')"}; - $cmdautoon = qq{"ftui.setFhemStatus('set $name clientAction 0 setreading $cname $autord 1')"}; - $cmdautooff = qq{"ftui.setFhemStatus('set $name clientAction 0 setreading $cname $autord 0')"}; - $implan = qq{"ftui.setFhemStatus('set $name clientAction 0 consumerImmediatePlanning $c')"}; + $cmdautoon = qq{"ftui.setFhemStatus('set $name clientAction 0 setreading $cname $autord 1')"}; + $cmdautooff = qq{"ftui.setFhemStatus('set $name clientAction 0 setreading $cname $autord 0')"}; + $implan = qq{"ftui.setFhemStatus('set $name clientAction 0 consumerImmediatePlanning $c')"}; } - + $cmdon = q{} if(!$oncom); $cmdoff = q{} if(!$offcom); - $cmdautoon = q{} if(!$autord); - $cmdautooff = q{} if(!$autord); + $cmdautoon = q{} if(!$autord); + $cmdautooff = q{} if(!$autord); my $swicon = q{}; # Schalter ein/aus Icon my $auicon = q{}; # Schalter Automatic Icon my $isricon = q{}; # Zustand IsRecommended Icon - + $paref->{consumer} = $c; - - my ($planstate,$starttime,$stoptime) = __getPlanningStateAndTimes ($paref); + + my ($planstate,$starttime,$stoptime) = __getPlanningStateAndTimes ($paref); my $pstate = $caicon eq "times" ? $hqtxt{pstate}{$lang} : $htitles{pstate}{$lang}; my $surplusinfo = isConsRcmd($hash, $c) ? $htitles{splus}{$lang} : $htitles{nosplus}{$lang}; - + $pstate =~ s//$planstate/xs; $pstate =~ s//$starttime/xs; - $pstate =~ s//$stoptime/xs; - $pstate =~ s/\s+/ /gxs if($caicon eq "times"); + $pstate =~ s//$stoptime/xs; + $pstate =~ s/\s+/ /gxs if($caicon eq "times"); + + if ($clink) { + $calias = qq{$calias}; + } if($caicon ne "none") { if(isInTimeframe($hash, $c)) { # innerhalb Planungszeitraum ? @@ -6879,7 +7087,7 @@ sub _graphicConsumerLegend { $isricon = "".FW_makeImage($caicon, '')." "; if($planstate =~ /priority/xs) { my (undef,$color) = split('@', $caicon); - $color = $color ? '@'.$color : ''; + $color = $color ? '@'.$color : ''; $isricon = "".FW_makeImage('it_ups_charging'.$color, '')." "; } } @@ -6893,78 +7101,87 @@ sub _graphicConsumerLegend { $isricon = "".FW_makeImage($caicon.'@grey', '')." "; } } - } - + } + if($modulo % 2){ $ctable .= qq{}; $tro = 1; } - + if(!$auto) { $staticon = FW_makeImage('ios_off_fill@red', $htitles{iaaf}{$lang}); $auicon = " $staticon"; - } - + } + if ($auto) { $staticon = FW_makeImage('ios_on_till_fill@orange', $htitles{ieas}{$lang}); $auicon = " $staticon"; } - + if (isConsumerPhysOff($hash, $c)) { # Schaltzustand des Consumerdevices off if($cmdon) { $staticon = FW_makeImage('ios_off_fill@red', $htitles{iave}{$lang}); - $swicon = " $staticon"; + $swicon = " $staticon"; } else { $staticon = FW_makeImage('ios_off_fill@grey', $htitles{ians}{$lang}); - $swicon = " $staticon"; + $swicon = " $staticon"; } - } - + } + if (isConsumerPhysOn($hash, $c)) { # Schaltzustand des Consumerdevices on if($cmdoff) { - $staticon = FW_makeImage('ios_on_fill@green', $htitles{ieva}{$lang}); + $staticon = FW_makeImage('ios_on_fill@green', $htitles{ieva}{$lang}); $swicon = " $staticon"; } else { - $staticon = FW_makeImage('ios_on_fill@grey', $htitles{iens}{$lang}); - $swicon = " $staticon"; + $staticon = FW_makeImage('ios_on_fill@grey', $htitles{iens}{$lang}); + $swicon = " $staticon"; } } - - if ($clegendstyle eq 'icon') { + + if ($clegendstyle eq 'icon') { $cicon = FW_makeImage($cicon); - - $ctable .= ""; - $ctable .= ""; - $ctable .= ""; - $ctable .= ""; - $ctable .= ""; - } + + $ctable .= ""; + $ctable .= ""; + $ctable .= ""; + $ctable .= ""; + $ctable .= ""; + } else { my (undef,$co) = split('@', $cicon); - $co = '' if (!$co); - + $co = '' if (!$co); + $ctable .= ""; $ctable .= ""; $ctable .= ""; $ctable .= ""; $ctable .= ""; } - + if(!($modulo % 2)) { $ctable .= qq{}; $tro = 0; } - + else { + $ctable .= qq{}; + $ctable .= qq{}; + } + $modulo++; } - + delete $paref->{consumer}; - + $ctable .= qq{} if($tro); - $ctable .= qq{
$hqtxt{cnsm}{$lang} $hqtxt{eiau}{$lang} $hqtxt{auto}{$lang}          $hqtxt{cnsm}{$lang} $blk $blk $blk $blk $blk

$calias $cicon $isricon $swicon $auicon $calias $cicon $isricon $swicon $auicon $calias $isricon $swicon $auicon
        
}; + if ($clegend ne 'bottom') { + $ctable .= qq{
}; + } + + $ctable .= qq{}; + return $ctable; } @@ -6985,9 +7202,9 @@ sub _beamGraphicFirstHour { my $t = NexthoursVal ($hash, "NextHour00", "starttime", '0000-00-00 24'); my ($year,$month,$day_str,$thishour) = $t =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; my ($val1,$val2,$val3,$val4) = (0,0,0,0); - + $thishour++; - + $hfcg->{0}{time_str} = $thishour; $thishour = int($thishour); # keine führende Null @@ -6995,7 +7212,7 @@ sub _beamGraphicFirstHour { $hfcg->{0}{day_str} = $day_str; $day = int($day_str); $hfcg->{0}{day} = $day; - $hfcg->{0}{mktime} = fhemTimeLocal(0,0,$thishour,$day,int($month)-1,$year-1900); # gleich die Unix Zeit dazu holen + $hfcg->{0}{mktime} = fhemTimeLocal(0,0,$thishour,$day,int($month)-1,$year-1900); # gleich die Unix Zeit dazu holen if ($offset) { $hfcg->{0}{time} += $offset; @@ -7008,7 +7225,7 @@ sub _beamGraphicFirstHour { } $hfcg->{0}{time_str} = sprintf('%02d', $hfcg->{0}{time}); - + $val1 = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, "pvfc", 0); $val2 = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, "pvrl", 0); $val3 = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, "gcons", 0); @@ -7053,8 +7270,8 @@ sub _beamGraphicRemainingHours { $maxVal //= $hfcg->{0}{beam1}; # Startwert wenn kein Wert bereits via attr vorgegeben ist my ($val1,$val2,$val3,$val4); - - my $maxCon = $hfcg->{0}{beam1}; + + my $maxCon = $hfcg->{0}{beam1}; my $maxDif = $hfcg->{0}{diff}; # für Typ diff my $minDif = $hfcg->{0}{diff}; # für Typ diff @@ -7079,12 +7296,12 @@ sub _beamGraphicRemainingHours { # Sonderfall Mitternacht $ds = strftime "%d", localtime($hfcg->{0}{mktime} - (3600 * (abs($offset-$i+1)))) if ($hfcg->{$i}{time} == 24); # V0.49.4 - + $val1 = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, "pvfc", 0); - $val2 = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, "pvrl", 0); + $val2 = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, "pvrl", 0); $val3 = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, "gcons", 0); $val4 = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, "confc", 0); - + $hfcg->{$i}{weather} = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, 'weatherid', 999); $hfcg->{$i}{wcc} = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, 'wcc', '-'); } @@ -7113,7 +7330,7 @@ sub _beamGraphicRemainingHours { $hfcg->{$i}{beam2} //= 0; $hfcg->{$i}{diff} = $hfcg->{$i}{beam1} - $hfcg->{$i}{beam2}; - $maxVal = $hfcg->{$i}{beam1} if ($hfcg->{$i}{beam1} > $maxVal); + $maxVal = $hfcg->{$i}{beam1} if ($hfcg->{$i}{beam1} > $maxVal); $maxCon = $hfcg->{$i}{beam2} if ($hfcg->{$i}{beam2} > $maxCon); $maxDif = $hfcg->{$i}{diff} if ($hfcg->{$i}{diff} > $maxDif); $minDif = $hfcg->{$i}{diff} if ($hfcg->{$i}{diff} < $minDif); @@ -7148,82 +7365,82 @@ sub _beamGraphic { my $colorb1 = $paref->{colorb1}; my $colorb2 = $paref->{colorb2}; my $fcolor1 = $paref->{fcolor1}; - my $fcolor2 = $paref->{fcolor2}; + my $fcolor2 = $paref->{fcolor2}; my $offset = $paref->{offset}; my $thishour = $paref->{thishour}; - my $maxVal = $paref->{maxVal}; - my $maxCon = $paref->{maxCon}; - my $maxDif = $paref->{maxDif}; - my $minDif = $paref->{minDif}; + my $maxVal = $paref->{maxVal}; + my $maxCon = $paref->{maxCon}; + my $maxDif = $paref->{maxDif}; + my $minDif = $paref->{minDif}; my $beam1cont = $paref->{beam1cont}; my $beam2cont = $paref->{beam2cont}; - + $lotype = 'single' if ($beam1cont eq $beam2cont); # User Auswahl Layout überschreiben bei gleichen Beamcontent ! # Wenn Table class=block alleine steht, zieht es bei manchen Styles die Ausgabe auf 100% Seitenbreite # lässt sich durch einbetten in eine zusätzliche Table roomoverview eindämmen # Die Tabelle ist recht schmal angelegt, aber nur so lassen sich Umbrüche erzwingen - + my ($val,$z2,$z3,$z4,$he); my $ret; - + $ret .= __weatherOnBeam ($paref); - + my $m = $paref->{modulo} % 2; if($show_diff eq 'top') { # Zusätzliche Zeile Ertrag - Verbrauch $ret .= ""; my $ii; - for my $i (0..($maxhours*2)-1) { # gleiche Bedingung wie oben - next if (!$show_night && ($hfcg->{$i}{weather} > 99) - && !$hfcg->{$i}{beam1} + for my $i (0..($maxhours*2)-1) { # gleiche Bedingung wie oben + next if (!$show_night && ($hfcg->{$i}{weather} > 99) + && !$hfcg->{$i}{beam1} && !$hfcg->{$i}{beam2}); $ii++; # wieviele Stunden haben wir bisher angezeigt ? - + last if ($ii > $maxhours); # vorzeitiger Abbruch $val = formatVal6($hfcg->{$i}{diff},$kw,$hfcg->{$i}{weather}); - + if ($val ne ' ') { # Forum: https://forum.fhem.de/index.php/topic,117864.msg1166215.html#msg1166215 - $val = $hfcg->{$i}{diff} < 0 ? ''.$val.'' : - $val > 0 ? '+'.$val : + $val = $hfcg->{$i}{diff} < 0 ? ''.$val.'' : + $val > 0 ? '+'.$val : $val; # negative Zahlen in Fettschrift, 0 aber ohne + } - - $ret .= "$val"; + + $ret .= "$val"; } - $ret .= ""; # freier Platz am Ende + $ret .= ""; # freier Platz am Ende } $ret .= ""; # Neue Zeile mit freiem Platz am Anfang my $ii = 0; - + for my $i (0..($maxhours*2)-1) { # gleiche Bedingung wie oben - next if (!$show_night && defined($hfcg->{$i}{weather}) - && ($hfcg->{$i}{weather} > 99) - && !$hfcg->{$i}{beam1} + next if (!$show_night && defined($hfcg->{$i}{weather}) + && ($hfcg->{$i}{weather} > 99) + && !$hfcg->{$i}{beam1} && !$hfcg->{$i}{beam2}); $ii++; last if ($ii > $maxhours); - # Achtung Falle, Division by Zero möglich, - # maxVal kann gerade bei kleineren maxhours Ausgaben in der Nacht leicht auf 0 fallen + # Achtung Falle, Division by Zero möglich, + # maxVal kann gerade bei kleineren maxhours Ausgaben in der Nacht leicht auf 0 fallen $height = 200 if (!$height); # Fallback, sollte eigentlich nicht vorkommen, außer der User setzt es auf 0 $maxVal = 1 if (!int $maxVal); $maxCon = 1 if (!$maxCon); - # Der zusätzliche Offset durch $fsize verhindert bei den meisten Skins + # Der zusätzliche Offset durch $fsize verhindert bei den meisten Skins # dass die Grundlinie der Balken nach unten durchbrochen wird if ($lotype eq 'single') { $he = int(($maxVal-$hfcg->{$i}{beam1}) / $maxVal*$height) + $fsize; $z3 = int($height + $fsize - $he); - } + } if ($lotype eq 'double') { # Berechnung der Zonen - # he - freier der Raum über den Balken. fsize wird nicht verwendet, da bei diesem Typ keine Zahlen über den Balken stehen + # he - freier der Raum über den Balken. fsize wird nicht verwendet, da bei diesem Typ keine Zahlen über den Balken stehen # z2 - der Ertrag ggf mit Icon # z3 - der Verbrauch , bei zu kleinem Wert wird der Platz komplett Zone 2 zugeschlagen und nicht angezeigt # z2 und z3 nach Bedarf tauschen, wenn der Verbrauch größer als der Ertrag ist @@ -7231,24 +7448,24 @@ sub _beamGraphic { $maxVal = $maxCon if ($maxCon > $maxVal); # wer hat den größten Wert ? if ($hfcg->{$i}{beam1} > $hfcg->{$i}{beam2}) { # Beam1 oben , Beam2 unten - $z2 = $hfcg->{$i}{beam1}; $z3 = $hfcg->{$i}{beam2}; - } + $z2 = $hfcg->{$i}{beam1}; $z3 = $hfcg->{$i}{beam2}; + } else { # tauschen, Verbrauch ist größer als Ertrag - $z3 = $hfcg->{$i}{beam1}; $z2 = $hfcg->{$i}{beam2}; + $z3 = $hfcg->{$i}{beam1}; $z2 = $hfcg->{$i}{beam2}; } $he = int(($maxVal-$z2)/$maxVal*$height); $z2 = int(($z2 - $z3)/$maxVal*$height); $z3 = int($height - $he - $z2); # was von maxVal noch übrig ist - + if ($z3 < int($fsize/2)) { # dünnen Strichbalken vermeiden / ca. halbe Zeichenhöhe - $z2 += $z3; - $z3 = 0; + $z2 += $z3; + $z3 = 0; } } - if ($lotype eq 'diff') { + if ($lotype eq 'diff') { # Berechnung der Zonen # he - freier der Raum über den Balken , Zahl positiver Wert + fsize # z2 - positiver Balken inkl Icon @@ -7261,12 +7478,12 @@ sub _beamGraphic { if ($maxValBeam) { # Feste Aufteilung +/- , jeder 50 % bei maxValBeam = 0 $px_pos = int($height/2); $px_neg = $height - $px_pos; # Rundungsfehler vermeiden - } - else { # Dynamische hoch/runter Verschiebung der Null-Linie + } + else { # Dynamische hoch/runter Verschiebung der Null-Linie if ($minDif >= 0 ) { # keine negativen Balken vorhanden, die Positiven bekommen den gesammten Raum $px_neg = 0; $px_pos = $height; - } + } else { if ($maxDif > 0) { $px_neg = int($height * abs($minDif) / ($maxDif + abs($minDif))); # Wieviel % entfallen auf unten ? @@ -7282,7 +7499,7 @@ sub _beamGraphic { if ($hfcg->{$i}{diff} >= 0) { # Zone 2 & 3 mit ihren direkten Werten vorbesetzen $z2 = $hfcg->{$i}{diff}; $z3 = abs($minDif); - } + } else { $z2 = $maxDif; $z3 = abs($hfcg->{$i}{diff}); # Nur Betrag ohne Vorzeichen @@ -7294,10 +7511,10 @@ sub _beamGraphic { $z4 = (!$px_neg || !$minDif) ? 0 : int((abs($minDif)-$z3)/abs($minDif)*$px_neg); # Teilung durch 0 unbedingt vermeiden $z3 = ($px_neg - $z4); # Beiden Zonen die Werte ausgeben könnten muß fsize als zusätzlicher Raum zugeschlagen werden ! - $he += $fsize; + $he += $fsize; $z4 += $fsize if ($z3); # komplette Grafik ohne negativ Balken, keine Ausgabe von z3 & z4 } - + # das style des nächsten TD bestimmt ganz wesentlich das gesammte Design # das \n erleichtert das lesen des Seitenquelltext beim debugging # vertical-align:bottom damit alle Balken und Ausgaben wirklich auf der gleichen Grundlinie sitzen @@ -7313,22 +7530,22 @@ sub _beamGraphic { if ($hfcg->{$i}{beam1} || $show_night) { # Balken nur einfärben wenn der User via Attr eine Farbe vorgibt, sonst bestimmt class odd von TR alleine die Farbe my $style = "style=\"padding-bottom:0px; vertical-align:top; margin-left:auto; margin-right:auto;"; - $style .= defined $colorb1 ? " background-color:#$colorb1\"" : '"'; # Syntaxhilight + $style .= defined $colorb1 ? " background-color:#$colorb1\"" : '"'; # Syntaxhilight $ret .= ""; $ret .= ""; - - my $sicon = 1; + + my $sicon = 1; #$ret .= $is{$i} if (defined ($is{$i}) && $sicon); # inject the new icon if defined ################################## #$ret .= consinject($hash,$i,@consumers) if($s); - + $ret .= ""; } } - + if ($lotype eq 'double') { my ($color1, $color2, $style1, $style2, $v); my $style = "style='padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; @@ -7352,7 +7569,7 @@ sub _beamGraphic { $val = formatVal6($hfcg->{$i}{beam2},$kw,$hfcg->{$i}{weather}); $color1 = $colorb2; $style1 = $style." background-color:#$color1; color:#$fcolor2;'"; - + if($z3) { $v = formatVal6($hfcg->{$i}{beam1},$kw,$hfcg->{$i}{weather}); $color2 = $colorb1; @@ -7362,14 +7579,14 @@ sub _beamGraphic { $ret .= ""; $ret .= "$val"; - + # inject the new icon if defined ################################## #$ret .= consinject($hash,$i,@consumers) if($s); - + $ret .= ""; - if ($z3) { # die Zone 3 lassen wir bei zu kleinen Werten auch ganz weg + if ($z3) { # die Zone 3 lassen wir bei zu kleinen Werten auch ganz weg $ret .= ""; $ret .= "$v"; } @@ -7393,7 +7610,7 @@ sub _beamGraphic { $ret .= ""; $ret .= ""; $ret .= ""; - } + } else { # ohne Farbe $z2 = 2 if ($hfcg->{$i}{diff} == 0); # Sonderfall, hier wird die 0 gebraucht ! if ($z2 && $val) { # z2 weglassen wenn nicht unbedigt nötig bzw. wenn zuvor he mit val keinen Wert hatte @@ -7401,7 +7618,7 @@ sub _beamGraphic { $ret .= ""; } } - + if ($hfcg->{$i}{diff} < 0) { # Negativ Balken anzeigen ? $style .= " background-color:#$colorb2'"; # mit Farbe 2 colorb2 füllen $ret .= ""; @@ -7422,21 +7639,21 @@ sub _beamGraphic { if ($show_diff eq 'bottom') { # zusätzliche diff Anzeige $val = formatVal6($hfcg->{$i}{diff},$kw,$hfcg->{$i}{weather}); $val = ($hfcg->{$i}{diff} < 0) ? ''.$val.'' : ($val > 0 ) ? '+'.$val : $val if ($val ne ' '); # negative Zahlen in Fettschrift, 0 aber ohne + - $ret .= "$val"; + $ret .= "$val"; } $ret .= ""; - $ret .= $hfcg->{$i}{time} == $thishour ? # wenn Hervorhebung nur bei gestztem Attr 'graphicHistoryHour' ? dann hinzufügen: "&& $offset < 0" - ''.$hfcg->{$i}{time_str}.'' : + $ret .= $hfcg->{$i}{time} == $thishour ? # wenn Hervorhebung nur bei gesetztem Attr 'graphicHistoryHour' ? dann hinzufügen: "&& $offset < 0" + ''.$hfcg->{$i}{time_str}.'' : $hfcg->{$i}{time_str}; - + if($hfcg->{$i}{time} == $thishour) { $thishour = 99; # nur einmal verwenden ! } - - $ret .=""; + + $ret .=""; } - + $paref->{modulo}++; $ret .= ""; @@ -7446,7 +7663,7 @@ return $ret; } ################################################################ -# Wetter Icon Zeile +# Wetter Icon Zeile ################################################################ sub __weatherOnBeam { my $paref = shift; @@ -7458,57 +7675,62 @@ sub __weatherOnBeam { my $colorw = $paref->{colorw}; # Wetter Icon Farbe my $colorwn = $paref->{colorwn}; # Wetter Icon Farbe Nacht my $width = $paref->{width}; - + my $lang = $paref->{lang}; + my $ret = q{}; - + return $ret if(!$weather); - - my $m = $paref->{modulo} % 2; + + my $m = $paref->{modulo} % 2; + my $debug = $paref->{debug}; $ret .= ""; # freier Platz am Anfang my $ii; for my $i (0..($maxhours*2)-1) { last if (!exists($hfcg->{$i}{weather})); - next if (!$show_night && defined($hfcg->{$i}{weather}) - && ($hfcg->{$i}{weather} > 99) - && !$hfcg->{$i}{beam1} + next if (!$show_night && defined($hfcg->{$i}{weather}) + && ($hfcg->{$i}{weather} > 99) + && !$hfcg->{$i}{beam1} && !$hfcg->{$i}{beam2}); # Lässt Nachticons aber noch durch wenn es einen Wert gibt , ToDo : klären ob die Nacht richtig gesetzt wurde $ii++; # wieviele Stunden Icons haben wir bisher beechnet ? last if ($ii > $maxhours); # ToDo : weather_icon sollte im Fehlerfall Title mit der ID besetzen um in FHEMWEB sofort die ID sehen zu können if (exists($hfcg->{$i}{weather}) && defined($hfcg->{$i}{weather})) { - my ($icon_name, $title) = $hfcg->{$i}{weather} > 100 ? - weather_icon($hfcg->{$i}{weather}-100) : - weather_icon($hfcg->{$i}{weather}); - + my ($icon_name, $title) = $hfcg->{$i}{weather} > 100 ? + weather_icon ($name, $lang, $hfcg->{$i}{weather}-100) : + weather_icon ($name, $lang, $hfcg->{$i}{weather}); + my $wcc = $hfcg->{$i}{wcc} // "-"; # Bewölkungsgrad ergänzen - - if(isNumeric ($wcc)) { # Javascript Fehler vermeiden: https://forum.fhem.de/index.php/topic,117864.msg1233661.html#msg1233661 + + if(isNumeric ($wcc)) { # Javascript Fehler vermeiden: https://forum.fhem.de/index.php/topic,117864.msg1233661.html#msg1233661 $wcc += 0; } - + $title .= ': '.$wcc; - - if($icon_name eq 'unknown') { - Log3 ($name, 4, "$name - unknown weather id: ".$hfcg->{$i}{weather}.", please inform the maintainer"); + + if($icon_name eq 'unknown') { + if($debug =~ /graphic/x) { + Log (1, "$name DEBUG> unknown weather id: ".$hfcg->{$i}{weather}.", please inform the maintainer"); + } } - + $icon_name .= $hfcg->{$i}{weather} < 100 ? '@'.$colorw : '@'.$colorwn; my $val = FW_makeImage($icon_name) // q{}; if ($val eq $icon_name) { # passendes Icon beim User nicht vorhanden ! ( attr web iconPath falsch/prüfen/update ? ) $val = '???'; - if(AttrVal ($name, 'ctrlDebug', 0)) { # nur für Debugging - Log (1, qq{DEBUG> $name - the icon "$weather_ids{$hfcg->{$i}{weather}}{icon}" not found. Please check attribute "iconPath" of your FHEMWEB instance and/or update your FHEM software}); + + if($debug =~ /graphic/x) { # nur für Debugging + Log (1, qq{$name DEBUG> - the icon "$weather_ids{$hfcg->{$i}{weather}}{icon}" not found. Please check attribute "iconPath" of your FHEMWEB instance and/or update your FHEM software}); } } - + $ret .= "$val"; - } + } else { # mit $hfcg->{$i}{weather} = undef kann man unten leicht feststellen ob für diese Spalte bereits ein Icon ausgegeben wurde oder nicht - $ret .= ""; + $ret .= ""; $hfcg->{$i}{weather} = undef; # ToDo : prüfen ob noch nötig } } @@ -7533,8 +7755,7 @@ sub _flowGraphic { my $flowgconTime = $paref->{flowgconsTime}; my $consDist = $paref->{flowgconsDist}; my $css = $paref->{css}; - - #my $style = 'width:'.$flowgsize.'px; height:'.$flowgsize.'px;'; + my $style = 'width:98%; height:'.$flowgsize.'px;'; my $animation = $flowgani ? '@keyframes dash { to { stroke-dashoffset: 0; } }' : ''; # Animation Ja/Nein my $cpv = ReadingsNum($name, 'Current_PV', 0); @@ -7545,12 +7766,12 @@ sub _flowGraphic { my $cc_dummy = $cc; my $batin = ReadingsNum($name, 'Current_PowerBatIn', undef); my $batout = ReadingsNum($name, 'Current_PowerBatOut', undef); - my $soc = sprintf "%.1f", ReadingsNum($name, 'Current_BatCharge', 100); - - my $bat_color = $soc < 26 ? 'flowg bat25' : - $soc < 76 ? 'flowg bat50' : + my $soc = ReadingsNum($name, 'Current_BatCharge', 100); + + my $bat_color = $soc < 26 ? 'flowg bat25' : + $soc < 76 ? 'flowg bat50' : 'flowg bat75'; - + my $hasbat = 1; if (!defined($batin) && !defined($batout)) { @@ -7562,27 +7783,27 @@ sub _flowGraphic { else { $csc -= $batout; } - + my $grid_color = $cgfi ? 'flowg grid_color1' : 'flowg grid_color2'; $grid_color = 'flowg grid_color3' if (!$cgfi && !$cgc && $batout); # dritte Farbe my $cgc_style = $cgc ? 'flowg active_in' : 'flowg inactive_in'; my $batout_style = $batout ? 'flowg active_out active_bat_out' : 'flowg inactive_in'; - - my $cgc_direction = 'M490,305 L670,510'; # Batterientladung ins Netz - - if($batout) { + + my $cgc_direction = 'M490,305 L670,510'; # Batterientladung ins Netz + + if($batout) { my $cgfo = $cgfi - $cpv; if($cgfo > 1) { $cgc_style = 'flowg active_out'; $cgc_direction = 'M670,510 L490,305'; $cgfi -= $cgfo; - $cgc = $cgfo; + $cgc = $cgfo; } } - - my $batout_direction = 'M902,305 L730,510'; # Batterientladung aus Netz - + + my $batout_direction = 'M902,305 L730,510'; # Batterientladung aus Netz + if($batin) { my $gbi = $batin - $cpv; @@ -7590,23 +7811,24 @@ sub _flowGraphic { $batin -= $gbi; $batout_style = 'flowg active_in'; $batout_direction = 'M730,510 L902,305'; - $batout = $gbi; + $batout = $gbi; } } - - my $sun_color = $cpv ? 'flowg sun_active' : 'flowg sun_inactive'; - my $batin_style = $batin ? 'flowg active_in active_bat_in' : 'flowg inactive_out'; - my $csc_style = $csc && $cpv ? 'flowg active_out' : 'flowg inactive_out'; - my $cgfi_style = $cgfi ? 'flowg active_out' : 'flowg inactive_out'; - + + my $sun_color = $cpv ? 'flowg sun_active' : 'flowg sun_inactive'; + my $batin_style = $batin ? 'flowg active_in active_bat_in' : 'flowg inactive_out'; + my $csc_style = $csc && $cpv ? 'flowg active_out' : 'flowg inactive_out'; + my $cgfi_style = $cgfi ? 'flowg active_out' : 'flowg inactive_out'; + my $vbox_default = $flowgconTime ? '5 -25 800 700' : '5 -25 800 680'; + my $ret = << "END0"; - - - + + + @@ -7635,11 +7857,11 @@ sub _flowGraphic { - + - + - + END0 @@ -7651,32 +7873,32 @@ END0 my $consumer_start = 0; my $currentPower = 0; my @consumers; - + if ($flowgcons) { my $type = $paref->{type}; @consumers = sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}; # definierte Verbraucher ermitteln - $consumercount = scalar @consumers; + $consumercount = scalar @consumers; if ($consumercount % 2) { - $consumer_start = 350 - ($consDist * (($consumercount -1) / 2)); - } + $consumer_start = 350 - ($consDist * (($consumercount -1) / 2)); + } else { $consumer_start = 350 - (($consDist / 2) * ($consumercount-1)); } - + #$consumer_start = 0 if $consumer_start < 0; $pos_left = $consumer_start + 15; - + for my $c0 (@consumers) { my $calias = ConsumerVal ($hash, $c0, "alias", ""); # Name des Consumerdevices $currentPower = ReadingsNum ($name, "consumer${c0}_currentPower", 0); my $cicon = substConsumerIcon ($hash, $c0); # Icon des Consumerdevices $cc_dummy -= $currentPower; - + $ret .= ''; $ret .= "$calias".FW_makeImage($cicon, ''); $ret .= ' '; - + $pos_left += $consDist; } } @@ -7694,14 +7916,14 @@ END1 $ret .= '' if ($soc > 88); $ret .= ''; } - + if ($flowgconX) { # Dummy Consumer my $dumcol = $cc_dummy <= 0 ? '@grey' : q{}; # Einfärbung Consumer Dummy - $ret .= ''; + $ret .= ''; $ret .= "consumer_X".FW_makeImage('light_light_dim_100'.$dumcol, ''); $ret .= ' '; } - + $ret .= << "END2"; @@ -7712,106 +7934,145 @@ END2 if ($hasbat) { $ret .= << "END3"; - + END3 } - - if ($flowgconX) { # Dummy Consumer + + if ($flowgconX) { # Dummy Consumer my $consumer_style = 'flowg inactive_out'; $consumer_style = 'flowg active_in' if($cc_dummy > 1); - + my $chain_color = ""; # Farbe der Laufkette Consumer-Dummy if($cc_dummy > 0.5) { $chain_color = 'style="stroke: #'.substr(Color::pahColor(0,500,1000,$cc_dummy,[0,255,0, 127,255,0, 255,255,0, 255,127,0, 255,0,0]),0,6).';"'; #$chain_color = 'style="stroke: #DF0101;"'; } - - $ret .= qq{}; + + $ret .= qq{}; } - + ## Consumer Laufketten ######################## if ($flowgcons) { $pos_left = $consumer_start * 2; my $pos_left_start = 0; my $distance = 25; - + if ($consumercount % 2) { - $pos_left_start = 700 - ($distance * (($consumercount -1) / 2)); - } + $pos_left_start = 700 - ($distance * (($consumercount -1) / 2)); + } else { $pos_left_start = 700 - ((($distance ) / 2) * ($consumercount-1)); } - - for my $c1 (@consumers) { + + for my $c1 (@consumers) { my $power = ConsumerVal ($hash, $c1, "power", 0); my $rpcurr = ConsumerVal ($hash, $c1, "rpcurr", ""); # Reading für akt. Verbrauch angegeben ? $currentPower = ReadingsNum ($name, "consumer${c1}_currentPower", 0); - + if (!$rpcurr && isConsumerPhysOn($hash, $c1)) { # Workaround wenn Verbraucher ohne Leistungsmessung $currentPower = $power; } - - my $p = $currentPower; + + my $p = $currentPower; $p = (($currentPower / $power) * 100) if ($power > 0); - + my $consumer_style = 'flowg inactive_out'; $consumer_style = 'flowg active_out' if($p > $defpopercent); my $chain_color = ""; # Farbe der Laufkette des Consumers - + if($p > 0.5) { $chain_color = 'style="stroke: #'.substr(Color::pahColor(0,50,100,$p,[0,255,0, 127,255,0, 255,255,0, 255,127,0, 255,0,0]),0,6).';"'; #$chain_color = 'style="stroke: #DF0101;"'; } - + $ret .= qq{}; # Design Consumer Laufkette - + $pos_left += ($consDist * 2); $pos_left_start += $distance; - } + } } - - ## Angaben Dummy-Verbraucher + + ## Angaben Dummy-Verbraucher ############################# $cc_dummy = sprintf("%.0f",$cc_dummy); - - ## Textangaben an Grafikelementen + + ## Textangaben an Grafikelementen ################################### $ret .= qq{$cpv} if ($cpv); - #$ret .= qq{$soc %} if ($hasbat); - $ret .= qq{$soc %} if ($hasbat); # Lage Ladungs Text + $ret .= qq{$soc %} if ($hasbat); # Lage Ladungs Text $ret .= qq{$csc} if ($csc && $cpv); $ret .= qq{$cgfi} if ($cgfi); - $ret .= qq{$cgc} if ($cgc); - $ret .= qq{$batout} if ($batout && $hasbat); - $ret .= qq{$batin} if ($batin && $hasbat); + $ret .= qq{$cgc} if ($cgc); + $ret .= qq{$batout} if ($batout && $hasbat); + $ret .= qq{$batin} if ($batin && $hasbat); $ret .= qq{$cc}; # Current_Consumption Anlage - #$ret .= qq{$cc_dummy} if ($flowgconX && $flowgconPower); # Current_Consumption Dummy $ret .= qq{$cc_dummy} if ($flowgconX && $flowgconPower); # Current_Consumption Dummy - + ## Consumer Anzeige ##################### if ($flowgcons) { $pos_left = ($consumer_start * 2) - 50; # -XX -> Start Lage Consumer Beschriftung - - for my $c2 (@consumers) { + + for my $c2 (@consumers) { $currentPower = sprintf("%.1f", ReadingsNum($name, "consumer${c2}_currentPower", 0)); + $currentPower =~ s/\.0$// if (int($currentPower) > 0); # .0 am Ende interessiert nicht my $consumerTime = ConsumerVal ($hash, $c2, "remainTime", ""); # Restlaufzeit my $rpcurr = ConsumerVal ($hash, $c2, "rpcurr", ""); # Readingname f. current Power - + if (!$rpcurr) { # Workaround wenn Verbraucher ohne Leistungsmessung $currentPower = isConsumerPhysOn($hash, $c2) ? 'on' : 'off'; } + + #$ret .= qq{$currentPower} if ($flowgconPower); # Lage Consumer Consumption + #$ret .= qq{$consumerTime} if ($flowgconTime); # Lage Consumer Restlaufzeit + + # Verbrauchszahl abhängig von der Größe entsprechend auf der x-Achse verschieben + # Hackeritis - geht mit Sicherheit auch einfacher/sinnvoller + if (length($currentPower) >= 5) { + $pos_left -= 40; + } + elsif (length($currentPower) >= 4) { + $pos_left -= 25; + } + elsif (length($currentPower) >= 3 and $currentPower ne "0.0") { + $pos_left -= 5; + } + elsif (length($currentPower) >= 2 and $currentPower ne "0.0") { + $pos_left += 7; + } + elsif (length($currentPower) == 1) { + $pos_left += 25; + } + + $ret .= qq{$currentPower} if ($flowgconPower); # Lage Consumer Consumption + $ret .= qq{$consumerTime} if ($flowgconTime); # Lage Consumer Restlaufzeit + + # Verbrauchszahl abhängig von der Größe entsprechend auf der x-Achse wieder zurück an den Ursprungspunkt + # Hackeritis - geht mit Sicherheit auch einfacher/sinnvoller + if (length($currentPower) >= 5) { + $pos_left += 40; + } + elsif (length($currentPower) >= 4) { + $pos_left += 25; + } + elsif (length($currentPower) >= 3 and $currentPower ne "0.0") { + $pos_left += 5; + } + elsif (length($currentPower) >= 2 and $currentPower ne "0.0") { + $pos_left -= 7; + } + elsif (length($currentPower) == 1) { + $pos_left -= 25; + } - $ret .= qq{$currentPower} if ($flowgconPower); # Lage Consumer Consumption - $ret .= qq{$consumerTime} if ($flowgconTime); # Lage Consumer Restlaufzeit $pos_left += ($consDist * 2); } } $ret .= qq{}; - + return $ret; } @@ -7820,27 +8081,27 @@ return $ret; # und setze ggf. Ersatzwerte # $c - Consumer Nummer ################################################################ -sub substConsumerIcon { +sub substConsumerIcon { my $hash = shift; my $c = shift; - + my $name = $hash->{NAME}; my $cicon = ConsumerVal ($hash, $c, "icon", ""); # Icon des Consumerdevices angegeben ? - + if (!$cicon) { - $cicon = 'light_light_dim_100'; + $cicon = 'light_light_dim_100'; } - + my $color; - ($cicon,$color) = split '@', $cicon; - + ($cicon,$color) = split '@', $cicon; + if (!$color) { $color = isConsumerPhysOn($hash, $c) ? 'darkorange' : ''; } - + $cicon .= '@'.$color if($color); - + return $cicon; } @@ -7852,17 +8113,22 @@ sub consinject { my $name = $hash->{NAME}; my $ret = ""; + my $debug = AttrVal ($name, 'ctrlDebug', 'none'); + for (@consumers) { if ($_) { my ($cons,$im,$start,$end) = split (':', $_); - Log3 ($name, 4, "$name - Consumer to show -> $cons, relative to current time -> start: $start, end: $end") if($i<1); - + + if($debug =~ /graphic/x) { + Log (1, qq{$name DEBUG> Consumer to show -> $cons, relative to current time -> start: $start, end: $end}) if($i<1); + } + if ($im && ($i >= $start) && ($i <= $end)) { $ret .= FW_makeImage($im); } } } - + return $ret; } @@ -7870,11 +8136,11 @@ return $ret; # Balkenbreite normieren # # Die Balkenbreite wird bestimmt durch den Wert. -# Damit alle Balken die gleiche Breite bekommen, müssen die Werte auf +# Damit alle Balken die gleiche Breite bekommen, müssen die Werte auf # 6 Ausgabezeichen angeglichen werden. -# "align=center" gleicht gleicht es aus, alternativ könnte man sie auch +# "align=center" gleicht gleicht es aus, alternativ könnte man sie auch # komplett rechtsbündig ausgeben. -# Es ergibt bei fast allen Styles gute Ergebnisse, Ausnahme IOS12 & 6, da diese +# Es ergibt bei fast allen Styles gute Ergebnisse, Ausnahme IOS12 & 6, da diese # beiden Styles einen recht großen Font benutzen. # Wird Wetter benutzt, wird die Balkenbreite durch das Icon bestimmt # @@ -7897,22 +8163,22 @@ sub formatVal6 { my $t = $v - int($v); # Nachkommstelle ? if(!$t) { # glatte Zahl ohne Nachkommastelle - if(!$v) { + if(!$v) { return ' '; # 0 nicht anzeigen, passt eigentlich immer bis auf einen Fall im Typ diff - } - elsif ($v < 10) { - return '  '.$n.$v.'  '; - } - else { - return '  '.$n.$v.' '; } - } + elsif ($v < 10) { + return '  '.$n.$v.'  '; + } + else { + return '  '.$n.$v.' '; + } + } else { # mit Nachkommastelle -> zwei Zeichen mehr .X - if ($v < 10) { - return ' '.$n.$v.' '; - } - else { - return $n.$v.' '; + if ($v < 10) { + return ' '.$n.$v.' '; + } + else { + return $n.$v.' '; } } } @@ -7920,7 +8186,7 @@ sub formatVal6 { return ($n eq '-') ? ($v*-1) : $v if defined($w); # Werte bleiben in Watt - if (!$v) { return ' '; } ## no critic "Cascading" # keine Anzeige bei Null + if (!$v) { return ' '; } ## no critic "Cascading" # keine Anzeige bei Null elsif ($v < 10) { return '  '.$n.$v.'  '; } # z.B. 0 elsif ($v < 100) { return ' '.$n.$v.'  '; } elsif ($v < 1000) { return ' '.$n.$v.' '; } @@ -7932,17 +8198,17 @@ sub formatVal6 { # Zuordungstabelle "WeatherId" angepasst auf FHEM Icons ############################################################################### sub weather_icon { - my $id = shift; + my $name = shift; + my $lang = shift; + my $id = shift; $id = int $id; - my $lang = AttrVal ("global", "language", "EN"); - my $txt = $lang eq "DE" ? "txtd" : "txte"; - + if(defined $weather_ids{$id}) { return $weather_ids{$id}{icon}, encode("utf8", $weather_ids{$id}{$txt}); } - + return 'unknown',''; } @@ -7953,28 +8219,28 @@ sub checkdwdattr { my $name = shift; my $dwddev = shift; my $amref = shift; - + my @fcprop = map { trim($_) } split ",", AttrVal($dwddev, "forecastProperties", "pattern"); my $fcr = AttrVal($dwddev, "forecastResolution", 3); my $err; - + my @aneeded; for my $am (@$amref) { next if($am ~~ @fcprop); push @aneeded, $am; } - + if (@aneeded) { $err = qq{ERROR - device "$dwddev" -> attribute "forecastProperties" must contain: }.join ",",@aneeded; } - + if($fcr != 1) { $err .= ", " if($err); $err .= qq{ERROR - device "$dwddev" -> attribute "forecastResolution" must be set to "1"}; } - + Log3 ($name, 2, "$name - $err") if($err); - + return $err; } @@ -7982,14 +8248,14 @@ return $err; # ist Batterie installiert ? # 1 - ja, 0 - nein ################################################################ -sub useBattery { +sub useBattery { my $name = shift; my $badev = ReadingsVal($name, "currentBatteryDev", ""); # aktuelles Meter device für Batteriewerte my ($a,$h) = parseParams ($badev); $badev = $a->[0] // ""; return if(!$badev || !$defs{$badev}); - + return ($badev, $a ,$h); } @@ -7997,13 +8263,13 @@ return ($badev, $a ,$h); # wird PV Autokorrektur verwendet ? # 1 - ja, 0 - nein ################################################################ -sub useAutoCorrection { +sub useAutoCorrection { my $name = shift; my $dcauto = ReadingsVal ($name, 'pvCorrectionFactor_Auto', 'off'); return 1 if($dcauto =~ /^on/xs); - + return; } @@ -8011,26 +8277,26 @@ return; # Korrekturen und Qualität berechnen / speichern # bei useAutoCorrection ################################################################ -sub calcCorrAndQuality { +sub calcCorrAndQuality { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $t = $paref->{t}; # aktuelle Unix-Zeit return if(!useAutoCorrection ($name)); # nur bei "on" automatische Varianzkalkulation - + my $idts = ReadingsTimestamp($name, "currentInverterDev", ""); # Definitionstimestamp des Inverterdevice return if(!$idts); - + $idts = timestringToTimestamp ($idts); if($t - $idts < 7200) { my $rmh = sprintf "%.1f", ((7200 - ($t - $idts)) / 3600); - - Log3 ($name, 4, "$name - Variance calculation in standby. It starts in $rmh hours."); - - readingsSingleUpdate ($hash, "pvCorrectionFactor_Auto", "on (remains in standby for $rmh hours)", 0); - return; + + Log3 ($name, 4, "$name - Variance calculation in standby. It starts in $rmh hours."); + + readingsSingleUpdate ($hash, "pvCorrectionFactor_Auto", "on (remains in standby for $rmh hours)", 0); + return; } else { readingsSingleUpdate($hash, "pvCorrectionFactor_Auto", "on", 0); @@ -8038,54 +8304,58 @@ sub calcCorrAndQuality { _calcCAQfromDWDcloudcover ($paref); _calcCAQwithSolCastPercentil ($paref); - + return; } ################################################################ # Korrekturfaktoren und Qualität in Abhängigkeit von DWD # Bewölkung errechnen: -# Abweichung PVreal / PVforecast bei eingeschalteter automat. +# Abweichung PVreal / PVforecast bei eingeschalteter automat. # Korrektur berechnen, im Circular Hash speichern ################################################################ -sub _calcCAQfromDWDcloudcover { +sub _calcCAQfromDWDcloudcover { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $chour = $paref->{chour}; my $daref = $paref->{daref}; - + return if(isSolCastUsed ($hash)); - + my $maxvar = AttrVal($name, 'affectMaxDayVariance', $defmaxvar); # max. Korrekturvarianz + my $debug = $paref->{debug}; for my $h (1..23) { next if(!$chour || $h > $chour); - + my $fcval = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$h)."_PVforecast", 0); next if(!$fcval); - + my $pvval = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$h)."_PVreal", 0); next if(!$pvval); - + my $cdone = ReadingsVal ($name, "pvCorrectionFactor_".sprintf("%02d",$h)."_autocalc", ""); - + if($cdone eq "done") { - Log3 ($name, 5, "$name - pvCorrectionFactor Hour: ".sprintf("%02d",$h)." already calculated"); + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> pvCorrectionFactor Hour: ".sprintf("%02d",$h)." already calculated"); + } + next; } - + $paref->{hour} = $h; my ($pvhis,$fchis,$dnum,$range) = __avgCloudcoverCorrFromHistory ($paref); # historische PV / Forecast Vergleichswerte ermitteln - - my ($oldfac, $oldq) = CircularAutokorrVal ($hash, sprintf("%02d",$h), $range, 0); # bisher definierter Korrekturfaktor/KF-Qualität der Stunde des Tages der entsprechenden Bewölkungsrange + + my ($oldfac, $oldq) = CircularAutokorrVal ($hash, sprintf("%02d",$h), $range, 0); # bisher definierter Korrekturfaktor/KF-Qualität der Stunde des Tages der entsprechenden Bewölkungsrange $oldfac = 1 if(1 * $oldfac == 0); - + my $factor; my ($usenhd) = __useNumHistDays ($name); # ist Attr affectNumHistDays gesetzt ? - + if($dnum) { # Werte in History vorhanden -> haben Prio ! - $dnum = $dnum + 1; + $dnum = $dnum + 1; $pvval = ($pvval + $pvhis) / $dnum; # Ertrag aktuelle Stunde berücksichtigen $fcval = ($fcval + $fchis) / $dnum; # Vorhersage aktuelle Stunde berücksichtigen $factor = sprintf "%.2f", ($pvval / $fcval); # Faktorberechnung: reale PV / Prognose @@ -8097,11 +8367,13 @@ sub _calcCAQfromDWDcloudcover { } else { # ganz neuer Wert $factor = sprintf "%.2f", ($pvval / $fcval); - $dnum = 1; + $dnum = 1; } - - Log3 ($name, 4, "$name - variance -> range: $range, hour: $h, days: $dnum, real: $pvval, forecast: $fcval, factor: $factor"); - + + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> variance -> range: $range, hour: $h, days: $dnum, real: $pvval, forecast: $fcval, factor: $factor"); + } + if(abs($factor - $oldfac) > $maxvar) { $factor = sprintf "%.2f", ($factor > $oldfac ? $oldfac + $maxvar : $oldfac - $maxvar); Log3 ($name, 3, "$name - new limited Variance factor: $factor (old: $oldfac) for hour: $h"); @@ -8109,115 +8381,137 @@ sub _calcCAQfromDWDcloudcover { else { Log3 ($name, 3, "$name - new Variance factor: $factor (old: $oldfac) for hour: $h calculated") if($factor != $oldfac); } - + if(defined $range) { - my $type = $paref->{type}; - - Log3 ($name, 5, "$name - write correction factor into circular Hash: Factor $factor, Hour $h, Range $range"); - + my $type = $paref->{type}; + + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> write correction factor into circular Hash: Factor $factor, Hour $h, Range $range"); + } + $data{$type}{$name}{circular}{sprintf("%02d",$h)}{pvcorrf}{$range} = $factor; # Korrekturfaktor für Bewölkung Range 0..10 für die jeweilige Stunde als Datenquelle eintragen $data{$type}{$name}{circular}{sprintf("%02d",$h)}{quality}{$range} = $dnum; # Korrekturfaktor Qualität } else { $range = ""; } - + push @$daref, "pvCorrectionFactor_".sprintf("%02d",$h)."<>".$factor." (automatic - old factor: $oldfac, cloudiness range: $range, days in range: $dnum)"; - push @$daref, "pvCorrectionFactor_".sprintf("%02d",$h)."_autocalc<>done"; + push @$daref, "pvCorrectionFactor_".sprintf("%02d",$h)."_autocalc<>done"; } - + return; } ################################################################ -# Berechne Durchschnitte PV Vorhersage / PV Ertrag +# Berechne Durchschnitte PV Vorhersage / PV Ertrag # aus Werten der PV History ################################################################ -sub __avgCloudcoverCorrFromHistory { +sub __avgCloudcoverCorrFromHistory { my $paref = shift; - my $hash = $paref->{hash}; - my $name = $paref->{name}; - my $type = $paref->{type}; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $type = $paref->{type}; my $hour = $paref->{hour}; # Stunde des Tages für die der Durchschnitt bestimmt werden soll my $day = $paref->{day}; # aktueller Tag - + $hour = sprintf("%02d",$hour); + my $debug = $paref->{debug}; my $pvhh = $data{$type}{$name}{pvhist}; - + my ($usenhd, $calcd) = __useNumHistDays ($name); # ist Attr affectNumHistDays gesetzt ? und welcher Wert my @k = sort {$a<=>$b} keys %{$pvhh}; my $ile = $#k; # Index letztes Arrayelement my ($idx) = grep {$k[$_] eq "$day"} (0..@k-1); # Index des aktuellen Tages - + if(defined $idx) { my $ei = $idx-1; $ei = $ei < 0 ? $ile : $ei; my @efa; - + for my $e (0..$calcmaxd) { last if($e == $calcmaxd || $k[$ei] == $day); unshift @efa, $k[$ei]; $ei--; } - + my $chwcc = HistoryVal ($hash, $day, $hour, "wcc", undef); # Wolkenbedeckung Heute & abgefragte Stunde - + if(!defined $chwcc) { - Log3 ($name, 4, "$name - Day $day has no cloudiness value set for hour $hour, no past averages can be calculated."); + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> Day $day has no cloudiness value set for hour $hour, no past averages can be calculated"); + } return; } - - my $range = calcRange ($chwcc); # V 0.50.1 - + + my $range = calcRange ($chwcc); # V 0.50.1 + if(scalar(@efa)) { - Log3 ($name, 4, "$name - PV History -> Raw Days ($calcmaxd) for average check: ".join " ",@efa); + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> Raw Days ($calcmaxd) for average check: ".join " ",@efa); + } } else { # vermeide Fehler: Illegal division by zero - Log3 ($name, 4, "$name - PV History -> Day $day has index $idx. Use only current day for average calc"); + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> Day $day has index $idx. Use only current day for average calc"); + } return (undef,undef,undef,$range); - } - - Log3 ($name, 4, "$name - cloudiness range of day/hour $day/$hour is: $range"); - - my $dnum = 0; + } + + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> cloudiness range of day/hour $day/$hour is: $range"); + } + + my $dnum = 0; my ($pvrl,$pvfc) = (0,0); - + for my $dayfa (@efa) { my $histwcc = HistoryVal ($hash, $dayfa, $hour, "wcc", undef); # historische Wolkenbedeckung - - if(!defined $histwcc) { - Log3 ($name, 4, "$name - PV History -> Day $dayfa has no cloudiness value set for hour $hour, this history dataset is ignored."); - next; - } - - $histwcc = calcRange ($histwcc); # V 0.50.1 - if($range == $histwcc) { + if(!defined $histwcc) { + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> Day $dayfa has no cloudiness value set for hour $hour, this history dataset is ignored."); + } + next; + } + + $histwcc = calcRange ($histwcc); # V 0.50.1 + + if($range == $histwcc) { $pvrl += HistoryVal ($hash, $dayfa, $hour, "pvrl", 0); $pvfc += HistoryVal ($hash, $dayfa, $hour, "pvfc", 0); $dnum++; - Log3 ($name, 5, "$name - PV History -> historical Day/hour $dayfa/$hour included - cloudiness range: $range"); + + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> historical Day/hour $dayfa/$hour included - cloudiness range: $range"); + } + last if( $dnum == $calcd); } else { - Log3 ($name, 5, "$name - PV History -> current/historical cloudiness range different: $range/$histwcc Day/hour $dayfa/$hour discarded."); + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> current/historical cloudiness range different: $range/$histwcc Day/hour $dayfa/$hour discarded."); + } } } - + if(!$dnum) { - Log3 ($name, 5, "$name - PV History -> all cloudiness ranges were different/not set -> no historical averages calculated"); + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> all cloudiness ranges were different/not set -> no historical averages calculated"); + } return (undef,undef,undef,$range); } - + my $pvhis = sprintf "%.2f", $pvrl; my $fchis = sprintf "%.2f", $pvfc; - - Log3 ($name, 5, "$name - PV History -> Summary - cloudiness range: $range, days: $dnum, pvHist:$pvhis, fcHist:$fchis"); - + + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> Summary - cloudiness range: $range, days: $dnum, pvHist:$pvhis, fcHist:$fchis"); + } return ($pvhis,$fchis,$dnum,$range); } - + return; } @@ -8226,202 +8520,211 @@ return; # $usenhd: 1 - ja, 0 - nein # $nhd : Anzahl der zu verwendenden HistDays ################################################################ -sub __useNumHistDays { +sub __useNumHistDays { my $name = shift; my $usenhd = 0; my $nhd = AttrVal($name, 'affectNumHistDays', $calcmaxd+1); - + if($nhd == $calcmaxd+1) { $nhd = $calcmaxd; } else { $usenhd = 1; } - + return ($usenhd, $nhd); } ################################################################ -# PVreal mit den SolCast Forecast vergleichen und den +# PVreal mit den SolCast Forecast vergleichen und den # Korrekturfaktor berechnen / speichern ################################################################ -sub _calcCAQwithSolCastPercentil { +sub _calcCAQwithSolCastPercentil { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $chour = $paref->{chour}; # aktuelle Stunde my $date = $paref->{date}; my $daref = $paref->{daref}; - + return if(!isSolCastUsed ($hash)); - - my $debug = AttrVal ($name, 'ctrlDebug', 0); - + + my $debug = $paref->{debug}; + for my $h (1..23) { next if(!$chour || $h > $chour); - + my $pvval = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$h)."_PVreal", 0); next if(!$pvval); - + my $cdone = ReadingsVal ($name, "pvCorrectionFactor_".sprintf("%02d",$h)."_autocalc", ""); - + if($cdone eq "done") { Log3 ($name, 5, "$name - pvCorrectionFactor Hour: ".sprintf("%02d",$h)." already calculated"); next; } - + $paref->{hour} = $h; my ($dnum,$avgperc) = __avgSolCastPercFromHistory ($paref); # historischen Percentilfaktor / Qualität ermitteln - - my ($oldperc, $oldq) = CircularAutokorrVal ($hash, sprintf("%02d",$h), 'percentile', 1.0); # bisher definiertes Percentil/Qualität der Stunde des Tages der entsprechenden Bewölkungsrange + + my ($oldperc, $oldq) = CircularAutokorrVal ($hash, sprintf("%02d",$h), 'percentile', 1.0); # bisher definiertes Percentil/Qualität der Stunde des Tages der entsprechenden Bewölkungsrange $oldperc = 1.0 if(1 * $oldperc == 0 || $oldperc >= 10); - + my @sts = split ",", ReadingsVal($name, 'inverterStrings', ''); my $tmstr = $date.' '.sprintf("%02d",$h-1).':00:00'; - + my $est50 = 0; for my $s (@sts) { $est50 += SolCastAPIVal ($hash, $s, $tmstr, 'pv_estimate50', 0); # Standardpercentil - } - - if(!$est50) { # kein Standardpercentile vorhanden - Log (1, qq{DEBUG> $name percentile -> hour: $h, the percentile factor can't be calculated because of the default percentile has no value yet}) if($debug); - next; } - - my $perc = sprintf "%.2f", ($pvval / $est50); # berechneter Faktor der Stunde -> speichern in pvHistory - + + if(!$est50) { # kein Standardpercentile vorhanden + if($debug =~ /pvCorrection/x) { + Log (1, qq{$name DEBUG> percentile -> hour: $h, the correction factor can't be calculated because of the default percentile has no value yet}); + } + next; + } + + my $perc = sprintf "%.2f", ($pvval / $est50); # berechneter Faktor der Stunde -> speichern in pvHistory + $paref->{pvcorrf} = $perc.'/1'; # Percentilfaktor in History speichern $paref->{nhour} = sprintf("%02d",$h); $paref->{histname} = "pvcorrfactor"; - + setPVhistory ($paref); - + delete $paref->{histname}; - delete $paref->{nhour}; - delete $paref->{pvcorrf}; - - if ($debug) { # nur für Debugging - Log (1, qq{DEBUG> $name PV estimates for hour of day "$h": $est50}); - Log (1, qq{DEBUG> $name percentile factor -> number checked days: $dnum, pvreal: $pvval, percentile factor: $perc}); - } - + delete $paref->{nhour}; + delete $paref->{pvcorrf}; + + if ($debug =~ /pvCorrection/x) { # nur für Debugging + Log (1, qq{$name DEBUG> PV estimates for hour of day "$h": $est50}); + Log (1, qq{$name DEBUG> correction factor -> number checked days: $dnum, pvreal: $pvval, correction factor: $perc}); + } + my ($usenhd) = __useNumHistDays ($name); # ist Attr affectNumHistDays gesetzt ? - + if($dnum) { # Werte in History vorhanden -> haben Prio ! $avgperc = $avgperc * $dnum; - $dnum++; + $dnum++; $perc = sprintf "%.2f", (($avgperc + $perc) / $dnum); - - if ($debug) { - Log (1, qq{DEBUG> $name percentile -> old avg percentile: }.($avgperc/($dnum-1)).qq{, new avg percentile: }.$perc); - } + + if ($debug =~ /pvCorrection/x) { + Log (1, qq{$name DEBUG> percentile -> old avg correction: }.($avgperc/($dnum-1)).qq{, new avg correction: }.$perc); + } } elsif($oldperc && !$usenhd) { # keine Werte in History vorhanden, aber in CircularVal && keine Beschränkung durch Attr affectNumHistDays $oldperc = $oldperc * $oldq; $dnum = $oldq + 1; $perc = sprintf "%.0f", (($oldperc + $perc) / $dnum); - - if ($debug) { - Log (1, qq{DEBUG> $name percentile -> old circular percentile: }.($oldperc/$oldq).qq{, new percentile: }.$perc * 10); + + if ($debug =~ /pvCorrection/x) { + Log (1, qq{$name DEBUG> percentile -> old circular correction: }.($oldperc/$oldq).qq{, new correction: }.$perc); } } else { # ganz neuer Wert $dnum = 1; - if ($debug) { - Log (1, qq{DEBUG> $name percentile -> new percentile: }.$perc); - } + if ($debug =~ /pvCorrection/x) { + Log (1, qq{$name DEBUG> percentile -> new correction factor: }.$perc); + } } - - Log3 ($name, 5, "$name - write percentile into circular Hash: $perc, Hour $h"); - - my $type = $paref->{type}; - + + Log3 ($name, 5, "$name - write correction factor into circular Hash: $perc, Hour $h"); + + my $type = $paref->{type}; + $data{$type}{$name}{circular}{sprintf("%02d",$h)}{pvcorrf}{percentile} = $perc; # bestes Percentil für die jeweilige Stunde speichern $data{$type}{$name}{circular}{sprintf("%02d",$h)}{quality}{percentile} = $dnum; # Percentil Qualität - + push @$daref, "pvCorrectionFactor_".sprintf("%02d",$h)."<>".$perc." (automatic - old factor: $oldperc, average days: $dnum)"; - push @$daref, "pvCorrectionFactor_".sprintf("%02d",$h)."_autocalc<>done"; + push @$daref, "pvCorrectionFactor_".sprintf("%02d",$h)."_autocalc<>done"; } - + return; } ################################################################ -# Berechne das durchschnittlich verwendete Percentil +# Berechne das durchschnittlich verwendete Percentil # aus Werten der PV History ################################################################ -sub __avgSolCastPercFromHistory { +sub __avgSolCastPercFromHistory { my $paref = shift; my $hash = $paref->{hash}; - my $name = $paref->{name}; - my $type = $paref->{type}; + my $name = $paref->{name}; + my $type = $paref->{type}; my $hour = $paref->{hour}; # Stunde des Tages für die der Durchschnitt bestimmt werden soll my $day = $paref->{day}; # aktueller Tag - - $hour = sprintf("%02d",$hour); + + $hour = sprintf("%02d",$hour); + my $debug = $paref->{debug}; my $pvhh = $data{$type}{$name}{pvhist}; - + my ($usenhd, $calcd) = __useNumHistDays ($name); # ist Attr affectNumHistDays gesetzt ? und welcher Wert my @k = sort {$a<=>$b} keys %{$pvhh}; my $ile = $#k; # Index letztes Arrayelement my ($idx) = grep {$k[$_] eq "$day"} (0..@k-1); # Index des aktuellen Tages - + return 0 if(!defined $idx); - + my $ei = $idx-1; $ei = $ei < 0 ? $ile : $ei; my @efa; - + for my $e (0..$calcmaxd) { last if($e == $calcmaxd || $k[$ei] == $day); unshift @efa, $k[$ei]; $ei--; } - + if(scalar(@efa)) { - Log3 ($name, 4, "$name - PV History -> Raw Days ($calcmaxd) for average check: ".join " ",@efa); + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> Raw Days ($calcmaxd) for average check: ".join " ",@efa); + } } else { # vermeide Fehler: Illegal division by zero - Log3 ($name, 4, "$name - PV History -> Day $day has index $idx. Use only current day for average calc"); + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> Day $day has index $idx. Use only current day for average calc"); + } return 0; - } - - my ($dnum, $percsum) = (0,0); + } + + my ($dnum, $percsum) = (0,0); my ($perc, $qual) = (1,0); - + for my $dayfa (@efa) { my $histval = HistoryVal ($hash, $dayfa, $hour, 'pvcorrf', undef); # historischen Percentilfaktor/Qualität - + next if(!defined $histval); ($perc, $qual) = split "/", $histval; # Percentilfaktor und Qualität splitten next if(!$perc || $qual eq 'm'); # manuell eingestellte Percentile überspringen - + $perc = 1 if(!$perc || $perc >= 10); - - Log3 ($name, 5, qq{$name - PV History -> historical Day/hour $dayfa/$hour included - percentile factor: $perc}); - + + if($debug =~ /pvCorrection/x) { + Log (1, "$name DEBUG> PV History -> historical Day/hour $dayfa/$hour included - percentile factor: $perc"); + } + $dnum++; $percsum += $perc ; - + last if($dnum == $calcd); } - + if(!$dnum) { Log3 ($name, 5, "$name - PV History -> no historical percentile factor selected"); return 0; } - + $perc = sprintf "%.2f", ($percsum/$dnum); - + Log3 ($name, 5, "$name - PV History -> Summary - days: $dnum, average percentile factor: $perc"); - + return ($dnum,$perc); } @@ -8430,18 +8733,18 @@ return ($dnum,$perc); ################################################################ sub calcRange { my $range = shift; - - #$range = sprintf "%.0f", $range/10; - $range = sprintf "%.0f", $range; + + #$range = sprintf "%.0f", $range/10; + $range = sprintf "%.0f", $range; return $range; } ################################################################ -# PV und PV Forecast in History-Hash speichern zur +# PV und PV Forecast in History-Hash speichern zur # Berechnung des Korrekturfaktors über mehrere Tage ################################################################ -sub setPVhistory { +sub setPVhistory { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; @@ -8455,12 +8758,12 @@ sub setPVhistory { my $batinthishour = $paref->{batinthishour}; # Batterieladung in Stunde my $btotin = $paref->{batintotal}; # totale Batterieladung my $batoutthishour = $paref->{batoutthishour}; # Batterieentladung in Stunde - my $btotout = $paref->{batouttotal}; # totale Batterieentladung + my $btotout = $paref->{batouttotal}; # totale Batterieentladung my $calcpv = $paref->{calcpv} // 0; my $gcthishour = $paref->{gctotthishour} // 0; # Netzbezug my $fithishour = $paref->{gftotthishour} // 0; # Netzeinspeisung my $con = $paref->{con} // 0; # realer Hausverbrauch Energie - my $confc = $paref->{confc} // 0; # Verbrauchsvorhersage + my $confc = $paref->{confc} // 0; # Verbrauchsvorhersage my $consumerco = $paref->{consumerco}; # Verbrauch eines Verbrauchers my $wid = $paref->{wid} // -1; my $wcc = $paref->{wcc} // 0; # Wolkenbedeckung @@ -8468,197 +8771,200 @@ sub setPVhistory { my $pvcorrf = $paref->{pvcorrf} // "1.00/0"; # pvCorrectionFactor my $temp = $paref->{temp}; # Außentemperatur my $val = $paref->{val} // qq{}; # Wert zur Speicherung in pvHistory (soll mal generell verwendet werden -> Change) - + my $type = $hash->{TYPE}; - - $data{$type}{$name}{pvhist}{$day}{99}{dayname} = $dayname; + + $data{$type}{$name}{pvhist}{$day}{99}{dayname} = $dayname; if($histname eq "batinthishour") { # Batterieladung $val = $batinthishour; - $data{$type}{$name}{pvhist}{$day}{$nhour}{batin} = $batinthishour; - + $data{$type}{$name}{pvhist}{$day}{$nhour}{batin} = $batinthishour; + my $batinsum = 0; for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq "99"); $batinsum += HistoryVal ($hash, $day, $k, "batin", 0); } - $data{$type}{$name}{pvhist}{$day}{99}{batin} = $batinsum; + $data{$type}{$name}{pvhist}{$day}{99}{batin} = $batinsum; } - + if($histname eq "batoutthishour") { # Batterieentladung $val = $batoutthishour; - $data{$type}{$name}{pvhist}{$day}{$nhour}{batout} = $batoutthishour; - + $data{$type}{$name}{pvhist}{$day}{$nhour}{batout} = $batoutthishour; + my $batoutsum = 0; for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq "99"); $batoutsum += HistoryVal ($hash, $day, $k, "batout", 0); } - $data{$type}{$name}{pvhist}{$day}{99}{batout} = $batoutsum; + $data{$type}{$name}{pvhist}{$day}{99}{batout} = $batoutsum; } - + if($histname eq "pvrl") { # realer Energieertrag $val = $ethishour; - $data{$type}{$name}{pvhist}{$day}{$nhour}{pvrl} = $ethishour; - + $data{$type}{$name}{pvhist}{$day}{$nhour}{pvrl} = $ethishour; + my $pvrlsum = 0; for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq "99"); $pvrlsum += HistoryVal ($hash, $day, $k, "pvrl", 0); } - $data{$type}{$name}{pvhist}{$day}{99}{pvrl} = $pvrlsum; + $data{$type}{$name}{pvhist}{$day}{99}{pvrl} = $pvrlsum; } - + if($histname eq "pvfc") { # prognostizierter Energieertrag $val = $calcpv; - $data{$type}{$name}{pvhist}{$day}{$nhour}{pvfc} = $calcpv; + $data{$type}{$name}{pvhist}{$day}{$nhour}{pvfc} = $calcpv; my $pvfcsum = 0; for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq "99"); $pvfcsum += HistoryVal ($hash, $day, $k, "pvfc", 0); } - $data{$type}{$name}{pvhist}{$day}{99}{pvfc} = $pvfcsum; + $data{$type}{$name}{pvhist}{$day}{99}{pvfc} = $pvfcsum; } - + if($histname eq "confc") { # prognostizierter Hausverbrauch $val = $confc; - $data{$type}{$name}{pvhist}{$day}{$nhour}{confc} = $confc; + $data{$type}{$name}{pvhist}{$day}{$nhour}{confc} = $confc; my $confcsum = 0; for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq "99"); $confcsum += HistoryVal ($hash, $day, $k, "confc", 0); } - $data{$type}{$name}{pvhist}{$day}{99}{confc} = $confcsum; - } - + $data{$type}{$name}{pvhist}{$day}{99}{confc} = $confcsum; + } + if($histname eq "cons") { # bezogene Energie $val = $gcthishour; - $data{$type}{$name}{pvhist}{$day}{$nhour}{gcons} = $gcthishour; + $data{$type}{$name}{pvhist}{$day}{$nhour}{gcons} = $gcthishour; my $gcsum = 0; for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq "99"); $gcsum += HistoryVal ($hash, $day, $k, "gcons", 0); } - $data{$type}{$name}{pvhist}{$day}{99}{gcons} = $gcsum; + $data{$type}{$name}{pvhist}{$day}{99}{gcons} = $gcsum; } - + if($histname eq "gfeedin") { # eingespeiste Energie $val = $fithishour; - $data{$type}{$name}{pvhist}{$day}{$nhour}{gfeedin} = $fithishour; + $data{$type}{$name}{pvhist}{$day}{$nhour}{gfeedin} = $fithishour; my $gfisum = 0; for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq "99"); $gfisum += HistoryVal ($hash, $day, $k, "gfeedin", 0); } - $data{$type}{$name}{pvhist}{$day}{99}{gfeedin} = $gfisum; + $data{$type}{$name}{pvhist}{$day}{99}{gfeedin} = $gfisum; } - + if($histname eq "con") { # Energieverbrauch des Hauses $val = $con; - $data{$type}{$name}{pvhist}{$day}{$nhour}{con} = $con; + $data{$type}{$name}{pvhist}{$day}{$nhour}{con} = $con; my $consum = 0; for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq "99"); $consum += HistoryVal ($hash, $day, $k, "con", 0); } - $data{$type}{$name}{pvhist}{$day}{99}{con} = $consum; + $data{$type}{$name}{pvhist}{$day}{99}{con} = $consum; } - + if($histname =~ /csm[et][0-9]+$/xs) { # Verbrauch eines Verbrauchers $val = $consumerco; - $data{$type}{$name}{pvhist}{$day}{$nhour}{$histname} = $consumerco; + $data{$type}{$name}{pvhist}{$day}{$nhour}{$histname} = $consumerco; if($histname =~ /csme[0-9]+$/xs) { my $sum = 0; - + for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq "99"); my $csme = HistoryVal ($hash, $day, $k, "$histname", 0); next if(!$csme); - + $sum += $csme; } - - $data{$type}{$name}{pvhist}{$day}{99}{$histname} = $sum; - } + + $data{$type}{$name}{pvhist}{$day}{99}{$histname} = $sum; + } } - + if($histname =~ /cyclescsm[0-9]+$/xs) { # Anzahl Tageszyklen des Verbrauchers - $data{$type}{$name}{pvhist}{$day}{99}{$histname} = $val; + $data{$type}{$name}{pvhist}{$day}{99}{$histname} = $val; } - + if($histname =~ /minutescsm[0-9]+$/xs) { # Anzahl Aktivminuten des Verbrauchers $data{$type}{$name}{pvhist}{$day}{$nhour}{$histname} = $val; my $minutes = 0; my $num = substr ($histname,10,2); - + for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq "99"); my $csmm = HistoryVal ($hash, $day, $k, "$histname", 0); next if(!$csmm); - + $minutes += $csmm; } - + my $cycles = HistoryVal ($hash, $day, 99, "cyclescsm${num}", 0); - $data{$type}{$name}{pvhist}{$day}{99}{"hourscsme${num}"} = ceil ($minutes / 60 ) if($cycles); + $data{$type}{$name}{pvhist}{$day}{99}{"hourscsme${num}"} = ceil ($minutes / 60 ) if($cycles); } - + if($histname eq "etotal") { # etotal des Wechselrichters $val = $etotal; $data{$type}{$name}{pvhist}{$day}{$nhour}{etotal} = $etotal; - $data{$type}{$name}{pvhist}{$day}{99}{etotal} = q{}; + $data{$type}{$name}{pvhist}{$day}{99}{etotal} = q{}; } - + if($histname eq "batintotal") { # totale Batterieladung $val = $btotin; $data{$type}{$name}{pvhist}{$day}{$nhour}{batintotal} = $btotin; - $data{$type}{$name}{pvhist}{$day}{99}{batintotal} = q{}; + $data{$type}{$name}{pvhist}{$day}{99}{batintotal} = q{}; } - + if($histname eq "batouttotal") { # totale Batterieentladung $val = $btotout; $data{$type}{$name}{pvhist}{$day}{$nhour}{batouttotal} = $btotout; - $data{$type}{$name}{pvhist}{$day}{99}{batouttotal} = q{}; + $data{$type}{$name}{pvhist}{$day}{99}{batouttotal} = q{}; } - + if($histname eq "weatherid") { # Wetter ID $val = $wid; $data{$type}{$name}{pvhist}{$day}{$nhour}{weatherid} = $wid; - $data{$type}{$name}{pvhist}{$day}{99}{weatherid} = q{}; + $data{$type}{$name}{pvhist}{$day}{99}{weatherid} = q{}; } - + if($histname eq "weathercloudcover") { # Wolkenbedeckung $val = $wcc; - $data{$type}{$name}{pvhist}{$day}{$nhour}{wcc} = $wcc; - $data{$type}{$name}{pvhist}{$day}{99}{wcc} = q{}; + $data{$type}{$name}{pvhist}{$day}{$nhour}{wcc} = $wcc; + $data{$type}{$name}{pvhist}{$day}{99}{wcc} = q{}; } - + if($histname eq "weatherrainprob") { # Niederschlagswahrscheinlichkeit $val = $wrp; - $data{$type}{$name}{pvhist}{$day}{$nhour}{wrp} = $wrp; - $data{$type}{$name}{pvhist}{$day}{99}{wrp} = q{}; + $data{$type}{$name}{pvhist}{$day}{$nhour}{wrp} = $wrp; + $data{$type}{$name}{pvhist}{$day}{99}{wrp} = q{}; } - + if($histname eq "pvcorrfactor") { # pvCorrectionFactor $val = $pvcorrf; - $data{$type}{$name}{pvhist}{$day}{$nhour}{pvcorrf} = $pvcorrf; - $data{$type}{$name}{pvhist}{$day}{99}{pvcorrf} = q{}; + $data{$type}{$name}{pvhist}{$day}{$nhour}{pvcorrf} = $pvcorrf; + $data{$type}{$name}{pvhist}{$day}{99}{pvcorrf} = q{}; } - + if($histname eq "temperature") { # Außentemperatur $val = $temp; - $data{$type}{$name}{pvhist}{$day}{$nhour}{temp} = $temp; - $data{$type}{$name}{pvhist}{$day}{99}{temp} = q{}; + $data{$type}{$name}{pvhist}{$day}{$nhour}{temp} = $temp; + $data{$type}{$name}{pvhist}{$day}{99}{temp} = q{}; } - - Log3 ($name, 5, "$name - set PV History Day: $day, Hour: $nhour, Key: $histname, Value: $val"); - + + my $debug = $paref->{debug}; + if($debug =~ /saveData2Cache/x) { + Log (1, "$name DEBUG> save PV History Day: $day, Hour: $nhour, Key: $histname, Value: $val"); + } + return; } @@ -8666,18 +8972,18 @@ return; # liefert aktuelle Einträge des in $htol # angegebenen internen Hash ################################################################ -sub listDataPool { +sub listDataPool { my $hash = shift; my $htol = shift; - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + my ($sq,$h); - - my $sub = sub { + + my $sub = sub { my $day = shift; - my $ret; + my $ret; for my $key (sort{$a<=>$b} keys %{$h->{$day}}) { my $pvrl = HistoryVal ($hash, $day, $key, "pvrl", "-"); my $pvfc = HistoryVal ($hash, $day, $key, "pvfc", "-"); @@ -8696,9 +9002,9 @@ sub listDataPool { my $batin = HistoryVal ($hash, $day, $key, "batin", "-"); my $btotout = HistoryVal ($hash, $day, $key, "batouttotal", "-"); my $batout = HistoryVal ($hash, $day, $key, "batout", "-"); - + $ret .= "\n " if($ret); - $ret .= $key." => etotal: $etotal, pvfc: $pvfc, pvrl: $pvrl"; + $ret .= $key." => etotal: $etotal, pvfc: $pvfc, pvrl: $pvrl"; $ret .= "\n "; $ret .= "confc: $confc, con: $con, gcon: $gcon, gfeedin: $gfeedin"; $ret .= "\n "; @@ -8710,7 +9016,7 @@ sub listDataPool { $ret .= ", temp: $temp" if($temp); $ret .= ", pvcorrf: $pvcorrf"; $ret .= ", dayname: $dayname" if($dayname); - + my $csm; for my $c (1..$maxconsumer) { $c = sprintf "%02d", $c; @@ -8720,39 +9026,39 @@ sub listDataPool { my $csme = HistoryVal ($hash, $day, $key, "csme${c}", undef); my $csmm = HistoryVal ($hash, $day, $key, "minutescsm${c}", undef); my $csmh = HistoryVal ($hash, $day, $key, "hourscsme${c}", undef); - + if(defined $csmc) { $csm .= "cyclescsm${c}: $csmc"; $nl = 1; } - + if(defined $csmt) { $csm .= ", " if($nl); $csm .= "csmt${c}: $csmt"; $nl = 1; } - + if(defined $csme) { $csm .= ", " if($nl); $csm .= "csme${c}: $csme"; $nl = 1; } - + if(defined $csmm) { $csm .= ", " if($nl); $csm .= "minutescsm${c}: $csmm"; $nl = 1; } - + if(defined $csmh) { $csm .= ", " if($nl); $csm .= "hourscsme${c}: $csmh"; $nl = 1; } - + $csm .= "\n " if($nl); } - + if($csm) { $ret .= "\n "; $ret .= $csm; @@ -8760,17 +9066,17 @@ sub listDataPool { } return $ret; }; - + if ($htol eq "pvhist") { $h = $data{$type}{$name}{pvhist}; if (!keys %{$h}) { return qq{PV cache is empty.}; } for my $idx (sort{$a<=>$b} keys %{$h}) { - $sq .= $idx." => ".$sub->($idx)."\n"; + $sq .= $idx." => ".$sub->($idx)."\n"; } } - + if ($htol eq "consumer") { $h = $data{$type}{$name}{consumers}; if (!keys %{$h}) { @@ -8780,9 +9086,9 @@ sub listDataPool { if ($i !~ /^[0-9]{2}$/ix) { # bereinigen ungültige consumer, Forum: https://forum.fhem.de/index.php/topic,117864.msg1173219.html#msg1173219 delete $data{$type}{$name}{consumers}{$i}; Log3 ($name, 3, qq{$name - INFO - invalid consumer key "$i" was deleted from consumer Hash}); - } + } } - + for my $idx (sort{$a<=>$b} keys %{$h}) { my $cret; for my $ckey (sort keys %{$h->{$idx}}) { @@ -8799,10 +9105,10 @@ sub listDataPool { } } - $sq .= $idx." => ".$cret."\n"; + $sq .= $idx." => ".$cret."\n"; } - } - + } + if ($htol eq "circular") { $h = $data{$type}{$name}{circular}; if (!keys %{$h}) { @@ -8825,7 +9131,7 @@ sub listDataPool { my $batout = CircularVal ($hash, $idx, "batout", "-"); my $tdayDvtn = CircularVal ($hash, $idx, "tdayDvtn", "-"); my $ydayDvtn = CircularVal ($hash, $idx, "ydayDvtn", "-"); - + my $pvcf = qq{}; if(ref $pvcorrf eq "HASH") { for my $f (sort keys %{$h->{$idx}{pvcorrf}}) { @@ -8838,22 +9144,22 @@ sub listDataPool { else { $pvcf = $pvcorrf; } - + my $cfq = qq{}; if(ref $quality eq "HASH") { for my $q (sort {$a<=>$b} keys %{$h->{$idx}{quality}}) { $cfq .= " " if($cfq); $cfq .= "$q=".$h->{$idx}{quality}{$q}; my $ct1 = ($cfq =~ tr/=// // 0) / 10; - $cfq .= "\n " if($ct1 =~ /^([1-9])?$/); + $cfq .= "\n " if($ct1 =~ /^([1-9])?$/); } } else { $cfq = $quality; } - + $sq .= "\n" if($sq); - + if($idx != 99) { $sq .= $idx." => pvfc: $pvfc, pvrl: $pvrl, batin: $batin, batout: $batout\n"; $sq .= " confc: $confc, gcon: $gcons, gfeedin: $gfeedin, wcc: $wccv, wrp: $wrprb\n"; @@ -8866,7 +9172,7 @@ sub listDataPool { } } } - + if ($htol eq "nexthours") { $h = $data{$type}{$name}{nexthours}; if (!keys %{$h}) { @@ -8884,8 +9190,8 @@ sub listDataPool { my $rad1h = NexthoursVal ($hash, $idx, "Rad1h", "-"); my $pvcorrf = NexthoursVal ($hash, $idx, "pvcorrf", "-"); my $temp = NexthoursVal ($hash, $idx, "temp", "-"); - my $confc = NexthoursVal ($hash, $idx, "confc", "-"); - my $confcex = NexthoursVal ($hash, $idx, "confcEx", "-"); + my $confc = NexthoursVal ($hash, $idx, "confc", "-"); + my $confcex = NexthoursVal ($hash, $idx, "confcEx", "-"); $sq .= "\n" if($sq); $sq .= $idx." => starttime: $nhts, hourofday: $hod, today: $today\n"; $sq .= " pvfc: $pvfc, confc: $confc, confcEx: $confcex\n"; @@ -8893,7 +9199,7 @@ sub listDataPool { $sq .= " Rad1h: $rad1h, crange: $crange, correff: $pvcorrf"; } } - + if ($htol eq "qualities") { $h = $data{$type}{$name}{nexthours}; if (!keys %{$h}) { @@ -8911,7 +9217,7 @@ sub listDataPool { $sq .= "starttime: $nhts, wcc: $neff, crange: $crange, quality: $q, used factor: $f"; } } - + if ($htol eq "current") { $h = $data{$type}{$name}{current}; if (!keys %{$h}) { @@ -8919,68 +9225,68 @@ sub listDataPool { } for my $idx (sort keys %{$h}) { if (ref $h->{$idx} ne "ARRAY") { - $sq .= $idx." => ".$h->{$idx}."\n"; + $sq .= $idx." => ".$h->{$idx}."\n"; } else { my $aser = join " ",@{$h->{$idx}}; - $sq .= $idx." => ".$aser."\n"; - } + $sq .= $idx." => ".$aser."\n"; + } } } - + my $git = sub { my $it = shift; my @sorted = sort keys %$it; my $key = shift @sorted; - + my $ret = {}; $ret = { $key => $it->{$key} } if($key); - + return $ret; }; - + if ($htol eq "solcastdata") { $h = $data{$type}{$name}{solcastapi}; if (!keys %{$h}) { return qq{SolCast API values cache is empty.}; - } + } my $pve = q{}; my $itref = dclone $h; # Deep Copy von $h - + for my $idx (sort keys %{$itref}) { - my $s1; + my $s1; my $sp1 = _ldpspaces ($idx, q{}); $sq .= $idx." => "; - + while (my ($tag, $item) = each %{$git->($itref->{$idx})}) { $sq .= ($s1 ? $sp1 : "").$tag." => "; - + if (ref $item eq "HASH") { - my $s2; + my $s2; my $sp2 = _ldpspaces ($tag, $sp1); - + while (my ($tag1, $item1) = each %{$git->($itref->{$idx}{$tag})}) { - $sq .= ($s2 ? $sp2 : "")."$tag1: ".$item1."\n"; + $sq .= ($s2 ? $sp2 : "")."$tag1: ".$item1."\n"; $s2 = 1; delete $itref->{$idx}{$tag}{$tag1}; } } - - $s1 = 1; + + $s1 = 1; $sq .= "\n" if($sq !~ /\n$/xs); - - delete $itref->{$idx}{$tag}; - } + + delete $itref->{$idx}{$tag}; + } } } - + return $sq; } ################################################################ # Berechnung führende Spaces für Hashanzeige -# $str - String dessen Länge für die Anzahl Spaces +# $str - String dessen Länge für die Anzahl Spaces # herangezogen wird # $sp - vorhandener Space-String der erweitert wird ################################################################ @@ -8988,74 +9294,75 @@ sub _ldpspaces { my $str = shift; my $sp = shift // q{}; my $const = shift // 4; - + my $le = $const + length $str; my $spn = $sp; - + for (my $i = 0; $i < $le; $i++) { $spn .= " "; } - + return $spn; } ################################################################ # validiert die aktuelle Anlagenkonfiguration ################################################################ -sub checkPlantConfig { +sub checkPlantConfig { my $hash = shift; - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - my $lang = AttrVal ("global", 'language', 'EN'); + my $lang = AttrVal ($name, 'ctrlLanguage', AttrVal ('global', 'language', $deflang)); my $cf = 0; # config fault: 1 -> Konfig fehlerhaft, 0 -> Konfig ok my $wn = 0; # Warnung wenn 1 + my $io = 0; # Info wenn 1 my $ok = FW_makeImage('10px-kreis-gruen.png', ''); my $nok = FW_makeImage('10px-kreis-rot.png', ''); my $warn = FW_makeImage('message_attention@orange', ''); my $info = FW_makeImage('message_info', ''); - + my $result = { # Ergebnishash 'String Configuration' => { 'state' => $ok, 'result' => '', 'note' => '', 'info' => 0, 'warn' => 0, 'fault' => 0 }, 'DWD Weather Attributes' => { 'state' => $ok, 'result' => '', 'note' => '', 'info' => 0, 'warn' => 0, 'fault' => 0 }, 'Common Settings' => { 'state' => $ok, 'result' => '', 'note' => '', 'info' => 0, 'warn' => 0, 'fault' => 0 }, }; - - my $sub = sub { + + my $sub = sub { my $string = shift; my $ret; - + for my $key (sort keys %{$data{$type}{$name}{strings}{$string}}) { $ret .= ", " if($ret); $ret .= $key.": ".$data{$type}{$name}{strings}{$string}{$key}; } - + return $ret; }; - + ## Check Strings ################## - + my $err = createStringConfig ($hash); - + if ($err) { $result->{'String Configuration'}{state} = $nok; $result->{'String Configuration'}{result} = $err; $result->{'String Configuration'}{fault} = 1; } - + for my $sn (sort keys %{$data{$type}{$name}{strings}}) { my $sp = $sn." => ".$sub->($sn)."
"; $result->{'String Configuration'}{note} .= $sn." => ".$sub->($sn)."
"; - + if ($data{$type}{$name}{strings}{$sn}{peak} >= 500) { $result->{'String Configuration'}{result} .= qq{The peak value of string "$sn" is very high. }; $result->{'String Configuration'}{result} .= qq{It seems to be given in Wp instead of kWp.
}; $result->{'String Configuration'}{state} = $warn; $result->{'String Configuration'}{warn} = 1; } - + if (!isSolCastUsed ($hash)) { # Strahlungsdevice DWD if ($sp !~ /dir.*?peak.*?tilt/x) { $result->{'String Configuration'}{state} = $nok; @@ -9069,32 +9376,32 @@ sub checkPlantConfig { } } } - - $result->{'String Configuration'}{result} = $hqtxt{fullfd}{$lang} if(!$result->{'String Configuration'}{fault} && !$result->{'String Configuration'}{warn}); - + + $result->{'String Configuration'}{result} = $hqtxt{fulfd}{$lang} if(!$result->{'String Configuration'}{fault} && !$result->{'String Configuration'}{warn}); + ## Check Attribute DWD Wetterdevice ##################################### - my $fcname = ReadingsVal($name, 'currentForecastDev', ''); - + my $fcname = ReadingsVal($name, 'currentForecastDev', ''); + if (!$fcname || !$defs{$fcname}) { $result->{'DWD Weather Attributes'}{state} = $nok; $result->{'DWD Weather Attributes'}{result} .= qq{The DWD device "$fcname" doesn't exist.
}; - $result->{'DWD Weather Attributes'}{fault} = 1; + $result->{'DWD Weather Attributes'}{fault} = 1; } else { - $result->{'DWD Weather Attributes'}{note} = qq{checked attributes of device "$fcname":
}. join ' ', @dweattrmust; + $result->{'DWD Weather Attributes'}{note} = qq{checked attributes of device "$fcname":
}. join ' ', @dweattrmust; $err = checkdwdattr ($name, $fcname, \@dweattrmust); - + if ($err) { $result->{'DWD Weather Attributes'}{state} = $nok; $result->{'DWD Weather Attributes'}{result} = $err; $result->{'DWD Weather Attributes'}{fault} = 1; } else { - $result->{'DWD Weather Attributes'}{result} = $hqtxt{fullfd}{$lang}; + $result->{'DWD Weather Attributes'}{result} = $hqtxt{fulfd}{$lang}; } } - + ## Check Attribute DWD Radiation Device ######################################### if (!isSolCastUsed ($hash)) { @@ -9103,28 +9410,28 @@ sub checkPlantConfig { $result->{'DWD Radiation Attributes'}{note} = ''; $result->{'DWD Radiation Attributes'}{fault} = 0; - my $raname = ReadingsVal($name, 'currentRadiationDev', ''); - + my $raname = ReadingsVal($name, 'currentRadiationDev', ''); + if (!$raname || !$defs{$raname}) { $result->{'DWD Radiation Attributes'}{state} = $nok; $result->{'DWD Radiation Attributes'}{result} .= qq{The DWD device "$raname" doesn't exist
}; - $result->{'DWD Radiation Attributes'}{fault} = 1; + $result->{'DWD Radiation Attributes'}{fault} = 1; } else { - $result->{'DWD Radiation Attributes'}{note} .= qq{checked attributes of device "$raname":
}. join ' ', @draattrmust; + $result->{'DWD Radiation Attributes'}{note} .= qq{checked attributes of device "$raname":
}. join ' ', @draattrmust; $err = checkdwdattr ($name, $raname, \@draattrmust); - + if ($err) { $result->{'DWD Radiation Attributes'}{state} = $nok; $result->{'DWD Radiation Attributes'}{result} = $err; $result->{'DWD Radiation Attributes'}{fault} = 1; } else { - $result->{'DWD Radiation Attributes'}{result} = $hqtxt{fullfd}{$lang}; + $result->{'DWD Radiation Attributes'}{result} = $hqtxt{fulfd}{$lang}; } } } - + ## Check Rooftop und Roof Ident Pair Settings (SolCast) ######################################################### if (isSolCastUsed ($hash)) { @@ -9132,178 +9439,206 @@ sub checkPlantConfig { $result->{'Roof Ident Pair Settings'}{result} = ''; $result->{'Roof Ident Pair Settings'}{note} = ''; $result->{'Roof Ident Pair Settings'}{fault} = 0; - + $result->{'Rooftop Settings'}{state} = $ok; $result->{'Rooftop Settings'}{result} = ''; $result->{'Rooftop Settings'}{note} = ''; $result->{'Rooftop Settings'}{fault} = 0; my $rft = ReadingsVal($name, 'moduleRoofTops', ''); - + if (!$rft) { $result->{'Rooftop Settings'}{state} = $nok; $result->{'Rooftop Settings'}{result} .= qq{No RoofTops are defined
}; $result->{'Rooftop Settings'}{note} .= qq{Set your Rooftops with "set $name moduleRoofTops" command.
}; $result->{'Rooftop Settings'}{fault} = 1; - + $result->{'Roof Ident Pair Settings'}{state} = $nok; $result->{'Roof Ident Pair Settings'}{result} .= qq{Setting the Rooftops is a necessary preparation for the definition of Roof Ident Pairs
}; $result->{'Roof Ident Pair Settings'}{note} .= qq{See the "Rooftop Settings" section below.
}; $result->{'Roof Ident Pair Settings'}{fault} = 1; } else { - $result->{'Rooftop Settings'}{result} .= $hqtxt{fullfd}{$lang}; - $result->{'Rooftop Settings'}{note} .= qq{Rooftops defined: }.$rft.qq{
}; - } - - my ($a,$h) = parseParams ($rft); - + $result->{'Rooftop Settings'}{result} .= $hqtxt{fulfd}{$lang}; + $result->{'Rooftop Settings'}{note} .= qq{Rooftops defined: }.$rft.qq{
}; + } + + my ($a,$h) = parseParams ($rft); + while (my ($is, $pk) = each %$h) { my $rtid = SolCastAPIVal ($hash, '?IdPair', '?'.$pk, 'rtid', ''); my $apikey = SolCastAPIVal ($hash, '?IdPair', '?'.$pk, 'apikey', ''); - + if(!$rtid || !$apikey) { my $res = qq{String "$is" has no Roof Ident Pair "$pk" defined or has no Rooftop-ID and/or SolCast-API key assigned.
}; my $note = qq{Set the Roof Ident Pair "$pk" with "set $name roofIdentPair".
}; - + $result->{'Roof Ident Pair Settings'}{state} = $nok; $result->{'Roof Ident Pair Settings'}{result} .= $res; $result->{'Roof Ident Pair Settings'}{note} .= $note; $result->{'Roof Ident Pair Settings'}{fault} = 1; } else { - $result->{'Roof Ident Pair Settings'}{result} = $hqtxt{fullfd}{$lang} if(!$result->{'Roof Ident Pair Settings'}{fault}); + $result->{'Roof Ident Pair Settings'}{result} = $hqtxt{fulfd}{$lang} if(!$result->{'Roof Ident Pair Settings'}{fault}); $result->{'Roof Ident Pair Settings'}{note} .= qq{checked "$is" Roof Ident Pair "$pk":
rtid=$rtid, apikey=$apikey
}; - } - } + } + } } - - ## Allgemeine Settings + + ## Allgemeine Settings ######################## - my $eocr = AttrVal ($name, 'event-on-change-reading', ''); - - if (!$eocr) { + my $eocr = AttrVal ($name, 'event-on-change-reading', ''); + my $einstds = ""; + + if (!$eocr || $eocr ne '.*') { + $einstds = 'to .*' if($eocr ne '.*'); $result->{'Common Settings'}{state} = $info; - $result->{'Common Settings'}{result} .= qq{Attribute 'event-on-change-reading' is not set.
}; + $result->{'Common Settings'}{result} .= qq{Attribute 'event-on-change-reading' is not set $einstds.
}; $result->{'Common Settings'}{note} .= qq{Setting attribute 'event-on-change-reading = .*' is recommended to improve the runtime performance.
}; $result->{'Common Settings'}{info} = 1; } - + if ($lang ne 'DE') { $result->{'Common Settings'}{state} = $info; - $result->{'Common Settings'}{result} .= qq{The global attribute 'language' is set to '$lang'.
}; - $result->{'Common Settings'}{note} .= qq{Setting attribute 'language = DE' is recommended. When set like this, most of the outputs are in German.
}; - $result->{'Common Settings'}{info} = 1; + $result->{'Common Settings'}{result} .= qq{The language is set to '$lang'.
}; + $result->{'Common Settings'}{note} .= qq{If the local attribute "ctrlLanguage" or the global attribute "language" is changed to "DE" most of the outputs are in German.
}; + $result->{'Common Settings'}{info} = 1; } - + ## allg. Settings bei Nutzung SolCast ####################################### if (isSolCastUsed ($hash)) { my $gdn = AttrVal ('global', 'dnsServer', ''); - my $cfd = AttrVal ($name, 'affectCloudfactorDamping', ''); - my $rfd = AttrVal ($name, 'affectRainfactorDamping', ''); + my $cfd = AttrVal ($name, 'affectCloudfactorDamping', ''); + my $rfd = AttrVal ($name, 'affectRainfactorDamping', ''); my $osi = AttrVal ($name, 'ctrlOptimizeSolCastInterval', 0); my $pcf = ReadingsVal ($name, 'pvCorrectionFactor_Auto', ''); - + my $lam = SolCastAPIVal ($hash, '?All', '?All', 'response_message', 'success'); - + if (!$pcf || $pcf ne 'on') { $result->{'Common Settings'}{state} = $info; $result->{'Common Settings'}{result} .= qq{pvCorrectionFactor_Auto is set to "$pcf"
}; $result->{'Common Settings'}{note} .= qq{set pvCorrectionFactor_Auto to "on" is recommended if the SolCast efficiency factor is already adjusted.
}; } - + if ($cfd eq '' || $cfd != 0) { $result->{'Common Settings'}{state} = $warn; $result->{'Common Settings'}{result} .= qq{Attribute affectCloudfactorDamping is set to "$cfd"
}; $result->{'Common Settings'}{note} .= qq{set affectCloudfactorDamping explicitly to "0" is recommended.
}; $result->{'Common Settings'}{warn} = 1; } - + if ($rfd eq '' || $rfd != 0) { $result->{'Common Settings'}{state} = $warn; $result->{'Common Settings'}{result} .= qq{Attribute affectRainfactorDamping is set to "$rfd"
}; $result->{'Common Settings'}{note} .= qq{set affectRainfactorDamping explicitly to "0" is recommended.
}; $result->{'Common Settings'}{warn} = 1; } - + if (!$osi) { $result->{'Common Settings'}{state} = $warn; $result->{'Common Settings'}{result} .= qq{Attribute ctrlOptimizeSolCastInterval is set to "$osi"
}; $result->{'Common Settings'}{note} .= qq{set ctrlOptimizeSolCastInterval to "1" is recommended.
}; $result->{'Common Settings'}{warn} = 1; } - - if($lam ne 'success' ) { - $result->{'Common Settings'}{state} = $nok; - $result->{'Common Settings'}{result} .= qq{The last message from SolCast API is "$lam".
}; - $result->{'Common Settings'}{note} .= qq{Check the validity of your API key and Rooftop indentificators.
}; - $result->{'Common Settings'}{fault} = 1; + + if ($lam =~ /You have exceeded your free daily limit/i) { + $result->{'API Access'}{state} = $warn; + $result->{'API Access'}{result} .= qq{The last message from SolCast API is:
"$lam"
}; + $result->{'API Access'}{note} .= qq{Wait until the next day when the limit resets.
}; + $result->{'API Access'}{warn} = 1; } - + elsif ($lam ne 'success') { + $result->{'API Access'}{state} = $nok; + $result->{'API Access'}{result} .= qq{The last message from SolCast API is:
"$lam"
}; + $result->{'API Access'}{note} .= qq{Check the validity of your API key and Rooftop identificators.
}; + $result->{'API Access'}{fault} = 1; + } + if (!$gdn) { - $result->{'Common Settings'}{state} = $nok; - $result->{'Common Settings'}{result} .= qq{Attribute dnsServer in global device is not set.
}; - $result->{'Common Settings'}{note} .= qq{set global attribute dnsServer to the IP Adresse of your DNS Server.
}; - $result->{'Common Settings'}{fault} = 1; + $result->{'API Access'}{state} = $nok; + $result->{'API Access'}{result} .= qq{Attribute dnsServer in global device is not set.
}; + $result->{'API Access'}{note} .= qq{set global attribute dnsServer to the IP Adresse of your DNS Server.
}; + $result->{'API Access'}{fault} = 1; } - + if(!$result->{'Common Settings'}{fault} && !$result->{'Common Settings'}{warn} && !$result->{'Common Settings'}{info}) { - $result->{'Common Settings'}{result} = $hqtxt{fullfd}{$lang}; - $result->{'Common Settings'}{note} .= qq{checked parameter:
}; + $result->{'Common Settings'}{result} = $hqtxt{fulfd}{$lang}; + $result->{'Common Settings'}{note} .= qq{checked parameters:
}; $result->{'Common Settings'}{note} .= qq{affectCloudfactorDamping, affectRainfactorDamping, ctrlOptimizeSolCastInterval
}; - $result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto, event-on-change-reading, global language
}; + $result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto, event-on-change-reading, ctrlLanguage, global language
}; } } - + ## allg. Settings bei Nutzung DWD Radiation ############################################# if (!isSolCastUsed ($hash)) { my $pcf = ReadingsVal ($name, 'pvCorrectionFactor_Auto', ''); - + if (!$pcf || $pcf ne 'on') { $result->{'Common Settings'}{state} = $warn; $result->{'Common Settings'}{result} .= qq{pvCorrectionFactor_Auto is set to "$pcf"
}; $result->{'Common Settings'}{note} .= qq{set pvCorrectionFactor_Auto to "on" is recommended
}; $result->{'Common Settings'}{warn} = 1; } - + if(!$result->{'Common Settings'}{warn} && !$result->{'Common Settings'}{info}) { - $result->{'Common Settings'}{result} = $hqtxt{fullfd}{$lang}; - $result->{'Common Settings'}{note} .= qq{checked parameter:
}; - $result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto, event-on-change-reading
}; + $result->{'Common Settings'}{result} = $hqtxt{fulfd}{$lang}; + $result->{'Common Settings'}{note} .= qq{checked parameters:
}; + $result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto, event-on-change-reading, ctrlLanguage, global language
}; } } - + ## Ausgabe ############ - + my $out = qq{}; $out .= qq{}.$hqtxt{plntck}{$lang}.qq{

}; - + $out .= qq{}; - $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; $out .= qq{}; + + my $hz = keys %{$result}; + my $hc = 0; for my $key (sort keys %{$result}) { + $hc++; $cf = $result->{$key}{fault} if($result->{$key}{fault}); $wn = $result->{$key}{warn} if($result->{$key}{warn}); + $io = $result->{$key}{info} if($result->{$key}{info}); $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; - $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; + $out .= qq{}; $out .= qq{}; + #if ($hc < $hz) { # Tabelle wird auf Tablet zu groß mit Zwischenzeilen + # $out .= qq{}; + # $out .= qq{}; + # $out .= qq{}; + #} + $out .= qq{}; } $out .= qq{
Object State Result Note
Object State       Result       Note
$key $result->{$key}{state} $result->{$key}{result} $result->{$key}{note} $key $result->{$key}{state}       $result->{$key}{result}       $result->{$key}{note}
 
}; $out .= qq{}; - - $out .= "

"; - - if($cf) { + + $out .= "
"; + + if($cf) { $out .= encode ("utf8", $hqtxt{strnok}{$lang}); } elsif ($wn) { @@ -9312,10 +9647,11 @@ sub checkPlantConfig { else { $out .= encode ("utf8", $hqtxt{strok}{$lang}); } - + + $out =~ s/ (Bitte eventuelle Hinweise|Please note any information).*// if(!$io); $out =~ s//$info/gx; $out =~ s//$warn/gx; - + return $out; } @@ -9330,10 +9666,10 @@ return $out; ################################################################ sub limitArray { my $href = shift; - my $limit = shift // 3; - + my $limit = shift // 3; + return if(ref $href ne "ARRAY"); - + while (scalar @{$href} > $limit) { shift @{$href}; } @@ -9346,43 +9682,44 @@ return; ################################################################ sub timestampToTimestring { my $epoch = shift; - + my $lang = shift; + my ($lyear,$lmonth,$lday,$lhour,$lmin,$lsec) = (localtime($epoch))[5,4,3,2,1,0]; my $ts; - + $lyear += 1900; # year is 1900 based $lmonth++; # month number is zero based - + my ($sec,$min,$hour,$day,$mon,$year) = (localtime(time))[0,1,2,3,4,5]; # Standard f. z.B. Readingstimstamp - $year += 1900; - $mon++; - + $year += 1900; + $mon++; + my $realts = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year,$mon,$day,$hour,$min,$sec); my $tsdef = sprintf("%04d-%02d-%02d %02d:%s", $lyear,$lmonth,$lday,$lhour,"00:00"); # engl. Variante für Logging-Timestamps etc. (Minute/Sekunde == 00) my $tsfull = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $lyear,$lmonth,$lday,$lhour,$lmin,$lsec); # engl. Variante Vollzeit - - if(AttrVal("global", "language", "EN") eq "DE") { + + if($lang eq "DE") { $ts = sprintf("%02d.%02d.%04d %02d:%02d:%02d", $lday,$lmonth,$lyear,$lhour,$lmin,$lsec); - } + } else { $ts = $tsdef; } - + return ($ts, $tsdef, $realts, $tsfull); } ################################################################ -# einen Zeitstring YYYY-MM-TT hh:mm:ss in einen Unix +# einen Zeitstring YYYY-MM-TT hh:mm:ss in einen Unix # Timestamp umwandeln ################################################################ -sub timestringToTimestamp { +sub timestringToTimestamp { my $tstring = shift; my($y, $mo, $d, $h, $m, $s) = $tstring =~ /([0-9]{4})-([0-9]{2})-([0-9]{2})\s([0-9]{2}):([0-9]{2}):([0-9]{2})/xs; return if(!$mo || !$y); - + my $timestamp = fhemTimeLocal($s, $m, $h, $d, $mo-1, $y-1900); - + return $timestamp; } @@ -9398,21 +9735,21 @@ return $timestamp; sub createReadingsFromArray { my $hash = shift; my $daref = shift; - my $doevt = shift // 0; + my $doevt = shift // 0; return if(!scalar @$daref); - + readingsBeginUpdate($hash); - + for my $elem (@$daref) { my ($rn,$rval,$ts) = split "<>", $elem, 3; - readingsBulkUpdate ($hash, $rn, $rval, undef, $ts); + readingsBulkUpdate ($hash, $rn, $rval, undef, $ts); } readingsEndUpdate($hash, $doevt); - + undef @$daref; - + return; } @@ -9421,30 +9758,30 @@ return; ################################################################ sub singleUpdateState { my $paref = shift; - + my $hash = $paref->{hash}; my $val = $paref->{state} // 'unknown'; my $evt = $paref->{evt} // 0; - + readingsSingleUpdate ($hash, 'state', $val, $evt); - + return; } ################################################################ -# alle Readings eines Devices oder nur Reading-Regex +# alle Readings eines Devices oder nur Reading-Regex # löschen ################################################################ sub deleteReadingspec { my $hash = shift; my $spec = shift // ".*"; - + my $readingspec = '^'.$spec.'$'; - + for my $reading ( grep { /$readingspec/x } keys %{$hash->{READINGS}} ) { readingsDelete($hash, $reading); } - + return; } @@ -9455,56 +9792,56 @@ sub createAssociatedWith { my $hash = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + RemoveInternalTimer($hash, "FHEM::SolarForecast::createAssociatedWith"); - + if($init_done == 1) { my @nd; my ($afc,$ara,$ain,$ame,$aba,$h); - + my $fcdev = ReadingsVal($name, "currentForecastDev", ""); # Weather forecast Device ($afc,$h) = parseParams ($fcdev); - $fcdev = $afc->[0] // ""; - + $fcdev = $afc->[0] // ""; + my $radev = ReadingsVal($name, "currentRadiationDev", ""); # Radiation forecast Device ($ara,$h) = parseParams ($radev); $radev = $ara->[0] // ""; - + my $indev = ReadingsVal($name, "currentInverterDev", ""); # Inverter Device ($ain,$h) = parseParams ($indev); $indev = $ain->[0] // ""; - + my $medev = ReadingsVal($name, "currentMeterDev", ""); # Meter Device ($ame,$h) = parseParams ($medev); $medev = $ame->[0] // ""; - + my $badev = ReadingsVal($name, "currentBatteryDev", ""); # Battery Device ($aba,$h) = parseParams ($badev); $badev = $aba->[0] // ""; - + for my $c (sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}) { # Consumer Devices - my $codev = AttrVal($name, "consumer${c}", ""); + my $codev = AttrVal($name, "consumer${c}", ""); my ($ac,$hc) = parseParams ($codev); - $codev = $ac->[0] // ""; - - push @nd, $codev if($codev); + $codev = $ac->[0] // ""; + + push @nd, $codev if($codev); } - + push @nd, $fcdev; push @nd, $radev if($radev ne $fcdev && $radev !~ /SolCast-API/xs); push @nd, $indev; push @nd, $medev; push @nd, $badev; - + if(@nd) { # $hash->{NOTIFYDEV} = join ",", @nd; # zur Zeit nicht benutzt readingsSingleUpdate ($hash, ".associatedWith", join(" ",@nd), 0); } - } + } else { InternalTimer(gettimeofday()+3, "FHEM::SolarForecast::createAssociatedWith", $hash, 0); } - + return; } @@ -9514,19 +9851,20 @@ return; ################################################################ sub deleteConsumerPlanning { my $hash = shift; - my $c = shift; - + my $c = shift; + my $type = $hash->{TYPE}; my $name = $hash->{NAME}; my $calias = ConsumerVal ($hash, $c, "alias", ""); - + delete $data{$type}{$name}{consumers}{$c}{planstate}; delete $data{$type}{$name}{consumers}{$c}{planswitchon}; delete $data{$type}{$name}{consumers}{$c}{planswitchoff}; delete $data{$type}{$name}{consumers}{$c}{plandelete}; - - deleteReadingspec ($hash, "consumer${c}.*" ); - + delete $data{$type}{$name}{consumers}{$c}{ehodpieces}; + + deleteReadingspec ($hash, "consumer${c}.*"); + Log3($name, 3, qq{$name - Consumer planning of "$calias" deleted}); return; @@ -9537,7 +9875,7 @@ return; ################################################################ sub setModel { my $hash = shift; - + if (isSolCastUsed ($hash)) { $hash->{MODEL} = 'SolCastAPI'; } @@ -9545,7 +9883,7 @@ sub setModel { $hash->{MODEL} = 'DWD'; deleteReadingspec ($hash, 'nextSolCastCall'); } - + return; } @@ -9556,36 +9894,36 @@ sub setTimeTracking { my $hash = shift; my $st = shift; # Startzeitstempel my $tkn = shift; # Name des Zeitschlüssels - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + $data{$type}{$name}{current}{$tkn} = sprintf "%.4f", tv_interval($st); - + return; } ################################################################ # Funktion liefert 1 wenn Consumer physisch "eingeschaltet" -# ist, d.h. der Wert onreg des Readings rswstate wahr ist +# ist, d.h. der Wert onreg des Readings rswstate wahr ist ################################################################ sub isConsumerPhysOn { my $hash = shift; my $c = shift; - my $name = $hash->{NAME}; - + my $name = $hash->{NAME}; + my $cname = ConsumerVal ($hash, $c, "name", ""); # Devicename Customer if(!$defs{$cname}) { Log3($name, 1, qq{$name - the consumer device "$cname" is invalid, the "on" state can't be identified}); return 0; } - - my $reg = ConsumerVal ($hash, $c, "onreg", "on"); + + my $reg = ConsumerVal ($hash, $c, "onreg", "on"); my $rswstate = ConsumerVal ($hash, $c, "rswstate", "state"); # Reading mit Schaltstatus my $swstate = ReadingsVal ($cname, $rswstate, "undef"); - - if ($swstate =~ m/^$reg$/x) { + + if ($swstate =~ m/^$reg$/x) { return 1; } @@ -9594,25 +9932,25 @@ return 0; ################################################################ # Funktion liefert 1 wenn Consumer physisch "ausgeschaltet" -# ist, d.h. der Wert offreg des Readings rswstate wahr ist +# ist, d.h. der Wert offreg des Readings rswstate wahr ist ################################################################ sub isConsumerPhysOff { my $hash = shift; - my $c = shift; + my $c = shift; my $name = $hash->{NAME}; - + my $cname = ConsumerVal ($hash, $c, "name", ""); # Devicename Customer if(!$defs{$cname}) { Log3($name, 1, qq{$name - the consumer device "$cname" is invalid, the "off" state can't be identified}); return 0; } - + my $reg = ConsumerVal ($hash, $c, "offreg", "off"); - my $rswstate = ConsumerVal ($hash, $c, "rswstate", "state"); # Reading mit Schaltstatus + my $rswstate = ConsumerVal ($hash, $c, "rswstate", "state"); # Reading mit Schaltstatus my $swstate = ReadingsVal ($cname, $rswstate, "undef"); - - if ($swstate =~ m/^$reg$/x) { + + if ($swstate =~ m/^$reg$/x) { return 1; } @@ -9631,7 +9969,7 @@ sub isConsumerLogOn { my $hash = shift; my $c = shift; my $pcurr = shift // 0; - + my $name = $hash->{NAME}; my $cname = ConsumerVal ($hash, $c, "name", ""); # Devicename Customer @@ -9639,29 +9977,29 @@ sub isConsumerLogOn { Log3($name, 1, qq{$name - the consumer device "$cname" is invalid, the "on" state can't be identified}); return 0; } - - if(isConsumerPhysOff($hash, $c)) { # Device ist physisch ausgeschaltet - return 0; + + if(isConsumerPhysOff($hash, $c)) { # Device ist physisch ausgeschaltet + return 0; } - + my $type = $hash->{TYPE}; my $nompower = ConsumerVal ($hash, $c, "power", 0); # nominale Leistung lt. Typenschild my $rpcurr = ConsumerVal ($hash, $c, "rpcurr", ""); # Reading für akt. Verbrauch angegeben ? - my $ethreshold = ConsumerVal ($hash, $c, "energythreshold", 0); # Schwellenwert (Wh pro Stunde) ab der ein Verbraucher als aktiv gewertet wird - + my $ethreshold = ConsumerVal ($hash, $c, "energythreshold", 0); # Schwellenwert (Wh pro Stunde) ab der ein Verbraucher als aktiv gewertet wird + if (!$rpcurr && isConsumerPhysOn($hash, $c)) { # Workaround wenn Verbraucher ohne Leistungsmessung $pcurr = $nompower; } - - my $currpowerpercent = $pcurr; + + my $currpowerpercent = $pcurr; $currpowerpercent = ($pcurr / $nompower) * 100 if($nompower > 0); - + $data{$type}{$name}{consumers}{$c}{currpowerpercent} = $currpowerpercent; - + if($pcurr > $ethreshold || $currpowerpercent > $defpopercent) { # Verbraucher ist logisch aktiv return 1; } - + return 0; } @@ -9679,30 +10017,30 @@ sub isAddSwitchOnCond { my $info = q{}; my $err = q{}; - + my $dswoncond = ConsumerVal ($hash, $c, "dswoncond", ""); # Device zur Lieferung einer zusätzlichen Einschaltbedingung - + if($dswoncond && !$defs{$dswoncond}) { $err = qq{ERROR - the device "$dswoncond" doesn't exist! Check the key "swoncond" in attribute "consumer${c}"}; - return (0, $info, $err); - } - + return (0, $info, $err); + } + my $rswoncond = ConsumerVal ($hash, $c, "rswoncond", ""); # Reading zur Lieferung einer zusätzlichen Einschaltbedingung my $swoncondregex = ConsumerVal ($hash, $c, "swoncondregex", ""); # Regex einer zusätzliche Einschaltbedingung my $condval = ReadingsVal ($dswoncond, $rswoncond, ""); # Wert zum Vergleich mit Regex - - if ($condval =~ m/^$swoncondregex$/x) { + + if ($condval =~ m/^$swoncondregex$/x) { return (1, $info, $err); } - - $info = qq{The device "$dswoncond", reading "$rswoncond" doen't match the Regex "$swoncondregex"}; + + $info = qq{The device "$dswoncond", reading "$rswoncond" doen't match the Regex "$swoncondregex"}; return (0, $info, $err); } ################################################################ # Funktion liefert "1" wenn eine Ausschaltbedingung -# erfüllt ist +# erfüllt ist # ("swoffcond" oder "interruptable" im Consumer Attribut) # Der Inhalt von "interruptable" wird optional in $cond # übergeben. @@ -9722,37 +10060,37 @@ sub isAddSwitchOffCond { my $dswoffcond = q{}; # Device zur Lieferung einer Ausschaltbedingung my $rswoffcond = q{}; # Reading zur Lieferung einer Ausschaltbedingung my $swoffcondregex = q{}; # Regex der Ausschaltbedingung (wenn wahr) - + if ($cond) { ($dswoffcond,$rswoffcond,$swoffcondregex) = split ":", $cond; } else { - $dswoffcond = ConsumerVal ($hash, $c, "dswoffcond", ""); - $rswoffcond = ConsumerVal ($hash, $c, "rswoffcond", ""); - $swoffcondregex = ConsumerVal ($hash, $c, "swoffcondregex", ""); + $dswoffcond = ConsumerVal ($hash, $c, "dswoffcond", ""); + $rswoffcond = ConsumerVal ($hash, $c, "rswoffcond", ""); + $swoffcondregex = ConsumerVal ($hash, $c, "swoffcondregex", ""); } - + if($dswoffcond && !$defs{$dswoffcond}) { $err = qq{ERROR - the device "$dswoffcond" doesn't exist! Check the key "swoffcond" or "interruptable" in attribute "consumer${c}"}; - return (0, $info, $err); - } - + return (0, $info, $err); + } + my $condval = ReadingsVal ($dswoffcond, $rswoffcond, ""); - + if ($hyst && isNumeric ($condval)) { # Hysterese berücksichtigen $condval -= $hyst; } - + if ($condval && $condval =~ m/^$swoffcondregex$/x) { $info = qq{value "$condval" (hysteresis = $hyst) match the Regex "$swoffcondregex" \n}. qq{-> Switch-off condition or interrupt in the "switch-off context", DO NOT switch on or DO NOT continue in the "switch-on context"\n} - ; + ; return (1, $info, $err); } - + $info = qq{device: "$dswoffcond", reading: "$rswoffcond" , value: "$condval" (hysteresis = $hyst) doesn't match Regex: "$swoffcondregex" \n}. qq{-> DO NOT Switch-off or DO NOT interrupt in the "switch-off context", Switching on or continuing in the "switch-on" context\n} - ; + ; return (0, $info, $err); } @@ -9784,31 +10122,33 @@ sub isInterruptable { my $hash = shift; my $c = shift; my $hyst = shift // 0; - + my $name = $hash->{NAME}; my $intable = ConsumerVal ($hash, $c, 'interruptable', 0); - + if ($intable eq '0') { return 0; } elsif ($intable eq '1') { return 1; } - + + my $debug = AttrVal ($name, 'ctrlDebug', 'none'); + my ($swoffcond,$info,$err) = isAddSwitchOffCond ($hash, $c, $intable, $hyst); Log3 ($name, 1, "$name - $err") if($err); - if (AttrVal ($name, 'ctrlDebug', 0)) { # nur für Debugging - Log (1, qq{DEBUG> $name consumer "$c" - isInterruptable Info: $info}); + if ($debug =~ /consumerSwitching/x) { # nur für Debugging + Log (1, qq{$name DEBUG> consumer "$c" - isInterruptable Info: $info}); } - + if ($swoffcond) { return 2; } else { return 3; } - + return; } @@ -9817,13 +10157,13 @@ return; ################################################################ sub isNumeric { my $val = shift // q{empty}; - + my $ret = 0; - + if($val =~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/xs) { $ret = 1; } - + return $ret; } @@ -9832,14 +10172,14 @@ return $ret; ################################################################ sub isSolCastUsed { my $hash = shift; - + my $api = ReadingsVal ($hash->{NAME}, 'currentRadiationDev', 'DWD'); my $ret = 0; - + if($api =~ /SolCast/xs) { $ret = 1; } - + return $ret; } @@ -9850,17 +10190,17 @@ return $ret; sub lastConsumerSwitchtime { my $hash = shift; my $c = shift; - my $name = $hash->{NAME}; - + my $name = $hash->{NAME}; + my $cname = ConsumerVal ($hash, $c, "name", ""); # Devicename Customer if(!$defs{$cname}) { Log3($name, 1, qq{$name - the consumer device "$cname" is invalid, the last switching time can't be identified}); return; } - + my $rswstate = ConsumerVal ($hash, $c, "rswstate", "state"); # Reading mit Schaltstatus - my $swtime = ReadingsTimestamp ($cname, $rswstate, ""); # Zeitstempel im Format 2016-02-16 19:34:24 + my $swtime = ReadingsTimestamp ($cname, $rswstate, ""); # Zeitstempel im Format 2016-02-16 19:34:24 my $swtimets = timestringToTimestamp ($swtime) if($swtime); # Unix Timestamp Format erzeugen return ($swtime, $swtimets); @@ -9871,8 +10211,8 @@ return ($swtime, $swtimets); # einfache Form ################################################################ sub simplifyCstate { - my $ps = shift; - + my $ps = shift; + $ps = $ps =~ /planned/xs ? 'planned' : $ps =~ /no\splanning/xs ? 'suspended' : $ps =~ /switching\son/xs ? 'starting' : @@ -9883,9 +10223,9 @@ sub simplifyCstate { $ps =~ /interrupting/xs ? 'interrupting' : $ps =~ /interrupted/xs ? 'interrupted' : $ps =~ /continuing/xs ? 'continuing' : - $ps =~ /continued/xs ? 'continued' : + $ps =~ /continued/xs ? 'continued' : "unknown"; - + return $ps; } @@ -9894,14 +10234,14 @@ return $ps; ################################################################ sub checkRegex { my $regexp = shift // return; - + eval { "Hallo" =~ m/^$regexp$/; 1; - } + } or do { my $err = (split " at", $@)[0]; return "Bad regexp: ".$err; }; - + return; } @@ -9939,10 +10279,10 @@ sub HistoryVal { my $hod = shift; my $key = shift; my $def = shift; - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + if(defined($data{$type}{$name}{pvhist}) && defined($data{$type}{$name}{pvhist}{$day}) && defined($data{$type}{$name}{pvhist}{$day}{$hod}) && @@ -9969,7 +10309,7 @@ return $def; # gfeedin - reale Netzeinspeisung # batin - Batterieladung (Wh) # batout - Batterieentladung (Wh) -# weatherid - DWD Wetter id +# weatherid - DWD Wetter id # weathertxt - DWD Wetter Text # wcc - DWD Wolkendichte # wrp - DWD Regenwahrscheinlichkeit @@ -9985,10 +10325,10 @@ sub CircularVal { my $hod = shift; my $key = shift; my $def = shift; - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + if(defined($data{$type}{$name}{circular}) && defined($data{$type}{$name}{circular}{$hod}) && defined($data{$type}{$name}{circular}{$hod}{$key})) { @@ -10000,7 +10340,7 @@ return $def; ################################################################ # Wert des Autokorrekturfaktors und dessen Qualität -# für eine bestimmte Bewölkungs-Range aus dem circular-Hash +# für eine bestimmte Bewölkungs-Range aus dem circular-Hash # zurückliefern # Usage: # ($f,$q) = CircularAutokorrVal ($hash, $hod, $range, $def) @@ -10018,20 +10358,20 @@ sub CircularAutokorrVal { my $hod = shift; my $range = shift; my $def = shift; - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + my $pvcorrf = $def; my $quality = $def; - + if(defined($data{$type}{$name}{circular}) && defined($data{$type}{$name}{circular}{$hod}) && defined($data{$type}{$name}{circular}{$hod}{pvcorrf}) && defined($data{$type}{$name}{circular}{$hod}{pvcorrf}{$range})) { $pvcorrf = $data{$type}{$name}{circular}{$hod}{pvcorrf}{$range}; } - + if(defined($data{$type}{$name}{circular}) && defined($data{$type}{$name}{circular}{$hod}) && defined($data{$type}{$name}{circular}{$hod}{quality}) && @@ -10051,7 +10391,7 @@ return ($pvcorrf, $quality); # $key: starttime - Startzeit der abgefragten nächsten Stunde # hourofday - Stunde des Tages # pvforecast - PV Vorhersage in Wh -# weatherid - DWD Wetter id +# weatherid - DWD Wetter id # cloudcover - DWD Wolkendichte # cloudrange - berechnete Bewölkungsrange # rainprob - DWD Regenwahrscheinlichkeit @@ -10059,7 +10399,7 @@ return ($pvcorrf, $quality); # confc - prognostizierter Hausverbrauch (Wh) # confcEx - prognostizierter Hausverbrauch ohne registrierte Consumer (Wh) # today - 1 wenn heute -# correff - verwendeter Korrekturfaktor bzw. SolCast Percentil/Qualität +# correff - verwendeter Korrekturfaktor bzw. SolCast Percentil/Qualität # $def: Defaultwert # ######################################################################################### @@ -10068,10 +10408,10 @@ sub NexthoursVal { my $hod = shift; my $key = shift; my $def = shift; - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + if(defined($data{$type}{$name}{nexthours}) && defined($data{$type}{$name}{nexthours}{$hod}) && defined($data{$type}{$name}{nexthours}{$hod}{$key})) { @@ -10107,11 +10447,11 @@ return $def; sub CurrentVal { my $hash = shift; my $key = shift; - my $def = shift; - + my $def = shift; + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + if(defined($data{$type}{$name}{current}) && defined($data{$type}{$name}{current}{$key})) { return $data{$type}{$name}{current}{$key}; @@ -10135,18 +10475,19 @@ return $def; # mintime - min. Einschalt- bzw. Zykluszeit # onreg - Regex für phys. Zustand "ein" # offreg - Regex für phys. Zustand "aus" -# oncom - Einschaltkommando +# oncom - Einschaltkommando # offcom - Ausschaltkommando # onoff - logischer ein/aus Zustand des am Consumer angeschlossenen Endverbrauchers # retotal - Reading der Leistungsmessung # uetotal - Unit der Leistungsmessung # rpcurr - Readingname des aktuellen Verbrauchs -# powerthreshold - Schwellenwert d. aktuellen Leistung(W) ab der ein Verbraucher als aktiv gewertet wird -# energythreshold - Schwellenwert (Wh pro Stunde) ab der ein Verbraucher als aktiv gewertet wird +# powerthreshold - Schwellenwert d. aktuellen Leistung(W) ab der ein Verbraucher als aktiv gewertet wird +# energythreshold - Schwellenwert (Wh pro Stunde) ab der ein Verbraucher als aktiv gewertet wird # upcurr - Unit des aktuellen Verbrauchs # avgenergy - initialer / gemessener Durchschnittsverbrauch pro Stunde # avgruntime - durchschnittliche Einschalt- bzw. Zykluszeit (Minuten) # epieces - prognostizierte Energiescheiben (Hash) +# ehodpieces - geplante Energiescheiben nach Tagesstunde (hour of day) (Hash) # dswoncond - Device zur Lieferung einer zusätzliche Einschaltbedingung # rswoncond - Reading zur Lieferung einer zusätzliche Einschaltbedingung # swoncondregex - Regex einer zusätzliche Einschaltbedingung @@ -10163,11 +10504,11 @@ sub ConsumerVal { my $hash = shift; my $co = shift; my $key = shift; - my $def = shift; - + my $def = shift; + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + if(defined($data{$type}{$name}{consumers}) && defined($data{$type}{$name}{consumers}{$co}{$key}) && defined($data{$type}{$name}{consumers}{$co}{$key})) { @@ -10185,8 +10526,8 @@ return $def; # $tring: Stringname aus "inverterStrings" (?All für allg. Werte) # $ststr: Startzeit der Form YYYY-MM-DD hh:00:00 # $key: pv_estimate - PV Schätzung in Wh -# $def: Defaultwert -# +# $def: Defaultwert +# # Sonderabfragen # SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_time', $def) - letzte Abfrage Zeitstring # SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_timestamp', $def) - letzte Abfrage Unix Timestamp @@ -10195,9 +10536,9 @@ return $def; # SolCastAPIVal ($hash, '?All', '?All', 'todayDoneAPIcalls', $def) - heute ausgeführte API Calls (hat u.U. mehrere Requests) # SolCastAPIVal ($hash, '?All', '?All', 'todayRemainingAPIcalls', $def) - heute noch mögliche API Calls (ungl. Requests !) # SolCastAPIVal ($hash, '?All', '?All', 'solCastAPIcallMultiplier',$def) - APIcalls = APIRequests * solCastAPIcallMultiplier -# SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $def) - aktuelles API Request Intervall +# SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $def) - aktuelles API Request Intervall # SolCastAPIVal ($hash, '?All', '?All', 'response_message', $def) - letzte SolCast API Antwort -# SolCastAPIVal ($hash, '?IdPair', '?', 'rtid', $def) - RoofTop-ID, = Paarschlüssel +# SolCastAPIVal ($hash, '?IdPair', '?', 'rtid', $def) - RoofTop-ID, = Paarschlüssel # SolCastAPIVal ($hash, '?IdPair', '?', 'apikey', $def) - API-Key, = Paarschlüssel # ############################################################################################################################# @@ -10207,10 +10548,10 @@ sub SolCastAPIVal { my $ststr = shift; my $key = shift; my $def = shift; - + my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - + if(defined $data{$type}{$name}{solcastapi} && defined $data{$type}{$name}{solcastapi}{$string} && defined $data{$type}{$name}{solcastapi}{$string}{$ststr} && @@ -10240,18 +10581,18 @@ return $def;

SolarForecast


-Das Modul SolarForecast erstellt auf Grundlage der Werte aus generischen Quellendevices eine +Das Modul SolarForecast erstellt auf Grundlage der Werte aus generischen Quellendevices eine Vorhersage für den solaren Ertrag und integriert weitere Informationen als Grundlage für darauf aufbauende Steuerungen.
-Die solare Vorhersage basiert auf der durch den Deutschen Wetterdienst (Model DWD) oder der -SolCast API (Model SolCastAPI) prognostizierten -Globalstrahlung am Anlagenstandort. Wegen der erreichbaren Genauigkeit wird die Nutzung der SolCast API empfohlen !

+Die solare Vorhersage basiert auf der durch den Deutschen Wetterdienst (Model DWD) oder der +SolCast API (Model SolCastAPI) prognostizierten +Globalstrahlung am Anlagenstandort. Wegen der erreichbaren Genauigkeit wird die Nutzung der SolCast API empfohlen!

Die Nutzung der SolCast API beschränkt sich auf die kostenlose Version unter Verwendung von Rooftop Sites.
-In zugeordneten DWD_OpenData Device(s) ist die passende Wetterstation mit dem Attribut "forecastStation" -festzulegen um meteorologische Daten (Bewölkung, Sonnenaufgang, u.a.) bzw. eine Strahlungsprognose (Model DWD) für diesen +In zugeordneten DWD_OpenData Device(s) ist die passende Wetterstation mit dem Attribut "forecastStation" +festzulegen um meteorologische Daten (Bewölkung, Sonnenaufgang, u.a.) bzw. eine Strahlungsprognose (Model DWD) für diesen Standort zu erhalten.
-Über die PV Erzeugungsprognose hinaus werden Verbrauchswerte bzw. Netzbezugswerte erfasst und für eine +Über die PV Erzeugungsprognose hinaus werden Verbrauchswerte bzw. Netzbezugswerte erfasst und für eine Verbrauchsprognose verwendet.

Das Modul errechnet aus den Prognosewerten einen zukünftigen Energieüberschuß der zur Betriebsplanung von Verbrauchern @@ -10262,70 +10603,70 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. Define

- +
    Ein SolarForecast Device wird erstellt mit:

    - +
      define <name> SolarForecast

    - - Nach der Definition des Devices sind in Abhängigkeit der verwendeten Prognosequellen zwingend weitere + + Nach der Definition des Devices sind in Abhängigkeit der verwendeten Prognosequellen zwingend weitere anlagenspezifische Angaben mit den entsprechenden set-Kommandos zu hinterlegen.
    - Mit nachfolgenden set-Kommandos werden für die Funktion des Moduls maßgebliche Informationen + Mit nachfolgenden set-Kommandos werden für die Funktion des Moduls maßgebliche Informationen hinterlegt:

      - + - - - + + + - +
      currentForecastDev DWD_OpenData Device welches meteorologische Daten (z.B. Bewölkung) liefert
      currentRadiationDev DWD_OpenData Device bzw. SolCast-API zur Lieferung von Strahlungsdaten
      currentInverterDev Device welches PV Leistungsdaten liefert
      currentMeterDev Device welches Netz I/O-Daten liefert
      currentBatteryDev Device welches Batterie Leistungsdaten liefert (sofern vorhanden)
      inverterStrings Bezeichner der vohandenen Anlagenstrings
      moduleDirection Ausrichtung (Azimuth) der Anlagenstrings
      modulePeakString die DC-Peakleistung der Anlagenstrings
      inverterStrings Bezeichner der vorhandenen Anlagenstrings
      moduleDirection Ausrichtung (Azimut) der Anlagenstrings
      modulePeakString die DC-Peakleistung der Anlagenstrings
      roofIdentPair die Identifikationsdaten (bei Nutzung der SolCast API)
      moduleRoofTops die Rooftop Parameter (bei Nutzung der SolCast API)
      moduleTiltAngle die Neigungswinkel der der Anlagenmodule
      moduleTiltAngle die Neigungswinkel der der Anlagenmodule

    - - Um eine Anpassung an die persönliche Anlage zu ermöglichen, können Korrekturfaktoren manuell - (set <name> pvCorrectionFactor_XX) bzw. automatisiert (set <name> pvCorrectionFactor_Auto on) bestimmt + + Um eine Anpassung an die persönliche Anlage zu ermöglichen, können Korrekturfaktoren manuell + (set <name> pvCorrectionFactor_XX) bzw. automatisiert (set <name> pvCorrectionFactor_Auto on) bestimmt werden. Die manuelle Anpassung ist nur für das Model DWD einsetzbar. - Weiterhin kann mit den Attributen affectCloudfactorDamping - und affectRainfactorDamping der Beeinflussungsgrad von + Weiterhin kann mit den Attributen affectCloudfactorDamping + und affectRainfactorDamping der Beeinflussungsgrad von Bewölkungs- und Regenprognosen eingestellt werden.

    - + Hinweis
    - Bei Nutzung des DWD für die solare Vorhersage wird empfohlen die automatische Vorhersagekorrektur unmittelbar - einzuschalten, da das SolarForecast Device eine lange Zeit benötigt um die Optimierung der Korrekturfaktoren zu erreichen. - + Bei Nutzung des DWD für die solare Vorhersage wird empfohlen die automatische Vorhersagekorrektur unmittelbar + einzuschalten, da das SolarForecast Device eine lange Zeit benötigt um die Optimierung der Korrekturfaktoren zu erreichen. +

- + Consumer Integration

- +
    - Der Nutzer kann Verbraucher (z.B. Schaltsteckdosen) direkt im Modul registrieren und die Planung der - Ein/Ausschaltzeiten sowie deren Ausführung vom SolarForecast Modul übernehmen lassen. Die Registrierung erfolgt mit den - ConsumerXX-Attributen. In den Attributen werden neben dem FHEM Consumer Device eine Vielzahl von obligatorischen oder + Der Nutzer kann Verbraucher (z.B. Schaltsteckdosen) direkt im Modul registrieren und die Planung der + Ein/Ausschaltzeiten sowie deren Ausführung vom SolarForecast Modul übernehmen lassen. Die Registrierung erfolgt mit den + ConsumerXX-Attributen. In den Attributen werden neben dem FHEM Consumer Device eine Vielzahl von obligatorischen oder optionalen Schlüsseln angegeben die das Einplanungs- und Schaltverhalten des Consumers beeinflussen.
    - Die Schlüssel sind in der ConsumerXX-Hilfe detailliiert beschreiben, erfordern unter Umständen aber eine gewisse - Einarbeitung. Um sich in den Umgang mit der Consumersteuerung anzueignen, bietet es sich an zunächst einen oder + Die Schlüssel sind in der ConsumerXX-Hilfe detailliiert beschreiben, erfordern unter Umständen aber eine gewisse + Einarbeitung. Um sich in den Umgang mit der Consumersteuerung anzueignen, bietet es sich an zunächst einen oder mehrere Dummies anzulegen und diese Devices als Consumer zu registrieren.

    - - Zu diesem Zweck eignet sich ein Dummy Device nach diesem Muster: + + Zu diesem Zweck eignet sich ein Dummy Device nach diesem Muster:

    - +
      - define SolCastDummy dummy + define SolCastDummy dummy
      attr SolCastDummy userattr nomPower
      attr SolCastDummy alias SolarForecast Consumer Dummy
      attr SolCastDummy cmdIcon on:remotecontrol/black_btn_GREEN off:remotecontrol/black_btn_RED
      @@ -10343,18 +10684,18 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.
    - Set + Set
      - +
        -
      • consumerImmediatePlanning <Verbrauchernummer>

        - - Es wird das sofortige Einschalten des Verbrauchers zur aktuellen Zeit eingeplant. - Eventuell im consumerXX Attribut gesetzte Schlüssel notbefore, notafter bzw. mode werden nicht +
      • consumerImmediatePlanning <Verbrauchernummer>

        + + Es wird das sofortige Einschalten des Verbrauchers zur aktuellen Zeit eingeplant. + Eventuell im consumerXX Attribut gesetzte Schlüssel notbefore, notafter bzw. mode werden nicht beachtet.

        - +
          Beispiel:
          set <name> consumerImmediatePlanning 01
          @@ -10362,19 +10703,19 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.

        - +
          -
        • currentBatteryDev <Meter Device Name> pin=<Readingname>:<Einheit> pout=<Readingname>:<Einheit> [intotal=<Readingname>:<Einheit>] [outtotal=<Readingname>:<Einheit>] [charge=<Readingname>]

          - - Legt ein beliebiges Device und seine Readings zur Lieferung der Batterie Leistungsdaten fest. +
        • currentBatteryDev <Meter Device Name> pin=<Readingname>:<Einheit> pout=<Readingname>:<Einheit> [intotal=<Readingname>:<Einheit>] [outtotal=<Readingname>:<Einheit>] [charge=<Readingname>]

          + + Legt ein beliebiges Device und seine Readings zur Lieferung der Batterie Leistungsdaten fest. Das Modul geht davon aus dass der numerische Wert der Readings immer positiv ist. Es kann auch ein Dummy Device mit entsprechenden Readings sein. Die Bedeutung des jeweiligen "Readingname" ist:
          - -
            - - + +
              +
            + @@ -10382,179 +10723,179 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.
            pin Reading welches die aktuelle Batterieladung liefert
            pout Reading welches die aktuelle Batterieentladung liefert
            intotal Reading welches die totale Batterieladung liefert (fortlaufender Zähler)
            charge Reading welches den aktuellen Ladezustand (in Prozent) liefert
            Einheit die jeweilige Einheit (W,Wh,kW,kWh)
            -
          +

        - - Sonderfälle: Sollte das Reading für pin und pout identisch, aber vorzeichenbehaftet sein, + + Sonderfälle: Sollte das Reading für pin und pout identisch, aber vorzeichenbehaftet sein, können die Schlüssel pin und pout wie folgt definiert werden:

          pin=-pout    (ein negativer Wert von pout wird als pin verwendet)
          pout=-pin    (ein negativer Wert von pin wird als pout verwendet)

        - + Die Einheit entfällt in dem jeweiligen Sonderfall.

        - +
          Beispiel:
          set <name> currentBatteryDev BatDummy pin=BatVal:W pout=-pin intotal=BatInTot:Wh outtotal=BatOutTot:Wh

          # Device BatDummy liefert die aktuelle Batterieladung im Reading "BatVal" (W), die Batterieentladung im gleichen Reading mit negativen Vorzeichen,
          # die summarische Ladung im Reading "intotal" (Wh), sowie die summarische Entladung im Reading "outtotal" (Wh) -
        +

    - +
      -
    • currentForecastDev

      - - Legt das Device (Typ DWD_OpenData) fest, welches die benötigten Wetterdaten (Bewölkung, Niederschlag, - Sonnenauf- bzw. untergang usw.) liefert. - Ist noch kein Device dieses Typs vorhanden, muß es manuell definiert werden +
    • currentForecastDev

      + + Legt das Device (Typ DWD_OpenData) fest, welches die benötigten Wetterdaten (Bewölkung, Niederschlag, + Sonnenauf- bzw. untergang usw.) liefert. + Ist noch kein Device dieses Typs vorhanden, muß es manuell definiert werden (siehe DWD_OpenData Commandref).
      Im ausgewählten DWD_OpenData Device müssen mindestens diese Attribute gesetzt sein:

        - - +
        + - +
        forecastDays 1
        forecastProperties TTT,Neff,R101,ww,SunUp,SunRise,SunSet
        forecastResolution 1
        forecastResolution 1
        forecastStation <Stationscode der ausgewerteten DWD Station>
        -
      +

- +
    -
  • currentInverterDev <Inverter Device Name> pv=<Readingname>:<Einheit> etotal=<Readingname>:<Einheit> [capacity=<max. WR-Leistung>]

    - - Legt ein beliebiges Device und dessen Readings zur Lieferung der aktuellen PV Erzeugungswerte fest. - Es kann auch ein Dummy Device mit entsprechenden Readings sein. - Die Werte mehrerer Inverterdevices führt man z.B. in einem Dummy Device zusammen und gibt dieses Device mit den +
  • currentInverterDev <Inverter Device Name> pv=<Readingname>:<Einheit> etotal=<Readingname>:<Einheit> [capacity=<max. WR-Leistung>]

    + + Legt ein beliebiges Device und dessen Readings zur Lieferung der aktuellen PV Erzeugungswerte fest. + Es kann auch ein Dummy Device mit entsprechenden Readings sein. + Die Werte mehrerer Inverterdevices führt man z.B. in einem Dummy Device zusammen und gibt dieses Device mit den entsprechenden Readings an.
    Die Angabe von capacity ist optional, wird aber zur Optimierung der Vorhersagegenauigkeit dringend empfohlen.
    - -
      - - + +
        +
      +
      pv Reading welches die aktuelle PV-Erzeugung liefert
      etotal Reading welches die gesamte erzeugten Energie liefert (ein stetig aufsteigender Zähler)
      Einheit die jeweilige Einheit (W,kW,Wh,kWh)
      capacity Bemessungsleistung des Wechselrichters gemäß Datenblatt (max. möglicher Output in Watt)
      -
    +

- +
    Beispiel:
    set <name> currentInverterDev STP5000 pv=total_pac:kW etotal=etotal:kWh capacity=5000

    - # Device STP5000 liefert PV-Werte. Die aktuell erzeugte Leistung im Reading "total_pac" (kW) und die tägliche Erzeugung im + # Device STP5000 liefert PV-Werte. Die aktuell erzeugte Leistung im Reading "total_pac" (kW) und die tägliche Erzeugung im Reading "etotal" (kWh). Die max. Leistung des Wechselrichters beträgt 5000 Watt.

- +
    -
  • currentMeterDev <Meter Device Name> gcon=<Readingname>:<Einheit> contotal=<Readingname>:<Einheit> gfeedin=<Readingname>:<Einheit> feedtotal=<Readingname>:<Einheit>

    - - Legt ein beliebiges Device und seine Readings zur Energiemessung fest. +
  • currentMeterDev <Meter Device Name> gcon=<Readingname>:<Einheit> contotal=<Readingname>:<Einheit> gfeedin=<Readingname>:<Einheit> feedtotal=<Readingname>:<Einheit>

    + + Legt ein beliebiges Device und seine Readings zur Energiemessung fest. Das Modul geht davon aus dass der numerische Wert der Readings immer positiv ist. Es kann auch ein Dummy Device mit entsprechenden Readings sein. Die Bedeutung des jeweiligen "Readingname" ist:
    - -
      - - + +
        +
      +
      gcon Reading welches die aktuell aus dem Netz bezogene Leistung liefert
      contotal Reading welches die Summe der aus dem Netz bezogenen Energie liefert
      gfeedin Reading welches die aktuell in das Netz eingespeiste Leistung liefert
      feedtotal Reading welches die Summe der in das Netz eingespeisten Energie liefert
      Einheit die jeweilige Einheit (W,kW,Wh,kWh)
      -
    +

- - Sonderfälle: Sollte das Reading für gcon und gfeedin identisch, aber vorzeichenbehaftet sein, + + Sonderfälle: Sollte das Reading für gcon und gfeedin identisch, aber vorzeichenbehaftet sein, können die Schlüssel gfeedin und gcon wie folgt definiert werden:

    gfeedin=-gcon    (ein negativer Wert von gcon wird als gfeedin verwendet)
    gcon=-gfeedin    (ein negativer Wert von gfeedin wird als gcon verwendet)

- + Die Einheit entfällt in dem jeweiligen Sonderfall.

- +
    Beispiel:
    set <name> currentMeterDev Meter gcon=Wirkleistung:W contotal=BezWirkZaehler:kWh gfeedin=-gcon feedtotal=EinWirkZaehler:kWh

    - # Device Meter liefert den aktuellen Netzbezug im Reading "Wirkleistung" (W), + # Device Meter liefert den aktuellen Netzbezug im Reading "Wirkleistung" (W), die Summe des Netzbezugs im Reading "BezWirkZaehler" (kWh), die aktuelle Einspeisung in "Wirkleistung" wenn "Wirkleistung" negativ ist, die Summe der Einspeisung im Reading "EinWirkZaehler" (kWh) -
+
- +
    -
  • currentRadiationDev

    - - Legt die Quelle zur Lieferung der solaren Strahlungsdaten fest. Es kann ein Device vom Typ DWD_OpenData oder +
  • currentRadiationDev

    + + Legt die Quelle zur Lieferung der solaren Strahlungsdaten fest. Es kann ein Device vom Typ DWD_OpenData oder die SolCast API ausgewählt werden. Die Verwendung der SolCast API wird wegen Vorhersagequalität empfohlen.

    - + Bei Nutzung der SolCast API müssen vorab ein oder mehrere API-keys (Accounts) sowie ein oder mehrere Rooftop-ID's auf der SolCast Webseite angelegt werden. - Ein Rooftop ist im SolarForecast-Kontext mit einem inverterString + Ein Rooftop ist im SolarForecast-Kontext mit einem inverterString gleichzusetzen.
    - Es wird empfohlen bei Einsatz der SolCast API die Attribute + Es wird empfohlen bei Einsatz der SolCast API die Attribute affectCloudfactorDamping und - affectRainfactorDamping explizit auf 0 bzw. + affectRainfactorDamping explizit auf 0 bzw. pvCorrectionFactor_Auto auf "off" zu setzen. - +

    - - Soll der DWD-Dienst zur Lieferung von Strahlungsdaten dienen und ist noch kein Device des Typs DWD_OpenData vorhanden, + + Soll der DWD-Dienst zur Lieferung von Strahlungsdaten dienen und ist noch kein Device des Typs DWD_OpenData vorhanden, muß es manuell definiert werden (siehe DWD_OpenData Commandref).
    Im ausgewählten DWD_OpenData Device müssen mindestens diese Attribute gesetzt sein:

      - - +
      + - + - +
      forecastDays 1
      forecastProperties Rad1h
      forecastResolution 1
      forecastResolution 1
      forecastStation <Stationscode der ausgewerteten DWD Station>
      Hinweis: Die ausgewählte DWD Station muß Strahlungswerte (Rad1h Readings) liefern.
      Nicht alle Stationen liefern diese Daten.
      Nicht alle Stationen liefern diese Daten!
      -
    +

- +
    -
  • energyH4Trigger <1on>=<Wert> <1off>=<Wert> [<2on>=<Wert> <2off>=<Wert> ...]

    - +
  • energyH4Trigger <1on>=<Wert> <1off>=<Wert> [<2on>=<Wert> <2off>=<Wert> ...]

    + Generiert Trigger bei Über- bzw. Unterschreitung der 4-Stunden PV Vorhersage (NextHours_Sum04_PVforecast).
    - Überschreiten die letzten drei Messungen der 4-Stunden PV Vorhersagen eine definierte Xon-Bedingung, wird das Reading - energyH4Trigger_X = on erstellt/gesetzt. - Unterschreiten die letzten drei Messungen der 4-Stunden PV Vorhersagen eine definierte Xoff-Bedingung, wird das Reading + Überschreiten die letzten drei Messungen der 4-Stunden PV Vorhersagen eine definierte Xon-Bedingung, wird das Reading + energyH4Trigger_X = on erstellt/gesetzt. + Unterschreiten die letzten drei Messungen der 4-Stunden PV Vorhersagen eine definierte Xoff-Bedingung, wird das Reading energyH4Trigger_X = off erstellt/gesetzt.
    Es kann eine beliebige Anzahl von Triggerbedingungen angegeben werden. Xon/Xoff-Bedingungen müssen nicht zwingend paarweise definiert werden.

    - +
      Beispiel:
      set <name> energyH4Trigger 1on=2000 1off=1700 2on=2500 2off=2000 3off=1500
      @@ -10562,97 +10903,97 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.

    - +
      -
    • inverterStrings <Stringname1>[,<Stringname2>,<Stringname3>,...]

      - - Bezeichnungen der aktiven Strings. Diese Bezeichnungen werden als Schlüssel in den weiteren +
    • inverterStrings <Stringname1>[,<Stringname2>,<Stringname3>,...]

      + + Bezeichnungen der aktiven Strings. Diese Bezeichnungen werden als Schlüssel in den weiteren Settings verwendet.

      - +
        Beispiel:
        set <name> inverterStrings Ostdach,Südgarage,S3
        -
      +

- +
  • modulePeakString <Stringname1>=<Peak> [<Stringname2>=<Peak> <Stringname3>=<Peak> ...]

    - - Die DC Peakleistung des Strings "StringnameX" in kWp. Der Stringname ist ein Schlüsselwert des + + Die DC Peakleistung des Strings "StringnameX" in kWp. Der Stringname ist ein Schlüsselwert des Readings inverterStrings.

    - +
      Beispiel:
      set <name> modulePeakString Ostdach=5.1 Südgarage=2.0 S3=7.2
      -
    +

- +
    -
  • moduleDirection <Stringname1>=<dir> [<Stringname2>=<dir> <Stringname3>=<dir> ...]
    +
  • moduleDirection <Stringname1>=<dir> [<Stringname2>=<dir> <Stringname3>=<dir> ...]
    (nur bei Verwendung des DWD_OpenData RadiationDev)

    - - Ausrichtung <dir> der Solarmodule im String "StringnameX". Der Stringname ist ein Schlüsselwert des + + Ausrichtung <dir> der Solarmodule im String "StringnameX". Der Stringname ist ein Schlüsselwert des Readings inverterStrings.
    Die Richtungsangabe <dir> kann eine der folgenden Werte sein:

      - - +
      + - +
      N Nordausrichtung
      NE Nord-Ost Ausrichtung
      E Ostausrichtung
      E Ostausrichtung
      SE Süd-Ost Ausrichtung
      S Südausrichtung
      SW Süd-West Ausrichtung
      W Westausrichtung
      NW Nord-West Ausrichtung
      -
    +

- +
    Beispiel:
    set <name> moduleDirection Ostdach=E Südgarage=S S3=NW
    -
+
- +
  • moduleRoofTops <Stringname1>=<pk> [<Stringname2>=<pk> <Stringname3>=<pk> ...]
    (nur bei Verwendung Model SolCastAPI)

    - - Es erfolgt die Zuordnung des Strings "StringnameX" zu einem Schlüssel <pk>. Der Schlüssel <pk> wurde mit dem - Setter roofIdentPair angelegt. Damit wird bei Abruf des Rooftops (=String) - in der SolCast API die zu verwendende Rooftop-ID sowie der zu verwendende API-Key festgelegt.
    - Der StringnameX ist ein Schlüsselwert des Readings inverterStrings. + + Es erfolgt die Zuordnung des Strings "StringnameX" zu einem Schlüssel <pk>. Der Schlüssel <pk> wurde mit dem + Setter roofIdentPair angelegt. Damit wird bei Abruf des Rooftops (=String) + in der SolCast API die zu verwendende Rooftop-ID sowie der zu verwendende API-Key festgelegt.
    + Der StringnameX ist ein Schlüsselwert des Readings inverterStrings.

    - +
      Beispiel:
      set <name> moduleRoofTops Ostdach=p1 Südgarage=p2 S3=p3
      -
    +

- +
    -
  • moduleTiltAngle <Stringname1>=<Winkel> [<Stringname2>=<Winkel> <Stringname3>=<Winkel> ...]
    +
  • moduleTiltAngle <Stringname1>=<Winkel> [<Stringname2>=<Winkel> <Stringname3>=<Winkel> ...]
    (nur bei Verwendung des DWD_OpenData RadiationDev)

    - + Neigungswinkel der Solarmodule. Der Stringname ist ein Schlüsselwert des Readings inverterStrings.
    - Mögliche Neigungswinkel sind: 0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90 + Mögliche Neigungswinkel sind: 0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90 (0 = waagerecht, 90 = senkrecht).

    - +
      Beispiel:
      set <name> moduleTiltAngle Ostdach=40 Südgarage=60 S3=30
      @@ -10660,39 +11001,39 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.

    - +
      -
    • plantConfiguration

      - +
    • plantConfiguration

      + Je nach ausgewählter Kommandooption werden folgende Operationen ausgeführt:

        - - +
        +
        check Zeigt die aktuelle Stringkonfiguration. Es wird gleichzeitig eine Plausibilitätsprüfung
        vorgenommen und das Ergebnis sowie eventuelle Anweisungen zur Fehlerbehebung ausgegeben.
        save sichert wichtige Parameter der Anlagenkonfiguration
        restore stellt eine gesicherte Anlagenkonfiguration wieder her
        -
      +

- +

- +
    -
  • pvCorrectionFactor_XX <Zahl>

    - +
  • pvCorrectionFactor_XX <Zahl>

    + Manueller Korrekturfaktor für die Stunde XX des Tages zur Anpassung der Vorhersage an die individuelle Anlage.
    - (default: 1.0) + (default: 1.0)

- +
    -
  • reset

    - - Löscht die aus der Drop-Down Liste gewählte Datenquelle, zu der Funktion gehörende Readings oder weitere interne +
  • reset

    + + Löscht die aus der Drop-Down Liste gewählte Datenquelle, zu der Funktion gehörende Readings oder weitere interne Datenstrukturen.

      - - +
      + @@ -10798,8 +11139,8 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. - - + + @@ -10811,104 +11152,104 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.
      consumerPlanning löscht die Planungsdaten aller registrierten Verbraucher
      Um die Planungsdaten nur eines Verbrauchers zu löschen verwendet man:
        set <name> reset consumerPlanning <Verbrauchernummer>
      Um alle bisher gespeicherten PV Korrekturfaktoren aus den Caches zu löschen:
        set <name> reset pvCorrection cached
      Um gespeicherte PV Korrekturfaktoren einer bestimmten Stunde aus den Caches zu löschen:
        set <name> reset pvCorrection cached <Stunde>
        (z.B. set <name> reset pvCorrection cached 10)
        set <name> reset pvCorrection cached <Stunde>
        (z.B. set <name> reset pvCorrection cached 10)
      pvHistory löscht den Speicher aller historischen Tage (01 ... 31)
      Um einen bestimmten historischen Tag zu löschen:
        set <name> reset pvHistory <Tag> (z.B. set <name> reset pvHistory 08)
        set <name> reset roofIdentPair <pk> (z.B. set <name> reset roofIdentPair p1)
      -
    +

- +
  • roofIdentPair <pk> rtid=<Rooftop-ID> apikey=<SolCast API Key>
    (nur bei Verwendung Model SolCastAPI)

    - - Der Abruf jedes in SolCast Rooftop Sites + + Der Abruf jedes in SolCast Rooftop Sites angelegten Rooftops ist mit der Angabe eines Paares Rooftop-ID und API-Key zu identifizieren.
    - Der Schlüssel <pk> kennzeichnet eindeutig ein verbundenes Paar Rooftop-ID / API-Key. Es können beliebig viele + Der Schlüssel <pk> kennzeichnet eindeutig ein verbundenes Paar Rooftop-ID / API-Key. Es können beliebig viele Paare nacheinander angelegt werden. In dem Fall ist jeweils ein neuer Name für "<pk>" zu verwenden.

    - - Der Schlüssel <pk> wird im Setter moduleRoofTops der abzurufenden + + Der Schlüssel <pk> wird im Setter moduleRoofTops der abzurufenden Rooftops (=Strings) zugeordnet.

    - +
      Beispiele:
      set <name> roofIdentPair p1 rtid=92fc-6796-f574-ae5f apikey=oNHDbkKuC_eGEvZe7ECLl6-T1jLyfOgC
      set <name> roofIdentPair p2 rtid=f574-ae5f-92fc-6796 apikey=eGEvZe7ECLl6_T1jLyfOgC_oNHDbkKuC
      -
    - -
    +
+ +

- +
    -
  • writeHistory

    - - Die vom Device gesammelten historischen PV Daten werden in ein File geschrieben. Dieser Vorgang wird per default - regelmäßig im Hintergrund ausgeführt. Im Internal "HISTFILE" wird der Filename und der Zeitpunkt der letzten - Speicherung dokumentiert.
    +
  • writeHistory

    + + Die vom Device gesammelten historischen PV Daten werden in eine Datei geschrieben. Dieser Vorgang wird per default + regelmäßig im Hintergrund ausgeführt. Im Internal "HISTFILE" wird der Dateiname und der Zeitpunkt der letzten + Speicherung dokumentiert.

- +
- + - Get -
    + Get +
        -
      • data

        +
      • data

        Startet die Datensammlung zur Bestimmung der solaren Vorhersage und anderer Werte. -
      • +

      - +
      • forecastQualities

        - Zeigt die aktuell verwendeten Korrekturfaktoren mit der jeweiligen Startzeit zur Bestimmung der PV Vorhersage sowie + Zeigt die aktuell verwendeten Korrekturfaktoren mit der jeweiligen Startzeit zur Bestimmung der PV Vorhersage sowie deren Qualitäten an. - Die Qualität ergibt sich aus der Anzahl der bereits in der Vergangenheit bewerteten Tage mit einer - identischen Bewölkungsrange. -
      • + Die Qualität ergibt sich aus der Anzahl der bereits in der Vergangenheit bewerteten Tage mit einer + identischen Bewölkungsrange. +

      - +
      • html

        Die Solar Grafik wird als HTML-Code abgerufen und wiedergegeben.
        Die Grafik kann abgerufen und in eigenen Code eingebettet werden. Auf einfache Weise kann dies durch die Definition eines weblink-Devices vorgenommen werden:

        - +
          define wl.SolCast5 weblink htmlCode { FHEM::SolarForecast::pageAsHtml ('SolCast5') }

        'SolCast5' ist der Name des einzubindenden SolarForecast-Device. -
      • +

      - +
        -
      • nextHours

        +
      • nextHours

        Listet die erwarteten Werte der kommenden Stunden auf.

        - +
          - - +
          + - + @@ -10917,23 +11258,23 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. - +
          starttime Startzeit des Datensatzes
          hourofday laufende Stunde des Tages
          pvfc erwartete PV Erzeugung (Wh)
          today "1" wenn Startdatum am aktuellen Tag
          confc erwarteter Energieverbrauch inklusive der Anteile registrierter Verbraucher
          confcEx erwarteter Energieverbrauch ohne der Anteile registrierter Verbraucher
          wid ID des vorhergesagten Wetters
          wid ID des vorhergesagten Wetters
          wcc vorhergesagter Grad der Bewölkung
          crange berechneter Bewölkungsbereich
          correff verwendeter Korrekturfaktor/Qualität
          Faktor/1...X - Korrektur aus Store genutzt (höhere Zahl = bessere Qualität)
          wrp vorhergesagter Grad der Regenwahrscheinlichkeit
          Rad1h vorhergesagte Globalstrahlung
          temp vorhergesagte Außentemperatur
          temp vorhergesagte Außentemperatur
        -
      • +

      - +
      • pvHistory

        - Listet die historischen Werte der letzten Tage (max. 31) sortiert nach dem Tagesdatum und Stunde. - Die Stundenangaben beziehen sich auf die jeweilige Stunde des Tages, z.B. bezieht sich die Stunde 09 auf die Zeit + Listet die historischen Werte der letzten Tage (max. 31) sortiert nach dem Tagesdatum und Stunde. + Die Stundenangaben beziehen sich auf die jeweilige Stunde des Tages, z.B. bezieht sich die Stunde 09 auf die Zeit von 08 - 09 Uhr.

        - +
          - - +
          + @@ -10956,22 +11297,22 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.
          etotal totaler Energieertrag (Wh) zu Beginn der Stunde
          pvfc der prognostizierte PV Ertrag (Wh)
          pvrl reale PV Erzeugung (Wh)
          cyclescsmXX Anzahl aktive Zyklen von ConsumerXX des Tages
        -
      • +

      - +
      • pvCircular

        - Listet die vorhandenen Werte im Ringspeicher auf. - Die Stundenangaben 00 - 24 beziehen sich auf die Stunde des Tages, z.B. bezieht sich die Stunde 09 auf die Zeit von + Listet die vorhandenen Werte im Ringspeicher auf. + Die Stundenangaben 00 - 24 beziehen sich auf die Stunde des Tages, z.B. bezieht sich die Stunde 09 auf die Zeit von 08 - 09 Uhr.
        - Die Stunde 99 hat eine Sonderfunktion.
        + Die Stunde 99 hat eine Sonderfunktion.
        Erläuterung der Werte:

        - +
          - - +
          + @@ -10990,66 +11331,66 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.
          pvfc PV Vorhersage für die nächsten 24h ab aktueller Stunde des Tages
          pvrl reale PV Erzeugung der letzten 24h (Achtung: pvforecast und pvreal beziehen sich nicht auf den gleichen Zeitraum!)
          confc erwarteter Energieverbrauch (Wh)
          ydayDvtn gestrige Abweichung PV Prognose/Erzeugung in %
        - -
      • + +

      - +
      • roofTopData
        (nur bei Verwendung Model SolCastAPI)

        - - Die erwarteten solaren Strahlungsdaten der definierten RoofTops werden von der SolCast API abgerufen. -
      • + + Die erwarteten solaren Strahlungsdaten der definierten RoofTops werden von der SolCast API abgerufen. +

      - +
      • solCastData
        (nur bei Verwendung Model SolCastAPI)

        - + Listet die im Kontext der SolCast-API gespeicherten Daten auf. - Verwaltungsdatensätze sind mit einem führenden '?' gekennzeichnet. + Verwaltungsdatensätze sind mit einem führenden '?' gekennzeichnet. Die von der API gelieferten Vorhersagedaten bzgl. des PV Ertrages (Wh) sind auf eine Stunde konsolidiert.

        - +
          - - - +
          currentAPIinterval das aktuell verwendete API Abrufintervall in Sekunden
          + + - - + + -
          currentAPIinterval das aktuell verwendete API Abrufintervall in Sekunden
          lastretrieval_time Zeit des letzten SolCast API Abrufs
          lastretrieval_timestamp Unix Timestamp des letzten SolCast API Abrufs
          pv_estimate erwartete PV Erzeugung von SolCast API (Wh)
          todayDoneAPIrequests Anzahl der ausgeführten API Requests am aktuellen Tag
          todayRemainingAPIrequests Anzahl der verbleibenden API Requests am aktuellen Tag
          todayDoneAPIcalls Anzahl der ausgeführten API Abrufe am aktuellen Tag
          todayRemainingAPIcalls Anzahl der noch möglichen API Abrufe am aktuellen Tag
          (ein Abruf kann mehrere API Requests ausführen)
          todayRemainingAPIcalls Anzahl der noch möglichen API Abrufe am aktuellen Tag
          (ein Abruf kann mehrere API Requests ausführen)
          todayMaxAPIcalls Anzahl der maximal möglichen API Abrufe pro Tag
          +
        -
      • +

      - +
      • valConsumerMaster

        Listet die aktuell ermittelten Stammdaten der im Device registrierten Verbraucher auf. -
      • +

      - +
      • valCurrent

        Listet aktuelle Betriebsdaten, Kennzahlen und Status auf. -
      • +

      - +

    @@ -11061,19 +11402,19 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.
  • affect70percentRule
    Wenn gesetzt, wird die prognostizierte Leistung entsprechend der 70% Regel begrenzt.

    - -
      - - + +
        +
      +
      0 keine Begrenzung der prognostizierten PV-Erzeugung (default)
      1 die prognostizierte PV-Erzeugung wird auf 70% der installierten Stringleistung(en) begrenzt
      dynamic die prognostizierte PV-Erzeugung wird begrenzt wenn 70% der installierten
      Stringleistung(en) zzgl. des prognostizierten Verbrauchs überschritten wird
      -
    +

- +
  • affectBatteryPreferredCharge
    Es werden Verbraucher mit dem Mode can erst dann eingeschaltet, wenn die angegebene Batterieladung (%) @@ -11082,16 +11423,16 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. (default: 0)

  • - +
  • affectCloudfactorDamping
    Prozentuale Mehrgewichtung des Bewölkungsfaktors bei der solaren Vorhersage.
    - Größere Werte vermindern, kleinere Werte erhöhen tendenziell den prognostizierten PV Ertrag (Dämpfung der PV + Größere Werte vermindern, kleinere Werte erhöhen tendenziell den prognostizierten PV Ertrag (Dämpfung der PV Prognose durch den Bewölkungsfaktor).
    - (default: 35) -
  • -
    - + (default: 35) + +
    +
  • affectConsForecastIdentWeekdays
    Wenn gesetzt, werden zur Berechnung der Verbrauchsprognose nur gleiche Wochentage (Mo..So) einbezogen.
    @@ -11099,46 +11440,46 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. (default: 0)

  • - +
  • affectMaxDayVariance <Zahl>
    (nur bei Verwendung Model DWD)

    - + Maximale Änderungsgröße des PV Vorhersagefaktors (Reading pvCorrectionFactor_XX) pro Tag.
    (default: 0.5)

  • - +
  • affectNumHistDays
    - Anzahl der in den Caches verfügbaren historischen Tage, die zur Berechnung der Autokorrekturwerte der + Anzahl der in den Caches verfügbaren historischen Tage, die zur Berechnung der Autokorrekturwerte der PV Vorhersage verwendet werden sollen.
    (default: alle verfügbaren Daten in pvHistory und pvCircular)

  • - +
  • affectRainfactorDamping
    Prozentuale Mehrgewichtung des Regenprognosefaktors bei der solaren Vorhersage.
    - Größere Werte vermindern, kleinere Werte erhöhen tendenziell den prognostizierten PV Ertrag (Dämpfung der PV + Größere Werte vermindern, kleinere Werte erhöhen tendenziell den prognostizierten PV Ertrag (Dämpfung der PV Prognose durch den Regenfaktor).
    - (default: 10) -
  • -
    - + (default: 10) + +
    +
  • alias
    In Verbindung mit "ctrlShowLink" ein beliebiger Anzeigename.

  • - +
  • consumerAdviceIcon
    Definiert die Art der Information über die geplanten Schaltzeiten eines Verbrauchers in der Verbraucherlegende. -

    -
      - - +

      +
        +
      + @@ -11146,63 +11487,71 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.
      <Icon>@<Farbe> Aktivierungsempfehlung wird durch Icon und Farbe (optional) dargestellt (default: light_light_dim_100@gold)
      (die Planungsdaten werden als Mouse-Over Text angezeigt
      times es werden der Planungsstatus und die geplanten Schaltzeiten als Text angezeigt
  • -
    +
  • consumerLegend
    - Definiert die Lage bzw. Darstellungsweise der Verbraucherlegende sofern Verbraucher im SolarForecast Device + Definiert die Lage bzw. Darstellungsweise der Verbraucherlegende sofern Verbraucher im SolarForecast Device registriert sind.
    (default: icon_top)

  • + +
  • consumerLink
    + Wenn gesetzt, kann man in der Verbraucher-Liste (consumerLegend) die jeweiligen Verbraucher anklicken und gelangt + direkt zur Detailansicht des jeweiligen Geräts auf einer neuen Browserseite.
    + (default: 1) +
  • +
    +
  • consumerXX <Device Name> type=<type> power=<power> [mode=<mode>] [icon=<Icon>] [mintime=<minutes>]
    [on=<Kommando>] [off=<Kommando>] [swstate=<Readingname>:<on-Regex>:<off-Regex>] [notbefore=<Stunde>] [notafter=<Stunde>]
    [auto=<Readingname>] [pcurr=<Readingname>:<Einheit>[:<Schwellenwert>]] [etotal=<Readingname>:<Einheit>[:<Schwellenwert>]]
    [swoncond=<Device>:<Reading>:<Regex>] [swoffcond=<Device>:<Reading>:<Regex>] [interruptable=<Option>]


    - + Registriert einen Verbraucher <Device Name> beim SolarForecast Device. Dabei ist <Device Name> 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 + 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 + Ist der Schüssel "auto" definiert, kann der Automatikmodus in der integrierten Verbrauchergrafik mit den entsprechenden Drucktasten umgeschaltet werden. Das angegebene Reading wird ggf. im Consumer Device angelegt falls es nicht vorhanden ist.

    - - Mit dem optionalen Schlüssel swoncond kann eine zusätzliche externe Bedingung definiert werden um den Einschaltvorgang des - Consumers freizugeben. Ist die Bedingung (Regex) nicht erfüllt, erfolgt kein Einschalten des Verbrauchers auch wenn die - sonstigen Voraussetzungen wie Zeitplanung, on-Schlüssel, auto-Mode und aktuelle PV-Leistung gegeben sind. Es erfolgt somit eine + + Mit dem optionalen Schlüssel swoncond kann eine zusätzliche externe Bedingung definiert werden um den Einschaltvorgang des + Consumers freizugeben. Ist die Bedingung (Regex) nicht erfüllt, erfolgt kein Einschalten des Verbrauchers auch wenn die + sonstigen Voraussetzungen wie Zeitplanung, on-Schlüssel, auto-Mode und aktuelle PV-Leistung gegeben sind. Es erfolgt somit eine UND-Verknüpfung des Schlüssels swoncond mit den weiteren Einschaltbedingungen.

    - Der optionale Schlüssel swoffcond definiert eine vorrangige Ausschaltbedingung (Regex). Sobald diese - Bedingung erfüllt ist, wird der Consumer ausgeschaltet auch wenn die geplante Endezeit (consumerXX_planned_stop) + Der optionale Schlüssel swoffcond definiert eine vorrangige Ausschaltbedingung (Regex). Sobald diese + Bedingung erfüllt ist, wird der Consumer ausgeschaltet auch wenn die geplante Endezeit (consumerXX_planned_stop) noch nicht erreicht ist (ODER-Verknüpfung). Weitere Bedingungen wie off-Schlüssel und auto-Mode müssen zum automatischen Ausschalten erfüllt sein.

    - Mit dem optionalen Schlüssel interruptable kann während der geplanten Einschaltzeit eine automatische + Mit dem optionalen Schlüssel interruptable kann während der geplanten Einschaltzeit eine automatische Unterbrechung sowie Wiedereinschaltung des Verbrauchers vorgenommen werden. - Der Verbraucher wird temporär ausgeschaltet (interrupted) und wieder eingeschaltet (continued) wenn die - Interrupt-Bedingung nicht mehr vorliegt. - Die verbleibende Laufzeit wird durch einen Interrupt nicht beeinflusst ! + Der Verbraucher wird temporär ausgeschaltet (interrupted) und wieder eingeschaltet (continued) wenn die + Interrupt-Bedingung nicht mehr vorliegt. + Die verbleibende Laufzeit wird durch einen Interrupt nicht beeinflusst!

    - + Der Schlüssel power gibt die nominale Leistungsaufnahme des Verbrauchers gemäß seines Datenblattes an. Dieser Wert wird verwendet um das Schalten des Verbrauchers in Abhängigkeit des aktuellen PV-Überschusses zu steuern. Ist power=0 gesetzt, wird der Verbraucher unabhängig von einem ausreichenden PV-Überschuß geschaltet.

    - -
      - - + +
        +
      + - - - + + + @@ -11211,7 +11560,13 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. - + + + + + + + @@ -11245,97 +11600,122 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.
      type Typ des Verbrauchers. Folgende Typen sind erlaubt:
      dishwasher - Verbaucher ist eine Spülmaschine
      dryer - Verbaucher ist ein Wäschetrockner
      washingmachine - Verbaucher ist eine Waschmaschine
      heater - Verbaucher ist ein Heizstab
      charger - Verbaucher ist eine Ladeeinrichtung (Akku, Auto, etc.)
      other - Verbraucher ist keiner der vorgenannten Typen
      power nominale Leistungsaufnahme des Verbrauchers (siehe Datenblatt) in W
      charger - Verbaucher ist eine Ladeeinrichtung (Akku, Auto, Fahrrad, etc.)
      other - Verbraucher ist keiner der vorgenannten Typen
      power nominale Leistungsaufnahme des Verbrauchers (siehe Datenblatt) in W
      (kann auf "0" gesetzt werden)
      mode Planungsmodus des Verbrauchers (optional). Erlaubt sind:
      can - Die Einplanung erfolgt zum Zeitpunkt mit wahrscheinlich genügend verfügbaren PV Überschuß (default)
                 Der Start des Verbrauchers erfolgt auch bei ungenügendem PV-Überschuß.
      icon Icon zur Darstellung des Verbrauchers in der Übersichtsgrafik (optional)
      mintime Mindestlaufzeit bzw. typische Laufzeit des Verbrauchers nach dem initialen Einschalten in Minuten (optional)
      Die Standard mintime richtet sich nach dem Verbrauchertyp, ist aber mindestens 60 Minuten.
      Die Standard mintime richtet sich nach dem Verbrauchertyp, ist aber mindestens 60 Minuten.
      Default pro Verbrauchertyp:
      - dishwasher: 180 Minuten
      - dryer: 90 Minuten
      - washingmachine: 120 Minuten
      - heater: 240 Minuten
      - charger: 120 Minuten
      on Set-Kommando zum Einschalten des Verbrauchers (optional)
      off Set-Kommando zum Ausschalten des Verbrauchers (optional)
      swstate Reading welches den Schaltzustand des Consumers anzeigt (default: 'state').

    - +
      Beispiele:
      attr <name> consumer01 wallplug icon=scene_dishwasher@orange type=dishwasher mode=can power=2500 on=on off=off notafter=20 etotal=total:kWh:5
      attr <name> consumer02 WPxw type=heater mode=can power=3000 mintime=180 on="on-for-timer 3600" notafter=12 auto=automatic
      attr <name> consumer03 Shelly.shellyplug2 type=other power=300 mode=must icon=it_ups_on_battery mintime=120 on=on off=off swstate=state:on:off auto=automatic pcurr=relay_0_power:W etotal:relay_0_energy_Wh:Wh swoncond=EcoFlow:data_data_socSum:-?([1-7][0-9]|[0-9]) swoffcond:EcoFlow:data_data_socSum:100
      attr <name> consumer04 Shelly.shellyplug3 icon=scene_microwave_oven type=heater power=2000 mode=must notbefore=07 mintime=600 on=on off=off etotal=relay_0_energy_Wh:Wh pcurr=relay_0_power:W auto=automatic interruptable=eg.wz.wandthermostat:diff-temp:(22)(\.[2-9])|([2-9][3-9])(\.[0-9]):0.2
      -
    -
  • + +
    - +
  • ctrlAutoRefresh
    - Wenn gesetzt, werden aktive Browserseiten des FHEMWEB-Devices welches das SolarForecast-Device aufgerufen hat, nach der - eingestellten Zeit (Sekunden) neu geladen. Sollen statt dessen Browserseiten eines bestimmten FHEMWEB-Devices neu + Wenn gesetzt, werden aktive Browserseiten des FHEMWEB-Devices welches das SolarForecast-Device aufgerufen hat, nach der + eingestellten Zeit (Sekunden) neu geladen. Sollen statt dessen Browserseiten eines bestimmten FHEMWEB-Devices neu geladen werden, kann dieses Device mit dem Attribut "ctrlAutoRefreshFW" festgelegt werden.

  • - +
  • ctrlAutoRefreshFW
    Ist "ctrlAutoRefresh" aktiviert, kann mit diesem Attribut das FHEMWEB-Device bestimmt werden dessen aktive Browserseiten regelmäßig neu geladen werden sollen.

  • - +
  • ctrlConsRecommendReadings
    Für die ausgewählten Consumer (Nummer) werden Readings der Form consumerXX_ConsumptionRecommended erstellt.
    Diese Readings signalisieren ob das Einschalten dieses Consumers abhängig von seinen Verbrauchsdaten und der aktuellen - PV-Erzeugung bzw. des aktuellen Energieüberschusses empfohlen ist. Der Wert des erstellten Readings korreliert + PV-Erzeugung bzw. des aktuellen Energieüberschusses empfohlen ist. Der Wert des erstellten Readings korreliert mit den berechneten Planungsdaten das Consumers, kann aber von dem Planungszeitraum abweichen.
    -
    +

  • - +
  • ctrlDebug
    - Aktiviert/deaktiviert Debug-Meldungen im Modul. + Aktiviert/deaktiviert verschiedene Debug Module. Ist ausschließlich "none" selektiert erfolgt keine DEBUG-Ausgabe. + Zur Ausgabe von Debug Meldungen muß der globale verbose Level mindestens "1" sein.
    + Die Debug Module können miteinander kombiniert werden:

    + +
      + + + + + + + + + + + +
      collectData detailliierte Datensammlung
      consumerPlanning Consumer Einplanungsprozesse
      consumerSwitching Operationen des internen Consumer Schaltmodul
      consumption Verbrauchskalkulation und -nutzung
      graphic Informationen der Modulgrafik
      pvCorrection Erstellung und Anwendung der Autokorrektur
      radiationProcess Sammlung und Verarbeitung der Solarstrahlungsdaten
      saveData2Cache Datenspeicherung in internen Speicherstrukturen
      solcastProcess Abruf und Verarbeitung von SolCast API Daten
      +

  • - +
  • ctrlInterval <Sekunden>
    Zeitintervall der Datensammlung.
    - Ist ctrlInterval explizit auf "0" gesetzt, erfolgt keine automatische Datensammlung und muss mit "get <name> data" - manuell erfolgen.
    + Ist ctrlInterval explizit auf "0" gesetzt, erfolgt keine automatische Datensammlung und muss mit + "get <name> data" manuell erfolgen.
    (default: 70)

  • + +
  • ctrlLanguage <DE | EN>
    + Legt die benutzte Sprache des Devices fest. Die Sprachendefinition hat Auswirkungen auf die Modulgrafik und + verschiedene Readinginhalte.
    + Ist das Attribut nicht gesetzt, definiert sich die Sprache durch die Einstellung des globalen Attributs "language".
    + (default: EN) +

  • +
  • ctrlNextDayForecastReadings <01,02,..,24>
    Wenn gesetzt, werden Readings der Form Tomorrow_Hour<hour>_PVforecast erstellt.
    - Diese Readings enthalten die voraussichtliche PV Erzeugung des kommenden Tages. Dabei ist <hour> die + Diese Readings enthalten die voraussichtliche PV Erzeugung des kommenden Tages. Dabei ist <hour> die Stunde des Tages.

    - +
      Beispiel:
      attr <name> ctrlNextDayForecastReadings 09,11
      # erstellt Readings für die Stunde 09 (08:00-09:00) und 11 (10:00-11:00) des kommenden Tages -
    - + +

  • - +
  • ctrlOptimizeSolCastInterval
    (nur bei Verwendung Model SolCastAPI)

    - + Das default Abrufintervall der SolCast API beträgt 1 Stunde. Ist dieses Attribut gesetzt erfolgt ein dynamische - Anpassung des Intervalls mit dem Ziel die maximal möglichen Abrufe innerhalb von Sonnenauf- und untergang + Anpassung des Intervalls mit dem Ziel die maximal möglichen Abrufe innerhalb von Sonnenauf- und untergang auszunutzen.
    (default: 0)

  • - +
  • ctrlShowLink
    Anzeige des Links zur Detailansicht des Device über dem Grafikbereich
    (default: 1)

  • - +
  • ctrlStatisticReadings
    Für die ausgewählten Kennzahlen und Indikatoren werden Readings erstellt.

    - -
      - - + +
        +
      + @@ -11351,128 +11731,128 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.
      allStringsFullfilled Erfüllungsstatus der fehlerfreien Generierung aller Strings
      currentAPIinterval das aktuelle Abrufintervall der SolCast API (nur Model SolCastAPI) in Sekunden
      lastretrieval_time der letze Abrufzeitpunkt der SolCast API (nur Model SolCastAPI)
      todayRemainingAPIcalls die Anzahl der am aktuellen Tag noch möglichen SolCast API Calls (nur Model SolCastAPI)
      todayRemainingAPIrequests die Anzahl der am aktuellen Tag noch möglichen SolCast API Requests (nur Model SolCastAPI)
      -
    -
    + +

  • - +
  • flowGraphicCss
    - Definiert den Style für die Energieflußgrafik. Das Attribut wird automatisch vorbelegt. + Definiert den Style für die Energieflußgrafik. Das Attribut wird automatisch vorbelegt. Zum Ändern des flowGraphicCss-Attributes bitte den Default übernehmen und anpassen:

    - -
      - .flowg.text { stroke: none; fill: gray; font-size: 60px; }
      - .flowg.sun_active { stroke: orange; fill: orange; }
      - .flowg.sun_inactive { stroke: gray; fill: gray; }
      - .flowg.bat25 { stroke: red; fill: red; }
      - .flowg.bat50 { stroke: darkorange; fill: darkorange; }
      - .flowg.bat75 { stroke: green; fill: green; }
      - .flowg.grid_color1 { fill: green; }
      - .flowg.grid_color2 { fill: red; }
      - .flowg.grid_color3 { fill: gray; }
      + +
        + .flowg.text { stroke: none; fill: gray; font-size: 60px; }
        + .flowg.sun_active { stroke: orange; fill: orange; }
        + .flowg.sun_inactive { stroke: gray; fill: gray; }
        + .flowg.bat25 { stroke: red; fill: red; }
        + .flowg.bat50 { stroke: darkorange; fill: darkorange; }
        + .flowg.bat75 { stroke: green; fill: green; }
        + .flowg.grid_color1 { fill: green; }
        + .flowg.grid_color2 { fill: red; }
        + .flowg.grid_color3 { fill: gray; }
        .flowg.inactive_in { stroke: gray; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.2; }
        .flowg.inactive_out { stroke: gray; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.2; }
        .flowg.active_in { stroke: red; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; }
        .flowg.active_out { stroke: darkorange; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; }
        .flowg.active_bat_in { stroke: darkorange; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; }
        - .flowg.active_bat_out { stroke: green; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; }
        -
      - + .flowg.active_bat_out { stroke: green; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; }
      +
    +
  • -
    - +
    +
  • flowGraphicAnimate
    - Animiert die Energieflußgrafik sofern angezeigt. + Animiert die Energieflußgrafik sofern angezeigt. Siehe auch Attribut graphicSelect.
    (default: 0)

  • - +
  • flowGraphicConsumerDistance
    - Steuert den Abstand zwischen den Consumer-Icons in der Energieflußgrafik sofern angezeigt. + Steuert den Abstand zwischen den Consumer-Icons in der Energieflußgrafik sofern angezeigt. Siehe auch Attribut flowGraphicShowConsumer.
    (default: 130)

  • - +
  • flowGraphicShowConsumer
    - Unterdrückt die Anzeige der Verbraucher in der Energieflußgrafik wenn auf "0" gesetzt.
    + Unterdrückt die Anzeige der Verbraucher in der Energieflußgrafik wenn auf "0" gesetzt.
    (default: 1)

  • - +
  • flowGraphicShowConsumerDummy
    - Zeigt bzw. unterdrückt den Dummy-Verbraucher in der Energieflußgrafik.
    + Zeigt bzw. unterdrückt den Dummy-Verbraucher in der Energieflußgrafik.
    Dem Dummy-Verbraucher wird der Energieverbrauch zugewiesen der anderen Verbrauchern nicht zugeordnet werden konnte.
    (default: 1)
  • -
    - +
    +
  • flowGraphicShowConsumerPower
    - Zeigt bzw. unterdrückt den Energieverbrauch der Verbraucher in der Energieflußgrafik.
    + Zeigt bzw. unterdrückt den Energieverbrauch der Verbraucher in der Energieflußgrafik.
    (default: 1)
  • -
    - +
    +
  • flowGraphicShowConsumerRemainTime
    - Zeigt bzw. unterdrückt die Restlaufzeit (in Minuten) der Verbraucher in der Energieflußgrafik.
    + Zeigt bzw. unterdrückt die Restlaufzeit (in Minuten) der Verbraucher in der Energieflußgrafik.
    (default: 1)
  • -
    - +
    +
  • flowGraphicSize <Pixel>
    - Größe der Energieflußgrafik sofern angezeigt. + Größe der Energieflußgrafik sofern angezeigt. Siehe auch Attribut graphicSelect.
    (default: 400)

  • - +
  • graphicBeam1Color
    - Farbauswahl der primären Balken. + Farbauswahl der primären Balken.

  • - +
  • graphicBeam1FontColor
    Auswahl der Schriftfarbe des primären Balken.
    (default: 0D0D0D)
  • -
    - +
    +
  • graphicBeam1Content
    Legt den darzustellenden Inhalt der primären Balken fest. - -
      - - + +
        +
      +
      pvReal reale PV-Erzeugung (default)
      pvForecast prognostizierte PV-Erzeugung
      gridconsumption Energie Bezug aus dem Netz
      consumptionForecast prognostizierter Energieverbrauch
      -
    +
  • -
    - +
    +
  • graphicBeam1MaxVal <0...val>
    - Festlegung des maximalen Betrags des primären Balkens (Stundenwert) zur Berechnung der maximalen Balkenhöhe. + Festlegung des maximalen Betrags des primären Balkens (Stundenwert) zur Berechnung der maximalen Balkenhöhe. Dadurch erfolgt eine Anpassung der zulässigen Gesamthöhe der Grafik.
    Mit dem Wert "0" erfolgt eine dynamische Anpassung.
    (default: 0)

  • - +
  • graphicBeam2Color
    Farbauswahl der sekundären Balken. Die zweite Farbe ist nur sinnvoll für den Anzeigedevice-Typ "pvco" und "diff". @@ -11484,24 +11864,24 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. Auswahl der Schriftfarbe des sekundären Balken.
    (default: 000000)
  • -
    - +
    +
  • graphicBeam2Content
    - Legt den darzustellenden Inhalt der sekundären Balken fest. + Legt den darzustellenden Inhalt der sekundären Balken fest. -
      - - +
        +
      +
      pvForecast prognostizierte PV-Erzeugung (default)
      pvReal reale PV-Erzeugung
      gridconsumption Energie Bezug aus dem Netz
      consumptionForecast prognostizierter Energieverbrauch
      -
    +

  • - +
  • graphicBeamHeight <value>
    Höhe der Balken in px und damit Bestimmung der gesammten Höhe. @@ -11509,47 +11889,47 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. (default: 200)

  • - +
  • graphicBeamWidth <value>
    - Breite der Balken der Balkengrafik in px. Ohne gesetzen Attribut wird die Balkenbreite durch das Modul + Breite der Balken der Balkengrafik in px. Ohne gesetzen Attribut wird die Balkenbreite durch das Modul automatisch bestimmt.
  • -
    - +
    +
  • graphicEnergyUnit <Wh | kWh>
    - Definiert die Einheit zur Anzeige der elektrischen Leistung in der Grafik. Der Wert wird auf eine + Definiert die Einheit zur Anzeige der elektrischen Leistung in der Grafik. Der Wert wird auf eine Nachkommastelle gerundet.
    (default: Wh)

  • - +
  • graphicHeaderDetail
    Detaillierungsgrad des Grafik Kopfbereiches.
    (default: all) - -
      - - + +
        +
      + - +
      all Anzeige Erzeugung (PV), Verbrauch (CO), Link zur Detailanzeige + Aktualisierungszeit (default)
      co nur Verbrauch (CO)
      pv nur Erzeugung (PV)
      pvco Erzeugung (PV) und Verbrauch (CO)
      pvco Erzeugung (PV) und Verbrauch (CO)
      statusLink Link zur Detailanzeige + Statusinformationen
      -
    +
  • -
    - +
    +
  • graphicHeaderShow
    - Anzeigen/Verbergen des Grafik Tabellenkopfes mit Prognosedaten sowie bestimmten aktuellen und + Anzeigen/Verbergen des Grafik Tabellenkopfes mit Prognosedaten sowie bestimmten aktuellen und statistischen Werten.
    (default: 1)
  • -
    +
  • graphicHistoryHour
    @@ -11557,73 +11937,73 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. (default: 2)

  • - +
  • graphicHourCount <4...24>
    Anzahl der Balken/Stunden in der Balkengrafk.
    (default: 24)

  • - +
  • graphicHourStyle
    Format der Zeitangabe in der Balkengrafik.

    - -
      - - + +
        +
      +
      nicht gesetzt nur Stundenangabe ohne Minuten (default)
      :00 Stunden sowie Minuten zweistellig, z.B. 10:00
      :0 Stunden sowie Minuten einstellig, z.B. 8:0
      -
    +

  • - +
  • graphicLayoutType <single | double | diff>
    Layout der Balkengrafik.
    - Der darzustellende Inhalt der Balken wird durch die Attribute graphicBeam1Content bzw. - graphicBeam2Content bestimmt. + Der darzustellende Inhalt der Balken wird durch die Attribute graphicBeam1Content bzw. + graphicBeam2Content bestimmt.

    - -
      - - - - - + +
        +
      double - zeigt den primären Balken und den sekundären Balken an (default)
      single - zeigt nur den primären Balken an
      diff - Differenzanzeige. Es gilt: <Differenz> = <Wert primärer Balken> - <Wert sekundärer Balken>
      + + + +
      double zeigt den primären Balken und den sekundären Balken an (default)
      single zeigt nur den primären Balken an
      diff Differenzanzeige. Es gilt: <Differenz> = <Wert primärer Balken> - <Wert sekundärer Balken>
  • -
    - +
    +
  • graphicSelect
    Wählt die anzuzeigende Grafik des Moduls aus.
    - Zur Anpassung der Energieflußgrafik steht neben den flowGraphic.*-Attributen auch + Zur Anpassung der Energieflußgrafik steht neben den flowGraphic.*-Attributen auch das Attribut flowGraphicCss zur Verfügung.

    - -
      - - + +
        +
      +
      both zeigt Energiefluß- und Balkengrafik an (default)
      flow zeigt die Energieflußgrafik an
      forecast zeigt die Balkengrafik an
      none es wird keine Grafik angezeigt
      -
    +

  • - +
  • graphicShowDiff <no | top | bottom>
    - Zusätzliche Anzeige der Differenz "graphicBeam1Content - graphicBeam2Content" im Kopf- oder Fußbereich der + Zusätzliche Anzeige der Differenz "graphicBeam1Content - graphicBeam2Content" im Kopf- oder Fußbereich der Balkengrafik.
    (default: no)

  • - +
  • graphicShowNight
    Anzeigen/Verbergen der Nachtstunden (ohne Ertragsprognose) in der Balkengrafik.
    @@ -11637,43 +12017,43 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. (default: 1)

  • - +
  • graphicStartHtml <HTML-String>
    - Angabe eines beliebigen HTML-Strings der vor dem Grafik-Code ausgeführt wird. + Angabe eines beliebigen HTML-Strings der vor dem Grafik-Code ausgeführt wird.

  • graphicEndHtml <HTML-String>
    - Angabe eines beliebigen HTML-Strings der nach dem Grafik-Code ausgeführt wird. + Angabe eines beliebigen HTML-Strings der nach dem Grafik-Code ausgeführt wird.

  • - +
  • graphicSpaceSize <value>
    - Legt fest wieviel Platz in px über oder unter den Balken (bei Anzeigetyp Differential (diff)) zur Anzeige der - Werte freigehalten wird. Bei Styles mit große Fonts kann der default-Wert zu klein sein bzw. rutscht ein + Legt fest wieviel Platz in px über oder unter den Balken (bei Anzeigetyp Differential (diff)) zur Anzeige der + Werte freigehalten wird. Bei Styles mit großen Fonts kann der default-Wert zu klein sein bzw. rutscht ein Balken u.U. über die Grundlinie. In diesen Fällen bitte den Wert erhöhen.
    (default: 24)

  • - +
  • graphicWeatherColor
    Farbe der Wetter-Icons in der Balkengrafik für die Tagesstunden.
  • -
    +
  • graphicWeatherColorNight
    Farbe der Wetter-Icons für die Nachtstunden.
  • -
    +
    - + =end html_DE @@ -11720,7 +12100,7 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. "HttpUtils": 0, "JSON": 4.020, "FHEM::SynoModules::SMUtils": 1.0220, - "Time::HiRes": 0 + "Time::HiRes": 0 }, "recommends": { "FHEM::Meta": 0, @@ -11743,7 +12123,7 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. "x_branch": "dev", "x_filepath": "fhem/contrib/", "x_raw": "https://svn.fhem.de/fhem/trunk/fhem/contrib/DS_Starter/76_SolarForecast.pm" - } + } } } }
      -
    • powerTrigger <1on>=<Wert> <1off>=<Wert> [<2on>=<Wert> <2off>=<Wert> ...]

      - +
    • powerTrigger <1on>=<Wert> <1off>=<Wert> [<2on>=<Wert> <2off>=<Wert> ...]

      + Generiert Trigger bei Über- bzw. Unterschreitung bestimmter PV Erzeugungswerte (Current_PV).
      - Überschreiten die letzten drei Messungen der PV Erzeugung eine definierte Xon-Bedingung, wird das Reading - powerTrigger_X = on erstellt/gesetzt. - Unterschreiten die letzten drei Messungen der PV Erzeugung eine definierte Xoff-Bedingung, wird das Reading + Überschreiten die letzten drei Messungen der PV Erzeugung eine definierte Xon-Bedingung, wird das Reading + powerTrigger_X = on erstellt/gesetzt. + Unterschreiten die letzten drei Messungen der PV Erzeugung eine definierte Xoff-Bedingung, wird das Reading powerTrigger_X = off erstellt/gesetzt.
      Es kann eine beliebige Anzahl von Triggerbedingungen angegeben werden. Xon/Xoff-Bedingungen müssen nicht zwingend paarweise definiert werden.

      - +
        Beispiel:
        set <name> powerTrigger 1on=1000 1off=500 2on=2000 2off=1000 3on=1600 4off=1100
        @@ -10700,27 +11041,27 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.

      - +
        -
      • pvCorrectionFactor_Auto on | off

        - +
      • pvCorrectionFactor_Auto on | off

        + Schaltet die automatische Vorhersagekorrektur ein/aus. Die Wirkungsweise unterscheidet sich zwischen dem Model DWD und dem Model SolCastAPI.

        - + Model SolCastAPI:
        - Eine eingeschaltete Autokorrektur ermittelt am Ende jeder relevanten Stunde durch Vergleich von PV Prognose und - realer Erzeugung das beste Percentil (10-90). + Eine eingeschaltete Autokorrektur ermittelt am Ende jeder relevanten Stunde durch Vergleich von PV Prognose und + realer Erzeugung das beste Percentil (10-90). Bevor man die Autokorrektur eingeschaltet, ist die Prognose mit folgenden Schritten zu optimieren:

        • - definiere im RoofTop-Editor der SolCast API den - efficiency factor + definiere im RoofTop-Editor der SolCast API den + efficiency factor entsprechend dem Alter der Anlage.
          Bei einer 8 Jahre alten Anlage wäre er 84 (100 - (8 x 2%)).
        • - nach Sonnenuntergang wird das Reading Today_PVdeviation erstellt, welches die Abweichung zwischen Prognose und + nach Sonnenuntergang wird das Reading Today_PVdeviation erstellt, welches die Abweichung zwischen Prognose und realer PV Erzeugung in Prozent darstellt.
        • @@ -10729,57 +11070,57 @@ Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. Tagesabweichung gefunden ist
        • - ist man der Auffassung die optimale Einstellung gefunden zu haben, kann vCorrectionFactor_Auto on gesetzt werden um + ist man der Auffassung die optimale Einstellung gefunden zu haben, kann pvCorrectionFactor_Auto on gesetzt werden um eine automatische Auswahl des optimalen Percentils zu aktivieren
        • -
        +

      - Idealerweise wird dieser Prozess in einer Phase stabiler meteorologischer Bedingungen (gleichmäßige Sonne bzw. + Idealerweise wird dieser Prozess in einer Phase stabiler meteorologischer Bedingungen (gleichmäßige Sonne bzw. Bewölkung) durchgeführt.
      - Ist die minimale Tagesabweichung gefunden, kann die Autokorrektur aktiviert werden um für jede Stunde separat das - beste Percentil ermitteln zu lassen. Dieser Vorgang ist dynamisch und verwendet ebenso historische Werte zur + Ist die minimale Tagesabweichung gefunden, kann die Autokorrektur aktiviert werden um für jede Stunde separat das + beste Percentil ermitteln zu lassen. Dieser Vorgang ist dynamisch und verwendet ebenso historische Werte zur Durchschnittsbildung. Siehe auch Attribut affectNumHistDays. -

      - +

      + Model DWD:
      - Ist die Autokorrektur eingeschaltet, wird für jede Stunde ein Korrekturfaktor der Solarvorhersage berechnet und + Ist die Autokorrektur eingeschaltet, wird für jede Stunde ein Korrekturfaktor der Solarvorhersage berechnet und intern gespeichert. - Dazu wird die tatsächliche Energieerzeugung mit dem vorhergesagten Wert des aktuellen Tages und Stunde verglichen, - die Korrekturwerte historischer Tage unter Berücksichtigung der Bewölkung einbezogen und daraus ein neuer Korrekturfaktor + Dazu wird die tatsächliche Energieerzeugung mit dem vorhergesagten Wert des aktuellen Tages und Stunde verglichen, + die Korrekturwerte historischer Tage unter Berücksichtigung der Bewölkung einbezogen und daraus ein neuer Korrekturfaktor abgeleitet. Es werden nur historische Daten mit gleicher Bewölkungsrange einbezogen.
      - Zukünftig erwartete PV Erzeugungen werden mit den gespeicherten Korrekturfaktoren optimiert.
      - Bei aktivierter Autokorrektur haben die Attribute + Zukünftig erwartete PV Erzeugungen werden mit den gespeicherten Korrekturfaktoren optimiert.
      + Bei aktivierter Autokorrektur haben die Attribute affectCloudfactorDamping und - affectRainfactorDamping nur noch eine untergeordnete - Bedeutung.
      + affectRainfactorDamping nur noch eine untergeordnete + Bedeutung.

      Die automatische Vorhersagekorrektur ist lernend und benötigt Zeit um die Korrekturwerte zu optimieren. - Nach der Aktivierung sind nicht sofort optimale Vorhersagen zu erwarten !
      + Nach der Aktivierung sind nicht sofort optimale Vorhersagen zu erwarten!

      (default: off)