######################################################################################################################## # $Id$ ######################################################################################################################### # 76_SolarForecast.pm # # (c) 2020-2024 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 # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # # This copyright notice MUST APPEAR in all copies of the script! # ######################################################################################################################### # # Leerzeichen entfernen: sed -i 's/[[:space:]]*$//' 76_SolarForecast.pm # ######################################################################################################################### main::LoadModule ('Astro'); # Astro Modul für Sonnenkennzahlen laden package FHEM::SolarForecast; ## no critic 'package' use strict; use warnings; use POSIX; use GPUtils qw(GP_Import GP_Export); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt use Time::HiRes qw(gettimeofday tv_interval); eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' eval "use FHEM::Utility::CTZ qw(:all);1;" or my $ctzAbsent = 1; ## no critic 'eval' use Encode; use Color; use utf8; use HttpUtils; eval "use JSON;1;" or my $jsonabs = 'JSON'; ## no critic 'eval' # Debian: sudo apt-get install libjson-perl eval "use AI::DecisionTree;1;" or my $aidtabs = 'AI::DecisionTree'; ## no critic 'eval' use FHEM::SynoModules::SMUtils qw( checkModVer evaljson getClHash delClHash moduleVersion trim ); # Hilfsroutinen Modul use Data::Dumper; use Blocking; use Storable qw(dclone freeze thaw nstore store retrieve); use MIME::Base64; no if $] >= 5.017011, warnings => 'experimental::smartmatch'; # Run before module compilation BEGIN { # Import from main:: GP_Import( qw( attr asyncOutput AnalyzePerlCommand AnalyzeCommandChain AttrVal AttrNum BlockingCall BlockingKill CommandAttr CommandGet CommandSet CommandSetReading data defs delFromDevAttrList delFromAttrList devspec2array deviceEvents DoTrigger Debug fhemTimeLocal fhemTimeGm fhem FileWrite FileRead FileDelete FmtTime FmtDateTime FW_makeImage getKeyValue getAllAttr getAllGets getAllSets HttpUtils_NonblockingGet HttpUtils_BlockingGet init_done InternalTimer InternalVal IsDisabled Log Log3 modules parseParams perlSyntaxCheck readingsSingleUpdate readingsBulkUpdate readingsBulkUpdateIfChanged readingsBeginUpdate readingsDelete readingsEndUpdate ReadingsNum ReadingsTimestamp ReadingsVal RemoveInternalTimer ReplaceEventMap readingFnAttributes setKeyValue sortTopicNum sunrise_abs_dat sunset_abs_dat FW_cmd FW_directNotify FW_ME FW_subdir FW_pH FW_room FW_detail FW_widgetOverride FW_wname readyfnlist ) ); # Export to main context with different name # my $pkg = caller(0); # my $main = $pkg; # $main =~ s/^(?:.+::)?([^:]+)$/main::$1\_/g; # foreach (@_) { # *{ $main . $_ } = *{ $pkg . '::' . $_ }; # } GP_Export( qw( Initialize pageAsHtml NexthoursVal ) ); } # Versions History intern my %vNotesIntern = ( "1.17.5" => "04.04.2024 currentInverterDev: check syntax of key capacity if set, change defmaxvar back from 0.8 to 0.5 ". "currentMeterDev: [conprice=::] [feedprice=::] ". "___setOpenMeteoAPIcallKeyData: new sub to calculate the minimum Open-Meteo request intervalls ", "1.17.4" => "01.04.2024 fix ctrlWeatherDev1 Drop-Down list if no DWD Device exists, edit commandref ", "1.17.3" => "31.03.2024 edit commandref, valDecTree: more infos in aiRuleStrings output, integrate OpenMeteoDWDEnsemble-API ". "change Call interval Open-Meteo API to 900s, OpenMeteo-API: fix todayDoneAPIcalls, implement callequivalent". "aiTrain: change default start to hour 2, change AI acceptable result limits ", "1.17.2" => "29.03.2024 aiTrain: better status info, limit ctrlWeatherDev2/3 to can only use DWD Devices ". "integrate OpenMeteoWorld-API with the 'Best match' Weather model ", "1.17.1" => "27.03.2024 add AI to OpenMeteoDWD-API, changed AI train debuglog, new attr ctrlAIshiftTrainStart ". "_specialActivities: split tasks to several time slots, bugfixes ". "AI: modify aiAddInstance, Customize pattern training data ". "add batteryTrigger to save plantconfig, valDecTree: more infos in get aiRuleStrings ", "1.17.0" => "24.03.2024 new DWD ICON API, change defmaxvar from 0.5 to 0.8, attr ctrlWeatherDev1 can select OpenMeteoDWD-API ", "1.16.8" => "16.03.2024 plantConfigCheck: adjust pvCorrectionFactor_Auto check, settings of forecastRefresh ". "rename reading nextSolCastCall to nextRadiationAPICall ". "currentMeterDev: new optional keys conprice, feedprice ". "destroy runtime data when delete device ", "1.16.7" => "12.03.2024 prevent duplicates in NOTIFYDEV, Forum: https://forum.fhem.de/index.php?msg=1306875 ", "1.16.6" => "11.03.2024 plantConfigCheck: join forecastProperties with ',' ", "1.16.5" => "04.03.2024 setPVhistory: code changes, plantConfigCheck: check forecastRefresh ". "check age of weather data according to used MOSMIX variant ", "1.16.4" => "02.03.2024 __getDWDSolarData: change check reading to fcx_12_x, internal code changes ". "plantConfiguration: save/restore relevant readings AND attributes ". "visual LED display whether the weather data is current (age < 2h) ", "1.16.3" => "24.02.2024 store pvcorrf, quality, pvrlsum, pvfcsum, dnumsum with value . in pvCircular ". "get pvcorrf / quality from neff in combination with sun altitude (CircularSunCloudkorrVal) ". "delete CircularCloudkorrVal, show sun position in beamgrafic weather mouse over ". "split pvCorrection into pvCorrectionRead and pvCorrectionWrite ". "_checkSetupNotComplete: improve setup Wizzard for ForecastSolar-API ", "1.16.2" => "22.02.2024 minor changes, R101 -> RR1c, totalrain instead of weatherrainprob, delete wrp r101 ". "delete wrp from circular & airaw, remove rain2bin, __getDWDSolarData: change \$runh, ". "fix Illegal division by zero Forum: https://forum.fhem.de/index.php?msg=1304009 ". "DWD API: check age of Rad1h data, store pvcorrf of sunalt with value 200+x in pvCircular ", "1.16.1" => "14.02.2024 ___isCatFiltered: add eval for regex evaluation, add sunaz to AI raw and get, fillup AI hash ", "1.16.0" => "12.02.2024 new command get dwdCatalog ", "1.15.5" => "11.02.2024 change forecastQualities output, new limits for 'accurate' and 'spreaded' results from AI ". "checkPlantConfig: change common check info output ". "fix load Astro ", "1.15.4" => "10.02.2024 integrate sun position from Astro module, setPVhistory: change some writes ". "_transferAPIRadiationValues: consider 'accurate' or 'spreaded' result from AI". "___calcPeaklossByTemp: bugfix temp, rename moduleDirection to moduleAzimuth ". "rename moduleTiltAngle to moduleDeclination, checkPlantConfig: check global altitude attr ", "1.15.3" => "06.02.2024 Header: add links to the API website dependend from the used API ", "1.15.2" => "05.02.2024 __mergeDataWeather: fix merger failures, number of temperature decimal places ". "cicrular Hash: replace 'percentile' by 'simple' ", "1.15.1" => "04.02.2024 checkPlantConfig: fix check attribute ctrlWeatherDevX ", "1.15.0" => "03.02.2024 reduce cpu utilization, add attributes ctrlWeatherDev2, ctrlWeatherDev3 ", "1.14.3" => "02.02.2024 _transferWeatherValues: first step of multi weather device merger ", "1.14.2" => "02.02.2024 fix warning, _transferAPIRadiationValues: Consider upper and lower deviation limit AI to API forecast ", "1.14.1" => "01.02.2024 language support for ___setConsumerPlanningState -> supplement, fix setting 'swoncond not met' ", "1.14.0" => "31.01.2024 data maintenance, new func _addDynAttr for adding attributes at runtime ". "replace setter currentWeatherDev by attr ctrlWeatherDev1, new data with func CircularSumVal ". "rewrite correction factor calculation with _calcCaQcomplex, _calcCaQsimple, __calcNewFactor ", "1.13.0" => "27.01.2024 minor change of deleteOldBckpFiles, Setter writeHistory replaced by operatingMemory ". "save, backup and recover in-memory operating data ", "1.12.0" => "26.01.2024 create backup files and delete old generations of them ", "1.11.1" => "26.01.2024 fix ___switchonTimelimits ", "1.11.0" => "25.01.2024 consumerXX: notbefore, notafter format extended to possible perl code {...} ", "1.10.0" => "24.01.2024 consumerXX: notbefore, notafter format extended to hh[:mm], new sub checkCode, checkhhmm ", "1.9.0" => "23.01.2024 modify disable, add operationMode: active/inactive ", "1.8.0" => "22.01.2024 add 'noLearning' Option to Setter pvCorrectionFactor_Auto ", "1.7.1" => "20.01.2024 optimize battery management ", "1.7.0" => "18.01.2024 Changeover Start centralTask completely to runTask, ". "aiAddRawData: Weekday from pvHistory not taken into account greater than current day ". "__reviewSwitchTime: new function for review consumer planning state ". "___switchonTimelimits: The current time is taken into account during planning ". "take info-tag into consumerxx Reading ". "fix deletion of currentBatteryDev, currentInverterDev, currentMeterDev ", "1.6.5" => "10.01.2024 new function runTask in ReadyFn to run centralTask definitely at end/begin of an hour ", "1.6.4" => "09.01.2024 fix get Automatic State, use key switchdev for auto-Reading if switchdev is set in consumer attr ", "1.6.3" => "08.01.2024 optimize battery management once more ", "1.6.2" => "07.01.2024 optimize battery management ", "1.6.1" => "04.01.2024 new sub __setPhysSwState, edit ___setConsumerPlanningState, boost performance of collectAllRegConsumers ". "CurrentVal ctrunning - Central Task running Statusbit, edit comref ", "1.6.0" => "22.12.2023 store daily batmaxsoc in pvHistory, new attr ctrlBatSocManagement, reading Battery_OptimumTargetSoC ". "currentBatteryDev: new optional key 'cap', adapt cloud2bin,temp2bin,rain2bin ". "minor internal changes, isAddSwitchOffCond: change hysteresis algo, ctrlDebug: new entry batteryManagement ". "check longitude, latitude in general audit, use coordinates (if set) for sun calc ", "1.5.1" => "07.12.2023 function _getftui can now process arguments (compatibility to new ftui widgets), plant check ". "reviews SolarForecast widget files ", "1.5.0" => "05.12.2023 new getter ftuiFramefiles ", "1.4.3" => "03.12.2023 hidden set or attr commands in user specific header area when called by 'get ... html' ". "plantConfig: check module update in repo ", "1.4.2" => "02.12.2023 ___getFWwidget: codechange ___getFWwidget using __widgetFallback function ", "1.4.1" => "01.12.2023 ___getFWwidget: adjust for FHEMWEB feature forum:#136019 ", "1.4.0" => "29.11.2023 graphicHeaderOwnspec: can manage attr / sets of other devs by @ ", "1.3.0" => "27.11.2023 new Attr graphicHeaderOwnspecValForm ", "1.2.0" => "25.11.2023 graphicHeaderOwnspec: show readings of other devs by @, Set/reset batteryTrigger ", "1.1.3" => "24.11.2023 rename reset arguments according possible adjustable textField width ", "1.1.2" => "20.11.2023 ctrlDebug Adjustment of column width, must have new fhemweb.js Forum:#135850 ", "1.1.1" => "19.11.2023 graphicHeaderOwnspec: fix ignoring the last element of allsets/allattr ", "1.1.0" => "14.11.2023 graphicHeaderOwnspec: possible add set/attr commands, new setter consumerNewPlanning ", "1.0.10" => "31.10.2023 fix warnings, edit comref ", "1.0.9" => "29.10.2023 _aiGetSpread: set spread from 50 to 20 ", "1.0.8" => "22.10.2023 codechange: add central readings store array, new function storeReading, writeCacheToFile ". "solcastapi in sub __delObsoleteAPIData, save freespace if flowGraphicShowConsumer=0 is set ". "pay attention to attr graphicEnergyUnit in __createOwnSpec ", "1.0.7" => "21.10.2023 more design options for graphicHeaderOwnspec and a possible line title ", "1.0.6" => "19.10.2023 new attr ctrlGenPVdeviation ", "1.0.5" => "11.10.2023 new sub _aiGetSpread for estimate AI results stepwise, allow key 'noshow' values 0,1,2,3 ". "calculate conForecastTillNextSunrise accurate to the minute ", "1.0.4" => "10.10.2023 fix: print always Log in _calcCaQ* subroutines even if calaculated factors are equal ". "new consumer attr key 'noshow' ", "1.0.3" => "08.10.2023 change graphic header PV/CO detail, new attr graphicHeaderOwnspec, internal code changes ". "fix isAddSwitchOffCond 0 Forum: https://forum.fhem.de/index.php?msg=1288877 ". "change calcValueImproves and subroutines ", "1.0.2" => "05.10.2023 replace calcRange by cloud2bin ", "1.0.1" => "03.10.2023 fixes in comRef, bug fix Forum: https://forum.fhem.de/index.php?msg=1288637 ", "1.0.0" => "01.10.2023 preparation for check in ", "0.83.3" => "28.09.2023 fix Illegal division by zero, Forum: https://forum.fhem.de/index.php?msg=1288032 ". "delete AllPVforecastsToEvent after event generation ", "0.83.2" => "26.09.2023 setter reset consumption ", "0.83.1" => "26.09.2023 change currentRadiationDev to currentRadiationAPI, new attr ctrlAIdataStorageDuration ". "new elements todayConsumptionForecast, conForecastTillNextSunrise for attr ctrlStatisticReadings ". "add entry text in guided procedure ", "0.83.0" => "19.09.2023 add manageTrain for AI Training in parallel process ", "0.82.4" => "16.09.2023 generate DWD API graphics header information and extend plant check for DWD API errors, minor fixes ", "0.82.3" => "14.09.2023 more mouse over information in graphic header, ai support in autocorrection selectable ". "substitute use of Test2::Suite ", "0.82.2" => "11.09.2023 activate implementation of DWD AI support, add runTimeTrainAI ", "0.82.1" => "08.09.2023 rebuild implementation of DWD AI support, some error fixing (FHEM restarts between 0 and 1) ", "0.82.0" => "02.09.2023 first implementation of DWD AI support, new ctrlDebug aiProcess aiData, reset aiData ", "0.81.1" => "30.08.2023 show forecast qualities when pressing quality icon in forecast grafic, store rad1h (model DWD) in ". "pvhistory, removed: affectCloudfactorDamping, affectRainfactorDamping ", "0.81.0" => "27.08.2023 development version for Victron VRM API, __PvFcSimpleDnumHist changed, available setter ". "are now API specific, switch currentForecastDev to currentWeatherDev ". "affectCloudfactorDamping default 0, affectRainfactorDamping default 0 ". "call consumption forecast from Victron VRM API ", "0.80.20"=> "15.08.2023 hange calculation in ___setSolCastAPIcallKeyData once again, fix some warnings ", "0.80.19"=> "10.08.2023 fix Illegal division by zero, Forum: https://forum.fhem.de/index.php?msg=1283836 ", "0.80.18"=> "07.08.2023 change calculation of todayDoneAPIcalls in ___setSolCastAPIcallKeyData, add \$lagtime ". "Forum: https://forum.fhem.de/index.php?msg=1283487 ", "0.80.17"=> "05.08.2023 change sequence of _createSummaries in centralTask, ComRef edited ", "0.80.16"=> "26.07.2023 new consumer type noSchedule, expand maxconsumer to 16, minor changes/fixes ", "0.80.15"=> "24.07.2023 new sub getDebug, new key switchdev in consumer attributes, change Debug consumtion ". "reorg data in pvHistory when a hour of day was deleted ", "0.80.14"=> "21.07.2023 __substConsumerIcon: use isConsumerLogOn instead of isConsumerPhysOn ", "0.80.13"=> "18.07.2023 include parameter DoN in nextHours hash, new KPI's todayConForecastTillSunset, currentRunMtsConsumer_XX ". "minor fixes and improvements ", "0.80.12"=> "16.07.2023 preparation for alternative switch device in consumer attribute, revise CommandRef ". "fix/improve sub ___readCandQ and much more, get pvHistory -> one specific day selectable ". "get valConsumerMaster -> one specific consumer selectable, enhance consumer key locktime by on-locktime ", "0.80.11"=> "14.07.2023 minor fixes and improvements ", "0.80.10"=> "13.07.2023 new key spignorecond in consumer attributes ", "0.80.9" => "13.07.2023 new method of prediction quality calculation -> sub __calcFcQuality, minor bug fixes ", "0.80.8" => "12.07.2023 store battery values initdaybatintot, initdaybatouttot, batintot, batouttot in circular hash ". "new Attr ctrlStatisticReadings parameter todayBatIn, todayBatOut ", "0.80.7" => "10.07.2023 Model SolCastAPI: retrieve forecast data of 72h (old 48), create statistic reading dayAfterTomorrowPVforecast if possible ", "0.80.6" => "09.07.2023 get ... html has some possible arguments now ", "0.80.5" => "07.07.2023 calculate _calcCaQcomplex, _calcCaQsimple both at every time, change setter pvCorrectionFactor_Auto: on_simple, on_complex, off ", "0.80.4" => "06.07.2023 new transferprocess for DWD data from solcastapi-Hash to estimate calculation, consolidated ". "the autocorrection model ", "0.80.3" => "03.06.2023 preparation for get DWD radiation data to solcastapi-Hash, fix sub isConsumerLogOn (use powerthreshold) ", "0.80.2" => "02.06.2023 new ctrlDebug keys epiecesCalc, change selfconsumption with graphic Adjustment, moduleAzimuth ". "accepts azimut values -180 .. 0 .. 180 as well as azimut identifier (S, SE ..) ", "0.80.1" => "31.05.2023 adapt _calcCaQsimple to calculate corrfactor like _calcCaQcomplex ", "0.80.0" => "28.05.2023 Support for Forecast.Solar-API (https://doc.forecast.solar/api), rename Getter solCastData to solApiData ". "rename ctrlDebug keys: solcastProcess -> apiProcess, solcastAPIcall -> apiCall ". "calculate cloudiness correction factors proactively and store it in circular hash ". "new reading Current_Surplus, ___noPlanRelease -> only one call releases the consumer planning ", "0.79.3" => "21.05.2023 new CircularVal initdayfeedin, deactivate \$hash->{HELPER}{INITFEEDTOTAL}, \$hash->{HELPER}{INITCONTOTAL} ". "new statistic Readings statistic_todayGridFeedIn, statistic_todayGridConsumption ", "0.79.2" => "21.05.2023 change process to calculate solCastAPIcallMultiplier, todayMaxAPIcalls ", "0.79.1" => "19.05.2023 extend debug apiProcess, new key apiCall ", "0.79.0" => "13.05.2023 new consumer key locktime ", "0.78.2" => "11.05.2023 extend debug radiationProcess ", "0.78.1" => "08.05.2023 change default icon it_ups_on_battery to batterie ", "0.78.0" => "07.05.2023 activate NotifyFn Forum:https://forum.fhem.de/index.php?msg=1275005, new Consumerkey asynchron ", "0.77.1" => "07.05.2023 rewrite function pageRefresh ", "0.77.0" => "03.05.2023 new attribute ctrlUserExitFn ", "0.76.0" => "01.05.2023 new ctrlStatisticReadings SunMinutes_Remain, SunHours_Remain ", "0.75.3" => "23.04.2023 fix Illegal division by zero at ./FHEM/76_SolarForecast.pm line 6199 ", "0.75.2" => "16.04.2023 some minor changes ", "0.75.1" => "24.03.2023 change epieces for consumer type washingmachine, PV Vorhersage auf WR Kapazität begrenzen ", "0.75.0" => "16.02.2023 new attribute ctrlSolCastAPImaxReq, rename attr ctrlOptimizeSolCastInterval to ctrlSolCastAPIoptimizeReq ", "0.74.8" => "11.02.2023 change description of 'mintime', mintime with SunPath value possible ", "0.74.7" => "23.01.2023 fix evaljson evaluation ", "0.1.0" => "09.12.2020 initial Version " ); ## default Variablen ###################### my @da; # Readings-Store my $deflang = 'EN'; # default Sprache wenn nicht konfiguriert my @chours = (5..21); # Stunden des Tages mit möglichen Korrekturwerten my $kJtokWh = 0.0002777777778; # Umrechnungsfaktor kJ in kWh my $kJtoWh = 0.2777777778; # Umrechnungsfaktor kJ in Wh my $WhtokJ = 3.6; # Umrechnungsfaktor Wh in kJ my $defmaxvar = 0.5; # max. Varianz pro Tagesberechnung Autokorrekturfaktor my $definterval = 70; # Standard Abfrageintervall my $defslidenum = 3; # max. Anzahl der Arrayelemente in Schieberegistern my $weatherDevMax = 3; # max. Anzahl Wetter Devices (Attr ctrlWeatherDevX) my $maxSoCdef = 95; # default Wert (%) auf den die Batterie maximal aufgeladen werden soll bzw. als aufgeladen gilt my $carecycledef = 20; # max. Anzahl Tage die zwischen der Batterieladung auf maxSoC liegen dürfen my $batSocChgDay = 5; # prozentuale SoC Änderung pro Tag my @widgetreadings = (); # Array der Hilfsreadings als Attributspeicher my $root = $attr{global}{modpath}; # Pfad zu dem Verzeichnis der FHEM Module my $cachedir = $root."/FHEM/FhemUtils"; # Directory für Cachefiles my $pvhcache = $root."/FHEM/FhemUtils/PVH_SolarForecast_"; # Filename-Fragment für PV History (wird mit Devicename ergänzt) my $pvccache = $root."/FHEM/FhemUtils/PVC_SolarForecast_"; # Filename-Fragment für PV Circular (wird mit Devicename ergänzt) my $plantcfg = $root."/FHEM/FhemUtils/PVCfg_SolarForecast_"; # Filename-Fragment für PV Anlagenkonfiguration (wird mit Devicename ergänzt) my $csmcache = $root."/FHEM/FhemUtils/PVCsm_SolarForecast_"; # Filename-Fragment für Consumer Status (wird mit Devicename ergänzt) my $scpicache = $root."/FHEM/FhemUtils/ScApi_SolarForecast_"; # Filename-Fragment für Werte aus SolCast API (wird mit Devicename ergänzt) my $aitrained = $root."/FHEM/FhemUtils/AItra_SolarForecast_"; # Filename-Fragment für AI Trainingsdaten (wird mit Devicename ergänzt) my $airaw = $root."/FHEM/FhemUtils/AIraw_SolarForecast_"; # Filename-Fragment für AI Input Daten = Raw Trainigsdaten my $dwdcatalog = $root."/FHEM/FhemUtils/DWDcat_SolarForecast"; # Filename-Fragment für DWD Stationskatalog my $dwdcatgpx = $root."/FHEM/FhemUtils/DWDcat_SolarForecast.gpx"; # Filename-Fragment für DWD Stationskatalog my $aitrblto = 7200; # KI Training BlockingCall Timeout my $aibcthhld = 0.2; # Schwelle der KI Trainigszeit ab der BlockingCall benutzt wird my $aitrstartdef = 2; # default Stunde f. Start AI-Training my $aistdudef = 1095; # default Haltezeit KI Raw Daten (Tage) my $aiSpreadUpLim = 120; # obere Abweichungsgrenze (%) AI 'Spread' von API Prognose my $aiSpreadLowLim = 80; # untere Abweichungsgrenze (%) AI 'Spread' von API Prognose my $aiAccUpLim = 130; # obere Abweichungsgrenze (%) AI 'Accurate' von API Prognose my $aiAccLowLim = 70; # untere Abweichungsgrenze (%) AI 'Accurate' von API Prognose my $calcmaxd = 30; # Anzahl Tage die zur Berechnung Vorhersagekorrektur verwendet werden my @dweattrmust = qw(TTT Neff RR1c ww SunUp SunRise SunSet); # Werte die im Attr forecastProperties des Weather-DWD_Opendata Devices mindestens gesetzt sein müssen my @draattrmust = qw(Rad1h); # Werte die im Attr forecastProperties des Radiation-DWD_Opendata Devices mindestens gesetzt sein müssen my $whistrepeat = 851; # Wiederholungsintervall Cache File Daten schreiben my $solapirepdef = 3600; # default Abrufintervall SolCast API (s) my $forapirepdef = 900; # default Abrufintervall ForecastSolar API (s) my $ometeorepdef = 900; # default Abrufintervall Open-Meteo API (s) my $vrmapirepdef = 300; # default Abrufintervall Victron VRM API Forecast my $solcmaxreqdef = 50; # max. täglich mögliche Requests SolCast API my $ometmaxreq = 9500; # Beschränkung auf max. mögliche Requests Open-Meteo API my $leadtime = 3600; # relative Zeit vor Sonnenaufgang zur Freigabe API Abruf / Verbraucherplanung my $lagtime = 1800; # Nachlaufzeit relativ zu Sunset bis Sperrung API Abruf 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 $maxconsumer = 16; # maximale Anzahl der möglichen Consumer (Attribut) my $epiecMaxCycles = 10; # Anzahl Einschaltzyklen (Consumer) für verbraucherspezifische Energiestück Ermittlung my @ctypes = qw(dishwasher dryer washingmachine heater charger other noSchedule); # erlaubte Consumer Typen my $defmintime = 60; # default Einplanungsdauer in Minuten my $defctype = "other"; # default Verbrauchertyp my $defcmode = "can"; # default Planungsmode der Verbraucher my $defpopercent = 1.0; # Standard % aktuelle Leistung an nominaler Leistung gemäß Typenschild my $defhyst = 0; # default Hysterese my $caicondef = 'clock@gold'; # default consumerAdviceIcon my $flowGSizedef = 400; # default flowGraphicSize my $histhourdef = 2; # default Anzeige vorangegangene Stunden my $wthcolddef = 'C7C979'; # Wetter Icon Tag default Farbe my $wthcolndef = 'C7C7C7'; # Wetter Icon Nacht default Farbe my $b1coldef = 'FFAC63'; # default Farbe Beam 1 my $b1fontcoldef = '0D0D0D'; # default Schriftfarbe Beam 1 my $b2coldef = 'C4C4A7'; # default Farbe Beam 2 my $b2fontcoldef = '000000'; # default Schriftfarbe Beam 2 my $fgCDdef = 130; # Abstand Verbrauchericons zueinander my $bPath = 'https://svn.fhem.de/trac/browser/trunk/fhem/contrib/SolarForecast/'; # Basispfad Abruf contrib SolarForecast Files my $pPath = '?format=txt'; # Download Format my $cfile = 'controls_solarforecast.txt'; # Name des Conrrolfiles # default CSS-Style my $cssdef = qq{.flowg.text { stroke: none; fill: gray; font-size: 60px; } \n}. qq{.flowg.sun_active { stroke: orange; fill: orange; } \n}. qq{.flowg.sun_inactive { stroke: gray; fill: gray; } \n}. qq{.flowg.bat25 { stroke: red; fill: red; } \n}. qq{.flowg.bat50 { stroke: darkorange; fill: darkorange; } \n}. qq{.flowg.bat75 { stroke: green; fill: green; } \n}. qq{.flowg.grid_color1 { fill: green; } \n}. qq{.flowg.grid_color2 { fill: red; } \n}. qq{.flowg.grid_color3 { fill: gray; } \n}. qq{.flowg.inactive_in { stroke: gray; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.2; } \n}. qq{.flowg.inactive_out { stroke: gray; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.2; } \n}. qq{.flowg.active_in { stroke: red; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; } \n}. qq{.flowg.active_out { stroke: darkorange; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; } \n}. qq{.flowg.active_bat_in { stroke: darkorange; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; } \n}. 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 aiProcess aiData apiCall apiProcess batteryManagement collectData consumerPlanning consumerSwitching consumption dwdComm epiecesCalc graphic notifyHandling pvCorrectionRead pvCorrectionWrite radiationProcess saveData2Cache ); # FTUI V2 Widget Files my @fs = qw( ftui_forecast.css widget_forecast.js ftui_smaportalspg.css widget_smaportalspg.js ); # Anlagenkonfiguration: maßgebliche Readings my @rconfigs = qw( pvCorrectionFactor_Auto currentBatteryDev currentInverterDev currentMeterDev currentRadiationAPI inverterStrings moduleAzimuth modulePeakString moduleDeclination moduleRoofTops batteryTrigger powerTrigger energyH4Trigger ); # Anlagenkonfiguration: maßgebliche Attribute my @aconfigs = qw( affect70percentRule affectBatteryPreferredCharge affectConsForecastIdentWeekdays affectConsForecastInPlanning affectMaxDayVariance affectSolCastPercentile consumerLegend consumerAdviceIcon consumerLink ctrlAIdataStorageDuration ctrlAutoRefresh ctrlAutoRefreshFW ctrlBackupFilesKeep ctrlBatSocManagement ctrlConsRecommendReadings ctrlGenPVdeviation ctrlInterval ctrlLanguage ctrlNextDayForecastReadings ctrlShowLink ctrlSolCastAPImaxReq ctrlSolCastAPIoptimizeReq ctrlStatisticReadings ctrlUserExitFn ctrlWeatherDev1 ctrlWeatherDev2 ctrlWeatherDev3 disable flowGraphicSize flowGraphicAnimate flowGraphicConsumerDistance flowGraphicShowConsumer flowGraphicShowConsumerDummy flowGraphicShowConsumerPower flowGraphicShowConsumerRemainTime flowGraphicCss graphicBeamHeight graphicBeamWidth graphicBeam1Color graphicBeam2Color graphicBeam1Content graphicBeam2Content graphicBeam1FontColor graphicBeam2FontColor graphicBeam1MaxVal graphicEnergyUnit graphicHeaderOwnspec graphicHeaderOwnspecValForm graphicHeaderDetail graphicHeaderShow graphicHistoryHour graphicHourCount graphicHourStyle graphicLayoutType graphicSelect graphicShowDiff graphicShowNight graphicShowWeather graphicSpaceSize graphicStartHtml graphicEndHtml graphicWeatherColor graphicWeatherColorNight ); for my $cinit (1..$maxconsumer) { # Anlagenkonfiguration: add Consumer Attribute $cinit = sprintf "%02d", $cinit; my $consumer = "consumer${cinit}"; push @aconfigs, $consumer; } my $allwidgets = 'icon|sortable|uzsu|knob|noArg|time|text|slider|multiple|select|bitfield|widgetList|colorpicker'; # Steuerhashes ################ my %hset = ( # Hash der Set-Funktion consumerImmediatePlanning => { fn => \&_setconsumerImmediatePlanning }, consumerNewPlanning => { fn => \&_setconsumerNewPlanning }, currentRadiationAPI => { fn => \&_setcurrentRadiationAPI }, modulePeakString => { fn => \&_setmodulePeakString }, inverterStrings => { fn => \&_setinverterStrings }, clientAction => { fn => \&_setclientAction }, currentInverterDev => { fn => \&_setinverterDevice }, currentMeterDev => { fn => \&_setmeterDevice }, currentBatteryDev => { fn => \&_setbatteryDevice }, energyH4Trigger => { fn => \&_setTrigger }, plantConfiguration => { fn => \&_setplantConfiguration }, batteryTrigger => { fn => \&_setTrigger }, operationMode => { fn => \&_setoperationMode }, powerTrigger => { fn => \&_setTrigger }, pvCorrectionFactor_05 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_06 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_07 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_08 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_09 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_10 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_11 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_12 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_13 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_14 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_15 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_16 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_17 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_18 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_19 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_20 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_21 => { fn => \&_setpvCorrectionFactor }, pvCorrectionFactor_Auto => { fn => \&_setpvCorrectionFactorAuto }, reset => { fn => \&_setreset }, roofIdentPair => { fn => \&_setroofIdentPair }, moduleRoofTops => { fn => \&_setmoduleRoofTops }, moduleDeclination => { fn => \&_setmoduleDeclination }, moduleAzimuth => { fn => \&_setmoduleAzimuth }, operatingMemory => { fn => \&_setoperatingMemory }, vrmCredentials => { fn => \&_setVictronCredentials }, aiDecTree => { fn => \&_setaiDecTree }, ); my %hget = ( # Hash für Get-Funktion (needcred => 1: Funktion benötigt gesetzte Credentials) data => { fn => \&_getdata, needcred => 0 }, html => { fn => \&_gethtml, needcred => 0 }, ftui => { fn => \&_getftui, needcred => 0 }, valCurrent => { fn => \&_getlistCurrent, needcred => 0 }, valConsumerMaster => { fn => \&_getlistvalConsumerMaster, needcred => 0 }, plantConfigCheck => { fn => \&_setplantConfiguration, needcred => 0 }, pvHistory => { fn => \&_getlistPVHistory, needcred => 0 }, pvCircular => { fn => \&_getlistPVCircular, needcred => 0 }, forecastQualities => { fn => \&_getForecastQualities, needcred => 0 }, nextHours => { fn => \&_getlistNextHours, needcred => 0 }, rooftopData => { fn => \&_getRoofTopData, needcred => 0 }, solApiData => { fn => \&_getlistSolCastData, needcred => 0 }, valDecTree => { fn => \&_getaiDecTree, needcred => 0 }, ftuiFramefiles => { fn => \&_ftuiFramefiles, needcred => 0 }, dwdCatalog => { fn => \&_getdwdCatalog, needcred => 0 }, ); my %hattr = ( # Hash für Attr-Funktion consumer => { fn => \&_attrconsumer }, ctrlConsRecommendReadings => { fn => \&_attrcreateConsRecRdgs }, ctrlStatisticReadings => { fn => \&_attrcreateStatisticRdgs }, ctrlWeatherDev1 => { fn => \&_attrWeatherDev }, ctrlWeatherDev2 => { fn => \&_attrWeatherDev }, ctrlWeatherDev3 => { fn => \&_attrWeatherDev }, ); my %htr = ( # Hash even/odd für 0 => { cl => 'even' }, 1 => { cl => 'odd' }, ); 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 }, "15" => { N => 85, NE => 90, E => 99, SE => 107, S => 111, SW => 107, W => 99, NW => 90 }, "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 }, "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 }, "55" => { N => 45, NE => 58, E => 83, SE => 105, S => 112, SW => 105, W => 83, NW => 58 }, "60" => { N => 42, NE => 55, E => 80, SE => 102, S => 111, SW => 102, W => 80, NW => 55 }, "65" => { N => 39, NE => 53, E => 77, SE => 99, S => 108, SW => 99, W => 77, NW => 53 }, "70" => { N => 37, NE => 50, E => 74, SE => 95, S => 104, SW => 95, W => 74, NW => 50 }, "75" => { N => 36, NE => 48, E => 70, SE => 90, S => 100, SW => 90, W => 70, NW => 48 }, "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 entry => { EN => qq{Warm welcome!
The next queries will guide you through the basic installation.
If all entries are made, please check the configuration finally with "set LINK plantConfiguration check" or by pressing the offered icon.
Please correct any errors and take note of possible hints.
(The display language can be changed with attribute "ctrlLanguage".)

}, DE => qq{Herzlich Willkommen!
Die nächsten Abfragen führen sie durch die Grundinstallation.
Sind alle Eingaben vorgenommen, prüfen sie bitte die Konfiguration abschließend mit "set LINK plantConfiguration check" oder mit Druck auf das angebotene Icon.
Korrigieren sie bitte eventuelle Fehler und beachten sie mögliche Hinweise.
(Die Anzeigesprache kann mit dem Attribut "ctrlLanguage" umgestellt werden.)

} }, cfd => { EN => qq{Please enter at least one weather forecast device with "attr LINK ctrlWeatherDev1"}, DE => qq{Bitte geben sie mindestens ein Wettervorhersage Device mit "attr LINK ctrlWeatherDev1" an} }, crd => { EN => qq{Please select the radiation forecast service with "set LINK currentRadiationAPI"}, DE => qq{Bitte geben sie den Strahlungsvorhersage Dienst mit "set LINK currentRadiationAPI" an} }, cid => { EN => qq{Please specify the Inverter device with "set LINK currentInverterDev"}, DE => qq{Bitte geben sie das Wechselrichter Device mit "set LINK currentInverterDev" an} }, mid => { EN => qq{Please specify the device for energy measurement with "set LINK currentMeterDev"}, DE => qq{Bitte geben sie das Device zur Energiemessung mit "set LINK currentMeterDev" an} }, ist => { EN => qq{Please define all of your used string names with "set LINK inverterStrings"}, DE => qq{Bitte geben sie alle von Ihnen verwendeten Stringnamen mit "set LINK inverterStrings" an} }, mps => { EN => qq{Please enter the DC peak power of each string with "set LINK modulePeakString"}, DE => qq{Bitte geben sie die DC Spitzenleistung von jedem String mit "set LINK modulePeakString" an} }, mdr => { EN => qq{Please specify the module direction with "set LINK moduleAzimuth"}, DE => qq{Bitte geben sie die Modulausrichtung mit "set LINK moduleAzimuth" an} }, mta => { EN => qq{Please specify the module tilt angle with "set LINK moduleDeclination"}, DE => qq{Bitte geben sie den Modulneigungswinkel mit "set LINK moduleDeclination" 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} }, 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"} }, coord => { EN => qq{Please set attributes 'latitude' and 'longitude' in global device}, DE => qq{Bitte setzen sie die Attribute 'latitude' und 'longitude' im global Device} }, cnsm => { EN => qq{Consumer}, DE => qq{Verbraucher} }, eiau => { EN => qq{Off/On}, DE => qq{Aus/Ein} }, auto => { EN => qq{Auto}, DE => qq{Auto} }, lupt => { EN => qq{last update:}, DE => qq{Stand:} }, object => { EN => qq{Object}, DE => qq{Prüfobjekt} }, swonnm => { EN => qq{swoncond not met}, DE => qq{swoncond nicht erfüllt} }, swonmt => { EN => qq{swoncond met}, DE => qq{swoncond erfüllt} }, swofmt => { EN => qq{swoffcond met}, DE => qq{swoffcond erfüllt} }, emsple => { EN => qq{max surplus forecast too low}, DE => qq{max Überschußprognose zu gering} }, nmspld => { EN => qq{no max surplus found for current day}, DE => qq{kein max Überschuss für den aktuellen Tag gefunden} }, state => { EN => qq{Status}, DE => qq{Status} }, result => { EN => qq{Result}, DE => qq{Ergebnis} }, attrib => { EN => qq{attribute}, DE => qq{Attribut} }, note => { EN => qq{Note}, DE => qq{Hinweis} }, dwdcat => { EN => qq{The Deutscher Wetterdienst Station Catalog}, DE => qq{Der Stationskatalog des Deutschen Wetterdienstes} }, nrsele => { EN => qq{No. selected entries:}, DE => qq{Anzahl ausgewählter Einträge:} }, wfmdcf => { EN => qq{Wait for more days with a consumption figure}, DE => qq{Warte auf weitere Tage mit einer Verbrauchszahl} }, autoct => { EN => qq{Autocorrection:}, DE => qq{Autokorrektur:} }, plntck => { EN => qq{Plant Configurationcheck Information}, DE => qq{Informationen zur Anlagenkonfigurationsprüfung} }, lbpcq => { EN => qq{Quality:}, DE => qq{Qualität:} }, lblPvh => { EN => qq{next 4h:}, DE => qq{nächste 4h:} }, lblPRe => { EN => qq{rest today:}, DE => qq{Rest heute:} }, lblPTo => { EN => qq{tomorrow:}, DE => qq{morgen:} }, lblPCu => { EN => qq{currently:}, DE => qq{aktuell:} }, bnsas => { EN => qq{from minutes before the upcoming sunrise}, DE => qq{ab Minuten vor dem kommenden Sonnenaufgang} }, dvtn => { EN => qq{Deviation}, DE => qq{Abweichung} }, pvgen => { EN => qq{Generation}, DE => qq{Erzeugung} }, conspt => { EN => qq{Consumption}, DE => qq{Verbrauch} }, tday => { EN => qq{today}, DE => qq{heute} }, ctnsly => { EN => qq{continuously}, DE => qq{fortlaufend} }, yday => { EN => qq{yesterday}, DE => qq{gestern} }, after => { EN => qq{after}, DE => qq{nach} }, aihtxt => { EN => qq{AI state:}, DE => qq{KI Status:} }, aimmts => { EN => qq{Perl module Test2::Suite is missing}, DE => qq{Perl Modul Test2::Suite ist nicht vorhanden} }, aiwook => { EN => qq{AI support works properly, but does not provide a value for the current hour}, DE => qq{KI Unterstützung arbeitet einwandfrei, liefert jedoch keinen Wert für die aktuelle Stunde} }, aiwhit => { EN => qq{the PV forecast value for the current hour is provided by the AI support}, DE => qq{der PV Vorhersagewert für die aktuelle Stunde wird von der KI Unterstützung geliefert} }, ailatr => { EN => qq{last AI training:}, DE => qq{letztes KI-Training:} }, aitris => { EN => qq{Runtime in seconds:}, DE => qq{Laufzeit in Sekunden:} }, airule => { EN => qq{List of strings that describe the tree in rule-form}, DE => qq{Liste von Zeichenfolgen, die den Baum in Form von Regeln beschreiben} }, ainode => { EN => qq{Number of nodes in the trained decision tree}, DE => qq{Anzahl der Knoten im trainierten Entscheidungsbaum} }, aidept => { EN => qq{Maximum number of decisions that would need to be made a classification}, DE => qq{Maximale Anzahl von Entscheidungen, die für eine Klassifizierung getroffen werden müssen} }, nxtscc => { EN => qq{next SolCast call}, DE => qq{nächste SolCast Abfrage} }, fulfd => { EN => qq{fulfilled}, DE => qq{erfüllt} }, widnin => { EN => qq{FHEM Tablet UI V2 is not installed.}, DE => qq{FHEM Tablet UI V2 ist nicht installiert.} }, widok => { EN => qq{The FHEM Tablet UI widget Files are up to date.}, DE => qq{Die FHEM Tablet UI Widget-Dateien sind aktuell.} }, widnup => { EN => qq{The SolarForecast FHEM Tablet UI widget files are not up to date.}, DE => qq{Die FHEM Tablet UI Widget-Dateien sind nicht aktuell.} }, widerr => { EN => qq{The FHEM Tablet UI V2 is installed but the update status of widget Files can't be checked.}, DE => qq{FTUI V2 ist installiert, der Aktualisierungsstatus der Widgets kann nicht geprüft werden.} }, 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 the device has just been redefined.}, DE => qq{Sollte nicht leer sein. Vielleicht wurde das Device erst neu definiert.} }, scnp => { EN => qq{Scheduling of the consumer is not provided}, DE => qq{Die Einplanung des Verbrauchers ist nicht vorgesehen} }, vrmcr => { EN => qq{Please set the Victron VRM Portal credentials with "set LINK vrmCredentials".}, DE => qq{Bitte setzen sie die Victron VRM Portal Zugangsdaten mit "set LINK vrmCredentials". } }, awd => { EN => qq{LINK is waiting for solar forecast data ...
}, DE => qq{LINK wartet auf Solarvorhersagedaten ...
} }, wexso => { EN => qq{switched externally}, DE => qq{von extern umgeschaltet} }, 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!} }, pstate => { EN => qq{Planning status: 
Info: 
On: 
Off: 
Remaining lock time:  seconds}, DE => qq{Planungsstatus: 
Info: 
Ein: 
Aus: 
verbleibende Sperrzeit:  Sekunden} }, ); my %htitles = ( # Hash Hilfetexte (Mouse Over) 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} }, iave => { EN => qq{Off -> Switch on consumer}, DE => qq{Aus -> Verbraucher einschalten} }, ians => { EN => qq{Off -> no on-command defined!}, DE => qq{Aus -> kein on-Kommando definiert!} }, 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!} }, natc => { EN => qq{automatic cycle:}, DE => qq{automatischer Zyklus:} }, predtime => { EN => qq{Prediction time Radiation data:}, DE => qq{Vorhersagezeitpunkt Strahlungsdaten:} }, dwdtime => { EN => qq{Forecast time Weather data}, DE => qq{Vorhersagezeitpunkt Wetterdaten} }, upd => { EN => qq{Click for update}, DE => qq{Klick für Update} }, on => { EN => qq{switched on}, DE => qq{eingeschaltet} }, off => { EN => qq{switched off}, DE => qq{ausgeschaltet} }, undef => { EN => qq{undefined}, DE => qq{undefiniert} }, dela => { EN => qq{delayed}, DE => qq{verzoegert} }, azimuth => { EN => qq{Azimuth}, DE => qq{Azimut} }, elevatio => { EN => qq{Elevation}, DE => qq{Höhe} }, sunpos => { EN => qq{Sun position (decimal degrees)}, DE => qq{Sonnenstand (Dezimalgrad)} }, conrec => { EN => qq{Current time is within the consumption planning}, DE => qq{Aktuelle Zeit liegt innerhalb der Verbrauchsplanung} }, conrecba => { EN => qq{Current time is within the consumption planning, Priority charging Battery is active}, DE => qq{Aktuelle Zeit liegt innerhalb der Verbrauchsplanung, Vorrangladen Batterie ist aktiv} }, connorec => { EN => qq{Consumption planning is outside current time\n(Click for immediate planning)}, DE => qq{Verbrauchsplanung liegt ausserhalb aktueller Zeit\n(Klick für sofortige Einplanung)} }, akorron => { EN => qq{switched off\nenable auto correction with:\nset pvCorrectionFactor_Auto on*}, DE => qq{ausgeschaltet\nAutokorrektur einschalten mit:\nset pvCorrectionFactor_Auto on*} }, splus => { EN => qq{PV surplus sufficient}, DE => qq{PV-Überschuß ausreichend} }, nosplus => { EN => qq{PV surplus insufficient}, DE => qq{PV-Überschuß unzureichend} }, plchk => { EN => qq{Configuration check of the plant}, DE => qq{Konfigurationsprüfung der Anlage} }, jtsfft => { EN => qq{Open the SolarForecast Forum}, DE => qq{Öffne das SolarForecast Forum} }, scaresps => { EN => qq{API request successful}, DE => qq{API Abfrage erfolgreich} }, dwfcrsu => { EN => qq{Weather data are up to date according to used DWD model}, DE => qq{Wetterdaten sind aktuell entsprechend des verwendeten DWD Modell} }, scarespf => { EN => qq{API request failed}, DE => qq{API Abfrage fehlgeschlagen} }, dapic => { EN => qq{API requests or request equivalents already carried out today}, DE => qq{Heute bereits durchgeführte API-Anfragen bzw. Anfragen-Äquivalente} }, rapic => { EN => qq{remaining API requests}, DE => qq{verfügbare API-Anfragen} }, yheyfdl => { EN => qq{You have exceeded your free daily limit!}, DE => qq{Sie haben Ihr kostenloses Tageslimit überschritten!} }, rlfaccpr => { EN => qq{Rate limit for API requests reached in current period!}, DE => qq{Abfragegrenze für API-Anfragen im aktuellen Zeitraums erreicht!} }, raricp => { EN => qq{remaining API requests in the current period}, DE => qq{verfügbare API-Anfragen der laufenden Periode} }, 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} }, norate => { EN => qq{not rated}, DE => qq{nicht bewertet} }, aimstt => { EN => qq{Perl module AI::DecisionTree is missing}, DE => qq{Perl Modul AI::DecisionTree ist nicht vorhanden} }, pstate => { EN => qq{Planning status: \nInfo: \n\nOn: \nOff: \nRemaining lock time:  seconds}, DE => qq{Planungsstatus: \nInfo: \n\nEin: \nAus: \nverbleibende Sperrzeit:  Sekunden} }, ainuse => { EN => qq{AI Perl module is installed, but the AI support is not used.\nRun 'set plantConfiguration check' for hints.}, DE => qq{KI Perl Modul ist installiert, aber die KI Unterstützung wird nicht verwendet.\nPrüfen sie 'set plantConfiguration check' für Hinweise.} }, arsrad2o => { EN => qq{API query successful but the radiation values are outdated.\nCheck the plant with 'set plantConfiguration check'.}, DE => qq{API Abfrage erfolgreich aber die Strahlungswerte sind veraltet.\nPrüfen sie die Anlage mit 'set plantConfiguration check'.} }, aswfc2o => { EN => qq{The weather data is outdated.\nCheck the plant with 'set plantConfiguration check'.}, DE => qq{Die Wetterdaten sind veraltet.\nPrüfen sie die Anlage mit 'set plantConfiguration check'.} }, ); my %weather_ids = ( # s => 0 , 0 - 3 DWD -> kein signifikantes Wetter # s => 1 , 45 - 99 DWD -> signifikantes Wetter '0' => { s => '0', icon => 'weather_sun', txtd => 'sonnig', txte => 'sunny' }, '1' => { s => '0', icon => 'weather_cloudy_light', txtd => 'Bewölkung abnehmend', txte => 'Cloudiness decreasing' }, '2' => { s => '0', icon => 'weather_cloudy', txtd => 'Bewölkung unverändert', txte => 'Cloudiness unchanged' }, '3' => { s => '0', icon => 'weather_cloudy_heavy', txtd => 'Bewölkung zunehmend', txte => 'Cloudiness increasing' }, '4' => { s => '0', icon => 'unknown', txtd => 'Sicht durch Rauch oder Asche vermindert', txte => 'Visibility reduced by smoke or ash' }, '5' => { s => '0', icon => 'unknown', txtd => 'trockener Dunst (relative Feuchte < 80 %)', txte => 'dry haze (relative humidity < 80 %)' }, '6' => { s => '0', icon => 'unknown', txtd => 'verbreiteter Schwebstaub, nicht vom Wind herangeführt', txte => 'widespread airborne dust, not brought in by the wind' }, '7' => { s => '0', icon => 'unknown', txtd => 'Staub oder Sand bzw. Gischt, vom Wind herangeführt', txte => 'Dust or sand or spray, brought in by the wind' }, '8' => { s => '0', icon => 'unknown', txtd => 'gut entwickelte Staub- oder Sandwirbel', txte => 'well-developed dust or sand vortex' }, '9' => { s => '0', icon => 'unknown', txtd => 'Staub- oder Sandsturm im Gesichtskreis, aber nicht an der Station', txte => 'Dust or sand storm in the visual circle, but not at the station' }, '10' => { s => '0', icon => 'weather_fog', txtd => 'Nebel', txte => 'Fog' }, '11' => { s => '0', icon => 'weather_rain_fog', txtd => 'Nebel mit Regen', txte => 'Fog with rain' }, '12' => { s => '0', icon => 'weather_fog', txtd => 'durchgehender Bodennebel', txte => 'continuous ground fog' }, '13' => { s => '0', icon => 'unknown', txtd => 'Wetterleuchten sichtbar, kein Donner gehört', txte => 'Weather light visible, no thunder heard' }, '14' => { s => '0', icon => 'unknown', txtd => 'Niederschlag im Gesichtskreis, nicht den Boden erreichend', txte => 'Precipitation in the visual circle, not reaching the ground' }, '15' => { s => '0', icon => 'unknown', txtd => 'Niederschlag in der Ferne (> 5 km), aber nicht an der Station', txte => 'Precipitation in the distance (> 5 km), but not at the station' }, '16' => { s => '0', icon => 'unknown', txtd => 'Niederschlag in der Nähe (< 5 km), aber nicht an der Station', txte => 'Precipitation in the vicinity (< 5 km), but not at the station' }, '17' => { s => '0', icon => 'unknown', txtd => 'Gewitter (Donner hörbar), aber kein Niederschlag an der Station', txte => 'Thunderstorm (thunder audible), but no precipitation at the station' }, '18' => { s => '0', icon => 'unknown', txtd => 'Markante Böen im Gesichtskreis, aber kein Niederschlag an der Station', txte => 'marked gusts in the visual circle, but no precipitation at the station' }, '19' => { s => '0', icon => 'unknown', txtd => 'Tromben (trichterförmige Wolkenschläuche) im Gesichtskreis', txte => 'Trombles (funnel-shaped cloud tubes) in the circle of vision' }, '20' => { s => '0', icon => 'unknown', txtd => 'nach Sprühregen oder Schneegriesel', txte => 'after drizzle or snow drizzle' }, '21' => { s => '0', icon => 'unknown', txtd => 'nach Regen', txte => 'after rain' }, '22' => { s => '0', icon => 'unknown', txtd => 'nach Schnefall', txte => 'after snowfall' }, '23' => { s => '0', icon => 'unknown', txtd => 'nach Schneeregen oder Eiskörnern', txte => 'after sleet or ice grains' }, '24' => { s => '0', icon => 'unknown', txtd => 'nach gefrierendem Regen', txte => 'after freezing rain' }, '25' => { s => '0', icon => 'unknown', txtd => 'nach Regenschauer', txte => 'after rain shower' }, '26' => { s => '0', icon => 'unknown', txtd => 'nach Schneeschauer', txte => 'after snow shower' }, '27' => { s => '0', icon => 'unknown', txtd => 'nach Graupel- oder Hagelschauer', txte => 'after sleet or hail showers' }, '28' => { s => '0', icon => 'unknown', txtd => 'nach Nebel', txte => 'after fog' }, '29' => { s => '0', icon => 'unknown', txtd => 'nach Gewitter', txte => 'after thunderstorm' }, '30' => { s => '0', icon => 'unknown', txtd => 'leichter oder mäßiger Sandsturm, an Intensität abnehmend', txte => 'light or moderate sandstorm, decreasing in intensity' }, '31' => { s => '0', icon => 'unknown', txtd => 'leichter oder mäßiger Sandsturm, unveränderte Intensität', txte => 'light or moderate sandstorm, unchanged intensity' }, '32' => { s => '0', icon => 'unknown', txtd => 'leichter oder mäßiger Sandsturm, an Intensität zunehmend', txte => 'light or moderate sandstorm, increasing in intensity' }, '33' => { s => '0', icon => 'unknown', txtd => 'schwerer Sandsturm, an Intensität abnehmend', txte => 'heavy sandstorm, decreasing in intensity' }, '34' => { s => '0', icon => 'unknown', txtd => 'schwerer Sandsturm, unveränderte Intensität', txte => 'heavy sandstorm, unchanged intensity' }, '35' => { s => '0', icon => 'unknown', txtd => 'schwerer Sandsturm, an Intensität zunehmend', txte => 'heavy sandstorm, increasing in intensity' }, '36' => { s => '0', icon => 'weather_snow_light', txtd => 'leichtes oder mäßiges Schneefegen, unter Augenhöhe', txte => 'light or moderate snow sweeping, below eye level' }, '37' => { s => '0', icon => 'weather_snow_heavy', txtd => 'starkes Schneefegen, unter Augenhöhe', txte => 'heavy snow sweeping, below eye level' }, '38' => { s => '0', icon => 'weather_snow_light', txtd => 'leichtes oder mäßiges Schneetreiben, über Augenhöhe', txte => 'light or moderate blowing snow, above eye level' }, '39' => { s => '0', icon => 'weather_snow_heavy', txtd => 'starkes Schneetreiben, über Augenhöhe', txte => 'heavy snow drifting, above eye level' }, '40' => { s => '0', icon => 'weather_fog', txtd => 'Nebel in einiger Entfernung', txte => 'Fog in some distance' }, '41' => { s => '0', icon => 'weather_fog', txtd => 'Nebel in Schwaden oder Bänken', txte => 'Fog in swaths or banks' }, '42' => { s => '0', icon => 'weather_fog', txtd => 'Nebel, Himmel erkennbar, dünner werdend', txte => 'Fog, sky recognizable, thinning' }, '43' => { s => '0', icon => 'weather_fog', txtd => 'Nebel, Himmel nicht erkennbar, dünner werdend', txte => 'Fog, sky not recognizable, thinning' }, '44' => { s => '0', icon => 'weather_fog', txtd => 'Nebel, Himmel erkennbar, unverändert', txte => 'Fog, sky recognizable, unchanged' }, '45' => { s => '1', icon => 'weather_fog', txtd => 'Nebel', txte => 'Fog' }, '46' => { s => '0', icon => 'weather_fog', txtd => 'Nebel, Himmel erkennbar, dichter werdend', txte => 'Fog, sky recognizable, becoming denser' }, '47' => { s => '0', icon => 'weather_fog', txtd => 'Nebel, Himmel nicht erkennbar, dichter werdend', txte => 'Fog, sky not visible, becoming denser' }, '48' => { s => '1', icon => 'weather_fog', txtd => 'Nebel mit Reifbildung', txte => 'Fog with frost formation' }, '49' => { s => '0', icon => 'weather_fog', txtd => 'Nebel mit Reifansatz, Himmel nicht erkennbar', txte => 'Fog with frost, sky not visible' }, '50' => { s => '0', icon => 'weather_rain', txtd => 'unterbrochener leichter Sprühregen', txte => 'intermittent light drizzle' }, '51' => { s => '1', icon => 'weather_rain_light', txtd => 'leichter Sprühregen', txte => 'light drizzle' }, '52' => { s => '0', icon => 'weather_rain', txtd => 'unterbrochener mäßiger Sprühregen', txte => 'intermittent moderate drizzle' }, '53' => { s => '1', icon => 'weather_rain_light', txtd => 'leichter Sprühregen', txte => 'light drizzle' }, '54' => { s => '0', icon => 'weather_rain_heavy', txtd => 'unterbrochener starker Sprühregen', txte => 'intermittent heavy drizzle' }, '55' => { s => '1', icon => 'weather_rain_heavy', txtd => 'starker Sprühregen', txte => 'heavy drizzle' }, '56' => { s => '1', icon => 'weather_rain_light', txtd => 'leichter gefrierender Sprühregen', txte => 'light freezing drizzle' }, '57' => { s => '1', icon => 'weather_rain_heavy', txtd => 'mäßiger oder starker gefrierender Sprühregen', txte => 'moderate or heavy freezing drizzle' }, '58' => { s => '0', icon => 'weather_rain_light', txtd => 'leichter Sprühregen mit Regen', txte => 'light drizzle with rain' }, '59' => { s => '0', icon => 'weather_rain_heavy', txtd => 'mäßiger oder starker Sprühregen mit Regen', txte => 'moderate or heavy drizzle with rain' }, '60' => { s => '0', icon => 'weather_rain_light', txtd => 'unterbrochener leichter Regen oder einzelne Regentropfen', txte => 'intermittent light rain or single raindrops' }, '61' => { s => '1', icon => 'weather_rain_light', txtd => 'leichter Regen', txte => 'light rain' }, '62' => { s => '0', icon => 'weather_rain', txtd => 'unterbrochener mäßiger Regen', txte => 'intermittent moderate rain' }, '63' => { s => '1', icon => 'weather_rain', txtd => 'mäßiger Regen', txte => 'moderate rain' }, '64' => { s => '0', icon => 'weather_rain_heavy', txtd => 'unterbrochener starker Regen', txte => 'intermittent heavy rain' }, '65' => { s => '1', icon => 'weather_rain_heavy', txtd => 'starker Regen', txte => 'heavy rain' }, '66' => { s => '1', icon => 'weather_rain_snow_light', txtd => 'leichter gefrierender Regen', txte => 'light freezing rain' }, '67' => { s => '1', icon => 'weather_rain_snow_heavy', txtd => 'mäßiger oder starker gefrierender Regen', txte => 'moderate or heavy freezing rain' }, '68' => { s => '0', icon => 'weather_rain_snow_light', txtd => 'leichter Schneeregen', txte => 'light sleet' }, '69' => { s => '0', icon => 'weather_rain_snow_heavy', txtd => 'mäßiger oder starker Schneeregen', txte => 'moderate or heavy sleet' }, '70' => { s => '0', icon => 'weather_snow_light', txtd => 'unterbrochener leichter Schneefall oder einzelne Schneeflocken', txte => 'intermittent light snowfall or single snowflakes' }, '71' => { s => '1', icon => 'weather_snow_light', txtd => 'leichter Schneefall', txte => 'light snowfall' }, '72' => { s => '0', icon => 'weather_snow', txtd => 'unterbrochener mäßiger Schneefall', txte => 'intermittent moderate snowfall' }, '73' => { s => '1', icon => 'weather_snow', txtd => 'mäßiger Schneefall', txte => 'moderate snowfall' }, '74' => { s => '0', icon => 'weather_snow_heavy', txtd => 'unterbrochener starker Schneefall', txte => 'intermittent heavy snowfall' }, '75' => { s => '1', icon => 'weather_snow_heavy', txtd => 'starker Schneefall', txte => 'heavy snowfall' }, '76' => { s => '0', icon => 'weather_frost', txtd => 'Eisnadeln (Polarschnee)', txte => 'Ice needles (polar snow)' }, '77' => { s => '1', icon => 'weather_frost', txtd => 'Schneegriesel', txte => 'Snow drizzle' }, '78' => { s => '0', icon => 'weather_frost', txtd => 'Schneekristalle', txte => 'Snow crystals' }, '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' }, '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' }, '85' => { s => '1', icon => 'weather_snow_light', txtd => 'leichter Schneeschauer', txte => 'light snow shower' }, '86' => { s => '1', icon => 'weather_snow_heavy', txtd => 'mäßiger oder starker Schneeschauer', txte => 'moderate or heavy snow shower' }, '87' => { s => '0', icon => 'weather_snow_heavy', txtd => 'mäßiger oder starker Graupelschauer', txte => 'moderate or heavy sleet shower' }, '88' => { s => '0', icon => 'unknown', txtd => 'leichter Hagelschauer', txte => 'light hailstorm' }, '89' => { s => '0', icon => 'unknown', txtd => 'mäßiger oder starker Hagelschauer', txte => 'moderate or heavy hailstorm' }, '90' => { s => '0', icon => 'weather_thunderstorm', txtd => '', txte => '' }, '91' => { s => '0', icon => 'weather_storm', txtd => '', txte => '' }, '92' => { s => '0', icon => 'weather_thunderstorm', txtd => '', txte => '' }, '93' => { s => '0', icon => 'weather_thunderstorm', txtd => '', txte => '' }, '94' => { s => '0', icon => 'weather_thunderstorm', txtd => '', txte => '' }, '95' => { s => '1', icon => 'weather_thunderstorm', txtd => 'leichtes oder mäßiges Gewitter ohne Graupel oder Hagel', txte => 'light or moderate thunderstorm without sleet or hail' }, '96' => { s => '1', icon => 'weather_storm', txtd => 'starkes Gewitter ohne Graupel oder Hagel,Gewitter mit Graupel oder Hagel', txte => 'strong thunderstorm without sleet or hail,thunderstorm with sleet or hail' }, '97' => { s => '0', icon => 'weather_storm', txtd => 'starkes Gewitter mit Regen oder Schnee', txte => 'heavy thunderstorm with rain or snow' }, '98' => { s => '0', icon => 'weather_storm', txtd => 'starkes Gewitter mit Sandsturm', txte => 'strong thunderstorm with sandstorm' }, '99' => { s => '1', icon => 'weather_storm', txtd => 'starkes Gewitter mit Graupel oder Hagel', txte => 'strong thunderstorm with sleet or hail' }, '100' => { s => '0', icon => 'weather_night', txtd => 'sternenklarer Himmel', txte => 'starry sky' }, ); my %hef = ( # Energiedaktoren für Verbrauchertypen "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.50, m => 0.30, l => 0.40, mt => 120 }, "noSchedule" => { f => 1.00, m => 1.00, l => 1.00, mt => $defmintime }, ); my %hcsr = ( # Funktiontemplate zur Erstellung optionaler Statistikreadings currentAPIinterval => { fnr => 1, fn => \&SolCastAPIVal, par => '', unit => '', def => 0 }, # par = Parameter zur spezifischen Verwendung lastretrieval_time => { fnr => 1, fn => \&SolCastAPIVal, par => '', unit => '', def => '-' }, lastretrieval_timestamp => { fnr => 1, fn => \&SolCastAPIVal, par => '', unit => '', def => '-' }, response_message => { fnr => 1, fn => \&SolCastAPIVal, par => '', unit => '', def => '-' }, todayMaxAPIcalls => { fnr => 1, fn => \&SolCastAPIVal, par => '', unit => '', def => 'apimaxreq' }, todayDoneAPIcalls => { fnr => 1, fn => \&SolCastAPIVal, par => '', unit => '', def => 0 }, todayDoneAPIrequests => { fnr => 1, fn => \&SolCastAPIVal, par => '', unit => '', def => 0 }, todayRemainingAPIcalls => { fnr => 1, fn => \&SolCastAPIVal, par => '', unit => '', def => 'apimaxreq' }, todayRemainingAPIrequests => { fnr => 1, fn => \&SolCastAPIVal, par => '', unit => '', def => 'apimaxreq' }, runTimeCentralTask => { fnr => 2, fn => \&CurrentVal, par => '', unit => '', def => '-' }, runTimeLastAPIAnswer => { fnr => 2, fn => \&CurrentVal, par => '', unit => '', def => '-' }, runTimeLastAPIProc => { fnr => 2, fn => \&CurrentVal, par => '', unit => '', def => '-' }, allStringsFullfilled => { fnr => 2, fn => \&CurrentVal, par => '', unit => '', def => 0 }, todayConForecastTillSunset => { fnr => 2, fn => \&CurrentVal, par => 'tdConFcTillSunset', unit => ' Wh', def => 0 }, runTimeTrainAI => { fnr => 3, fn => \&CircularVal, par => 99, unit => '', def => '-' }, SunHours_Remain => { fnr => 4, fn => \&CurrentVal, par => '', unit => '', def => 0 }, # fnr => 3 -> Custom Calc SunMinutes_Remain => { fnr => 4, fn => \&CurrentVal, par => '', unit => '', def => 0 }, dayAfterTomorrowPVforecast => { fnr => 4, fn => \&SolCastAPIVal, par => 'pv_estimate50', unit => '', def => 0 }, todayGridFeedIn => { fnr => 4, fn => \&CircularVal, par => 99, unit => '', def => 0 }, todayGridConsumption => { fnr => 4, fn => \&CircularVal, par => 99, unit => '', def => 0 }, todayBatIn => { fnr => 4, fn => \&CircularVal, par => 99, unit => '', def => 0 }, todayBatOut => { fnr => 4, fn => \&CircularVal, par => 99, unit => '', def => 0 }, daysUntilBatteryCare => { fnr => 4, fn => \&CircularVal, par => 99, unit => '', def => '-' }, todayConsumptionForecast => { fnr => 4, fn => \&NexthoursVal, par => 'confc', unit => ' Wh', def => '-' }, conForecastTillNextSunrise => { fnr => 4, fn => \&NexthoursVal, par => 'confc', unit => ' Wh', def => 0 }, ); for my $csr (1..$maxconsumer) { $csr = sprintf "%02d", $csr; $hcsr{'currentRunMtsConsumer_'.$csr}{fnr} = 4; $hcsr{'currentRunMtsConsumer_'.$csr}{fn} = \&ConsumerVal; $hcsr{'currentRunMtsConsumer_'.$csr}{par} = 'cycleTime'; $hcsr{'currentRunMtsConsumer_'.$csr}{unit} = ' min'; $hcsr{'currentRunMtsConsumer_'.$csr}{def} = 0; } # Funktiontemplate zur Speicherung von Werten in pvHistory # storname = Name des Elements in der pvHistory # nhour = evtl. abweichend von $nhour # fpar = Parameter zur spezifischen Verwendung my %hfspvh = ( radiation => { fn => \&_storeVal, storname => 'rad1h', validkey => undef, fpar => undef }, # irradiation DoN => { fn => \&_storeVal, storname => 'DoN', validkey => undef, fpar => undef }, # Tag 1 oder Nacht 0 batmaxsoc => { fn => \&_storeVal, storname => 'batmaxsoc', validkey => undef, fpar => undef }, # max. erreichter SOC des Tages batsetsoc => { fn => \&_storeVal, storname => 'batsetsoc', validkey => undef, fpar => undef }, # optimaler SOC für den Tag sunaz => { fn => \&_storeVal, storname => 'sunaz', validkey => undef, fpar => undef }, # Sonnenstand Azimuth sunalt => { fn => \&_storeVal, storname => 'sunalt', validkey => undef, fpar => undef }, # Sonnenstand Altitude etotal => { fn => \&_storeVal, storname => 'etotal', validkey => undef, fpar => undef }, # etotal des Wechselrichters batintotal => { fn => \&_storeVal, storname => 'batintotal', validkey => undef, fpar => undef }, # totale Batterieladung batouttotal => { fn => \&_storeVal, storname => 'batouttotal', validkey => undef, fpar => undef }, # totale Batterieentladung weatherid => { fn => \&_storeVal, storname => 'weatherid', validkey => undef, fpar => undef }, # Wetter ID weathercloudcover => { fn => \&_storeVal, storname => 'wcc', validkey => undef, fpar => undef }, # Wolkenbedeckung totalrain => { fn => \&_storeVal, storname => 'rr1c', validkey => undef, fpar => undef }, # Gesamtniederschlag (1-stündig) letzte 1 Stunde pvcorrfactor => { fn => \&_storeVal, storname => 'pvcorrf', validkey => undef, fpar => undef }, # pvCorrectionFactor temperature => { fn => \&_storeVal, storname => 'temp', validkey => undef, fpar => undef }, # Außentemperatur batinthishour => { fn => \&_storeVal, storname => 'batin', validkey => undef, fpar => 'comp99' }, # Batterieladung in Stunde batoutthishour => { fn => \&_storeVal, storname => 'batout', validkey => undef, fpar => 'comp99' }, # Batterieentladung in Stunde pvfc => { fn => \&_storeVal, storname => 'pvfc', validkey => undef, fpar => 'comp99' }, # prognostizierter Energieertrag confc => { fn => \&_storeVal, storname => 'confc', validkey => undef, fpar => 'comp99' }, # prognostizierter Hausverbrauch cons => { fn => \&_storeVal, storname => 'gcons', validkey => undef, fpar => 'comp99' }, # bezogene Energie gfeedin => { fn => \&_storeVal, storname => 'gfeedin', validkey => undef, fpar => 'comp99' }, # eingespeiste Energie con => { fn => \&_storeVal, storname => 'con', validkey => undef, fpar => 'comp99' }, # realer Hausverbrauch Energie pvrl => { fn => \&_storeVal, storname => 'pvrl', validkey => 'pvrlvd', fpar => 'comp99' }, # realer Energieertrag ); # 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}{nexthours} # NextHours Werte # $data{$type}{$name}{consumers} # Consumer Hash # $data{$type}{$name}{strings} # Stringkonfiguration Hash # $data{$type}{$name}{solcastapi} # Zwischenspeicher API-Daten # $data{$type}{$name}{aidectree}{object} # AI Decision Tree Object # $data{$type}{$name}{aidectree}{aitrained} # AI Decision Tree trainierte Daten # $data{$type}{$name}{aidectree}{airaw} # Rohdaten für AI Input = Raw Trainigsdaten # $data{$type}{$name}{func} # interne Funktionen # $data{$type}{$name}{weatherdata} # temporärer Speicher Wetterdaten # $data{$type}{$name}{dwdcatalog} # DWD Stationskatalog ################################################################ # Init Fn ################################################################ sub Initialize { my $hash = shift; my $fwd = join ",", devspec2array("TYPE=FHEMWEB:FILTER=STATE=Initialized"); my $hod = join ",", map { sprintf "%02d", $_} (1..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 $dm = join ",", @dd; $hash->{DefFn} = \&Define; $hash->{UndefFn} = \&Undef; $hash->{GetFn} = \&Get; $hash->{SetFn} = \&Set; $hash->{DeleteFn} = \&Delete; $hash->{FW_summaryFn} = \&FwFn; $hash->{FW_detailFn} = \&FwFn; $hash->{ShutdownFn} = \&Shutdown; $hash->{DbLog_splitFn} = \&DbLogSplit; $hash->{AttrFn} = \&Attr; $hash->{NotifyFn} = \&Notify; $hash->{ReadyFn} = \&runTask; $hash->{AttrList} = "affect70percentRule:1,dynamic,0 ". "affectBatteryPreferredCharge:slider,0,1,100 ". "affectConsForecastIdentWeekdays:1,0 ". "affectConsForecastInPlanning:1,0 ". "affectMaxDayVariance ". "affectSolCastPercentile:select,10,50,90 ". "consumerLegend:none,icon_top,icon_bottom,text_top,text_bottom ". "consumerAdviceIcon ". "consumerLink:0,1 ". "ctrlAIdataStorageDuration ". "ctrlAIshiftTrainStart:slider,1,1,23 ". "ctrlAutoRefresh:selectnumbers,120,0.2,1800,0,log10 ". "ctrlAutoRefreshFW:$fwd ". "ctrlBackupFilesKeep ". "ctrlBatSocManagement:textField-long ". "ctrlConsRecommendReadings:multiple-strict,$allcs ". "ctrlDebug:multiple-strict,$dm,#14 ". "ctrlGenPVdeviation:daily,continuously ". "ctrlInterval ". "ctrlLanguage:DE,EN ". "ctrlNextDayForecastReadings:multiple-strict,$hod ". "ctrlShowLink:1,0 ". "ctrlSolCastAPImaxReq:selectnumbers,5,5,60,0,lin ". "ctrlSolCastAPIoptimizeReq:1,0 ". "ctrlStatisticReadings:multiple-strict,$srd ". "ctrlUserExitFn:textField-long ". "ctrlWeatherDev1 ". "ctrlWeatherDev2 ". "ctrlWeatherDev3 ". "disable:1,0 ". "flowGraphicSize ". "flowGraphicAnimate:1,0 ". "flowGraphicConsumerDistance:slider,80,10,500 ". "flowGraphicShowConsumer:1,0 ". "flowGraphicShowConsumerDummy:1,0 ". "flowGraphicShowConsumerPower:0,1 ". "flowGraphicShowConsumerRemainTime:0,1 ". "flowGraphicCss:textField-long ". "graphicBeamHeight ". "graphicBeamWidth:slider,20,5,100 ". "graphicBeam1Color:colorpicker,RGB ". "graphicBeam2Color:colorpicker,RGB ". "graphicBeam1Content:pvForecast,pvReal,gridconsumption,consumptionForecast ". "graphicBeam2Content:pvForecast,pvReal,gridconsumption,consumptionForecast ". "graphicBeam1FontColor:colorpicker,RGB ". "graphicBeam2FontColor:colorpicker,RGB ". "graphicBeam1MaxVal ". "graphicEnergyUnit:Wh,kWh ". "graphicHeaderOwnspec:textField-long ". "graphicHeaderOwnspecValForm:textField-long ". "graphicHeaderDetail:multiple-strict,all,co,pv,own,status ". "graphicHeaderShow:1,0 ". "graphicHistoryHour:slider,0,1,23 ". "graphicHourCount:slider,4,1,24 ". "graphicHourStyle ". "graphicLayoutType:single,double,diff ". "graphicSelect:both,flow,forecast,none ". "graphicShowDiff:no,top,bottom ". "graphicShowNight:1,0 ". "graphicShowWeather:1,0 ". "graphicSpaceSize ". "graphicStartHtml ". "graphicEndHtml ". "graphicWeatherColor:colorpicker,RGB ". "graphicWeatherColorNight:colorpicker,RGB ". $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", # }; eval { FHEM::Meta::InitMod( __FILE__, $hash ) }; ## no critic 'eval' return; } ############################################################### # SolarForecast Define ############################################################### 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, name => $hash->{NAME}, type => $hash->{TYPE}, notes => \%vNotesIntern, useAPI => 0, useSMUtils => 1, useErrCodes => 0, useCTZ => 1, }; use version 0.77; our $VERSION = moduleVersion ($params); # Versionsinformationen setzen createAssociatedWith ($hash); $params->{file} = $pvhcache.$name; # Cache File PV History einlesen wenn vorhanden $params->{cachename} = 'pvhist'; $params->{title} = 'pvHistory'; _readCacheFile ($params); $params->{file} = $pvccache.$name; # Cache File PV Circular einlesen wenn vorhanden $params->{cachename} = 'circular'; $params->{title} = 'pvCircular'; _readCacheFile ($params); $params->{file} = $csmcache.$name; # Cache File Consumer einlesen wenn vorhanden $params->{cachename} = 'consumers'; $params->{title} = 'consumerMaster'; _readCacheFile ($params); $params->{file} = $scpicache.$name; # Cache File SolCast API Werte einlesen wenn vorhanden $params->{cachename} = 'solcastapi'; $params->{title} = 'solApiData'; _readCacheFile ($params); $params->{file} = $aitrained.$name; # AI Cache File einlesen wenn vorhanden $params->{cachename} = 'aitrained'; $params->{title} = 'aiTrainedData'; _readCacheFile ($params); $params->{file} = $airaw.$name; # AI Rawdaten File einlesen wenn vorhanden $params->{cachename} = 'airaw'; $params->{title} = 'aiRawData'; _readCacheFile ($params); singleUpdateState ( {hash => $hash, state => 'initialized', evt => 1} ); $readyfnlist{$name} = $hash; # Registrierung in Ready-Schleife InternalTimer (gettimeofday()+$whistrepeat, "FHEM::SolarForecast::periodicWriteMemcache", $hash, 0); # Einstieg periodisches Schreiben historische Daten return; } ################################################################ # Cachefile lesen ################################################################ sub _readCacheFile { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $file = $paref->{file}; my $cachename = $paref->{cachename}; my $title = $paref->{title}; if ($cachename eq 'aitrained') { my ($err, $dtree) = fileRetrieve ($file); if (!$err && $dtree) { my $valid = $dtree->isa('AI::DecisionTree'); if ($valid) { delete $data{$type}{$name}{aidectree}{aitrained}; $data{$type}{$name}{aidectree}{aitrained} = $dtree; $data{$type}{$name}{current}{aitrainstate} = 'ok'; Log3 ($name, 3, qq{$name - cached data "$title" restored}); return; } } delete $data{$type}{$name}{circular}{99}{aitrainLastFinishTs}; delete $data{$type}{$name}{circular}{99}{runTimeTrainAI}; return; } if ($cachename eq 'airaw') { my ($err, $data) = fileRetrieve ($file); if (!$err && $data) { delete $data{$type}{$name}{aidectree}{airaw}; $data{$type}{$name}{aidectree}{airaw} = $data; $data{$type}{$name}{current}{aitrawstate} = 'ok'; Log3 ($name, 3, qq{$name - cached data "$title" restored}); } return; } if ($cachename eq 'dwdcatalog') { my ($err, $dwdc) = fileRetrieve ($file); if (!$err && $dwdc) { delete $data{$type}{$name}{dwdcatalog}; $data{$type}{$name}{dwdcatalog} = $dwdc; debugLog ($paref, 'dwdComm', qq{$title restored}); } return; } if ($cachename eq 'plantconfig') { my ($err, $plantcfg) = fileRetrieve ($file); return $err if($err); my ($nr, $na); if ($plantcfg) { ($nr, $na) = _restorePlantConfig ($hash, $plantcfg); Log3 ($name, 3, qq{$name - cached data "$title" restored. Number of restored Readings/Attributes: $nr/$na}); } return ('', $nr, $na); } my ($error, @content) = FileRead ($file); if (!$error) { my $json = join "", @content; my ($success) = evaljson ($hash, $json); if ($success) { $data{$hash->{TYPE}}{$name}{$cachename} = decode_json ($json); Log3 ($name, 3, qq{$name - cached data "$title" restored}); } else { Log3 ($name, 1, qq{$name - WARNING - The content of file "$file" is not readable or may be corrupt}); } } return; } ############################################################### # SolarForecast Set ############################################################### sub Set { my ($hash, @a) = @_; return qq{"set X" needs at least an argument} if(@a < 2); my $name = shift @a; my $opt = shift @a; my @args = @a; my $arg = join " ", map { my $p = $_; $p =~ s/\s//xg; $p; } @a; ## no critic 'Map blocks' my $prop = shift @a; my $prop1 = shift @a; my $prop2 = shift @a; return if((controller($name))[1]); my ($setlist,@cfs,@condevs,@bkps); my ($fcd,$ind,$med,$cf,$sp,$coms) = ('','','','','',''); my $type = $hash->{TYPE}; my @re = qw( aiData batteryTriggerSet consumerMaster consumerPlanning consumption currentBatterySet currentInverterSet currentMeterSet energyH4TriggerSet inverterStringSet moduleRoofTopSet powerTriggerSet pvCorrection roofIdentPair pvHistory ); my $resets = join ",",@re; my @fcdevs = qw( OpenMeteoDWD-API OpenMeteoDWDEnsemble-API OpenMeteoWorld-API SolCast-API ForecastSolar-API VictronKI-API ); push @fcdevs, devspec2array ("TYPE=DWD_OpenData"); my $rdd = join ",", @fcdevs; for my $h (@chours) { push @cfs, 'pvCorrectionFactor_'. sprintf("%02d",$h); } $cf = join " ", @cfs; for my $c (sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}) { push @condevs, $c if($c); } $coms = @condevs ? join ",", @condevs : 'noArg'; my $ipai = isPrepared4AI ($hash); opendir (DIR, $cachedir); while (my $file = readdir (DIR)) { next unless (-f "$cachedir/$file"); next unless ($file =~ /_${name}_/); next unless ($file =~ /_\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}$/); push @bkps, 'recover-'.$file; } closedir (DIR); my $rf = @bkps ? ','.join ",", reverse sort @bkps : ''; ## allg. gültige Setter ######################### $setlist = "Unknown argument $opt, choose one of ". "consumerImmediatePlanning:$coms ". "consumerNewPlanning:$coms ". "currentRadiationAPI:$rdd ". "currentBatteryDev:textField-long ". "currentInverterDev:textField-long ". "currentMeterDev:textField-long ". "energyH4Trigger:textField-long ". "inverterStrings ". "modulePeakString ". "operatingMemory:backup,save".$rf." ". "operationMode:active,inactive ". "plantConfiguration:check,save,restore ". "powerTrigger:textField-long ". "pvCorrectionFactor_Auto:noLearning,on_simple".($ipai ? ',on_simple_ai,' : ',')."on_complex".($ipai ? ',on_complex_ai,' : ',')."off ". "reset:$resets ". $cf." " ; ## API spezifische Setter ########################### if (isSolCastUsed ($hash)) { $setlist .= "moduleRoofTops ". "roofIdentPair " ; } elsif (isForecastSolarUsed ($hash)) { $setlist .= "moduleAzimuth ". "moduleDeclination " ; } elsif (isVictronKiUsed ($hash)) { $setlist .= "vrmCredentials " ; } else { $setlist .= "moduleAzimuth ". "moduleDeclination " ; } ## KI spezifische Setter ########################## if ($ipai) { $setlist .= "aiDecTree:addInstances,addRawData,train "; } ## Batterie spezifische Setter ################################ if (isBatteryUsed ($name)) { $setlist .= "batteryTrigger:textField-long "; } ## inactive (Setter überschreiben) #################################### if ((controller($name))[2]) { $setlist = "operationMode:active,inactive "; } my $params = { hash => $hash, name => $name, type => $type, opt => $opt, arg => $arg, argsref => \@args, prop => $prop, prop1 => $prop1, prop2 => $prop2, lang => getLang ($hash), debug => getDebug ($hash) }; if($hset{$opt} && defined &{$hset{$opt}{fn}}) { my $ret = q{}; $ret = &{$hset{$opt}{fn}} ($params); return $ret; } return "$setlist"; } ################################################################ # Setter consumerImmediatePlanning ################################################################ sub _setconsumerImmediatePlanning { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $opt = $paref->{opt}; my $c = $paref->{prop}; my $evt = $paref->{prop1} // 0; # geändert V 1.1.0 - 1 -> 0 return qq{no consumer number specified} if(!$c); return qq{no valid consumer id "$c"} if(!ConsumerVal ($hash, $c, "name", "")); if (ConsumerVal ($hash, $c, 'type', $defctype) eq 'noSchedule') { debugLog ($paref, "consumerPlanning", qq{consumer "$c" - }.$hqtxt{scnp}{EN}); $paref->{ps} = 'noSchedule'; $paref->{consumer} = $c; ___setConsumerPlanningState ($paref); delete $paref->{ps}; delete $paref->{consumer}; return; } my $startts = time; my $mintime = ConsumerVal ($hash, $c, "mintime", $defmintime); if (isSunPath ($hash, $c)) { # SunPath ist in mintime gesetzt my (undef, $setshift) = sunShift ($hash, $c); # Verschiebung (Sekunden) Sonnenuntergang bei SunPath Verwendung my $tdiff = (CurrentVal ($hash, 'sunsetTodayTs', 0) + $setshift) - $startts; $mintime = $tdiff / 60; # Minuten } my $stopdiff = $mintime * 60; 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', ''); writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben Log3 ($name, 3, qq{$name - Consumer "$calias" $planstate}) if($planstate); centralTask ($hash, $evt); return; } ################################################################ # Setter consumerNewPlanning ################################################################ sub _setconsumerNewPlanning { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $c = $paref->{prop}; my $evt = $paref->{prop1} // 0; # geändert V 1.1.0 - 1 -> 0 return qq{no consumer number specified} if(!$c); return qq{no valid consumer id "$c"} if(!ConsumerVal ($hash, $c, 'name', '')); if ($c) { deleteConsumerPlanning ($hash, $c); writeCacheToFile ($hash, 'consumers', $csmcache.$name); # Cache File Consumer schreiben } centralTask ($hash, $evt); return; } ################################################################ # Setter currentRadiationAPI ################################################################ sub _setcurrentRadiationAPI { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $prop = $paref->{prop} // return qq{no radiation device specified}; if ($prop !~ /-API$/x && (!$defs{$prop} || $defs{$prop}{TYPE} ne "DWD_OpenData")) { return qq{The device "$prop" doesn't exist or has no TYPE "DWD_OpenData"}; } my $awdev1 = AttrVal ($name, 'ctrlWeatherDev1', ''); if (($awdev1 eq 'OpenMeteoDWD-API' && $prop ne 'OpenMeteoDWD-API') || ($awdev1 eq 'OpenMeteoDWDEnsemble-API' && $prop ne 'OpenMeteoDWDEnsemble-API') || ($awdev1 eq 'OpenMeteoWorld-API' && $prop ne 'OpenMeteoWorld-API')) { return "The attribute 'ctrlWeatherDev1' is set to '$awdev1'. \n". "Change that attribute to another weather device first if you want use an other API."; } if ($prop =~ /(SolCast|OpenMeteoDWD|OpenMeteoDWDEnsemble|OpenMeteoWorld)-API/xs) { 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, "currentRadiationAPI", $prop, 1); createAssociatedWith ($hash); writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben setModel ($hash); # Model setzen deleteReadingspec ($hash, 'nextRadiationAPICall'); return if(_checkSetupNotComplete ($hash)); # keine Stringkonfiguration wenn Setup noch nicht komplett if ($prop =~ /(ForecastSolar|OpenMeteoDWD|OpenMeteoDWDEnsemble|OpenMeteoWorld)-API/xs) { my ($set, $lat, $lon, $elev) = locCoordinates(); return qq{set attributes 'latitude' and 'longitude' in global device first} if(!$set); my $tilt = ReadingsVal ($name, 'moduleDeclination', ''); # Modul Neigungswinkel für jeden Stringbezeichner return qq{Please complete command "set $name moduleDeclination".} if(!$tilt); my $dir = ReadingsVal ($name, 'moduleAzimuth', ''); # Modul Ausrichtung für jeden Stringbezeichner return qq{Please complete command "set $name moduleAzimuth".} if(!$dir); } my $type = $hash->{TYPE}; $data{$type}{$name}{current}{allStringsFullfilled} = 0; # Stringkonfiguration neu prüfen lassen return; } ################################################################ # Setter roofIdentPair ################################################################ sub _setroofIdentPair { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; 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}; writeCacheToFile ($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; } ###################################################################### # Setter victronCredentials # user, pwd, # idsite nach /installation// aus: # https://vrm.victronenergy.com/installation/XXXXX/... ###################################################################### sub _setVictronCredentials { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; my $type = $hash->{TYPE}; my $msg; if(!$arg) { return qq{The command "$opt" needs an argument !}; } my ($a,$h) = parseParams ($arg); if ($a->[0] && $a->[0] eq 'delete') { delete $data{$type}{$name}{solcastapi}{'?VRM'}; $msg = qq{Credentials for the Victron VRM API are deleted. }; } else { if(!$h->{user} || !$h->{pwd} || !$h->{idsite}) { return qq{The syntax of "$opt" is not correct. Please consider the commandref.}; } my $serial = eval { freeze ($h) } or do { return "Serialization ERROR: $@" }; $data{$type}{$name}{solcastapi}{'?VRM'}{'?API'}{credentials} = chew ($serial); $msg = qq{Credentials for the Victron VRM API has been saved.}; } writeCacheToFile ($hash, 'solcastapi', $scpicache.$name); # Cache File SolCast API Werte schreiben return $msg; } ################################################################ # Setter moduleRoofTops ################################################################ sub _setmoduleRoofTops { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $arg = $paref->{arg} // return qq{no module RoofTop was provided}; 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); writeCacheToFile ($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; } ################################################################ # Setter currentInverterDev ################################################################ sub _setinverterDevice { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; 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.}; } if ($h->{capacity} && !isNumeric($h->{capacity})) { return qq{The syntax of key "capacity" is not correct. Please consider the commandref.}; } readingsSingleUpdate ($hash, 'currentInverterDev', $arg, 1); createAssociatedWith ($hash); writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben return; } ################################################################ # Setter inverterStrings ################################################################ sub _setinverterStrings { ## no critic "not used" my $paref = shift; 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; for my $k (keys %{$data{$type}{$name}{solcastapi}}) { next if ($k =~ /\?/xs || $k ~~ @istrings); delete $data{$type}{$name}{solcastapi}{$k}; } readingsSingleUpdate ($hash, 'inverterStrings', $prop, 1); writeCacheToFile ($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. moduleDeclination) again ! \n}. qq{Use "set $name plantConfiguration check" to validate your Setup.}; return $ret; } ################################################################ # Setter currentMeterDev ################################################################ sub _setmeterDevice { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; 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.}; } if ($h->{conprice}) { # Bezugspreis (Arbeitspreis) pro kWh my @acp = split ":", $h->{conprice}; return qq{Incorrect input for key 'conprice'. Please consider the commandref.} if(scalar(@acp) != 2 && scalar(@acp) != 3); } if ($h->{feedprice}) { # Einspeisevergütung pro kWh my @afp = split ":", $h->{feedprice}; return qq{Incorrect input for key 'feedprice'. Please consider the commandref.} if(scalar(@afp) != 2 && scalar(@afp) != 3); } ## alte Speicherwerte löschen ############################### delete $data{$type}{$name}{circular}{'99'}{feedintotal}; delete $data{$type}{$name}{circular}{'99'}{initdayfeedin}; delete $data{$type}{$name}{circular}{'99'}{gridcontotal}; delete $data{$type}{$name}{circular}{'99'}{initdaygcon}; readingsSingleUpdate ($hash, 'currentMeterDev', $arg, 1); createAssociatedWith ($hash); writeCacheToFile ($hash, 'plantconfig', $plantcfg.$name); # Anlagenkonfiguration File schreiben return; } ################################################################ # Setter currentBatteryDev ################################################################ sub _setbatteryDevice { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; 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) || ($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.}; } ## alte Speicherwerte löschen ############################### delete $data{$type}{$name}{circular}{'99'}{initdaybatintot}; delete $data{$type}{$name}{circular}{'99'}{batintot}; delete $data{$type}{$name}{circular}{'99'}{initdaybatouttot}; delete $data{$type}{$name}{circular}{'99'}{batouttot}; delete $data{$type}{$name}{circular}{'99'}{lastTsMaxSocRchd}; delete $data{$type}{$name}{circular}{'99'}{nextTsMaxSocChge}; readingsSingleUpdate ($hash, "currentBatteryDev", $arg, 1); createAssociatedWith ($hash); writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben return; } ################################################################ # Setter operationMode ################################################################ sub _setoperationMode { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $prop = $paref->{prop} // return qq{no mode specified}; singleUpdateState ( {hash => $hash, state => $prop, evt => 1} ); return; } ################################################################ # Setter powerTrigger / batterytrigger / energyH4Trigger ################################################################ sub _setTrigger { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; 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.}; } } if ($opt eq 'powerTrigger') { deleteReadingspec ($hash, 'powerTrigger.*'); readingsSingleUpdate ($hash, 'powerTrigger', $arg, 1); } elsif ($opt eq 'batteryTrigger') { deleteReadingspec ($hash, 'batteryTrigger.*'); readingsSingleUpdate ($hash, 'batteryTrigger', $arg, 1); } elsif ($opt eq 'energyH4Trigger') { deleteReadingspec ($hash, 'energyH4Trigger.*'); readingsSingleUpdate ($hash, 'energyH4Trigger', $arg, 1); } writeCacheToFile ($hash, 'plantconfig', $plantcfg.$name); # Anlagenkonfiguration File schreiben return; } ################################################################ # Setter modulePeakString ################################################################ sub _setmodulePeakString { ## no critic "not used" my $paref = shift; 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); 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); writeCacheToFile ($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; } ################################################################ # Setter moduleDeclination ################################################################ sub _setmoduleDeclination { ## no critic "not used" my $paref = shift; 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); if(!keys %$h) { return qq{The specified inclination angle has an incorrect format}; } while (my ($key, $value) = each %$h) { if($value !~ /^(?:$tilt)$/x) { return qq{The inclination angle of "$key" is incorrect}; } } readingsSingleUpdate ($hash, 'moduleDeclination', $arg, 1); writeCacheToFile ($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; } ################################################################ # Setter moduleAzimuth # # Angabe entweder als Azimut-Bezeichner oder direkte # Azimut Angabe -180 ...0...180 # ################################################################ sub _setmoduleAzimuth { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $arg = $paref->{arg} // return qq{no module direction was provided}; my $dirs = "N|NE|E|SE|S|SW|W|NW"; # mögliche Azimut-Bezeichner wenn keine direkte Azimut Angabe 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 && ($value !~ /^(?:-?[0-9]{1,3})$/x || $value < -180 || $value > 180)) { return qq{The module direction of "$key" is wrong: $value}; } } readingsSingleUpdate ($hash, 'moduleAzimuth', $arg, 1); writeCacheToFile ($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; } ################################################################ # Setter plantConfiguration ################################################################ sub _setplantConfiguration { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; my ($err,$nr,$na,@pvconf); $arg = 'check' if (!$arg); 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, $nr, $na) = writeCacheToFile ($hash, 'plantconfig', $plantcfg.$name); # Anlagenkonfiguration fileStore schreiben if ($err) { return $err; } else { return qq{Plant Configuration has been written to file "$plantcfg.$name". Number of saved Readings/Attributes: $nr/$na}; } } if ($arg eq "restore") { $paref->{file} = $plantcfg.$name; $paref->{cachename} = 'plantconfig'; $paref->{title} = 'Plant Configuration'; ($err, $nr, $na) = _readCacheFile ($paref); if (!$err) { if ($nr || $na) { return qq{Plant Configuration restored from file "$plantcfg.$name". Number of restored Readings/Attributes: $nr/$na}; } else { return qq{The Plant Configuration file "}.$plantcfg.$name.qq{" was empty, nothing restored}; } } else { return $err; } } return; } ################################################################ # asynchrone Ausgabe Ergbnis Plantconfig Check ################################################################ sub __plantCfgAsynchOut { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $out = $paref->{out}; asyncOutput($hash->{HELPER}{CL}{1}, $out); delClHash ($name); return; } ################################################################ # Setter pvCorrectionFactor ################################################################ sub _setpvCorrectionFactor { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $prop = $paref->{prop} // return qq{no correction value specified}; 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]; deleteReadingspec ($hash, "pvCorrectionFactor_${cfnum}_autocalc"); centralTask ($hash, 0); return; } ################################################################ # Setter pvCorrectionFactor_Auto ################################################################ sub _setpvCorrectionFactorAuto { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $prop = $paref->{prop} // return qq{no correction value specified}; if ($prop eq 'noLearning') { my $pfa = ReadingsVal ($name, 'pvCorrectionFactor_Auto', 'off'); # aktuelle Autokorrektureinstellung $prop = $pfa.' '.$prop; } 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"); } writeCacheToFile ($hash, 'plantconfig', $plantcfg.$name); # Anlagenkonfiguration sichern return; } ################################################################ # Setter reset ################################################################ sub _setreset { ## no critic "not used" my $paref = shift; 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 $dday = $paref->{prop1} // ""; # ein bestimmter Tag der pvHistory angegeben ? my $dhour = $paref->{prop2} // ""; # eine bestimmte Stunde eines Tages der pvHistory angegeben ? if ($dday) { $dday = sprintf "%02d", $dday; if ($dhour) { $dhour = sprintf "%02d", $dhour; delete $data{$type}{$name}{pvhist}{$dday}{$dhour}; Log3 ($name, 3, qq{$name - Day "$dday" hour "$dhour" deleted from pvHistory}); $paref->{reorg} = 1; # den Tag Stunde "99" reorganisieren $paref->{reorgday} = $dday; setPVhistory ($paref); delete $paref->{reorg}; delete $paref->{reorgday}; } else { delete $data{$type}{$name}{pvhist}{$dday}; Log3 ($name, 3, qq{$name - Day "$dday" deleted from pvHistory}); } } else { delete $data{$type}{$name}{pvhist}; Log3 ($name, 3, qq{$name - all days deleted from pvHistory}); } return; } if ($prop eq 'consumption') { my $dday = $paref->{prop1} // ""; # ein bestimmter Tag der pvHistory angegeben ? my $dhour = $paref->{prop2} // ""; # eine bestimmte Stunde eines Tages der pvHistory angegeben ? if ($dday) { if ($dhour) { delete $data{$type}{$name}{pvhist}{$dday}{$dhour}{con}; Log3 ($name, 3, qq{$name - consumption day "$dday" hour "$dhour" deleted from pvHistory}); $paref->{reorg} = 1; # den Tag Stunde "99" reorganisieren $paref->{reorgday} = $dday; setPVhistory ($paref); delete $paref->{reorg}; delete $paref->{reorgday}; } else { for my $hr (sort keys %{$data{$type}{$name}{pvhist}{$dday}}) { delete $data{$type}{$name}{pvhist}{$dday}{$hr}{con}; } Log3 ($name, 3, qq{$name - consumption day "$dday" deleted from pvHistory}); } } else { for my $dy (sort keys %{$data{$type}{$name}{pvhist}}) { for my $hr (sort keys %{$data{$type}{$name}{pvhist}{$dy}}) { delete $data{$type}{$name}{pvhist}{$dy}{$hr}{con}; } } Log3 ($name, 3, qq{$name - all saved consumption deleted from pvHistory}); } 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}; delete $data{$type}{$name}{circular}{$circh}{quality}; delete $data{$type}{$name}{circular}{$circh}{pvrlsum}; delete $data{$type}{$name}{circular}{$circh}{pvfcsum}; delete $data{$type}{$name}{circular}{$circh}{dnumsum}; 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 of hour "$circh" from pvCircular and pvHistory deleted}); return; } for my $hod (keys %{$data{$type}{$name}{circular}}) { delete $data{$type}{$name}{circular}{$hod}{pvcorrf}; delete $data{$type}{$name}{circular}{$hod}{quality}; delete $data{$type}{$name}{circular}{$hod}{pvrlsum}; delete $data{$type}{$name}{circular}{$hod}{pvfcsum}; delete $data{$type}{$name}{circular}{$hod}{dnumsum}; } 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 from pvCircular and pvHistory deleted}); } return; } if ($prop eq 'aiData') { delete $data{$type}{$name}{current}{aiinitstate}; delete $data{$type}{$name}{current}{aitrainstate}; delete $data{$type}{$name}{current}{aiaddistate}; delete $data{$type}{$name}{current}{aigetresult}; my @ftd = ( $airaw.$name, $aitrained.$name ); for my $f (@ftd) { my $err = FileDelete($f); if ($err) { Log3 ($name, 1, qq{$name - Message while deleting file "$f": $err}); } } aiInit ($paref); return; } if ($prop eq 'powerTriggerSet') { deleteReadingspec ($hash, "powerTrigger.*"); writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben return; } if ($prop eq 'batteryTriggerSet') { deleteReadingspec ($hash, "batteryTrigger.*"); writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); return; } if ($prop eq 'energyH4TriggerSet') { deleteReadingspec ($hash, "energyH4Trigger.*"); writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); return; } if ($prop eq 'moduleRoofTopSet') { deleteReadingspec ($hash, "moduleRoofTops"); writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); return; } 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}); } else { delete $data{$type}{$name}{solcastapi}{'?IdPair'}; Log3($name, 3, qq{$name - roofIdentPair: all pair keys deleted}); } writeCacheToFile ($hash, 'solcastapi', $scpicache.$name); # Cache File SolCast API Werte schreiben return; } if ($prop eq 'currentMeterSet') { readingsDelete ($hash, "Current_GridConsumption"); readingsDelete ($hash, "Current_GridFeedIn"); readingsDelete ($hash, 'currentMeterDev'); delete $data{$type}{$name}{circular}{'99'}{initdayfeedin}; delete $data{$type}{$name}{circular}{'99'}{gridcontotal}; delete $data{$type}{$name}{circular}{'99'}{initdaygcon}; delete $data{$type}{$name}{circular}{'99'}{feedintotal}; delete $data{$type}{$name}{current}{gridconsumption}; delete $data{$type}{$name}{current}{tomorrowconsumption}; delete $data{$type}{$name}{current}{gridfeedin}; delete $data{$type}{$name}{current}{consumption}; delete $data{$type}{$name}{current}{autarkyrate}; delete $data{$type}{$name}{current}{selfconsumption}; delete $data{$type}{$name}{current}{selfconsumptionrate}; writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben } if ($prop eq 'currentBatterySet') { readingsDelete ($hash, 'Current_PowerBatIn'); readingsDelete ($hash, 'Current_PowerBatOut'); readingsDelete ($hash, 'Current_BatCharge'); readingsDelete ($hash, 'currentBatteryDev'); deleteReadingspec ($hash, 'Battery_.*'); undef @{$data{$type}{$name}{current}{socslidereg}}; delete $data{$type}{$name}{circular}{'99'}{lastTsMaxSocRchd}; delete $data{$type}{$name}{circular}{'99'}{nextTsMaxSocChge}; delete $data{$type}{$name}{circular}{'99'}{initdaybatintot}; delete $data{$type}{$name}{circular}{'99'}{initdaybatouttot}; delete $data{$type}{$name}{circular}{'99'}{batintot}; delete $data{$type}{$name}{circular}{'99'}{batouttot}; delete $data{$type}{$name}{current}{powerbatout}; delete $data{$type}{$name}{current}{powerbatin}; delete $data{$type}{$name}{current}{batcharge}; writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben } if ($prop eq 'currentInverterSet') { undef @{$data{$type}{$name}{current}{genslidereg}}; readingsDelete ($hash, "Current_PV"); readingsDelete ($hash, "currentInverterDev"); deleteReadingspec ($hash, ".*_PVreal" ); writeCacheToFile ($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); } } writeCacheToFile ($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}; Log3($name, 3, qq{$name - Consumer "$calias" deleted from memory}); } else { for my $cs (keys %{$data{$type}{$name}{consumers}}) { my $calias = ConsumerVal ($hash, $cs, "alias", ""); delete $data{$type}{$name}{consumers}{$cs}; Log3($name, 3, qq{$name - Consumer "$calias" deleted from memory}); } } writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben } createAssociatedWith ($hash); return; } ################################################################ # Setter operatingMemory # (Ersatz für Setter writeHistory) ################################################################ sub _setoperatingMemory { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $prop = $paref->{prop} // return qq{no operation specified for command}; if ($prop eq 'save') { periodicWriteMemcache ($hash); # Cache File für PV History, PV Circular schreiben } if ($prop eq 'backup') { periodicWriteMemcache ($hash, 'bckp'); # Backup Files erstellen und alte Versionen löschen } if ($prop =~ /^recover-/xs) { # Sicherung wiederherstellen my $file = (split "-", $prop)[1]; Log3 ($name, 3, "$name - recover saved cache file: $file"); if ($file =~ /^PVH_/xs) { # Cache File PV History einlesen $paref->{cachename} = 'pvhist'; $paref->{title} = 'pvHistory'; } if ($file =~ /^PVC_/xs) { # Cache File PV Circular einlesen $paref->{cachename} = 'circular'; $paref->{title} = 'pvCircular'; } $paref->{file} = "$cachedir/$file"; _readCacheFile ($paref); delete $paref->{file}; delete $paref->{cachename}; delete $paref->{title}; } return; } ################################################################ # Setter clientAction # ohne Menüeintrag ! für Aktivität aus Grafik ################################################################ sub _setclientAction { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; my $argsref = $paref->{argsref}; if (!$arg) { return qq{The command "$opt" needs an argument !}; } my @args = @{$argsref}; my $c = shift @args; # Consumer Index (Nummer) my $evt = shift @args; # Readings Event (state wird 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 Log3 ($name, 4, qq{$name - Client Action received / execute: "$action $cname $tail"}); if ($action eq 'set') { CommandSet (undef, "$cname $tail"); my $async = ConsumerVal ($hash, $c, 'asynchron', 0); centralTask ($hash, $evt) if(!$async); # nur wenn Consumer synchron arbeitet direkte Statusabfrage, sonst via Notify return; } 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; } ################################################################ # Setter aiDecTree ################################################################ sub _setaiDecTree { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $prop = $paref->{prop} // return; if ($prop eq 'addInstances') { aiAddInstance ($paref); } if ($prop eq 'addRawData') { aiAddRawData ($paref); } if ($prop eq 'train') { manageTrain ($paref); } return; } ############################################################### # SolarForecast Get ############################################################### sub Get { my ($hash, @a) = @_; return "\"get X\" needs at least an argument" if ( @a < 2 ); 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 $type = $hash->{TYPE}; my @ho = qw (both both_noHead both_noCons both_noHead_noCons flow flow_noHead flow_noCons flow_noHead_noCons forecast forecast_noHead forecast_noCons forecast_noHead_noCons none ); my @pha = map {sprintf "%02d", $_} sort {$a<=>$b} keys %{$data{$type}{$name}{pvhist}}; my @vcm = map {sprintf "%02d", $_} sort {$a<=>$b} keys %{$data{$type}{$name}{consumers}}; my $hol = join ",", @ho; my $pvl = join ",", @pha; my $cml = join ",", @vcm; my $getlist = "Unknown argument $opt, choose one of ". "valConsumerMaster:#,$cml ". "data:noArg ". "dwdCatalog ". "forecastQualities:noArg ". "ftuiFramefiles:noArg ". "html:$hol ". "nextHours:noArg ". "pvCircular:noArg ". "pvHistory:#,$pvl ". "rooftopData:noArg ". "solApiData:noArg ". "valCurrent:noArg " ; ## KI spezifische Getter ########################## if (isPrepared4AI ($hash)) { $getlist .= "valDecTree:aiRawData,aiRuleStrings "; } my (undef, $disabled, $inactive) = controller ($name); return if($disabled || $inactive); my $params = { hash => $hash, name => $name, type => $type, opt => $opt, arg => $arg, t => int time, date => (strftime "%Y-%m-%d", localtime(time)), debug => getDebug ($hash), lang => getLang ($hash) }; if($hget{$opt} && defined &{$hget{$opt}{fn}}) { my $ret = q{}; if (!$hash->{CREDENTIALS} && $hget{$opt}{needcred}) { return qq{Credentials for "$opt" are not set. Please save the the credentials with the appropriate Set command."}; } $params->{force} = 1 if($opt eq 'rooftopData'); # forcierter (manueller) Abruf SolCast API $ret = &{$hget{$opt}{fn}} ($params); return $ret; } return $getlist; } ################################################################ # Getter roofTop data ################################################################ sub _getRoofTopData { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; delete $data{$type}{$name}{current}{dwdRad1hAge}; delete $data{$type}{$name}{current}{dwdRad1hAgeTS}; if($hash->{MODEL} eq 'SolCastAPI') { __getSolCastData ($paref); return; } elsif ($hash->{MODEL} eq 'ForecastSolarAPI') { __getForecastSolarData ($paref); return; } elsif ($hash->{MODEL} eq 'DWD') { my $ret = __getDWDSolarData ($paref); return $ret; } elsif ($hash->{MODEL} eq 'VictronKiAPI') { my $ret = __getVictronSolarData ($paref); return $ret; } elsif ($hash->{MODEL} =~ /^OpenMeteo/xs) { my $ret = __getopenMeteoData ($paref); return $ret; } return "$name is not a valid SolarForeCast Model: ".$hash->{MODEL}; } ################################################################ # Abruf SolCast roofTop data ################################################################ sub __getSolCastData { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $force = $paref->{force} // 0; my $t = $paref->{t} // time; my $debug = $paref->{debug}; my $lang = $paref->{lang}; my $msg; if($ctzAbsent) { $msg = qq{The library FHEM::Utility::CTZ is missing. Please update FHEM completely.}; Log3 ($name, 1, "$name - ERROR - $msg"); return $msg; } my $rmf = reqModFail(); if($rmf) { $msg = "You have to install the required perl module: ".$rmf; Log3 ($name, 1, "$name - ERROR - $msg"); return $msg; } ## statische SolCast API Kennzahlen ## (solCastAPIcallMultiplier, todayMaxAPIcalls) berechnen ########################################################## my %mx; my $maxcnt = 1; my $type = $paref->{type}; for my $pk (keys %{$data{$type}{$name}{solcastapi}{'?IdPair'}}) { my $apikey = SolCastAPIVal ($hash, '?IdPair', $pk, 'apikey', ''); next if(!$apikey); $mx{$apikey} += 1; $maxcnt = $mx{$apikey} if(!$maxcnt || $mx{$apikey} > $maxcnt); } my $apimaxreq = AttrVal ($name, 'ctrlSolCastAPImaxReq', $solcmaxreqdef); my $madc = sprintf "%.0f", ($apimaxreq / $maxcnt); # max. tägliche Anzahl API Calls my $mpk = $maxcnt; # Requestmultiplikator $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{solCastAPIcallMultiplier} = $mpk; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayMaxAPIcalls} = $madc; ######################### if (!$force) { # regulärer SolCast API Abruf my $trc = SolCastAPIVal ($hash, '?All', '?All', 'todayRemainingAPIcalls', $madc); my $etxt = $hqtxt{bnsas}{$lang}; $etxt =~ s{}{($leadtime/60)}eg; if ($trc <= 0) { readingsSingleUpdate ($hash, 'nextRadiationAPICall', $etxt, 1); return qq{SolCast free daily limit is used up}; } my $date = $paref->{date}; 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 - $leadtime || $t > $sstime + $lagtime) { readingsSingleUpdate($hash, 'nextRadiationAPICall', $etxt, 1); return "The current time is not between sunrise minus ".($leadtime/60)." minutes and sunset"; } my $lrt = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_timestamp', 0); my $apiitv = SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $solapirepdef); 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}; } } if ($debug =~ /apiCall/x) { Log3 ($name, 1, "$name DEBUG> SolCast API Call - max possible daily API requests: $apimaxreq"); Log3 ($name, 1, "$name DEBUG> SolCast API Call - Requestmultiplier: $mpk"); Log3 ($name, 1, "$name DEBUG> SolCast API Call - possible daily API Calls: $madc"); } $paref->{allstrings} = ReadingsVal ($name, 'inverterStrings', ''); $paref->{firstreq} = 1; # 1. Request, V 0.80.18 __solCast_ApiRequest ($paref); return; } ################################################################################################ # SolCast Api Request # # 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=.... # ################################################################################################ sub __solCast_ApiRequest { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $allstrings = $paref->{allstrings}; # alle Strings my $debug = $paref->{debug}; if (!$allstrings) { # alle Strings wurden abgerufen writeCacheToFile ($hash, 'solcastapi', $scpicache.$name); # Cache File SolCast API Werte schreiben return; } 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', ''); 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". "&hours=72". # Forum:#134226 -> Abruf 72h statt 48h "&api_key=". $apikey; debugLog ($paref, "apiProcess|apiCall", qq{Request SolCast API for PV-String "$string": $url}); my $caller = (caller(0))[3]; # Rücksprungmarke my $param = { url => $url, timeout => 30, hash => $hash, name => $name, type => $paref->{type}, debug => $debug, caller => \&$caller, stc => [gettimeofday], allstrings => $allstrings, string => $string, lang => $paref->{lang}, firstreq => $paref->{firstreq}, method => "GET", callback => \&__solCast_ApiResponse }; if($debug =~ /apiCall/x) { $param->{loglevel} = 1; } HttpUtils_NonblockingGet ($param); return; } ############################################################### # SolCast Api Response ############################################################### sub __solCast_ApiResponse { my $paref = shift; my $err = shift; my $myjson = shift; my $hash = $paref->{hash}; my $name = $paref->{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 $debug = $paref->{debug}; my $type = $paref->{type}; $paref->{t} = time; my $msg; my $sta = [gettimeofday]; # Start Response Verarbeitung if ($err ne "") { $msg = 'SolCast API server response: '.$err; Log3 ($name, 1, "$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'; Log3 ($name, 1, "$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); debugLog ($paref, "apiProcess", qq{SolCast API server response for string "$string":\n}. Dumper $jdata); ## bei Überschreitung Limit kommt: #################################### # 'response_status' => { # 'message' => 'You have exceeded your free daily limit.', # 'errors' => [], # 'error_code' => 'TooManyRequests' # } if (defined $jdata->{'response_status'}) { $msg = 'SolCast API server response: '.$jdata->{'response_status'}{'message'}; Log3 ($name, 3, "$name - $msg"); ___setSolCastAPIcallKeyData ($paref); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $jdata->{'response_status'}{'message'}; if ($jdata->{'response_status'}{'error_code'} eq 'TooManyRequests') { $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayRemainingAPIrequests} = 0; } 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 if($debug =~ /apiProcess|apiCall/x) { my $apimaxreq = AttrVal ($name, 'ctrlSolCastAPImaxReq', $solcmaxreqdef); Log3 ($name, 1, "$name DEBUG> SolCast API Call - response status: ".$jdata->{'response_status'}{'message'}); Log3 ($name, 1, "$name DEBUG> SolCast API Call - todayRemainingAPIrequests: ".SolCastAPIVal($hash, '?All', '?All', 'todayRemainingAPIrequests', $apimaxreq)); } return; } my ($period,$starttmstr); my $perc = AttrVal ($name, 'affectSolCastPercentile', 50); # das gewählte zu nutzende Percentil debugLog ($paref, "apiProcess", qq{SolCast API used percentile: }. $perc); $perc = q{} if($perc == 50); my $k = 0; 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, $lang, $petstr); if ($err) { Log3 ($name, 1, "$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 $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); $k++; next; } delete $data{$type}{$name}{solcastapi}{$string}{$starttmstr}; $k++; } $k = 0; while ($jdata->{'forecasts'}[$k]) { if (!$jdata->{'forecasts'}[$k]{'pv_estimate'.$perc}) { # keine PV Prognose -> Datensatz überspringen -> Verarbeitungszeit sparen $k++; next; } my $petstr = $jdata->{'forecasts'}[$k]{'period_end'}; ($err, $starttmstr) = ___convPendToPstart ($name, $lang, $petstr); my $pvest50 = $jdata->{'forecasts'}[$k]{'pv_estimate'.$perc}; $period = $jdata->{'forecasts'}[$k]{'period'}; $period =~ s/.*(\d\d).*/$1/; if ($debug =~ /apiProcess/x) { # nur für Debugging if (exists $data{$type}{$name}{solcastapi}{$string}{$starttmstr}) { Log3 ($name, 1, qq{$name DEBUG> SolCast API Hash - Start Date/Time: }. $starttmstr); Log3 ($name, 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++; } } Log3 ($name, 4, qq{$name - SolCast API answer received for string "$string"}); ___setSolCastAPIcallKeyData ($paref); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = 'success'; my $param = { hash => $hash, name => $name, type => $type, debug => $debug, 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' ############################################################### sub ___convPendToPstart { my $name = shift; my $lang = shift; my $petstr = shift; my $cpar = { name => $name, pattern => '%Y-%m-%dT%H:%M:%S', dtstring => $petstr, tzcurrent => 'UTC', 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, $lang))[1]; ($cdatest) = split " ", $nt; $chrst = 23; } } my $starttmstr = $cdatest." ".(sprintf "%02d", $chrst).":00:00"; # Startzeit von pv_estimate return ($err, $starttmstr); } ################################################################ # Kennzahlen des letzten Abruf SolCast API setzen # $t - Unix Timestamp ################################################################ sub ___setSolCastAPIcallKeyData { my $paref = shift; my $hash = $paref->{hash}; my $lang = $paref->{lang}; my $debug = $paref->{debug}; my $t = $paref->{t} // time; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; $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 my $apimaxreq = AttrVal ($name, 'ctrlSolCastAPImaxReq', $solcmaxreqdef); my $mpl = SolCastAPIVal ($hash, '?All', '?All', 'solCastAPIcallMultiplier', 1); my $ddc = SolCastAPIVal ($hash, '?All', '?All', 'todayDoneAPIcalls', 0); $ddc += 1 if($paref->{firstreq}); my $drc = SolCastAPIVal ($hash, '?All', '?All', 'todayMaxAPIcalls', $apimaxreq / $mpl) - $ddc; # verbleibende SolCast API Calls am aktuellen Tag $drc = 0 if($drc < 0); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIrequests} = $ddc * $mpl; my $drr = $apimaxreq - ($mpl * $ddc); $drr = 0 if($drr < 0); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayRemainingAPIrequests} = $drr; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayRemainingAPIcalls} = $drc; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIcalls} = $ddc; debugLog ($paref, "apiProcess|apiCall", "SolCast API Call - done API Calls: $ddc"); ## Berechnung des optimalen Request Intervalls ################################################ if (AttrVal($name, 'ctrlSolCastAPIoptimizeReq', 0)) { my $date = strftime "%Y-%m-%d", localtime($t); my $sunset = $date.' '.ReadingsVal ($name, "Today_SunSet", '00:00').':00'; my $sstime = timestringToTimestamp ($sunset); my $dart = $sstime - $t; # verbleibende Sekunden bis Sonnenuntergang $dart = 0 if($dart < 0); $drc += 1; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = $solapirepdef; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = int ($dart / $drc) if($dart && $drc); debugLog ($paref, "apiProcess|apiCall", "SolCast API Call - Sunset: $sunset, remain Sec to Sunset: $dart, new interval: ".SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $solapirepdef)); } else { $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = $solapirepdef; } #### my $apiitv = SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $solapirepdef); if($debug =~ /apiProcess|apiCall/x) { Log3 ($name, 1, "$name DEBUG> SolCast API Call - remaining API Calls: ".($drc - 1)); Log3 ($name, 1, "$name DEBUG> SolCast API Call - next API Call: ".(timestampToTimestring ($t + $apiitv, $lang))[0]); } readingsSingleUpdate ($hash, 'nextRadiationAPICall', $hqtxt{after}{$lang}.' '.(timestampToTimestring ($t + $apiitv, $lang))[0], 1); return; } ################################################################ # Abruf ForecastSolar-API data ################################################################ sub __getForecastSolarData { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $force = $paref->{force} // 0; my $t = $paref->{t} // time; my $lang = $paref->{lang}; if (!$force) { # regulärer API Abruf my $etxt = $hqtxt{bnsas}{$lang}; $etxt =~ s{}{($leadtime/60)}eg; my $date = strftime "%Y-%m-%d", localtime($t); 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 - $leadtime || $t > $sstime + $lagtime) { readingsSingleUpdate ($hash, 'nextRadiationAPICall', $etxt, 1); return "The current time is not between sunrise minus ".($leadtime/60)." minutes and sunset"; } my $lrt = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_timestamp', 0); my $apiitv = SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $forapirepdef); 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}; } } $paref->{allstrings} = ReadingsVal($name, 'inverterStrings', ''); __forecastSolar_ApiRequest ($paref); return; } ################################################################################################ # ForecastSolar Api Request # # Quelle Seite: https://doc.forecast.solar/api:estimate # Aufruf: https://api.forecast.solar/estimate/:lat/:lon/:dec/:az/:kwp # Beispiel: https://api.forecast.solar/estimate/51.285272/12.067722/45/S/5.13 # # Locate Check: https://api.forecast.solar/check/:lat/:lon # Docku: https://doc.forecast.solar/api # # :!: Please note that the forecasts are updated at the earliest every 15 min. # due to the weather data used, so it makes no sense to query more often than every 15 min.! # # :!: If you get an 404 Page not found please always double check your URL. # The API ist very strict configured to reject maleformed queries as early as possible to # minimize server load! # # :!: Each quarter (1st of month around midnight UTC) there is a scheduled maintenance planned. # You will get then a HTTP code 503 as response. # ################################################################################################ sub __forecastSolar_ApiRequest { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $allstrings = $paref->{allstrings}; # alle Strings my $debug = $paref->{debug}; if (!$allstrings) { # alle Strings wurden abgerufen writeCacheToFile ($hash, 'solcastapi', $scpicache.$name); # Cache File API Werte schreiben $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIcalls} += 1; return; } my $string; ($string, $allstrings) = split ",", $allstrings, 2; my ($set, $lat, $lon) = locCoordinates(); if (!$set) { my $err = qq{the attribute 'latitude' and/or 'longitude' in global device is not set}; singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); return $err; } my $tilt = StringVal ($hash, $string, 'tilt', ''); my $az = StringVal ($hash, $string, 'azimut', ''); my $peak = StringVal ($hash, $string, 'peak', ''); my $url = "https://api.forecast.solar/estimate/watthours/period/". $lat."/". $lon."/". $tilt."/". $az."/". $peak; debugLog ($paref, "apiCall", qq{ForecastSolar API Call - Request for PV-String "$string":\n$url}); my $caller = (caller(0))[3]; # Rücksprungmarke my $param = { url => $url, timeout => 30, hash => $hash, name => $name, type => $type, debug => $debug, header => 'Accept: application/json', caller => \&$caller, stc => [gettimeofday], allstrings => $allstrings, string => $string, lang => $paref->{lang}, method => "GET", callback => \&__forecastSolar_ApiResponse }; if ($debug =~ /apiCall/x) { $param->{loglevel} = 1; } HttpUtils_NonblockingGet ($param); return; } ############################################################### # ForecastSolar API Response ############################################################### sub __forecastSolar_ApiResponse { my $paref = shift; my $err = shift; my $myjson = shift; my $hash = $paref->{hash}; my $name = $paref->{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 $debug = $paref->{debug}; my $type = $paref->{type}; my $t = time; $paref->{t} = $t; my $msg; my $sta = [gettimeofday]; # Start Response Verarbeitung if ($err ne "") { $msg = 'ForecastSolar API server response: '.$err; Log3 ($name, 1, "$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 ForecastSolar API server response'; Log3 ($name, 1, "$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); debugLog ($paref, "apiProcess", qq{ForecastSolar API Call - response for string "$string":\n}. Dumper $jdata); ## bei Überschreitung des Stundenlimit kommt: ############################################### # message -> code 429 (sonst 0) # message -> type error (sonst 'success') # message -> text Rate limit for API calls reached. (sonst leer) # message -> ratelimit -> period 3600 # -> limit 12 # -> retry-at 2023-05-27T11:01:53+02:00 (= lokale Zeit) if ($jdata->{'message'}{'code'}) { $msg = "ForecastSolar API server ERROR response: $jdata->{'message'}{'text'} ($jdata->{'message'}{'code'})"; Log3 ($name, 3, "$name - $msg"); singleUpdateState ( {hash => $hash, state => $msg, evt => 1} ); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $jdata->{'message'}{'text'}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_code} = $jdata->{'message'}{'code'}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_timestamp} = $t; if (defined $jdata->{'message'}{'ratelimit'}{'remaining'}) { $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{requests_remaining} = $jdata->{'message'}{'ratelimit'}{'remaining'}; # verbleibende Requests in Periode } else { delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{requests_remaining}; # verbleibende Requests unbestimmt } if($debug =~ /apiCall/x) { Log3 ($name, 1, "$name DEBUG> ForecastSolar API Call - $msg"); Log3 ($name, 1, "$name DEBUG> ForecastSolar API Call - limit period: ".$jdata->{'message'}{'ratelimit'}{'period'}); Log3 ($name, 1, "$name DEBUG> ForecastSolar API Call - limit: ".$jdata->{'message'}{'ratelimit'}{'limit'}); } my $rtyat = timestringFormat ($jdata->{'message'}{'ratelimit'}{'retry-at'}); if ($rtyat) { my $rtyatts = timestringToTimestamp ($rtyat); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{retryat_time} = $rtyat; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{retryat_timestamp} = $rtyatts; debugLog ($paref, "apiCall", "ForecastSolar API Call - retry at: ".$rtyat." ($rtyatts)"); } $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 ___setForeCastAPIcallKeyData ($paref); return; } my $rt = timestringFormat ($jdata->{'message'}{'info'}{'time'}); my $rts = timestringToTimestamp ($rt); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_time} = $rt; # letzte Abrufzeit $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_timestamp} = $rts; # letzter Abrufzeitstempel $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $jdata->{'message'}{'type'}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_code} = $jdata->{'message'}{'code'}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{requests_remaining} = $jdata->{'message'}{'ratelimit'}{'remaining'}; # verbleibende Requests in Periode $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{requests_limit_period} = $jdata->{'message'}{'ratelimit'}{'period'}; # Requests Limit Periode $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{requests_limit} = $jdata->{'message'}{'ratelimit'}{'limit'}; # Requests Limit in Periode $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{place} = encode ("utf8", $jdata->{'message'}{'info'}{'place'}); if ($debug =~ /apiCall/x) { Log3 ($name, 1, qq{$name DEBUG> ForecastSolar API Call - server response for PV string "$string"}); Log3 ($name, 1, "$name DEBUG> ForecastSolar API Call - request time: ". $rt." ($rts)"); Log3 ($name, 1, "$name DEBUG> ForecastSolar API Call - requests remaining: ".$jdata->{'message'}{'ratelimit'}{'remaining'}); Log3 ($name, 1, "$name DEBUG> ForecastSolar API Call - status: ". $jdata->{'message'}{'type'}." ($jdata->{'message'}{'code'})"); } for my $k (sort keys %{$jdata->{'result'}}) { # Vorhersagedaten in Hash eintragen my $kts = (timestringToTimestamp ($k)) - 3600; # Endezeit der Periode auf Startzeit umrechnen my $starttmstr = (timestampToTimestring ($kts, $lang))[3]; $data{$type}{$name}{solcastapi}{$string}{$starttmstr}{pv_estimate50} = $jdata->{'result'}{$k}; debugLog ($paref, "apiProcess", "ForecastSolar API Call - PV estimate: ".$starttmstr.' => '.$jdata->{'result'}{$k}.' Wh'); } } Log3 ($name, 4, qq{$name - ForecastSolar API answer received for string "$string"}); ___setForeCastAPIcallKeyData ($paref); my $param = { hash => $hash, name => $name, type => $type, debug => $debug, 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); } ################################################################ # Kennzahlen des letzten Abruf ForecastSolar API setzen ################################################################ sub ___setForeCastAPIcallKeyData { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $lang = $paref->{lang}; my $debug = $paref->{debug}; my $t = $paref->{t} // time; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIrequests} += 1; ## Berechnung des optimalen Request Intervalls ################################################ my $snum = scalar (split ",", ReadingsVal($name, 'inverterStrings', 'Dummy')); # Anzahl der Strings (mindestens ein String als Dummy) my $period = SolCastAPIVal ($hash, '?All', '?All', 'requests_limit_period', 3600); # Requests Limit Periode my $limit = SolCastAPIVal ($hash, '?All', '?All', 'requests_limit', 12); # Request Limit in Periode $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = $forapirepdef; my $interval = int ($period / ($limit / $snum)); $interval = 900 if($interval < 900); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = $interval; #### my $apiitv = SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $forapirepdef); my $rtyatts = SolCastAPIVal ($hash, '?All', '?All', 'retryat_timestamp', 0); my $smt = q{}; if ($rtyatts && $rtyatts > $t) { # Zwangswartezeit durch API berücksichtigen $apiitv = $rtyatts - $t; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = $apiitv; $smt = '(forced waiting time)'; } readingsSingleUpdate ($hash, 'nextRadiationAPICall', $hqtxt{after}{$lang}.' '.(timestampToTimestring ($t + $apiitv, $lang))[0].' '.$smt, 1); return; } ################################################################################################## # Abruf DWD Strahlungsdaten und Rohdaten ohne Korrektur # speichern in solcastapi Hash # # !!!! NACHFOLGENDE INFO GILT NUR BEI DWD RAD1H VERWENDUNG !!!! # ############################################################# # # PV Forecast Rad1h in kWh / Wh # Berechnung nach Formel 1 aus http://www.ing-büro-junge.de/html/photovoltaik.html: # # * Faktor für Umwandlung kJ in kWh: 0.00027778 # * Eigene Modulfläche in qm z.B.: 31,04 # * Wirkungsgrad der Module in % z.B.: 16,52 # * Wirkungsgrad WR in % z.B.: 98,3 # * Korrekturwerte wegen Ausrichtung/Verschattung etc. # # 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: # # * Globalstrahlung: G = kWh/m2 (DWD Rad1h = kJ/m2) # * Korrektur mit Flächenfaktor f: Gk = G * f # * Globalstrahlung (STC): 1 kW/m2 # * Peak Leistung String (kWp): Pnenn = x kW # * Performance Ratio: PR (typisch 0,85 bis 0,9) # * weitere Korrekturwerte für Regen, Wolken etc.: Korr # # 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: # 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 __getDWDSolarData { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $date = $paref->{date}; # aktueller Tag "YYYY-MM-DD" my $t = $paref->{t} // time; my $lang = $paref->{lang}; my $type = $hash->{TYPE}; my $raname = ReadingsVal ($name, "currentRadiationAPI", ""); # Radiation Forecast API return if(!$raname || !$defs{$raname}); my $stime = $date.' 00:00:00'; # Startzeit Soll Übernahmedaten my $sts = timestringToTimestamp ($stime); my @strings = sort keys %{$data{$type}{$name}{strings}}; my $ret = q{}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_timestamp} = $t; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIrequests} += 1; my $fctime = ReadingsVal ($raname, 'fc_time', '-'); $data{$type}{$name}{current}{dwdRad1hDev} = $raname; $data{$type}{$name}{current}{dwdRad1hAge} = $fctime; $data{$type}{$name}{current}{dwdRad1hAgeTS} = timestringToTimestamp ($fctime); debugLog ($paref, "apiCall", "DWD API - collect DWD Radiation data with start >$stime<- device: $raname =>"); for my $num (0..47) { my $dateTime = strftime "%Y-%m-%d %H:%M:00", localtime($sts + (3600 * $num)); # laufendes Datum ' ' Zeit my $runh = int strftime "%H", localtime($sts + (3600 * $num) + 3600); # Stunde in 24h format (00-23), Rad1h = Absolute Globalstrahlung letzte 1 Stunde my ($fd,$fh) = calcDayHourMove (0, $num); next if($fh == 24); my $rad = ReadingsVal ($raname, "fc${fd}_${runh}_Rad1h", '0.00'); # kJ/m2 if ($runh == 12 && !$rad) { $ret = "The reading 'fc${fd}_${runh}_Rad1h' does not appear to be present or has an unusual value.\nRun 'set $name plantConfiguration check' for further information."; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $ret; debugLog ($paref, "apiCall", "DWD API - ERROR - got unusual data of starttime: $dateTime. ".$ret); } else { debugLog ($paref, "apiCall", "DWD API - got data -> starttime: $dateTime, reading: fc${fd}_${runh}_Rad1h, rad: $rad kJ/m2"); } $data{$type}{$name}{solcastapi}{'?All'}{$dateTime}{Rad1h} = sprintf "%.0f", $rad; for my $string (@strings) { # für jeden String der Config .. my $peak = $data{$type}{$name}{strings}{$string}{peak}; # String Peak (kWp) $peak *= 1000; # kWp in Wp umrechnen my $ta = $data{$type}{$name}{strings}{$string}{tilt}; # Neigungswinkel Solarmodule my $dir = $data{$type}{$name}{strings}{$string}{dir}; # Ausrichtung der Solarmodule my $af = $hff{$ta}{$dir} / 100; # Flächenfaktor: http://www.ing-büro-junge.de/html/photovoltaik.html my $pv = sprintf "%.1f", ($rad * $af * $kJtokWh * $peak * $prdef); # Rad wird in kW/m2 erwartet debugLog ($paref, "apiProcess", "DWD API - PV estimate String >$string< => $pv Wh"); $data{$type}{$name}{solcastapi}{$string}{$dateTime}{pv_estimate50} = $pv; # Startzeit wird verwendet, nicht laufende Stunde } } $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = 'success' if(!$ret); return; } ################################################################ # Abruf Victron VRM API Forecast ################################################################ sub __getVictronSolarData { my $paref = shift; my $hash = $paref->{hash}; my $force = $paref->{force} // 0; my $t = $paref->{t} // time; my $lang = $paref->{lang}; my $lrt = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_timestamp', 0); my $apiitv = $vrmapirepdef; if (!$force) { 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}; } } readingsSingleUpdate ($hash, 'nextRadiationAPICall', $hqtxt{after}{$lang}.' '.(timestampToTimestring ($t + $apiitv, $lang))[0], 1); __VictronVRM_ApiRequestLogin ($paref); return; } ################################################################ # Victron VRM API Login # https://vrm-api-docs.victronenergy.com/#/ ################################################################ sub __VictronVRM_ApiRequestLogin { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $debug = $paref->{debug}; my $type = $paref->{type}; my $url = 'https://vrmapi.victronenergy.com/v2/auth/login'; debugLog ($paref, "apiProcess|apiCall", qq{Request VictronVRM API Login: $url}); my $caller = (caller(0))[3]; # Rücksprungmarke my ($user, $pwd, $idsite); my $serial = SolCastAPIVal ($hash, '?VRM', '?API', 'credentials', ''); if ($serial) { my $h = eval { thaw (assemble ($serial)) }; # Deserialisierung $user = $h->{user} // q{}; $pwd = $h->{pwd} // q{}; $idsite = $h->{idsite} // q{}; debugLog ($paref, "apiCall", qq{Used credentials for Login: user->$user, pwd->$pwd, idsite->$idsite}); } else { my $msg = "Victron VRM API credentials are not set or couldn't be decrypted. Use 'set $name vrmCredentials' to set it."; Log3 ($name, 2, "$name - $msg"); singleUpdateState ( {hash => $hash, state => $msg, evt => 1} ); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $msg; return; } my $param = { url => $url, timeout => 30, hash => $hash, name => $name, type => $paref->{type}, stc => [gettimeofday], debug => $debug, caller => \&$caller, lang => $paref->{lang}, idsite => $idsite, header => { "Content-Type" => "application/json" }, data => qq({ "username": "$user", "password": "$pwd" }), method => 'POST', callback => \&__VictronVRM_ApiResponseLogin }; if($debug =~ /apiCall/x) { $param->{loglevel} = 1; } HttpUtils_NonblockingGet ($param); return; } ############################################################### # Victron VRM API Login Response ############################################################### sub __VictronVRM_ApiResponseLogin { my $paref = shift; my $err = shift; my $myjson = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $caller = $paref->{caller}; my $stc = $paref->{stc}; my $lang = $paref->{lang}; my $debug = $paref->{debug}; my $msg; my $t = time; my $sta = [gettimeofday]; # Start Response Verarbeitung if ($err ne "") { $msg = 'Victron VRM API error response: '.$err; Log3 ($name, 1, "$name - $msg"); singleUpdateState ( {hash => $hash, state => $msg, evt => 1} ); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $err; $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 Victron VRM API response'; Log3 ($name, 1, "$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); if (defined $jdata->{'error_code'}) { $msg = 'Victron VRM API error_code response: '.$jdata->{'error_code'}; Log3 ($name, 3, "$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 $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $jdata->{'error_code'}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_timestamp} = $t; if($debug =~ /apiProcess|apiCall/x) { Log3 ($name, 1, "$name DEBUG> SolCast API Call - error_code: ".$jdata->{'error_code'}); Log3 ($name, 1, "$name DEBUG> SolCast API Call - errors: " .$jdata->{'errors'}); } return; } else { $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = 'success'; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{idUser} = $jdata->{'idUser'}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{verification_mode} = $jdata->{'verification_mode'}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_timestamp} = $t; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{token} = 'got successful at '.SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_time', '-');; debugLog ($paref, "apiProcess", qq{Victron VRM API response Login:\n}. Dumper $jdata); $paref->{token} = $jdata->{'token'}; __VictronVRM_ApiRequestForecast ($paref); } } return; } ################################################################################################ # Victron VRM API Forecast Data # https://vrm-api-docs.victronenergy.com/#/ # https://vrmapi.victronenergy.com/v2/installations//stats?type=forecast&interval=hours&start=&end= ################################################################################################ sub __VictronVRM_ApiRequestForecast { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $token = $paref->{token}; my $debug = $paref->{debug}; my $lang = $paref->{lang}; my $idsite = $paref->{idsite}; my $tstart = time; my $tend = time + 259200; # 172800 = 2 Tage my $url = "https://vrmapi.victronenergy.com/v2/installations/$idsite/stats?type=forecast&interval=hours&start=$tstart&end=$tend"; debugLog ($paref, "apiProcess|apiCall", qq{Request VictronVRM API Forecast: $url}); my $caller = (caller(0))[3]; # Rücksprungmarke my $param = { url => $url, timeout => 30, hash => $hash, name => $name, type => $paref->{type}, stc => [gettimeofday], debug => $debug, token => $token, caller => \&$caller, lang => $paref->{lang}, header => { "Content-Type" => "application/json", "x-authorization" => "Bearer $token" }, method => 'GET', callback => \&__VictronVRM_ApiResponseForecast }; if($debug =~ /apiCall/x) { $param->{loglevel} = 1; } HttpUtils_NonblockingGet ($param); return; } ############################################################### # Victron VRM API Forecast Response ############################################################### sub __VictronVRM_ApiResponseForecast { my $paref = shift; my $err = shift; my $myjson = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $caller = $paref->{caller}; my $stc = $paref->{stc}; my $lang = $paref->{lang}; my $debug = $paref->{debug}; my $msg; my $t = time; my $sta = [gettimeofday]; # Start Response Verarbeitung if ($err ne "") { $msg = 'Victron VRM API Forecast response: '.$err; Log3 ($name, 1, "$name - $msg"); singleUpdateState ( {hash => $hash, state => $msg, evt => 1} ); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $err; $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 Victron VRM API Forecast response'; Log3 ($name, 1, "$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); if (defined $jdata->{'error_code'}) { $msg = 'Victron VRM API Forecast response: '.$jdata->{'error_code'}; Log3 ($name, 3, "$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 $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $jdata->{'error_code'}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_timestamp} = $t; if($debug =~ /apiProcess|apiCall/x) { Log3 ($name, 1, "$name DEBUG> SolCast API Call - error_code: ".$jdata->{'error_code'}); Log3 ($name, 1, "$name DEBUG> SolCast API Call - errors: " .$jdata->{'errors'}); } return; } else { $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIrequests} += 1; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIcalls} += 1; my $k = 0; while ($jdata->{'records'}{'solar_yield_forecast'}[$k]) { if (ref $jdata->{'records'}{'solar_yield_forecast'}[$k] ne "ARRAY") { # Forum: https://forum.fhem.de/index.php?msg=1288637 $k++; next; } my $starttmstr = $jdata->{'records'}{'solar_yield_forecast'}[$k][0]; # Millisekunden geliefert my $val = $jdata->{'records'}{'solar_yield_forecast'}[$k][1]; $starttmstr = (timestampToTimestring ($starttmstr, $lang))[3]; debugLog ($paref, "apiProcess", "Victron VRM API - PV estimate: ".$starttmstr.' => '.$val.' Wh'); if ($val) { $val = sprintf "%.0f", $val; my $string = ReadingsVal ($name, 'inverterStrings', '?'); $data{$type}{$name}{solcastapi}{$string}{$starttmstr}{pv_estimate50} = $val; } $k++; } $k = 0; while ($jdata->{'records'}{'vrm_consumption_fc'}[$k]) { if (ref $jdata->{'records'}{'vrm_consumption_fc'}[$k] ne "ARRAY") { # Forum: https://forum.fhem.de/index.php?msg=1288637 $k++; next; } my $starttmstr = $jdata->{'records'}{'vrm_consumption_fc'}[$k][0]; # Millisekunden geliefert my $val = $jdata->{'records'}{'vrm_consumption_fc'}[$k][1]; $starttmstr = (timestampToTimestring ($starttmstr, $lang))[3]; if ($val) { $val = sprintf "%.2f", $val; my $string = ReadingsVal ($name, 'inverterStrings', '?'); $data{$type}{$name}{solcastapi}{$string.'_co'}{$starttmstr}{co_estimate} = $val; } $k++; } } } $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 __VictronVRM_ApiRequestLogout ($paref); return; } ################################################################ # Victron VRM API Logout # https://vrm-api-docs.victronenergy.com/#/ ################################################################ sub __VictronVRM_ApiRequestLogout { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $token = $paref->{token}; my $debug = $paref->{debug}; my $url = 'https://vrmapi.victronenergy.com/v2/auth/logout'; debugLog ($paref, "apiProcess|apiCall", qq{Request VictronVRM API Logout: $url}); my $caller = (caller(0))[3]; # Rücksprungmarke my $param = { url => $url, timeout => 30, hash => $hash, name => $name, type => $paref->{type}, debug => $debug, caller => \&$caller, lang => $paref->{lang}, header => { "Content-Type" => "application/json", "x-authorization" => "Bearer $token" }, method => 'GET', callback => \&__VictronVRM_ApiResponseLogout }; if($debug =~ /apiCall/x) { $param->{loglevel} = 1; } HttpUtils_NonblockingGet ($param); return; } ############################################################### # Victron VRM API Logout Response ############################################################### sub __VictronVRM_ApiResponseLogout { my $paref = shift; my $err = shift; my $myjson = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $msg; if ($err ne "") { $msg = 'Victron VRM API error response: '.$err; Log3 ($name, 1, "$name - $msg"); return; } elsif ($myjson ne "") { # Evaluiere ob Daten im JSON-Format empfangen wurden my ($success) = evaljson($hash, $myjson); if(!$success) { $msg = 'ERROR - invalid Victron VRM API response'; Log3 ($name, 1, "$name - $msg"); return; } my $jdata = decode_json ($myjson); debugLog ($paref, "apiCall", qq{Victron VRM API response Logout:\n}. Dumper $jdata); } return; } ################################################################################################ # Abruf Open-Meteo API Daten ################################################################################################ sub __getopenMeteoData { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $force = $paref->{force} // 0; my $t = $paref->{t}; my $lang = $paref->{lang}; my $debug = $paref->{debug}; my $donearq = SolCastAPIVal ($hash, '?All', '?All', 'todayDoneAPIrequests', 0); if ($donearq >= $ometmaxreq) { my $msg = "The limit of maximum $ometmaxreq daily API requests is reached or already exceeded. Process is exited."; Log3 ($name, 1, "$name - ERROR - $msg"); return $msg; } if (!$force) { # regulärer API Abruf my $lrt = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_timestamp', 0); if ($lrt && $t < $lrt + $ometeorepdef) { my $rt = $lrt + $ometeorepdef - $t; return qq{The waiting time to the next Open-Meteo API call has not expired yet. The remaining waiting time is $rt seconds}; } } debugLog ($paref, 'apiCall', qq{Open-Meteo API Call - the daily API requests -> limited to: $ometmaxreq, done: $donearq}); my $submodel = InternalVal ($hash->{NAME}, 'MODEL', ''); $paref->{allstrings} = ReadingsVal ($name, 'inverterStrings', ''); $paref->{submodel} = $submodel eq 'OpenMeteoDWDAPI' ? 'DWD ICON Seamless' : $submodel eq 'OpenMeteoDWDEnsembleAPI' ? 'DWD ICON Seamless Ensemble' : $submodel eq 'OpenMeteoWorldAPI' ? 'World Best Match' : 'unknown'; return "The Weather Model '$submodel' is not a valid Open-Meteo Weather Model" if($paref->{submodel} eq 'unknown'); $paref->{callequivalent} = $submodel eq 'OpenMeteoDWDEnsembleAPI' ? 20 : 1; $paref->{begin} = 1; __openMeteoDWD_ApiRequest ($paref); return; } ######################################################################################################################## # Open-Meteo DWD ICON API Request # Open data weather forecasts from the German weather service DWD # Quelle Seite: https://open-meteo.com/ # # Aufruf: https://api.open-meteo.com/v1/dwd-icon?latitude=<>&longitude=<>&hourly=&daily=&forecast_hours=<>&tilt=<>&azimuth=<> # # Beispiel: https://api.open-meteo.com/v1/dwd-icon?latitude=51.285272&longitude=12.067722&hourly=temperature_2m,rain,weather_code,cloud_cover,is_day,global_tilted_irradiance_instant&daily=sunrise,sunset&forecast_hours=48&tilt=45&azimuth=0 # # temperature_2m - Air temperature at 2 meters above ground # rain - Regen aus Großwetterlagen der vorangegangenen Stunde in Millimeter # weather_code - Wetterlage als numerischer Code. Befolgen Sie die WMO-Wetterinterpretationscodes. # cloud_cover - Gesamtbewölkung als Flächenanteil (%) # is_day - Tag oder Nacht # timeformat - Wenn das Format unixtime gewählt wird, werden alle Zeitwerte in UNIX-Epochenzeit in Sekunden # zurückgegeben. Bitte beachten Sie, dass alle Zeitstempel in GMT+0 sind! # global_tilted_irradiance_instant - Gesamte Strahlung, die auf eine geneigte Scheibe fällt, als Durchschnitt der # (GTI) vorangegangenen Stunde. # Die Berechnung erfolgt unter der Annahme einer festen Albedo von 20% und eines # isotropen Himmels. (in W/m²) # timezone - If auto is set as a time zone, the coordinates will be automatically resolved to the local time zone. # ######################################################################################################################## sub __openMeteoDWD_ApiRequest { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $allstrings = $paref->{allstrings}; # alle Strings my $debug = $paref->{debug}; my $lang = $paref->{lang}; my $t = $paref->{t} // int time; my $submodel = $paref->{submodel}; # abzufragendes Wettermodell if (!$allstrings) { # alle Strings wurden abgerufen writeCacheToFile ($hash, 'solcastapi', $scpicache.$name); my $apiitv = SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $ometeorepdef); readingsSingleUpdate ($hash, 'nextRadiationAPICall', $hqtxt{after}{$lang}.' '.(timestampToTimestring ($t + $apiitv, $lang))[0], 1); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIcalls} += 1; return; } my $string; ($string, $allstrings) = split ",", $allstrings, 2; my ($set, $lat, $lon, $elev) = locCoordinates(); if (!$set) { my $err = qq{the attribute 'latitude' and/or 'longitude' in global device is not set}; singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); return $err; } my $tilt = StringVal ($hash, $string, 'tilt', ''); my $az = StringVal ($hash, $string, 'azimut', ''); my $url = "https://api.open-meteo.com/v1/forecast?"; $url = "https://ensemble-api.open-meteo.com/v1/ensemble?" if($submodel =~ /Ensemble/xs); # Ensemble Modell gewählt $url .= "models=icon_seamless" if($submodel =~ /DWD\sICON\sSeamless/xs); $url .= "models=best_match" if($submodel eq 'World Best Match'); $url .= "&latitude=".$lat; $url .= "&longitude=".$lon; $url .= "&hourly=temperature_2m,rain,weather_code,cloud_cover,is_day,global_tilted_irradiance"; $url .= "¤t=temperature_2m,weather_code,cloud_cover" if($submodel !~ /Ensemble/xs); $url .= "&minutely_15=global_tilted_irradiance" if($submodel !~ /Ensemble/xs); $url .= "&daily=sunrise,sunset" if($submodel !~ /Ensemble/xs); $url .= "&forecast_hours=48"; $url .= "&forecast_days=2"; $url .= "&tilt=".$tilt; $url .= "&azimuth=".$az; debugLog ($paref, 'apiCall', qq{Open-Meteo API Call - Request for PV-String "$string" with Weather Model >$submodel<:\n$url}); my $caller = (caller(0))[3]; # Rücksprungmarke my $param = { url => $url, timeout => 30, hash => $hash, name => $name, type => $paref->{type}, debug => $debug, header => 'Accept: application/json', submodel => $submodel, begin => $paref->{begin}, callequivalent => $paref->{callequivalent}, caller => \&$caller, stc => [gettimeofday], allstrings => $allstrings, string => $string, lang => $paref->{lang}, method => "GET", callback => \&__openMeteoDWD_ApiResponse }; if ($debug =~ /apiCall/x) { $param->{loglevel} = 1; } HttpUtils_NonblockingGet ($param); return; } ################################################################################################ # Open-Meteo DWD ICON API Response # # Rad1h vom DWD - Globalstrahlung in kJ/m2 # # Berechnung nach Formel 2 aus http://www.ing-büro-junge.de/html/photovoltaik.html: # # * Globalstrahlung: G = kWh/m2 (GTI = W/m2), (DWD Rad1h = kJ/m2) # * Korrektur mit Flächenfaktor f: Gk = G * f # * Globalstrahlung (STC): 1 kW/m2 # * Peak Leistung String (kWp): Pnenn = x kW # * Performance Ratio: PR (typisch 0,85 bis 0,9) # * weitere Korrekturwerte für Regen, Wolken etc.: Korr # # pv (Wh) = GTI * f / 1000 (kWh/m2) / 1 kW/m2 * Pnenn (kW) * PR * Korr * 1000 # (GTI * f) ist bereits in dem API-Ergebnis $rad enthalten in Wh/m2 # -> $rad / 1000 (kWh/m2) / 1 kW/m2 * Pnenn (kW) * PR * Korr (bezogen auf 1 Stunde) # -> my $pv = sprintf "%.0f", ($rad / 1000 * $peak * $prdef); # ################################################################################################ sub __openMeteoDWD_ApiResponse { my $paref = shift; my $err = shift; my $myjson = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $caller = $paref->{caller}; my $string = $paref->{string}; my $allstrings = $paref->{allstrings}; my $stc = $paref->{stc}; # Startzeit API Abruf my $lang = $paref->{lang}; my $debug = $paref->{debug}; my $submodel = $paref->{submodel}; my $t = int time; my $sta = [gettimeofday]; # Start Response Verarbeitung $paref->{t} = $t; my $msg; if ($err ne "") { $msg = 'Open-Meteo DWD ICON API server response: '.$err; Log3 ($name, 1, "$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 Open-Meteo DWD ICON API server response'; Log3 ($name, 1, "$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 $rt = (timestampToTimestring ($t, $lang))[3]; my $jdata = decode_json ($myjson); # debugLog ($paref, 'apiProcess', qq{Open-Meteo API Call - response for string "$string":\n}. Dumper $jdata); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_time} = $rt; # letzte Abrufzeit $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{lastretrieval_timestamp} = $t; # letzter Abrufzeitstempel ## bei Fehler in API intern kommt ################################### # error: true # reason: if ($jdata->{'error'}) { $msg = "Open-Meteo DWD ICON API server ERROR response: ".$jdata->{'reason'}; Log3 ($name, 3, "$name - $msg"); singleUpdateState ( {hash => $hash, state => $msg, evt => 1} ); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = $jdata->{'reason'}; $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; } $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message} = 'success'; if ($debug =~ /apiCall/xs) { Log3 ($name, 1, qq{$name DEBUG> Open-Meteo API Call - server response for PV string "$string"}); Log3 ($name, 1, "$name DEBUG> Open-Meteo API Call - status: success"); } my $date = strftime "%Y-%m-%d", localtime(time); my $refts = timestringToTimestamp ($date.' 00:00:00'); # Referenztimestring my $peak = StringVal ($hash, $string, 'peak', 0); # String Peak (kWp) $peak *= 1000; # kWp in Wp ## Akt. Werte ################# my ($curwid, $curwcc, $curtmp, $curstr); if (defined $jdata->{current}{time}) { ($err, $curstr) = timestringUTCtoLocal ($name, $jdata->{current}{time}, '%Y-%m-%dT%H:%M'); if ($err) { $msg = 'ERROR - Open-Meteo invalid time conversion: '.$err; Log3 ($name, 1, "$name - $msg"); singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); return; } $curwid = $jdata->{current}{weather_code}; $curwcc = $jdata->{current}{cloud_cover}; $curtmp = $jdata->{current}{temperature_2m}; } ## Stundenwerte ################# my $k = 0; while ($jdata->{hourly}{time}[$k]) { ($err, my $otmstr) = timestringUTCtoLocal ($name, $jdata->{hourly}{time}[$k], '%Y-%m-%dT%H:%M'); if ($err) { $msg = 'ERROR - Open-Meteo invalid time conversion: '.$err; Log3 ($name, 1, "$name - $msg"); singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); return; } my $ots = timestringToTimestamp ($otmstr); my $pvtmstr = (timestampToTimestring ($ots-3600))[0]; # Strahlung wird als Durchschnitt der !vorangegangenen! Stunde geliefert! if (timestringToTimestamp($pvtmstr) < $refts) { $k++; next; # Daten älter als akt. Tag 00:00:00 verwerfen } my $rad1wh = $jdata->{hourly}{global_tilted_irradiance}[$k]; # Wh/m2 my $rad = 10 * (sprintf "%.0f", ($rad1wh * $WhtokJ) / 10); # Umrechnung Wh/m2 in kJ/m2 -> my $pv = sprintf "%.2f", int ($rad1wh / 1000 * $peak * $prdef); # Rad wird in kWh/m2 erwartet my $don = $jdata->{hourly}{is_day}[$k]; my $temp = $jdata->{hourly}{temperature_2m}[$k]; my $rain = $jdata->{hourly}{rain}[$k]; # Regen in Millimeter = kg/m2 my $wid = ($don ? 0 : 100) + $jdata->{hourly}{weather_code}[$k]; my $wcc = $jdata->{hourly}{cloud_cover}[$k]; if ($k == 0 && $curwid) { $curwid = ($don ? 0 : 100) + $curwid } if ($debug =~ /apiProcess/xs) { Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $pvtmstr - Rad1Wh: $rad1wh, Rad1kJ: $rad, PV est: $pv Wh"); Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $pvtmstr - RR1c: $rain"); Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $otmstr - DoN: $don"); Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $otmstr - Temp: $temp"); Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $otmstr - Weather Code: $wid"); Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $otmstr - Cloud Cover: $wcc"); if ($k == 0) { Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $otmstr - current Temp: $curtmp") if(defined $curtmp); Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $curstr - current Weather Code: $curwid") if(defined $curwid); Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $curstr - current Cloud Cover: $curwcc") if(defined $curwcc); } } $data{$type}{$name}{solcastapi}{$string}{$pvtmstr}{pv_estimate50} = $pv; # Startstunde verschieben my $fwtg = formatWeatherTimestrg ($pvtmstr); if ($paref->{begin}) { # im ersten Call den DS löschen -> dann Aufsummierung delete $data{$type}{$name}{solcastapi}{'?All'}{$pvtmstr}{Rad1h}; } $data{$type}{$name}{solcastapi}{'?All'}{$pvtmstr}{Rad1h} += $rad; # Startstunde verschieben, Rad Werte aller Strings addieren $data{$type}{$name}{solcastapi}{'?All'}{$fwtg}{rr1c} = $rain; # Startstunde verschieben $fwtg = formatWeatherTimestrg ($otmstr); $data{$type}{$name}{solcastapi}{'?All'}{$fwtg}{don} = $don; $data{$type}{$name}{solcastapi}{'?All'}{$fwtg}{neff} = $wcc; $data{$type}{$name}{solcastapi}{'?All'}{$fwtg}{ww} = $wid; $data{$type}{$name}{solcastapi}{'?All'}{$fwtg}{ttt} = $temp; $data{$type}{$name}{solcastapi}{'?All'}{$fwtg}{UpdateTime} = $rt; if ($k == 0) { $data{$type}{$name}{solcastapi}{'?All'}{$fwtg}{neff} = $curwcc if(defined $curwcc); $data{$type}{$name}{solcastapi}{'?All'}{$fwtg}{ww} = $curwid if(defined $curwid); $data{$type}{$name}{solcastapi}{'?All'}{$fwtg}{ttt} = $curtmp if(defined $curtmp); } $k++; } ## Tageswerte ############### $k = 0; while ($jdata->{daily}{time}[$k]) { my $oday = $jdata->{daily}{time}[$k]; ($err, my $sunrise) = timestringUTCtoLocal ($name, $jdata->{daily}{sunrise}[$k], '%Y-%m-%dT%H:%M'); ($err, my $sunset) = timestringUTCtoLocal ($name, $jdata->{daily}{sunset}[$k], '%Y-%m-%dT%H:%M'); if ($err) { $msg = 'ERROR - Open-Meteo invalid time conversion: '.$err; Log3 ($name, 1, "$name - $msg"); singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); return; } if ($k == 0) { $data{$type}{$name}{solcastapi}{'?All'}{sunrise}{today} = $sunrise; $data{$type}{$name}{solcastapi}{'?All'}{sunset}{today} = $sunset; if ($debug =~ /apiProcess/xs) { Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API - Sunrise Today: $sunrise"); Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API - SunSet Today: $sunset"); } } if ($k == 1) { $data{$type}{$name}{solcastapi}{'?All'}{sunrise}{tomorrow} = $sunrise; $data{$type}{$name}{solcastapi}{'?All'}{sunset}{tomorrow} = $sunset; if ($debug =~ /apiProcess/xs) { Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API - Sunrise Tomorrow: $sunrise"); Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API - SunSet Tomorrow: $sunset"); } } $k++; } } ___setOpenMeteoAPIcallKeyData ($paref); Log3 ($name, 4, qq{$name - Open-Meteo DWD ICON API answer received for string "$string"}); my $param = { hash => $hash, name => $name, type => $type, debug => $debug, allstrings => $allstrings, submodel => $submodel, callequivalent => $paref->{callequivalent}, 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); } ################################################################ # Kennzahlen aus letzten Open-Meteo Request ableiten ################################################################ sub ___setOpenMeteoAPIcallKeyData { my $paref = shift; my $hash = $paref->{hash}; my $lang = $paref->{lang}; my $debug = $paref->{debug}; my $cequ = $paref->{callequivalent}; my $t = $paref->{t} // time; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIrequests} += $cequ; my $dar = SolCastAPIVal ($hash, '?All', '?All', 'todayDoneAPIrequests', 0); my $dac = SolCastAPIVal ($hash, '?All', '?All', 'todayDoneAPIcalls', 0); my $asc = CurrentVal ($hash, 'allstringscount', 1); my $drr = $ometmaxreq - $dar; $drr = 0 if($drr < 0); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayRemainingAPIrequests} = $drr; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = $ometeorepdef; ## Berechnung des optimalen Request Intervalls ################################################ my $edate = strftime "%Y-%m-%d 23:58:00", localtime($t); my $ets = timestringToTimestamp ($edate); my $rmdif = $ets - int $t; if ($drr) { my $optrep = $rmdif / ($drr / ($cequ * $asc)); $optrep = $ometeorepdef if($optrep < $ometeorepdef); $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{currentAPIinterval} = $optrep; } debugLog ($paref, "apiProcess|apiCall", "Open-Meteo API Call - remaining API Requests: $drr, Request equivalents p. call: $cequ, new call interval: ".SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $ometeorepdef)); return; } ############################################################### # Getter data ############################################################### sub _getdata { my $paref = shift; my $hash = $paref->{hash}; return centralTask ($hash); } ############################################################### # Getter html ############################################################### sub _gethtml { my $paref = shift; my $name = $paref->{name}; my $arg = $paref->{arg} // 'both'; return pageAsHtml ($name, '-', $arg); } ############################################################### # Getter ftui # ohne Eintrag in Get-Liste ############################################################### sub _getftui { my $paref = shift; my $name = $paref->{name}; my $arg = $paref->{arg} // ''; return pageAsHtml ($name, 'ftui', $arg); } ############################################################### # Getter pvHistory ############################################################### sub _getlistPVHistory { my $paref = shift; my $hash = $paref->{hash}; my $arg = $paref->{arg}; my $ret = listDataPool ($hash, 'pvhist', $arg); $ret .= lineFromSpaces ($ret, 20); $ret =~ s/\n/
/g; return $ret; } ############################################################### # Getter pvCircular ############################################################### sub _getlistPVCircular { my $paref = shift; my $hash = $paref->{hash}; my $ret = listDataPool ($hash, 'circular'); $ret .= lineFromSpaces ($ret, 20); return $ret; } ############################################################### # Getter nextHours ############################################################### sub _getlistNextHours { my $paref = shift; my $hash = $paref->{hash}; my $ret = listDataPool ($hash, 'nexthours'); $ret .= lineFromSpaces ($ret, 10); return $ret; } ############################################################### # Getter pvQualities ############################################################### sub _getForecastQualities { my $paref = shift; my $hash = $paref->{hash}; my $arg = $paref->{arg} // q{}; my $ret = listDataPool ($hash, 'qualities'); if ($arg eq 'imgget') { # Ausgabe aus dem Grafikheader Qualitätsicon $ret =~ s/\n/
/g; } return $ret; } ############################################################### # Getter valCurrent ############################################################### sub _getlistCurrent { my $paref = shift; my $hash = $paref->{hash}; my $ret = listDataPool ($hash, 'current'); $ret .= lineFromSpaces ($ret, 5); return $ret; } ############################################################### # Getter valConsumerMaster ############################################################### sub _getlistvalConsumerMaster { my $paref = shift; my $hash = $paref->{hash}; my $arg = $paref->{arg}; my $ret = listDataPool ($hash, 'consumer', $arg); $ret .= lineFromSpaces ($ret, 10); return $ret; } ############################################################### # Getter solApiData ############################################################### sub _getlistSolCastData { my $paref = shift; my $hash = $paref->{hash}; my $ret = listDataPool ($hash, 'solApiData'); $ret .= lineFromSpaces ($ret, 10); return $ret; } ############################################################### # Getter dwdCatalog ############################################################### sub _getdwdCatalog { my $paref = shift; my $arg = $paref->{arg} // 'byID'; my $name = $paref->{name}; my $type = $paref->{type}; my ($aa,$ha) = parseParams ($arg); my $sort = 'byID' ~~ @$aa ? 'byID' : 'byName' ~~ @$aa ? 'byName' : 'byID'; my $export = 'exportgpx' ~~ @$aa ? 'exportgpx' : ''; my $force = 'force' ~~ @$aa ? 'force' : ''; $paref->{sort} = $sort; $paref->{export} = $export; $paref->{filtid} = $ha->{id} ? $ha->{id} : ''; $paref->{filtnam} = $ha->{name} ? $ha->{name} : ''; $paref->{filtlat} = $ha->{lat} ? $ha->{lat} : ''; $paref->{filtlon} = $ha->{lon} ? $ha->{lon} : ''; my $msg = "The DWD Station catalog is initially loaded into SolarForecast.\n". "Please execute the command 'get $name $paref->{opt} $arg' again."; if ($force) { __dwdStatCatalog_Request ($paref); return 'The DWD Station Catalog is forced to loaded into SolarForecast.'; } if (!scalar keys %{$data{$type}{$name}{dwdcatalog}}) { # Katalog ist nicht geladen _readCacheFile ({ hash => $paref->{hash}, name => $name, type => $type, debug => $paref->{debug}, file => $dwdcatalog, cachename => 'dwdcatalog', title => 'DWD Station Catalog' } ); if (!scalar keys %{$data{$type}{$name}{dwdcatalog}}) { # Ladung von File nicht erfolgreich __dwdStatCatalog_Request ($paref); return $msg; } } return __generateCatOut ($paref); return; } ############################################################### # Ausgabe DWD Katalog formatieren ############################################################### sub __generateCatOut { my $paref = shift; my $arg = $paref->{arg}; my $name = $paref->{name}; my $type = $paref->{type}; my $lang = $paref->{lang}; my $sort = $paref->{sort}; my $export = $paref->{export}; my $filtid = $paref->{filtid}; my $filtnam = $paref->{filtnam}; my $filtlat = $paref->{filtlat}; my $filtlon = $paref->{filtlon}; my $filter = $filtid ? 'id:'.$filtid : ''; $filter .= ',' if($filter && $filtnam); $filter .= $filtnam ? 'name:'.$filtnam : ''; $filter .= ',' if($filter && $filtlat); $filter .= $filtlat ? 'lat:'.$filtlat : ''; $filter .= ',' if($filter && $filtlon); $filter .= $filtlon ? 'lon:'.$filtlon : ''; my $select = 'sort='.$sort; if ($filter) { $select .= ' filter='; $select .= trim ($filter); } $select .= ' ' if($export); $select .= $export; # Katalog Organisation (default ist 'byID) ############################################ my ($err, $isfil); my %temp; if ($sort eq 'byName') { for my $id (keys %{$data{$type}{$name}{dwdcatalog}}) { $paref->{id} = $id; ($err, $isfil) = ___isCatFiltered ($paref); return (split " at", $err)[0] if($err); next if($isfil); my $nid = $data{$type}{$name}{dwdcatalog}{$id}{stnam}; $temp{$nid}{stnam} = $data{$type}{$name}{dwdcatalog}{$id}{stnam}; $temp{$nid}{id} = $data{$type}{$name}{dwdcatalog}{$id}{id}; $temp{$nid}{latdec} = $data{$type}{$name}{dwdcatalog}{$id}{latdec}; # Latitude Dezimalgrad $temp{$nid}{londec} = $data{$type}{$name}{dwdcatalog}{$id}{londec}; # Longitude Dezimalgrad $temp{$nid}{elev} = $data{$type}{$name}{dwdcatalog}{$id}{elev}; } } elsif ($sort eq 'byID') { for my $id (keys %{$data{$type}{$name}{dwdcatalog}}) { $paref->{id} = $id; ($err, $isfil) = ___isCatFiltered ($paref); return (split " at", $err)[0] if($err); next if($isfil); $temp{$id}{stnam} = $data{$type}{$name}{dwdcatalog}{$id}{stnam}; $temp{$id}{id} = $data{$type}{$name}{dwdcatalog}{$id}{id}; $temp{$id}{latdec} = $data{$type}{$name}{dwdcatalog}{$id}{latdec}; # Latitude Dezimalgrad $temp{$id}{londec} = $data{$type}{$name}{dwdcatalog}{$id}{londec}; # Longitude Dezimalgrad $temp{$id}{elev} = $data{$type}{$name}{dwdcatalog}{$id}{elev}; } } if ($export eq 'exportgpx') { # DWD Katalog als gpx speichern my @data = (); push @data, ''; push @data, ''; for my $idx (sort keys %temp) { my $londec = $temp{"$idx"}{londec}; my $latdec = $temp{"$idx"}{latdec}; my $elev = $temp{"$idx"}{elev}; my $id = $temp{"$idx"}{id}; my $stnam = $temp{"$idx"}{stnam}; push @data, qq{}; push @data, qq{ $elev}; push @data, qq{ $stnam (ID=$id, Latitude=$latdec, Longitude=$londec)}; push @data, qq{ City}; push @data, qq{}; } push @data, ''; $err = FileWrite ( {FileName => $dwdcatgpx, ForceType => 'file' }, @data ); if (!$err) { debugLog ($paref, 'dwdComm', qq{DWD catalog saved as gpx content: }.$dwdcatgpx); } else { Log3 ($name, 1, "$name - ERROR - $err"); return $err; } } my $noe = scalar keys %temp; ## Ausgabe ############ my $out = ''; $out .= ''.encode('utf8', $hqtxt{dwdcat}{$lang}).'
'; # The Deutscher Wetterdienst Station Catalog $out .= encode('utf8', $hqtxt{nrsele}{$lang}).' '.$noe.'
'; # Selected entries $out .= "($select)

"; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; for my $key (sort keys %temp) { $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; } $out .= qq{
ID NAME LATITUDE LONGITUDE ELEVATION
$temp{"$key"}{id} $temp{"$key"}{stnam} $temp{"$key"}{latdec} $temp{"$key"}{londec} $temp{"$key"}{elev}
}; $out .= qq{}; undef %temp; return $out; } ############################################################### # Ausgabe DWD Katalog Einträge filtern ############################################################### sub ___isCatFiltered { my $paref = shift; my $id = $paref->{id}; my $name = $paref->{name}; my $type = $paref->{type}; my $filtid = $paref->{filtid}; my $filtnam = $paref->{filtnam}; my $filtlat = $paref->{filtlat}; my $filtlon = $paref->{filtlon}; my $isfil = 0; eval {$isfil = 1 if($filtid && $id !~ /^$filtid$/ixs); $isfil = 1 if($filtnam && $data{$type}{$name}{dwdcatalog}{$id}{stnam} !~ /^$filtnam$/ixs); $isfil = 1 if($filtlat && $data{$type}{$name}{dwdcatalog}{$id}{latdec} !~ /^$filtlat$/ixs); $isfil = 1 if($filtlon && $data{$type}{$name}{dwdcatalog}{$id}{londec} !~ /^$filtlon$/ixs); }; if ($@) { return $@; } return ('', $isfil); } #################################################################################################################### # Download DWD Stationskatalog # https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.cfg?view=nasPublication&nn=16102 #################################################################################################################### sub __dwdStatCatalog_Request { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $debug = $paref->{debug}; my $url = "https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.cfg?view=nasPublication&nn=16102"; debugLog ($paref, 'dwdComm', "Download DWD Station catalog from URL: $url"); my $param = { url => $url, timeout => 10, hash => $hash, name => $name, debug => $debug, stc => [gettimeofday], lang => $paref->{lang}, method => 'GET', callback => \&__dwdStatCatalog_Response }; if ($debug =~ /dwdComm/x) { $param->{loglevel} = 1; } HttpUtils_NonblockingGet ($param); return; } ############################################################### # Download DWD Stationskatalog Response # Für die Stationsliste im cfg-Format gilt: # Die Angabe der Längen- und Breitengrade erfolgt in der Form # Grad und Minuten, also beispielsweise wird die Angabe 53◦ 23′ # in Grad und Minuten hier mit Punkt als 53.23 repräsentiert. ############################################################### sub __dwdStatCatalog_Response { my $paref = shift; my $err = shift; my $data = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $stc = $paref->{stc}; # Startzeit API Abruf my $lang = $paref->{lang}; my $debug = $paref->{debug}; my $msg; my $sta = [gettimeofday]; # Start Response Verarbeitung if ($err ne "") { Log3 ($name, 1, "$name - ERROR - $err"); return; } elsif ($data ne "") { my @datarr = split "\n", $data; my $type = $hash->{TYPE}; for my $s (@datarr) { $s = encode ('utf8', $s); my ($id, $tail) = split " ", $s, 2; next if($id !~ /[A-Z0-9]+$/xs || $id eq 'ID'); my $ri = rindex ($tail, " "); my $elev = substr ($tail, $ri + 1); # Meereshöhe $tail = trim (substr ($tail, 0, $ri)); $ri = rindex ($tail, " "); my $lon = substr ($tail, $ri + 1); # Longitude $tail = trim (substr ($tail, 0, $ri)); $ri = rindex ($tail, " "); my $lat = substr ($tail, $ri + 1); # Latitude $tail = trim (substr ($tail, 0, $ri)); my ($icao, $stnam) = split " ", $tail, 2; # ICAO = International Civil Aviation Organization, Stationsname my ($latg, $latm) = split /\./, $lat; # in Grad und Minuten splitten my ($long, $lonm) = split /\./, $lon; my $latdec = sprintf "%.2f", ($latg + ($latm / 60)); my $londec = sprintf "%.2f", ($long + ($lonm / 60)); $data{$type}{$name}{dwdcatalog}{$id}{id} = $id; $data{$type}{$name}{dwdcatalog}{$id}{stnam} = $stnam; $data{$type}{$name}{dwdcatalog}{$id}{icao} = $icao; $data{$type}{$name}{dwdcatalog}{$id}{lat} = $lat; $data{$type}{$name}{dwdcatalog}{$id}{latdec} = $latdec; # Latitude Dezimalgrad $data{$type}{$name}{dwdcatalog}{$id}{lon} = $lon; $data{$type}{$name}{dwdcatalog}{$id}{londec} = $londec; # Longitude Dezimalgrad $data{$type}{$name}{dwdcatalog}{$id}{elev} = $elev; } $err = writeCacheToFile ($hash, 'dwdcatalog', $dwdcatalog); # DWD Stationskatalog speichern if (!$err) { debugLog ($paref, 'dwdComm', qq{DWD catalog saved into file: }.$dwdcatalog); } else { Log3 ($name, 1, "$name - ERROR - $err"); } _readCacheFile ({ hash => $hash, name => $name, type => $type, debug => $debug, file => $dwdcatalog, cachename => 'dwdcatalog', title => 'DWD Station Catalog' } ); } my $prt = sprintf "%.4f", (tv_interval ($stc) - tv_interval ($sta)); # Laufzeit ermitteln debugLog ($paref, 'dwdComm', "DWD Station Catalog retrieval and processing required >$prt< seconds"); return; } ############################################################### # Getter aiDecTree ############################################################### sub _getaiDecTree { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $arg = $paref->{arg} // return; my $ret; if($arg eq 'aiRawData') { $ret = listDataPool ($hash, 'aiRawData'); } if($arg eq 'aiRuleStrings') { $ret = __getaiRuleStrings ($paref); } $ret .= lineFromSpaces ($ret, 5); return $ret; } ################################################################ # Gibt eine Liste von Zeichenketten zurück, die den AI # Entscheidungsbaum in Form von Regeln beschreiben ################################################################ sub __getaiRuleStrings { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $lang = $paref->{lang}; return 'the AI usage is not prepared' if(!isPrepared4AI ($hash)); my $dtree = AiDetreeVal ($hash, 'aitrained', undef); if (!$dtree) { return 'AI trained object is missed'; } my $rs = 'no rules delivered'; my (@rsl, $nodes, $depth); eval { @rsl = $dtree->rule_statements(); # Returns a list of strings that describe the tree in rule-form $nodes = $dtree->nodes(); # Returns the number of nodes in the trained decision tree $depth = $dtree->depth(); # Returns the depth of the tree. This is the maximum number of decisions that would need to be made to classify an unseen instance, i.e. the length of the longest path from the tree's root to a leaf. 1; } or do { return $@; }; my $atf = CircularVal ($hash, 99, 'aitrainLastFinishTs', 0); $atf = ''.$hqtxt{ailatr}{$lang}.' '.($atf ? (timestampToTimestring ($atf, $lang))[0] : '-'); my $art = $hqtxt{aitris}{$lang}.' '.CircularVal ($hash, 99, 'runTimeTrainAI', '-'); if (@rsl) { my $l = scalar @rsl; $rs = "Number of Rules: $l / Number of Nodes: $nodes / Depth: $depth\n"; $rs .= "Rules: ".$hqtxt{airule}{$lang}."\n"; $rs .= "Nodes: ".$hqtxt{ainode}{$lang}."\n"; $rs .= "Depth: ".$hqtxt{aidept}{$lang}; $rs .= "\n\n"; $rs .= $atf.' / '.$art; $rs .= "\n\n"; $rs .= join "\n", @rsl; } return $rs; } ############################################################### # Getter ftuiFramefiles # hole Dateien aus dem online Verzeichnis # /fhem/contrib/SolarForecast/ # Ablage entsprechend Definition in controls_solarforecast.txt ############################################################### sub _ftuiFramefiles { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $ret; my $upddo = 0; my $cfurl = $bPath.$cfile.$pPath; for my $file (@fs) { my $lencheck = 1; my ($cmerr, $cmupd, $cmmsg, $cmrec, $cmfile, $cmlen) = checkModVer ($name, $file, $cfurl); if ($cmerr && $cmmsg =~ /Automatic\scheck/xs && $cmrec =~ /Compare\syour\slocal/xs) { # lokales control file ist noch nicht vorhanden -> update ohne Längencheck $cmfile = 'FHEM/'.$cfile; $file = $cfile; $lencheck = 0; $cmerr = 0; $cmupd = 1; Log3 ($name, 3, "$name - automatic install local control file $root/$cmfile"); } if ($cmerr) { $ret = "$cmmsg
$cmrec"; return $ret; } if ($cmupd) { $upddo = 1; $ret = __updPreFile ( { name => $name, root => $root, cmfile => $cmfile, cmlen => $cmlen, bPath => $bPath, file => $file, pPath => $pPath, lencheck => $lencheck } ); return $ret if($ret); } } ## finales Update control File ################################ $ret = __updPreFile ( { name => $name, root => $root, cmfile => 'FHEM/'.$cfile, cmlen => 0, bPath => $bPath, file => $cfile, pPath => $pPath, lencheck => 0, finalupd => 1 } ); return $ret if($ret); if (!$upddo) { return 'SolarForecast FTUI files are already up to date'; } return 'SolarForecast FTUI files updated'; } ############################################################### # File zum Abruf von url vorbereiten und in das # Zielverzeichnis schreiben ############################################################### sub __updPreFile { my $pars = shift; my $name = $pars->{name}; my $root = $pars->{root}; my $cmfile = $pars->{cmfile}; my $cmlen = $pars->{cmlen}; my $bPath = $pars->{bPath}; my $file = $pars->{file}; my $pPath = $pars->{pPath}; my $lencheck = $pars->{lencheck}; my $finalupd = $pars->{finalupd} // 0; my $err; my $dir = $cmfile; $dir =~ m,^(.*)/([^/]*)$,; $dir = $1; $dir = "" if(!defined $dir); # file in . my @p = split "/", $dir; for (my $i = 0; $i < int @p; $i++) { my $path = "$root/".join ("/", @p[0..$i]); if (!-d $path) { $err = "The FTUI does not appear to be installed.
"; $err .= "Please check whether the path $path is present and accessible.
"; $err .= "After installing FTUI, come back and execute the get command again."; return $err; #my $ok = mkdir $path; #if (!$ok) { # $err = "MKDIR ERROR: $!"; # Log3 ($name, 1, "$name - $err"); # return $err; #} #else { # Log3 ($name, 3, "$name - MKDIR $path"); #} } } ($err, my $remFile) = __updGetUrl ($name, $bPath.$file.$pPath); if ($err) { Log3 ($name, 1, "$name - $err"); return $err; } if ($lencheck && length $remFile ne $cmlen) { $err = "update ERROR: length of $file is not $cmlen Bytes"; Log3 ($name, 1, "$name - $err"); return $err; } $err = __updWriteFile ($root, $cmfile, $remFile); if ($err) { Log3 ($name, 1, "$name - $err"); return $err; } Log3 ($name, 3, "$name - update done $file to $root/$cmfile ".($cmlen ? "(length: $cmlen Bytes)" : '')); if(!$lencheck && !$finalupd) { return 'SolarForecast update control file installed. Please retry the get command to update FTUI files.'; } return; } ############################################################### # File von url holen ############################################################### sub __updGetUrl { my $name = shift; my $url = shift; $url =~ s/%/%25/g; my %upd_connecthash; my $unicodeEncoding = 1; $upd_connecthash{url} = $url; $upd_connecthash{keepalive} = ($url =~ m/localUpdate/ ? 0 : 1); # Forum #49798 $upd_connecthash{forceEncoding} = '' if($unicodeEncoding); my ($err, $data) = HttpUtils_BlockingGet (\%upd_connecthash); if ($err) { $err = "update ERROR: $err"; return ($err, ''); } if (!$data) { $err = 'update ERROR: empty file received'; return ($err, ''); } return ('', $data); } ############################################################### # Updated File schreiben ############################################################### sub __updWriteFile { my $root = shift; my $fName = shift; my $content = shift; my $fPath = "$root/$fName"; my $err; if (!open(FD, ">$fPath")) { $err = "update ERROR open $fPath failed: $!"; return $err; } binmode(FD); print FD $content; close(FD); my $written = -s "$fPath"; if ($written != length $content) { $err = "update ERROR writing $fPath failed: $!"; return $err; } return; } ################################################################ # Attr # $cmd can be "del" or "set" # $name is device name # aName and aVal are Attribute name and value ################################################################ sub Attr { my $cmd = shift; my $name = shift; my $aName = shift; my $aVal = shift; my $hash = $defs{$name}; my $type = $hash->{TYPE}; my ($do,$val, $err); ### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !! ###################################################################################################################### #if ($cmd eq 'set' && $aName eq 'affectNumHistDays') { # if (!$init_done) { # return qq{Device "$name" -> The attribute '$aName' is obsolete and will be deleted soon. Please press "save config" when restart is finished.}; # } # else { # return qq{The attribute '$aName' is obsolete and will be deleted soon.}; # } #} ###################################################################################################################### if ($aName eq 'disable') { if($cmd eq 'set') { $do = $aVal ? 1 : 0; } $do = 0 if($cmd eq 'del'); $val = ($do == 1 ? 'disabled' : 'initialized'); singleUpdateState ( {hash => $hash, state => $val, evt => 1} ); } if ($aName eq 'ctrlAutoRefresh') { delete $hash->{HELPER}{AREFRESH}; delete $hash->{AUTOREFRESH}; } if ($aName eq 'ctrlNextDayForecastReadings') { deleteReadingspec ($hash, "Tomorrow_Hour.*"); } if ($aName eq 'ctrlBatSocManagement' && $init_done) { if ($cmd eq 'set') { return qq{Define the key "cap" with "set $name currentBatteryDev" before this attribute.} if(ReadingsVal ($name, 'currentBatteryDev', '') !~ /\s+cap=/xs); my ($lowSoc, $upSoc, $maxsoc, $careCycle) = __parseAttrBatSoc ($name, $aVal); return 'The attribute syntax is wrong' if(!$lowSoc || !$upSoc || $lowSoc !~ /[0-9]+$/xs); if (!($lowSoc > 0 && $lowSoc < $upSoc && $upSoc < $maxsoc && $maxsoc <= 100)) { return 'The specified values are not plausible. Compare the attribute help.'; } } else { deleteReadingspec ($hash, 'Battery_.*'); } delete $data{$type}{$name}{circular}{'99'}{lastTsMaxSocRchd}; delete $data{$type}{$name}{circular}{'99'}{nextTsMaxSocChge}; } if ($aName eq 'ctrlGenPVdeviation' && $aVal eq 'daily') { my $type = $hash->{TYPE}; deleteReadingspec ($hash, 'Today_PVdeviation'); delete $data{$type}{$name}{circular}{99}{tdayDvtn}; } if ($aName eq 'graphicHeaderOwnspecValForm') { if ($cmd ne 'set') { delete $data{$type}{$name}{func}{ghoValForm}; return; } my $code = $aVal; ($err, $code) = checkCode ($name, $code); return $err if($err); $data{$type}{$name}{func}{ghoValForm} = $code; } if ($cmd eq 'set') { if ($aName eq 'ctrlInterval' || $aName eq 'ctrlBackupFilesKeep' || $aName eq 'ctrlAIdataStorageDuration') { unless ($aVal =~ /^[0-9]+$/x) { return qq{Invalid value for $aName. Use only figures 0-9!}; } } 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 "ctrlSolCastAPIoptimizeReq") { if (!isSolCastUsed ($hash)) { return qq{The attribute $aName is only valid for device model "SolCastAPI".}; } } if ($aName eq 'ctrlUserExitFn' && $init_done) { ($err) = checkCode ($name, $aVal, 'cc1'); return $err if($err); } } my $params = { hash => $hash, name => $name, type => $hash->{TYPE}, cmd => $cmd, 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); return $ret; } return; } ################################################################ # Attr consumer ################################################################ sub _attrconsumer { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; 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, $valid); 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 (exists $h->{switchdev}) { my $dswitch = $h->{switchdev}; # alternatives Schaltdevice if(!$defs{$dswitch}) { return qq{The device "$dswitch" doesn't exist!}; } } if ($h->{power} !~ /^[0-9]+$/xs) { return qq{The key "power" must be specified only by numbers without decimal places}; } if (exists $h->{mode} && $h->{mode} !~ /^(?:can|must)$/xs) { return qq{The mode "$h->{mode}" isn't allowed!}; } if (exists $h->{notbefore}) { if ($h->{notbefore} =~ m/^\s*\{.*\}\s*$/xs) { ($err) = checkCode ($name, $h->{notbefore}, 'cc1'); return $err if($err); } else { $valid = checkhhmm ($h->{notbefore}); return qq{The syntax "notbefore=$h->{notbefore}" is wrong!} if(!$valid); } } if (exists $h->{notafter}) { if ($h->{notafter} =~ m/^\s*\{.*\}\s*$/xs) { ($err) = checkCode ($name, $h->{notafter}, 'cc1'); return $err if($err); } else { $valid = checkhhmm ($h->{notafter}); return qq{The syntax "notafter=$h->{notafter}" is wrong!} if(!$valid); } } if (exists $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"}; } } if (exists $h->{swoncond}) { # Check Regex my (undef,undef,$regex) = split ":", $h->{swoncond}; $err = checkRegex ($regex); return $err if($err); } if (exists $h->{swoffcond}) { # Check Regex my (undef,undef,$regex) = split ":", $h->{swoffcond}; $err = checkRegex ($regex); return $err if($err); } if (exists $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); } if (exists $h->{mintime}) { # Check Regex my $mintime = $h->{mintime}; if (!isNumeric ($mintime) && $mintime !~ /^SunPath/xsi) { return qq(The key "mintime" must be an integer or a string starting with "SunPath."); } } } else { my $day = strftime "%d", localtime(time); # aktueller Tag (range 01 to 31) 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 } writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben $data{$type}{$name}{current}{consumerCollected} = 0; # Consumer neu sammeln InternalTimer (gettimeofday()+0.5, 'FHEM::SolarForecast::centralTask', [$name, 0], 0); InternalTimer (gettimeofday()+2, 'FHEM::SolarForecast::createAssociatedWith', $hash, 0); return; } ################################################################ # Attr ctrlConsRecommendReadings ################################################################ 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"); } return; } ################################################################ # Attr ctrlStatisticReadings ################################################################ sub _attrcreateStatisticRdgs { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $aName = $paref->{aName}; my $aVal = $paref->{aVal}; my $te = 'currentRunMtsConsumer_'; if ($aVal =~ /$te/xs && $init_done) { my @aa = split ",", $aVal; for my $arg (@aa) { next if($arg !~ /$te/xs); my $cn = (split "_", $arg)[1]; # Consumer Nummer extrahieren if (!AttrVal ($name, 'consumer'.$cn, '')) { return qq{The consumer "consumer$cn" is currently not registered as an active consumer!}; } } } return; } ################################################################ # Attr ctrlWeatherDevX ################################################################ sub _attrWeatherDev { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $aVal = $paref->{aVal} // return qq{no weather forecast device specified} if($paref->{cmd} eq 'set'); my $aName = $paref->{aName}; return if(!$init_done); if ($paref->{cmd} eq 'set' ) { if ($aVal !~ /^OpenMeteo/xs && (!$defs{$aVal} || $defs{$aVal}{TYPE} ne "DWD_OpenData")) { return qq{The device "$aVal" doesn't exist or has no TYPE 'DWD_OpenData'}; } if ($aVal =~ /^OpenMeteo/xs) { if ($aName ne 'ctrlWeatherDev1') { return qq{Only the leading attribute 'ctrlWeatherDev1' can set to '$aVal'}; } #CommandSet (undef, "$name currentRadiationAPI $aVal"); # automatisch currentRadiationAPI setzen wenn ctrlWeatherDev1 InternalTimer (gettimeofday()+1, 'FHEM::SolarForecast::__setRadAPIdelayed', $hash, 0); # automatisch currentRadiationAPI setzen wenn ctrlWeatherDev1 return; } my $err = checkdwdattr ($name, $aVal, \@dweattrmust); return $err if($err); } InternalTimer (gettimeofday()+2, 'FHEM::SolarForecast::createAssociatedWith', $hash, 0); return; } ################################################################ # currentRadiationAPI verzögert aus Attr setzen ################################################################ sub __setRadAPIdelayed { my $hash = shift; my $name = $hash->{NAME}; my $awdev1 = AttrVal ($name, 'ctrlWeatherDev1', ''); CommandSet (undef, "$name currentRadiationAPI $awdev1"); # automatisch currentRadiationAPI setzen return; } ################################################################################### # Eventverarbeitung # - Aktualisierung Consumerstatus bei asynchronen Consumern ################################################################################### 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. 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((controller($myName))[1] || !$myHash->{NOTIFYDEV}); my $events = deviceEvents($dev_hash, 1); return if(!$events); my $cdref = CurrentVal ($myHash, 'consumerdevs', ''); # alle registrierten Consumer und Schaltdevices my @consumers = (); @consumers = @{$cdref} if(ref $cdref eq "ARRAY"); return if(!@consumers); my $debug = getDebug ($myHash); # Debug Mode if($devName ~~ @consumers) { my ($cname, $cindex); my $type = $myHash->{TYPE}; for my $c (sort{$a<=>$b} keys %{$data{$type}{$myName}{consumers}}) { my ($cname, $dswname) = getCDnames ($myHash, $c); if ($devName eq $cname) { $cindex = $c; last; } if ($devName eq $dswname) { $cindex = $c; if ($debug =~ /notifyHandling/x) { Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - Event device >$devName< is switching device of consumer >$cname< (index: $c)}); } last; } } if (!$cindex) { Log3 ($myName, 2, qq{$myName notifyHandling - Device >$devName< has no consumer index and/or ist not a known switching device. Exiting...}); return; } my $async = ConsumerVal ($myHash, $cindex, 'asynchron', 0); my $rswstate = ConsumerVal ($myHash, $cindex, 'rswstate', 'state'); if ($debug =~ /notifyHandling/x) { Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - Consumer >$cindex< asynchronous mode: $async}); } return if(!$async); # Consumer synchron -> keine Weiterverarbeitung my ($reading,$value,$unit); for my $event (@{$events}) { $event = "" if(!defined($event)); my @parts = split (/: /,$event, 2); $reading = shift @parts; if (@parts == 2) { $value = $parts[0]; $unit = $parts[1]; } else { $value = join(": ", @parts); $unit = ""; } if (!defined($reading)) { $reading = ""; } if (!defined($value)) { $value = ""; } if ($value eq "") { if ($event =~ /^.*:\s$/) { $reading = (split(":", $event))[0]; } else { $reading = "state"; $value = $event; } } if ($reading eq $rswstate) { if ($debug =~ /notifyHandling/x) { Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - start centralTask by Notify device: $devName, reading: $reading, value: $value}); } centralTask ($myHash, 0); # keine Events in SolarForecast außer 'state' } } } return; } ############################################################### # DbLog_splitFn ############################################################### sub DbLogSplit { my $event = shift; my $device = shift; my ($reading, $value, $unit) = ("","",""); if($event =~ /\s(k?Wh?|%)$/xs) { my @parts = split(/\s/x, $event, 3); $reading = $parts[0]; $reading =~ tr/://d; $value = $parts[1]; $unit = $parts[2]; # Log3 ($device, 1, qq{$device - Split for DbLog done -> Reading: $reading, Value: $value, Unit: $unit}); } return ($reading, $value, $unit); } ################################################################ # Shutdown ################################################################ sub Shutdown { my $hash = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; writeCacheToFile ($hash, 'pvhist', $pvhcache.$name); # Cache File für PV History schreiben writeCacheToFile ($hash, 'circular', $pvccache.$name); # Cache File für PV Circular schreiben writeCacheToFile ($hash, 'consumers', $csmcache.$name); # Cache File Consumer schreiben writeCacheToFile ($hash, 'solcastapi', $scpicache.$name); # Cache File SolCast API Werte schreiben 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 # internen Timern. ################################################################ sub Undef { my $hash = shift; my $name = shift; RemoveInternalTimer($hash); delete $readyfnlist{$name}; 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. ################################################################# sub Delete { my $hash = shift; my $arg = shift; my $name = $hash->{NAME}; my @ftd = ( $pvhcache.$name, $pvccache.$name, $plantcfg.$name, $csmcache.$name, $scpicache.$name, $airaw.$name, $aitrained.$name ); opendir (DIR, $cachedir); while (my $file = readdir (DIR)) { next unless (-f "$cachedir/$file"); next unless ($file =~ /_${name}_/); next unless ($file =~ /_\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}$/); push @ftd, "$cachedir/$file"; } closedir (DIR); for my $f (@ftd) { my $err = FileDelete ($f); if ($err) { Log3 ($name, 1, qq{$name - Message while deleting file "$f": $err}); } } my $type = $hash->{TYPE}; delete $data{$type}{$name}; return; } ################################################################ # Timer schreiben Memory Struktur in File ################################################################ sub periodicWriteMemcache { my $hash = shift; my $bckp = shift // ''; my $name = $hash->{NAME}; RemoveInternalTimer ($hash, "FHEM::SolarForecast::periodicWriteMemcache"); InternalTimer (gettimeofday()+$whistrepeat, "FHEM::SolarForecast::periodicWriteMemcache", $hash, 0); my (undef, $disabled, $inactive) = controller ($name); return if($disabled || $inactive); writeCacheToFile ($hash, 'circular', $pvccache.$name); # Cache File PV Circular schreiben writeCacheToFile ($hash, 'pvhist', $pvhcache.$name); # Cache File PV History schreiben writeCacheToFile ($hash, 'solcastapi', $scpicache.$name); # Cache File SolCast API Werte schreiben $hash->{LCACHEFILE} = "last write time: ".FmtTime(gettimeofday())." whole Operating Memory"; Log3 ($name, 4, "$name - The working memory >circular pvhist solcastapi< has been saved to persistance"); if ($bckp) { my $tstr = (timestampToTimestring (0))[2]; $tstr =~ s/[-: ]/_/g; writeCacheToFile ($hash, "circular", $pvccache.$name.'_'.$tstr); # Cache File PV Circular Sicherung schreiben writeCacheToFile ($hash, "pvhist", $pvhcache.$name.'_'.$tstr); # Cache File PV History Sicherung schreiben deleteOldBckpFiles ($name, 'PVH_SolarForecast_'.$name); # alte Backup Files löschen deleteOldBckpFiles ($name, 'PVC_SolarForecast_'.$name); } return; } ################################################################ # Backupfiles löschen ################################################################ sub deleteOldBckpFiles { my $name = shift; my $file = shift; my $dfk = AttrVal ($name, 'ctrlBackupFilesKeep', 3); my $bfform = $file.'_.*'; if (!opendir (DH, $cachedir)) { Log3 ($name, 1, "$name - ERROR - Can't open path '$cachedir'"); return; } my @files = sort grep {/^$bfform$/} readdir(DH); return if(!@files); my $fref = stat ("$cachedir/$file"); if ($fref) { if ($fref =~ /ARRAY/) { @files = sort { (@{stat "$cachedir/$a"})[9] cmp (@{stat "$cachedir/$b"})[9] } @files; } else { @files = sort { (stat "$cachedir/$a")[9] cmp (stat "$cachedir/$b")[9] } @files; } } closedir (DH); Log3 ($name, 4, "$name - Backup files were found in '$cachedir' directory: ".join(', ',@files)); my $max = int @files - $dfk; for (my $i = 0; $i < $max; $i++) { my $done = 1; unlink "$cachedir/$files[$i]" or do { Log3 ($name, 1, "$name - WARNING - Could not delete '$cachedir/$files[$i]': $!"); $done = 0; }; Log3 ($name, 3, "$name - old backup file '$cachedir/$files[$i]' deleted") if($done); } return; } ################################################################ # Daten in File wegschreiben ################################################################ sub writeCacheToFile { my $hash = shift; my $cachename = shift; my $file = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my @data; my ($error, $err, $lw); if ($cachename eq 'aitrained') { my $dtree = AiDetreeVal ($hash, 'aitrained', ''); return if(ref $dtree ne 'AI::DecisionTree'); $error = fileStore ($dtree, $file); if ($error) { $err = qq{ERROR while writing AI data to file "$file": $error}; Log3 ($name, 1, "$name - $err"); return $err; } $lw = gettimeofday(); $hash->{LCACHEFILE} = "last write time: ".FmtTime($lw)." File: $file"; singleUpdateState ( {hash => $hash, state => "wrote cachefile $cachename successfully", evt => 1} ); return; } if ($cachename eq 'airaw') { my $data = AiRawdataVal ($hash, '', '', ''); if ($data) { $error = fileStore ($data, $file); } if ($error) { $err = qq{ERROR while writing AI data to file "$file": $error}; Log3 ($name, 1, "$name - $err"); return $err; } $lw = gettimeofday(); $hash->{LCACHEFILE} = "last write time: ".FmtTime($lw)." File: $file"; singleUpdateState ( {hash => $hash, state => "wrote cachefile $cachename successfully", evt => 1} ); return; } if ($cachename eq 'dwdcatalog') { if (scalar keys %{$data{$type}{$name}{dwdcatalog}}) { $error = fileStore ($data{$type}{$name}{dwdcatalog}, $file); } else { return "The DWD Station Catalog is empty"; } if ($error) { $err = qq{ERROR while writing DWD Station Catalog to file "$file": $error}; Log3 ($name, 1, "$name - $err"); return $err; } return; } if ($cachename eq 'plantconfig') { my ($plantcfg, $nr, $na) = _storePlantConfig ($hash); if (scalar keys %{$plantcfg}) { $error = fileStore ($plantcfg, $file); } if ($error) { $err = qq{ERROR writing cache file "$file": $error}; Log3 ($name, 1, "$name - $err"); return $err; } $lw = gettimeofday(); $hash->{LCACHEFILE} = "last write time: ".FmtTime($lw)." File: $file"; singleUpdateState ( {hash => $hash, state => "wrote cachefile $cachename successfully", evt => 1} ); return ('', $nr, $na); } return if(!$data{$type}{$name}{$cachename}); my $json = encode_json ($data{$type}{$name}{$cachename}); push @data, $json; $error = FileWrite ($file, @data); if ($error) { $err = qq{ERROR writing cache file "$file": $error}; Log3 ($name, 1, "$name - $err"); return $err; } $lw = gettimeofday(); $hash->{LCACHEFILE} = "last write time: ".FmtTime($lw)." File: $file"; singleUpdateState ( {hash => $hash, state => "wrote cachefile $cachename successfully", evt => 1} ); return; } ################################################################ # Anlagenkonfiguration mit fileStore sichern ################################################################ sub _storePlantConfig { my $hash = shift; my $name = $hash->{NAME}; my $plantcfg; my ($nr, $na) = (0,0); for my $rcfg (@rconfigs) { my $val = ReadingsVal ($name, $rcfg, ''); next if(!$val); $plantcfg->{$rcfg} = $val; $nr++; } for my $acfg (@aconfigs) { my $val = AttrVal ($name, $acfg, ''); next if(!$val); $plantcfg->{$acfg} = $val; $na++; } return ($plantcfg, $nr, $na); } ################################################################ # Anlagenkonfiguration aus fileRetrieve wiederherstellen ################################################################ sub _restorePlantConfig { my $hash = shift; my $plantcfg = shift; my $name = $hash->{NAME}; my ($nr, $na) = (0,0); while (my ($key, $val) = each %{$plantcfg}) { if ($key ~~ @rconfigs) { # Reading wiederherstellen CommandSetReading (undef,"$name $key $val"); $nr++; } if ($key ~~ @aconfigs) { # Attribut wiederherstellen CommandAttr (undef, "$name $key $val"); $na++; } } return ($nr, $na); } ################################################################ # centralTask Start Management # Achtung: relevant für CPU Auslastung! ################################################################ sub runTask { my $hash = shift; return if(!$init_done || CurrentVal ($hash, 'ctrunning', 0)); my $t = time; my $ms = strftime "%M:%S", localtime($t); my ($min, $sec) = split ':', $ms; # aktuelle Minute (00-59), aktuelle Sekunde (00-61) $min = int $min; $sec = int $sec; if ($sec > 10) { # Attribute zur Laufzeit hinzufügen if (!exists $hash->{HELPER}{S10DONE}) { $hash->{HELPER}{S10DONE} = 1; _addDynAttr ($hash); # relevant für CPU Auslastung!! } } else { delete $hash->{HELPER}{S10DONE}; } my $name = $hash->{NAME}; my ($interval, $disabled, $inactive) = controller ($name); if (!$interval) { $hash->{MODE} = 'Manual'; storeReading ('nextCycletime', 'Manual'); return; } if ($disabled) { $hash->{MODE} = 'disabled'; return; } if ($inactive) { $hash->{MODE} = 'inactive'; return; } my $nct = CurrentVal ($hash, 'nextCycleTime', 0); # gespeicherte nächste CyleTime if ($t >= $nct) { my $new = $t + $interval; # nächste Wiederholungszeit $hash->{MODE} = 'Automatic - next Cycletime: '.FmtTime($new); $data{$hash->{TYPE}}{$name}{current}{nextCycleTime} = $new; storeReading ('nextCycletime', FmtTime($new)); centralTask ($hash, 1); } my $debug = getDebug ($hash); if ($min == 59 && $sec > 48) { if (!defined $hash->{HELPER}{S48DONE}) { $hash->{HELPER}{S48DONE} = 1; if ($debug =~ /collectData/x) { Log3 ($name, 1, "$name DEBUG> INFO - runTask starts data collection at the end of an hour"); } centralTask ($hash, 1); } } else { delete $hash->{HELPER}{S48DONE}; } if ($min == 0 && $sec > 3) { if (!defined $hash->{HELPER}{S03DONE}) { $hash->{HELPER}{S03DONE} = 1; if ($debug =~ /collectData/x) { Log3 ($name, 1, "$name DEBUG> INFO - runTask starts data collection at the beginning of an hour"); } centralTask ($hash, 1); } } else { delete $hash->{HELPER}{S03DONE}; } return; } ################################################################ # neue Attribute zur Laufzeit hinzufügen # Device spezifische ".AttrList" überschreibt Modul AttrList ! ################################################################ sub _addDynAttr { my $hash = shift; my $type = $hash->{TYPE}; ## Attr ctrlWeatherDevX zur Laufzeit hinzufügen ################################################# my $adwds = ''; my @alldwd = devspec2array ("TYPE=DWD_OpenData"); $adwds = join ",", @alldwd if(@alldwd); my @deva = split " ", $modules{$type}{AttrList}; my $atd = 'ctrlWeatherDev'; @deva = grep {!/$atd/} @deva; for my $step (1..$weatherDevMax) { if ($step == 1) { push @deva, ($adwds ? "ctrlWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API,$adwds" : "ctrlWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API"); next; } push @deva, ($adwds ? "ctrlWeatherDev".$step.":$adwds" : ""); } $hash->{".AttrList"} = join " ", @deva; return; } ################################################################ # Zentraler Datenabruf ################################################################ sub centralTask { my $par = shift; my $evt = shift // 1; # Readings Event generieren my ($hash, $name); if (ref $par eq 'HASH') { # Standard Fn Aufruf $hash = $par; $name = $hash->{NAME}; } elsif (ref $par eq 'ARRAY') { # Array Referenz wurde übergeben $name = $par->[0]; $evt = $par->[1] // 1; # Readings Event generieren $hash = $defs{$name}; } else { Log (1, "ERROR module ".__PACKAGE__." - function >centralTask< was called with wrong data reference type: >".(ref $par)."<"); return; } my $type = $hash->{TYPE}; my $cst = [gettimeofday]; # Zyklus-Startzeit RemoveInternalTimer ($hash, 'FHEM::SolarForecast::centralTask'); RemoveInternalTimer ($hash, 'FHEM::SolarForecast::singleUpdateState'); ### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !! ########################################################################################################################## my $nscc = ReadingsVal ($name, 'nextSolCastCall', ''); # 14.03.2024 if ($nscc) { readingsSingleUpdate ($hash, 'nextRadiationAPICall', $nscc, 0); deleteReadingspec ($hash, 'nextSolCastCall'); } if (keys %{$data{$type}{$name}{aidectree}{airaw}}) { # 27.03.2024 for my $idx (sort keys %{$data{$type}{$name}{aidectree}{airaw}}) { my $val = AiRawdataVal ($hash, $idx, 'rad1h', undef); if (!defined $val) { delete $data{$type}{$name}{aidectree}{airaw}{$idx}; $val = 'aaaaaaaaaa'; } if ($val =~ /\.[0-9]{1}$/xs) { delete $data{$type}{$name}{aidectree}{airaw}{$idx}; } if ($val =~ /\.00$/xs) { my $renv = int $val; $data{$type}{$name}{aidectree}{airaw}{$idx}{rad1h} = $renv; } } } ####################################################################################################################### return if(!$init_done); setModel ($hash); # Model setzen my (undef, $disabled, $inactive) = controller ($name); return if($disabled || $inactive); # disabled / inactive if (CurrentVal ($hash, 'ctrunning', 0)) { Log3 ($name, 3, "$name - INFO - central task was called when it was already running ... end this call"); $data{$type}{$name}{current}{ctrunning} = 0; return; } 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} ); # Central Task running Statusbit return; } } my $t = time; # aktuelle Unix-Zeit my $date = strftime "%Y-%m-%d", localtime($t); # aktuelles Datum my $chour = strftime "%H", localtime($t); # aktuelle Stunde in 24h format (00-23) my $minute = strftime "%M", localtime($t); # aktuelle Minute (00-59) my $day = strftime "%d", localtime($t); # aktueller Tag (range 01 to 31) my $dayname = strftime "%a", localtime($t); # aktueller Wochentagsname my $debug = getDebug ($hash); # Debug Module $data{$type}{$name}{current}{ctrunning} = 1; # Central Task running Statusbit my $centpars = { hash => $hash, name => $name, type => $type, t => $t, date => $date, minute => $minute, chour => $chour, day => $day, dayname => $dayname, debug => $debug, lang => getLang ($hash), state => 'running', evt => 0 }; 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'; # kann durch Subs überschrieben werden! collectAllRegConsumers ($centpars); # alle Verbraucher Infos laden _specialActivities ($centpars); # zusätzliche Events generieren + Sonderaufgaben _transferWeatherValues ($centpars); # Wetterwerte übertragen readingsDelete ($hash, 'AllPVforecastsToEvent'); _getRoofTopData ($centpars); # Strahlungswerte/Forecast-Werte in solcastapi-Hash erstellen _transferAPIRadiationValues ($centpars); # Raw Erzeugungswerte aus solcastapi-Hash übertragen und Forecast mit/ohne Korrektur erstellen _calcMaxEstimateToday ($centpars); # heutigen Max PV Estimate & dessen Tageszeit ermitteln _transferInverterValues ($centpars); # WR Werte übertragen _transferMeterValues ($centpars); # Energy Meter auswerten _transferBatteryValues ($centpars); # Batteriewerte einsammeln _batSocTarget ($centpars); # Batterie Optimum Ziel SOC berechnen _createSummaries ($centpars); # Zusammenfassungen erstellen _manageConsumerData ($centpars); # Consumer Daten sammeln und Zeiten planen _estConsumptionForecast ($centpars); # Verbrauchsprognose erstellen _evaluateThresholds ($centpars); # Schwellenwerte bewerten und signalisieren _calcReadingsTomorrowPVFc ($centpars); # zusätzliche Readings Tomorrow_HourXX_PVforecast berechnen calcTodayPVdeviation ($centpars); # Vorhersageabweichung erstellen (nach Sonnenuntergang) calcValueImproves ($centpars); # neue Korrekturfaktor/Qualität und berechnen und speichern, AI anreichern saveEnergyConsumption ($centpars); # Energie Hausverbrauch speichern genStatisticReadings ($centpars); # optionale Statistikreadings erstellen userExit ($centpars); # User spezifische Funktionen ausführen setTimeTracking ($hash, $cst, 'runTimeCentralTask'); # Zyklus-Laufzeit ermitteln createReadingsFromArray ($hash, $evt); # Readings erzeugen if ($evt) { $centpars->{evt} = $evt; InternalTimer(gettimeofday()+1, "FHEM::SolarForecast::singleUpdateState", $centpars, 0); } else { $centpars->{evt} = 1; singleUpdateState ($centpars); } $data{$type}{$name}{current}{ctrunning} = 0; return; } ################################################################ # Erstellen der Stringkonfiguration # Stringhash: $data{$type}{$name}{strings} ################################################################ 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 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; $data{$type}{$name}{current}{allstringspeak} += $pp * 1000; # insgesamt installierte Peakleistung in W } else { 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; } else { return qq{Check "moduleRoofTops" -> the stringname "$is" is not defined as valid string in reading "inverterStrings"}; } } } elsif (isVictronKiUsed ($hash)) { my $invs = ReadingsVal ($name, 'inverterStrings', ''); if ($invs ne 'KI-based') { return qq{You use a KI based model. Please set only "KI-based" as String with command "set $name inverterStrings".}; } } elsif (!isVictronKiUsed ($hash)) { my $tilt = ReadingsVal ($name, 'moduleDeclination', ''); # Modul Neigungswinkel für jeden Stringbezeichner return qq{Please complete command "set $name moduleDeclination".} if(!$tilt); my ($at,$ht) = parseParams ($tilt); while (my ($key, $value) = each %$ht) { if ($key ~~ @istrings) { $data{$type}{$name}{strings}{$key}{tilt} = $value; } else { return qq{Check "moduleDeclination" -> the stringname "$key" is not defined as valid string in reading "inverterStrings"}; } } my $dir = ReadingsVal ($name, 'moduleAzimuth', ''); # Modul Ausrichtung für jeden Stringbezeichner return qq{Please complete command "set $name moduleAzimuth".} if(!$dir); my ($ad,$hd) = parseParams ($dir); my $iwrong = qq{Please check the input of set "moduleAzimuth". It seems to be wrong.}; while (my ($key, $value) = each %$hd) { if ($key ~~ @istrings) { $data{$type}{$name}{strings}{$key}{dir} = _azimuth2ident ($value) // return $iwrong; $data{$type}{$name}{strings}{$key}{azimut} = _ident2azimuth ($value) // return $iwrong; } else { return qq{Check "moduleAzimuth" -> the stringname "$key" is not defined as valid string in reading "inverterStrings"}; } } } if(!keys %{$data{$type}{$name}{strings}}) { return qq{The string configuration seems to be incomplete. \n}. qq{Please check the settings of inverterStrings, modulePeakString, moduleAzimuth, moduleDeclination }. 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; } 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; } ################################################################ # formt die Azimut Angabe in Azimut-Bezeichner um # Azimut-Bezeichner werden direkt zurück gegeben ################################################################ sub _azimuth2ident { my $az = shift; return $az if($az =~ /^[A-Za-z]*$/xs); my $id = $az == -180 ? 'N' : $az <= -158 ? 'N' : $az <= -134 ? 'NE' : $az == -135 ? 'NE' : $az <= -113 ? 'NE' : $az <= -89 ? 'E' : $az == -90 ? 'E' : $az <= -68 ? 'E' : $az <= -44 ? 'SE' : $az == -45 ? 'SE' : $az <= -23 ? 'SE' : $az <= -1 ? 'S' : $az == 0 ? 'S' : $az <= 23 ? 'S' : $az <= 44 ? 'SW' : $az == 45 ? 'SW' : $az <= 67 ? 'SW' : $az <= 89 ? 'W' : $az == 90 ? 'W' : $az <= 112 ? 'W' : $az <= 134 ? 'NW' : $az == 135 ? 'NW' : $az <= 157 ? 'NW' : $az <= 179 ? 'N' : $az == 180 ? 'N' : undef; return $id; } ################################################################ # formt einen Azimut-Bezeichner in ein Azimut um # numerische werden direkt zurück gegeben ################################################################ sub _ident2azimuth { my $id = shift; return $id if(isNumeric ($id)); my $az = $id eq 'N' ? -180 : $id eq 'NE' ? -135 : $id eq 'E' ? -90 : $id eq 'SE' ? -45 : $id eq 'S' ? 0 : $id eq 'SW' ? 45 : $id eq 'W' ? 90 : $id eq 'NW' ? 135 : undef; return $az; } ################################################################ # Steuerparameter berechnen / festlegen ################################################################ sub controller { my $name = shift; my $interval = AttrVal ($name, 'ctrlInterval', $definterval); # 0 wenn manuell gesteuert my $idval = IsDisabled ($name); my $disabled = $idval == 1 ? 1 : 0; my $inactive = $idval == 3 ? 1 : 0; return ($interval, $disabled, $inactive); } ################################################################ # Sonderaufgaben ! ################################################################ sub _specialActivities { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $date = $paref->{date}; # aktuelles Datum my $chour = $paref->{chour}; my $minute = $paref->{minute}; 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); storeReading ('LastHourPVforecast', "$pvfc Wh", $ts1); $pvrl = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$chour)."_PVreal", 0); storeReading ('LastHourPVreal', "$pvrl Wh", $ts1); $gcon = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$chour)."_GridConsumption", 0); storeReading ('LastHourGridconsumptionReal', "$gcon Wh", $ts1); ## Planungsdaten spezifisch löschen (Anfang und Ende nicht am selben Tag) ########################################################################## 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 $simpCstat = simplifyCstate (ConsumerVal ($hash, $c, "planstate", "")); if ($t > $planswitchoff && $simpCstat =~ /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'; } } ## bestimmte einmalige Aktionen ################################## $chour = int $chour; $minute = int $minute; my $aitrh = AttrVal ($name, 'ctrlAIshiftTrainStart', $aitrstartdef); # Stunde f. Start AI-Training ## Task 1 ########### if ($chour == 0 && $minute >= 0) { if (!defined $hash->{HELPER}{T1RUN}) { $hash->{HELPER}{T1RUN} = 1; Log3 ($name, 4, "$name - Daily special tasks - Task 1 started"); $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); storeReading ('LastHourPVforecast', "$pvfc Wh", $ts); $pvrl = ReadingsNum ($name, "Today_Hour24_PVreal", 0); storeReading ('LastHourPVreal', "$pvrl Wh", $ts); $gcon = ReadingsNum ($name, "Today_Hour24_GridConsumption", 0); storeReading ('LastHourGridconsumptionReal', "$gcon Wh", $ts); 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"); deleteReadingspec ($hash, "Today_PVreal"); for my $wdr (@widgetreadings) { # Array der Hilfsreadings (Attributspeicher) löschen deleteReadingspec ($hash, $wdr); } 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}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{solCastAPIcallMultiplier}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayMaxAPIcalls}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{response_message}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{idUser}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{token}; delete $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{verification_mode}; delete $data{$type}{$name}{circular}{'99'}{initdayfeedin}; delete $data{$type}{$name}{circular}{'99'}{initdaygcon}; delete $data{$type}{$name}{circular}{'99'}{initdaybatintot}; delete $data{$type}{$name}{circular}{'99'}{initdaybatouttot}; delete $data{$type}{$name}{current}{sunriseToday}; delete $data{$type}{$name}{current}{sunriseTodayTs}; delete $data{$type}{$name}{current}{sunsetToday}; delete $data{$type}{$name}{current}{sunsetTodayTs}; $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 writeCacheToFile ($hash, 'plantconfig', $plantcfg.$name); # Anlagenkonfiguration sichern Log3 ($name, 3, "$name - history day >$day< deleted"); Log3 ($name, 4, "$name - Daily special tasks - Task 1 finished"); } } else { delete $hash->{HELPER}{T1RUN}; } ## Task 2 ########### if ($chour == 0 && $minute >= 2) { if (!defined $hash->{HELPER}{T2RUN}) { $hash->{HELPER}{T2RUN} = 1; Log3 ($name, 4, "$name - Daily special tasks - Task 2 started"); for my $c (keys %{$data{$type}{$name}{consumers}}) { # Planungsdaten regulär löschen 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'; } writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben Log3 ($name, 4, "$name - Daily special tasks - Task 2 finished"); } } else { delete $hash->{HELPER}{T2RUN}; } ## Task 3 ########### if ($chour == 0 && $minute >= 5) { if (!defined $hash->{HELPER}{T3RUN}) { $hash->{HELPER}{T3RUN} = 1; Log3 ($name, 4, "$name - Daily special tasks - Task 3 started"); __createAdditionalEvents ($paref); # zusätzliche Events erzeugen - PV Vorhersage bis Ende des kommenden Tages __delObsoleteAPIData ($paref); # Bereinigung obsoleter Daten im solcastapi Hash Log3 ($name, 4, "$name - Daily special tasks - Task 3 finished"); } } else { delete $hash->{HELPER}{T3RUN}; } ## Task 4 ########### if ($chour == 0 && $minute >= 9) { if (!defined $hash->{HELPER}{T4RUN}) { $hash->{HELPER}{T4RUN} = 1; Log3 ($name, 4, "$name - Daily special tasks - Task 4 started"); __deleteHiddenReadings ($paref); # verstecktes Steuerungsreading löschen periodicWriteMemcache ($hash, 'bckp'); # Backup Files erstellen und alte Versionen löschen Log3 ($name, 4, "$name - Daily special tasks - Task 4 finished"); } } else { delete $hash->{HELPER}{T4RUN}; } ## Task 5 ########### if ($chour == $aitrh && $minute >= 15) { if (!defined $hash->{HELPER}{T5RUN}) { $hash->{HELPER}{T5RUN} = 1; Log3 ($name, 4, "$name - Daily special tasks - Task 5 started"); aiDelRawData ($paref); # KI Raw Daten löschen welche die maximale Haltezeit überschritten haben $paref->{taa} = 1; aiAddInstance ($paref); # AI füllen, trainieren und sichern delete $paref->{taa}; Log3 ($name, 4, "$name - Daily special tasks - Task 5 finished"); } } else { delete $hash->{HELPER}{T5RUN}; } return; } ############################################################################# # versteckte Steuerungsreadings löschen ############################################################################# sub __deleteHiddenReadings { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; for my $n (1..24) { $n = sprintf "%02d", $n; deleteReadingspec ($hash, ".pvCorrectionFactor_${n}_cloudcover"); deleteReadingspec ($hash, ".pvCorrectionFactor_${n}_apipercentil"); deleteReadingspec ($hash, ".signaldone_${n}"); if (ReadingsVal ($name, "pvCorrectionFactor_Auto", "off") =~ /on/xs) { deleteReadingspec ($hash, "pvCorrectionFactor_${n}.*"); } } return; } ############################################################################# # zusätzliche Events erzeugen - PV Vorhersage bis Ende des kommenden Tages ############################################################################# sub __createAdditionalEvents { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; for my $idx (sort keys %{$data{$type}{$name}{nexthours}}) { my $nhts = NexthoursVal ($hash, $idx, 'starttime', undef); my $nhfc = NexthoursVal ($hash, $idx, 'pvfc', undef); next if(!defined $nhts || !defined $nhfc); my ($dt, $h) = $nhts =~ /([\w-]+)\s(\d{2})/xs; storeReading ('AllPVforecastsToEvent', "$nhfc Wh", $dt." ".$h.":59:59"); } return; } ############################################################################# # solcastapi Hash veraltete Daten löschen ############################################################################# sub __delObsoleteAPIData { my $paref = shift; my $hash = $paref->{hash}; 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 for my $scd (sort keys %{$data{$type}{$name}{solcastapi}{$idx}}) { my $ds = timestringToTimestamp ($scd); delete $data{$type}{$name}{solcastapi}{$idx}{$scd} if ($ds && $ds < $refts); } } for my $idx (keys %{$data{$type}{$name}{solcastapi}{'?All'}}) { # Wetterindexe löschen delete $data{$type}{$name}{solcastapi}{'?All'}{$idx} if($idx =~ /^fc?([0-9]{1,2})_?([0-9]{1,2})$/xs); } my @as = split ",", ReadingsVal($name, 'inverterStrings', ''); return if(!scalar @as); for my $k (keys %{$data{$type}{$name}{strings}}) { # veraltete Strings aus Strings-Hash löschen next if($k =~ /\?All/); next if($k ~~ @as); delete $data{$type}{$name}{strings}{$k}; Log3 ($name, 2, "$name - obsolete PV-String >$k< was deleted from Strings-Hash"); } return; } ################################################################ # Wetter Werte aus dem angebenen Wetterdevice extrahieren ################################################################ sub _transferWeatherValues { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $t = $paref->{t}; # Epoche Zeit my $chour = $paref->{chour}; my ($valid, $fcname, $apiu) = isWeatherDevValid ($hash, 'ctrlWeatherDev1'); # Standard Weather Forecast Device return if(!$valid); my $type = $paref->{type}; delete $data{$type}{$name}{weatherdata}; # Wetterdaten Hash löschen $paref->{apiu} = $apiu; # API wird verwendet $paref->{fcname} = $fcname; __sunRS ($paref); # Sonnenauf- und untergang delete $paref->{fcname}; delete $paref->{apiu}; my ($fctime, $fctimets); # Alter der DWD Daten if (!$apiu) { $fctime = ReadingsVal ($fcname, 'fc_time', '-'); $fctimets = timestringToTimestamp ($fctime); } else { $fctime = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_time', '-'); $fctimets = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_timestamp', '-'); } $data{$type}{$name}{current}{dwdWfchAge} = $fctime; $data{$type}{$name}{current}{dwdWfchAgeTS} = $fctimets; for my $step (1..$weatherDevMax) { $paref->{step} = $step; __readDataWeather ($paref); # Wetterdaten in einen Hash einlesen delete $paref->{step}; } __mergeDataWeather ($paref); # Wetterdaten zusammenfügen for my $num (0..46) { my ($fd, $fh) = calcDayHourMove ($chour, $num); last if($fd > 1); my $wid = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{ww}; # signifikantes Wetter = Wetter ID my $wwd = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{wwd}; # Wetter Beschreibung my $neff = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{neff}; # Effektive Wolkendecke my $rr1c = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{rr1c}; # Gesamtniederschlag (1-stündig) letzte 1 Stunde my $temp = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{ttt}; # Außentemperatur my $don = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{don}; # Tag/Nacht-Grenze my $nhtstr = "NextHour".sprintf "%02d", $num; $data{$type}{$name}{nexthours}{$nhtstr}{weatherid} = $wid; $data{$type}{$name}{nexthours}{$nhtstr}{cloudcover} = $neff; $data{$type}{$name}{nexthours}{$nhtstr}{totalrain} = $rr1c; $data{$type}{$name}{nexthours}{$nhtstr}{rainrange} = $rr1c; $data{$type}{$name}{nexthours}{$nhtstr}{temp} = $temp; $data{$type}{$name}{nexthours}{$nhtstr}{DoN} = $don; my $fh1 = $fh + 1; # = hod 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} = $wwd; $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{wcc} = $neff; $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{rr1c} = $rr1c; $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{temp} = $temp; if ($num == 0) { # aktuelle Außentemperatur $data{$type}{$name}{current}{temp} = $temp; } } if ($fd == 0 && $fh1) { # Weather in pvHistory speichern $paref->{val} = $wid; $paref->{histname} = 'weatherid'; $paref->{nhour} = sprintf "%02d", $fh1; setPVhistory ($paref); $paref->{val} = $neff // 0; $paref->{histname} = 'weathercloudcover'; setPVhistory ($paref); $paref->{val} = $rr1c; $paref->{histname} = 'totalrain'; setPVhistory ($paref); $paref->{val} = $temp; $paref->{histname} = 'temperature'; setPVhistory ($paref); $paref->{val} = $don; $paref->{histname} = 'DoN'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{val}; delete $paref->{nhour}; } } return; } ################################################################ # lese Wetterdaten aus Device im Attribut ctrlWeatherDevX # X = laufende Schleifenvariable $step ################################################################ sub __readDataWeather { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $chour = $paref->{chour}; # aktuelles Datum my $type = $paref->{type}; my $step = $paref->{step}; my ($valid, $fcname, $apiu) = isWeatherDevValid ($hash, 'ctrlWeatherDev'.$step); # Weather Forecast Device return if(!$valid); if ($apiu) { # eine API wird verwendet $paref->{fcname} = $fcname; ___readDataWeatherAPI ($paref); delete $paref->{fcname}; return; } my $err = checkdwdattr ($name, $fcname, \@dweattrmust); $paref->{state} = $err if($err); debugLog ($paref, 'collectData', "collect Weather data step $step - device: $fcname =>"); for my $n (0..46) { my ($fd, $fh) = calcDayHourMove ($chour, $n); last if($fd > 1); my $wid = ReadingsNum ($fcname, "fc${fd}_${fh}_ww", undef); # Signifikantes Wetter zum Vorhersagezeitpunkt my $wwd = ReadingsVal ($fcname, "fc${fd}_${fh}_wwd", ''); # Wetter Beschreibung my $neff = ReadingsNum ($fcname, "fc${fd}_${fh}_Neff", 0); # Effektiver Bedeckungsgrad zum Vorhersagezeitpunkt my $temp = ReadingsNum ($fcname, "fc${fd}_${fh}_TTT", 0); # 2m-Temperatur zum Vorhersagezeitpunkt my $sunup = ReadingsNum ($fcname, "fc${fd}_${fh}_SunUp", 0); # 1 - Tag my $fh1 = $fh + 1; my $fd1 = $fd; if ($fh1 == 24) { $fh1 = 0; $fd1++; } last if($fd1 > 1); my $rr1c = ReadingsNum ($fcname, "fc${fd1}_${fh1}_RR1c", 0); # Gesamtniederschlag (1-stündig) letzte 1 Stunde if (defined $wid && !$sunup) { $wid += 100; } debugLog ($paref, 'collectData', "Weather $step: fc${fd}_${fh}, don: $sunup, ww: ".(defined $wid ? $wid : '').", RR1c: $rr1c, TTT: $temp, Neff: $neff"); $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{ww} = $wid; $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{wwd} = $wwd; $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{neff} = $neff; $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{rr1c} = $rr1c; $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{ttt} = $temp; $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{don} = $sunup; } return; } ################################################################ # lese Wetterdaten aus API Speicher (solcastapi) ################################################################ sub ___readDataWeatherAPI { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $step = $paref->{step}; my $fcname = $paref->{fcname}; debugLog ($paref, 'collectData', "collect Weather data step $step - API: $fcname =>"); for my $idx (sort keys %{$data{$type}{$name}{solcastapi}{'?All'}}) { if ($idx =~ /^fc?([0-9]{1,2})_?([0-9]{1,2})$/xs) { # valider Weather API Index my $rr1c = $data{$type}{$name}{solcastapi}{'?All'}{$idx}{rr1c}; my $wid = $data{$type}{$name}{solcastapi}{'?All'}{$idx}{ww}; my $neff = $data{$type}{$name}{solcastapi}{'?All'}{$idx}{neff}; my $don = $data{$type}{$name}{solcastapi}{'?All'}{$idx}{don}; my $ttt = $data{$type}{$name}{solcastapi}{'?All'}{$idx}{ttt}; $data{$type}{$name}{weatherdata}{$idx}{$step}{ww} = $wid if(defined $wid); $data{$type}{$name}{weatherdata}{$idx}{$step}{neff} = $neff if(defined $neff); $data{$type}{$name}{weatherdata}{$idx}{$step}{rr1c} = $rr1c if(defined $rr1c); $data{$type}{$name}{weatherdata}{$idx}{$step}{ttt} = $ttt if(defined $ttt); $data{$type}{$name}{weatherdata}{$idx}{$step}{don} = $don if(defined $don); debugLog ($paref, 'collectData', "Weather $step: $idx". ", don: ". (defined $don ? $don : ''). ", ww: ". (defined $wid ? $wid : ''). ", RR1c: ".(defined $rr1c ? $rr1c : ''). ", TTT: ". (defined $ttt ? $ttt : ''). ", Neff: ".(defined $neff ? $neff : '') ); } } return; } ################################################################ # Wetterdaten mergen ################################################################ sub __mergeDataWeather { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; debugLog ($paref, 'collectData', "merge Weather data =>"); my $ds = 0; for my $wd (1..$weatherDevMax) { my ($valid, $fcname, $apiu) = isWeatherDevValid ($hash, 'ctrlWeatherDev'.$wd); # Weather Forecast Device $ds++ if($valid); } my ($q, $m) = (0,0); for my $key (sort keys %{$data{$type}{$name}{weatherdata}}) { my ($z, $neff, $rr1c, $temp) = (0,0,0,0); $data{$type}{$name}{weatherdata}{$key}{merge}{don} = $data{$type}{$name}{weatherdata}{$key}{1}{don}; $data{$type}{$name}{weatherdata}{$key}{merge}{ww} = $data{$type}{$name}{weatherdata}{$key}{1}{ww}; $data{$type}{$name}{weatherdata}{$key}{merge}{wwd} = $data{$type}{$name}{weatherdata}{$key}{1}{wwd}; $data{$type}{$name}{weatherdata}{$key}{merge}{neff} = $data{$type}{$name}{weatherdata}{$key}{1}{neff}; $data{$type}{$name}{weatherdata}{$key}{merge}{rr1c} = $data{$type}{$name}{weatherdata}{$key}{1}{rr1c}; $data{$type}{$name}{weatherdata}{$key}{merge}{ttt} = $data{$type}{$name}{weatherdata}{$key}{1}{ttt}; for my $step (1..$ds) { $q++; my $n = $data{$type}{$name}{weatherdata}{$key}{$step}{neff}; my $r = $data{$type}{$name}{weatherdata}{$key}{$step}{rr1c}; my $t = $data{$type}{$name}{weatherdata}{$key}{$step}{ttt}; next if(!isNumeric ($n) || !isNumeric ($r) || !isNumeric ($t)); $neff += $n; $rr1c += $r; $temp += $t; $z++; $m++; } next if(!$z); $data{$type}{$name}{weatherdata}{$key}{merge}{neff} = sprintf "%.0f", ($neff / $z); $data{$type}{$name}{weatherdata}{$key}{merge}{rr1c} = sprintf "%.2f", ($rr1c / $z); $data{$type}{$name}{weatherdata}{$key}{merge}{ttt} = sprintf "%.2f", ($temp / $z); debugLog ($paref, 'collectData', "Weather merged: $key, ". "don: $data{$type}{$name}{weatherdata}{$key}{merge}{don}, ". "ww: ".(defined $data{$type}{$name}{weatherdata}{$key}{1}{ww} ? $data{$type}{$name}{weatherdata}{$key}{1}{ww} : '').", ". "RR1c: $data{$type}{$name}{weatherdata}{$key}{merge}{rr1c}, ". "TTT: $data{$type}{$name}{weatherdata}{$key}{merge}{ttt}, ". "Neff: $data{$type}{$name}{weatherdata}{$key}{merge}{neff}"); } debugLog ($paref, 'collectData', "Number of Weather datasets mergers - delivered: $q, merged: $m, failures: ".($q - $m)); return; } ################################################################ # Sonnenauf- und untergang bei gesetzten global # latitude/longitude Koordinaten berechnen, sonst aus DWD # Device extrahieren ################################################################ sub __sunRS { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $t = $paref->{t}; # aktuelle Zeit my $fcname = $paref->{fcname}; my $type = $paref->{type}; my $date = $paref->{date}; # aktuelles Datum my $apiu = $paref->{apiu}; my ($fc0_sr, $fc0_ss, $fc1_sr, $fc1_ss); my ($cset, undef, undef, undef) = locCoordinates(); debugLog ($paref, 'collectData', "collect sunrise/sunset times - device: $fcname =>"); if ($cset) { my $alt = 'HORIZON=-0.833'; # default from https://metacpan.org/release/JFORGET/DateTime-Event-Sunrise-0.0505/view/lib/DateTime/Event/Sunrise.pm $fc0_sr = substr (sunrise_abs_dat ($t, $alt), 0, 5); # SunRise heute $fc0_ss = substr (sunset_abs_dat ($t, $alt), 0, 5); # SunSet heute $fc1_sr = substr (sunrise_abs_dat ($t + 86400, $alt), 0, 5); # SunRise morgen $fc1_ss = substr (sunset_abs_dat ($t + 86400, $alt), 0, 5); # SunSet morgen } else { if (!$apiu) { # Daten aus DWD Device holen $fc0_sr = ReadingsVal ($fcname, 'fc0_SunRise', '23:59'); $fc0_ss = ReadingsVal ($fcname, 'fc0_SunSet', '00:00'); $fc1_sr = ReadingsVal ($fcname, 'fc1_SunRise', '23:59'); $fc1_ss = ReadingsVal ($fcname, 'fc1_SunSet', '00:00'); } else { # Daten aus solcastapi (API) holen $fc0_sr = substr (SolCastAPIVal ($hash, '?All', 'sunrise', 'today', '23:59:59'), 0, 5); $fc0_ss = substr (SolCastAPIVal ($hash, '?All', 'sunset', 'today', '00:00:00'), 0, 5); $fc1_sr = substr (SolCastAPIVal ($hash, '?All', 'sunrise', 'tomorrow', '23:59:59'), 0, 5); $fc1_ss = substr (SolCastAPIVal ($hash, '?All', 'sunset', 'tomorrow', '00:00:00'), 0, 5); } } $data{$type}{$name}{current}{sunriseToday} = $date.' '.$fc0_sr.':00'; $data{$type}{$name}{current}{sunriseTodayTs} = timestringToTimestamp ($date.' '.$fc0_sr.':00'); $data{$type}{$name}{current}{sunsetToday} = $date.' '.$fc0_ss.':00'; $data{$type}{$name}{current}{sunsetTodayTs} = timestringToTimestamp ($date.' '.$fc0_ss.':00'); debugLog ($paref, 'collectData', "sunrise/sunset today: $fc0_sr / $fc0_ss, sunrise/sunset tomorrow: $fc1_sr / $fc1_ss"); storeReading ('Today_SunRise', $fc0_sr); storeReading ('Today_SunSet', $fc0_ss); storeReading ('Tomorrow_SunRise', $fc1_sr); storeReading ('Tomorrow_SunSet', $fc1_ss); my $fc0_sr_mm = sprintf "%02d", (split ":", $fc0_sr)[0]; my $fc0_ss_mm = sprintf "%02d", (split ":", $fc0_ss)[0]; my $fc1_sr_mm = sprintf "%02d", (split ":", $fc1_sr)[0]; my $fc1_ss_mm = sprintf "%02d", (split ":", $fc1_ss)[0]; return ($fc0_sr_mm, $fc0_ss_mm, $fc1_sr_mm, $fc1_ss_mm); } ################################################################ # Strahlungsvorhersage Werte aus solcastapi-Hash # übertragen und PV Vorhersage berechnen / in Nexthours # speichern ################################################################ sub _transferAPIRadiationValues { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $t = $paref->{t}; # Epoche Zeit my $chour = $paref->{chour}; my $date = $paref->{date}; 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 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, $lang))[1]; my $nhtstr = 'NextHour'.sprintf "%02d", $num; my ($wtday, $hod) = $wantdt =~ /(\d{2})\s(\d{2}):/xs; $hod = sprintf "%02d", int $hod + 1; # Stunde des Tages my $rad1h = SolCastAPIVal ($hash, '?All', $wantdt, 'Rad1h', undef); $paref->{wantdt} = $wantdt; $paref->{wantts} = $wantts; $paref->{wtday} = $wtday; $paref->{hod} = $hod; $paref->{nhtstr} = $nhtstr; $paref->{num} = $num; $paref->{fh1} = $fh1; $paref->{fd} = $fd; $data{$type}{$name}{nexthours}{$nhtstr}{starttime} = $wantdt; $data{$type}{$name}{nexthours}{$nhtstr}{hourofday} = $hod; $data{$type}{$name}{nexthours}{$nhtstr}{today} = $fd == 0 ? 1 : 0; $data{$type}{$name}{nexthours}{$nhtstr}{rad1h} = $rad1h; my $sunalt = HistoryVal ($hash, $wtday, $hod, 'sunalt', undef); my $sunaz = HistoryVal ($hash, $wtday, $hod, 'sunaz', undef); if (!defined $sunalt || !defined $sunaz) { __calcSunPosition ($paref); $sunalt = HistoryVal ($hash, $wtday, $hod, 'sunalt', undef); $sunaz = HistoryVal ($hash, $wtday, $hod, 'sunaz', undef); } if (defined $sunaz) { $data{$type}{$name}{nexthours}{$nhtstr}{sunaz} = $sunaz; } else { $sunaz = NexthoursVal ($hash, $nhtstr, 'sunaz', 0); } if (defined $sunalt) { $data{$type}{$name}{nexthours}{$nhtstr}{sunalt} = $sunalt; } else { $sunalt = NexthoursVal ($hash, $nhtstr, 'sunalt', 0); } $paref->{sabin} = sunalt2bin ($sunalt); my $est = __calcPVestimates ($paref); my ($msg, $pvaifc) = aiGetResult ($paref); # KI Entscheidungen abfragen $data{$type}{$name}{nexthours}{$nhtstr}{pvapifc} = $est; # durch API gelieferte PV Forecast delete $paref->{fd}; delete $paref->{fh1}; delete $paref->{num}; delete $paref->{nhtstr}; delete $paref->{hod}; delete $paref->{wtday}; delete $paref->{wantdt}; delete $paref->{wantts}; delete $paref->{sabin}; my $useai = 0; my $pvfc; if ($msg eq 'accurate' || $msg eq 'spreaded') { my $aivar = 100; $aivar = 100 * $pvaifc / $est if($est); if ($msg eq 'accurate' && $aivar >= $aiAccLowLim && $aivar <= $aiAccUpLim) { # KI liefert 'accurate' Treffer -> verwenden $data{$type}{$name}{nexthours}{$nhtstr}{aihit} = 1; $pvfc = $pvaifc; $useai = 1; debugLog ($paref, 'aiData', qq{AI Hit - accurate result found -> variance $aivar, hod: $hod, Rad1h: $rad1h, pvfc: $pvfc Wh}); } if ($msg eq 'spreaded' && $aivar >= $aiSpreadLowLim && $aivar <= $aiSpreadUpLim) { # Abweichung AI von Standardvorhersage begrenzen $data{$type}{$name}{nexthours}{$nhtstr}{aihit} = 1; $pvfc = $pvaifc; $useai = 1; debugLog ($paref, 'aiData', qq{AI Hit - spreaded result found and is in tolerance -> hod: $hod, Rad1h: $rad1h, pvfc: $pvfc Wh}); } } else { debugLog ($paref, 'aiData', $msg); } if ($useai) { $data{$type}{$name}{nexthours}{$nhtstr}{pvaifc} = $pvaifc; # durch AI gelieferte PV Forecast } else { delete $data{$type}{$name}{nexthours}{$nhtstr}{pvaifc}; $data{$type}{$name}{nexthours}{$nhtstr}{aihit} = 0; $pvfc = $est; debugLog ($paref, 'aiData', "use PV from API (no AI or AI result tolerance overflow) -> hod: $hod, Rad1h: ".(defined $rad1h ? $rad1h : '-').", pvfc: $pvfc Wh"); } $data{$type}{$name}{nexthours}{$nhtstr}{pvfc} = $pvfc; # resultierende PV Forecast zuweisen 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}{pvapifc} = NexthoursVal ($hash, $nhtstr, 'pvapifc', undef); $data{$type}{$name}{circular}{sprintf "%02d",$fh1}{pvfc} = $pvfc; $data{$type}{$name}{circular}{sprintf "%02d",$fh1}{pvaifc} = NexthoursVal ($hash, $nhtstr, 'pvaifc', undef); $data{$type}{$name}{circular}{sprintf "%02d",$fh1}{aihit} = NexthoursVal ($hash, $nhtstr, 'aihit', 0); } if ($fd == 0 && int $pvfc > 0) { # Vorhersagedaten des aktuellen Tages zum manuellen Vergleich in Reading speichern storeReading ('Today_Hour'.sprintf ("%02d",$fh1).'_PVforecast', "$pvfc Wh"); } if ($fd == 0 && $fh1) { $paref->{nhour} = sprintf "%02d", $fh1; $paref->{val} = $pvfc; $paref->{histname} = 'pvfc'; setPVhistory ($paref); $paref->{val} = $rad1h; $paref->{histname} = 'radiation'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{val}; delete $paref->{nhour}; } } storeReading ('.lastupdateForecastValues', $t); # Statusreading letzter update return; } ################################################################ # Ermittlung der Sonnenpositionen # Az,Alt = Azimuth und Höhe (in Dezimalgrad) des Körpers # über dem Horizont ################################################################ sub __calcSunPosition { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $t = $paref->{t}; # Epoche Zeit my $chour = $paref->{chour}; my $wtday = $paref->{wtday}; my $num = $paref->{num}; my $nhtstr = $paref->{nhtstr}; my ($fd, $fh) = calcDayHourMove ($chour, $num); last if($fd > 1); my $tstr = (timestampToTimestring ($t + ($num * 3600)))[3]; my ($date, $h, $m, $s) = split /[ :]/, $tstr; $tstr = $date.' '.$h.':30:00'; my ($az, $alt); eval { $az = sprintf "%.0f", FHEM::Astro::Get (undef, 'global', 'text', 'SunAz', $tstr); # statt Astro_Get geht auch FHEM::Astro::Get $alt = sprintf "%.0f", FHEM::Astro::Get (undef, 'global', 'text', 'SunAlt', $tstr); }; if ($@) { Log3 ($name, 1, "$name - ERROR - $@"); return; } my $hodn = sprintf "%02d", $h + 1; $data{$type}{$name}{nexthours}{$nhtstr}{sunaz} = $az; $data{$type}{$name}{nexthours}{$nhtstr}{sunalt} = $alt; debugLog ($paref, 'collectData', "Sun position: day: $wtday, hod: $hodn, $tstr, azimuth: $az, altitude: $alt"); if ($fd == 0 && $hodn) { # Sun Position in pvHistory speichern $paref->{nhour} = $hodn; $paref->{val} = $az; $paref->{histname} = 'sunaz'; setPVhistory ($paref); $paref->{val} = $alt; $paref->{histname} = 'sunalt'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{val}; delete $paref->{nhour}; } return; } ######################################################################### # API Erzeugungsprognose mit gewählter Autokorrekturmethode anpassen ######################################################################### sub __calcPVestimates { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $wantdt = $paref->{wantdt}; my $hod = $paref->{hod}; my $fd = $paref->{fd}; my $num = $paref->{num}; my $debug = $paref->{debug}; my $reld = $fd == 0 ? "today" : $fd == 1 ? "tomorrow" : "unknown"; my $totalrain = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "totalrain", 0); # Gesamtniederschlag während der letzten Stunde kg/m2 my $cloudcover = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "cloudcover", 0); # effektive Wolkendecke nächste Stunde X my $temp = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "temp", $tempbasedef); # vorhergesagte Temperatur Stunde X my ($acu, $aln) = isAutoCorrUsed ($name); $paref->{cloudcover} = $cloudcover; my ($hc, $hq) = ___readCandQ ($paref); # liest den anzuwendenden Korrekturfaktor delete $paref->{cloudcover}; my ($lh,$sq,$peakloss, $modtemp); my $pvsum = 0; my $peaksum = 0; for my $string (sort keys %{$data{$type}{$name}{strings}}) { my $peak = StringVal ($hash, $string, 'peak', 0); # String Peak (kWp) if ($acu =~ /on_complex/xs) { $paref->{peak} = $peak; $paref->{cloudcover} = $cloudcover; $paref->{temp} = $temp; ($peakloss, $modtemp) = ___calcPeaklossByTemp ($paref); # Reduktion Peakleistung durch Temperaturkoeffizienten der Module (vorzeichengehaftet) $peak += $peakloss; delete $paref->{peak}; delete $paref->{cloudcover}; delete $paref->{temp}; } $peak *= 1000; my $est = SolCastAPIVal ($hash, $string, $wantdt, 'pv_estimate50', 0) * $hc; # Korrekturfaktor anwenden my $pv = sprintf "%.1f", $est; if ($debug =~ /radiationProcess/xs) { $lh = { # Log-Hash zur Ausgabe "modulePeakString" => $peak. " W", "Estimated PV generation (raw)" => $est. " Wh", "Estimated PV generation (calc)" => $pv. " Wh", }; if ($acu =~ /on_complex/xs) { $lh->{"Module Temp (calculated)"} = $modtemp. " °C"; $lh->{"Win(+)/Loss(-) String Peak Power by Temp"} = $peakloss." kWp"; } $sq = q{}; for my $idx (sort keys %{$lh}) { $sq .= $idx." => ".$lh->{$idx}."\n"; } Log3 ($name, 1, "$name DEBUG> PV API estimate for $reld Hour ".sprintf ("%02d", $hod)." string $string ->\n$sq"); } $pvsum += $pv; $peaksum += $peak; } $data{$type}{$name}{current}{allstringspeak} = $peaksum; # temperaturbedingte Korrektur der installierten Peakleistung in W $pvsum = $peaksum if($peaksum && $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; # PV Vorhersage auf WR Kapazität begrenzen debugLog ($paref, "radiationProcess", "PV forecast start time $wantdt limited to $pvsum Watt due to inverter capacity"); } my $logao = qq{}; $paref->{pvsum} = $pvsum; $paref->{peaksum} = $peaksum; ($pvsum, $logao) = ___70percentRule ($paref); if ($debug =~ /radiationProcess/xs) { $lh = { # Log-Hash zur Ausgabe "Starttime" => $wantdt, "Forecasted temperature" => $temp." °C", "Cloudcover" => $cloudcover, "Total Rain last hour" => $totalrain." kg/m2", "PV Correction mode" => ($acu ? $acu : 'no'), "PV correction factor" => $hc, "PV correction quality" => $hq, "PV generation forecast" => $pvsum." Wh ".$logao, }; $sq = q{}; for my $idx (sort keys %{$lh}) { $sq .= $idx." => ".$lh->{$idx}."\n"; } Log3 ($name, 1, "$name DEBUG> PV API estimate for $reld Hour ".sprintf ("%02d", $hod)." summary: \n$sq"); } return $pvsum; } ###################################################################### # Complex: # Liest bewölkungsabhängige Korrekturfaktor/Qualität und # speichert die Werte im Nexthours / pvHistory Hash # # Simple: # Liest Korrekturfaktor/Qualität aus pvCircular simple und # speichert die Werte im Nexthours / pvHistory Hash ###################################################################### sub ___readCandQ { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $num = $paref->{num}; my $fh1 = $paref->{fh1}; my $fd = $paref->{fd}; my $cc = $paref->{cloudcover}; my $sabin = $paref->{sabin}; my ($acu, $aln) = isAutoCorrUsed ($name); # Autokorrekturmodus my $sunalt = NexthoursVal ($hash, "NextHour".sprintf("%02d",$num), 'sunalt', undef); # Sun Altitude my $hc = ReadingsNum ($name, 'pvCorrectionFactor_'.sprintf("%02d",$fh1), 1.00); # Voreinstellung RAW-Korrekturfaktor my $hq = '-'; # keine Qualität definiert my $crang = 'simple'; delete $data{$type}{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}{cloudrange}; if ($acu =~ /on_complex/xs) { # Autokorrektur complex soll genutzt werden $crang = cloud2bin ($cc); # Range errechnen ($hc, $hq) = CircularSunCloudkorrVal ($hash, sprintf("%02d",$fh1), $sabin, $crang, undef); # Korrekturfaktor/Qualität der Stunde des Tages (complex) $hq //= '-'; $hc //= 1; # Korrekturfaktor = 1 (keine Korrektur) # keine Qualität definiert $hc = 1 if(1 * $hc == 0); # 0.0-Werte ignorieren (Schleifengefahr) $data{$type}{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}{cloudrange} = $crang; } elsif ($acu =~ /on_simple/xs) { ($hc, $hq) = CircularSunCloudkorrVal ($hash, sprintf("%02d",$fh1), $sabin, 'simple', undef); # Korrekturfaktor/Qualität der Stunde des Tages (simple) $hq //= '-'; $hc //= 1; # Korrekturfaktor = 1 $hc = 1 if(1 * $hc == 0); # 0.0-Werte ignorieren (Schleifengefahr) } else { # keine Autokorrektur ($hc, $hq) = CircularSunCloudkorrVal ($hash, sprintf("%02d",$fh1), $sabin, 'simple', undef); # Korrekturfaktor/Qualität der Stunde des Tages (simple) $hq //= '-'; $hc = 1; } $hc = sprintf "%.2f", $hc; debugLog ($paref, 'pvCorrectionRead', "read parameters - fd: $fd, hod: ".sprintf("%02d",$fh1).", Sun Altitude Bin: $sabin, Cloud range: $crang, corrf: $hc, quality: $hq"); $data{$type}{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}{pvcorrf} = $hc."/".$hq; if($fd == 0 && $fh1) { $paref->{val} = $hc.'/'.$hq; $paref->{nhour} = sprintf "%02d", $fh1; $paref->{histname} = 'pvcorrfactor'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{val}; delete $paref->{nhour}; } return ($hc, $hq); } ################################################################### # 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 # 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: # # Leistungsverlust = -0,4%/K * 45K * 10 kWp = 1,8 kWp # # https://www.enerix.de/photovoltaiklexikon/temperaturkoeffizient/ # ################################################################### sub ___calcPeaklossByTemp { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; 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 * ($modtemp - $tempbasedef) * $peak / 100; return ($peakloss, $modtemp); } ################################################################ # 70% Regel kalkulieren ################################################################ sub ___70percentRule { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $pvsum = $paref->{pvsum}; my $peaksum = $paref->{peaksum}; 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) { $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 ################################################################ sub _calcMaxEstimateToday { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $date = $paref->{date}; my $maxest = 0; my $maxtim = '-'; 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); storeReading ('Today_MaxPVforecast', $maxest.' Wh'); storeReading ('Today_MaxPVforecastTime', $maxtim); return; } ################################################################ # Werte Inverter Device ermitteln und übertragen ################################################################ 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 $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); my $pvuf = $pvunit =~ /^kW$/xi ? 1000 : 1; 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 storeReading ('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) debugLog ($paref, "collectData", "collect Inverter data - device: $indev =>"); debugLog ($paref, "collectData", "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 ? $paref->{val} = $etotal; $paref->{nhour} = sprintf "%02d", $nhour; $paref->{histname} = 'etotal'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; 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 $ethishour = 0 if($ethishour < 0); storeReading ('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 my ($acu, $aln) = isAutoCorrUsed ($name); $paref->{val} = $ethishour; $paref->{nhour} = sprintf "%02d", $nhour; $paref->{histname} = 'pvrl'; $paref->{pvrlvd} = $aln; # 1: beim Learning berücksichtigen, 0: nicht setPVhistory ($paref); delete $paref->{pvrlvd}; delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; return; } ################################################################ # Werte Meter Device ermitteln und übertragen ################################################################ sub _transferMeterValues { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $t = $paref->{t}; my $chour = $paref->{chour}; 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); if ($h->{conprice}) { # Bezugspreis (Arbeitspreis) pro kWh my @acp = split ":", $h->{conprice}; if (scalar(@acp) == 3) { $data{$type}{$name}{current}{ePurchasePrice} = ReadingsNum ($acp[0], $acp[1], 0); $data{$type}{$name}{current}{ePurchasePriceCcy} = $acp[2]; } if (scalar(@acp) == 2) { if (isNumeric($acp[0])) { $data{$type}{$name}{current}{ePurchasePrice} = $acp[0]; $data{$type}{$name}{current}{ePurchasePriceCcy} = $acp[1]; } else { $data{$type}{$name}{current}{ePurchasePrice} = ReadingsNum ($medev, $acp[0], 0); $data{$type}{$name}{current}{ePurchasePriceCcy} = $acp[1]; } } } if ($h->{feedprice}) { # Einspeisevergütung pro kWh my @afp = split ":", $h->{feedprice}; if (scalar(@afp) == 3) { $data{$type}{$name}{current}{eFeedInTariff} = ReadingsNum ($afp[0], $afp[1], 0); $data{$type}{$name}{current}{eFeedInTariffCcy} = $afp[2]; } if (scalar(@afp) == 2) { if (isNumeric($afp[0])) { $data{$type}{$name}{current}{eFeedInTariff} = $afp[0]; $data{$type}{$name}{current}{eFeedInTariffCcy} = $afp[1]; } else { $data{$type}{$name}{current}{eFeedInTariff} = ReadingsNum ($medev, $afp[0], 0); $data{$type}{$name}{current}{eFeedInTariffCcy} = $afp[1]; } } } $gfunit //= $gcunit; $gcunit //= $gfunit; 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); } storeReading ('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 storeReading ('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 $ftuf = $ftunit =~ /^kWh$/xi ? 1000 : 1; my $fitotal = ReadingsNum ($medev, $ft, 0) * $ftuf; # Einspeisung total (Wh) $data{$type}{$name}{circular}{99}{gridcontotal} = $gctotal; # Total Netzbezug speichern $data{$type}{$name}{circular}{99}{feedintotal} = $fitotal; # Total Feedin speichern debugLog ($paref, "collectData", "collect Meter data - device: $medev =>"); debugLog ($paref, "collectData", "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 $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 CircularVal ($hash, 99, 'initdaygcon', undef)) { $docon = 1; } else { $data{$type}{$name}{circular}{99}{initdaygcon} = $gctotal; } } elsif (!defined CircularVal ($hash, 99, 'initdaygcon', undef)) { $data{$type}{$name}{circular}{99}{initdaygcon} = $gctotal - $gcdaypast - ReadingsNum ($name, "Today_Hour".sprintf("%02d",$chour+1)."_GridConsumption", 0); } else { $docon = 1; } if ($docon) { my $gctotthishour = int ($gctotal - ($gcdaypast + CircularVal ($hash, 99, 'initdaygcon', 0))); if ($gctotthishour < 0) { $gctotthishour = 0; } my $nhour = $chour + 1; storeReading ('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->{val} = $gctotthishour; $paref->{nhour} = sprintf "%02d", $nhour; $paref->{histname} = 'cons'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; } my $dofeed = 0; if ($gfdaypast == 0) { # Management der Stundenberechnung auf Basis Totalwerte GridFeedIn if (defined CircularVal ($hash, 99, 'initdayfeedin', undef)) { $dofeed = 1; } else { $data{$type}{$name}{circular}{99}{initdayfeedin} = $fitotal; } } elsif (!defined CircularVal ($hash, 99, 'initdayfeedin', undef)) { $data{$type}{$name}{circular}{99}{initdayfeedin} = $fitotal - $gfdaypast - ReadingsNum ($name, "Today_Hour".sprintf("%02d",$chour+1)."_GridFeedIn", 0); } else { $dofeed = 1; } if ($dofeed) { my $gftotthishour = int ($fitotal - ($gfdaypast + CircularVal ($hash, 99, 'initdayfeedin', 0))); if ($gftotthishour < 0) { $gftotthishour = 0; } my $nhour = $chour+1; storeReading ('Today_Hour'.sprintf("%02d",$nhour).'_GridFeedIn', $gftotthishour.' Wh'); $data{$type}{$name}{circular}{sprintf("%02d",$nhour)}{gfeedin} = $gftotthishour; $paref->{val} = $gftotthishour; $paref->{nhour} = sprintf "%02d", $nhour; $paref->{histname} = 'gfeedin'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; } return; } ################################################################ # Batteriewerte sammeln ################################################################ sub _transferBatteryValues { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $chour = $paref->{chour}; my $day = $paref->{day}; my ($badev,$a,$h) = isBatteryUsed ($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 my $instcap = $h->{cap}; # numerischer Wert (Wh) oder Readingname installierte Batteriekapazität return if(!$pin || !$pou); $pounit //= $piunit; $piunit //= $pounit; $boutunit //= $binunit; $binunit //= $boutunit; 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 $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 $soc = ReadingsNum ($badev, $batchr, 0); if ($instcap && !isNumeric ($instcap)) { # wenn $instcap Reading Wert abfragen my ($bcapr,$bcapunit) = split ':', $instcap; $bcapunit //= 'Wh'; $instcap = ReadingsNum ($badev, $bcapr, 0); $instcap = $instcap * ($bcapunit =~ /^kWh$/xi ? 1000 : 1); } my $debug = $paref->{debug}; if ($debug =~ /collectData/x) { Log3 ($name, 1, "$name DEBUG> collect Battery data: device=$badev =>"); Log3 ($name, 1, "$name DEBUG> pin=$pbi W, pout=$pbo W, totalin: $btotin Wh, totalout: $btotout Wh, soc: $soc"); } my $params; if ($pin eq "-pout") { # Spezialfall pin bei neg. pout $params = { dev => $badev, rdg => $pou, rdgf => $pouf }; ($pbo,$pbi) = substSpecialCases ($params); } if ($pou eq "-pin") { # Spezialfall pout bei neg. pin $params = { dev => $badev, rdg => $pin, rdgf => $piuf }; ($pbi,$pbo) = substSpecialCases ($params); } # Batterielade-, enladeenergie in Circular speichern ###################################################### if (!defined CircularVal ($hash, 99, 'initdaybatintot', undef)) { $data{$type}{$name}{circular}{99}{initdaybatintot} = $btotin; # total Batterieladung zu Tagbeginn (Wh) } if (!defined CircularVal ($hash, 99, 'initdaybatouttot', undef)) { # total Batterieentladung zu Tagbeginn (Wh) $data{$type}{$name}{circular}{99}{initdaybatouttot} = $btotout; } $data{$type}{$name}{circular}{99}{batintot} = $btotin; # aktuell total Batterieladung $data{$type}{$name}{circular}{99}{batouttot} = $btotout; # aktuell total Batterieentladung my $nhour = $chour+1; # Batterieladung aktuelle Stunde in pvHistory speichern ######################################################### my $histbatintot = HistoryVal ($hash, $day, sprintf("%02d",$nhour), "batintotal", undef); # totale Batterieladung zu Beginn einer Stunde my $batinthishour; if (!defined $histbatintot) { # totale Batterieladung der aktuelle Stunde gesetzt ? $paref->{val} = $btotin; $paref->{nhour} = sprintf "%02d", $nhour; $paref->{histname} = 'batintotal'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; my $bitot = CurrentVal ($hash, "batintotal", $btotin); $batinthishour = int ($btotin - $bitot); } else { $batinthishour = int ($btotin - $histbatintot); } $batinthishour = 0 if($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->{val} = $batinthishour; $paref->{nhour} = sprintf "%02d", $nhour; $paref->{histname} = 'batinthishour'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; # Batterieentladung aktuelle Stunde in pvHistory speichern ############################################################ 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 ? $paref->{val} = $btotout; $paref->{nhour} = sprintf "%02d", $nhour; $paref->{histname} = 'batouttotal'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; my $botot = CurrentVal ($hash, 'batouttotal', $btotout); $batoutthishour = int ($btotout - $botot); } else { $batoutthishour = int ($btotout - $histbatouttot); } $batoutthishour = 0 if($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->{val} = $batoutthishour; $paref->{nhour} = sprintf "%02d", $nhour; $paref->{histname} = 'batoutthishour'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; # täglichen max. SOC in pvHistory speichern ############################################# my $batmaxsoc = HistoryVal ($hash, $day, 99, 'batmaxsoc', 0); # gespeicherter max. SOC des Tages if ($soc >= $batmaxsoc) { $paref->{val} = $soc; $paref->{nhour} = 99; $paref->{histname} = 'batmaxsoc'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; } ###### storeReading ('Today_Hour'.sprintf("%02d",$nhour).'_BatIn', $batinthishour.' Wh'); storeReading ('Today_Hour'.sprintf("%02d",$nhour).'_BatOut', $batoutthishour.' Wh'); storeReading ('Current_PowerBatIn', (int $pbi).' W'); storeReading ('Current_PowerBatOut', (int $pbo).' W'); storeReading ('Current_BatCharge', $soc.' %'); $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}{batcharge} = $soc; # aktuelle Batterieladung $data{$type}{$name}{current}{batinstcap} = $instcap; # installierte Batteriekapazität push @{$data{$type}{$name}{current}{socslidereg}}, $soc; # Schieberegister Batterie SOC limitArray ($data{$type}{$name}{current}{socslidereg}, $defslidenum); return; } ################################################################ # Batterie SOC optimalen Sollwert berechnen ################################################################ sub _batSocTarget { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $t = $paref->{t}; # aktuelle Zeit return if(!isBatteryUsed ($name)); my $oldd2care = CircularVal ($hash, 99, 'days2care', 0); my $ltsmsr = CircularVal ($hash, 99, 'lastTsMaxSocRchd', undef); my $batcharge = CurrentVal ($hash, 'batcharge', 0); # aktuelle Ladung in % __batSaveSocKeyFigures ($paref) if(!$ltsmsr || $batcharge >= $maxSoCdef || $oldd2care < 0); my $cgbt = AttrVal ($name, 'ctrlBatSocManagement', undef); my ($lowSoc, $upSoc, $maxsoc, $careCycle) = __parseAttrBatSoc ($name, $cgbt); return if(!$lowSoc ||!$upSoc); $paref->{careCycle} = $careCycle; __batSaveSocKeyFigures ($paref) if($batcharge >= $maxsoc); delete $paref->{careCycle}; my $nt; my $chargereq = 0; # Ladeanforderung wenn SoC unter Minimum SoC gefallen ist my $target = $lowSoc; my $yday = strftime "%d", localtime($t - 86400); # Vortag (range 01 to 31) my $batymaxsoc = HistoryVal ($hash, $yday, 99, 'batmaxsoc', 0); # gespeicherter max. SOC des Vortages my $batysetsoc = HistoryVal ($hash, $yday, 99, 'batsetsoc', $lowSoc); # gespeicherter SOC Sollwert des Vortages $target = $batymaxsoc < $maxsoc ? $batysetsoc + $batSocChgDay : $batymaxsoc >= $maxsoc ? $batysetsoc - $batSocChgDay : $batysetsoc; # neuer Min SOC für den laufenden Tag debugLog ($paref, 'batteryManagement', "SoC calc Step1 - compare with SoC history -> new Target: $target %"); ## Pflege-SoC (Soll SoC $maxSoCdef bei $batSocChgDay % Steigerung p. Tag) ########################################################################### my $sunset = CurrentVal ($hash, 'sunsetTodayTs', $t); my $delayts = $sunset - 5400; # Pflege-SoC/Erhöhung SoC erst ab 1,5h vor Sonnenuntergang berechnen/anwenden my $la = ''; if ($t > $delayts) { my $ntsmsc = CircularVal ($hash, 99, 'nextTsMaxSocChge', $t); my $days2care = ceil (($ntsmsc - $t) / 86400); # verbleibende Tage bis der Batterie Pflege-SoC (default 95%) erreicht sein soll $paref->{days2care} = $days2care; __batSaveSocKeyFigures ($paref); delete $paref->{days2care}; my $careSoc = $maxsoc - ($days2care * $batSocChgDay); # Pflege-SoC um rechtzeitig den $maxsoc zu erreichen bei 5% Steigerung pro Tag $target = $careSoc < $target ? $target : $careSoc; # resultierender Target-SoC unter Berücksichtigung $caresoc $la = "note remaining days until care SoC ($days2care days) -> Target: $target %"; } else { $nt = (timestampToTimestring ($delayts, $paref->{lang}))[0]; $la = "note remaining days until care SoC -> calculation & activation postponed to after $nt"; } debugLog ($paref, 'batteryManagement', "SoC calc Step2 - $la"); ## Aufladewahrscheinlichkeit beachten ####################################### my $pvfctm = ReadingsNum ($name, 'Tomorrow_PVforecast', 0); # PV Prognose morgen my $pvfctd = ReadingsNum ($name, 'RestOfDayPVforecast', 0); # PV Prognose Rest heute my $csopt = ReadingsNum ($name, 'Battery_OptimumTargetSoC', $lowSoc); # aktuelles SoC Optimum my $pvexpect = $pvfctm > $pvfctd ? $pvfctm : $pvfctd; my $batinstcap = CurrentVal ($hash, 'batinstcap', 0); # installierte Batteriekapazität Wh my $cantarget = 100 - (100 / $batinstcap) * $pvexpect; # berechneter möglicher Min SOC nach Berücksichtigung Ladewahrscheinlichkeit my $newtarget = sprintf "%.0f", ($cantarget < $target ? $cantarget : $target); # Abgleich möglicher Min SOC gg. berechneten Min SOC my $logadd = ''; if ($newtarget > $csopt && $t > $delayts) { # Erhöhung des SoC (wird ab Sonnenuntergang angewendet) $target = $newtarget; $logadd = "(new target > $csopt % and Sunset has passed)"; } elsif ($newtarget > $csopt && $t <= $delayts) { # bisheriges Optimum bleibt vorerst $target = $csopt; $nt = (timestampToTimestring ($delayts, $paref->{lang}))[0]; $logadd = "(calculated new target $newtarget % is only activated after $nt)"; } elsif ($newtarget < $csopt) { # Targetminderung sofort umsetzen -> Freiplatz für Ladeprognose $target = $newtarget; $logadd = "(new target < current Target SoC $csopt)"; } else { # bisheriges Optimum bleibt $target = $newtarget; $logadd = "(no change)"; } debugLog ($paref, 'batteryManagement', "SoC calc Step3 - note charging probability -> Target: $target % ".$logadd); ## low/up-Grenzen beachten ############################ $target = $target > $upSoc ? $upSoc : $target < $lowSoc ? $lowSoc : $target; debugLog ($paref, 'batteryManagement', "SoC calc Step4 - observe low/up limits -> Target: $target %"); ## auf 5er Schritte anpassen (40,45,50,...) ############################################# my $flo = floor ($target / 5); my $rmn = $target - ($flo * 5); my $add = $rmn <= 2.5 ? 0 : 5; $target = ($flo * 5) + $add; debugLog ($paref, 'batteryManagement', "SoC calc Step5 - rounding the SoC to steps of 5 -> Target: $target %"); ## Zwangsladeanforderung ########################## if ($batcharge < $target) { $chargereq = 1; } debugLog ($paref, 'batteryManagement', "SoC calc Step6 - force charging request: ". ($chargereq ? 'yes (battery charge is below minimum SoC)' : 'no (Battery is sufficiently charged)')); ## pvHistory/Readings schreiben ################################# $paref->{val} = $target; $paref->{nhour} = 99; $paref->{histname} = 'batsetsoc'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; storeReading ('Battery_OptimumTargetSoC', $target.' %'); storeReading ('Battery_ChargeRequest', $chargereq); return; } ################################################################ # Parse ctrlBatSocManagement ################################################################ sub __parseAttrBatSoc { my $name = shift; my $cgbt = shift // return; my ($pa,$ph) = parseParams ($cgbt); my $lowSoc = $ph->{lowSoc}; my $upSoc = $ph->{upSoC}; my $maxsoc = $ph->{maxSoC} // $maxSoCdef; # optional (default: $maxSoCdef) my $careCycle = $ph->{careCycle} // $carecycledef; # Ladungszyklus (Maintenance) für maxSoC in Tagen return ($lowSoc, $upSoc, $maxsoc, $careCycle); } ################################################################ # Batterie Kennzahlen speichern ################################################################ sub __batSaveSocKeyFigures { my $paref = shift; my $name = $paref->{name}; my $type = $paref->{type}; my $t = $paref->{t}; # aktuelle Zeit my $careCycle = $paref->{careCycle} // $carecycledef; if (defined $paref->{days2care}) { $data{$type}{$name}{circular}{99}{days2care} = $paref->{days2care}; # verbleibende Tage bis zum Pflege-SoC erreicht werden soll return; } $data{$type}{$name}{circular}{99}{lastTsMaxSocRchd} = $t; # Timestamp des letzten Erreichens von >= maxSoC $data{$type}{$name}{circular}{99}{nextTsMaxSocChge} = $t + (86400 * $careCycle); # Timestamp bis zu dem die Batterie mindestens einmal maxSoC erreichen soll return; } ################################################################ # Zusammenfassungen erstellen ################################################################ sub _createSummaries { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $day = $paref->{day}; 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 }; my $next2HoursSum = { "PV" => 0, "Consumption" => 0 }; my $next3HoursSum = { "PV" => 0, "Consumption" => 0 }; my $next4HoursSum = { "PV" => 0, "Consumption" => 0 }; my $restOfDaySum = { "PV" => 0, "Consumption" => 0 }; my $tomorrowSum = { "PV" => 0, "Consumption" => 0 }; my $todaySumFc = { "PV" => 0, "Consumption" => 0 }; my $todaySumRe = { "PV" => 0, "Consumption" => 0 }; my $tdConFcTillSunset = 0; my $remainminutes = 60 - $minute; # verbleibende Minuten der aktuellen Stunde my $restofhourpvfc = (NexthoursVal($hash, "NextHour00", 'pvfc', 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), 'pvfc', 0); my $confc = NexthoursVal ($hash, "NextHour".sprintf("%02d",$h), 'confc', 0); my $istdy = NexthoursVal ($hash, "NextHour".sprintf("%02d",$h), 'today', 0); my $don = NexthoursVal ($hash, "NextHour".sprintf("%02d",$h), 'DoN', 0); $pvfc = 0 if($pvfc < 0); # PV Prognose darf nicht negativ sein $confc = 0 if($confc < 0); # Verbrauchsprognose darf nicht negativ sein 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); } if ($h <= 3) { $next3HoursSum->{PV} += $pvfc 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); } if ($h <= 4) { $next4HoursSum->{PV} += $pvfc 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); } if ($istdy) { $restOfDaySum->{PV} += $pvfc; $restOfDaySum->{Consumption} += $confc; $tdConFcTillSunset += $confc if($don); } else { $tomorrowSum->{PV} += $pvfc; } } for my $th (1..24) { $todaySumFc->{PV} += HistoryVal ($hash, $day, sprintf("%02d", $th), 'pvfc', 0); $todaySumRe->{PV} += HistoryVal ($hash, $day, sprintf("%02d", $th), 'pvrl', 0); } my $pvre = int $todaySumRe->{PV}; 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); $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; my $divi = $selfconsumption + $batout + $gcon; $selfconsumptionrate = sprintf "%.0f", $selfconsumption / $pvgen * 100 if($pvgen * 1 > 0); $autarkyrate = sprintf "%.0f", ($selfconsumption + $batout) / $divi * 100 if($divi); # vermeide Illegal division by zero $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; $data{$type}{$name}{current}{tdConFcTillSunset} = $tdConFcTillSunset; storeReading ('Current_Consumption', $consumption. ' W'); storeReading ('Current_SelfConsumption', $selfconsumption. ' W'); storeReading ('Current_SelfConsumptionRate', $selfconsumptionrate. ' %'); storeReading ('Current_Surplus', $surplus. ' W'); storeReading ('Current_AutarkyRate', $autarkyrate. ' %'); storeReading ('Today_PVreal', $pvre. ' Wh'); storeReading ('Tomorrow_ConsumptionForecast', $tconsum. ' Wh') if(defined $tconsum); storeReading ('NextHours_Sum01_PVforecast', (int $next1HoursSum->{PV}). ' Wh'); storeReading ('NextHours_Sum02_PVforecast', (int $next2HoursSum->{PV}). ' Wh'); storeReading ('NextHours_Sum03_PVforecast', (int $next3HoursSum->{PV}). ' Wh'); storeReading ('NextHours_Sum04_PVforecast', (int $next4HoursSum->{PV}). ' Wh'); storeReading ('RestOfDayPVforecast', (int $restOfDaySum->{PV}). ' Wh'); storeReading ('Tomorrow_PVforecast', (int $tomorrowSum->{PV}). ' Wh'); storeReading ('Today_PVforecast', (int $todaySumFc->{PV}). ' Wh'); storeReading ('NextHours_Sum04_ConsumptionForecast', (int $next4HoursSum->{Consumption}).' Wh'); storeReading ('RestOfDayConsumptionForecast', (int $restOfDaySum->{Consumption}). ' Wh'); return; } ################################################################ # Consumer - Energieverbrauch aufnehmen # - Masterdata ergänzen # - Schaltzeiten planen ################################################################ sub _manageConsumerData { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $t = $paref->{t}; # aktuelle Zeit my $date = $paref->{date}; # aktuelles Datum my $chour = $paref->{chour}; my $day = $paref->{day}; my $nhour = $chour + 1; $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 $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; storeReading ("consumer${c}_currentPower", $pcurr.' W'); } ## Verbrauch auslesen + speichern ################################### my $ethreshold = 0; 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 ## 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; storeReading ("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->{val} = $consumerco; # Verbrauch des Consumers aktuelle Stunde $paref->{histname} = "csme${c}"; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{val}; } $paref->{val} = $etot; # Totalverbrauch des Verbrauchers $paref->{histname} = "csmt${c}"; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{val}; } 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") { $data{$type}{$name}{consumers}{$c}{onoff} = 'on'; $data{$type}{$name}{consumers}{$c}{startTime} = $t; # startTime ist nicht von "Automatic" abhängig -> nicht identisch mit planswitchon !!! $data{$type}{$name}{consumers}{$c}{cycleStarttime} = $t; $data{$type}{$name}{consumers}{$c}{cycleTime} = 0; 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); } else { $data{$type}{$name}{consumers}{$c}{cycleTime} = (($t - ConsumerVal ($hash, $c, 'cycleStarttime', $t)) / 60); } $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}{lastMinutesOn} = 0; } } } else { # Verbraucher soll nicht aktiv sein $data{$type}{$name}{consumers}{$c}{onoff} = 'off'; $data{$type}{$name}{consumers}{$c}{cycleTime} = 0; $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); $paref->{val} = ceil ConsumerVal ($hash, $c, "minutesOn", 0); # Verbrauchsminuten akt. Stunde des Consumers $paref->{histname} = "minutescsm${c}"; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{val}; ## 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++; } if ($dnum) { if ($consumerco && $runhours) { $data{$type}{$name}{consumers}{$c}{avgenergy} = sprintf "%.2f", ($consumerco/$runhours); # Durchschnittsverbrauch pro Stunde in Wh } else { delete $data{$type}{$name}{consumers}{$c}{avgenergy}; } $data{$type}{$name}{consumers}{$c}{avgruntime} = sprintf "%.2f", (($runhours / $dnum) * 60); # Durchschnittslaufzeit am Tag in Minuten } $paref->{consumer} = $c; __getAutomaticState ($paref); # Automatic Status des Consumers abfragen __calcEnergyPieces ($paref); # Energieverbrauch auf einzelne Stunden für Planungsgrundlage aufteilen __planInitialSwitchTime ($paref); # Consumer Switch Zeiten planen __setTimeframeState ($paref); # Timeframe Status ermitteln __setConsRcmdState ($paref); # Consumption Recommended Status setzen __switchConsumer ($paref); # Consumer schalten __reviewSwitchTime ($paref); # Planungsdaten überprüfen und ggf. neu planen __remainConsumerTime ($paref); # Restlaufzeit Verbraucher ermitteln __setPhysSwState ($paref); # physischen Schaltzustand festhalten ## Consumer Schaltstatus und Schaltzeit für Readings ermitteln ################################################################ my $costate = isConsumerPhysOn ($hash, $c) ? "on" : isConsumerPhysOff ($hash, $c) ? "off" : "unknown"; $data{$type}{$name}{consumers}{$c}{state} = $costate; my ($pstate,$starttime,$stoptime,$supplmnt) = __getPlanningStateAndTimes ($paref); my ($iilt,$rlt) = isInLocktime ($paref); # Sperrzeit Status ermitteln my $mode = ConsumerVal ($hash, $c, 'mode', 'can'); my $constate = "name='$alias' state='$costate'"; $constate .= " mode='$mode' planningstate='$pstate'"; $constate .= " remainLockTime='$rlt'" if($rlt); $constate .= " info='$supplmnt'" if($supplmnt); storeReading ("consumer${c}", $constate); # Consumer Infos storeReading ("consumer${c}_planned_start", $starttime) if($starttime); # Consumer Start geplant storeReading ("consumer${c}_planned_stop", $stoptime) if($stoptime); # Consumer Stop geplant } delete $paref->{consumer}; delete $paref->{nhour}; return; } ################################################################ # Consumer Status Automatic Modus abfragen und im # Hash consumers aktualisieren ################################################################ sub __getAutomaticState { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $c = $paref->{consumer}; my $consumer = ConsumerVal ($hash, $c, 'name', ''); # Name Consumer Device 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"); return; } my $dswitch = ConsumerVal ($hash, $c, 'dswitch', ''); # alternatives Schaltdevice if ($dswitch) { if (!$defs{$dswitch}) { my $err = qq{ERROR - the device "$dswitch" doesn't exist anymore! Delete or change the attribute "consumer${c}".}; Log3 ($name, 1, "$name - $err"); return; } } else { $dswitch = $consumer; } my $autord = ConsumerVal ($hash, $c, 'autoreading', ''); # Readingname f. Automatiksteuerung my $auto = 1; $auto = ReadingsVal ($dswitch, $autord, 1) if($autord); # Reading für Ready-Bit -> Einschalten möglich ? $data{$type}{$name}{consumers}{$c}{auto} = $auto; # Automaticsteuerung: 1 - Automatic ein, 0 - Automatic aus return; } ################################################################### # Energieverbrauch auf einzelne Stunden für Planungsgrundlage # aufteilen # Consumer specific epieces ermitteln + speichern # (in Wh) ################################################################### sub __calcEnergyPieces { my $paref = shift; my $hash = $paref->{hash}; 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); delete $paref->{etot}; } else { delete $data{$type}{$name}{consumers}{$c}{epiecAVG}; delete $data{$type}{$name}{consumers}{$c}{epiecAVG_hours}; delete $data{$type}{$name}{consumers}{$c}{epiecStartEtotal}; delete $data{$type}{$name}{consumers}{$c}{epiecHist}; delete $data{$type}{$name}{consumers}{$c}{epiecHour}; for my $h (1..$epiecMaxCycles) { 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); if (isSunPath ($hash, $c)) { # SunPath ist in mintime gesetzt my ($riseshift, $setshift) = sunShift ($hash, $c); my $tdiff = (CurrentVal ($hash, 'sunsetTodayTs', 0) + $setshift) - (CurrentVal ($hash, 'sunriseTodayTs', 0) + $riseshift); $mintime = $tdiff / 60; } my $hours = ceil ($mintime / 60); # Einplanungsdauer in h my $ctote = ConsumerVal ($hash, $c, "avgenergy", undef); # gemessener durchschnittlicher Energieverbrauch pro Stunde (Wh) $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 $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); } return; } #################################################################################### # Verbraucherspezifische Energiestück Ermittlung # # epiecMaxCycles => gibt an wie viele Zyklen betrachtet werden # sollen # epiecHist => ist die Nummer des Zyklus der aktuell # benutzt wird. # # epiecHist_x => 1=.. 2=.. 3=.. 4=.. epieces eines Zyklus # epiecHist_x_hours => Stunden des Durchlauf bzw. wie viele # Einträge epiecHist_x hat # epiecAVG => 1=.. 2=.. durchschnittlicher Verbrauch pro Betriebsstunde # 1, 2, .. usw. # wäre ein KPI um eine angepasste Einschaltung zu # realisieren # epiecAVG_hours => durchschnittliche Betriebsstunden für einen Ein/Aus-Zyklus # #################################################################################### sub ___csmSpecificEpieces { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; 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 $tsloff = defined $data{$type}{$name}{consumers}{$c}{lastOnTime} ? $t - $data{$type}{$name}{consumers}{$c}{lastOnTime} : 99; debugLog ($paref, "epiecesCalc", qq{specificEpieces -> consumer "$c" - time since last Switch Off (tsloff): $tsloff seconds}); if ($tsloff < 300) { # erst nach Auszeit >= X Sekunden wird ein neuer epiec-Zyklus gestartet my $ecycle = ""; 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 if(ConsumerVal ($hash, $c, "epiecHist", 0) > $epiecMaxCycles); $ecycle = "epiecHist_".ConsumerVal ($hash, $c, "epiecHist", 0); delete $data{$type}{$name}{consumers}{$c}{$ecycle}; # Löschen, wird neu erfasst } $ecycle = "epiecHist_".ConsumerVal ($hash, $c, "epiecHist", 0); # Zyklusnummer für Namen $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 debugLog ($paref, "epiecesCalc", qq{specificEpieces -> consumer "$c" - current cycle number (ecycle): $ecycle}); debugLog ($paref, "epiecesCalc", qq{specificEpieces -> consumer "$c" - Operating hour after switch on (epiecHour): $epiecHour}); if (ConsumerVal ($hash, $c, "epiecHour", 0) != $epiecHour) { # Betriebsstundenwechsel ? Differenz von etot noch auf die vorherige Betriebsstunde anrechnen my $epiecHour_last = $epiecHour - 1; $data{$type}{$name}{consumers}{$c}{$ecycle}{$epiecHour_last} = sprintf '%.2f', ($etot - ConsumerVal ($hash, $c, "epiecStartEtotal", 0)) if($epiecHour > 1); $data{$type}{$name}{consumers}{$c}{epiecStartEtotal} = $etot; debugLog ($paref, "epiecesCalc", qq{specificEpieces -> consumer "$c" - Operating hours change - new etotal (epiecStartEtotal): $etot}); } my $ediff = $etot - ConsumerVal ($hash, $c, "epiecStartEtotal", 0); $data{$type}{$name}{consumers}{$c}{$ecycle}{$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 debugLog ($paref, "epiecesCalc", qq{specificEpieces -> consumer "$c" - energy consumption in operating hour $epiecHour (ediff): $ediff}); } else { # Durchschnitt ermitteln if (ConsumerVal ($hash, $c, "epiecHour", 0) > 0) { my $hours = 0; for my $h (1..$epiecMaxCycles) { # durchschnittliche Betriebsstunden über alle epieces ermitteln und aufrunden $hours += ConsumerVal ($hash, $c, "epiecHist_".$h."_hours", 0); } my $avghours = ceil ($hours / $epiecMaxCycles); $data{$type}{$name}{consumers}{$c}{epiecAVG_hours} = $avghours; # durchschnittliche Betriebsstunden pro Zyklus debugLog ($paref, "epiecesCalc", qq{specificEpieces -> consumer "$c" - Average operating hours per cycle (epiecAVG_hours): $avghours}); delete $data{$type}{$name}{consumers}{$c}{epiecAVG}; # Durchschnitt für epics ermitteln for my $hour (1..$avghours) { # jede Stunde durchlaufen my $hoursE = 1; for my $h (1..$epiecMaxCycles) { # jedes epiec durchlaufen my $ecycle = "epiecHist_".$h; if (defined $data{$type}{$name}{consumers}{$c}{$ecycle}{$hour}) { if ($data{$type}{$name}{consumers}{$c}{$ecycle}{$hour} > 5) { $data{$type}{$name}{consumers}{$c}{epiecAVG}{$hour} += $data{$type}{$name}{consumers}{$c}{$ecycle}{$hour}; $hoursE += 1; } } } my $eavg = defined $data{$type}{$name}{consumers}{$c}{epiecAVG}{$hour} ? $data{$type}{$name}{consumers}{$c}{epiecAVG}{$hour} : 0; my $ahval = sprintf '%.2f', $eavg / $hoursE; # Durchschnitt ermittelt und speichern $data{$type}{$name}{consumers}{$c}{epiecAVG}{$hour} = $ahval; debugLog ($paref, "epiecesCalc", qq{specificEpieces -> consumer "$c" - Average epiece of operating hour $hour: $ahval}); } } $data{$type}{$name}{consumers}{$c}{epiecHour} = -1; # epiecHour auf initialwert setzen für nächsten durchlauf } return; } ################################################################### # Consumer Schaltzeiten planen ################################################################### sub __planInitialSwitchTime { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $c = $paref->{consumer}; my $debug = $paref->{debug}; my $dnp = ___noPlanRelease ($paref); if ($dnp) { if ($debug =~ /consumerPlanning/x) { Log3 ($name, 4, qq{$name DEBUG> Planning consumer "$c" - name: }.ConsumerVal ($hash, $c, 'name', ''). qq{ alias: }.ConsumerVal ($hash, $c, 'alias', '')); Log3 ($name, 4, qq{$name DEBUG> Planning consumer "$c" - $dnp}); } return; } if ($debug =~ /consumerPlanning/x) { Log3 ($name, 1, qq{$name DEBUG> ############### consumerPlanning consumer "$c" ############### }); Log3 ($name, 1, qq{$name DEBUG> Planning consumer "$c" - name: }.ConsumerVal ($hash, $c, 'name', ''). qq{ alias: }.ConsumerVal ($hash, $c, 'alias', '')); } if (ConsumerVal ($hash, $c, 'type', $defctype) eq 'noSchedule') { debugLog ($paref, "consumerPlanning", qq{consumer "$c" - }.$hqtxt{scnp}{EN}); $paref->{ps} = 'noSchedule'; ___setConsumerPlanningState ($paref); delete $paref->{ps}; return; } ___doPlanning ($paref); return; } ################################################################### # Entscheidung ob die Planung für den Consumer # vorgenommen werden soll oder nicht ################################################################### sub ___noPlanRelease { my $paref = shift; my $hash = $paref->{hash}; my $t = $paref->{t}; # aktuelle Zeit my $c = $paref->{consumer}; my $dnp = 0; # 0 -> Planung, 1 -> keine Planung if (ConsumerVal ($hash, $c, 'planstate', undef)) { # Verbraucher ist schon geplant/gestartet/fertig $dnp = qq{consumer is already planned}; } elsif (isSolCastUsed ($hash) || isForecastSolarUsed ($hash)) { my $tdc = SolCastAPIVal ($hash, '?All', '?All', 'todayDoneAPIcalls', 0); if ($tdc < 1) { # Planung erst nach dem zweiten API Abruf freigeben $dnp = qq{do not plan because off "todayDoneAPIcalls" is not set}; } } else { # Planung erst ab "$leadtime" vor Sonnenaufgang freigeben my $sunrise = CurrentVal ($hash, 'sunriseTodayTs', 32529945600); if ($t < $sunrise - $leadtime) { $dnp = "do not plan because off current time is less than sunrise minus ".($leadtime / 3600)." hour"; } } return $dnp; } ################################################################### # Consumer Review Schaltzeiten und neu planen wenn der # Consumer noch nicht in Operation oder finished ist # (nach Consumer Schaltung) ################################################################### sub __reviewSwitchTime { my $paref = shift; my $hash = $paref->{hash}; my $c = $paref->{consumer}; my $pstate = ConsumerVal ($hash, $c, 'planstate', ''); my $plswon = ConsumerVal ($hash, $c, 'planswitchon', 0); # bisher geplante Switch on Zeit my $simpCstat = simplifyCstate ($pstate); my $t = $paref->{t}; if ($simpCstat =~ /planned|suspended/xs) { if ($t < $plswon || $t > $plswon + 300) { # geplante Switch-On Zeit ist 5 Min überschritten und immer noch "planned" my $minute = $paref->{minute}; for my $m (qw(15 45)) { if (int $minute >= $m) { if (!defined $hash->{HELPER}{$c.'M'.$m.'DONE'}) { my $name = $paref->{name}; $hash->{HELPER}{$c.'M'.$m.'DONE'} = 1; debugLog ($paref, "consumerPlanning", qq{consumer "$c" - Review switch time planning name: }.ConsumerVal ($hash, $c, 'name', ''). qq{ alias: }.ConsumerVal ($hash, $c, 'alias', '')); ___doPlanning ($paref); } } else { delete $hash->{HELPER}{$c.'M'.$m.'DONE'}; } } } } else { delete $hash->{HELPER}{$c.'M15DONE'}; delete $hash->{HELPER}{$c.'M45DONE'}; } return; } ################################################################### # Consumer Planung ausführen ################################################################### sub ___doPlanning { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $c = $paref->{consumer}; my $debug = $paref->{debug}; my $type = $paref->{type}; my $lang = $paref->{lang}; my $nh = $data{$type}{$name}{nexthours}; my $cicfip = AttrVal ($name, 'affectConsForecastInPlanning', 0); # soll Consumption Vorhersage in die Überschußermittlung eingehen ? debugLog ($paref, "consumerPlanning", qq{consumer "$c" - Consider consumption forecast in consumer planning: }.($cicfip ? 'yes' : 'no')); my %max; my %mtimes; ## max. PV-Forecast bzw. Überschuß (bei gesetzen affectConsForecastInPlanning) ermitteln ########################################################################################## for my $idx (sort keys %{$nh}) { my $pvfc = NexthoursVal ($hash, $idx, 'pvfc', 0); my $confcex = NexthoursVal ($hash, $idx, 'confcEx', 0); # prognostizierter Verbrauch ohne registrierte Consumer my $spexp = $pvfc - ($cicfip ? $confcex : 0); # 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) { my $ts = timestringToTimestamp ($max{$k}{starttime}); $max{$order}{spexp} = $k; $max{$order}{ts} = $ts; $max{$order}{starttime} = $max{$k}{starttime}; $max{$order}{nexthour} = $max{$k}{nexthour}; $max{$order}{today} = $max{$k}{today}; $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; } debugLog ($paref, "consumerPlanning", qq{consumer "$c" - epiece1: $epiece1}); my $mode = ConsumerVal ($hash, $c, 'mode', 'can'); my $calias = ConsumerVal ($hash, $c, 'alias', ''); my $mintime = ConsumerVal ($hash, $c, 'mintime', $defmintime); # Einplanungsdauer debugLog ($paref, "consumerPlanning", qq{consumer "$c" - mode: $mode, mintime: $mintime, relevant method: surplus}); if (isSunPath ($hash, $c)) { # SunPath ist in mintime gesetzt my ($riseshift, $setshift) = sunShift ($hash, $c); my $tdiff = (CurrentVal ($hash, 'sunsetTodayTs', 0) + $setshift) - (CurrentVal ($hash, 'sunriseTodayTs', 0) + $riseshift); $mintime = $tdiff / 60; if ($debug =~ /consumerPlanning/x) { Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - Sunrise is shifted by >}.($riseshift / 60).'< minutes'); Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - Sunset is shifted by >}. ($setshift / 60).'< minutes'); Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - mintime calculated: }.$mintime.' minutes'); } } my $stopdiff = $mintime * 60; $paref->{maxref} = \%max; $paref->{mintime} = $mintime; $paref->{stopdiff} = $stopdiff; if ($mode eq "can") { # Verbraucher kann geplant werden if ($debug =~ /consumerPlanning/x) { for my $m (sort{$a<=>$b} keys %mtimes) { Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - surplus expected: $mtimes{$m}{spexp}, }. qq{starttime: }.$mtimes{$m}{starttime}.", ". qq{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 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:'; $paref->{startts} = $startts; $paref->{stopts} = $startts + $stopdiff; ___setConsumerPlanningState ($paref); ___saveEhodpieces ($paref); delete $paref->{ps}; delete $paref->{startts}; delete $paref->{stopts}; last; } else { $paref->{supplement} = encode('utf8', $hqtxt{emsple}{$lang}); # 'erwarteter max Überschuss weniger als' $paref->{ps} = 'suspended:'; ___setConsumerPlanningState ($paref); delete $paref->{ps}; delete $paref->{supplement}; } } } else { # Verbraucher _muß_ geplant werden if ($debug =~ /consumerPlanning/x) { for my $o (sort{$a<=>$b} keys %max) { Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - surplus: $max{$o}{spexp}, }. qq{starttime: }.$max{$o}{starttime}.", ". qq{nexthour: $max{$o}{nexthour}, today: $max{$o}{today}}); } } my $done; for my $o (sort{$a<=>$b} keys %max) { next if(!$max{$o}{today}); # der max-Wert von heute ist auszuwählen $paref->{elem} = $o; ___planMust ($paref); delete $paref->{elem}; $done = 1; last; } if (!$done) { $paref->{supplement} = encode('utf8', $hqtxt{nmspld}{$lang}); # 'kein max Überschuss für den aktuellen Tag gefunden' $paref->{ps} = 'suspended:'; ___setConsumerPlanningState ($paref); delete $paref->{ps}; delete $paref->{supplement}; } } my $planstate = ConsumerVal ($hash, $c, 'planstate', ''); my $planspmlt = ConsumerVal ($hash, $c, 'planSupplement', ''); if ($planstate) { Log3 ($name, 3, qq{$name - Consumer "$calias" $planstate $planspmlt}); } writeCacheToFile ($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 $supplmnt = $paref->{supplement} // ''; my $startts = $paref->{startts}; # Unix Timestamp für geplanten Switch on my $stopts = $paref->{stopts}; # Unix Timestamp für geplanten Switch off my $lonts = $paref->{lastAutoOnTs}; # Timestamp des letzten On-Schaltens bzw. letzter Fortsetzung im Automatikmodus my $loffts = $paref->{lastAutoOffTs}; # Timestamp des letzten Off-Schaltens bzw. letzter Unterbrechnung im Automatikmodus my $lang = $paref->{lang}; $data{$type}{$name}{consumers}{$c}{planSupplement} = $supplmnt; return if(!$ps); my ($starttime,$stoptime); if (defined $lonts) { $data{$type}{$name}{consumers}{$c}{lastAutoOnTs} = $lonts; } if (defined $loffts) { $data{$type}{$name}{consumers}{$c}{lastAutoOffTs} = $loffts; } if ($startts) { $starttime = (timestampToTimestring ($startts, $lang))[3]; $data{$type}{$name}{consumers}{$c}{planswitchon} = $startts; } if ($stopts) { $stoptime = (timestampToTimestring ($stopts, $lang))[3]; $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; } ################################################################ # Consumer Zeiten MUST planen ################################################################ sub ___planMust { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $c = $paref->{consumer}; my $maxref = $paref->{maxref}; 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 = floor ($mintime / 2 / 60); # die halbe Gesamtplanungsdauer in h als Vorlaufzeit einkalkulieren my $startts = $maxts - ($half * 3600); my $starttime = (timestampToTimestring ($startts, $lang))[3]; $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->{stopts} = $stopts; # Unix Timestamp für geplanten Switch off ___setConsumerPlanningState ($paref); ___saveEhodpieces ($paref); delete $paref->{ps}; delete $paref->{startts}; delete $paref->{stopts}; return; } ################################################################ # Einschaltgrenzen berücksichtigen und Korrektur # zurück liefern # notbefore, notafter muß in der Form "hh[:mm]" vorliegen ################################################################ sub ___switchonTimelimits { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $c = $paref->{consumer}; my $date = $paref->{date}; my $starttime = $paref->{starttime}; my $lang = $paref->{lang}; my $t = $paref->{t}; my $startts; if (isSunPath ($hash, $c)) { # SunPath ist in mintime gesetzt my ($riseshift, $setshift) = sunShift ($hash, $c); $startts = CurrentVal ($hash, 'sunriseTodayTs', 0) + $riseshift; $starttime = (timestampToTimestring ($startts, $lang))[3]; debugLog ($paref, "consumerPlanning", qq{consumer "$c" - starttime is set to >$starttime< due to >SunPath< is used}); } my $origtime = $starttime; my $notbefore = ConsumerVal ($hash, $c, "notbefore", 0); my $notafter = ConsumerVal ($hash, $c, "notafter", 0); my ($err, $vala, $valb); if ($notbefore =~ m/^\s*\{.*\}\s*$/xs) { # notbefore als Perl-Code definiert ($err, $valb) = checkCode ($name, $notbefore, 'cc1'); if (!$err && checkhhmm ($valb)) { $notbefore = $valb; debugLog ($paref, "consumerPlanning", qq{consumer "$c" - got 'notbefore' function result: $valb}); } else { Log3 ($name, 1, "$name - ERROR - the result of the Perl code in 'notbefore' is incorrect: $valb"); $notbefore = 0; } } if ($notafter =~ m/^\s*(\{.*\})\s*$/xs) { # notafter als Perl-Code definiert ($err, $vala) = checkCode ($name, $notafter, 'cc1'); if (!$err && checkhhmm ($vala)) { $notafter = $vala; debugLog ($paref, "consumerPlanning", qq{consumer "$c" - got 'notafter' function result: $vala}) } else { Log3 ($name, 1, "$name - ERROR - the result of the Perl code in the 'notafter' key is incorrect: $vala"); $notafter = 0; } } my ($nbfhh, $nbfmm, $nafhh, $nafmm); if ($notbefore) { ($nbfhh, $nbfmm) = split ":", $notbefore; $nbfmm //= '00'; $notbefore = (int $nbfhh) . $nbfmm; } if ($notafter) { ($nafhh, $nafmm) = split ":", $notafter; $nafmm //= '00'; $notafter = (int $nafhh) . $nafmm; } debugLog ($paref, "consumerPlanning", qq{consumer "$c" - used 'notbefore' term: }.(defined $notbefore ? $notbefore : '')); debugLog ($paref, "consumerPlanning", qq{consumer "$c" - used 'notafter' term: } .(defined $notafter ? $notafter : '')); my $change = q{}; if ($t > timestringToTimestamp ($starttime)) { $starttime = (timestampToTimestring ($t, $lang))[3]; $change = 'current time'; } my ($starthour, $startminute) = $starttime =~ /\s(\d{2}):(\d{2}):/xs; my $start = (int $starthour) . $startminute; if ($notbefore && $start < $notbefore) { $nbfhh = sprintf "%02d", $nbfhh; $starttime =~ s/\s(\d{2}):(\d{2}):/ $nbfhh:$nbfmm:/x; $change = 'notbefore'; } if ($notafter && $start > $notafter) { $nafhh = sprintf "%02d", $nafhh; $starttime =~ s/\s(\d{2}):(\d{2}):/ $nafhh:$nafmm:/x; $change = 'notafter'; } if ($change) { my $cname = ConsumerVal ($hash, $c, "name", ""); debugLog ($paref, "consumerPlanning", qq{consumer "$c" - Planned starttime of "$cname" changed from "$origtime" to "$starttime" due to $change condition}); } return $starttime; } ################################################################ # Löschmethode der Planungsdaten setzen ################################################################ sub ___setPlanningDeleteMeth { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $c = $paref->{consumer}; 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; } ################################################################ # Timeframe Status ermitteln ################################################################ sub __setTimeframeState { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $c = $paref->{consumer}; 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 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; } ################################################################ # Consumption Recommended Status setzen ################################################################ sub __setConsRcmdState { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $c = $paref->{consumer}; my $debug = $paref->{debug}; my $surplus = CurrentVal ($hash, 'surplus', 0); # aktueller Energieüberschuß 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 my ($spignore, $info, $err) = isSurplusIgnoCond ($hash, $c, $debug); # Vorhandensein PV Überschuß ignorieren ? Log3 ($name, 1, "$name - $err") if($err); if (!$nompower || $surplus - $rescons > 0 || $spignore) { $data{$type}{$name}{consumers}{$c}{isConsumptionRecommended} = 1; # Einschalten des Consumers günstig bzw. Freigabe für "on" von Überschußseite erteilt } else { $data{$type}{$name}{consumers}{$c}{isConsumptionRecommended} = 0; } if ($ccr =~ /$c/xs) { storeReading ("consumer${c}_ConsumptionRecommended", ConsumerVal ($hash, $c, 'isConsumptionRecommended', 0)); } return; } ################################################################ # Planungsdaten Consumer prüfen und ggf. starten/stoppen ################################################################ sub __switchConsumer { my $paref = shift; my $state = $paref->{state}; $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 $paref->{state} = $state; return; } ################################################################ # Verbraucher einschalten ################################################################ sub ___switchConsumerOn { 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 = $paref->{debug}; my $lang = $paref->{lang}; my ($cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name if(!$defs{$dswname}) { $state = qq{ERROR - the device "$dswname" is invalid. Please check device names in consumer "$c" attribute}; Log3 ($name, 1, "$name - $state"); return $state; } 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 $calias = ConsumerVal ($hash, $c, 'alias', $cname); # Consumer Device Alias my $simpCstat = simplifyCstate ($pstate); my $isInTime = isInTimeframe ($hash, $c); my ($swoncond,$swoffcond,$infon,$infoff,$err); ($swoncond,$infon,$err) = isAddSwitchOnCond ($hash, $c); # zusätzliche Switch on Bedingung Log3 ($name, 1, "$name - $err") if($err); ($swoffcond,$infoff,$err) = isAddSwitchOffCond ($hash, $c); # zusätzliche Switch off Bedingung Log3 ($name, 1, "$name - $err") if($err); my ($iilt,$rlt) = isInLocktime ($paref); # Sperrzeit Status ermitteln 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); Log3 ($name, 1, qq{$name DEBUG> ############### consumerSwitching consumer "$c" ############### }); Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - general switching parameters => }. qq{auto mode: $auto, current Consumption: $cons W, nompower: $nompow, surplus: $sp W, }. qq{planstate: $pstate, starttime: }.($startts ? (timestampToTimestring ($startts, $lang))[0] : "undef") ); Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - isInLocktime: $iilt}.($rlt ? ", remainLockTime: $rlt seconds" : '')); Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - current Context is >switch on< => }. qq{swoncond: $swoncond, on-command: $oncom } ); Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - isAddSwitchOnCond Info: $infon}) if($swoncond && $infon); Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - isAddSwitchOffCond Info: $infoff}) if($swoffcond && $infoff); Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - device >$dswname< is used as switching device}); if ($simpCstat =~ /planned|priority|starting/xs && $isInTime && $iilt) { Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - switching on postponed by >isInLocktime<}); } } my $isintable = isInterruptable ($hash, $c, 0, 1); # mit Ausgabe Interruptable Info im Debug my $isConsRcmd = isConsRcmd ($hash, $c); my $supplmnt = ConsumerVal ($hash, $c, 'planSupplement', ''); $paref->{supplement} = '' if($supplmnt =~ /swoncond\snot|swoncond\snicht/xs && $swoncond); $paref->{supplement} = encode('utf8', $hqtxt{swonnm}{$lang}) if(!$swoncond); # 'swoncond not met' $paref->{supplement} = encode('utf8', $hqtxt{swofmt}{$lang}) if($swoffcond); # 'swoffcond met' if (defined $paref->{supplement}) { ___setConsumerPlanningState ($paref); delete $paref->{supplement}; } if ($auto && $oncom && $swoncond && !$swoffcond && !$iilt && # kein Einschalten wenn zusätzliche Switch off Bedingung oder Sperrzeit zutrifft $simpCstat =~ /planned|priority|starting/xs && $isInTime) { # Verbraucher Start ist geplant && Startzeit überschritten my $mode = ConsumerVal ($hash, $c, "mode", $defcmode); # Consumer Planungsmode my $enable = ___enableSwitchByBatPrioCharge ($paref); # Vorrangladung Batterie ? debugLog ($paref, "consumerSwitching", qq{$name DEBUG> Consumer switch enable by battery state: $enable}); if ($mode eq "can" && !$enable) { # Batterieladung - keine Verbraucher "Einschalten" Freigabe $paref->{ps} = "priority charging battery"; ___setConsumerPlanningState ($paref); delete $paref->{ps}; } elsif ($mode eq "must" || $isConsRcmd) { # "Muss"-Planung oder Überschuß > Leistungsaufnahme (can) CommandSet(undef,"$dswname $oncom"); $paref->{ps} = "switching on:"; ___setConsumerPlanningState ($paref); delete $paref->{ps}; $state = qq{switching Consumer '$calias' to '$oncom'}; writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben Log3 ($name, 2, "$name - $state (Automatic = $auto)"); } } elsif ((($isintable == 1 && $isConsRcmd) || # unterbrochenen Consumer fortsetzen ($isintable == 3 && $isConsRcmd)) && $isInTime && $auto && $oncom && !$iilt && $simpCstat =~ /interrupted|interrupting/xs) { CommandSet(undef,"$dswname $oncom"); $paref->{ps} = "continuing:"; ___setConsumerPlanningState ($paref); delete $paref->{ps}; my $cause = $isintable == 3 ? 'interrupt condition no longer present' : 'existing surplus'; $state = qq{switching Consumer '$calias' to '$oncom', cause: $cause}; writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben Log3 ($name, 2, "$name - $state"); } return $state; } ################################################################ # Verbraucher ausschalten ################################################################ sub ___switchConsumerOff { 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 = $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); 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 ($cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name my $offcom = ConsumerVal ($hash, $c, 'offcom', ''); # Set Command für "off" my ($swoffcond,$infoff,$err) = isAddSwitchOffCond ($hash, $c); # zusätzliche Switch off Bedingung my $simpCstat = simplifyCstate ($pstate); my $cause; Log3 ($name, 1, "$name - $err") if($err); my ($iilt,$rlt) = isInLocktime ($paref); # Sperrzeit Status ermitteln if ($debug =~ /consumerSwitching/x) { # nur für Debugging Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - current Context is >switch off< => }. qq{swoffcond: $swoffcond, off-command: $offcom} ); Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - isAddSwitchOffCond Info: $infoff}) if($swoffcond && $infoff); if ($stopts && $t >= $stopts && $iilt) { Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - switching off postponed by >isInLocktime<}); } } my $isintable = isInterruptable ($hash, $c, $hyst, 1); # mit Ausgabe Interruptable Info im Debug if(($swoffcond || ($stopts && $t >= $stopts)) && !$iilt && ($auto && $offcom && $simpCstat =~ /started|starting|stopping|interrupt|continu/xs)) { CommandSet(undef,"$dswname $offcom"); $paref->{ps} = "switching off:"; ___setConsumerPlanningState ($paref); delete $paref->{ps}; $cause = $swoffcond ? "switch-off condition (key swoffcond) is true" : "planned switch-off time reached/exceeded"; $state = qq{switching Consumer '$calias' to '$offcom', cause: $cause}; writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben Log3 ($name, 2, "$name - $state (Automatic = $auto)"); } elsif ((($isintable && !isConsRcmd ($hash, $c)) || $isintable == 2) && # Consumer unterbrechen isInTimeframe ($hash, $c) && $auto && $offcom && !$iilt && $simpCstat =~ /started|continued|interrupting/xs) { CommandSet(undef,"$dswname $offcom"); $paref->{ps} = "interrupting:"; ___setConsumerPlanningState ($paref); delete $paref->{ps}; $cause = $isintable == 2 ? 'interrupt condition' : 'surplus shortage'; $state = qq{switching Consumer '$calias' to '$offcom', cause: $cause}; writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben Log3 ($name, 2, "$name - $state"); } return $state; } ################################################################ # Consumer aktuelle Schaltzustände ermitteln & # logische Zustände ableiten/setzen ################################################################ sub ___setConsumerSwitchingState { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $c = $paref->{consumer}; my $t = $paref->{t}; my $state = $paref->{state}; my $simpCstat = simplifyCstate (ConsumerVal ($hash, $c, 'planstate', '')); my $calias = ConsumerVal ($hash, $c, 'alias', ''); # Consumer Device Alias my $auto = ConsumerVal ($hash, $c, 'auto', 1); my $oldpsw = ConsumerVal ($hash, $c, 'physoffon', 'off'); # gespeicherter physischer Schaltzustand my $dowri = 0; debugLog ($paref, "consumerSwitching", qq{consumer "$c" - current planning state: $simpCstat \n}); if (isConsumerPhysOn ($hash, $c) && $simpCstat eq 'starting') { my $mintime = ConsumerVal ($hash, $c, "mintime", $defmintime); if (isSunPath ($hash, $c)) { # SunPath ist in mintime gesetzt my (undef, $setshift) = sunShift ($hash, $c); $mintime = (CurrentVal ($hash, 'sunsetTodayTs', 0) + $setshift) - $t; $mintime /= 60; } my $stopdiff = $mintime * 60; $paref->{ps} = "switched on:"; $paref->{startts} = $t; $paref->{lastAutoOnTs} = $t; $paref->{stopts} = $t + $stopdiff; ___setConsumerPlanningState ($paref); delete $paref->{ps}; delete $paref->{startts}; delete $paref->{lastAutoOnTs}; delete $paref->{stopts}; $state = qq{Consumer '$calias' switched on}; $dowri = 1; } elsif (isConsumerPhysOff ($hash, $c) && $simpCstat eq 'stopping') { $paref->{ps} = "switched off:"; $paref->{stopts} = $t; $paref->{lastAutoOffTs} = $t; ___setConsumerPlanningState ($paref); delete $paref->{ps}; delete $paref->{stopts}; delete $paref->{lastAutoOffTs}; $state = qq{Consumer '$calias' switched off}; $dowri = 1; } elsif (isConsumerPhysOn ($hash, $c) && $simpCstat eq 'continuing') { $paref->{ps} = "continued:"; $paref->{lastAutoOnTs} = $t; ___setConsumerPlanningState ($paref); delete $paref->{ps}; delete $paref->{lastAutoOnTs}; $state = qq{Consumer '$calias' switched on (continued)}; $dowri = 1; } elsif (isConsumerPhysOff ($hash, $c) && $simpCstat eq 'interrupting') { $paref->{ps} = "interrupted:"; $paref->{lastAutoOffTs} = $t; ___setConsumerPlanningState ($paref); delete $paref->{ps}; delete $paref->{lastAutoOffTs}; $state = qq{Consumer '$calias' switched off (interrupted)}; $dowri = 1; } elsif ($oldpsw eq 'off' && isConsumerPhysOn ($hash, $c)){ $paref->{supplement} = "$hqtxt{wexso}{$paref->{lang}}"; ___setConsumerPlanningState ($paref); delete $paref->{supplement}; $state = qq{Consumer '$calias' was external switched on}; $dowri = 1; } elsif ($oldpsw eq 'on' && isConsumerPhysOff ($hash, $c)) { $paref->{supplement} = "$hqtxt{wexso}{$paref->{lang}}"; ___setConsumerPlanningState ($paref); delete $paref->{supplement}; $state = qq{Consumer '$calias' was external switched off}; $dowri = 1; } if ($dowri) { writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben Log3 ($name, 2, "$name - $state"); } return $state; } ################################################################ # Restlaufzeit Verbraucher ermitteln ################################################################ sub __remainConsumerTime { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; 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 $data{$type}{$name}{consumers}{$c}{remainTime} = 0; 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; } ################################################################ # Consumer physischen Schaltstatus setzen ################################################################ sub __setPhysSwState { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $c = $paref->{consumer}; my $pon = isConsumerPhysOn ($hash, $c) ? 'on' : 'off'; $data{$type}{$name}{consumers}{$c}{physoffon} = $pon; return; } ################################################################ # Freigabe Einschalten Verbraucher durch Batterie Vorrangladung # return 0 -> keine Einschaltfreigabe Verbraucher # return 1 -> Einschaltfreigabe Verbraucher ################################################################ sub ___enableSwitchByBatPrioCharge { my $paref = shift; 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) = isBatteryUsed ($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 return $ena; } ################################################################### # Consumer Planstatus und Planzeit ermitteln ################################################################### sub __getPlanningStateAndTimes { my $paref = shift; my $hash = $paref->{hash}; my $c = $paref->{consumer}; my $lang = $paref->{lang}; my $simpCstat = simplifyCstate (ConsumerVal ($hash, $c, 'planstate', '')); my $supplmnt = ConsumerVal ($hash, $c, 'planSupplement', ''); my $startts = ConsumerVal ($hash, $c, 'planswitchon', ''); my $stopts = ConsumerVal ($hash, $c, 'planswitchoff', ''); my $starttime = ''; my $stoptime = ''; $starttime = (timestampToTimestring ($startts, $lang))[0] if($startts); $stoptime = (timestampToTimestring ($stopts, $lang))[0] if($stopts); return ($simpCstat, $starttime, $stoptime, $supplmnt); } ################################################################ # Energieverbrauch Vorhersage kalkulieren # # 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 $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 $acref = $data{$type}{$name}{consumers}; ## Verbrauchsvorhersage für den nächsten Tag ############################################## my $tomorrow = strftime "%a", localtime($t+86400); # Wochentagsname kommender Tag my $totcon = 0; my $dnum = 0; debugLog ($paref, "consumption", "################### Consumption forecast for the next day ###################"); 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); debugLog ($paref, "consumption", "History Consumption day >$n<: $dcon"); $totcon += $dcon; $dnum++; } if ($dnum) { my $tomavg = int ($totcon / $dnum); $data{$type}{$name}{current}{tomorrowconsumption} = $tomavg; # prognostizierter Durchschnittsverbrauch aller (gleicher) Wochentage debugLog ($paref, "consumption", "estimated Consumption for tomorrow: $tomavg, days for avg: $dnum"); } else { 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, "05" => 0, "06" => 0, "07" => 0, "08" => 0, "09" => 0, "10" => 0, "11" => 0, "12" => 0, "13" => 0, "14" => 0, "15" => 0, "16" => 0, "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, "13" => 0, "14" => 0, "15" => 0, "16" => 0, "17" => 0, "18" => 0, "19" => 0, "20" => 0, "21" => 0, "22" => 0, "23" => 0, "24" => 0, }; debugLog ($paref, "consumption", "################### Consumption forecast for the next hours ###################"); for my $k (sort keys %{$data{$type}{$name}{nexthours}}) { my $nhtime = NexthoursVal ($hash, $k, "starttime", undef); # Startzeit next if(!$nhtime); $dnum = 0; my $consumerco = 0; 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) 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); 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); } $conhex->{$nhhr} += $hcon - $consumerco if($hcon >= $consumerco); # prognostizierter Verbrauch Ex registrierter Verbraucher $conh->{$nhhr} += $hcon; $dnum++; } if ($dnum) { $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; $paref->{val} = $conavg; $paref->{nhour} = sprintf "%02d", $nhhr; $paref->{histname} = 'confc'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; } debugLog ($paref, "consumption", "estimated Consumption for $nhday -> starttime: $nhtime, confc: $conavg, days for avg: $dnum, hist. consumption registered consumers: ".sprintf "%.2f", $consumerco); } } return; } ################################################################ # Schwellenwerte auswerten und signalisieren ################################################################ sub _evaluateThresholds { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $bt = ReadingsVal($name, 'batteryTrigger', ''); my $pt = ReadingsVal($name, 'powerTrigger', ''); my $eh4t = ReadingsVal($name, 'energyH4Trigger', ''); if ($bt) { $paref->{cobj} = 'socslidereg'; $paref->{tname} = 'batteryTrigger'; $paref->{tholds} = $bt; __evaluateArray ($paref); } if ($pt) { $paref->{cobj} = 'genslidereg'; $paref->{tname} = 'powerTrigger'; $paref->{tholds} = $pt; __evaluateArray ($paref); } if ($eh4t) { $paref->{cobj} = 'h4fcslidereg'; $paref->{tname} = 'energyH4Trigger'; $paref->{tholds} = $eh4t; __evaluateArray ($paref); } delete $paref->{cobj}; delete $paref->{tname}; delete $paref->{tholds}; return; } ################################################################ # Threshold-Array auswerten und Readings vorbereiten ################################################################ sub __evaluateArray { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $cobj = $paref->{cobj}; # das CurrentVal Objekt, z.B. genslidereg my $tname = $paref->{tname}; # Thresholdname, z.B. powerTrigger my $tholds = $paref->{tholds}; # Triggervorgaben, z.B. aus Reading powerTrigger my $aaref = CurrentVal ($hash, $cobj, ''); my @aa = (); @aa = @{$aaref} if (ref $aaref eq 'ARRAY'); return if(scalar @aa < $defslidenum); my $gen1 = $aa[0]; my $gen2 = $aa[1]; my $gen3 = $aa[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}); storeReading ("${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}); storeReading ("${tname}_${knum}", 'off') if(ReadingsVal($name, "${tname}_${knum}", "on") eq "on"); } } return; } ################################################################ # zusätzliche Readings Tomorrow_HourXX_PVforecast # berechnen ################################################################ sub _calcReadingsTomorrowPVFc { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; 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', ''); next if(!$h); 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, 'pvfc', 0); storeReading ('Tomorrow_Hour'.$h.'_PVforecast', $pvfc.' Wh'); } return; } ################################################################ # Korrektur von Today_PVreal + # berechnet die prozentuale Abweichung von Today_PVforecast # und Today_PVreal ################################################################ sub calcTodayPVdeviation { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $t = $paref->{t}; my $date = $paref->{date}; my $day = $paref->{day}; my $pvfc = ReadingsNum ($name, 'Today_PVforecast', 0); my $pvre = ReadingsNum ($name, 'Today_PVreal', 0); return if(!$pvre); my $dp; if (AttrVal ($name, 'ctrlGenPVdeviation', 'daily') eq 'daily') { my $sstime = timestringToTimestamp ($date.' '.ReadingsVal ($name, "Today_SunSet", '22:00').':00'); return if($t < $sstime); my $diff = $pvfc - $pvre; $dp = sprintf "%.2f" , (100 * $diff / $pvre); } else { my $rodfc = ReadingsNum ($name, 'RestOfDayPVforecast', 0); my $dayfc = $pvre + $rodfc; # laufende Tagesprognose aus PVreal + Prognose Resttag $dp = sprintf "%.2f", (100 * ($pvfc - $dayfc) / $dayfc); } $data{$type}{$name}{circular}{99}{tdayDvtn} = $dp; storeReading ('Today_PVdeviation', $dp.' %'); return; } ################################################################ # Korrekturen und Qualität berechnen / speichern # sowie AI Quellen Daten hinzufügen ################################################################ sub calcValueImproves { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $chour = $paref->{chour}; my $t = $paref->{t}; # aktuelle Unix-Zeit my $idts = ReadingsTimestamp ($name, 'currentInverterDev', ''); # Definitionstimestamp des Inverterdevice return if(!$idts); my ($acu, $aln) = isAutoCorrUsed ($name); if ($acu) { $idts = timestringToTimestamp ($idts); readingsSingleUpdate ($hash, '.pvCorrectionFactor_Auto_Soll', ($aln ? $acu : $acu.' noLearning'), 0) if($acu =~ /on/xs); if ($t - $idts < 7200) { my $rmh = sprintf "%.2f", ((7200 - ($t - $idts)) / 3600); readingsSingleUpdate ($hash, 'pvCorrectionFactor_Auto', "standby (remains in standby for $rmh hours)", 0); Log3 ($name, 4, "$name - Correction usage is in standby. It starts in $rmh hours."); return; } else { my $acuset = ReadingsVal ($name, '.pvCorrectionFactor_Auto_Soll', 'on_simple'); readingsSingleUpdate ($hash, 'pvCorrectionFactor_Auto', $acuset, 0); } } else { readingsSingleUpdate ($hash, '.pvCorrectionFactor_Auto_Soll', 'off', 0); } $paref->{acu} = $acu; $paref->{aln} = $aln; for my $h (1..23) { next if(!$chour || $h > $chour); $paref->{h} = $h; _calcCaQcomplex ($paref); # Korrekturberechnung mit Bewölkung duchführen/speichern _calcCaQsimple ($paref); # einfache Korrekturberechnung duchführen/speichern _addHourAiRawdata ($paref); # AI Raw Data hinzufügen delete $paref->{h}; } delete $paref->{aln}; delete $paref->{acu}; return; } ################################################################ # PV Ist/Forecast ermitteln und Korrekturfaktoren, Qualität # in Abhängigkeit Bewölkung errechnen und speichern (komplex) ################################################################ sub _calcCaQcomplex { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $debug = $paref->{debug}; my $acu = $paref->{acu}; my $aln = $paref->{aln}; # Autolearning my $h = $paref->{h}; my $day = $paref->{day}; # aktueller Tag my $sr = ReadingsVal ($name, '.pvCorrectionFactor_'.sprintf("%02d",$h).'_cloudcover', ''); if ($sr eq 'done') { # Log3 ($name, 1, "$name DEBUG> Complex Corrf -> factor Hour: ".sprintf("%02d",$h)." already calculated"); return; } if (!$aln) { storeReading ('.pvCorrectionFactor_'.sprintf("%02d",$h).'_cloudcover', 'done'); debugLog ($paref, 'pvCorrectionWrite', "Autolearning is switched off for hour: $h -> skip the recalculation of the complex correction factor"); return; } my $pvrl = CircularVal ($hash, sprintf("%02d",$h), 'pvrl', 0); my $pvfc = CircularVal ($hash, sprintf("%02d",$h), 'pvapifc', 0); if (!$pvrl || !$pvfc) { storeReading ('.pvCorrectionFactor_'.sprintf("%02d",$h).'_cloudcover', 'done'); return; } my $chwcc = HistoryVal ($hash, $day, sprintf("%02d",$h), 'wcc', 0); # Wolkenbedeckung Heute & abgefragte Stunde my $sunalt = HistoryVal ($hash, $day, sprintf("%02d",$h), 'sunalt', 0); # Sonne Altitude my $crang = cloud2bin ($chwcc); my $sabin = sunalt2bin ($sunalt); $paref->{pvrl} = $pvrl; $paref->{pvfc} = $pvfc; $paref->{crang} = $crang; $paref->{sabin} = $sabin; $paref->{calc} = 'Complex'; my ($oldfac, $factor, $dnum) = __calcNewFactor ($paref); delete $paref->{pvrl}; delete $paref->{pvfc}; delete $paref->{crang}; delete $paref->{sabin}; delete $paref->{calc}; storeReading ('.pvCorrectionFactor_'.sprintf("%02d",$h).'_cloudcover', 'done'); if ($acu =~ /on_complex/xs) { storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h), $factor." (automatic - old factor: $oldfac, Sun Alt range: $sabin, Cloud range: $crang, Days in range: $dnum)"); storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h).'_autocalc', 'done'); } return; } ################################################################ # PV Ist/Forecast ermitteln und Korrekturfaktoren, Qualität # ohne Nebenfaktoren errechnen und speichern (simple) ################################################################ sub _calcCaQsimple { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $date = $paref->{date}; my $acu = $paref->{acu}; my $aln = $paref->{aln}; # Autolearning my $h = $paref->{h}; my $day = $paref->{day}; # aktueller Tag my $sr = ReadingsVal ($name, '.pvCorrectionFactor_'.sprintf("%02d",$h).'_apipercentil', ''); if($sr eq "done") { # debugLog ($paref, 'pvCorrectionWrite', "Simple Corrf factor Hour: ".sprintf("%02d",$h)." already calculated"); return; } if (!$aln) { storeReading ('.pvCorrectionFactor_'.sprintf("%02d",$h).'_apipercentil', 'done'); debugLog ($paref, 'pvCorrectionWrite', "Autolearning is switched off for hour: $h -> skip the recalculation of the simple correction factor"); return; } my $pvrl = CircularVal ($hash, sprintf("%02d",$h), 'pvrl', 0); my $pvfc = CircularVal ($hash, sprintf("%02d",$h), 'pvapifc', 0); if (!$pvrl || !$pvfc) { storeReading ('.pvCorrectionFactor_'.sprintf("%02d",$h).'_apipercentil', 'done'); return; } my $sunalt = HistoryVal ($hash, $day, sprintf("%02d",$h), 'sunalt', 0); # Sonne Altitude my $sabin = sunalt2bin ($sunalt); $paref->{pvrl} = $pvrl; $paref->{pvfc} = $pvfc; $paref->{sabin} = $sabin; $paref->{crang} = 'simple'; $paref->{calc} = 'Simple'; my ($oldfac, $factor, $dnum) = __calcNewFactor ($paref); delete $paref->{pvrl}; delete $paref->{pvfc}; delete $paref->{sabin}; delete $paref->{crang}; delete $paref->{calc}; storeReading ('.pvCorrectionFactor_'.sprintf("%02d",$h).'_apipercentil', 'done'); if ($acu =~ /on_simple/xs) { storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h), $factor." (automatic - old factor: $oldfac, Days in range: $dnum)"); storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h).'_autocalc', 'done'); } return; } ################################################################ # den neuen Korrekturfaktur berechnen ################################################################ sub __calcNewFactor { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $pvrl = $paref->{pvrl}; my $pvfc = $paref->{pvfc}; my $crang = $paref->{crang}; my $sabin = $paref->{sabin}; my $h = $paref->{h}; my $calc = $paref->{calc}; my $factor; my $pvrlsum = $pvrl; my $pvfcsum = $pvfc; debugLog ($paref, 'pvCorrectionWrite', "$calc Corrf -> Start calculation correction factor for hour: $h"); my ($oldfac, $oldq) = CircularSunCloudkorrVal ($hash, sprintf("%02d",$h), $sabin, $crang, 0); # bisher definierter Korrekturfaktor / Qualität my ($pvhis, $fchis, $dnum) = CircularSumVal ($hash, sprintf("%02d",$h), $sabin, $crang, 0); $oldfac = 1 if(1 * $oldfac == 0); debugLog ($paref, 'pvCorrectionWrite', "$calc Corrf -> read historical values: pv real sum: $pvhis, pv forecast sum: $fchis, days sum: $dnum"); if ($dnum) { # Werte in History vorhanden -> haben Prio ! $dnum++; $pvrlsum = $pvrl + $pvhis; $pvfcsum = $pvfc + $fchis; $pvrl = $pvrlsum / $dnum; $pvfc = $pvfcsum / $dnum; $factor = sprintf "%.2f", ($pvrl / $pvfc); # Faktorberechnung: reale PV / Prognose } elsif ($oldfac && (!$pvhis || !$fchis)) { # Circular Hash liefert einen vorhandenen Korrekturfaktor aber keine gespeicherten PV-Werte $dnum = 1; $factor = sprintf "%.2f", ($pvrl / $pvfc); $factor = sprintf "%.2f", ($factor + $oldfac) / 2; } else { # ganz neuer Wert $dnum = 1; $factor = sprintf "%.2f", ($pvrl / $pvfc); } my $maxvar = AttrVal ($name, 'affectMaxDayVariance', $defmaxvar); # max. Korrekturvarianz $factor = 1.00 if(1 * $factor == 0); # 0.00-Werte ignorieren (Schleifengefahr) if (abs($factor - $oldfac) > $maxvar) { $factor = sprintf "%.2f", ($factor > $oldfac ? $oldfac + $maxvar : $oldfac - $maxvar); Log3 ($name, 3, "$name - new $calc correction factor calculated (limited by affectMaxDayVariance): $factor (old: $oldfac) for hour: $h"); } else { Log3 ($name, 3, "$name - new $calc correction factor for hour $h calculated: $factor (old: $oldfac)"); } $pvrl = sprintf "%.0f", $pvrl; $pvfc = sprintf "%.0f", $pvfc; my $qual = __calcFcQuality ($pvfc, $pvrl); # Qualität der Vorhersage für die vergangene Stunde debugLog ($paref, 'pvCorrectionWrite', "$calc Corrf -> determined values - hour: $h, Sun Altitude range: $sabin, Cloud range: $crang, old factor: $oldfac, new factor: $factor, days: $dnum"); debugLog ($paref, 'pvCorrectionWrite|saveData2Cache', "$calc Corrf -> write correction values into Circular - hour: $h, Sun Altitude range: $sabin, Cloud range: $crang, factor: $factor, quality: $qual, days: $dnum"); if ($crang ne 'simple') { my $idx = $sabin.'.'.$crang; # value für pvcorrf Sonne Altitude $data{$type}{$name}{circular}{sprintf("%02d",$h)}{pvrlsum}{$idx} = $pvrlsum; # PV Erzeugung Summe speichern $data{$type}{$name}{circular}{sprintf("%02d",$h)}{pvfcsum}{$idx} = $pvfcsum; # PV Prognose Summe speichern $data{$type}{$name}{circular}{sprintf("%02d",$h)}{dnumsum}{$idx} = $dnum; # Anzahl aller historischen Tade dieser Range $data{$type}{$name}{circular}{sprintf("%02d",$h)}{pvcorrf}{$idx} = $factor; $data{$type}{$name}{circular}{sprintf("%02d",$h)}{quality}{$idx} = $qual; } else { $data{$type}{$name}{circular}{sprintf("%02d",$h)}{pvrlsum}{$crang} = $pvrlsum; $data{$type}{$name}{circular}{sprintf("%02d",$h)}{pvfcsum}{$crang} = $pvfcsum; $data{$type}{$name}{circular}{sprintf("%02d",$h)}{dnumsum}{$crang} = $dnum; $data{$type}{$name}{circular}{sprintf("%02d",$h)}{pvcorrf}{$crang} = $factor; $data{$type}{$name}{circular}{sprintf("%02d",$h)}{quality}{$crang} = $qual; } return ($oldfac, $factor, $dnum); } ################################################################ # Berechnen Tag / Stunden Verschieber # aus aktueller Stunde + lfd. Nummer ################################################################ sub calcDayHourMove { my $chour = shift; my $num = shift; my $fh = $chour + $num; my $fd = int ($fh / 24) ; $fh = $fh - ($fd * 24); return ($fd, $fh); } ################################################################ # Berechnen Tag / Stunden Verschieber ab aktuellen Tag # Input: YYYY-MM-DD HH:MM:SS # Output: $fd - 0 (Heute), 1 (Morgen), 2 (Übermorgen), .... # $fh - Stunde von $fd ohne führende Null # Return: fc${fd}_${fh} ################################################################ sub formatWeatherTimestrg { my $date = shift // return; my $cdate = strftime "%Y-%m-%d", localtime(time); my $refts = timestringToTimestamp ($cdate.' 00:00:00'); # Referenztimestring my $datts = timestringToTimestamp ($date); my $fd = int (($datts - $refts) / 86400); my $fh = int ((split /[ :]/, $date)[1]); return "fc${fd}_${fh}"; } ################################################################ # Spezialfall auflösen wenn Wert von $val2 dem # Redingwert von $val1 entspricht sofern $val1 negativ ist ################################################################ sub substSpecialCases { my $paref = shift; my $dev = $paref->{dev}; my $rdg = $paref->{rdg}; my $rdgf = $paref->{rdgf}; my $val1 = ReadingsNum ($dev, $rdg, 0) * $rdgf; my $val2; if($val1 <= 0) { $val2 = abs($val1); $val1 = 0; } else { $val2 = 0; } return ($val1,$val2); } ################################################################ # Energieverbrauch des Hauses in History speichern ################################################################ sub saveEnergyConsumption { my $paref = shift; my $name = $paref->{name}; my $chour = $paref->{chour}; my $shr = $chour+1; my $pvrl = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$shr)."_PVreal", 0); my $gfeedin = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$shr)."_GridFeedIn", 0); my $gcon = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$shr)."_GridConsumption", 0); my $batin = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$shr)."_BatIn", 0); my $batout = ReadingsNum ($name, "Today_Hour".sprintf("%02d",$shr)."_BatOut", 0); my $con = $pvrl - $gfeedin + $gcon - $batin + $batout; $paref->{val} = $con; $paref->{nhour} = sprintf "%02d", $shr; $paref->{histname} = 'con'; setPVhistory ($paref); delete $paref->{histname}; delete $paref->{nhour}; delete $paref->{val}; return; } ################################################################ # optionale Statistikreadings erstellen ################################################################ sub genStatisticReadings { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $t = $paref->{t}; # aktueller UNIX Timestamp my @srd = sort keys (%hcsr); my @csr = split ',', AttrVal ($name, 'ctrlStatisticReadings', ''); for my $item (@srd) { next if($item ~~ @csr); deleteReadingspec ($hash, 'statistic_'.$item); deleteReadingspec ($hash, 'statistic_'.$item.'_.*') if($item eq 'todayConsumptionForecast'); } return if(!@csr); for my $kpi (@csr) { my $def = $hcsr{$kpi}{def}; my $par = $hcsr{$kpi}{par}; if ($def eq 'apimaxreq') { $def = AttrVal ($name, 'ctrlSolCastAPImaxReq', $solcmaxreqdef); } if ($hcsr{$kpi}{fnr} == 1) { storeReading ('statistic_'.$kpi, &{$hcsr{$kpi}{fn}} ($hash, '?All', '?All', $kpi, $def)); } if ($hcsr{$kpi}{fnr} == 2) { $par = $kpi if(!$par); storeReading ('statistic_'.$kpi, &{$hcsr{$kpi}{fn}} ($hash, $par, $def).$hcsr{$kpi}{unit}); } if ($hcsr{$kpi}{fnr} == 3) { storeReading ('statistic_'.$kpi, &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, $kpi, $def).$hcsr{$kpi}{unit}); } if ($hcsr{$kpi}{fnr} == 4) { if ($kpi eq 'SunHours_Remain') { my $ss = &{$hcsr{$kpi}{fn}} ($hash, 'sunsetTodayTs', $def); my $shr = ($ss - $t) / 3600; $shr = $shr < 0 ? 0 : $shr; storeReading ('statistic_'.$kpi, sprintf "%.2f", $shr); } if ($kpi eq 'SunMinutes_Remain') { my $ss = &{$hcsr{$kpi}{fn}} ($hash, 'sunsetTodayTs', $def); my $smr = ($ss - $t) / 60; $smr = $smr < 0 ? 0 : $smr; storeReading ('statistic_'.$kpi, sprintf "%.0f", $smr); } if ($kpi eq 'runTimeTrainAI') { my $rtaitr = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, $kpi, $def); storeReading ('statistic_'.$kpi, $rtaitr); } if ($kpi eq 'daysUntilBatteryCare') { my $d2c = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'days2care', $def); storeReading ('statistic_'.$kpi, $d2c); } if ($kpi eq 'todayGridFeedIn') { my $idfi = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'initdayfeedin', $def); # initialer Tagesstartwert my $cfi = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'feedintotal', $def); # aktuelles total Feed In my $dfi = $cfi - $idfi; storeReading ('statistic_'.$kpi, (sprintf "%.1f", $dfi).' Wh'); } if ($kpi eq 'todayGridConsumption') { my $idgcon = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'initdaygcon', $def); # initialer Tagesstartwert my $cgcon = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'gridcontotal', $def); # aktuelles total Netzbezug my $dgcon = $cgcon - $idgcon; storeReading ('statistic_'.$kpi, (sprintf "%.1f", $dgcon).' Wh'); } if ($kpi eq 'todayBatIn') { my $idbitot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'initdaybatintot', $def); # initialer Tagesstartwert Batterie In total my $cbitot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'batintot', $def); # aktuelles total Batterie In my $dbi = $cbitot - $idbitot; storeReading ('statistic_'.$kpi, (sprintf "%.1f", $dbi).' Wh'); } if ($kpi eq 'todayBatOut') { my $idbotot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'initdaybatouttot', $def); # initialer Tagesstartwert Batterie Out total my $cbotot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'batouttot', $def); # aktuelles total Batterie Out my $dbo = $cbotot - $idbotot; storeReading ('statistic_'.$kpi, (sprintf "%.1f", $dbo).' Wh'); } if ($kpi eq 'dayAfterTomorrowPVforecast') { # PV Vorhersage Summe für Übermorgen (falls Werte vorhanden), Forum:#134226 my $dayaftertomorrow = strftime "%Y-%m-%d", localtime($t + 172800); # Datum von Übermorgen my @allstrings = split ",", ReadingsVal ($name, 'inverterStrings', ''); my $fcsumdat = 0; my $type = $paref->{type}; for my $strg (@allstrings) { for my $starttmstr (sort keys %{$data{$type}{$name}{solcastapi}{$strg}}) { next if($starttmstr !~ /$dayaftertomorrow/xs); my $val = &{$hcsr{$kpi}{fn}} ($hash, $strg, $starttmstr, $hcsr{$kpi}{par}, $def); $fcsumdat += $val; debugLog ($paref, 'radiationProcess', "dayaftertomorrow PV forecast (raw) - $strg -> $starttmstr -> $val Wh"); } } if ($fcsumdat) { storeReading ('statistic_'.$kpi, (int $fcsumdat). ' Wh'); } else { storeReading ('statistic_'.$kpi, $fcsumdat. ' (no data available)'); } } if ($kpi =~ /currentRunMtsConsumer_/xs) { my $c = (split "_", $kpi)[1]; # Consumer Nummer extrahieren if (!AttrVal ($name, 'consumer'.$c, '')) { deleteReadingspec ($hash, 'statistic_currentRunMtsConsumer_'.$c); return; } my $mion = &{$hcsr{$kpi}{fn}} ($hash, $c, $hcsr{$kpi}{par}, $def); storeReading ('statistic_'.$kpi, (sprintf "%.0f", $mion).$hcsr{$kpi}{unit}); } if ($kpi eq 'todayConsumptionForecast') { my $type = $paref->{type}; for my $idx (sort keys %{$data{$type}{$name}{nexthours}}) { my $istoday = NexthoursVal ($hash, $idx, 'today', 0); last if(!$istoday); my $hod = NexthoursVal ($hash, $idx, 'hourofday', '01'); my $confc = &{$hcsr{$kpi}{fn}} ($hash, $idx, $hcsr{$kpi}{par}, $def); storeReading ('statistic_'.$kpi.'_'.$hod, $confc.$hcsr{$kpi}{unit}); } } if ($kpi eq 'conForecastTillNextSunrise') { my $type = $paref->{type}; my $confc = 0; my $dono = 1; my $hrs = 0; my $sttm = ''; for my $idx (sort keys %{$data{$type}{$name}{nexthours}}) { my $don = NexthoursVal ($hash, $idx, 'DoN', 2); # Wechsel von 0 -> 1 relevant last if($don == 2); $confc += &{$hcsr{$kpi}{fn}} ($hash, $idx, $hcsr{$kpi}{par}, $def); $sttm = NexthoursVal ($hash, $idx, 'starttime', ''); $hrs++; # Anzahl berücksichtigte Stunden if ($dono == 0 && $don == 1) { last; } $dono = $don; } my $sttmp = timestringToTimestamp ($sttm) // return; $sttmp += 3600; # Beginnzeitstempel auf volle Stunde ergänzen my $mhrs = $hrs * 60; # berücksichtigte volle Minuten my $mtsr = ($sttmp - $t) / 60; # Minuten bis nächsten Sonnenaufgang (gerundet) $confc = $confc / $mhrs * $mtsr; storeReading ('statistic_'.$kpi, ($confc ? (sprintf "%.0f", $confc).$hcsr{$kpi}{unit} : '-')); } } } return; } ################################################################ # Grunddaten aller registrierten Consumer speichern ################################################################ sub collectAllRegConsumers { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; return if(CurrentVal ($hash, 'consumerCollected', 0)); # Abbruch wenn Consumer bereits gesammelt 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; } push @{$data{$type}{$name}{current}{consumerdevs}}, $consumer; # alle Consumerdevices in CurrentHash eintragen my $dswitch = $hc->{switchdev}; # alternatives Schaltdevice if ($dswitch) { if (!$defs{$dswitch}) { my $err = qq{ERROR - the device "$dswitch" doesn't exist anymore! Delete or change the attribute "consumer${c}".}; Log3 ($name, 1, "$name - $err"); next; } push @{$data{$type}{$name}{current}{consumerdevs}}, $dswitch; # Switchdevice zusätzlich in CurrentHash eintragen } else { $dswitch = $consumer; } 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}) { my $pcurr = $hc->{pcurr}; ($rpcurr,$upcurr,$pthreshold) = split ":", $pcurr; } my $asynchron; if (exists $hc->{asynchron}) { $asynchron = $hc->{asynchron}; } my $noshow; if (exists $hc->{noshow}) { # Consumer ausblenden in Grafik $noshow = $hc->{noshow}; } 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 ($dswoffcond,$rswoffcond,$swoffcondregex) = split ":", $hc->{swoffcond}; } my ($dspignorecond,$rigncond,$spignorecondregex); if(exists $hc->{spignorecond}) { # Bedingung um vorhandenen PV Überschuß zu ignorieren ($dspignorecond,$rigncond,$spignorecondregex) = split ":", $hc->{spignorecond}; } my $interruptable = 0; my ($hyst); if (exists $hc->{interruptable} && $hc->{interruptable} ne '0') { $interruptable = $hc->{interruptable}; ($interruptable,$hyst) = $interruptable =~ /(.*):(.*)$/xs if($interruptable ne '1'); } delete $data{$type}{$name}{consumers}{$c}{sunriseshift}; delete $data{$type}{$name}{consumers}{$c}{sunsetshift}; my ($riseshift, $setshift); if (exists $hc->{mintime}) { # Check Regex my $mintime = $hc->{mintime}; if ($mintime =~ /^SunPath/xsi) { (undef, $riseshift, $setshift) = split ":", $mintime, 3; $riseshift *= 60 if($riseshift); $setshift *= 60 if($setshift); } } my $clt; if (exists $hc->{locktime}) { $clt = $hc->{locktime}; } my $rauto = $hc->{auto} // q{}; my $ctype = $hc->{type} // $defctype; $data{$type}{$name}{consumers}{$c}{name} = $consumer; # Name des Verbrauchers (Device) $data{$type}{$name}{consumers}{$c}{alias} = $alias; # Alias des Verbrauchers (Device) $data{$type}{$name}{consumers}{$c}{type} = $hc->{type} // $defctype; # Typ des Verbrauchers $data{$type}{$name}{consumers}{$c}{power} = $hc->{power}; # Leistungsaufnahme des Verbrauchers in W $data{$type}{$name}{consumers}{$c}{avgenergy} = q{}; # Initialwert Energieverbrauch (evtl. Überschreiben in manageConsumerData) $data{$type}{$name}{consumers}{$c}{mintime} = $hc->{mintime} // $hef{$ctype}{mt}; # Initialwert min. Einplanungsdauer (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}{offcom} = $hc->{off} // q{}; # Setter Ausschaltkommando $data{$type}{$name}{consumers}{$c}{dswitch} = $dswitch; # Switchdevice zur Kommandoausführung $data{$type}{$name}{consumers}{$c}{autoreading} = $rauto; # Readingname zur Automatiksteuerung $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}{rpcurr} = $rpcurr // q{}; # Reading der aktuellen Leistungsaufnahme $data{$type}{$name}{consumers}{$c}{upcurr} = $upcurr // q{}; # Unit der aktuellen Leistungsaufnahme $data{$type}{$name}{consumers}{$c}{energythreshold} = $ethreshold; # Schwellenwert (Wh pro Stunde) ab der ein Verbraucher als aktiv gewertet wird $data{$type}{$name}{consumers}{$c}{powerthreshold} = $pthreshold; # 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 $data{$type}{$name}{consumers}{$c}{asynchron} = $asynchron // 0; # Arbeitsweise FHEM Consumer Device $data{$type}{$name}{consumers}{$c}{noshow} = $noshow // 0; # ausblenden in Grafik $data{$type}{$name}{consumers}{$c}{locktime} = $clt // '0:0'; # Sperrzeit im Automatikmodus ('offlt:onlt') $data{$type}{$name}{consumers}{$c}{onreg} = $onreg // 'on'; # Regex für 'ein' $data{$type}{$name}{consumers}{$c}{offreg} = $offreg // 'off'; # Regex für 'aus' $data{$type}{$name}{consumers}{$c}{dswoncond} = $dswoncond // q{}; # Device zur Lieferung einer zusätzliche Einschaltbedingung $data{$type}{$name}{consumers}{$c}{rswoncond} = $rswoncond // q{}; # Reading zur Lieferung einer zusätzliche Einschaltbedingung $data{$type}{$name}{consumers}{$c}{swoncondregex} = $swoncondregex // q{}; # Regex einer zusätzliche Einschaltbedingung $data{$type}{$name}{consumers}{$c}{dswoffcond} = $dswoffcond // q{}; # Device zur Lieferung einer vorrangigen Ausschaltbedingung $data{$type}{$name}{consumers}{$c}{rswoffcond} = $rswoffcond // q{}; # Reading zur Lieferung einer vorrangigen Ausschaltbedingung $data{$type}{$name}{consumers}{$c}{swoffcondregex} = $swoffcondregex // q{}; # Regex einer vorrangigen Ausschaltbedingung $data{$type}{$name}{consumers}{$c}{dspignorecond} = $dspignorecond // q{}; # Device liefert Ignore Bedingung $data{$type}{$name}{consumers}{$c}{rigncond} = $rigncond // q{}; # Reading liefert Ignore Bedingung $data{$type}{$name}{consumers}{$c}{spignorecondregex} = $spignorecondregex // q{}; # Regex der Ignore Bedingung $data{$type}{$name}{consumers}{$c}{interruptable} = $interruptable; # Ein-Zustand des Verbrauchers ist unterbrechbar $data{$type}{$name}{consumers}{$c}{hysteresis} = $hyst // $defhyst; # Hysterese $data{$type}{$name}{consumers}{$c}{sunriseshift} = $riseshift if(defined $riseshift); # Verschiebung (Sekunden) Sonnenaufgang bei SunPath Verwendung $data{$type}{$name}{consumers}{$c}{sunsetshift} = $setshift if(defined $setshift); # Verschiebung (Sekunden) Sonnenuntergang bei SunPath Verwendung } $data{$type}{$name}{current}{consumerCollected} = 1; Log3 ($name, 3, "$name - all registered consumers collected"); return; } ################################################################ # FHEMWEB Fn ################################################################ sub FwFn { my ($FW_wname, $name, $room, $pageHash) = @_; # pageHash is set for summaryFn. my $hash = $defs{$name}; $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) { pageRefresh ($hash); } return $ret; } ########################################################################### # Seitenrefresh festgelegt durch SolarForecast-Attribut "ctrlAutoRefresh" # und "ctrlAutoRefreshFW" ########################################################################### sub pageRefresh { my $hash = shift; my $name = $hash->{NAME}; my $al = AttrVal($name, 'ctrlAutoRefresh', 0); if($al) { my $rftime = gettimeofday()+$al; if (!$hash->{HELPER}{AREFRESH} || $hash->{HELPER}{AREFRESH} <= gettimeofday()) { RemoveInternalTimer ($hash, \&pageRefresh); InternalTimer($rftime, \&pageRefresh, $hash, 0); my $rd = AttrVal ($name, 'ctrlAutoRefreshFW', $hash->{HELPER}{FW}); { map { FW_directNotify("#FHEMWEB:$_", "location.reload('true')", "") } $rd } ## no critic 'Map blocks' $hash->{HELPER}{AREFRESH} = $rftime; $hash->{AUTOREFRESH} = FmtDateTime($rftime); } } else { delete $hash->{HELPER}{AREFRESH}; delete $hash->{AUTOREFRESH}; RemoveInternalTimer ($hash, \&pageRefresh); } return; } ################################################################ # Grafik als HTML zurück liefern (z.B. für Widget) ################################################################ sub pageAsHtml { my $name = shift; my $ftui = shift // ''; my $gsel = shift // ''; # direkte Auswahl welche Grafik zurück gegeben werden soll (both, flow, forecast) my $ret = ""; $ret .= entryGraphic ($name, $ftui, $gsel, 1); $ret .= ""; return $ret; } ################################################################ # Einstieg Grafikanzeige ################################################################ sub entryGraphic { my $name = shift; my $ftui = shift // ''; my $gsel = shift // ''; # direkte Auswahl welche Grafik zurück gegeben werden soll (both, flow, forecast) my $pah = shift // 0; # 1 wenn durch pageAsHtml aufgerufen 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 $hash->{HELPER}{SPGROOM} = $FW_room ? $FW_room : ""; # Raum aus dem das SolarForecastSPG-Device die Funktion aufrief $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 $maxhours = AttrNum ($name, 'graphicHourCount', 24); my $alias = AttrVal ($name, 'alias', $name); # Linktext als Aliasname oder Devicename setzen my $html_start = AttrVal ($name, 'graphicStartHtml', undef); # beliebige HTML Strings die vor der Grafik ausgegeben werden 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 $w = $width * $maxhours; # gesammte Breite der Ausgabe , WetterIcon braucht ca. 34px my $offset = -1 * AttrNum ($name, 'graphicHistoryHour', $histhourdef); my $dlink = qq{$alias}; if (!$gsel) { $gsel = AttrVal ($name, 'graphicSelect', 'both'); # Auswahl der anzuzeigenden Grafiken } my $paref = { hash => $hash, name => $name, type => $hash->{TYPE}, ftui => $ftui, pah => $pah, maxhours => $maxhours, t => time, modulo => 1, 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), beam1cont => AttrVal ($name, 'graphicBeam1Content', 'pvReal'), 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 lotype => AttrVal ($name, 'graphicLayoutType', 'double'), kw => AttrVal ($name, 'graphicEnergyUnit', 'Wh'), height => AttrNum ($name, 'graphicBeamHeight', 200), width => $width, fsize => AttrNum ($name, 'graphicSpaceSize', 24), maxVal => AttrNum ($name, 'graphicBeam1MaxVal', 0), # dyn. Anpassung der Balkenhöhe oder statisch ? show_night => AttrNum ($name, 'graphicShowNight', 0), # alle Balken (Spalten) anzeigen ? show_diff => AttrVal ($name, 'graphicShowDiff', 'no'), # zusätzliche Anzeige $di{} in allen Typen weather => AttrNum ($name, 'graphicShowWeather', 1), colorw => AttrVal ($name, 'graphicWeatherColor', $wthcolddef), # Wetter Icon Farbe Tag colorwn => AttrVal ($name, 'graphicWeatherColorNight', $wthcolndef), # Wetter Icon Farbe Nacht 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 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 flowgconsPower => AttrVal ($name, 'flowGraphicShowConsumerPower' , 1), # Verbraucher Leistung 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 genpvdva => AttrVal ($name, 'ctrlGenPVdeviation', 'daily'), # Methode der Abweichungsberechnung lang => AttrVal ($name, 'ctrlLanguage', AttrVal ('global', 'language', $deflang)), debug => getDebug ($hash), # Debug Module }; my $ret = q{}; $ret .= "$dlink
" if(AttrVal($name, 'ctrlShowLink', 0)); $ret .= $html_start if (defined($html_start)); #$ret .= ""; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "
"; # Headerzeile generieren ########################## my $header = _graphicHeader ($paref); # Verbraucherlegende und Steuerung ################################### my $legendtxt = _graphicConsumerLegend ($paref); # Headerzeile und/oder Verbraucherlegende ausblenden ###################################################### if ($gsel =~ /_noHead/xs) { $header = q{}; } if ($gsel =~ /_noCons/xs) { $legendtxt = q{}; } $ret .= "\n"; # das \n erleichtert das Lesen der debug Quelltextausgabe my $m = $paref->{modulo} % 2; 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; # Balkengrafik ################ if ($gsel =~ /both/xs || $gsel =~ /forecast/xs) { 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->{maxDif} = $back->{maxDif}; # für Typ diff $paref->{minDif} = $back->{minDif}; # für Typ diff # Balkengrafik ################ $ret .= _beamGraphic ($paref); } $m = $paref->{modulo} % 2; # Flußgrafik ############## if ($gsel =~ /both/xs || $gsel =~ /flow/xs) { $ret .= ""; my $fg = _flowGraphic ($paref); $ret .= ""; $ret .= ""; $paref->{modulo}++; } $m = $paref->{modulo} % 2; # Legende unten ################# if ($legendtxt && ($clegend eq 'bottom')) { $ret .= ""; #$ret .= ""; $ret .= ""; } $ret .= "
$header
"; $ret .= $legendtxt; $ret .= "
"; $ret .= "$fg
"; $ret .= ""; $ret .= "$legendtxt
"; $ret .= "
"; $ret .= $html_end if (defined($html_end)); return $ret; } ################################################################ # Vollständigkeit Setup prüfen ################################################################ sub _checkSetupNotComplete { my $hash = shift; my $ret = q{}; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; ### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !! ########################################################################################## ########################################################################################## my $is = ReadingsVal ($name, 'inverterStrings', undef); # String Konfig my $wedev = AttrVal ($name, 'ctrlWeatherDev1', undef); # Device Vorhersage Wetterdaten (Bewölkung etc.) my $radev = ReadingsVal ($name, 'currentRadiationAPI', 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 $maz = ReadingsVal ($name, 'moduleAzimuth', undef); # Modulausrichtung Konfig (Azimut) my $mdec = ReadingsVal ($name, 'moduleDeclination', undef); # Modul Neigungswinkel Konfig my $mrt = ReadingsVal ($name, 'moduleRoofTops', undef); # RoofTop Konfiguration (SolCast API) my $vrmcr = SolCastAPIVal ($hash, '?VRM', '?API', 'credentials', ''); # Victron VRM Credentials gesetzt my ($coset, $lat, $lon) = locCoordinates(); # Koordinaten im global device my $rip; $rip = 1 if(exists $data{$type}{$name}{solcastapi}{'?IdPair'}); # es existiert mindestens ein Paar RoofTop-ID / API-Key my $pv0 = NexthoursVal ($hash, 'NextHour00', 'pvfc', undef); # der erste PV ForeCast Wert my $link = qq{$name}; my $height = AttrNum ($name, 'graphicBeamHeight', 200); my $lang = getLang ($hash); my (undef, $disabled, $inactive) = controller ($name); if ($disabled || $inactive) { $ret .= ""; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "
"; $ret .= qq{SolarForecast device $link is disabled or inactive}; $ret .= "
"; return $ret; } ## Anlagen Check-Icon ####################### my $cmdplchk = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=get $name plantConfigCheck', function(data){FW_okDialog(data)})"}; my $img = FW_makeImage('edit_settings@grey'); my $chkicon = "$img"; my $chktitle = $htitles{plchk}{$lang}; if (!$is || !$wedev || !$radev || !$indev || !$medev || !$peaks || (isSolCastUsed ($hash) ? (!$rip || !$mrt) : isVictronKiUsed ($hash) ? !$vrmcr : (!$maz || !$mdec )) || (isForecastSolarUsed ($hash) ? !$coset : '') || (isOpenMeteoUsed ($hash) ? !$coset : '') || !defined $pv0) { $ret .= ""; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= qq{"; $ret .= ""; $ret .= "
"; $ret .= $hqtxt{entry}{$lang}; # Entry Text if (!$wedev) { ## no critic 'Cascading' $ret .= $hqtxt{cfd}{$lang}; } elsif (!$is) { $ret .= $hqtxt{ist}{$lang}; } elsif (!$peaks) { $ret .= $hqtxt{mps}{$lang}; } elsif (!$radev) { $ret .= $hqtxt{crd}{$lang}; } elsif (!$indev) { $ret .= $hqtxt{cid}{$lang}; } elsif (!$medev) { $ret .= $hqtxt{mid}{$lang}; } elsif (!$rip && isSolCastUsed ($hash)) { $ret .= $hqtxt{rip}{$lang}; } elsif (!$mrt && isSolCastUsed ($hash)) { $ret .= $hqtxt{mrt}{$lang}; } elsif (!$maz && !isSolCastUsed ($hash) && !isVictronKiUsed ($hash)) { $ret .= $hqtxt{mdr}{$lang}; } elsif (!$mdec && !isSolCastUsed ($hash) && !isVictronKiUsed ($hash)) { $ret .= $hqtxt{mta}{$lang}; } elsif (!$vrmcr && isVictronKiUsed ($hash)) { $ret .= $hqtxt{vrmcr}{$lang}; } elsif (!$coset && isForecastSolarUsed ($hash)) { $ret .= $hqtxt{coord}{$lang}; } elsif (!$coset && isOpenMeteoUsed ($hash)) { $ret .= $hqtxt{coord}{$lang}; } elsif (!defined $pv0) { $ret .= $hqtxt{awd}{$lang}; $ret .= "
$chkicon}; } $ret .= "
"; $ret =~ s/LINK/$link/gxs; return $ret; } return; } ################################################################ # forecastGraphic Headerzeile generieren ################################################################ 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}; my $name = $paref->{name}; 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 $co4h = ReadingsNum ($name, "NextHours_Sum04_ConsumptionForecast", 0); my $coRe = ReadingsNum ($name, "RestOfDayConsumptionForecast", 0); my $coTo = ReadingsNum ($name, "Tomorrow_ConsumptionForecast", 0); my $coCu = CurrentVal ($hash, 'consumption', 0); my $pv4h = ReadingsNum ($name, "NextHours_Sum04_PVforecast", 0); my $pvRe = ReadingsNum ($name, "RestOfDayPVforecast", 0); my $pvTo = ReadingsNum ($name, "Tomorrow_PVforecast", 0); my $pvCu = ReadingsNum ($name, "Current_PV", 0); if ($kw eq 'kWh') { $co4h = sprintf ("%.1f", $co4h/1000)." kWh"; $coRe = sprintf ("%.1f", $coRe/1000)." kWh"; $coTo = sprintf ("%.1f", $coTo/1000)." kWh"; $coCu = sprintf ("%.1f", $coCu/1000)." kW"; $pv4h = sprintf ("%.1f", $pv4h/1000)." kWh"; $pvRe = sprintf ("%.1f", $pvRe/1000)." kWh"; $pvTo = sprintf ("%.1f", $pvTo/1000)." kWh"; $pvCu = sprintf ("%.1f", $pvCu/1000)." kW"; } else { $co4h .= " Wh"; $coRe .= " Wh"; $coTo .= " Wh"; $coCu .= " W"; $pv4h .= " Wh"; $pvRe .= " Wh"; $pvTo .= " Wh"; $pvCu .= " W"; } my $lupt = $hqtxt{lupt}{$lang}; my $autoct = $hqtxt{autoct}{$lang}; my $aihtxt = $hqtxt{aihtxt}{$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{}; # Header Link + Status + Update Button ######################################### if ($hdrDetail =~ /all|status/xs) { my ($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"; } 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')"}; } ## Anlagen Check-Icon ####################### $img = FW_makeImage('edit_settings@grey'); my $chkicon = "$img"; my $chktitle = $htitles{plchk}{$lang}; ## Forum Thread-Icon ###################### $img = FW_makeImage('time_note@grey'); my $fthicon = "$img"; my $fthtitle = $htitles{jtsfft}{$lang}; ## Update-Icon ################ my $upicon = __createUpdateIcon ($paref); ## Sonnenauf- und untergang / Wetterdaten Aktualität ###################################################### my $sriseimg = FW_makeImage('weather_sunrise@darkorange'); my $ssetimg = FW_makeImage('weather_sunset@LightCoral'); my $srisetxt = ReadingsVal ($name, 'Today_SunRise', '-'); my $ssettxt = ReadingsVal ($name, 'Today_SunSet', '-'); my ($err, $resh) = isWeatherAgeExceeded ($paref); $img = FW_makeImage ('10px-kreis-gruen.png', $htitles{dwfcrsu}{$lang}.' '.$resh->{mosmix}.' '.$htitles{dwdtime}{$lang}.': '.$resh->{fctime}); if (!$err && $resh->{exceed}) { my $agewfc = $htitles{aswfc2o}{$lang}; $agewfc =~ s//$name/xs; $img = FW_makeImage ('10px-kreis-gelb.png', $agewfc.' '.$htitles{dwdtime}{$lang}.': '.$resh->{fctime}); } my $waicon = "$img"; # Icon Wetterdaten Alter ## Autokorrektur-Icon ###################### my $acicon = __createAutokorrIcon ($paref); ## Solare API Sektion ######################## my $api = isSolCastUsed ($hash) ? 'SolCast:' : isForecastSolarUsed ($hash) ? 'Forecast.Solar:' : isVictronKiUsed ($hash) ? 'VictronVRM:' : isDWDUsed ($hash) ? 'DWD:' : isOpenMeteoUsed ($hash) ? 'OpenMeteo:' : q{}; my $nscc = ReadingsVal ($name, 'nextRadiationAPICall', '?'); my $lrt = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_time', '-'); my $scrm = SolCastAPIVal ($hash, '?All', '?All', 'response_message', '-'); 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"; } } if ($api =~ /SolCast/xs) { $api .= ' '.$lrt; if ($scrm eq 'success') { $img = FW_makeImage ('10px-kreis-gruen.png', $htitles{scaresps}{$lang}.' '.$htitles{natc}{$lang}.' '.$nscc); } elsif ($scrm =~ /Rate limit for API calls reached/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', $htitles{scarespf}{$lang}.': '. $scrm); } $scicon = "$img"; $api .= '  '.$scicon; $api .= ''; $api .= '  ('; $api .= SolCastAPIVal ($hash, '?All', '?All', 'todayDoneAPIrequests', 0); $api .= '/'; $api .= SolCastAPIVal ($hash, '?All', '?All', 'todayRemainingAPIrequests', $solcmaxreqdef); $api .= ')'; $api .= ''; } elsif ($api =~ /Forecast.Solar/xs) { $api .= ' '.$lrt; if ($scrm eq 'success') { $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{rlfaccpr}{$lang}); } else { $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', 'requests_remaining', '-'); $api .= ')'; $api .= ''; } elsif ($api =~ /VictronVRM/xs) { $api .= ' '.$lrt; if ($scrm eq 'success') { $img = FW_makeImage('10px-kreis-gruen.png', $htitles{scaresps}{$lang}.' '.$htitles{natc}{$lang}.' '.$nscc); } else { $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 .= ''; } elsif ($api =~ /DWD/xs) { $nscc = ReadingsVal ($name, 'nextCycletime', '?'); $api .= ' '.$lrt; if ($scrm eq 'success') { ($err, $resh) = isRad1hAgeExceeded ($paref); $img = FW_makeImage ('10px-kreis-gruen.png', $htitles{scaresps}{$lang}.' '.$htitles{dwfcrsu}{$lang}.' '.$resh->{mosmix}.' '.$htitles{predtime}{$lang}.' '.$resh->{fctime}); if (!$err && $resh->{exceed}) { my $agetit = $htitles{arsrad2o}{$lang}; $agetit =~ s//$name/xs; $img = FW_makeImage ('10px-kreis-gelb.png', $agetit.' '.$htitles{predtime}{$lang}.' '.$resh->{fctime}); } } else { $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 .= ''; } elsif ($api =~ /OpenMeteo/xs) { $api .= ' '.$lrt; if ($scrm eq 'success') { $img = FW_makeImage ('10px-kreis-gruen.png', $htitles{scaresps}{$lang}.' '.$htitles{natc}{$lang}.' '.$nscc); } else { $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', $ometmaxreq); $api .= ')'; $api .= ''; } ## Qualitäts-Icon ###################### my $pcqicon = __createQuaIcon ($paref); ## KI Status ############## my $aiicon = __createAIicon ($paref); ## Abweichung PV Prognose/Erzeugung ##################################### my $tdayDvtn = CircularVal ($hash, 99, 'tdayDvtn', '-'); my $ydayDvtn = CircularVal ($hash, 99, '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 $genpvdva = $paref->{genpvdva}; my $dvtntxt = $hqtxt{dvtn}{$lang}.' '; my $tdaytxt = ($genpvdva eq 'daily' ? $hqtxt{tday}{$lang} : $hqtxt{ctnsly}{$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}; $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 ######################## if ($hdrDetail =~ /all|pv/xs) { $header .= ""; $header .= ""; $header .= ""; $header .= ""; $header .= ""; $header .= ""; $header .= ""; } # Header Information co ######################### if ($hdrDetail =~ /all|co/xs) { $header .= ""; $header .= ""; $header .= ""; $header .= ""; $header .= ""; $header .= ""; $header .= ""; } if ($hdrDetail =~ /all|pv|co/xs) { $header .= qq{}; $header .= qq{}; $header .= qq{}; } # Header User Spezifikation ############################# my $ownv = __createOwnSpec ($paref); $header .= $ownv if($ownv); $header .= qq{
$dlink $chkicon $fthicon $lupt $lup   $upicon $api
$sriseimg   $srisetxt     $ssetimg   $ssettxt     $waicon $autoct    $acicon       $lbpcq    $pcqicon       $aihtxt    $aiicon $dvtntxt}; $header .= qq{}; $header .= qq{$tdaytxt}; $header .= qq{}; $header .= qq{, }; $header .= qq{}; $header .= qq{$ydaytxt}; $header .= qq{}; $header .= qq{

".$hqtxt{pvgen}{$lang}." $lblPvCu $pvCu$lblPv4h $pv4h$lblPvRe $pvRe$lblPvTo $pvTo
".$hqtxt{conspt}{$lang}." $lblPvCu$coCu$lblPv4h$co4h$lblPvRe$coRe$lblPvTo$coTo

}; return $header; } ################################################################ # erstelle Update-Icon ################################################################ sub __createUpdateIcon { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $lang = $paref->{lang}; my $ftui = $paref->{ftui}; my $upstate = ReadingsVal ($name, 'state', ''); my $naup = ReadingsVal ($name, 'nextCycletime', ''); 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 ($img, $upicon); if ($upstate =~ /updated|successfully|switched/ix) { $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"; } return $upicon; } ################################################################ # erstelle Autokorrektur-Icon ################################################################ sub __createAutokorrIcon { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $lang = $paref->{lang}; my $aciimg; my $acitit = q{}; my ($acu, $aln) = isAutoCorrUsed ($name); if ($acu =~ /on/xs) { $aciimg = FW_makeImage ('10px-kreis-gruen.png', $htitles{on}{$lang}." ($acu)"); } elsif ($acu =~ /standby/ixs) { my $pcfa = ReadingsVal ($name, 'pvCorrectionFactor_Auto', 'off'); my ($rtime) = $pcfa =~ /for (.*?) hours/x; my $img = FW_makeImage ('10px-kreis-gelb.png', $htitles{dela}{$lang}); $aciimg = "$img (Start in ".$rtime." h)"; } else { $acitit = $htitles{akorron}{$lang}; $acitit =~ s//$name/xs; $aciimg = '-'; } my $acicon = qq{$aciimg}; return $acicon; } ################################################################ # erstelle Qualitäts-Icon ################################################################ sub __createQuaIcon { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $lang = $paref->{lang}; my $ftui = $paref->{ftui}; my $pvfc00 = NexthoursVal ($hash, 'NextHour00', 'pvfc', undef); my $pvcorrf00 = NexthoursVal ($hash, "NextHour00", "pvcorrf", "-/-"); my ($pcf,$pcq) = split "/", $pvcorrf00; my $pvcanz = qq{factor: $pcf / quality: $pcq}; my $cmdfcqal = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=get $name forecastQualities imgget', function(data){FW_okDialog(data)})"}; if ($ftui eq 'ftui') { $cmdfcqal = qq{"ftui.setFhemStatus('get $name forecastQualities imgget')"}; } $pcq =~ s/-/-1/xs; my $pcqimg = $pcq < 0.00 ? FW_makeImage ('15px-blank', $pvcanz) : $pcq < 0.60 ? FW_makeImage ('10px-kreis-rot.png', $pvcanz) : $pcq < 0.80 ? FW_makeImage ('10px-kreis-gelb.png', $pvcanz) : FW_makeImage ('10px-kreis-gruen.png', $pvcanz); my $pcqtit = q(); if(!$pvfc00 || $pcq == -1) { $pcqimg = "-"; $pcqtit = $htitles{norate}{$lang}; } my $pcqicon = qq{$pcqimg}; return $pcqicon; } ################################################################ # erstelle KI Icon ################################################################ sub __createAIicon { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $lang = $paref->{lang}; my $aiprep = isPrepared4AI ($hash, 'full'); # isPrepared4AI full vor Abfrage 'aicanuse' ausführen ! my $aicanuse = CurrentVal ($hash, 'aicanuse', ''); my $aitst = CurrentVal ($hash, 'aitrainstate', 'ok'); my $aihit = NexthoursVal ($hash, 'NextHour00', 'aihit', 0); my $aitit = $aidtabs ? $htitles{aimstt}{$lang} : $aicanuse ne 'ok' ? $htitles{ainuse}{$lang} : q{}; $aitit =~ s//$name/xs; my $atf = CircularVal ($hash, 99, 'aitrainLastFinishTs', 0); $atf = $hqtxt{ailatr}{$lang}.' '.($atf ? (timestampToTimestring ($atf, $lang))[0] : '-'); my $aiimg = $aidtabs ? '--' : $aicanuse ne 'ok' ? '-' : $aitst ne 'ok' ? FW_makeImage ('10px-kreis-rot.png', $aitst) : $aihit ? FW_makeImage ('10px-kreis-gruen.png', $hqtxt{aiwhit}{$lang}.' '.$atf) : FW_makeImage ('10px-kreis-gelb.png', $hqtxt{aiwook}{$lang}.' '.$atf); my $aiicon = qq{$aiimg}; return $aiicon; } ################################################################ # erstelle Übersicht eigener Readings ################################################################ sub __createOwnSpec { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $dstyle = $paref->{dstyle}; # TD-Style my $hdrDetail = $paref->{hdrDetail}; my $pah = $paref->{pah}; # 1 wenn durch pageAsHtml abgerufen my $vinr = 4; # Spezifikationen in einer Zeile my $spec = AttrVal ($name, 'graphicHeaderOwnspec', ''); my $uatr = AttrVal ($name, 'graphicEnergyUnit', 'Wh'); my $show = $hdrDetail =~ /all|own/xs ? 1 : 0; return if(!$spec || !$show); my $allsets = ' '.FW_widgetOverride ($name, getAllSets ($name), 'set').' '; my $allattrs = ' '.FW_widgetOverride ($name, getAllAttr ($name), 'attr').' '; # Leerzeichen wichtig für Regexvergleich my @fields = split (/\s+/sx, $spec); my (@cats, @vals); for my $f (@fields) { if ($f =~ /^\#(.*)/xs) { push @cats, $1; next; } push @vals, $f; } my $ownv; my $rows = ceil (scalar(@vals) / $vinr); my $col = 0; for (my $i = 1 ; $i <= $rows; $i++) { my ($h, $v, $u); for (my $k = 0 ; $k < $vinr; $k++) { ($h->{$k}{label}, $h->{$k}{elm}) = split ":", $vals[$col] if($vals[$col]); # Label und darzustellendes Element $h->{$k}{elm} //= ''; my ($elm, $dev) = split "@", $h->{$k}{elm}; # evtl. anderes Devices $dev //= $name; $col++; if (!$h->{$k}{label}) { undef $h->{$k}{label}; next; } my $setcmd = ___getFWwidget ($name, $dev, $elm, $allsets, 'set'); # Set-Kommandos identifizieren if ($setcmd) { if ($pah) { # bei get pageAsHtml setter/attr nicht anzeigen (js Fehler) undef $h->{$k}{label}; $setcmd = ''; } $v->{$k} = $setcmd; $u->{$k} = q{}; debugLog ($paref, 'graphic', "graphicHeaderOwnspec - set-command genereated:\n$setcmd"); next; } my $attrcmd = ___getFWwidget ($name, $dev, $elm, $allattrs, 'attr'); # Attr-Kommandos identifizieren if ($attrcmd) { if ($pah) { # bei get pageAsHtml setter/attr nicht anzeigen (js Fehler) undef $h->{$k}{label}; $attrcmd = ''; } $v->{$k} = $attrcmd; $u->{$k} = q{}; debugLog ($paref, 'graphic', "graphicHeaderOwnspec - attr-command genereated:\n$attrcmd"); next; } $v->{$k} = ReadingsVal ($dev, $elm, ''); if ($v->{$k} =~ /^\s*(-?\d+(\.\d+)?)/xs) { ($v->{$k}, $u->{$k}) = split /\s+/, ReadingsVal ($dev, $elm, ''); # Value und Unit trennen wenn Value numerisch } $v->{$k} //= q{}; $u->{$k} //= q{}; $paref->{dev} = $dev; $paref->{rdg} = $elm; $paref->{val} = $v->{$k}; $paref->{unit} = $u->{$k}; ($v->{$k}, $u->{$k}) = ___ghoValForm ($paref); delete $paref->{dev}; delete $paref->{rdg}; delete $paref->{val}; delete $paref->{unit}; next if(!$u->{$k}); if ($uatr eq 'kWh') { if ($u->{$k} =~ /^Wh/xs) { $v->{$k} = sprintf "%.1f",($v->{$k} / 1000); $u->{$k} = 'kWh'; } } if ($uatr eq 'Wh') { if ($u->{$k} =~ /^kWh/xs) { $v->{$k} = sprintf "%.0f",($v->{$k} * 1000); $u->{$k} = 'Wh'; } } } $ownv .= ""; $ownv .= "".($cats[$i-1] ? ''.$cats[$i-1].'' : '').""; $ownv .= "".$h->{0}{label}.": ".$v->{0}." ".$u->{0}."" if(defined $h->{0}{label}); $ownv .= "".$h->{1}{label}.": ".$v->{1}." ".$u->{1}."" if(defined $h->{1}{label}); $ownv .= "".$h->{2}{label}.": ".$v->{2}." ".$u->{2}."" if(defined $h->{2}{label}); $ownv .= "".$h->{3}{label}.": ".$v->{3}." ".$u->{3}."" if(defined $h->{3}{label}); $ownv .= ""; } $ownv .= qq{}; $ownv .= qq{
}; $ownv .= qq{}; return $ownv; } ################################################################ # liefert ein FHEMWEB set/attr Widget zurück ################################################################ sub ___getFWwidget { my $name = shift; my $dev = shift // $name; # Device des Elements, default=$name my $elm = shift; # zu prüfendes Element (setter / attribut) my $allc = shift; # Kommandovorrat -> ist Element enthalten? my $ctyp = shift // 'set'; # Kommandotyp: set/attr return if(!$elm); my $widget = ''; my ($current, $reading); if ($dev ne $name) { # Element eines anderen Devices verarbeiten if ($ctyp eq 'set') { $allc = ' '.FW_widgetOverride ($dev, getAllSets($dev), 'set').' '; # Leerzeichen wichtig für Regexvergleich } elsif ($ctyp eq 'attr') { $allc = ' '.FW_widgetOverride ($dev, getAllAttr($dev), 'attr').' '; } } if ($allc =~ /\s$elm:?(.*?)\s/xs) { # Element in allen Sets oder Attr enthalten my $arg = $1; if (!$arg || $arg eq 'textField' || $arg eq 'textField-long') { # Label (Reading) ausblenden -> siehe fhemweb.js function FW_createTextField Zeile 1657 $arg = 'textFieldNL'; } if ($arg !~ /^\#/xs && $arg !~ /^$allwidgets/xs) { $arg = '#,'.$arg; } if ($arg =~ 'slider') { # Widget slider in selectnumbers für Kopfgrafik umsetzen my ($wid, $min, $step, $max, $float) = split ",", $arg; $arg = "selectnumbers,$min,$step,$max,0,lin"; } if ($ctyp eq 'attr') { # Attributwerte als verstecktes Reading abbilden $current = AttrVal ($dev, $elm, ''); $reading = '.'.$dev.'_'.$elm; } else { $current = ReadingsVal ($dev, $elm, ''); if($dev ne $name) { $reading = '.'.$dev.'_'.$elm; # verstecktes Reading in SolCast abbilden wenn Set-Kommando aus fremden Device } else { $reading = $elm; } } if ($reading && $reading =~ /^\./xs) { # verstecktes Reading für spätere Löschung merken push @widgetreadings, $reading; readingsSingleUpdate ($defs{$name}, $reading, $current, 0); } $widget = ___widgetFallback ( { name => $name, dev => $dev, ctyp => $ctyp, elm => $elm, reading => $reading, arg => $arg } ); if (!$widget) { $widget = FW_pH ("cmd=$ctyp $dev $elm", $elm, 0, "", 1, 1); } } return $widget; } ################################################################ # adaptierte FW_widgetFallbackFn aus FHEMWEB ################################################################ sub ___widgetFallback { my $pars = shift; my $name = $pars->{name}; my $dev = $pars->{dev}; my $ctyp = $pars->{ctyp}; my $elm = $pars->{elm}; my $reading = $pars->{reading}; my $arg = $pars->{arg}; return '' if(!$arg || $arg eq "noArg"); my $current = ReadingsVal ($name, $reading, undef); if (!defined $current) { $reading = 'state'; $current = ' '; } if ($current =~ /(().*?)/xs) { # Eleminierung von störenden HTML Elementen aus aktuellem Readingwert $current = ' '; } $current =~ s/$elm //; $current = ReplaceEventMap ($dev, $current, 1); return "
"; } ################################################################ # ownHeader ValueFormat ################################################################ sub ___ghoValForm { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $dev = $paref->{dev}; my $rdg = $paref->{rdg}; my $val = $paref->{val}; my $unit = $paref->{unit}; my $type = $paref->{type}; my $fn = $data{$type}{$name}{func}{ghoValForm}; return ($val, $unit) if(!$fn || !$dev || !$rdg || !defined $val); my $DEVICE = $dev; my $READING = $rdg; my $VALUE = $val; my $UNIT = $unit; my $err; if (!ref $fn && $fn =~ m/^\{.*\}$/xs) { # normale Funktionen my $efn = eval $fn; if ($@) { Log3 ($name, 1, "$name - ERROR in execute graphicHeaderOwnspecValForm: ".$@); $err = $@; } else { if (ref $efn ne 'HASH') { $val = $VALUE; $unit = $UNIT; } else { $fn = $efn; } } } if (ref $fn eq 'HASH') { # Funktionshash my $vf = ""; $vf = $fn->{$rdg} if(exists $fn->{$rdg}); $vf = $fn->{"$dev.$rdg"} if(exists $fn->{"$dev.$rdg"}); $vf = $fn->{"$rdg.$val"} if(exists $fn->{"$rdg.$val"}); $vf = $fn->{"$dev.$rdg.$val"} if(exists $fn->{"$dev.$rdg.$val"}); $fn = $vf; if ($fn =~ m/^%/xs) { $val = sprintf $fn, $val; } elsif ($fn ne "") { my $vnew = eval $fn; if ($@) { Log3 ($name, 1, "$name - ERROR in execute graphicHeaderOwnspecValForm: ".$@); $err = $@; } else { $val = $vnew; } } } if ($val =~ /^\s*(-?\d+(\.\d+)?)/xs) { # Value und Unit numerischer Werte trennen ($val, my $u1) = split /\s+/, $val; $unit = $u1 ? $u1 : $unit; } if ($err) { $err = (split "at", $err)[0]; $paref->{state} = 'ERROR - graphicHeaderOwnspecValForm: '.$err; singleUpdateState ($paref); } return ($val, $unit); } ################################################################ # Consumer in forecastGraphic (Balken) anzeigen # (Hat zur Zeit keine Wirkung !) ################################################################ sub _showConsumerInGraphicBeam { my $paref = shift; my $hash = $paref->{hash}; 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($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; } $start = int($start); $end = int($end); my $flag = 0; # default kein Tagesverschieber #correct the hour for accurate display ####################################### if ($start < $hfcg->{0}{time}) { # gridconsumption seems to be tomorrow $start = 24-$hfcg->{0}{time}+$start; $flag = 1; } else { $start -= $hfcg->{0}{time}; } if ($flag) { # gridconsumption seems to be tomorrow $end = 24-$hfcg->{0}{time}+$end; } else { $end -= $hfcg->{0}{time}; } $_ .= ":".$start.":".$end; } else { $_ .= ":24:24"; } } return; } ################################################################ # Verbraucherlegende und Steuerung ################################################################ 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{}; $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; my $cnum = @consumers; if ($cnum > 1) { $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; $ctable .= qq{}; } else { my $blk = ' ' x 8; $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) { next if(isConsumerNoshow ($hash, $c) =~ /^[12]$/xs); # Consumer ausblenden my $caicon = $paref->{caicon}; # Consumer AdviceIcon my ($cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name my $calias = ConsumerVal ($hash, $c, 'alias', $cname); # Alias 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 $c 0 set $dswname $oncom')"}; my $cmdoff = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction $c 0 set $dswname $offcom')"}; my $cmdautoon = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction $c 0 setreading $dswname $autord 1')"}; my $cmdautooff = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction $c 0 setreading $dswname $autord 0')"}; my $implan = qq{"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name clientAction $c 0 consumerImmediatePlanning $c')"}; if ($ftui eq "ftui") { $cmdon = qq{"ftui.setFhemStatus('set $name clientAction $c 0 set $dswname $oncom')"}; $cmdoff = qq{"ftui.setFhemStatus('set $name clientAction $c 0 set $dswname $offcom')"}; $cmdautoon = qq{"ftui.setFhemStatus('set $name clientAction $c 0 setreading $cname $autord 1')"}; $cmdautooff = qq{"ftui.setFhemStatus('set $name clientAction $c 0 setreading $cname $autord 0')"}; $implan = qq{"ftui.setFhemStatus('set $name clientAction $c 0 consumerImmediatePlanning $c')"}; } $cmdon = q{} if(!$oncom); $cmdoff = q{} if(!$offcom); $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,$supplmnt) = __getPlanningStateAndTimes ($paref); $supplmnt = '-' if(!$supplmnt); my ($iilt,$rlt) = isInLocktime ($paref); # Sperrzeit Status ermitteln 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//$supplmnt/xs; $pstate =~ s//$starttime/xs; $pstate =~ s//$stoptime/xs; $pstate =~ s//$rlt/xs; $pstate =~ s/\s+/ /gxs if($caicon eq "times"); if ($clink) { $calias = qq{$c - $calias}; } if ($caicon ne "none") { if (isInTimeframe($hash, $c)) { # innerhalb Planungszeitraum ? if ($caicon eq "times") { $isricon = $pstate.'
'.$surplusinfo; } else { $isricon = "".FW_makeImage($caicon, '')." "; if ($planstate =~ /priority/xs) { my (undef,$color) = split '@', $caicon; $color = $color ? '@'.$color : ''; $isricon = "".FW_makeImage('batterie'.$color, '')." "; } } } else { if ($caicon eq "times") { $isricon = $pstate.'
'.$surplusinfo; } else { ($caicon) = split '@', $caicon; $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"; } else { $staticon = FW_makeImage('ios_off_fill@grey', $htitles{ians}{$lang}); $swicon = " $staticon"; } } if (isConsumerPhysOn($hash, $c)) { # Schaltzustand des Consumerdevices on if($cmdoff) { $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"; } } if ($clegendstyle eq 'icon') { $cicon = FW_makeImage($cicon); $ctable .= ""; $ctable .= ""; $ctable .= ""; $ctable .= ""; $ctable .= ""; } else { my (undef,$co) = split '@', $cicon; $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); if ($clegend ne 'bottom') { $ctable .= qq{}; } $ctable .= qq{
$hqtxt{cnsm}{$lang} $hqtxt{eiau}{$lang} $hqtxt{auto}{$lang}          $hqtxt{cnsm}{$lang} $hqtxt{eiau}{$lang} $hqtxt{auto}{$lang} $blk $blk $blk $blk $blk

$calias $cicon $isricon $swicon $auicon $calias $isricon $swicon $auicon
        

}; return $ctable; } ################################################################ # Werte aktuelle Stunde für forecastGraphic ################################################################ sub _beamGraphicFirstHour { my $paref = shift; my $hash = $paref->{hash}; my $hfcg = $paref->{hfcg}; my $offset = $paref->{offset}; my $hourstyle = $paref->{hourstyle}; my $beam1cont = $paref->{beam1cont}; my $beam2cont = $paref->{beam2cont}; my $day; my $stt = NexthoursVal ($hash, "NextHour00", "starttime", '0000-00-00 24'); my ($year,$month,$day_str,$thishour) = $stt =~ 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 $hfcg->{0}{time} = $thishour; $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 if ($offset) { $hfcg->{0}{time} += $offset; if ($hfcg->{0}{time} < 0) { $hfcg->{0}{time} += 24; my $n_day = strftime "%d", localtime($hfcg->{0}{mktime} - (3600 * abs($offset))); # Achtung : Tageswechsel - day muss jetzt neu berechnet werden ! $hfcg->{0}{day} = int($n_day); $hfcg->{0}{day_str} = $n_day; } $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); $val4 = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'confc', 0); $hfcg->{0}{weather} = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'weatherid', 999); $hfcg->{0}{wcc} = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'wcc', '-'); $hfcg->{0}{sunalt} = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'sunalt', '-'); $hfcg->{0}{sunaz} = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'sunaz', '-'); } else { $val1 = CircularVal ($hash, $hfcg->{0}{time_str}, 'pvfc', 0); $val2 = CircularVal ($hash, $hfcg->{0}{time_str}, 'pvrl', 0); $val3 = CircularVal ($hash, $hfcg->{0}{time_str}, 'gcons', 0); $val4 = CircularVal ($hash, $hfcg->{0}{time_str}, 'confc', 0); $hfcg->{0}{weather} = CircularVal ($hash, $hfcg->{0}{time_str}, 'weatherid', 999); #$val4 = (ReadingsVal($name,"ThisHour_IsConsumptionRecommended",'no') eq 'yes' ) ? $icon : 999; } $hfcg->{0}{time_str} = sprintf('%02d', $hfcg->{0}{time}-1).$hourstyle; $hfcg->{0}{beam1} = ($beam1cont eq 'pvForecast') ? $val1 : ($beam1cont eq 'pvReal') ? $val2 : ($beam1cont eq 'gridconsumption') ? $val3 : $val4; $hfcg->{0}{beam2} = ($beam2cont eq 'pvForecast') ? $val1 : ($beam2cont eq 'pvReal') ? $val2 : ($beam2cont eq 'gridconsumption') ? $val3 : $val4; $hfcg->{0}{diff} = $hfcg->{0}{beam1} - $hfcg->{0}{beam2}; return ($thishour); } ################################################################ # Werte restliche Stunden für forecastGraphic ################################################################ sub _beamGraphicRemainingHours { my $paref = shift; my $hash = $paref->{hash}; my $hfcg = $paref->{hfcg}; my $offset = $paref->{offset}; my $maxhours = $paref->{maxhours}; my $hourstyle = $paref->{hourstyle}; my $beam1cont = $paref->{beam1cont}; my $beam2cont = $paref->{beam2cont}; my $maxVal = $paref->{maxVal}; # dyn. Anpassung der Balkenhöhe oder statisch ? $maxVal //= $hfcg->{0}{beam1}; # Startwert wenn kein Wert bereits via attr vorgegeben ist my ($val1,$val2,$val3,$val4); my $maxCon = $hfcg->{0}{beam1}; my $maxDif = $hfcg->{0}{diff}; # für Typ diff my $minDif = $hfcg->{0}{diff}; # für Typ diff for my $i (1..($maxhours*2)-1) { # doppelte Anzahl berechnen my $val1 = 0; $val2 = 0; $val3 = 0; $val4 = 0; $hfcg->{$i}{time} = $hfcg->{0}{time} + $i; while ($hfcg->{$i}{time} > 24) { $hfcg->{$i}{time} -= 24; # wird bis zu 2x durchlaufen } $hfcg->{$i}{time_str} = sprintf('%02d', $hfcg->{$i}{time}); my $nh; # next hour if ($offset < 0) { if ($i <= abs($offset)) { # $daystr stimmt nur nach Mitternacht, vor Mitternacht muß $hfcg->{0}{day_str} als Basis verwendet werden ! my $ds = strftime "%d", localtime($hfcg->{0}{mktime} - (3600 * abs($offset+$i))); # V0.49.4 # 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); $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', '-'); $hfcg->{$i}{sunalt} = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, 'sunalt', '-'); $hfcg->{$i}{sunaz} = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, 'sunaz', '-'); } else { $nh = sprintf '%02d', $i + $offset; } } else { $nh = sprintf '%02d', $i; } if (defined $nh) { $val1 = NexthoursVal ($hash, 'NextHour'.$nh, 'pvfc', 0); $val4 = NexthoursVal ($hash, 'NextHour'.$nh, 'confc', 0); $hfcg->{$i}{weather} = NexthoursVal ($hash, 'NextHour'.$nh, 'weatherid', 999); $hfcg->{$i}{wcc} = NexthoursVal ($hash, 'NextHour'.$nh, 'cloudcover', '-'); $hfcg->{$i}{sunalt} = NexthoursVal ($hash, 'NextHour'.$nh, 'sunalt', '-'); $hfcg->{$i}{sunaz} = NexthoursVal ($hash, 'NextHour'.$nh, 'sunaz', '-'); #$val4 = (ReadingsVal($name,"NextHour".$ii."_IsConsumptionRecommended",'no') eq 'yes') ? $icon : undef; } $hfcg->{$i}{time_str} = sprintf('%02d', $hfcg->{$i}{time}-1).$hourstyle; $hfcg->{$i}{beam1} = ($beam1cont eq 'pvForecast') ? $val1 : ($beam1cont eq 'pvReal') ? $val2 : ($beam1cont eq 'gridconsumption') ? $val3 : $val4; $hfcg->{$i}{beam2} = ($beam2cont eq 'pvForecast') ? $val1 : ($beam2cont eq 'pvReal') ? $val2 : ($beam2cont eq 'gridconsumption') ? $val3 : $val4; $hfcg->{$i}{beam1} //= 0; $hfcg->{$i}{beam2} //= 0; $hfcg->{$i}{diff} = $hfcg->{$i}{beam1} - $hfcg->{$i}{beam2}; $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); } my $back = { maxVal => $maxVal, maxCon => $maxCon, maxDif => $maxDif, minDif => $minDif, }; return ($back); } ################################################################ # Balkenausgabe für forecastGraphic ################################################################ sub _beamGraphic { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $hfcg = $paref->{hfcg}; my $maxhours = $paref->{maxhours}; my $weather = $paref->{weather}; my $show_night = $paref->{show_night}; # alle Balken (Spalten) anzeigen ? my $show_diff = $paref->{show_diff}; # zusätzliche Anzeige $di{} in allen Typen my $lotype = $paref->{lotype}; my $height = $paref->{height}; my $fsize = $paref->{fsize}; my $kw = $paref->{kw}; my $colorb1 = $paref->{colorb1}; my $colorb2 = $paref->{colorb2}; my $fcolor1 = $paref->{fcolor1}; 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 $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} && !$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; # negative Zahlen in Fettschrift, 0 aber ohne + } $ret .= "$val"; } $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 && $hfcg->{$i}{weather} > 99 && !$hfcg->{$i}{beam1} && !$hfcg->{$i}{beam2}); $ii++; last if ($ii > $maxhours); # 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 # 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 # 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 $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}; } else { # tauschen, Verbrauch ist größer als Ertrag $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; } } if ($lotype eq 'diff') { # Berechnung der Zonen # he - freier der Raum über den Balken , Zahl positiver Wert + fsize # z2 - positiver Balken inkl Icon # z3 - negativer Balken # z4 - Zahl negativer Wert + fsize my ($px_pos,$px_neg); my $maxValBeam = 0; # ToDo: maxValBeam noch aus Attribut graphicBeam1MaxVal ableiten 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 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 ? $px_pos = $height-$px_neg; # der Rest ist oben } else { # keine positiven Balken vorhanden, die Negativen bekommen den gesammten Raum $px_neg = $height; $px_pos = 0; } } } 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 } # Alle vorbesetzen Werte umrechnen auf echte Ausgabe px $he = (!$px_pos || !$maxDif) ? 0 : int(($maxDif-$z2)/$maxDif*$px_pos); # Teilung durch 0 vermeiden $z2 = ($px_pos - $he) ; $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; $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 $ret .="\n"; if ($lotype eq 'single') { $val = formatVal6 ($hfcg->{$i}{beam1}, $kw, $hfcg->{$i}{weather}); $ret .=""; # mit width=100% etwas bessere Füllung der Balken $ret .=""; $ret .="'; 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 $ret .= ""; $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;"; $ret .="
".$val.'
"; my $sicon = 1; # inject the new icon if defined ################################## #$ret .= consinject($hash,$i,@consumers) if($s); $ret .= "
\n"; # mit width=100% etwas bessere Füllung der Balken # der Freiraum oben kann beim größten Balken ganz entfallen $ret .="" if($he); if ($hfcg->{$i}{beam1} > $hfcg->{$i}{beam2}) { # wer ist oben, Beam2 oder Beam1 ? Wert und Farbe für Zone 2 & 3 vorbesetzen $val = formatVal6 ($hfcg->{$i}{beam1}, $kw, $hfcg->{$i}{weather}); $color1 = $colorb1; $style1 = $style." background-color:#$color1; color:#$fcolor1;'"; if ($z3) { # die Zuweisung können wir uns sparen wenn Zone 3 nachher eh nicht ausgegeben wird $v = formatVal6 ($hfcg->{$i}{beam2}, $kw, $hfcg->{$i}{weather}); $color2 = $colorb2; $style2 = $style." background-color:#$color2; color:#$fcolor2;'"; } } else { $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; $style2 = $style." background-color:#$color2; color:#$fcolor1;'"; } } $ret .= ""; $ret .= ""; if ($z3) { # die Zone 3 lassen wir bei zu kleinen Werten auch ganz weg $ret .= ""; $ret .= ""; } } if ($lotype eq 'diff') { # Type diff my $style = "style='padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; $ret .= "
$val"; # inject the new icon if defined ################################## #$ret .= consinject($hash,$i,@consumers) if($s); $ret .= "
$v
\n"; # Tipp : das nachfolgende border=0 auf 1 setzen hilft sehr Ausgabefehler zu endecken $val = ($hfcg->{$i}{diff} > 0) ? formatVal6 ($hfcg->{$i}{diff}, $kw, $hfcg->{$i}{weather}) : ''; $val = '   0  ' if ($hfcg->{$i}{diff} == 0); # Sonderfall , hier wird die 0 gebraucht ! if ($val) { $ret .= ""; $ret .= ""; } if ($hfcg->{$i}{diff} >= 0) { # mit Farbe 1 colorb1 füllen $style .= " background-color:#$colorb1'"; $z2 = 1 if ($hfcg->{$i}{diff} == 0); # Sonderfall , 1px dünnen Strich ausgeben $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 $ret .= ""; $ret .= ""; } } if ($hfcg->{$i}{diff} < 0) { # Negativ Balken anzeigen ? $style .= " background-color:#$colorb2'"; # mit Farbe 2 colorb2 füllen $ret .= ""; $ret .= ""; } elsif ($z3) { # ohne Farbe $ret .= ""; $ret .= ""; } if($z4) { # kann entfallen wenn auch z3 0 ist $val = ($hfcg->{$i}{diff} < 0) ? formatVal6 ($hfcg->{$i}{diff}, $kw, $hfcg->{$i}{weather}) : ' '; $ret .= ""; $ret .= ""; } } 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 .= ""; } $ret .= "
".$val."
"; $ret .= "
".$val."
$val
"; $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 .="
"; } $paref->{modulo}++; $ret .= ""; $ret .= ""; return $ret; } ################################################################ # Wetter Icon Zeile ################################################################ sub __weatherOnBeam { my $paref = shift; my $name = $paref->{name}; my $hfcg = $paref->{hfcg}; my $maxhours = $paref->{maxhours}; my $weather = $paref->{weather}; my $show_night = $paref->{show_night}; # alle Balken (Spalten) anzeigen ? 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 $ii = 0; $ret .= ""; # freier Platz am Anfang for my $i (0..($maxhours * 2) - 1) { last if (!defined ($hfcg->{$i}{weather})); $hfcg->{$i}{weather} = 999 if(!defined $hfcg->{$i}{weather}); my $wcc = $hfcg->{$i}{wcc} // '-'; # Bewölkungsgrad ergänzen debugLog ($paref, 'graphic', "weather id beam number >$i< (start hour $hfcg->{$i}{time_str}): wid $hfcg->{$i}{weather} / wcc $wcc") if($ii < $maxhours); if (!$show_night && $hfcg->{$i}{weather} > 99 && !$hfcg->{$i}{beam1} && !$hfcg->{$i}{beam2}) { debugLog ($paref, 'graphic', "weather id >$i< don't show night condition ... is skipped") if($ii < $maxhours); next; }; # 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 my ($icon_name, $title) = $hfcg->{$i}{weather} > 100 ? weather_icon ($name, $lang, $hfcg->{$i}{weather}-100) : weather_icon ($name, $lang, $hfcg->{$i}{weather}); $wcc += 0 if(isNumeric ($wcc)); # Javascript Fehler vermeiden: https://forum.fhem.de/index.php/topic,117864.msg1233661.html#msg1233661 $title .= ': '.$wcc; $title .= ' '; $title .= $htitles{sunpos}{$lang}.':'; $title .= ' '; $title .= $htitles{elevatio}{$lang}.' '.$hfcg->{$i}{sunalt}; $title .= ' '; $title .= $htitles{azimuth}{$lang}.' '.$hfcg->{$i}{sunaz}; if ($icon_name eq 'unknown') { debugLog ($paref, "graphic", "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 =~ /title="$icon_name"/xs) { # passendes Icon beim User nicht vorhanden ! ( attr web iconPath falsch/prüfen/update ? ) $val = '???'; debugLog ($paref, "graphic", qq{ERROR - the icon "$weather_ids{$hfcg->{$i}{weather}}{icon}.svg" not found. Please check attribute "iconPath" of your FHEMWEB instance and/or update your FHEM software}); } $ret .= "$val"; } $ret .= ""; # freier Platz am Ende der Icon Zeile return $ret; } ################################################################ # Energieflußgrafik ################################################################ sub _flowGraphic { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $flowgsize = $paref->{flowgsize}; my $flowgani = $paref->{flowgani}; my $flowgcons = $paref->{flowgcons}; my $flowgconX = $paref->{flowgconX}; my $flowgconPower = $paref->{flowgconsPower}; my $flowgconTime = $paref->{flowgconsTime}; my $consDist = $paref->{flowgconsDist}; my $css = $paref->{css}; 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); my $cgc = ReadingsNum ($name, 'Current_GridConsumption', 0); my $cgfi = ReadingsNum ($name, 'Current_GridFeedIn', 0); my $csc = ReadingsNum ($name, 'Current_SelfConsumption', 0); my $cc = CurrentVal ($hash, 'consumption', 0); my $cc_dummy = $cc; my $batin = ReadingsNum ($name, 'Current_PowerBatIn', undef); my $batout = ReadingsNum ($name, 'Current_PowerBatOut', undef); 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)) { $hasbat = 0; $batin = 0; $batout = 0; $soc = 0; } 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 $cgfo = $cgfi - $cpv; if($cgfo > 1) { $cgc_style = 'flowg active_out'; $cgc_direction = 'M670,510 L490,305'; $cgfi -= $cgfo; $cgc = $cgfo; } } my $batout_direction = 'M902,305 L730,510'; # Batterientladung aus Netz if($batin) { my $gbi = $batin - $cpv; if($gbi > 1) { $batin -= $gbi; $batout_style = 'flowg active_in'; $batout_direction = 'M730,510 L902,305'; $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 $vbox_default = !$flowgcons ? '5 -25 800 480' : $flowgconTime ? '5 -25 800 700' : '5 -25 800 680'; my $ret = << "END0"; END0 ## get consumer list and display in Graphics ############################################## my $pos_left = 0; my $consumercount = 0; my $consumer_start = 0; my $currentPower = 0; my @consumers; if ($flowgcons) { my $type = $paref->{type}; for my $c (sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}) { # definierte Verbraucher ermitteln next if(isConsumerNoshow ($hash, $c) =~ /^[13]$/xs); # ausgeblendete Consumer nicht berücksichtigen push @consumers, $c; } $consumercount = scalar @consumers; if ($consumercount % 2) { $consumer_start = 350 - ($consDist * (($consumercount -1) / 2)); } else { $consumer_start = 350 - (($consDist / 2) * ($consumercount-1)); } $pos_left = $consumer_start + 15; for my $c (@consumers) { my $calias = ConsumerVal ($hash, $c, "alias", ""); # Name des Consumerdevices $currentPower = ReadingsNum ($name, "consumer${c}_currentPower", 0); my $cicon = __substConsumerIcon ($hash, $c, $currentPower); # Icon des Consumerdevices $cc_dummy -= $currentPower; $ret .= ''; $ret .= "$calias".FW_makeImage($cicon, ''); $ret .= ' '; $pos_left += $consDist; } } if ($hasbat) { $ret .= << "END1"; END1 $ret .= '' if ($soc > 12); $ret .= '' if ($soc > 38); $ret .= '' if ($soc > 63); $ret .= '' if ($soc > 88); $ret .= ''; } if ($flowgconX) { # Dummy Consumer my $dumcol = $cc_dummy <= 0 ? '@grey' : q{}; # Einfärbung Consumer Dummy $ret .= ''; $ret .= "consumer_X".FW_makeImage('light_light_dim_100'.$dumcol, ''); $ret .= ' '; } $ret .= << "END2"; END2 if ($hasbat) { $ret .= << "END3"; END3 } 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{}; } ## 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)); } else { $pos_left_start = 700 - ((($distance ) / 2) * ($consumercount-1)); } for my $c (@consumers) { my $power = ConsumerVal ($hash, $c, "power", 0); my $rpcurr = ConsumerVal ($hash, $c, "rpcurr", ""); # Reading für akt. Verbrauch angegeben ? $currentPower = ReadingsNum ($name, "consumer${c}_currentPower", 0); if (!$rpcurr && isConsumerPhysOn($hash, $c)) { # Workaround wenn Verbraucher ohne Leistungsmessung $currentPower = $power; } 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 ############################# $cc_dummy = sprintf("%.0f",$cc_dummy); ## Textangaben an Grafikelementen ################################### $ret .= qq{$cpv} if ($cpv); $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{$cc}; # Current_Consumption Anlage $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 $c (@consumers) { $currentPower = sprintf "%.1f", ReadingsNum($name, "consumer${c}_currentPower", 0); $currentPower =~ s/\.0$// if (int($currentPower) > 0); # .0 am Ende interessiert nicht my $consumerTime = ConsumerVal ($hash, $c, "remainTime", ""); # Restlaufzeit my $rpcurr = ConsumerVal ($hash, $c, "rpcurr", ""); # Readingname f. current Power if (!$rpcurr) { # Workaround wenn Verbraucher ohne Leistungsmessung $currentPower = isConsumerPhysOn($hash, $c) ? '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 ################################################################################## 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 ######################################################################################################### 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; } $pos_left += ($consDist * 2); } } $ret .= qq{}; return $ret; } ################################################################ # prüfe ob Verbrauchericon + Farbe angegeben ist # und setze ggf. Ersatzwerte # $c - Consumer Nummer ################################################################ sub __substConsumerIcon { my $hash = shift; my $c = shift; my $pcurr = shift; my $name = $hash->{NAME}; my $cicon = ConsumerVal ($hash, $c, "icon", ""); # Icon des Consumerdevices angegeben ? if (!$cicon) { $cicon = 'light_light_dim_100'; } my $color; ($cicon,$color) = split '@', $cicon; if (!$color) { $color = isConsumerLogOn ($hash, $c, $pcurr) ? 'darkorange' : ''; } $cicon .= '@'.$color if($color); return $cicon; } ################################################################ # Inject consumer icon ################################################################ sub consinject { my ($hash,$i,@consumers) = @_; my $name = $hash->{NAME}; my $ret = ""; my $debug = getDebug ($hash); # Debug Module for (@consumers) { if ($_) { my ($cons,$im,$start,$end) = split (':', $_); if($debug =~ /graphic/x) { Log3 ($name, 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; } ############################################################################### # Balkenbreite normieren # # Die Balkenbreite wird bestimmt durch den Wert. # 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 # komplett rechtsbündig ausgeben. # 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 # ############################################################################### sub formatVal6 { my $v = shift; my $kw = shift; my $w = shift; my $n = ' '; # positive Zahl if ($v < 0) { $n = '-'; # negatives Vorzeichen merken $v = abs($v); } if ($kw eq 'kWh') { # bei Anzeige in kWh muss weniger aufgefüllt werden $v = sprintf "%.1f",($v / 1000); $v += 0; # keine 0.0 oder 6.0 etc return ($n eq '-') ? ($v * -1) : $v if(defined $w); my $t = $v - int($v); # Nachkommstelle ? if (!$t) { # glatte Zahl ohne Nachkommastelle 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.' '; } } else { # mit Nachkommastelle -> zwei Zeichen mehr .X if ($v < 10) { return ' '.$n.$v.' '; } else { return $n.$v.' '; } } } return ($n eq '-') ? ($v * -1) : $v if(defined $w); # Werte bleiben in Watt 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.' '; } elsif ($v < 10000) { return $n.$v.' '; } else { return $n.$v; } # mehr als 10.000 W :) } ############################################################################### # Zuordungstabelle "WeatherId" angepasst auf FHEM Icons ############################################################################### sub weather_icon { my $name = shift; my $lang = shift; my $id = shift; $id = int $id; 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',''); } ################################################################ # benötigte Attribute im DWD Device checken ################################################################ 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; } ################################################################ # AI Daten für die abgeschlossene Stunde hinzufügen ################################################################ sub _addHourAiRawdata { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $aln = $paref->{aln}; # Autolearning my $h = $paref->{h}; my $rho = sprintf "%02d", $h; my $sr = ReadingsVal ($name, ".signaldone_".$rho, ""); return if($sr eq "done"); if (!$aln) { storeReading ('.signaldone_'.sprintf("%02d",$h), 'done'); debugLog ($paref, 'pvCorrectionRead', "Autolearning is switched off for hour: $h -> skip add AI raw data"); return; } debugLog ($paref, 'aiProcess', "start add AI raw data for hour: $h"); $paref->{ood} = 1; # Only One Day $paref->{rho} = $rho; aiAddRawData ($paref); # Raw Daten für AI hinzufügen und sichern delete $paref->{ood}; delete $paref->{rho}; storeReading ('.signaldone_'.sprintf("%02d",$h), 'done'); return; } ################################################################ # Qualität der Vorhersage berechnen ################################################################ sub __calcFcQuality { my $pvfc = shift; # PV Vorhersagewert my $pvrl = shift; # PV reale Erzeugung return if(!$pvfc || !$pvrl); $pvrl = sprintf "%.0f", $pvrl; $pvfc = sprintf "%.0f", $pvfc; my $diff = $pvfc - $pvrl; my $hdv = 1 - abs ($diff / $pvrl); # Abweichung der Stunde, 1 = bestmöglicher Wert $hdv = $hdv < 0 ? 0 : $hdv; $hdv = sprintf "%.2f", $hdv; return $hdv; } ############################################################### # Eintritt in den KI Train Prozess normal/Blocking ############################################################### sub manageTrain { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; if (CircularVal ($hash, 99, 'runTimeTrainAI', 0) < $aibcthhld) { BlockingKill ($hash->{HELPER}{AIBLOCKRUNNING}) if(defined $hash->{HELPER}{AIBLOCKRUNNING}); debugLog ($paref, 'aiProcess', qq{AI Training is started in main process}); aiTrain ($paref); } else { delete $hash->{HELPER}{AIBLOCKRUNNING} if(defined $hash->{HELPER}{AIBLOCKRUNNING}{pid} && $hash->{HELPER}{AIBLOCKRUNNING}{pid} =~ /DEAD/xs); if (defined $hash->{HELPER}{AIBLOCKRUNNING}{pid}) { Log3 ($name, 3, qq{$name - another AI Training with PID "$hash->{HELPER}{AIBLOCKRUNNING}{pid}" is already running ... start Training aborted}); return; } $paref->{block} = 1; $hash->{HELPER}{AIBLOCKRUNNING} = BlockingCall ( "FHEM::SolarForecast::aiTrain", $paref, "FHEM::SolarForecast::finishTrain", $aitrblto, "FHEM::SolarForecast::abortTrain", $hash ); if (defined $hash->{HELPER}{AIBLOCKRUNNING}) { $hash->{HELPER}{AIBLOCKRUNNING}{loglevel} = 3; # Forum https://forum.fhem.de/index.php/topic,77057.msg689918.html#msg689918 debugLog ($paref, 'aiProcess', qq{AI Training BlockingCall PID "$hash->{HELPER}{AIBLOCKRUNNING}{pid}" with Timeout "$aitrblto" started}); } } return; } ############################################################### # Restaufgaben nach AI Train ############################################################### sub finishTrain { my $serial = decode_base64 (shift); my $paref = eval { thaw ($serial) }; # Deserialisierung my $name = $paref->{name}; my $hash = $defs{$name}; my $type = $hash->{TYPE}; my $aicanuse = $paref->{aicanuse}; my $aitrainstate = $paref->{aitrainstate}; my $runTimeTrainAI = $paref->{runTimeTrainAI}; my $aiinitstate = $paref->{aiinitstate}; my $aitrainFinishTs = $paref->{aitrainLastFinishTs}; $data{$type}{$name}{current}{aiAddedToTrain} = 0; $data{$type}{$name}{current}{aicanuse} = $aicanuse; $data{$type}{$name}{current}{aitrainstate} = $aitrainstate; $data{$type}{$name}{current}{aiinitstate} = $aiinitstate if(defined $aiinitstate); $data{$type}{$name}{circular}{99}{runTimeTrainAI} = $runTimeTrainAI if(defined $runTimeTrainAI); # !! in Circular speichern um zu persistieren, setTimeTracking speichert zunächst in Current !! $data{$type}{$name}{circular}{99}{aitrainLastFinishTs} = $aitrainFinishTs if(defined $aitrainFinishTs); if ($aitrainstate eq 'ok') { _readCacheFile ({ hash => $hash, name => $name, type => $type, file => $aitrained.$name, cachename => 'aitrained', title => 'aiTrainedData' } ); } $paref->{debug} = getDebug ($hash); if (defined $hash->{HELPER}{AIBLOCKRUNNING}) { debugLog ($paref, 'aiProcess', qq{AI Training BlockingCall PID "$hash->{HELPER}{AIBLOCKRUNNING}{pid}" finished, state: $aitrainstate}); delete($hash->{HELPER}{AIBLOCKRUNNING}); } else { debugLog ($paref, 'aiProcess', qq{AI Training finished, state: $aitrainstate}); } return; } #################################################################################################### # Abbruchroutine BlockingCall Timeout #################################################################################################### sub abortTrain { my $hash = shift; my $cause = shift // "Timeout: process terminated"; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; Log3 ($name, 1, "$name -> BlockingCall $hash->{HELPER}{AIBLOCKRUNNING}{fn} pid:$hash->{HELPER}{AIBLOCKRUNNING}{pid} aborted: $cause"); delete($hash->{HELPER}{AIBLOCKRUNNING}); $data{$type}{$name}{current}{aitrainstate} = 'Traing (Child) process timed out'; $data{$type}{$name}{current}{aiAddedToTrain} = 0; return; } ################################################################ # KI Instanz(en) aus Raw Daten Hash # $data{$type}{$name}{aidectree}{airaw} hinzufügen ################################################################ sub aiAddInstance { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $taa = $paref->{taa}; # do train after add return if(!isPrepared4AI ($hash)); my $err = aiInit ($paref); return if($err); my $dtree = AiDetreeVal ($hash, 'object', undef); $data{$type}{$name}{current}{aiAddedToTrain} = 0; for my $idx (sort keys %{$data{$type}{$name}{aidectree}{airaw}}) { next if(!$idx); my $pvrl = AiRawdataVal ($hash, $idx, 'pvrl', undef); next if(!defined $pvrl); my $hod = AiRawdataVal ($hash, $idx, 'hod', undef); next if(!defined $hod); my $rad1h = AiRawdataVal ($hash, $idx, 'rad1h', 0); next if($rad1h <= 0); my $temp = AiRawdataVal ($hash, $idx, 'temp', 20); my $wcc = AiRawdataVal ($hash, $idx, 'wcc', 0); my $rr1c = AiRawdataVal ($hash, $idx, 'rr1c', 0); my $sunalt = AiRawdataVal ($hash, $idx, 'sunalt', 0); eval { $dtree->add_instance (attributes => { rad1h => $rad1h, temp => $temp, wcc => $wcc, rr1c => $rr1c, sunalt => $sunalt, hod => $hod }, result => $pvrl ); 1; } or do { Log3 ($name, 1, "$name - aiAddInstance ERROR: $@"); $data{$type}{$name}{current}{aiaddistate} = $@; return; }; $data{$type}{$name}{current}{aiAddedToTrain}++; debugLog ($paref, 'aiProcess', qq{AI Instance added $idx - hod: $hod, sunalt: $sunalt, rad1h: $rad1h, pvrl: $pvrl, wcc: $wcc, rr1c: $rr1c, temp: $temp}, 4); } debugLog ($paref, 'aiProcess', "AI Instance add - ".$data{$type}{$name}{current}{aiAddedToTrain}." entities added for training ".(AttrVal ($name, 'verbose', 3) != 4 ? '(set verbose 4 for output more detail)' : '')); $data{$type}{$name}{aidectree}{object} = $dtree; $data{$type}{$name}{current}{aiaddistate} = 'ok'; if ($taa) { manageTrain ($paref); } return; } ################################################################ # KI trainieren ################################################################ sub aiTrain { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $block = $paref->{block} // 0; my ($serial, $err); if (!isPrepared4AI ($hash)) { $err = CurrentVal ($hash, 'aicanuse', ''); $serial = encode_base64 (Serialize ( { name => $name, aitrainstate => "Train: not performed -> $err", aicanuse => $err } ), ""); $block ? return ($serial) : return \&finishTrain ($serial); } my $cst = [gettimeofday]; # Zyklus-Startzeit my $dtree = AiDetreeVal ($hash, 'object', undef); if (!$dtree) { $err = 'no AI::DecisionTree object present'; $serial = encode_base64 (Serialize ( {name => $name, aitrainstate => "Train: not performed -> $err", aiinitstate => "Init: $err", aicanuse => 'ok' } ), ""); $block ? return ($serial) : return \&finishTrain ($serial); } eval { $dtree->train(); 1; } or do { Log3 ($name, 1, "$name - aiTrain ERROR: $@"); $err = (split / at/, $@)[0]; $serial = encode_base64 (Serialize ( {name => $name, aitrainstate => "Train: $err", aicanuse => 'ok' } ), ""); $block ? return ($serial) : return \&finishTrain ($serial); }; $data{$type}{$name}{aidectree}{aitrained} = $dtree; $err = writeCacheToFile ($hash, 'aitrained', $aitrained.$name); if (!$err) { debugLog ($paref, 'aiProcess', qq{AI trained number of entities: }. $data{$type}{$name}{current}{aiAddedToTrain}); debugLog ($paref, 'aiProcess', qq{AI trained and saved data into file: }.$aitrained.$name); debugLog ($paref, 'aiProcess', qq{Training instances and their associated information where purged from the AI object}); } setTimeTracking ($hash, $cst, 'runTimeTrainAI'); # Zyklus-Laufzeit ermitteln $serial = encode_base64 (Serialize ( {name => $name, aitrainstate => 'ok', runTimeTrainAI => CurrentVal ($hash, 'runTimeTrainAI', ''), aitrainLastFinishTs => int time, aicanuse => 'ok' } ) , ""); delete $data{$type}{$name}{current}{runTimeTrainAI}; $block ? return ($serial) : return \&finishTrain ($serial); return; } ################################################################ # AI Ergebnis für ermitteln ################################################################ sub aiGetResult { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $hod = $paref->{hod}; my $nhtstr = $paref->{nhtstr}; return 'AI usage is not prepared' if(!isPrepared4AI ($hash, 'full')); my $dtree = AiDetreeVal ($hash, 'aitrained', undef); if (!$dtree) { return 'AI trained object is missed'; } my $rad1h = NexthoursVal ($hash, $nhtstr, "rad1h", 0); return "no rad1h for hod: $hod" if($rad1h <= 0); debugLog ($paref, 'aiData', "Start AI result check for hod: $hod"); my $wcc = NexthoursVal ($hash, $nhtstr, 'cloudcover', 0); my $rr1c = NexthoursVal ($hash, $nhtstr, 'totalrain', 0); my $temp = NexthoursVal ($hash, $nhtstr, 'temp', 20); my $sunalt = NexthoursVal ($hash, $nhtstr, 'sunalt', 0); my $sunaz = NexthoursVal ($hash, $nhtstr, 'sunaz', 0); my $tbin = temp2bin ($temp); my $cbin = cloud2bin ($wcc); my $sabin = sunalt2bin ($sunalt); my $pvaifc; eval { $pvaifc = $dtree->get_result (attributes => { rad1h => $rad1h, temp => $tbin, wcc => $cbin, rr1c => $rr1c, sunalt => $sabin, sunaz => $sunaz, hod => $hod } ); }; if ($@) { Log3 ($name, 1, "$name - aiGetResult ERROR: $@"); return $@; } if (defined $pvaifc) { debugLog ($paref, 'aiData', qq{AI accurate result found: pvaifc: $pvaifc (hod: $hod, sunaz: $sunaz, sunalt: $sabin, Rad1h: $rad1h, wcc: $wcc, rr1c: $rr1c, temp: $tbin)}); return ('accurate', $pvaifc); } (my $msg, $pvaifc) = _aiGetSpread ( { hash => $hash, name => $name, type => $type, rad1h => $rad1h, temp => $tbin, wcc => $cbin, rr1c => $rr1c, sunalt => $sabin, sunaz => $sunaz, hod => $hod, dtree => $dtree, debug => $paref->{debug} } ); if (defined $pvaifc) { return ($msg, $pvaifc); } return 'No AI decition delivered'; } ################################################################ # AI Ergebnis aus einer positiven und negativen # rad1h-Abweichung schätzen ################################################################ sub _aiGetSpread { my $paref = shift; my $rad1h = $paref->{rad1h}; my $temp = $paref->{temp}; my $wcc = $paref->{wcc}; my $rr1c = $paref->{rr1c}; my $sunalt = $paref->{sunalt}; my $sunaz = $paref->{sunaz}; my $hod = $paref->{hod}; my $dtree = $paref->{dtree}; my $dtn = 20; # positive und negative rad1h Abweichung testen mit Schrittweite "$step" my $step = 10; my ($pos, $neg, $p, $n); debugLog ($paref, 'aiData', qq{AI no accurate result found with initial value "Rad1h: $rad1h" (hod: $hod)}); debugLog ($paref, 'aiData', qq{AI test Rad1h variance "$dtn" and positive/negative spread with step size "$step"}); my $gra = { temp => $temp, wcc => $wcc, rr1c => $rr1c, sunalt => $sunalt, sunaz => $sunaz, hod => $hod }; for ($p = $rad1h + $step; $p <= $rad1h + $dtn; $p += $step) { $p = sprintf "%.2f", $p; $gra->{rad1h} = $p; debugLog ($paref, 'aiData', qq{AI positive test value "Rad1h: $p"}); eval { $pos = $dtree->get_result (attributes => $gra); }; if ($@) { return $@; } if ($pos) { debugLog ($paref, 'aiData', qq{AI positive tolerance hit: $pos Wh}); last; } } for ($n = $rad1h - $step; $n >= $rad1h - $dtn; $n -= $step) { last if($n <= 0); $n = sprintf "%.2f", $n; $gra->{rad1h} = $n; debugLog ($paref, 'aiData', qq{AI negative test value "Rad1h: $n"}); eval { $neg = $dtree->get_result (attributes => $gra); }; if ($@) { return $@; } if ($neg) { debugLog ($paref, 'aiData', qq{AI negative tolerance hit: $neg Wh}); last; } } my $pvaifc = $pos && $neg ? sprintf "%.0f", (($pos + $neg) / 2) : undef; if (defined $pvaifc) { debugLog ($paref, 'aiData', qq{AI determined average result: pvaifc: $pvaifc Wh (hod: $hod, sunaz: $sunaz, sunalt: $sunalt, wcc: $wcc, rr1c: $rr1c, temp: $temp)}); return ('spreaded', $pvaifc); } return 'No AI decition delivered'; } ################################################################ # KI initialisieren ################################################################ sub aiInit { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; if (!isPrepared4AI ($hash)) { my $err = CurrentVal ($hash, 'aicanuse', ''); debugLog ($paref, 'aiProcess', $err); $data{$type}{$name}{current}{aiinitstate} = $err; return $err; } my $dtree = new AI::DecisionTree ( verbose => 0, noise_mode => 'pick_best' ); $data{$type}{$name}{aidectree}{object} = $dtree; $data{$type}{$name}{current}{aiinitstate} = 'ok'; Log3 ($name, 3, "$name - AI::DecisionTree initialized"); return; } ################################################################ # Daten der Raw Datensammlung hinzufügen ################################################################ sub aiAddRawData { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $day = $paref->{day} // strftime "%d", localtime(time); # aktueller Tag (range 01 to 31) my $ood = $paref->{ood} // 0; # only one (current) day my $rho = $paref->{rho}; # only this hour of day delete $data{$type}{$name}{current}{aitrawstate}; my $err; my $dosave = 0; for my $pvd (sort keys %{$data{$type}{$name}{pvhist}}) { next if(!$pvd); if ($ood) { next if($pvd ne $paref->{day}); } last if(int $pvd > int $day); for my $hod (sort keys %{$data{$type}{$name}{pvhist}{$pvd}}) { next if(!$hod || $hod eq '99' || ($rho && $hod ne $rho)); my $pvrlvd = HistoryVal ($hash, $pvd, $hod, 'pvrlvd', 1); if (!$pvrlvd) { # Datensatz ignorieren wenn als invalid gekennzeichnet debugLog ($paref, 'aiProcess', qq{AI raw data is marked as invalid and is ignored - day: $pvd, hod: $hod}); next; } my $rad1h = HistoryVal ($hash, $pvd, $hod, 'rad1h', undef); next if(!$rad1h || $rad1h <= 0); ### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !! ####################################################################################################################### next if($rad1h =~ /\.[0-9]{1}$/xs); # 29.03.2024 -> einen Monat drin lassen wegen pvHistory turn if ($rad1h =~ /\.00$/xs) { # 29.03.2024 -> einen Monat drin lassen wegen pvHistory turn $rad1h = int $rad1h; } ####################################################################################################################### my $pvrl = HistoryVal ($hash, $pvd, $hod, 'pvrl', undef); next if(!$pvrl || $pvrl <= 0); my $ridx = _aiMakeIdxRaw ($pvd, $hod); my $temp = HistoryVal ($hash, $pvd, $hod, 'temp', 20); my $wcc = HistoryVal ($hash, $pvd, $hod, 'wcc', 0); my $rr1c = HistoryVal ($hash, $pvd, $hod, 'rr1c', 0); my $sunalt = HistoryVal ($hash, $pvd, $hod, 'sunalt', 0); my $sunaz = HistoryVal ($hash, $pvd, $hod, 'sunaz', 0); my $tbin = temp2bin ($temp); my $cbin = cloud2bin ($wcc); my $sabin = sunalt2bin ($sunalt); $data{$type}{$name}{aidectree}{airaw}{$ridx}{rad1h} = $rad1h; $data{$type}{$name}{aidectree}{airaw}{$ridx}{temp} = $tbin; $data{$type}{$name}{aidectree}{airaw}{$ridx}{wcc} = $cbin; $data{$type}{$name}{aidectree}{airaw}{$ridx}{rr1c} = $rr1c; $data{$type}{$name}{aidectree}{airaw}{$ridx}{hod} = $hod; $data{$type}{$name}{aidectree}{airaw}{$ridx}{pvrl} = $pvrl; $data{$type}{$name}{aidectree}{airaw}{$ridx}{sunalt} = $sabin; $data{$type}{$name}{aidectree}{airaw}{$ridx}{sunaz} = $sunaz; $dosave++; debugLog ($paref, 'aiProcess', "AI raw add - idx: $ridx, day: $pvd, hod: $hod, sunalt: $sabin, sunaz: $sunaz, rad1h: $rad1h, pvrl: $pvrl, wcc: $cbin, rr1c: $rr1c, temp: $tbin", 4); } } debugLog ($paref, 'aiProcess', "AI raw add - $dosave entities added to raw data pool ".(AttrVal ($name, 'verbose', 3) != 4 ? '(set verbose 4 for output more detail)' : '')); if ($dosave) { $err = writeCacheToFile ($hash, 'airaw', $airaw.$name); if (!$err) { $data{$type}{$name}{current}{aitrawstate} = 'ok'; debugLog ($paref, 'aiProcess', "AI raw data saved into file: ".$airaw.$name); } } return; } ################################################################ # Daten aus Raw Datensammlung löschen welche die maximale # Haltezeit (Tage) überschritten haben ################################################################ sub aiDelRawData { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; if (!keys %{$data{$type}{$name}{aidectree}{airaw}}) { return; } my $hd = AttrVal ($name, 'ctrlAIdataStorageDuration', $aistdudef); # Haltezeit KI Raw Daten (Tage) my $ht = time - ($hd * 86400); my $day = strftime "%d", localtime($ht); my $didx = _aiMakeIdxRaw ($day, '00', $ht); # Daten mit idx <= $didx löschen debugLog ($paref, 'aiProcess', qq{AI Raw delete data equal or less than index >$didx<}); delete $data{$type}{$name}{current}{aitrawstate}; my ($err, $dosave); for my $idx (sort keys %{$data{$type}{$name}{aidectree}{airaw}}) { next if(!$idx || $idx > $didx); delete $data{$type}{$name}{aidectree}{airaw}{$idx}; $dosave = 1; debugLog ($paref, 'aiProcess', qq{AI Raw data deleted - idx: $idx}); } if ($dosave) { $err = writeCacheToFile ($hash, 'airaw', $airaw.$name); if (!$err) { $data{$type}{$name}{current}{aitrawstate} = 'ok'; debugLog ($paref, 'aiProcess', qq{AI raw data saved into file: }.$airaw.$name); } } return; } ################################################################ # den Index für AI raw Daten erzeugen ################################################################ sub _aiMakeIdxRaw { my $day = shift; my $hod = shift; my $t = shift // time; my $ridx = strftime "%Y%m", localtime($t); $ridx .= $day.$hod; return $ridx; } ################################################################ # History-Hash verwalten ################################################################ sub setPVhistory { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $day = $paref->{day}; my $dayname = $paref->{dayname}; # aktueller Wochentagsname my $nhour = $paref->{nhour}; my $histname = $paref->{histname}; my $val = $paref->{val}; # Wert zur Speicherung in pvHistory (soll mal generell verwendet werden -> Change) my $pvrlvd = $paref->{pvrlvd}; # 1: Eintrag 'pvrl' wird im Lernprozess berücksichtigt my $reorg = $paref->{reorg} // 0; # Neuberechnung von Werten in Stunde "99" nach Löschen von Stunden eines Tages my $reorgday = $paref->{reorgday} // q{}; # Tag der reorganisiert werden soll $data{$type}{$name}{pvhist}{$day}{99}{dayname} = $dayname if($day); if ($hfspvh{$histname} && defined &{$hfspvh{$histname}{fn}}) { &{$hfspvh{$histname}{fn}} ($paref); return; } if ($histname =~ /csm[et][0-9]+$/xs) { # Verbrauch eines Verbrauchers $data{$type}{$name}{pvhist}{$day}{$nhour}{$histname} = $val; 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} = sprintf "%.2f", $sum; } } 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}"} = sprintf "%.2f", ($minutes / 60 ) if($cycles); } if ($histname =~ /cyclescsm[0-9]+$/xs) { # Anzahl Tageszyklen des Verbrauchers $data{$type}{$name}{pvhist}{$day}{99}{$histname} = $val; } if ($reorg) { # Reorganisation Stunde "99" if (!$reorgday) { Log3 ($name, 1, "$name - ERROR reorg pvHistory - the day of reorganization is invalid or empty: >$reorgday<"); return; } my ($r1, $r2, $r3, $r4, $r5, $r6, $r7, $r8) = (0,0,0,0,0,0,0,0); for my $k (keys %{$data{$type}{$name}{pvhist}{$reorgday}}) { next if($k eq "99"); $r1 += HistoryVal ($hash, $reorgday, $k, 'batin', 0); $r2 += HistoryVal ($hash, $reorgday, $k, 'batout', 0); $r3 += HistoryVal ($hash, $reorgday, $k, 'pvrl', 0); $r4 += HistoryVal ($hash, $reorgday, $k, 'pvfc', 0); $r5 += HistoryVal ($hash, $reorgday, $k, 'confc', 0); $r6 += HistoryVal ($hash, $reorgday, $k, 'gcons', 0); $r7 += HistoryVal ($hash, $reorgday, $k, 'gfeedin', 0); $r8 += HistoryVal ($hash, $reorgday, $k, 'con', 0); } $data{$type}{$name}{pvhist}{$reorgday}{99}{batin} = $r1; $data{$type}{$name}{pvhist}{$reorgday}{99}{batout} = $r2; $data{$type}{$name}{pvhist}{$reorgday}{99}{pvrl} = $r3; $data{$type}{$name}{pvhist}{$reorgday}{99}{pvfc} = $r4; $data{$type}{$name}{pvhist}{$reorgday}{99}{confc} = $r5; $data{$type}{$name}{pvhist}{$reorgday}{99}{gcons} = $r6; $data{$type}{$name}{pvhist}{$reorgday}{99}{gfeedin} = $r7; $data{$type}{$name}{pvhist}{$reorgday}{99}{con} = $r8; debugLog ($paref, 'saveData2Cache', "setPVhistory -> Day >$reorgday< reorganized keys: batin, batout, pvrl, pvfc, con, confc, gcons, gfeedin"); } if ($histname) { debugLog ($paref, 'saveData2Cache', "setPVhistory -> store Day: $day, Hour: $nhour, Key: $histname, Value: ".(defined $val ? $val : 'undef')); } return; } ################################################################ # Wert mit optional weiteren Berechnungen in pvHistory speichen ################################################################ sub _storeVal { ## no critic "not used" my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $paref->{type}; my $day = $paref->{day}; my $nhour = $paref->{nhour}; my $histname = $paref->{histname}; my $val = $paref->{val}; my $store = $hfspvh{$histname}{storname}; my ($validkey, $validval); $data{$type}{$name}{pvhist}{$day}{$nhour}{$store} = $val; if (defined $hfspvh{$histname}{validkey}) { $validkey = $hfspvh{$histname}{validkey}; $validval = $paref->{$validkey}; $data{$type}{$name}{pvhist}{$day}{$nhour}{$validkey} = $validval; } debugLog ($paref, 'saveData2Cache', "setPVhistory -> stored simple - Day: $day, Hour: $nhour, Key: $store, Value: ".(defined $val ? $val : 'undef'). (defined $validkey ? ", ValidKey: $validkey, ValidValue: $validval" : '') ); if (defined $hfspvh{$histname}{fpar} && $hfspvh{$histname}{fpar} eq 'comp99') { my $sum = 0; for my $k (keys %{$data{$type}{$name}{pvhist}{$day}}) { next if($k eq '99'); $sum += HistoryVal ($hash, $day, $k, $store, 0); } $data{$type}{$name}{pvhist}{$day}{99}{$store} = $sum; debugLog ($paref, 'saveData2Cache', "setPVhistory -> stored compute - Day: $day, Hour: 99, Key: $store, Value: $sum"); } return; } ################################################################ # liefert aktuelle Einträge des in $htol # angegebenen internen Hash ################################################################ sub listDataPool { my $hash = shift; my $htol = shift; my $par = shift // q{}; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my ($sq,$h); my $sub = sub { my $day = shift; my $ret; for my $key (sort {$a<=>$b} keys %{$h->{$day}}) { my $pvrl = HistoryVal ($hash, $day, $key, 'pvrl', '-'); my $pvrlvd = HistoryVal ($hash, $day, $key, 'pvrlvd', '-'); my $pvfc = HistoryVal ($hash, $day, $key, 'pvfc', '-'); my $gcon = HistoryVal ($hash, $day, $key, 'gcons', '-'); my $con = HistoryVal ($hash, $day, $key, 'con', '-'); my $confc = HistoryVal ($hash, $day, $key, 'confc', '-'); my $gfeedin = HistoryVal ($hash, $day, $key, 'gfeedin', '-'); my $wid = HistoryVal ($hash, $day, $key, 'weatherid', '-'); my $wcc = HistoryVal ($hash, $day, $key, 'wcc', '-'); my $rr1c = HistoryVal ($hash, $day, $key, 'rr1c', '-'); my $temp = HistoryVal ($hash, $day, $key, 'temp', undef); my $pvcorrf = HistoryVal ($hash, $day, $key, 'pvcorrf', '-'); my $dayname = HistoryVal ($hash, $day, $key, 'dayname', undef); my $etotal = HistoryVal ($hash, $day, $key, 'etotal', '-'); my $btotin = HistoryVal ($hash, $day, $key, 'batintotal', '-'); my $batin = HistoryVal ($hash, $day, $key, 'batin', '-'); my $btotout = HistoryVal ($hash, $day, $key, 'batouttotal', '-'); my $batout = HistoryVal ($hash, $day, $key, 'batout', '-'); my $batmsoc = HistoryVal ($hash, $day, $key, 'batmaxsoc', '-'); my $batssoc = HistoryVal ($hash, $day, $key, 'batsetsoc', '-'); my $rad1h = HistoryVal ($hash, $day, $key, 'rad1h', '-'); my $sunaz = HistoryVal ($hash, $day, $key, 'sunaz', '-'); my $sunalt = HistoryVal ($hash, $day, $key, 'sunalt', '-'); my $don = HistoryVal ($hash, $day, $key, 'DoN', '-'); $ret .= "\n " if($ret); $ret .= $key." => "; $ret .= "etotal: $etotal, pvfc: $pvfc, pvrl: $pvrl, pvrlvd: $pvrlvd, rad1h: $rad1h"; $ret .= "\n "; $ret .= "confc: $confc, con: $con, gcon: $gcon, gfeedin: $gfeedin"; $ret .= "\n "; $ret .= "DoN: $don, sunaz: $sunaz, sunalt: $sunalt"; $ret .= "\n "; $ret .= "batintotal: $btotin, batin: $batin, batouttotal: $btotout, batout: $batout"; $ret .= "\n " if($key eq '99'); $ret .= "batmaxsoc: $batmsoc, batsetsoc: $batssoc" if($key eq '99'); $ret .= "\n "; $ret .= "wid: $wid"; $ret .= ", wcc: $wcc"; $ret .= ", rr1c: $rr1c"; $ret .= ", temp: $temp" if($temp); $ret .= ", pvcorrf: $pvcorrf"; $ret .= ", dayname: $dayname" if($dayname); my $csm; for my $c (1..$maxconsumer) { $c = sprintf "%02d", $c; my $nl = 0; my $csmc = HistoryVal ($hash, $day, $key, "cyclescsm${c}", undef); my $csmt = HistoryVal ($hash, $day, $key, "csmt${c}", undef); 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; } } return $ret; }; if ($htol eq "pvhist") { $h = $data{$type}{$name}{pvhist}; if (!keys %{$h}) { return qq{PV cache is empty.}; } for my $i (keys %{$h}) { if (!isNumeric ($i)) { delete $data{$type}{$name}{pvhist}{$i}; Log3 ($name, 2, qq{$name - INFO - invalid key "$i" was deleted from pvHistory storage}); } } for my $idx (sort{$a<=>$b} keys %{$h}) { next if($par && $idx ne $par); $sq .= $idx." => ".$sub->($idx)."\n"; } } if ($htol eq "consumer") { $h = $data{$type}{$name}{consumers}; if (!keys %{$h}) { return qq{Consumer cache is empty.}; } for my $i (keys %{$h}) { 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, 2, qq{$name - INFO - invalid consumer key "$i" was deleted from consumer storage}); } } for my $idx (sort{$a<=>$b} keys %{$h}) { next if($par && $idx ne $par); my $cret; for my $ckey (sort keys %{$h->{$idx}}) { if(ref $h->{$idx}{$ckey} eq "HASH") { my $hk = qq{}; for my $f (sort {$a<=>$b} keys %{$h->{$idx}{$ckey}}) { $hk .= " " if($hk); $hk .= "$f=".$h->{$idx}{$ckey}{$f}; } $cret .= $ckey." => ".$hk."\n "; } else { $cret .= $ckey." => ".ConsumerVal ($hash, $idx, $ckey, "")."\n "; } } $sq .= $idx." => ".$cret."\n"; } } if ($htol eq "circular") { $h = $data{$type}{$name}{circular}; if (!keys %{$h}) { return qq{Circular cache is empty.}; } for my $idx (sort keys %{$h}) { my $pvrl = CircularVal ($hash, $idx, 'pvrl', '-'); my $pvfc = CircularVal ($hash, $idx, 'pvfc', '-'); my $pvrlsum = CircularVal ($hash, $idx, 'pvrlsum', '-'); my $pvfcsum = CircularVal ($hash, $idx, 'pvfcsum', '-'); my $dnumsum = CircularVal ($hash, $idx, 'dnumsum', '-'); my $pvaifc = CircularVal ($hash, $idx, 'pvaifc', '-'); my $pvapifc = CircularVal ($hash, $idx, 'pvapifc', '-'); my $aihit = CircularVal ($hash, $idx, 'aihit', '-'); my $confc = CircularVal ($hash, $idx, 'confc', '-'); my $gcons = CircularVal ($hash, $idx, 'gcons', '-'); my $gfeedin = CircularVal ($hash, $idx, 'gfeedin', '-'); my $wid = CircularVal ($hash, $idx, "weatherid", '-'); my $wtxt = CircularVal ($hash, $idx, "weathertxt", '-'); my $wccv = CircularVal ($hash, $idx, "wcc", '-'); my $rr1c = CircularVal ($hash, $idx, "rr1c", '-'); my $temp = CircularVal ($hash, $idx, "temp", '-'); my $pvcorrf = CircularVal ($hash, $idx, "pvcorrf", '-'); my $quality = CircularVal ($hash, $idx, "quality", '-'); my $batin = CircularVal ($hash, $idx, "batin", '-'); my $batout = CircularVal ($hash, $idx, "batout", '-'); my $tdayDvtn = CircularVal ($hash, $idx, "tdayDvtn", '-'); my $ydayDvtn = CircularVal ($hash, $idx, "ydayDvtn", '-'); my $ltsmsr = CircularVal ($hash, $idx, 'lastTsMaxSocRchd', '-'); my $ntsmsc = CircularVal ($hash, $idx, 'nextTsMaxSocChge', '-'); my $dtocare = CircularVal ($hash, $idx, 'days2care', '-'); my $fitot = CircularVal ($hash, $idx, "feedintotal", '-'); my $idfi = CircularVal ($hash, $idx, "initdayfeedin", '-'); my $gcontot = CircularVal ($hash, $idx, "gridcontotal", '-'); my $idgcon = CircularVal ($hash, $idx, "initdaygcon", '-'); my $idbitot = CircularVal ($hash, $idx, "initdaybatintot", '-'); my $bitot = CircularVal ($hash, $idx, "batintot", '-'); my $idbotot = CircularVal ($hash, $idx, "initdaybatouttot", '-'); my $botot = CircularVal ($hash, $idx, "batouttot", '-'); my $rtaitr = CircularVal ($hash, $idx, "runTimeTrainAI", '-'); my $fsaitr = CircularVal ($hash, $idx, "aitrainLastFinishTs", '-'); my $pvcf = _ldchash2val ( {pool => $h, idx => $idx, key => 'pvcorrf', cval => $pvcorrf} ); my $cfq = _ldchash2val ( {pool => $h, idx => $idx, key => 'quality', cval => $quality} ); my $pvrs = _ldchash2val ( {pool => $h, idx => $idx, key => 'pvrlsum', cval => $pvrlsum} ); my $pvfs = _ldchash2val ( {pool => $h, idx => $idx, key => 'pvfcsum', cval => $pvfcsum} ); my $dnus = _ldchash2val ( {pool => $h, idx => $idx, key => 'dnumsum', cval => $dnumsum} ); $sq .= "\n" if($sq); if ($idx != 99) { $sq .= $idx." => pvapifc: $pvapifc, pvaifc: $pvaifc, pvfc: $pvfc, aihit: $aihit, pvrl: $pvrl\n"; $sq .= " batin: $batin, batout: $batout, confc: $confc, gcon: $gcons, gfeedin: $gfeedin, wcc: $wccv, rr1c: $rr1c\n"; $sq .= " temp: $temp, wid: $wid, wtxt: $wtxt\n"; $sq .= " pvcorrf: $pvcf\n"; $sq .= " quality: $cfq\n"; $sq .= " pvrlsum: $pvrs\n"; $sq .= " pvfcsum: $pvfs\n"; $sq .= " dnumsum: $dnus"; } else { $sq .= $idx." => tdayDvtn: $tdayDvtn, ydayDvtn: $ydayDvtn \n"; $sq .= " feedintotal: $fitot, initdayfeedin: $idfi \n"; $sq .= " gridcontotal: $gcontot, initdaygcon: $idgcon \n"; $sq .= " batintot: $bitot, initdaybatintot: $idbitot \n"; $sq .= " batouttot: $botot, initdaybatouttot: $idbotot \n"; $sq .= " lastTsMaxSocRchd: $ltsmsr, nextTsMaxSocChge: $ntsmsc, days2care:$dtocare \n"; $sq .= " runTimeTrainAI: $rtaitr, aitrainLastFinishTs: $fsaitr \n"; } } } if ($htol eq "nexthours") { $h = $data{$type}{$name}{nexthours}; if (!keys %{$h}) { return qq{NextHours cache is empty.}; } for my $idx (sort keys %{$h}) { my $nhts = NexthoursVal ($hash, $idx, 'starttime', '-'); my $hod = NexthoursVal ($hash, $idx, 'hourofday', '-'); my $today = NexthoursVal ($hash, $idx, 'today', '-'); my $pvfc = NexthoursVal ($hash, $idx, 'pvfc', '-'); my $pvapifc = NexthoursVal ($hash, $idx, 'pvapifc', '-'); # PV Forecast der API my $pvaifc = NexthoursVal ($hash, $idx, 'pvaifc', '-'); # PV Forecast der KI my $aihit = NexthoursVal ($hash, $idx, 'aihit', '-'); # KI ForeCast Treffer Status my $wid = NexthoursVal ($hash, $idx, 'weatherid', '-'); my $neff = NexthoursVal ($hash, $idx, 'cloudcover', '-'); my $crang = NexthoursVal ($hash, $idx, 'cloudrange', '-'); my $rr1c = NexthoursVal ($hash, $idx, 'totalrain', '-'); my $rrange = NexthoursVal ($hash, $idx, 'rainrange', '-'); 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 $don = NexthoursVal ($hash, $idx, 'DoN', '-'); my $sunaz = NexthoursVal ($hash, $idx, 'sunaz', '-'); my $sunalt = NexthoursVal ($hash, $idx, 'sunalt', '-'); $sq .= "\n" if($sq); $sq .= $idx." => "; $sq .= "starttime: $nhts, hourofday: $hod, today: $today"; $sq .= "\n "; $sq .= "pvapifc: $pvapifc, pvaifc: $pvaifc, pvfc: $pvfc, aihit: $aihit, confc: $confc"; $sq .= "\n "; $sq .= "confcEx: $confcex, DoN: $don, wid: $wid, wcc: $neff, rr1c: $rr1c, temp=$temp"; $sq .= "\n "; $sq .= "rad1h: $rad1h, sunaz: $sunaz, sunalt: $sunalt"; $sq .= "\n "; $sq .= "rrange: $rrange, crange: $crang, correff: $pvcorrf"; } } if ($htol eq "qualities") { $h = $data{$type}{$name}{nexthours}; if (!keys %{$h}) { return qq{NextHours cache is empty.}; } for my $idx (sort keys %{$h}) { my $nhfc = NexthoursVal ($hash, $idx, 'pvfc', undef); next if(!$nhfc); my $nhts = NexthoursVal ($hash, $idx, 'starttime', '-'); my $pvcorrf = NexthoursVal ($hash, $idx, 'pvcorrf', '-/-'); my $aihit = NexthoursVal ($hash, $idx, 'aihit', '-'); my $pvfc = NexthoursVal ($hash, $idx, 'pvfc', '-'); my $neff = NexthoursVal ($hash, $idx, 'cloudcover', '-'); my $sunalt = NexthoursVal ($hash, $idx, 'sunalt', '-'); my ($f,$q) = split "/", $pvcorrf; $sq .= "\n" if($sq); $sq .= "Start: $nhts, Quality: $q, Factor: $f, AI usage: $aihit, PV expect: $pvfc Wh, Sun Alt: $sunalt, Cloud: $neff"; } } if ($htol eq "current") { $h = $data{$type}{$name}{current}; if (!keys %{$h}) { return qq{Current values cache is empty.}; } for my $idx (sort keys %{$h}) { if (ref $h->{$idx} ne "ARRAY") { $sq .= $idx." => ".(defined $h->{$idx} ? $h->{$idx} : '')."\n"; } else { my $aser = join " ",@{$h->{$idx}}; $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 "solApiData") { $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 $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 $sp2 = _ldpspaces ($tag, $sp1); while (my ($tag1, $item1) = each %{$git->($itref->{$idx}{$tag})}) { $sq .= ($s2 ? $sp2 : "")."$tag1: ".$item1."\n"; $s2 = 1; delete $itref->{$idx}{$tag}{$tag1}; } } $s1 = 1; $sq .= "\n" if($sq !~ /\n$/xs); delete $itref->{$idx}{$tag}; } } } if ($htol eq "aiRawData") { $h = $data{$type}{$name}{aidectree}{airaw}; my $maxcnt = keys %{$h}; if (!$maxcnt) { return qq{aiRawData values cache is empty.}; } $sq = "Number of datasets: ".$maxcnt."\n"; for my $idx (sort keys %{$h}) { my $hod = AiRawdataVal ($hash, $idx, 'hod', '-'); my $sunalt = AiRawdataVal ($hash, $idx, 'sunalt', '-'); my $sunaz = AiRawdataVal ($hash, $idx, 'sunaz', '-'); my $rad1h = AiRawdataVal ($hash, $idx, 'rad1h', '-'); my $wcc = AiRawdataVal ($hash, $idx, 'wcc', '-'); my $rr1c = AiRawdataVal ($hash, $idx, 'rr1c', '-'); my $pvrl = AiRawdataVal ($hash, $idx, 'pvrl', '-'); my $temp = AiRawdataVal ($hash, $idx, 'temp', '-'); $sq .= "\n"; $sq .= "$idx => hod: $hod, sunaz: $sunaz, sunalt: $sunalt, rad1h: $rad1h, wcc: $wcc, rr1c: $rr1c, pvrl: $pvrl, temp: $temp"; } } return $sq; } ################################################################ # Hashwert aus CircularVal in formatierten String umwandeln ################################################################ sub _ldchash2val { my $paref = shift; my $pool = $paref->{pool}; my $idx = $paref->{idx}; my $key = $paref->{key}; my $cval = $paref->{cval}; my $ret = qq{}; my $ret2 = qq{}; if (ref $cval eq 'HASH') { no warnings 'numeric'; for my $f (sort {$a<=>$b} keys %{$pool->{$idx}{$key}}) { next if($f eq 'simple'); if ($f !~ /\./xs) { $ret .= " " if($ret); $ret .= "$f=".$pool->{$idx}{$key}{$f}; my $ct = ($ret =~ tr/=// // 0) / 10; $ret .= "\n " if($ct =~ /^[1-9](.{1})?$/); } elsif ($f =~ /\./xs) { $ret2 .= " " if($ret2); $ret2 .= "$f=".$pool->{$idx}{$key}{$f}; my $ct2 = ($ret2 =~ tr/=// // 0) / 10; $ret2 .= "\n " if($ct2 =~ /^[1-9](.{1})?$/); } } if ($ret2) { $ret .= "\n " if($ret && $ret !~ /\n\s+$/xs); $ret .= $ret2; } use warnings; if (defined $pool->{$idx}{$key}{simple}) { $ret .= "\n " if($ret && $ret !~ /\n\s+$/xs); $ret .= " " if($ret); $ret .= "simple=".$pool->{$idx}{$key}{simple}; } } else { $ret = $cval; } return $ret; } ################################################################ # Berechnung führende Spaces für Hashanzeige # $str - String dessen Länge für die Anzahl Spaces # herangezogen wird # $sp - vorhandener Space-String der erweitert wird ################################################################ sub _ldpspaces { my $str = shift; my $sp = shift // q{}; my $const = shift // 4; my $le = $const + length Encode::decode('UTF-8', $str); my $spn = $sp; for (my $i = 0; $i < $le; $i++) { $spn .= " "; } return $spn; } ################################################################ # validiert die aktuelle Anlagenkonfiguration ################################################################ sub checkPlantConfig { my $hash = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; setModel ($hash); # Model setzen my $lang = AttrVal ($name, 'ctrlLanguage', AttrVal ('global', 'language', $deflang)); my $pcf = ReadingsVal ($name, 'pvCorrectionFactor_Auto', 'off'); my $raname = ReadingsVal ($name, 'currentRadiationAPI', ''); my ($acu, $aln) = isAutoCorrUsed ($name); 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 Properties' => { 'state' => $ok, 'result' => '', 'note' => '', 'info' => 0, 'warn' => 0, 'fault' => 0 }, 'Common Settings' => { 'state' => $ok, 'result' => '', 'note' => '', 'info' => 0, 'warn' => 0, 'fault' => 0 }, 'FTUI Widget Files' => { 'state' => $ok, 'result' => '', 'note' => '', 'info' => 0, 'warn' => 0, 'fault' => 0 }, }; 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) && !isVictronKiUsed ($hash)) { if ($sp !~ /dir.*?peak.*?tilt/x) { $result->{'String Configuration'}{state} = $nok; $result->{'String Configuration'}{fault} = 1; # Test Vollständigkeit: z.B. Süddach => dir: S, peak: 5.13, tilt: 45 } } elsif (isVictronKiUsed ($hash)) { if($sp !~ /KI-based\s=>\speak/xs) { $result->{'String Configuration'}{state} = $nok; $result->{'String Configuration'}{fault} = 1; } } else { # Strahlungsdevice SolCast-API if($sp !~ /peak.*?pk/x) { $result->{'String Configuration'}{state} = $nok; $result->{'String Configuration'}{fault} = 1; # Test Vollständigkeit } } } $result->{'String Configuration'}{result} = $hqtxt{fulfd}{$lang} if(!$result->{'String Configuration'}{fault} && !$result->{'String Configuration'}{warn}); ## Check Attribute DWD Wetterdevice ##################################### my $mosm = ''; my $resh; for my $step (1..$weatherDevMax) { #my $fcname = AttrVal ($name, 'ctrlWeatherDev'.$step, ''); my ($valid, $fcname, $apiu) = isWeatherDevValid ($hash, 'ctrlWeatherDev'.$step); next if(!$fcname && $step ne 1); if (!$valid) { $result->{'DWD Weather Properties'}{state} = $nok; if (!$fcname) { $result->{'DWD Weather Properties'}{result} .= qq{No DWD device is defined in attribute "ctrlWeatherDev$step".
}; } else { $result->{'DWD Weather Properties'}{result} .= qq{The DWD device "$fcname" doesn't exist.
}; } $result->{'DWD Weather Properties'}{fault} = 1; } else { if (!$apiu) { $err = checkdwdattr ($name, $fcname, \@dweattrmust); if ($err) { $result->{'DWD Weather Properties'}{state} = $nok; $result->{'DWD Weather Properties'}{result} .= $err.'
'; $result->{'DWD Weather Properties'}{fault} = 1; } else { $mosm = AttrVal ($fcname, 'forecastRefresh', 6) == 6 ? 'MOSMIX_L' : 'MOSMIX_S'; if ($mosm eq 'MOSMIX_L') { $result->{'DWD Weather Properties'}{state} = $info; $result->{'DWD Weather Properties'}{result} .= qq(The device "$fcname" uses "$mosm" which is only updated by DWD every 6 hours.
); $result->{'DWD Weather Properties'}{info} = 1; } $result->{'DWD Weather Properties'}{result} .= $hqtxt{fulfd}{$lang}." ($hqtxt{attrib}{$lang}: ctrlWeatherDev$step)
"; } $result->{'DWD Weather Properties'}{note} .= qq{checked parameters and attributes of device "$fcname":
}; $result->{'DWD Weather Properties'}{note} .= 'forecastProperties -> '.join (',', @dweattrmust).'
'; $result->{'DWD Weather Properties'}{note} .= 'forecastRefresh '.($mosm eq 'MOSMIX_L' ? '-> set attribute to below "6" if possible' : '').'
'; } else { $result->{'DWD Weather Properties'}{result} .= $hqtxt{fulfd}{$lang}." ($hqtxt{attrib}{$lang}: ctrlWeatherDev$step)
"; } } } ## Alter DWD Wetterdaten ########################## ($err, $resh) = isWeatherAgeExceeded ( {hash => $hash, name => $name, lang => $lang} ); if (!$err && $resh->{exceed}) { $result->{'DWD Weather Properties'}{state} = $warn; $result->{'DWD Weather Properties'}{note} .= qq{The Prediction time of Weather data is older than expected when using $resh->{mosmix}.
}; $result->{'DWD Weather Properties'}{note} .= qq{Data time forecast: $resh->{fctime}
}; $result->{'DWD Weather Properties'}{note} .= qq{Check the DWD device(s) for proper functioning of the data retrieval.
}; $result->{'DWD Weather Properties'}{warn} = 1; } $result->{'DWD Weather Properties'}{note} .= '
'; $result->{'DWD Weather Properties'}{note} .= qq{checked global Weather parameters:
}; $result->{'DWD Weather Properties'}{note} .= 'MOSMIX variant or ICON Forecast Model, Age of Weather data.
'; ## Check DWD Radiation Device ############################### if (isDWDUsed ($hash)) { $result->{'DWD Radiation Properties'}{state} = $ok; $result->{'DWD Radiation Properties'}{result} = ''; $result->{'DWD Radiation Properties'}{note} = ''; $result->{'DWD Radiation Properties'}{fault} = 0; if (!$raname || !$defs{$raname}) { $result->{'DWD Radiation Properties'}{state} = $nok; $result->{'DWD Radiation Properties'}{result} .= qq{The DWD device "$raname" doesn't exist
}; $result->{'DWD Radiation Properties'}{fault} = 1; } else { $err = checkdwdattr ($name, $raname, \@draattrmust); if ($err) { $result->{'DWD Radiation Properties'}{state} = $nok; $result->{'DWD Radiation Properties'}{result} .= $err.'
'; $result->{'DWD Radiation Properties'}{note} .= qq{
Check the parameters set in device '$raname': attribute 'forecastProperties'
}; $result->{'DWD Radiation Properties'}{fault} = 1; } else { $mosm = AttrVal ($raname, 'forecastRefresh', 6) == 6 ? 'MOSMIX_L' : 'MOSMIX_S'; if ($mosm eq 'MOSMIX_L') { $result->{'DWD Radiation Properties'}{state} = $info; $result->{'DWD Radiation Properties'}{result} .= qq(The device "$raname" uses "$mosm" which is only updated by DWD every 6 hours.
); $result->{'DWD Radiation Properties'}{info} = 1; } } } ## Alter DWD Radiation ####################### ($err, $resh) = isRad1hAgeExceeded ( {hash => $hash, name => $name, lang => $lang} ); if (!$err && $resh->{exceed}) { $result->{'DWD Radiation Properties'}{state} = $warn; $result->{'DWD Radiation Properties'}{note} .= qq{The Prediction time of radiation data (Rad1h) is older than expected when using $resh->{mosmix}.
}; $result->{'DWD Radiation Properties'}{note} .= qq{Data time forecast: $resh->{fctime}
}; $result->{'DWD Radiation Properties'}{note} .= qq{Check the DWD device '$raname' for proper functioning of the data retrieval.
}; $result->{'DWD Radiation Properties'}{warn} = 1; } if (!$result->{'DWD Radiation Properties'}{fault}) { $result->{'DWD Radiation Properties'}{result} .= $hqtxt{fulfd}{$lang}.'
'; } $result->{'DWD Radiation Properties'}{note} .= '
'; $result->{'DWD Radiation Properties'}{note} .= qq{checked global Radiation parameters:
}; $result->{'DWD Radiation Properties'}{note} .= 'MOSMIX variant, Age of Radiation data.
'; $result->{'DWD Radiation Properties'}{note} .= qq{
checked parameters and attributes device "$raname":
}; $result->{'DWD Radiation Properties'}{note} .= 'forecastProperties -> '.join (',', @draattrmust).'
'; $result->{'DWD Radiation Properties'}{note} .= 'forecastRefresh '.($mosm eq 'MOSMIX_L' ? '-> set attribute to below "6" if possible' : '').'
'; } ## Check Rooftop und Roof Ident Pair Settings (SolCast) ######################################################### if (isSolCastUsed ($hash)) { $result->{'Roof Ident Pair Settings'}{state} = $ok; $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{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{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 (auch API spezifisch) ############################################## my $eocr = AttrVal ($name, 'event-on-change-reading', ''); my $aiprep = isPrepared4AI ($hash, 'full'); my $aiusemsg = CurrentVal ($hash, 'aicanuse', ''); my ($cset, $lat, $lon, $alt) = locCoordinates(); 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 $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 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; } if (!$lat) { $result->{'Common Settings'}{state} = $warn; $result->{'Common Settings'}{result} .= qq{Attribute latitude in global device is not set.
}; $result->{'Common Settings'}{note} .= qq{Set the coordinates of your installation in the latitude attribute of the global device.
}; $result->{'Common Settings'}{warn} = 1; } if (!$lon) { $result->{'Common Settings'}{state} = $warn; $result->{'Common Settings'}{result} .= qq{Attribute longitude in global device is not set.
}; $result->{'Common Settings'}{note} .= qq{Set the coordinates of your installation in the longitude attribute of the global device.
}; $result->{'Common Settings'}{warn} = 1; } if (!$alt) { $result->{'Common Settings'}{state} = $nok; $result->{'Common Settings'}{result} .= qq{Attribute altitude in global device is not set.
}; $result->{'Common Settings'}{note} .= qq{Set the altitude in meters above sea level in the altitude attribute of the global device.
}; $result->{'Common Settings'}{fault} = 1; } if (!$aiprep) { $result->{'Common Settings'}{state} = $info; $result->{'Common Settings'}{result} .= qq{The AI support is not used.
}; $result->{'Common Settings'}{note} .= qq{$aiusemsg.
}; $result->{'Common Settings'}{info} = 1; } my ($cmerr, $cmupd, $cmmsg, $cmrec) = checkModVer ($name, '76_SolarForecast', 'https://fhem.de/fhemupdate/controls_fhem.txt'); if (!$cmerr && !$cmupd) { $result->{'Common Settings'}{note} .= qq{$cmmsg
}; $result->{'Common Settings'}{note} .= qq{checked module:
}; $result->{'Common Settings'}{note} .= qq{76_SolarForecast
}; } if ($cmerr) { $result->{'Common Settings'}{state} = $warn; $result->{'Common Settings'}{result} .= qq{$cmmsg
}; $result->{'Common Settings'}{note} .= qq{$cmrec
}; $result->{'Common Settings'}{warn} = 1; } if ($cmupd) { $result->{'Common Settings'}{state} = $warn; $result->{'Common Settings'}{result} .= qq{$cmmsg
}; $result->{'Common Settings'}{note} .= qq{$cmrec
}; $result->{'Common Settings'}{warn} = 1; } if ($result->{'Common Settings'}{result}) { $result->{'Common Settings'}{result} .= '
'; } if (isForecastSolarUsed ($hash)) { # allg. Settings bei Nutzung Forecast.Solar API if ($pcf !~ /on/xs) { $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_complex" is recommended.
}; } if (!$result->{'Common Settings'}{fault}) { $result->{'Common Settings'}{result} .= $hqtxt{fulfd}{$lang}.'
'; $result->{'Common Settings'}{note} .= qq{
checked parameters and attributes:
}; $result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto
}; } } if (isOpenMeteoUsed ($hash)) { # allg. Settings bei Nutzung Open-Meteo API if ($pcf !~ /on/xs) { $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_complex" is recommended.
}; } if (!$result->{'Common Settings'}{fault}) { $result->{'Common Settings'}{result} .= $hqtxt{fulfd}{$lang}.'
'; $result->{'Common Settings'}{note} .= qq{
checked parameters and attributes:
}; $result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto
}; } } if (isSolCastUsed ($hash)) { # allg. Settings bei Nutzung SolCast API my $gdn = AttrVal ('global', 'dnsServer', ''); my $osi = AttrVal ($name, 'ctrlSolCastAPIoptimizeReq', 0); my $lam = SolCastAPIVal ($hash, '?All', '?All', 'response_message', 'success'); if ($pcf !~ /on/xs) { $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_complex" is recommended if the SolCast efficiency factor is already adjusted.
}; } if (!$osi) { $result->{'Common Settings'}{state} = $warn; $result->{'Common Settings'}{result} .= qq{Attribute ctrlSolCastAPIoptimizeReq is set to "$osi"
}; $result->{'Common Settings'}{note} .= qq{set ctrlSolCastAPIoptimizeReq to "1" is recommended.
}; $result->{'Common Settings'}{warn} = 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->{'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'}{result} .= $hqtxt{fulfd}{$lang}.'
'; $result->{'Common Settings'}{note} .= qq{
checked parameters and attributes:
}; $result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto, ctrlSolCastAPIoptimizeReq, global dnsServer
}; } } if (isDWDUsed ($hash)) { # allg. Settings bei Nutzung DWD API my $lam = SolCastAPIVal ($hash, '?All', '?All', 'response_message', 'success'); if ($aidtabs) { $result->{'Common Settings'}{state} = $info; $result->{'Common Settings'}{result} .= qq{The Perl module AI::DecisionTree is missing.
}; $result->{'Common Settings'}{note} .= qq{If you want use AI support, please install it with e.g. "sudo apt-get install libai-decisiontree-perl".
}; $result->{'Common Settings'}{info} = 1; } if ($pcf !~ /on/xs) { $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_complex" or "on_complex_ai" is recommended.
}; } if ($lam ne 'success') { $result->{'API Access'}{state} = $nok; $result->{'API Access'}{result} .= qq{DWD last message:
"$lam"
}; $result->{'API Access'}{note} .= qq{Check the setup of the device "$raname".
}; $result->{'API Access'}{note} .= qq{It is possible that not all readings are transmitted when "$raname" is newly set up or was changed.
}; $result->{'API Access'}{note} .= qq{In this case, wait until tomorrow and check again.
}; $result->{'API Access'}{fault} = 1; } if (!$result->{'Common Settings'}{fault}) { $result->{'Common Settings'}{result} .= $hqtxt{fulfd}{$lang}.'
'; $result->{'Common Settings'}{note} .= qq{
checked Perl modules:
}; $result->{'Common Settings'}{note} .= qq{AI::DecisionTree
}; $result->{'Common Settings'}{note} .= qq{
checked parameters and attributes:
}; $result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto
}; } } if (isVictronKiUsed ($hash)) { # allg. Settings bei Nutzung VictronKI-API my $gdn = AttrVal ('global', 'dnsServer', ''); my $vrmcr = SolCastAPIVal ($hash, '?VRM', '?API', 'credentials', ''); if ($pcf !~ /on/xs) { $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_complex" is recommended.
}; $result->{'Common Settings'}{warn} = 1; } if (!$vrmcr) { $result->{'API Access'}{state} = $nok; $result->{'API Access'}{result} .= qq{The Victron VRM Portal credentials are not set.
}; $result->{'API Access'}{note} .= qq{set the credentials with command "set $name vrmCredentials".
}; $result->{'API Access'}{fault} = 1; } if (!$gdn) { $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'}{result} .= $hqtxt{fulfd}{$lang}.'
'; $result->{'Common Settings'}{note} .= qq{
checked parameters and attributes:
}; $result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto, global dnsServer, vrmCredentials
}; } } if (!$result->{'Common Settings'}{fault}) { $result->{'Common Settings'}{note} .= qq{global latitude, global longitude, global altitude, global language
}; $result->{'Common Settings'}{note} .= qq{event-on-change-reading, ctrlLanguage
}; } ## FTUI Widget Support ######################## my $tpath = "$root/www/tablet/css"; my $upd = 0; $err = 0; if (!-d $tpath) { $result->{'FTUI Widget Files'}{result} .= $hqtxt{widnin}{$lang}; $result->{'FTUI Widget Files'}{note} .= qq{There is no need to install SolarForecast FTUI widgets.
}; } else { my $cfurl = $bPath.$cfile.$pPath; for my $file (@fs) { ($cmerr, $cmupd, $cmmsg, $cmrec) = checkModVer ($name, $file, $cfurl); $err = 1 if($cmerr); $upd = 1 if($cmupd); } if ($err) { $result->{'FTUI Widget Files'}{state} = $warn; $result->{'FTUI Widget Files'}{result} .= $hqtxt{widerr}{$lang}.'
'; $result->{'FTUI Widget Files'}{result} .= $cmmsg.'
'; $result->{'FTUI Widget Files'}{note} .= qq{Update the FHEM Tablet UI Widget Files with the command:
}; $result->{'FTUI Widget Files'}{note} .= qq{"get $name ftuiFramefiles".
}; $result->{'FTUI Widget Files'}{note} .= qq{After that do the test again. If the error is permanent, please inform the maintainer.
}; $result->{'FTUI Widget Files'}{warn} = 1; $upd = 0; } if ($upd) { $result->{'FTUI Widget Files'}{state} = $warn; $result->{'FTUI Widget Files'}{result} .= $hqtxt{widnup}{$lang}; $result->{'FTUI Widget Files'}{note} .= qq{Update the FHEM Tablet UI Widget Files with the command:
}; $result->{'FTUI Widget Files'}{note} .= qq{"get $name ftuiFramefiles".
}; $result->{'FTUI Widget Files'}{warn} = 1; } if (!$result->{'FTUI Widget Files'}{fault} && !$result->{'FTUI Widget Files'}{warn} && !$result->{'FTUI Widget Files'}{info}) { $result->{'FTUI Widget Files'}{result} .= $hqtxt{widok}{$lang}; $result->{'FTUI Widget Files'}{note} .= qq{checked Files:
}; $result->{'FTUI Widget Files'}{note} .= (join ', ', @fs).qq{
}; } } ## Ausgabe ############ my $out = qq{}; $out .= qq{}.$hqtxt{plntck}{$lang}.qq{ - Model: $hash->{MODEL}

}; $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; my $cf = 0; # config fault: 1 -> Konfig fehlerhaft, 0 -> Konfig ok my $wn = 0; # Warnung wenn 1 my $io = 0; # Info wenn 1 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}); $result->{$key}{state} = $warn if($result->{$key}{warn}); $result->{$key}{state} = $nok if($result->{$key}{fault}); $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; $out .= qq{}; if ($hc < $hz) { # Zwischenzeile $out .= qq{}; $out .= qq{}; $out .= qq{}; } } $out .= qq{
$hqtxt{object}{$lang} $hqtxt{state}{$lang}       $hqtxt{result}{$lang}       $hqtxt{note}{$lang}
$key $result->{$key}{state} $result->{$key}{result} $result->{$key}{note}
 
}; $out .= qq{}; $out .= "
"; if($cf) { $out .= encode ("utf8", $hqtxt{strnok}{$lang}); } elsif ($wn) { $out .= encode ("utf8", $hqtxt{strwn}{$lang}); } 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; } ################################################################ # Array auf eine festgelegte Anzahl Elemente beschränken, # Das älteste Element wird entfernt # # $href = Referenz zum Array # $limit = die Anzahl Elemente auf die gekürzt werden soll # (default 3) # ################################################################ sub limitArray { my $href = shift; my $limit = shift // 3; return if(ref $href ne "ARRAY"); while (scalar @{$href} > $limit) { shift @{$href}; } return; } ################################################################ # Timestrings berechnen # gibt Zeitstring in lokaler Zeit zurück ################################################################ sub timestampToTimestring { my $epoch = shift; my $lang = shift // ''; return if($epoch !~ /[0-9]/xs); if (length ($epoch) == 13) { # Millisekunden $epoch = $epoch / 1000; } my ($lyear,$lmonth,$lday,$lhour,$lmin,$lsec) = (localtime($epoch))[5,4,3,2,1,0]; my $tm; $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++; my $realtm = sprintf ("%04d-%02d-%02d %02d:%02d:%02d", $year,$mon,$day,$hour,$min,$sec); # engl. Variante von aktuellen timestamp my $tmdef = sprintf ("%04d-%02d-%02d %02d:%s", $lyear,$lmonth,$lday,$lhour,"00:00"); # engl. Variante von $epoch für Logging-Timestamps etc. (Minute/Sekunde == 00) my $tmfull = sprintf ("%04d-%02d-%02d %02d:%02d:%02d", $lyear,$lmonth,$lday,$lhour,$lmin,$lsec); # engl. Variante Vollzeit von $epoch if ($lang eq "DE") { $tm = sprintf ("%02d.%02d.%04d %02d:%02d:%02d", $lday,$lmonth,$lyear,$lhour,$lmin,$lsec); # deutsche Variante Vollzeit von $epoch } else { $tm = $tmfull; } return ($tm, $tmdef, $realtm, $tmfull); } ################################################################ # einen Zeitstring YYYY-MM-TT hh:mm:ss in einen Unix # Timestamp umwandeln ################################################################ 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; } ################################################################ # einen Zeitstring YYYY-MM-TT hh:mm:ss in einen Unix # Timestamp GMT umwandeln ################################################################ sub timestringToTimestampGMT { 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 $tsgm = fhemTimeGm ($s, $m, $h, $d, $mo-1, $y-1900); return $tsgm; } ############################################################### # Konvertiere UTC zu lokaler Zeit ############################################################### sub timestringUTCtoLocal { my $name = shift; my $timstr = shift; my $pattern = shift // '%Y-%m-%dT%H:%M:%S'; my ($err, $ctime) = convertTimeZone ( { name => $name, pattern => $pattern, dtstring => $timstr, tzcurrent => 'UTC', tzconv => 'local', writelog => 0 } ); if ($err) { $err = 'ERROR while converting time zone: '.$err; } return ($err, $ctime); } ################################################################ # Zeitstring der Form 2023-05-27T14:24:30+02:00 formatieren # in YYYY-MM-TT hh:mm:ss ################################################################ sub timestringFormat { my $tstring = shift; return if(!$tstring); $tstring = (split '\+', $tstring)[0]; $tstring =~ s/T/ /g; return $tstring; } ################################################################ # Speichern Readings, Wert, Zeit in zentralen Readings Store ################################################################ sub storeReading { my $rdg = shift; my $val = shift; my $ts1 = shift; my $cmps = $rdg.'<>'.$val; $cmps .= '<>'.$ts1 if(defined $ts1); push @da, $cmps; return; } ################################################################ # Readings aus Array erstellen # $doevt: 1-Events erstellen, 0-keine Events erstellen # # readingsBulkUpdate($hash,$reading,$value,$changed,$timestamp) # ################################################################ sub createReadingsFromArray { my $hash = shift; my $doevt = shift // 0; return if(!scalar @da); readingsBeginUpdate ($hash); for my $elem (@da) { my ($rn,$rval,$ts) = split "<>", $elem, 3; readingsBulkUpdate ($hash, $rn, $rval, undef, $ts); } readingsEndUpdate ($hash, $doevt); undef @da; return; } ################################################################ # "state" updaten ################################################################ 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; } ################################################################ # erstellt einen Debug-Eintrag im Log ################################################################ sub debugLog { my $paref = shift; my $dreg = shift; # Regex zum Vergleich my $dmsg = shift; # auszugebender Meldungstext my $verbose = shift // 1; my $name = $paref->{name}; my $debug = $paref->{debug}; if ($debug =~ /$dreg/x) { Log3 ($name, $verbose, "$name DEBUG> $dmsg"); } return; } ################################################################ # 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; } ###################################################################################### # NOTIFYDEV und "Probably associated with" erstellen ###################################################################################### sub createAssociatedWith { my $hash = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; RemoveInternalTimer($hash, "FHEM::SolarForecast::createAssociatedWith"); if ($init_done) { my (@cd, @nd); my ($afc, $ara, $ain, $ame, $aba, $h); my $fcdev1 = AttrVal ($name, 'ctrlWeatherDev1', ''); # Weather forecast Device 1 ($afc,$h) = parseParams ($fcdev1); $fcdev1 = $afc->[0] // ""; my $fcdev2 = AttrVal ($name, 'ctrlWeatherDev2', ''); # Weather forecast Device 2 ($afc,$h) = parseParams ($fcdev2); $fcdev2 = $afc->[0] // ""; my $fcdev3 = AttrVal ($name, 'ctrlWeatherDev3', ''); # Weather forecast Device 3 ($afc,$h) = parseParams ($fcdev3); $fcdev3 = $afc->[0] // ""; my $radev = ReadingsVal($name, 'currentRadiationAPI', ''); # 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 $consumer = AttrVal($name, "consumer${c}", ""); my ($ac,$hc) = parseParams ($consumer); my $codev = $ac->[0] // ''; my $dswitch = $hc->{switchdev} // ''; # alternatives Schaltdevice push @cd, $codev if($codev); push @cd, $dswitch if($dswitch); } @nd = @cd; push @nd, $fcdev1 if($fcdev1 && $fcdev1 !~ /-API/xs); push @nd, $fcdev2 if($fcdev2 && $fcdev2 !~ /-API/xs); push @nd, $fcdev3 if($fcdev3 && $fcdev3 !~ /-API/xs); push @nd, $radev if($radev && $radev !~ /-API/xs); push @nd, $indev; push @nd, $medev; push @nd, $badev; my @ndn = (); for my $e (@nd) { next if($e ~~ @ndn); push @ndn, $e; } my %seen; if (@cd) { $hash->{NOTIFYDEV} = join ",", grep { !$seen{$_ }++ } @cd; } if (@nd) { undef %seen; my $asw = join " ", grep { !$seen{$_ }++ } @nd; readingsSingleUpdate ($hash, ".associatedWith", $asw, 0); } } else { InternalTimer(gettimeofday()+3, "FHEM::SolarForecast::createAssociatedWith", $hash, 0); } return; } ################################################################ # Planungsdaten Consumer löschen # $c - Consumer Nummer ################################################################ sub deleteConsumerPlanning { my $hash = 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}{planSupplement}; delete $data{$type}{$name}{consumers}{$c}{planswitchon}; delete $data{$type}{$name}{consumers}{$c}{planswitchoff}; delete $data{$type}{$name}{consumers}{$c}{plandelete}; delete $data{$type}{$name}{consumers}{$c}{ehodpieces}; deleteReadingspec ($hash, "consumer${c}.*"); Log3($name, 3, qq{$name - Consumer planning of "$calias" deleted}); return; } ################################################################ # Internal MODEL und Model abhängige Setzungen / Löschungen ################################################################ sub setModel { my $hash = shift; my $api = ReadingsVal ($hash->{NAME}, 'currentRadiationAPI', 'DWD'); if ($api =~ /SolCast-/xs) { $hash->{MODEL} = 'SolCastAPI'; } elsif ($api =~ /ForecastSolar-/xs) { $hash->{MODEL} = 'ForecastSolarAPI'; } elsif ($api =~ /VictronKI-/xs) { $hash->{MODEL} = 'VictronKiAPI'; } elsif ($api =~ /OpenMeteoDWDEnsemble-/xs) { $hash->{MODEL} = 'OpenMeteoDWDEnsembleAPI'; } elsif ($api =~ /OpenMeteoDWD-/xs) { $hash->{MODEL} = 'OpenMeteoDWDAPI'; } elsif ($api =~ /OpenMeteoWorld-/xs) { $hash->{MODEL} = 'OpenMeteoWorldAPI'; } else { $hash->{MODEL} = 'DWD'; } return; } ################################################################ # Laufzeit Ergebnis erfassen und speichern ################################################################ 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; } ################################################################ # Voraussetzungen zur Nutzung der KI prüfen, Status setzen # und Prüfungsergebnis (0/1) zurückgeben ################################################################ sub isPrepared4AI { my $hash = shift; my $full = shift // q{}; # wenn true -> auch Auswertung ob on_.*_ai gesetzt ist my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my ($acu, $aln) = isAutoCorrUsed ($name); my $err; if (!isDWDUsed($hash) && !isOpenMeteoUsed($hash)) { $err = qq(The selected SolarForecast Model cannot use AI support); } elsif ($aidtabs) { $err = qq(The Perl module AI::DecisionTree is missing. Please install it with e.g. "sudo apt-get install libai-decisiontree-perl" for AI support); } elsif ($full && $acu !~ /ai/xs) { $err = "Set pvCorrectionFactor_Auto to '_ai' for switch on AI support"; } if ($err) { $data{$type}{$name}{current}{aicanuse} = $err; return 0; } $data{$type}{$name}{current}{aicanuse} = 'ok'; return 1; } ################################################################ # Funktion liefert 1 wenn Consumer physisch "eingeschaltet" # ist, d.h. der Wert onreg des Readings rswstate wahr ist ################################################################ sub isConsumerPhysOn { my $hash = shift; my $c = shift; my $name = $hash->{NAME}; my ($cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name if(!$defs{$dswname}) { Log3($name, 1, qq{$name - ERROR - the device "$dswname" is invalid. Please check device names in consumer "$c" attribute}); return 0; } my $reg = ConsumerVal ($hash, $c, 'onreg', 'on'); my $rswstate = ConsumerVal ($hash, $c, 'rswstate', 'state'); # Reading mit Schaltstatus my $swstate = ReadingsVal ($dswname, $rswstate, 'undef'); if ($swstate =~ m/^$reg$/x) { return 1; } return 0; } ################################################################ # Funktion liefert 1 wenn Consumer physisch "ausgeschaltet" # ist, d.h. der Wert offreg des Readings rswstate wahr ist ################################################################ sub isConsumerPhysOff { my $hash = shift; my $c = shift; my $name = $hash->{NAME}; my ($cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name if(!$defs{$dswname}) { Log3($name, 1, qq{$name - ERROR - the device "$dswname" is invalid. Please check device names in consumer "$c" attribute}); return 0; } my $reg = ConsumerVal ($hash, $c, 'offreg', 'off'); my $rswstate = ConsumerVal ($hash, $c, 'rswstate', 'state'); # Reading mit Schaltstatus my $swstate = ReadingsVal ($dswname, $rswstate, 'undef'); if ($swstate =~ m/^$reg$/x) { return 1; } return 0; } ################################################################ # Funktion liefert 1 wenn Consumer logisch "eingeschaltet" # ist, d.h. wenn der Energieverbrauch über einem bestimmten # Schwellenwert oder der prozentuale Verbrauch über dem # Defaultwert $defpopercent ist. # # Logisch "on" schließt physisch "on" mit ein. ################################################################ sub isConsumerLogOn { my $hash = shift; my $c = shift; my $pcurr = shift // 0; 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; } 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 $pthreshold = ConsumerVal ($hash, $c, "powerthreshold", 0); # Schwellenwert (W) ab der ein Verbraucher als aktiv gewertet wird if (!$rpcurr && isConsumerPhysOn($hash, $c)) { # Workaround wenn Verbraucher ohne Leistungsmessung $pcurr = $nompower; } my $currpowerpercent = $pcurr; $currpowerpercent = ($pcurr / $nompower) * 100 if($nompower > 0); $data{$type}{$name}{consumers}{$c}{currpowerpercent} = $currpowerpercent; if($pcurr > $pthreshold || $currpowerpercent > $defpopercent) { # Verbraucher ist logisch aktiv return 1; } return 0; } ################################################################ # Consumer $c in Grafik ausblenden (1) oder nicht (0) # 0 - nicht aublenden (default) # 1 - ausblenden # 2 - nur in Consumerlegende ausblenden # 3 - nur in Flowgrafik ausblenden ################################################################ sub isConsumerNoshow { my $hash = shift; my $c = shift; my $noshow = ConsumerVal ($hash, $c, 'noshow', 0); # Schalter "Ausblenden" if (!isNumeric ($noshow)) { # Key "noshow" enthält Signalreading my $rdg = $noshow; my ($dev, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name if ($noshow =~ /:/xs) { ($dev, $rdg) = split ":", $noshow; } $noshow = ReadingsNum ($dev, $rdg, 0); } if ($noshow !~ /^[0123]$/xs) { # nue Ergebnisse 0..3 zulassen $noshow = 0; } return $noshow; } ################################################################ # Funktion liefert "1" wenn die zusätzliche Einschaltbedingung # aus dem Schlüssel "swoncond" im Consumer Attribut wahr ist # # $info - den Info-Status # $err - einen Error-Status # ################################################################ sub isAddSwitchOnCond { my $hash = shift; my $c = shift; 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); } 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) { return (1, $info, $err); } $info = qq{The device "$dswoncond", reading "$rswoncond" doesn't match the Regex "$swoncondregex"}; return (0, $info, $err); } ################################################################ # Funktion liefert "1" wenn eine Ausschaltbedingung # erfüllt ist # ("swoffcond" oder "interruptable" im Consumer Attribut) # Der Inhalt von "interruptable" wird optional in $cond # übergeben. # # $info - den Info-Status # $err - einen Error-Status # ################################################################ sub isAddSwitchOffCond { my $hash = shift; my $c = shift; my $cond = shift // q{}; my $hyst = shift // 0; # Hysterese my $swoff = 0; my $info = q{}; my $err = q{}; 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', ''); } 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); } my $condval = ReadingsVal ($dswoffcond, $rswoffcond, undef); if (defined $condval) { if ($condval =~ m/^$swoffcondregex$/x) { $info = qq{value "$condval" matches the Regex "$swoffcondregex" \n}; $info .= "-> !Interrupt! "; $swoff = 1; } else { $info = qq{value "$condval" doesn't match the Regex "$swoffcondregex" \n}; $swoff = 0; } if ($hyst && isNumeric ($condval)) { # Hysterese berücksichtigen $condval -= $hyst; if ($condval =~ m/^$swoffcondregex$/x) { $info = qq{value "$condval" (included hysteresis = $hyst) matches the Regex "$swoffcondregex" \n}; $info .= "-> !Interrupt! "; $swoff = 1; } else { $info = qq{device: "$dswoffcond", reading: "$rswoffcond" , value: "$condval" (included hysteresis = $hyst) doesn't match Regex: "$swoffcondregex" \n}; $swoff = 0; } } $info .= qq{-> the effect depends on the switch context\n}; } return ($swoff, $info, $err); } ################################################################ # Funktion liefert "1" wenn die angegebene Bedingung # aus dem Consumerschlüssel 'spignorecond' erfüllt ist. # # $info - den Info-Status # $err - einen Error-Status # ################################################################ sub isSurplusIgnoCond { my $hash = shift; my $c = shift; my $debug = shift; my $info = q{}; my $err = q{}; my $digncond = ConsumerVal ($hash, $c, 'dspignorecond', ''); # Device zur Lieferung einer "Überschuß Ignore-Bedingung" if($digncond && !$defs{$digncond}) { $err = qq{ERROR - the device "$digncond" doesn't exist! Check the key "spignorecond" in attribute "consumer${c}"}; return (0, $info, $err); } my $rigncond = ConsumerVal ($hash, $c, 'rigncond', ''); # Reading zur Lieferung einer zusätzlichen Einschaltbedingung my $spignorecondregex = ConsumerVal ($hash, $c, 'spignorecondregex', ''); # Regex einer zusätzliche Einschaltbedingung my $condval = ReadingsVal ($digncond, $rigncond, ''); # Wert zum Vergleich mit Regex if ($condval && $debug =~ /consumerSwitching/x) { my $name = $hash->{NAME}; Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - PV surplus ignore condition ist set - device: $digncond, reading: $rigncond, condition: $spignorecondregex}); } if ($condval && $condval =~ m/^$spignorecondregex$/x) { return (1, $info, $err); } $info = qq{The device "$digncond", reading "$rigncond" doesn't match the Regex "$spignorecondregex"}; return (0, $info, $err); } ################################################################ # liefert den Status des Timeframe von Consumer $c ################################################################ sub isInTimeframe { my $hash = shift; my $c = shift; return ConsumerVal ($hash, $c, 'isIntimeframe', 0); } ################################################################ # liefert Entscheidung ob sich Consumer $c noch in der # Sperrzeit befindet ################################################################ sub isInLocktime { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $c = $paref->{consumer}; my $t = $paref->{t}; my $iilt = 0; my $rlt = 0; my $lt = 0; my $clt = 0; my $ltt = isConsumerPhysOn ($hash, $c) ? 'onlt' : # Typ der Sperrzeit isConsumerPhysOff ($hash, $c) ? 'offlt' : ''; my ($cltoff, $clton) = split ":", ConsumerVal ($hash, $c, 'locktime', '0:0'); $clton //= 0; # $clton undef möglich, da Angabe optional if ($ltt eq 'onlt') { $lt = ConsumerVal ($hash, $c, 'lastAutoOnTs', 0); $clt = $clton; } elsif ($ltt eq 'offlt') { $lt = ConsumerVal ($hash, $c, 'lastAutoOffTs', 0); $clt = $cltoff; } if ($t - $lt <= $clt) { $iilt = 1; $rlt = $clt - ($t - $lt); # remain lock time } return ($iilt, $rlt); } ################################################################ # liefert den Status "Consumption Recommended" von Consumer $c ################################################################ sub isConsRcmd { my $hash = shift; my $c = shift; return ConsumerVal ($hash, $c, 'isConsumptionRecommended', 0); } ################################################################ # ist Batterie installiert ? # 1 - ja, 0 - nein ################################################################ sub isBatteryUsed { 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); } ################################################################ # ist Consumer $c unterbrechbar (1|2) oder nicht (0|3) ################################################################ sub isInterruptable { my $hash = shift; my $c = shift; my $hyst = shift // 0; my $print = shift // 0; # Print out Debug Info 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 = getDebug ($hash); # Debug Module my ($swoffcond,$info,$err) = isAddSwitchOffCond ($hash, $c, $intable, $hyst); Log3 ($name, 1, "$name - $err") if($err); if ($print && $debug =~ /consumerSwitching/x) { Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - Interrupt Info: $info}); } if ($swoffcond) { return 2; } else { return 3; } return; } ################################################################ # Prüfung auf numerischen Wert (vorzeichenbehaftet) ################################################################ sub isNumeric { my $val = shift // q{empty}; my $ret = 0; if($val =~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/xs) { $ret = 1; } return $ret; } ################################################################ # Prüfung auf Verwendung von DWD als Strahlungsquelle ################################################################ sub isDWDUsed { my $hash = shift; my $ret = 0; if (InternalVal ($hash->{NAME}, 'MODEL', '') eq 'DWD') { $ret = 1; } return $ret; } ################################################################ # Prüfung auf Verwendung von SolCast API ################################################################ sub isSolCastUsed { my $hash = shift; my $ret = 0; if (InternalVal ($hash->{NAME}, 'MODEL', '') eq 'SolCastAPI') { $ret = 1; } return $ret; } ################################################################ # Prüfung auf Verwendung von ForecastSolar API ################################################################ sub isForecastSolarUsed { my $hash = shift; my $ret = 0; if (InternalVal ($hash->{NAME}, 'MODEL', '') eq 'ForecastSolarAPI') { $ret = 1; } return $ret; } ################################################################ # Prüfung auf Verwendung von Victron VRM API (KI basierend) ################################################################ sub isVictronKiUsed { my $hash = shift; my $ret = 0; if (InternalVal ($hash->{NAME}, 'MODEL', '') eq 'VictronKiAPI') { $ret = 1; } return $ret; } ################################################################ # Prüfung auf Verwendung von Open-Meteo API ################################################################ sub isOpenMeteoUsed { my $hash = shift; my $ret = 0; if (InternalVal ($hash->{NAME}, 'MODEL', '') =~ /^OpenMeteo/xs) { $ret = 1; } return $ret; } ################################################################ # welche PV Autokorrektur wird verwendet ? # Standard bei nur "on" -> on_simple # $aln: 1 - Lernen aktiviert (default) # 0 - Lernen deaktiviert ################################################################ sub isAutoCorrUsed { my $name = shift; my $cauto = ReadingsVal ($name, 'pvCorrectionFactor_Auto', 'off'); my $acu = $cauto =~ /on_simple_ai/xs ? 'on_simple_ai' : $cauto =~ /on_simple/xs ? 'on_simple' : $cauto =~ /on_complex_ai/xs ? 'on_complex_ai' : $cauto =~ /on_complex/xs ? 'on_complex' : $cauto =~ /standby/xs ? 'standby' : $cauto =~ /on/xs ? 'on_simple' : q{}; my $aln = $cauto =~ /noLearning/xs ? 0 : 1; return ($acu, $aln); } ################################################################ # liefert Status ob SunPath in mintime gesetzt ist ################################################################ sub isSunPath { my $hash = shift; my $c = shift; my $is = 0; my $mintime = ConsumerVal ($hash, $c, 'mintime', $defmintime); if ($mintime =~ /SunPath/xsi) { $is = 1; my $sunset = CurrentVal ($hash, 'sunsetTodayTs', 1); my $sunrise = CurrentVal ($hash, 'sunriseTodayTs', 5); if ($sunrise > $sunset) { $is = 0; my $name = $hash->{NAME}; Log3($name, 1, qq{$name - ERROR - consumer >$c< use >mintime=SunPath< but readings >Today_SunRise< / >Today_SunSet< are not set properly.}); } } return $is; } ##################################################################### # Prüft ob das in ctrlWeatherDevX # übergebene Weather Device valide ist # return - $valid -> ist die Angabe valide (1) # $apiu -> wird ein Device oder API verwendet ##################################################################### sub isWeatherDevValid { my $hash = shift; my $wdev = shift; my $valid = ''; my $apiu = ''; my $fcname = AttrVal ($hash->{NAME}, $wdev, ''); # Weather Forecast Device if ($fcname) { $valid = 1 } if (!$defs{$fcname} || $defs{$fcname}{TYPE} ne "DWD_OpenData") { $valid = '' } if (isOpenMeteoUsed($hash) && $fcname =~ /^OpenMeteo/xs) { $valid = 1; $apiu = $fcname; } return ($valid, $fcname, $apiu); } ################################################################### # püft das Alter fc_time aller Wetterdevices # $result->{agedv} : Name des DWD mit ältesten Daten # $result->{mosmix}: gewählte MOSMIX Variante # $result->{fctime}: Datenstand (Forecast Time) # $result->{exceed}: Bewertung ob zulässiges Alter überschritten ################################################################### sub isWeatherAgeExceeded { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $lang = $paref->{lang}; my $currts = int time; my $agets = $currts; my $resh->{agedv} = '-'; $resh->{mosmix} = '-'; $resh->{exceed} = ''; $resh->{fctime} = '-'; my ($newts, $th); for my $step (1..$weatherDevMax) { my ($valid, $fcname, $apiu) = isWeatherDevValid ($hash, 'ctrlWeatherDev'.$step); next if(!$fcname && $step ne 1); if (!$apiu) { if (!$fcname || !$valid) { if (!$fcname) { return (qq{No DWD device is defined in attribute "ctrlWeatherDev$step"}, $resh); } else { return (qq{The DWD device "$fcname" doesn't exist}, $resh); } } my $fct = ReadingsVal ($fcname, 'fc_time', ''); return (qq{The reading 'fc_time' ($fcname) doesn't exist or is empty}, $resh) if(!$fct); $newts = timestringToTimestamp ($fct); if ($newts <= $agets) { $agets = $newts; $resh->{agedv} = $fcname; $resh->{apiu} = $apiu; } } else { $newts = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_timestamp', $agets); if ($newts <= $agets) { $agets = $newts; $resh->{agedv} = $fcname; $resh->{apiu} = $apiu; } } } if (!$resh->{apiu}) { # DWD Device ist Wetterdatenlieferant $resh->{mosmix} = AttrVal ($resh->{agedv}, 'forecastRefresh', 6) == 6 ? 'MOSMIX_L' : 'MOSMIX_S'; $th = $resh->{mosmix} eq 'MOSMIX_S' ? 7200 : 25200; } else { # API ist Wetterdatenlieferant $resh->{mosmix} = 'ICON'; $th = 5400; } $resh->{exceed} = $currts - $agets > $th ? 1 : 0; $resh->{fctime} = (timestampToTimestring ($agets, $lang))[0]; return ('', $resh); } ################################################################### # püft das Alter fc_time des DWD Rad1h Devices # $result->{agedv} : Name des DWD Rad1h Devices # $result->{mosmix}: gewählte MOSMIX Variante # $result->{fctime}: Datenstand (Forecast Time) # $result->{exceed}: Bewertung ob zulässiges Alter überschritten ################################################################### sub isRad1hAgeExceeded { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $lang = $paref->{lang}; my $currts = int time; my $fcname = CurrentVal ($hash, 'dwdRad1hDev', ''); my $resh->{agedv} = '-'; $resh->{mosmix} = '-'; $resh->{exceed} = ''; $resh->{fctime} = '-'; if (!$fcname || !$defs{$fcname}) { if (!$fcname) { return (qq{No DWD device is defined in "currentRadiationAPI"}, $resh); } else { return (qq{The DWD device "$fcname" doesn't exist}, $resh); } } my $fct = ReadingsVal ($fcname, 'fc_time', ''); return (qq{The reading 'fc_time' ($fcname) doesn't exist or is empty}, $resh) if(!$fct); $resh->{agedv} = $fcname; $resh->{mosmix} = AttrVal ($resh->{agedv}, 'forecastRefresh', 6) == 1 ? 'MOSMIX_S' : 'MOSMIX_L'; my $agets = timestringToTimestamp ($fct); my $th = $resh->{mosmix} eq 'MOSMIX_S' ? 7200 : 25200; $resh->{exceed} = $currts - $agets > $th ? 1 : 0; $resh->{fctime} = (timestampToTimestring ($agets, $lang))[0]; return ('', $resh); } ################################################################ # Verschiebung von Sonnenaufgang / Sonnenuntergang # bei Verwendung von mintime = SunPath ################################################################ sub sunShift { my $hash = shift; my $c = shift; my $riseshift = ConsumerVal ($hash, $c, 'sunriseshift', 0); # Verschiebung (Sekunden) Sonnenaufgang bei SunPath Verwendung my $setshift = ConsumerVal ($hash, $c, 'sunsetshift', 0); # Verschiebung (Sekunden) Sonnenuntergang bei SunPath Verwendung return ($riseshift, $setshift); } ################################################################ # Prüfung ob global Attr latitude und longitude gesetzt sind # gibt latitude, longitude und altitude zurück ################################################################ sub locCoordinates { my $set = 0; my $lat = AttrVal ('global', 'latitude', ''); my $lon = AttrVal ('global', 'longitude', ''); my $alt = AttrVal ('global', 'altitude', 0); if ($lat && $lon) { $set = 1; } return ($set, $lat, $lon, $alt); } ################################################################ # liefert die Zeit des letzten Schaltvorganges ################################################################ sub lastConsumerSwitchtime { my $hash = shift; my $c = shift; my $name = $hash->{NAME}; my ($cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name if(!$defs{$dswname}) { Log3($name, 1, qq{$name - ERROR - The last switching time can't be identified due to the device "$dswname" is invalid. Please check device names in consumer "$c" attribute}); return; } my $rswstate = ConsumerVal ($hash, $c, 'rswstate', 'state'); # Reading mit Schaltstatus my $swtime = ReadingsTimestamp ($dswname, $rswstate, ''); # Zeitstempel im Format 2016-02-16 19:34:24 my $swtimets; $swtimets = timestringToTimestamp ($swtime) if($swtime); # Unix Timestamp Format erzeugen return ($swtime, $swtimets); } ################################################################ # transformiert den ausführlichen Consumerstatus in eine # einfache Form ################################################################ sub simplifyCstate { my $ps = shift; $ps = $ps =~ /planned/xs ? 'planned' : $ps =~ /suspended/xs ? 'suspended' : $ps =~ /switching\son/xs ? 'starting' : $ps =~ /switched\son/xs ? 'started' : $ps =~ /switching\soff/xs ? 'stopping' : $ps =~ /switched\soff/xs ? 'finished' : $ps =~ /priority/xs ? 'priority' : $ps =~ /interrupting/xs ? 'interrupting' : $ps =~ /interrupted/xs ? 'interrupted' : $ps =~ /continuing/xs ? 'continuing' : $ps =~ /continued/xs ? 'continued' : $ps =~ /noSchedule/xs ? 'noSchedule' : 'unknown'; return $ps; } ################################################################ # Prüfung eines übergebenen Regex ################################################################ sub checkRegex { my $regexp = shift // return; eval { "Hallo" =~ m/^$regexp$/; 1; } or do { my $err = (split " at", $@)[0]; return "Bad regexp: ".$err; }; return; } ################################################################ # prüfen Angabe hh[:mm] ################################################################ sub checkhhmm { my $val = shift; my $valid = 0; if ($val =~ /^([0-9]{1,2})(:[0-5]{1}[0-9]{1})?$/xs) { $valid = 1 if(int $1 < 24); } return $valid; } ################################################################ # prüfen validen Code in $val ################################################################ sub checkCode { my $name = shift; my $val = shift; my $cc1 = shift // 0; # wenn 1 checkCode1 ausführen my $err; if (!$val || $val !~ m/^\s*\{.*\}\s*$/xs) { return qq{Usage of $name is wrong. The function has to be specified as "{}"}; } if ($cc1) { ($err, $val) = checkCode1 ($name, $val); return ($err, $val); } my %specials = ( "%DEVICE" => $name, "%READING" => $name, "%VALUE" => 1, "%UNIT" => 'kW', ); $err = perlSyntaxCheck ($val, %specials); return $err if($err); if ($val =~ m/^\{.*\}$/xs && $val =~ m/=>/ && $val !~ m/\$/ ) { # Attr wurde als Hash definiert my $av = eval $val; return $@ if($@); $av = eval $val; $val = $av if(ref $av eq "HASH"); } return ('', $val); } ################################################################ # prüfen validen Code in $val ################################################################ sub checkCode1 { my $name = shift; my $val = shift; my $hash = $defs{$name}; $val =~ m/^\s*(\{.*\})\s*$/xs; $val = $1; $val = eval $val; return $@ if($@); return ('', $val); } ################################################################ # die eingestellte Modulsprache ermitteln ################################################################ sub getLang { my $hash = shift; my $name = $hash->{NAME}; my $glang = AttrVal ('global', 'language', $deflang); my $lang = AttrVal ($name, 'ctrlLanguage', $glang); return $lang; } ################################################################ # den eingestellte Debug Modus ermitteln ################################################################ sub getDebug { my $hash = shift; my $debug = AttrVal ($hash->{NAME}, 'ctrlDebug', 'none'); return $debug; } ################################################################ # Namen des Consumerdevices und des zugeordneten # Switch Devices ermitteln ################################################################ sub getCDnames { my $hash = shift; my $c = shift; my $cname = ConsumerVal ($hash, $c, "name", ""); # Name des Consumerdevices my $dswname = ConsumerVal ($hash, $c, 'dswitch', $cname); # alternatives Switch Device return ($cname, $dswname); } ################################################################ # diskrete Temperaturen in "Bins" wandeln ################################################################ sub temp2bin { my $val = shift; my $bin = $val > 35 ? 35 : $val > 32 ? 35 : $val > 30 ? 30 : $val > 27 ? 30 : $val > 25 ? 25 : $val > 22 ? 25 : $val > 20 ? 20 : $val > 17 ? 20 : $val > 15 ? 15 : $val > 12 ? 15 : $val > 10 ? 10 : $val > 7 ? 10 : $val > 5 ? 5 : $val > 2 ? 5 : $val > 0 ? 0 : -5; return $bin; } ################################################################ # diskrete Bewölkung in "Bins" wandeln ################################################################ sub cloud2bin { my $val = shift; my $bin = $val == 100 ? '100' : $val > 97 ? '100' : $val > 95 ? '95' : $val > 92 ? '95' : $val > 90 ? '90' : $val > 87 ? '90' : $val > 85 ? '85' : $val > 82 ? '85' : $val > 80 ? '80' : $val > 77 ? '80' : $val > 75 ? '75' : $val > 72 ? '75' : $val > 70 ? '70' : $val > 67 ? '70' : $val > 65 ? '65' : $val > 62 ? '65' : $val > 60 ? '60' : $val > 57 ? '60' : $val > 55 ? '55' : $val > 52 ? '55' : $val > 50 ? '50' : $val > 47 ? '50' : $val > 45 ? '45' : $val > 42 ? '45' : $val > 40 ? '40' : $val > 37 ? '40' : $val > 35 ? '35' : $val > 32 ? '35' : $val > 30 ? '30' : $val > 27 ? '30' : $val > 25 ? '25' : $val > 22 ? '25' : $val > 20 ? '20' : $val > 17 ? '20' : $val > 15 ? '15' : $val > 12 ? '15' : $val > 10 ? '10' : $val > 7 ? '10' : $val > 5 ? '05' : $val > 2 ? '05' : '00'; return $bin; } ################################################################ # diskrete Sonnen Höhe (altitude) in "Bins" wandeln ################################################################ sub sunalt2bin { my $val = shift; my $bin = $val == 90 ? 90 : $val > 87 ? 90 : $val > 85 ? 85 : $val > 82 ? 85 : $val > 80 ? 80 : $val > 77 ? 80 : $val > 75 ? 75 : $val > 72 ? 75 : $val > 70 ? 70 : $val > 67 ? 70 : $val > 65 ? 65 : $val > 62 ? 65 : $val > 60 ? 60 : $val > 57 ? 60 : $val > 55 ? 55 : $val > 52 ? 55 : $val > 50 ? 50 : $val > 47 ? 50 : $val > 45 ? 45 : $val > 42 ? 45 : $val > 40 ? 40 : $val > 37 ? 40 : $val > 35 ? 35 : $val > 32 ? 35 : $val > 30 ? 30 : $val > 27 ? 30 : $val > 25 ? 25 : $val > 22 ? 25 : $val > 20 ? 20 : $val > 17 ? 20 : $val > 15 ? 15 : $val > 12 ? 15 : $val > 10 ? 10 : $val > 7 ? 10 : $val > 5 ? 5 : $val > 2 ? 5 : 0; return $bin; } ############################################################################### # verscrambelt einen String ############################################################################### sub chew { my $sstr = shift; $sstr = encode_base64 ($sstr, ''); my @key = qw(1 3 4 5 6 3 2 1 9); my $len = scalar @key; my $i = 0; my $dstr = join "", map { $i = ($i + 1) % $len; chr((ord($_) + $key[$i]) % 256) } split //, $sstr; ## no critic 'Map blocks'; return $dstr; } ############################################################################### # entpackt einen mit chew behandelten String ############################################################################### sub assemble { my $sstr = shift; my @key = qw(1 3 4 5 6 3 2 1 9); my $len = scalar @key; my $i = 0; my $dstr = join "", map { $i = ($i + 1) % $len; chr((ord($_) - $key[$i] + 256) % 256) } split //, $sstr; ## no critic 'Map blocks'; $dstr = decode_base64 ($dstr); return $dstr; } ############################################################### # Daten Serialisieren ############################################################### sub Serialize { my $data = shift; my $name = $data->{name}; my $serial = eval { freeze ($data) } or do { Log3 ($name, 1, "$name - Serialization ERROR: $@"); return; }; return $serial; } ################################################################ # Funktion um mit Storable eine Struktur in ein File # zu schreiben ################################################################ sub fileStore { my $obj = shift; my $file = shift; my $err; my $ret = eval { nstore ($obj, $file) }; if (!$ret || $@) { $err = $@ ? $@ : 'I/O problems or other internal error'; } return $err; } ################################################################ # Funktion um mit Storable eine Struktur aus einem File # zu lesen ################################################################ sub fileRetrieve { my $file = shift; my ($err, $obj); if (-e $file) { eval { $obj = retrieve ($file) }; if (!$obj || $@) { $err = $@ ? $@ : 'I/O error while reading'; } } return ($err, $obj); } ############################################################### # erzeugt eine Zeile Leerzeichen. Die Anzahl der # Leerzeichen ist etwas größer als die Zeichenzahl des # längsten Teilstrings (Trenner \n) ############################################################### sub lineFromSpaces { my $str = shift // return; my $an = shift // 5; my @sps = split "\n", $str; my $mlen = 1; for my $s (@sps) { my $len = length (trim $s); $mlen = $len if($len && $len > $mlen); } my $ret = "\n"; $ret .= " " x ($mlen + $an); return $ret; } ################################################################ # Funktion um userspezifische Programmaufrufe nach # Aktualisierung aller Readings zu ermöglichen ################################################################ sub userExit { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $uefn = AttrVal ($name, 'ctrlUserExitFn', ''); return if(!$uefn); $uefn =~ s/\s*#.*//g; # Kommentare entfernen $uefn = join ' ', split(/\s+/sx, $uefn); # Funktion aus Attr ctrlUserExitFn serialisieren if ($uefn =~ m/^\s*(\{.*\})\s*$/xs) { # unnamed Funktion direkt in ctrlUserExitFn mit {...} $uefn = $1; eval $uefn; if ($@) { Log3 ($name, 1, "$name - ERROR in specific userExitFn: ".$@); } } return; } ############################################################################### # Wert des pvhist-Hash zurückliefern # Usage: # HistoryVal ($hash, $day, $hod, $key, $def) # # $day: Tag des Monats (01,02,...,31) # $hod: Stunde des Tages (01,02,...,24,99) # $key: etotal - totale PV Erzeugung (Wh) # pvrl - realer PV Ertrag # pvfc - PV Vorhersage # confc - Vorhersage Hausverbrauch (Wh) # gcons - realer Netzbezug # gfeedin - reale Netzeinspeisung # batintotal - totale Batterieladung (Wh) # batin - Batterieladung der Stunde (Wh) # batouttotal - totale Batterieentladung (Wh) # batout - Batterieentladung der Stunde (Wh) # batmsoc - max. SOC des Tages (%) # batmaxsoc - maximum SOC (%) des Tages # batsetsoc - optimaler (berechneter) SOC (%) für den Tag # weatherid - Wetter ID # wcc - Grad der Bewölkung # temp - Außentemperatur # rr1c - Gesamtniederschlag (1-stündig) letzte 1 Stunde kg/m2 # pvcorrf - PV Autokorrekturfaktor f. Stunde des Tages # dayname - Tagesname (Kürzel) # csmt${c} - Totalconsumption Consumer $c (1..$maxconsumer) # csme${c} - Consumption Consumer $c (1..$maxconsumer) in $hod # sunaz - Azimuth der Sonne (in Dezimalgrad) # sunalt - Höhe der Sonne (in Dezimalgrad) # $def: Defaultwert # ############################################################################### sub HistoryVal { my $hash = shift; my $day = shift; 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}) && defined($data{$type}{$name}{pvhist}{$day}{$hod}{$key})) { return $data{$type}{$name}{pvhist}{$day}{$hod}{$key}; } return $def; } ##################################################################################################### # Wert des circular-Hash zurückliefern # Achtung: die Werte im circular-Hash haben nicht # zwingend eine Beziehung zueinander !! # # Usage: # CircularVal ($hash, $hod, $key, $def) # # $hod: Stunde des Tages (01,02,...,24) bzw. 99 (besondere Verwendung) # $key: pvrl - realer PV Ertrag # pvfc - PV Vorhersage # pvrlsum - Summe PV Ertrag über die gesamte Laufzeit # pvfcsum - Summe PV Prognose über die gesamte Laufzeit # dnumsum - Anzahl der Tage der Durchschnittsberechnung über die gesamte Laufzeit # confc - Vorhersage Hausverbrauch (Wh) # gcons - realer Netzbezug # gfeedin - reale Netzeinspeisung # batin - Batterieladung (Wh) # batout - Batterieentladung (Wh) # weatherid - DWD Wetter id # weathertxt - DWD Wetter Text # wcc - DWD Wolkendichte # rr1c - Gesamtniederschlag (1-stündig) letzte 1 Stunde kg/m2 # temp - Außentemperatur # pvcorrf - PV Autokorrekturfaktoren (HASH), # - . # lastTsMaxSocRchd - Timestamp des letzten Erreichens von SoC >= maxSoC # nextTsMaxSocChge - Timestamp bis zu dem die Batterie mindestens einmal maxSoC erreichen soll # days2care - verbleibende Tage bis der Batterie Pflege-SoC (default $maxSoCdef) erreicht sein soll # tdayDvtn - heutige Abweichung PV Prognose/Erzeugung in % # ydayDvtn - gestrige Abweichung PV Prognose/Erzeugung in % # initdayfeedin - initialer Wert für "gridfeedin" zu Beginn des Tages (Wh) # feedintotal - Einspeisung PV Energie total (Wh) # initdaygcon - initialer Wert für "gcon" zu Beginn des Tages (Wh) # initdaybatintot - initialer Wert für Batterie intotal zu Beginn des Tages (Wh) # batintot - Batterie intotal (Wh) # initdaybatouttot - initialer Wert für Batterie outtotal zu Beginn des Tages (Wh) # batouttot - Batterie outtotal (Wh) # gridcontotal - Netzbezug total (Wh) # # $def: Defaultwert # ##################################################################################################### sub CircularVal { my $hash = shift; 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})) { return $data{$type}{$name}{circular}{$hod}{$key}; } return $def; } ################################################################### # Wert des Autokorrekturfaktors # für eine bestimmte Sun Altitude-Range aus dem Circular-Hash # zurückliefern # Usage: # $f = CircularSunCloudkorrVal ($hash, $hod, $sabin, $crang, $def) # # $f: Korrekturfaktor f. Stunde des Tages # # $hod: Stunde des Tages (01,02,...,24) # $sabin: Sun Altitude Bin (0..90) # $crang: Bewölkung Bin (0..100) oder "simple" # $def: Defaultwert # ################################################################### sub CircularSunCloudkorrVal { my $hash = shift; my $hod = shift; my $sabin = shift; my $crang = shift; my $def = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my $corrf = $def; my $qual = $def; my $idx = 'simple'; if ($crang ne 'simple') { $idx = $sabin.'.'.$crang; } 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}{$idx})) { $corrf = $data{$type}{$name}{circular}{$hod}{pvcorrf}{$idx}; } if (defined($data{$type}{$name}{circular}) && defined($data{$type}{$name}{circular}{$hod}) && defined($data{$type}{$name}{circular}{$hod}{quality}) && defined($data{$type}{$name}{circular}{$hod}{quality}{$idx})) { $qual = $data{$type}{$name}{circular}{$hod}{quality}{$idx}; } return ($corrf, $qual); } ######################################################################################################## # Die durchschnittliche reale PV Erzeugung, PV Prognose und Tage # einer bestimmten Bewölkungs-Range aus dem circular-Hash zurückliefern # Usage: # ($pvrlsum, $pvfcsum, $dnumsum) = CircularSumVal ($hash, $hod, $sabin, $crang, $def) # # $pvrlsum: Summe reale PV Erzeugung pro Bewölkungsbereich über die gesamte Laufzeit # $pvfcsum: Summe PV Prognose pro Bewölkungsbereich über die gesamte Laufzeit # $dnumsum: Anzahl Tage pro Bewölkungsbereich über die gesamte Laufzeit # # $hod: Stunde des Tages (01,02,..,24) # $sabin: Sun Altitude Bin (0..90) # $crang: Bewölkung Bin (1..100) oder "simple" # $def: Defaultwert # ####################################################################################################### sub CircularSumVal { my $hash = shift; my $hod = shift; my $sabin = shift; my $crang = shift; my $def = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my $pvrlsum = $def; my $pvfcsum = $def; my $dnumsum = $def; my $idx = 'simple'; if ($crang ne 'simple') { $idx = $sabin.'.'.$crang; } if (defined($data{$type}{$name}{circular}) && defined($data{$type}{$name}{circular}{$hod}) && defined($data{$type}{$name}{circular}{$hod}{pvrlsum}) && defined($data{$type}{$name}{circular}{$hod}{pvrlsum}{$idx})) { $pvrlsum = $data{$type}{$name}{circular}{$hod}{pvrlsum}{$idx}; } if (defined($data{$type}{$name}{circular}) && defined($data{$type}{$name}{circular}{$hod}) && defined($data{$type}{$name}{circular}{$hod}{pvfcsum}) && defined($data{$type}{$name}{circular}{$hod}{pvfcsum}{$idx})) { $pvfcsum = $data{$type}{$name}{circular}{$hod}{pvfcsum}{$idx}; } if (defined($data{$type}{$name}{circular}) && defined($data{$type}{$name}{circular}{$hod}) && defined($data{$type}{$name}{circular}{$hod}{dnumsum}) && defined($data{$type}{$name}{circular}{$hod}{dnumsum}{$idx})) { $dnumsum = $data{$type}{$name}{circular}{$hod}{dnumsum}{$idx}; } return ($pvrlsum, $pvfcsum, $dnumsum); } ######################################################################################### # Wert des nexthours-Hash zurückliefern # Usage: # NexthoursVal ($hash, $nhr, $key, $def) # # $nhr: nächste Stunde (NextHour00, NextHour01,...) # $key: starttime - Startzeit der abgefragten nächsten Stunde # hourofday - Stunde des Tages # pvfc - PV Vorhersage in Wh # pvaifc - erwartete PV Erzeugung der KI (Wh) # aihit - Trefferstatus KI # weatherid - DWD Wetter id # cloudcover - DWD Wolkendichte # cloudrange - berechnete Bewölkungsrange # totalrain - Gesamtniederschlag während der letzten Stunde kg/m2 # rad1h - Globalstrahlung (kJ/m2) # confc - prognostizierter Hausverbrauch (Wh) # confcEx - prognostizierter Hausverbrauch ohne registrierte Consumer (Wh) # today - 1 wenn heute # correff - verwendeter Korrekturfaktor / Qualität # DoN - Sonnenauf- und untergangsstatus (0 - Nacht, 1 - Tag) # $def: Defaultwert # ######################################################################################### sub NexthoursVal { my $hash = shift; my $nhr = 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}{$nhr}) && defined($data{$type}{$name}{nexthours}{$nhr}{$key})) { return $data{$type}{$name}{nexthours}{$nhr}{$key}; } return $def; } ################################################################################################### # Wert des current-Hash zurückliefern # Usage: # CurrentVal ($hash, $key, $def) # # $key: generation - aktuelle PV Erzeugung # aiinitstate - Initialisierungsstatus der KI # aitrainstate - Traisningsstatus der KI # aiaddistate - Add Instanz Status der KI # batcharge - Bat SOC in % # batinstcap - installierte Batteriekapazität in Wh # ctrunning - aktueller Ausführungsstatus des Central Task # dwdRad1hAge - Alter des Rad1h Wertes als Datumstring # dwdRad1hAgeTS - Alter des Rad1h Wertes als Unix Timestamp # genslidereg - Schieberegister PV Erzeugung (Array) # h4fcslidereg - Schieberegister 4h PV Forecast (Array) # socslidereg - Schieberegister Batterie SOC (Array) # consumption - aktueller Verbrauch (W) # consumerdevs - alle registrierten Consumerdevices (Array) # consumerCollected - Statusbit Consumer Attr gesammelt und ausgewertet # gridconsumption - aktueller Netzbezug # powerbatin - Batterie Ladeleistung # powerbatout - Batterie Entladeleistung # temp - aktuelle Außentemperatur # surplus - aktueller PV Überschuß # tomorrowconsumption - Verbrauch des kommenden Tages # invertercapacity - Bemessungsleistung der Wechselrichters (max. W) # allstringspeak - Peakleistung aller Strings nach temperaturabhängiger Korrektur # allstringscount - aktuelle Anzahl der Anlagenstrings # tomorrowconsumption - erwarteter Gesamtverbrauch am morgigen Tag # sunriseToday - Sonnenaufgang heute # sunriseTodayTs - Sonnenaufgang heute Unix Timestamp # sunsetToday - Sonnenuntergang heute # sunsetTodayTs - Sonnenuntergang heute Unix Timestamp # # $def: Defaultwert # ################################################################################################### sub CurrentVal { my $hash = shift; my $key = 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}; } return $def; } ################################################################################################### # Wert des String Hash zurückliefern # Usage: # StringVal ($hash, $strg, $key, $def) # # $strg: - Name des Strings aus modulePeakString # $key: peak - Peakleistung aus modulePeakString # tilt - Neigungswinkel der Module aus moduleDeclination # dir - Ausrichtung der Module als Azimut-Bezeichner (N,NE,E,SE,S,SW,W,NW) # azimut - Ausrichtung der Module als Azimut Angabe -180 .. 0 .. 180 # # $def: Defaultwert # ################################################################################################### sub StringVal { my $hash = shift; my $strg = shift; my $key = shift; my $def = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; if (defined $data{$type}{$name}{strings} && defined $data{$type}{$name}{strings}{$strg} && defined $data{$type}{$name}{strings}{$strg}{$key}) { return $data{$type}{$name}{strings}{$strg}{$key}; } return $def; } ################################################################################################### # Wert AI::DecisionTree Objects zurückliefern # Usage: # AiDetreeVal ($hash, key, $def) # # key: object - das AI Object # aitrained - AI trainierte Daten # airaw - Rohdaten für AI Input = Raw Trainigsdaten # # $def: Defaultwert # ################################################################################################### sub AiDetreeVal { my $hash = shift; my $key = shift; my $def = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; if (defined $data{$type}{$name}{aidectree} && defined $data{$type}{$name}{aidectree}{$key}) { return $data{$type}{$name}{aidectree}{$key}; } return $def; } ################################################################################################### # Wert AI Raw Data zurückliefern # Usage: # AiRawdataVal ($hash, $idx, $key, $def) # AiRawdataVal ($hash, '', '', $def) -> den gesamten Hash airaw lesen # # $idx: - Index # $key: rad1h - Strahlungsdaten # temp - Temeperatur als Bin # wcc - Bewölkung als Bin # rr1c - Gesamtniederschlag (1-stündig) letzte 1 Stunde kg/m2 # hod - Stunde des Tages # sunalt - Höhe der Sonne (in Dezimalgrad) # pvrl - reale PV Erzeugung # # $def: Defaultwert # ################################################################################################### sub AiRawdataVal { my $hash = shift; my $idx = shift; my $key = shift; my $def = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; if (!$idx && !$key) { if (defined $data{$type}{$name}{aidectree}{airaw}) { return $data{$type}{$name}{aidectree}{airaw}; } } if (defined $data{$type}{$name}{aidectree}{airaw} && defined $data{$type}{$name}{aidectree}{airaw}{$idx} && defined $data{$type}{$name}{aidectree}{airaw}{$idx}{$key}) { return $data{$type}{$name}{aidectree}{airaw}{$idx}{$key}; } return $def; } ################################################################################################################### # Wert des consumer-Hash zurückliefern # Usage: # ConsumerVal ($hash, $co, $key, $def) # # $co: Consumer Nummer (01,02,03,...) # $key: name - Name des Verbrauchers (Device) # alias - Alias des Verbrauchers (Device) # autoreading - Readingname f. Automatiksteuerung # type - Typ des Verbrauchers # state - Schaltstatus des Consumers # power - nominale Leistungsaufnahme des Verbrauchers in W # mode - Planungsmode des Verbrauchers # icon - Icon für den Verbraucher # mintime - min. Einplanungsdauer # onreg - Regex für phys. Zustand "ein" # offreg - Regex für phys. Zustand "aus" # oncom - Einschaltkommando # offcom - Ausschaltkommando # physoffon - physischer Schaltzustand ein/aus # onoff - logischer ein/aus Zustand des am Consumer angeschlossenen Endverbrauchers # asynchron - Arbeitsweise des FHEM Consumer Devices # 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 # 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 # planstate - Planungsstatus # planSupplement - Ergänzung zum Planungsstatus # rswoncond - Reading zur Lieferung einer zusätzliche Einschaltbedingung # swoncondregex - Regex einer zusätzliche Einschaltbedingung # dswoffcond - Device zur Lieferung einer vorrangige Ausschaltbedingung # rswoffcond - Reading zur Lieferung einer vorrangige Ausschaltbedingung # swoffcondregex - Regex einer einer vorrangige Ausschaltbedingung # isIntimeframe - ist Zeit innerhalb der Planzeit ein/aus # interruptable - Consumer "on" ist während geplanter "ein"-Zeit unterbrechbar # lastAutoOnTs - Timestamp des letzten On-Schaltens bzw. letzter Fortsetzung (nur Automatik-Modus) # lastAutoOffTs - Timestamp des letzten Off-Schaltens bzw. letzter Unterbrechnung (nur Automatik-Modus) # hysteresis - Hysterese # sunriseshift - Verschiebung (Sekunden) Sonnenaufgang bei SunPath Verwendung # sunsetshift - Verschiebung (Sekunden) Sonnenuntergang bei SunPath Verwendung # # $def: Defaultwert # #################################################################################################################### sub ConsumerVal { my $hash = shift; my $co = shift; my $key = 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})) { return $data{$type}{$name}{consumers}{$co}{$key}; } return $def; } ########################################################################################################################################################## # Wert des solcastapi-Hash zurückliefern # Usage: # SolCastAPIVal ($hash, $tring, $ststr, $key, $def) # # $tring: Stringname aus "inverterStrings" (?All für allg. Werte) # $ststr: Startzeit der Form YYYY-MM-DD hh:00:00 # $key: pv_estimate50 - PV Schätzung in Wh # Rad1h - vorhergesagte Globalstrahlung (Model DWD) # $def: Defaultwert # # Sonderabfragen # SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_time', $def) - letzte Abfrage Zeitstring # SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_timestamp', $def) - letzte Abfrage Unix Timestamp # SolCastAPIVal ($hash, '?All', '?All', 'todayDoneAPIrequests', $def) - heute ausgeführte API Requests # SolCastAPIVal ($hash, '?All', '?All', 'todayRemainingAPIrequests $def) - heute verbleibende API Requests # 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', 'response_message', $def) - letzte API Antwort # SolCastAPIVal ($hash, '?All', $ststr, 'Rad1h', $def) - Globalstrahlung mit Startzeit # SolCastAPIVal ($hash, '?All', '?All', 'place', $def) - ForecastSolarAPI -> Location der Anlage # SolCastAPIVal ($hash, '?All', '?All', 'requests_limit', $def) - ForecastSolarAPI -> Request Limit innerhalb der Periode # SolCastAPIVal ($hash, '?All', '?All', 'requests_limit_period', $def) - ForecastSolarAPI -> Periode für Request Limit # SolCastAPIVal ($hash, '?All', '?All', 'requests_remaining', $def) - ForecastSolarAPI -> verbleibende Requests innerhalb der laufenden Periode # SolCastAPIVal ($hash, '?All', '?All', 'response_code', $def) - ForecastSolarAPI -> letzter Antwortcode # SolCastAPIVal ($hash, '?All', '?All', 'retryat_time', $def) - ForecastSolarAPI -> Zwangsverzögerung des nächsten Calls bis Uhrzeit # SolCastAPIVal ($hash, '?All', '?All', 'retryat_timestamp', $def) - ForecastSolarAPI -> Zwangsverzögerung des nächsten Calls bis UNIX-Zeitstempel # # SolCastAPIVal ($hash, '?IdPair', '?', 'rtid', $def) - RoofTop-ID, = Paarschlüssel # SolCastAPIVal ($hash, '?IdPair', '?', 'apikey', $def) - API-Key, = Paarschlüssel # ########################################################################################################################################################## sub SolCastAPIVal { my $hash = shift; my $string = shift; 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} && defined $data{$type}{$name}{solcastapi}{$string}{$ststr}{$key}) { return $data{$type}{$name}{solcastapi}{$string}{$ststr}{$key}; } return $def; } 1; =pod =item summary Visualization of solar predictions for PV systems and Consumer control =item summary_DE Visualisierung von solaren Vorhersagen für PV Anlagen und Verbrauchersteuerung =begin html

SolarForecast


The SolarForecast module generates a forecast for the solar yield on the basis of the values from generic sources and integrates further information as a basis for control systems based on this forecast.
To create the solar forecast, the SolarForecast module can use different services and sources:

    DWD solar forecast based on the radiation forecast of the German Weather Service (Model DWD)
    SolCast-API uses forecast data of the SolCast API (Model SolCastAPI)
    ForecastSolar-API uses forecast data of the Forecast.Solar API (Model ForecastSolarAPI)
    VictronKI-API Victron Energy API of the VRM Portal (Model VictronKiAPI)

AI support can be enabled when using the Model DWD.
The use of the mentioned API's is limited to the respective free version of the selected service.
In the assigned DWD_OpenData Device (attribute "ctrlWeatherDevX") the suitable weather station is to be specified to get meteorological data (cloudiness, sunrise, etc.) or a radiation forecast (Model DWD) for the plant location.

In addition to the PV generation forecast, consumption values or grid reference values are recorded and used for a consumption forecast.
The module calculates a future energy surplus from the forecast values, which is used to plan the operation of consumers. Furthermore, the module offers Consumer Integration for integrated planning and control of PV surplus dependent consumer circuits.

At the first definition of the module the user is supported by a Guided Procedure to make all initial entries.
At the end of the process and after relevant changes to the system or device configuration, it is essential to perform a set <name> plantConfiguration ceck to ensure that the system configuration is correct.
    Define

      A SolarForecast Device is created with:

        define <name> SolarForecast

      After the definition of the device, depending on the forecast sources used, it is mandatory to store additional plant-specific information with the corresponding set commands.
      The following set commands and attributes are used to store information that is relevant for the function of the module:

        ctrlWeatherDevX DWD_OpenData Device which provides meteorological data (e.g. cloud cover)
        currentRadiationAPI DWD_OpenData Device or API for the delivery of radiation data.
        currentInverterDev Device which provides PV performance data
        currentMeterDev Device which supplies network I/O data
        currentBatteryDev Device which provides battery performance data (if available)
        inverterStrings Identifier of the existing plant strings
        moduleAzimuth Azimuth of the plant strings
        modulePeakString the DC peak power of the plant strings
        roofIdentPair the identification data (when using the SolCast API)
        moduleRoofTops the Rooftop parameters (when using the SolCast API)
        moduleDeclination the inclination angles of the plant modules

      In order to enable an adjustment to the personal system, correction factors can be manually fixed or automatically applied dynamically.

    Consumer Integration

      The user can register consumers (e.g. switchable sockets) directly in the module and let the SolarForecast module take over the planning of the on/off times as well as their execution. Registration is done using the ConsumerXX attributes. In addition to the FHEM consumer device, a number of mandatory or optional keys are specified in the attributes that influence the scheduling and switching behavior of the consumer.
      The keys are described in detail in the ConsumerXX help. In order to learn how to use the consumer control, it is advisable to first create one or more dummies and register these devices as consumers.

      A dummy device according to this pattern is suitable for this purpose:

        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
        attr SolCastDummy devStateIcon off:light_light_dim_100@grey on:light_light_dim_100@darkorange
        attr SolCastDummy group Solarforecast
        attr SolCastDummy icon solar_icon
        attr SolCastDummy nomPower 1000
        attr SolCastDummy readingList BatIn BatOut BatVal BatInTot BatOutTot bezW einW Batcharge Temp automatic
        attr SolCastDummy room Energy,Testroom
        attr SolCastDummy setList BatIn BatOut BatVal BatInTot BatOutTot bezW einW Batcharge on off Temp
        attr SolCastDummy userReadings actpow {ReadingsVal ($name, 'state', 'off') eq 'on' ? AttrVal ($name, 'nomPower', 100) : 0}


    Set
      • aiDecTree

        If AI support is enabled in the SolarForecast Device, various AI actions can be performed manually. The manual execution of the AI actions is generally not necessary, since the processing of all necessary steps is already performed automatically in the module.

          addInstances - The AI is enriched with the currently available PV, radiation and environmental data.
          addRawData - Relevant PV, radiation and environmental data are extracted and stored for later use.
          train - The AI is trained with the available data.
            Successfully generated decision data is stored in the file system.

      • batteryTrigger <1on>=<Value> <1off>=<Value> [<2on>=<Value> <2off>=<Value> ...]

        Generates triggers when the battery charge exceeds or falls below certain values (SoC in %).
        If the last three SoC measurements exceed a defined Xon-Bedingung, the reading batteryTrigger_X = on is created/set.
        If the last three SoC measurements fall below a defined Xoff-Bedingung, the reading batteryTrigger_X = off is created/set.
        Any number of trigger conditions can be specified. Xon/Xoff conditions do not necessarily have to be defined in pairs.

          Example:
          set <name> batteryTrigger 1on=30 1off=10 2on=70 2off=20 3on=15 4off=90

      • consumerNewPlanning <Consumer number>

        The existing planning of the specified consumer is deleted.
        The new planning is carried out immediately, taking into account the parameters set in the consumerXX attribute.

          Beispiel:
          set <name> consumerNewPlanning 01

      • consumerImmediatePlanning <Consumer number>

        Immediate switching on of the consumer at the current time is scheduled. Any keys notbefore, notafter respectively mode set in the consumerXX attribute are ignored

          Example:
          set <name> consumerImmediatePlanning 01

      • currentBatteryDev <Battery Device Name> pin=<Readingname>:<Unit> pout=<Readingname>:<Unit> [intotal=<Readingname>:<Unit>] [outtotal=<Readingname>:<Unit>] [cap=<Option>] [charge=<Readingname>]

        Specifies an arbitrary Device and its Readings to deliver the battery performance data. The module assumes that the numerical value of the readings is always positive. It can also be a dummy device with corresponding readings. The meaning of the respective "Readingname" is:

          pin Reading which provides the current battery charging power
          pout Reading which provides the current battery discharge rate
          intotal Reading which provides the total battery charge as a continuous counter (optional)
          outtotal Reading which provides the total battery discharge as a continuous counter (optional)
          cap installed battery capacity (optional). Option can be:
          numerical value - direct indication of the battery capacity in Wh
          <Readingname>:<unit> - Reading which provides the capacity and unit (Wh, kWh)
          charge Reading which provides the current state of charge (SOC in percent) (optional)
          Unit the respective unit (W,Wh,kW,kWh)

        Special cases: If the reading for pin and pout should be identical but signed, the keys pin and pout can be defined as follows:

          pin=-pout    (a negative value of pout is used as pin)
          pout=-pin    (a negative value of pin is used as pout)

        The unit is omitted in the particular special case.

          Example:
          set <name> currentBatteryDev BatDummy pin=BatVal:W pout=-pin intotal=BatInTot:Wh outtotal=BatOutTot:Wh cap=BatCap:kWh

          # Device BatDummy returns the current battery charge in the reading "BatVal" (W), the battery discharge in the same reading with negative sign,
          # the summary charge in the reading "intotal" (Wh), as well as the summary discharge in the reading "outtotal". (Wh)

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

        Specifies any Device and its Readings to deliver the current PV generation values. It can also be a dummy device with appropriate readings. The values of several inverter devices are merged e.g. in a dummy device and this device is specified with the corresponding readings.
        Specifying capacity is optional, but strongly recommended to optimize prediction accuracy.

          pv Reading which provides the current PV generation
          etotal Reading which provides the total PV energy generated (a steadily increasing counter).
          Einheit the respective unit (W,kW,Wh,kWh)
          capacity Rated power of the inverter according to data sheet (max. possible output in watts)

          Example:
          set <name> currentInverterDev STP5000 pv=total_pac:kW etotal=etotal:kWh capacity=5000

          # Device STP5000 provides PV values. The current generated power in the reading "total_pac" (kW) and the daily generation in the reading "etotal" (kWh). The maximum power of the inverter is 5000 Watt.

      • currentMeterDev <Meter Device Name> gcon=<Readingname>:<Unit> contotal=<Readingname>:<Unit> gfeedin=<Readingname>:<Unit> feedtotal=<Readingname>:<Unit> [conprice=<Field>] [feedprice=<Field>]

        Sets any device and its readings for energy measurement. The module assumes that the numeric value of the readings is positive. It can also be a dummy device with corresponding readings. The meaning of the respective "Readingname" is:

          gcon Reading which supplies the power currently drawn from the grid
          contotal Reading which provides the sum of the energy drawn from the grid (a constantly increasing meter)
          gfeedin Reading which supplies the power currently fed into the grid
          feedtotal Reading which provides the sum of the energy fed into the grid (a constantly increasing meter)
          Unit the respective unit (W,kW,Wh,kWh)
          conprice Price for the purchase of one kWh (optional). The <field> can be specified in one of the following variants:
          <Price>:<Currency> - Price as a numerical value and its currency
          <Reaging>:<Currency> - Reading of the meter device that contains the price : Currency
          <Device>:<Reaging>:<Currency> - any device and reading containing the price : Currency
          feedprice Remuneration for the feed-in of one kWh (optional). The <field> can be specified in one of the following variants:
          <Remuneration>:<Currency> - Remuneration as a numerical value and its currency
          <Reaging>:<Currency> - Reading of the meter device that contains the remuneration : Currency
          <Device>:<Reaging>:<Currency> - any device and reading containing the remuneration : Currency

        Special cases: If the reading for gcon and gfeedin should be identical but signed, the keys gfeedin and gcon can be defined as follows:

          gfeedin=-gcon    (a negative value of gcon is used as gfeedin)
          gcon=-gfeedin    (a negative value of gfeedin is used as gcon)

        The unit is omitted in the particular special case.

          Example:
          set <name> currentMeterDev Meter gcon=Wirkleistung:W contotal=BezWirkZaehler:kWh gfeedin=-gcon feedtotal=EinWirkZaehler:kWh conprice=powerCost:€ feedprice=0.1269:€

          # Device Meter provides the current grid reference in the reading "Wirkleistung" (W), the sum of the grid reference in the reading "BezWirkZaehler" (kWh), the current feed in "Wirkleistung" if "Wirkleistung" is negative, the sum of the feed in the reading "EinWirkZaehler". (kWh)

      • currentRadiationAPI

        Defines the source for the delivery of the solar radiation data. You can select a device of the type DWD_OpenData or an implemented API can be selected.
        Note: If OpenMeteoDWD-API is set in the 'ctrlWeatherDev1' attribute, no radiation data service other than OpenMeteoDWD-API can be selected.

        OpenMeteoDWD-API
        Open-Meteo is an open source weather API and offers free access for non-commercial purposes. No API key is required. Open-Meteo leverages a powerful combination of global (11 km) and mesoscale (1 km) weather models from esteemed national weather services. This API provides access to the renowned ICON weather models of the German Weather Service (DWD), which provide 15-minute data for short-term forecasts in Central Europe and global forecasts with a resolution of 11 km. The ICON model is a preferred choice for general weather forecast APIs when no other high-resolution weather models are available. The models DWD Icon D2, DWD Icon EU and DWD Icon Global models are merged into a seamless forecast. The comprehensive and clearly laid out API Documentation is available on the service's website.

        OpenMeteoDWDEnsemble-API
        This Open-Meteo API variant provides access to the DWD's global Ensemble Prediction System (EPS).
        The ensemble models ICON-D2-EPS, ICON-EU-EPS and ICON-EPS are seamlessly combined.
        Ensemble weather forecasts are a special type of forecasting method that takes into account the uncertainties in weather forecasting. They do this by running several simulations or models with slight differences in the starting conditions or settings. Each simulation, known as an ensemble member, represents a possible outcome of the weather. In this implementation, 40 ensemble members per weather feature are combined and the most probable result is used.

        OpenMeteoWorld-API
        As a variant of the Open Meteo service, the OpenMeteoWorld API provides the optimum forecast for a specific location worldwide. The OpenMeteoWorld API seamlessly combines weather models from well-known organizations such as NOAA (National Oceanic and Atmospheric Administration), DWD (German Weather Service), CMCC (Canadian) and ECMWF (European Centre for Medium-Range Weather Forecasts). The providers' models are combined for each location worldwide to produce the best possible forecast. The services and weather models are used automatically based on the location coordinates contained in the API call.

        SolCast-API
        API usage requires one or more API-keys (accounts) and one or more Rooftop-ID's in advance created on the SolCast website. A rooftop is equivalent to one inverterString in the SolarForecast context.
        Free API usage is limited to one daily rate API requests. The number of defined strings (rooftops) increases the number of API requests required. The module optimizes the query cycles with the attribute ctrlSolCastAPIoptimizeReq .

        ForecastSolar-API
        Free use of the Forecast.Solar API. does not require registration. API requests are limited to 12 within one hour in the free version. There is no daily limit. The module automatically determines the optimal query interval depending on the configured strings.

        VictronKI-API
        This API can be applied by users of the Victron Energy VRM Portal. This API is AI based. As string the value "AI-based" has to be entered in the setup of the inverterStrings.
        In the Victron Energy VRM Portal, the location of the PV system must be specified as a prerequisite.
        See also the blog post Introducing Solar Production Forecast.

        DWD_OpenData Device
        The DWD service is integrated via a FHEM device of type DWD_OpenData. If there is no device of type DWD_OpenData yet, it must be defined in advance (look at DWD_OpenData Commandref).
        To obtain a good radiation forecast, a DWD station located near the plant site should be used.
        Unfortunately, not all DWD stations provide the required Rad1h values.
        Explanations of the stations are listed in Stationslexikon.
        At least the following attributes must be set in the selected DWD_OpenData Device:

          forecastDays 1
          forecastProperties Rad1h
          forecastResolution 1
          forecastStation <Station code of the evaluated DWD station>
          Note: The selected DWD station must provide radiation values (Rad1h Readings).
          Not all stations provide this data!

      • energyH4Trigger <1on>=<Value> <1off>=<Value> [<2on>=<Value> <2off>=<Value> ...]

        Generates triggers on exceeding or falling below the 4-hour PV forecast (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. If the last three measurements of the 4-hour PV predictions exceed a defined Xon condition, the Reading energyH4Trigger_X = off is created/set.
        Any number of trigger conditions can be specified. Xon/Xoff conditions do not necessarily have to be defined in pairs.

          Example:
          set <name> energyH4Trigger 1on=2000 1off=1700 2on=2500 2off=2000 3off=1500

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

        Designations of the active strings. These names are used as keys in the further settings.
        When using an AI based API (e.g. VictronKI API) only "KI-based" has to be entered regardless of which real strings exist.

          Examples:
          set <name> inverterStrings eastroof,southgarage,S3
          set <name> inverterStrings KI-based

      • moduleAzimuth <Stringname1>=<dir> [<Stringname2>=<dir> <Stringname3>=<dir> ...]
        (only model DWD, ForecastSolarAPI)

        Alignment <dir> of the solar modules in the string "StringnameX". The string name is a key value of the inverterStrings reading.
        The direction specification <dir> can be specified as an azimuth identifier or as an azimuth value:

          IdentifierAzimuth
          N -180 North orientation
          NE -135 North-East orientation
          E -90 East orientation
          SE -45 South-east orientation
          S 0 South orientation
          SW 45 South-west orientation
          W 90 West orientation
          NW 135 North-West orientation

        Azimuth values are integers in the range -180 to 180. Azimuth intermediate values that do not exactly match an identifier are abstracted to the nearest identifier if the selected API works with identifiers only. The module uses the more accurate azimuth value if the API supports its use, e.g. the ForecastSolar API.

          Example:
          set <name> moduleAzimuth eastroof=-90 southgarage=S S3=NW

      • moduleDeclination <Stringname1>=<Angle> [<Stringname2>=<Angle> <Stringname3>=<Angle> ...]
        (only model DWD, ForecastSolarAPI)

        Tilt angle of the solar modules. The string name is a key value of the reading inverterStrings.
        Possible angles of inclination are: 0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90 (0 = horizontal, 90 = vertical).

          Example:
          set <name> moduleDeclination eastroof=40 southgarage=60 S3=30

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

        The DC peak power of the string "StringnameX" in kWp. The string name is a key value of the Reading inverterStrings.
        When using an AI-based API (e.g. Model VictronKiAPI), the peak powers of all existing strings are to be assigned as a sum to the string name KI-based.

          Examples:
          set <name> modulePeakString eastroof=5.1 southgarage=2.0 S3=7.2
          set <name> modulePeakString KI-based=14.3 (for AI based API)

      • moduleRoofTops <Stringname1>=<pk> [<Stringname2>=<pk> <Stringname3>=<pk> ...]
        (only when using Model SolCastAPI)

        The string "StringnameX" is assigned to a key <pk>. The key <pk> was created with the setter roofIdentPair. This is used to specify the rooftop ID and API key to be used in the SolCast API.
        The string nameX is a key value of the reading inverterStrings.

          Example:
          set <name> moduleRoofTops eastroof=p1 southgarage=p2 S3=p3

      • operatingMemory backup | save | recover-<File>

        The pvHistory (PVH) and pvCircular (PVC) components of the internal cache database are stored in the file system.
        The target directory is "../FHEM/FhemUtils". This process is carried out regularly by the module in the background.

          backup Saves the active in-memory structures with the current timestamp.
          ctrlBackupFilesKeep generations of the files are saved. Older versions are deleted.
          Files: PVH_SolarForecast_<name>_<Timestamp>, PVC_SolarForecast_<name>_<Timestamp>
          save The active in-memory structures are saved.
          Files: PVH_SolarForecast_<name>, PVC_SolarForecast_<name>
          recover-<File> Restores the data of the selected backup file as an active in-memory structure.
          To avoid inconsistencies, the PVH.* and PVC.* files should be restored in pairs
          with the same time stamp.


      • operationMode

        The SolarForecast device is deactivated with inactive. The active option reactivates the device. The behavior corresponds to the "disable" attribute, but is particularly suitable for use in Perl scripts as compared to the "disable" attribute, it is not necessary to save the device configuration.

      • plantConfiguration

        Depending on the selected command option, the following operations are performed:

          check Checks the current plant configuration. A plausibility check
          is performed and the result and any notes or errors are output.
          save Secures important parameters of the plant configuration.
          The operation is performed automatically every day shortly after 00:00.
          restore Restores a saved plant configuration

      • powerTrigger <1on>=<Value> <1off>=<Value> [<2on>=<Value> <2off>=<Value> ...]

        Generates triggers when certain PV generation values (Current_PV) are exceeded or not reached.
        If the last three measurements of PV generation exceed a defined Xon condition, the Reading powerTrigger_X = on is created/set. If the last three measurements of the PV generation fall below a defined Xoff-Bedingung, the Reading powerTrigger_X = off is created/set.
        Any number of trigger conditions can be specified. Xon/Xoff conditions do not necessarily have to be defined in pairs.

          Example:
          set <name> powerTrigger 1on=1000 1off=500 2on=2000 2off=1000 3on=1600 4off=1100

      • pvCorrectionFactor_Auto

        Switches the automatic prediction correction on/off. The mode of operation differs depending on the selected method. The correction behaviour can be influenced with the affectMaxDayVariance attribute.
        (default: off)

        on_simple(_ai):
        In this method, the hourly predicted amount of energy is compared with the real amount of energy generated and a correction factor used for the future for the respective hour. The forecast data provided by the selected API is not additionally related to other conditions such as cloud cover or temperatures.
        If the AI support is switched on (on_simple_ai) and a PV forecast value is supplied by the AI, this value is used instead of the API value.

        on_complex(_ai):
        In this method, the hourly predicted amount of energy is compared with the real amount of energy generated and a correction factor used for the future for the respective hour. The forecast data provided by the selected API is also additionally linked to other conditions such as cloud cover or temperatures.
        If AI support is switched on (on_complex_ai) and a PV forecast value is provided by the AI, this value is used instead of the API value.

        Note: The automatic prediction correction is learning and needs time to optimise the correction values. After activation, optimal predictions cannot be expected immediately!

        Below are some API-specific tips that are merely best practice recommendations.

        Model OpenMeteo...API:
        The recommended autocorrection method is on_complex or on_complex_ai.

        Model SolCastAPI:
        The recommended autocorrection method is on_complex.
        Before turning on autocorrection, optimise the forecast with the following steps:

        • In the RoofTop editor of the SolCast API, define the efficiency factor according to the age of the plant.
          With an 8-year-old plant, it would be 84 (100 - (8 x 2%)).
        • after sunset, the Reading Today_PVdeviation is created, which shows the deviation between the forecast and the real PV generation in percent.
        • according to the deviation, adjust the efficiency factor in steps until an optimum is found, i.e. the smallest daily deviation is found
        • If you think you have found the optimal setting, you can set pvCorrectionFactor_Auto on*.

        Ideally, this process is carried out in a phase of stable meteorological conditions (uniform sun or cloud cover). cloud cover).

        Model VictronKiAPI:
        This model is based on Victron Energy's AI-supported API. The recommended autocorrect method is on_complex.

        Model DWD:
        The recommended autocorrect method is on_complex or on_complex_ai.

        Model ForecastSolarAPI:
        The recommended autocorrect method is on_complex.

      • pvCorrectionFactor_XX <Zahl>

        Manual correction factor for hour XX of the day to adjust the forecast to the individual installation.
        (default: 1.0)

      • reset

        Deletes the data source selected from the drop-down list, readings belonging to the function or other internal data structures.

          aiData deletes an existing AI instance including all training data and reinitialises it
          batteryTriggerSet deletes the trigger points of the battery storage
          consumerPlanning deletes the planning data of all registered consumers
          To delete the planning data of only one consumer, use:
            set <name> reset consumerPlanning <Consumer number>
          The module carries out an automatic rescheduling of the consumer circuit.
          consumerMaster deletes the data of all registered consumers from the memory
          To delete the data of only one consumer use:
            set <name> reset consumerMaster <Consumer number>
          consumption deletes the stored consumption values of the house
          To delete the consumption values of a specific day:
            set <name> reset consumption <Day> (e.g. set <name> reset consumption 08)
          To delete the consumption values of a specific hour of a day:
            set <name> reset consumption <Day> <Hour> (e.g. set <name> reset consumption 08 10)
          currentBatterySet deletes the set battery device and corresponding data.
          currentInverterSet deletes the set inverter device and corresponding data.
          currentMeterSet deletes the set meter device and corresponding data.
          energyH4TriggerSet deletes the 4-hour energy trigger points
          inverterStringSet deletes the string configuration of the installation
          powerTriggerSet deletes the trigger points for PV generation values
          pvCorrection deletes the readings pvCorrectionFactor*
          To delete all previously stored PV correction factors from the caches:
            set <name> reset pvCorrection cached
          To delete stored PV correction factors of a certain hour from the caches:
            set <name> reset pvCorrection cached <Hour>
            (e.g. set <name> reset pvCorrection cached 10)
          pvHistory deletes the memory of all historical days (01 ... 31)
          To delete a specific historical day:
            set <name> reset pvHistory <Day> (e.g. set <name> reset pvHistory 08)
          To delete a specific hour of a historical day:
            set <name> reset pvHistory <Day> <Hour> (e.g. set <name> reset pvHistory 08 10)
          moduleRoofTopSet deletes the SolCast API Rooftops
          roofIdentPair deletes all saved SolCast API Rooftop ID / API Key pairs.
          To delete a specific pair, specify its key <pk>:
            set <name> reset roofIdentPair <pk> (e.g. set <name> reset roofIdentPair p1)

      • roofIdentPair <pk> rtid=<Rooftop-ID> apikey=<SolCast API Key>
        (only when using Model SolCastAPI)

        The retrieval of each rooftop created in SolCast Rooftop Sites is to be identified by specifying a pair Rooftop-ID and API-Key.
        The key <pk> uniquely identifies a linked Rooftop ID / API key pair. Any number of pairs can be created one after the other. In that case, a new name for "<pk>" is to be used in each case.

        The key <pk> is assigned in the setter moduleRoofTops to the Rooftops (=Strings) to be retrieved.

          Examples:
          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


      • vrmCredentials user=<Benutzer> pwd=<Paßwort> idsite=<idSite>
        (only when using Model VictronKiAPI)

        If the Victron VRM API is used, the required access data must be stored with this set command.

          user Username for the Victron VRM Portal
          pwd Password for access to the Victron VRM Portal
          idsite idSite is the identifier "XXXXXX" in the Victron VRM Portal Dashboard URL.
          URL of the Victron VRM Dashboard:
          https://vrm.victronenergy.com/installation/XXXXXX/dashboard

        To delete the stored credentials, only the argument delete must be passed to the command.

          Examples:
          set <name> vrmCredentials user=john@example.com pwd=somepassword idsite=212008
          set <name> vrmCredentials delete


    Get
      • data

        Starts data collection to determine the solar forecast and other values.

      • dwdCatalog

        The German Weather Service (DWD) provides a catalog of MOSMIX stations.
        The stations provide data whose meaning is explained in this Overview. The DWD distinguishes between MOSMIX_L and MOSMIX_S stations, which differ in terms of update frequency and data volume.
        This command reads the catalog into SolarForecast and saves it in the file ./FHEM/FhemUtils/DWDcat_SolarForecast.
        The catalog can be extensively filtered and saved in GPS Exchange Format (GPX). The latitude and logitude coordinates are displayed in decimal degrees.
        Regex expressions in the corresponding keys are used for filtering. The Regex is enclosed in ^...$ for evaluation.
        The following parameters can be specified. Without parameters, the entire catalog is output:

          byID The output is sorted by station ID. (default)
          byName The output is sorted by station name.
          force The latest version of the DWD station catalog is loaded into the system.
          exportgpx The (filtered) stations are saved in the file ./FHEM/FhemUtils/DWDcat_SolarForecast.gpx.
          This file can be displayed in the GPX viewer, for example.
          id=<Regex> Filtering is carried out according to station ID.
          name=<Regex> Filtering is carried out according to station name.
          lat=<Regex> Filtering is carried out according to latitude.
          lon=<Regex> Filtering is carried out according to longitude.

          Example:
          get <name> dwdCatalog byName exportgpx lat=(48|49|50|51|52)\..* lon=([5-9]|10|11|12|13|14|15)\..*
          # filters the stations largely to German locations beginning with "ST" and exports the data in GPS Exchange format

      • forecastQualities

        Shows the correction factors currently used to determine the PV forecast with the respective start time and the average forecast quality achieved so far for this period.

      • ftuiFramefiles

        SolarForecast provides widgets for FHEM Tablet UI v2 (FTUI2).
        If FTUI2 is installed on the system, the files for the framework can be loaded into the FTUI directory structure with this command.
        The setup and use of the widgets is described in Wiki SolarForecast FTUI Widget.

      • html

        The SolarForecast graphic is retrieved and displayed as HTML code.
        Note: By the attribute graphicHeaderOwnspec generated set or attribute commands in the user-specific area of the header are generally hidden for technical reasons.
        One of the following selections can be given as an argument to the command:

          both displays the header, consumer legend, energy flow graph and forecast graph (default)
          both_noHead displays the consumer legend, energy flow graph and forecast graph
          both_noCons displays the header, energy flow and prediction graphic
          both_noHead_noCons displays energy flow and prediction graphs
          flow displays the header, the consumer legend and energy flow graphic
          flow_noHead displays the consumer legend and the energy flow graph
          flow_noCons displays the header and the energy flow graph
          flow_noHead_noCons displays the energy flow graph
          forecast displays the header, the consumer legend and the forecast graphic
          forecast_noHead displays the consumer legend and the forecast graph
          forecast_noCons displays the header and the forecast graphic
          forecast_noHead_noCons displays the forecast graph
          none displays only the header and the consumer legend

        The graphic can be retrieved and embedded in your own code. This can be done in a simple way by defining a weblink device:

          define wl.SolCast5 weblink htmlCode { FHEM::SolarForecast::pageAsHtml ('SolCast5', '-', '<argument>') }

        'SolCast5' is the name of the SolarForecast device to be included. <argument> is one of the above described selection options.

      • nextHours

        Lists the expected values for the coming hours.

          aihit delivery status of the AI for the PV forecast (0-no delivery, 1-delivery)
          confc expected energy consumption including the shares of registered consumers
          confcEx expected energy consumption without the shares of registered consumers
          crange calculated cloud area
          correff correction factor/quality used
          factor/- -> no quality defined
          factor/0..1 - quality of the PV forecast (1 = best quality)
          DoN sunrise and sunset status (0 - night, 1 - day)
          hourofday current hour of the day
          pvapifc expected PV generation (Wh) of the used API incl. a possible correction
          pvaifc expected PV generation of the AI (Wh)
          pvfc PV generation forecast used (Wh)
          rad1h predicted global radiation
          starttime start time of the record
          sunaz Azimuth of the sun (in decimal degrees)
          sunalt Altitude of the sun (in decimal degrees)
          temp predicted outdoor temperature
          today has value '1' if start date on current day
          rr1c Total precipitation during the last hour kg/m2
          rrange range of total rain
          wid ID of the predicted weather
          wcc predicted degree of cloudiness

      • pvHistory

        Shows the content of the pvHistory data memory sorted by date and hour. The selection list can be used to jump to a specific day. The drop-down list contains the days currently available in the memory. Without an argument, the entire data storage is listed. The hour specifications refer to the respective hour of the day, e.g. the hour 09 refers to the time from 08 o'clock to 09 o'clock.

          batintotal total battery charge (Wh) at the beginning of the hour
          batin Hour battery charge (Wh)
          batouttotal total battery discharge (Wh) at the beginning of the hour
          batout Battery discharge of the hour (Wh)
          batmaxsoc maximum SOC (%) of the day
          batsetsoc optimum SOC setpoint (%) for the day
          confc expected energy consumption (Wh)
          con real energy consumption (Wh) of the house
          csmtXX total energy consumption of ConsumerXX
          csmeXX Energy consumption of ConsumerXX in the hour of the day (hour 99 = daily energy consumption)
          cyclescsmXX Number of active cycles of ConsumerXX of the day
          DoN Sunrise and sunset status (0 - night, 1 - day)
          etotal total energy yield (Wh) at the beginning of the hour
          gcon real power consumption (Wh) from the electricity grid
          gfeedin real feed-in (Wh) into the electricity grid
          hourscsmeXX average hours of an active cycle of ConsumerXX of the day
          minutescsmXX total active minutes in the hour of ConsumerXX
          pvfc the predicted PV yield (Wh)
          pvrl real PV generation (Wh)
          pvrlvd 1-'pvrl' is valid and is taken into account in the learning process, 0-'pvrl' is assessed as abnormal
          pvcorrf Autocorrection factor used / forecast quality achieved
          rad1h global radiation (kJ/m2)
          sunalt Altitude of the sun (in decimal degrees)
          sunaz Azimuth of the sun (in decimal degrees)
          wid Weather identification number
          wcc effective cloud cover
          rr1c Total precipitation during the last hour kg/m2

      • pvCircular

        Lists the existing values in the ring buffer. The hours 01 - 24 refer to the hour of the day, e.g. the hour 09 refers to the time from 08 - 09 o'clock.
        Hour 99 has a special function.
        The values of the keys pvcorrf, quality, pvrlsum, pvfcsum and dnumsum are coded in the form <range sun elevation>.<cloud cover range>.
        Explanation of the values:

          aihit Delivery status of the AI for the PV forecast (0-no delivery, 1-delivery)
          batin Battery charge (Wh)
          batout Battery discharge (Wh)
          batouttot total energy drawn from the battery (Wh)
          batintot total energy charged into the battery (Wh)
          confc expected energy consumption (Wh)
          days2care remaining days until the battery maintenance SoC (default 95%) is reached
          dnumsum Number of days per cloudy area over the entire term
          feedintotal total PV energy fed into the public grid (Wh)
          gcon real power drawn from the electricity grid
          gfeedin real power feed-in to the electricity grid
          gridcontotal total energy drawn from the public grid (Wh)
          initdayfeedin initial PV feed-in value at the beginning of the current day (Wh)
          initdaygcon initial grid reference value at the beginning of the current day (Wh)
          initdaybatintot initial value of the total energy charged into the battery at the beginning of the current day. (Wh)
          initdaybatouttot initial value of the total energy drawn from the battery at the beginning of the current day. (Wh)
          lastTsMaxSocRchd Timestamp of last achievement of battery SoC >= maxSoC (default 95%)
          nextTsMaxSocChge Timestamp by which the battery should reach maxSoC at least once
          pvapifc expected PV generation (Wh) of the API used
          pvaifc PV forecast (Wh) of the AI for the next 24h from the current hour of the day
          pvfc PV forecast used for the next 24h from the current hour of the day
          pvcorrf Autocorrection factors for the hour of the day, where 'simple' is the simple correction factor.
          pvfcsum summary PV forecast per cloud area over the entire term
          pvrl real PV generation of the last 24h (Attention: pvforecast and pvreal do not refer to the same period!)
          pvrlsum summary real PV generation per cloud area over the entire term
          quality Quality of the autocorrection factors (0..1), where 'simple' is the quality of the simple correction factor.
          runTimeTrainAI Duration of the last AI training
          aitrainLastFinishTs Timestamp of the last successful AI training
          tdayDvtn Today's deviation PV forecast/generation in %
          temp Outdoor temperature
          wcc Degree of cloud cover
          rr1c Total precipitation during the last hour kg/m2
          wid ID of the predicted weather
          wtxt Description of the predicted weather
          ydayDvtn Deviation PV forecast/generation in % on the previous day

      • rooftopData

        The expected solar radiation data or PV generation data are retrieved from the selected API.

      • solApiData

        Lists the data stored in the context of the API call. Administrative records are marked with a leading '? The predicted global irradiation and PV yield (Wh) data provided by the API is consolidated to one hour. The available characteristic values differ depending on the API used.

          currentAPIinterval the currently used API retrieval interval in seconds
          lastretrieval_time Time of the last API call
          lastretrieval_timestamp Unix timestamp of the last API call
          Rad1h if available, expected Global Irradiation (GI) or Global tilted Irradiation (GTI) in kJ/m2
          pv_estimate expected PV generation (Wh)
          KI-based_co expected Energy consumption (Wh)
          todayDoneAPIrequests Number of executed API requests on the current day
          todayRemainingAPIrequests Number of remaining SolCast API requests on the current day
          todayDoneAPIcalls Number of executed API calls on the current day
          todayRemainingAPIcalls Number of SolCast API calls still possible on the current day
          (one call can execute several SolCast API requests)
          todayMaxAPIcalls Maximum number of SolCast API calls per day

      • valConsumerMaster

        Shows the data of the consumers currently registered in the SolarForecast Device.
        The drop-down list can be used to jump to a specific consumer. The drop-down list contains the consumers or consumer numbers currently available in the data memory. Without an argument, the entire data memory is listed.

      • valCurrent

        Lists current operating data, key figures and status.

      • valDecTree

        If AI support is activated in the SolarForecast Device, various AI-relevant data can be displayed :

          aiRawData The PV, radiation and environmental data currently stored for the AI.
          aiRuleStrings Returns a list that describes the AI's decision tree in the form of rules.
          Note: While the order of the rules is not predictable, the
          order of criteria within each rule, however, reflects the order
          in which the criteria are considered in the decision-making process.


    Attribute

      • affect70percentRule
        If set, the predicted power is limited according to the 70% rule.

          0 No limit on the forecast PV generation (default)
          1 the predicted PV generation is limited to 70% of the installed string power(s)
          dynamic the predicted PV generation is limited when 70% of the installed
          string(s) power plus the predicted consumption is exceeded.

      • affectBatteryPreferredCharge
        Consumers with the can mode are only switched on when the specified battery charge (%) is reached.
        Consumers with the must mode do not observe the priority charging of the battery.
        (default: 0)

      • affectConsForecastInPlanning
        If set, the consumption forecast is also taken into account in addition to the PV forecast when scheduling the consumer.
        Standard consumer planning is based on the PV forecast only.
        (default: 0)

      • affectConsForecastIdentWeekdays
        If set, only the same weekdays (Mon..Sun) are included in the calculation of the consumption forecast.
        Otherwise, all weekdays are used equally for calculation.
        (default: 0)

      • affectMaxDayVariance <Zahl>
        Maximum adjustment of the PV prediction factor (Reading pvCorrectionFactor_XX) that can be made in relation to one hour per day.
        This setting has no influence on the learning and forecasting behavior of any AI support used (pvCorrectionFactor_Auto).
        (default: 0.8)

      • affectSolCastPercentile <10 | 50 | 90>
        (only when using Model SolCastAPI)

        Selection of the probability range of the delivered SolCast data. SolCast provides the 10 and 90 percent probability around the forecast mean (50).
        (default: 50)

      • alias
        In connection with "ctrlShowLink" any display name.

      • consumerAdviceIcon
        Defines the type of information about the planned switching times of a consumer in the consumer legend.

          <Icon>@<Colour> Activation recommendation is represented by icon and colour (optional) (default: light_light_dim_100@gold)
          (the planning data is displayed as mouse-over text)
          times the planning status and the planned switching times are displayed as text
          none no display of the planning data

      • consumerLegend
        Defines the position or display mode of the load legend if loads are registered in the SolarForecast Device.
        (default: icon_top)

      • consumerLink
        If set, you can click on the respective consumer in the consumer list (consumerLegend) and get directly to the detailed view of the respective device on a new browser page.
        (default: 1)

      • consumerXX <Device Name> type=<type> power=<power> [switchdev=<device>]
        [mode=<mode>] [icon=<Icon>] [mintime=<minutes> | SunPath[:<Offset_Sunrise>:<Offset_Sunset>]]
        [on=<command>] [off=<command>] [swstate=<Readingname>:<on-Regex>:<off-Regex>] [asynchron=<Option>]
        [notbefore=<Expression>] [notafter=<Expression>] [locktime=<offlt>[:<onlt>]]
        [auto=<Readingname>] [pcurr=<Readingname>:<Unit>[:<Threshold>]] [etotal=<Readingname>:<Einheit>[:<Threshold>]]
        [swoncond=<Device>:<Reading>:<Regex>] [swoffcond=<Device>:<Reading>:<Regex>] [spignorecond=<Device>:<Reading>:<Regex>]
        [interruptable=<Option>] [noshow=<Option>]


        Registers a consumer <Device Name> with the SolarForecast Device. In this case, <Device Name> is a consumer device already created in FHEM, e.g. a switchable socket. Most of the keys are optional, but are a prerequisite for certain functionalities and are filled with default values.
        If the dish is defined "auto", the automatic mode in the integrated consumer graphic can be switched with the corresponding push-buttons. If necessary, the specified reading is created in the consumer device if it is not available.

        With the optional key swoncond, an additional external condition can be defined to enable the switch-on process of the consumer. If the condition (Regex) is not fulfilled, the load is not switched on, even if the other conditions such as other conditions such as scheduling, on key, auto mode and current PV power are fulfilled. Thus, there is an AND-link of the key swoncond with the further switch-on conditions.

        The optional key swoffcond defines a priority switch-off condition (Regex). As soon as this condition is fulfilled, the consumer is switched off even if the planned end time (consumerXX_planned_stop) has not yet been reached (OR link). Further conditions such as off key and auto mode must be be fulfilled for automatic switch-off.

        With the optional interruptable key, an automatic interruption and reconnection of the consumer during the planned switch-on time. The load is temporarily switched off (interrupted) and switched on again (continued) when the interrupt condition is no longer present. The remaining runtime is not affected by an interrupt!

        The power key indicates the nominal power consumption of the consumer according to its data sheet. This value is used to schedule the switching times of the load and to control the switching depending on the actual PV surplus at the time of scheduling. This value is used to schedule the switching times of the load and to control the switching depending on the actual PV surplus at the time of scheduling.

          .
          type Type of consumer. The following types are allowed:
          dishwasher - Consumer is a dishwasher
          dryer - Consumer is a tumble dryer
          washingmachine - Consumer is a washing machine
          heater - Consumer is a heating rod
          charger - Consumer is a charging device (battery, car, bicycle, etc.)
          other - Consumer is none of the above types
          noSchedule - there is no scheduling or automatic switching for the consumer.
                                 Display functions or manual switching are available.
          power nominal power consumption of the consumer (see data sheet) in W
          (can be set to "0")
          switchdev The specified <device> is assigned to the consumer as a switch device (optional). Switching operations are performed with this device.
          The key is useful for consumers where energy measurement and switching is carried out with different devices
          e.g. Homematic or readingsProxy. If switchdev is specified, the keys on, off, swstate, auto, asynchronous refer to this device.
          mode Consumer planning mode (optional). Allowed are:
          can - Scheduling takes place at the time when there is probably enough PV surplus available (default).
                   The consumer is not started at the time of planning if the PV surplus is insufficient.
          must - The consumer is optimally planned, even if there will probably not be enough PV surplus.
                     The load is started even if there is insufficient PV surplus, provided that a set "swoncond" condition is met and "swoffcond" is not met.
          icon Icon to represent the consumer in the overview graphic (optional)
          mintime Scheduling duration (minutes or "SunPath") of the consumer. (optional)
          By specifying SunPath, planning is done according to sunrise and sunset.
          SunPath[:<Offset_Sunrise>:<Offset_Sunset>] - scheduling takes place from sunrise to sunset.
          Optionally, a positive / negative shift (minutes) of the planning time regarding sunrise or sunset can be specified.
          If mintime is not specified, a standard scheduling duration according to the following table is used.
          Default mintime by consumer type:
          - dishwasher: 180 minutes
          - dryer: 90 minutes
          - washingmachine: 120 minutes
          - heater: 240 minutes
          - charger: 120 minutes
          - other: 60 minutes
          on Set command for switching on the consumer (optional)
          off Set command for switching off the consumer (optional)
          swstate Reading which indicates the switching status of the consumer (default: 'state').
          on-Regex - regular expression for the state 'on' (default: 'on')
          off-Regex - regular expression for the state 'off' (default: 'off')
          asynchron the type of switching status determination in the consumer device. The status of the consumer is only determined after a switching command
          by polling within a data collection interval (synchronous) or additionally by event processing (asynchronous).
          0 - only synchronous processing of switching states (default)
          1 - additional asynchronous processing of switching states through event processing
          notbefore Schedule start time consumer not before specified time 'hour[:minute]' (optional)
          The <Expression> has the format hh[:mm] or is Perl code enclosed in {...} that returns hh[:mm].
          notafter Schedule start time consumer not after specified time 'hour[:minute]' (optional)
          The <Expression> has the format hh[:mm] or is Perl code enclosed in {...} that returns hh[:mm].
          auto Reading in the consumer device which enables or blocks the switching of the consumer (optional)
          If the key switchdev is given, the reading is set and evaluated in this device.
          Reading value = 1 - switching enabled (default), 0: switching blocked
          pcurr Reading:Unit (W/kW) which provides the current energy consumption (optional)
          :<Threshold> (W) - From this power reference on, the consumer is considered active. The specification is optional (default: 0)
          etotal Reading:Unit (Wh/kWh) of the consumer device that supplies the sum of the consumed energy (optional)
          :<Threshold> (Wh) - From this energy consumption per hour, the consumption is considered valid. Optional specification (default: 0)
          swoncond Condition that must also be fulfilled in order to switch on the consumer (optional). The scheduled cycle is started.
          Device - Device to supply the additional switch-on condition
          Reading - Reading for delivery of the additional switch-on condition
          Regex - regular expression that must be satisfied for a 'true' condition to be true
          swoffcond priority condition to switch off the consumer (optional). The scheduled cycle is stopped.
          Device - Device to supply the priority switch-off condition
          Reading - Reading for the delivery of the priority switch-off condition
          Regex - regular expression that must be satisfied for a 'true' condition to be true
          spignorecond Condition to ignore a missing PV surplus (optional). If the condition is fulfilled, the load is switched on according to
          the planning even if there is no PV surplus at the time.
          CAUTION: Using both keys spignorecond and interruptable can lead to undesired behaviour!
          Device - Device to deliver the condition
          Reading - Reading which contains the condition
          Regex - regular expression that must be satisfied for a 'true' condition to be true
          interruptable defines the possible interruption options for the consumer after it has been started (optional)
          0 - Load is not temporarily switched off even if the PV surplus falls below the required energy (default)
          1 - Load is temporarily switched off if the PV surplus falls below the required energy
          Device:Reading:Regex[:Hysteresis] - Load is temporarily interrupted if the value of the specified
          Device:Readings match on the regex or if is insufficient PV surplus (if power not equal to 0).
          If the value no longer matches, the interrupted load is switched on again if there is sufficient
          PV surplus provided (if power is not 0).
          If the optional hysteresis is specified, the hysteresis value is subtracted from the reading value and the regex is then applied.
          If this and the original reading value match, the consumer is temporarily interrupted.
          The consumer is continued if both the original and the subtracted readings value do not (or no longer) match.
          locktime Blocking times in seconds for switching the consumer (optional).
          offlt - Blocking time in seconds after the consumer has been switched off or interrupted
          onlt - Blocking time in seconds after the consumer has been switched on or continued
          The consumer is only switched again when the corresponding blocking time has elapsed.
          Note: The 'locktime' switch is only effective in automatic mode.
          noshow Hide or show consumers in graphic (optional).
          0 - the consumer is displayed (default)
          1 - the consumer is hidden
          2 - the consumer is hidden in the consumer legend
          3 - the consumer is hidden in the flow chart
          [Device:]Reading - Reading in the consumer or optionally an alternative device.
          If the reading has the value 0 or is not present, the consumer is displayed.
          The effect of the possible reading values 1, 2 and 3 is as described.

          Examples:
          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
          attr <name> consumer05 Shelly.shellyplug4 icon=sani_buffer_electric_heater_side type=heater mode=must power=1000 notbefore=7 notafter=20:10 auto=automatic pcurr=actpow:W on=on off=off mintime=SunPath interruptable=1
          attr <name> consumer06 Shelly.shellyplug5 icon=sani_buffer_electric_heater_side type=heater mode=must power=1000 notbefore=07:05 notafter={return'20:05'} auto=automatic pcurr=actpow:W on=on off=off mintime=SunPath:60:-120 interruptable=1
          attr <name> consumer07 SolCastDummy icon=sani_buffer_electric_heater_side type=heater mode=can power=600 auto=automatic pcurr=actpow:W on=on off=off mintime=15 asynchron=1 locktime=300:1200 interruptable=1 noshow=noShow

      • ctrlAIdataStorageDuration <Tage>
        If the corresponding prerequisites are met, training data is collected and stored for the module-internal AI.
        The data is deleted when it has exceeded the specified holding period (days).
        (default: 1095)

      • ctrlAIshiftTrainStart <1...23>
        Daily training takes place when using the internal AI.
        Training begins approx. 15 minutes after the hour specified in the attribute.
        For example, with a set value of '3', training would start at around 03:15.
        (default: 2)

      • ctrlAutoRefresh
        If set, active browser pages of the FHEMWEB device that has called up the SolarForecast device are reloaded after the set time (seconds). If browser pages of a certain FHEMWEB device are to be reloaded instead, this device can be specified with the attribute "ctrlAutoRefreshFW".

      • ctrlAutoRefreshFW
        If "ctrlAutoRefresh" is activated, this attribute can be used to determine the FHEMWEB device whose active browser pages should be regularly reloaded.

      • ctrlBackupFilesKeep <Integer>
        Defines the number of generations of backup files (see also set <name> operatingMemory backup).
        (default: 3)

      • ctrlBatSocManagement lowSoc=<Value> upSoC=<Value> [maxSoC=<Value>] [careCycle=<Value>]

        If a battery device (currentBatteryDev) is installed, this attribute activates the battery SoC management.
        The Battery_OptimumTargetSoC reading contains the optimum minimum SoC calculated by the module.
        The Battery_ChargeRequest reading is set to '1' if the current SoC has fallen below the minimum SoC.
        In this case, the battery should be forcibly charged, possibly with mains power.
        The readings can be used to control the SoC (State of Charge) and to control the charging current used for the battery.
        The module itself does not control the battery.

          lowSoc lower minimum SoC, the battery is not discharged lower than this value (> 0)
          upSoC upper minimum SoC, the usual value of the optimum SoC is between 'lowSoC'
          and this value.
          maxSoC Maximum minimum SoC, SoC value that must be reached at least every 'careCycle' days
          in order to balance the charge in the storage network.
          The specification is optional (<= 100, default: 95)
          careCycle Maximum interval in days that may occur between two states of charge
          of at least 'maxSoC'. The specification is optional (default: 20)

        All values are whole numbers in %. The following applies: 'lowSoc' < 'upSoC' < 'maxSoC'.
        The optimum SoC is determined according to the following scheme:

        1. Starting from 'lowSoc', the minimum SoC is increased by 5% on the following day but not higher than
        'upSoC', if 'maxSoC' has not been reached on the current day.
        2. If 'maxSoC' is reached (again) on the current day, the minimum SoC is reduced by 5%, but not lower than 'lowSoc'.
        3. Minimum SoC is reduced so that the predicted PV energy of the current or following day
        can be absorbed by the battery. Minimum SoC is not reduced lower than 'lowSoc'.
        4. The module records the last point in time at the 'maxSoC' level in order to ensure a charge to 'maxSoC'
        at least every 'careCycle' days. For this purpose, the optimized SoC is changed depending on the remaining days
        until the next 'careCycle' point in such a way that 'maxSoC' is mathematically achieved by a daily 5% SoC increase
        at the 'careCycle' time point. If 'maxSoC' is reached in the meantime, the 'careCycle' period starts again.

          Example:
          attr <name> ctrlBatSocManagement lowSoc=10 upSoC=50 maxSoC=99 careCycle=25

      • ctrlConsRecommendReadings
        Readings of the form consumerXX_ConsumptionRecommended are created for the selected consumers (number).
        These readings indicate whether it is recommended to switch on this consumer depending on its consumption data and the current PV generation or the current energy surplus. The value of the reading created correlates with the calculated planning data of the consumer, but may deviate from the planning period.

      • ctrlDebug
        Enables/disables various debug modules. If only "none" is selected, there is no DEBUG output. For the output of debug messages the verbose level of the device must be at least "1".
        The debug level can be combined with each other:

          aiProcess Data enrichment and training process for AI support
          aiData Data use AI in the forecasting process
          apiCall Retrieval API interface without data output
          apiProcess API data retrieval and processing
          batteryManagement Battery management control values (SoC)
          collectData detailed data collection
          consumerPlanning Consumer scheduling processes
          consumerSwitching Operations of the internal consumer switching module
          consumption Consumption calculation and use
          dwdComm Communication with the website or server of the German Weather Service (DWD)
          epiecesCalc Calculation of specific energy consumption per operating hour and consumer
          graphic Module graphic information
          notifyHandling Sequence of event processing in the module
          pvCorrectionRead Application of PV correction factors
          pvCorrectionWrite Calculation of PV correction factors
          radiationProcess Collection and processing of solar radiation data
          saveData2Cache Data storage in internal memory structures

      • ctrlGenPVdeviation
        Specifies the method for calculating the deviation between predicted and real PV generation. The Reading Today_PVdeviation is created depending on this setting.

          daily Calculation and creation of Today_PVdeviation is done after sunset (default)
          continuously Calculation and creation of Today_PVdeviation is done continuously

      • ctrlInterval <Sekunden>
        Repetition interval of the data collection.
        Regardless of the set interval, data is collected automatically a few seconds before the end and after the start of a full hour.
        If ctrlInterval is explicitly set to "0", no automatic data collection takes place and must be carried out externally with "get <name> data".
        (default: 70)

      • ctrlLanguage <DE | EN>
        Defines the used language of the device. The language definition has an effect on the module graphics and various reading contents.
        If the attribute is not set, the language is defined by setting the global attribute "language".
        (default: EN)

      • ctrlNextDayForecastReadings <01,02,..,24>
        If set, readings of the form Tomorrow_Hour<hour>_PVforecast are created.
        These readings contain the expected PV generation of the coming day. Here <hour> is the hour of the day.

          Example:
          attr <name> ctrlNextDayForecastReadings 09,11
          # creates readings for hour 09 (08:00-09:00) and 11 (10:00-11:00) of the coming day

      • ctrlShowLink
        Display of the link to the detailed view of the device above the graphic area.
        (default: 1)

      • ctrlSolCastAPImaxReq
        (only when using Model SolCastAPI)

        The setting of the maximum possible daily requests to the SolCast API.
        This value is specified by SolCast and may change according to the SolCast license model.
        (default: 50)

      • ctrlSolCastAPIoptimizeReq
        (only when using Model SolCastAPI)

        The default retrieval interval of the SolCast API is 1 hour. If this attribute is set, the interval is dynamically adjustment of the interval with the goal to use the maximum possible fetches within sunrise and sunset.
        (default: 0)

      • ctrlStatisticReadings
        Readings are created for the selected key figures and indicators with the naming scheme 'statistic_<indicator>'. Selectable key figures / indicators are:

          allStringsFullfilled Fulfillment status of error-free generation of all strings
          conForecastTillNextSunrise Consumption forecast from current hour to the coming sunrise
          currentAPIinterval the current call interval of the SolCast API (only model SolCastAPI) in seconds
          currentRunMtsConsumer_XX the running time (minutes) of the consumer "XX" since the last switch-on. (0 - consumer is off)
          dayAfterTomorrowPVforecast provides the forecast of PV generation for the day after tomorrow (if available) without autocorrection (raw data)
          daysUntilBatteryCare Days until the next battery maintenance (reaching the charge 'maxSoC' from attribute ctrlBatSocManagement)
          lastretrieval_time the last call time of the API (only Model SolCastAPI, ForecastSolarAPI)
          lastretrieval_timestamp the timestamp of the last call time of the API (only Model SolCastAPI, ForecastSolarAPI)
          response_message the last status message of the API (only Model SolCastAPI, ForecastSolarAPI)
          runTimeCentralTask the runtime of the last SolarForecast interval (total process) in seconds
          runTimeTrainAI the runtime of the last AI training cycle in seconds
          runTimeLastAPIAnswer the last response time of the API call to a request in seconds (only model SolCastAPI, ForecastSolarAPI)
          runTimeLastAPIProc the last process time for processing the received API data (only model SolCastAPI, ForecastSolarAPI)
          SunMinutes_Remain the remaining minutes until sunset of the current day
          SunHours_Remain the remaining hours until sunset of the current day
          todayConsumptionForecast Consumption forecast per hour of the current day (01-24)
          todayConForecastTillSunset Consumption forecast from current hour to hour before sunset
          todayDoneAPIcalls the number of API calls executed on the current day (only model SolCastAPI, ForecastSolarAPI)
          todayDoneAPIrequests the number of API requests executed on the current day (only model SolCastAPI, ForecastSolarAPI)
          todayGridConsumption the energy drawn from the public grid on the current day
          todayGridFeedIn PV energy fed into the public grid on the current day
          todayMaxAPIcalls the maximum possible number of SolCast API calls (SolCastAPI model only).
          A call can contain multiple API requests.
          todayRemainingAPIcalls the number of SolCast API calls still possible on the current day (only model SolCastAPI)
          todayRemainingAPIrequests the number of SolCast API requests still possible on the current day (only model SolCastAPI)
          todayBatIn the energy charged into the battery on the current day
          todayBatOut the energy taken from the battery on the current day


      • ctrlUserExitFn {<Code>}
        After each cycle (see the ctrlInterval attribute), the code given in this attribute is executed. The code is to be enclosed in curly brackets {...}.
        The code is passed the variables $name and $hash, which contain the name of the SolarForecast device and its hash.
        In the SolarForecast Device, readings can be created and modified using the storeReading function.

          Beispiel:
          {
          my $batdev = (split " ", ReadingsVal ($name, 'currentBatteryDev', ''))[0];
          my $pvfc = ReadingsNum ($name, 'RestOfDayPVforecast', 0);
          my $cofc = ReadingsNum ($name, 'RestOfDayConsumptionForecast', 0);
          my $diff = $pvfc - $cofc;

          storeReading ('userFn_Battery_device', $batdev);
          storeReading ('userFn_estimated_surplus', $diff);
          }

      • ctrlWeatherDevX

        Specifies the device or API for providing the required weather data (cloud cover, precipitation, etc.).
        The attribute 'ctrlWeatherDev1' specifies the leading weather service and is mandatory.
        If an Open-Meteo API is selected in the 'ctrlWeatherDev1' attribute, this Open-Meteo service is automatically set as the source of the radiation data (Setter currentRadiationAPI).

        OpenMeteoDWD-API
        Open-Meteo is an open source weather API and offers free access for non-commercial purposes. No API key is required. Open-Meteo leverages a powerful combination of global (11 km) and mesoscale (1 km) weather models from esteemed national weather services. This API provides access to the renowned ICON weather models of the German Weather Service (DWD), which provide 15-minute data for short-term forecasts in Central Europe and global forecasts with a resolution of 11 km. The ICON model is a preferred choice for general weather forecast APIs when no other high-resolution weather models are available. The models DWD Icon D2, DWD Icon EU and DWD Icon Global models are merged into a seamless forecast. The comprehensive and clearly laid out API Documentation is available on the service's website.

        OpenMeteoDWDEnsemble-API
        This Open-Meteo API variant provides access to the DWD's global Ensemble Prediction System (EPS).
        The ensemble models ICON-D2-EPS, ICON-EU-EPS and ICON-EPS are seamlessly combined.
        Ensemble weather forecasts are a special type of forecasting method that takes into account the uncertainties in weather forecasting. They do this by running several simulations or models with slight differences in the starting conditions or settings. Each simulation, known as an ensemble member, represents a possible outcome of the weather. In this implementation, 40 ensemble members per weather feature are combined and the most probable result is used.

        OpenMeteoWorld-API
        As a variant of the Open Meteo service, the OpenMeteoWorld API provides the optimum forecast for a specific location worldwide. The OpenMeteoWorld API seamlessly combines weather models from well-known organizations such as NOAA (National Oceanic and Atmospheric Administration), DWD (German Weather Service), CMCC (Canadian) and ECMWF (European Centre for Medium-Range Weather Forecasts). The providers' models are combined for each location worldwide to produce the best possible forecast. The services and weather models are used automatically based on the location coordinates contained in the API call.

        DWD Device
        As an alternative to Open-Meteo, an FHEM 'DWD_OpenData' device can be used to supply the weather data.
        If no device of this type exists, at least one DWD_OpenData device must first be defined. (see DWD_OpenData Commandref).
        If more than one ctrlWeatherDevX is specified, the average of all weather stations is determined if the respective value was supplied and is numerical.
        Otherwise, the data from 'ctrlWeatherDev1' is always used as the leading weather device.
        At least these attributes must be set in the selected DWD_OpenData Device:

          forecastDays 1
          forecastProperties TTT,Neff,RR1c,ww,SunUp,SunRise,SunSet
          forecastResolution 1
          forecastStation <Station code of the evaluated DWD station>

        Note: If the latitude and longitude attributes are set in the global device, the sunrise and sunset result from this information.

      • flowGraphicCss
        Defines the style for the energy flow graph. The attribute is automatically preset. To change the flowGraphicCss attribute, please accept the default and adjust it:

          .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; }

      • flowGraphicAnimate
        Animates the energy flow graph if displayed. Siehe auch Attribut graphicSelect.
        (default: 0)

      • flowGraphicConsumerDistance
        Controls the spacing between consumer icons in the energy flow graph if displayed. Siehe auch Attribut flowGraphicShowConsumer.
        (default: 130)

      • flowGraphicShowConsumer
        Suppresses the display of loads in the energy flow graph when set to "0".
        (default: 1)

      • flowGraphicShowConsumerDummy
        Shows or suppresses the dummy consumer in the energy flow graph.
        The dummy consumer is assigned the energy consumption that could not be assigned to other consumers.
        (default: 1)

      • flowGraphicShowConsumerPower
        Shows or suppresses the energy consumption of the loads in the energy flow graph.
        (default: 1)

      • flowGraphicShowConsumerRemainTime
        Shows or suppresses the remaining time (in minutes) of the loads in the energy flow graph.
        (default: 1)

      • flowGraphicSize <Pixel>
        Size of the energy flow graph if displayed. Siehe auch Attribut graphicSelect.
        (default: 400)

      • graphicBeam1Color
        Color selection of the primary bars.

      • graphicBeam1FontColor
        Selection of the font color of the primary bar.
        (default: 0D0D0D)

      • graphicBeam1Content
        Defines the content of the primary bars to be displayed.
          pvReal real PV generation (default)
          pvForecast Forecast PV generation
          gridconsumption Energy purchase from the grid
          consumptionForecast predicted energy consumption

      • graphicBeam1MaxVal <0...val>
        Setting the maximum amount of the primary bar (hourly value) to calculate the maximum bar height. This results in an adjustment of the total allowed height of the graphic.
        With the value "0" a dynamic adjustment takes place.
        (default: 0)

      • graphicBeam2Color
        Color selection of the secondary bars. The second color is only useful for the display device type "pvco" and "diff".

      • graphicBeam2FontColor
        Selection of the font color of the secondary bar.
        (default: 000000)

      • graphicBeam2Content
        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>
        Height of the bars in px and thus determination of the total height. In connection with "graphicHourCount" it is possible to create quite small graphic outputs.
        (default: 200)

      • graphicBeamWidth <value>
        Width of the bars of the bar chart in px. If no attribute is set, the width of the bars is determined by the module automatically.

      • graphicEnergyUnit <Wh | kWh>
        Defines the unit for displaying the electrical power in the graph. The kilowatt hour is rounded to one decimal place.
        (default: Wh)

      • graphicHeaderDetail
        Selection of the zones of the graphic header to be displayed.
        (default: all)
          all all zones of the head area (default)
          co show consumption range
          pv show creation area
          own user zone (see graphicHeaderOwnspec)
          status status information area

      • graphicHeaderOwnspec <Label>:<Reading>[@Device] <Label>:<Set>[@Device] <Label>:<Attr>[@Device] ...
        Display of any readings, set commands and attributes of the device in the graphic header.
        Readings, set commands and attributes of other devices can be displayed by specifying the optional [@Device].
        The values to be displayed are separated by spaces. Four values (fields) are displayed per line.
        The input can be made in multiple lines. Values with the units "Wh" or "kWh" are converted according to the setting of the attribute graphicEnergyUnit.

        Each value is to be defined by a label and the corresponding reading connected by ":".
        Spaces in the label are to be inserted by "&nbsp;", a line break by "<br>".
        An empty field in a line is created by ":".
        A line title can be inserted by specifying "#:<Text>", an empty title by entering "#".

          Example:
          attr <name> graphicHeaderOwnspec #
          AutarkyRate:Current_AutarkyRate
          Surplus:Current_Surplus
          current&nbsp;Gridconsumption:Current_GridConsumption
          :
          #
          CO&nbsp;until&nbsp;sunset:statistic_todayConForecastTillSunset
          PV&nbsp;Day&nbsp;after&nbsp;tomorrow:statistic_dayAfterTomorrowPVforecast
          :
          :
          #Battery
          in&nbsp;today:statistic_todayBatIn
          out&nbsp;today:statistic_todayBatOut
          :
          :
          #Settings
          Autocorrection:pvCorrectionFactor_Auto : : :
          Consumer<br>Replanning:consumerNewPlanning : : :
          Consumer<br>Quickstart:consumerImmediatePlanning : : :
          Weather:graphicShowWeather : : :
          History:graphicHistoryHour : : :
          GraphicSize:flowGraphicSize : : :
          ShowNight:graphicShowNight : : :
          Debug:ctrlDebug : : :

      • graphicHeaderOwnspecValForm
        The readings to be displayed with the attribute graphicHeaderOwnspec can be manipulated with sprintf and other Perl operations.
        There are two basic notation options that cannot be combined with each other.
        The notations are always specified within two curly brackets {...}.

        Notation 1:
        A simple formatting of readings of your own device with sprintf is carried out as shown in line 'Current_AutarkyRate' or 'Current_GridConsumption'.
        Other Perl operations are to be bracketed with (). The respective readings values and units are available via the variables $VALUE and $UNIT.
        Readings of other devices are specified by '<Device>.<Reading>'.

          {
          'Current_AutarkyRate' => "%.1f %%",
          'Current_GridConsumption' => "%.2f $UNIT",
          'SMA_Energymeter.Cover_RealPower' => q/($VALUE)." W"/,
          'SMA_Energymeter.L2_Cover_RealPower' => "($VALUE).' W'",
          'SMA_Energymeter.L1_Cover_RealPower' => '(sprintf "%.2f", ($VALUE / 1000))." kW"',
          }

        Notation 2:
        The manipulation of reading values and units is done via Perl If ... else structures.
        The device, reading, reading value and unit are available to the structure with the variables $DEVICE, $READING, $VALUE and $UNIT.
        If the variables are changed, the new values are transferred to the display accordingly.

          {
          if ($READING eq 'Current_AutarkyRate') {
             $VALUE = sprintf "%.1f", $VALUE;
             $UNIT = "%";
          }
          elsif ($READING eq 'Current_GridConsumption') {
             ...
          }
          }

      • graphicHeaderShow
        Show/hide the graphic table header with forecast data and certain current and statistical values.
        (default: 1)

      • graphicHistoryHour
        Number of previous hours displayed in the bar graph.
        (default: 2)

      • graphicHourCount <4...24>
        Number of bars/hours in the bar graph.
        (default: 24)

      • graphicHourStyle
        Format of the time in the bar graph.

          nicht gesetzt hours only without minutes (default)
          :00 Hours as well as minutes in two digits, e.g. 10:00
          :0 Hours as well as minutes single-digit, e.g. 8:0

      • graphicLayoutType <single | double | diff>
        Layout of the bar graph.
        The content of the bars to be displayed is determined by the graphicBeam1Content or graphicBeam2Content attributes.

          double displays the primary bar and the secondary bar (default)
          single displays only the primary bar
          diff difference display. It is valid: <Difference> = <Value primary bar> - <Value secondary bar>

      • graphicSelect
        Selects the graphic segments of the module to be displayed.
        To customize the energy flow graphic, the flowGraphicCss attribute is available in addition to the flowGraphic.* attributes.

          both displays the header, consumer legend, energy flow and prediction graph (default)
          flow displays the header, the consumer legend and energy flow graphic
          forecast displays the header, the consumer legend and the prediction graphic
          none displays only the header and the consumer legend

      • graphicShowDiff <no | top | bottom>
        Additional display of the difference "graphicBeam1Content - graphicBeam2Content" in the header or footer area of the bar graphic.
        (default: no)

      • graphicShowNight
        Show/hide the night hours (without yield forecast) in the bar graph.
        (default: 0)

      • graphicShowWeather
        Show/hide weather icons in the bar graph.
        (default: 1)

      • graphicStartHtml <HTML-String>
        Specify any HTML string to be executed before the graphics code.

      • graphicEndHtml <HTML-String>
        Specify any HTML string that will be executed after the graphic code.

      • graphicSpaceSize <value>
        Defines how much space in px above or below the bars (with display type differential (diff)) is kept free for displaying the values. For styles with large fonts the default value may be too small or a bar may slip over the baseline. In these cases please increase the value.
        (default: 24)

      • graphicWeatherColor
        Color of the weather icons in the bar graph for the daytime hours.

      • graphicWeatherColorNight
        Color of the weather icons for the night hours.

=end html =begin html_DE

SolarForecast


Das Modul SolarForecast erstellt auf Grundlage der Werte aus generischen Quellen eine Vorhersage für den solaren Ertrag und integriert weitere Informationen als Grundlage für darauf aufbauende Steuerungen.
Zur Erstellung der solaren Vorhersage kann das Modul SolarForecast unterschiedliche Dienste und Quellen nutzen:

    DWD solare Vorhersage basierend auf der Strahlungsprognose des Deutschen Wetterdienstes (Model DWD)
    SolCast-API verwendet Prognosedaten der SolCast API (Model SolCastAPI)
    ForecastSolar-API verwendet Prognosedaten der Forecast.Solar API (Model ForecastSolarAPI)
    VictronKI-API Victron Energy API des VRM Portals (Model VictronKiAPI)

Bei Verwendung des Model DWD kann eine KI-Unterstützung aktiviert werden.
Die Nutzung der erwähnten API's beschränkt sich auf die jeweils kostenlose Version des Dienstes.
Im zugeordneten DWD_OpenData Device (Attribut "ctrlWeatherDevX") ist die passende Wetterstation festzulegen um meteorologische Daten (Bewölkung, Sonnenaufgang, u.a.) bzw. eine Strahlungsprognose (Model DWD) für den Anlagenstandort zu erhalten.

Ü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 genutzt wird. Weiterhin bietet das Modul eine Consumer Integration zur integrierten Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen.

Bei der ersten Definition des Moduls wird der Benutzer über eine Guided Procedure unterstützt um alle initialen Eingaben vorzunehmen.
Am Ende des Vorganges und nach relevanten Änderungen der Anlagen- bzw. Devicekonfiguration sollte unbedingt mit einem set <name> plantConfiguration ceck die ordnungsgemäße Anlagenkonfiguration geprüft werden.
    Define

      Ein SolarForecast Device wird erstellt mit:

        define <name> SolarForecast

      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 und Attributen werden für die Funktion des Moduls maßgebliche Informationen hinterlegt:

        ctrlWeatherDevX DWD_OpenData Device welches meteorologische Daten (z.B. Bewölkung) liefert
        currentRadiationAPI DWD_OpenData Device bzw. 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 vorhandenen Anlagenstrings
        moduleAzimuth 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)
        moduleDeclination die Neigungswinkel der der Anlagenmodule

      Um eine Anpassung an die persönliche Anlage zu ermöglichen, können Korrekturfaktoren manuell fest bzw. automatisiert dynamisch angewendet werden.

    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 optionalen Schlüsseln angegeben die das Einplanungs- und Schaltverhalten des Consumers beeinflussen.
      Die Schlüssel sind in der ConsumerXX-Hilfe detailliiert beschreiben. 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:

        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
        attr SolCastDummy devStateIcon off:light_light_dim_100@grey on:light_light_dim_100@darkorange
        attr SolCastDummy group Solarprognose
        attr SolCastDummy icon solar_icon
        attr SolCastDummy nomPower 1000
        attr SolCastDummy readingList BatIn BatOut BatVal BatInTot BatOutTot bezW einW Batcharge Temp automatic
        attr SolCastDummy room Energie,Testraum
        attr SolCastDummy setList BatIn BatOut BatVal BatInTot BatOutTot bezW einW Batcharge on off Temp
        attr SolCastDummy userReadings actpow {ReadingsVal ($name, 'state', 'off') eq 'on' ? AttrVal ($name, 'nomPower', 100) : 0}


    Set
      • aiDecTree

        Ist der KI Support im SolarForecast Device aktiviert, können verschiedene KI-Aktionen manuell ausgeführt werden. Die manuelle Ausführung der KI Aktionen ist im Allgemeinen nicht notwendig, da die Abarbeitung aller nötigen Schritte bereits automatisch im Modul vorgenommen wird.

          addInstances - Die KI wird mit den aktuell vorhandenen PV-, Strahlungs- und Umweltdaten angereichert.
          addRawData - Relevante PV-, Strahlungs- und Umweltdaten werden extrahiert und für die spätere Verwendung gespeichert.
          train - Die KI wird mit den verfügbaren Daten trainiert.
            Erfolgreich generierte Entscheidungsdaten werden im Filesystem gespeichert.

      • batteryTrigger <1on>=<Wert> <1off>=<Wert> [<2on>=<Wert> <2off>=<Wert> ...]

        Generiert Trigger bei Über- bzw. Unterschreitung bestimmter Batterieladungswerte (SoC in %).
        Überschreiten die letzten drei SoC-Messungen eine definierte Xon-Bedingung, wird das Reading batteryTrigger_X = on erstellt/gesetzt.
        Unterschreiten die letzten drei SoC-Messungen eine definierte Xoff-Bedingung, wird das Reading batteryTrigger_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> batteryTrigger 1on=30 1off=10 2on=70 2off=20 3on=15 4off=90

      • consumerNewPlanning <Verbrauchernummer>

        Es wird die vorhandene Planung des angegebenen Verbrauchers gelöscht.
        Die Neuplanung wird unter Berücksichtigung der im consumerXX Attribut gesetzten Parameter sofort vorgenommen.

          Beispiel:
          set <name> consumerNewPlanning 01

      • 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

      • currentBatteryDev <Batterie Device Name> pin=<Readingname>:<Einheit> pout=<Readingname>:<Einheit> [intotal=<Readingname>:<Einheit>] [outtotal=<Readingname>:<Einheit>] [cap=<Option>] [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:

          pin Reading welches die aktuelle Batterieladeleistung liefert
          pout Reading welches die aktuelle Batterieentladeleistung liefert
          intotal Reading welches die totale Batterieladung als fortlaufenden Zähler liefert (optional)
          outtotal Reading welches die totale Batterieentladung als fortlaufenden Zähler liefert (optional)
          cap installierte Batteriekapazität (optional). Option kann sein:
          numerischer Wert - direkte Angabe der Batteriekapazität in Wh
          <Readingname>:<Einheit> - Reading welches die Kapazität liefert und Einheit (Wh, kWh)
          charge Reading welches den aktuellen Ladezustand (SOC in Prozent) liefert (optional)
          Einheit die jeweilige Einheit (W,Wh,kW,kWh)

        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 cap=BatCap:kWh

          # 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)

      • 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 erzeugte PV-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 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> [conprice=<Feld>] [feedprice=<Feld>]

        Legt ein beliebiges Device und seine Readings zur Energiemessung fest. Das Modul geht davon aus, dass der numerische Wert der Readings 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 (ein sich stetig erhöhender Zähler)
          gfeedin Reading welches die aktuell in das Netz eingespeiste Leistung liefert
          feedtotal Reading welches die Summe der in das Netz eingespeisten Energie liefert (ein sich stetig erhöhender Zähler)
          Einheit die jeweilige Einheit (W,kW,Wh,kWh)
          conprice Preis für den Bezug einer kWh (optional). Die Angabe <Feld> ist in einer der folgenden Varianten möglich:
          <Preis>:<Währung> - Preis als numerischer Wert und dessen Währung
          <Reaging>:<Währung> - Reading des Meter Device das den Preis enthält : Währung
          <Device>:<Reaging>:<Währung> - beliebiges Device und Reading welches den Preis enthält : Währung
          feedprice Vergütung für die Einspeisung einer kWh (optional). Die Angabe <Feld> ist in einer der folgenden Varianten möglich:
          <Vergütung>:<Währung> - Vergütung als numerischer Wert und dessen Währung
          <Reaging>:<Währung> - Reading des Meter Device das die Vergütung enthält : Währung
          <Device>:<Reaging>:<Währung> - beliebiges Device und Reading welches die Vergütung enthält : Währung

        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 conprice=powerCost:€ feedprice=0.1269:€

          # 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)

      • currentRadiationAPI

        Legt die Quelle zur Lieferung der solaren Strahlungsdaten fest. Es kann ein Device vom Typ DWD_OpenData oder eine implementierte API eines Dienstes ausgewählt werden.
        Hinweis: Ist eine OpenMeteo API im Attribut 'ctrlWeatherDev1' gesetzt, kann kein anderer Strahlungsdatendienst als diese OpenMeteo API ausgewählt werden.

        OpenMeteoDWD-API
        Open-Meteo ist eine Open-Source-Wetter-API und bietet kostenlosen Zugang für nicht-kommerzielle Zwecke. Es ist kein API-Schlüssel erforderlich. Open-Meteo nutzt eine leistungsstarke Kombination aus globalen (11 km) und mesoskaligen (1 km) Wettermodellen von angesehenen nationalen Wetterdiensten. Diese API bietet Zugang zu den renommierten ICON-Wettermodellen des Deutschen Wetterdienstes (DWD), die 15-minütige Daten für kurzfristige Vorhersagen in Mitteleuropa und globale Vorhersagen mit einer Auflösung von 11 km liefern. Das ICON-Modell ist eine bevorzugte Wahl für allgemeine Wettervorhersage-APIs, wenn keine anderen hochauflösenden Wettermodelle verfügbar sind. Es werden die Modelle DWD Icon D2, DWD Icon EU und DWD Icon Global zu einer nahtlosen Vorhersage zusammengeführt. Auf der Webseite des Dienstes ist die umfangreiche und übersichtliche API Dokumentation verfügbar.

        OpenMeteoDWDEnsemble-API
        Diese Open-Meteo API Variante bietet Zugang zum globalen Ensemble-Vorhersagesystem (EPS) des DWD.
        Es werden die Ensemble Modelle ICON-D2-EPS, ICON-EU-EPS und ICON-EPS nahtlos vereint.
        Ensemble-Wetterprognosen sind eine spezielle Art von Vorhersagemethode, die die Unsicherheiten bei der Wettervorhersage berücksichtigt. Sie tun dies, indem sie mehrere Simulationen oder Modelle mit leichten Unterschieden in den Startbedingungen oder Einstellungen ausführen. Jede Simulation, bekannt als Ensemblemitglied, stellt ein mögliches Ergebnis des Wetters dar. In der vorliegenden Implementierung werden 40 Ensemblemitglieder pro Wettermerkmal zusammengeführt und das wahrscheinlichste Ergbnis verwendet.

        OpenMeteoWorld-API
        Als Variante des Open-Meteo Dienstes liefert die OpenMeteoWorld-API die optimale Vorhersage für einen bestimmten Ort weltweit. Die OpenMeteoWorld-API vereint nahtlos Wettermodelle bekannter Organisationen wie NOAA (National Oceanic and Atmospheric Administration), DWD (Deutscher Wetterdienst), CMCC (Canadian) und ECMWF (Europäisches Zentrum für mittelfristige Wettervorhersage). Für jeden Ort weltweit werden die Modelle der Anbieter kombiniert, um die bestmögliche Vorhersage zu erstellen. Die Nutzung der Dienste und Wettermodelle erfolgt automatisch anhand der im API Aufruf enthaltenen Standortkoordinaten.

        SolCast-API
        Die API-Nutzung benötigt vorab ein oder mehrere API-keys (Accounts) sowie ein oder mehrere Rooftop-ID's die auf der SolCast Webseite angelegt werden müssen. Ein Rooftop ist im SolarForecast-Kontext mit einem inverterString gleichzusetzen.
        Die kostenfreie API-Nutzung ist auf eine Tagesrate API-Anfragen begrenzt. Die Anzahl definierter Strings (Rooftops) erhöht die Anzahl erforderlicher API-Anfragen. Das Modul optimiert die Abfragezyklen mit dem Attribut ctrlSolCastAPIoptimizeReq .

        ForecastSolar-API
        Die kostenfreie Nutzung der Forecast.Solar API erfordert keine Registrierung. Die API-Anfragen sind in der kostenfreien Version auf 12 innerhalb einer Stunde begrenzt. Ein Tageslimit gibt es dabei nicht. Das Modul ermittelt automatisch das optimale Abfrageintervall in Abhängigkeit der konfigurierten Strings.

        VictronKI-API
        Diese API kann durch Nutzer des Victron Energy VRM Portals angewendet werden. Diese API ist KI basierend. Als String ist der Wert "KI-based" im Setup der inverterStrings einzutragen.
        Im Victron Energy VRM Portal ist als Voraussetzung der Standort der PV-Anlage anzugeben.
        Siehe dazu auch den Blog-Beitrag Introducing Solar Production Forecast.

        DWD_OpenData Device
        Der DWD-Dienst wird über ein FHEM Device vom Typ DWD_OpenData eingebunden. Ist noch kein Device des Typs DWD_OpenData vorhanden, muß es vorab definiert werden (siehe DWD_OpenData Commandref).
        Um eine gute Strahlungsprognose zu erhalten, sollte eine nahe dem Anlagenstandort gelegene DWD-Station genutzt werden.
        Leider liefern nicht alle DWD-Stationen die benötigten Rad1h-Werte.
        Erläuterungen zu den Stationen sind im Stationslexikon aufgeführt.
        Im ausgewählten DWD_OpenData Device müssen mindestens die folgenden Attribute gesetzt sein:

          forecastDays 1
          forecastProperties Rad1h
          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!

      • 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 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

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

        Bezeichnungen der aktiven Strings. Diese Bezeichnungen werden als Schlüssel in den weiteren Settings verwendet.
        Bei Nutzung einer KI basierenden API (z.B. VictronKI-API) ist nur "KI-based" einzutragen unabhängig davon welche realen Strings existieren.

          Beispiele:
          set <name> inverterStrings Ostdach,Südgarage,S3
          set <name> inverterStrings KI-based

      • moduleAzimuth <Stringname1>=<dir> [<Stringname2>=<dir> <Stringname3>=<dir> ...]
        (nur Model DWD, ForecastSolarAPI)

        Ausrichtung <dir> der Solarmodule im String "StringnameX". Der Stringname ist ein Schlüsselwert des Readings inverterStrings.
        Die Richtungsangabe <dir> kann als Azimut Kennung oder als Azimut Wert angegeben werden:

          Kennung Azimut
          N -180 Nordausrichtung
          NE -135 Nord-Ost Ausrichtung
          E -90 Ostausrichtung
          SE -45 Süd-Ost Ausrichtung
          S 0 Südausrichtung
          SW 45 Süd-West Ausrichtung
          W 90 Westausrichtung
          NW 135 Nord-West Ausrichtung

        Azimut Werte sind Ganzzahlen im Bereich von -180 bis 180. Azimut Zwischenwerte, die nicht exakt auf eine Kennung passen, werden auf die nächstgelegene Kennung abstrahiert wenn die gewählte API nur mit Kennungen arbeitet. Das Modul verwendet den genaueren Azimut Wert sofern die API die Verwendung unterstützt, z.B. die ForecastSolar-API.

          Beispiel:
          set <name> moduleAzimuth Ostdach=-90 Südgarage=S S3=NW

      • moduleDeclination <Stringname1>=<Winkel> [<Stringname2>=<Winkel> <Stringname3>=<Winkel> ...]
        (nur Model DWD, ForecastSolarAPI)

        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 (0 = waagerecht, 90 = senkrecht).

          Beispiel:
          set <name> moduleDeclination Ostdach=40 Südgarage=60 S3=30

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

        Die DC Peakleistung des Strings "StringnameX" in kWp. Der Stringname ist ein Schlüsselwert des Readings inverterStrings.
        Bei Verwendung einer KI basierenden API (z.B. Model VictronKiAPI) sind die Peakleistungen aller vorhandenen Strings als Summe dem Stringnamen KI-based zuzuordnen.

          Beispiele:
          set <name> modulePeakString Ostdach=5.1 Südgarage=2.0 S3=7.2
          set <name> modulePeakString KI-based=14.3 (bei KI basierender API)

      • 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.

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

      • operatingMemory backup | save | recover-<Datei>

        Die Komponenten pvHistory (PVH) und pvCircular (PVC) der internen Cache Datenbank werden im Filesystem gespeichert.
        Das Zielverzeichnis ist "../FHEM/FhemUtils". Dieser Vorgang wird vom Modul regelmäßig im Hintergrund ausgeführt.

          backup Sichert die aktiven In-Memory Strukturen mit dem aktuellen Zeitstempel.
          Es werden ctrlBackupFilesKeep Generationen der Dateien gespeichert. Ältere Versionen werden gelöscht.
          Dateien: PVH_SolarForecast_<name>_<Zeitstempel>, PVC_SolarForecast_<name>_<Zeitstempel>
          save Die aktiven In-Memory Strukturen werden gespeichert.
          Dateien: PVH_SolarForecast_<name>, PVC_SolarForecast_<name>
          recover-<Datei> Stellt die Daten der ausgewählten Sicherungsdatei als aktive In-Memory Struktur wieder her.
          Um Inkonsistenzen zu vermeiden, sollten die Dateien PVH.* und PVC.* mit dem gleichen
          Zeitstempel paarweise recovert werden.


      • operationMode

        Mit inactive wird das SolarForecast Gerät deaktiviert. Die active Option aktiviert das Gerät wieder. Das Verhalten entspricht dem "disable"-Attribut, eignet sich aber vor allem zum Einsatz in Perl-Skripten da gegenüber dem "disable"-Attribut keine Speicherung der Gerätekonfiguration nötig ist.

      • plantConfiguration

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

          check Prüft die aktuelle Anlagenkonfiguration. Es wird eine Plausibilitätsprüfung
          vorgenommen und das Ergebnis sowie eventuelle Hinweise bzw. Fehler ausgegeben.
          save sichert wichtige Parameter der Anlagenkonfiguration.
          Die Operation wird täglich kurz nach 00:00 Uhr automatisch ausgeführt.
          restore stellt eine gesicherte Anlagenkonfiguration wieder her

      • 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 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

      • pvCorrectionFactor_Auto

        Schaltet die automatische Vorhersagekorrektur ein/aus. Die Wirkungsweise unterscheidet sich je nach gewählter Methode.
        Das Korrekturverhalten kann mit dem Attribut affectMaxDayVariance beeinflusst werden.
        (default: off)

        noLearning:
        Mit dieser Option wird die erzeugte PV Energie der aktuellen Stunde vom Lernprozess (Korrekturfaktoren sowie KI) ausgeschlossen.
        Die zuvor eingestellte Autokorrekturmethode wird weiterhin angewendet.

        on_simple(_ai):
        Bei dieser Methode wird die stündlich vorhergesagte mit der real erzeugten Energiemenge verglichen und daraus ein für die Zukunft verwendeter Korrekturfaktor für die jeweilige Stunde erstellt. Die von der gewählten API gelieferten Prognosedaten werden nicht zusätzlich mit weiteren Bedingungen wie den Bewölkungszustand oder Temperaturen in Beziehung gesetzt.
        Ist die KI-Unterstützung eingeschaltet (on_simple_ai) und wird durch die KI ein PV-Prognosewert geliefert, wird dieser Wert anstatt des API-Wertes verwendet.

        on_complex(_ai):
        Bei dieser Methode wird die stündlich vorhergesagte mit der real erzeugten Energiemenge verglichen und daraus ein für die Zukunft verwendeter Korrekturfaktor für die jeweilige Stunde erstellt. Die von der gewählten API gelieferten Prognosedaten werden außerdem zusätzlich mit weiteren Bedingungen wie den Bewölkungszustand oder Temperaturen verknüpft.
        Ist die KI-Unterstützung eingeschaltet (on_complex_ai) und wird durch die KI ein PV-Prognosewert geliefert, wird dieser Wert anstatt des API-Wertes verwendet.

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

        Nachfolgend einige API-spezifische Hinweise die lediglich Best Practice Empfehlungen darstellen.

        Model OpenMeteo...API:
        Die empfohlene Autokorrekturmethode ist on_complex bzw. on_complex_ai.

        Model SolCastAPI:
        Die empfohlene Autokorrekturmethode ist on_complex.
        Bevor man die Autokorrektur eingeschaltet, ist die Prognose mit folgenden Schritten zu optimieren:

        • 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 realer PV Erzeugung in Prozent darstellt.
        • entsprechend der Abweichung passe den efficiency factor in Schritten an bis ein Optimum, d.h. die kleinste Tagesabweichung gefunden ist
        • ist man der Auffassung die optimale Einstellung gefunden zu haben, kann pvCorrectionFactor_Auto on* gesetzt werden.

        Idealerweise wird dieser Prozess in einer Phase stabiler meteorologischer Bedingungen (gleichmäßige Sonne bzw. Bewölkung) durchgeführt.

        Model VictronKiAPI:
        Dieses Model basiert auf der KI gestützten API von Victron Energy. Die empfohlene Autokorrekturmethode ist on_complex.

        Model DWD:
        Die empfohlene Autokorrekturmethode ist on_complex bzw. on_complex_ai.

        Model ForecastSolarAPI:
        Die empfohlene Autokorrekturmethode ist on_complex.

      • pvCorrectionFactor_XX <Zahl>

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

      • reset

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

          aiData löscht eine vorhandene KI Instanz inklusive aller Trainingsdaten und initialisiert sie neu
          batteryTriggerSet löscht die Triggerpunkte des Batteriespeichers
          consumerPlanning löscht die Planungsdaten aller registrierten Verbraucher
          Um die Planungsdaten nur eines Verbrauchers zu löschen verwendet man:
            set <name> reset consumerPlanning <Verbrauchernummer>
          Das Modul führt eine automatische Neuplanung der Verbraucherschaltung durch.
          consumerMaster löscht die Daten aller registrierten Verbraucher aus dem Speicher
          Um die Daten nur eines Verbrauchers zu löschen verwendet man:
            set <name> reset consumerMaster <Verbrauchernummer>
          consumption löscht die gespeicherten Verbrauchswerte des Hauses
          Um die Verbrauchswerte eines bestimmten Tages zu löschen:
            set <name> reset consumption <Tag> (z.B. set <name> reset consumption 08)
          Um die Verbrauchswerte einer bestimmten Stunde eines Tages zu löschen:
            set <name> reset consumption <Tag> <Stunde> (z.B. set <name> reset consumption 08 10)
          currentBatterySet löscht das eingestellte Batteriedevice und korrespondierende Daten
          currentInverterSet löscht das eingestellte Inverterdevice und korrespondierende Daten
          currentMeterSet löscht das eingestellte Meterdevice und korrespondierende Daten
          energyH4TriggerSet löscht die 4-Stunden Energie Triggerpunkte
          inverterStringSet löscht die Stringkonfiguration der Anlage
          powerTriggerSet löscht die Triggerpunkte für PV Erzeugungswerte
          pvCorrection löscht die Readings pvCorrectionFactor*
          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)
          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)
          Um eine bestimmte Stunde eines historischer Tages zu löschen:
            set <name> reset pvHistory <Tag> <Stunde> (z.B. set <name> reset pvHistory 08 10)
          moduleRoofTopSet löscht die SolCast API Rooftops
          roofIdentPair löscht alle gespeicherten SolCast API Rooftop-ID / API-Key Paare
          Um ein bestimmtes Paar zu löschen ist dessen Schlüssel <pk> anzugeben:
            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 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 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 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


      • vrmCredentials user=<Benutzer> pwd=<Paßwort> idsite=<idSite>
        (nur bei Verwendung Model VictronKiAPI)

        Wird die Victron VRM API genutzt, sind mit diesem set-Befehl die benötigten Zugangsdaten zu hinterlegen.

          user Benutzername für das Victron VRM Portal
          pwd Paßwort für den Zugang zum Victron VRM Portal
          idsite idSite ist der Bezeichner "XXXXXX" in der Victron VRM Portal Dashboard URL.
          URL des Victron VRM Dashboard ist:
          https://vrm.victronenergy.com/installation/XXXXXX/dashboard

        Um die gespeicherten Credentials zu löschen, ist dem Kommando nur das Argument delete zu übergeben.

          Beispiele:
          set <name> vrmCredentials user=john@example.com pwd=somepassword idsite=212008
          set <name> vrmCredentials delete


    Get
      • data

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

      • dwdCatalog

        Der Deutsche Wetterdienst (DWD) stellt einen Katalog der MOSMIX Stationen zur Verfügung.
        Die Stationen liefern Daten deren Bedeutung in dieser Übersicht erläutert ist. Der DWD unterscheidet dabei zwischen MOSMIX_L und MOSMIX_S Stationen die sich durch Aktualisierungfrequenz und Datenumfang unterscheiden.
        Mit diesem Kommando wird der Katalog in SolarForecast eingelesen und in der Datei ./FHEM/FhemUtils/DWDcat_SolarForecast gespeichert.
        Der Katalog kann umfangreich gefiltert und im GPS Exchange Format (GPX) gespeichert werden. Die Koordinaten Latitude und Logitude werden in Dezimalgrad ausgegeben.
        Zur Filterung werden Regex-Ausdrücke in den entsprechenden Schlüsseln verwendet. Der Regex wird zur Auswertung in ^...$ eingeschlossen.
        Folgende Parameter können angegeben werden. Ohne Parameter erfolgt die Ausgabe des gesamten Katalogs:

          byID Die Ausgabe erfolgt sortiert nach Stations-ID. (default)
          byName Die Ausgabe erfolgt sortiert nach Stations-Name.
          force Es wird die neueste Version des DWD Stationskatalogs in das System geladen.
          exportgpx Die (gefilterten) Stationen werden in der Datei ./FHEM/FhemUtils/DWDcat_SolarForecast.gpx gespeichert.
          Diese Datei kann z.B. im GPX-Viewer dargestellt werden.
          id=<Regex> Es erfolgt eine Filterung nach Stations-ID.
          name=<Regex> Es erfolgt eine Filterung nach Stations-Name.
          lat=<Regex> Es erfolgt eine Filterung nach Latitude.
          lon=<Regex> Es erfolgt eine Filterung nach Longitude.

          Beispiel:
          get <name> dwdCatalog byName exportgpx lat=(48|49|50|51|52)\..* lon=([5-9]|10|11|12|13|14|15)\..*
          # filtert die Stationen weitgehend auf deutsche Orte beginnend mit "ST" und exportiert die Daten im GPS Exchange Format

      • forecastQualities

        Zeigt die zur Bestimmung der PV Vorhersage aktuell verwendeten Korrekturfaktoren mit der jeweiligen Startzeit sowie die bisher im Durchschnitt erreichte Vorhersagequalität dieses Zeitraumes an.

      • ftuiFramefiles

        SolarForecast stellt Widgets für FHEM Tablet UI v2 (FTUI2) zur Verfügung.
        Ist FTUI2 auf dem System installiert, können die Dateien für das Framework mit diesem Kommando in die FTUI-Verzeichnisstruktur geladen werden.
        Die Einrichtung und Verwendung der Widgets ist im Wiki SolarForecast FTUI Widget beschrieben.

      • html

        Die SolarForecast Grafik wird als HTML-Code abgerufen und wiedergegeben.
        Hinweis: Durch das Attribut graphicHeaderOwnspec generierte set-Kommandos oder Attribut-Befehle im Anwender spezifischen Bereich des Headers werden aus technischen Gründen generell ausgeblendet.
        Als Argument kann dem Befehl eine der folgenden Selektionen mitgegeben werden:

          both zeigt den Header, die Verbraucherlegende, Energiefluß- und Vorhersagegrafik an (default)
          both_noHead zeigt die Verbraucherlegende, Energiefluß- und Vorhersagegrafik an
          both_noCons zeigt den Header, Energiefluß- und Vorhersagegrafik an
          both_noHead_noCons zeigt Energiefluß- und Vorhersagegrafik an
          flow zeigt den Header, die Verbraucherlegende und Energieflußgrafik an
          flow_noHead zeigt die Verbraucherlegende und die Energieflußgrafik an
          flow_noCons zeigt den Header und die Energieflußgrafik an
          flow_noHead_noCons zeigt die Energieflußgrafik an
          forecast zeigt den Header, die Verbraucherlegende und die Vorhersagegrafik an
          forecast_noHead zeigt die Verbraucherlegende und die Vorhersagegrafik an
          forecast_noCons zeigt den Header und die Vorhersagegrafik an
          forecast_noHead_noCons zeigt die Vorhersagegrafik an
          none zeigt nur den Header und die Verbraucherlegende an

        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', '-', '<argument>') }

        'SolCast5' ist der Name des einzubindenden SolarForecast-Device. <argument> ist eine der oben beschriebenen Auswahlmöglichkeiten.

      • nextHours

        Listet die erwarteten Werte der kommenden Stunden auf.

          aihit Lieferstatus der KI für die PV Vorhersage (0-keine Lieferung, 1-Lieferung)
          confc erwarteter Energieverbrauch inklusive der Anteile registrierter Verbraucher
          confcEx erwarteter Energieverbrauch ohne der Anteile registrierter Verbraucher
          crange berechneter Bewölkungsbereich
          correff verwendeter Korrekturfaktor/Qualität
          Faktor/- -> keine Qualität definiert
          Faktor/0..1 - Qualität der PV Prognose (1 = beste Qualität)
          DoN Sonnenauf- und untergangsstatus (0 - Nacht, 1 - Tag)
          hourofday laufende Stunde des Tages
          pvapifc erwartete PV Erzeugung (Wh) der verwendeten API inkl. einer eventuellen Korrektur
          pvaifc erwartete PV Erzeugung der KI (Wh)
          pvfc verwendete PV Erzeugungsprognose (Wh)
          rad1h vorhergesagte Globalstrahlung
          starttime Startzeit des Datensatzes
          sunaz Azimuth der Sonne (in Dezimalgrad)
          sunalt Höhe der Sonne (in Dezimalgrad)
          temp vorhergesagte Außentemperatur
          today hat Wert '1' wenn Startdatum am aktuellen Tag
          rr1c Gesamtniederschlag in der letzten Stunde kg/m2
          rrange Bereich des Gesamtniederschlags
          wid ID des vorhergesagten Wetters
          wcc vorhergesagter Grad der Bewölkung

      • pvHistory

        Zeigt den Inhalt des pvHistory Datenspeichers sortiert nach dem Tagesdatum und Stunde. Mit der Auswahlliste kann ein bestimmter Tag angesprungen werden. Die Drop-Down Liste enthält die aktuell im Speicher verfügbaren Tage. Ohne Argument wird der gesamte Datenspeicher gelistet. Die Stundenangaben beziehen sich auf die jeweilige Stunde des Tages, z.B. bezieht sich die Stunde 09 auf die Zeit von 08 Uhr bis 09 Uhr.

          batintotal totale Batterieladung (Wh) zu Beginn der Stunde
          batin Batterieladung der Stunde (Wh)
          batouttotal totale Batterieentladung (Wh) zu Beginn der Stunde
          batout Batterieentladung der Stunde (Wh)
          batmaxsoc maximaler SOC (%) des Tages
          batsetsoc optimaler SOC Sollwert (%) für den Tag
          csmtXX Energieverbrauch total von ConsumerXX
          csmeXX Energieverbrauch von ConsumerXX in der Stunde des Tages (Stunde 99 = Tagesenergieverbrauch)
          confc erwarteter Energieverbrauch (Wh)
          con realer Energieverbrauch (Wh) des Hauses
          cyclescsmXX Anzahl aktive Zyklen von ConsumerXX des Tages
          DoN Sonnenauf- und untergangsstatus (0 - Nacht, 1 - Tag)
          etotal totaler Energieertrag (Wh) zu Beginn der Stunde
          gcon realer Leistungsbezug (Wh) aus dem Stromnetz
          gfeedin reale Einspeisung (Wh) in das Stromnetz
          hourscsmeXX durchschnittliche Stunden eines Aktivzyklus von ConsumerXX des Tages
          minutescsmXX Summe Aktivminuten in der Stunde von ConsumerXX
          pvfc der prognostizierte PV Ertrag (Wh)
          pvrl reale PV Erzeugung (Wh)
          pvrlvd 1-'pvrl' ist gültig und wird im Lernprozess berücksichtigt, 0-'pvrl' ist als abnormal bewertet
          pvcorrf verwendeter Autokorrekturfaktor / erreichte Prognosequalität
          rad1h Globalstrahlung (kJ/m2)
          sunalt Höhe der Sonne (in Dezimalgrad)
          sunaz Azimuth der Sonne (in Dezimalgrad)
          wid Identifikationsnummer des Wetters
          wcc effektive Wolkenbedeckung
          rr1c Gesamtniederschlag in der letzten Stunde kg/m2

      • pvCircular

        Listet die vorhandenen Werte im Ringspeicher auf. Die Stundenangaben 01 - 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 Werte der Schlüssel pvcorrf, quality, pvrlsum, pvfcsum und dnumsum sind in der Form <Bereich Sonnenstand Höhe>.<Bewölkungsbereich> kodiert.
        Erläuterung der Werte:

          aihit Lieferstatus der KI für die PV Vorhersage (0-keine Lieferung, 1-Lieferung)
          batin Batterieladung (Wh)
          batout Batterieentladung (Wh)
          batouttot total aus der Batterie entnommene Energie (Wh)
          batintot total in die Batterie geladene Energie (Wh)
          confc erwarteter Energieverbrauch (Wh)
          days2care verbleibende Tage bis der Batterie Pflege-SoC (default 95%) erreicht sein soll
          dnumsum Anzahl Tage pro Bewölkungsbereich über die gesamte Laufzeit
          feedintotal in das öffentliche Netz total eingespeiste PV Energie (Wh)
          gcon realer Leistungsbezug aus dem Stromnetz
          gfeedin reale Leistungseinspeisung in das Stromnetz
          gridcontotal vom öffentlichen Netz total bezogene Energie (Wh)
          initdayfeedin initialer PV Einspeisewert zu Beginn des aktuellen Tages (Wh)
          initdaygcon initialer Netzbezugswert zu Beginn des aktuellen Tages (Wh)
          initdaybatintot initialer Wert der total in die Batterie geladenen Energie zu Beginn des aktuellen Tages (Wh)
          initdaybatouttot initialer Wert der total aus der Batterie entnommenen Energie zu Beginn des aktuellen Tages (Wh)
          lastTsMaxSocRchd Timestamp des letzten Erreichens von Batterie SoC >= maxSoC (default 95%)
          nextTsMaxSocChge Timestamp bis zu dem die Batterie mindestens einmal maxSoC erreichen soll
          pvapifc erwartete PV Erzeugung (Wh) der verwendeten API
          pvaifc PV Vorhersage (Wh) der KI für die nächsten 24h ab aktueller Stunde des Tages
          pvfc verwendete PV Prognose für die nächsten 24h ab aktueller Stunde des Tages
          pvcorrf Autokorrekturfaktoren für die Stunde des Tages, wobei 'simple' der einfach berechnete Korrekturfaktor ist.
          pvfcsum Summe PV Prognose pro Bewölkungsbereich über die gesamte Laufzeit
          pvrl reale PV Erzeugung der letzten 24h (Achtung: pvforecast und pvreal beziehen sich nicht auf den gleichen Zeitraum!)
          pvrlsum Summe reale PV Erzeugung pro Bewölkungsbereich über die gesamte Laufzeit
          quality Qualität der Autokorrekturfaktoren (0..1), wobei 'simple' die Qualität des einfach berechneten Korrekturfaktors ist.
          runTimeTrainAI Laufzeit des letzten KI Trainings
          aitrainLastFinishTs Timestamp des letzten erfolgreichen KI Trainings
          tdayDvtn heutige Abweichung PV Prognose/Erzeugung in %
          temp Außentemperatur
          wcc Grad der Wolkenüberdeckung
          rr1c Gesamtniederschlag in der letzten Stunde kg/m2
          wid ID des vorhergesagten Wetters
          wtxt Beschreibung des vorhergesagten Wetters
          ydayDvtn Abweichung PV Prognose/Erzeugung in % am Vortag

      • rooftopData

        Die erwarteten solaren Strahlungsdaten bzw. PV Erzeugungsdaten werden von der gewählten API abgerufen.

      • solApiData

        Listet die im Kontext des API-Abrufs gespeicherten Daten auf. Verwaltungsdatensätze sind mit einem führenden '?' gekennzeichnet. Die von der API gelieferten Vorhersagedaten bzgl. der Globalstrahlung Rad1h und des PV Ertrages (Wh) sind auf eine Stunde konsolidiert. Die verfügbaren Kennwerte unterscheiden sich je nach verwendeter API.

          currentAPIinterval das aktuell verwendete API Abrufintervall in Sekunden
          lastretrieval_time Zeit des letzten API Abrufs
          lastretrieval_timestamp Unix Timestamp des letzten API Abrufs
          Rad1h wenn vorhanden, erwartete Globalstrahlung (GI) bzw. globale Schräglagenstrahlung (GTI) in kJ/m2
          pv_estimate erwartete PV Erzeugung (Wh)
          KI-based_co erwarteter Energieverbrauch (Wh)
          todayDoneAPIrequests Anzahl der ausgeführten API Requests am aktuellen Tag
          todayRemainingAPIrequests Anzahl der verbleibenden SolCast API Requests am aktuellen Tag
          todayDoneAPIcalls Anzahl der ausgeführten API Abrufe am aktuellen Tag
          todayRemainingAPIcalls Anzahl der noch möglichen SolCast API Abrufe am aktuellen Tag
          (ein Abruf kann mehrere SolCast API Requests ausführen)
          todayMaxAPIcalls Anzahl der maximal möglichen SolCast API Abrufe pro Tag

      • valConsumerMaster

        Zeigt die Daten der aktuell im SolarForecast Device registrierten Verbraucher.
        Mit der Auswahlliste kann ein bestimmter Verbraucher angesprungen werden. Die Drop-Down Liste enthält die aktuell im Datenspeicher verfügbaren Verbraucher bzw. Verbrauchernummern. Ohne Argument wird der gesamte Datenspeicher gelistet.

      • valCurrent

        Listet aktuelle Betriebsdaten, Kennzahlen und Status auf.

      • valDecTree

        Ist der KI Support im SolarForecast Device aktiviert, können verschiedene KI relevante Daten angezeigt werden :

          aiRawData Die aktuell für die KI gespeicherten PV-, Strahlungs- und Umweltdaten.
          aiRuleStrings Gibt eine Liste zurück, die den Entscheidungsbaum der KI in Form von Regeln beschreibt.
          Hinweis: Die Reihenfolge der Regeln ist zwar nicht vorhersehbar, die
          Reihenfolge der Kriterien innerhalb jeder Regel spiegelt jedoch die Reihenfolge
          wider, in der die Kriterien bei der Entscheidungsfindung geprüft werden.


    Attribute

      • 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 (%) erreicht ist.
        Verbraucher mit dem Mode must beachten die Vorrangladung der Batterie nicht.
        (default: 0)

      • affectConsForecastInPlanning
        Wenn gesetzt, wird bei der Einplanung der Consumer zusätzlich zur PV Prognose ebenfalls die Prognose des Verbrauchs berücksichtigt.
        Die Standardplanung der Consumer erfolgt lediglich auf Grundlage der PV Prognose.
        (default: 0)

      • affectConsForecastIdentWeekdays
        Wenn gesetzt, werden zur Berechnung der Verbrauchsprognose nur gleiche Wochentage (Mo..So) einbezogen.
        Anderenfalls werden alle Wochentage gleichberechtigt zur Kalkulation verwendet.
        (default: 0)

      • affectMaxDayVariance <Zahl>
        Maximale Anpassung des PV Vorhersagefaktors (Reading pvCorrectionFactor_XX) die bezogen auf eine Stunde pro Tag vorgenommen werden kann.
        Auf das Lern- und Prognoseverhalten einer eventuell verwendeten KI-Unterstützung (pvCorrectionFactor_Auto) hat diese Einstellung keinen Einfluß.
        (default: 0.8)

      • affectSolCastPercentile <10 | 50 | 90>
        (nur bei Verwendung Model SolCastAPI)

        Auswahl des Wahrscheinlichkeitsbereiches der gelieferten SolCast-Daten. SolCast liefert die 10- und 90-prozentige Wahrscheinlichkeit um den Prognosemittelwert (50) herum.
        (default: 50)

      • alias
        In Verbindung mit "ctrlShowLink" ein beliebiger Anzeigename.

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

          <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
          none keine Anzeige der Planungsdaten

      • consumerLegend
        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> [switchdev=<device>]
        [mode=<mode>] [icon=<Icon>] [mintime=<minutes> | SunPath[:<Offset_Sunrise>:<Offset_Sunset>]]
        [on=<Kommando>] [off=<Kommando>] [swstate=<Readingname>:<on-Regex>:<off-Regex>] [asynchron=<Option>]
        [notbefore=<Ausdruck>] [notafter=<Ausdruck>] [locktime=<offlt>[:<onlt>]]
        [auto=<Readingname>] [pcurr=<Readingname>:<Einheit>[:<Schwellenwert>]] [etotal=<Readingname>:<Einheit>[:<Schwellenwert>]]
        [swoncond=<Device>:<Reading>:<Regex>] [swoffcond=<Device>:<Reading>:<Regex>] [spignorecond=<Device>:<Reading>:<Regex>]
        [interruptable=<Option>] [noshow=<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 default-Werten besetzt.
        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 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) 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 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 Schlüssel power gibt die nominale Leistungsaufnahme des Verbrauchers gemäß seines Datenblattes an. Dieser Wert wird verwendet um die Schaltzeiten des Verbrauchers zu planen und das Schalten in Abhängigkeit des tatsächlichen PV-Überschusses zum Einplanungszeitpunkt zu steuern. Ist power=0 gesetzt, wird der Verbraucher unabhängig von einem ausreichend vorhandenem PV-Überschuß wie eingeplant geschaltet.

          type Typ des Verbrauchers. Folgende Typen sind erlaubt:
          dishwasher - Verbraucher ist eine Spülmaschine
          dryer - Verbraucher ist ein Wäschetrockner
          washingmachine - Verbraucher ist eine Waschmaschine
          heater - Verbraucher ist ein Heizstab
          charger - Verbraucher ist eine Ladeeinrichtung (Akku, Auto, Fahrrad, etc.)
          other - Verbraucher ist keiner der vorgenannten Typen
          noSchedule - für den Verbraucher erfolgt keine Einplanung oder automatische Schaltung.
                                 Anzeigefunktionen oder manuelle Schaltungen sind verfügbar.
          power nominale Leistungsaufnahme des Verbrauchers (siehe Datenblatt) in W
          (kann auf "0" gesetzt werden)
          switchdev Das angegebene <device> wird als Schalter Device dem Verbraucher zugeordnet (optional). Schaltvorgänge werden mit diesem Gerät
          ausgeführt. Der Schlüssel ist für Verbraucher nützlich bei denen Energiemessung und Schaltung mit verschiedenen Geräten vorgenommen
          wird, z.B. Homematic oder readingsProxy. Ist switchdev angegeben, beziehen sich die Schlüssel on, off, swstate, auto, asynchron auf dieses Gerät.
          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 zum Planungszeitpunkt unterbleibt bei ungenügendem PV-Überschuß.
          must - der Verbraucher wird optimiert eingeplant auch wenn wahrscheinlich nicht genügend PV Überschuß vorhanden sein wird
                     Der Start des Verbrauchers erfolgt auch bei ungenügendem PV-Überschuß sofern eine gesetzte "swoncond" Bedingung erfüllt und "swoffcond" nicht erfüllt ist.
          icon Icon zur Darstellung des Verbrauchers in der Übersichtsgrafik (optional)
          mintime Einplanungsdauer (Minuten oder "SunPath") des Verbrauchers. (optional)
          Mit der Angabe von SunPath erfolgt die Planung entsprechend des Sonnenauf- und untergangs.
          SunPath[:<Offset_Sunrise>:<Offset_Sunset>] - die Einplanung erfolgt von Sonnenaufgang bis Sonnenuntergang.
          Optional kann eine positive / negative Verschiebung (Minuten) der Planungszeit bzgl. Sonnenaufgang bzw. Sonnenuntergang angegeben werden.
          Ist mintime nicht angegeben, wird eine Standard Einplanungsdauer gemäß nachfolgender Tabelle verwendet.
          Default mintime nach Verbrauchertyp:
          - dishwasher: 180 Minuten
          - dryer: 90 Minuten
          - washingmachine: 120 Minuten
          - heater: 240 Minuten
          - charger: 120 Minuten
          - other: 60 Minuten
          on Set-Kommando zum Einschalten des Verbrauchers (optional)
          off Set-Kommando zum Ausschalten des Verbrauchers (optional)
          swstate Reading welches den Schaltzustand des Verbrauchers anzeigt (default: 'state').
          on-Regex - regulärer Ausdruck für den Zustand 'ein' (default: 'on')
          off-Regex - regulärer Ausdruck für den Zustand 'aus' (default: 'off')
          asynchron die Art der Schaltstatus Ermittlung im Verbraucher Device. Die Statusermittlung des Verbrauchers nach einem Schaltbefehl erfolgt nur
          durch Abfrage innerhalb eines Datensammelintervals (synchron) oder zusätzlich durch Eventverarbeitung (asynchron).
          0 - ausschließlich synchrone Verarbeitung von Schaltzuständen (default)
          1 - zusätzlich asynchrone Verarbeitung von Schaltzuständen durch Eventverarbeitung
          notbefore Startzeitpunkt Verbraucher nicht vor angegebener Zeit 'Stunde[:Minute]' einplanen (optional)
          Der <Ausdruck> hat das Format hh[:mm] oder ist in {...} eingeschlossener Perl-Code der hh[:mm] zurückgibt.
          notafter Startzeitpunkt Verbraucher nicht nach angegebener Zeit 'Stunde[:Minute]' einplanen (optional)
          Der <Ausdruck> hat das Format hh[:mm] oder ist in {...} eingeschlossener Perl-Code der hh[:mm] zurückgibt.
          auto Reading im Verbraucherdevice welches das Schalten des Verbrauchers freigibt bzw. blockiert (optional)
          Ist der Schlüssel switchdev angegeben, wird das Reading in diesem Device gesetzt und ausgewertet.
          Readingwert = 1 - Schalten freigegeben (default), 0: Schalten blockiert
          pcurr Reading:Einheit (W/kW) welches den aktuellen Energieverbrauch liefert (optional)
          :<Schwellenwert> (W) - Ab diesem Leistungsbezug wird der Verbraucher als aktiv gewertet. Die Angabe ist optional (default: 0)
          etotal Reading:Einheit (Wh/kWh) des Consumer Device, welches die Summe der verbrauchten Energie liefert (optional)
          :<Schwellenwert> (Wh) - Ab diesem Energieverbrauch pro Stunde wird der Verbrauch als gültig gewertet. Optionale Angabe (default: 0)
          swoncond Bedingung die zusätzlich erfüllt sein muß um den Verbraucher einzuschalten (optional). Der geplante Zyklus wird gestartet.
          Device - Device zur Lieferung der zusätzlichen Einschaltbedingung
          Reading - Reading zur Lieferung der zusätzlichen Einschaltbedingung
          Regex - regulärer Ausdruck der für eine 'wahre' Bedingung erfüllt sein muß
          swoffcond vorrangige Bedingung um den Verbraucher auszuschalten (optional). Der geplante Zyklus wird gestoppt.
          Device - Device zur Lieferung der vorrangigen Ausschaltbedingung
          Reading - Reading zur Lieferung der vorrangigen Ausschaltbedingung
          Regex - regulärer Ausdruck der für eine 'wahre' Bedingung erfüllt sein muß
          spignorecond Bedingung um einen fehlenden PV Überschuß zu ignorieren (optional). Bei erfüllter Bedingung wird der Verbraucher entsprechend
          der Planung eingeschaltet auch wenn zu dem Zeitpunkt kein PV Überschuß vorliegt.
          ACHTUNG: Die Verwendung beider Schlüssel spignorecond und interruptable kann zu einem unerwünschten Verhalten führen!
          Device - Device zur Lieferung der Bedingung
          Reading - Reading welches die Bedingung enthält
          Regex - regulärer Ausdruck der für eine 'wahre' Bedingung erfüllt sein muß
          interruptable definiert die möglichen Unterbrechungsoptionen für den Verbraucher nachdem er gestartet wurde (optional)
          0 - Verbraucher wird nicht temporär ausgeschaltet auch wenn der PV Überschuß die benötigte Energie unterschreitet (default)
          1 - Verbraucher wird temporär ausgeschaltet falls der PV Überschuß die benötigte Energie unterschreitet
          Device:Reading:Regex[:Hysterese] - Verbraucher wird temporär unterbrochen wenn der Wert des angegebenen
          Device:Readings auf den Regex matched oder unzureichender PV Überschuß (wenn power ungleich 0) vorliegt.
          Matched der Wert nicht mehr, wird der unterbrochene Verbraucher wieder eingeschaltet sofern ausreichender
          PV Überschuß (wenn power ungleich 0) vorliegt.
          Ist die optionale Hysterese angegeben, wird der Hysteresewert vom Readingswert subtrahiert und danach der Regex angewendet.
          Matched dieser und der originale Readingswert, wird der Verbraucher temporär unterbrochen.
          Der Verbraucher wird fortgesetzt, wenn sowohl der originale als auch der substrahierte Readingswert nicht (mehr) matchen.
          locktime Sperrzeiten in Sekunden für die Schaltung des Verbrauchers (optional).
          offlt - Sperrzeit in Sekunden nachdem der Verbraucher ausgeschaltet oder unterbrochen wurde
          onlt - Sperrzeit in Sekunden nachdem der Verbraucher eingeschaltet oder fortgesetzt wurde
          Der Verbraucher wird erst wieder geschaltet wenn die entsprechende Sperrzeit abgelaufen ist.
          Hinweis: Der Schalter 'locktime' ist nur im Automatik-Modus wirksam.
          noshow Verbraucher in Grafik ausblenden oder einblenden (optional).
          0 - der Verbraucher wird eingeblendet (default)
          1 - der Verbraucher wird ausgeblendet
          2 - der Verbraucher wird in der Verbraucherlegende ausgeblendet
          3 - der Verbraucher wird in der Flußgrafik ausgeblendet
          [Device:]Reading - Reading im Verbraucher oder optional einem alternativen Device.
          Hat das Reading den Wert 0 oder ist nicht vorhanden, wird der Verbraucher eingeblendet.
          Die Wirkung der möglichen Readingwerte 1, 2 und 3 ist wie beschrieben.

          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
          attr <name> consumer05 Shelly.shellyplug4 icon=sani_buffer_electric_heater_side type=heater mode=must power=1000 notbefore=7 notafter=20:10 auto=automatic pcurr=actpow:W on=on off=off mintime=SunPath interruptable=1
          attr <name> consumer06 Shelly.shellyplug5 icon=sani_buffer_electric_heater_side type=heater mode=must power=1000 notbefore=07:20 notafter={return'20:05'} auto=automatic pcurr=actpow:W on=on off=off mintime=SunPath:60:-120 interruptable=1
          attr <name> consumer07 SolCastDummy icon=sani_buffer_electric_heater_side type=heater mode=can power=600 auto=automatic pcurr=actpow:W on=on off=off mintime=15 asynchron=1 locktime=300:1200 interruptable=1 noshow=noShow

      • ctrlAIdataStorageDuration <Tage>
        Sind die entsprechenden Voraussetzungen gegeben, werden Trainingsdaten für die modulinterne KI gesammelt und gespeichert.
        Die Daten werden gelöscht wenn sie die angegebene Haltedauer (Tage) überschritten haben.
        (default: 1095)

      • ctrlAIshiftTrainStart <1...23>
        Bei Nutzung der internen KI erfolgt ein tägliches Training.
        Der Start des Trainings erfolgt ca. 15 Minuten nach der im Attribut festgelegten vollen Stunde.
        Zum Beispiel würde bei einem eingestellten Wert von '3' das Traning ca. 03:15 Uhr starten.
        (default: 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 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.

      • ctrlBackupFilesKeep <Ganzzahl>
        Legt die Anzahl der Generationen von Sicherungsdateien (siehe set <name> operatingMemory backup) fest.
        (default: 3)

      • ctrlBatSocManagement lowSoc=<Wert> upSoC=<Wert> [maxSoC=<Wert>] [careCycle=<Wert>]

        Sofern ein Batterie Device (currentBatteryDev) installiert ist, aktiviert dieses Attribut das Batterie SoC-Management.
        Das Reading Battery_OptimumTargetSoC enthält den vom Modul berechneten optimalen Mindest-SoC.
        Das Reading Battery_ChargeRequest wird auf '1' gesetzt, wenn der aktuelle SoC unter den Mindest-SoC gefallen ist.
        In diesem Fall sollte die Batterie, unter Umständen mit Netzstrom, zwangsgeladen werden.
        Die Readings können zur Steuerung des SoC (State of Charge) sowie zur Steuerung des verwendeten Ladestroms der Batterie verwendet werden.
        Durch das Modul selbst findet keine Steuerung der Batterie statt.

          lowSoc unterer Mindest-SoC, die Batterie wird nicht tiefer als dieser Wert entladen (> 0)
          upSoC oberer Mindest-SoC, der übliche Wert des optimalen SoC bewegt sich zwischen 'lowSoC'
          und diesem Wert.
          maxSoC maximaler Mindest-SoC, SoC Wert der mindestens im Abstand von 'careCycle' Tagen erreicht
          werden muß um den Ladungsausgleich im Speicherverbund auszuführen.
          Die Angabe ist optional (<= 100, default: 95)
          careCycle maximaler Abstand in Tagen, der zwischen zwei Ladungszuständen von mindestens 'maxSoC'
          auftreten darf. Die Angabe ist optional (default: 20)

        Alle Werte sind ganze Zahlen in %. Dabei gilt: 'lowSoc' < 'upSoC' < 'maxSoC'.
        Die Ermittlung des optimalen SoC erfolgt nach folgendem Schema:

        1. Ausgehend von 'lowSoc' wird der Mindest-SoC am folgenden Tag um 5%, aber nicht höher als
        'upSoC' inkrementiert, sofern am laufenden Tag 'maxSoC' nicht erreicht wurde.
        2. Wird am laufenden Tag 'maxSoC' (wieder) erreicht, wird Mindest-SoC um 5%, aber nicht tiefer als 'lowSoc', verringert.
        3. Mindest-SoC wird soweit verringert, dass die prognostizierte PV Energie des aktuellen bzw. des folgenden Tages
        von der Batterie aufgenommen werden kann. Mindest-SoC wird nicht tiefer als 'lowSoc' verringert.
        4. Das Modul erfasst den letzten Zeitpunkt am 'maxSoC'-Level, um eine Ladung auf 'maxSoC' mindestens alle 'careCycle'
        Tage zu realisieren. Zu diesem Zweck wird der optimierte SoC in Abhängigkeit der Resttage bis zum nächsten
        'careCycle' Zeitpunkt derart verändert, dass durch eine tägliche 5% SoC-Steigerung 'maxSoC' am 'careCycle' Zeitpunkt
        rechnerisch erreicht wird. Wird zwischenzeitlich 'maxSoC' erreicht, beginnt der 'careCycle' Zeitraum erneut.

          Beispiel:
          attr <name> ctrlBatSocManagement lowSoc=10 upSoC=50 maxSoC=99 careCycle=25

      • 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 mit den berechneten Planungsdaten das Consumers, kann aber von dem Planungszeitraum abweichen.

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

          aiProcess Datenanreicherung und Trainingsprozess der KI Unterstützung
          aiData Datennutzung KI im Prognoseprozess
          apiCall Abruf API Schnittstelle ohne Datenausgabe
          apiProcess Abruf und Verarbeitung von API Daten
          batteryManagement Steuerungswerte des Batterie Managements (SoC)
          collectData detailliierte Datensammlung
          consumerPlanning Consumer Einplanungsprozesse
          consumerSwitching Operationen des internen Consumer Schaltmodul
          consumption Verbrauchskalkulation und -nutzung
          dwdComm Kommunikation mit Webseite oder Server des Deutschen Wetterdienst (DWD)
          epiecesCalc Berechnung des spezifischen Energieverbrauchs je Betriebsstunde und Verbraucher
          graphic Informationen der Modulgrafik
          notifyHandling Ablauf der Eventverarbeitung im Modul
          pvCorrectionRead Anwendung PV Korrekturfaktoren
          pvCorrectionWrite Berechnung PV Korrekturfaktoren
          radiationProcess Sammlung und Verarbeitung der Solarstrahlungsdaten
          saveData2Cache Datenspeicherung in internen Speicherstrukturen

      • ctrlGenPVdeviation
        Legt die Methode zur Berechnung der Abweichung von prognostizierter und realer PV Erzeugung fest. Das Reading Today_PVdeviation wird in Abhängigkeit dieser Einstellung erstellt.

          daily Berechnung und Erstellung von Today_PVdeviation erfolgt nach Sonnenuntergang (default)
          continuously Berechnung und Erstellung von Today_PVdeviation erfolgt fortlaufend

      • ctrlInterval <Sekunden>
        Wiederholungsintervall der Datensammlung.
        Unabhängig vom eingestellten Intervall erfolgt einige Sekunden vor dem Ende sowie nach dem Beginn einer vollen Stunde eine automatische Datensammlung.
        Ist ctrlInterval explizit auf "0" gesetzt, erfolgt keinerlei automatische Datensammlung und muss mit "get <name> data" extern 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 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

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

      • ctrlSolCastAPImaxReq
        (nur bei Verwendung Model SolCastAPI)

        Die Einstellung der maximal möglichen täglichen Requests an die SolCast API.
        Dieser Wert wird von SolCast vorgegeben und kann sich entsprechend des SolCast Lizenzmodells ändern.
        (default: 50)

      • ctrlSolCastAPIoptimizeReq
        (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 auszunutzen.
        (default: 0)

      • ctrlStatisticReadings
        Für die ausgewählten Kennzahlen und Indikatoren werden Readings mit dem Namensschema 'statistic_<Indikator>' erstellt. Auswählbare Kennzahlen / Indikatoren sind:

          allStringsFullfilled Erfüllungsstatus der fehlerfreien Generierung aller Strings
          conForecastTillNextSunrise Verbrauchsprognose von aktueller Stunde bis zum kommenden Sonnenaufgang
          currentAPIinterval das aktuelle Abrufintervall der SolCast API (nur Model SolCastAPI) in Sekunden
          currentRunMtsConsumer_XX die Laufzeit (Minuten) des Verbrauchers "XX" seit dem letzten Einschalten. (0 - Verbraucher ist aus)
          dayAfterTomorrowPVforecast liefert die Vorhersage der PV Erzeugung für Übermorgen (sofern verfügbar) ohne Autokorrektur (Rohdaten).
          daysUntilBatteryCare Tage bis zur nächsten Batteriepflege (Erreichen der Ladung 'maxSoC' aus Attribut ctrlBatSocManagement)
          lastretrieval_time der letzte Abrufzeitpunkt der API (nur Model SolCastAPI, ForecastSolarAPI)
          lastretrieval_timestamp der Timestamp der letzen Abrufzeitpunkt der API (nur Model SolCastAPI, ForecastSolarAPI)
          response_message die letzte Statusmeldung der API (nur Model SolCastAPI, ForecastSolarAPI)
          runTimeCentralTask die Laufzeit des letzten SolarForecast Intervalls (Gesamtprozess) in Sekunden
          runTimeTrainAI die Laufzeit des letzten KI Trainingszyklus in Sekunden
          runTimeLastAPIAnswer die letzte Antwortzeit des API Abrufs auf einen Request in Sekunden (nur Model SolCastAPI, ForecastSolarAPI)
          runTimeLastAPIProc die letzte Prozesszeit zur Verarbeitung der empfangenen API Daten (nur Model SolCastAPI, ForecastSolarAPI)
          SunMinutes_Remain die verbleibenden Minuten bis Sonnenuntergang des aktuellen Tages
          SunHours_Remain die verbleibenden Stunden bis Sonnenuntergang des aktuellen Tages
          todayConsumptionForecast Verbrauchsprognose pro Stunde des aktuellen Tages (01-24)
          todayConForecastTillSunset Verbrauchsprognose von aktueller Stunde bis Stunde vor Sonnenuntergang
          todayDoneAPIcalls die Anzahl der am aktuellen Tag ausgeführten API Calls (nur Model SolCastAPI, ForecastSolarAPI)
          todayDoneAPIrequests die Anzahl der am aktuellen Tag ausgeführten API Requests (nur Model SolCastAPI, ForecastSolarAPI)
          todayGridConsumption die aus dem öffentlichen Netz bezogene Energie am aktuellen Tag
          todayGridFeedIn die in das öffentliche Netz eingespeiste PV Energie am aktuellen Tag
          todayMaxAPIcalls die maximal mögliche Anzahl SolCast API Calls (nur Model SolCastAPI).
          Ein Call kann mehrere API Requests enthalten.
          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)
          todayBatIn die am aktuellen Tag in die Batterie geladene Energie
          todayBatOut die am aktuellen Tag aus der Batterie entnommene Energie


      • ctrlUserExitFn {<Code>}
        Nach jedem Zyklus (siehe Attribut ctrlInterval ) wird der in diesem Attribut abgegebene Code ausgeführt. Der Code ist in geschweifte Klammern {...} einzuschließen.
        Dem Code werden die Variablen $name und $hash übergeben, die den Namen des SolarForecast Device und dessen Hash enthalten.
        Im SolarForecast Device können Readings über die Funktion storeReading erzeugt und geändert werden.

          Beispiel:
          {
          my $batdev = (split " ", ReadingsVal ($name, 'currentBatteryDev', ''))[0];
          my $pvfc = ReadingsNum ($name, 'RestOfDayPVforecast', 0);
          my $cofc = ReadingsNum ($name, 'RestOfDayConsumptionForecast', 0);
          my $diff = $pvfc - $cofc;

          storeReading ('userFn_Battery_device', $batdev);
          storeReading ('userFn_estimated_surplus', $diff);
          }

      • ctrlWeatherDevX

        Gibt das Gerät oder die API zur Lieferung der erforderlichen Wetterdaten (Wolkendecke, Niederschlag usw.) an.
        Das Attribut 'ctrlWeatherDev1' definiert den führenden Wetterdienst und ist zwingend erforderlich.
        Ist eine Open-Meteo API im Attribut 'ctrlWeatherDev1' ausgewählt, wird dieser Open-Meteo Dienst automatisch auch als Quelle der Strahlungsdaten (Setter currentRadiationAPI) eingestellt.

        OpenMeteoDWD-API
        Open-Meteo ist eine Open-Source-Wetter-API und bietet kostenlosen Zugang für nicht-kommerzielle Zwecke. Es ist kein API-Schlüssel erforderlich. Open-Meteo nutzt eine leistungsstarke Kombination aus globalen (11 km) und mesoskaligen (1 km) Wettermodellen von angesehenen nationalen Wetterdiensten. Diese API bietet Zugang zu den renommierten ICON-Wettermodellen des Deutschen Wetterdienstes (DWD), die 15-minütige Daten für kurzfristige Vorhersagen in Mitteleuropa und globale Vorhersagen mit einer Auflösung von 11 km liefern. Das ICON-Modell ist eine bevorzugte Wahl für allgemeine Wettervorhersage-APIs, wenn keine anderen hochauflösenden Wettermodelle verfügbar sind. Es werden die Modelle DWD Icon D2, DWD Icon EU und DWD Icon Global zu einer nahtlosen Vorhersage zusammengeführt. Auf der Webseite des Dienstes ist die umfangreiche und übersichtliche API Dokumentation verfügbar.

        OpenMeteoDWDEnsemble-API
        Diese Open-Meteo API Variante bietet Zugang zum globalen Ensemble-Vorhersagesystem (EPS) des DWD.
        Es werden die Ensemble Modelle ICON-D2-EPS, ICON-EU-EPS und ICON-EPS nahtlos vereint.
        Ensemble-Wetterprognosen sind eine spezielle Art von Vorhersagemethode, die die Unsicherheiten bei der Wettervorhersage berücksichtigt. Sie tun dies, indem sie mehrere Simulationen oder Modelle mit leichten Unterschieden in den Startbedingungen oder Einstellungen ausführen. Jede Simulation, bekannt als Ensemblemitglied, stellt ein mögliches Ergebnis des Wetters dar. In der vorliegenden Implementierung werden 40 Ensemblemitglieder pro Wettermerkmal zusammengeführt und das wahrscheinlichste Ergbnis verwendet.

        OpenMeteoWorld-API
        Als Variante des Open-Meteo Dienstes liefert die OpenMeteoWorld-API die optimale Vorhersage für einen bestimmten Ort weltweit. Die OpenMeteoWorld-API vereint nahtlos Wettermodelle bekannter Organisationen wie NOAA (National Oceanic and Atmospheric Administration), DWD (Deutscher Wetterdienst), CMCC (Canadian) und ECMWF (Europäisches Zentrum für mittelfristige Wettervorhersage). Für jeden Ort weltweit werden die Modelle der Anbieter kombiniert, um die bestmögliche Vorhersage zu erstellen. Die Nutzung der Dienste und Wettermodelle erfolgt automatisch anhand der im API Aufruf enthaltenen Standortkoordinaten.

        DWD Gerät
        Alternativ zu Open-Meteo kann ein FHEM 'DWD_OpenData'-Gerät zur Lieferung der Wetterdaten dienen.
        Ist noch kein Gerät dieses Typs vorhanden, muß zunächst mindestens ein DWD_OpenData Gerät definiert werden (siehe DWD_OpenData Commandref).
        Sind mehr als ein ctrlWeatherDevX angegeben, wird der Durchschnitt aller Wetterstationen ermittelt sofern der jeweilige Wert geliefert wurde und numerisch ist.
        Anderenfalls werden immer die Daten von 'ctrlWeatherDev1' als führendes Wetterdevice genutzt.
        Im ausgewählten DWD_OpenData Gerät müssen mindestens diese Attribute gesetzt sein:

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

        Hinweis: Sind die Attribute latitude und longitude im global Device gesetzt, ergibt sich der Sonnenauf- und Sonnenuntergang aus diesen Angaben.

      • flowGraphicCss
        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.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; }

      • flowGraphicAnimate
        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. Siehe auch Attribut flowGraphicShowConsumer.
        (default: 130)

      • flowGraphicShowConsumer
        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.
        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.
        (default: 1)

      • flowGraphicShowConsumerRemainTime
        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. Siehe auch Attribut graphicSelect.
        (default: 400)

      • graphicBeam1Color
        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. 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".

      • graphicBeam2FontColor
        Auswahl der Schriftfarbe des sekundären Balken.
        (default: 000000)

      • graphicBeam2Content
        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. In Verbindung mit "graphicHourCount" lassen sich damit auch recht kleine Grafikausgaben erzeugen.
        (default: 200)

      • graphicBeamWidth <value>
        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. Die Kilowattstunde wird auf eine Nachkommastelle gerundet.
        (default: Wh)

      • graphicHeaderDetail
        Auswahl der anzuzeigenden Zonen des Grafik Kopfbereiches.
        (default: all)
          all alle Zonen des Kopfbereiches (default)
          co Verbrauchsbereich anzeigen
          pv Erzeugungsbereich anzeigen
          own Nutzerzone (siehe graphicHeaderOwnspec)
          status Bereich der Statusinformationen

      • graphicHeaderOwnspec <Label>:<Reading>[@Device] <Label>:<Set>[@Device] <Label>:<Attr>[@Device] ...
        Anzeige beliebiger Readings, Set-Kommandos und Attribute des SolarForecast Devices im Grafikkopf.
        Durch Angabe des optionalen [@Device] können Readings, Set-Kommandos und Attribute anderer Devices angezeigt werden.
        Die anzuzeigenden Werte werden durch Leerzeichen getrennt. Es werden vier Werte (Felder) pro Zeile dargestellt.
        Die Eingabe kann mehrzeilig erfolgen. Werte mit den Einheiten "Wh" bzw. "kWh" werden entsprechend der Einstellung des Attributs graphicEnergyUnit umgerechnet.

        Jeder Wert ist jeweils durch ein Label und das dazugehörige Reading verbunden durch ":" zu definieren.
        Leerzeichen im Label sind durch "&nbsp;" einzufügen, ein Zeilenumbruch durch "<br>".
        Ein leeres Feld in einer Zeile wird durch ":" erzeugt.
        Ein Zeilentitel kann durch Angabe von "#:<Text>" eingefügt werden, ein leerer Titel durch die Eingabe von "#".

          Beispiel:
          attr <name> graphicHeaderOwnspec #
          AutarkyRate:Current_AutarkyRate
          Überschuß:Current_Surplus
          aktueller&nbsp;Netzbezug:Current_GridConsumption
          :
          #
          CO&nbsp;bis&nbsp;Sonnenuntergang:statistic_todayConForecastTillSunset
          PV&nbsp;Übermorgen:statistic_dayAfterTomorrowPVforecast
          InverterRelay:gridrelay_status@MySTP_5000
          :
          #Batterie
          in&nbsp;heute:statistic_todayBatIn
          out&nbsp;heute:statistic_todayBatOut
          :
          :
          #Settings
          Autokorrektur:pvCorrectionFactor_Auto : : :
          Consumer<br>Neuplanung:consumerNewPlanning : : :
          Consumer<br>Sofortstart:consumerImmediatePlanning : : :
          Wetter:graphicShowWeather : : :
          History:graphicHistoryHour : : :
          GraphicSize:flowGraphicSize : : :
          ShowNight:graphicShowNight : : :
          Debug:ctrlDebug : : :

      • graphicHeaderOwnspecValForm
        Die mit dem Attribut graphicHeaderOwnspec anzuzeigenden Readings können mit sprintf und anderen Perl Operationen manipuliert werden.
        Es stehen zwei grundsätzliche, miteinander nicht kombinierbare Möglichkeiten der Notation zur Verfügung.
        Die Angabe der Notationen erfolgt grundsätzlich innerhalb von zwei geschweiften Klammern {...}.

        Notation 1:
        Eine einfache Formatierung von Readings des eigenen Devices mit sprintf erfolgt wie in Zeile 'Current_AutarkyRate' bzw. 'Current_GridConsumption' angegeben.
        Andere Perl Operationen sind mit () zu klammern. Die jeweiligen Readingswerte und Einheiten stehen über die Variablen $VALUE und $UNIT zur Verfügung.
        Readings anderer Devices werden durch die Angabe '<Device>.<Reading>' spezifiziert.

          {
          'Current_AutarkyRate' => "%.1f %%",
          'Current_GridConsumption' => "%.2f $UNIT",
          'SMA_Energymeter.Cover_RealPower' => q/($VALUE)." W"/,
          'SMA_Energymeter.L2_Cover_RealPower' => "($VALUE).' W'",
          'SMA_Energymeter.L1_Cover_RealPower' => '(sprintf "%.2f", ($VALUE / 1000))." kW"',
          }

        Notation 2:
        Die Manipulation von Readingwerten und Einheiten erfolgt über Perl If ... else Strukturen.
        Der Struktur stehen Device, Reading, Readingwert und Einheit mit den Variablen $DEVICE, $READING, $VALUE und $UNIT zur Verfügung.
        Bei Änderung der Variablen werden die neuen Werte entsprechend in die Anzeige übernommen.

          {
          if ($READING eq 'Current_AutarkyRate') {
             $VALUE = sprintf "%.1f", $VALUE;
             $UNIT = "%";
          }
          elsif ($READING eq 'Current_GridConsumption') {
             ...
          }
          }

      • graphicHeaderShow
        Anzeigen/Verbergen des Grafik Tabellenkopfes mit Prognosedaten sowie bestimmten aktuellen und statistischen Werten.
        (default: 1)

      • graphicHistoryHour
        Anzahl der vorangegangenen Stunden die in der Balkengrafik dargestellt werden.
        (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.

          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 anzuzeigenden Grafiksegmente des Moduls aus.
        Zur Anpassung der Energieflußgrafik steht neben den flowGraphic.*-Attributen auch das Attribut flowGraphicCss zur Verfügung.

          both zeigt den Header, die Verbraucherlegende, Energiefluß- und Vorhersagegrafik an (default)
          flow zeigt den Header, die Verbraucherlegende und Energieflußgrafik an
          forecast zeigt den Header, die Verbraucherlegende und die Vorhersagegrafik an
          none zeigt nur den Header und die Verbraucherlegende an

      • graphicShowDiff <no | top | bottom>
        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.
        (default: 0)

      • graphicShowWeather
        Wettericons in der Balkengrafik anzeigen/verbergen.
        (default: 1)

      • graphicStartHtml <HTML-String>
        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.

      • 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ß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 =for :application/json;q=META.json 76_SolarForecast.pm { "abstract": "Creation of solar forecasts of PV systems including consumption forecasts and consumer management", "x_lang": { "de": { "abstract": "Erstellung solarer Vorhersagen von PV Anlagen inklusive Verbrauchsvorhersagen und Verbrauchermanagement" } }, "keywords": [ "inverter", "photovoltaik", "electricity", "forecast", "graphics", "Autarky", "Consumer", "PV" ], "version": "v1.1.1", "release_status": "stable", "author": [ "Heiko Maaz " ], "x_fhem_maintainer": [ "DS_Starter" ], "x_fhem_maintainer_github": [ "nasseeder1" ], "prereqs": { "runtime": { "requires": { "FHEM": 5.00918799, "perl": 5.014, "POSIX": 0, "GPUtils": 0, "Encode": 0, "Blocking": 0, "Color": 0, "utf8": 0, "HttpUtils": 0, "JSON": 4.020, "FHEM::SynoModules::SMUtils": 1.0270, "Time::HiRes": 0, "MIME::Base64": 0, "Storable": 0 }, "recommends": { "FHEM::Meta": 0, "FHEM::Utility::CTZ": 1.00, "DateTime": 0, "DateTime::Format::Strptime": 0, "AI::DecisionTree": 0, "Data::Dumper": 0 }, "suggests": { } } }, "resources": { "x_wiki": { "web": "https://wiki.fhem.de/wiki/SolarForecast_-_Solare_Prognose_(PV_Erzeugung)_und_Verbrauchersteuerung", "title": "SolarForecast - Solare Prognose (PV Erzeugung) und Verbrauchersteuerung" }, "repository": { "x_dev": { "type": "svn", "url": "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter", "web": "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter/76_SolarForecast.pm", "x_branch": "dev", "x_filepath": "fhem/contrib/", "x_raw": "https://svn.fhem.de/fhem/trunk/fhem/contrib/DS_Starter/76_SolarForecast.pm" } } } } =end :application/json;q=META.json =cut