mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-04-26 16:19:32 +00:00
27160 lines
1.4 MiB
27160 lines
1.4 MiB
########################################################################################################################
|
||
# $Id$
|
||
#########################################################################################################################
|
||
# 76_SolarForecast.pm
|
||
#
|
||
# (c) 2020-2025 by Heiko Maaz e-mail: Heiko dot Maaz at t-online dot de
|
||
# with credits to: kask, Prof. Dr. Peter Henning, Wzut, ch.eick (and much more FHEM users)
|
||
#
|
||
# 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 <http://www.gnu.org/licenses/>.
|
||
#
|
||
# 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);
|
||
use Math::Trig;
|
||
use List::Util qw(max);
|
||
|
||
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 Test::Memory::Usage; # https://metacpan.org/pod/Test::Memory::Usage
|
||
|
||
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' # Debian: sudo apt-get install libai-decisiontree-perl
|
||
|
||
use FHEM::SynoModules::ErrCodes qw(:all); # Error Code Modul
|
||
use FHEM::SynoModules::SMUtils qw (checkModVer
|
||
delClHash
|
||
evaljson
|
||
getClHash
|
||
moduleVersion
|
||
trim
|
||
); # Hilfsroutinen Modul
|
||
|
||
use Data::Dumper;
|
||
use Blocking;
|
||
use Storable qw(dclone freeze thaw nstore store retrieve);
|
||
use MIME::Base64;
|
||
|
||
# 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
|
||
GetFileFromURL
|
||
GetHttpFile
|
||
init_done
|
||
InternalTimer
|
||
InternalVal
|
||
IsDisabled
|
||
Log
|
||
Log3
|
||
modules
|
||
parseParams
|
||
perlSyntaxCheck
|
||
readingsSingleUpdate
|
||
readingsBulkUpdate
|
||
readingsBulkUpdateIfChanged
|
||
readingsBeginUpdate
|
||
readingsDelete
|
||
readingsEndUpdate
|
||
ReadingsNum
|
||
ReadingsTimestamp
|
||
ReadingsVal
|
||
RemoveInternalTimer
|
||
ReplaceEventMap
|
||
readingFnAttributes
|
||
setKeyValue
|
||
sunrise_abs_dat
|
||
sunset_abs_dat
|
||
FW_cmd
|
||
FW_directNotify
|
||
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.45.6" => "12.02.2025 Notification System: print out next planned file pull, timestringsFromOffset: allow +- offsets ".
|
||
"new sub _calcConsumptionForecast_circular to prepare the evaluation of consumption days in pvCircular ",
|
||
"1.45.5" => "09.02.2025 change constant GMFILEREPEAT, GMFILERANDOM, Pull Message File from GitHub Repo ",
|
||
"1.45.4" => "08.02.2025 change constant GMFILEREPEAT + new constant GMFILERANDOM ",
|
||
"1.45.3" => "06.02.2025 __readDataWeather: if no values of hour 01 (00:00+) use val of hour 24 of day before ".
|
||
"new special reading todayConsumption ",
|
||
"1.45.2" => "05.02.2025 aiAddRawData: temp, con, wcc, rr1c, rad1h = undef if no value in pvhistory, fix isWeatherDevValid ".
|
||
"__readDataWeather(API): fix no values of hour 01 (00:00+), weather_ids: more weather entries ".
|
||
"change weather display management (don), some minor bugfixes ",
|
||
"1.45.1" => "02.02.2025 _specialActivities: Task 1 __deleteEveryHourControls changed, all Tasks adapted ".
|
||
"_retrieveMessageFile: fix path in __updWriteFile, fix https://forum.fhem.de/index.php?msg=1332721 ",
|
||
"1.45.0" => "01.02.2025 new function timestringsFromOffset, _batChargeRecmd: change condition for load release ".
|
||
"_addHourAiRawdata: add hour 24 (of day before), remove x-migrate -> auto migrate pv data ".
|
||
"Pool output width limited to 140 characters, checkPlantConfig: add installen Perl Modules check ",
|
||
"1.44.5" => "30.01.2025 temp2bin: expand to more negative bins, bugfix: https://forum.fhem.de/index.php?msg=1332421 ".
|
||
"expand data collection dayname, con for AI support, Task 1: delete readings Error, Errorcode ",
|
||
"1.44.4" => "26.01.2025 _getlistPVCircular: change width of output, new sub _listDataPoolPvHist, fix bug in hrepl Hash ".
|
||
"remove Attr graphicBeam1MaxVal,ctrlAreaFactorUsage ",
|
||
"1.44.3" => "25.01.2025 Notification System: minor changes, special Readings todayBatInSum todayBatOutSum ",
|
||
"1.44.2" => "23.01.2025 _batChargeRecmd: user storeffdef, show historical battery SoC when displaying the battery in the bar graph ",
|
||
"1.44.1" => "20.01.2025 Notification system: minor fixes, integration of controls_solarforecast_messages_test/prod ".
|
||
"Define: random start of Timer subs, consumerXX: consumer device may have specified an own alias ",
|
||
"1.44.0" => "19.01.2025 _listDataPoolCircular: may select a dedicated hour, add temporary Migrate funktion x_migrate ".
|
||
"fix interruptable key check in consumer attr Forum:https://forum.fhem.de/index.php?msg=1331073 ".
|
||
"set prdef to 1.0, Implementation of a Messaging System ",
|
||
"1.43.6" => "17.01.2025 _calcCaQcomplex: additional write pvrl, pvfc to separate circular hash Array elements, listDataPool: show these Arrays ",
|
||
"1.43.5" => "15.01.2025 _flowGraphic: calculate the resulting SoC as a cluster of batteries ",
|
||
"1.43.4" => "14.01.2025 batsocslidereg: calculate the SoC as summary over all capacities in Wh, bugfix https://forum.fhem.de/index.php?msg=1330559 ",
|
||
"1.43.3" => "13.01.2025 add Wiki icon in graphic header, _calcConsumptionForecast: switch calc from average to median, edit comref ",
|
||
"1.43.2" => "12.01.2025 _batChargeRecmd: bugfix calc socwh, Attr graphicBeam1MaxVal, (experimental) ctrlAreaFactorUsage are obsolete ".
|
||
"trackFlex now default in DWD Model, replace title Charging recommendation by Charging release ".
|
||
"_saveEnergyConsumption: add dowrite flag, edit comref ",
|
||
"1.43.1" => "11.01.2025 _batChargeRecmd: bugfix PV daily surplus update, _collectAllRegConsumers: fix interruptable hysteresis ".
|
||
"__batteryOnBeam: show soc forecast for hour 00 and fix english translation ".
|
||
"_batChargeRecmd: consider battery capacity as part of total capacity ",
|
||
"1.43.0" => "10.01.2025 graphicShowNight: add possible Time Sync of chart bar level 1 and the other ".
|
||
"_addDynAttr: minor fix for graphicBeamXContent, new attr ctrlNextHoursSoCForecastReadings ",
|
||
"1.42.0" => "07.01.2025 change socslidereg to batsocslidereg, _batChargeRecmd: add value to nexthours ".
|
||
"entryGraphic: enrich hfcg hash, __normDecPlaces: use it from/to battery, ".
|
||
"setupBatteryDevXX : new icon & show key, colour of icon can be changed separately, maxbatteries set to 3 ".
|
||
"medianArray: switch to simpel array sort, Task 1: delete Weather-API status data at night ".
|
||
"add SoC forecast to NextHours store, Battery bar chart: display of the device in the bar chart level 1 or 2 ".
|
||
"add batsocXX to pvHistory, add batsocforecast_XX to Attr graphicBeamXContent ".
|
||
" _addDynAttr: add graphicBeamXContent at runtime, attr ctrlBackupFilesKeep can set to 0 ",
|
||
"1.41.4" => "02.01.2025 minor change of Logtext, new special Readings BatPowerIn_Sum, BatPowerOut_Sum ".
|
||
"rename ctrlStatisticReadings to ctrlSpecialReadings ",
|
||
"1.41.3" => "01.01.2025 write/read battery values 0 .. maxbatteries to/from pvhistrory ".
|
||
"change ctrlBatSocManagement to ctrlBatSocManagement01 ",
|
||
"1.41.2" => "30.12.2024 __setConsRcmdState: more Debug Info, change Reading: Current_BatCharge -> Current_BatCharge_XX ".
|
||
"Current_PowerBatOut -> Current_PowerBatOut_XX, Current_PowerBatIn -> Current_PowerBatIn_XX ".
|
||
"Today_HourXX_PPrealXX -> Today_HourXX_PPreal_XX, Current_PPXX -> Current_PP_XX ".
|
||
"Battery_OptimumTargetSoC -> Battery_OptimumTargetSoC_XX, Battery_ChargeRequest -> Battery_ChargeRequest_XX ".
|
||
"Battery_ChargeRecommended -> Battery_ChargeRecommended_XX ".
|
||
"Today_HourXX_BatIn -> Today_HourXX_BatIn_XX, Today_HourXX_BatOut -> Today_HourXX_BatOut_XX ",
|
||
"1.41.1" => "29.12.2024 ctrlStatisticReadings: change daysUntilBatteryCare to daysUntilBatteryCare_XX until max batteries ".
|
||
"todayBatIn to todayBatIn_XX until max batteries, todayBatOut to todayBatOut_XX until max batteries ",
|
||
"1.41.0" => "28.12.2024 _batSocTarget: minor code change, change setupBatteryDev to setupBatteryDev01, getter valBattery ",
|
||
"1.40.0" => "21.12.2024 new consumer key 'surpmeth' to calculate surplus in various variants for consumer switching ",
|
||
"1.39.8" => "21.12.2024 prepare of new consumer key 'surpmeth', _batSocTarget: improve care SoC management when dark doldrums ",
|
||
"1.39.7" => "18.12.2024 ConsumptionRecommended calc method medianArray, change local owndata to global data ",
|
||
"1.39.6" => "17.12.2024 replace global data-store by local owndata-store, remove sub _composeRemoteObj, delHashRefDeep removed ".
|
||
"add current key (array) 'surplusslidereg' + sub avgArray, batteryManagement: fix care SoC management ",
|
||
"1.39.5" => "12.12.2024 __createAdditionalEvents: Warning in fhem Log if 'AllPVforecastsToEvent' events not created ".
|
||
"Notify: create cetralTask Events ",
|
||
"1.39.4" => "10.12.2024 fix Check Rooftop and Roof Ident Pair Settings (SolCast) ",
|
||
"1.39.3" => "09.12.2024 fix mode in consumerXX-Reading if mode is device/reading combination, show Mode in ".
|
||
"consumer legend mouse-over ",
|
||
"1.39.2" => "08.12.2024 rollout delHashRefDeep, extended consumer key 'mode' by device/reading combination ",
|
||
"1.39.1" => "07.12.2024 new control releaseCentralTask, new delHashRefDeep in some cases ".
|
||
"possible asynchron mode for setupBatteryDev ",
|
||
"1.39.0" => "04.12.2024 possible asynchron mode for setupMeterDev, setupInverterDevXX ".
|
||
"include FHEM::SynoModules::ErrCodes ",
|
||
"1.38.0" => "30.11.2024 optimize data handling, rename getter solApiData to radiationApiData, ".
|
||
"set setupStringAzimuth, setupStringDeclination is checked due to dependencies to OpenMeteo ".
|
||
"attr setupRadiationAPI and setupWeatherDev1 can be set largely independently of each other ".
|
||
"rename sub SolCastAPIVal to RadiationAPIVal ",
|
||
"1.37.9" => "29.11.2024 activate StatusAPI-Hash, Separation of radiation API-data, API-state data, weather-API data ",
|
||
"1.37.8" => "28.11.2024 edit commref, func searchCacheFiles for renaming Cache files when device is renamed ".
|
||
"_saveEnergyConsumption: extended for Debug collectData, preparation of weatherApiData ".
|
||
"new func WeatherAPIVal, StatusAPIVal ",
|
||
"1.37.7" => "26.11.2024 Attr flowGraphicControl: key shift changed to shiftx, new key shifty ".
|
||
"change: 'trackFlex' && \$wcc >= 70 to \$wcc >= 80 ".
|
||
"obsolete Attr deleted: flowGraphicCss, flowGraphicSize, flowGraphicAnimate, flowGraphicConsumerDistance, ".
|
||
"flowGraphicShowConsumer, flowGraphicShowConsumerDummy, flowGraphicShowConsumerPower, ".
|
||
"flowGraphicShowConsumerRemainTime, flowGraphicShift, affect70percentRule, ctrlAutoRefresh, ctrlAutoRefreshFW ",
|
||
"1.37.6" => "01.11.2024 minor code change, Attr setupBatteryDev: the key 'cap' is mandatory now ",
|
||
"1.37.5" => "31.10.2024 attr setupInverterDevXX: new key 'limit', the key 'capacity' is now mandatory ".
|
||
"Attr affect70percentRule, ctrlAutoRefresh, ctrlAutoRefreshFW deleted ",
|
||
"1.37.4" => "29.10.2024 both attr graphicStartHtml, graphicEndHtml removed, fix flowGraphic when device name contains '.' ",
|
||
"1.37.3" => "25.10.2024 _flowGraphic: grid, dummy and battery displacement by kask ".
|
||
"Attr flowGraphicControl: new key h2consumerdist, animate=1 is default now ",
|
||
"1.37.2" => "24.10.2024 _flowGraphic: show Producer Row only if more than one Producer is defined ",
|
||
"1.37.1" => "23.10.2024 state: 'The setup routine is still incomplete' if setup is incomplete ".
|
||
"change: 'trackFlex' && \$wcc >= 80 to \$wcc >= 70, implement Rename function ".
|
||
"_flowGraphic: eliminate numbers in device name - Forum: https://forum.fhem.de/index.php?msg=1323229 ",
|
||
"1.37.0" => "22.10.2024 attr setupInverterDevXX up to 03 inverters with accorded strings, setupInverterDevXX: keys strings and feed ".
|
||
"_flowGraphic: controlhash for producer, new attr flowGraphicControl and replace the attributes: ".
|
||
"flowGraphicAnimate flowGraphicConsumerDistance flowGraphicShowConsumer flowGraphicShowConsumerDummy ".
|
||
"flowGraphicShowConsumerPower flowGraphicShowConsumerRemainTime flowGraphicShift flowGraphicCss ".
|
||
"flowGraphicControl: new keys strokecolina, strokecolsig, strokecolstd, strokewidth ",
|
||
"1.36.1" => "14.10.2024 _flowGraphic: consumer distance modified by kask, Coloring of icons corrected when creating 0 ",
|
||
"1.36.0" => "13.10.2024 new Getter valInverter, valStrings and valProducer, preparation for multiple inverters ".
|
||
"rename setupInverterDev to setupInverterDev01, new attr affectConsForecastLastDays ".
|
||
"Model DWD: dayAfterTomorrowPVforecast now available ".
|
||
"delete etotal from HistoryVal, _flowGraphic: move PV Icon up to the producers row ".
|
||
"change sequence of _createSummaries in centraltask - Forum: https://forum.fhem.de/index.php?msg=1322425 ",
|
||
"1.35.0" => "09.10.2024 _flowGraphic: replace inverter icon by FHEM SVG-Icon (sun/moon), sun or icon of moon phases according ".
|
||
"day/night new optional key 'icon' in attr setupInverterDev, resize all flowgraphic icons to a standard ".
|
||
"scaling, __switchConsumer: run ___setConsumerSwitchingState before switch subs ".
|
||
"no Readings pvCorrectionFactor_XX_autocalc are written anymore ".
|
||
"__switchConsumer: change Debug info and process, ___doPlanning: fix Log Output and use replanning or planning ",
|
||
"1.34.1" => "04.10.2024 _flowGraphic: replace house by FHEM SVG-Icon ",
|
||
"1.34.0" => "03.10.2024 implement ___areaFactorTrack for calculation of direct area factor and share of direct radiation ".
|
||
"note in Reading pvCorrectionFactor_XX if AI prediction was used in relevant hour ".
|
||
"AI usage depending either of available number of rules or difference to api forecast ".
|
||
"minor fix in ___readCandQ, new experimental attribute ctrlAreaFactorUsage ".
|
||
"optional icon in attr setupOtherProducerXX, integrate Producer to _flowGraphic (kask) ".
|
||
"don't show Consumer or Producer if it isn't defined any kind of it ".
|
||
"Optimization of space in the flow chart above generators and below consumers ".
|
||
"_beamGraphic: implement barcount to Limit the number of bars in level 2 if the number of bars in ".
|
||
"level 1 is less than graphicHourCount (fall/winter) ",
|
||
"1.33.1" => "27.09.2024 bugfix of 1.33.0, add aiRulesNumber to pvCircular, limits of AI trained datasets for ".
|
||
"AI use (aiAccTRNMin, aiSpreadTRNMin)",
|
||
"1.33.0" => "26.09.2024 substitute area factor hash by ___areaFactorFix function ",
|
||
"1.32.0" => "02.09.2024 new attr setupOtherProducerXX, report calculation and storage of negative consumption values ".
|
||
"Forum: https://forum.fhem.de/index.php?msg=1319083 ".
|
||
"bugfix in _calcConsumptionForecast, new ctrlDebug consumption_long ",
|
||
"1.31.0" => "20.08.2024 rename attributes ctrlWeatherDevX to setupWeatherDevX ",
|
||
"1.30.0" => "18.08.2024 new attribute flowGraphicShift, Forum:https://forum.fhem.de/index.php?msg=1318597 ",
|
||
"1.29.4" => "03.08.2024 delete writeCacheToFile from _getRoofTopData, _specialActivities: avoid loop caused by \@widgetreadings ",
|
||
"1.29.3" => "20.07.2024 eleminate hand over \$hash in _getRoofTopData routines, fix label 'gcon' to 'gcons' ",
|
||
"1.29.2" => "17.06.2024 ___readCandQ: improve manual setting of pvCorrectionFactor_XX ",
|
||
"1.29.1" => "17.06.2024 fix Warnings, Forum: https://forum.fhem.de/index.php?msg=1315283, fix roofIdentPair ",
|
||
"1.29.0" => "16.06.2024 _setreset: improve reset consumerMaster ".
|
||
"tranformed setter moduleAzimuth to setupStringAzimuth ".
|
||
"tranformed setter moduleDeclination to setupStringDeclination ".
|
||
"tranformed setter moduleRoofTops to setupRoofTops ",
|
||
"1.28.0" => "15.06.2024 new consumer key exconfc, Forum: https://forum.fhem.de/index.php?msg=1315111 ",
|
||
"1.27.0" => "12.06.2024 __VictronVRM_ApiResponseLogin: check token not empty ".
|
||
"transformed setter modulePeakString to attr setupStringPeak ",
|
||
"1.26.0" => "10.06.2024 transformed setter currentRadiationAPI to attr setupRadiationAPI ",
|
||
"1.25.2" => "09.06.2024 _specialActivities: change delete readings exec ",
|
||
"1.25.1" => "08.06.2024 Illegal division by zero Forum:https://forum.fhem.de/index.php?msg=1314730 ",
|
||
"1.25.0" => "05.06.2024 transformed setter inverterStrings to attr setupInverterStrings, _calcTodayPVdeviation: fix continuously calc again ",
|
||
"1.24.0" => "03.06.2024 transformed setter currentInverterDev to attr setupInverterDev, _calcTodayPVdeviation: fix continuously calc ",
|
||
"1.23.0" => "02.06.2024 transformed setter currentBatteryDev to attr setupBatteryDev, _transferInverterValues: change output for DEBUG ".
|
||
"new key attrInvChangedTs in circular, prepare transformation of currentInverterDev ".
|
||
"_calcTodayPVdeviation: fix daily calc ",
|
||
"1.22.0" => "01.06.2024 transformed setter currentMeterDev to attr setupMeterDev, plantConfiguration: setModel after restore ".
|
||
"delete reset currentMeterSet ",
|
||
"1.21.5" => "30.05.2024 listDataPool: list current can operate three hash levels, first preparation for remote objects ",
|
||
"1.21.4" => "28.05.2024 __getCyclesAndRuntime: rename numberDayStarts to cycleDayNum ".
|
||
"currentRunMtsConsumer_XX: edit commandref, Consumers: replace avgruntime by runtimeAvgDay ".
|
||
"ctrlStatisticReadings: new runTimeAvgDayConsumer_XX, pvHistory: new key avgcycmntscsmXX",
|
||
"1.21.3" => "27.05.2024 __getCyclesAndRuntime: change procedure determine consumer runtime and cycles per day ".
|
||
"__calcPVestimates: correct printout 'Estimated PV generation (calc)' and '(raw)' ".
|
||
"ctrlDebug: consumerSwitching splitted into separated consumers ",
|
||
"1.21.2" => "26.05.2024 __VictronVRM_ApiRequestForecast: change request time from current time to '<current hour>:00:00'",
|
||
"1.21.1" => "23.05.2024 new sub isDeviceValid, replace Smartmatch Forum:#137776 ",
|
||
"1.21.0" => "14.05.2024 currentMeterDev: meter can be a Day meter, contotal and feedtotal can be reset at day begin ",
|
||
"1.20.0" => "12.05.2024 graphicBeamXContent: gridfeedin available, beamGraphic: Mouse-Over shows beamcontent text ".
|
||
"complete command printout in Debug mode, ___switchConsumerOn: add continuing ",
|
||
"1.19.0" => "11.05.2024 conprice, feedprice saved in pvHistory, graphicBeamXContent: energycosts, feedincome available ",
|
||
"1.18.0" => "08.05.2024 add secondary level of the bar chart, new attr graphicBeam3Content, graphicBeam4Content ".
|
||
"graphicBeam3Color, graphicBeam4Color, graphicBeam3FontColor, graphicBeam4FontColor ".
|
||
"value consumption available for attr graphicBeamXContent ".
|
||
"rename graphicBeamHeight to graphicBeamHeightLevel1 ",
|
||
"1.17.12"=> "06.05.2024 attr ctrlInterval: immediate impact when set ",
|
||
"1.17.11"=> "04.05.2024 correction in commandref, delete attr affectMaxDayVariance ",
|
||
"1.17.10"=> "19.04.2024 _calcTodayPVdeviation: avoid Illegal division by zero, Forum: https://forum.fhem.de/index.php?msg=1311121 ",
|
||
"1.17.9" => "17.04.2024 _batSocTarget: fix Illegal division by zero, Forum: https://forum.fhem.de/index.php?msg=1310930 ",
|
||
"1.17.8" => "16.04.2024 _calcTodayPVdeviation: change of calculation ",
|
||
"1.17.7" => "09.04.2024 export pvHistory to CSV, making attr affectMaxDayVariance obsolete ",
|
||
"1.17.6" => "07.04.2024 new sub writeToHistory with many internal changes in pvHistory write process ".
|
||
"_transferInverterValues: react on inverter etotal behavior ",
|
||
"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=<Devicename>:<Readingname>:<Einheit>] [feedprice=<Devicename>:<Readingname>:<Einheit>] ".
|
||
"___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 aiAddInstancePV, 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 <sunalt2bin>.<cloud2bin> 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, rr1c 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 ",
|
||
"0.1.0" => "09.12.2020 initial Version "
|
||
);
|
||
|
||
## Konstanten
|
||
######################
|
||
use constant {
|
||
LPOOLLENLIM => 140, # Breitenbegrenzung der Ausgabe von List Pooldaten
|
||
KJ2KWH => 0.0002777777778, # Umrechnungsfaktor kJ in kWh
|
||
KJ2WH => 0.2777777778, # Umrechnungsfaktor kJ in Wh
|
||
WH2KJ => 3.6, # Umrechnungsfaktor Wh in kJ
|
||
DEFLANG => 'EN', # default Sprache wenn nicht konfiguriert
|
||
DEFMAXVAR => 0.75, # max. Varianz pro Tagesberechnung Autokorrekturfaktor (geändert V.45.0 mit Median Verfahren)
|
||
DEFINTERVAL => 70, # Standard Abfrageintervall
|
||
SLIDENUMMAX => 3, # max. Anzahl der Arrayelemente in Schieberegistern
|
||
SPLSLIDEMAX => 20, # max. Anzahl der Arrayelemente in Schieberegister PV Überschuß
|
||
WHISTREPEAT => 851, # Wiederholungsintervall Cache File Daten schreiben
|
||
EPIECMAXCYCLES => 10, # Anzahl Einschaltzyklen (Consumer) für verbraucherspezifische Energiestück Ermittlung
|
||
|
||
MAXWEATHERDEV => 3, # max. Anzahl Wetter Devices (Attr setupWeatherDevX)
|
||
MAXBATTERIES => 3, # maximale Anzahl der möglichen Batterien
|
||
MAXCONSUMER => 16, # maximale Anzahl der möglichen Consumer (Attribut)
|
||
MAXPRODUCER => 3, # maximale Anzahl der möglichen anderen Produzenten (Attribut)
|
||
MAXINVERTER => 3, # maximale Anzahl der möglichen Inverter
|
||
|
||
MAXSOCDEF => 95, # default Wert (%) auf den die Batterie maximal aufgeladen werden soll bzw. als aufgeladen gilt
|
||
CARECYCLEDEF => 20, # default max. Anzahl Tage die zwischen der Batterieladung auf maxSoC liegen dürfen
|
||
BATSOCCHGDAY => 5, # Batterie: prozentuale SoC Anpassung pro Tag
|
||
|
||
GMFBLTO => 30, # Timeout Aholen Message File aus contrib
|
||
GMFILEREPEAT => 3600, # Base Wiederholungsuntervall Abholen Message File aus contrib
|
||
GMFILERANDOM => 10800, # Random AddOn zu GMFILEREPEAT
|
||
IDXLIMIT => 900000, # Notification System: Indexe > IDXLIMIT sind reserviert für Steuerungsaufgaben
|
||
|
||
AITRBLTO => 7200, # KI Training BlockingCall Timeout
|
||
AIBCTHHLD => 0.2, # Schwelle der KI Trainigszeit ab der BlockingCall benutzt wird
|
||
AITRSTARTDEF => 2, # default Stunde f. Start AI-Training
|
||
AISTDUDEF => 1825, # default Haltezeit KI Raw Daten (Tage)
|
||
AISPREADUPLIM => 120, # obere Abweichungsgrenze (%) AI 'Spread' von API Prognose
|
||
AISPREADLOWLIM => 80, # untere Abweichungsgrenze (%) AI 'Spread' von API Prognose
|
||
AIACCUPLIM => 130, # obere Abweichungsgrenze (%) AI 'Accurate' von API Prognose
|
||
AIACCLOWLIM => 70, # untere Abweichungsgrenze (%) AI 'Accurate' von API Prognose
|
||
AIACCTRNMIN => 5500, # Mindestanzahl KI Trainingssätze für Verwendung "KI Accurate"
|
||
AISPREADTRNMIN => 7000, # Mindestanzahl KI Trainingssätze für Verwendung "KI Spreaded"
|
||
|
||
SOLAPIREPDEF => 3600, # default Abrufintervall SolCast API (s)
|
||
FORAPIREPDEF => 900, # default Abrufintervall ForecastSolar API (s)
|
||
OMETEOREPDEF => 900, # default Abrufintervall Open-Meteo API (s)
|
||
VRMAPIREPDEF => 300, # default Abrufintervall Victron VRM API Forecast
|
||
SOLCMAXREQDEF => 50, # max. täglich mögliche Requests SolCast API
|
||
OMETMAXREQ => 9700, # Beschränkung auf max. mögliche Requests Open-Meteo API
|
||
LEADTIME => 3600, # relative Zeit vor Sonnenaufgang zur Freigabe API Abruf / Verbraucherplanung
|
||
LAGTIME => 1800, # Nachlaufzeit relativ zu Sunset bis Sperrung API Abruf
|
||
|
||
PRDEF => 1.0, # default Performance Ratio (PR)
|
||
STOREFFDEF => 0.9, # default Batterie Effizienz (https://www.energie-experten.org/erneuerbare-energien/photovoltaik/stromspeicher/wirkungsgrad)
|
||
TEMPCOEFFDEF => -0.45, # default Temperaturkoeffizient Pmpp (%/°C) lt. Datenblatt Solarzelle
|
||
TEMPMODINC => 25, # default Temperaturerhöhung an Solarzellen gegenüber Umgebungstemperatur bei wolkenlosem Himmel
|
||
TEMPBASEDEF => 25, # Temperatur Module bei Nominalleistung
|
||
|
||
DEFMINTIME => 60, # default Einplanungsdauer in Minuten
|
||
DEFCTYPE => 'other', # default Verbrauchertyp
|
||
DEFCMODE => 'can', # default Planungsmode der Verbraucher
|
||
DEFPOPERCENT => 1.0, # Standard % aktuelle Leistung an nominaler Leistung gemäß Typenschild
|
||
DEFHYST => 0, # default Hysterese
|
||
|
||
CAICONDEF => 'clock@gold', # default consumerAdviceIcon
|
||
FLOWGSIZEDEF => 400, # default flowGraphicSize
|
||
HISTHOURDEF => 2, # default Anzeige vorangegangene Stunden
|
||
WTHCOLDDEF => 'C7C979', # Wetter Icon Tag default Farbe
|
||
WTHCOLNDEF => 'C7C7C7', # Wetter Icon Nacht default Farbe
|
||
B1COLDEF => 'FFAC63', # default Farbe Beam 1
|
||
B1FONTCOLDEF => '0D0D0D', # default Schriftfarbe Beam 1
|
||
B2COLDEF => 'C4C4A7', # default Farbe Beam 2
|
||
B2FONTCOLDEF => '000000', # default Schriftfarbe Beam 2
|
||
B3COLDEF => 'BED6C0', # default Farbe Beam 3
|
||
B3FONTCOLDEF => '000000', # default Schriftfarbe Beam 3
|
||
B4COLDEF => 'DBDBD0', # default Farbe Beam 4
|
||
B4FONTCOLDEF => '000000', # default Schriftfarbe Beam 4
|
||
FGCDDEF => 130, # Abstand Verbrauchericons zueinander
|
||
|
||
FGSCALEDEF => 0.10, # Flußgrafik: Scale Normativ Icons
|
||
STROKCOLSTDDEF => 'darkorange', # Flußgrafik: Standardfarbe aktive normale Kette
|
||
STROKCOLSIGDEF => 'red', # Flußgrafik: Standardfarbe aktive Signal-Kette
|
||
STROKCOLINADEF => 'gray', # Flußgrafik: Standardfarbe inaktive Kette
|
||
STROKWIDTHDEF => 25, # Flußgrafik: Standard Breite der Kette
|
||
PRODICONDEF => 'sani_garden_pump', # default Producer-Icon
|
||
CICONDEF => 'light_light_dim_100', # default Consumer-Icon
|
||
CICONCOLDEF => 'darkorange', # default Consumer-Icon Färbung
|
||
BICONDEF => 'measure_battery_75', # default Batterie-Icon
|
||
BICCOLRCDDEF => 'grey', # default Batterie-Icon Färbung bei Ladefreigabe und Inaktivität
|
||
BICCOLNRCDDEF => '#cccccc', # default Batterie-Icon Färbung bei fehlender Ladefreigabe
|
||
BCHGICONCOLDEF => 'darkorange', # default 'Aufladen' Batterie-Icon Färbung
|
||
BDCHICONCOLDEF => '#b32400', # default 'Entladen' Batterie-Icon Färbung
|
||
HOMEICONDEF => 'control_building_control@grey', # default Home-Icon
|
||
NODEICONDEF => 'virtualbox', # default Knoten-Icon
|
||
INVICONDEF => 'weather_sun', # default Inverter-icon
|
||
MOONICONDEF => 2, # default Mond-Phase (aus %hmoon)
|
||
MOONCOLDEF => 'lightblue', # default Mond Färbung
|
||
ACTCOLDEF => 'orange', # default Färbung Icon wenn aktiv
|
||
INACTCOLDEF => 'grey', # default Färbung Icon wenn inaktiv
|
||
|
||
BPATH => 'https://svn.fhem.de/trac/browser/trunk/fhem/contrib/SolarForecast/', # Basispfad Abruf contrib SolarForecast Files
|
||
PPATH => '?format=txt', # Download Format
|
||
CFILE => 'controls_solarforecast.txt', # Controlfile Update FTUI-Files
|
||
BGHPATH => 'https://raw.githubusercontent.com/nasseeder1/FHEM-SolarForecast/refs/heads/main/', # Basispfad GitHub SolarForecast Files
|
||
PGHPATH => '', # GitHub Post Pfad
|
||
};
|
||
|
||
## Standardvariablen
|
||
######################
|
||
my @da; # zentraler temporärer Readings-Store
|
||
my @chours = (5..21); # Stunden des Tages mit möglichen Korrekturwerten
|
||
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 $statcache = $root."/FHEM/FhemUtils/StatApi_SolarForecast_"; # Filename-Fragment für Status-API Werte (wird mit Devicename ergänzt)
|
||
my $weathercache = $root."/FHEM/FhemUtils/WeatherApi_SolarForecast_"; # Filename-Fragment für Weather-API Werte (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 für DWD Stationskatalog
|
||
my $dwdcatgpx = $root."/FHEM/FhemUtils/DWDcat_SolarForecast.gpx"; # Export Filename für DWD Stationskatalog im gpx-Format
|
||
my $pvhexprtcsv = $root."/FHEM/FhemUtils/PVH_Export_SolarForecast_"; # Filename-Fragment für PV History Exportfile (wird mit Devicename ergänzt)
|
||
|
||
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 @ctypes = qw(dishwasher dryer washingmachine heater charger other
|
||
noSchedule); # erlaubte Consumer Typen
|
||
|
||
my $msgfiletest = 'controls_solarforecast_messages_test.txt'; # TEST Input-File Notification System
|
||
my $msgfileprod = 'controls_solarforecast_messages_prod.txt'; # PRODUKTIVES Input-File Notification System
|
||
|
||
my $messagefile = $msgfileprod;
|
||
# mögliche Debug-Module
|
||
my @dd = qw( aiProcess
|
||
aiData
|
||
apiCall
|
||
apiProcess
|
||
batteryManagement
|
||
collectData
|
||
consumerPlanning
|
||
consumption
|
||
consumption_long
|
||
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
|
||
setupStringAzimuth
|
||
setupStringDeclination
|
||
batteryTrigger
|
||
powerTrigger
|
||
energyH4Trigger
|
||
);
|
||
# Anlagenkonfiguration: maßgebliche Attribute
|
||
my @aconfigs = qw( affectBatteryPreferredCharge affectConsForecastIdentWeekdays
|
||
affectConsForecastInPlanning affectSolCastPercentile
|
||
consumerLegend consumerAdviceIcon consumerLink
|
||
ctrlAIdataStorageDuration ctrlBackupFilesKeep
|
||
ctrlConsRecommendReadings ctrlGenPVdeviation ctrlInterval
|
||
ctrlLanguage ctrlNextDayForecastReadings ctrlNextHoursSoCForecastReadings
|
||
ctrlShowLink ctrlSolCastAPImaxReq
|
||
ctrlSolCastAPIoptimizeReq ctrlSpecialReadings ctrlUserExitFn
|
||
setupWeatherDev1 setupWeatherDev2 setupWeatherDev3
|
||
disable
|
||
flowGraphicControl graphicBeamWidth
|
||
graphicBeamHeightLevel1 graphicBeamHeightLevel2
|
||
graphicBeam1Content graphicBeam2Content graphicBeam3Content graphicBeam4Content
|
||
graphicBeam1Color graphicBeam2Color graphicBeam3Color graphicBeam4Color
|
||
graphicBeam1FontColor graphicBeam2FontColor graphicBeam3FontColor graphicBeam4FontColor
|
||
graphicEnergyUnit graphicHeaderOwnspec graphicHeaderOwnspecValForm
|
||
graphicHeaderDetail graphicHeaderShow graphicHistoryHour graphicHourCount graphicHourStyle
|
||
graphicLayoutType graphicSelect graphicShowDiff graphicShowNight graphicShowWeather
|
||
graphicSpaceSize graphicWeatherColor graphicWeatherColorNight
|
||
setupMeterDev setupInverterStrings setupRadiationAPI setupStringPeak
|
||
setupRoofTops
|
||
);
|
||
|
||
for my $cn (1..MAXCONSUMER) {
|
||
$cn = sprintf "%02d", $cn;
|
||
push @aconfigs, "consumer${cn}"; # Anlagenkonfiguration: add Consumer Attribute
|
||
push @dd, "consumerSwitching${cn}"; # ctrlDebug: add specific Consumer
|
||
}
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
push @aconfigs, "setupBatteryDev${bn}"; # Anlagenkonfiguration: add Battery Attribute
|
||
push @aconfigs, "ctrlBatSocManagement${bn}";
|
||
}
|
||
|
||
for my $in (1..MAXINVERTER) {
|
||
$in = sprintf "%02d", $in;
|
||
push @aconfigs, "setupInverterDev${in}"; # Anlagenkonfiguration: add Inverter Attribute
|
||
}
|
||
|
||
for my $pn (1..MAXPRODUCER) {
|
||
$pn = sprintf "%02d", $pn;
|
||
push @aconfigs, "setupOtherProducer${pn}"; # Anlagenkonfiguration: add Producer Attribute
|
||
}
|
||
|
||
my $allwidgets = 'icon|sortable|uzsu|knob|noArg|time|text|slider|multiple|select|bitfield|widgetList|colorpicker';
|
||
|
||
## Steuerhashes
|
||
#########################
|
||
|
||
my %svicons = ( # Schweregrad Icons Mitteilungssystem
|
||
'0' => 'message_mail@grey', # Standard Mitteilungs-Icon 0 - keine Mitteilung
|
||
'1' => 'message_mail_open@darkorange', # Standard Mitteilungs-Icon 1 - Mitteilung
|
||
'2' => 'message_attention@darkorange', # Standard Mitteilungs-Icon 2 - Warnung
|
||
'3' => 'message_attention@red', # Standard Mitteilungs-Icon 3 - Fehler / Problem
|
||
);
|
||
|
||
my %hset = ( # Hash der Set-Funktion
|
||
consumerImmediatePlanning => { fn => \&_setconsumerImmediatePlanning },
|
||
consumerNewPlanning => { fn => \&_setconsumerNewPlanning },
|
||
clientAction => { fn => \&_setclientAction },
|
||
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 },
|
||
setupStringDeclination => { fn => \&_setstringDeclination },
|
||
setupStringAzimuth => { fn => \&_setstringAzimuth },
|
||
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 },
|
||
valBattery => { fn => \&_getlistvalBattery, needcred => 0 },
|
||
valCurrent => { fn => \&_getlistCurrent, needcred => 0 },
|
||
valInverter => { fn => \&_getlistvalInverter, needcred => 0 },
|
||
valProducer => { fn => \&_getlistvalProducer, needcred => 0 },
|
||
valStrings => { fn => \&_getlistvalStrings, 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 },
|
||
radiationApiData => { fn => \&_getlistRadiationApiData, needcred => 0 },
|
||
weatherApiData => { fn => \&_getlistWeatherApiData, needcred => 0 },
|
||
statusApiData => { fn => \&_getlistStatusApiData, needcred => 0 },
|
||
valDecTree => { fn => \&_getaiDecTree, needcred => 0 },
|
||
ftuiFramefiles => { fn => \&_ftuiFramefiles, needcred => 0 },
|
||
dwdCatalog => { fn => \&_getdwdCatalog, needcred => 0 },
|
||
outputMessages => { fn => \&_getoutputMessages, needcred => 0 },
|
||
);
|
||
|
||
my %hattr = ( # Hash für Attr-Funktion
|
||
consumer => { fn => \&_attrconsumer },
|
||
ctrlConsRecommendReadings => { fn => \&_attrcreateConsRecRdgs },
|
||
ctrlSpecialReadings => { fn => \&_attrcreateSpecialRdgs },
|
||
ctrlDebug => { fn => \&_attrctrlDebug },
|
||
setupWeatherDev1 => { fn => \&_attrWeatherDev },
|
||
setupWeatherDev2 => { fn => \&_attrWeatherDev },
|
||
setupWeatherDev3 => { fn => \&_attrWeatherDev },
|
||
setupMeterDev => { fn => \&_attrMeterDev },
|
||
setupInverterStrings => { fn => \&_attrInverterStrings },
|
||
setupRadiationAPI => { fn => \&_attrRadiationAPI },
|
||
setupStringPeak => { fn => \&_attrStringPeak },
|
||
setupRoofTops => { fn => \&_attrRoofTops },
|
||
flowGraphicControl => { fn => \&_attrflowGraphicControl },
|
||
);
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$hattr{'setupBatteryDev'.$bn}{fn} = \&_attrBatteryDev;
|
||
}
|
||
|
||
for my $in (1..MAXINVERTER) {
|
||
$in = sprintf "%02d", $in;
|
||
$hattr{'setupInverterDev'.$in}{fn} = \&_attrInverterDev;
|
||
}
|
||
|
||
for my $prn (1..MAXPRODUCER) {
|
||
$prn = sprintf "%02d", $prn;
|
||
$hattr{'setupOtherProducer'.$prn}{fn} = \&_attrProducerDev;
|
||
}
|
||
|
||
my %htr = ( # Hash even/odd für <tr>
|
||
0 => { cl => 'even' },
|
||
1 => { cl => 'odd' },
|
||
);
|
||
|
||
# Hash Mondphasen
|
||
my %hmoon = (
|
||
0 => { icon => 'weather_moon_phases_1_new', DE => 'Neumond', EN => 'new moon' },
|
||
1 => { icon => 'weather_moon_phases_2', DE => 'zunehmende Sichel', EN => 'increasing crescent' },
|
||
2 => { icon => 'weather_moon_phases_3_half', DE => 'erstes Viertel', EN => 'first quarter' },
|
||
3 => { icon => 'weather_moon_phases_4', DE => 'zunehmender Mond', EN => 'waxing moon' },
|
||
4 => { icon => 'weather_moon_phases_5_full', DE => 'Vollmond', EN => 'full moon' },
|
||
5 => { icon => 'weather_moon_phases_6', DE => 'abnehmender Mond', EN => 'waning moon' },
|
||
6 => { icon => 'weather_moon_phases_7_half', DE => 'letztes Viertel', EN => 'last quarter' },
|
||
7 => { icon => 'weather_moon_phases_8', DE => 'abnehmende Sichel', EN => 'decreasing crescent' },
|
||
);
|
||
|
||
my %hrepl = ( # Zeichenersetzungen
|
||
'0' => 'a',
|
||
'1' => 'b',
|
||
'2' => 'c',
|
||
'3' => 'd',
|
||
'4' => 'e',
|
||
'5' => 'f',
|
||
'6' => 'g',
|
||
'7' => 'h',
|
||
'8' => 'i',
|
||
'9' => 'j',
|
||
'\.' => 'k',
|
||
);
|
||
|
||
my %hqtxt = ( # Hash (Setup) Texte
|
||
entry => { EN => qq{<b>Warm welcome!</b><br>
|
||
The next queries will guide you through the basic installation.<br>
|
||
If all entries are made, please check the configuration finally with
|
||
"set LINK plantConfiguration check" or by pressing the offered icon.<br>
|
||
Please correct any errors and take note of possible hints.<br>
|
||
(The display language can be changed with attribute "ctrlLanguage".)<hr><br> },
|
||
DE => qq{<b>Herzlich Willkommen!</b><br>
|
||
Die nächsten Abfragen führen sie durch die Grundinstallation.<br>
|
||
Sind alle Eingaben vorgenommen, prüfen sie bitte die Konfiguration abschließend mit
|
||
"set LINK plantConfiguration check" oder mit Druck auf das angebotene Icon.<br>
|
||
Korrigieren sie bitte eventuelle Fehler und beachten sie mögliche Hinweise.<br>
|
||
(Die Anzeigesprache kann mit dem Attribut "ctrlLanguage" umgestellt werden.)<hr><br>} },
|
||
cfd => { EN => qq{Please enter at least one weather forecast device with "attr LINK setupWeatherDev1"},
|
||
DE => qq{Bitte geben sie mindestens ein Wettervorhersage Device mit "attr LINK setupWeatherDev1" an} },
|
||
crd => { EN => qq{Please select the radiation forecast service with "attr LINK setupRadiationAPI"},
|
||
DE => qq{Bitte geben sie den Strahlungsvorhersage Dienst mit "attr LINK setupRadiationAPI" an} },
|
||
cid => { EN => qq{Please specify the Inverter device with "attr LINK setupInverterDev01"},
|
||
DE => qq{Bitte geben sie das Wechselrichter Device mit "attr LINK setupInverterDev01" an} },
|
||
mid => { EN => qq{Please specify the device for energy measurement with "attr LINK setupMeterDev"},
|
||
DE => qq{Bitte geben sie das Device zur Energiemessung mit "attr LINK setupMeterDev" an} },
|
||
ist => { EN => qq{Please define all of your used string names with "attr LINK setupInverterStrings"},
|
||
DE => qq{Bitte geben sie alle von Ihnen verwendeten Stringnamen mit "attr LINK setupInverterStrings" an} },
|
||
mps => { EN => qq{Please enter the DC peak power of each string with "attr LINK setupStringPeak"},
|
||
DE => qq{Bitte geben sie die DC Spitzenleistung von jedem String mit "attr LINK setupStringPeak" an} },
|
||
mdr => { EN => qq{Please specify the module direction with "set LINK setupStringAzimuth"},
|
||
DE => qq{Bitte geben sie die Modulausrichtung mit "set LINK setupStringAzimuth" an} },
|
||
mta => { EN => qq{Please specify the module tilt angle with "set LINK setupStringDeclination"},
|
||
DE => qq{Bitte geben sie den Modulneigungswinkel mit "set LINK setupStringDeclination" 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 "attr LINK setupRoofTops"},
|
||
DE => qq{Bitte setzen sie die Zuordnung String / Rooftop Identifikation mit "attr LINK setupRoofTops"} },
|
||
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 <WT> minutes before the upcoming sunrise},
|
||
DE => qq{ab <WT> 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} },
|
||
simsg => { EN => qq{Message},
|
||
DE => qq{Mitteilung} },
|
||
msgsys => { EN => qq{Notification system},
|
||
DE => qq{Mitteilungssystem} },
|
||
msgimp => { EN => qq{Importance},
|
||
DE => qq{Wichtigkeit} },
|
||
number => { EN => qq{Number},
|
||
DE => qq{Nummer} },
|
||
impcha => { EN => qq{Input channels},
|
||
DE => qq{Eingangskanäle} },
|
||
scedld => { EN => qq{scheduled},
|
||
DE => qq{geplant} },
|
||
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 ... <br>},
|
||
DE => qq{LINK wartet auf Solarvorhersagedaten ... <br>} },
|
||
wexso => { EN => qq{switched externally},
|
||
DE => qq{von extern umgeschaltet} },
|
||
legimp => { EN => qq{Legend Importance: 1 - general Message, 2 - important Message, 3 - Error or Problem},
|
||
DE => qq{Legende Wichtigkeit: 1 - allgemeine Mitteilung, 2 - wichtige Mitteilung, 3 - Fehler oder Problem} },
|
||
strok => { EN => qq{Congratulations 😊, the system configuration is error-free. Please note any information (<I>).},
|
||
DE => qq{Herzlichen Glückwunsch 😊, die Anlagenkonfiguration ist fehlerfrei. Bitte eventuelle Hinweise (<I>) beachten.} },
|
||
strwn => { EN => qq{Looks quite good 😐, the system configuration is basically OK. Please note the warnings (<W>).},
|
||
DE => qq{Sieht ganz gut aus 😐, die Anlagenkonfiguration ist prinzipiell in Ordnung. Bitte beachten Sie die Warnungen (<W>).} },
|
||
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: <pstate><br>Info: <supplmnt><br>Mode: <mode><br>On: <start><br>Off: <stop><br>Remaining lock time: <RLT> seconds},
|
||
DE => qq{Planungsstatus: <pstate><br>Info: <supplmnt><br>Modus: <mode><br>Ein: <start><br>Aus: <stop><br>verbleibende Sperrzeit: <RLT> Sekunden} },
|
||
dmgsig => { EN => qq{Read messages are not signaled again until a FHEM restart, but are retained if they are relevant.},
|
||
DE => qq{Gelesene Mitteilungen werden bis zu einem FHEM Neustart nicht wieder signalisiert, bleiben bei Relevanz jedoch erhalten.} },
|
||
);
|
||
|
||
my %htitles = ( # Hash Hilfetexte (Mouse Over)
|
||
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} },
|
||
ischawth => { EN => qq{is charged with},
|
||
DE => qq{wird aufgeladen mit} },
|
||
isdchawt => { EN => qq{is discharged with},
|
||
DE => qq{wird entladen mit} },
|
||
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)} },
|
||
enconsrl => { EN => qq{real Energy consumption},
|
||
DE => qq{realer Energieverbrauch} },
|
||
enconsfc => { EN => qq{forecasted energy consumption},
|
||
DE => qq{prognostizierter Energieverbrauch} },
|
||
enpchcst => { EN => qq{Energy purchase costs},
|
||
DE => qq{Kosten Energiebezug} },
|
||
rengfeed => { EN => qq{Remuneration for the grid feed-in},
|
||
DE => qq{Vergütung Netzeinspeisung} },
|
||
enppubgd => { EN => qq{Energy purchase from the public grid},
|
||
DE => qq{Energiebezug aus dem öffentlichen Netz} },
|
||
enfeedgd => { EN => qq{Feed-in},
|
||
DE => qq{Einspeisung} },
|
||
pvgenerl => { EN => qq{real PV generation},
|
||
DE => qq{reale PV-Erzeugung} },
|
||
pvgenefc => { EN => qq{forecasted PV generation},
|
||
DE => qq{prognostizierte PV-Erzeugung} },
|
||
onlybatw => { EN => qq{Battery},
|
||
DE => qq{Batterie} },
|
||
socofbat => { EN => qq{State of Charge battery},
|
||
DE => qq{Ladung Batterie} },
|
||
socbacur => { EN => qq{SoC current},
|
||
DE => qq{SoC aktuell} },
|
||
socbatfc => { EN => qq{SoC forecast},
|
||
DE => qq{SoC Prognose} },
|
||
socbaths => { EN => qq{SoC at the end of the hour},
|
||
DE => qq{SoC am Ende der Stunde} },
|
||
bcharrel => { EN => qq{Charging release (activate release for charging the battery if necessary)},
|
||
DE => qq{Ladefreigabe (evtl. Freigabe zum Laden der Batterie aktivieren)} },
|
||
bncharel => { EN => qq{no Charging release (possibly deactivate release for charging the battery)},
|
||
DE => qq{keine Ladefreigabe (evtl. Freigabe zum Laden der Batterie deaktivieren)} },
|
||
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 <NAME> pvCorrectionFactor_Auto on*},
|
||
DE => qq{ausgeschaltet\nAutokorrektur einschalten mit:\nset <NAME> 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} },
|
||
opwiki => { EN => qq{Open the Wiki (German language)},
|
||
DE => qq{Öffne das Wiki} },
|
||
outpmsg => { EN => qq{Messages are available - press the button to open them},
|
||
DE => qq{Mitteilungen sind vorhanden - drücke die Taste um sie zu öffnen} },
|
||
nomsgfo => { EN => qq{there are no new messages},
|
||
DE => qq{es sind keine neuen Mitteilungen vorhanden} },
|
||
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} },
|
||
dumtxt => { EN => qq{Consumption that cannot be allocated to registered consumers},
|
||
DE => qq{Verbrauch der den registrierten Verbrauchern nicht zugeordnet werden kann} },
|
||
pstate => { EN => qq{Planning status: <pstate>\nInfo: <supplmnt>\n\nMode: <mode>\nOn: <start>\nOff: <stop>\nRemaining lock time: <RLT> seconds},
|
||
DE => qq{Planungsstatus: <pstate>\nInfo: <supplmnt>\n\nModus: <mode>\nEin: <start>\nAus: <stop>\nverbleibende Sperrzeit: <RLT> Sekunden} },
|
||
ainuse => { EN => qq{AI Perl module is installed, but the AI support is not used.\nRun 'set <NAME> plantConfiguration check' for hints.},
|
||
DE => qq{KI Perl Modul ist installiert, aber die KI Unterstützung wird nicht verwendet.\nPrüfen sie 'set <NAME> plantConfiguration check' für Hinweise.} },
|
||
arsrad2o => { EN => qq{API query successful but the radiation values are outdated.\nCheck the plant with 'set <NAME> plantConfiguration check'.},
|
||
DE => qq{API Abfrage erfolgreich aber die Strahlungswerte sind veraltet.\nPrüfen sie die Anlage mit 'set <NAME> plantConfiguration check'.} },
|
||
aswfc2o => { EN => qq{The weather data is outdated.\nCheck the plant with 'set <NAME> plantConfiguration check'.},
|
||
DE => qq{Die Wetterdaten sind veraltet.\nPrüfen sie die Anlage mit 'set <NAME> 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 => 'wolkenloser Himmel', txte => 'cloudless sky' },
|
||
'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 => 'weather_sleet', txtd => 'mäßiger oder starker Hagelschauer', txte => 'moderate or heavy hailstorm' },
|
||
|
||
'90' => { s => '0', icon => 'weather_storm', txtd => 'starke Hagelschauer ohne Gewitter', txte => 'heavy hail showers without thunderstorms' },
|
||
'91' => { s => '0', icon => 'weather_rain_light', txtd => 'leichter Regen, letzte Stunde Gewitter hörbar', txte => 'light rain, last hour thunderstorm audible' },
|
||
'92' => { s => '0', icon => 'weather_rain_heavy', txtd => 'starker Regen, letzte Stunde Gewitter hörbar', txte => 'heavy rain, last hour thunderstorm audible' },
|
||
'93' => { s => '0', icon => 'weather_rain_snow_light', txtd => 'leichter Schnee oder Regen-Hagel, letzte Stunde Gewitter hörbar', txte => 'light snow or rain hail, thunderstorms audible for the last hour' },
|
||
'94' => { s => '0', icon => 'weather_rain_snow_heavy', txtd => 'starker Schnee oder Regen-Hagel, letzte Stunde Gewitter hörbar', txte => 'heavy snow or rain hail, thunderstorm audible for the last hour' },
|
||
'95' => { s => '1', icon => 'weather_thunderstorm', txtd => 'leichtes oder mäßiges Gewitter mit Regen oder Schnee', txte => 'light or moderate thunderstorm with rain or snow' },
|
||
'96' => { s => '1', icon => 'weather_thunderstorm', txtd => 'leichtes oder mäßiges Gewitter mit Hagel', txte => 'light or moderate thunderstorm with hail' },
|
||
'97' => { s => '0', icon => 'weather_thunderstorm', txtd => 'schweres Gewitter mit Regen oder Schnee', txte => 'heavy thunderstorm with rain or snow' },
|
||
'98' => { s => '0', icon => 'weather_thunderstorm', txtd => 'Gewitter mit Sandsturm', txte => 'Thunderstorm with sandstorm' },
|
||
'99' => { s => '1', icon => 'weather_thunderstorm', txtd => 'Gewitter mit Graupel oder Hagel', txte => 'Thunderstorm with sleet or hail' },
|
||
|
||
'100' => { s => '0', icon => 'weather_night_starry', txtd => 'sternenklarer Himmel', txte => 'starry sky' },
|
||
'101' => { s => '0', icon => 'weather_night_cloudy_light', txtd => 'Bewölkung abnehmend', txte => 'Cloudiness decreasing' },
|
||
'102' => { s => '0', icon => 'weather_night_cloudy', txtd => 'Bewölkung unverändert', txte => 'Cloudiness unchanged' },
|
||
'103' => { s => '0', icon => 'weather_night_cloudy_heavy', txtd => 'Bewölkung zunehmend', txte => 'Cloudiness increasing' },
|
||
'110' => { s => '0', icon => 'weather_night_fog', txtd => 'Nebel', txte => 'Fog' },
|
||
'111' => { s => '0', icon => 'weather_night_rain_fog', txtd => 'Nebel mit Regen', txte => 'Fog with rain' },
|
||
'112' => { s => '0', icon => 'weather_night_fog', txtd => 'durchgehender Bodennebel', txte => 'continuous ground fog' },
|
||
|
||
'136' => { s => '0', icon => 'weather_night_snow_light', txtd => 'leichtes oder mäßiges Schneefegen, unter Augenhöhe', txte => 'light or moderate snow sweeping, below eye level' },
|
||
'137' => { s => '0', icon => 'weather_night_snow_heavy', txtd => 'starkes Schneefegen, unter Augenhöhe', txte => 'heavy snow sweeping, below eye level' },
|
||
'138' => { s => '0', icon => 'weather_night_snow_light', txtd => 'leichtes oder mäßiges Schneetreiben, über Augenhöhe', txte => 'light or moderate blowing snow, above eye level' },
|
||
'139' => { s => '0', icon => 'weather_night_snow_heavy', txtd => 'starkes Schneetreiben, über Augenhöhe', txte => 'heavy snow drifting, above eye level' },
|
||
|
||
'140' => { s => '0', icon => 'weather_night_fog', txtd => 'Nebel in einiger Entfernung', txte => 'Fog in some distance' },
|
||
'141' => { s => '0', icon => 'weather_night_fog', txtd => 'Nebel in Schwaden oder Bänken', txte => 'Fog in swaths or banks' },
|
||
'142' => { s => '0', icon => 'weather_night_fog', txtd => 'Nebel, Himmel erkennbar, dünner werdend', txte => 'Fog, sky recognizable, thinning' },
|
||
'143' => { s => '0', icon => 'weather_night_fog', txtd => 'Nebel, Himmel nicht erkennbar, dünner werdend', txte => 'Fog, sky not recognizable, thinning' },
|
||
'144' => { s => '0', icon => 'weather_night_fog', txtd => 'Nebel, Himmel erkennbar, unverändert', txte => 'Fog, sky recognizable, unchanged' },
|
||
'145' => { s => '1', icon => 'weather_night_fog', txtd => 'Nebel', txte => 'Fog' },
|
||
'146' => { s => '0', icon => 'weather_night_fog', txtd => 'Nebel, Himmel erkennbar, dichter werdend', txte => 'Fog, sky recognizable, becoming denser' },
|
||
'147' => { s => '0', icon => 'weather_night_fog', txtd => 'Nebel, Himmel nicht erkennbar, dichter werdend', txte => 'Fog, sky not visible, becoming denser' },
|
||
'148' => { s => '1', icon => 'weather_night_fog', txtd => 'Nebel mit Reifbildung', txte => 'Fog with frost formation' },
|
||
'149' => { s => '0', icon => 'weather_night_fog', txtd => 'Nebel mit Reifansatz, Himmel nicht erkennbar', txte => 'Fog with frost, sky not visible' },
|
||
|
||
'150' => { s => '0', icon => 'weather_night_rain', txtd => 'unterbrochener leichter Sprühregen', txte => 'intermittent light drizzle' },
|
||
'151' => { s => '1', icon => 'weather_night_rain_light', txtd => 'leichter Sprühregen', txte => 'light drizzle' },
|
||
'152' => { s => '0', icon => 'weather_night_rain', txtd => 'unterbrochener mäßiger Sprühregen', txte => 'intermittent moderate drizzle' },
|
||
'153' => { s => '1', icon => 'weather_night_rain_light', txtd => 'leichter Sprühregen', txte => 'light drizzle' },
|
||
'154' => { s => '0', icon => 'weather_night_rain_heavy', txtd => 'unterbrochener starker Sprühregen', txte => 'intermittent heavy drizzle' },
|
||
'155' => { s => '1', icon => 'weather_night_rain_heavy', txtd => 'starker Sprühregen', txte => 'heavy drizzle' },
|
||
'156' => { s => '1', icon => 'weather_night_rain_light', txtd => 'leichter gefrierender Sprühregen', txte => 'light freezing drizzle' },
|
||
'157' => { s => '1', icon => 'weather_night_rain_heavy', txtd => 'mäßiger oder starker gefrierender Sprühregen', txte => 'moderate or heavy freezing drizzle' },
|
||
'158' => { s => '0', icon => 'weather_night_rain_light', txtd => 'leichter Sprühregen mit Regen', txte => 'light drizzle with rain' },
|
||
'159' => { s => '0', icon => 'weather_night_rain_heavy', txtd => 'mäßiger oder starker Sprühregen mit Regen', txte => 'moderate or heavy drizzle with rain' },
|
||
|
||
'160' => { s => '0', icon => 'weather_night_rain_light', txtd => 'unterbrochener leichter Regen oder einzelne Regentropfen', txte => 'intermittent light rain or single raindrops' },
|
||
'161' => { s => '1', icon => 'weather_night_rain_light', txtd => 'leichter Regen', txte => 'light rain' },
|
||
'162' => { s => '0', icon => 'weather_night_rain', txtd => 'unterbrochener mäßiger Regen', txte => 'intermittent moderate rain' },
|
||
'163' => { s => '1', icon => 'weather_night_rain', txtd => 'mäßiger Regen', txte => 'moderate rain' },
|
||
'164' => { s => '0', icon => 'weather_night_rain_heavy ', txtd => 'unterbrochener starker Regen', txte => 'intermittent heavy rain' },
|
||
'165' => { s => '1', icon => 'weather_night_rain_heavy', txtd => 'starker Regen', txte => 'heavy rain' },
|
||
'166' => { s => '1', icon => 'weather_night_snow_rain_light', txtd => 'leichter gefrierender Regen', txte => 'light freezing rain' },
|
||
'167' => { s => '1', icon => 'weather_night_snow_rain_heavy', txtd => 'mäßiger oder starker gefrierender Regen', txte => 'moderate or heavy freezing rain' },
|
||
'168' => { s => '0', icon => 'weather_night_snow_rain_light', txtd => 'leichter Schneeregen', txte => 'light sleet' },
|
||
'169' => { s => '0', icon => 'weather_night_snow_rain_heavy', txtd => 'mäßiger oder starker Schneeregen', txte => 'moderate or heavy sleet' },
|
||
|
||
'170' => { s => '0', icon => 'weather_night_snow_light', txtd => 'unterbrochener leichter Schneefall oder einzelne Schneeflocken', txte => 'intermittent light snowfall or single snowflakes' },
|
||
'171' => { s => '1', icon => 'weather_night_snow_light', txtd => 'leichter Schneefall', txte => 'light snowfall' },
|
||
'172' => { s => '0', icon => 'weather_night_snow', txtd => 'unterbrochener mäßiger Schneefall', txte => 'intermittent moderate snowfall' },
|
||
'173' => { s => '1', icon => 'weather_night_snow', txtd => 'mäßiger Schneefall', txte => 'moderate snowfall' },
|
||
'174' => { s => '0', icon => 'weather_night_snow_heavy', txtd => 'unterbrochener starker Schneefall', txte => 'intermittent heavy snowfall' },
|
||
'175' => { s => '1', icon => 'weather_night_snow_heavy', txtd => 'starker Schneefall', txte => 'heavy snowfall' },
|
||
'176' => { s => '0', icon => 'weather_frost', txtd => 'Eisnadeln (Polarschnee)', txte => 'Ice needles (polar snow)' },
|
||
'177' => { s => '1', icon => 'weather_frost', txtd => 'Schneegriesel', txte => 'Snow drizzle' },
|
||
'178' => { s => '0', icon => 'weather_frost', txtd => 'Schneekristalle', txte => 'Snow crystals' },
|
||
'179' => { s => '0', icon => 'weather_frost', txtd => 'Eiskörner (gefrorene Regentropfen)', txte => 'Ice grains (frozen raindrops)' },
|
||
|
||
'180' => { s => '1', icon => 'weather_night_rain_light', txtd => 'leichter Regenschauer', txte => 'light rain shower' },
|
||
'181' => { s => '1', icon => 'weather_night_rain', txtd => 'mäßiger oder starker Regenschauer', txte => 'moderate or heavy rain shower' },
|
||
'182' => { s => '1', icon => 'weather_night_rain_heavy', txtd => 'sehr starker Regenschauer', txte => 'very heavy rain shower' },
|
||
'183' => { s => '0', icon => 'weather_night_snow', txtd => 'mäßiger oder starker Schneeregenschauer', txte => 'moderate or heavy sleet shower' },
|
||
'184' => { s => '0', icon => 'weather_night_snow_light', txtd => 'leichter Schneeschauer', txte => 'light snow shower' },
|
||
'185' => { s => '1', icon => 'weather_night_snow_light', txtd => 'leichter Schneeschauer', txte => 'light snow shower' },
|
||
'186' => { s => '1', icon => 'weather_night_snow_heavy', txtd => 'mäßiger oder starker Schneeschauer', txte => 'moderate or heavy snow shower' },
|
||
'187' => { s => '0', icon => 'weather_night_snow_heavy', txtd => 'mäßiger oder starker Graupelschauer', txte => 'moderate or heavy sleet shower' },
|
||
'189' => { s => '0', icon => 'weather_sleet', txtd => 'mäßiger oder starker Hagelschauer', txte => 'moderate or heavy hailstorm' },
|
||
|
||
'190' => { s => '0', icon => 'weather_storm', txtd => 'starke Hagelschauer ohne Gewitter', txte => 'heavy hail showers without thunderstorms' },
|
||
'191' => { s => '0', icon => 'weather_night_rain_light', txtd => 'leichter Regen, letzte Stunde Gewitter hörbar', txte => 'light rain, last hour thunderstorm audible' },
|
||
'192' => { s => '0', icon => 'weather_night_rain_heavy', txtd => 'starker Regen, letzte Stunde Gewitter hörbar', txte => 'heavy rain, last hour thunderstorm audible' },
|
||
'193' => { s => '0', icon => 'weather_night_snow_rain_light', txtd => 'leichter Schnee oder Regen-Hagel, letzte Stunde Gewitter hörbar', txte => 'light snow or rain hail, thunderstorms audible for the last hour' },
|
||
'194' => { s => '0', icon => 'weather_night_snow_rain_heavy', txtd => 'starker Schnee oder Regen-Hagel, letzte Stunde Gewitter hörbar', txte => 'heavy snow or rain hail, thunderstorm audible for the last hour' },
|
||
'195' => { s => '1', icon => 'weather_night_thunderstorm_light', txtd => 'leichtes oder mäßiges Gewitter mit Regen oder Schnee', txte => 'light or moderate thunderstorm with rain or snow' },
|
||
'196' => { s => '1', icon => 'weather_night_thunderstorm_light', txtd => 'leichtes oder mäßiges Gewitter mit Hagel', txte => 'light or moderate thunderstorm with hail' },
|
||
'197' => { s => '0', icon => 'weather_night_thunderstorm', txtd => 'schweres Gewitter mit Regen oder Schnee', txte => 'heavy thunderstorm with rain or snow' },
|
||
'198' => { s => '0', icon => 'weather_night_thunderstorm', txtd => 'Gewitter mit Sandsturm', txte => 'Thunderstorm with sandstorm' },
|
||
'199' => { s => '1', icon => 'weather_night_thunderstorm', txtd => 'Gewitter mit Graupel oder Hagel', txte => 'Thunderstorm with sleet or hail' },
|
||
);
|
||
|
||
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 => \&StatusAPIVal, par => '', unit => '', def => 0 }, # par = Parameter zur spezifischen Verwendung
|
||
lastretrieval_time => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => '-' },
|
||
lastretrieval_timestamp => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => '-' },
|
||
response_message => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => '-' },
|
||
todayMaxAPIcalls => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 'apimaxreq' },
|
||
todayDoneAPIcalls => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 0 },
|
||
todayDoneAPIrequests => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 0 },
|
||
todayRemainingAPIcalls => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 'apimaxreq' },
|
||
todayRemainingAPIrequests => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 'apimaxreq' },
|
||
runTimeCentralTask => { fnr => 2, fn => \&CurrentVal, par => '', unit => ' s', 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 => ' s', def => '-' },
|
||
todayConsumption => { fnr => 3, fn => \&CircularVal, par => 99, unit => ' Wh', def => 0 },
|
||
BatPowerIn_Sum => { fnr => 4, fn => \&CurrentVal, par => 'batpowerinsum', unit => ' W', def => '-' },
|
||
BatPowerOut_Sum => { fnr => 4, fn => \&CurrentVal, par => 'batpoweroutsum', unit => ' W', 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 => \&RadiationAPIVal, 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 },
|
||
todayConsumptionForecast => { fnr => 4, fn => \&NexthoursVal, par => 'confc', unit => ' Wh', def => '-' },
|
||
conForecastTillNextSunrise => { fnr => 4, fn => \&NexthoursVal, par => 'confc', unit => ' Wh', def => 0 },
|
||
todayBatInSum => { fnr => 4, fn => \&CircularVal, par => 99, unit => ' Wh', def => 0 },
|
||
todayBatOutSum => { fnr => 4, fn => \&CircularVal, par => 99, 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;
|
||
|
||
$hcsr{'runTimeAvgDayConsumer_'.$csr}{fnr} = 4;
|
||
$hcsr{'runTimeAvgDayConsumer_'.$csr}{fn} = \&ConsumerVal;
|
||
$hcsr{'runTimeAvgDayConsumer_'.$csr}{par} = 'runtimeAvgDay';
|
||
$hcsr{'runTimeAvgDayConsumer_'.$csr}{unit} = ' min';
|
||
$hcsr{'runTimeAvgDayConsumer_'.$csr}{def} = 0;
|
||
}
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
|
||
$hcsr{'daysUntilBatteryCare_'.$bn}{fnr} = 4;
|
||
$hcsr{'daysUntilBatteryCare_'.$bn}{fn} = \&CircularVal;
|
||
$hcsr{'daysUntilBatteryCare_'.$bn}{par} = 99;
|
||
$hcsr{'daysUntilBatteryCare_'.$bn}{unit} = '';
|
||
$hcsr{'daysUntilBatteryCare_'.$bn}{def} = '-';
|
||
|
||
$hcsr{'todayBatIn_'.$bn}{fnr} = 4;
|
||
$hcsr{'todayBatIn_'.$bn}{fn} = \&CircularVal;
|
||
$hcsr{'todayBatIn_'.$bn}{par} = 99;
|
||
$hcsr{'todayBatIn_'.$bn}{unit} = ' Wh';
|
||
$hcsr{'todayBatIn_'.$bn}{def} = 0;
|
||
|
||
$hcsr{'todayBatOut_'.$bn}{fnr} = 4;
|
||
$hcsr{'todayBatOut_'.$bn}{fn} = \&CircularVal;
|
||
$hcsr{'todayBatOut_'.$bn}{par} = 99;
|
||
$hcsr{'todayBatOut_'.$bn}{unit} = ' Wh';
|
||
$hcsr{'todayBatOut_'.$bn}{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
|
||
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
|
||
weatherid => { fn => \&_storeVal, storname => 'weatherid', validkey => undef, fpar => undef }, # Wetter ID
|
||
weathercloudcover => { fn => \&_storeVal, storname => 'wcc', validkey => undef, fpar => undef }, # Wolkenbedeckung
|
||
rr1c => { 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
|
||
conprice => { fn => \&_storeVal, storname => 'conprice', validkey => undef, fpar => undef }, # Bezugspreis pro kWh der Stunde
|
||
feedprice => { fn => \&_storeVal, storname => 'feedprice', validkey => undef, fpar => undef }, # Einspeisevergütung pro kWh der Stunde
|
||
pvfc => { fn => \&_storeVal, storname => 'pvfc', validkey => undef, fpar => 'comp99' }, # prognostizierter Energieertrag
|
||
confc => { fn => \&_storeVal, storname => 'confc', validkey => undef, fpar => 'comp99' }, # prognostizierter Hausverbrauch
|
||
gcons => { 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 PV
|
||
);
|
||
|
||
for my $in (1..MAXINVERTER) {
|
||
$in = sprintf "%02d", $in;
|
||
$hfspvh{'pvrl'.$in}{fn} = \&_storeVal; # realer Energieertrag Inverter
|
||
$hfspvh{'pvrl'.$in}{storname} = 'pvrl'.$in;
|
||
$hfspvh{'pvrl'.$in}{validkey} = undef;
|
||
$hfspvh{'pvrl'.$in}{fpar} = 'comp99';
|
||
|
||
$hfspvh{'etotali'.$in}{fn} = \&_storeVal; # etotal Inverter
|
||
$hfspvh{'etotali'.$in}{storname} = 'etotali'.$in;
|
||
$hfspvh{'etotali'.$in}{validkey} = undef;
|
||
$hfspvh{'etotali'.$in}{fpar} = undef;
|
||
}
|
||
|
||
for my $pn (1..MAXPRODUCER) {
|
||
$pn = sprintf "%02d", $pn;
|
||
$hfspvh{'pprl'.$pn}{fn} = \&_storeVal; # realer Energieertrag sonstiger Erzeuger
|
||
$hfspvh{'pprl'.$pn}{storname} = 'pprl'.$pn;
|
||
$hfspvh{'pprl'.$pn}{validkey} = undef;
|
||
$hfspvh{'pprl'.$pn}{fpar} = 'comp99';
|
||
|
||
$hfspvh{'etotalp'.$pn}{fn} = \&_storeVal; # etotal sonstiger Erzeuger
|
||
$hfspvh{'etotalp'.$pn}{storname} = 'etotalp'.$pn;
|
||
$hfspvh{'etotalp'.$pn}{validkey} = undef;
|
||
$hfspvh{'etotalp'.$pn}{fpar} = undef;
|
||
}
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$hfspvh{'batintotal'.$bn}{fn} = \&_storeVal; # totale Batterieladung
|
||
$hfspvh{'batintotal'.$bn}{storname} = 'batintotal'.$bn;
|
||
$hfspvh{'batintotal'.$bn}{validkey} = undef;
|
||
$hfspvh{'batintotal'.$bn}{fpar} = undef;
|
||
|
||
$hfspvh{'batouttotal'.$bn}{fn} = \&_storeVal; # totale Batterieentladung
|
||
$hfspvh{'batouttotal'.$bn}{storname} = 'batouttotal'.$bn;
|
||
$hfspvh{'batouttotal'.$bn}{validkey} = undef;
|
||
$hfspvh{'batouttotal'.$bn}{fpar} = undef;
|
||
|
||
$hfspvh{'batinthishour'.$bn}{fn} = \&_storeVal; # Batterieladung in Stunde
|
||
$hfspvh{'batinthishour'.$bn}{storname} = 'batin'.$bn;
|
||
$hfspvh{'batinthishour'.$bn}{validkey} = undef;
|
||
$hfspvh{'batinthishour'.$bn}{fpar} = 'comp99';
|
||
|
||
$hfspvh{'batoutthishour'.$bn}{fn} = \&_storeVal; # Batterieentladung in Stunde
|
||
$hfspvh{'batoutthishour'.$bn}{storname} = 'batout'.$bn;
|
||
$hfspvh{'batoutthishour'.$bn}{validkey} = undef;
|
||
$hfspvh{'batoutthishour'.$bn}{fpar} = 'comp99';
|
||
|
||
$hfspvh{'batmaxsoc'.$bn}{fn} = \&_storeVal; # max. erreichter SOC des Tages
|
||
$hfspvh{'batmaxsoc'.$bn}{storname} = 'batmaxsoc'.$bn;
|
||
$hfspvh{'batmaxsoc'.$bn}{validkey} = undef;
|
||
$hfspvh{'batmaxsoc'.$bn}{fpar} = undef;
|
||
|
||
$hfspvh{'batsetsoc'.$bn}{fn} = \&_storeVal; # gesetzter optimaler SOC für den Tag
|
||
$hfspvh{'batsetsoc'.$bn}{storname} = 'batsetsoc'.$bn;
|
||
$hfspvh{'batsetsoc'.$bn}{validkey} = undef;
|
||
$hfspvh{'batsetsoc'.$bn}{fpar} = undef;
|
||
|
||
$hfspvh{'batsoc'.$bn}{fn} = \&_storeVal; # aktueller SOC für Tag / Stunde
|
||
$hfspvh{'batsoc'.$bn}{storname} = 'batsoc'.$bn;
|
||
$hfspvh{'batsoc'.$bn}{validkey} = undef;
|
||
$hfspvh{'batsoc'.$bn}{fpar} = undef;
|
||
}
|
||
|
||
|
||
# Information zu verwendeten internen Datenhashes
|
||
####################################################
|
||
# Daten die ein Reload überleben und mit reloadCacheFiles nachgeladen werden müssen:
|
||
# $data{$name}{pvhist} # historische Werte
|
||
# $data{$name}{weatherapi} # Zwischenspeicher API-Wetterdaten
|
||
# $data{$name}{solcastapi} # Zwischenspeicher API-Solardaten
|
||
# $data{$name}{statusapi} # Zwischenspeicher API-Status
|
||
# $data{$name}{circular} # Ringspeicher
|
||
# $data{$name}{consumers} # Consumer Daten
|
||
# $data{$name}{aidectree}{aitrained} # AI Decision Tree trainierte Daten
|
||
# $data{$name}{aidectree}{airaw} # Rohdaten für AI Input = Raw Trainigsdaten
|
||
|
||
# temporäre Daten die pro Zyklus neu erstellt werden:
|
||
# $data{$name}{current} # temporärer Speicher Current Daten (enthält beim Lesen Cachefiles geladene Statusdaten)
|
||
# $data{$name}{nexthours} # temporärer Speicher NextHours Daten
|
||
# $data{$name}{inverters} # temporärer Speicher Inverter Daten
|
||
# $data{$name}{producers} # temporärer Speicher non-PV Producer Daten
|
||
# $data{$name}{batteries} # temporärer Speicher Battery Daten
|
||
# $data{$name}{weatherdata} # temporärer Speicher Wetterdaten
|
||
# $data{$name}{func} # temporäre interne Funktionen
|
||
# $data{$name}{dwdcatalog} # temporärer Speicher DWD Stationskatalog
|
||
# $data{$name}{strings} # temporärer Speicher Stringkonfiguration
|
||
# $data{$name}{aidectree}{object} # AI Decision Tree Object
|
||
# $data{$name}{messages} # Mitteilungssystem - permanent erneuerter Speicher
|
||
# $data{$name}{filemessages} # Mitteilungssystem - Input vom Message File
|
||
# $data{$name}{preparedmessages} # Mitteilungssystem - vorbereitete Messages innerhalb des Code
|
||
|
||
################################################################
|
||
# Init Fn
|
||
################################################################
|
||
sub Initialize {
|
||
my $hash = shift;
|
||
|
||
my $hod = join ",", map { sprintf "%02d", $_} (1..24);
|
||
my $srd = join ",", sort keys (%hcsr);
|
||
|
||
my ($consumer, $setupbat, $ctrlbatsm, $setupprod, $setupinv, @allc);
|
||
for my $c (1..MAXCONSUMER) {
|
||
$c = sprintf "%02d", $c;
|
||
$consumer .= "consumer${c}:textField-long ";
|
||
push @allc, $c;
|
||
}
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$setupbat .= "setupBatteryDev${bn}:textField-long ";
|
||
$ctrlbatsm .= "ctrlBatSocManagement${bn}:textField-long ";
|
||
}
|
||
|
||
for my $in (1..MAXINVERTER) {
|
||
$in = sprintf "%02d", $in;
|
||
$setupinv .= "setupInverterDev${in}:textField-long ";
|
||
}
|
||
|
||
for my $prn (1..MAXPRODUCER) {
|
||
$prn = sprintf "%02d", $prn;
|
||
$setupprod .= "setupOtherProducer${prn}:textField-long ";
|
||
}
|
||
|
||
my $allcs = join ",", @allc;
|
||
my $dm = 'none,'.join ",", sort @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->{RenameFn} = \&Rename;
|
||
$hash->{DbLog_splitFn} = \&DbLogSplit;
|
||
$hash->{AttrFn} = \&Attr;
|
||
$hash->{NotifyFn} = \&Notify;
|
||
$hash->{ReadyFn} = \&runTask;
|
||
$hash->{AttrList} = "affectBatteryPreferredCharge:slider,0,1,100 ".
|
||
"affectConsForecastIdentWeekdays:1,0 ".
|
||
"affectConsForecastInPlanning:1,0 ".
|
||
"affectConsForecastLastDays:slider,1,1,31 ".
|
||
"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 ".
|
||
"ctrlBackupFilesKeep ".
|
||
"ctrlConsRecommendReadings:multiple-strict,$allcs ".
|
||
"ctrlDebug:multiple-strict,$dm,#10 ".
|
||
"ctrlGenPVdeviation:daily,continuously ".
|
||
"ctrlInterval ".
|
||
"ctrlLanguage:DE,EN ".
|
||
"ctrlNextDayForecastReadings:multiple-strict,$hod ".
|
||
"ctrlNextHoursSoCForecastReadings ".
|
||
"ctrlShowLink:1,0 ".
|
||
"ctrlSolCastAPImaxReq:selectnumbers,5,5,60,0,lin ".
|
||
"ctrlSolCastAPIoptimizeReq:1,0 ".
|
||
"ctrlSpecialReadings:multiple-strict,$srd ".
|
||
"ctrlUserExitFn:textField-long ".
|
||
"disable:1,0 ".
|
||
"flowGraphicControl:textField-long ".
|
||
"graphicBeamHeightLevel1 ".
|
||
"graphicBeamHeightLevel2 ".
|
||
"graphicBeamWidth:slider,20,5,100 ".
|
||
"graphicBeam1Content ".
|
||
"graphicBeam2Content ".
|
||
"graphicBeam3Content ".
|
||
"graphicBeam4Content ".
|
||
"graphicBeam1Color:colorpicker,RGB ".
|
||
"graphicBeam2Color:colorpicker,RGB ".
|
||
"graphicBeam3Color:colorpicker,RGB ".
|
||
"graphicBeam4Color:colorpicker,RGB ".
|
||
"graphicBeam1FontColor:colorpicker,RGB ".
|
||
"graphicBeam2FontColor:colorpicker,RGB ".
|
||
"graphicBeam3FontColor:colorpicker,RGB ".
|
||
"graphicBeam4FontColor:colorpicker,RGB ".
|
||
"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,01 ".
|
||
"graphicShowWeather:1,0 ".
|
||
"graphicSpaceSize ".
|
||
"graphicWeatherColor:colorpicker,RGB ".
|
||
"graphicWeatherColorNight:colorpicker,RGB ".
|
||
"setupInverterStrings ".
|
||
"setupMeterDev:textField-long ".
|
||
"setupWeatherDev1 ".
|
||
"setupWeatherDev2 ".
|
||
"setupWeatherDev3 ".
|
||
"setupRoofTops ".
|
||
"setupRadiationAPI ".
|
||
"setupStringPeak ".
|
||
$setupbat.
|
||
$setupinv.
|
||
$setupprod.
|
||
$consumer.
|
||
$ctrlbatsm.
|
||
$readingFnAttributes;
|
||
|
||
## Hinweis: graphicBeamXContent wird in _addDynAttr hinzugefügt
|
||
|
||
### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !!
|
||
##########################################################################################################################
|
||
# my $av1 = "obsolete#-#the#attribute#will#be#deleted#soon";
|
||
# $hash->{AttrList} .= " graphicBeam1MaxVal:$av1 ctrlAreaFactorUsage:$av1 ";
|
||
##########################################################################################################################
|
||
|
||
$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} = { "setupBatteryDev" => "setupBatteryDev01", # 28.12.24
|
||
"ctrlBatSocManagement" => "ctrlBatSocManagement01", # 01.01.25
|
||
"ctrlStatisticReadings" => "ctrlSpecialReadings", # 02.01.25
|
||
};
|
||
|
||
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 => $name,
|
||
type => $type,
|
||
notes => \%vNotesIntern,
|
||
useAPI => 0,
|
||
useSMUtils => 1,
|
||
useErrCodes => 1,
|
||
useCTZ => 1,
|
||
};
|
||
|
||
use version 0.77; our $VERSION = moduleVersion ($params); # Versionsinformationen setzen
|
||
delete $params->{hash};
|
||
|
||
createAssociatedWith ($hash);
|
||
reloadCacheFiles ($params);
|
||
singleUpdateState ( {hash => $hash, state => 'initialized', evt => 1} );
|
||
|
||
$readyfnlist{$name} = $hash; # Registrierung in Ready-Schleife
|
||
InternalTimer (gettimeofday() + WHISTREPEAT + int(rand(300)), "FHEM::SolarForecast::periodicWriteMemcache", $hash, 0); # Einstieg periodisches Schreiben historische Daten
|
||
InternalTimer (gettimeofday() + 120 + int(rand(300)), "FHEM::SolarForecast::getMessageFileNonBlocking", $hash, 0);
|
||
|
||
return;
|
||
}
|
||
|
||
###############################################################
|
||
# 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
|
||
energyH4TriggerSet
|
||
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{$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 ".
|
||
"energyH4Trigger:textField-long ".
|
||
"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 ".
|
||
"setupStringAzimuth ".
|
||
"setupStringDeclination ".
|
||
$cf." "
|
||
;
|
||
|
||
## API spezifische Setter
|
||
###########################
|
||
if (isSolCastUsed ($hash)) {
|
||
$setlist .= "roofIdentPair "
|
||
;
|
||
}
|
||
elsif (isVictronKiUsed ($hash)) {
|
||
$setlist .= "vrmCredentials "
|
||
;
|
||
}
|
||
|
||
## 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 = {
|
||
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 $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
|
||
my $hash = $defs{$name};
|
||
|
||
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 $name = $paref->{name};
|
||
my $c = $paref->{prop};
|
||
my $evt = $paref->{prop1} // 0; # geändert V 1.1.0 - 1 -> 0
|
||
my $hash = $defs{$name};
|
||
|
||
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 roofIdentPair
|
||
################################################################
|
||
sub _setroofIdentPair { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $opt = $paref->{opt};
|
||
my $arg = $paref->{arg};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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: <pairkey> rtid=<Rooftop ID> apikey=<api key>};
|
||
}
|
||
|
||
if (!$h->{rtid} || !$h->{apikey}) {
|
||
return qq{The syntax of "$opt" is not correct. Please consider the commandref.};
|
||
}
|
||
|
||
$data{$name}{statusapi}{'?IdPair'}{'?'.$pk}{rtid} = $h->{rtid};
|
||
$data{$name}{statusapi}{'?IdPair'}{'?'.$pk}{apikey} = $h->{apikey};
|
||
|
||
writeCacheToFile ($hash, 'statusapi', $statcache.$name); # Status-API Cache sichern
|
||
|
||
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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $opt = $paref->{opt};
|
||
my $arg = $paref->{arg};
|
||
|
||
my $hash = $defs{$name};
|
||
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{$name}{statusapi}{'?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{$name}{statusapi}{'?VRM'}{'?API'}{credentials} = chew ($serial);
|
||
$msg = qq{Credentials for the Victron VRM API has been saved.};
|
||
}
|
||
|
||
writeCacheToFile ($hash, 'statusapi', $statcache.$name); # Status-API Cache sichern
|
||
|
||
return $msg;
|
||
}
|
||
|
||
################################################################
|
||
# Setter operationMode
|
||
################################################################
|
||
sub _setoperationMode { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $prop = $paref->{prop} // return qq{no mode specified};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
singleUpdateState ( {hash => $hash, state => $prop, evt => 1} );
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Setter powerTrigger / batterytrigger / energyH4Trigger
|
||
################################################################
|
||
sub _setTrigger { ## no critic "not used"
|
||
my $paref = shift;
|
||
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.};
|
||
}
|
||
}
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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 setupStringDeclination
|
||
################################################################
|
||
sub _setstringDeclination { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $arg = $paref->{arg} // return qq{no tilt angle was provided};
|
||
|
||
# my $tilt = join "|", sort keys %hff;
|
||
my $atilt = '0|5|10|15|20|25|30|35|40|45|50|55|60|65|70|75|80|85|90';
|
||
|
||
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 !~ /^(?:$atilt)$/x) {
|
||
return qq{The inclination angle of "$key" is incorrect};
|
||
}
|
||
}
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
readingsSingleUpdate ($hash, 'setupStringDeclination', $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 setupStringAzimuth
|
||
#
|
||
# Angabe entweder als Azimut-Bezeichner oder direkte
|
||
# Azimut Angabe -180 ...0...180
|
||
#
|
||
################################################################
|
||
sub _setstringAzimuth { ## no critic "not used"
|
||
my $paref = shift;
|
||
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};
|
||
}
|
||
}
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
readingsSingleUpdate ($hash, 'setupStringAzimuth', $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 / (verborgener) Getter plantConfiguration
|
||
################################################################
|
||
sub _setplantConfiguration { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $opt = $paref->{opt};
|
||
my $arg = $paref->{arg};
|
||
|
||
my $hash = $defs{$name};
|
||
my ($err,$nr,$na,@pvconf);
|
||
|
||
$arg = 'check' if (!$arg);
|
||
|
||
if ($arg eq "check") {
|
||
my $out = checkPlantConfig ($hash);
|
||
$out = qq{<html>$out</html>};
|
||
|
||
## 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) {
|
||
setModel ($hash);
|
||
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 $name = $paref->{name};
|
||
my $out = $paref->{out};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
asyncOutput($hash->{HELPER}{CL}{1}, $out);
|
||
delClHash ($name);
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Setter pvCorrectionFactor
|
||
################################################################
|
||
sub _setpvCorrectionFactor { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $opt = $paref->{opt};
|
||
my $prop = $paref->{prop} // return qq{no correction value specified};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if ($prop !~ /[0-9,.]/x) {
|
||
return qq{The correction value must be specified by numbers and optionally with decimal places};
|
||
}
|
||
|
||
$prop =~ s/,/./x;
|
||
|
||
my ($acu, $aln) = isAutoCorrUsed ($name);
|
||
my $mode = $acu =~ /on/xs ? 'manual flex' : 'manual fix';
|
||
|
||
readingsSingleUpdate ($hash, $opt, $prop." ($mode)", 1);
|
||
centralTask ($hash, 0);
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Setter pvCorrectionFactor_Auto
|
||
################################################################
|
||
sub _setpvCorrectionFactorAuto { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $opt = $paref->{opt};
|
||
my $prop = $paref->{prop} // return qq{no correction value specified};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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}", "");
|
||
|
||
if ($rv !~ /manual/xs) {
|
||
deleteReadingspec ($hash, "pvCorrectionFactor_${n}.*");
|
||
}
|
||
else {
|
||
$rv =~ s/flex/fix/xs;
|
||
readingsSingleUpdate ($hash, "pvCorrectionFactor_${n}", $rv, 0);
|
||
}
|
||
}
|
||
}
|
||
elsif ($prop =~ /on/xs) {
|
||
for my $n (1..24) {
|
||
$n = sprintf "%02d", $n;
|
||
my $rv = ReadingsVal ($name, "pvCorrectionFactor_${n}", "");
|
||
|
||
if ($rv =~ /manual/xs) {
|
||
$rv =~ s/fix/flex/xs;
|
||
readingsSingleUpdate ($hash, "pvCorrectionFactor_${n}", $rv, 0);
|
||
}
|
||
}
|
||
}
|
||
|
||
writeCacheToFile ($hash, 'plantconfig', $plantcfg.$name); # Anlagenkonfiguration sichern
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Setter reset
|
||
################################################################
|
||
sub _setreset { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $prop = $paref->{prop} // return qq{no source specified for reset};
|
||
my $type = $paref->{type};
|
||
my $hash = $defs{$name};
|
||
|
||
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{$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;
|
||
$paref->{histname} = '';
|
||
|
||
setPVhistory ($paref);
|
||
|
||
delete $paref->{reorg};
|
||
delete $paref->{reorgday};
|
||
delete $paref->{histname};
|
||
}
|
||
else {
|
||
delete $data{$name}{pvhist}{$dday};
|
||
Log3 ($name, 3, qq{$name - Day "$dday" deleted from pvHistory});
|
||
}
|
||
}
|
||
else {
|
||
delete $data{$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{$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;
|
||
$paref->{histname} = '';
|
||
|
||
setPVhistory ($paref);
|
||
|
||
delete $paref->{reorg};
|
||
delete $paref->{reorgday};
|
||
delete $paref->{histname};
|
||
}
|
||
else {
|
||
for my $hr (sort keys %{$data{$name}{pvhist}{$dday}}) {
|
||
delete $data{$name}{pvhist}{$dday}{$hr}{con};
|
||
}
|
||
|
||
Log3 ($name, 3, qq{$name - consumption day "$dday" deleted from pvHistory});
|
||
}
|
||
}
|
||
else {
|
||
for my $dy (sort keys %{$data{$name}{pvhist}}) {
|
||
for my $hr (sort keys %{$data{$name}{pvhist}{$dy}}) {
|
||
delete $data{$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{$name}{circular}{$circh}{pvcorrf};
|
||
delete $data{$name}{circular}{$circh}{quality};
|
||
delete $data{$name}{circular}{$circh}{pvrlsum};
|
||
delete $data{$name}{circular}{$circh}{pvfcsum};
|
||
delete $data{$name}{circular}{$circh}{dnumsum};
|
||
|
||
for my $k (keys %{$data{$name}{circular}{$circh}}) {
|
||
delete $data{$name}{circular}{$circh}{$k} if($k =~ /^(pvrl_|pvfc_)/xs);
|
||
}
|
||
|
||
for my $hid (keys %{$data{$name}{pvhist}}) {
|
||
delete $data{$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{$name}{circular}}) {
|
||
delete $data{$name}{circular}{$hod}{pvcorrf};
|
||
delete $data{$name}{circular}{$hod}{quality};
|
||
delete $data{$name}{circular}{$hod}{pvrlsum};
|
||
delete $data{$name}{circular}{$hod}{pvfcsum};
|
||
delete $data{$name}{circular}{$hod}{dnumsum};
|
||
|
||
for my $k (keys %{$data{$name}{circular}{$hod}}) {
|
||
delete $data{$name}{circular}{$hod}{$k} if($k =~ /^(pvrl_|pvfc_)/xs);
|
||
}
|
||
}
|
||
|
||
for my $hid (keys %{$data{$name}{pvhist}}) {
|
||
for my $hidh (keys %{$data{$name}{pvhist}{$hid}}) {
|
||
delete $data{$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{$name}{current}{aiinitstate};
|
||
delete $data{$name}{current}{aitrainstate};
|
||
delete $data{$name}{current}{aiaddistate};
|
||
delete $data{$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;
|
||
}
|
||
|
||
readingsDelete ($hash, $prop);
|
||
|
||
if ($prop eq 'roofIdentPair') {
|
||
my $pk = $paref->{prop1} // ""; # ein bestimmter PairKey angegeben ?
|
||
|
||
if ($pk) {
|
||
delete $data{$name}{statusapi}{'?IdPair'}{'?'.$pk};
|
||
Log3 ($name, 3, qq{$name - roofIdentPair: pair key "$pk" deleted});
|
||
}
|
||
else {
|
||
delete $data{$name}{statusapi}{'?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 'consumerPlanning') { # Verbraucherplanung resetten
|
||
my $c = $paref->{prop1} // ""; # bestimmten Verbraucher setzen falls angegeben
|
||
|
||
if ($c) {
|
||
deleteConsumerPlanning ($hash, $c);
|
||
}
|
||
else {
|
||
for my $cs (keys %{$data{$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) {
|
||
$paref->{c} = $c;
|
||
delConsumerFromMem ($paref); # spezifischen Consumer aus History löschen
|
||
}
|
||
else {
|
||
for my $c (keys %{$data{$name}{consumers}}) {
|
||
$paref->{c} = $c;
|
||
delConsumerFromMem ($paref); # alle Consumer aus History löschen
|
||
}
|
||
}
|
||
|
||
delete $paref->{c};
|
||
$data{$name}{current}{consumerCollected} = 0; # Consumer neu sammeln
|
||
|
||
writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben
|
||
centralTask ($hash, 0);
|
||
}
|
||
|
||
createAssociatedWith ($hash);
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Setter operatingMemory
|
||
# (Ersatz für Setter writeHistory)
|
||
################################################################
|
||
sub _setoperatingMemory { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $prop = $paref->{prop} // return qq{no operation specified for command};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if ($prop eq 'save') {
|
||
periodicWriteMemcache ($hash); # Cache Files 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 $name = $paref->{name};
|
||
my $opt = $paref->{opt};
|
||
my $arg = $paref->{arg};
|
||
my $argsref = $paref->{argsref};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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 $name = $paref->{name};
|
||
my $prop = $paref->{prop} // return;
|
||
|
||
if ($prop eq 'addInstances') {
|
||
aiAddInstancePV ($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{$name}{pvhist}};
|
||
my @cla = map {sprintf "%02d", $_} sort {$a<=>$b} keys %{$data{$name}{circular}};
|
||
my @vcm = map {sprintf "%02d", $_} sort {$a<=>$b} keys %{$data{$name}{consumers}};
|
||
my @vba = map {sprintf "%02d", $_} sort {$a<=>$b} keys %{$data{$name}{batteries}};
|
||
my @vin = map {sprintf "%02d", $_} sort {$a<=>$b} keys %{$data{$name}{inverters}};
|
||
my @vpn = map {sprintf "%02d", $_} sort {$a<=>$b} keys %{$data{$name}{producers}};
|
||
my @vst = sort keys %{$data{$name}{strings}};
|
||
|
||
my $hol = join ",", @ho;
|
||
my $pvl = join ",", @pha;
|
||
my $cll = join ",", @cla;
|
||
my $cml = join ",", @vcm;
|
||
my $bal = join ",", @vba;
|
||
my $inl = join ",", @vin;
|
||
my $pnl = join ",", @vpn;
|
||
my $str = join ",", @vst;
|
||
|
||
my $getlist = "Unknown argument $opt, choose one of ".
|
||
"valBattery:#,$bal ".
|
||
"valConsumerMaster:#,$cml ".
|
||
"valInverter:#,$inl ".
|
||
"valProducer:#,$pnl ".
|
||
"valStrings:#,$str ".
|
||
"data:noArg ".
|
||
"dwdCatalog ".
|
||
"forecastQualities:noArg ".
|
||
"ftuiFramefiles:noArg ".
|
||
"html:$hol ".
|
||
"nextHours:noArg ".
|
||
"pvCircular:#,$cll ".
|
||
"pvHistory:#,exportToCsv,$pvl ".
|
||
"rooftopData:noArg ".
|
||
"radiationApiData:noArg ".
|
||
"statusApiData:noArg ".
|
||
"valCurrent:noArg ".
|
||
"weatherApiData:noArg "
|
||
;
|
||
|
||
## KI spezifische Getter
|
||
##########################
|
||
my $vdtopt = q{};
|
||
if (!$aidtabs) { # AI::DecisionTree ist installiert
|
||
$vdtopt = 'aiRawData';
|
||
}
|
||
|
||
if (isPrepared4AI ($hash)) {
|
||
$vdtopt .= ',' if($vdtopt);
|
||
$vdtopt .= 'aiRuleStrings';
|
||
}
|
||
|
||
if ($vdtopt) {
|
||
$getlist .= "valDecTree:$vdtopt ";
|
||
}
|
||
|
||
my (undef, $disabled, $inactive) = controller ($name);
|
||
return if($disabled || $inactive);
|
||
|
||
my $t = int time;
|
||
my $params = {
|
||
name => $name,
|
||
type => $type,
|
||
opt => $opt,
|
||
arg => $arg,
|
||
t => $t,
|
||
chour => (strftime "%H", localtime($t)), # aktuelle Stunde in 24h format (00-23)
|
||
date => (strftime "%Y-%m-%d", localtime($t)),
|
||
day => (strftime "%d", localtime($t)), # aktueller Tag (range 01 .. 31)
|
||
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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $hash = $defs{$name};
|
||
|
||
delete $data{$name}{current}{dwdRad1hAge};
|
||
delete $data{$name}{current}{dwdRad1hAgeTS};
|
||
|
||
my ($rapi, $wapi) = getStatusApiName ($hash); # $rapi - Radiation-API, $wapi - Weather-API
|
||
my $ret = "$name is not a valid Radiation ($rapi) and/or Weather ($wapi) Model";
|
||
|
||
|
||
if ($rapi eq 'SolCast') {
|
||
$ret = __getSolCastData ($paref);
|
||
}
|
||
elsif ($rapi eq 'ForecastSolar') {
|
||
$ret = __getForecastSolarData ($paref);
|
||
}
|
||
elsif ($rapi eq 'DWD') {
|
||
$ret = __getDWDSolarData ($paref);
|
||
}
|
||
elsif ($rapi eq 'VictronKi') {
|
||
$ret = __getVictronSolarData ($paref);
|
||
}
|
||
|
||
if ($rapi eq 'OpenMeteo' || $wapi eq 'OpenMeteo') {
|
||
if ($rapi eq 'OpenMeteo') {
|
||
$paref->{reqm} = 'MODEL';
|
||
}
|
||
else {
|
||
$paref->{reqm} = 'WEATHERMODEL';
|
||
}
|
||
|
||
$ret = __getopenMeteoData ($paref);
|
||
}
|
||
|
||
delete $paref->{reqm};
|
||
|
||
return $ret;
|
||
}
|
||
|
||
################################################################
|
||
# Abruf SolCast roofTop data
|
||
################################################################
|
||
sub __getSolCastData {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $force = $paref->{force} // 0;
|
||
my $t = $paref->{t} // time;
|
||
my $debug = $paref->{debug};
|
||
my $lang = $paref->{lang};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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{$name}{statusapi}{'?IdPair'}}) {
|
||
my $apikey = StatusAPIVal ($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{$name}{statusapi}{SolCast}{'?All'}{solCastAPIcallMultiplier} = $mpk;
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{todayMaxAPIcalls} = $madc;
|
||
|
||
#########################
|
||
|
||
if (!$force) { # regulärer SolCast API Abruf
|
||
my ($rapi, $wapi) = getStatusApiName ($hash);
|
||
my $trc = StatusAPIVal ($hash, $rapi, '?All', 'todayRemainingAPIcalls', $madc);
|
||
my $etxt = $hqtxt{bnsas}{$lang};
|
||
$etxt =~ s{<WT>}{(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 = StatusAPIVal ($hash, $rapi, '?All', 'lastretrieval_timestamp', 0);
|
||
my $apiitv = StatusAPIVal ($hash, $rapi, '?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} = AttrVal ($name, 'setupInverterStrings', '');
|
||
$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 $name = $paref->{name};
|
||
my $allstrings = $paref->{allstrings}; # alle Strings
|
||
my $debug = $paref->{debug};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if (!$allstrings) { # alle Strings wurden abgerufen
|
||
return;
|
||
}
|
||
|
||
my $string;
|
||
($string, $allstrings) = split ",", $allstrings, 2;
|
||
|
||
my $rft = AttrVal ($name, 'setupRoofTops', '');
|
||
my ($a,$h) = parseParams ($rft);
|
||
|
||
my $pk = $h->{$string} // q{};
|
||
my $roofid = StatusAPIVal ($hash, '?IdPair', '?'.$pk, 'rtid', '');
|
||
my $apikey = StatusAPIVal ($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,
|
||
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 $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 $hash = $defs{$name};
|
||
my $sta = [gettimeofday]; # Start Response Verarbeitung
|
||
|
||
if ($err ne "") {
|
||
$msg = 'SolCast API server response: '.$err;
|
||
|
||
Log3 ($name, 1, "$name - $msg");
|
||
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{response_message} = $err;
|
||
|
||
singleUpdateState ( {hash => $hash, state => $msg, evt => 1} );
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$name}{current}{runTimeLastAPIAnswer} = sprintf "%.4f", (tv_interval($stc) - tv_interval($sta)); # API Laufzeit ermitteln
|
||
|
||
return;
|
||
}
|
||
|
||
my $jdata = decode_json ($myjson);
|
||
|
||
if ($debug eq 'apiProcess') {
|
||
Log3 ($name, 1, qq{$name DEBUG> 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{$name}{statusapi}{SolCast}{'?All'}{response_message} = $jdata->{'response_status'}{'message'};
|
||
|
||
if ($jdata->{'response_status'}{'error_code'} eq 'TooManyRequests') {
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{todayRemainingAPIrequests} = 0;
|
||
}
|
||
|
||
singleUpdateState ( {hash => $hash, state => $msg, evt => 1} );
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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: ".StatusAPIVal ($hash, 'SolCast', '?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 = RadiationAPIVal ($hash, $string, $starttmstr, 'pv_estimate50', 0) / (60/$period);
|
||
$data{$name}{solcastapi}{$string}{$starttmstr}{pv_estimate50} = sprintf "%.0f", $est50 if($est50);
|
||
|
||
$k++;
|
||
next;
|
||
}
|
||
|
||
delete $data{$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/;
|
||
|
||
$pvest50 = sprintf "%.0f", ($pvest50 * ($period/60) * 1000);
|
||
|
||
if ($debug =~ /apiProcess/x) { # nur für Debugging
|
||
if (exists $data{$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: }.$pvest50.qq{, contains already: }.RadiationAPIVal ($hash, $string, $starttmstr, 'pv_estimate50', 0));
|
||
}
|
||
}
|
||
|
||
$data{$name}{solcastapi}{$string}{$starttmstr}{pv_estimate50} += $pvest50;
|
||
|
||
$k++;
|
||
}
|
||
}
|
||
|
||
Log3 ($name, 4, qq{$name - SolCast API answer received for string "$string"});
|
||
|
||
___setSolCastAPIcallKeyData ($paref);
|
||
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{response_message} = 'success';
|
||
|
||
my $param = {
|
||
name => $name,
|
||
type => $type,
|
||
debug => $debug,
|
||
allstrings => $allstrings,
|
||
lang => $lang
|
||
};
|
||
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $lang = $paref->{lang};
|
||
my $debug = $paref->{debug};
|
||
my $t = $paref->{t} // time;
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{lastretrieval_timestamp} = $t; # letzter Abrufzeitstempel
|
||
|
||
my $apimaxreq = AttrVal ($name, 'ctrlSolCastAPImaxReq', SOLCMAXREQDEF);
|
||
my $mpl = StatusAPIVal ($hash, 'SolCast', '?All', 'solCastAPIcallMultiplier', 1);
|
||
my $ddc = StatusAPIVal ($hash, 'SolCast', '?All', 'todayDoneAPIcalls', 0);
|
||
|
||
$ddc += 1 if($paref->{firstreq});
|
||
my $drc = StatusAPIVal ($hash, 'SolCast', '?All', 'todayMaxAPIcalls', $apimaxreq / $mpl) - $ddc; # verbleibende SolCast API Calls am aktuellen Tag
|
||
$drc = 0 if($drc < 0);
|
||
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{todayDoneAPIrequests} = $ddc * $mpl;
|
||
|
||
my $drr = $apimaxreq - ($mpl * $ddc);
|
||
$drr = 0 if($drr < 0);
|
||
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{todayRemainingAPIrequests} = $drr;
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{todayRemainingAPIcalls} = $drc;
|
||
$data{$name}{statusapi}{SolCast}{'?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{$name}{statusapi}{SolCast}{'?All'}{currentAPIinterval} = SOLAPIREPDEF;
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{currentAPIinterval} = int ($dart / $drc) if($dart && $drc);
|
||
|
||
debugLog ($paref, "apiProcess|apiCall", "SolCast API Call - Sunset: $sunset, remain Sec to Sunset: $dart, new interval: ".StatusAPIVal ($hash, 'SolCast', '?All', 'currentAPIinterval', SOLAPIREPDEF));
|
||
}
|
||
else {
|
||
$data{$name}{statusapi}{SolCast}{'?All'}{currentAPIinterval} = SOLAPIREPDEF;
|
||
}
|
||
|
||
####
|
||
|
||
my $apiitv = StatusAPIVal ($hash, 'SolCast', '?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 $name = $paref->{name};
|
||
my $force = $paref->{force} // 0;
|
||
my $t = $paref->{t} // time;
|
||
my $lang = $paref->{lang};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if (!$force) { # regulärer API Abruf
|
||
my $etxt = $hqtxt{bnsas}{$lang};
|
||
$etxt =~ s{<WT>}{(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 = StatusAPIVal ($hash, 'ForecastSolar', '?All', 'lastretrieval_timestamp', 0);
|
||
my $apiitv = StatusAPIVal ($hash, 'ForecastSolar', '?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} = AttrVal ($name, 'setupInverterStrings', '');
|
||
|
||
__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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $allstrings = $paref->{allstrings}; # alle Strings
|
||
my $debug = $paref->{debug};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if (!$allstrings) { # alle Strings wurden abgerufen
|
||
$data{$name}{statusapi}{ForecastSolar}{'?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', '<unknown>');
|
||
my $az = StringVal ($hash, $string, 'azimut', '<unknown>');
|
||
my $peak = StringVal ($hash, $string, 'peak', '<unknown>');
|
||
|
||
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,
|
||
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 $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 $hash = $defs{$name};
|
||
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{$name}{statusapi}{ForecastSolar}{'?All'}{response_message} = $err;
|
||
|
||
singleUpdateState ( {hash => $hash, state => $msg, evt => 1} );
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$name}{current}{runTimeLastAPIAnswer} = sprintf "%.4f", (tv_interval($stc) - tv_interval($sta)); # API Laufzeit ermitteln
|
||
|
||
return;
|
||
}
|
||
|
||
my $jdata = decode_json ($myjson);
|
||
|
||
if ($debug eq 'apiProcess') {
|
||
Log3 ($name, 1, qq{$name DEBUG> 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{$name}{statusapi}{ForecastSolar}{'?All'}{response_message} = $jdata->{'message'}{'text'};
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{response_code} = $jdata->{'message'}{'code'};
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{lastretrieval_timestamp} = $t;
|
||
|
||
if (defined $jdata->{'message'}{'ratelimit'}{'remaining'}) {
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{requests_remaining} = $jdata->{'message'}{'ratelimit'}{'remaining'}; # verbleibende Requests in Periode
|
||
}
|
||
else {
|
||
delete $data{$name}{statusapi}{ForecastSolar}{'?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{$name}{statusapi}{ForecastSolar}{'?All'}{retryat_time} = $rtyat;
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{retryat_timestamp} = $rtyatts;
|
||
|
||
debugLog ($paref, "apiCall", "ForecastSolar API Call - retry at: ".$rtyat." ($rtyatts)");
|
||
}
|
||
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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{$name}{statusapi}{ForecastSolar}{'?All'}{lastretrieval_time} = $rt; # letzte Abrufzeit
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{lastretrieval_timestamp} = $rts; # letzter Abrufzeitstempel
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{response_message} = $jdata->{'message'}{'type'};
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{response_code} = $jdata->{'message'}{'code'};
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{requests_remaining} = $jdata->{'message'}{'ratelimit'}{'remaining'}; # verbleibende Requests in Periode
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{requests_limit_period} = $jdata->{'message'}{'ratelimit'}{'period'}; # Requests Limit Periode
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{requests_limit} = $jdata->{'message'}{'ratelimit'}{'limit'}; # Requests Limit in Periode
|
||
$data{$name}{statusapi}{ForecastSolar}{'?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{$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 = {
|
||
name => $name,
|
||
type => $type,
|
||
debug => $debug,
|
||
allstrings => $allstrings,
|
||
lang => $lang
|
||
};
|
||
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $lang = $paref->{lang};
|
||
my $debug = $paref->{debug};
|
||
my $t = $paref->{t} // time;
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{todayDoneAPIrequests} += 1;
|
||
|
||
## Berechnung des optimalen Request Intervalls
|
||
################################################
|
||
my $snum = scalar (split ",", AttrVal ($name, 'setupInverterStrings', 'Dummy')); # Anzahl der Strings (mindestens ein String als Dummy)
|
||
my $period = StatusAPIVal ($hash, 'ForecastSolar', '?All', 'requests_limit_period', 3600); # Requests Limit Periode
|
||
my $limit = StatusAPIVal ($hash, 'ForecastSolar', '?All', 'requests_limit', 12); # Request Limit in Periode
|
||
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{currentAPIinterval} = FORAPIREPDEF;
|
||
|
||
my $interval = int ($period / ($limit / $snum));
|
||
$interval = 900 if($interval < 900);
|
||
|
||
$data{$name}{statusapi}{ForecastSolar}{'?All'}{currentAPIinterval} = $interval;
|
||
|
||
####
|
||
|
||
my $apiitv = StatusAPIVal ($hash, 'ForecastSolar', '?All', 'currentAPIinterval', FORAPIREPDEF);
|
||
my $rtyatts = StatusAPIVal ($hash, 'ForecastSolar', '?All', 'retryat_timestamp', 0);
|
||
my $smt = q{};
|
||
|
||
if ($rtyatts && $rtyatts > $t) { # Zwangswartezeit durch API berücksichtigen
|
||
$apiitv = $rtyatts - $t;
|
||
$data{$name}{statusapi}{ForecastSolar}{'?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
|
||
#
|
||
# Berechnung nach Formel 1 aus http://www.ing-büro-junge.de/html/photovoltaik.html
|
||
# als Jahreserträge:
|
||
#
|
||
# * 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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $date = $paref->{date}; # aktuelles Datum "YYYY-MM-DD"
|
||
my $day = $paref->{day}; # aktuelles Tagesdatum 01 .. 31
|
||
my $t = $paref->{t} // time;
|
||
my $lang = $paref->{lang};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
my $raname = AttrVal ($name, 'setupRadiationAPI', ''); # Radiation Forecast API
|
||
return if(!$raname || !$defs{$raname});
|
||
|
||
my $fcdays = AttrVal ($raname, 'forecastDays', 1); # Anzahl Forecast Days in DWD Device
|
||
my $stime = $date.' 00:00:00'; # Startzeit Soll Übernahmedaten
|
||
my $sts = timestringToTimestamp ($stime);
|
||
my @strings = sort keys %{$data{$name}{strings}};
|
||
my $ret = q{};
|
||
|
||
$data{$name}{statusapi}{DWD}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3];
|
||
$data{$name}{statusapi}{DWD}{'?All'}{lastretrieval_timestamp} = $t;
|
||
$data{$name}{statusapi}{DWD}{'?All'}{todayDoneAPIrequests} += 1;
|
||
|
||
my $fctime = ReadingsVal ($raname, 'fc_time', '-');
|
||
$data{$name}{current}{dwdRad1hDev} = $raname;
|
||
$data{$name}{current}{dwdRad1hAge} = $fctime;
|
||
$data{$name}{current}{dwdRad1hAgeTS} = timestringToTimestamp ($fctime);
|
||
|
||
debugLog ($paref, "apiCall", "DWD API - collect DWD Radiation data with start >$stime<- device: $raname =>");
|
||
|
||
my $end = (24 + $fcdays * 24) - 1; # default 47
|
||
|
||
for my $num (0..$end) { # V 1.36.0
|
||
my ($fd,$fh) = calcDayHourMove (0, $num);
|
||
next if($fh == 24);
|
||
|
||
my $dateTime = strftime "%Y-%m-%d %H:%M:00", localtime($sts + (3600 * $num)); # abzurufendes 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 $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{$name}{statusapi}{DWD}{'?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{$name}{solcastapi}{'?All'}{$dateTime}{Rad1h} = sprintf "%.0f", $rad;
|
||
|
||
my $cafd = 'trackFlex'; # Art der Flächenfaktor Berechnung ('fix' wäre alternativ möglich = alte Methode)
|
||
my ($ddate, $dtime) = split ' ', $dateTime; # abzurufendes Datum + Zeit
|
||
my $hod = sprintf "%02d", ((split ':', $dtime)[0] + 1); # abzurufende Zeit
|
||
my $dday = (split '-', $ddate)[2]; # abzurufender Tag: 01, 02 ... 31
|
||
|
||
for my $string (@strings) { # für jeden String der Config ..
|
||
my $peak = $data{$name}{strings}{$string}{peak}; # String Peak (kWp)
|
||
$peak *= 1000; # kWp in Wp umrechnen
|
||
my $ti = $data{$name}{strings}{$string}{tilt}; # Neigungswinkel Solarmodule
|
||
my $az = $data{$name}{strings}{$string}{azimut}; # Ausrichtung der Solarmodule
|
||
$az += 180; # Umsetzung -180 - 180 in 0 - 360
|
||
|
||
my ($af, $pv, $sdr, $wcc);
|
||
|
||
if ($cafd =~ /track/xs) { # Flächenfaktor Sonnenstand geführt
|
||
($af, $sdr, $wcc) = ___areaFactorTrack ( { name => $name,
|
||
day => $day,
|
||
dday => $dday,
|
||
chour => $paref->{chour},
|
||
hod => $hod,
|
||
tilt => $ti,
|
||
azimut => $az
|
||
}
|
||
);
|
||
|
||
$wcc = 0 if(!isNumeric($wcc));
|
||
$wcc = cloud2bin($wcc);
|
||
|
||
debugLog ($paref, "apiProcess", "DWD API - Value of sunaz/sunalt not stored in pvHistory, workaround using 1.00/0.75")
|
||
if(!isNumeric($af));
|
||
|
||
$af = 1.00 if(!isNumeric($af));
|
||
$sdr = 0.75 if(!isNumeric($sdr));
|
||
|
||
if ($cafd eq 'trackFlex' && $wcc >= 80) { # Direktstrahlung + Diffusstrahlung
|
||
my $dirrad = $rad * $sdr; # Anteil Direktstrahlung an Globalstrahlung
|
||
my $difrad = $rad - $dirrad; # Anteil Diffusstrahlung an Globalstrahlung
|
||
|
||
$pv = sprintf "%.1f", ((($dirrad * $af) + $difrad) * KJ2KWH * $peak * PRDEF); # Rad wird in kW/m2 erwartet
|
||
}
|
||
else { # Flächenfaktor auf volle Rad1h anwenden
|
||
$pv = sprintf "%.1f", ($rad * $af * KJ2KWH * $peak * PRDEF);
|
||
}
|
||
}
|
||
else { # Flächenfaktor Fix
|
||
$af = ___areaFactorFix ($ti, $az); # Flächenfaktor: https://wiki.fhem.de/wiki/Ertragsprognose_PV
|
||
$pv = sprintf "%.1f", ($rad * $af * KJ2KWH * $peak * PRDEF); # Rad wird in kW/m2 erwartet
|
||
}
|
||
|
||
$data{$name}{solcastapi}{$string}{$dateTime}{pv_estimate50} = $pv; # Startzeit wird verwendet, nicht laufende Stunde
|
||
|
||
debugLog ($paref, "apiProcess", "DWD API - PV estimate String >$string< => $dateTime, $pv Wh, Afactor: $af ($cafd)");
|
||
}
|
||
}
|
||
|
||
$data{$name}{statusapi}{DWD}{'?All'}{response_message} = 'success' if(!$ret);
|
||
|
||
return;
|
||
}
|
||
|
||
##################################################################################################
|
||
# Flächenfaktor Photovoltaik
|
||
# Prof. Dr. Peter A. Henning, September 2024
|
||
# ersetzt die Tabelle auf Basis http://www.ing-büro-junge.de/html/photovoltaik.html
|
||
# siehe Wiki: https://wiki.fhem.de/wiki/Ertragsprognose_PV
|
||
##################################################################################################
|
||
sub ___areaFactorFix {
|
||
my $tilt = shift;
|
||
my $azimut = shift;
|
||
|
||
my $pi180 = 0.0174532918889; # Grad in Radiant Umrechnungsfaktor
|
||
|
||
my $x = $tilt * sin ($azimut * $pi180);
|
||
my $y = $tilt * cos ($azimut * $pi180);
|
||
my $x2 = $x**2;
|
||
my $x4 = $x2**2;
|
||
|
||
my $af = 3.808301895960147E-7 - 8.650170178954599E-11 * $x2 + 5.50016483344622E-15 * $x4;
|
||
$af = $af * $y + 0.00007319316326291892 - 3.604294916743569E-9 * $x2 - 2.343747951073022E-13 * $x4;
|
||
$af = $af * $y - 0.00785953342909065 + 1.1197340251684106E-6 * $x2 - 8.99915952119488E-11 * $x4;
|
||
$af = $af * $y - 0.8432627150525525 + 0.00010392051567819936 * $x2 - 3.979206287671085E-9 * $x4;
|
||
$af = $af * $y + 99.49627151067648 - 0.006340200119196879 * $x2 + 2.052575360270524E-7 * $x4;
|
||
$af = sprintf "%.2f", ($af / 100); # Prozenz in Faktor
|
||
|
||
return $af;
|
||
}
|
||
|
||
##########################################################################################################
|
||
# Flächenfaktor Photovoltaik und Direktstrahlungsanteilsfaktor in Abhängigkeit des Sonnenstandes
|
||
#
|
||
# Die Globalstrahlung (Summe aus diffuser und direkter Sonnenstrahlung)
|
||
# ----------------------------------------------------------------------
|
||
# Die Globalstrahlung ist die am Boden von einer horizontalen Ebene empfangene Sonnenstrahlung
|
||
# und setzt sich aus der direkten Strahlung (der Schatten werfenden Strahlung) und der
|
||
# gestreuten Sonnenstrahlung (diffuse Himmelsstrahlung) aus der Himmelshalbkugel zusammen.
|
||
# Bei Sonnenhöhen von mehr als 50° und wolkenlosem Himmel besteht die Globalstrahlung zu ca. 3/4
|
||
# aus direkter Sonnenstrahlung, bei tiefen Sonnenständen (bis etwa 10°) nur noch zu ca. 1/3.
|
||
#
|
||
# Direktstrahlung = Globalstrahlung * 0.75 (bei > 50° sunalt)
|
||
# Direktstrahlung = Globalstrahlung * 0.33 (bei <= 10° sunalt)
|
||
#
|
||
# Quelle: https://www.dwd.de/DE/leistungen/solarenergie/globalstrahlung.html?nn=16102&lsbId=416798
|
||
#
|
||
# Return:
|
||
# $daf - direct Area Faktor für den Anteil Direktstrahlung der Globalstrahlung
|
||
# $sdr - Share of direct radiation = Faktor Anteil Direktstrahlung an Globalstrahlung (0.33 .. 0.75)
|
||
#
|
||
##########################################################################################################
|
||
sub ___areaFactorTrack {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $day = $paref->{day}; # aktueller Tag 01 .. 31
|
||
my $dday = $paref->{dday}; # abzufragender Tag: 01 .. 31
|
||
my $chour = $paref->{chour}; # aktuelle Stunde (00 .. 23)
|
||
my $hod = $paref->{hod}; # abzufragende Stunde des Tages 01, 02 ... 24
|
||
my $tilt = $paref->{tilt}; # String Anstellwinkel / Neigung
|
||
my $azimut = $paref->{azimut}; # String Ausrichtung / Azimut
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
my ($sunalt, $sunaz, $wcc);
|
||
|
||
if ($dday eq $day) {
|
||
$sunalt = HistoryVal ($hash, $dday, $hod, 'sunalt', undef); # Sonne Höhe (Altitude)
|
||
$sunaz = HistoryVal ($hash, $dday, $hod, 'sunaz', undef); # Sonne Azimuth
|
||
$wcc = HistoryVal ($hash, $dday, $hod, 'wcc', 0); # Bewölkung
|
||
}
|
||
else {
|
||
my $nhtstr = 'NextHour'.sprintf "%02d", (23 - (int $chour) + $hod);
|
||
$sunalt = NexthoursVal ($hash, $nhtstr, 'sunalt', undef);
|
||
$sunaz = NexthoursVal ($hash, $nhtstr, 'sunaz', undef);
|
||
$wcc = NexthoursVal ($hash, $nhtstr, 'wcc', 0);
|
||
}
|
||
|
||
return ('-', '-', '-') if(!defined $sunalt || !defined $sunaz);
|
||
|
||
my $pi180 = 0.0174532918889; # PI/180
|
||
|
||
#-- Normale der Anlage (Nordrichtung = y-Achse, Ostrichtung = x-Achse)
|
||
my $nz = cos ($tilt * $pi180);
|
||
my $ny = sin ($tilt * $pi180) * cos ($azimut * $pi180);
|
||
my $nx = sin ($tilt * $pi180) * sin ($azimut * $pi180);
|
||
|
||
#-- Vektor zur Sonne
|
||
my $sz = sin ($sunalt * $pi180);
|
||
my $sy = cos ($sunalt * $pi180) * cos ($sunaz * $pi180);
|
||
my $sx = cos ($sunalt * $pi180) * sin ($sunaz * $pi180);
|
||
|
||
#-- Normale N = ($nx,$ny,$nz) Richtung Sonne S = ($sx,$sy,$sz)
|
||
my $daf = $nx * $sx + $ny * $sy + $nz * $sz;
|
||
$daf = max ($daf, 0);
|
||
$daf += 1 if($daf);
|
||
$daf = sprintf "%.2f", $daf;
|
||
|
||
## Schätzung Anteil Direktstrahlung an Globalstrahlung
|
||
########################################################
|
||
my $drif = 0.0105; # Faktor Zunahme Direktstrahlung pro Grad sunalt von 10° bis 50°
|
||
my $sdr = $sunalt <= 10 ? 0.33 :
|
||
$sunalt > 10 && $sunalt <= 50 ? (($sunalt - 10) * 0.0105) + 0.33 :
|
||
0.75;
|
||
|
||
return ($daf, $sdr, $wcc);
|
||
}
|
||
|
||
####################################################################################################
|
||
# Abruf Victron VRM API Forecast
|
||
#
|
||
# https://community.victronenergy.com/questions/216543/new-vrm-feature-solar-forecast.html
|
||
# API Beschreibung: https://vrm-api-docs.victronenergy.com/#/operations/installations/idSite/stats
|
||
####################################################################################################
|
||
sub __getVictronSolarData {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $force = $paref->{force} // 0;
|
||
my $t = $paref->{t};
|
||
my $lang = $paref->{lang};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
my $lrt = StatusAPIVal ($hash, 'VictronKi', '?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 $name = $paref->{name};
|
||
my $debug = $paref->{debug};
|
||
my $type = $paref->{type};
|
||
|
||
my $hash = $defs{$name};
|
||
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 = StatusAPIVal ($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{$name}{statusapi}{VictronKi}{'?All'}{response_message} = $msg;
|
||
return;
|
||
}
|
||
|
||
my $param = {
|
||
url => $url,
|
||
timeout => 30,
|
||
name => $name,
|
||
type => $paref->{type},
|
||
stc => [gettimeofday],
|
||
debug => $debug,
|
||
caller => \&$caller,
|
||
lang => $paref->{lang},
|
||
chour => $paref->{chour}, # aktuelle Stunde in 24h format (00-23)
|
||
date => $paref->{date},
|
||
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 $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 $hash = $defs{$name};
|
||
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{$name}{statusapi}{VictronKi}{'?All'}{response_message} = $err;
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$name}{current}{runTimeLastAPIAnswer} = sprintf "%.4f", (tv_interval($stc) - tv_interval($sta)); # API Laufzeit ermitteln
|
||
|
||
$data{$name}{statusapi}{VictronKi}{'?All'}{response_message} = $jdata->{'error_code'};
|
||
$data{$name}{statusapi}{VictronKi}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit
|
||
$data{$name}{statusapi}{VictronKi}{'?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{$name}{statusapi}{VictronKi}{'?All'}{response_message} = 'success';
|
||
$data{$name}{statusapi}{VictronKi}{'?All'}{idUser} = $jdata->{'idUser'};
|
||
$data{$name}{statusapi}{VictronKi}{'?All'}{verification_mode} = $jdata->{'verification_mode'};
|
||
$data{$name}{statusapi}{VictronKi}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit
|
||
$data{$name}{statusapi}{VictronKi}{'?All'}{lastretrieval_timestamp} = $t;
|
||
|
||
if ($debug eq 'apiProcess') {
|
||
Log3 ($name, 1, qq{$name DEBUG> Victron VRM API response Login:\n}. Dumper $jdata);
|
||
}
|
||
|
||
if (defined $jdata->{'token'}) {
|
||
$data{$name}{statusapi}{VictronKi}{'?All'}{token} = 'got successful at '.StatusAPIVal ($hash, 'VictronKi', '?All', 'lastretrieval_time', '-');
|
||
$paref->{token} = $jdata->{'token'};
|
||
__VictronVRM_ApiRequestForecast ($paref);
|
||
}
|
||
else {
|
||
$data{$name}{statusapi}{VictronKi}{'?All'}{response_message} = 'empty Token';
|
||
}
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
######################################################################################################
|
||
# Victron VRM API Forecast Data
|
||
# https://vrm-api-docs.victronenergy.com/#/
|
||
# # API Beschreibung: https://vrm-api-docs.victronenergy.com/#/operations/installations/idSite/stats
|
||
######################################################################################################
|
||
sub __VictronVRM_ApiRequestForecast {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $token = $paref->{token};
|
||
my $debug = $paref->{debug};
|
||
my $lang = $paref->{lang};
|
||
my $idsite = $paref->{idsite};
|
||
my $chour = $paref->{chour}; # aktuelle Stunde in 24h format (00-23)
|
||
my $date = $paref->{date};
|
||
|
||
my $hash = $defs{$name};
|
||
my $tstart = timestringToTimestamp ("$date $chour:00:00");
|
||
my $tend = $tstart + 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,
|
||
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 $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 $hash = $defs{$name};
|
||
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{$name}{statusapi}{VictronKi}{'?All'}{response_message} = $err;
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$name}{current}{runTimeLastAPIAnswer} = sprintf "%.4f", (tv_interval($stc) - tv_interval($sta)); # API Laufzeit ermitteln
|
||
|
||
$data{$name}{statusapi}{VictronKi}{'?All'}{response_message} = $jdata->{'error_code'};
|
||
$data{$name}{statusapi}{VictronKi}{'?All'}{lastretrieval_time} = (timestampToTimestring ($t, $lang))[3]; # letzte Abrufzeit
|
||
$data{$name}{statusapi}{VictronKi}{'?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{$name}{statusapi}{VictronKi}{'?All'}{todayDoneAPIrequests} += 1;
|
||
$data{$name}{statusapi}{VictronKi}{'?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 = AttrVal ($name, 'setupInverterStrings', '?');
|
||
|
||
$data{$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];
|
||
|
||
debugLog ($paref, "apiProcess", "Victron VRM API - CO estimate: ".$starttmstr.' => '.$val.' Wh');
|
||
|
||
if ($val) {
|
||
$val = sprintf "%.2f", $val;
|
||
my $string = AttrVal ($name, 'setupInverterStrings', '?');
|
||
|
||
$data{$name}{solcastapi}{$string.'_co'}{$starttmstr}{co_estimate} = $val;
|
||
}
|
||
|
||
$k++;
|
||
}
|
||
}
|
||
}
|
||
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval ($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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 $name = $paref->{name};
|
||
my $token = $paref->{token};
|
||
my $debug = $paref->{debug};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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,
|
||
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 $name = $paref->{name};
|
||
my $debug = $paref->{debug};
|
||
my $hash = $defs{$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);
|
||
|
||
if ($debug eq 'apiCall') {
|
||
Log3 ($name, 1, qq{$name DEBUG> Victron VRM API response Logout:\n}. Dumper $jdata);
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################################################
|
||
# Abruf Open-Meteo API Daten
|
||
################################################################################################
|
||
sub __getopenMeteoData {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $force = $paref->{force} // 0;
|
||
my $t = $paref->{t};
|
||
my $lang = $paref->{lang};
|
||
my $debug = $paref->{debug};
|
||
my $reqm = $paref->{reqm};
|
||
|
||
my $hash = $defs{$name};
|
||
my $donearq = StatusAPIVal ($hash, 'OpenMeteo', '?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 = StatusAPIVal ($hash, 'OpenMeteo', '?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}, $reqm, '');
|
||
|
||
$paref->{allstrings} = AttrVal ($name, 'setupInverterStrings', '');
|
||
$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;
|
||
$paref->{requestmode} = $reqm;
|
||
|
||
__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=<Werte Komma getrennt>&daily=<Werte Komma getrennt>&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 $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 Datenmodell
|
||
my $requestmode = $paref->{requestmode};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if (!$allstrings) { # alle Strings wurden abgerufen
|
||
my $apiitv = StatusAPIVal ($hash, 'OpenMeteo', '?All', 'currentAPIinterval', OMETEOREPDEF);
|
||
readingsSingleUpdate ($hash, 'nextRadiationAPICall', $hqtxt{after}{$lang}.' '.(timestampToTimestring ($t + $apiitv, $lang))[0], 1);
|
||
|
||
$data{$name}{statusapi}{OpenMeteo}{'?All'}{todayDoneAPIcalls} += 1;
|
||
|
||
return;
|
||
}
|
||
|
||
my ($string, $err);
|
||
($string, $allstrings) = split ",", $allstrings, 2;
|
||
my ($set, $lat, $lon, $elev) = locCoordinates();
|
||
|
||
if (!$set) {
|
||
$err = qq{ERROR - the attribute 'latitude' and/or 'longitude' in global device is not set};
|
||
Log3 ($name, 1, "$name - $err");
|
||
return;
|
||
}
|
||
|
||
my $tilt = StringVal ($hash, $string, 'tilt', '<unknown>');
|
||
my $az = StringVal ($hash, $string, 'azimut', '<unknown>');
|
||
|
||
if ($requestmode eq 'WEATHERMODEL' && $string eq 'KI-based') {$tilt = 0; $az = 0;} # Dummy Settings
|
||
|
||
if ($tilt eq '<unknown>' || $az eq '<unknown>') {
|
||
$err = qq{ERROR OpenMeteo API Call - the reading 'setupStringAzimuth' and/or 'setupStringDeclination' is not set};
|
||
Log3 ($name, 1, "$name - $err");
|
||
return;
|
||
}
|
||
|
||
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 Data Model >$submodel<:\n$url});
|
||
debugLog ($paref, 'apiCall|apiProcess', qq{Open-Meteo API Call - Request Mode: $requestmode});
|
||
|
||
my $caller = (caller(0))[3]; # Rücksprungmarke
|
||
|
||
my $param = {
|
||
url => $url,
|
||
timeout => 30,
|
||
name => $name,
|
||
type => $paref->{type},
|
||
debug => $debug,
|
||
header => 'Accept: application/json',
|
||
submodel => $submodel,
|
||
begin => $paref->{begin},
|
||
callequivalent => $paref->{callequivalent},
|
||
requestmode => $requestmode,
|
||
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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $caller = $paref->{caller};
|
||
my $string = $paref->{string};
|
||
my $allstrings = $paref->{allstrings};
|
||
my $requestmode = $paref->{requestmode}; # MODEL oder WEATHERMODEL
|
||
my $stc = $paref->{stc}; # Startzeit API Abruf
|
||
my $lang = $paref->{lang};
|
||
my $debug = $paref->{debug};
|
||
my $submodel = $paref->{submodel};
|
||
|
||
my $hash = $defs{$name};
|
||
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{$name}{statusapi}{OpenMeteo}{'?All'}{response_message} = $err;
|
||
|
||
singleUpdateState ( {hash => $hash, state => $msg, evt => 1} );
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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);
|
||
|
||
$data{$name}{statusapi}{OpenMeteo}{'?All'}{lastretrieval_time} = $rt;
|
||
$data{$name}{statusapi}{OpenMeteo}{'?All'}{lastretrieval_timestamp} = $t;
|
||
|
||
## bei Fehler in API intern kommt
|
||
###################################
|
||
# error: true
|
||
# reason: <Grund>
|
||
|
||
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{$name}{statusapi}{OpenMeteo}{'?All'}{response_message} = $jdata->{'reason'};
|
||
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$name}{current}{runTimeLastAPIAnswer} = sprintf "%.4f", (tv_interval($stc) - tv_interval($sta)); # API Laufzeit ermitteln
|
||
|
||
return;
|
||
}
|
||
|
||
$data{$name}{statusapi}{OpenMeteo}{'?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 * WH2KJ) / 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 = $jdata->{hourly}{weather_code}[$k];
|
||
my $wcc = $jdata->{hourly}{cloud_cover}[$k];
|
||
|
||
if ($debug =~ /apiProcess/xs) {
|
||
if ($requestmode eq 'MODEL') {
|
||
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);
|
||
}
|
||
}
|
||
|
||
if ($requestmode eq 'MODEL') {
|
||
$data{$name}{solcastapi}{$string}{$pvtmstr}{pv_estimate50} = $pv; # Startstunde verschieben
|
||
|
||
if ($paref->{begin}) { # im ersten Call den DS löschen -> dann Aufsummierung
|
||
delete $data{$name}{solcastapi}{'?All'}{$pvtmstr}{Rad1h};
|
||
}
|
||
|
||
$data{$name}{solcastapi}{'?All'}{$pvtmstr}{Rad1h} += $rad; # Startstunde verschieben, Rad Werte aller Strings addieren
|
||
}
|
||
|
||
## Wetterdaten
|
||
################
|
||
my $fwtg = formatWeatherTimestrg ($pvtmstr); # Zeit gemäß DWD_OpenData-Format
|
||
|
||
$data{$name}{weatherapi}{OpenMeteo}{$fwtg}{rr1c} = $rain;
|
||
$data{$name}{weatherapi}{OpenMeteo}{$fwtg}{StartTime} = $pvtmstr;
|
||
|
||
$fwtg = formatWeatherTimestrg ($otmstr); # Zeit gemäß DWD_OpenData-Format
|
||
|
||
$data{$name}{weatherapi}{OpenMeteo}{$fwtg}{don} = $don;
|
||
$data{$name}{weatherapi}{OpenMeteo}{$fwtg}{neff} = $wcc;
|
||
$data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ww} = $wid;
|
||
$data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ttt} = $temp;
|
||
$data{$name}{weatherapi}{OpenMeteo}{$fwtg}{UpdateTime} = $rt;
|
||
$data{$name}{weatherapi}{OpenMeteo}{$fwtg}{StartTime} = $otmstr;
|
||
|
||
if ($k == 0) {
|
||
$data{$name}{weatherapi}{OpenMeteo}{$fwtg}{neff} = $curwcc if(defined $curwcc);
|
||
$data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ww} = $curwid if(defined $curwid);
|
||
$data{$name}{weatherapi}{OpenMeteo}{$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{$name}{weatherapi}{OpenMeteo}{sunrise}{today} = $sunrise;
|
||
$data{$name}{weatherapi}{OpenMeteo}{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{$name}{weatherapi}{OpenMeteo}{sunrise}{tomorrow} = $sunrise;
|
||
$data{$name}{weatherapi}{OpenMeteo}{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 = {
|
||
name => $name,
|
||
type => $type,
|
||
debug => $debug,
|
||
allstrings => $allstrings,
|
||
submodel => $submodel,
|
||
callequivalent => $paref->{callequivalent},
|
||
requestmode => $requestmode,
|
||
lang => $lang
|
||
};
|
||
|
||
$data{$name}{current}{runTimeLastAPIProc} = sprintf "%.4f", tv_interval($sta); # Verarbeitungszeit ermitteln
|
||
$data{$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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $lang = $paref->{lang};
|
||
my $debug = $paref->{debug};
|
||
my $cequ = $paref->{callequivalent};
|
||
my $t = $paref->{t} // time;
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
$data{$name}{statusapi}{OpenMeteo}{'?All'}{todayDoneAPIrequests} += $cequ;
|
||
|
||
my $dar = StatusAPIVal ($hash, 'OpenMeteo', '?All', 'todayDoneAPIrequests', 0);
|
||
my $dac = StatusAPIVal ($hash, 'OpenMeteo', '?All', 'todayDoneAPIcalls', 0);
|
||
my $asc = CurrentVal ($hash, 'allstringscount', 1);
|
||
|
||
my $drr = OMETMAXREQ - $dar;
|
||
$drr = 0 if($drr < 0);
|
||
|
||
$data{$name}{statusapi}{OpenMeteo}{'?All'}{todayRemainingAPIrequests} = $drr;
|
||
$data{$name}{statusapi}{OpenMeteo}{'?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{$name}{statusapi}{OpenMeteo}{'?All'}{currentAPIinterval} = $optrep;
|
||
}
|
||
|
||
debugLog ($paref, "apiProcess|apiCall", "Open-Meteo API Call - remaining API Requests: $drr, Request equivalents p. call: $cequ, new call interval: ".StatusAPIVal ($hash, 'OpenMeteo', '?All', 'currentAPIinterval', OMETEOREPDEF));
|
||
|
||
return;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter data
|
||
###############################################################
|
||
sub _getdata {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $defs{$name};
|
||
|
||
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);
|
||
}
|
||
|
||
################################################################
|
||
# verborgener Getter outputMessages
|
||
################################################################
|
||
sub _getoutputMessages { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $defs{$name};
|
||
|
||
my $out = outputMessages ($paref);
|
||
$out = qq{<html>$out</html>};
|
||
|
||
$data{$name}{messages}{999999}{RD} = 1; # Lesekennzeichen setzen
|
||
|
||
## asynchrone Ausgabe
|
||
#######################
|
||
#$err = getClHash($hash);
|
||
#$paref->{out} = $out;
|
||
#InternalTimer(gettimeofday()+3, "FHEM::SolarForecast::__plantCfgAsynchOut", $paref, 0);
|
||
|
||
return $out;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter pvQualities
|
||
###############################################################
|
||
sub _getForecastQualities {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $arg = $paref->{arg} // q{};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'qualities');
|
||
|
||
if ($arg eq 'imgget') { # Ausgabe aus dem Grafikheader Qualitätsicon
|
||
$ret =~ s/\n/<br>/g;
|
||
}
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter pvHistory
|
||
###############################################################
|
||
sub _getlistPVHistory {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $arg = $paref->{arg};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'pvhist', $arg);
|
||
return if(!$ret);
|
||
|
||
$ret .= lineFromSpaces ($ret, 20);
|
||
$ret =~ s/\n/<br>/g;
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter pvCircular
|
||
###############################################################
|
||
sub _getlistPVCircular {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $arg = $paref->{arg};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'circular', $arg);
|
||
$ret .= lineFromSpaces ($ret, 10);
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter nextHours
|
||
###############################################################
|
||
sub _getlistNextHours {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'nexthours');
|
||
$ret .= lineFromSpaces ($ret, 20);
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter valCurrent
|
||
###############################################################
|
||
sub _getlistCurrent {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'current');
|
||
$ret .= lineFromSpaces ($ret, 30);
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter valBattery
|
||
###############################################################
|
||
sub _getlistvalBattery {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $arg = $paref->{arg};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'batteries', $arg);
|
||
$ret .= lineFromSpaces ($ret, 30);
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter valConsumerMaster
|
||
###############################################################
|
||
sub _getlistvalConsumerMaster {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $arg = $paref->{arg};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'consumers', $arg);
|
||
$ret .= lineFromSpaces ($ret, 10);
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter valInverter
|
||
###############################################################
|
||
sub _getlistvalInverter {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $arg = $paref->{arg};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'inverters', $arg);
|
||
$ret .= lineFromSpaces ($ret, 30);
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter valProducer
|
||
###############################################################
|
||
sub _getlistvalProducer {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $arg = $paref->{arg};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'producers', $arg);
|
||
$ret .= lineFromSpaces ($ret, 30);
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter valStrings
|
||
###############################################################
|
||
sub _getlistvalStrings {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $arg = $paref->{arg};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'strings', $arg);
|
||
$ret .= lineFromSpaces ($ret, 30);
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter radiationApiData
|
||
###############################################################
|
||
sub _getlistRadiationApiData {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'radiationApiData');
|
||
$ret .= lineFromSpaces ($ret, 10);
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter weatherApiData
|
||
###############################################################
|
||
sub _getlistWeatherApiData {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'weatherApiData');
|
||
$ret .= lineFromSpaces ($ret, 20);
|
||
|
||
return $ret;
|
||
}
|
||
|
||
###############################################################
|
||
# Getter statusApiData
|
||
###############################################################
|
||
sub _getlistStatusApiData {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $defs{$name};
|
||
|
||
my $ret = listDataPool ($hash, 'statusApiData');
|
||
$ret .= lineFromSpaces ($ret, 20);
|
||
|
||
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 = grep (/byID/, @$aa) ? 'byID' :
|
||
grep (/byName/, @$aa) ? 'byName' :
|
||
'byID';
|
||
my $export = grep (/exportgpx/, @$aa) ? 'exportgpx' : '';
|
||
my $force = grep (/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{$name}{dwdcatalog}}) { # Katalog ist nicht geladen
|
||
readCacheFile ({ name => $name,
|
||
type => $type,
|
||
debug => $paref->{debug},
|
||
file => $dwdcatalog,
|
||
cachename => 'dwdcatalog',
|
||
title => 'DWD Station Catalog'
|
||
}
|
||
);
|
||
|
||
if (!scalar keys %{$data{$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{$name}{dwdcatalog}}) {
|
||
$paref->{id} = $id;
|
||
|
||
($err, $isfil) = ___isCatFiltered ($paref);
|
||
return (split " at", $err)[0] if($err);
|
||
next if($isfil);
|
||
|
||
my $nid = $data{$name}{dwdcatalog}{$id}{stnam};
|
||
$temp{$nid}{stnam} = $data{$name}{dwdcatalog}{$id}{stnam};
|
||
$temp{$nid}{id} = $data{$name}{dwdcatalog}{$id}{id};
|
||
$temp{$nid}{latdec} = $data{$name}{dwdcatalog}{$id}{latdec}; # Latitude Dezimalgrad
|
||
$temp{$nid}{londec} = $data{$name}{dwdcatalog}{$id}{londec}; # Longitude Dezimalgrad
|
||
$temp{$nid}{elev} = $data{$name}{dwdcatalog}{$id}{elev};
|
||
}
|
||
}
|
||
elsif ($sort eq 'byID') {
|
||
for my $id (keys %{$data{$name}{dwdcatalog}}) {
|
||
$paref->{id} = $id;
|
||
($err, $isfil) = ___isCatFiltered ($paref);
|
||
return (split " at", $err)[0] if($err);
|
||
next if($isfil);
|
||
|
||
$temp{$id}{stnam} = $data{$name}{dwdcatalog}{$id}{stnam};
|
||
$temp{$id}{id} = $data{$name}{dwdcatalog}{$id}{id};
|
||
$temp{$id}{latdec} = $data{$name}{dwdcatalog}{$id}{latdec}; # Latitude Dezimalgrad
|
||
$temp{$id}{londec} = $data{$name}{dwdcatalog}{$id}{londec}; # Longitude Dezimalgrad
|
||
$temp{$id}{elev} = $data{$name}{dwdcatalog}{$id}{elev};
|
||
}
|
||
}
|
||
|
||
if ($export eq 'exportgpx') { # DWD Katalog als gpx speichern
|
||
my @data = ();
|
||
push @data, '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>';
|
||
push @data, '<gpx xmlns="http://www.topografix.com/GPX/1/1" creator="FHEM::SolarForecast"';
|
||
push @data, 'version="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"';
|
||
push @data, 'xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">';
|
||
|
||
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{<wpt lat="$latdec" lon="$londec">};
|
||
push @data, qq{ <ele>$elev</ele>};
|
||
push @data, qq{ <name>$stnam (ID=$id, Latitude=$latdec, Longitude=$londec)</name>};
|
||
push @data, qq{ <sym>City</sym>};
|
||
push @data, qq{</wpt>};
|
||
}
|
||
|
||
push @data, '</gpx>';
|
||
|
||
$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 = '<html>';
|
||
$out .= '<b>'.encode('utf8', $hqtxt{dwdcat}{$lang}).'</b><br>'; # The Deutscher Wetterdienst Station Catalog
|
||
$out .= encode('utf8', $hqtxt{nrsele}{$lang}).' '.$noe.'<br>'; # Selected entries
|
||
$out .= "($select) <br><br>";
|
||
|
||
$out .= qq{<table class="roomoverview" style="text-align:left; border:1px solid; padding:5px; border-spacing:5px; margin-left:auto; margin-right:auto;">};
|
||
$out .= qq{<tr style="font-weight:bold;">};
|
||
$out .= qq{<td style="text-decoration:underline; padding: 5px;"> ID </td>};
|
||
$out .= qq{<td style="text-decoration:underline; padding: 5px;"> NAME </td>};
|
||
$out .= qq{<td style="text-decoration:underline; padding: 5px;"> LATITUDE </td>};
|
||
$out .= qq{<td style="text-decoration:underline; padding: 5px;"> LONGITUDE </td>};
|
||
$out .= qq{<td style="text-decoration:underline; padding: 5px;"> ELEVATION </td>};
|
||
$out .= qq{</tr>};
|
||
$out .= qq{<tr></tr>};
|
||
|
||
for my $key (sort keys %temp) {
|
||
$out .= qq{<tr>};
|
||
$out .= qq{<td style="padding: 5px; "> $temp{"$key"}{id} </td>};
|
||
$out .= qq{<td style="padding: 5px; white-space:nowrap;"> $temp{"$key"}{stnam} </td>};
|
||
$out .= qq{<td style="padding: 5px; "> $temp{"$key"}{latdec} </td>};
|
||
$out .= qq{<td style="padding: 5px; "> $temp{"$key"}{londec} </td>};
|
||
$out .= qq{<td style="padding: 5px; "> $temp{"$key"}{elev} </td>};
|
||
$out .= qq{</tr>};
|
||
}
|
||
|
||
$out .= qq{</table>};
|
||
$out .= qq{</html>};
|
||
|
||
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{$name}{dwdcatalog}{$id}{stnam} !~ /^$filtnam$/ixs);
|
||
$isfil = 1 if($filtlat && $data{$name}{dwdcatalog}{$id}{latdec} !~ /^$filtlat$/ixs);
|
||
$isfil = 1 if($filtlon && $data{$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 $name = $paref->{name};
|
||
my $debug = $paref->{debug};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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,
|
||
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 $dat = shift;
|
||
|
||
my $name = $paref->{name};
|
||
my $stc = $paref->{stc}; # Startzeit API Abruf
|
||
my $lang = $paref->{lang};
|
||
my $debug = $paref->{debug};
|
||
|
||
my $msg;
|
||
my $hash = $defs{$name};
|
||
my $sta = [gettimeofday]; # Start Response Verarbeitung
|
||
|
||
if ($err ne "") {
|
||
Log3 ($name, 1, "$name - ERROR - $err");
|
||
|
||
return;
|
||
}
|
||
elsif ($dat ne "") {
|
||
my @datarr = split "\n", $dat;
|
||
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{$name}{dwdcatalog}{$id}{id} = $id;
|
||
$data{$name}{dwdcatalog}{$id}{stnam} = $stnam;
|
||
$data{$name}{dwdcatalog}{$id}{icao} = $icao;
|
||
$data{$name}{dwdcatalog}{$id}{lat} = $lat;
|
||
$data{$name}{dwdcatalog}{$id}{latdec} = $latdec; # Latitude Dezimalgrad
|
||
$data{$name}{dwdcatalog}{$id}{lon} = $lon;
|
||
$data{$name}{dwdcatalog}{$id}{londec} = $londec; # Longitude Dezimalgrad
|
||
$data{$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 ({ 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 $name = $paref->{name};
|
||
my $arg = $paref->{arg} // return;
|
||
|
||
my $ret;
|
||
my $hash = $defs{$name};
|
||
|
||
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 $name = $paref->{name};
|
||
my $lang = $paref->{lang};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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 = '<b>'.$hqtxt{ailatr}{$lang}.' </b>'.($atf ? (timestampToTimestring ($atf, $lang))[0] : '-');
|
||
my $art = $hqtxt{aitris}{$lang}.' '.CircularVal ($hash, 99, 'runTimeTrainAI', '-');
|
||
|
||
if (@rsl) {
|
||
my $l = scalar @rsl;
|
||
$rs = "<b>Number of Rules: $l / Number of Nodes: $nodes / Depth: $depth</b>\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 $name = $paref->{name};
|
||
my $hash = $defs{$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<br>$cmrec";
|
||
return $ret;
|
||
}
|
||
|
||
if ($cmupd) {
|
||
$upddo = 1;
|
||
$ret = __updPreFile ( { name => $name,
|
||
root => $root,
|
||
cmfile => $cmfile,
|
||
cmlen => $cmlen,
|
||
file => $file,
|
||
lencheck => $lencheck
|
||
}
|
||
);
|
||
|
||
return $ret if($ret);
|
||
}
|
||
}
|
||
|
||
## finales Update control File
|
||
################################
|
||
$ret = __updPreFile ( { name => $name,
|
||
root => $root,
|
||
cmfile => 'FHEM/'.CFILE,
|
||
cmlen => 0,
|
||
file => CFILE,
|
||
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 $file = $pars->{file};
|
||
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.<br>";
|
||
$err .= "Please check whether the path $path is present and accessible.<br>";
|
||
$err .= "After installing FTUI, come back and execute the get command again.";
|
||
return $err;
|
||
}
|
||
}
|
||
|
||
($err, my $remFile) = __httpBlockingGet ($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 __httpBlockingGet {
|
||
my $name = shift;
|
||
my $url = shift;
|
||
|
||
$url =~ s/%/%25/g;
|
||
my %connecthash;
|
||
my $unicodeEncoding = 1;
|
||
|
||
$connecthash{url} = $url;
|
||
$connecthash{keepalive} = ($url =~ m/localUpdate/ ? 0 : 1); # Forum #49798
|
||
$connecthash{forceEncoding} = '' if($unicodeEncoding);
|
||
|
||
my ($err, $dat) = HttpUtils_BlockingGet (\%connecthash);
|
||
|
||
if ($err) {
|
||
$err = "GetUrl ERROR: $err";
|
||
return ($err, '');
|
||
}
|
||
|
||
if (!$dat) {
|
||
$err = 'WARNING - empty file received';
|
||
return ($err, '');
|
||
}
|
||
|
||
return ('', $dat);
|
||
}
|
||
|
||
###############################################################
|
||
# 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 =~ /^graphicBeam1MaxVal|ctrlAreaFactorUsage$/) {
|
||
# my $msg = "The attribute $aName is obsolete and will be deleted soon. Please save your Configuration.";
|
||
# if (!$init_done) {
|
||
# Log3 ($name, 1, "$name - $msg");
|
||
# return qq{Device "$name" -> $msg};
|
||
# }
|
||
# else {
|
||
# return $msg;
|
||
# }
|
||
#}
|
||
######################################################################################################################
|
||
|
||
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 'ctrlNextDayForecastReadings') {
|
||
deleteReadingspec ($hash, "Tomorrow_Hour.*");
|
||
}
|
||
|
||
if ($aName eq 'ctrlNextHoursSoCForecastReadings') {
|
||
deleteReadingspec ($hash, "Battery_NextHour.._SoCforecast_..");
|
||
}
|
||
|
||
if ($aName =~ /ctrlBatSocManagement/xs && $init_done) {
|
||
my $bn = (split 'ctrlBatSocManagement', $aName)[1];
|
||
|
||
if ($cmd eq 'set') {
|
||
return qq{Define the key 'cap' with "attr $name setupBatteryDev${bn}" before this attribute in the correct form.}
|
||
if(!BatteryVal ($hash, $bn, 'binstcap', 0)); # https://forum.fhem.de/index.php?msg=1310930
|
||
|
||
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{$name}{circular}{99}{'lastTsMaxSocRchd'.$bn};
|
||
delete $data{$name}{circular}{99}{'nextTsMaxSocChge'.$bn};
|
||
}
|
||
|
||
if ($aName eq 'ctrlGenPVdeviation' && $aVal eq 'daily') {
|
||
readingsDelete ($hash, 'Today_PVdeviation');
|
||
delete $data{$name}{circular}{99}{tdayDvtn};
|
||
}
|
||
|
||
if ($aName eq 'graphicHeaderOwnspecValForm') {
|
||
$err = isGhoValFormValid ($name, $aVal);
|
||
return $err if($err);
|
||
}
|
||
|
||
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 ($init_done && $aName eq 'ctrlSolCastAPIoptimizeReq') {
|
||
if (!isSolCastUsed ($hash)) {
|
||
return qq{The attribute $aName is only valid for device model "SolCastAPI".};
|
||
}
|
||
}
|
||
|
||
if ($init_done && $aName eq 'ctrlUserExitFn') {
|
||
($err) = checkCode ($name, $aVal, 'cc1');
|
||
return $err if($err);
|
||
}
|
||
|
||
if ($init_done && $aName eq 'ctrlInterval') {
|
||
_newCycTime ($hash, time, $aVal);
|
||
my $nct = CurrentVal ($hash, 'nextCycleTime', 0); # gespeicherte nächste CyleTime
|
||
readingsSingleUpdate ($hash, 'nextCycletime', (!$nct ? 'Manual / Event-controlled' : FmtTime($nct)), 0);
|
||
return;
|
||
}
|
||
}
|
||
|
||
my $params = {
|
||
name => $name,
|
||
type => $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 $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 $hash = $defs{$name};
|
||
|
||
if ($cmd eq "set") {
|
||
my ($err, $codev, $h) = isDeviceValid ( { name => $name, obj => $aVal, method => 'string' } );
|
||
return $err if($err);
|
||
|
||
if (!$h->{type} || !exists $h->{power}) {
|
||
return qq{The syntax of "$aName" is not correct. Please consider the commandref.};
|
||
}
|
||
|
||
my $alowt = grep (/^$h->{type}$/, @ctypes) ? 1 : 0;
|
||
if (!$alowt) {
|
||
return qq{The type "$h->{type}" isn't allowed!};
|
||
}
|
||
|
||
if (exists $h->{switchdev}) { # alternatives Schaltdevice
|
||
($err) = isDeviceValid ( { name => $name, obj => $h->{switchdev}, method => 'string' } );
|
||
return $err if($err);
|
||
}
|
||
|
||
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) {
|
||
if ($h->{mode} =~ /.*:.*/xs) {
|
||
my ($dv, $rd) = split ':', $h->{mode};
|
||
($err) = isDeviceValid ( { name => $name, obj => $dv, method => 'string' } );
|
||
return $err if($err);
|
||
|
||
my $mode = ReadingsVal ($dv, $rd, '');
|
||
if ($mode !~ /^(?:can|must)$/xs) {
|
||
return "The reading '$rd' of device '$dv' is invalid or doesn't contain a valid mode";
|
||
}
|
||
}
|
||
else {
|
||
return qq{The mode "$h->{mode}" isn't allowed!};
|
||
}
|
||
}
|
||
|
||
if (exists $h->{surpmeth}) {
|
||
if ($h->{surpmeth} =~ /.*:.*/xs) {
|
||
my ($dv, $rd) = split ':', $h->{surpmeth};
|
||
($err) = isDeviceValid ( { name => $name, obj => $dv, method => 'string' } );
|
||
return $err if($err);
|
||
|
||
if (!isNumeric( ReadingsVal ($dv, $rd, '') )) {
|
||
return "The reading '$rd' of device '$dv' is invalid or doesn't contain a valid numeric value";
|
||
}
|
||
}
|
||
elsif ($h->{surpmeth} !~ /^[2-9]$|^1[0-9]$|^20$|^median$|^default$/xs) {
|
||
return qq{The surpmeth "$h->{surpmeth}" is wrong. It must contain a '<device>:<reading>', 'median', 'default' or an integer value of '2 .. 20'.};
|
||
}
|
||
}
|
||
|
||
my $valid;
|
||
|
||
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
|
||
if ($h->{interruptable} !~ /^[01]$/xs) {
|
||
my ($dev,$rd,$regex,$hyst) = split ":", $h->{interruptable};
|
||
|
||
if (!$dev || !$rd || !defined $regex) {
|
||
return qq{A Device, Reading and Regex must be specified for the 'interruptable' key!};
|
||
}
|
||
|
||
$err = checkRegex ($regex);
|
||
return "interruptable: $err" if($err);
|
||
|
||
if ($hyst && !isNumeric ($hyst)) {
|
||
return qq{The hysteresis of key "interruptable" must be a numeric value};
|
||
}
|
||
}
|
||
}
|
||
|
||
if (exists $h->{swoncond}) { # Check Regex
|
||
my (undef,undef,$regex) = split ":", $h->{swoncond};
|
||
|
||
$err = checkRegex ($regex);
|
||
return "swoncond: $err" if($err);
|
||
}
|
||
|
||
if (exists $h->{swoffcond}) { # Check Regex
|
||
my (undef,undef,$regex) = split ":", $h->{swoffcond};
|
||
|
||
$err = checkRegex ($regex);
|
||
return "swoffcond: $err" if($err);
|
||
}
|
||
|
||
if (exists $h->{swstate}) { # Check Regex
|
||
my (undef,$onregex,$offregex) = split ":", $h->{swstate};
|
||
|
||
$err = checkRegex ($onregex);
|
||
return "swstate on-Regex: $err" if($err);
|
||
|
||
$err = checkRegex ($offregex);
|
||
return "swstate off-Regex: $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;
|
||
|
||
$paref->{c} = $c;
|
||
delConsumerFromMem ($paref); # Consumerdaten aus History löschen
|
||
|
||
deleteReadingspec ($hash, "consumer${c}.*");
|
||
}
|
||
|
||
writeCacheToFile ($hash, 'consumers', $csmcache.$name); # Cache File Consumer schreiben
|
||
|
||
$data{$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 $name = $paref->{name};
|
||
my $aName = $paref->{aName};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if ($aName eq 'ctrlConsRecommendReadings') {
|
||
deleteReadingspec ($hash, "consumer.*_ConsumptionRecommended");
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr ctrlSpecialReadings
|
||
################################################################
|
||
sub _attrcreateSpecialRdgs { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aName = $paref->{aName};
|
||
my $aVal = $paref->{aVal};
|
||
|
||
my $te = 'currentRunMtsConsumer_|runTimeAvgDayConsumer_';
|
||
|
||
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 ctrlDebug
|
||
################################################################
|
||
sub _attrctrlDebug { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aName = $paref->{aName};
|
||
my $aVal = $paref->{aVal};
|
||
|
||
my $te = 'consumerSwitching';
|
||
|
||
if ($aVal =~ /$te/xs && $init_done) {
|
||
my @aa = split ",", $aVal;
|
||
|
||
for my $elm (@aa) {
|
||
next if($elm !~ /$te/xs);
|
||
$elm =~ /([0-9]{2})/xs; # Consumer Nummer filetieren
|
||
|
||
if (!AttrVal ($name, 'consumer'.$1, '')) {
|
||
return qq{The consumer 'consumer$1' is currently not registered as an active consumer!};
|
||
}
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr flowGraphicControl
|
||
################################################################
|
||
sub _attrflowGraphicControl { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $aVal = $paref->{aVal};
|
||
my $cmd = $paref->{cmd};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
for my $av ( qw( animate
|
||
consumerdist
|
||
h2consumerdist
|
||
shiftx
|
||
shifty
|
||
showconsumer
|
||
showconsumerremaintime
|
||
size
|
||
showconsumerdummy
|
||
showconsumerpower
|
||
strokecolstd
|
||
strokecolsig
|
||
strokecolina
|
||
strokewidth
|
||
) ) {
|
||
|
||
delete $data{$name}{current}{$av};
|
||
}
|
||
|
||
if ($cmd eq 'set') {
|
||
my $valid = {
|
||
animate => '0|1',
|
||
consumerdist => '[89]\d{1}|[1234]\d{2}|500',
|
||
h2consumerdist => '\d{1,3}',
|
||
shiftx => '-?[0-7]\d{0,1}|-?80',
|
||
shifty => '\d+',
|
||
size => '\d+',
|
||
showconsumer => '0|1',
|
||
showconsumerdummy => '0|1',
|
||
showconsumerremaintime => '0|1',
|
||
showconsumerpower => '0|1',
|
||
strokecolstd => '.*',
|
||
strokecolsig => '.*',
|
||
strokecolina => '.*',
|
||
strokewidth => '\d+',
|
||
};
|
||
|
||
my ($a, $h) = parseParams ($aVal);
|
||
|
||
for my $key (keys %{$h}) {
|
||
my $comp = $valid->{$key};
|
||
next if(!$comp);
|
||
|
||
if ($h->{$key} =~ /^$comp$/xs) {
|
||
$data{$name}{current}{$key} = $h->{$key};
|
||
}
|
||
else {
|
||
return "The key '$key=$h->{$key}' is not specified correctly. Please use a valid value.";
|
||
}
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr setupMeterDev
|
||
################################################################
|
||
sub _attrMeterDev { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aVal = $paref->{aVal};
|
||
my $aName = $paref->{aName};
|
||
my $type = $paref->{type};
|
||
|
||
return if(!$init_done);
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if ($paref->{cmd} eq 'set') {
|
||
my ($err, $medev, $h) = isDeviceValid ( { name => $name, obj => $aVal, method => 'string' } );
|
||
return $err if($err);
|
||
|
||
if (!$h->{gcon} || !$h->{contotal} || !$h->{gfeedin} || !$h->{feedtotal}) {
|
||
return qq{The syntax of '$aName' 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);
|
||
}
|
||
}
|
||
elsif ($paref->{cmd} eq 'del') {
|
||
readingsDelete ($hash, "Current_GridConsumption");
|
||
readingsDelete ($hash, "Current_GridFeedIn");
|
||
delete $data{$name}{circular}{99}{initdayfeedin};
|
||
delete $data{$name}{circular}{99}{gridcontotal};
|
||
delete $data{$name}{circular}{99}{initdaygcon};
|
||
delete $data{$name}{circular}{99}{feedintotal};
|
||
delete $data{$name}{current}{gridconsumption};
|
||
delete $data{$name}{current}{tomorrowconsumption};
|
||
delete $data{$name}{current}{gridfeedin};
|
||
delete $data{$name}{current}{consumption};
|
||
delete $data{$name}{current}{autarkyrate};
|
||
delete $data{$name}{current}{selfconsumption};
|
||
delete $data{$name}{current}{selfconsumptionrate};
|
||
delete $data{$name}{current}{eFeedInTariff};
|
||
delete $data{$name}{current}{eFeedInTariffCcy};
|
||
delete $data{$name}{current}{ePurchasePrice};
|
||
delete $data{$name}{current}{ePurchasePriceCcy};
|
||
}
|
||
|
||
InternalTimer (gettimeofday() + 2, 'FHEM::SolarForecast::createAssociatedWith', $hash, 0);
|
||
InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::writeCacheToFile', [$name, 'plantconfig', $plantcfg.$name], 0); # Anlagenkonfiguration File schreiben
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr setupOtherProducer
|
||
################################################################
|
||
sub _attrProducerDev { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aVal = $paref->{aVal};
|
||
my $aName = $paref->{aName};
|
||
my $type = $paref->{type};
|
||
|
||
return if(!$init_done);
|
||
|
||
my $hash = $defs{$name};
|
||
my $pn = (split 'Producer', $aName)[1];
|
||
|
||
if ($paref->{cmd} eq 'set') {
|
||
my ($err, $dev, $h) = isDeviceValid ( { name => $name, obj => $aVal, method => 'string' } );
|
||
return $err if($err);
|
||
|
||
if (!$h->{pcurr} || !$h->{etotal}) {
|
||
return qq{The syntax of '$aName' is not correct. Please consider the commandref.};
|
||
}
|
||
|
||
delete $data{$name}{producers}{$pn}{picon};
|
||
}
|
||
elsif ($paref->{cmd} eq 'del') {
|
||
for my $k (keys %{$data{$name}{producers}}) {
|
||
delete $data{$name}{producers}{$k} if($k eq $pn);
|
||
}
|
||
|
||
readingsDelete ($hash, 'Current_PP_'.$pn);
|
||
deleteReadingspec ($hash, ".*_PPreal_".$pn);
|
||
|
||
for my $hod (keys %{$data{$name}{circular}}) {
|
||
delete $data{$name}{circular}{$hod}{'pprl'.$pn};
|
||
}
|
||
}
|
||
|
||
InternalTimer (gettimeofday() + 0.5, 'FHEM::SolarForecast::centralTask', [$name, 0], 0);
|
||
InternalTimer (gettimeofday() + 2, 'FHEM::SolarForecast::createAssociatedWith', $hash, 0);
|
||
InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::writeCacheToFile', [$name, 'plantconfig', $plantcfg.$name], 0); # Anlagenkonfiguration File schreiben
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr setupInverterDev
|
||
################################################################
|
||
sub _attrInverterDev { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aVal = $paref->{aVal};
|
||
my $aName = $paref->{aName};
|
||
my $type = $paref->{type};
|
||
|
||
return if(!$init_done);
|
||
|
||
my $hash = $defs{$name};
|
||
my $in = (split 'setupInverterDev', $aName)[1];
|
||
|
||
if ($paref->{cmd} eq 'set') {
|
||
my ($err, $indev, $h) = isDeviceValid ( { name => $name, obj => $aVal, method => 'string' } );
|
||
return $err if($err);
|
||
|
||
if ($in ne '01' && !AttrVal ($name, 'setupInverterDev01', '')) {
|
||
return qq{Set the first Inverter device with attribute 'setupInverterDev01'};
|
||
}
|
||
|
||
if (!$h->{pv} || !$h->{etotal} || !$h->{capacity}) {
|
||
return qq{One or more of the keys 'pv, etotal, capacity' are missing. Please consider the commandref.};
|
||
}
|
||
|
||
if (!isNumeric($h->{capacity})) {
|
||
return qq{The value of key 'capacity' must be numeric. Please consider the commandref.};
|
||
}
|
||
|
||
if ($h->{limit}) {
|
||
if (!isNumeric($h->{limit}) || $h->{limit} < 0 || $h->{limit} > 100) {
|
||
return qq{The value of key 'limit' is not valid. Please consider the commandref.};
|
||
}
|
||
}
|
||
|
||
if ($h->{feed} && $h->{feed} !~ /^grid|bat$/xs) {
|
||
return qq{The value of key 'feed' is not valid. Please consider the commandref.};
|
||
}
|
||
|
||
if ($h->{strings}) {
|
||
for my $s (split ',', $h->{strings}) {
|
||
if (!grep /^$s$/, keys %{$data{$name}{strings}}) {
|
||
return qq{The string '$s' is not a valid string name defined in attribute 'setupInverterStrings'.};
|
||
}
|
||
}
|
||
}
|
||
|
||
$data{$name}{circular}{99}{attrInvChangedTs} = int time;
|
||
|
||
delete $data{$name}{inverters}{$in}{invertercap};
|
||
delete $data{$name}{inverters}{$in}{ilimit};
|
||
delete $data{$name}{inverters}{$in}{iicon};
|
||
delete $data{$name}{inverters}{$in}{istrings};
|
||
delete $data{$name}{inverters}{$in}{iasynchron};
|
||
delete $data{$name}{inverters}{$in}{ifeed};
|
||
}
|
||
elsif ($paref->{cmd} eq 'del') {
|
||
delete $data{$name}{inverters}{$in};
|
||
|
||
readingsDelete ($hash, 'Current_PV');
|
||
undef @{$data{$name}{current}{genslidereg}};
|
||
|
||
if ($in eq '01') { # wenn der letzte Inverter gelöscht wurde
|
||
deleteReadingspec ($hash, '.*_PVreal' );
|
||
delete $data{$name}{circular}{99}{attrInvChangedTs};
|
||
}
|
||
}
|
||
|
||
InternalTimer (gettimeofday() + 0.5, 'FHEM::SolarForecast::centralTask', [$name, 0], 0);
|
||
InternalTimer (gettimeofday() + 2, 'FHEM::SolarForecast::createAssociatedWith', $hash, 0);
|
||
InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::writeCacheToFile', [$name, 'plantconfig', $plantcfg.$name], 0); # Anlagenkonfiguration File schreiben
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr setupInverterStrings
|
||
################################################################
|
||
sub _attrInverterStrings { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aVal = $paref->{aVal};
|
||
my $aName = $paref->{aName};
|
||
my $type = $paref->{type};
|
||
|
||
return if(!$init_done);
|
||
|
||
if ($paref->{cmd} eq 'set') {
|
||
if ($aVal =~ /\?/xs) {
|
||
return qq{The inverter string designation is wrong. An inverter string name must not contain a '?' character!};
|
||
}
|
||
|
||
my @istrings = split ",", $aVal;
|
||
|
||
for my $k (keys %{$data{$name}{solcastapi}}) {
|
||
next if ($k =~ /\?/xs || grep /^$k$/, @istrings);
|
||
delete $data{$name}{solcastapi}{$k};
|
||
}
|
||
}
|
||
|
||
InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::writeCacheToFile', [$name, 'plantconfig', $plantcfg.$name], 0); # Anlagenkonfiguration File schreiben
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr setupStringPeak
|
||
################################################################
|
||
sub _attrStringPeak { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aVal = $paref->{aVal};
|
||
|
||
return if(!$init_done);
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if ($paref->{cmd} eq 'set') {
|
||
$aVal =~ s/,/./xg;
|
||
|
||
my ($a,$h) = parseParams ($aVal);
|
||
|
||
if (!keys %$h) {
|
||
return qq{The attribute content 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};
|
||
}
|
||
}
|
||
|
||
return if(_checkSetupNotComplete ($hash)); # keine Stringkonfiguration wenn Setup noch nicht komplett
|
||
|
||
my @istrings = split ",", AttrVal ($name, 'setupInverterStrings', ''); # Stringbezeichner
|
||
|
||
if (!@istrings) {
|
||
return qq{Define all used strings with command "attr $name setupInverterStrings" first.};
|
||
}
|
||
|
||
while (my ($strg, $pp) = each %$h) {
|
||
if (!grep /^$strg$/, @istrings) {
|
||
return qq{The stringname '$strg' is not defined as valid string in attribute 'setupInverterStrings'};
|
||
}
|
||
}
|
||
}
|
||
|
||
InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::writeCacheToFile', [$name, 'plantconfig', $plantcfg.$name], 0); # Anlagenkonfiguration File schreiben
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr setupRoofTops
|
||
################################################################
|
||
sub _attrRoofTops { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aVal = $paref->{aVal};
|
||
|
||
return if(!$init_done);
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if ($paref->{cmd} eq 'set') {
|
||
my ($a,$h) = parseParams ($aVal);
|
||
|
||
if (!keys %$h) {
|
||
return qq{The attribute content has wrong format};
|
||
}
|
||
|
||
while (my ($is, $pk) = each %$h) {
|
||
my $rtid = StatusAPIVal ($hash, '?IdPair', '?'.$pk, 'rtid', '');
|
||
my $apikey = StatusAPIVal ($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".} ;
|
||
}
|
||
}
|
||
|
||
my @istrings = split ",", AttrVal ($name, 'setupInverterStrings', ''); # Stringbezeichner
|
||
|
||
if (!@istrings) {
|
||
return qq{Define all used strings with command "attr $name setupInverterStrings" first.};
|
||
}
|
||
|
||
while (my ($strg, $pp) = each %$h) {
|
||
if (!grep /^$strg$/, @istrings) {
|
||
return qq{The stringname '$strg' is not defined as valid string in attribute 'setupInverterStrings'};
|
||
}
|
||
}
|
||
}
|
||
|
||
InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::writeCacheToFile', [$name, 'plantconfig', $plantcfg.$name], 0); # Anlagenkonfiguration File schreiben
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr setupBatteryDev
|
||
################################################################
|
||
sub _attrBatteryDev { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aVal = $paref->{aVal};
|
||
my $aName = $paref->{aName};
|
||
my $type = $paref->{type};
|
||
|
||
return if(!$init_done);
|
||
|
||
my $hash = $defs{$name};
|
||
my $bn = (split 'setupBatteryDev', $aName)[1];
|
||
|
||
if ($paref->{cmd} eq 'set') {
|
||
my ($err, $badev, $h) = isDeviceValid ( { name => $name, obj => $aVal, method => 'string' } );
|
||
return $err if($err);
|
||
|
||
if (!$h->{pin} || !$h->{pout} || !$h->{cap}) {
|
||
return qq{One or more of the keys 'pin, pout, cap' are missing. 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.};
|
||
}
|
||
|
||
delete $data{$name}{batteries}{$bn}{basynchron};
|
||
delete $data{$name}{batteries}{$bn}{bicon};
|
||
delete $data{$name}{batteries}{$bn}{bshowingraph};
|
||
}
|
||
elsif ($paref->{cmd} eq 'del') {
|
||
readingsDelete ($hash, 'Current_PowerBatIn_'.$bn);
|
||
readingsDelete ($hash, 'Current_PowerBatOut_'.$bn);
|
||
readingsDelete ($hash, 'Current_BatCharge_'.$bn);
|
||
readingsDelete ($hash, 'Battery_ChargeRecommended_'.$bn);
|
||
readingsDelete ($hash, 'Battery_ChargeRequest_'.$bn);
|
||
readingsDelete ($hash, 'Battery_OptimumTargetSoC_'.$bn);
|
||
deleteReadingspec ($hash, "NextHour.._Bat_${bn}_SoCforecast");
|
||
|
||
undef @{$data{$name}{current}{batsocslidereg}};
|
||
|
||
delete $data{$name}{circular}{99}{'lastTsMaxSocRchd'.$bn};
|
||
delete $data{$name}{circular}{99}{'nextTsMaxSocChge'.$bn};
|
||
delete $data{$name}{circular}{99}{'initdaybatintot'.$bn};
|
||
delete $data{$name}{circular}{99}{'initdaybatouttot'.$bn};
|
||
delete $data{$name}{circular}{99}{'batintot'.$bn};
|
||
delete $data{$name}{circular}{99}{'batouttot'.$bn};
|
||
delete $data{$name}{circular}{99}{'days2care'.$bn};
|
||
|
||
delete $data{$name}{batteries}{$bn};
|
||
}
|
||
|
||
InternalTimer (gettimeofday() + 0.5, 'FHEM::SolarForecast::centralTask', [$name, 0], 0);
|
||
InternalTimer (gettimeofday() + 2, 'FHEM::SolarForecast::createAssociatedWith', $hash, 0);
|
||
InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::writeCacheToFile', [$name, 'plantconfig', $plantcfg.$name], 0); # Anlagenkonfiguration File schreiben
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr setupWeatherDevX
|
||
################################################################
|
||
sub _attrWeatherDev { ## no critic "not used"
|
||
my $paref = shift;
|
||
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);
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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 'setupWeatherDev1') {
|
||
return qq{Only the leading weather attribute 'setupWeatherDev1' can set to '$aVal'};
|
||
}
|
||
|
||
my @istrings = split ",", AttrVal ($name, 'setupInverterStrings', ''); # Stringbezeichner
|
||
|
||
if ((!ReadingsVal ($name, 'setupStringAzimuth', '') || !ReadingsVal ($name, 'setupStringDeclination', '')) &&
|
||
!grep /KI-based/, @istrings) {
|
||
return qq{Execute 'set $name setupStringAzimuth' and/or 'set $name setupStringDeclination' first.};
|
||
}
|
||
}
|
||
|
||
if ($aVal !~ /-API$/xs) { # Attribute des DWD-Devices prüfen
|
||
my $err = checkdwdattr ($name, $aVal, \@dweattrmust);
|
||
return $err if($err);
|
||
}
|
||
}
|
||
|
||
InternalTimer (gettimeofday() + 1, 'FHEM::SolarForecast::__harmonizeAPIdelayed', $hash, 0);
|
||
InternalTimer (gettimeofday() + 2, 'FHEM::SolarForecast::setModel', $hash, 0);
|
||
InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::createAssociatedWith', $hash, 0);
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr setupRadiationAPI
|
||
################################################################
|
||
sub _attrRadiationAPI { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aVal = $paref->{aVal};
|
||
my $aName = $paref->{aName};
|
||
my $type = $paref->{type};
|
||
|
||
return if(!$init_done);
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if ($paref->{cmd} eq 'set') {
|
||
if ($aVal !~ /-API$/x && (!$defs{$aVal} || $defs{$aVal}{TYPE} ne "DWD_OpenData")) {
|
||
return qq{The device "$aVal" doesn't exist or has no TYPE "DWD_OpenData"};
|
||
}
|
||
|
||
#my $awdev1 = AttrVal ($name, 'setupWeatherDev1', '');
|
||
|
||
#if (($awdev1 eq 'OpenMeteoDWD-API' && $aVal ne 'OpenMeteoDWD-API') ||
|
||
# ($awdev1 eq 'OpenMeteoDWDEnsemble-API' && $aVal ne 'OpenMeteoDWDEnsemble-API') ||
|
||
# ($awdev1 eq 'OpenMeteoWorld-API' && $aVal ne 'OpenMeteoWorld-API')) {
|
||
# return "The attribute 'setupWeatherDev1' is set to '$awdev1'. \n".
|
||
# "Change that attribute to another weather device first if you want use an other API.";
|
||
#}
|
||
|
||
if ($aVal =~ /(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);
|
||
}
|
||
|
||
return if(_checkSetupNotComplete ($hash)); # keine Stringkonfiguration wenn Setup noch nicht komplett
|
||
|
||
if ($aVal =~ /(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, 'setupStringDeclination', ''); # Modul Neigungswinkel für jeden Stringbezeichner
|
||
return qq{Please complete command "set $name setupStringDeclination".} if(!$tilt);
|
||
|
||
my $dir = ReadingsVal ($name, 'setupStringAzimuth', ''); # Modul Ausrichtung für jeden Stringbezeichner
|
||
return qq{Please complete command "set $name setupStringAzimuth".} if(!$dir);
|
||
}
|
||
|
||
$data{$name}{current}{allStringsFullfilled} = 0; # Stringkonfiguration neu prüfen lassen
|
||
}
|
||
|
||
readingsDelete ($hash, 'nextRadiationAPICall');
|
||
|
||
InternalTimer (gettimeofday() + 1, 'FHEM::SolarForecast::__harmonizeAPIdelayed', $hash, 0);
|
||
InternalTimer (gettimeofday() + 2, 'FHEM::SolarForecast::setModel', $hash, 0); # Model setzen
|
||
InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::createAssociatedWith', $hash, 0);
|
||
InternalTimer (gettimeofday() + 4, 'FHEM::SolarForecast::writeCacheToFile', [$name, 'plantconfig', $plantcfg.$name], 0); # Anlagenkonfiguration File schreiben
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr graphicBeamXContent
|
||
################################################################
|
||
sub _attrgraphicBeamXContent { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $aVal = $paref->{aVal};
|
||
my $cmd = $paref->{cmd};
|
||
|
||
return if(!$init_done);
|
||
|
||
my $medev = AttrVal ($name, 'setupMeterDev', ''); # aktuelles Meter device
|
||
my ($a,$h) = parseParams ($medev);
|
||
|
||
if ($cmd eq 'set') {
|
||
if ($aVal eq 'energycosts') {
|
||
return "Define key 'conprice' in the setupMeterDev attribute first before setting $aVal" if(!defined $h->{conprice});
|
||
}
|
||
|
||
if ($aVal eq 'feedincome') {
|
||
return "Define key 'feedprice' in the setupMeterDev attribute first before setting $aVal" if(!defined $h->{feedprice});
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Attr setupRadiationAPI und setupWeatherDev1
|
||
# harmonisieren wenn erforderlich
|
||
# setupRadiationAPI ist führend
|
||
################################################################
|
||
sub __harmonizeAPIdelayed {
|
||
my $hash = shift;
|
||
|
||
my $name = $hash->{NAME};
|
||
my $wedev1 = AttrVal ($name, 'setupWeatherDev1', '');
|
||
my $radapi = AttrVal ($name, 'setupRadiationAPI', '');
|
||
|
||
return if($wedev1 eq $radapi);
|
||
|
||
if ($radapi =~ /OpenMeteo/xs && $wedev1 =~ /OpenMeteo/xs) { # auf OpenMeteo Datenmodell harmonisieren
|
||
CommandAttr (undef, "$name setupWeatherDev1 $radapi");
|
||
}
|
||
|
||
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).
|
||
|
||
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 $debug = getDebug ($myHash); # Debug Mode
|
||
|
||
my ($err, $medev, $bname, $iname, $h, $async);
|
||
|
||
## Meter Event?
|
||
#################
|
||
($err, $medev, $h) = isDeviceValid ( { name => $myName, obj => 'setupMeterDev', method => 'attr' } );
|
||
|
||
if (!$err) {
|
||
if ($devName eq $medev) {
|
||
$async = $h->{asynchron} // 0;
|
||
|
||
if ($debug =~ /notifyHandling/x) {
|
||
Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - Event of Meter device >$devName< received - asynchronous mode: $async});
|
||
}
|
||
|
||
if ($async) {
|
||
if (CurrentVal ($myHash, 'ctrunning', 0)) {
|
||
if ($debug =~ /notifyHandling/x) {
|
||
Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - central task was called from NOTIFY when it is already running ... end this call});
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
centralTask ($myHash, 1);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
## Battery Event?
|
||
###################
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$bname = BatteryVal ($myHash, $bn, 'bname', '');
|
||
|
||
if ($devName eq $bname) {
|
||
$async = BatteryVal ($myHash, $bn, 'basynchron', 0);
|
||
|
||
if ($debug =~ /notifyHandling/x) {
|
||
Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - Event of Battery device >$devName< received - asynchronous mode: $async});
|
||
}
|
||
|
||
if ($async) {
|
||
if (CurrentVal ($myHash, 'ctrunning', 0)) {
|
||
if ($debug =~ /notifyHandling/x) {
|
||
Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - central task was called from NOTIFY when it is already running ... end this call});
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
centralTask ($myHash, 1);
|
||
return;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
## Inverter Event?
|
||
####################
|
||
for my $in (1..MAXINVERTER) {
|
||
$in = sprintf "%02d", $in;
|
||
$iname = InverterVal ($myHash, $in, 'iname', '');
|
||
|
||
if ($devName eq $iname) {
|
||
$async = InverterVal ($myHash, $in, 'iasynchron', 0);
|
||
|
||
if ($debug =~ /notifyHandling/x) {
|
||
Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - Event of Inverter device >$devName< received - asynchronous mode: $async});
|
||
}
|
||
|
||
if ($async) {
|
||
if (CurrentVal ($myHash, 'ctrunning', 0)) {
|
||
if ($debug =~ /notifyHandling/x) {
|
||
Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - central task was called from NOTIFY when it is already running ... end this call});
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
centralTask ($myHash, 1);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
## consumer Event?
|
||
####################
|
||
my $cdref = CurrentVal ($myHash, 'consumerdevs', ''); # alle registrierten Consumer und Schaltdevices
|
||
my @consumers = ();
|
||
@consumers = @{$cdref} if(ref $cdref eq "ARRAY");
|
||
|
||
if (@consumers && grep /^$devName$/, @consumers) {
|
||
my ($cname, $cindex, $dswname);
|
||
|
||
for my $c (sort{$a<=>$b} keys %{$data{$myName}{consumers}}) {
|
||
($err, $cname, $dswname) = getCDnames ($myHash, $c);
|
||
|
||
if ($devName eq $cname) {
|
||
$cindex = $c;
|
||
|
||
if ($debug =~ /notifyHandling/x) {
|
||
Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - Event of consumer >$devName< (index: $c) received});
|
||
}
|
||
|
||
last;
|
||
}
|
||
|
||
if ($devName eq $dswname) {
|
||
$cindex = $c;
|
||
|
||
if ($debug =~ /notifyHandling/x) {
|
||
Log3 ($myName, 1, qq{$myName DEBUG> notifyHandling - Event of device >$devName< which is switching device of consumer >$cname< (index: $c) received});
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
$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);
|
||
}
|
||
|
||
################################################################
|
||
# Rename
|
||
################################################################
|
||
sub Rename {
|
||
my $new_name = shift;
|
||
my $old_name = shift;
|
||
|
||
my $hash = $defs{$old_name};
|
||
my $type = (split '::', __PACKAGE__)[1];
|
||
|
||
$data{$new_name} = $data{$old_name};
|
||
delete $data{$old_name};
|
||
|
||
my @ftd = searchCacheFiles ($old_name);
|
||
|
||
for my $oldf (@ftd) {
|
||
my $newf = $oldf;
|
||
$newf =~ s/_SolarForecast_${old_name}/_SolarForecast_${new_name}/xsg;
|
||
rename ($oldf, $newf) or
|
||
Log3 ($new_name, 2, qq{$new_name - WARNING - File "$oldf" could not be renamed: $!});
|
||
}
|
||
|
||
# Log3 ($new_name, 1, qq{$new_name - Dump -> \n}. Dumper $data{$new_name});
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# 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
|
||
writeCacheToFile ($hash, 'statusapi', $statcache.$name); # Status-API Cache sichern
|
||
writeCacheToFile ($hash, 'weatherapi', $weathercache.$name); # Weather-API Cache sichern
|
||
|
||
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 = searchCacheFiles ($name);
|
||
|
||
for my $f (@ftd) {
|
||
my $err = FileDelete ($f);
|
||
|
||
if ($err) {
|
||
Log3 ($name, 1, qq{$name - Message while deleting file "$f": $err});
|
||
}
|
||
else {
|
||
Log3 ($name, 3, qq{$name - INFO - File "$f" deleted.});
|
||
}
|
||
}
|
||
|
||
delete $data{$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 Strahlungsdaten-API Werte schreiben
|
||
writeCacheToFile ($hash, 'statusapi', $statcache.$name); # Status-API Cache sichern
|
||
writeCacheToFile ($hash, 'weatherapi', $weathercache.$name); # Weather-API Cache sichern
|
||
|
||
$hash->{LCACHEFILE} = "last write time: ".FmtTime(gettimeofday())." whole Operating Memory";
|
||
|
||
Log3 ($name, 4, "$name - The working memory >circular, pvhist, solcastapi, statusapi, weatherapi< 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;
|
||
}
|
||
|
||
################################################################
|
||
# Consumer Daten aus History löschen
|
||
################################################################
|
||
sub delConsumerFromMem {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $c = $paref->{c};
|
||
|
||
my $hash = $defs{$name};
|
||
my $calias = ConsumerVal ($hash, $c, 'alias', '');
|
||
|
||
for my $d (1..31) {
|
||
$d = sprintf("%02d", $d);
|
||
delete $data{$name}{pvhist}{$d}{99}{"csme${c}"};
|
||
delete $data{$name}{pvhist}{$d}{99}{"cyclescsm${c}"};
|
||
delete $data{$name}{pvhist}{$d}{99}{"hourscsme${c}"};
|
||
delete $data{$name}{pvhist}{$d}{99}{"avgcycmntscsm${c}"};
|
||
|
||
for my $i (1..24) {
|
||
$i = sprintf("%02d", $i);
|
||
delete $data{$name}{pvhist}{$d}{$i}{"csmt${c}"};
|
||
delete $data{$name}{pvhist}{$d}{$i}{"csme${c}"};
|
||
delete $data{$name}{pvhist}{$d}{$i}{"minutescsm${c}"};
|
||
}
|
||
}
|
||
|
||
delete $data{$name}{consumers}{$c};
|
||
|
||
Log3 ($name, 3, qq{$name - Consumer "$c - $calias" deleted from memory});
|
||
|
||
return;
|
||
}
|
||
|
||
#################################################################
|
||
# Cache Files im Cache Directory suchen und als Array
|
||
# zurückliefern
|
||
#################################################################
|
||
sub searchCacheFiles {
|
||
my $name = shift;
|
||
|
||
my @ftd;
|
||
|
||
opendir (DIR, $cachedir);
|
||
|
||
while (my $file = readdir (DIR)) {
|
||
next unless (-f "$cachedir/$file");
|
||
next unless ($file =~ /_SolarForecast_${name}/);
|
||
push @ftd, "$cachedir/$file";
|
||
}
|
||
|
||
closedir (DIR);
|
||
|
||
return @ftd;
|
||
}
|
||
|
||
################################################################
|
||
# gesicherte Cache-Files vom Filesystem nachladen falls die
|
||
# jeweiligen Online-Speicher nicht gefüllt sind (zum
|
||
# Beispiel nach einem reload 76_SolarForecast.pm)
|
||
################################################################
|
||
sub reloadCacheFiles {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
|
||
return if(CurrentVal ($name, 'cachefilesloaded', 0));
|
||
|
||
$paref->{file} = $pvhcache.$name; # Cache File PV History einlesen wenn vorhanden
|
||
$paref->{cachename} = 'pvhist';
|
||
$paref->{title} = 'pvHistory';
|
||
readCacheFile ($paref);
|
||
|
||
$paref->{file} = $pvccache.$name; # Cache File PV Circular einlesen wenn vorhanden
|
||
$paref->{cachename} = 'circular';
|
||
$paref->{title} = 'pvCircular';
|
||
readCacheFile ($paref);
|
||
|
||
$paref->{file} = $csmcache.$name; # Cache File Consumer einlesen wenn vorhanden
|
||
$paref->{cachename} = 'consumers';
|
||
$paref->{title} = 'consumerMaster';
|
||
readCacheFile ($paref);
|
||
|
||
$paref->{file} = $scpicache.$name; # Cache File SolCast API Werte einlesen wenn vorhanden
|
||
$paref->{cachename} = 'solcastapi';
|
||
$paref->{title} = 'radiationApiData';
|
||
readCacheFile ($paref);
|
||
|
||
$paref->{file} = $statcache.$name; # Cache File API-Status einlesen wenn vorhanden
|
||
$paref->{cachename} = 'statusapi';
|
||
$paref->{title} = 'statusApiData';
|
||
readCacheFile ($paref);
|
||
|
||
$paref->{file} = $weathercache.$name; # Cache File Weather-API Daten einlesen wenn vorhanden
|
||
$paref->{cachename} = 'weatherapi';
|
||
$paref->{title} = 'weatherApiData';
|
||
readCacheFile ($paref);
|
||
|
||
$paref->{file} = $aitrained.$name; # AI Cache File einlesen wenn vorhanden
|
||
$paref->{cachename} = 'aitrained';
|
||
$paref->{title} = 'aiTrainedData';
|
||
readCacheFile ($paref);
|
||
|
||
$paref->{file} = $airaw.$name; # AI Rawdaten File einlesen wenn vorhanden
|
||
$paref->{cachename} = 'airaw';
|
||
$paref->{title} = 'aiRawData';
|
||
readCacheFile ($paref);
|
||
|
||
delete $paref->{file};
|
||
delete $paref->{cachename};
|
||
delete $paref->{title};
|
||
|
||
$data{$name}{current}{cachefilesloaded} = 1;
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Cachefile lesen
|
||
################################################################
|
||
sub readCacheFile {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $file = $paref->{file};
|
||
my $cachename = $paref->{cachename};
|
||
my $title = $paref->{title};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if ($cachename eq 'aitrained') {
|
||
my ($err, $dtree) = fileRetrieve ($file);
|
||
|
||
if (!$err && $dtree) {
|
||
my $valid = $dtree->isa('AI::DecisionTree');
|
||
|
||
if ($valid) {
|
||
$data{$name}{aidectree}{aitrained} = $dtree;
|
||
$data{$name}{current}{aitrainstate} = 'ok';
|
||
|
||
Log3 ($name, 3, qq{$name - cached data "$title" restored});
|
||
return;
|
||
}
|
||
}
|
||
|
||
delete $data{$name}{circular}{99}{aitrainLastFinishTs};
|
||
delete $data{$name}{circular}{99}{runTimeTrainAI};
|
||
return;
|
||
}
|
||
|
||
if ($cachename eq 'airaw') {
|
||
my ($err, $dat) = fileRetrieve ($file);
|
||
|
||
if (!$err && $dat) {
|
||
$data{$name}{aidectree}{airaw} = $dat;
|
||
$data{$name}{current}{aitrawstate} = 'ok';
|
||
|
||
Log3 ($name, 3, qq{$name - cached data "$title" restored});
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if ($cachename eq 'statusapi') {
|
||
my ($err, $statapi) = fileRetrieve ($file);
|
||
|
||
if (!$err && $statapi) {
|
||
$data{$name}{statusapi} = $statapi;
|
||
|
||
Log3 ($name, 3, qq{$name - cached data "$title" restored});
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if ($cachename eq 'weatherapi') {
|
||
my ($err, $wthtapi) = fileRetrieve ($file);
|
||
|
||
if (!$err && $wthtapi) {
|
||
$data{$name}{weatherapi} = $wthtapi;
|
||
|
||
Log3 ($name, 3, qq{$name - cached data "$title" restored});
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if ($cachename eq 'dwdcatalog') {
|
||
my ($err, $dwdc) = fileRetrieve ($file);
|
||
|
||
if (!$err && $dwdc) {
|
||
$data{$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{$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;
|
||
}
|
||
|
||
################################################################
|
||
# Daten in File wegschreiben
|
||
################################################################
|
||
sub writeCacheToFile {
|
||
my $hash = shift;
|
||
my $cachename = shift;
|
||
my $file = shift;
|
||
|
||
my $name;
|
||
if (ref $hash eq 'HASH') {
|
||
$name = $hash->{NAME};
|
||
}
|
||
elsif (ref $hash eq 'ARRAY') { # Array Referenz wurde übergeben
|
||
$name = $hash->[0];
|
||
$cachename = $hash->[1];
|
||
$file = $hash->[2];
|
||
$hash = $defs{$name};
|
||
}
|
||
|
||
my $type = $hash->{TYPE};
|
||
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 $dat = AiRawdataVal ($hash, '', '', undef);
|
||
|
||
if (defined $dat) {
|
||
$error = fileStore ($dat, $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{$name}{dwdcatalog}}) {
|
||
$error = fileStore ($data{$name}{dwdcatalog}, $file);
|
||
|
||
if ($error) {
|
||
$err = qq{ERROR while writing DWD Station Catalog to file "$file": $error};
|
||
Log3 ($name, 1, "$name - $err");
|
||
return $err;
|
||
}
|
||
}
|
||
else {
|
||
return "The DWD Station Catalog is empty";
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if ($cachename eq 'statusapi') {
|
||
if (scalar keys %{$data{$name}{statusapi}}) {
|
||
$error = fileStore ($data{$name}{statusapi}, $file);
|
||
|
||
if ($error) {
|
||
$err = qq{ERROR while writing API Status to file "$file": $error};
|
||
Log3 ($name, 1, "$name - $err");
|
||
return $err;
|
||
}
|
||
}
|
||
else {
|
||
return "The API Status is empty";
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if ($cachename eq 'weatherapi') {
|
||
if (scalar keys %{$data{$name}{weatherapi}}) {
|
||
$error = fileStore ($data{$name}{weatherapi}, $file);
|
||
|
||
if ($error) {
|
||
$err = qq{ERROR while writing API Status to file "$file": $error};
|
||
Log3 ($name, 1, "$name - $err");
|
||
return $err;
|
||
}
|
||
}
|
||
else {
|
||
return "The API Status is empty";
|
||
}
|
||
|
||
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(!keys %{$data{$name}{$cachename}});
|
||
push my @arr, encode_json ($data{$name}{$cachename});
|
||
|
||
$error = FileWrite ($file, @arr);
|
||
|
||
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 (grep /^$key$/, @rconfigs) { # Reading wiederherstellen
|
||
CommandSetReading (undef,"$name $key $val");
|
||
$nr++;
|
||
}
|
||
|
||
if (grep /^$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 / Event-controlled';
|
||
storeReading ('nextCycletime', 'Manual / Event-controlled');
|
||
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) {
|
||
_newCycTime ($hash, $t, $interval);
|
||
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");
|
||
}
|
||
|
||
releaseCentralTask ($hash);
|
||
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");
|
||
}
|
||
|
||
releaseCentralTask ($hash);
|
||
centralTask ($hash, 1);
|
||
}
|
||
}
|
||
else {
|
||
delete $hash->{HELPER}{S03DONE};
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# neue Zykluszeit bestimmen
|
||
################################################################
|
||
sub _newCycTime {
|
||
my $hash = shift;
|
||
my $t = shift;
|
||
my $interval = shift;
|
||
|
||
my $name = $hash->{NAME};
|
||
|
||
if (!$interval) {
|
||
$hash->{MODE} = 'Manual / Event-controlled';
|
||
$data{$name}{current}{nextCycleTime} = 0;
|
||
storeReading ('nextCycletime', 'Manual / Event-controlled');
|
||
return;
|
||
}
|
||
|
||
my $new = $t + $interval; # nächste Wiederholungszeit
|
||
$hash->{MODE} = 'Automatic / Event-controlled - next planned Cycletime: '.FmtTime($new);
|
||
|
||
$data{$name}{current}{nextCycleTime} = $new;
|
||
storeReading ('nextCycletime', FmtTime($new));
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# neue Attribute zur Laufzeit hinzufügen
|
||
# Device spezifische ".AttrList" überschreibt Modul AttrList!
|
||
# relevant für CPU Auslastung!!
|
||
################################################################
|
||
sub _addDynAttr {
|
||
my $hash = shift;
|
||
my $name = $hash->{NAME};
|
||
my $type = $hash->{TYPE};
|
||
|
||
my $adwds = '';
|
||
my @alldwd = devspec2array ("TYPE=DWD_OpenData");
|
||
$adwds = join ",", @alldwd if(@alldwd);
|
||
|
||
my @fcdevs = qw( OpenMeteoDWD-API
|
||
OpenMeteoDWDEnsemble-API
|
||
OpenMeteoWorld-API
|
||
SolCast-API
|
||
ForecastSolar-API
|
||
VictronKI-API
|
||
);
|
||
|
||
push @fcdevs, @alldwd if(@alldwd);
|
||
my $rdd = join ",", @fcdevs;
|
||
|
||
## Attributhüllen entfernen
|
||
#############################
|
||
my @deva = split " ", $modules{$type}{AttrList};
|
||
my $atd = 'setupWeatherDev|setupRadiationAPI|graphicBeam*Content|ctrlNextHoursSoCForecastReadings';
|
||
@deva = grep {!/$atd/} @deva;
|
||
|
||
## Attr setupWeatherDevX / setupRadiationAPI zur Laufzeit hinzufügen
|
||
######################################################################
|
||
for my $step (1..MAXWEATHERDEV) {
|
||
if ($step == 1) {
|
||
push @deva, ($adwds ? "setupWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API,$adwds" :
|
||
"setupWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API");
|
||
next;
|
||
}
|
||
|
||
push @deva, ($adwds ? "setupWeatherDev".$step.":$adwds" : "");
|
||
}
|
||
|
||
push @deva, "setupRadiationAPI:$rdd ";
|
||
|
||
## Attr graphicBeamXContent, ctrlNextDayForecastReadings zur Laufzeit hinzufügen
|
||
##################################################################################
|
||
my $gbc;
|
||
|
||
if (isBatteryUsed ($name)) {
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$gbc .= 'batsocforecast_'.$bn.',';
|
||
}
|
||
|
||
my $hod = join ",", (map { sprintf "%02d", $_} (0..23));
|
||
push @deva, "ctrlNextHoursSoCForecastReadings:multiple-strict,$hod";
|
||
}
|
||
|
||
$gbc .= 'consumption,consumptionForecast,energycosts,feedincome,gridconsumption,gridfeedin,pvForecast,pvReal';
|
||
|
||
push @deva, "graphicBeam1Content:$gbc";
|
||
push @deva, "graphicBeam2Content:$gbc";
|
||
push @deva, "graphicBeam3Content:$gbc";
|
||
push @deva, "graphicBeam4Content:$gbc";
|
||
|
||
$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');
|
||
|
||
return if(!$init_done);
|
||
|
||
setModel ($hash); # Model setzen
|
||
|
||
my (undef, $disabled, $inactive) = controller ($name);
|
||
return if($disabled || $inactive); # disabled / inactive
|
||
|
||
reloadCacheFiles ( {name => $name, type => $type} ); # Cache-Files vom Filesystem nachladen falls nötig
|
||
|
||
### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !!
|
||
##########################################################################################################################
|
||
|
||
delete $data{$name}{circular}{99}{days2care}; # 29.12.2024
|
||
delete $data{$name}{circular}{'00'}; # 04.02.2025
|
||
|
||
$data{$name}{circular}{99}{initdaybatintot01} = delete $data{$name}{circular}{99}{initdaybatintot} if(defined $data{$name}{circular}{99}{initdaybatintot}); # 29.12.2024
|
||
$data{$name}{circular}{99}{initdaybatouttot01} = delete $data{$name}{circular}{99}{initdaybatouttot} if(defined $data{$name}{circular}{99}{initdaybatouttot}); # 29.12.2024
|
||
$data{$name}{circular}{99}{batintot01} = delete $data{$name}{circular}{99}{batintot} if(defined $data{$name}{circular}{99}{batintot}); # 29.12.2024
|
||
$data{$name}{circular}{99}{batouttot01} = delete $data{$name}{circular}{99}{batouttot} if(defined $data{$name}{circular}{99}{batouttot}); # 29.12.2024
|
||
$data{$name}{circular}{99}{lastTsMaxSocRchd01} = delete $data{$name}{circular}{99}{lastTsMaxSocRchd} if(defined $data{$name}{circular}{99}{lastTsMaxSocRchd}); # 30.12.2024
|
||
$data{$name}{circular}{99}{nextTsMaxSocChge01} = delete $data{$name}{circular}{99}{nextTsMaxSocChge} if(defined $data{$name}{circular}{99}{nextTsMaxSocChge}); # 30.12.2024
|
||
|
||
readingsDelete ($hash, 'Current_BatCharge'); # 30.12.2024
|
||
readingsDelete ($hash, 'Current_PowerBatOut'); # 30.12.2024
|
||
readingsDelete ($hash, 'Current_PowerBatIn'); # 30.12.2024
|
||
readingsDelete ($hash, 'Battery_OptimumTargetSoC'); # 30.12.2024
|
||
readingsDelete ($hash, 'Battery_ChargeRequest'); # 30.12.2024
|
||
readingsDelete ($hash, 'Battery_ChargeRecommended'); # 30.12.2024
|
||
deleteReadingspec ($hash, 'Today_.*_BatIn'); # 30.12.2024
|
||
deleteReadingspec ($hash, 'Today_.*_BatOut'); # 30.12.2024
|
||
deleteReadingspec ($hash, 'statistic_.*'); # 02.01.2025
|
||
readingsDelete ($hash, '.migrated'); # 01.02.25
|
||
|
||
for my $ck (keys %{$data{$name}{circular}}) { # 30.12.2024
|
||
$data{$name}{circular}{$ck}{batin01} = delete $data{$name}{circular}{$ck}{batin} if(defined $data{$name}{circular}{$ck}{batin});
|
||
$data{$name}{circular}{$ck}{batout01} = delete $data{$name}{circular}{$ck}{batout} if(defined $data{$name}{circular}{$ck}{batout});
|
||
}
|
||
|
||
for my $pn (1..MAXPRODUCER) { # 30.12.2024
|
||
$pn = sprintf "%02d", $pn;
|
||
readingsDelete ($hash, 'Current_PP'.$pn);
|
||
deleteReadingspec ($hash, '.*PPreal'.$pn);
|
||
}
|
||
|
||
for my $dy (sort keys %{$data{$name}{pvhist}}) { # 01.01.2025
|
||
for my $hr (sort keys %{$data{$name}{pvhist}{$dy}}) {
|
||
$data{$name}{pvhist}{$dy}{$hr}{batintotal01} = delete $data{$name}{pvhist}{$dy}{$hr}{batintotal} if(defined $data{$name}{pvhist}{$dy}{$hr}{batintotal});
|
||
$data{$name}{pvhist}{$dy}{$hr}{batouttotal01} = delete $data{$name}{pvhist}{$dy}{$hr}{batouttotal} if(defined $data{$name}{pvhist}{$dy}{$hr}{batouttotal});
|
||
$data{$name}{pvhist}{$dy}{$hr}{batin01} = delete $data{$name}{pvhist}{$dy}{$hr}{batin} if(defined $data{$name}{pvhist}{$dy}{$hr}{batin});
|
||
$data{$name}{pvhist}{$dy}{$hr}{batout01} = delete $data{$name}{pvhist}{$dy}{$hr}{batout} if(defined $data{$name}{pvhist}{$dy}{$hr}{batout});
|
||
$data{$name}{pvhist}{$dy}{$hr}{batmaxsoc01} = delete $data{$name}{pvhist}{$dy}{$hr}{batmaxsoc} if(defined $data{$name}{pvhist}{$dy}{$hr}{batmaxsoc});
|
||
$data{$name}{pvhist}{$dy}{$hr}{batsetsoc01} = delete $data{$name}{pvhist}{$dy}{$hr}{batsetsoc} if(defined $data{$name}{pvhist}{$dy}{$hr}{batsetsoc});
|
||
}
|
||
}
|
||
|
||
my $n = 0; # 01.02.25 -> Datenmigration pvrlsum, pvfcsum, dnumsum in pvrl_*, pvfc_*
|
||
for my $hh (1..24) {
|
||
$hh = sprintf "%02d", $hh;
|
||
|
||
for my $cul (sort keys %{$data{$name}{circular}{$hh}}) {
|
||
next if($cul ne 'dnumsum');
|
||
|
||
for my $dns (sort keys %{$data{$name}{circular}{$hh}{$cul}}) {
|
||
next if($dns eq 'simple');
|
||
|
||
my ($sabin, $crang) = split /\./, $dns;
|
||
my ($pvsum, $fcsum, $dnum) = CircularSumVal ($hash, $hh, $sabin, $crang, undef);
|
||
|
||
delete $data{$name}{circular}{$hh}{pvrlsum}{$dns};
|
||
delete $data{$name}{circular}{$hh}{pvfcsum}{$dns};
|
||
delete $data{$name}{circular}{$hh}{dnumsum}{$dns};
|
||
|
||
next if(!defined $pvsum || !defined $fcsum || !$dnum);
|
||
|
||
my $pvavg = sprintf "%.0f", ($pvsum / $dnum);
|
||
my $fcavg = sprintf "%.0f", ($fcsum / $dnum);
|
||
|
||
push @{$data{$name}{circular}{$hh}{'pvrl_'.$sabin}{"$crang"}}, $pvavg;
|
||
push @{$data{$name}{circular}{$hh}{'pvfc_'.$sabin}{"$crang"}}, $fcavg;
|
||
|
||
$n++;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($n) {
|
||
Log3 ($name, 1, "$name - NOTE - the stored PV real and forecast datasets (quantity: $n) were migrated to the new module structure");
|
||
}
|
||
|
||
##########################################################################################################################
|
||
|
||
if (!CurrentVal ($hash, 'allStringsFullfilled', 0)) { # die String Konfiguration erstellen wenn noch nicht erfolgreich ausgeführt
|
||
my $ret = _createStringConfig ($hash);
|
||
|
||
if ($ret) {
|
||
if (!CurrentVal ($hash, 'setupcomplete', 0)) {
|
||
$ret = 'The setup routine is still incomplete';
|
||
}
|
||
singleUpdateState ( {hash => $hash, state => $ret, evt => 1} ); # Central Task running Statusbit
|
||
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (CurrentVal ($hash, 'ctrunning', 0)) {
|
||
Log3 ($name, 3, "$name - INFO - central task was called when it was already running ... end this call");
|
||
return;
|
||
}
|
||
|
||
$data{$name}{current}{ctrunning} = 1; # Central Task running Statusbit
|
||
InternalTimer (gettimeofday() + 2.0, "FHEM::SolarForecast::releaseCentralTask", $hash, 0); # Freigabe centralTask
|
||
|
||
my $t = time; # aktuelle Unix-Zeit
|
||
my $debug = getDebug ($hash); # Debug Module
|
||
|
||
my $dt = timestringsFromOffset ($t, 0);
|
||
my $chour = $dt->{hour};
|
||
|
||
my $centpars = {
|
||
name => $name,
|
||
type => $type,
|
||
t => $t,
|
||
date => $dt->{date}, # aktuelles Datum
|
||
minute => $dt->{minute}, # aktuelle Minute (00-59)
|
||
chour => $dt->{hour}, # aktuelle Stunde in 24h format (00-23)
|
||
day => $dt->{day}, # aktueller Tag (range 01 .. 31)
|
||
dayname => $dt->{dayname}, # aktueller Wochentagsname (locale-dependent!!)
|
||
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 ( {hash => $hash, state => $centpars->{state}, evt => $centpars->{evt}} );
|
||
|
||
$centpars->{state} = 'updated'; # kann durch Subs überschrieben werden!
|
||
|
||
_getMoonPhase ($centpars); # aktuelle Mondphase ermittteln und speichern
|
||
_collectAllRegConsumers ($centpars); # alle Verbraucher Infos laden
|
||
_specialActivities ($centpars); # zusätzliche Events generieren + Sonderaufgaben
|
||
_transferWeatherValues ($centpars); # Wetterwerte übertragen
|
||
|
||
readingsDelete ($hash, 'AllPVforecastsToEvent');
|
||
|
||
_getRoofTopData ($centpars); # Strahlungs/Wetter-Daten der gewählten API's abrufen und in internen Strukturen speichern
|
||
_transferInverterValues ($centpars); # WR Werte übertragen
|
||
_transferAPIRadiationValues ($centpars); # Raw Erzeugungswerte aus solcastapi-Hash übertragen und Forecast mit/ohne Korrektur erstellen
|
||
_calcMaxEstimateToday ($centpars); # heutigen Max PV Estimate & dessen Tageszeit ermitteln
|
||
_transferProducerValues ($centpars); # Werte anderer Erzeuger übertragen
|
||
_transferMeterValues ($centpars); # Energy Meter auswerten
|
||
_transferBatteryValues ($centpars); # Batteriewerte einsammeln
|
||
_batSocTarget ($centpars); # Batterie Optimum Ziel SOC berechnen
|
||
_batChargeRecmd ($centpars); # Batterie Ladefreigabe berechnen und erstellen
|
||
_manageConsumerData ($centpars); # Consumer Daten sammeln und Zeiten planen
|
||
_calcConsumptionForecast ($centpars); # Verbrauchsprognose erstellen
|
||
|
||
_calcConsumptionForecast_circular ($centpars); # Test neue Verbrauchsprognose
|
||
|
||
_evaluateThresholds ($centpars); # Schwellenwerte bewerten und signalisieren
|
||
_calcReadingsTomorrowPVFc ($centpars); # zusätzliche Readings Tomorrow_HourXX_PVforecast berechnen
|
||
_calcTodayPVdeviation ($centpars); # Vorhersageabweichung erstellen (nach Sonnenuntergang)
|
||
_calcDataEveryFullHour ($centpars); # Daten berechnen/speichern die nur einmal nach jeder vollen Stunde ermittelt werden
|
||
_saveEnergyConsumption ($centpars); # Energie Hausverbrauch speichern
|
||
_createSummaries ($centpars); # Zusammenfassungen erstellen
|
||
_genSpecialReadings ($centpars); # optionale Statistikreadings erstellen
|
||
|
||
userExit ($centpars); # User spezifische Funktionen ausführen
|
||
setTimeTracking ($hash, $cst, 'runTimeCentralTask'); # Zyklus-Laufzeit ermitteln
|
||
|
||
createReadingsFromArray ($hash, $evt); # Readings erzeugen
|
||
_readSystemMessages ($centpars); # Notification System - System Messages zusammenstellen
|
||
|
||
if ($evt) {
|
||
$centpars->{evt} = $evt;
|
||
InternalTimer (gettimeofday() + 1, "FHEM::SolarForecast::singleUpdateState", {hash => $hash, state => $centpars->{state}, evt => $centpars->{evt}}, 0);
|
||
}
|
||
else {
|
||
$centpars->{evt} = 1;
|
||
singleUpdateState ( {hash => $hash, state => $centpars->{state}, evt => $centpars->{evt}} );
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Erstellen der Stringkonfiguration
|
||
################################################################
|
||
sub _createStringConfig { ## no critic "not used"
|
||
my $hash = shift;
|
||
my $name = $hash->{NAME};
|
||
my $type = $hash->{TYPE};
|
||
|
||
delete $data{$name}{strings}; # Stringhash zurücksetzen
|
||
$data{$name}{current}{allStringsFullfilled} = 0;
|
||
|
||
my @istrings = split ",", AttrVal ($name, 'setupInverterStrings', ''); # Stringbezeichner
|
||
$data{$name}{current}{allstringscount} = scalar @istrings; # Anzahl der Anlagenstrings
|
||
|
||
if (!@istrings) {
|
||
return qq{Define all used strings with command "attr $name setupInverterStrings" first.};
|
||
}
|
||
|
||
my $peak = AttrVal ($name, 'setupStringPeak', ''); # kWp für jeden Stringbezeichner
|
||
return qq{Please complete attribute 'setupStringPeak'} if(!$peak);
|
||
|
||
my ($aa,$ha) = parseParams ($peak);
|
||
delete $data{$name}{current}{allstringspeak};
|
||
|
||
while (my ($strg, $pp) = each %$ha) {
|
||
if (grep /^$strg$/, @istrings) {
|
||
$data{$name}{strings}{$strg}{peak} = $pp;
|
||
$data{$name}{current}{allstringspeak} += $pp * 1000; # insgesamt installierte Peakleistung in W
|
||
}
|
||
else {
|
||
return qq{Check 'setupStringPeak' -> the stringname '$strg' is not defined as valid string in attribute 'setupInverterStrings'};
|
||
}
|
||
}
|
||
|
||
if (isSolCastUsed ($hash)) { # SolCast-API Strahlungsquelle
|
||
my $mrt = AttrVal ($name, 'setupRoofTops', ''); # RoofTop Konfiguration -> Zuordnung <pk>
|
||
return qq{Please complete command "attr $name setupRoofTops".} if(!$mrt);
|
||
|
||
my ($ad,$hd) = parseParams ($mrt);
|
||
|
||
while (my ($is, $pk) = each %$hd) {
|
||
if (grep /^$is$/, @istrings) {
|
||
$data{$name}{strings}{$is}{pk} = $pk;
|
||
}
|
||
else {
|
||
return qq{Check "setupRoofTops" -> the stringname "$is" is not defined as valid string in attribute "setupInverterStrings"};
|
||
}
|
||
}
|
||
}
|
||
elsif (isVictronKiUsed ($hash)) {
|
||
my $invs = AttrVal ($name, 'setupInverterStrings', '');
|
||
|
||
if ($invs ne 'KI-based') {
|
||
return qq{You use a KI based model. Please set only "KI-based" as String with command "attr $name setupInverterStrings".};
|
||
}
|
||
}
|
||
|
||
if (!grep /^KI-based$/, @istrings) {
|
||
my $tilt = ReadingsVal ($name, 'setupStringDeclination', ''); # Modul Neigungswinkel für jeden Stringbezeichner
|
||
return qq{Please complete command "set $name setupStringDeclination"} if(!$tilt);
|
||
|
||
my ($at,$ht) = parseParams ($tilt);
|
||
|
||
while (my ($key, $value) = each %$ht) {
|
||
if (grep /^$key$/, @istrings) {
|
||
$data{$name}{strings}{$key}{tilt} = $value;
|
||
}
|
||
else {
|
||
return qq{Check "setupStringDeclination" -> the stringname "$key" is not defined as valid string in attribute "setupInverterStrings"};
|
||
}
|
||
}
|
||
|
||
my $dir = ReadingsVal ($name, 'setupStringAzimuth', ''); # Modul Ausrichtung für jeden Stringbezeichner
|
||
return qq{Please complete command "set $name setupStringAzimuth"} if(!$dir);
|
||
|
||
my ($ad,$hd) = parseParams ($dir);
|
||
my $iwrong = qq{Please check the input of set "setupStringAzimuth". It seems to be wrong.};
|
||
|
||
while (my ($key, $value) = each %$hd) {
|
||
if (grep /^$key$/, @istrings) {
|
||
$data{$name}{strings}{$key}{azimut} = __ident2azimuth ($value) // return $iwrong;
|
||
}
|
||
else {
|
||
return qq{Check "setupStringAzimuth" -> the stringname "$key" is not defined as valid string in attribute "setupInverterStrings"};
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!keys %{$data{$name}{strings}}) {
|
||
return qq{The string configuration seems to be incomplete. \n}.
|
||
qq{Please check the settings of setupInverterStrings, setupStringPeak, setupStringAzimuth, setupStringDeclination }.
|
||
qq{and/or setupRoofTops if SolCast-API is used.};
|
||
}
|
||
|
||
my @sca = keys %{$data{$name}{strings}}; # Gegencheck ob nicht mehr Strings in setupInverterStrings enthalten sind als eigentlich verwendet
|
||
my @tom;
|
||
|
||
for my $sn (@istrings) {
|
||
next if(grep /^$sn$/, @sca);
|
||
push @tom, $sn;
|
||
}
|
||
|
||
if (@tom) {
|
||
return qq{Some Strings are not used. Please delete this string names from "setupInverterStrings" :}.join ",",@tom;
|
||
}
|
||
|
||
$data{$name}{current}{allStringsFullfilled} = 1;
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# 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;
|
||
}
|
||
|
||
################################################################
|
||
# Ermittlung der Mondphase
|
||
################################################################
|
||
sub _getMoonPhase {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $t = $paref->{t}; # Epoche Zeit
|
||
|
||
my $moonphasei;
|
||
my $tstr = (timestampToTimestring ($t))[2];
|
||
|
||
eval {
|
||
$moonphasei = FHEM::Astro::Get (undef, 'global', 'text', 'MoonPhaseI', $tstr);
|
||
1;
|
||
}
|
||
or do { Log3 ($name, 1, "$name - ERROR - $@");
|
||
return;
|
||
};
|
||
|
||
$data{$name}{current}{moonPhaseI} = $moonphasei;
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Grunddaten aller registrierten Consumer speichern
|
||
################################################################
|
||
sub _collectAllRegConsumers {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $defs{$name};
|
||
|
||
return if(CurrentVal ($hash, 'consumerCollected', 0)); # Abbruch wenn Consumer bereits gesammelt
|
||
|
||
delete $data{$name}{current}{consumerdevs};
|
||
|
||
for my $c (1..MAXCONSUMER) {
|
||
$c = sprintf "%02d", $c;
|
||
my ($err, $consumer, $hc, $alias) = isDeviceValid ( { name => $name, obj => "consumer${c}", method => 'attr' } );
|
||
next if($err);
|
||
|
||
push @{$data{$name}{current}{consumerdevs}}, $consumer; # alle Consumerdevices in CurrentHash eintragen
|
||
|
||
my $dswitch = $hc->{switchdev}; # alternatives Schaltdevice
|
||
|
||
if ($dswitch) {
|
||
my ($err) = isDeviceValid ( { name => $name, obj => $dswitch, method => 'string' } );
|
||
next if($err);
|
||
|
||
push @{$data{$name}{current}{consumerdevs}}, $dswitch if($dswitch ne $consumer); # Switchdevice zusätzlich in CurrentHash eintragen
|
||
}
|
||
else {
|
||
$dswitch = $consumer;
|
||
}
|
||
|
||
$alias = AttrVal ($consumer, 'alias', $consumer) if(!$alias);
|
||
|
||
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 $exconfc;
|
||
if (exists $hc->{exconfc}) { # Consumer Verbrauch von Erstelleung der Verbrauchsprognose ausschließen
|
||
$exconfc = $hc->{exconfc};
|
||
}
|
||
|
||
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};
|
||
|
||
if ($interruptable ne '1') {
|
||
(my $dv, my $rd, my $reg, $hyst) = split ':', $interruptable;
|
||
$interruptable = "$dv:$rd:$reg";
|
||
}
|
||
}
|
||
|
||
$hyst = DEFHYST if(!$hyst);
|
||
|
||
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};
|
||
}
|
||
|
||
delete $data{$name}{consumers}{$c}{sunriseshift};
|
||
delete $data{$name}{consumers}{$c}{sunsetshift};
|
||
delete $data{$name}{consumers}{$c}{icon};
|
||
|
||
my $rauto = $hc->{auto} // q{};
|
||
my $ctype = $hc->{type} // DEFCTYPE;
|
||
|
||
$data{$name}{consumers}{$c}{name} = $consumer; # Name des Verbrauchers (Device)
|
||
$data{$name}{consumers}{$c}{alias} = $alias; # Alias des Verbrauchers (Device)
|
||
$data{$name}{consumers}{$c}{type} = $hc->{type} // DEFCTYPE; # Typ des Verbrauchers
|
||
$data{$name}{consumers}{$c}{power} = $hc->{power}; # Leistungsaufnahme des Verbrauchers in W
|
||
$data{$name}{consumers}{$c}{avgenergy} = q{}; # Initialwert Energieverbrauch (evtl. Überschreiben in manageConsumerData)
|
||
$data{$name}{consumers}{$c}{mintime} = $hc->{mintime} // $hef{$ctype}{mt}; # Initialwert min. Einplanungsdauer (evtl. Überschreiben in manageConsumerData)
|
||
$data{$name}{consumers}{$c}{mode} = $hc->{mode} // DEFCMODE; # Planungsmode des Verbrauchers
|
||
$data{$name}{consumers}{$c}{oncom} = $hc->{on} // q{}; # Setter Einschaltkommando
|
||
$data{$name}{consumers}{$c}{offcom} = $hc->{off} // q{}; # Setter Ausschaltkommando
|
||
$data{$name}{consumers}{$c}{dswitch} = $dswitch; # Switchdevice zur Kommandoausführung
|
||
$data{$name}{consumers}{$c}{autoreading} = $rauto; # Readingname zur Automatiksteuerung
|
||
$data{$name}{consumers}{$c}{retotal} = $rtot // q{}; # Reading der Leistungsmessung
|
||
$data{$name}{consumers}{$c}{uetotal} = $utot // q{}; # Unit der Leistungsmessung
|
||
$data{$name}{consumers}{$c}{rpcurr} = $rpcurr // q{}; # Reading der aktuellen Leistungsaufnahme
|
||
$data{$name}{consumers}{$c}{upcurr} = $upcurr // q{}; # Unit der aktuellen Leistungsaufnahme
|
||
$data{$name}{consumers}{$c}{energythreshold} = $ethreshold; # Schwellenwert (Wh pro Stunde) ab der ein Verbraucher als aktiv gewertet wird
|
||
$data{$name}{consumers}{$c}{powerthreshold} = $pthreshold; # Schwellenwert d. aktuellen Leistung(W) ab der ein Verbraucher als aktiv gewertet wird
|
||
$data{$name}{consumers}{$c}{notbefore} = $hc->{notbefore} // q{}; # nicht einschalten vor Stunde in 24h Format (00-23)
|
||
$data{$name}{consumers}{$c}{notafter} = $hc->{notafter} // q{}; # nicht einschalten nach Stunde in 24h Format (00-23)
|
||
$data{$name}{consumers}{$c}{rswstate} = $rswstate // 'state'; # Schaltstatus Reading
|
||
$data{$name}{consumers}{$c}{asynchron} = $asynchron // 0; # Arbeitsweise FHEM Consumer Device
|
||
$data{$name}{consumers}{$c}{noshow} = $noshow // 0; # ausblenden in Grafik
|
||
$data{$name}{consumers}{$c}{exconfc} = $exconfc // 0; # Verbrauch von Erstelleung der Verbrauchsprognose ausschließen
|
||
$data{$name}{consumers}{$c}{surpmeth} = $hc->{surpmeth} // 'default'; # Ermittlungsmethode des PV-Überschusses, default -> direkte Messung
|
||
$data{$name}{consumers}{$c}{locktime} = $clt // '0:0'; # Sperrzeit im Automatikmodus ('offlt:onlt')
|
||
$data{$name}{consumers}{$c}{onreg} = $onreg // 'on'; # Regex für 'ein'
|
||
$data{$name}{consumers}{$c}{offreg} = $offreg // 'off'; # Regex für 'aus'
|
||
$data{$name}{consumers}{$c}{dswoncond} = $dswoncond // q{}; # Device zur Lieferung einer zusätzliche Einschaltbedingung
|
||
$data{$name}{consumers}{$c}{rswoncond} = $rswoncond // q{}; # Reading zur Lieferung einer zusätzliche Einschaltbedingung
|
||
$data{$name}{consumers}{$c}{swoncondregex} = $swoncondregex // q{}; # Regex einer zusätzliche Einschaltbedingung
|
||
$data{$name}{consumers}{$c}{dswoffcond} = $dswoffcond // q{}; # Device zur Lieferung einer vorrangigen Ausschaltbedingung
|
||
$data{$name}{consumers}{$c}{rswoffcond} = $rswoffcond // q{}; # Reading zur Lieferung einer vorrangigen Ausschaltbedingung
|
||
$data{$name}{consumers}{$c}{swoffcondregex} = $swoffcondregex // q{}; # Regex einer vorrangigen Ausschaltbedingung
|
||
$data{$name}{consumers}{$c}{dspignorecond} = $dspignorecond // q{}; # Device liefert Ignore Bedingung
|
||
$data{$name}{consumers}{$c}{rigncond} = $rigncond // q{}; # Reading liefert Ignore Bedingung
|
||
$data{$name}{consumers}{$c}{spignorecondregex} = $spignorecondregex // q{}; # Regex der Ignore Bedingung
|
||
$data{$name}{consumers}{$c}{interruptable} = $interruptable; # Ein-Zustand des Verbrauchers ist unterbrechbar
|
||
$data{$name}{consumers}{$c}{hysteresis} = $hyst; # Hysterese
|
||
$data{$name}{consumers}{$c}{sunriseshift} = $riseshift if(defined $riseshift); # Verschiebung (Sekunden) Sonnenaufgang bei SunPath Verwendung
|
||
$data{$name}{consumers}{$c}{sunsetshift} = $setshift if(defined $setshift); # Verschiebung (Sekunden) Sonnenuntergang bei SunPath Verwendung
|
||
$data{$name}{consumers}{$c}{icon} = $hc->{icon} if(defined $hc->{icon}); # Icon für den Verbraucher
|
||
}
|
||
|
||
$data{$name}{current}{consumerCollected} = 1;
|
||
|
||
Log3 ($name, 3, "$name - all registered consumers collected");
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Sonderaufgaben !
|
||
################################################################
|
||
sub _specialActivities {
|
||
my $paref = shift;
|
||
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 $hash = $defs{$name};
|
||
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{$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);
|
||
}
|
||
}
|
||
|
||
## 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) {
|
||
if (!defined $hash->{HELPER}{T1RUN}) {
|
||
$hash->{HELPER}{T1RUN} = 1;
|
||
|
||
Log3 ($name, 4, "$name - Daily special tasks - Task 1 started");
|
||
|
||
__deleteEveryHourControls ($paref); # Sperrsignale der Stundenwerte-Steuerung löschen
|
||
|
||
Log3 ($name, 4, "$name - Daily special tasks - Task 1 finished");
|
||
}
|
||
}
|
||
else {
|
||
delete $hash->{HELPER}{T1RUN};
|
||
}
|
||
|
||
## Task 2
|
||
###########
|
||
if ($chour == 0 && $minute >= 0) {
|
||
if (!defined $hash->{HELPER}{T2RUN}) {
|
||
$hash->{HELPER}{T2RUN} = 1;
|
||
|
||
Log3 ($name, 4, "$name - Daily special tasks - Task 2 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.*|.*_PV.*|.*_PPreal.*|.*_Bat.*)|powerTrigger_.*|Today_MaxPVforecast.*)');
|
||
readingsDelete ($hash, 'Today_PVdeviation');
|
||
readingsDelete ($hash, 'Today_PVreal');
|
||
|
||
readingsDelete ($hash, 'Error');
|
||
readingsDelete ($hash, 'Errorcode');
|
||
|
||
if (scalar(@widgetreadings)) { # vermeide Schleife falls FHEMWEB geöfffnet
|
||
my @acopy = @widgetreadings;
|
||
@widgetreadings = ();
|
||
|
||
for my $wdr (@acopy) { # Array der Hilfsreadings (Attributspeicher) löschen
|
||
readingsDelete ($hash, $wdr);
|
||
}
|
||
}
|
||
|
||
my ($rapi, $wapi) = getStatusApiName ($hash);
|
||
delete $data{$name}{statusapi}{$rapi}{'?All'} if($rapi); # Radiation-API Statusdaten (Tageszähler) löschen
|
||
delete $data{$name}{statusapi}{$wapi}{'?All'} if($wapi); # V 1.42.0 - Weather-API Statusdaten (Tageszähler) löschen
|
||
|
||
delete $data{$name}{circular}{99}{initdayfeedin};
|
||
delete $data{$name}{circular}{99}{initdaygcon};
|
||
delete $data{$name}{current}{sunriseToday};
|
||
delete $data{$name}{current}{sunriseTodayTs};
|
||
delete $data{$name}{current}{sunsetToday};
|
||
delete $data{$name}{current}{sunsetTodayTs};
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
delete $data{$name}{circular}{99}{'initdaybatintot'.$bn};
|
||
delete $data{$name}{circular}{99}{'initdaybatouttot'.$bn};
|
||
}
|
||
|
||
$data{$name}{circular}{99}{ydayDvtn} = CircularVal ($hash, 99, 'tdayDvtn', '-');
|
||
delete $data{$name}{circular}{99}{tdayDvtn};
|
||
|
||
delete $data{$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 2 finished");
|
||
}
|
||
}
|
||
else {
|
||
delete $hash->{HELPER}{T2RUN};
|
||
}
|
||
|
||
## Task 3
|
||
###########
|
||
if ($chour == 0 && $minute >= 2) {
|
||
if (!defined $hash->{HELPER}{T3RUN}) {
|
||
$hash->{HELPER}{T3RUN} = 1;
|
||
|
||
Log3 ($name, 4, "$name - Daily special tasks - Task 3 started");
|
||
|
||
for my $c (keys %{$data{$name}{consumers}}) { # Planungsdaten regulär löschen
|
||
next if(ConsumerVal ($hash, $c, "plandelete", "regular") ne "regular");
|
||
deleteConsumerPlanning ($hash, $c);
|
||
}
|
||
|
||
writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben
|
||
|
||
Log3 ($name, 4, "$name - Daily special tasks - Task 3 finished");
|
||
}
|
||
}
|
||
else {
|
||
delete $hash->{HELPER}{T3RUN};
|
||
}
|
||
|
||
## Task 4
|
||
###########
|
||
if ($chour == 0 && $minute >= 5) {
|
||
if (!defined $hash->{HELPER}{T4RUN}) {
|
||
$hash->{HELPER}{T4RUN} = 1;
|
||
|
||
Log3 ($name, 4, "$name - Daily special tasks - Task 4 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 4 finished");
|
||
}
|
||
}
|
||
else {
|
||
delete $hash->{HELPER}{T4RUN};
|
||
}
|
||
|
||
## Task 5
|
||
###########
|
||
if ($chour == 0 && $minute >= 9) {
|
||
if (!defined $hash->{HELPER}{T5RUN}) {
|
||
$hash->{HELPER}{T5RUN} = 1;
|
||
|
||
Log3 ($name, 4, "$name - Daily special tasks - Task 5 started");
|
||
|
||
if (AttrVal ($name, 'ctrlBackupFilesKeep', 3)) {
|
||
periodicWriteMemcache ($hash, 'bckp'); # Backup Files erstellen und alte Versionen löschen (unterbleibt bei ctrlBackupFilesKeep == 0)
|
||
}
|
||
|
||
Log3 ($name, 4, "$name - Daily special tasks - Task 5 finished");
|
||
}
|
||
}
|
||
else {
|
||
delete $hash->{HELPER}{T5RUN};
|
||
}
|
||
|
||
## Task 6
|
||
###########
|
||
if ($chour == $aitrh && $minute >= 15) {
|
||
if (!defined $hash->{HELPER}{T6RUN}) {
|
||
$hash->{HELPER}{T6RUN} = 1;
|
||
|
||
Log3 ($name, 4, "$name - Daily special tasks - Task 6 started");
|
||
|
||
aiDelRawData ($paref); # KI Raw Daten löschen welche die maximale Haltezeit überschritten haben
|
||
|
||
$paref->{taa} = 1;
|
||
aiAddInstancePV ($paref); # AI PV-Forecast füllen, trainieren und sichern
|
||
delete $paref->{taa};
|
||
|
||
Log3 ($name, 4, "$name - Daily special tasks - Task 6 finished");
|
||
}
|
||
}
|
||
else {
|
||
delete $hash->{HELPER}{T6RUN};
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
#############################################################################
|
||
# Readings der pvCorrectionFactor-Steuerung löschen
|
||
#############################################################################
|
||
sub __deleteEveryHourControls {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $defs{$name};
|
||
|
||
for my $n (0..24) {
|
||
$n = sprintf "%02d", $n;
|
||
|
||
### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !!
|
||
##########################################################################################################################
|
||
readingsDelete ($hash, ".pvCorrectionFactor_${n}_cloudcover"); # 01.02.2025
|
||
readingsDelete ($hash, ".pvCorrectionFactor_${n}_apipercentil");
|
||
##########################################################################################################################
|
||
|
||
readingsDelete ($hash, ".signaldone_${n}");
|
||
|
||
if (ReadingsVal ($name, 'pvCorrectionFactor_Auto', 'off') =~ /on/xs) {
|
||
my $pcf = ReadingsVal ($name, "pvCorrectionFactor_${n}", '');
|
||
($pcf) = split " / ", $pcf if($pcf =~ /\s\/\s/xs);
|
||
|
||
if ($pcf !~ /manual/xs) { # manuell gesetzte pcf-Readings nicht löschen
|
||
readingsDelete ($hash, "pvCorrectionFactor_${n}"); # V 1.37.0
|
||
}
|
||
else {
|
||
readingsSingleUpdate ($hash, "pvCorrectionFactor_${n}", $pcf, 0);
|
||
}
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
#############################################################################
|
||
# zusätzliche Events erzeugen - PV Vorhersage bis Ende des kommenden Tages
|
||
#############################################################################
|
||
sub __createAdditionalEvents {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $hash = $defs{$name};
|
||
|
||
my $done = 0;
|
||
|
||
for my $idx (sort keys %{$data{$name}{nexthours}}) {
|
||
my $nhts = NexthoursVal ($hash, $idx, 'starttime', undef);
|
||
my $nhfc = NexthoursVal ($hash, $idx, 'pvfc', undef);
|
||
next if(!defined $nhts || !defined $nhfc);
|
||
|
||
$done = 1;
|
||
my ($dt, $h) = $nhts =~ /([\w-]+)\s(\d{2})/xs;
|
||
storeReading ('AllPVforecastsToEvent', "$nhfc Wh", $dt." ".$h.":59:59");
|
||
}
|
||
|
||
if (!$done) {
|
||
Log3 ($name, 2, "$name - WARNING - Events of 'AllPVforecastsToEvent' were not created due to no data in 'nexthours'");
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
#############################################################################
|
||
# solcastapi Hash veraltete Daten löschen
|
||
#############################################################################
|
||
sub __delObsoleteAPIData {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $date = $paref->{date}; # aktuelles Datum
|
||
my $hash = $defs{$name};
|
||
|
||
my ($rapi, $wapi) = getStatusApiName ($hash);
|
||
|
||
## Wetter-API Daten löschen
|
||
#############################
|
||
if (keys %{$data{$name}{weatherapi}}) {
|
||
if ($wapi ne 'OpenMeteo') {
|
||
delete $data{$name}{weatherapi}{OpenMeteo};
|
||
}
|
||
}
|
||
|
||
## Status-API Daten löschen
|
||
#############################
|
||
if (keys %{$data{$name}{statusapi}}) {
|
||
delete $data{$name}{statusapi}{OpenMeteo} if($rapi ne 'OpenMeteo' && $wapi ne 'OpenMeteo');
|
||
delete $data{$name}{statusapi}{ForecastSolar} if($rapi ne 'ForecastSolar');
|
||
delete $data{$name}{statusapi}{SolCast} if($rapi ne 'SolCast');
|
||
delete $data{$name}{statusapi}{'?IdPair'} if($rapi ne 'SolCast');
|
||
delete $data{$name}{statusapi}{DWD} if($rapi ne 'DWD');
|
||
delete $data{$name}{statusapi}{VictronKi} if($rapi ne 'VictronKi');
|
||
delete $data{$name}{statusapi}{'?VRM'} if($rapi ne 'VictronKi');
|
||
}
|
||
|
||
## Solar-API Daten löschen
|
||
#############################
|
||
if (keys %{$data{$name}{solcastapi}}) {
|
||
my $refts = timestringToTimestamp ($date.' 00:00:00'); # Referenztimestring
|
||
|
||
for my $idx (sort keys %{$data{$name}{solcastapi}}) { # alle Datumschlüssel kleiner aktueller Tag 00:00:00 selektieren
|
||
for my $scd (sort keys %{$data{$name}{solcastapi}{$idx}}) {
|
||
my $ds = timestringToTimestamp ($scd);
|
||
delete $data{$name}{solcastapi}{$idx}{$scd} if($ds && $ds < $refts);
|
||
}
|
||
}
|
||
}
|
||
|
||
## veraltete Strings aus Strings-Hash löschen
|
||
################################################
|
||
my @as = split ",", AttrVal ($name, 'setupInverterStrings', '');
|
||
if (scalar @as) {
|
||
for my $k (keys %{$data{$name}{strings}}) {
|
||
next if($k =~ /\?All/);
|
||
next if(grep /^$k$/, @as);
|
||
|
||
delete $data{$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 $name = $paref->{name};
|
||
my $chour = $paref->{chour};
|
||
my $hash = $defs{$name};
|
||
|
||
my ($valid, $fcname, $apiu) = isWeatherDevValid ($hash, 'setupWeatherDev1'); # Standard Weather Forecast Device
|
||
return if(!$valid);
|
||
|
||
delete $data{$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 {
|
||
my ($rapi, $wapi) = getStatusApiName ($hash);
|
||
$fctime = StatusAPIVal ($hash, $wapi, '?All', 'lastretrieval_time', '-');
|
||
$fctimets = StatusAPIVal ($hash, $wapi, '?All', 'lastretrieval_timestamp', '-');
|
||
}
|
||
|
||
$data{$name}{current}{dwdWfchAge} = $fctime;
|
||
$data{$name}{current}{dwdWfchAgeTS} = $fctimets;
|
||
|
||
for my $step (1..MAXWEATHERDEV) {
|
||
$paref->{step} = $step;
|
||
__readDataWeather ($paref); # Wetterdaten aus Device in Wetter-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{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{ww}; # signifikantes Wetter = Wetter ID
|
||
my $wwd = $data{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{wwd}; # Wetter Beschreibung
|
||
my $wcc = $data{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{neff}; # Effektive Wolkendecke
|
||
my $rr1c = $data{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{rr1c}; # Gesamtniederschlag (1-stündig) letzte 1 Stunde
|
||
my $temp = $data{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{ttt}; # Außentemperatur
|
||
my $don = $data{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{don}; # Tag/Nacht-Grenze
|
||
|
||
my $nhtstr = "NextHour".sprintf "%02d", $num;
|
||
$data{$name}{nexthours}{$nhtstr}{weatherid} = $wid;
|
||
$data{$name}{nexthours}{$nhtstr}{wcc} = $wcc;
|
||
$data{$name}{nexthours}{$nhtstr}{rr1c} = $rr1c;
|
||
$data{$name}{nexthours}{$nhtstr}{rainrange} = $rr1c;
|
||
$data{$name}{nexthours}{$nhtstr}{temp} = $temp;
|
||
$data{$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{$name}{circular}{sprintf("%02d",$fh1)}{weatherid} = $wid;
|
||
$data{$name}{circular}{sprintf("%02d",$fh1)}{weathertxt} = $wwd;
|
||
$data{$name}{circular}{sprintf("%02d",$fh1)}{wcc} = $wcc;
|
||
$data{$name}{circular}{sprintf("%02d",$fh1)}{rr1c} = $rr1c;
|
||
$data{$name}{circular}{sprintf("%02d",$fh1)}{temp} = $temp;
|
||
|
||
if ($num == 0) { # aktuelle Außentemperatur
|
||
$data{$name}{current}{temp} = $temp;
|
||
}
|
||
}
|
||
|
||
if ($fd == 0 && $fh1) { # Weather in pvHistory speichern
|
||
writeToHistory ( { paref => $paref, key => 'weatherid', val => $wid, hour => $fh1 } );
|
||
writeToHistory ( { paref => $paref, key => 'weathercloudcover', val => $wcc // 0, hour => $fh1 } );
|
||
writeToHistory ( { paref => $paref, key => 'rr1c', val => $rr1c, hour => $fh1 } );
|
||
writeToHistory ( { paref => $paref, key => 'temperature', val => $temp, hour => $fh1 } );
|
||
writeToHistory ( { paref => $paref, key => 'DoN', val => $don, hour => $fh1 } );
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# lese Wetterdaten aus Device im Attribut setupWeatherDevX
|
||
# X = laufende Schleifenvariable $step
|
||
################################################################
|
||
sub __readDataWeather {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $chour = $paref->{chour}; # aktuelles Datum
|
||
my $type = $paref->{type};
|
||
my $step = $paref->{step};
|
||
my $t = $paref->{t};
|
||
my $hash = $defs{$name};
|
||
|
||
my ($valid, $fcname, $apiu) = isWeatherDevValid ($hash, 'setupWeatherDev'.$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", undef); # Effektiver Bedeckungsgrad zum Vorhersagezeitpunkt
|
||
my $temp = ReadingsNum ($fcname, "fc${fd}_${fh}_TTT", undef); # 2m-Temperatur zum Vorhersagezeitpunkt
|
||
my $sunup = ReadingsNum ($fcname, "fc${fd}_${fh}_SunUp", 0); # 1 - Tag
|
||
|
||
if (!$n) { # Hour 00 -> Werte des vorigen Tag / hour 24 verwenden
|
||
my $dt = timestringsFromOffset ($t, -86400);
|
||
$wid //= HistoryVal ($name, $dt->{day}, '24', 'weatherid', undef);
|
||
$neff //= HistoryVal ($name, $dt->{day}, '24', 'wcc', undef);
|
||
$temp //= HistoryVal ($name, $dt->{day}, '24', 'temp', undef);
|
||
}
|
||
|
||
if (!$sunup && defined $wid && defined $weather_ids{$wid}{icon} && $weather_ids{$wid}{icon} ne 'unknown') { # Nacht-Icons
|
||
$wid += 100;
|
||
}
|
||
|
||
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 -> wir schuen in die Zukunft
|
||
|
||
debugLog ($paref, 'collectData', "Weather $step: fc${fd}_${fh}, don: $sunup, wid: ".(defined $wid ? $wid : '<undef>').", RR1c: $rr1c, TTT: ".(defined $temp ? $temp : '<undef>').", Neff: ".(defined $neff ? $neff : '<undef>'));
|
||
|
||
$data{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{ww} = $wid;
|
||
$data{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{wwd} = $wwd;
|
||
$data{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{neff} = $neff;
|
||
$data{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{rr1c} = $rr1c;
|
||
$data{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{ttt} = $temp;
|
||
$data{$name}{weatherdata}{"fc${fd}_${fh}"}{$step}{don} = $sunup;
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# lese Wetterdaten aus API Speicher (solcastapi)
|
||
################################################################
|
||
sub ___readDataWeatherAPI {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $step = $paref->{step};
|
||
my $fcname = $paref->{fcname};
|
||
my $hash = $defs{$name};
|
||
|
||
debugLog ($paref, 'collectData', "collect Weather data step $step - API: $fcname =>");
|
||
|
||
my ($rapi, $wapi) = getStatusApiName ($hash);
|
||
|
||
for my $idx (sort keys %{$data{$name}{weatherapi}{$wapi}}) {
|
||
if ($idx =~ /^fc?([0-9]{1,2})_?([0-9]{1,2})$/xs) { # valider Weather API Index
|
||
my $rr1c = WeatherAPIVal ($hash, $wapi, $idx, 'rr1c', undef);
|
||
my $wid = WeatherAPIVal ($hash, $wapi, $idx, 'ww', undef);
|
||
my $neff = WeatherAPIVal ($hash, $wapi, $idx, 'neff', undef);
|
||
my $don = WeatherAPIVal ($hash, $wapi, $idx, 'don', 0);
|
||
my $ttt = WeatherAPIVal ($hash, $wapi, $idx, 'ttt', undef);
|
||
|
||
if ($idx eq 'fc0_0') {
|
||
$wid //= WeatherAPIVal ($hash, $wapi, 'fc0_1', 'ww', undef);
|
||
$neff //= WeatherAPIVal ($hash, $wapi, 'fc0_1', 'neff', undef);
|
||
$ttt //= WeatherAPIVal ($hash, $wapi, 'fc0_1', 'ttt', undef);
|
||
}
|
||
|
||
if (!$don && defined $wid && defined $weather_ids{$wid}{icon} && $weather_ids{$wid}{icon} ne 'unknown') { # Nacht-Icons
|
||
$wid += 100;
|
||
}
|
||
|
||
$data{$name}{weatherdata}{$idx}{$step}{ww} = $wid if(defined $wid);
|
||
$data{$name}{weatherdata}{$idx}{$step}{neff} = $neff if(defined $neff);
|
||
$data{$name}{weatherdata}{$idx}{$step}{rr1c} = $rr1c if(defined $rr1c);
|
||
$data{$name}{weatherdata}{$idx}{$step}{ttt} = $ttt if(defined $ttt);
|
||
$data{$name}{weatherdata}{$idx}{$step}{don} = $don if(defined $don);
|
||
|
||
debugLog ($paref, 'collectData', "Weather $step: $idx".
|
||
", don: ". (defined $don ? $don : '<undef>').
|
||
", wid: ". (defined $wid ? $wid : '<undef>').
|
||
", RR1c: ".(defined $rr1c ? $rr1c : '<undef>').
|
||
", TTT: ". (defined $ttt ? $ttt : '<undef>').
|
||
", Neff: ".(defined $neff ? $neff : '<undef>')
|
||
);
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Wetterdaten mergen
|
||
################################################################
|
||
sub __mergeDataWeather {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $hash = $defs{$name};
|
||
|
||
debugLog ($paref, 'collectData', "merge Weather data =>");
|
||
|
||
my $ds = 0;
|
||
|
||
for my $wd (1..MAXWEATHERDEV) {
|
||
my ($valid, $fcname, $apiu) = isWeatherDevValid ($hash, 'setupWeatherDev'.$wd); # Weather Forecast Device
|
||
$ds++ if($valid);
|
||
}
|
||
|
||
my ($q, $m) = (0,0);
|
||
|
||
for my $key (sort keys %{$data{$name}{weatherdata}}) {
|
||
my ($z, $neff, $rr1c, $temp) = (0,0,0,0);
|
||
|
||
$data{$name}{weatherdata}{$key}{merge}{don} = $data{$name}{weatherdata}{$key}{1}{don};
|
||
$data{$name}{weatherdata}{$key}{merge}{ww} = $data{$name}{weatherdata}{$key}{1}{ww};
|
||
$data{$name}{weatherdata}{$key}{merge}{wwd} = $data{$name}{weatherdata}{$key}{1}{wwd};
|
||
$data{$name}{weatherdata}{$key}{merge}{neff} = $data{$name}{weatherdata}{$key}{1}{neff};
|
||
$data{$name}{weatherdata}{$key}{merge}{rr1c} = $data{$name}{weatherdata}{$key}{1}{rr1c};
|
||
$data{$name}{weatherdata}{$key}{merge}{ttt} = $data{$name}{weatherdata}{$key}{1}{ttt};
|
||
|
||
for my $step (1..$ds) {
|
||
$q++;
|
||
|
||
my $n = $data{$name}{weatherdata}{$key}{$step}{neff};
|
||
my $r = $data{$name}{weatherdata}{$key}{$step}{rr1c};
|
||
my $t = $data{$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{$name}{weatherdata}{$key}{merge}{neff} = sprintf "%.0f", ($neff / $z);
|
||
$data{$name}{weatherdata}{$key}{merge}{rr1c} = sprintf "%.2f", ($rr1c / $z);
|
||
$data{$name}{weatherdata}{$key}{merge}{ttt} = sprintf "%.2f", ($temp / $z);
|
||
|
||
debugLog ($paref, 'collectData', "Weather merged: $key, ".
|
||
"don: $data{$name}{weatherdata}{$key}{merge}{don}, ".
|
||
"wid: ".(defined $data{$name}{weatherdata}{$key}{1}{ww} ? $data{$name}{weatherdata}{$key}{1}{ww} : '<undef>').", ".
|
||
"RR1c: $data{$name}{weatherdata}{$key}{merge}{rr1c}, ".
|
||
"TTT: $data{$name}{weatherdata}{$key}{merge}{ttt}, ".
|
||
"Neff: $data{$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 $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 $hash = $defs{$name};
|
||
|
||
my ($fc0_sr, $fc0_ss, $fc1_sr, $fc1_ss);
|
||
|
||
my ($cset, undef, undef, undef) = locCoordinates();
|
||
|
||
debugLog ($paref, 'collectData', "collect sunrise/sunset times - device: $fcname =>");
|
||
|
||
my ($rapi, $wapi) = getStatusApiName ($hash);
|
||
|
||
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 (WeatherAPIVal ($hash, $wapi, 'sunrise', 'today', '23:59:59'), 0, 5);
|
||
$fc0_ss = substr (WeatherAPIVal ($hash, $wapi, 'sunset', 'today', '00:00:00'), 0, 5);
|
||
$fc1_sr = substr (WeatherAPIVal ($hash, $wapi, 'sunrise', 'tomorrow', '23:59:59'), 0, 5);
|
||
$fc1_ss = substr (WeatherAPIVal ($hash, $wapi, 'sunset', 'tomorrow', '00:00:00'), 0, 5);
|
||
}
|
||
}
|
||
|
||
$data{$name}{current}{sunriseToday} = $date.' '.$fc0_sr.':00';
|
||
$data{$name}{current}{sunriseTodayTs} = timestringToTimestamp ($date.' '.$fc0_sr.':00');
|
||
|
||
$data{$name}{current}{sunsetToday} = $date.' '.$fc0_ss.':00';
|
||
$data{$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);
|
||
}
|
||
|
||
################################################################
|
||
# Werte Inverter Device ermitteln und übertragen
|
||
################################################################
|
||
sub _transferInverterValues {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $t = $paref->{t}; # aktuelle Unix-Zeit
|
||
my $chour = $paref->{chour};
|
||
my $day = $paref->{day};
|
||
|
||
my $hash = $defs{$name};
|
||
my ($acu, $aln) = isAutoCorrUsed ($name);
|
||
my $nhour = $chour + 1;
|
||
my $warn = '';
|
||
my $pvsum = 0; # Summe aktuelle PV aller Inverter
|
||
my $ethishoursum = 0; # Summe Erzeugung akt. Stunde aller Inverter
|
||
|
||
for my $in (1..MAXINVERTER) {
|
||
$in = sprintf "%02d", $in;
|
||
|
||
my ($err, $indev, $h) = isDeviceValid ( { name => $name, obj => 'setupInverterDev'.$in, method => 'attr' } );
|
||
next if($err);
|
||
|
||
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)
|
||
|
||
next 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
|
||
|
||
my $etuf = $etunit =~ /^kWh$/xi ? 1000 : 1;
|
||
my $etotal = ReadingsNum ($indev, $edread, 0) * $etuf; # Erzeugung total (Wh)
|
||
my $histetot = HistoryVal ($hash, $day, sprintf("%02d",$nhour), 'etotali'.$in, 0); # etotal zu Beginn einer Stunde
|
||
|
||
my ($ethishour, $etotsvd);
|
||
|
||
if (!$histetot) { # etotal der aktuelle Stunde gesetzt ?
|
||
writeToHistory ( { paref => $paref, key => 'etotali'.$in, val => $etotal, hour => $nhour } );
|
||
|
||
$etotsvd = InverterVal ($hash, $in, 'ietotal', $etotal);
|
||
$ethishour = int ($etotal - $etotsvd);
|
||
}
|
||
else {
|
||
$ethishour = int ($etotal - $histetot);
|
||
|
||
if (defined $h->{capacity} && $ethishour > 2 * $h->{capacity}) { # Schutz vor plötzlichem Anstieg von 0 auf mehr als doppelte WR-Kapazität
|
||
Log3 ($name, 1, "$name - WARNING - The generated PV of Inverter '$indev' is much more higher than capacity set in inverter key 'capacity'. It seems to be a failure and Energy Total is reinitialized.");
|
||
$warn = ' (WARNING: too much generated PV was registered - see log file)';
|
||
|
||
writeToHistory ( { paref => $paref, key => 'etotali'.$in, val => $etotal, hour => $nhour } );
|
||
|
||
$etotsvd = InverterVal ($hash, $in, 'ietotal', $etotal);
|
||
$ethishour = int ($etotal - $etotsvd);
|
||
}
|
||
}
|
||
|
||
if ($ethishour < 0) {
|
||
$ethishour = 0;
|
||
my $vl = 3;
|
||
my $pre = '- WARNING -';
|
||
|
||
if ($paref->{debug} =~ /collectData/xs) { # V 1.23.0 Forum: https://forum.fhem.de/index.php?msg=1314453
|
||
$vl = 1;
|
||
$pre = 'DEBUG> - WARNING -';
|
||
}
|
||
|
||
Log3 ($name, $vl, "$name $pre The Total Energy of Inverter '$indev' is lower than the value saved before. This situation is unexpected and the Energy generated of current hour of this inverter is set to '0'.");
|
||
$warn = ' (WARNING invalid real PV occured - see Logfile)';
|
||
}
|
||
|
||
my $feed = $h->{feed} // 'default';
|
||
|
||
$data{$name}{inverters}{$in}{igeneration} = $pv; # Hilfshash Wert current generation, Forum: https://forum.fhem.de/index.php/topic,117864.msg1139251.html#msg1139251
|
||
$data{$name}{inverters}{$in}{ietotal} = $etotal; # aktuellen etotal des WR speichern
|
||
$data{$name}{inverters}{$in}{iname} = $indev; # Name des Inverterdevices
|
||
$data{$name}{inverters}{$in}{ialias} = AttrVal ($indev, 'alias', $indev); # Alias Inverter
|
||
$data{$name}{inverters}{$in}{invertercap} = $h->{capacity} if(defined $h->{capacity}); # optionale Angabe max. WR-Leistung
|
||
$data{$name}{inverters}{$in}{ilimit} = $h->{limit} // 100; # Wirkleistungsbegrenzung
|
||
$data{$name}{inverters}{$in}{iicon} = $h->{icon} if($h->{icon}); # Icon des Inverters
|
||
$data{$name}{inverters}{$in}{istrings} = $h->{strings} if($h->{strings}); # dem Inverter zugeordnete Strings
|
||
$data{$name}{inverters}{$in}{iasynchron} = $h->{asynchron} if($h->{asynchron}); # Inverter Mode
|
||
$data{$name}{inverters}{$in}{ifeed} = $feed; # Eigenschaften der Energielieferung
|
||
|
||
$pvsum += $pv;
|
||
$ethishoursum += $ethishour;
|
||
|
||
writeToHistory ( { paref => $paref, key => 'pvrl'.$in, val => $ethishour, hour => $nhour } );
|
||
|
||
debugLog ($paref, "collectData", "collect Inverter $in data - device: $indev, delivery: $feed =>");
|
||
debugLog ($paref, "collectData", "pv: $pv W, etotal: $etotal Wh");
|
||
}
|
||
|
||
storeReading ('Current_PV', $pvsum.' W');
|
||
storeReading ('Today_Hour'.sprintf("%02d",$nhour).'_PVreal', $ethishoursum.' Wh'.$warn);
|
||
|
||
$data{$name}{circular}{sprintf("%02d",$nhour)}{pvrl} = $ethishoursum; # Ringspeicher PV real Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350
|
||
|
||
push @{$data{$name}{current}{genslidereg}}, $pvsum; # Schieberegister PV Erzeugung
|
||
limitArray ($data{$name}{current}{genslidereg}, SLIDENUMMAX);
|
||
|
||
writeToHistory ( { paref => $paref, key => 'pvrl', val => $ethishoursum, hour => $nhour, valid => $aln } ); # valid=1: beim Learning berücksichtigen, 0: nicht
|
||
|
||
debugLog ($paref, "collectData", "summary data of all Inverters - pv: $pvsum W, this hour Generation: $ethishoursum Wh");
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Strahlungsvorhersage Werte aus solcastapi-Hash
|
||
# übertragen und PV Vorhersage berechnen / in Nexthours
|
||
# speichern
|
||
################################################################
|
||
sub _transferAPIRadiationValues {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $t = $paref->{t}; # Epoche Zeit
|
||
my $chour = $paref->{chour};
|
||
my $date = $paref->{date};
|
||
my $hash = $defs{$name};
|
||
|
||
return if(!keys %{$data{$name}{solcastapi}});
|
||
|
||
my @strings = sort keys %{$data{$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{$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, $wthour) = $wantdt =~ /(\d{2})\s(\d{2}):/xs;
|
||
my $hod = sprintf "%02d", int $wthour + 1; # Stunde des Tages
|
||
my $rad1h = RadiationAPIVal ($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{$name}{nexthours}{$nhtstr}{starttime} = $wantdt;
|
||
$data{$name}{nexthours}{$nhtstr}{hourofday} = $hod;
|
||
$data{$name}{nexthours}{$nhtstr}{today} = $fd == 0 ? 1 : 0;
|
||
$data{$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{$name}{nexthours}{$nhtstr}{sunaz} = $sunaz;
|
||
}
|
||
else {
|
||
$sunaz = NexthoursVal ($hash, $nhtstr, 'sunaz', 0);
|
||
}
|
||
|
||
if (defined $sunalt) {
|
||
$data{$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{$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 $airn = CircularVal ($hash, 99, 'aiRulesNumber', 0);
|
||
my $aivar = 100;
|
||
$aivar = sprintf "%.0f", (100 * $pvaifc / $est) if($est); # Übereinstimmungsgrad KI Forecast zu API Forecast in %
|
||
|
||
if ($msg eq 'accurate') { # KI liefert 'accurate' Treffer -> verwenden
|
||
if ($airn >= AIACCTRNMIN || ($aivar >= AIACCLOWLIM && $aivar <= AIACCUPLIM)) {
|
||
$data{$name}{nexthours}{$nhtstr}{aihit} = 1;
|
||
$pvfc = $pvaifc;
|
||
$useai = 1;
|
||
|
||
debugLog ($paref, 'aiData', qq{AI Hit - accurate result used -> aiRulesNum: $airn, variance: $aivar, hod: $hod, Rad1h: $rad1h, pvfc: $pvfc Wh});
|
||
}
|
||
}
|
||
elsif ($msg eq 'spreaded') { # Abweichung AI von Standardvorhersage begrenzen
|
||
if ($airn >= AISPREADTRNMIN || ($aivar >= AISPREADLOWLIM && $aivar <= AISPREADUPLIM)) {
|
||
$data{$name}{nexthours}{$nhtstr}{aihit} = 1;
|
||
$pvfc = $pvaifc;
|
||
$useai = 1;
|
||
|
||
debugLog ($paref, 'aiData', qq{AI Hit - spreaded result used -> aiRulesNum: $airn, hod: $hod, Rad1h: $rad1h, pvfc: $pvfc Wh});
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
debugLog ($paref, 'aiData', $msg);
|
||
}
|
||
|
||
if ($useai) {
|
||
$data{$name}{nexthours}{$nhtstr}{pvaifc} = $pvaifc; # durch AI gelieferte PV Forecast
|
||
}
|
||
else {
|
||
delete $data{$name}{nexthours}{$nhtstr}{pvaifc};
|
||
$data{$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{$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{$name}{circular}{sprintf "%02d",$fh1}{pvapifc} = NexthoursVal ($hash, $nhtstr, 'pvapifc', undef);
|
||
$data{$name}{circular}{sprintf "%02d",$fh1}{pvfc} = $pvfc;
|
||
$data{$name}{circular}{sprintf "%02d",$fh1}{pvaifc} = NexthoursVal ($hash, $nhtstr, 'pvaifc', undef);
|
||
$data{$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) {
|
||
writeToHistory ( { paref => $paref, key => 'pvfc', val => $pvfc, hour => $fh1 } );
|
||
writeToHistory ( { paref => $paref, key => 'radiation', val => $rad1h, hour => $fh1 } );
|
||
}
|
||
}
|
||
|
||
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 $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 $hash = $defs{$name};
|
||
|
||
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{$name}{nexthours}{$nhtstr}{sunaz} = $az;
|
||
$data{$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
|
||
writeToHistory ( { paref => $paref, key => 'sunaz', val => $az, hour => $hodn } );
|
||
writeToHistory ( { paref => $paref, key => 'sunalt', val => $alt, hour => $hodn } );
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
#########################################################################
|
||
# API Erzeugungsprognose mit gewählter Autokorrekturmethode anpassen
|
||
#########################################################################
|
||
sub __calcPVestimates {
|
||
my $paref = shift;
|
||
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 $hash = $defs{$name};
|
||
my $reld = $fd == 0 ? "today" : $fd == 1 ? "tomorrow" : "unknown";
|
||
my $rr1c = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "rr1c", 0); # Gesamtniederschlag während der letzten Stunde kg/m2
|
||
my $wcc = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "wcc", 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->{wcc} = $wcc;
|
||
my ($hc, $hq) = ___readCandQ ($paref); # liest den anzuwendenden Korrekturfaktor
|
||
delete $paref->{wcc};
|
||
|
||
my ($lh,$sq,$peakloss, $modtemp);
|
||
my $pvsum = 0;
|
||
my $peaksum = 0;
|
||
|
||
for my $string (sort keys %{$data{$name}{strings}}) {
|
||
my $peak = StringVal ($hash, $string, 'peak', 0); # String Peak (kWp)
|
||
|
||
if ($acu =~ /on_complex/xs) {
|
||
$paref->{peak} = $peak;
|
||
$paref->{wcc} = $wcc;
|
||
$paref->{temp} = $temp;
|
||
|
||
($peakloss, $modtemp) = ___calcPeaklossByTemp ($paref); # Reduktion Peakleistung durch Temperaturkoeffizienten der Module (vorzeichengehaftet)
|
||
$peak += $peakloss;
|
||
|
||
delete $paref->{peak};
|
||
delete $paref->{wcc};
|
||
delete $paref->{temp};
|
||
}
|
||
|
||
$peak *= 1000;
|
||
my $est = RadiationAPIVal ($hash, $string, $wantdt, 'pv_estimate50', 0);
|
||
my $pv = sprintf "%.1f", ($est * $hc); # Korrekturfaktor anwenden
|
||
|
||
my $invcap = 0;
|
||
|
||
for my $in (keys %{$data{$name}{inverters}}) {
|
||
my $istrings = InverterVal ($hash, $in, 'istrings', ''); # dem Inverter zugeordnete Strings
|
||
next if(!grep /^$string$/, (split ',', $istrings));
|
||
|
||
$invcap = InverterVal ($hash, $in, 'invertercap', 0); # Max. Leistung des Inverters
|
||
|
||
last;
|
||
}
|
||
|
||
if ($invcap && $pv > $invcap) {
|
||
$pv = $invcap; # PV Vorhersage auf WR Kapazität begrenzen
|
||
|
||
debugLog ($paref, "radiationProcess", "PV forecast start time $wantdt limited to $pv Wh due to inverter capacity");
|
||
}
|
||
|
||
if ($debug =~ /radiationProcess/xs) {
|
||
$lh = { # Log-Hash zur Ausgabe
|
||
"String Peak" => $peak. " W",
|
||
"Estimated PV generation (raw)" => $est. " Wh",
|
||
"Estimated PV generation (calc)" => $pv. " Wh",
|
||
"PV correction factor" => $hc,
|
||
"PV correction quality" => $hq,
|
||
};
|
||
|
||
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{$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
|
||
$pvsum = sprintf "%.0f", $pvsum;
|
||
|
||
if ($debug =~ /radiationProcess/xs) {
|
||
$lh = { # Log-Hash zur Ausgabe
|
||
"Starttime" => $wantdt,
|
||
"Forecasted temperature" => $temp." °C",
|
||
"Cloudcover" => $wcc,
|
||
"Total Rain last hour" => $rr1c." kg/m2",
|
||
"PV Correction mode" => ($acu ? $acu : 'no'),
|
||
"PV generation forecast" => $pvsum." Wh",
|
||
};
|
||
|
||
$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 aus pvCircular
|
||
# 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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $num = $paref->{num};
|
||
my $fh1 = $paref->{fh1};
|
||
my $fd = $paref->{fd};
|
||
my $wcc = $paref->{wcc};
|
||
my $sabin = $paref->{sabin};
|
||
my $hash = $defs{$name};
|
||
|
||
my ($acu, $aln) = isAutoCorrUsed ($name); # Autokorrekturmodus
|
||
my $sunalt = NexthoursVal ($hash, "NextHour".sprintf("%02d",$num), 'sunalt', undef); # Sun Altitude
|
||
my $hcraw = ReadingsNum ($name, 'pvCorrectionFactor_'.sprintf("%02d",$fh1), 1.00); # Voreinstellung RAW-Korrekturfaktor (evtl. manuell gesetzt)
|
||
my $cpcf = ReadingsVal ($name, 'pvCorrectionFactor_'.sprintf("%02d",$fh1), ''); # aktuelles pcf-Reading
|
||
my $hq = '-'; # keine Qualität definiert
|
||
my $crang = 'simple';
|
||
my $hc;
|
||
|
||
delete $data{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}{cloudrange};
|
||
|
||
if ($acu =~ /on_complex/xs) { # Autokorrektur complex soll genutzt werden
|
||
$crang = cloud2bin ($wcc); # Range errechnen
|
||
($hc, $hq) = CircularSunCloudkorrVal ($hash, sprintf("%02d",$fh1), $sabin, $crang, undef); # Korrekturfaktor/Qualität der Stunde des Tages (complex)
|
||
|
||
$data{$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)
|
||
}
|
||
else { # keine Autokorrektur
|
||
($hc, $hq) = CircularSunCloudkorrVal ($hash, sprintf("%02d",$fh1), $sabin, 'simple', undef); # Korrekturfaktor/Qualität der Stunde des Tages (simple)
|
||
$hc = 1;
|
||
}
|
||
|
||
$hq //= '-'; # keine Qualität definiert
|
||
$hq = sprintf "%.2f", $hq if(isNumeric ($hq));
|
||
$hc //= $hcraw; # Korrekturfaktor Voreinstellung
|
||
$hc = 1 if(1 * $hc == 0); # 0.0-Werte ignorieren (Schleifengefahr)
|
||
$hc = sprintf "%.2f", $hc;
|
||
|
||
if ($cpcf =~ /manual\sfix/xs) { # Voreinstellung pcf-Reading verwenden wenn 'manual fix'
|
||
$hc = $hcraw;
|
||
debugLog ($paref, 'pvCorrectionRead', "use 'manual fix' - fd: $fd, hod: ".sprintf("%02d",$fh1).", corrf: $hc, quality: $hq");
|
||
}
|
||
else {
|
||
my $flex = $cpcf =~ /manual\sflex/xs ? "use 'manual flex'" : 'read parameters';
|
||
debugLog ($paref, 'pvCorrectionRead', "$flex - fd: $fd, hod: ".sprintf("%02d",$fh1).", Sun Altitude Bin: $sabin, Cloud range: $crang, corrf: $hc, quality: $hq");
|
||
}
|
||
|
||
$data{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}{pvcorrf} = $hc."/".$hq;
|
||
|
||
if ($fd == 0 && $fh1) {
|
||
writeToHistory ( { paref => $paref, key => 'pvcorrfactor', val => $hc.'/'.$hq, hour => $fh1 } );
|
||
}
|
||
|
||
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 $name = $paref->{name};
|
||
my $peak = $paref->{peak} // return (0,0);
|
||
my $wcc = $paref->{wcc} // return (0,0); # vorhergesagte Wolkendecke Stunde X
|
||
my $temp = $paref->{temp} // return (0,0); # vorhergesagte Temperatur Stunde X
|
||
|
||
my $modtemp = $temp + (TEMPMODINC * (1 - ($wcc/100))); # kalkulierte Modultemperatur
|
||
my $peakloss = sprintf "%.2f", TEMPCOEFFDEF * ($modtemp - TEMPBASEDEF) * $peak / 100;
|
||
|
||
return ($peakloss, $modtemp);
|
||
}
|
||
|
||
################################################################
|
||
# den Maximalwert PV Vorhersage für Heute ermitteln
|
||
################################################################
|
||
sub _calcMaxEstimateToday {
|
||
my $paref = shift;
|
||
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 anderer Erzeuger ermitteln und übertragen
|
||
################################################################
|
||
sub _transferProducerValues {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $t = $paref->{t}; # aktuelle Unix-Zeit
|
||
my $chour = $paref->{chour};
|
||
my $day = $paref->{day};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
for my $pn (1..MAXPRODUCER) {
|
||
$pn = sprintf "%02d", $pn;
|
||
my ($err, $prdev, $h) = isDeviceValid ( { name => $name, obj => 'setupOtherProducer'.$pn, method => 'attr' } );
|
||
next if($err);
|
||
|
||
my $type = $paref->{type};
|
||
|
||
my ($pcread, $pcunit) = split ":", $h->{pcurr}; # Readingname/Unit für aktuelle Erzeugung
|
||
my ($edread, $etunit) = split ":", $h->{etotal}; # Readingname/Unit für Energie total (Erzeugung)
|
||
|
||
next if(!$pcread || !$edread);
|
||
|
||
my $pu = $pcunit =~ /^kW$/xi ? 1000 : 1;
|
||
my $p = ReadingsNum ($prdev, $pcread, 0) * $pu; # aktuelle Erzeugung (W)
|
||
$p = $p < 0 ? 0 : $p;
|
||
|
||
my $etu = $etunit =~ /^kWh$/xi ? 1000 : 1;
|
||
my $etotal = ReadingsNum ($prdev, $edread, 0) * $etu; # Erzeugung total (Wh)
|
||
|
||
my $nhour = $chour + 1;
|
||
my $histetot = HistoryVal ($hash, $day, sprintf("%02d",$nhour), 'etotalp'.$pn, 0); # etotal zu Beginn einer Stunde
|
||
my $warn = '';
|
||
|
||
my ($ethishour, $etotsvd);
|
||
|
||
if (!$histetot) { # etotal der aktuelle Stunde gesetzt ?
|
||
writeToHistory ( { paref => $paref, key => 'etotalp'.$pn, val => $etotal, hour => $nhour } );
|
||
|
||
$etotsvd = ProducerVal ($hash, $pn, 'petotal', $etotal);
|
||
$ethishour = int ($etotal - $etotsvd);
|
||
}
|
||
else {
|
||
$ethishour = int ($etotal - $histetot);
|
||
}
|
||
|
||
$data{$name}{producers}{$pn}{pgeneration} = $p;
|
||
$data{$name}{producers}{$pn}{petotal} = $etotal; # aktuellen etotal des WR speichern
|
||
$data{$name}{producers}{$pn}{pname} = $prdev; # Name des Producerdevices
|
||
$data{$name}{producers}{$pn}{palias} = AttrVal ($prdev, 'alias', $prdev); # Alias Producer
|
||
$data{$name}{producers}{$pn}{picon} = $h->{icon} if($h->{icon}); # Icon des Producers
|
||
$data{$name}{producers}{$pn}{pfeed} = 'default'; # Eigenschaften der Energielieferung
|
||
|
||
if ($ethishour < 0) {
|
||
$ethishour = 0;
|
||
my $vl = 3;
|
||
my $pre = '- WARNING -';
|
||
|
||
if ($paref->{debug} =~ /collectData/xs) {
|
||
$vl = 1;
|
||
$pre = 'DEBUG> - WARNING -';
|
||
}
|
||
|
||
Log3 ($name, $vl, "$name $pre The Total Energy of Producer$pn '$prdev' is lower than the value saved before. This situation is unexpected and the Energy generated of current hour is set to '0'.");
|
||
$warn = ' (WARNING $prdev invalid real produced energy occured - see Logfile)';
|
||
}
|
||
|
||
storeReading ('Current_PP_'.$pn, sprintf("%.1f", $p).' W');
|
||
storeReading ('Today_Hour'.sprintf("%02d",$nhour).'_PPreal_'.$pn, $ethishour.' Wh'.$warn);
|
||
|
||
$data{$name}{circular}{sprintf("%02d",$nhour)}{'pprl'.$pn} = $ethishour; # Ringspeicher P real
|
||
|
||
writeToHistory ( { paref => $paref, key => 'pprl'.$pn, val => $ethishour, hour => $nhour } );
|
||
|
||
debugLog ($paref, "collectData", "collect Producer $pn data - device: $prdev =>");
|
||
debugLog ($paref, "collectData", "pcurr: $p W, etotal: $etotal Wh");
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Werte Meter Device ermitteln und übertragen
|
||
################################################################
|
||
sub _transferMeterValues {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $t = $paref->{t};
|
||
my $chour = $paref->{chour};
|
||
|
||
my $hash = $defs{$name};
|
||
my ($err, $medev, $h) = isDeviceValid ( { name => $name, obj => 'setupMeterDev', method => 'attr' } );
|
||
return if($err);
|
||
|
||
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);
|
||
|
||
my $nhour = $chour + 1;
|
||
|
||
if ($h->{conprice}) { # Bezugspreis (Arbeitspreis) pro kWh
|
||
my @acp = split ":", $h->{conprice};
|
||
|
||
if (scalar(@acp) == 3) {
|
||
$data{$name}{current}{ePurchasePrice} = ReadingsNum ($acp[0], $acp[1], 0);
|
||
$data{$name}{current}{ePurchasePriceCcy} = $acp[2];
|
||
}
|
||
elsif (scalar(@acp) == 2) {
|
||
if (isNumeric($acp[0])) {
|
||
$data{$name}{current}{ePurchasePrice} = $acp[0];
|
||
$data{$name}{current}{ePurchasePriceCcy} = $acp[1];
|
||
}
|
||
else {
|
||
$data{$name}{current}{ePurchasePrice} = ReadingsNum ($medev, $acp[0], 0);
|
||
$data{$name}{current}{ePurchasePriceCcy} = $acp[1];
|
||
}
|
||
}
|
||
|
||
writeToHistory ( { paref => $paref, # Bezugspreis in pvHistory speichern
|
||
key => 'conprice',
|
||
val => CurrentVal ($hash, 'ePurchasePrice', 0),
|
||
hour => $nhour
|
||
}
|
||
);
|
||
}
|
||
|
||
if ($h->{feedprice}) { # Einspeisevergütung pro kWh
|
||
my @afp = split ":", $h->{feedprice};
|
||
|
||
if (scalar(@afp) == 3) {
|
||
$data{$name}{current}{eFeedInTariff} = ReadingsNum ($afp[0], $afp[1], 0);
|
||
$data{$name}{current}{eFeedInTariffCcy} = $afp[2];
|
||
}
|
||
elsif (scalar(@afp) == 2) {
|
||
if (isNumeric($afp[0])) {
|
||
$data{$name}{current}{eFeedInTariff} = $afp[0];
|
||
$data{$name}{current}{eFeedInTariffCcy} = $afp[1];
|
||
}
|
||
else {
|
||
$data{$name}{current}{eFeedInTariff} = ReadingsNum ($medev, $afp[0], 0);
|
||
$data{$name}{current}{eFeedInTariffCcy} = $afp[1];
|
||
}
|
||
}
|
||
|
||
writeToHistory ( { paref => $paref, # Einspeisevergütung in pvHistory speichern
|
||
key => 'feedprice',
|
||
val => CurrentVal ($hash, 'eFeedInTariff', 0),
|
||
hour => $nhour
|
||
}
|
||
);
|
||
}
|
||
|
||
$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);
|
||
}
|
||
|
||
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{$name}{circular}{99}{gridcontotal} = $gctotal; # Total Netzbezug speichern
|
||
$data{$name}{circular}{99}{feedintotal} = $fitotal; # Total Feedin speichern
|
||
$data{$name}{current}{gridconsumption} = int $gco; # Current grid consumption Forum: https://forum.fhem.de/index.php/topic,117864.msg1139251.html#msg1139251
|
||
$data{$name}{current}{gridfeedin} = int $gfin; # Wert current grid Feed in
|
||
|
||
debugLog ($paref, "collectData", "collect Meter data - device: $medev =>");
|
||
debugLog ($paref, "collectData", "gcon: $gco W, gfeedin: $gfin W, contotal: $gctotal Wh, feedtotal: $fitotal Wh");
|
||
|
||
|
||
## Management aus dem Netz bezogener Energie
|
||
##############################################
|
||
my $gcdaypast = 0;
|
||
my $gfdaypast = 0;
|
||
my $docon = 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 $idgcon = CircularVal ($hash, 99, 'initdaygcon', undef);
|
||
|
||
if (!$gctotal) {
|
||
$data{$name}{circular}{99}{initdaygcon} = 0;
|
||
Log3 ($name, 3, "$name - WARNING - '$medev' - the total energy drawn from grid was reset and is registered with >0<.");
|
||
}
|
||
elsif ($gcdaypast == 0) { # Management der Stundenberechnung auf Basis Totalwerte GridConsumtion
|
||
if (defined $idgcon) {
|
||
$docon = 1;
|
||
}
|
||
else {
|
||
$data{$name}{circular}{99}{initdaygcon} = $gctotal;
|
||
}
|
||
}
|
||
elsif (!defined $idgcon) {
|
||
$data{$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;
|
||
}
|
||
|
||
storeReading ('Today_Hour'.sprintf("%02d",$nhour).'_GridConsumption', $gctotthishour.' Wh');
|
||
$data{$name}{circular}{sprintf("%02d",$nhour)}{gcons} = $gctotthishour; # Hilfshash Wert Bezug (Wh) Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350
|
||
|
||
writeToHistory ( { paref => $paref, key => 'gcons', val => $gctotthishour, hour => $nhour } );
|
||
}
|
||
|
||
## Management der in das Netz eingespeister (nur vom Meter gemessene) Energie
|
||
###############################################################################
|
||
my $dofeed = 0;
|
||
my $idfin = CircularVal ($hash, 99, 'initdayfeedin', undef);
|
||
|
||
if (!$fitotal) {
|
||
$data{$name}{circular}{99}{initdayfeedin} = 0;
|
||
Log3 ($name, 3, "$name - WARNING - '$medev' - the total energy feed in to grid was reset and is registered with >0<.");
|
||
}
|
||
elsif ($gfdaypast == 0) { # Management der Stundenberechnung auf Basis Totalwerte GridFeedIn
|
||
if (defined $idfin) {
|
||
$dofeed = 1;
|
||
}
|
||
else {
|
||
$data{$name}{circular}{99}{initdayfeedin} = $fitotal;
|
||
}
|
||
}
|
||
elsif (!defined $idfin) {
|
||
$data{$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;
|
||
}
|
||
|
||
storeReading ('Today_Hour'.sprintf("%02d",$nhour).'_GridFeedIn', $gftotthishour.' Wh');
|
||
$data{$name}{circular}{sprintf("%02d",$nhour)}{gfeedin} = $gftotthishour;
|
||
|
||
writeToHistory ( { paref => $paref, key => 'gfeedin', val => $gftotthishour, hour => $nhour } );
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Batteriewerte sammeln
|
||
################################################################
|
||
sub _transferBatteryValues {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $chour = $paref->{chour};
|
||
my $day = $paref->{day};
|
||
|
||
my $hash = $defs{$name};
|
||
my $num = 0;
|
||
my $pbisum = 0;
|
||
my $pbosum = 0;
|
||
my $bcapsum = 0;
|
||
my $socsum;
|
||
my $socwhsum;
|
||
|
||
delete $data{$name}{current}{batpowerinsum};
|
||
delete $data{$name}{current}{batpoweroutsum};
|
||
delete $data{$name}{current}{batcapsum};
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
|
||
my ($err, $badev, $h) = isDeviceValid ( { name => $name, obj => 'setupBatteryDev'.$bn, method => 'attr' } );
|
||
next if($err);
|
||
|
||
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 = int (ReadingsNum ($badev, $pou, 0) * $pouf); # aktuelle Batterieentladung (W)
|
||
my $pbi = int (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) {
|
||
if (!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);
|
||
}
|
||
|
||
$bcapsum += $instcap; # Summe installierte Bat Kapazität
|
||
$data{$name}{batteries}{$bn}{binstcap} = $instcap; # Summe installierte Batterie Kapazität
|
||
}
|
||
else {
|
||
delete $data{$name}{batteries}{$bn}{binstcap};
|
||
}
|
||
|
||
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, -entladeenergie in Circular speichern
|
||
#######################################################
|
||
if (!defined CircularVal ($hash, 99, 'initdaybatintot'.$bn, undef)) {
|
||
$data{$name}{circular}{99}{'initdaybatintot'.$bn} = $btotin; # total Batterieladung zu Tagbeginn (Wh)
|
||
}
|
||
|
||
if (!defined CircularVal ($hash, 99, 'initdaybatouttot'.$bn, undef)) { # total Batterieentladung zu Tagbeginn (Wh)
|
||
$data{$name}{circular}{99}{'initdaybatouttot'.$bn} = $btotout;
|
||
}
|
||
|
||
$data{$name}{circular}{99}{'batintot'.$bn} = $btotin; # aktuell total Batterieladung (Wh)
|
||
$data{$name}{circular}{99}{'batouttot'.$bn} = $btotout; # aktuell total Batterieentladung (Wh)
|
||
|
||
my $nhour = $chour + 1;
|
||
|
||
# Batterieladung aktuelle Stunde in pvHistory speichern
|
||
#########################################################
|
||
my $histbatintot = HistoryVal ($hash, $day, sprintf("%02d",$nhour), 'batintotal'.$bn, undef); # totale Batterieladung zu Beginn einer Stunde
|
||
my $batinthishour;
|
||
|
||
if (!defined $histbatintot) { # totale Batterieladung der aktuelle Stunde gesetzt?
|
||
writeToHistory ( { paref => $paref, key => 'batintotal'.$bn, val => $btotin, hour => $nhour } );
|
||
$batinthishour = 0;
|
||
}
|
||
else {
|
||
$batinthishour = int ($btotin - $histbatintot);
|
||
}
|
||
|
||
$batinthishour = 0 if($batinthishour < 0);
|
||
$data{$name}{circular}{sprintf("%02d",$nhour)}{'batin'.$bn} = $batinthishour; # Ringspeicher Battery In Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350
|
||
|
||
writeToHistory ( { paref => $paref, key => 'batinthishour'.$bn, val => $batinthishour, hour => $nhour } );
|
||
|
||
# Batterieentladung aktuelle Stunde in pvHistory speichern
|
||
############################################################
|
||
my $histbatouttot = HistoryVal ($hash, $day, sprintf("%02d",$nhour), 'batouttotal'.$bn, undef); # totale Betterieladung zu Beginn einer Stunde
|
||
my $batoutthishour;
|
||
|
||
if (!defined $histbatouttot) { # totale Betterieladung der aktuelle Stunde gesetzt?
|
||
writeToHistory ( { paref => $paref, key => 'batouttotal'.$bn, val => $btotout, hour => $nhour } );
|
||
$batoutthishour = 0;
|
||
}
|
||
else {
|
||
$batoutthishour = int ($btotout - $histbatouttot);
|
||
}
|
||
|
||
$batoutthishour = 0 if($batoutthishour < 0);
|
||
$data{$name}{circular}{sprintf("%02d",$nhour)}{'batout'.$bn} = $batoutthishour; # Ringspeicher Battery In Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350
|
||
|
||
writeToHistory ( { paref => $paref, key => 'batoutthishour'.$bn, val => $batoutthishour, hour => $nhour } );
|
||
|
||
# täglichen max. SOC in pvHistory speichern
|
||
#############################################
|
||
my $batmaxsoc = HistoryVal ($hash, $day, 99, 'batmaxsoc'.$bn, 0); # gespeicherter max. SOC des Tages
|
||
|
||
if ($soc >= $batmaxsoc) {
|
||
writeToHistory ( { paref => $paref, key => 'batmaxsoc'.$bn, val => $soc, hour => 99 } );
|
||
}
|
||
|
||
######
|
||
|
||
storeReading ('Today_Hour'.sprintf("%02d",$nhour).'_BatIn_'. $bn, $batinthishour. ' Wh');
|
||
storeReading ('Today_Hour'.sprintf("%02d",$nhour).'_BatOut_'.$bn, $batoutthishour.' Wh');
|
||
storeReading ('Current_PowerBatIn_'. $bn, $pbi.' W');
|
||
storeReading ('Current_PowerBatOut_'.$bn, $pbo.' W');
|
||
storeReading ('Current_BatCharge_'. $bn, $soc.' %');
|
||
|
||
$data{$name}{batteries}{$bn}{bname} = $badev; # Batterie Devicename
|
||
$data{$name}{batteries}{$bn}{balias} = AttrVal ($badev, 'alias', $badev); # Alias Batterie Device
|
||
$data{$name}{batteries}{$bn}{bpowerin} = $pbi; # momentane Batterieladung
|
||
$data{$name}{batteries}{$bn}{bpowerout} = $pbo; # momentane Batterieentladung
|
||
$data{$name}{batteries}{$bn}{bcharge} = $soc; # Batterie SoC (%)
|
||
$data{$name}{batteries}{$bn}{basynchron} = $h->{asynchron} // 0; # asynchroner Modus = X
|
||
$data{$name}{batteries}{$bn}{bicon} = $h->{icon} if($h->{icon}); # Batterie Icon
|
||
$data{$name}{batteries}{$bn}{bshowingraph} = $h->{show} // 0; # Batterie in Balkengrafik anzeigen
|
||
$data{$name}{batteries}{$bn}{bchargewh} = BatteryVal ($name, $bn, 'binstcap', 0) * $soc / 100; # Batterie SoC (Wh)
|
||
|
||
writeToHistory ( { paref => $paref, key => 'batsoc'.$bn, val => $soc, hour => $nhour } );
|
||
|
||
$num++;
|
||
$socsum += $soc;
|
||
$socwhsum += BatteryVal ($name, $bn, 'binstcap', 0) * $soc / 100; # Batterie SoC in Wh
|
||
|
||
$pbisum += $pbi;
|
||
$pbosum += $pbo;
|
||
}
|
||
|
||
if ($num) {
|
||
my $soctotal = sprintf "%.0f", ($socwhsum / $bcapsum * 100) if($bcapsum); # resultierender SoC (%) aller Batterien als "eine"
|
||
push @{$data{$name}{current}{batsocslidereg}}, $soctotal; # Schieberegister average SOC aller Batterien
|
||
limitArray ($data{$name}{current}{batsocslidereg}, SLIDENUMMAX);
|
||
|
||
$data{$name}{current}{batpowerinsum} = $pbisum; # summarische laufende Batterieladung
|
||
$data{$name}{current}{batpoweroutsum} = $pbosum; # summarische laufende Batterieentladung
|
||
$data{$name}{current}{batcapsum} = $bcapsum; # Summe installierte Batterie Kapazität
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Batterie SOC optimalen Sollwert berechnen
|
||
################################################################
|
||
sub _batSocTarget {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $t = $paref->{t}; # aktuelle Zeit
|
||
my $debug = $paref->{debug};
|
||
|
||
return if(!isBatteryUsed ($name));
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
|
||
my ($err, $badev, $h) = isDeviceValid ( { name => $name, obj => 'setupBatteryDev'.$bn, method => 'attr' } );
|
||
next if($err);
|
||
|
||
my $oldd2care = CircularVal ($hash, 99, 'days2care'.$bn, 0);
|
||
my $ltsmsr = CircularVal ($hash, 99, 'lastTsMaxSocRchd'.$bn, undef);
|
||
my $soc = BatteryVal ($hash, $bn, 'bcharge', 0); # aktuelle Ladung in %
|
||
my $batinstcap = BatteryVal ($hash, $bn, 'binstcap', 0); # installierte Batteriekapazität Wh
|
||
my $cgbt = AttrVal ($name, 'ctrlBatSocManagement'.$bn, undef);
|
||
|
||
if ($cgbt && !$batinstcap) {
|
||
Log3 ($name, 1, "$name - WARNING - Attribute ctrlBatSocManagement${bn} is active, but the required key 'cap' is not setup in setupBatteryDev. Exit.");
|
||
return;
|
||
}
|
||
|
||
my ($lowSoc, $upSoc, $maxsoc, $careCycle) = __parseAttrBatSoc ($name, $cgbt);
|
||
return if(!$lowSoc ||!$upSoc);
|
||
|
||
$paref->{batnmb} = $bn;
|
||
$paref->{careCycle} = $careCycle;
|
||
|
||
__batSaveSocKeyFigures ($paref) if(!$ltsmsr || $soc >= $maxsoc || $soc >= MAXSOCDEF || $oldd2care < 0);
|
||
|
||
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 $tdconsset = CurrentVal ($hash, 'tdConFcTillSunset', 0); # Verbrauch bis Sonnenuntergang Wh
|
||
my $batymaxsoc = HistoryVal ($hash, $yday, 99, 'batmaxsoc'.$bn, 0); # gespeicherter max. SOC des Vortages
|
||
my $batysetsoc = HistoryVal ($hash, $yday, 99, 'batsetsoc'.$bn, $lowSoc); # gespeicherter SOC Sollwert des Vortages
|
||
|
||
$target = $batymaxsoc < $maxsoc ? $batysetsoc + BATSOCCHGDAY :
|
||
$batymaxsoc >= $maxsoc ? $batysetsoc - BATSOCCHGDAY :
|
||
$batysetsoc; # neuer Min SOC für den laufenden Tag
|
||
|
||
## erwartete PV ermitteln & Anteilsfaktor Bat an Gesamtbatteriekapazität anwenden
|
||
###################################################################################
|
||
my $pvfctm = ReadingsNum ($name, 'Tomorrow_PVforecast', 0); # PV Prognose morgen
|
||
my $pvfctd = ReadingsNum ($name, 'RestOfDayPVforecast', 0); # PV Prognose Rest heute
|
||
my $pvexpraw = $pvfctm > $pvfctd ? $pvfctm : $pvfctd - $tdconsset; # erwartete (Rest) PV-Leistung des Tages
|
||
$pvexpraw = $pvexpraw > 0 ? $pvexpraw : 0; # erwartete PV-Leistung inkl. Verbrauchsprognose bis Sonnenuntergang
|
||
|
||
my $sf = __batCapShareFactor ($hash, $bn); # Anteilsfaktor der Batterie XX Kapazität an Gesamtkapazität
|
||
my $pvexpect = $sf * $pvexpraw;
|
||
|
||
if ($debug eq 'batteryManagement') {
|
||
Log3 ($name, 1, "$name DEBUG> Bat $bn SoC Step1 - basics -> Battery share factor of total capacity: $sf");
|
||
Log3 ($name, 1, "$name DEBUG> Bat $bn SoC Step1 - basics -> Expected energy for charging raw: $pvexpraw Wh");
|
||
Log3 ($name, 1, "$name DEBUG> Bat $bn SoC Step1 - basics -> Expected energy for charging after application Share factor: $pvexpect Wh");
|
||
Log3 ($name, 1, "$name DEBUG> Bat $bn SoC Step1 - compare with SoC history -> preliminary 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 = '';
|
||
my $careSoc = $target;
|
||
|
||
my $ntsmsc = CircularVal ($hash, 99, 'nextTsMaxSocChge'.$bn, $t);
|
||
my $days2care = floor (($ntsmsc - $t) / 86400); # verbleibende Tage bis der Batterie Pflege-SoC (default 95%) erreicht sein soll
|
||
my $docare = 0; # keine Zwangsanwendung care SoC
|
||
|
||
my $whneed = ($maxsoc / 100 * $batinstcap) - ($soc / 100 * $batinstcap); # benötigte Ladeenergie in Wh bis $maxsoc
|
||
$whneed = $whneed < 0 ? 0 : $whneed;
|
||
$whneed = sprintf "%.0f", $whneed;
|
||
|
||
if ($t > $delayts || $pvexpect < $whneed || !$days2care) {
|
||
$paref->{days2care} = $days2care;
|
||
__batSaveSocKeyFigures ($paref);
|
||
delete $paref->{days2care};
|
||
|
||
$careSoc = $maxsoc - ($days2care * BATSOCCHGDAY); # Pflege-SoC um rechtzeitig den $maxsoc zu erreichen bei 5% Steigerung pro Tag
|
||
$careSoc = $careSoc < $lowSoc ? $lowSoc : $careSoc;
|
||
|
||
if ($careSoc >= $target) {
|
||
$target = $careSoc; # resultierender Target-SoC unter Berücksichtigung $caresoc
|
||
$docare = 1; # Zwangsanwendung care SoC
|
||
}
|
||
|
||
$la = "calc care SoC -> docare: $docare, care SoC: $careSoc %, Remaining days until care SoC: $days2care, Target: $target %";
|
||
}
|
||
else {
|
||
$nt = (timestampToTimestring ($delayts, $paref->{lang}))[0];
|
||
$la = "calc care SoC -> docare: $docare, care SoC: $careSoc %, use preliminary Target: $target % (care SoC calculation & activation postponed to after $nt)";
|
||
}
|
||
|
||
debugLog ($paref, 'batteryManagement', "Bat $bn SoC Step2 - basics -> Energy expected for charging: $pvexpect Wh, need until maxsoc: $whneed Wh");
|
||
debugLog ($paref, 'batteryManagement', "Bat $bn SoC Step2 - $la");
|
||
|
||
## Aufladewahrscheinlichkeit beachten
|
||
#######################################
|
||
my $csopt = ReadingsNum ($name, 'Battery_OptimumTargetSoC_'.$bn, $lowSoc); # aktuelles SoC Optimum
|
||
my $cantarget = sprintf "%.0f", (100 - (100 / $batinstcap) * $pvexpect); # berechneter max. möglicher Minimum-SOC nach Berücksichtigung Ladewahrscheinlichkeit
|
||
my $newtarget = sprintf "%.0f", ($cantarget < $target ? $cantarget : $target); # Abgleich möglicher Minimum-SOC gg. berechneten Minimum-SOC
|
||
|
||
debugLog ($paref, 'batteryManagement', "Bat $bn SoC Step3 - basics -> cantarget: $cantarget %, newtarget: $newtarget %");
|
||
|
||
if ($newtarget > $careSoc) {
|
||
$docare = 0; # keine Zwangsanwendung care SoC
|
||
}
|
||
else {
|
||
$newtarget = $careSoc;
|
||
}
|
||
|
||
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 && !$docare) { # bisheriges Optimum bleibt vorerst
|
||
$target = $csopt;
|
||
$nt = (timestampToTimestring ($delayts, $paref->{lang}))[0];
|
||
$logadd = "(new target $newtarget % is 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', "Bat $bn SoC Step3 - charging probability -> docare: $docare, Target: $target % ".$logadd);
|
||
|
||
## low/up-Grenzen beachten
|
||
############################
|
||
$target = $docare ? $target :
|
||
$target > $upSoc ? $upSoc :
|
||
$target < $lowSoc ? $lowSoc :
|
||
$target;
|
||
|
||
debugLog ($paref, 'batteryManagement', "Bat $bn SoC Step4 - basics -> docare: $docare, lowSoc: $lowSoc %, upSoc: $upSoc %");
|
||
debugLog ($paref, 'batteryManagement', "Bat $bn SoC 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', "Bat $bn SoC Step5 - rounding the SoC to steps of 5 % -> Target: $target %");
|
||
|
||
## Zwangsladeanforderung
|
||
##########################
|
||
if ($soc < $target) {
|
||
$chargereq = 1;
|
||
}
|
||
|
||
debugLog ($paref, 'batteryManagement', "Bat $bn SoC Step6 - force charging request: ".
|
||
($chargereq ? 'yes (battery charge is below minimum SoC)' : 'no (Battery is sufficiently charged)'));
|
||
|
||
## pvHistory/Readings schreiben
|
||
#################################
|
||
writeToHistory ( { paref => $paref, key => 'batsetsoc'.$bn, val => $target, hour => 99 } );
|
||
storeReading ('Battery_OptimumTargetSoC_'.$bn, $target.' %');
|
||
storeReading ('Battery_ChargeRequest_'.$bn, $chargereq);
|
||
|
||
delete $paref->{batnmb};
|
||
delete $paref->{careCycle};
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Parse ctrlBatSocManagementXX
|
||
################################################################
|
||
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 $bn = $paref->{batnmb}; # Batterienummer (01, 02, ...)
|
||
my $t = $paref->{t}; # aktuelle Zeit
|
||
my $careCycle = $paref->{careCycle};
|
||
|
||
if (defined $paref->{days2care}) {
|
||
$data{$name}{circular}{99}{'days2care'.$bn} = $paref->{days2care}; # verbleibende Tage bis zum Pflege-SoC erreicht werden soll
|
||
return;
|
||
}
|
||
|
||
$data{$name}{circular}{99}{'lastTsMaxSocRchd'.$bn} = $t; # Timestamp des letzten Erreichens von >= maxSoC
|
||
$data{$name}{circular}{99}{'nextTsMaxSocChge'.$bn} = $t + (86400 * $careCycle); # Timestamp bis zu dem die Batterie mindestens einmal maxSoC erreichen soll
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Anteilsfaktor der Batterie XX Kapazität an Gesamtkapazität
|
||
################################################################
|
||
sub __batCapShareFactor {
|
||
my $hash = shift;
|
||
my $bn = shift; # Batterienummer
|
||
|
||
my $binstcap = BatteryVal ($hash, $bn, 'binstcap', 1); # Kapazität der Batterie XX
|
||
my $batcapsum = CurrentVal ($hash, 'batcapsum', $binstcap); # Summe installierte Batterie Kapazität
|
||
|
||
my $sf = (100 * $binstcap / $batcapsum) / 100; # Anteilsfaktor der Batt XX Kapazität an Gesamtkapazität
|
||
|
||
return $sf;
|
||
}
|
||
|
||
################################################################
|
||
# Erstellung Batterie Ladefreigabe + SoC Prognose
|
||
################################################################
|
||
sub _batChargeRecmd {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $chour = $paref->{chour};
|
||
my $t = $paref->{t};
|
||
|
||
return if(!isBatteryUsed ($name));
|
||
|
||
my $hash = $defs{$name};
|
||
my $rodpvfc = ReadingsNum ($name, 'RestOfDayPVforecast', 0); # PV Prognose Rest des Tages
|
||
my $tompvfc = ReadingsNum ($name, 'Tomorrow_PVforecast', 0); # PV Prognose nächster Tag
|
||
my $confcss = CurrentVal ($hash, 'tdConFcTillSunset', 0); # Verbrauchsprognose bis Sonnenuntergang
|
||
my $tomconfc = ReadingsNum ($name, 'Tomorrow_ConsumptionForecast', 0); # Verbrauchsprognose nächster Tag
|
||
my $pvCu = ReadingsNum ($name, 'Current_PV', 0); # aktuelle PV Erzeugung
|
||
my $curcon = ReadingsNum ($name, 'Current_Consumption', 0); # aktueller Verbrauch
|
||
my $inplim = 0;
|
||
|
||
## Inverter Limits ermitteln
|
||
##############################
|
||
for my $in (1..MAXINVERTER) {
|
||
$in = sprintf "%02d", $in;
|
||
my $iname = InverterVal ($hash, $in, 'iname', '');
|
||
next if(!$iname);
|
||
|
||
my $feed = InverterVal ($hash, $in, 'ifeed', 'default');
|
||
next if($feed eq 'grid'); # Inverter 'Grid' ausschließen
|
||
|
||
my $icap = InverterVal ($hash, $in, 'invertercap', 0);
|
||
my $limit = InverterVal ($hash, $in, 'ilimit', 100); # Wirkleistungsbegrenzung (default keine Begrenzung)
|
||
my $aplim = $icap * $limit / 100;
|
||
$inplim += $aplim; # max. Leistung aller WR mit Berücksichtigung Wirkleistungsbegrenzung
|
||
|
||
debugLog ($paref, 'batteryManagement', "Bat XX Charge Rcmd - Inverter '$iname' cap: $icap W, Power limit: $limit % -> Pmax eff: $aplim W");
|
||
}
|
||
|
||
debugLog ($paref, 'batteryManagement', "Bat XX Charge Rcmd - Summary Power limit of all Inverter (except feed 'grid'): $inplim W");
|
||
|
||
## Schleife über alle Batterien
|
||
#################################
|
||
for my $bn (1..MAXBATTERIES) { # für jede Batterie
|
||
$bn = sprintf "%02d", $bn;
|
||
|
||
my ($err, $badev, $h) = isDeviceValid ( { name => $name, obj => 'setupBatteryDev'.$bn, method => 'attr' } );
|
||
next if($err);
|
||
|
||
my $batinstcap = BatteryVal ($hash, $bn, 'binstcap', 0); # installierte Batteriekapazität Wh
|
||
|
||
if (!$inplim || !$batinstcap) {
|
||
debugLog ($paref, 'batteryManagement', "WARNING - The requirements for dynamic battery charge recommendation are not met. Exit.");
|
||
return;
|
||
}
|
||
|
||
my $csoc = BatteryVal ($hash, $bn, 'bcharge', 0); # aktuelle Ladung in %
|
||
my $batoptsoc = ReadingsNum ($name, 'Battery_OptimumTargetSoC_'.$bn, 0); # aktueller optimierter SoC
|
||
my $cgbt = AttrVal ($name, 'ctrlBatSocManagement'.$bn, undef);
|
||
my $sf = __batCapShareFactor ($hash, $bn); # Anteilsfaktor der Batterie XX Kapazität an Gesamtkapazität
|
||
my $lowSoc = 0;
|
||
|
||
if ($cgbt) {
|
||
($lowSoc) = __parseAttrBatSoc ($name, $cgbt);
|
||
}
|
||
|
||
my $batoptsocwh = $batinstcap * $batoptsoc / 100; # optimaler SoC in Wh
|
||
my $lowSocwh = $batinstcap * $lowSoc / 100; # lowSoC in Wh
|
||
|
||
debugLog ($paref, 'batteryManagement', "Bat $bn Charge Rcmd - Installed Battery capacity: $batinstcap Wh, Percentage of total capacity: ".(sprintf "%.1f", $sf*100)." %");
|
||
debugLog ($paref, 'batteryManagement', "Bat $bn Charge Rcmd - The PV generation, consumption and surplus listed below are based on the battery's share of the total capacity!");
|
||
|
||
my $socwh = sprintf "%.0f", ($batinstcap * $csoc / 100); # aktueller SoC in Wh
|
||
|
||
## Auswertung für jede kommende Stunde
|
||
########################################
|
||
for my $num (0..47) {
|
||
my ($fd,$fh) = calcDayHourMove ($chour, $num);
|
||
next if($fd > 1);
|
||
|
||
my $nhr = sprintf "%02d", $num;
|
||
my $today = NexthoursVal ($hash, 'NextHour'.$nhr, 'today', 0);
|
||
my $confc = NexthoursVal ($hash, 'NextHour'.$nhr, 'confc', 0);
|
||
my $pvfc = NexthoursVal ($hash, 'NextHour'.$nhr, 'pvfc', 0);
|
||
my $stt = NexthoursVal ($hash, 'NextHour'.$nhr, 'starttime', '');
|
||
$stt = (split /[-:]/, $stt)[2] if($stt);
|
||
|
||
my $crel = 0; # Ladefreigabe 0 per Default
|
||
my $spday = 0;
|
||
|
||
## Aufteilung Energie auf Batterie XX im Verhältnis aller Bat
|
||
###############################################################
|
||
$pvfc = sprintf "%.0f", $sf * $pvfc;
|
||
$confcss = sprintf "%.0f", $sf * $confcss;
|
||
$confc = sprintf "%.0f", $sf * $confc;
|
||
$rodpvfc = sprintf "%.0f", $sf * $rodpvfc;
|
||
$tomconfc = sprintf "%.0f", $sf * $tomconfc;
|
||
$tompvfc = sprintf "%.0f", $sf * $tompvfc;
|
||
|
||
## (Rest) PV-Überschuß für den Tag
|
||
####################################
|
||
if ($pvfc) {
|
||
if ($today) { # heutiger Tag
|
||
$confcss -= $confc; # Verbrauch bis Sonnenuntergang - Verbrauch Fc aktuelle Stunde
|
||
$confcss = 0 if($confcss < 0);
|
||
$rodpvfc -= $pvfc;
|
||
$spday = $rodpvfc - $confcss; # PV-Überschußprognose (Rest) heutiger Tag
|
||
}
|
||
else { # nächster Tag
|
||
$tomconfc -= $confc;
|
||
$tomconfc = 0 if($tomconfc < 0);
|
||
$tompvfc -= $pvfc;
|
||
$spday = $tompvfc - $tomconfc;
|
||
}
|
||
}
|
||
|
||
$spday = 0 if($spday < 0); # PV Überschuß Prognose bis Sonnenuntergang
|
||
|
||
## Ladefreigabe
|
||
#################
|
||
my $whneed = $batinstcap - $socwh;
|
||
my $maxfctim = timestringToTimestamp (ReadingsVal ($name, 'Today_MaxPVforecastTime', '')) // $t;
|
||
|
||
my $sfmargin = $whneed * 0.25; # Sicherheitszuschlag: X% der benötigten Ladeenergie (Wh)
|
||
|
||
if ( $whneed + $sfmargin >= $spday || $t > $maxfctim) {$crel = 1} # Ladefreigabe wenn benötigte Ladeenergie >= Restüberschuß des Tages zzgl. Sicherheitsaufschlag
|
||
if ( !$num && ($pvCu - $curcon) >= $inplim ) {$crel = 1} # Ladefreigabe wenn akt. PV Leistung >= WR-Leistungsbegrenzung
|
||
|
||
## SOC-Prognose
|
||
#################
|
||
$socwh += $crel ? ($pvfc - $confc) * STOREFFDEF : -$confc / STOREFFDEF; # PV Prognose nur einbeziehen wenn Ladefreigabe
|
||
|
||
$socwh = $socwh < $lowSocwh ? $lowSocwh :
|
||
$socwh < $batoptsocwh ? $batoptsocwh : # SoC Prognose in Wh
|
||
$socwh > $batinstcap ? $batinstcap :
|
||
$socwh;
|
||
|
||
$socwh = sprintf "%.0f", $socwh;
|
||
my $progsoc = sprintf "%.1f", (100 * $socwh / $batinstcap); # Prognose SoC in %
|
||
|
||
__createNextHoursSFCReadings ( {name => $name,
|
||
nhr => $nhr,
|
||
bn => $bn,
|
||
progsoc => $progsoc
|
||
}
|
||
); # Readings NextHourXX_Bat_XX_ChargeForecast erstellen
|
||
|
||
my $msg = "(currsoc: $csoc %, SoCfc: $progsoc %, soc: $socwh Wh, pvfc: $pvfc, confc: $confc, Surp Day: $spday Wh, Curr PV: $pvCu W, Curr Consumption: $curcon W, Limit: $inplim W)";
|
||
|
||
if ($num) {
|
||
$msg = "(SoCfc: $progsoc %, soc: $socwh Wh, pvfc: $pvfc, confc: $confc, Surp Day: $spday Wh)";
|
||
}
|
||
else {
|
||
storeReading ('Battery_ChargeRecommended_'.$bn, $crel); # Reading nur für aktuelle Stunde
|
||
}
|
||
|
||
$data{$name}{nexthours}{'NextHour'.$nhr}{'rcdchargebat'.$bn} = $crel;
|
||
$data{$name}{nexthours}{'NextHour'.$nhr}{'soc'.$bn} = $progsoc;
|
||
|
||
debugLog ($paref, 'batteryManagement', "Bat $bn relLoad $stt -> $crel $msg");
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# zusätzliche Readings NextHourXX_Bat_XX_ChargeForecast
|
||
# erstellen
|
||
################################################################
|
||
sub __createNextHoursSFCReadings {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $nhr = $paref->{nhr}; # nächste Stunde
|
||
my $bn = $paref->{bn}; # Batterienummer
|
||
my $progsoc = $paref->{progsoc}; # prognostizierter SoC
|
||
|
||
my $hods = AttrVal ($name, 'ctrlNextHoursSoCForecastReadings', '');
|
||
|
||
return if(!$hods);
|
||
|
||
if (grep { /$nhr/x } split ',', $hods) {
|
||
storeReading ('Battery_NextHour'.$nhr.'_SoCforecast_'.$bn, $progsoc.' %');
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Zusammenfassungen erstellen
|
||
################################################################
|
||
sub _createSummaries {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $day = $paref->{day};
|
||
my $chour = $paref->{chour}; # aktuelle Stunde
|
||
my $minute = $paref->{minute}; # aktuelle Minute
|
||
|
||
my $hash = $defs{$name};
|
||
$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{$name}{current}{h4fcslidereg}}, int $next4HoursSum->{PV}; # Schieberegister 4h Summe Forecast
|
||
limitArray ($data{$name}{current}{h4fcslidereg}, SLIDENUMMAX);
|
||
|
||
my $gcon = CurrentVal ($hash, 'gridconsumption', 0); # aktueller Netzbezug
|
||
my $tconsum = CurrentVal ($hash, 'tomorrowconsumption', undef); # Verbrauchsprognose für folgenden Tag
|
||
my $gfeedin = CurrentVal ($hash, 'gridfeedin', 0);
|
||
|
||
my $batin = 0;
|
||
my $batout = 0;
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$batin += BatteryVal ($hash, $bn, 'bpowerin', 0); # Summe momentane Batterieladung
|
||
$batout += BatteryVal ($hash, $bn, 'bpowerout', 0); # Summe momentane Batterieentladung
|
||
}
|
||
|
||
my $pvgen = 0;
|
||
my $pv2grid = 0; # PV-Erzeugung zu Grid-only
|
||
|
||
for my $in (1..MAXINVERTER) { # Summe alle Inverter
|
||
$in = sprintf "%02d", $in;
|
||
my $pvi = InverterVal ($hash, $in, 'igeneration', 0);
|
||
my $feed = InverterVal ($hash, $in, 'ifeed', '');
|
||
$pvgen += $pvi;
|
||
$pv2grid += $pvi if($feed eq 'grid');
|
||
}
|
||
|
||
my $othprod = 0; # Summe Otherproducer
|
||
|
||
for my $pn (1..MAXPRODUCER) { # Erzeugung sonstiger Producer hinzufügen
|
||
$pn = sprintf "%02d", $pn;
|
||
$othprod += ProducerVal ($hash, $pn, 'pgeneration', 0);
|
||
}
|
||
|
||
my $consumption = int ($pvgen - $pv2grid + $othprod - $gfeedin + $gcon - $batin + $batout); # ohne PV2Grid
|
||
my $selfconsumption = int ($pvgen - $pv2grid - $gfeedin - $batin);
|
||
$selfconsumption = $selfconsumption < 0 ? 0 : $selfconsumption;
|
||
|
||
my $surplus = int ($pvgen - $pv2grid + $othprod - $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{$name}{current}{consumption} = $consumption;
|
||
$data{$name}{current}{selfconsumption} = $selfconsumption;
|
||
$data{$name}{current}{selfconsumptionrate} = $selfconsumptionrate;
|
||
$data{$name}{current}{autarkyrate} = $autarkyrate;
|
||
$data{$name}{current}{tdConFcTillSunset} = $tdConFcTillSunset;
|
||
$data{$name}{current}{surplus} = $surplus;
|
||
|
||
push @{$data{$name}{current}{surplusslidereg}}, $surplus; # Schieberegister PV Überschuß
|
||
limitArray ($data{$name}{current}{surplusslidereg}, SPLSLIDEMAX);
|
||
|
||
storeReading ('Current_GridFeedIn', (int $gfeedin). ' W'); # V 1.37.0
|
||
storeReading ('Current_GridConsumption', (int $gcon). ' W'); # V 1.37.0
|
||
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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $t = $paref->{t}; # aktuelle Zeit
|
||
my $chour = $paref->{chour};
|
||
my $day = $paref->{day};
|
||
|
||
my $hash = $defs{$name};
|
||
my $nhour = $chour + 1;
|
||
$paref->{nhour} = sprintf "%02d", $nhour;
|
||
|
||
for my $c (sort{$a<=>$b} keys %{$data{$name}{consumers}}) {
|
||
$paref->{consumer} = $c;
|
||
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{$name}{consumers}{$c}{old_etotal} = $etot;
|
||
$data{$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);
|
||
|
||
if ($consumerco < 0) { # V1.32.0
|
||
$consumerco = 0;
|
||
my $vl = 3;
|
||
my $pre = '- WARNING -';
|
||
|
||
if ($paref->{debug} =~ /consumption/xs) {
|
||
$vl = 1;
|
||
$pre = 'DEBUG> - WARNING -';
|
||
}
|
||
|
||
Log3 ($name, $vl, "$name $pre The calculated Energy consumption of >$consumer< is negative. This appears to be an error and the energy consumption of the consumer for the current hour is set to '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};
|
||
}
|
||
|
||
readingsDelete ($hash, "consumer${c}_currentPower") if(!$etotread && !$paread);
|
||
|
||
$paref->{pcurr} = $pcurr;
|
||
|
||
__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
|
||
__getCyclesAndRuntime ($paref); # Verbraucher - Laufzeit, Tagesstarts und Aktivminuten pro Stunde ermitteln
|
||
__reviewSwitchTime ($paref); # Planungsdaten überprüfen und ggf. neu planen
|
||
__remainConsumerTime ($paref); # Restlaufzeit Verbraucher ermitteln
|
||
|
||
delete $paref->{pcurr};
|
||
|
||
## Durchschnittsverbrauch / Betriebszeit ermitteln + speichern
|
||
################################################################
|
||
my $consumerco = 0;
|
||
my $runhours = 0;
|
||
my $dnum = 0;
|
||
|
||
for my $n (sort{$a<=>$b} keys %{$data{$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{$name}{consumers}{$c}{avgenergy} = sprintf "%.2f", ($consumerco / $runhours); # Durchschnittsverbrauch pro Stunde in Wh
|
||
}
|
||
else {
|
||
delete $data{$name}{consumers}{$c}{avgenergy};
|
||
}
|
||
|
||
$data{$name}{consumers}{$c}{runtimeAvgDay} = sprintf "%.2f", (($runhours / $dnum) * 60); # Durchschnittslaufzeit am Tag in Minuten
|
||
}
|
||
|
||
## Consumer Schaltstatus und Schaltzeit für Readings ermitteln
|
||
################################################################
|
||
my $costate = isConsumerPhysOn ($hash, $c) ? 'on' :
|
||
isConsumerPhysOff ($hash, $c) ? 'off' :
|
||
"unknown";
|
||
|
||
$data{$name}{consumers}{$c}{state} = $costate;
|
||
my ($pstate,$starttime,$stoptime,$supplmnt) = __getPlanningStateAndTimes ($paref);
|
||
my ($iilt,$rlt) = isInLocktime ($paref); # Sperrzeit Status ermitteln
|
||
my $mode = getConsumerPlanningMode ($hash, $c); # Planungsmode 'can' oder 'must'
|
||
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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $c = $paref->{consumer};
|
||
|
||
my $hash = $defs{$name};
|
||
my $consumer = ConsumerVal ($hash, $c, 'name', ''); # Name Consumer Device
|
||
my ($err) = isDeviceValid ( { name => $name,
|
||
obj => $consumer,
|
||
method => 'string',
|
||
}
|
||
);
|
||
return if($err);
|
||
|
||
my $dswitch = ConsumerVal ($hash, $c, 'dswitch', ''); # alternatives Schaltdevice
|
||
|
||
if ($dswitch) {
|
||
($err) = isDeviceValid ( { name => $name, obj => $dswitch, method => 'string' } );
|
||
return if($err);
|
||
}
|
||
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{$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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $c = $paref->{consumer};
|
||
|
||
my $hash = $defs{$name};
|
||
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{$name}{consumers}{$c}{epiecAVG};
|
||
delete $data{$name}{consumers}{$c}{epiecAVG_hours};
|
||
delete $data{$name}{consumers}{$c}{epiecStartEtotal};
|
||
delete $data{$name}{consumers}{$c}{epiecHist};
|
||
delete $data{$name}{consumers}{$c}{epiecHour};
|
||
|
||
for my $h (1..EPIECMAXCYCLES) {
|
||
delete $data{$name}{consumers}{$c}{"epiecHist_".$h};
|
||
delete $data{$name}{consumers}{$c}{"epiecHist_".$h."_hours"};
|
||
}
|
||
}
|
||
|
||
delete $data{$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{$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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $c = $paref->{consumer};
|
||
my $etot = $paref->{etot};
|
||
my $t = $paref->{t};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if (ConsumerVal ($hash, $c, "onoff", "off") eq "on") { # Status "Aus" verzögern um Pausen im Waschprogramm zu überbrücken
|
||
$data{$name}{consumers}{$c}{lastOnTime} = $t;
|
||
}
|
||
|
||
my $tsloff = defined $data{$name}{consumers}{$c}{lastOnTime} ?
|
||
$t - $data{$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{$name}{consumers}{$c}{epiecStartTime} = $t;
|
||
$data{$name}{consumers}{$c}{epiecHist} += 1;
|
||
$data{$name}{consumers}{$c}{epiecHist} = 1 if(ConsumerVal ($hash, $c, "epiecHist", 0) > EPIECMAXCYCLES);
|
||
|
||
$ecycle = "epiecHist_".ConsumerVal ($hash, $c, "epiecHist", 0);
|
||
|
||
delete $data{$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{$name}{consumers}{$c}{$ecycle}{$epiecHour_last} = sprintf '%.2f', ($etot - ConsumerVal ($hash, $c, "epiecStartEtotal", 0)) if($epiecHour > 1);
|
||
$data{$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{$name}{consumers}{$c}{$ecycle}{$epiecHour} = sprintf '%.2f', $ediff;
|
||
$data{$name}{consumers}{$c}{epiecHour} = $epiecHour;
|
||
$data{$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{$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{$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{$name}{consumers}{$c}{$ecycle}{$hour}) {
|
||
if ($data{$name}{consumers}{$c}{$ecycle}{$hour} > 5) {
|
||
$data{$name}{consumers}{$c}{epiecAVG}{$hour} += $data{$name}{consumers}{$c}{$ecycle}{$hour};
|
||
$hoursE += 1;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
my $eavg = defined $data{$name}{consumers}{$c}{epiecAVG}{$hour} ?
|
||
$data{$name}{consumers}{$c}{epiecAVG}{$hour} :
|
||
0;
|
||
|
||
my $ahval = sprintf '%.2f', $eavg / $hoursE; # Durchschnitt ermittelt und speichern
|
||
$data{$name}{consumers}{$c}{epiecAVG}{$hour} = $ahval;
|
||
|
||
debugLog ($paref, "epiecesCalc", qq{specificEpieces -> consumer "$c" - Average epiece of operating hour $hour: $ahval});
|
||
}
|
||
}
|
||
|
||
$data{$name}{consumers}{$c}{epiecHour} = -1; # epiecHour auf initialwert setzen für nächsten durchlauf
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
###################################################################
|
||
# Consumer Schaltzeiten planen
|
||
###################################################################
|
||
sub __planInitialSwitchTime {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $c = $paref->{consumer};
|
||
my $debug = $paref->{debug};
|
||
|
||
my $hash = $defs{$name};
|
||
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 $name = $paref->{name};
|
||
my $t = $paref->{t}; # aktuelle Zeit
|
||
my $c = $paref->{consumer};
|
||
|
||
my $hash = $defs{$name};
|
||
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 ($rapi, $wapi) = getStatusApiName ($hash);
|
||
my $tdc = StatusAPIVal ($hash, $rapi, '?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 $name = $paref->{name};
|
||
my $c = $paref->{consumer};
|
||
|
||
my $hash = $defs{$name};
|
||
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', ''));
|
||
|
||
$paref->{replan} = 1; # V 1.35.0
|
||
___doPlanning ($paref);
|
||
delete $paref->{replan};
|
||
}
|
||
}
|
||
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 $name = $paref->{name};
|
||
my $c = $paref->{consumer};
|
||
my $debug = $paref->{debug};
|
||
my $type = $paref->{type};
|
||
my $lang = $paref->{lang};
|
||
my $nh = $data{$name}{nexthours};
|
||
my $cicfip = AttrVal ($name, 'affectConsForecastInPlanning', 0); # soll Consumption Vorhersage in die Überschußermittlung eingehen ?
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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{$name}{consumers}{$c}{epieces}{1};
|
||
}
|
||
else {
|
||
return;
|
||
}
|
||
|
||
debugLog ($paref, "consumerPlanning", qq{consumer "$c" - epiece1: $epiece1});
|
||
|
||
my $mode = getConsumerPlanningMode ($hash, $c); # Planungsmode 'can' oder 'must'
|
||
my $calias = ConsumerVal ($hash, $c, 'alias', '');
|
||
my $mintime = ConsumerVal ($hash, $c, 'mintime', DEFMINTIME); # Einplanungsdauer
|
||
my $oldplanstate = ConsumerVal ($hash, $c, 'planstate', ''); # V. 1.35.0
|
||
|
||
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} = $paref->{replan} ? 'replanned:' : 'planned:'; # V 1.35.0
|
||
$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 && ($planstate ne $oldplanstate)) { # V 1.35.0
|
||
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 $name = $paref->{name};
|
||
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 $hash = $defs{$name};
|
||
my $p = 1;
|
||
delete $data{$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{$name}{consumers}{$c}{epieces}{$p} ?
|
||
$data{$name}{consumers}{$c}{epieces}{$p} :
|
||
0;
|
||
}
|
||
else {
|
||
last;
|
||
}
|
||
|
||
$chod = sprintf '%02d', $chod;
|
||
$data{$name}{consumers}{$c}{ehodpieces}{$chod} = sprintf '%.2f', $ep if($ep);
|
||
|
||
$p++;
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Planungsdaten bzw. aktuelle Planungszustände setzen
|
||
################################################################
|
||
sub ___setConsumerPlanningState {
|
||
my $paref = shift;
|
||
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{$name}{consumers}{$c}{planSupplement} = $supplmnt;
|
||
|
||
return if(!$ps);
|
||
|
||
my ($starttime,$stoptime);
|
||
|
||
if (defined $lonts) {
|
||
$data{$name}{consumers}{$c}{lastAutoOnTs} = $lonts;
|
||
}
|
||
|
||
if (defined $loffts) {
|
||
$data{$name}{consumers}{$c}{lastAutoOffTs} = $loffts;
|
||
}
|
||
|
||
if ($startts) {
|
||
$starttime = (timestampToTimestring ($startts, $lang))[3];
|
||
$data{$name}{consumers}{$c}{planswitchon} = $startts;
|
||
}
|
||
|
||
if ($stopts) {
|
||
$stoptime = (timestampToTimestring ($stopts, $lang))[3];
|
||
$data{$name}{consumers}{$c}{planswitchoff} = $stopts;
|
||
}
|
||
|
||
$ps .= " " if($starttime || $stoptime);
|
||
$ps .= $starttime if($starttime);
|
||
$ps .= $stoptime if(!$starttime && $stoptime);
|
||
$ps .= " - ".$stoptime if($starttime && $stoptime);
|
||
|
||
$data{$name}{consumers}{$c}{planstate} = $ps;
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Consumer Zeiten MUST planen
|
||
################################################################
|
||
sub ___planMust {
|
||
my $paref = shift;
|
||
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 $name = $paref->{name};
|
||
my $c = $paref->{consumer};
|
||
my $date = $paref->{date};
|
||
my $starttime = $paref->{starttime};
|
||
my $lang = $paref->{lang};
|
||
my $t = $paref->{t};
|
||
|
||
my $hash = $defs{$name};
|
||
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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $c = $paref->{consumer};
|
||
|
||
my $hash = $defs{$name};
|
||
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{$name}{consumers}{$c}{plandelete} = "specific";
|
||
}
|
||
else { # Planungsdaten Löschmethode jeden Tag in Stunde 0 (_specialActivities)
|
||
$data{$name}{consumers}{$c}{plandelete} = "regular";
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Timeframe Status ermitteln
|
||
################################################################
|
||
sub __setTimeframeState {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $c = $paref->{consumer};
|
||
my $t = $paref->{t}; # aktueller Unixtimestamp
|
||
|
||
my $hash = $defs{$name};
|
||
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{$name}{consumers}{$c}{isIntimeframe} = 1;
|
||
}
|
||
else {
|
||
$data{$name}{consumers}{$c}{isIntimeframe} = 0;
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Consumption Recommended Status setzen
|
||
################################################################
|
||
sub __setConsRcmdState {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $c = $paref->{consumer};
|
||
my $debug = $paref->{debug};
|
||
|
||
my $hash = $defs{$name};
|
||
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 ($method, $surplus) = determSurplus ($hash, $c); # Consumer spezifische Ermittlung des Energieüberschußes
|
||
|
||
if ($debug =~ /consumerSwitching${c}/x) {
|
||
Log3 ($name, 1, qq{$name DEBUG> ############### consumerSwitching consumer "$c" ###############});
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - ConsumptionRecommended calc method: $method, value: }.
|
||
(defined $surplus ? $surplus : 'undef'));
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - additional consumption after switching on (if currently 'off'): $rescons W});
|
||
}
|
||
|
||
my ($spignore, $info, $err) = isSurplusIgnoCond ($hash, $c, $debug); # PV Überschuß ignorieren?
|
||
Log3 ($name, 1, "$name - $err") if($err);
|
||
|
||
if (!defined $surplus) { # $surplus kann undef sein! -> dann bisherigen isConsumptionRecommended verwenden
|
||
$data{$name}{consumers}{$c}{isConsumptionRecommended} = ReadingsVal ($name, "consumer${c}_ConsumptionRecommended", 0);
|
||
}
|
||
elsif (!$nompower || $surplus - $rescons > 0 || $spignore) {
|
||
$data{$name}{consumers}{$c}{isConsumptionRecommended} = 1; # Einschalten des Consumers günstig bzw. Freigabe für "on" von Überschußseite erteilt
|
||
}
|
||
else {
|
||
$data{$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 $name = $paref->{name};
|
||
my $c = $paref->{consumer};
|
||
my $debug = $paref->{debug};
|
||
my $state = $paref->{state};
|
||
|
||
$paref->{fscss} = 1; # erster Subaufruf Consumer Schaltzustände ermitteln & setzen
|
||
$state = ___setConsumerSwitchingState ($paref);
|
||
delete $paref->{fscss};
|
||
|
||
$paref->{befsw} = 1; # Status vor Switching
|
||
__setPhysLogSwState ($paref); # physischen / logischen Schaltzustand festhalten
|
||
|
||
$state = ___switchConsumerOn ($paref); # Verbraucher Einschaltbedingung prüfen + auslösen
|
||
$state = ___switchConsumerOff ($paref); # Verbraucher Ausschaltbedingung prüfen + auslösen
|
||
$state = ___setConsumerSwitchingState ($paref); # Consumer Schaltzustände nach Switching ermitteln & setzen
|
||
|
||
$paref->{befsw} = 0; # Status nach Switching
|
||
__setPhysLogSwState ($paref); # physischen / logischen Schaltzustand festhalten
|
||
delete $paref->{befsw};
|
||
|
||
$paref->{state} = $state;
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Verbraucher einschalten
|
||
################################################################
|
||
sub ___switchConsumerOn {
|
||
my $paref = shift;
|
||
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 $hash = $defs{$name};
|
||
my ($err, $cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name
|
||
|
||
if ($err) {
|
||
$state = 'ERROR - '.$err;
|
||
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);
|
||
|
||
($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${c}/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> consumer "$c" - general switching parameters => }.
|
||
qq{auto mode: $auto, Current household 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" - in Context '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|continuing/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 = getConsumerPlanningMode ($hash, $c); # Planungsmode 'can' oder 'must'
|
||
my $enable = ___enableSwitchByBatPrioCharge ($paref); # Vorrangladung Batterie ?
|
||
|
||
debugLog ($paref, "consumerSwitching${c}", qq{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)
|
||
$state = qq{switching Consumer '$calias' to '$oncom', command: "set $dswname $oncom"};
|
||
|
||
if ($debug =~ /consumerSwitching${c}/x) {
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - send switch command now: "set $dswname $oncom"});
|
||
}
|
||
else {
|
||
Log3 ($name, 2, "$name - $state (Automatic = $auto)");
|
||
}
|
||
|
||
CommandSet (undef, "$dswname $oncom");
|
||
|
||
$paref->{ps} = "switching on:";
|
||
___setConsumerPlanningState ($paref);
|
||
delete $paref->{ps};
|
||
|
||
writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben
|
||
}
|
||
}
|
||
elsif ((($isintable == 1 && $isConsRcmd) || # unterbrochenen Consumer fortsetzen
|
||
($isintable == 3 && $isConsRcmd)) &&
|
||
$isInTime && $auto && $oncom && !$iilt &&
|
||
$simpCstat =~ /interrupted|interrupting|continuing/xs) {
|
||
|
||
my $cause = $isintable == 3 ? 'interrupt condition no longer present' : 'existing surplus';
|
||
$state = qq{switching Consumer '$calias' to '$oncom', command: "set $dswname $oncom", cause: $cause};
|
||
|
||
if ($debug =~ /consumerSwitching${c}/x) {
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - send switch command now: "set $dswname $oncom"});
|
||
}
|
||
else {
|
||
Log3 ($name, 2, "$name - $state");
|
||
}
|
||
|
||
CommandSet (undef, "$dswname $oncom");
|
||
|
||
$paref->{ps} = "continuing:";
|
||
___setConsumerPlanningState ($paref);
|
||
delete $paref->{ps};
|
||
|
||
writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben
|
||
}
|
||
|
||
return $state;
|
||
}
|
||
|
||
################################################################
|
||
# Verbraucher ausschalten
|
||
################################################################
|
||
sub ___switchConsumerOff {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $c = $paref->{consumer};
|
||
my $t = $paref->{t}; # aktueller Unixtimestamp
|
||
my $state = $paref->{state};
|
||
my $debug = $paref->{debug};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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 $hyst = ConsumerVal ($hash, $c, "hysteresis", DEFHYST); # Hysterese
|
||
|
||
my $mode = getConsumerPlanningMode ($hash, $c); # Planungsmode 'can' oder 'must'
|
||
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 (undef, $cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name
|
||
|
||
my $cause;
|
||
|
||
Log3 ($name, 1, "$name - $err") if($err);
|
||
|
||
my ($iilt,$rlt) = isInLocktime ($paref); # Sperrzeit Status ermitteln
|
||
|
||
if ($debug =~ /consumerSwitching${c}/x) { # nur für Debugging
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - in Context '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)) {
|
||
$cause = $swoffcond ? "switch-off condition (key swoffcond) is true" : "planned switch-off time reached/exceeded";
|
||
$state = qq{switching Consumer '$calias' to '$offcom', command: "set $dswname $offcom", cause: $cause};
|
||
|
||
if ($debug =~ /consumerSwitching${c}/x) {
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - send switch command now: "set $dswname $offcom"});
|
||
}
|
||
else {
|
||
Log3 ($name, 2, "$name - $state (Automatic = $auto)");
|
||
}
|
||
|
||
CommandSet (undef,"$dswname $offcom");
|
||
|
||
$paref->{ps} = "switching off:";
|
||
___setConsumerPlanningState ($paref);
|
||
delete $paref->{ps};
|
||
|
||
writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben
|
||
}
|
||
elsif ((($isintable && !isConsRcmd ($hash, $c)) || $isintable == 2) && # Consumer unterbrechen
|
||
isInTimeframe ($hash, $c) && $auto && $offcom && !$iilt &&
|
||
$simpCstat =~ /started|continued|interrupting/xs) {
|
||
$cause = $isintable == 2 ? 'interrupt condition' : 'surplus shortage';
|
||
$state = qq{switching Consumer '$calias' to '$offcom', command: "set $dswname $offcom", cause: $cause};
|
||
|
||
if ($debug =~ /consumerSwitching${c}/x) {
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - send switch command now: "set $dswname $offcom"});
|
||
}
|
||
else {
|
||
Log3 ($name, 2, "$name - $state (Automatic = $auto)");
|
||
}
|
||
|
||
CommandSet (undef,"$dswname $offcom");
|
||
|
||
$paref->{ps} = "interrupting:";
|
||
___setConsumerPlanningState ($paref);
|
||
delete $paref->{ps};
|
||
|
||
writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben
|
||
}
|
||
|
||
return $state;
|
||
}
|
||
|
||
################################################################
|
||
# Consumer aktuelle Schaltzustände ermitteln &
|
||
# logische Zustände ableiten/setzen
|
||
################################################################
|
||
sub ___setConsumerSwitchingState {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $c = $paref->{consumer};
|
||
my $t = $paref->{t};
|
||
my $state = $paref->{state};
|
||
my $fscss = $paref->{fscss}; # erster Subaufruf: 1
|
||
|
||
my $hash = $defs{$name};
|
||
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${c}", qq{consumer "$c" - current planning state: $simpCstat});
|
||
|
||
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) {
|
||
if (!$fscss) {
|
||
writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben
|
||
}
|
||
|
||
Log3 ($name, 2, "$name - $state");
|
||
}
|
||
|
||
return $state;
|
||
}
|
||
|
||
################################################################
|
||
# Verbraucher - Laufzeit, Tagesstarts und Aktivminuten pro
|
||
# Stunde ermitteln
|
||
# Stundenwechsel + Tageswechsel Management
|
||
#
|
||
# startTime - wichtig für Wechselmanagement!!
|
||
################################################################
|
||
sub __getCyclesAndRuntime {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $t = $paref->{t};
|
||
my $chour = $paref->{chour};
|
||
my $day = $paref->{day}; # aktueller Tag (range 01 to 31)
|
||
my $date = $paref->{date}; # aktuelles Datum
|
||
my $pcurr = $paref->{pcurr};
|
||
my $c = $paref->{consumer};
|
||
my $debug = $paref->{debug};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
my ($starthour, $startday);
|
||
|
||
if (isConsumerLogOn ($hash, $c, $pcurr)) { # Verbraucher ist logisch "an"
|
||
if (ConsumerVal ($hash, $c, 'onoff', 'off') eq 'off') { # Status im letzen Zyklus war "off"
|
||
$data{$name}{consumers}{$c}{onoff} = 'on';
|
||
$data{$name}{consumers}{$c}{startTime} = $t; # startTime ist nicht von "Automatic" abhängig -> nicht identisch mit planswitchon !!!
|
||
$data{$name}{consumers}{$c}{cycleStarttime} = $t;
|
||
$data{$name}{consumers}{$c}{cycleTime} = 0;
|
||
$data{$name}{consumers}{$c}{lastMinutesOn} = ConsumerVal ($hash, $c, 'minutesOn', 0);
|
||
|
||
$data{$name}{consumers}{$c}{cycleDayNum}++; # Anzahl der On-Schaltungen am Tag
|
||
}
|
||
else {
|
||
$data{$name}{consumers}{$c}{cycleTime} = (($t - ConsumerVal ($hash, $c, 'cycleStarttime', $t)) / 60); # Minuten
|
||
}
|
||
|
||
$starthour = strftime "%H", localtime(ConsumerVal ($hash, $c, 'startTime', $t));
|
||
$startday = strftime "%d", localtime(ConsumerVal ($hash, $c, 'startTime', $t)); # aktueller Tag (range 01 to 31)
|
||
|
||
if ($chour eq $starthour) {
|
||
my $runtime = (($t - ConsumerVal ($hash, $c, 'startTime', $t)) / 60); # in Minuten ! (gettimeofday sind ms !)
|
||
$data{$name}{consumers}{$c}{minutesOn} = ConsumerVal ($hash, $c, 'lastMinutesOn', 0) + $runtime;
|
||
}
|
||
else { # Stundenwechsel
|
||
if (ConsumerVal ($hash, $c, 'onoff', 'off') eq 'on') { # Status im letzen Zyklus war "on"
|
||
my $newst = timestringToTimestamp ($date.' '.sprintf("%02d", $chour).':00:00');
|
||
$data{$name}{consumers}{$c}{startTime} = $newst;
|
||
$data{$name}{consumers}{$c}{minutesOn} = ($t - ConsumerVal ($hash, $c, 'startTime', $newst)) / 60; # in Minuten ! (gettimeofday sind ms !)
|
||
$data{$name}{consumers}{$c}{lastMinutesOn} = 0;
|
||
|
||
if ($day ne $startday) { # Tageswechsel
|
||
$data{$name}{consumers}{$c}{cycleDayNum} = 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else { # Verbraucher soll nicht aktiv sein
|
||
$starthour = strftime "%H", localtime(ConsumerVal ($hash, $c, 'startTime', 1));
|
||
$startday = strftime "%d", localtime(ConsumerVal ($hash, $c, 'startTime', 1)); # aktueller Tag (range 01 to 31)
|
||
|
||
if ($chour ne $starthour) { # Stundenwechsel
|
||
$data{$name}{consumers}{$c}{minutesOn} = 0;
|
||
}
|
||
|
||
if ($day ne $startday) { # Tageswechsel
|
||
$data{$name}{consumers}{$c}{cycleDayNum} = 0;
|
||
}
|
||
|
||
$data{$name}{consumers}{$c}{onoff} = 'off';
|
||
}
|
||
|
||
if ($debug =~ /consumerSwitching${c}/xs) {
|
||
my $sr = 'still running';
|
||
my $son = isConsumerLogOn ($hash, $c, $pcurr) ? $sr : ConsumerVal ($hash, $c, 'cycleTime', 0) * 60; # letzte Cycle-Zeitdauer in Sekunden
|
||
my $cst = ConsumerVal ($hash, $c, 'cycleStarttime', 0);
|
||
$son = $son && $son ne $sr ? timestampToTimestring ($cst + $son, $paref->{lang}) :
|
||
$son eq $sr ? $sr :
|
||
'-';
|
||
$cst = $cst ? timestampToTimestring ($cst, $paref->{lang}) : '-';
|
||
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - cycleDayNum: }.ConsumerVal ($hash, $c, 'cycleDayNum', 0));
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - last cycle start time: $cst});
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - last cycle end time: $son \n});
|
||
}
|
||
|
||
## History schreiben
|
||
######################
|
||
$paref->{val} = ConsumerVal ($hash, $c, "cycleDayNum", 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 speichern
|
||
$paref->{histname} = "minutescsm${c}";
|
||
setPVhistory ($paref);
|
||
|
||
delete $paref->{histname};
|
||
delete $paref->{val};
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Restlaufzeit Verbraucher ermitteln
|
||
################################################################
|
||
sub __remainConsumerTime {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $c = $paref->{consumer};
|
||
my $t = $paref->{t}; # aktueller Unixtimestamp
|
||
|
||
my $hash = $defs{$name};
|
||
my ($planstate,$startstr,$stoptstr) = __getPlanningStateAndTimes ($paref);
|
||
my $stopts = ConsumerVal ($hash, $c, 'planswitchoff', undef); # geplante Unix Stopzeit
|
||
|
||
$data{$name}{consumers}{$c}{remainTime} = 0;
|
||
|
||
if (isInTimeframe($hash, $c) && (($planstate =~ /started/xs && isConsumerPhysOn($hash, $c)) | $planstate =~ /interrupt|continu/xs)) {
|
||
my $remainTime = $stopts - $t ;
|
||
$data{$name}{consumers}{$c}{remainTime} = sprintf "%.0f", ($remainTime / 60) if($remainTime > 0);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Consumer physischen & logischen Schaltstatus setzen
|
||
################################################################
|
||
sub __setPhysLogSwState {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $c = $paref->{consumer};
|
||
my $pcurr = $paref->{pcurr};
|
||
my $befsw = $paref->{befsw}; # Status vor Switching:1, danach 0 | undef
|
||
my $debug = $paref->{debug};
|
||
|
||
my $hash = $defs{$name};
|
||
my $cpo = isConsumerPhysOn ($hash, $c) ? 'on' : 'off';
|
||
my $clo = isConsumerLogOn ($hash, $c, $pcurr) ? 'on' : 'off';
|
||
|
||
$data{$name}{consumers}{$c}{physoffon} = $cpo;
|
||
$data{$name}{consumers}{$c}{logoffon} = $clo;
|
||
|
||
if ($debug =~ /consumerSwitching${c}/xs) {
|
||
my $ao = $befsw ? 'before switching' : 'after switching';
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - physical Switchstate $ao: $cpo});
|
||
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - logical Switchstate $ao: $clo});
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Freigabe Einschalten Verbraucher durch Batterie Vorrangladung
|
||
# return 0 -> keine Einschaltfreigabe Verbraucher
|
||
# return 1 -> Einschaltfreigabe Verbraucher
|
||
################################################################
|
||
sub ___enableSwitchByBatPrioCharge {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $c = $paref->{consumer};
|
||
|
||
my $hash = $defs{$name};
|
||
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 $bcharge = BatteryVal ($hash, '01', 'bcharge', 0); # aktuelle Ladung in %
|
||
$ena = 0 if($bcharge < $pcb); # keine Freigabe wenn Batterieladung kleiner Soll-Ladung
|
||
|
||
return $ena;
|
||
}
|
||
|
||
###################################################################
|
||
# Consumer Planstatus und Planzeit ermitteln
|
||
###################################################################
|
||
sub __getPlanningStateAndTimes {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $c = $paref->{consumer};
|
||
my $lang = $paref->{lang};
|
||
|
||
my $hash = $defs{$name};
|
||
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
|
||
################################################################
|
||
sub _calcConsumptionForecast {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $chour = $paref->{chour};
|
||
my $t = $paref->{t};
|
||
my $day = $paref->{day}; # aktuelles Tagdatum (01...31)
|
||
my $dayname = $paref->{dayname}; # aktueller Tagname
|
||
|
||
my $hash = $defs{$name};
|
||
my $acref = $data{$name}{consumers};
|
||
my $swdfcfc = AttrVal ($name, 'affectConsForecastIdentWeekdays', 0); # nutze nur gleiche Wochentage (Mo...So) für Verbrauchsvorhersage
|
||
|
||
## Beachtung der letzten X Tage falls gesetzt
|
||
###############################################
|
||
my $acld = AttrVal ($name, 'affectConsForecastLastDays', 0);
|
||
my @dtn; # Array der zu beachtenden Tage
|
||
|
||
if ($acld) {
|
||
for my $l (1..$acld) {
|
||
my $dday = strftime "%d", localtime($t - $l * 86400); # resultierender Tag (range 01..)
|
||
push @dtn, $dday;
|
||
}
|
||
}
|
||
|
||
## Verbrauchsvorhersage für den kommenden Tag
|
||
##############################################
|
||
my $tomorrow = strftime "%a", localtime($t+86400); # Wochentagsname kommender Tag
|
||
my (@cona, $exconfc, $csme);
|
||
|
||
debugLog ($paref, 'consumption|consumption_long', "################### Consumption forecast for the next day ###################");
|
||
debugLog ($paref, 'consumption|consumption_long', "Date(s) to take note: ".join ',', @dtn) if(@dtn);
|
||
|
||
for my $n (sort{$a<=>$b} keys %{$data{$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);
|
||
}
|
||
|
||
if (@dtn) {
|
||
if (!grep /^$n$/, @dtn) {
|
||
debugLog ($paref, 'consumption|consumption_long', "Day >$n< should not be observed, ignore it.");
|
||
next;
|
||
}
|
||
}
|
||
|
||
my $dcon = HistoryVal ($hash, $n, 99, 'con', 0);
|
||
|
||
if (!$dcon) {
|
||
debugLog ($paref, 'consumption|consumption_long', "Day >$n< has no registered consumption, ignore it.");
|
||
next;
|
||
}
|
||
|
||
for my $c (sort{$a<=>$b} keys %{$acref}) { # historischer Verbrauch aller registrierten Verbraucher aufaddieren
|
||
$exconfc = ConsumerVal ($hash, $c, 'exconfc', 0); # 1 -> Consumer Verbrauch von Erstelleung der Verbrauchsprognose ausschließen
|
||
$csme = HistoryVal ($hash, $n, 99, "csme${c}", 0);
|
||
|
||
if ($exconfc) {
|
||
$dcon -= $csme;
|
||
debugLog ($paref, 'consumption|consumption_long', "Consumer '$c' values excluded from forecast calc by 'exconfc' - day: $n, csme: $csme");
|
||
}
|
||
}
|
||
|
||
debugLog ($paref, 'consumption|consumption_long', "History Consumption day >$n< considering possible exclusions: $dcon");
|
||
|
||
push @cona, $dcon;
|
||
}
|
||
|
||
my $dnum = scalar @cona;
|
||
|
||
if ($dnum) {
|
||
my $tomcon = sprintf "%.0f", medianArray (\@cona);
|
||
$data{$name}{current}{tomorrowconsumption} = $tomcon; # prognostizierter Verbrauch (Median) aller (gleicher) Wochentage
|
||
|
||
debugLog ($paref, 'consumption|consumption_long', "estimated Consumption for tomorrow: $tomcon, days for median: $dnum");
|
||
}
|
||
else {
|
||
my $lang = $paref->{lang};
|
||
$data{$name}{current}{tomorrowconsumption} = $hqtxt{wfmdcf}{$lang};
|
||
}
|
||
|
||
## Verbrauchsvorhersage für die kommenden Stunden
|
||
##################################################
|
||
debugLog ($paref, 'consumption|consumption_long', "################### Consumption forecast for the next hours ###################");
|
||
debugLog ($paref, 'consumption|consumption_long', "Date(s) to take note: ".join ',', @dtn) if(@dtn);
|
||
|
||
for my $k (sort keys %{$data{$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)
|
||
|
||
my (@conhfc, @conhfcex);
|
||
|
||
for my $m (sort{$a<=>$b} keys %{$data{$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);
|
||
}
|
||
|
||
if (@dtn) {
|
||
if (!grep /^$m$/, @dtn) {
|
||
debugLog ($paref, 'consumption|consumption_long', "Day >$m< should not be observed, ignore it.");
|
||
next;
|
||
}
|
||
}
|
||
|
||
my $hcon = HistoryVal ($hash, $m, $nhhr, 'con', 0); # historische Verbrauchswerte
|
||
next if(!$hcon);
|
||
|
||
debugLog ($paref, 'consumption_long', " historical Consumption added for $nhday -> date: $m, hod: $nhhr -> $hcon Wh");
|
||
|
||
if ($hcon < 0) { # V1.32.0
|
||
my $vl = 3;
|
||
my $pre = '- WARNING -';
|
||
|
||
if ($paref->{debug} =~ /consumption/xs) {
|
||
$vl = 1;
|
||
$pre = 'DEBUG> - WARNING -';
|
||
}
|
||
|
||
Log3 ($name, $vl, "$name $pre The stored Energy consumption of day/hour $m/$nhhr is negative. This appears to be an error. The incorrect value can be deleted with 'set $name reset consumption $m $nhhr'.");
|
||
}
|
||
|
||
for my $c (sort{$a<=>$b} keys %{$acref}) { # historischen Verbrauch aller registrierten Verbraucher aufaddieren
|
||
$exconfc = ConsumerVal ($hash, $c, 'exconfc', 0); # 1 -> Consumer Verbrauch von Erstelleung der Verbrauchsprognose ausschließen
|
||
$csme = HistoryVal ($hash, $m, $nhhr, "csme${c}", 0);
|
||
$consumerco += $csme;
|
||
|
||
if ($exconfc) {
|
||
debugLog ($paref, 'consumption_long', "Consumer '$c' values excluded from forecast calc by 'exconfc' - day: $m, hour: $nhhr, csme: $csme");
|
||
$consumerco -= $csme; # V1.32.0
|
||
$hcon -= $csme; # V1.32.0, excludierte Verbraucherconsumption von Forecast ausschließen
|
||
}
|
||
}
|
||
|
||
push @conhfcex, ($hcon - $consumerco) if($hcon >= $consumerco); # prognostizierter Verbrauch (Median) Ex registrierter Verbraucher
|
||
push @conhfc, $hcon;
|
||
|
||
$dnum++;
|
||
}
|
||
|
||
if ($dnum) {
|
||
my $conavgex = sprintf "%.0f", medianArray (\@conhfcex) if(scalar @conhfcex);
|
||
$data{$name}{nexthours}{$k}{confcEx} = $conavgex;
|
||
my $conavg = sprintf "%.0f", medianArray (\@conhfc) if(scalar @conhfc);
|
||
$data{$name}{nexthours}{$k}{confc} = $conavg; # prognostizierter Verbrauch (Median) auf Grundlage aller gleicher Wochentage pro Stunde
|
||
|
||
if (NexthoursVal ($hash, $k, 'today', 0)) { # nur Werte des aktuellen Tag speichern
|
||
$data{$name}{circular}{sprintf("%02d",$nhhr)}{confc} = $conavg;
|
||
writeToHistory ( { paref => $paref, key => 'confc', val => $conavg, hour => $nhhr } );
|
||
}
|
||
|
||
debugLog ($paref, 'consumption|consumption_long', "estimated Consumption for $nhday -> starttime: $nhtime, confc: $conavg, days for avg: $dnum, hist. consumption registered consumers: ".sprintf "%.2f", $consumerco);
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Energieverbrauch Vorhersage kalkulieren (Median)
|
||
################################################################
|
||
sub _calcConsumptionForecast_circular {
|
||
my $paref = shift;
|
||
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 $hash = $defs{$name};
|
||
my $acref = $data{$name}{consumers};
|
||
my $swdfcfc = AttrVal ($name, 'affectConsForecastIdentWeekdays', 0); # nutze nur gleiche Wochentage (Mo...So) für Verbrauchsvorhersage
|
||
|
||
## Beachtung der letzten X Tage falls gesetzt
|
||
###############################################
|
||
my $acld = AttrVal ($name, 'affectConsForecastLastDays', 0);
|
||
|
||
## Verbrauchsvorhersage für den kommenden Tag
|
||
##############################################
|
||
my $dt = timestringsFromOffset ($t, 86400);
|
||
my $tomdayname = $dt->{dayname}; # Wochentagsname kommender Tag
|
||
|
||
my (@cona, $exconfc, $csme, %usage);
|
||
|
||
$usage{tom}{con} = 0;
|
||
|
||
debugLog ($paref, 'consumption|consumption_long', "################### Consumption forecast for the next day (new median) ###################");
|
||
|
||
## Verbrauch der hod-Stunden 01..24 u. gesamten Tag ermitteln
|
||
###############################################################
|
||
for my $h (1..24) { # Median für jede Stunde / Tag berechnen
|
||
my $hh = sprintf "%02d", $h;
|
||
|
||
my @conh;
|
||
|
||
if ($swdfcfc) { # nur gleiche Tage (Mo...So) einbeziehen der Stunde
|
||
push @conh, @{$data{$name}{circular}{$hh}{con_all}{"$tomdayname"}};
|
||
}
|
||
else {
|
||
for my $dy (keys %{$data{$name}{circular}{$hh}{con_all}}) { # alle aufgezeichneten Wochentage in der Stunde berücksichtigen
|
||
push @conh, @{$data{$name}{circular}{$hh}{con_all}{$dy}};
|
||
}
|
||
}
|
||
|
||
my $hnum = scalar @conh;
|
||
|
||
if ($hnum) {
|
||
my $hcon = sprintf "%.0f", medianArray (\@conh);
|
||
$usage{$hh}{con} = $hcon; # prognostizierter Verbrauch (Median) der Stunde hh (Hour of Day)
|
||
$usage{$hh}{num} = $hnum;
|
||
$usage{tom}{con} += $hcon; # Summe prognostizierter Verbrauch (Median) des Tages
|
||
$usage{tom}{num} += $hnum;
|
||
}
|
||
}
|
||
|
||
## historische Werte Verbraucher exkludieren / geplante inkludieren
|
||
#####################################################################
|
||
my $tomnum = $usage{tom}{num} // 0;
|
||
|
||
if ($tomnum) {
|
||
my $exnum = 0;
|
||
my $ex = 0;
|
||
|
||
for my $n (sort{$a<=>$b} keys %{$data{$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 $tomdayname);
|
||
}
|
||
|
||
for my $c (sort{$a<=>$b} keys %{$acref}) { # historischer Verbrauch aller registrierten Verbraucher aufaddieren
|
||
$exconfc = ConsumerVal ($hash, $c, 'exconfc', 0); # 1 -> Consumer Verbrauch von Erstelleung der Verbrauchsprognose ausschließen
|
||
|
||
if ($exconfc) {
|
||
## Tageswert Excludes finden und summieren
|
||
############################################
|
||
$csme = HistoryVal ($hash, $n, 99, "csme${c}", 0);
|
||
|
||
if ($csme > 0) {
|
||
$ex += $csme;
|
||
$exnum++;
|
||
|
||
debugLog ($paref, 'consumption|consumption_long', "Consumer '$c' hist cons registered by 'exconfc' for excl. - day: $n, csme: $csme");
|
||
}
|
||
|
||
## hist. On-Stunden aller Tage aufnehmen
|
||
##########################################
|
||
for my $h (1..24) {
|
||
my $hh = sprintf "%02d", $h;
|
||
$csme = HistoryVal ($hash, $n, $hh, "csme${c}", 0);
|
||
|
||
if ($csme) {
|
||
$usage{$hh}{histcon} += $csme;
|
||
$usage{$hh}{histnum}++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
for my $h (1..24) {
|
||
my $hh = sprintf "%02d", $h;
|
||
|
||
if (defined $usage{$hh}{histnum}) {
|
||
my $exhcon = sprintf "%.0f", ($usage{$hh}{histcon} / $usage{$hh}{histnum}); # durchschnittlichen Verbrauchswert
|
||
$usage{$hh}{con} -= $exhcon;
|
||
|
||
debugLog ($paref, 'consumption|consumption_long', "excl. $exhcon Wh for Hour $hh, Considered value numbers: ".$usage{$hh}{histnum});
|
||
}
|
||
|
||
debugLog ($paref, 'consumption|consumption_long', "estimated cons of Hour $hh: ".$usage{$hh}{con}." Wh, Considered value numbers: ".$usage{$hh}{num});
|
||
}
|
||
|
||
$ex = sprintf "%.0f", ($ex / $exnum) if($exnum); # Ex Tageswert Durchschnitt bilden
|
||
$usage{tom}{con} -= $ex;
|
||
|
||
debugLog ($paref, 'consumption|consumption_long', "estimated cons Tomorrow: ".$usage{tom}{con}." Wh, Considered value numbers: $tomnum, exclude: $ex Wh (avg of $exnum)");
|
||
}
|
||
else {
|
||
my $lang = $paref->{lang};
|
||
# $data{$name}{current}{tomorrowconsumption} = $hqtxt{wfmdcf}{$lang};
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Schwellenwerte auswerten und signalisieren
|
||
################################################################
|
||
sub _evaluateThresholds {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
|
||
my $bt = ReadingsVal($name, 'batteryTrigger', '');
|
||
my $pt = ReadingsVal($name, 'powerTrigger', '');
|
||
my $eh4t = ReadingsVal($name, 'energyH4Trigger', '');
|
||
|
||
if ($bt) {
|
||
$paref->{cobj} = 'batsocslidereg';
|
||
$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 $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 $hash = $defs{$name};
|
||
my $aaref = CurrentVal ($hash, $cobj, '');
|
||
my @aa = ();
|
||
@aa = @{$aaref} if (ref $aaref eq 'ARRAY');
|
||
|
||
return if(scalar @aa < SLIDENUMMAX);
|
||
|
||
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
|
||
# erstellen
|
||
################################################################
|
||
sub _calcReadingsTomorrowPVFc {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
|
||
my $hash = $defs{$name};
|
||
my $h = $data{$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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $t = $paref->{t};
|
||
my $date = $paref->{date};
|
||
my $day = $paref->{day};
|
||
|
||
my $hash = $defs{$name};
|
||
my $pvfc = ReadingsNum ($name, 'Today_PVforecast', 0);
|
||
my $pvre = ReadingsNum ($name, 'Today_PVreal', 0);
|
||
|
||
return if(!$pvre || !$pvfc); # Illegal division by zero verhindern
|
||
|
||
my $dp;
|
||
|
||
if (AttrVal ($name, 'ctrlGenPVdeviation', 'daily') eq 'daily') {
|
||
my $sstime = timestringToTimestamp ($date.' '.ReadingsVal ($name, "Today_SunSet", '22:00').':00');
|
||
return if($t < $sstime);
|
||
|
||
$dp = sprintf "%.2f", (100 - (100 * $pvre / $pvfc)); # V 1.23.0
|
||
}
|
||
else {
|
||
my $pvfcd = ReadingsNum ($name, 'RestOfDayPVforecast', 0) - $pvfc; # PV Prognose bis jetzt
|
||
return if(!$pvfcd); # Illegal division by zero verhindern
|
||
|
||
$dp = sprintf "%.2f", (100 - (100 * $pvre / abs $pvfcd)); # V 1.25.0
|
||
}
|
||
|
||
$data{$name}{circular}{99}{tdayDvtn} = $dp;
|
||
|
||
storeReading ('Today_PVdeviation', $dp.' %');
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Werte berechnen und speichern die nach jeder vollen Stunde
|
||
# nur einmal ermittelt werden:
|
||
# - PV-Prognosekorrekturfaktoren und deren Qualität
|
||
# - AI Quellendaten hinzufügen
|
||
# - Verbrauchswerte für Medianauswertung
|
||
################################################################
|
||
sub _calcDataEveryFullHour {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $chour = $paref->{chour};
|
||
my $t = $paref->{t}; # aktuelle Unix-Zeit
|
||
|
||
my $hash = $defs{$name};
|
||
my $idts = CircularVal ($hash, 99, "attrInvChangedTs", ''); # Definitionstimestamp des Attr setupInverterDev01
|
||
|
||
return if(!$idts);
|
||
|
||
my ($acu, $aln) = isAutoCorrUsed ($name);
|
||
|
||
if ($acu) {
|
||
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 (0..23) {
|
||
next if($h > $chour);
|
||
|
||
if (int $chour == 0) { # 00:XX -> Stunde 24 des Vortages speichern
|
||
my $dt = timestringsFromOffset ($t, -3600);
|
||
$paref->{yt} = $t - 3600; # Timestamp Vortag
|
||
$paref->{yday} = $dt->{day}; # vorheriger Tag (range 01 .. 31)
|
||
$paref->{ydayname} = $dt->{dayname};
|
||
$h = 24; # Stunde 24 am Vortag!
|
||
}
|
||
|
||
next if(!$h);
|
||
|
||
my $hh = sprintf "%02d", $h;
|
||
$paref->{cpcf} = ReadingsVal ($name, 'pvCorrectionFactor_'.$hh, ''); # aktuelles pvCorf-Reading
|
||
$paref->{aihit} = CircularVal ($hash, $hh, 'aihit', 0); # AI verwendet?
|
||
$paref->{h} = $h;
|
||
|
||
next if(ReadingsVal ($name, '.signaldone_'.$hh, '') eq "done");
|
||
|
||
_calcCaQsimple ($paref); # einfache Korrekturberechnung duchführen/speichern
|
||
_calcCaQcomplex ($paref); # Korrekturberechnung mit Bewölkung duchführen/speichern
|
||
_addHourAiRawdata ($paref); # AI Raw Data hinzufügen
|
||
_addCon2CircArray ($paref); # Hausverbrauch der vergangenen Stunde zum con-Array im Circular Speicher hinzufügen
|
||
|
||
storeReading ('.signaldone_'.$hh, 'done'); # Sperrsignal (erledigt) setzen
|
||
|
||
delete $paref->{h};
|
||
delete $paref->{cpcf};
|
||
delete $paref->{aihit};
|
||
delete $paref->{yday};
|
||
delete $paref->{ydayname};
|
||
delete $paref->{yt};
|
||
}
|
||
|
||
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 $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 $aihit = $paref->{aihit};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if (!$aln) {
|
||
debugLog ($paref, 'pvCorrectionWrite', "Autolearning is switched off for hour: $h -> skip the recalculation of the complex correction factor");
|
||
return;
|
||
}
|
||
|
||
my $hh = sprintf "%02d", $h;
|
||
my $pvrl = CircularVal ($hash, $hh, 'pvrl', 0); # real erzeugte PV Energie am Ende der vorherigen Stunde
|
||
my $pvfc = CircularVal ($hash, $hh, 'pvapifc', 0); # vorhergesagte PV Energie am Ende der vorherigen Stunde
|
||
|
||
if (!$pvrl || !$pvfc) {
|
||
return;
|
||
}
|
||
|
||
my $chwcc = HistoryVal ($hash, $day, $hh, 'wcc', 0); # Wolkenbedeckung heute & abgefragte Stunde
|
||
my $sunalt = HistoryVal ($hash, $day, $hh, 'sunalt', 0); # Sonne Altitude
|
||
my $crang = cloud2bin ($chwcc);
|
||
my $sabin = sunalt2bin ($sunalt);
|
||
|
||
## Speicherarrays schreiben
|
||
#############################
|
||
push @{$data{$name}{circular}{$hh}{'pvrl_'.$sabin}{"$crang"}}, $pvrl;
|
||
push @{$data{$name}{circular}{$hh}{'pvfc_'.$sabin}{"$crang"}}, $pvfc;
|
||
|
||
## neuen Korrekturfaktor berechnen
|
||
####################################
|
||
$paref->{pvrl} = $pvrl;
|
||
$paref->{pvfc} = $pvfc;
|
||
$paref->{crang} = $crang;
|
||
$paref->{sabin} = $sabin;
|
||
$paref->{calc} = 'Complex';
|
||
|
||
my ($oldfac, $factor, $dnum) = __calcNewFactor_migrated ($paref); # migrierte Daten verwenden
|
||
|
||
delete $paref->{pvrl};
|
||
delete $paref->{pvfc};
|
||
delete $paref->{crang};
|
||
delete $paref->{sabin};
|
||
delete $paref->{calc};
|
||
|
||
$aihit = $aihit ? ' AI result used,' : '';
|
||
|
||
if ($acu =~ /on_complex/xs) {
|
||
if ($paref->{cpcf} !~ /manual/xs) { # pcf-Reading nur überschreiben wenn nicht 'manual xxx' gesetzt
|
||
storeReading ('pvCorrectionFactor_'.$hh, $factor." (automatic - old factor: $oldfac,$aihit Sun Alt range: $sabin, Cloud range: $crang, Days in range: $dnum)");
|
||
}
|
||
else {
|
||
storeReading ('pvCorrectionFactor_'.$hh, $paref->{cpcf}." / flexmatic result $factor for Sun Alt range: $sabin,$aihit Cloud range: $crang, Days in range: $dnum");
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# PV Ist/Forecast ermitteln und Korrekturfaktoren, Qualität
|
||
# ohne Nebenfaktoren errechnen und speichern (simple)
|
||
################################################################
|
||
sub _calcCaQsimple {
|
||
my $paref = shift;
|
||
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 $aihit = $paref->{aihit};
|
||
|
||
my $hash = $defs{$name};
|
||
my $hh = sprintf "%02d", $h;
|
||
|
||
if (!$aln) {
|
||
debugLog ($paref, 'pvCorrectionWrite', "Autolearning is switched off for hour: $h -> skip the recalculation of the simple correction factor");
|
||
return;
|
||
}
|
||
|
||
my $pvrl = CircularVal ($hash, $hh, 'pvrl', 0);
|
||
my $pvfc = CircularVal ($hash, $hh, 'pvapifc', 0);
|
||
|
||
if (!$pvrl || !$pvfc) {
|
||
return;
|
||
}
|
||
|
||
my $sunalt = HistoryVal ($hash, $day, $hh, '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_migrated ($paref); # migrierte Daten verwenden
|
||
|
||
delete $paref->{pvrl};
|
||
delete $paref->{pvfc};
|
||
delete $paref->{sabin};
|
||
delete $paref->{crang};
|
||
delete $paref->{calc};
|
||
|
||
$aihit = $aihit ? ' AI result used,' : '';
|
||
|
||
if ($acu =~ /on_simple/xs) {
|
||
if ($paref->{cpcf} !~ /manual/xs) { # pcf-Reading nur überschreiben wenn nicht 'manual xxx' gesetzt
|
||
storeReading ('pvCorrectionFactor_'.$hh, $factor." (automatic - old factor: $oldfac,$aihit Days in range: $dnum)");
|
||
}
|
||
else {
|
||
storeReading ('pvCorrectionFactor_'.$hh, $paref->{cpcf}." / flexmatic result $factor,$aihit Days in range: $dnum");
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# den Hausverbrauch der vergangenen Stunde zum con-Array
|
||
# im Circular Speicher hinzufügen
|
||
################################################################
|
||
sub _addCon2CircArray {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $h = $paref->{h};
|
||
my $yday = $paref->{yday}; # vorheriger Tag (falls gesetzt)
|
||
my $day = $paref->{day}; # aktueller Tag (range 01 to 31)
|
||
my $dayname = $paref->{dayname};
|
||
my $ydayname = $paref->{ydayname};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
$day = $yday if(defined $yday); # der vergangene Tag soll verarbeitet werden
|
||
$dayname = $ydayname if(defined $ydayname); # Name des Vortages
|
||
my $hh = sprintf "%02d", $h;
|
||
my $con = HistoryVal ($hash, $day, $hh, 'con', 0); # Consumption der abgefragten Stunde
|
||
|
||
push @{$data{$name}{circular}{$hh}{con_all}{"$dayname"}}, $con if($con >= 0); # Wert zum Speicherarray hinzufügen
|
||
|
||
debugLog ($paref, 'saveData2Cache', "add consumption into Array (con_all) in Circular - day: $day, hod: $hh, con: $con");
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# den neuen Korrekturfaktur berechnen (neue Median Funktion)
|
||
################################################################
|
||
sub __calcNewFactor_migrated {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
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 $hash = $defs{$name};
|
||
my ($factor, $pvcirc, $fccirc, $pvrlsum, $pvfcsum, $dnum);
|
||
|
||
my $hh = sprintf "%02d", $h;
|
||
my ($oldfac, $oldqal) = CircularSunCloudkorrVal ($hash, $hh, $sabin, $crang, 0); # bisher definierter Korrekturfaktor / Qualität
|
||
$oldfac = 1 if(1 * $oldfac == 0);
|
||
|
||
debugLog ($paref, 'pvCorrectionWrite', "$calc Corrf -> Start calculation correction factor for hour: $hh (migrated data structure)");
|
||
|
||
if ($calc eq 'Simple') {
|
||
($pvcirc, $fccirc, $dnum) = CircularSumVal ($hash, $hh, $sabin, 'simple', 0);
|
||
|
||
debugLog ($paref, 'pvCorrectionWrite', "$calc Corrf -> read stored values: PVreal sum: $pvcirc, PVforecast sum: $fccirc, days: $dnum");
|
||
|
||
if ($dnum) { # Werte in History vorhanden -> haben Prio !
|
||
$dnum++;
|
||
$pvrlsum = $pvrl + $pvcirc;
|
||
$pvfcsum = $pvfc + $fccirc;
|
||
$pvrl = $pvrlsum / $dnum;
|
||
$pvfc = $pvfcsum / $dnum;
|
||
$factor = sprintf "%.2f", ($pvrl / $pvfc); # Faktorberechnung: reale PV / Prognose
|
||
}
|
||
elsif ($oldfac && (!$pvcirc || !$fccirc)) { # 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);
|
||
}
|
||
}
|
||
else {
|
||
$pvrl = sprintf "%.0f", medianArray (\@{$data{$name}{circular}{$hh}{'pvrl_'.$sabin}{"$crang"}}); # neuen Median berechnen
|
||
$pvfc = sprintf "%.0f", medianArray (\@{$data{$name}{circular}{$hh}{'pvfc_'.$sabin}{"$crang"}}); # neuen Median berechnen
|
||
|
||
$dnum = scalar (@{$data{$name}{circular}{$hh}{'pvrl_'.$sabin}{"$crang"}});
|
||
$factor = sprintf "%.2f", ($pvrl / $pvfc);
|
||
|
||
debugLog ($paref, 'pvCorrectionWrite', "$calc Corrf -> read stored values: PVreal median: $pvrl, PVforecast median: $pvfc, days: $dnum");
|
||
}
|
||
|
||
$factor = 1.00 if(1 * $factor == 0); # 0.00-Werte ignorieren (Schleifengefahr)
|
||
|
||
## max. Faktorsteigerung berücksichtigen
|
||
##########################################
|
||
if (abs($factor - $oldfac) > DEFMAXVAR) {
|
||
$factor = sprintf "%.2f", ($factor > $oldfac ? $oldfac + DEFMAXVAR : $oldfac - DEFMAXVAR);
|
||
Log3 ($name, 3, "$name - new $calc correction factor calculated (limited by maximum Day Variance): $factor (old: $oldfac) for hour: $hh");
|
||
}
|
||
else {
|
||
Log3 ($name, 3, "$name - new $calc correction factor for hour $hh calculated: $factor (old: $oldfac)");
|
||
}
|
||
|
||
## Qualität berechnen
|
||
#######################
|
||
$oldfac = sprintf "%.2f", $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: $hh, 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: $hh, Sun Altitude range: $sabin, Cloud range: $crang, factor: $factor, quality: $qual, days: $dnum");
|
||
|
||
## neue Werte speichern
|
||
#########################
|
||
if ($calc eq 'Simple') {
|
||
$data{$name}{circular}{$hh}{pvrlsum}{'simple'} = $pvrlsum;
|
||
$data{$name}{circular}{$hh}{pvfcsum}{'simple'} = $pvfcsum;
|
||
$data{$name}{circular}{$hh}{dnumsum}{'simple'} = $dnum;
|
||
$data{$name}{circular}{$hh}{pvcorrf}{'simple'} = $factor;
|
||
$data{$name}{circular}{$hh}{quality}{'simple'} = $qual;
|
||
}
|
||
else {
|
||
my $idx = $sabin.'.'.$crang;
|
||
$data{$name}{circular}{$hh}{pvcorrf}{$idx} = $factor;
|
||
$data{$name}{circular}{$hh}{quality}{$idx} = $qual;
|
||
}
|
||
|
||
return ($oldfac, $factor, $dnum);
|
||
}
|
||
|
||
################################################################
|
||
# 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;
|
||
}
|
||
|
||
################################################################
|
||
# Energieverbrauch des Hauses in History speichern
|
||
################################################################
|
||
sub _saveEnergyConsumption {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $day = $paref->{day};
|
||
my $chour = $paref->{chour};
|
||
my $debug = $paref->{debug};
|
||
|
||
my $hod = sprintf "%02d", ($chour + 1);
|
||
my $pvrl = ReadingsNum ($name, 'Today_Hour'.$hod.'_PVreal', 0); # Reading enthält die Summe aller Inverterdevices
|
||
my $gfeedin = ReadingsNum ($name, 'Today_Hour'.$hod.'_GridFeedIn', 0);
|
||
my $gcon = ReadingsNum ($name, 'Today_Hour'.$hod.'_GridConsumption', 0);
|
||
|
||
my $batin = 0;
|
||
my $batout = 0;
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$batin += ReadingsNum ($name, 'Today_Hour'.$hod.'_BatIn_'.$bn, 0);
|
||
$batout += ReadingsNum ($name, 'Today_Hour'.$hod.'_BatOut_'.$bn, 0);
|
||
}
|
||
|
||
my $ppreal = 0;
|
||
|
||
for my $prn (1..MAXPRODUCER) { # V1.32.0 : Erzeugung sonstiger Producer (01..03) hinzufügen
|
||
$prn = sprintf "%02d", $prn;
|
||
$ppreal += ReadingsNum ($name, 'Today_Hour'.$hod.'_PPreal_'.$prn, 0);
|
||
}
|
||
|
||
my $con = $pvrl + $ppreal - $gfeedin + $gcon - $batin + $batout;
|
||
my $dowrite = 1;
|
||
|
||
if (int $paref->{minute} > 30 && $con < 0) { # V1.32.0 : erst den "eingeschwungenen" Zustand mit mehreren Meßwerten auswerten
|
||
$dowrite = 0;
|
||
my $vl = 3;
|
||
my $pre = '- WARNING -';
|
||
|
||
if ($debug =~ /consumption/xs) {
|
||
$vl = 1;
|
||
$pre = 'DEBUG> - WARNING -';
|
||
}
|
||
|
||
Log3 ($name, $vl, "$name $pre The calculated Energy consumption of the house is negative. This appears to be an error and is not saved. Check Readings _PVreal, _GridFeedIn, _GridConsumption, _BatIn_XX, _BatOut_XX of hour >$hod<");
|
||
}
|
||
|
||
if ($debug =~ /collectData/xs) {
|
||
Log3 ($name, 1, "$name DEBUG> EnergyConsumption input -> PV: $pvrl, PP: $ppreal, GridIn: $gfeedin, GridCon: $gcon, BatIn: $batin, BatOut: $batout");
|
||
Log3 ($name, 1, "$name DEBUG> EnergyConsumption result -> $con Wh");
|
||
}
|
||
|
||
if ($dowrite) {
|
||
writeToHistory ( { paref => $paref, key => 'con', val => $con, hour => $hod } );
|
||
$data{$name}{circular}{99}{todayConsumption} = HistoryVal ($name, $day, '99', 'con', undef);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# optionale "special" Readings erstellen
|
||
################################################################
|
||
sub _genSpecialReadings {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $day = $paref->{day};
|
||
my $t = $paref->{t}; # aktueller UNIX Timestamp
|
||
|
||
my $hash = $defs{$name};
|
||
my @srd = sort keys (%hcsr);
|
||
my @csr = split ',', AttrVal ($name, 'ctrlSpecialReadings', '');
|
||
my $prpo = 'special';
|
||
|
||
for my $item (@srd) {
|
||
next if(grep /^$item$/, @csr);
|
||
readingsDelete ($hash, $prpo.'_'.$item);
|
||
deleteReadingspec ($hash, $prpo.'_'.$item.'_.*') if($item eq 'todayConsumptionForecast');
|
||
}
|
||
|
||
return if(!@csr);
|
||
|
||
my ($rapi, $wapi) = getStatusApiName ($hash);
|
||
|
||
for my $kpi (@csr) {
|
||
my $def = $hcsr{$kpi}{def};
|
||
my $par = $hcsr{$kpi}{par};
|
||
|
||
if (!defined $def || !defined $par) {
|
||
Log3 ($name, 1, "$name - ERROR in Application - attribute ctrlSpecialReadings KPI '$kpi' has no Parameter or default value set. Set the attribute again or inform Maintainer.");
|
||
next;
|
||
}
|
||
|
||
if ($def eq 'apimaxreq') {
|
||
$def = isSolCastUsed ($hash) ? (AttrVal ($name, 'ctrlSolCastAPImaxReq', SOLCMAXREQDEF)) :
|
||
isOpenMeteoUsed ($hash) ? OMETMAXREQ :
|
||
'n.a.';
|
||
}
|
||
|
||
if ($hcsr{$kpi}{fnr} == 1) {
|
||
storeReading ($prpo.'_'.$kpi, &{$hcsr{$kpi}{fn}} ($hash, $rapi, '?All', $kpi, $def));
|
||
}
|
||
elsif ($hcsr{$kpi}{fnr} == 2) {
|
||
$par = $kpi if(!$par);
|
||
storeReading ($prpo.'_'.$kpi, &{$hcsr{$kpi}{fn}} ($hash, $par, $def).$hcsr{$kpi}{unit});
|
||
}
|
||
elsif ($hcsr{$kpi}{fnr} == 3) {
|
||
storeReading ($prpo.'_'.$kpi, &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, $kpi, $def).$hcsr{$kpi}{unit});
|
||
}
|
||
elsif ($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 ($prpo.'_'.$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 ($prpo.'_'.$kpi, sprintf "%.0f", $smr);
|
||
}
|
||
|
||
if ($kpi =~ /BatPower(In|Out)_Sum/xs) {
|
||
my $bsum = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, $def);
|
||
$bsum .= $bsum eq $def ? '' : $hcsr{$kpi}{unit};
|
||
|
||
storeReading ($prpo.'_'.$kpi, $bsum);
|
||
}
|
||
|
||
if ($kpi =~ /daysUntilBatteryCare_/xs) {
|
||
my $bn = (split "_", $kpi)[1]; # Batterienummer extrahieren
|
||
my $d2c = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'days2care'.$bn, $def);
|
||
|
||
storeReading ($prpo.'_'.$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 ($prpo.'_'.$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 ($prpo.'_'.$kpi, (sprintf "%.1f", $dgcon).' Wh');
|
||
}
|
||
|
||
if ($kpi eq 'todayBatInSum') { # Summe tägl. Ladeenergie (alle Batterien)
|
||
my $tdbisum = 0;
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
|
||
my $idbitot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'initdaybatintot'.$bn, $def); # initialer Tagesstartwert Batterie In total
|
||
my $cbitot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'batintot'.$bn, $def); # aktuell total Batterieladung (Wh)
|
||
|
||
$tdbisum += ($cbitot - $idbitot);
|
||
}
|
||
|
||
storeReading ($prpo.'_'.$kpi, (sprintf "%.1f", $tdbisum).' '.$hcsr{$kpi}{unit});
|
||
}
|
||
|
||
if ($kpi eq 'todayBatOutSum') { # Summe tägl. Entadeenergie (alle Batterien)
|
||
my $tdbosum = 0;
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
|
||
my $idbotot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'initdaybatouttot'.$bn, $def); # initialer Tagesstartwert Batterie Out total
|
||
my $cbotot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'batouttot'.$bn, $def); # aktuelles total Batterie Out
|
||
|
||
$tdbosum += ($cbotot - $idbotot);
|
||
}
|
||
|
||
storeReading ($prpo.'_'.$kpi, (sprintf "%.1f", $tdbosum).' '.$hcsr{$kpi}{unit});
|
||
}
|
||
|
||
if ($kpi =~ /todayBatIn_/xs) {
|
||
my $bn = (split "_", $kpi)[1]; # Batterienummer extrahieren
|
||
my $idbitot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'initdaybatintot'.$bn, $def); # initialer Tagesstartwert Batterie In total
|
||
my $cbitot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'batintot'.$bn, $def); # aktuell total Batterieladung (Wh)
|
||
|
||
my $dbi = $cbitot - $idbitot;
|
||
|
||
storeReading ($prpo.'_'.$kpi, (sprintf "%.1f", $dbi).' '.$hcsr{$kpi}{unit});
|
||
}
|
||
|
||
if ($kpi =~ /todayBatOut_/xs) {
|
||
my $bn = (split "_", $kpi)[1]; # Batterienummer extrahieren
|
||
my $idbotot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'initdaybatouttot'.$bn, $def); # initialer Tagesstartwert Batterie Out total
|
||
my $cbotot = &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, 'batouttot'.$bn, $def); # aktuelles total Batterie Out
|
||
|
||
my $dbo = $cbotot - $idbotot;
|
||
|
||
storeReading ($prpo.'_'.$kpi, (sprintf "%.1f", $dbo).' '.$hcsr{$kpi}{unit});
|
||
}
|
||
|
||
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 ",", AttrVal ($name, 'setupInverterStrings', '');
|
||
my $fcsumdat = 0;
|
||
my $type = $paref->{type};
|
||
|
||
for my $strg (@allstrings) {
|
||
for my $starttmstr (sort keys %{$data{$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 ($prpo.'_'.$kpi, (int $fcsumdat). ' Wh');
|
||
}
|
||
else {
|
||
storeReading ($prpo.'_'.$kpi, $fcsumdat. ' (no data available)');
|
||
}
|
||
}
|
||
|
||
if ($kpi =~ /currentRunMtsConsumer_/xs) {
|
||
my $c = (split "_", $kpi)[1]; # Consumer Nummer extrahieren
|
||
|
||
if (!AttrVal ($name, 'consumer'.$c, '')) {
|
||
readingsDelete ($hash, $prpo.'_currentRunMtsConsumer_'.$c);
|
||
return;
|
||
}
|
||
|
||
my $mion = &{$hcsr{$kpi}{fn}} ($hash, $c, $hcsr{$kpi}{par}, $def);
|
||
|
||
storeReading ($prpo.'_'.$kpi, (sprintf "%.0f", $mion).$hcsr{$kpi}{unit});
|
||
}
|
||
|
||
if ($kpi =~ /runTimeAvgDayConsumer_/xs) {
|
||
my $c = (split "_", $kpi)[1]; # Consumer Nummer extrahieren
|
||
|
||
if (!AttrVal ($name, 'consumer'.$c, '')) {
|
||
readingsDelete ($hash, $prpo.'_runTimeAvgDayConsumer_'.$c);
|
||
return;
|
||
}
|
||
|
||
my $radc = &{$hcsr{$kpi}{fn}} ($hash, $c, $hcsr{$kpi}{par}, $def);
|
||
|
||
storeReading ($prpo.'_'.$kpi, $radc.$hcsr{$kpi}{unit});
|
||
}
|
||
|
||
if ($kpi eq 'todayConsumptionForecast') {
|
||
my $type = $paref->{type};
|
||
|
||
for my $idx (sort keys %{$data{$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 ($prpo.'_'.$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{$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 ($prpo.'_'.$kpi, ($confc ? (sprintf "%.0f", $confc).$hcsr{$kpi}{unit} : '-'));
|
||
}
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
###################################################################################
|
||
# Erstellen System-Messages für Notification System
|
||
# Struktur:
|
||
# 0|SV|1
|
||
# 0|DE|Mitteilung ....
|
||
# 0|EN|Message...
|
||
# $data{$name}{preparedmessages}{999500}{TS}: Timestamp Stand prepared Messages
|
||
###################################################################################
|
||
sub _readSystemMessages {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
|
||
delete $data{$name}{preparedmessages};
|
||
|
||
my $midx = 0;
|
||
|
||
#if (!ReadingsVal ($name, '.migrated', 0)) {
|
||
# $midx++;
|
||
# $data{$name}{preparedmessages}{$midx}{SV} = 1;
|
||
# $data{$name}{preparedmessages}{$midx}{DE} = 'Die gespeicherten PV Daten können mit "get ... x_migrate" in ein neues Format umgesetzt werden welches den Median Ansatz bei der PV Prognose aktiviert und nutzt.';
|
||
# $data{$name}{preparedmessages}{$midx}{DE} .= '<br>Mit einem späteren Update des Moduls erfolgt diese Umstellung automatisch.';
|
||
# $data{$name}{preparedmessages}{$midx}{EN} = 'The stored PV data can be converted with “get ... x_migrate” into a new format which activates and uses the median approach in the PV forecast.';
|
||
# $data{$name}{preparedmessages}{$midx}{EN} .= '<br>With a later update of the module, this changeover will take place automatically.';
|
||
#}
|
||
|
||
$data{$name}{preparedmessages}{999500}{TS} = time;
|
||
|
||
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 = "<html>";
|
||
$ret .= entryGraphic ($name);
|
||
$ret .= "</html>";
|
||
|
||
return $ret;
|
||
}
|
||
|
||
################################################################
|
||
# 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 = "<html>";
|
||
$ret .= entryGraphic ($name, $ftui, $gsel, 1);
|
||
$ret .= "</html>";
|
||
|
||
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
|
||
|
||
AttrVal ($name, 'graphicShowNight', 0) =~ /(.)(.)?/xs;
|
||
my $show_night = $1 // 0;
|
||
my $layersync = $2 // 0;
|
||
|
||
my $w = $width * $maxhours; # gesammte Breite der Ausgabe , WetterIcon braucht ca. 34px
|
||
my $offset = -1 * AttrNum ($name, 'graphicHistoryHour', HISTHOURDEF);
|
||
my $dlink = qq{<a href="$::FW_ME$::FW_subdir?detail=$name">$alias</a>};
|
||
|
||
if (!$gsel) {
|
||
$gsel = AttrVal ($name, 'graphicSelect', 'both'); # Auswahl der anzuzeigenden Grafiken
|
||
}
|
||
|
||
my $paref = {
|
||
name => $name,
|
||
hash => $hash,
|
||
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'),
|
||
beam3cont => AttrVal ($name, 'graphicBeam3Content', ''),
|
||
beam4cont => AttrVal ($name, 'graphicBeam4Content', ''),
|
||
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, 'graphicBeamHeightLevel1', 200),
|
||
width => $width,
|
||
fsize => AttrNum ($name, 'graphicSpaceSize', 24),
|
||
layersync => $layersync, # Zeitsynchronisation zwischen Ebene 1 und den folgenden Balkengrafikebenen
|
||
show_night => $show_night, # alle Balken (Spalten) anzeigen ?
|
||
show_diff => AttrVal ($name, 'graphicShowDiff', 'no'), # zusätzliche Anzeige $di{} in allen Typen
|
||
weather => AttrNum ($name, 'graphicShowWeather', 1), # Wetter Icons anzeigen
|
||
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 => CurrentVal ($hash, 'size', FLOWGSIZEDEF), # Größe Energieflußgrafik
|
||
flowgani => CurrentVal ($hash, 'animate', 1), # Animation Energieflußgrafik
|
||
flowgxshift => CurrentVal ($hash, 'shiftx', 0), # X-Verschiebung der Flußgrafikbox (muß negiert werden)
|
||
flowgyshift => CurrentVal ($hash, 'shifty', 0), # Y-Verschiebung der Flußgrafikbox (muß negiert werden)
|
||
flowgcons => CurrentVal ($hash, 'showconsumer', 1), # Verbraucher in der Energieflußgrafik anzeigen
|
||
flowgconX => CurrentVal ($hash, 'showconsumerdummy', 1), # Dummyverbraucher in der Energieflußgrafik anzeigen
|
||
flowgconsPower => CurrentVal ($hash, 'showconsumerpower', 1), # Verbraucher Leistung in der Energieflußgrafik anzeigen
|
||
flowgconsTime => CurrentVal ($hash, 'showconsumerremaintime', 1), # Verbraucher Restlaufeit in der Energieflußgrafik anzeigen
|
||
flowgconsDist => CurrentVal ($hash, 'consumerdist', FGCDDEF), # Abstand Verbrauchericons zueinander
|
||
flowgh2cDist => CurrentVal ($hash, 'h2consumerdist', 0), # Erweiterung des vertikalen Abstandes Haus -> Consumer
|
||
genpvdva => AttrVal ($name, 'ctrlGenPVdeviation', 'daily'), # Methode der Abweichungsberechnung
|
||
lang => getLang ($hash),
|
||
debug => getDebug ($hash), # Debug Module
|
||
};
|
||
|
||
my $ret = q{};
|
||
|
||
$ret .= "<span>$dlink </span><br>" if(AttrVal($name, 'ctrlShowLink', 0));
|
||
|
||
#$ret .= "<style>TD.solarfc {text-align: center; padding-left:1px; padding-right:1px; margin:0px;}</style>";
|
||
$ret .= "<style>TD.solarfc {text-align: center; padding-left:5px; padding-right:5px; margin:0px;}</style>";
|
||
$ret .= "<table class='roomoverview' width='$w' style='width:".$w."px'><tr class='devTypeTr'></tr>";
|
||
$ret .= "<tr><td class='solarfc'>";
|
||
|
||
# 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<table class='block'>"; # das \n erleichtert das Lesen der debug Quelltextausgabe
|
||
my $m = $paref->{modulo} % 2;
|
||
|
||
if ($header) { # Header ausgeben
|
||
$ret .= "<tr class='$htr{$m}{cl}'>";
|
||
$ret .= "<td colspan='".($maxhours+2)."' align='center' style='word-break: normal'>$header</td>";
|
||
$ret .= "</tr>";
|
||
|
||
$paref->{modulo}++;
|
||
}
|
||
|
||
my $clegend = $paref->{clegend};
|
||
$m = $paref->{modulo} % 2;
|
||
|
||
if ($legendtxt && $clegend eq 'top') {
|
||
$ret .= "<tr class='$htr{$m}{cl}'>";
|
||
$ret .= "<td colspan='".($maxhours+2)."' align='center' style='padding-left: 10px; padding-top: 5px; padding-bottom: 5px; word-break: normal'>";
|
||
$ret .= $legendtxt;
|
||
$ret .= "</td>";
|
||
$ret .= "</tr>";
|
||
|
||
$paref->{modulo}++;
|
||
}
|
||
|
||
$m = $paref->{modulo} % 2;
|
||
|
||
## Balkengrafiken
|
||
###################
|
||
|
||
## Balkengrafik Ebene 1
|
||
#########################
|
||
if ($gsel =~ /both/xs || $gsel =~ /forecast/xs) {
|
||
my %hfcg1;
|
||
$paref->{chartlvl} = 1; # Balkengrafik Ebene 1
|
||
|
||
## Werte aktuelle Stunde
|
||
##########################
|
||
$paref->{hfcg} = \%hfcg1; # hfcg = hash forecast graphic
|
||
$paref->{thishour} = _beamGraphicFirstHour ($paref);
|
||
|
||
## get consumer list and display it in Graphics
|
||
#################################################
|
||
#_showConsumerInGraphicBeam ($paref); # keine Verwendung zur Zeit
|
||
|
||
## 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
|
||
|
||
$ret .= _beamGraphic ($paref);
|
||
|
||
delete $paref->{maxVal}; # bereinigen vor nächster Ebene
|
||
delete $paref->{maxCon};
|
||
delete $paref->{maxDif};
|
||
delete $paref->{minDif};
|
||
|
||
## Balkengrafik Ebene 2
|
||
#########################
|
||
if ($paref->{beam3cont} || $paref->{beam4cont}) { # Balkengrafik Ebene 2
|
||
my %hfcg2;
|
||
|
||
$paref->{chartlvl} = 2; # Balkengrafik Ebene 2
|
||
$paref->{beam1cont} = $paref->{beam3cont};
|
||
$paref->{beam2cont} = $paref->{beam4cont};
|
||
$paref->{colorb1} = AttrVal ($name, 'graphicBeam3Color', B3COLDEF);
|
||
$paref->{colorb2} = AttrVal ($name, 'graphicBeam4Color', B4COLDEF);
|
||
$paref->{fcolor1} = AttrVal ($name, 'graphicBeam3FontColor', B3FONTCOLDEF);
|
||
$paref->{fcolor2} = AttrVal ($name, 'graphicBeam4FontColor', B4FONTCOLDEF);
|
||
$paref->{height} = AttrVal ($name, 'graphicBeamHeightLevel2', $paref->{height});
|
||
$paref->{weather} = 0;
|
||
|
||
# Werte aktuelle Stunde
|
||
##########################
|
||
$paref->{hfcg} = \%hfcg2;
|
||
$paref->{thishour} = _beamGraphicFirstHour ($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 Ausgabe
|
||
########################
|
||
$ret .= _beamGraphic ($paref);
|
||
|
||
delete $paref->{maxVal}; # bereinigen vor nächster Ebene
|
||
delete $paref->{maxCon};
|
||
delete $paref->{maxDif};
|
||
delete $paref->{minDif};
|
||
}
|
||
|
||
$paref->{modulo}++;
|
||
}
|
||
|
||
$m = $paref->{modulo} % 2;
|
||
|
||
# Flußgrafik
|
||
##############
|
||
if ($gsel =~ /both/xs || $gsel =~ /flow/xs) {
|
||
$ret .= "<tr class='$htr{$m}{cl}'>";
|
||
my $fg = _flowGraphic ($paref);
|
||
$ret .= "<td colspan='".($maxhours+2)."' align='center' style='word-break: normal'>";
|
||
$ret .= "$fg</td>";
|
||
$ret .= "</tr>";
|
||
|
||
$paref->{modulo}++;
|
||
}
|
||
|
||
$m = $paref->{modulo} % 2;
|
||
|
||
# Legende unten
|
||
#################
|
||
if ($legendtxt && ($clegend eq 'bottom')) {
|
||
$ret .= "<tr class='$htr{$m}{cl}'>";
|
||
#$ret .= "<td colspan='".($maxhours+2)."' align='center' style='word-break: normal'>";
|
||
$ret .= "<td colspan='".($maxhours+2)."' align='center' style='padding-left: 10px; padding-top: 5px; padding-bottom: 5px; word-break: normal'>";
|
||
$ret .= "$legendtxt</td>";
|
||
$ret .= "</tr>";
|
||
}
|
||
|
||
$ret .= "</table>";
|
||
|
||
$ret .= "</td></tr>";
|
||
$ret .= "</table>";
|
||
|
||
return $ret;
|
||
}
|
||
|
||
################################################################
|
||
# Vollständigkeit Setup prüfen
|
||
################################################################
|
||
sub _checkSetupNotComplete {
|
||
my $hash = shift;
|
||
my $ret = q{};
|
||
|
||
my $name = $hash->{NAME};
|
||
my $type = $hash->{TYPE};
|
||
|
||
my $strings = AttrVal ($name, 'setupInverterStrings', undef); # String Konfig
|
||
my $wedev = AttrVal ($name, 'setupWeatherDev1', undef); # Device Vorhersage Wetterdaten (Bewölkung etc.)
|
||
my $radev = AttrVal ($name, 'setupRadiationAPI', undef); # Device Strahlungsdaten Vorhersage
|
||
my $indev = AttrVal ($name, 'setupInverterDev01', undef); # Inverter Device
|
||
my $medev = AttrVal ($name, 'setupMeterDev', undef); # Meter Device
|
||
my $peaks = AttrVal ($name, 'setupStringPeak', undef); # String Peak
|
||
my $maz = ReadingsVal ($name, 'setupStringAzimuth', undef); # Modulausrichtung Konfig (Azimut)
|
||
my $mdec = ReadingsVal ($name, 'setupStringDeclination', undef); # Modul Neigungswinkel Konfig
|
||
my $mrt = AttrVal ($name, 'setupRoofTops', undef); # RoofTop Konfiguration (SolCast API)
|
||
|
||
my $vrmcr = StatusAPIVal ($hash, '?VRM', '?API', 'credentials', ''); # Victron VRM Credentials gesetzt
|
||
|
||
my ($coset, $lat, $lon) = locCoordinates(); # Koordinaten im global device
|
||
my $rip;
|
||
$rip = 1 if(exists $data{$name}{statusapi}{'?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{<a href="$::FW_ME$::FW_subdir?detail=$name">$name</a>};
|
||
my $height = AttrNum ($name, 'graphicBeamHeightLevel1', 200);
|
||
my $lang = getLang ($hash);
|
||
|
||
my (undef, $disabled, $inactive) = controller ($name);
|
||
|
||
if ($disabled || $inactive) {
|
||
$ret .= "<table class='roomoverview'>";
|
||
$ret .= "<tr style='height:".$height."px'>";
|
||
$ret .= "<td>";
|
||
$ret .= qq{SolarForecast device $link is disabled or inactive};
|
||
$ret .= "</td>";
|
||
$ret .= "</tr>";
|
||
$ret .= "</table>";
|
||
|
||
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 = "<a onClick=$cmdplchk>$img</a>";
|
||
my $chktitle = $htitles{plchk}{$lang};
|
||
|
||
if (!$strings || !$wedev || !$radev || !$indev || !$medev || !$peaks ||
|
||
(isSolCastUsed ($hash) ? (!$rip || !$mrt) : isVictronKiUsed ($hash) ? !$vrmcr : (!$maz || !$mdec )) ||
|
||
(isForecastSolarUsed ($hash) ? !$coset : '') ||
|
||
(isOpenMeteoUsed ($hash) ? !$coset : '') ||
|
||
!defined $pv0) {
|
||
$ret .= "<table class='roomoverview'>";
|
||
$ret .= "<tr style='height:".$height."px'>";
|
||
$ret .= "<td>";
|
||
$ret .= $hqtxt{entry}{$lang}; # Entry Text
|
||
|
||
if (!$strings) { ## no critic 'Cascading'
|
||
$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 (!$wedev) {
|
||
$ret .= $hqtxt{cfd}{$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 .= "</td>";
|
||
$ret .= "</tr>";
|
||
$ret .= "<tr>";
|
||
$ret .= qq{<td align="left" title="$chktitle"> $chkicon};
|
||
}
|
||
|
||
$ret .= "</td>";
|
||
$ret .= "</tr>";
|
||
$ret .= "</table>";
|
||
$ret =~ s/LINK/$link/gxs;
|
||
|
||
delete $data{$name}{current}{setupcomplete};
|
||
|
||
return $ret;
|
||
}
|
||
|
||
$data{$name}{current}{setupcomplete} = 1;
|
||
|
||
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 $kw = $paref->{kw};
|
||
my $dstyle = $paref->{dstyle}; # TD-Style
|
||
|
||
my $hash = $defs{$name};
|
||
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);
|
||
|
||
my ($rapi, $wapi) = getStatusApiName ($hash); # Status-API Name
|
||
|
||
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{<table width='100%'>};
|
||
|
||
# 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 Kommando generieren
|
||
my $cmdoutmsg = qq{"FW_cmd('$::FW_ME$::FW_subdir?XHR=1&cmd=get $name outputMessages', function(data){FW_okDialog(data)})"}; # Message Ausgabe Kommando generieren
|
||
|
||
if ($ftui eq 'ftui') {
|
||
$cmdplchk = qq{"ftui.setFhemStatus('get $name plantConfigCheck')"};
|
||
$cmdoutmsg = qq{"ftui.setFhemStatus('get $name outputMessages')"};
|
||
}
|
||
|
||
## Anlagen Check-Icon
|
||
#######################
|
||
$img = FW_makeImage('edit_settings@grey');
|
||
my $chkicon = "<a onClick=$cmdplchk>$img</a>";
|
||
my $chktitle = $htitles{plchk}{$lang};
|
||
|
||
## Forum Thread-Icon
|
||
######################
|
||
$img = FW_makeImage('time_note@grey');
|
||
my $fthicon = "<a href='https://forum.fhem.de/index.php?topic=137058.0' target='_blank'>$img</a>";
|
||
my $fthtitle = $htitles{jtsfft}{$lang};
|
||
|
||
## Wiki-Icon
|
||
##############
|
||
$img = FW_makeImage ('edit_copy@grey');
|
||
my $wikicon = "<a href='https://wiki.fhem.de/wiki/SolarForecast_-_Solare_Prognose_(PV_Erzeugung)_und_Verbrauchersteuerung' target='_blank'>$img</a>";
|
||
my $wiktitle = $htitles{opwiki}{$lang};
|
||
|
||
## Message-Icon
|
||
#################
|
||
my ($micon, $midx) = fillupMessageSystem ($paref);
|
||
$img = FW_makeImage ($micon);
|
||
my $msgicon = $midx ? "<a onClick=$cmdoutmsg>$img</a>" : $img;
|
||
my $msgtitle = $midx ? $htitles{outpmsg}{$lang} : $htitles{nomsgfo}{$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>/$name/xs;
|
||
$img = FW_makeImage ('10px-kreis-gelb.png', $agewfc.' '.$htitles{dwdtime}{$lang}.': '.$resh->{fctime});
|
||
}
|
||
|
||
my $waicon = "<a>$img</a>"; # Icon Wetterdaten Alter
|
||
|
||
## Autokorrektur-Icon
|
||
######################
|
||
my $acicon = __createAutokorrIcon ($paref);
|
||
|
||
## Solare API Sektion
|
||
########################
|
||
my $api = isSolCastUsed ($hash) ? '<a href="https://solcast.com/live-and-forecast" style="color: inherit !important;" target="_blank">SolCast</a>:' :
|
||
isForecastSolarUsed ($hash) ? '<a href="https://forecast.solar" style="color: inherit !important;" target="_blank">Forecast.Solar</a>:' :
|
||
isVictronKiUsed ($hash) ? '<a href="https://www.victronenergy.com/blog/2023/07/05/new-vrm-solar-production-forecast-feature" style="color: inherit !important;" target="_blank">VictronVRM</a>:' :
|
||
isDWDUsed ($hash) ? '<a href="https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/met_verfahren_mosmix.html" style="color: inherit !important;" target="_blank">DWD</a>:' :
|
||
isOpenMeteoUsed ($hash) ? '<a href="https://open-meteo.com/" style="color: inherit !important;" target="_blank">OpenMeteo</a>:' :
|
||
q{};
|
||
|
||
my $nscc = ReadingsVal ($name, 'nextRadiationAPICall', '?');
|
||
my $lrt = StatusAPIVal ($hash, $rapi, '?All', 'lastretrieval_time', '-');
|
||
my $scrm = StatusAPIVal ($hash, $rapi, '?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 = "<a>$img</a>";
|
||
|
||
$api .= ' '.$scicon;
|
||
$api .= '<span title="'.$htitles{dapic}{$lang}.' / '.$htitles{rapic}{$lang}.'">';
|
||
$api .= ' (';
|
||
$api .= StatusAPIVal ($hash, $rapi, '?All', 'todayDoneAPIrequests', 0);
|
||
$api .= '/';
|
||
$api .= StatusAPIVal ($hash, $rapi, '?All', 'todayRemainingAPIrequests', SOLCMAXREQDEF);
|
||
$api .= ')';
|
||
$api .= '</span>';
|
||
}
|
||
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 = "<a>$img</a>";
|
||
|
||
$api .= ' '.$scicon;
|
||
$api .= '<span title="'.$htitles{dapic}{$lang}.' / '.$htitles{raricp}{$lang}.'">';
|
||
$api .= ' (';
|
||
$api .= StatusAPIVal ($hash, $rapi, '?All', 'todayDoneAPIrequests', 0);
|
||
$api .= '/';
|
||
$api .= StatusAPIVal ($hash, $rapi, '?All', 'requests_remaining', '-');
|
||
$api .= ')';
|
||
$api .= '</span>';
|
||
}
|
||
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 = "<a>$img</a>";
|
||
|
||
$api .= ' '.$scicon;
|
||
$api .= '<span title="'.$htitles{dapic}{$lang}.'">';
|
||
$api .= ' (';
|
||
$api .= StatusAPIVal ($hash, $rapi, '?All', 'todayDoneAPIrequests', 0);
|
||
$api .= ')';
|
||
$api .= '</span>';
|
||
}
|
||
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>/$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 = "<a>$img</a>";
|
||
|
||
$api .= ' '.$scicon;
|
||
$api .= '<span title="'.$htitles{dapic}{$lang}.'">';
|
||
$api .= ' (';
|
||
$api .= StatusAPIVal ($hash, $rapi, '?All', 'todayDoneAPIrequests', 0);
|
||
$api .= ')';
|
||
$api .= '</span>';
|
||
}
|
||
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 = "<a>$img</a>";
|
||
|
||
$api .= ' '.$scicon;
|
||
$api .= '<span title="'.$htitles{dapic}{$lang}.' / '.$htitles{rapic}{$lang}.'">';
|
||
$api .= ' (';
|
||
$api .= StatusAPIVal ($hash, $rapi, '?All', 'todayDoneAPIrequests', 0);
|
||
$api .= '/';
|
||
$api .= StatusAPIVal ($hash, $rapi, '?All', 'todayRemainingAPIrequests', OMETMAXREQ);
|
||
$api .= ')';
|
||
$api .= '</span>';
|
||
}
|
||
|
||
## 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}).': '."<b>".$tdayDvtn."</b>";
|
||
my $ydaytxt = $hqtxt{yday}{$lang}.': '."<b>".$ydayDvtn."</b>";
|
||
|
||
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{<a href="$::FW_ME$::FW_subdir?detail=$name">$alias</a>};
|
||
my $space = ' ';
|
||
my $disti = qq{<span title="$chktitle"> $chkicon </span> $space <span title="$fthtitle"> $fthicon </span> $space <span title="$wiktitle"> $wikicon </span> $space <span title="$msgtitle"> $msgicon </span>};
|
||
|
||
$header .= qq{<tr>};
|
||
$header .= qq{<td colspan="1" align="left" $dstyle> <b>$dlink</b> </td>};
|
||
$header .= qq{<td colspan="2" align="center" $dstyle> $disti </td>};
|
||
$header .= qq{<td colspan="3" align="left" $dstyle> $lupt $lup $upicon </td>};
|
||
$header .= qq{<td colspan="3" align="right" $dstyle> $api </td>};
|
||
$header .= qq{</tr>};
|
||
$header .= qq{<tr>};
|
||
$header .= qq{<td colspan="3" align="left" $dstyle> $sriseimg $srisetxt $ssetimg $ssettxt $waicon </td>};
|
||
$header .= qq{<td colspan="3" align="left" $dstyle> $autoct $acicon $lbpcq $pcqicon $aihtxt $aiicon </td>};
|
||
$header .= qq{<td colspan="3" align="right" $dstyle> $dvtntxt};
|
||
$header .= qq{<span title="$text_tdayDvtn">};
|
||
$header .= qq{$tdaytxt};
|
||
$header .= qq{</span>};
|
||
$header .= qq{, };
|
||
$header .= qq{<span title="$text_ydayDvtn">};
|
||
$header .= qq{$ydaytxt};
|
||
$header .= qq{</span>};
|
||
$header .= qq{</td>};
|
||
$header .= qq{</tr>};
|
||
$header .= qq{<tr>};
|
||
$header .= qq{<td colspan="9" align="left" $dstyle><hr></td>};
|
||
$header .= qq{</tr>};
|
||
}
|
||
|
||
# Header Information pv
|
||
########################
|
||
if ($hdrDetail =~ /all|pv/xs) {
|
||
$header .= "<tr>";
|
||
$header .= "<td $dstyle><b>".$hqtxt{pvgen}{$lang}." </b></td>";
|
||
$header .= "<td $dstyle><b>$lblPvCu</b></td> <td align=right $dstyle>$pvCu</td>";
|
||
$header .= "<td $dstyle><b>$lblPv4h</b></td> <td align=right $dstyle>$pv4h</td>";
|
||
$header .= "<td $dstyle><b>$lblPvRe</b></td> <td align=right $dstyle>$pvRe</td>";
|
||
$header .= "<td $dstyle><b>$lblPvTo</b></td> <td align=right $dstyle>$pvTo</td>";
|
||
$header .= "</tr>";
|
||
}
|
||
|
||
|
||
# Header Information co
|
||
#########################
|
||
if ($hdrDetail =~ /all|co/xs) {
|
||
$header .= "<tr>";
|
||
$header .= "<td $dstyle><b>".$hqtxt{conspt}{$lang}." </b></td>";
|
||
$header .= "<td $dstyle><b>$lblPvCu</b></td><td align=right $dstyle>$coCu</td>";
|
||
$header .= "<td $dstyle><b>$lblPv4h</b></td><td align=right $dstyle>$co4h</td>";
|
||
$header .= "<td $dstyle><b>$lblPvRe</b></td><td align=right $dstyle>$coRe</td>";
|
||
$header .= "<td $dstyle><b>$lblPvTo</b></td><td align=right $dstyle>$coTo</td>";
|
||
$header .= "</tr>";
|
||
}
|
||
|
||
if ($hdrDetail =~ /all|pv|co/xs) {
|
||
$header .= qq{<tr>};
|
||
$header .= qq{<td colspan="9" align="left" $dstyle><hr></td>};
|
||
$header .= qq{</tr>};
|
||
}
|
||
|
||
# Header User Spezifikation
|
||
#############################
|
||
my $ownv = __createOwnSpec ($paref);
|
||
$header .= $ownv if($ownv);
|
||
|
||
$header .= qq{</table>};
|
||
|
||
return $header;
|
||
}
|
||
|
||
################################################################
|
||
# erstelle Update-Icon
|
||
################################################################
|
||
sub __createUpdateIcon {
|
||
my $paref = shift;
|
||
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 = "<a onClick=$cmdupdate>$img</a>";
|
||
}
|
||
elsif ($upstate =~ /running/ix) {
|
||
$img = FW_makeImage('10px-kreis-gelb.png', 'running');
|
||
$upicon = "<a>$img</a>";
|
||
}
|
||
elsif ($upstate =~ /initialized/ix) {
|
||
$img = FW_makeImage('1px-spacer.png', 'initialized');
|
||
$upicon = "<a>$img</a>";
|
||
}
|
||
else {
|
||
$img = FW_makeImage('10px-kreis-rot.png', $htitles{upd}{$lang}.' ('.$htitles{natc}{$lang}.' '.$naup.')');
|
||
$upicon = "<a onClick=$cmdupdate>$img</a>";
|
||
}
|
||
|
||
return $upicon;
|
||
}
|
||
|
||
################################################################
|
||
# erstelle Autokorrektur-Icon
|
||
################################################################
|
||
sub __createAutokorrIcon {
|
||
my $paref = shift;
|
||
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>/$name/xs;
|
||
$aciimg = '-';
|
||
}
|
||
|
||
my $acicon = qq{<a title="$acitit">$aciimg</a>};
|
||
|
||
return $acicon;
|
||
}
|
||
|
||
################################################################
|
||
# erstelle Qualitäts-Icon
|
||
################################################################
|
||
sub __createQuaIcon {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $lang = $paref->{lang};
|
||
my $ftui = $paref->{ftui};
|
||
|
||
my $hash = $defs{$name};
|
||
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{<a title="$pcqtit", onClick=$cmdfcqal>$pcqimg</a>};
|
||
|
||
return $pcqicon;
|
||
}
|
||
|
||
################################################################
|
||
# erstelle KI Icon
|
||
################################################################
|
||
sub __createAIicon {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $lang = $paref->{lang};
|
||
|
||
my $hash = $defs{$name};
|
||
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>/$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{<a title="$aitit">$aiimg</a>};
|
||
|
||
return $aiicon;
|
||
}
|
||
|
||
################################################################
|
||
# erstelle Übersicht eigener Readings
|
||
################################################################
|
||
sub __createOwnSpec {
|
||
my $paref = shift;
|
||
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 = '<hidden by pageAsHtml>';
|
||
}
|
||
|
||
$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 = '<hidden by pageAsHtml>';
|
||
}
|
||
|
||
$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 .= "<tr>";
|
||
$ownv .= "<td $dstyle>".($cats[$i-1] ? '<b>'.$cats[$i-1].'</b>' : '')."</td>";
|
||
$ownv .= "<td $dstyle><b>".$h->{0}{label}.":</b></td> <td align=right $dstyle>".$v->{0}." ".$u->{0}."</td>" if(defined $h->{0}{label});
|
||
$ownv .= "<td $dstyle><b>".$h->{1}{label}.":</b></td> <td align=right $dstyle>".$v->{1}." ".$u->{1}."</td>" if(defined $h->{1}{label});
|
||
$ownv .= "<td $dstyle><b>".$h->{2}{label}.":</b></td> <td align=right $dstyle>".$v->{2}." ".$u->{2}."</td>" if(defined $h->{2}{label});
|
||
$ownv .= "<td $dstyle><b>".$h->{3}{label}.":</b></td> <td align=right $dstyle>".$v->{3}." ".$u->{3}."</td>" if(defined $h->{3}{label});
|
||
$ownv .= "</tr>";
|
||
}
|
||
|
||
$ownv .= qq{<tr>};
|
||
$ownv .= qq{<td colspan="9" align="left" $dstyle><hr></td>};
|
||
$ownv .= qq{</tr>};
|
||
|
||
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 =~ /((<td|<div|<\/div>).*?)/xs) { # Eleminierung von störenden HTML Elementen aus aktuellem Readingwert
|
||
$current = ' ';
|
||
}
|
||
|
||
$current =~ s/$elm //;
|
||
$current = ReplaceEventMap ($dev, $current, 1);
|
||
|
||
return "<div class='fhemWidget' cmd='$elm' reading='$reading' ".
|
||
"dev='$dev' arg='$arg' current='$current' type='$ctyp'></div>";
|
||
}
|
||
|
||
################################################################
|
||
# ownHeader ValueFormat
|
||
################################################################
|
||
sub ___ghoValForm {
|
||
my $paref = shift;
|
||
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 = AttrVal ($name, 'graphicHeaderOwnspecValForm', '');
|
||
|
||
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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $hfcg = $paref->{hfcg};
|
||
my $lang = $paref->{lang};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
# get consumer list and display it in Graphics
|
||
################################################
|
||
my @consumers = sort{$a<=>$b} keys %{$data{$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 $name = $paref->{name};
|
||
my ($clegendstyle, $clegend) = split '_', $paref->{clegend};
|
||
my $clink = $paref->{clink};
|
||
|
||
my @consumers = sort{$a<=>$b} keys %{$data{$name}{consumers}}; # definierte Verbraucher ermitteln
|
||
$clegend = '' if($clegendstyle eq 'none' || !int @consumers);
|
||
$paref->{clegend} = $clegend;
|
||
|
||
return if(!$clegend );
|
||
|
||
my $hash = $defs{$name};
|
||
my $ftui = $paref->{ftui};
|
||
my $lang = $paref->{lang};
|
||
my $dstyle = $paref->{dstyle}; # TD-Style
|
||
|
||
my $staticon;
|
||
|
||
## Tabelle Start
|
||
#################
|
||
my $ctable = qq{<table align='left' width='100%'>};
|
||
$ctable .= qq{<tr style='font-weight:bold; text-align:center;'>};
|
||
|
||
$ctable .= qq{<td style='text-align:left' $dstyle> $hqtxt{cnsm}{$lang} </td>};
|
||
$ctable .= qq{<td> </td>};
|
||
$ctable .= qq{<td> </td>};
|
||
$ctable .= qq{<td $dstyle> $hqtxt{eiau}{$lang} </td>};
|
||
$ctable .= qq{<td $dstyle> $hqtxt{auto}{$lang} </td>};
|
||
|
||
$ctable .= qq{<td> </td>};
|
||
$ctable .= qq{<td> </td>};
|
||
|
||
my $cnum = @consumers;
|
||
|
||
if ($cnum > 1) {
|
||
$ctable .= qq{<td style='text-align:left' $dstyle> $hqtxt{cnsm}{$lang} </td>};
|
||
$ctable .= qq{<td> </td>};
|
||
$ctable .= qq{<td> </td>};
|
||
$ctable .= qq{<td $dstyle> $hqtxt{eiau}{$lang} </td>};
|
||
$ctable .= qq{<td $dstyle> $hqtxt{auto}{$lang} </td>};
|
||
}
|
||
else {
|
||
my $blk = ' ' x 8;
|
||
$ctable .= qq{<td $dstyle> $blk </td>};
|
||
$ctable .= qq{<td> $blk </td>};
|
||
$ctable .= qq{<td> $blk </td>};
|
||
$ctable .= qq{<td $dstyle> $blk </td>};
|
||
$ctable .= qq{<td $dstyle> $blk </td>};
|
||
}
|
||
|
||
$ctable .= qq{</tr>};
|
||
|
||
if ($clegend ne 'top') {
|
||
$ctable .= qq{<tr><td colspan="12"><hr></td></tr>};
|
||
}
|
||
|
||
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 ($err, $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 $mode = getConsumerPlanningMode ($hash, $c); # Planungsmode 'can' oder 'must'
|
||
|
||
my $pstate = $caicon eq "times" ? $hqtxt{pstate}{$lang} : $htitles{pstate}{$lang};
|
||
my $surplusinfo = isConsRcmd($hash, $c) ? $htitles{splus}{$lang} : $htitles{nosplus}{$lang};
|
||
|
||
$pstate =~ s/<mode>/$mode/xs;
|
||
$pstate =~ s/<pstate>/$planstate/xs;
|
||
$pstate =~ s/<supplmnt>/$supplmnt/xs;
|
||
$pstate =~ s/<start>/$starttime/xs;
|
||
$pstate =~ s/<stop>/$stoptime/xs;
|
||
$pstate =~ s/<RLT>/$rlt/xs;
|
||
$pstate =~ s/\s+/ /gxs if($caicon eq "times");
|
||
|
||
if ($clink) {
|
||
$calias = qq{<a title="$cname" href="$::FW_ME$::FW_subdir?detail=$cname" style="color: inherit !important;" target="_blank">$c - $calias</a>};
|
||
}
|
||
|
||
if ($caicon ne "none") {
|
||
if (isInTimeframe($hash, $c)) { # innerhalb Planungszeitraum ?
|
||
if ($caicon eq "times") {
|
||
$isricon = $pstate.'<br>'.$surplusinfo;
|
||
}
|
||
else {
|
||
$isricon = "<a title='$htitles{conrec}{$lang}\n\n$surplusinfo\n$pstate' onClick=$implan>".FW_makeImage($caicon, '')." </a>";
|
||
if ($planstate =~ /priority/xs) {
|
||
my (undef,$color) = split '@', $caicon;
|
||
$color = $color ? '@'.$color : '';
|
||
$isricon = "<a title='$htitles{conrecba}{$lang}\n\n$surplusinfo\n$pstate' onClick=$implan>".FW_makeImage('batterie'.$color, '')." </a>";
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
if ($caicon eq "times") {
|
||
$isricon = $pstate.'<br>'.$surplusinfo;
|
||
}
|
||
else {
|
||
($caicon) = split '@', $caicon;
|
||
$isricon = "<a title='$htitles{connorec}{$lang}\n\n$surplusinfo\n$pstate' onClick=$implan>".FW_makeImage($caicon.'@grey', '')." </a>";
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($modulo % 2){
|
||
$ctable .= qq{<tr>};
|
||
$tro = 1;
|
||
}
|
||
|
||
if (!$auto) {
|
||
$staticon = FW_makeImage('ios_off_fill@red', $htitles{iaaf}{$lang});
|
||
$auicon = "<a title= '$htitles{iaaf}{$lang}' onClick=$cmdautoon> $staticon</a>";
|
||
}
|
||
|
||
if ($auto) {
|
||
$staticon = FW_makeImage('ios_on_till_fill@orange', $htitles{ieas}{$lang});
|
||
$auicon = "<a title='$htitles{ieas}{$lang}' onClick=$cmdautooff> $staticon</a>";
|
||
}
|
||
|
||
if (isConsumerPhysOff($hash, $c)) { # Schaltzustand des Consumerdevices off
|
||
if ($cmdon) {
|
||
$staticon = FW_makeImage('ios_off_fill@red', $htitles{iave}{$lang});
|
||
$swicon = "<a title='$htitles{iave}{$lang}' onClick=$cmdon> $staticon</a>";
|
||
}
|
||
else {
|
||
$staticon = FW_makeImage('ios_off_fill@grey', $htitles{ians}{$lang});
|
||
$swicon = "<a title='$htitles{ians}{$lang}'> $staticon</a>";
|
||
}
|
||
}
|
||
|
||
if (isConsumerPhysOn($hash, $c)) { # Schaltzustand des Consumerdevices on
|
||
if($cmdoff) {
|
||
$staticon = FW_makeImage('ios_on_fill@green', $htitles{ieva}{$lang});
|
||
$swicon = "<a title='$htitles{ieva}{$lang}' onClick=$cmdoff> $staticon</a>";
|
||
}
|
||
else {
|
||
$staticon = FW_makeImage('ios_on_fill@grey', $htitles{iens}{$lang});
|
||
$swicon = "<a title='$htitles{iens}{$lang}'> $staticon</a>";
|
||
}
|
||
}
|
||
|
||
if ($clegendstyle eq 'icon') {
|
||
$cicon = FW_makeImage($cicon);
|
||
$ctable .= "<td style='text-align:left; white-space:nowrap;' $dstyle>$calias </td>";
|
||
$ctable .= "<td style='text-align:center' $dstyle>$cicon </td>";
|
||
$ctable .= "<td style='text-align:center' $dstyle>$isricon </td>";
|
||
$ctable .= "<td style='text-align:center' $dstyle>$swicon </td>";
|
||
$ctable .= "<td style='text-align:center' $dstyle>$auicon </td>";
|
||
}
|
||
else {
|
||
my (undef,$co) = split '@', $cicon;
|
||
$co = '' if (!$co);
|
||
$ctable .= "<td style='text-align:left' $dstyle><font color='$co'>$calias </font></td>";
|
||
$ctable .= "<td> </td>";
|
||
$ctable .= "<td> $isricon </td>";
|
||
$ctable .= "<td style='text-align:center' $dstyle>$swicon </td>";
|
||
$ctable .= "<td style='text-align:center' $dstyle>$auicon </td>";
|
||
}
|
||
|
||
if (!($modulo % 2)) {
|
||
$ctable .= qq{</tr>};
|
||
$tro = 0;
|
||
}
|
||
else {
|
||
$ctable .= qq{<td> </td>};
|
||
$ctable .= qq{<td> </td>};
|
||
}
|
||
|
||
$modulo++;
|
||
}
|
||
|
||
delete $paref->{consumer};
|
||
|
||
$ctable .= qq{</tr>} if($tro);
|
||
|
||
if ($clegend ne 'bottom') {
|
||
$ctable .= qq{<tr><td colspan='12'><hr></td></tr>};
|
||
}
|
||
|
||
$ctable .= qq{</table>};
|
||
|
||
return $ctable;
|
||
}
|
||
|
||
################################################################
|
||
# Werte erste Stunde in Balkengrafik
|
||
################################################################
|
||
sub _beamGraphicFirstHour {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hfcg = $paref->{hfcg};
|
||
my $offset = $paref->{offset};
|
||
my $hourstyle = $paref->{hourstyle};
|
||
my $beam1cont = $paref->{beam1cont};
|
||
my $beam2cont = $paref->{beam2cont};
|
||
my $lang = $paref->{lang};
|
||
my $kw = $paref->{kw};
|
||
|
||
my ($day, $bn);
|
||
|
||
my $hash = $defs{$name};
|
||
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,$val5,$val6,$val7,$val8);
|
||
my $hbsocs;
|
||
|
||
$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
|
||
$hfcg->{0}{time} += $offset;
|
||
|
||
if ($hfcg->{0}{time} < 0) { # Tageswechsel: day muss jetzt neu berechnet werden !
|
||
$hfcg->{0}{time} += 24;
|
||
my $n_day = strftime "%d", localtime($hfcg->{0}{mktime} - (3600 * abs($offset)));
|
||
$hfcg->{0}{day} = int($n_day);
|
||
$hfcg->{0}{day_str} = $n_day;
|
||
}
|
||
|
||
$hfcg->{0}{time_str} = sprintf '%02d', $hfcg->{0}{time};
|
||
|
||
$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', '-');
|
||
$hfcg->{0}{don} = HistoryVal ($name, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'DoN', 0);
|
||
|
||
$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);
|
||
$val5 = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'con', 0);
|
||
$val6 = sprintf "%.2f", (HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'conprice', 0) * $val3 / 1000); # Energiekosten der Stunde
|
||
$val7 = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'gfeedin', 0);
|
||
$val8 = sprintf "%.2f", (HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'feedprice', 0) * $val7 / 1000); # Einspeisevergütung der Stunde
|
||
|
||
## Batterien Selektionshash erstellen
|
||
#######################################
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$hbsocs->{0}{$bn} = HistoryVal ($hash, $hfcg->{0}{day_str}, $hfcg->{0}{time_str}, 'batsoc'.$bn, 0);
|
||
}
|
||
|
||
## Zuordnung Werte zu den Balken entsprechend Selektion
|
||
#########################################################
|
||
$hfcg->{0}{beam1} = $beam1cont eq 'pvForecast' ? $val1 :
|
||
$beam1cont eq 'pvReal' ? $val2 :
|
||
$beam1cont eq 'gridconsumption' ? $val3 :
|
||
$beam1cont eq 'consumptionForecast' ? $val4 :
|
||
$beam1cont eq 'consumption' ? $val5 :
|
||
$beam1cont eq 'energycosts' ? $val6 :
|
||
$beam1cont eq 'gridfeedin' ? $val7 :
|
||
$beam1cont eq 'feedincome' ? $val8 :
|
||
$beam1cont =~ /batsocforecast_/xs ? $hbsocs->{0}{(split '_', $beam1cont)[1]} :
|
||
undef;
|
||
|
||
$hfcg->{0}{beam2} = $beam2cont eq 'pvForecast' ? $val1 :
|
||
$beam2cont eq 'pvReal' ? $val2 :
|
||
$beam2cont eq 'gridconsumption' ? $val3 :
|
||
$beam2cont eq 'consumptionForecast' ? $val4 :
|
||
$beam2cont eq 'consumption' ? $val5 :
|
||
$beam2cont eq 'energycosts' ? $val6 :
|
||
$beam2cont eq 'gridfeedin' ? $val7 :
|
||
$beam2cont eq 'feedincome' ? $val8 :
|
||
$beam2cont =~ /batsocforecast_/xs ? $hbsocs->{0}{(split '_', $beam2cont)[1]} :
|
||
undef;
|
||
|
||
$hfcg->{0}{beam1} //= 0;
|
||
$hfcg->{0}{beam2} //= 0;
|
||
$hfcg->{0}{diff} = $hfcg->{0}{beam1} - $hfcg->{0}{beam2};
|
||
|
||
my $epc = CurrentVal ($hash, 'ePurchasePriceCcy', 0);
|
||
my $efc = CurrentVal ($hash, 'eFeedInTariffCcy', 0);
|
||
|
||
$hfcg->{0}{beam1txt} = $beam1cont eq 'pvForecast' ? $htitles{pvgenefc}{$lang}." ($kw)" :
|
||
$beam1cont eq 'pvReal' ? $htitles{pvgenerl}{$lang}." ($kw)" :
|
||
$beam1cont eq 'gridconsumption' ? $htitles{enppubgd}{$lang}." ($kw)" :
|
||
$beam1cont eq 'consumptionForecast' ? $htitles{enconsfc}{$lang}." ($kw)" :
|
||
$beam1cont eq 'consumption' ? $htitles{enconsrl}{$lang}." ($kw)" :
|
||
$beam1cont eq 'energycosts' ? $htitles{enpchcst}{$lang}." ($epc)" :
|
||
$beam1cont eq 'gridfeedin' ? $htitles{enfeedgd}{$lang}." ($kw)" :
|
||
$beam1cont eq 'feedincome' ? $htitles{rengfeed}{$lang}." ($efc)" :
|
||
$beam1cont =~ /batsocforecast_/xs ? $htitles{socofbat}{$lang}." ".(split '_', $beam1cont)[1]." (%)" :
|
||
'';
|
||
$hfcg->{0}{beam2txt} = $beam2cont eq 'pvForecast' ? $htitles{pvgenefc}{$lang}." ($kw)" :
|
||
$beam2cont eq 'pvReal' ? $htitles{pvgenerl}{$lang}." ($kw)" :
|
||
$beam2cont eq 'gridconsumption' ? $htitles{enppubgd}{$lang}." ($kw)" :
|
||
$beam2cont eq 'consumptionForecast' ? $htitles{enconsfc}{$lang}." ($kw)" :
|
||
$beam2cont eq 'consumption' ? $htitles{enconsrl}{$lang}." ($kw)" :
|
||
$beam2cont eq 'energycosts' ? $htitles{enpchcst}{$lang}." ($epc)" :
|
||
$beam2cont eq 'gridfeedin' ? $htitles{enfeedgd}{$lang}." ($kw)" :
|
||
$beam2cont eq 'feedincome' ? $htitles{rengfeed}{$lang}." ($efc)" :
|
||
$beam2cont =~ /batsocforecast_/xs ? $htitles{socofbat}{$lang}." ".(split '_', $beam2cont)[1]." (%)" :
|
||
'';
|
||
|
||
$hfcg->{0}{time_str} = sprintf('%02d', $hfcg->{0}{time}-1).$hourstyle;
|
||
|
||
return $thishour;
|
||
}
|
||
|
||
################################################################
|
||
# Werte restliche Stunden in Balkengrafik
|
||
################################################################
|
||
sub _beamGraphicRemainingHours {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
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 ($val1,$val2,$val3,$val4,$val5,$val6,$val7,$val8);
|
||
my $hbsocs;
|
||
|
||
my $hash = $defs{$name};
|
||
my $maxVal = $hfcg->{0}{beam1}; # Startwert
|
||
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;
|
||
($val1,$val2,$val3,$val4,$val5,$val6,$val7,$val8) = (0,0,0,0,0,0,0,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
|
||
|
||
if ($hfcg->{$i}{time} == 24) { # Sonderfall Mitternacht
|
||
$ds = strftime "%d", localtime ($hfcg->{0}{mktime} - (3600 * (abs($offset-$i+1))));
|
||
}
|
||
|
||
$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', '-');
|
||
$hfcg->{$i}{don} = HistoryVal ($name, $ds, $hfcg->{$i}{time_str}, 'DoN', 0);
|
||
|
||
$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);
|
||
$val5 = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, 'con', 0);
|
||
$val6 = sprintf "%.2f", (HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, 'conprice', 0) * $val3 / 1000); # Energiekosten der Stunde
|
||
$val7 = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, 'gfeedin', 0);
|
||
$val8 = sprintf "%.2f", (HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, 'feedprice', 0) * $val7 / 1000); # Einspeisevergütung der Stunde
|
||
|
||
## Batterien Selektionshash erstellen
|
||
#######################################
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$hbsocs->{$i}{$bn} = HistoryVal ($hash, $ds, $hfcg->{$i}{time_str}, 'batsoc'.$bn, 0);
|
||
}
|
||
|
||
$hfcg->{$i}{day_str} = $ds;
|
||
$hfcg->{$i}{day} = int($ds);
|
||
}
|
||
else {
|
||
$nh = sprintf '%02d', ($i + $offset);
|
||
}
|
||
}
|
||
else {
|
||
$nh = sprintf '%02d', $i;
|
||
}
|
||
|
||
if (defined $nh) {
|
||
$hfcg->{$i}{weather} = NexthoursVal ($hash, 'NextHour'.$nh, 'weatherid', 999);
|
||
$hfcg->{$i}{wcc} = NexthoursVal ($hash, 'NextHour'.$nh, 'wcc', '-');
|
||
$hfcg->{$i}{sunalt} = NexthoursVal ($hash, 'NextHour'.$nh, 'sunalt', '-');
|
||
$hfcg->{$i}{sunaz} = NexthoursVal ($hash, 'NextHour'.$nh, 'sunaz', '-');
|
||
$hfcg->{$i}{don} = NexthoursVal ($hash, 'NextHour'.$nh, 'DoN', 0);
|
||
my $stt = NexthoursVal ($hash, 'NextHour'.$nh, 'starttime', '');
|
||
|
||
$val1 = NexthoursVal ($hash, 'NextHour'.$nh, 'pvfc', 0);
|
||
$val4 = NexthoursVal ($hash, 'NextHour'.$nh, 'confc', 0);
|
||
|
||
## Batterien Selektionshash anreichern
|
||
########################################
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$hbsocs->{$i}{$bn} = NexthoursVal ($hash, 'NextHour'.$nh, 'soc'.$bn, 0);
|
||
}
|
||
|
||
my $day_str = ($stt =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/xs)[2];
|
||
|
||
if ($day_str) {
|
||
$hfcg->{$i}{day_str} = $day_str;
|
||
$hfcg->{$i}{day} = int $day_str;
|
||
}
|
||
}
|
||
|
||
## Zuordnung Werte zu den Balken entsprechend Selektion
|
||
#########################################################
|
||
$hfcg->{$i}{beam1} = $beam1cont eq 'pvForecast' ? $val1 :
|
||
$beam1cont eq 'pvReal' ? $val2 :
|
||
$beam1cont eq 'gridconsumption' ? $val3 :
|
||
$beam1cont eq 'consumptionForecast' ? $val4 :
|
||
$beam1cont eq 'consumption' ? $val5 :
|
||
$beam1cont eq 'energycosts' ? $val6 :
|
||
$beam1cont eq 'gridfeedin' ? $val7 :
|
||
$beam1cont eq 'feedincome' ? $val8 :
|
||
$beam1cont =~ /batsocforecast_/xs ? $hbsocs->{$i}{(split '_', $beam1cont)[1]} :
|
||
undef;
|
||
|
||
$hfcg->{$i}{beam2} = $beam2cont eq 'pvForecast' ? $val1 :
|
||
$beam2cont eq 'pvReal' ? $val2 :
|
||
$beam2cont eq 'gridconsumption' ? $val3 :
|
||
$beam2cont eq 'consumptionForecast' ? $val4 :
|
||
$beam2cont eq 'consumption' ? $val5 :
|
||
$beam2cont eq 'energycosts' ? $val6 :
|
||
$beam2cont eq 'gridfeedin' ? $val7 :
|
||
$beam2cont eq 'feedincome' ? $val8 :
|
||
$beam2cont =~ /batsocforecast_/xs ? $hbsocs->{$i}{(split '_', $beam2cont)[1]} :
|
||
undef;
|
||
|
||
$hfcg->{$i}{time_str} = sprintf ('%02d', $hfcg->{$i}{time}-1).$hourstyle;
|
||
|
||
$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 $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};
|
||
my $barcount = $paref->{barcount} // 9999; # Sync Anzahl Balken dieser Ebene mit voriger Ebene
|
||
|
||
$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, $titz2, $titz3);
|
||
|
||
my $ret = q{};
|
||
$ret .= __weatherOnBeam ($paref) if($weather);
|
||
$ret .= __batteryOnBeam ($paref);
|
||
my $m = $paref->{modulo} % 2;
|
||
|
||
if ($show_diff eq 'top') { # Zusätzliche Zeile Ertrag - Verbrauch
|
||
$ret .= "<tr class='$htr{$m}{cl}'><td class='solarfc'></td>";
|
||
|
||
my $ii = 0;
|
||
|
||
for my $i (0..($maxhours * 2) - 1) { # gleiche Bedingung wie oben
|
||
next if(__dontNightshowSkipSync ($name, $paref, $i));
|
||
|
||
$ii++; # wieviele Stunden haben wir bisher angezeigt ?
|
||
last if($ii > $maxhours || $ii > $barcount); # vorzeitiger Abbruch
|
||
|
||
$val = normBeamWidth ($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 ? '<b>'.$val.'<b/>' :
|
||
$val > 0 ? '+' .$val :
|
||
$val; # negative Zahlen in Fettschrift, 0 aber ohne +
|
||
}
|
||
|
||
$ret .= "<td class='solarfc' style='vertical-align:middle; text-align:center;'>$val</td>";
|
||
}
|
||
|
||
$ret .= "<td class='solarfc'></td></tr>"; # freier Platz am Ende
|
||
}
|
||
|
||
$ret .= "<tr class='$htr{$m}{cl}'><td class='solarfc'></td>"; # Neue Zeile mit freiem Platz am Anfang
|
||
|
||
my $ii = 0;
|
||
|
||
for my $i (0..($maxhours * 2) - 1) { # gleiche Bedingung wie oben
|
||
next if(__dontNightshowSkipSync ($name, $paref, $i));
|
||
|
||
$ii++;
|
||
last if($ii > $maxhours || $ii > $barcount);
|
||
|
||
$paref->{barcount} = $ii; # Anzahl Balken zur Begrenzung der nächsten Ebene registrieren
|
||
|
||
$height = 200 if(!$height); # Fallback, sollte eigentlich nicht vorkommen, außer der User setzt es auf 0
|
||
$maxVal = 1 if(!int $maxVal); # maxVal kann gerade bei kleineren maxhours Ausgaben in der Nacht leicht auf 0 fallen
|
||
$maxCon = 1 if(!$maxCon);
|
||
|
||
# Berechnung der Zonen
|
||
########################
|
||
if ($lotype eq 'single') {
|
||
$he = int(($maxVal - $hfcg->{$i}{beam1}) / $maxVal * $height) + $fsize; # Der zusätzliche Offset durch $fsize verhindert bei den meisten Skins dass die Grundlinie der Balken nach unten durchbrochen wird
|
||
$z3 = int($height + $fsize - $he);
|
||
$titz3 = qq/title="$hfcg->{0}{beam1txt}"/;
|
||
}
|
||
|
||
if ($lotype eq 'double') {
|
||
# he - freier der Raum über den Balken. fsize wird nicht verwendet, da bei diesem Typ keine Zahlen über den Balken stehen
|
||
# z2 - primärer Balkenwert ggf. mit Icon
|
||
# z3 - sekundärer Balkenwert, bei zu kleinem Wert wird der Platz komplett Zone 2 zugeschlagen und nicht angezeigt
|
||
# z2 und z3 nach Bedarf tauschen, wenn sekundärer Balkenwert > primärer Balkenwert
|
||
|
||
$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};
|
||
$titz2 = qq/title="$hfcg->{0}{beam1txt}"/;
|
||
$titz3 = qq/title="$hfcg->{0}{beam2txt}"/;
|
||
}
|
||
else { # tauschen, Verbrauch ist größer als Ertrag
|
||
$z3 = $hfcg->{$i}{beam1};
|
||
$z2 = $hfcg->{$i}{beam2};
|
||
$titz2 = qq/title="$hfcg->{0}{beam2txt}"/;
|
||
$titz3 = qq/title="$hfcg->{0}{beam1txt}"/;
|
||
}
|
||
|
||
$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') {
|
||
# 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 maxVal 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
|
||
}
|
||
|
||
$titz2 = qq/title="$hfcg->{0}{beam1txt}"/;
|
||
$titz3 = qq/title="$hfcg->{0}{beam2txt}"/;
|
||
# 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
|
||
}
|
||
|
||
## Erstellung der Balken
|
||
##########################
|
||
|
||
# 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 .="<td style='text-align: center; padding-left:1px; padding-right:1px; margin:0px; vertical-align:bottom; padding-top:0px'>\n";
|
||
|
||
$he /= 10; # freier der Raum über den Balken
|
||
$he = $he < 20 ? 20 : $he;
|
||
|
||
if ($lotype eq 'single') {
|
||
$val = normBeamWidth ($hfcg->{$i}{beam1}, $kw, $hfcg->{$i}{weather});
|
||
|
||
$ret .="<table width='100%' height='100%'>"; # mit width=100% etwas bessere Füllung der Balken
|
||
$ret .="<tr class='$htr{$m}{cl}' style='height:".$he."px'>";
|
||
$ret .="<td class='solarfc' style='vertical-align:bottom; color:#$fcolor1;' $titz3>".$val;
|
||
$ret .="</td></tr>";
|
||
|
||
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 .= "<tr class='odd' style='height:".$z3."px;'>";
|
||
$ret .= "<td align='center' class='solarfc' $style $titz3>";
|
||
|
||
my $sicon = 1;
|
||
|
||
# inject the new icon if defined
|
||
##################################
|
||
#$ret .= consinject($hash,$i,@consumers) if($s);
|
||
|
||
$ret .= "</td></tr>";
|
||
}
|
||
}
|
||
|
||
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 .="<table width='100%' height='100%'>\n"; # mit width=100% etwas bessere Füllung der Balken
|
||
$ret .="<tr class='$htr{$m}{cl}' style='height:".$he."px'><td class='solarfc'></td></tr>" if(defined $he); # Freiraum über den Balken einfügen
|
||
|
||
if ($hfcg->{$i}{beam1} > $hfcg->{$i}{beam2}) { # wer ist oben, Beam2 oder Beam1 ? Wert und Farbe für Zone 2 & 3 vorbesetzen
|
||
$val = normBeamWidth ($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 = normBeamWidth ($hfcg->{$i}{beam2}, $kw, $hfcg->{$i}{weather});
|
||
$color2 = $colorb2;
|
||
$style2 = $style." background-color:#$color2; color:#$fcolor2;'";
|
||
}
|
||
}
|
||
else {
|
||
$val = normBeamWidth ($hfcg->{$i}{beam2}, $kw, $hfcg->{$i}{weather});
|
||
$color1 = $colorb2;
|
||
$style1 = $style." background-color:#$color1; color:#$fcolor2;'";
|
||
|
||
if ($z3) {
|
||
$v = normBeamWidth ($hfcg->{$i}{beam1}, $kw, $hfcg->{$i}{weather});
|
||
$color2 = $colorb1;
|
||
$style2 = $style." background-color:#$color2; color:#$fcolor1;'";
|
||
}
|
||
}
|
||
|
||
$ret .= "<tr class='odd' style='height:".$z2."px'>";
|
||
$ret .= "<td align='center' class='solarfc' $style1 $titz2>".$val;
|
||
|
||
# inject the new icon if defined
|
||
##################################
|
||
#$ret .= consinject($hash,$i,@consumers) if($s);
|
||
|
||
$ret .= "</td></tr>";
|
||
|
||
if ($z3) { # die Zone 3 lassen wir bei zu kleinen Werten auch ganz weg
|
||
$ret .= "<tr class='odd' style='height:".$z3."px'>";
|
||
$ret .= "<td align='center' class='solarfc' $style2 $titz3>".$v;
|
||
$ret .= "</td></tr>";
|
||
}
|
||
}
|
||
|
||
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 .= "<table width='100%' border='0'>\n"; # Tipp : das nachfolgende border=0 auf 1 setzen hilft sehr Ausgabefehler zu endecken
|
||
|
||
$val = ($hfcg->{$i}{diff} > 0) ? normBeamWidth ($hfcg->{$i}{diff}, $kw, $hfcg->{$i}{weather}) : '';
|
||
$val = ' 0 ' if($hfcg->{$i}{diff} == 0); # Sonderfall , hier wird die 0 gebraucht !
|
||
|
||
if ($val) {
|
||
$ret .= "<tr class='$htr{$m}{cl}' style='height:".$he."px'>";
|
||
$ret .= "<td class='solarfc' style='vertical-align:bottom; color:#$fcolor1;'>".$val;
|
||
$ret .= "</td></tr>";
|
||
}
|
||
|
||
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 .= "<tr class='odd' style='height:".$z2."px'>";
|
||
$ret .= "<td align='center' class='solarfc' $style $titz2>";
|
||
$ret .= "</td></tr>";
|
||
}
|
||
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 .= "<tr class='$htr{$m}{cl}' style='height:".$z2."px'>";
|
||
$ret .= "<td class='solarfc'>";
|
||
$ret .= "</td></tr>";
|
||
}
|
||
}
|
||
|
||
if ($hfcg->{$i}{diff} < 0) { # Negativ Balken anzeigen ?
|
||
$style .= " background-color:#$colorb2'"; # mit Farbe 2 colorb2 füllen
|
||
$ret .= "<tr class='odd' style='height:".$z3."px'>";
|
||
$ret .= "<td align='center' class='solarfc' $style $titz3>";
|
||
$ret .= "</td></tr>";
|
||
}
|
||
elsif ($z3) { # ohne Farbe
|
||
$ret .= "<tr class='$htr{$m}{cl}' style='height:".$z3."px'>";
|
||
$ret .= "<td class='solarfc'>";
|
||
$ret .= "</td></tr>";
|
||
}
|
||
|
||
if ($z4) { # kann entfallen wenn auch z3 0 ist
|
||
$val = $hfcg->{$i}{diff} < 0 ? normBeamWidth ($hfcg->{$i}{diff}, $kw, $hfcg->{$i}{weather}) : ' ';
|
||
$ret .= "<tr class='$htr{$m}{cl}' style='height:".$z4."px'>";
|
||
$ret .= "<td class='solarfc' style='vertical-align:top'>".$val;
|
||
$ret .= "</td></tr>";
|
||
}
|
||
}
|
||
|
||
if ($show_diff eq 'bottom') { # zusätzliche diff Anzeige
|
||
$val = normBeamWidth ($hfcg->{$i}{diff}, $kw, $hfcg->{$i}{weather});
|
||
$val = ($hfcg->{$i}{diff} < 0) ? '<b>'.$val.'<b/>' : ($val > 0 ) ? '+'.$val : $val if ($val ne ' '); # negative Zahlen in Fettschrift, 0 aber ohne +
|
||
$ret .= "<tr class='$htr{$m}{cl}'><td class='solarfc' style='vertical-align:middle; text-align:center;'>$val";
|
||
$ret .= "</td></tr>";
|
||
}
|
||
|
||
$ret .= "<tr class='$htr{$m}{cl}'><td class='solarfc' style='vertical-align:bottom; text-align:center;'>";
|
||
$ret .= $hfcg->{$i}{time} == $thishour ? # wenn Hervorhebung nur bei gesetztem Attr 'graphicHistoryHour' ? dann hinzufügen: "&& $offset < 0"
|
||
'<a class="changed" style="visibility:visible"><span>'.$hfcg->{$i}{time_str}.'</span></a>' :
|
||
$hfcg->{$i}{time_str};
|
||
|
||
if ($hfcg->{$i}{time} == $thishour) {
|
||
$thishour = 99; # nur einmal verwenden !
|
||
}
|
||
|
||
$ret .="</td></tr></table></td>";
|
||
}
|
||
|
||
$ret .= "<td class='solarfc'></td>";
|
||
$ret .= "</tr>";
|
||
|
||
return $ret;
|
||
}
|
||
|
||
############################################################################################
|
||
# liefert Signal ob Werte angezeigt werden sollen obwohl
|
||
# die Nachtstunden nicht angezeigt werden sowie die
|
||
# bei Synchronisation der nachfolgenden Balkendiagramm-Ebenen
|
||
# mit Balkendiagramm-Ebene 1
|
||
#
|
||
# skip = 0 - Wert soll angezeigt werden
|
||
# skip = 1 - Wert soll nicht angezeigt werden
|
||
# paref->skip = 1 - Synchronisation Anzeige des Balkens in nächsten Ebenen verhindern
|
||
# paref->noSkip = 1 - Synchronisation Anzeige des Balkens in nächsten Ebenen erzwingen
|
||
#
|
||
############################################################################################
|
||
sub __dontNightshowSkipSync {
|
||
my $name = shift;
|
||
my $paref = shift;
|
||
my $i = shift;
|
||
|
||
my $skip = 0;
|
||
|
||
if ($paref->{skip}{$i} && !$paref->{noSkip}{$i}) {
|
||
$skip = 1 if($paref->{layersync}); # Anwendung bei Zeitsynchronisation zwischen Ebene 1 und den folgenden Balkengrafikebenen
|
||
}
|
||
elsif (!$paref->{show_night} && !$paref->{hfcg}{$i}{don} &&
|
||
!$paref->{hfcg}{$i}{beam1} && !$paref->{hfcg}{beam2} &&
|
||
!$paref->{noSkip}{$i}) {
|
||
|
||
$paref->{skip}{$i} = 1 if($paref->{layersync}); # Anwendung bei Zeitsynchronisation zwischen Ebene 1 und den folgenden Balkengrafikebenen
|
||
$skip = 1;
|
||
}
|
||
else {
|
||
$paref->{noSkip}{$i} = 1 if($paref->{layersync}); # Anwendung bei Zeitsynchronisation zwischen Ebene 1 und den folgenden Balkengrafikebenen
|
||
}
|
||
|
||
return $skip;
|
||
}
|
||
|
||
################################################################
|
||
# Wetter Icon Zeile
|
||
################################################################
|
||
sub __weatherOnBeam {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hfcg = $paref->{hfcg};
|
||
my $maxhours = $paref->{maxhours};
|
||
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 $barcount = $paref->{barcount} // 9999; # Sync Anzahl Balken dieser Ebene mit voriger Ebene
|
||
|
||
my $m = $paref->{modulo} % 2;
|
||
my $ret = q{};
|
||
$ret .= "<tr class='$htr{$m}{cl}'><td class='solarfc'></td>"; # freier Platz am Anfang
|
||
|
||
my $ii = 0;
|
||
|
||
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);
|
||
|
||
my $skip = __dontNightshowSkipSync ($name, $paref, $i);
|
||
|
||
if ($skip) {
|
||
debugLog ($paref, 'graphic', "Weather position >$i< is skipped due to don't show night condition") if($ii < $maxhours);
|
||
next;
|
||
}
|
||
|
||
$ii++; # wieviele Stunden Icons haben sind beechnet?
|
||
last if($ii > $maxhours || $ii > $barcount);
|
||
# 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) = 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}{don} ? '@'.$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 = '<b>???<b/>';
|
||
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 .= "<td title='$title' class='solarfc' width='$width' style='margin:1px; vertical-align:middle align:center; padding-bottom:1px;'>$val</td>";
|
||
}
|
||
|
||
$ret .= "<td class='solarfc'></td></tr>"; # freier Platz am Ende der Icon Zeile
|
||
|
||
return $ret;
|
||
}
|
||
|
||
################################################################
|
||
# Batterieladeempfehlung in Balkengrafik
|
||
################################################################
|
||
sub __batteryOnBeam {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $maxhours = $paref->{maxhours};
|
||
my $show_night = $paref->{show_night}; # alle Balken (Spalten) anzeigen?
|
||
my $width = $paref->{width};
|
||
my $lang = $paref->{lang};
|
||
my $hfcg = $paref->{hfcg};
|
||
my $t = $paref->{t};
|
||
my $barcount = $paref->{barcount} // 9999; # Sync Anzahl Balken dieser Ebene mit voriger Ebene
|
||
|
||
my $hash = $defs{$name};
|
||
my $hh;
|
||
|
||
for my $idx (sort keys %{$data{$name}{nexthours}}) {
|
||
for my $bn (1..MAXBATTERIES) { # alle Batterien
|
||
$bn = sprintf "%02d", $bn;
|
||
my $rcdc = NexthoursVal ($name, $idx, 'rcdchargebat'.$bn, undef);
|
||
my $stt = NexthoursVal ($name, $idx, 'starttime', undef);
|
||
next if(!defined $stt || !defined $rcdc);
|
||
|
||
my (undef,undef,$day_str,$time_str) = $stt =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/xs;
|
||
|
||
$hh->{$day_str}{$time_str}{'rcdchargebat'.$bn} = $rcdc;
|
||
$hh->{$day_str}{$time_str}{'soc'.$bn} = NexthoursVal ($name, $idx, 'soc'.$bn, undef);
|
||
}
|
||
}
|
||
|
||
for my $kdx (sort keys %{$hfcg}) {
|
||
next if(!isNumeric ($kdx));
|
||
|
||
my $ds = $hfcg->{$kdx}{day_str};
|
||
my $ts = $hfcg->{$kdx}{time_str};
|
||
|
||
next if(!defined $ds || !defined $ts);
|
||
|
||
$ts = (split ":", $ts)[0]; # Forum: https://forum.fhem.de/index.php?msg=1332721
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
$ds = sprintf "%02d", $ds;
|
||
|
||
## Einfügen prepared NextHour Werte
|
||
#####################################
|
||
$hfcg->{$kdx}{'rcdchargebat'.$bn} = $hh->{$ds}{$ts}{'rcdchargebat'.$bn} if(defined $hh->{$ds}{$ts}{'rcdchargebat'.$bn});
|
||
$hfcg->{$kdx}{'soc'.$bn} = $hh->{$ds}{$ts}{'soc'.$bn} if(defined $hh->{$ds}{$ts}{'soc'.$bn});
|
||
|
||
## Auffüllen mit History Werten (Achtung: Stundenverschieber relativ zu Nexthours)
|
||
####################################################################################
|
||
if (!defined $hh->{$ds}{$ts}{'rcdchargebat'.$bn}) {
|
||
my $histsoc = HistoryVal ($hash, $ds, (sprintf "%02d", $ts+1), 'batsoc'.$bn, undef);
|
||
|
||
if (defined $histsoc) {
|
||
$hfcg->{$kdx}{'soc'.$bn} = $histsoc;
|
||
$hfcg->{$kdx}{'rcdchargebat'.$bn} = 'hist';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
## Werte in Anzeigehash einfügen
|
||
##################################
|
||
my $m = $paref->{modulo} % 2;
|
||
my $day = strftime "%d", localtime($t); # aktueller Tag (range 01 .. 31)
|
||
my $chour = strftime "%H", localtime($t); # aktuelle Stunde in 24h format (00-23)
|
||
my $ret = q{};
|
||
|
||
for my $bn (1..MAXBATTERIES) { # für jede definierte Batterie
|
||
$bn = sprintf "%02d", $bn;
|
||
my ($err, $badev, $h) = isDeviceValid ( { name => $name, obj => 'setupBatteryDev'.$bn, method => 'attr' } );
|
||
next if($err);
|
||
|
||
my $bshow = BatteryVal ($name, $bn, 'bshowingraph', 0);
|
||
next if($bshow != $paref->{chartlvl}); # Anzeige nur auf Grafikebene "chartlvl"
|
||
|
||
$ret .= "<tr class='$htr{$m}{cl}'><td class='solarfc'></td>"; # freier Platz am Anfang
|
||
my $ii = 0;
|
||
|
||
for my $i (0..($maxhours * 2) - 1) {
|
||
my $skip = __dontNightshowSkipSync ($name, $paref, $i);
|
||
|
||
if ($skip) {
|
||
debugLog ($paref, 'graphic', "Battery $bn recommandation pos >$i< skipped due to don't show night condition") if($ii < $maxhours);
|
||
next;
|
||
}
|
||
|
||
$ii++; # wieviele Stunden Icons sind bisher beechnet?
|
||
last if($ii > $maxhours || $ii > $barcount);
|
||
|
||
my $bname = BatteryVal ($name, $bn, 'bname', '');
|
||
my $balias = BatteryVal ($name, $bn, 'balias', $bname);
|
||
my $bpowerin = BatteryVal ($name, $bn, 'bpowerin', 0);
|
||
my $bpowerout = BatteryVal ($name, $bn, 'bpowerout', 0);
|
||
|
||
my $day_str = $hfcg->{$i}{day_str};
|
||
my $time_str = $hfcg->{$i}{time_str};
|
||
$time_str = (split ":", $time_str)[0]; # Forum: https://forum.fhem.de/index.php?msg=1332721
|
||
my $soc = $hfcg->{$i}{'soc'.$bn};
|
||
|
||
my ($bpower, $currsoc);
|
||
|
||
if ($day_str eq $day && $time_str eq $chour) { # akt. Leistung nur für aktuelle Stunde
|
||
$bpower = $bpowerin ? $bpowerin :
|
||
$bpowerout ? 0 - $bpowerout : # __substituteIcon: bpowerout als NEGATIVEN Wert übergeben!
|
||
0;
|
||
|
||
$currsoc = BatteryVal ($name, $bn, 'bcharge', 0);
|
||
}
|
||
|
||
my ($bicon, $title) = __substituteIcon ( { name => $name, # Icon / Status des Batterie Devices
|
||
pn => $bn,
|
||
ptyp => 'battery',
|
||
flag => $hfcg->{$i}{'rcdchargebat'.$bn},
|
||
msg1 => $balias,
|
||
soc => $soc,
|
||
pcurr => $bpower,
|
||
lang => $lang
|
||
}
|
||
);
|
||
|
||
$title .= defined $currsoc ? "\n".$htitles{socbacur}{$lang}.": ".$currsoc." %" : '';
|
||
my $image = defined $hfcg->{$i}{'rcdchargebat'.$bn} ? FW_makeImage ($bicon) : '';
|
||
|
||
$ret .= "<td title='$title' class='solarfc' width='$width' style='margin:1px; vertical-align:middle align:center; padding-bottom:1px;'>$image</td>";
|
||
|
||
debugLog ($paref, 'graphic', "Battery $bn pos >$i< day: $day_str, time: $time_str, Power ('-' = out): ".(defined $bpower ? $bpower : 'undef').
|
||
" W, Rcmd: ".(defined $hfcg->{$i}{'rcdchargebat'.$bn} ? $hfcg->{$i}{'rcdchargebat'.$bn} : 'undef').
|
||
", SoC: ".(defined $hfcg->{$i}{'soc'.$bn} ? $hfcg->{$i}{'soc'.$bn} : 'undef')." %");
|
||
}
|
||
|
||
$ret .= "<td class='solarfc'></td></tr>" if($ret); # freier Platz am Ende der Icon Zeile
|
||
}
|
||
|
||
return $ret;
|
||
}
|
||
|
||
######################################################################################
|
||
# Energieflußgrafik
|
||
# M - MoveTo setzt den aktuellen Punkt fest, von dem aus der Pfad starten soll
|
||
# (https://wiki.selfhtml.org/wiki/SVG/Tutorials/Pfade#MoveTo)
|
||
# L - LineTo zeichnet eine Linie vom aktuellen zum angegebenen Punkt
|
||
# (https://wiki.selfhtml.org/wiki/SVG/Tutorials/Pfade#LineTo)
|
||
######################################################################################
|
||
sub _flowGraphic {
|
||
my $paref = shift;
|
||
my $hash = $paref->{hash};
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $flowgsize = $paref->{flowgsize};
|
||
my $flowgani = $paref->{flowgani};
|
||
my $flowgxshift = $paref->{flowgxshift}; # X-Verschiebung der Flußgrafikbox (muß negiert werden)
|
||
my $flowgyshift = $paref->{flowgyshift}; # Y-Verschiebung der Flußgrafikbox (muß negiert werden)
|
||
my $flowgcons = $paref->{flowgcons}; # Verbraucher in der Energieflußgrafik anzeigen
|
||
my $flowgconsTime = $paref->{flowgconsTime}; # Verbraucher Restlaufeit in der Energieflußgrafik anzeigen
|
||
my $flowgconX = $paref->{flowgconX};
|
||
my $flowgconsPower = $paref->{flowgconsPower};
|
||
my $flowgPrdsPower = 1; # initial Producer akt. Erzeugung anzeigen
|
||
my $cdist = $paref->{flowgconsDist}; # Abstand Consumer zueinander
|
||
my $exth2cdist = $paref->{flowgh2cDist}; # vertikaler Abstand Home -> Consumer Zeile
|
||
my $lang = $paref->{lang};
|
||
|
||
my $cgc = ReadingsNum ($name, 'Current_GridConsumption', 0);
|
||
my $node2grid = ReadingsNum ($name, 'Current_GridFeedIn', 0); # vom Knoten zum Grid
|
||
my $cself = ReadingsNum ($name, 'Current_SelfConsumption', 0);
|
||
my $cc = CurrentVal ($hash, 'consumption', 0);
|
||
|
||
my $cc_dummy = $cc;
|
||
my $scale = FGSCALEDEF;
|
||
my $pdist = 130; # Abstand Producer zueinander
|
||
my $hasbat = 1; # initial Batterie vorhanden
|
||
my $stna = $name;
|
||
$stna .= int (rand (1500));
|
||
|
||
my ($lcp, $y_pos, $y_pos1);
|
||
|
||
for my $re (keys %hrepl) { # V 1.37.1 Ziffern etc. eliminieren, Forum: https://forum.fhem.de/index.php?msg=1323229
|
||
$stna =~ s/$re/$hrepl{$re}/gxs;
|
||
}
|
||
|
||
## definierte Batterien ermitteln und zusammenfassen
|
||
######################################################
|
||
my ($batin, $bat2home);
|
||
my $socwhsum = 0;
|
||
my $soc = 0;
|
||
|
||
for my $bn (1..MAXBATTERIES) { # für jede definierte Batterie
|
||
$bn = sprintf "%02d", $bn;
|
||
my ($err, $badev, $h) = isDeviceValid ( { name => $name, obj => 'setupBatteryDev'.$bn, method => 'attr' } );
|
||
next if($err);
|
||
|
||
my $batinpow = ReadingsNum ($name, 'Current_PowerBatIn_'.$bn, undef);
|
||
my $bat2homepow = ReadingsNum ($name, 'Current_PowerBatOut_'.$bn, undef);
|
||
$batin += $batinpow if(defined $batinpow);
|
||
$bat2home += $bat2homepow if(defined $bat2homepow);
|
||
|
||
$socwhsum += BatteryVal ($name, $bn, 'bchargewh', 0); # Batterie SoC in Wh
|
||
}
|
||
|
||
my $batcapsum = CurrentVal ($hash, 'batcapsum', 0); # Summe installierte Batterie Kapazität
|
||
$soc = sprintf "%.0f", ($socwhsum / $batcapsum * 100) if($batcapsum); # resultierender SoC (%) aller Batterien als Cluster
|
||
|
||
if (!defined $batin && !defined $bat2home) {
|
||
$hasbat = 0;
|
||
$batin = 0;
|
||
$bat2home = 0;
|
||
$soc = 0;
|
||
}
|
||
|
||
## Resultierende von Laden und Entladen berechnen
|
||
###################################################
|
||
my $x = $batin - $bat2home; # können theoretisch gleich groß sein -> 0 setzen und Resultierende neu berechnen
|
||
$batin = 0;
|
||
$bat2home = 0;
|
||
|
||
if ($x > 0) { $batin = $x; } else { $bat2home = abs $x; } # es darf nur $batin ODER $bat2home mit einem Wert > 0 geben
|
||
|
||
my $bat_color = $soc < 26 ? "$stna bat25" :
|
||
$soc < 76 ? "$stna bat50" :
|
||
"$stna bat75";
|
||
|
||
## definierte Producer + Inverter ermitteln und zusammenfassen
|
||
################################################################
|
||
my $pdcr = {}; # Hashref Producer
|
||
my $ppall = 0; # Summe Erzeugung alle nicht PV-Producer
|
||
my $pv2node = 0; # Summe PV-Erzeugung alle Inverter
|
||
my $pv2grid = 0;
|
||
my $pv2bat = 0;
|
||
my $lfn = 0;
|
||
|
||
for my $pn (1..MAXPRODUCER) {
|
||
$pn = sprintf "%02d", $pn;
|
||
my $p = ProducerVal ($hash, $pn, 'pgeneration', undef);
|
||
my $feed = ProducerVal ($hash, $pn, 'pfeed', 'default');
|
||
|
||
if (defined $p) {
|
||
$p = __normDecPlaces ($p);
|
||
$pdcr->{$lfn}{p} = $p; # aktuelle Erzeugung nicht PV-Producer
|
||
$pdcr->{$lfn}{pn} = $pn; # Producernummer
|
||
$pdcr->{$lfn}{feed} = $feed; # Eigenschaft der Energielieferung
|
||
$pdcr->{$lfn}{ptyp} = 'producer'; # Typ des Producers
|
||
$ppall += $p; # aktuelle Erzeuguung aller nicht PV-Producer
|
||
|
||
$lfn++;
|
||
}
|
||
}
|
||
|
||
for my $in (1..MAXINVERTER) {
|
||
$in = sprintf "%02d", $in;
|
||
my $p = InverterVal ($hash, $in, 'igeneration', undef);
|
||
my $feed = InverterVal ($hash, $in, 'ifeed', 'default');
|
||
|
||
if (defined $p) {
|
||
$p = __normDecPlaces ($p);
|
||
$pdcr->{$lfn}{pn} = $in; # Inverternummer
|
||
$pdcr->{$lfn}{feed} = $feed; # Eigenschaft der Energielieferung
|
||
$pdcr->{$lfn}{ptyp} = 'inverter'; # Typ des Producers
|
||
$pdcr->{$lfn}{p} = $p; # aktuelle PV
|
||
$pv2node += $p if($feed eq 'default'); # PV-Erzeugung Inverter für das Hausnetz
|
||
$pv2grid += $p if($feed eq 'grid'); # PV nur für das öffentliche Netz
|
||
$pv2bat += $p if($feed eq 'bat'); # Direktladen PV nur in die Batterie
|
||
|
||
$lfn++;
|
||
}
|
||
}
|
||
|
||
## Batterie Werte verarbeiten
|
||
###############################
|
||
my $grid2home_style = $cgc ? "$stna active_sig" : "$stna inactive"; # cgc current GridConsumption
|
||
my $bat2home_style = $bat2home ? "$stna active_normal" : "$stna inactive";
|
||
my $cgc_direction = "M250,515 L670,590";
|
||
|
||
if ($bat2home) { # Batterie wird entladen
|
||
my $cgfo = $node2grid - $pv2node; # pv2node -> PV-Erzeugung Inverter für das Hausnetz
|
||
|
||
if ($cgfo > 1) {
|
||
$grid2home_style = "$stna active_normal";
|
||
$cgc_direction = "M670,590 L250,515";
|
||
$node2grid -= $cgfo;
|
||
$cgc = $cgfo;
|
||
}
|
||
}
|
||
|
||
my $bat2home_direction = "M1200,515 L730,590";
|
||
my $node2bat = $batin;
|
||
|
||
if ($batin) { # Batterie wird geladen
|
||
my $home2bat = $batin - ($pv2node + $pv2bat);
|
||
|
||
if ($home2bat > 1) { # Batterieladung wird anteilig aus Hausnetz geladen
|
||
$node2bat -= $home2bat;
|
||
$bat2home_style = "$stna active_sig";
|
||
$bat2home_direction = "M730,590 L1200,515";
|
||
$bat2home = $home2bat;
|
||
}
|
||
}
|
||
|
||
## Producer Koordninaten Steuerhash
|
||
#####################################
|
||
my ($togrid, $tonode, $tobat) = __sortProducer ($pdcr); # lfn Producer sortiert nach ptyp und feed
|
||
|
||
my $psorted = {
|
||
'1togrid' => { xicon => -100, xchain => 150, ychain => 400, step => 30, count => scalar @{$togrid}, sorted => $togrid }, # Producer/PV nur zu Grid
|
||
'2tonode' => { xicon => 350, xchain => 700, ychain => 200, step => $pdist, count => scalar @{$tonode}, sorted => $tonode }, # Producer/PV zum Knoten
|
||
'3tobat' => { xicon => 750, xchain => 1370, ychain => 430, step => 30, count => scalar @{$tobat}, sorted => $tobat }, # Producer/PV nur zu Batterie
|
||
};
|
||
|
||
my $doproducerrow = 1;
|
||
$doproducerrow = 0 if(!$psorted->{'1togrid'}{count} && !$psorted->{'3tobat'}{count} && $psorted->{'2tonode'}{count} == 1);
|
||
|
||
## definierte Verbraucher ermitteln
|
||
#####################################
|
||
my $cnsmr = {}; # Hashref Consumer current power
|
||
|
||
for my $c (sort{$a<=>$b} keys %{$data{$name}{consumers}}) { # definierte Verbraucher ermitteln
|
||
next if(isConsumerNoshow ($hash, $c) =~ /^[13]$/xs); # auszublendende Consumer nicht berücksichtigen
|
||
$cnsmr->{$c}{p} = ReadingsNum ($name, "consumer${c}_currentPower", 0);
|
||
$cnsmr->{$c}{ptyp} = 'consumer';
|
||
}
|
||
|
||
my $consumercount = keys %{$cnsmr};
|
||
my @consumers = sort{$a<=>$b} keys %{$cnsmr};
|
||
|
||
## Werte / SteuerungVars anpassen
|
||
###################################
|
||
my $pnodesum = __normDecPlaces ($ppall + $pv2node); # Erzeugung Summe im Knoten
|
||
$pnodesum += abs $node2bat if($node2bat < 0); # Batterie ist voll und SolarLader liefert an Knoten
|
||
$node2bat -= $pv2bat; # Knoten-Bat -> abzüglich Direktladung (pv2bat)
|
||
$flowgcons = 0 if(!$consumercount); # Consumer Anzeige ausschalten wenn keine Consumer definiert
|
||
my $node2home = __normDecPlaces ($cself + $ppall); # Energiefluß vom Knoten zum Haus: Selbstverbrauch + alle Producer (Batterie-In/Solar-Ladegeräte sind nicht in SelfConsumtion enthalten)
|
||
|
||
|
||
## SVG Box initialisieren mit Grid-Icon
|
||
#########################################
|
||
my $vbwidth = 800; # width and height specify the viewBox size
|
||
my $vbminx = -10 * $flowgxshift; # min-x and min-y represent the smallest X and Y coordinates that the viewBox may have
|
||
my $vbminy = $doproducerrow ? -25 : 125; # Grafik höher positionieren wenn keine Poducerreihe angezeigt
|
||
|
||
my $vbhight = !$flowgcons ? 380 :
|
||
!$flowgconsTime ? 590 :
|
||
610;
|
||
$vbhight += $exth2cdist;
|
||
|
||
if ($doproducerrow) {$vbhight += 100}; # Höhe Box vergrößern wenn Poducerreihe angezeigt
|
||
|
||
$vbminy -= $flowgyshift; # Y-Verschiebung berücksichtigen
|
||
$vbhight += $flowgyshift; # Y-Verschiebung berücksichtigen
|
||
|
||
my $vbox = "$vbminx $vbminy $vbwidth $vbhight";
|
||
my $svgstyle = 'width:98%; height:'.$flowgsize.'px;';
|
||
my $animation = $flowgani ? '@keyframes dash { to { stroke-dashoffset: 0; } }' : ''; # Animation Ja/Nein
|
||
|
||
my $grid_color = $node2grid ? "$stna grid_green" :
|
||
!$node2grid && !$cgc && $bat2home ? "$stna grid_gray" :
|
||
"$stna grid_red";
|
||
|
||
my $strokecolstd = CurrentVal ($hash, 'strokecolstd', STROKCOLSTDDEF);
|
||
my $strokecolsig = CurrentVal ($hash, 'strokecolsig', STROKCOLSIGDEF);
|
||
my $strokecolina = CurrentVal ($hash, 'strokecolina', STROKCOLINADEF);
|
||
my $strokewidth = CurrentVal ($hash, 'strokewidth', STROKWIDTHDEF);
|
||
|
||
my $ret = << "END0";
|
||
<style>
|
||
.$stna.text { stroke: none; fill: gray; font-size: 60px; }
|
||
.$stna.bat25 { stroke: red; fill: red; }
|
||
.$stna.bat50 { stroke: darkorange; fill: darkorange; }
|
||
.$stna.bat75 { stroke: green; fill: green; }
|
||
.$stna.grid_green { fill: green; }
|
||
.$stna.grid_red { fill: red; }
|
||
.$stna.grid_gray { fill: gray; }
|
||
.$stna.inactive { stroke: $strokecolina; stroke-width: $strokewidth; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.2; }
|
||
.$stna.active_sig { stroke: $strokecolsig; stroke-width: $strokewidth; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; }
|
||
.$stna.active_normal { stroke: $strokecolstd; stroke-width: $strokewidth; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; }
|
||
|
||
$animation
|
||
</style>
|
||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="$vbox" style="$svgstyle" id="SVGPLOT_$stna">
|
||
|
||
<g id="grid_$stna" class="$grid_color" transform="translate(100,260),scale(3.0)">
|
||
<path d="M15.3,2H8.7L2,6.46V10H4V8H8v2.79l-4,9V22H6V20.59l6-3.27,6,3.27V22h2V19.79l-4-9V8h4v2h2V6.46ZM14,4V6H10V4ZM6.3,6,8,4.87V6Zm8,6L15,13.42,12,15,9,13.42,9.65,12ZM7.11,17.71,8.2,15.25l1.71.93Zm8.68-2.46,1.09,2.46-2.8-1.53ZM14,10H10V8h4Zm2-5.13L17.7,6H16Z"/>
|
||
</g>
|
||
END0
|
||
|
||
## Producer Icon - in Reihenfolge: zum Grid - zum Knoten - zur Batterie
|
||
#########################################################################
|
||
$paref->{stna} = $stna;
|
||
$paref->{pnodesum} = $pnodesum;
|
||
$paref->{psorted} = $psorted;
|
||
$paref->{pdcr} = $pdcr;
|
||
$paref->{pdist} = $pdist;
|
||
|
||
if (!$doproducerrow) {
|
||
$paref->{y_coord} = 165;
|
||
$ret .= __addProducerIcon ($paref); # Producer Icons row einfügen
|
||
}
|
||
else { # mehr als ein Producer vorhanden
|
||
$paref->{y_coord} = 0;
|
||
$ret .= __addProducerIcon ($paref); # Producer Icons row einfügen
|
||
|
||
$paref->{x_coord} = 360;
|
||
$paref->{y_coord} = 165;
|
||
$ret .= __addNodeIcon ($paref); # Knoten Icon
|
||
}
|
||
|
||
delete $paref->{stna};
|
||
delete $paref->{pnodesum};
|
||
delete $paref->{psorted};
|
||
delete $paref->{pdcr};
|
||
delete $paref->{pdist};
|
||
delete $paref->{x_coord};
|
||
delete $paref->{y_coord};
|
||
|
||
## Consumer Liste und Icons in Grafik anzeigen
|
||
################################################
|
||
my $cons_left = 0;
|
||
my $consumer_start = 0;
|
||
my $currentPower = 0;
|
||
$y_pos = 505 + $exth2cdist;
|
||
|
||
if ($flowgcons) {
|
||
if ($consumercount % 2) {
|
||
$consumer_start = 350 - ($cdist * ($consumercount -1) / 2);
|
||
}
|
||
else {
|
||
$consumer_start = 350 - ($cdist / 2 * ($consumercount-1));
|
||
}
|
||
|
||
$cons_left = $consumer_start + 15;
|
||
|
||
for my $c (@consumers) {
|
||
my $calias = ConsumerVal ($hash, $c, 'alias', ''); # Name des Consumerdevices
|
||
$currentPower = $cnsmr->{$c}{p};
|
||
|
||
my ($cicon) = __substituteIcon ( { hash => $hash, # Icon des Consumerdevices
|
||
name => $name,
|
||
pn => $c,
|
||
ptyp => $cnsmr->{$c}{ptyp},
|
||
pcurr => $currentPower,
|
||
lang => $lang
|
||
}
|
||
);
|
||
|
||
$cc_dummy -= $currentPower;
|
||
$cicon = FW_makeImage ($cicon, '');
|
||
($scale, $cicon) = __normIconScale ($name, $cicon);
|
||
|
||
$ret .= qq{<g id="consumer_${c}_$stna" transform="translate($cons_left,$y_pos),scale($scale)">};
|
||
$ret .= "<title>$calias</title>".$cicon;
|
||
$ret .= '</g> ';
|
||
|
||
$cons_left += $cdist;
|
||
}
|
||
}
|
||
|
||
## Batterie Icon
|
||
##################
|
||
if ($hasbat) {
|
||
$ret .= << "END1";
|
||
<g class="$bat_color" transform="translate(750,245),scale(.30) rotate (90)">
|
||
<path d="m 134.65625,89.15625 c -6.01649,0 -11,4.983509 -11,11 l 0,180 c 0,6.01649 4.98351,11 11,11 l 95.5,0 c 6.01631,0 11,-4.9825 11,-11 l 0,-180 c 0,-6.016491 -4.98351,-11 -11,-11 l -95.5,0 z m 0,10 95.5,0 c 0.60951,0 1,0.390491 1,1 l 0,180 c 0,0.6085 -0.39231,1 -1,1 l -95.5,0 c -0.60951,0 -1,-0.39049 -1,-1 l 0,-180 c 0,-0.609509 0.39049,-1 1,-1 z"/>
|
||
<path d="m 169.625,69.65625 c -6.01649,0 -11,4.983509 -11,11 l 0,14 10,0 0,-14 c 0,-0.609509 0.39049,-1 1,-1 l 25.5,0 c 0.60951,0 1,0.390491 1,1 l 0,14 10,0 0,-14 c 0,-6.016491 -4.98351,-11 -11,-11 l -25.5,0 z"/>
|
||
END1
|
||
|
||
$ret .= '<path d="m 221.141,266.334 c 0,3.313 -2.688,6 -6,6 h -65.5 c -3.313,0 -6,-2.688 -6,-6 v -6 c 0,-3.314 2.687,-6 6,-6 l 65.5,-20 c 3.313,0 6,2.686 6,6 v 26 z"/>' if ($soc > 12);
|
||
$ret .= '<path d="m 221.141,213.667 c 0,3.313 -2.688,6 -6,6 l -65.5,20 c -3.313,0 -6,-2.687 -6,-6 v -20 c 0,-3.313 2.687,-6 6,-6 l 65.5,-20 c 3.313,0 6,2.687 6,6 v 20 z"/>' if ($soc > 38);
|
||
$ret .= '<path d="m 221.141,166.667 c 0,3.313 -2.688,6 -6,6 l -65.5,20 c -3.313,0 -6,-2.687 -6,-6 v -20 c 0,-3.313 2.687,-6 6,-6 l 65.5,-20 c 3.313,0 6,2.687 6,6 v 20 z"/>' if ($soc > 63);
|
||
$ret .= '<path d="m 221.141,120 c 0,3.313 -2.688,6 -6,6 l -65.5,20 c -3.313,0 -6,-2.687 -6,-6 v -26 c 0,-3.313 2.687,-6 6,-6 h 65.5 c 3.313,0 6,2.687 6,6 v 6 z"/>' if ($soc > 88);
|
||
$ret .= '</g>';
|
||
}
|
||
|
||
## Home Icon
|
||
##############
|
||
my $hicon = FW_makeImage (HOMEICONDEF, '');
|
||
($scale, $hicon) = __normIconScale ($name, $hicon);
|
||
|
||
$ret .= qq{<g id="home_$stna" transform="translate(368,360),scale($scale)">}; # translate(X-Koordinate,Y-Koordinate), scale(<Größe>)-> Koordinaten ändern sich bei Größenänderung
|
||
$ret .= "<title>Home</title>".$hicon;
|
||
$ret .= '</g> ';
|
||
|
||
## Dummy Consumer Icon
|
||
########################
|
||
if ($flowgconX) {
|
||
my $dumtxt = $htitles{dumtxt}{$lang};
|
||
my $dumcol = $cc_dummy <= 0 ? '@grey' : q{}; # Einfärbung Consumer Dummy
|
||
my $dicon = FW_makeImage (CICONDEF.$dumcol, '');
|
||
($scale, $dicon) = __normIconScale ($name, $dicon);
|
||
|
||
$ret .= qq{<g id="dummy_$stna" transform="translate(660,360),scale($scale)">};
|
||
$ret .= "<title>$dumtxt</title>".$dicon;
|
||
$ret .= '</g> ';
|
||
}
|
||
|
||
## Laufketten Node->Home, Node->Grid, Bat->Home
|
||
#################################################
|
||
my $node2home_style = $node2home ? "$stna active_normal" : "$stna inactive";
|
||
my $node2grid_style = $node2grid ? "$stna active_normal" : "$stna inactive";
|
||
|
||
$ret .= << "END2";
|
||
<g transform="translate(50,50),scale(0.5)" stroke-width="27" fill="none">
|
||
<path id="node2home_$stna" class="$node2home_style" d="M700,400 L700,580" />
|
||
<path id="node2grid_$stna" class="$node2grid_style" d="M670,400 L250,480" />
|
||
<path id="grid2home_$stna" class="$grid2home_style" d="$cgc_direction" />
|
||
END2
|
||
|
||
## Laufketten PV->Batterie, Batterie->Home
|
||
##############################################
|
||
if ($hasbat) {
|
||
my $node2bat_style = $node2bat ? "$stna active_normal" : "$stna inactive";
|
||
my $batin_direction = $node2bat < 0 ? "M1200,480 L730,400" : "M730,400 L1200,480";
|
||
$node2bat = abs $node2bat;
|
||
|
||
$ret .= << "END3";
|
||
<path id="bat2home_$stna" class="$bat2home_style" d="$bat2home_direction" />
|
||
<path id="pv2bat_$stna" class="$node2bat_style" d="$batin_direction" />
|
||
END3
|
||
}
|
||
|
||
## Dummy Consumer Laufketten
|
||
##############################
|
||
if ($flowgconX) {
|
||
my $consumer_style = "$stna inactive";
|
||
$consumer_style = "$stna active_sig" 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{<path id="home2dummy_$stna" class="$consumer_style" $chain_color d="M790,690 L1200,690" />};
|
||
}
|
||
|
||
## Producer Laufketten - in Reihenfolge: zum Grid - zum Knoten - zur Batterie
|
||
## Laufkette nur anzeigen wenn Producerzeile angezeigt werden soll
|
||
###############################################################################
|
||
if ($doproducerrow) {
|
||
for my $st (sort keys %{$psorted}) {
|
||
my $left = $psorted->{$st}{start} * 2; # Übertrag aus Producer Icon Abschnitt
|
||
my $count = $psorted->{$st}{count};
|
||
my $xchain = $psorted->{$st}{xchain}; # X- Koordinate Kette am Ziel
|
||
my $ychain = $psorted->{$st}{ychain}; # Y- Koordinate Kette am Ziel
|
||
my $step = $psorted->{$st}{step};
|
||
my @sorted = @{$psorted->{$st}{sorted}};
|
||
|
||
if ($count % 2) {
|
||
$xchain = $xchain - ($pdist * ($count -1) / 2);
|
||
}
|
||
else {
|
||
$xchain = $xchain - ($pdist / 2 * ($count - 1));
|
||
}
|
||
|
||
my $producer_style;
|
||
|
||
for my $lfn (@sorted) {
|
||
my $pn = $pdcr->{$lfn}{pn};
|
||
my $p = $pdcr->{$lfn}{p};
|
||
$producer_style = $p > 0 ? "$stna active_normal" : "$stna inactive";
|
||
my $chain_color = ''; # Farbe der Laufkette des Producers
|
||
|
||
if ($p) {
|
||
#$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).';"';
|
||
}
|
||
|
||
$ret .= qq{<path id="genproducer_${pn}_$stna" class="$producer_style" $chain_color d=" M$left,130 L$xchain,$ychain" />};
|
||
$left += ($pdist * 2);
|
||
$xchain += $step;
|
||
}
|
||
}
|
||
}
|
||
|
||
## Consumer Laufketten
|
||
########################
|
||
if ($flowgcons) {
|
||
$cons_left = $consumer_start * 2;
|
||
my $cons_left_start = 0;
|
||
my $distance_con = 65;
|
||
$y_pos = 880 + 2 * $exth2cdist;
|
||
|
||
if ($consumercount % 2) {
|
||
$cons_left_start = 700 - ($distance_con * ($consumercount -1) / 2);
|
||
}
|
||
else {
|
||
$cons_left_start = 700 - ($distance_con / 2 * ($consumercount-1));
|
||
}
|
||
|
||
my $consumer_style;
|
||
|
||
for my $c (@consumers) {
|
||
my $power = ConsumerVal ($hash, $c, 'power', 0);
|
||
my $rpcurr = ConsumerVal ($hash, $c, 'rpcurr', ''); # Reading für akt. Verbrauch angegeben ?
|
||
$currentPower = $cnsmr->{$c}{p};
|
||
|
||
if (!$rpcurr && isConsumerPhysOn($hash, $c)) { # Workaround wenn Verbraucher ohne Leistungsmessung
|
||
$currentPower = $power;
|
||
}
|
||
|
||
my $p = $currentPower;
|
||
$p = (($currentPower / $power) * 100) if ($power > 0);
|
||
$consumer_style = $p > DEFPOPERCENT ? "$stna active_normal" : "$stna inactive";
|
||
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).';"';
|
||
}
|
||
|
||
$ret .= qq{<path id="home2consumer_${c}_$stna" class="$consumer_style" $chain_color d="M$cons_left_start,780 L$cons_left,$y_pos" />};
|
||
$cons_left += ($cdist * 2);
|
||
$cons_left_start += $distance_con;
|
||
}
|
||
}
|
||
|
||
## Textangaben an Grafikelementen
|
||
###################################
|
||
$cc_dummy = sprintf("%.0f", $cc_dummy); # Verbrauch Dummy-Consumer
|
||
$bat2home = __normDecPlaces ($bat2home);
|
||
$node2bat = __normDecPlaces ($node2bat);
|
||
|
||
$ret .= qq{<text class="$stna text" id="nodetxt_$stna" x="800" y="320" style="text-anchor: start;">$pnodesum</text>} if ($pnodesum > 0);
|
||
$ret .= qq{<text class="$stna text" id="batsoctxt_$stna" x="1380" y="520" style="text-anchor: start;">$soc %</text>} if ($hasbat); # Lage Text Batterieladungszustand
|
||
$ret .= qq{<text class="$stna text" id="node2hometxt_$stna" x="730" y="520" style="text-anchor: start;">$node2home</text>} if ($node2home);
|
||
$ret .= qq{<text class="$stna text" id="node2gridtxt_$stna" x="420" y="420" style="text-anchor: end;">$node2grid</text>} if ($node2grid);
|
||
$ret .= qq{<text class="$stna text" id="grid2hometxt_$stna" x="420" y="610" style="text-anchor: end;">$cgc</text>} if ($cgc);
|
||
$ret .= qq{<text class="$stna text" id="batouttxt_$stna" x="1000" y="610" style="text-anchor: start;">$bat2home</text>} if ($bat2home && $hasbat);
|
||
$ret .= qq{<text class="$stna text" id="node2battxt_$stna" x="1000" y="420" style="text-anchor: start;">$node2bat</text>} if ($node2bat && $hasbat);
|
||
$ret .= qq{<text class="$stna text" id="hometxt_$stna" x="600" y="750" style="text-anchor: end;">$cc</text>}; # Current_Consumption Anlage
|
||
$ret .= qq{<text class="$stna text" id="dummytxt_$stna" x="1380" y="710" style="text-anchor: start;">$cc_dummy</text>} if ($flowgconX && $flowgconsPower); # Current_Consumption Dummy
|
||
|
||
## Textangabe Producer - in Reihenfolge: zum Grid - zum Knoten - zur Batterie
|
||
## Textangabe nur anzeigen wenn Producerzeile angezeigt werden soll
|
||
###############################################################################
|
||
if ($doproducerrow) {
|
||
for my $st (sort keys %{$psorted}) {
|
||
my $left = $psorted->{$st}{start} * 2 - 70; # Übertrag aus Producer Icon Abschnitt, -XX -> Start Lage Producer Beschriftung
|
||
my @sorted = @{$psorted->{$st}{sorted}};
|
||
|
||
for my $lfn (@sorted) {
|
||
my $pn = $pdcr->{$lfn}{pn};
|
||
$currentPower = $pdcr->{$lfn}{p};
|
||
$lcp = length $currentPower;
|
||
|
||
# Leistungszahl abhängig von der Größe entsprechend auf der x-Achse verschieben
|
||
###############################################################################
|
||
if ($lcp >= 5) {$left -= 10}
|
||
elsif ($lcp == 4) {$left += 10}
|
||
elsif ($lcp == 3) {$left += 15}
|
||
elsif ($lcp == 2) {$left += 20}
|
||
elsif ($lcp == 1) {$left += 40}
|
||
|
||
$ret .= qq{<text class="$stna text" id="producertxt_${pn}_$stna" x="$left" y="100">$currentPower</text>} if($flowgPrdsPower);
|
||
|
||
# Leistungszahl wieder zurück an den Ursprungspunkt
|
||
####################################################
|
||
if ($lcp >= 5) {$left += 10}
|
||
elsif ($lcp == 4) {$left -= 10}
|
||
elsif ($lcp == 3) {$left -= 15}
|
||
elsif ($lcp == 2) {$left -= 20}
|
||
elsif ($lcp == 1) {$left -= 40}
|
||
|
||
$left += ($pdist * 2);
|
||
}
|
||
}
|
||
}
|
||
|
||
## Textangabe Consumer
|
||
########################
|
||
if ($flowgcons) {
|
||
$cons_left = ($consumer_start * 2) - 50; # -XX -> Start Lage Consumer Beschriftung
|
||
$y_pos = 1110 + 2 * $exth2cdist;
|
||
$y_pos1 = 1170 + 2 * $exth2cdist;
|
||
|
||
for my $c (@consumers) {
|
||
$currentPower = sprintf "%.1f", $cnsmr->{$c}{p};
|
||
$currentPower = sprintf "%.0f", $currentPower if($currentPower > 10);
|
||
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';
|
||
}
|
||
|
||
$lcp = length $currentPower;
|
||
|
||
#$ret .= qq{<text class="$stna text" id="consumertxt_${c}_$stna" x="$cons_left" y="1110" style="text-anchor: start;">$currentPower</text>} if ($flowgconsPower); # Lage Consumer Consumption
|
||
#$ret .= qq{<text class="$stna text" id="consumertxt_time_${c}_$stna" x="$cons_left" y="1170" style="text-anchor: start;">$consumerTime</text>} if ($flowgconsTime); # Lage Consumer Restlaufzeit
|
||
|
||
# Verbrauchszahl abhängig von der Größe entsprechend auf der x-Achse verschieben
|
||
##################################################################################
|
||
if ($lcp >= 5) {$cons_left -= 40}
|
||
elsif ($lcp == 4) {$cons_left -= 25}
|
||
elsif ($lcp == 3) {$cons_left -= 5 }
|
||
elsif ($lcp == 2) {$cons_left += 7 }
|
||
elsif ($lcp == 1) {$cons_left += 25}
|
||
|
||
$ret .= qq{<text class="$stna text" id="consumertxt_${c}_$stna" x="$cons_left" y="$y_pos">$currentPower</text>} if($flowgconsPower); # Lage Consumer Consumption
|
||
$ret .= qq{<text class="$stna text" id="consumertxt_time_${c}_$stna" x="$cons_left" y="$y_pos1">$consumerTime</text>} if($flowgconsTime); # Lage Consumer Restlaufzeit
|
||
|
||
# Verbrauchszahl wieder zurück an den Ursprungspunkt
|
||
######################################################
|
||
if ($lcp >= 5) {$cons_left += 40}
|
||
elsif ($lcp == 4) {$cons_left += 25}
|
||
elsif ($lcp == 3) {$cons_left += 5 }
|
||
elsif ($lcp == 2) {$cons_left -= 7 }
|
||
elsif ($lcp == 1) {$cons_left -= 25}
|
||
|
||
$cons_left += ($cdist * 2);
|
||
}
|
||
}
|
||
|
||
$ret .= qq{</g></svg>};
|
||
|
||
return $ret;
|
||
}
|
||
|
||
################################################################
|
||
# erzeugt eine Liste der Producernummern sortiert von
|
||
# links nach rechts:
|
||
# -> alle Inverter mit Feed-Typ 'grid'
|
||
# -> alle Producer (nicht PV)
|
||
# -> alle Inverter mit Feed-Typ 'default'
|
||
# -> alle Inverter mit Feed-Typ 'bat'
|
||
################################################################
|
||
sub __sortProducer {
|
||
my $pdcr = shift; # Hashref Producer
|
||
|
||
my @igrid = ();
|
||
my @togrid = ();
|
||
my @prod = ();
|
||
my @idef = ();
|
||
my @tonode = ();
|
||
my @ibat = ();
|
||
my @tobat = ();
|
||
|
||
for my $lfn (sort{$a<=>$b} keys %{$pdcr}) {
|
||
my $ptyp = $pdcr->{$lfn}{ptyp}; # producer | inverter
|
||
my $feed = $pdcr->{$lfn}{feed}; # default | grid | bat
|
||
|
||
push @igrid, $lfn if($ptyp eq 'inverter' && $feed eq 'grid');
|
||
push @prod, $lfn if($ptyp eq 'producer');
|
||
push @idef, $lfn if($ptyp eq 'inverter' && $feed eq 'default');
|
||
push @ibat, $lfn if($ptyp eq 'inverter' && $feed eq 'bat');
|
||
}
|
||
|
||
push @togrid, @igrid;
|
||
push @tonode, @prod;
|
||
push @tonode, @idef;
|
||
push @tobat, @ibat;
|
||
|
||
return (\@togrid, \@tonode, \@tobat);
|
||
}
|
||
|
||
################################################################
|
||
# Producer Icon einfügen
|
||
################################################################
|
||
sub __addProducerIcon {
|
||
my $paref = shift;
|
||
my $hash = $paref->{hash};
|
||
my $name = $paref->{name};
|
||
my $lang = $paref->{lang};
|
||
my $stna = $paref->{stna};
|
||
my $psorted = $paref->{psorted};
|
||
my $pdcr = $paref->{pdcr};
|
||
my $pdist = $paref->{pdist};
|
||
my $y_coord = $paref->{y_coord};
|
||
|
||
my ($scale, $ret);
|
||
|
||
for my $st (sort keys %{$psorted}) {
|
||
my $left = 0;
|
||
my $xicon = $psorted->{$st}{xicon};
|
||
my $count = $psorted->{$st}{count};
|
||
my @sorted = @{$psorted->{$st}{sorted}};
|
||
|
||
if ($count % 2) {
|
||
$xicon = $xicon - ($pdist * ($count - 1) / 2);
|
||
}
|
||
else {
|
||
$xicon = $xicon - ($pdist / 2 * ($count - 1));
|
||
}
|
||
|
||
$psorted->{$st}{start} = $xicon;
|
||
$left = $xicon + 5;
|
||
|
||
for my $lfn (@sorted) {
|
||
my $pn = $pdcr->{$lfn}{pn};
|
||
my ($picon, $ptxt) = __substituteIcon ( { hash => $hash, # Icon des Producerdevices
|
||
name => $name,
|
||
pn => $pn,
|
||
ptyp => $pdcr->{$lfn}{ptyp},
|
||
don => NexthoursVal ($hash, 'NextHour00', 'DoN', 0), # Tag oder Nacht
|
||
pcurr => $pdcr->{$lfn}{p},
|
||
lang => $lang
|
||
}
|
||
);
|
||
|
||
$picon = FW_makeImage ($picon, '');
|
||
($scale, $picon) = __normIconScale ($name, $picon);
|
||
|
||
$ret .= qq{<g id="producer_${pn}_$stna" fill="grey" transform="translate($left,$y_coord),scale($scale)">};
|
||
$ret .= "<title>$ptxt</title>".$picon;
|
||
$ret .= '</g> ';
|
||
|
||
$left += $pdist;
|
||
}
|
||
}
|
||
|
||
return $ret;
|
||
}
|
||
|
||
################################################################
|
||
# Knoten Icon einfügen
|
||
################################################################
|
||
sub __addNodeIcon {
|
||
my $paref = shift;
|
||
my $hash = $paref->{hash};
|
||
my $name = $paref->{name};
|
||
my $lang = $paref->{lang};
|
||
my $stna = $paref->{stna};
|
||
my $pnodesum = $paref->{pnodesum};
|
||
my $x_coord = $paref->{x_coord};
|
||
my $y_coord = $paref->{y_coord};
|
||
|
||
my $scale;
|
||
|
||
my ($nicon, $ntxt) = __substituteIcon ( { hash => $hash,
|
||
name => $name,
|
||
ptyp => 'node',
|
||
pcurr => $pnodesum,
|
||
lang => $lang
|
||
}
|
||
);
|
||
|
||
$nicon = FW_makeImage ($nicon, '');
|
||
($scale, $nicon) = __normIconScale ($name, $nicon);
|
||
|
||
my $ret = qq{<g id="node_$stna" transform="translate($x_coord,$y_coord),scale($scale)">}; # translate(X-Koordinate,Y-Koordinate), scale(<Größe>)-> Koordinaten ändern sich bei Größenänderung
|
||
$ret .= "<title>$ntxt</title>".$nicon;
|
||
$ret .= '</g> ';
|
||
|
||
return $ret;
|
||
}
|
||
|
||
################################################################
|
||
# prüfe ob Icon + Farbe angegeben ist
|
||
# und setze ggf. Ersatzwerte
|
||
# ptyp - Typ der Entität
|
||
# $pn - Positionsnummer (01...max)
|
||
# flag - ein beliebiges Statusflag zur Auswertung
|
||
# msg1 - Text zur freien Verwendung
|
||
# soc - der SOC bei Batterien
|
||
# $don - Day or Night
|
||
# $pcurr - aktuelle Leistung / Verbrauch
|
||
################################################################
|
||
sub __substituteIcon {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $paref->{hash} // $defs{$name};
|
||
my $ptyp = $paref->{ptyp};
|
||
my $pn = $paref->{pn};
|
||
my $msg1 = $paref->{msg1};
|
||
my $flag = $paref->{flag};
|
||
my $soc = $paref->{soc};
|
||
my $don = $paref->{don};
|
||
my $pcurr = $paref->{pcurr};
|
||
my $lang = $paref->{lang};
|
||
|
||
my ($color, $icon);
|
||
my $txt = '';
|
||
|
||
if ($ptyp eq 'consumer') { # Icon Consumer
|
||
($icon, $color) = split '@', ConsumerVal ($hash, $pn, 'icon', CICONDEF);
|
||
|
||
if (!$color) {
|
||
$color = isConsumerLogOn ($hash, $pn, $pcurr) ? CICONCOLDEF : '';
|
||
}
|
||
}
|
||
elsif ($ptyp eq 'battery') { # Icon Batterie
|
||
my ($ircmd, $icharge, $idischrg, $inorcmd) = split ':', BatteryVal ($hash, $pn, 'bicon', '');
|
||
|
||
my $soctxt = '';
|
||
my $pretxt = '';
|
||
my $socicon;
|
||
|
||
if (defined $soc) {
|
||
$soctxt = "\n".$htitles{socbatfc}{$lang}.": ".$soc." %"; # Text 'SoC Prognose'
|
||
|
||
$socicon = $soc >= 80 ? 'measure_battery_100' :
|
||
$soc >= 60 ? 'measure_battery_75' :
|
||
$soc >= 40 ? 'measure_battery_50' :
|
||
$soc >= 20 ? 'measure_battery_25' :
|
||
'measure_battery_0';
|
||
}
|
||
|
||
$ircmd = $ircmd ? $ircmd : '';
|
||
$inorcmd = $inorcmd ? $inorcmd : '';
|
||
$icharge = $icharge ? $icharge : '';
|
||
$idischrg = $idischrg ? $idischrg : '';
|
||
|
||
if (defined $flag) { # Empfehlungszeitraum
|
||
if ($flag) { # Ladefreigabe
|
||
($icon, $color) = split '@', $ircmd;
|
||
$icon = $icon ? $icon :
|
||
$socicon ? $socicon :
|
||
BICONDEF; # nur Farbe angegeben
|
||
|
||
$color //= BICCOLRCDDEF;
|
||
|
||
if ($flag eq 'hist') { # erreichter SoC vergangener Stunden
|
||
$pretxt = $htitles{onlybatw}{$lang}." $pn: $msg1";
|
||
}
|
||
else { # prognostizierte Ladefreigabe
|
||
$pretxt = $htitles{onlybatw}{$lang}." $pn: $msg1\n".$htitles{bcharrel}{$lang};
|
||
}
|
||
}
|
||
else { # keine Ladefreigabe
|
||
($icon, $color) = split '@', $inorcmd;
|
||
$icon = $icon ? $icon :
|
||
$socicon ? $socicon :
|
||
BICONDEF; # nur Farbe angegeben
|
||
|
||
$color //= BICCOLNRCDDEF;
|
||
$pretxt = $htitles{onlybatw}{$lang}." $pn: $msg1\n".$htitles{bncharel}{$lang};
|
||
}
|
||
}
|
||
|
||
if (defined $pcurr) { # aktueller Zusatnd
|
||
if ($pcurr > 0) { # Batterie wird aufgeladen
|
||
($icon, $color) = split '@', $icharge;
|
||
$icon = $icon ? $icon :
|
||
$socicon ? $socicon :
|
||
BICONDEF; # nur Farbe angegeben
|
||
|
||
$color //= BCHGICONCOLDEF;
|
||
$txt = "$pretxt\nStatus: ".$htitles{ischawth}{$lang}.' '.$pcurr.' W'.$soctxt;
|
||
}
|
||
elsif ($pcurr < 0) { # Batterie wird entladen
|
||
($icon, $color) = split '@', $idischrg;
|
||
$icon = $icon ? $icon :
|
||
$socicon ? $socicon :
|
||
BICONDEF; # nur Farbe angegeben
|
||
|
||
$color //= BDCHICONCOLDEF;
|
||
$txt = "$pretxt\nStatus: ".$htitles{isdchawt}{$lang}.' '.(abs $pcurr).' W'.$soctxt;
|
||
}
|
||
else { # Standby
|
||
($icon, $color) = split '@', $ircmd;
|
||
$icon = $icon ? $icon :
|
||
$socicon ? $socicon :
|
||
BICONDEF; # nur Farbe angegeben
|
||
|
||
$color //= BICCOLRCDDEF;
|
||
$txt = "$pretxt\nStatus: Standby".$soctxt;
|
||
}
|
||
}
|
||
else {
|
||
if (defined $flag && $flag eq 'hist') { # Text 'SoC am Ende der Stunde'
|
||
$soctxt = "\n".$htitles{socbaths}{$lang}.": ".$soc." %";
|
||
}
|
||
|
||
$txt = $pretxt.$soctxt; # resultierender Text
|
||
}
|
||
}
|
||
elsif ($ptyp eq 'producer') { # Icon Producer
|
||
($icon, $color) = split '@', ProducerVal ($hash, $pn, 'picon', PRODICONDEF);
|
||
$txt = ProducerVal ($hash, $pn, 'palias', '');
|
||
|
||
if (!$pcurr) {
|
||
$color = 'grey';
|
||
}
|
||
}
|
||
elsif ($ptyp eq 'inverter') { # Inverter, Smartloader
|
||
my ($iday, $inight) = split ':', InverterVal ($hash, $pn, 'iicon', INVICONDEF);
|
||
|
||
if ($don || $pcurr) { # Tag -> eigenes Icon oder Standard
|
||
$txt = InverterVal ($hash, $pn, 'ialias', '');
|
||
$iday = $iday ? $iday : INVICONDEF;
|
||
($icon, $color) = split '@', $iday;
|
||
$color = !$pcurr ? INACTCOLDEF :
|
||
$color ? $color :
|
||
ACTCOLDEF;
|
||
}
|
||
else { # Nacht -> eigenes Icon oder Mondphase
|
||
my $mpi = CurrentVal ($hash, 'moonPhaseI', MOONICONDEF);
|
||
|
||
if ($inight) { # eigenes Icon + ggf. Farbe
|
||
($icon, $color) = split '@', $inight;
|
||
$color = $color ? $color : INACTCOLDEF;
|
||
}
|
||
else {
|
||
$icon = $hmoon{$mpi}{icon}.'@'.MOONCOLDEF;
|
||
$txt = $hmoon{$mpi}{$lang};
|
||
($icon, $color) = split '@', $icon;
|
||
}
|
||
}
|
||
}
|
||
elsif ($ptyp eq 'node') { # Knoten-Icon
|
||
($icon, $color) = split '@', NODEICONDEF;
|
||
$color = !$pcurr ? INACTCOLDEF :
|
||
$color ? $color :
|
||
ACTCOLDEF;
|
||
}
|
||
|
||
$icon .= '@'.$color if($color);
|
||
|
||
return ($icon, $txt);
|
||
}
|
||
|
||
################################################################
|
||
# normiere Nachkommastellen
|
||
# Standard - .xx (zwei Nachkommastellen)
|
||
# wenn > 10 - xx (keine Nachkommastelle)
|
||
# wenn 0.0 - 0 (logisch 0)
|
||
################################################################
|
||
sub __normDecPlaces {
|
||
my $p = shift;
|
||
|
||
$p = sprintf "%.2f", $p;
|
||
$p = sprintf "%.0f", $p if($p > 10);
|
||
$p = 0 if($p == 0);
|
||
|
||
return $p;
|
||
}
|
||
|
||
################################################################
|
||
# berechne Icon width, height auf Sollnormativ
|
||
# width: 470pt
|
||
# height: 470pt
|
||
# scale: 0.10 Normativ FGSCALEDEF
|
||
################################################################
|
||
sub __normIconScale {
|
||
my $name = shift;
|
||
my $icon = shift;
|
||
my $dim = shift // 470; # Dimension
|
||
|
||
my $hscale = FGSCALEDEF; # Scale Normativ
|
||
my $wscale = FGSCALEDEF;
|
||
my ($width, $wunit) = $icon =~ /width="(\d+\.\d+|\d+)(.*?)"/xs;
|
||
my ($height, $hunit) = $icon =~ /height="(\d+\.\d+|\d+)(.*?)"/xs;
|
||
|
||
return ($hscale, $icon) if(!$width || !$height);
|
||
|
||
$wscale = $hunit eq 'pt' ? $dim * $wscale / $width :
|
||
$hunit eq 'px' ? $dim * $wscale / $width * 0.96 :
|
||
$hunit eq 'in' ? $dim * $wscale / $width * 0.0138889 :
|
||
$hunit eq 'mm' ? $dim * $wscale / $width * 0.352778 :
|
||
$hunit eq 'cm' ? $dim * $wscale / $width * 0.0352778 :
|
||
$hunit eq 'pc' ? $dim * $wscale / $width * 0.0833333 :
|
||
$wscale;
|
||
|
||
$hscale = $hunit eq 'pt' ? $dim * $hscale / $height :
|
||
$hunit eq 'px' ? $dim * $hscale / $height * 0.96 :
|
||
$hunit eq 'in' ? $dim * $hscale / $height * 0.0138889 :
|
||
$hunit eq 'mm' ? $dim * $hscale / $height * 0.352778 :
|
||
$hunit eq 'cm' ? $dim * $hscale / $height * 0.0352778 :
|
||
$hunit eq 'pc' ? $dim * $hscale / $height * 0.0833333 :
|
||
$hscale;
|
||
|
||
$wscale = sprintf "%.2f", $wscale;
|
||
$hscale = sprintf "%.2f", $hscale;
|
||
|
||
my $widthnormpt = (sprintf "%.0f", ($dim * (1 + $wscale))).'pt'; # Breite auf Normativ in pt skaliert
|
||
my $heightnormpt = (sprintf "%.0f", ($dim * (1 + $hscale))).'pt'; # Höhe auf Normativ in pt skaliert
|
||
|
||
$icon =~ s/width="(.*?)"/width="$widthnormpt"/;
|
||
$icon =~ s/height="(.*?)"/height="$heightnormpt"/;
|
||
|
||
return (FGSCALEDEF, $icon);
|
||
}
|
||
|
||
################################################################
|
||
# 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 normBeamWidth {
|
||
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(grep /^$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 $name = $paref->{name};
|
||
my $aln = $paref->{aln}; # Autolearning
|
||
my $h = $paref->{h};
|
||
|
||
my $hash = $defs{$name};
|
||
my $rho = sprintf "%02d", $h;
|
||
|
||
if (!$aln) {
|
||
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};
|
||
|
||
return;
|
||
}
|
||
|
||
####################################################################################################
|
||
# Abruf und Einlesen Messagefile nonBlocking
|
||
# $data{$name}{filemessages}{999000}{TSNEXT}: Timestamp nächster Pull Message File
|
||
####################################################################################################
|
||
sub getMessageFileNonBlocking {
|
||
my $hash = shift;
|
||
my $name = $hash->{NAME};
|
||
|
||
my $tsnext = gettimeofday() + GMFILEREPEAT + int(rand(GMFILERANDOM));
|
||
|
||
RemoveInternalTimer ($hash, "FHEM::SolarForecast::getMessageFileNonBlocking");
|
||
InternalTimer ($tsnext, "FHEM::SolarForecast::getMessageFileNonBlocking", $hash, 0);
|
||
|
||
my (undef, $disabled, $inactive) = controller ($name);
|
||
return if($disabled || $inactive);
|
||
|
||
delete $hash->{HELPER}{GMFRUNNING} if(defined $hash->{HELPER}{GMFRUNNING}{pid} && $hash->{HELPER}{GMFRUNNING}{pid} =~ /DEAD/xs);
|
||
|
||
if (defined $hash->{HELPER}{GMFRUNNING}{pid}) {
|
||
Log3 ($name, 3, qq{$name - another Message File Process with PID "$hash->{HELPER}{GMFRUNNING}{pid}" is already running ... get Message File is aborted});
|
||
return;
|
||
}
|
||
|
||
my $paref = { name => $name,
|
||
tsnext => $tsnext,
|
||
block => 1
|
||
};
|
||
|
||
$hash->{HELPER}{GMFRUNNING} = BlockingCall ( "FHEM::SolarForecast::_retrieveMessageFile",
|
||
$paref,
|
||
"FHEM::SolarForecast::_processMessageFile",
|
||
GMFBLTO,
|
||
"FHEM::SolarForecast::_abortGetMessageFile",
|
||
$hash
|
||
);
|
||
|
||
|
||
if (defined $hash->{HELPER}{GMFRUNNING}) {
|
||
$hash->{HELPER}{GMFRUNNING}{loglevel} = 3;
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
###############################################################
|
||
# Message File aus Quelle abholen
|
||
###############################################################
|
||
sub _retrieveMessageFile {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $block = $paref->{block} // 0;
|
||
|
||
Log3 ($name, 4, "$name - Notification System - Message File >$messagefile< is retrieved non blocking");
|
||
Log3 ($name, 4, "$name - Notification System - Message File Source: GitHub Repository");
|
||
|
||
my $valid = 1;
|
||
my ($err, $remfile) = __httpBlockingGet ($name, BGHPATH.$messagefile.PGHPATH);
|
||
|
||
$remfile = q{} if($remfile =~ /No\snode\strunk\/fhem\/contrib\/SolarForecast\//xs);
|
||
|
||
if ($err) {
|
||
$valid = 0;
|
||
Log3 ($name, 4, "$name - Notification System - retrieve of remote Message File faulty: $err");
|
||
}
|
||
|
||
if (!$remfile) {
|
||
$valid = 0;
|
||
Log3 ($name, 4, "$name - Notification System - no remote Message File >$messagefile< found");
|
||
}
|
||
|
||
if ($valid) {
|
||
$err = __updWriteFile ("$root/FHEM", $messagefile, $remfile);
|
||
|
||
if ($err) {
|
||
$valid = 0;
|
||
Log3 ($name, 1, "$name - $err");
|
||
}
|
||
else {
|
||
Log3 ($name, 4, "$name - Notification System - new Message File updated to $root/FHEM/$messagefile");
|
||
}
|
||
}
|
||
|
||
$paref->{valid} = $valid;
|
||
my $serial = encode_base64 (Serialize ( $paref ), ""); # Serialisierung
|
||
|
||
$block ? return ($serial) : return \&_processMessageFile ($serial);
|
||
|
||
return;
|
||
}
|
||
|
||
###############################################################
|
||
# Folgeroutine nach Message File aus Quelle abholen
|
||
###############################################################
|
||
sub _processMessageFile {
|
||
my $serial = decode_base64 (shift);
|
||
|
||
my $paref = eval { thaw ($serial) }; # Deserialisierung
|
||
my $name = $paref->{name};
|
||
my $valid = $paref->{valid};
|
||
my $hash = $defs{$name};
|
||
|
||
if ($valid) {
|
||
__readFileMessages ($paref);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
####################################################################################################
|
||
# Messagefile für Notification System lesen
|
||
# Filestruktur:
|
||
# 0|SV|1
|
||
# 0|DE|Mitteilung ....
|
||
# 0|EN|Message...
|
||
# $data{$name}{messages}{999000}{TS}: Timestamp Stand Message File
|
||
# $data{$name}{filemessages}{999000}{TSNEXT}: Timestamp nächster Pull Message File
|
||
####################################################################################################
|
||
sub __readFileMessages {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $tsnext = $paref->{tsnext};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
open (FD, "$root/FHEM/$messagefile") or do { return $! };
|
||
|
||
delete $data{$name}{filemessages};
|
||
|
||
my @locList = map { $_ =~ s/[\r\n]//; $_ } <FD>;
|
||
close (FD);
|
||
|
||
Log3 ($name, 4, "$name - Notification System - read local Message File >$messagefile< with ".scalar @locList." entries.");
|
||
|
||
for my $l (@locList) {
|
||
next if ($l =~ /^\#/xs);
|
||
my @l = split /\|/, $l, 3;
|
||
next if(!isNumeric ($l[0]));
|
||
next if($l[1] !~ /^(DE|EN|SV)$/xs);
|
||
|
||
$data{$name}{filemessages}{$l[0]}{$l[1]} = $l[2];
|
||
}
|
||
|
||
$data{$name}{filemessages}{999000}{TS} = time;
|
||
$data{$name}{filemessages}{999000}{TSNEXT} = $tsnext;
|
||
|
||
return;
|
||
}
|
||
|
||
####################################################################################################
|
||
# Abbruchroutine BlockingCall Timeout
|
||
####################################################################################################
|
||
sub _abortGetMessageFile {
|
||
my $hash = shift;
|
||
my $cause = shift // "Timeout: process terminated";
|
||
my $name = $hash->{NAME};
|
||
my $type = $hash->{TYPE};
|
||
|
||
Log3 ($name, 1, "$name -> BlockingCall $hash->{HELPER}{GMFRUNNING}{fn} pid:$hash->{HELPER}{AIBLOCKRUNNING}{pid} aborted: $cause");
|
||
|
||
delete $hash->{HELPER}{GMFRUNNING};
|
||
|
||
return;
|
||
}
|
||
|
||
####################################################################################################
|
||
# Mitteilungssystem füllen
|
||
# Schweregrad SV:
|
||
# 0 - keine Mitteilung
|
||
# 1 - Mitteilung
|
||
# 2 - Warnung
|
||
# 3 - Fehler / Problem
|
||
#
|
||
# Statusspeicher:
|
||
# $data{$name}{messages}{999999}{RD}: 1 - gelesen, 0 - ungelesen
|
||
# $data{$name}{messages}{999000}{TS}: Timestamp Stand Message File
|
||
# $data{$name}{filemessages}{999000}{TSNEXT}: Timestamp nächster Pull Message File
|
||
# $data{$name}{messages}{999500}{TS}: Timestamp Stand prepared Messages
|
||
####################################################################################################
|
||
sub fillupMessageSystem {
|
||
my $paref = shift;
|
||
my $hash = $paref->{hash};
|
||
my $name = $paref->{name};
|
||
my $lang = $paref->{lang};
|
||
|
||
my $otxt = q{};
|
||
my $ntxt = q{};
|
||
my $midx = 0;
|
||
my $max_sv = 0;
|
||
|
||
## Aufnahme Stand für alt/neu Vergleich + Clear Messages
|
||
##########################################################
|
||
for my $idx (sort keys %{$data{$name}{messages}}) {
|
||
next if($idx >= IDXLIMIT);
|
||
$otxt .= $data{$name}{messages}{$idx}{SV} if(defined $data{$name}{messages}{$idx}{SV});
|
||
$otxt .= $data{$name}{messages}{$idx}{DE} if(defined $data{$name}{messages}{$idx}{DE});
|
||
$otxt .= $data{$name}{messages}{$idx}{EN} if(defined $data{$name}{messages}{$idx}{EN});
|
||
|
||
delete $data{$name}{messages}{$idx};
|
||
}
|
||
|
||
## Messages füllen
|
||
########################################################################
|
||
# Integration File Messages
|
||
for my $mfi (sort keys %{$data{$name}{filemessages}}) {
|
||
next if($mfi >= IDXLIMIT);
|
||
$midx++;
|
||
$data{$name}{messages}{$midx}{SV} = $data{$name}{filemessages}{$mfi}{SV};
|
||
$data{$name}{messages}{$midx}{DE} = $data{$name}{filemessages}{$mfi}{DE};
|
||
$data{$name}{messages}{$midx}{EN} = $data{$name}{filemessages}{$mfi}{EN};
|
||
}
|
||
|
||
# Integration prepared Messages
|
||
for my $smi (sort keys %{$data{$name}{preparedmessages}}) {
|
||
next if($smi >= IDXLIMIT);
|
||
$midx++;
|
||
$data{$name}{messages}{$midx}{SV} = $data{$name}{preparedmessages}{$smi}{SV};
|
||
$data{$name}{messages}{$midx}{DE} = encode ("utf8", $data{$name}{preparedmessages}{$smi}{DE});
|
||
$data{$name}{messages}{$midx}{EN} = encode ("utf8", $data{$name}{preparedmessages}{$smi}{EN});
|
||
}
|
||
|
||
$data{$name}{messages}{999000}{TS} = $data{$name}{filemessages}{999000}{TS} // 0;
|
||
$data{$name}{messages}{999000}{TSNEXT} = $data{$name}{filemessages}{999000}{TSNEXT} // 0;
|
||
$data{$name}{messages}{999500}{TS} = $data{$name}{preparedmessages}{999500}{TS} // 0;
|
||
|
||
########################################################################
|
||
## Ende Messages auffüllen
|
||
|
||
|
||
## Vergleich auf geänderte Messages
|
||
#####################################
|
||
for my $idx (sort keys %{$data{$name}{messages}}) {
|
||
next if($idx >= IDXLIMIT);
|
||
$ntxt .= $data{$name}{messages}{$idx}{SV} if(defined $data{$name}{messages}{$idx}{SV});
|
||
$ntxt .= $data{$name}{messages}{$idx}{DE} if(defined $data{$name}{messages}{$idx}{DE});
|
||
$ntxt .= $data{$name}{messages}{$idx}{EN} if(defined $data{$name}{messages}{$idx}{EN});
|
||
}
|
||
|
||
if ($ntxt ne $otxt) { # es gibt neue Post! bzw. Änderungen -> Read-Bit läschen
|
||
delete $data{$name}{messages}{999999}{RD};
|
||
}
|
||
|
||
|
||
if ($midx && !defined $data{$name}{messages}{999999}{RD}) { # RD = Read-Bit (undef -> Messages nicht gelesen)
|
||
my @aidx = map { $_ } (1..$midx); # größte vorhandene Severity finden ...
|
||
my @values = map { $data{$name}{messages}{$_}{SV} } @aidx;
|
||
$max_sv = max(@values);
|
||
}
|
||
|
||
my $max_icon = $svicons{$max_sv}; # ... und das dazugehörige Icon
|
||
|
||
return ($max_icon, $midx);
|
||
}
|
||
|
||
################################################################
|
||
# Ausgabe des Mitteilungsystems
|
||
################################################################
|
||
sub outputMessages {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $lang = $paref->{lang};
|
||
|
||
my ($micon, $midx) = fillupMessageSystem ($paref); # Ergebnisse füllen (sind leer wenn Browser nicht refreshed)
|
||
my $tfl = $data{$name}{messages}{999000}{TS} ?
|
||
(timestampToTimestring ($data{$name}{messages}{999000}{TS}, $lang))[0] :
|
||
'n.a.';
|
||
|
||
my $tfn = $data{$name}{messages}{999000}{TSNEXT} ?
|
||
(timestampToTimestring ($data{$name}{messages}{999000}{TSNEXT}, $lang))[0] :
|
||
'n.a.';
|
||
|
||
my $tpm = $data{$name}{messages}{999500}{TS} ?
|
||
(timestampToTimestring ($data{$name}{messages}{999500}{TS}, $lang))[0] :
|
||
'n.a.';
|
||
## Ausgabe
|
||
############
|
||
my $out = qq{<html>};
|
||
$out .= qq{<b>$hqtxt{msgsys}{$lang}</b> <br><br>};
|
||
$out .= qq{$hqtxt{impcha}{$lang} - <b>File:</b> $tfl ($hqtxt{scedld}{$lang}: $tfn), <b>System:</b> $tpm <br>};
|
||
$out .= qq{($hqtxt{dmgsig}{$lang}) <br><br>};
|
||
|
||
$out .= qq{<table class="roomoverview" style="text-align:left; border:1px solid; padding:5px; border-spacing:5px; margin-left:auto; margin-right:auto;">};
|
||
$out .= qq{<tr style="font-weight:bold;">};
|
||
$out .= qq{<td style="text-decoration:underline;"> Pos. </td>};
|
||
$out .= qq{<td> </td>};
|
||
$out .= qq{<td style="text-decoration:underline;"> $hqtxt{msgimp}{$lang} </td>};
|
||
$out .= qq{<td> </td>};
|
||
$out .= qq{<td style="text-decoration:underline;"> $hqtxt{simsg}{$lang} </td>};
|
||
$out .= qq{</tr>};
|
||
$out .= qq{<tr></tr>};
|
||
|
||
my $hc = 0;
|
||
|
||
for my $key (sort keys %{$data{$name}{messages}}) {
|
||
next if($key >= IDXLIMIT);
|
||
|
||
$hc++;
|
||
#my $enmsg = encode ("utf8", $data{$name}{messages}{$key}{$lang});
|
||
my $enmsg = $data{$name}{messages}{$key}{$lang};
|
||
|
||
$out .= qq{<tr>};
|
||
$out .= qq{<td style="padding: 5px; text-align: center"> $key </td>};
|
||
$out .= qq{<td style="padding: 5px;"> </td>};
|
||
$out .= qq{<td style="padding: 5px; text-align: center"> $data{$name}{messages}{$key}{SV} </td>};
|
||
$out .= qq{<td style="padding: 5px;"> </td>};
|
||
$out .= qq{<td style="padding-right: 5px; text-align: left"> $enmsg </td>};
|
||
$out .= qq{</tr>};
|
||
|
||
if ($hc < $midx) { # Zwischenzeile
|
||
$out .= qq{<tr>};
|
||
$out .= qq{<td> </td>};
|
||
$out .= qq{</tr>};
|
||
}
|
||
}
|
||
|
||
$out .= qq{</table>};
|
||
$out .= qq{</html>};
|
||
|
||
$out .= "<br>";
|
||
$out .= $hqtxt{legimp}{$lang};
|
||
|
||
return $out;
|
||
}
|
||
|
||
###############################################################
|
||
# Eintritt in den KI Train Prozess normal/Blocking
|
||
###############################################################
|
||
sub manageTrain {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $hash = $defs{$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};
|
||
my $aiRulesNumber = $paref->{aiRulesNumber};
|
||
|
||
delete $data{$name}{circular}{99}{aiRulesNumber};
|
||
|
||
$data{$name}{current}{aiAddedToTrain} = 0;
|
||
$data{$name}{current}{aicanuse} = $aicanuse;
|
||
$data{$name}{current}{aitrainstate} = $aitrainstate;
|
||
$data{$name}{current}{aiinitstate} = $aiinitstate if(defined $aiinitstate);
|
||
$data{$name}{circular}{99}{runTimeTrainAI} = $runTimeTrainAI if(defined $runTimeTrainAI); # !! in Circular speichern um zu persistieren, setTimeTracking speichert zunächst in Current !!
|
||
$data{$name}{circular}{99}{aitrainLastFinishTs} = $aitrainFinishTs if(defined $aitrainFinishTs);
|
||
$data{$name}{circular}{99}{aiRulesNumber} = $aiRulesNumber if(defined $aiRulesNumber);
|
||
|
||
if ($aitrainstate eq 'ok') {
|
||
readCacheFile ({ 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{$name}{current}{aitrainstate} = 'Traing (Child) process timed out';
|
||
$data{$name}{current}{aiAddedToTrain} = 0;
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# KI Instanz(en) aus Raw Daten Hash erzeugen
|
||
################################################################
|
||
sub aiAddInstancePV { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $taa = $paref->{taa}; # do train after add
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
return if(!isPrepared4AI ($hash));
|
||
|
||
my $err = aiInit ($paref);
|
||
return if($err);
|
||
my $dtree = AiDetreeVal ($hash, 'object', undef);
|
||
|
||
$data{$name}{current}{aiAddedToTrain} = 0;
|
||
|
||
for my $idx (sort keys %{$data{$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', undef);
|
||
my $wcc = AiRawdataVal ($hash, $idx, 'wcc', undef);
|
||
my $rr1c = AiRawdataVal ($hash, $idx, 'rr1c', undef);
|
||
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 - aiAddInstancePV ERROR: $@");
|
||
$data{$name}{current}{aiaddistate} = $@;
|
||
return;
|
||
};
|
||
|
||
$data{$name}{current}{aiAddedToTrain}++;
|
||
|
||
debugLog ($paref, 'aiProcess', "AI Instance added $idx - hod: $hod, sunalt: $sunalt, rad1h: $rad1h, pvrl: $pvrl, wcc: ".(defined $wcc ? $wcc : '-').", rr1c: ".(defined $rr1c ? $rr1c : '-').", temp: ".(defined $temp ? $temp : '-'), 4);
|
||
}
|
||
|
||
debugLog ($paref, 'aiProcess', "AI Instance add - ".$data{$name}{current}{aiAddedToTrain}." entities added for training ".(AttrVal ($name, 'verbose', 3) != 4 ? '(set verbose 4 for output more detail)' : ''));
|
||
|
||
$data{$name}{aidectree}{object} = $dtree;
|
||
$data{$name}{current}{aiaddistate} = 'ok';
|
||
|
||
if ($taa) {
|
||
manageTrain ($paref);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# KI trainieren
|
||
################################################################
|
||
sub aiTrain { ## no critic "not used"
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $block = $paref->{block} // 0;
|
||
|
||
my $hash = $defs{$name};
|
||
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{$name}{aidectree}{aitrained} = $dtree;
|
||
$err = writeCacheToFile ($hash, 'aitrained', $aitrained.$name);
|
||
|
||
if (!$err) {
|
||
debugLog ($paref, 'aiProcess', qq{AI trained number of entities: }. $data{$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,
|
||
aiRulesNumber => scalar $dtree->rule_statements(), # Returns a list of strings that describe the tree in rule-form
|
||
aicanuse => 'ok'
|
||
}
|
||
)
|
||
, "");
|
||
|
||
delete $data{$name}{current}{runTimeTrainAI};
|
||
|
||
$block ? return ($serial) : return \&finishTrain ($serial);
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# AI Ergebnis für ermitteln
|
||
################################################################
|
||
sub aiGetResult {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $hod = $paref->{hod};
|
||
my $nhtstr = $paref->{nhtstr};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
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, 'wcc', 0);
|
||
my $rr1c = NexthoursVal ($hash, $nhtstr, 'rr1c', 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 ( { 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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if (!isPrepared4AI ($hash)) {
|
||
delete $data{$name}{circular}{99}{aiRulesNumber};
|
||
delete $data{$name}{circular}{99}{runTimeTrainAI};
|
||
delete $data{$name}{circular}{99}{aitrainLastFinishTs};
|
||
|
||
my $err = CurrentVal ($hash, 'aicanuse', '');
|
||
|
||
debugLog ($paref, 'aiProcess', $err);
|
||
|
||
$data{$name}{current}{aiinitstate} = $err;
|
||
return $err;
|
||
}
|
||
|
||
my $dtree = new AI::DecisionTree ( verbose => 0, noise_mode => 'pick_best' );
|
||
|
||
$data{$name}{aidectree}{object} = $dtree;
|
||
$data{$name}{current}{aiinitstate} = 'ok';
|
||
|
||
Log3 ($name, 3, "$name - AI::DecisionTree initialized");
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Daten der Raw Datensammlung hinzufügen
|
||
################################################################
|
||
sub aiAddRawData {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $yday = $paref->{yday}; # vorheriger Tag (falls gesetzt)
|
||
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
|
||
my $dayname = $paref->{dayname};
|
||
my $ydayname = $paref->{ydayname};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
delete $data{$name}{current}{aitrawstate};
|
||
|
||
my $err;
|
||
my $dosave = 0;
|
||
|
||
$day = $yday if(defined $yday); # der vergangene Tag soll verarbeitet werden
|
||
$dayname = $ydayname if(defined $ydayname); # Name des Vortages
|
||
|
||
for my $pvd (sort keys %{$data{$name}{pvhist}}) {
|
||
next if(!$pvd);
|
||
|
||
if ($ood) {
|
||
next if($pvd ne $day);
|
||
}
|
||
|
||
last if(int $pvd > int $day);
|
||
|
||
for my $hod (sort keys %{$data{$name}{pvhist}{$pvd}}) {
|
||
next if(!$hod || $hod eq '99' || ($rho && $hod ne $rho));
|
||
|
||
my $ridx = _aiMakeIdxRaw ($pvd, $hod, $paref->{yt});
|
||
|
||
my $temp = HistoryVal ($hash, $pvd, $hod, 'temp', undef);
|
||
my $sunalt = HistoryVal ($hash, $pvd, $hod, 'sunalt', 0);
|
||
my $sunaz = HistoryVal ($hash, $pvd, $hod, 'sunaz', 0);
|
||
my $con = HistoryVal ($hash, $pvd, $hod, 'con', undef);
|
||
my $wcc = HistoryVal ($hash, $pvd, $hod, 'wcc', undef);
|
||
my $rr1c = HistoryVal ($hash, $pvd, $hod, 'rr1c', undef);
|
||
my $rad1h = HistoryVal ($hash, $pvd, $hod, 'rad1h', undef);
|
||
|
||
my $tbin = temp2bin ($temp) if(defined $temp);
|
||
my $cbin = cloud2bin ($wcc) if(defined $wcc);
|
||
my $sabin = sunalt2bin ($sunalt);
|
||
|
||
$data{$name}{aidectree}{airaw}{$ridx}{sunalt} = $sabin;
|
||
$data{$name}{aidectree}{airaw}{$ridx}{sunaz} = $sunaz;
|
||
$data{$name}{aidectree}{airaw}{$ridx}{dayname} = $dayname;
|
||
$data{$name}{aidectree}{airaw}{$ridx}{hod} = $hod;
|
||
$data{$name}{aidectree}{airaw}{$ridx}{temp} = $tbin if(defined $tbin);
|
||
$data{$name}{aidectree}{airaw}{$ridx}{con} = $con if(defined $con && $con >= 0);
|
||
$data{$name}{aidectree}{airaw}{$ridx}{wcc} = $cbin if(defined $cbin);
|
||
$data{$name}{aidectree}{airaw}{$ridx}{rr1c} = $rr1c if(defined $rr1c);
|
||
$data{$name}{aidectree}{airaw}{$ridx}{rad1h} = $rad1h if(defined $rad1h && $rad1h > 0);
|
||
|
||
$dosave++;
|
||
|
||
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 $pvrl = HistoryVal ($hash, $pvd, $hod, 'pvrl', undef);
|
||
$data{$name}{aidectree}{airaw}{$ridx}{pvrl} = $pvrl if(defined $pvrl && $pvrl > 0);
|
||
|
||
debugLog ($paref, 'aiProcess', "AI raw add - idx: $ridx, day: $pvd, hod: $hod, sunalt: $sabin, sunaz: $sunaz, rad1h: ".(defined $rad1h ? $rad1h : '-').", pvrl: ".(defined $pvrl ? $pvrl : '-').", con: ".(defined $con ? $con : '-').", wcc: ".(defined $cbin ? $cbin : '-').", rr1c: ".(defined $rr1c ? $rr1c : '-').", temp: ".(defined $tbin ? $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{$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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
if (!keys %{$data{$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{$name}{current}{aitrawstate};
|
||
|
||
my ($err, $dosave);
|
||
|
||
for my $idx (sort keys %{$data{$name}{aidectree}{airaw}}) {
|
||
next if(!$idx || $idx > $didx);
|
||
delete $data{$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{$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;
|
||
}
|
||
|
||
################################################################
|
||
# einen Schlüssel-Wert in die pvHistory schreiben
|
||
# $valid - Wert für Valid-Key festgelegt in $hfspvh Hash
|
||
# z.B. pvrlvd = 1: beim Learning berücksichtigen, 0: nicht
|
||
################################################################
|
||
sub writeToHistory {
|
||
my $ph = shift;
|
||
my $paref = $ph->{paref};
|
||
my $key = $ph->{key};
|
||
my $val = $ph->{val};
|
||
my $hour = $ph->{hour};
|
||
my $valid = $ph->{valid};
|
||
|
||
$paref->{val} = $val;
|
||
$paref->{nhour} = sprintf "%02d", $hour;
|
||
$paref->{histname} = $key;
|
||
|
||
if (defined $hfspvh{$key}{validkey}) {
|
||
$paref->{$hfspvh{$key}{validkey}} = $valid;
|
||
}
|
||
|
||
setPVhistory ($paref);
|
||
|
||
delete $paref->{histname};
|
||
delete $paref->{nhour};
|
||
delete $paref->{val};
|
||
delete $paref->{$hfspvh{$key}{validkey}} if(defined $hfspvh{$key}{validkey});
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# History-Hash verwalten
|
||
################################################################
|
||
sub setPVhistory {
|
||
my $paref = shift;
|
||
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 $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
|
||
|
||
my $hash = $defs{$name};
|
||
|
||
$data{$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{$name}{pvhist}{$day}{$nhour}{$histname} = $val;
|
||
|
||
if ($histname =~ /csme[0-9]+$/xs) {
|
||
my $sum = 0;
|
||
|
||
for my $k (keys %{$data{$name}{pvhist}{$day}}) {
|
||
next if($k eq "99");
|
||
my $csme = HistoryVal ($hash, $day, $k, "$histname", 0);
|
||
next if(!$csme);
|
||
|
||
$sum += $csme;
|
||
}
|
||
|
||
$data{$name}{pvhist}{$day}{99}{$histname} = sprintf "%.2f", $sum;
|
||
}
|
||
}
|
||
|
||
if ($histname =~ /minutescsm[0-9]+$/xs) { # Anzahl Aktivminuten des Verbrauchers
|
||
$data{$name}{pvhist}{$day}{$nhour}{$histname} = $val;
|
||
my $minutes = 0;
|
||
my $num = substr ($histname,10,2);
|
||
|
||
for my $k (keys %{$data{$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);
|
||
|
||
if ($cycles) {
|
||
$data{$name}{pvhist}{$day}{99}{"hourscsme${num}"} = sprintf "%.2f", ($minutes / 60 );
|
||
$data{$name}{pvhist}{$day}{99}{"avgcycmntscsm${num}"} = sprintf "%.2f", ($minutes / $cycles);
|
||
}
|
||
}
|
||
|
||
if ($histname =~ /cyclescsm[0-9]+$/xs) { # Anzahl Tageszyklen des Verbrauchers
|
||
$data{$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 ($r3, $r4, $r5, $r6, $r7, $r8) = (0,0,0,0,0,0);
|
||
my $ien = {}; # Hashref Inverter energy
|
||
my $pen = {}; # Hashref Producer energy
|
||
my $bin = {};
|
||
my $bot = {};
|
||
|
||
for my $k (keys %{$data{$name}{pvhist}{$reorgday}}) {
|
||
next if($k eq "99");
|
||
|
||
$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);
|
||
|
||
## Reorg Inverter
|
||
##################
|
||
for my $in (1..MAXINVERTER) {
|
||
$in = sprintf "%02d", $in;
|
||
my $e = HistoryVal ($hash, $reorgday, $k, 'pvrl'.$in, undef);
|
||
$ien->{$in} += $e if(defined $e);
|
||
}
|
||
|
||
## Reorg Producer
|
||
##################
|
||
for my $pn (1..MAXPRODUCER) {
|
||
$pn = sprintf "%02d", $pn;
|
||
my $e = HistoryVal ($hash, $reorgday, $k, 'pprl'.$pn, undef);
|
||
$pen->{$pn} += $e if(defined $e);
|
||
}
|
||
|
||
## Reorg Battery
|
||
##################
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
my $bi = HistoryVal ($hash, $reorgday, $k, 'batin'.$bn, undef);
|
||
my $bo = HistoryVal ($hash, $reorgday, $k, 'batout'.$bn, undef);
|
||
$bin->{$bn} += $bi if(defined $bi);
|
||
$bot->{$bn} += $bo if(defined $bo);
|
||
}
|
||
}
|
||
|
||
$data{$name}{pvhist}{$reorgday}{99}{pvrl} = $r3;
|
||
$data{$name}{pvhist}{$reorgday}{99}{pvfc} = $r4;
|
||
$data{$name}{pvhist}{$reorgday}{99}{confc} = $r5;
|
||
$data{$name}{pvhist}{$reorgday}{99}{gcons} = $r6;
|
||
$data{$name}{pvhist}{$reorgday}{99}{gfeedin} = $r7;
|
||
$data{$name}{pvhist}{$reorgday}{99}{con} = $r8;
|
||
|
||
for my $in (keys %{$ien}) {
|
||
$data{$name}{pvhist}{$reorgday}{99}{'pvrl'.$in} = $ien->{$in};
|
||
}
|
||
|
||
for my $pn (keys %{$pen}) {
|
||
$data{$name}{pvhist}{$reorgday}{99}{'pprl'.$pn} = $pen->{$pn};
|
||
}
|
||
|
||
for my $bn (keys %{$bin}) {
|
||
$data{$name}{pvhist}{$reorgday}{99}{'batin'.$bn} = $bin->{$bn};
|
||
$data{$name}{pvhist}{$reorgday}{99}{'batout'.$bn} = $bot->{$bn};
|
||
}
|
||
|
||
debugLog ($paref, 'saveData2Cache', "setPVhistory -> Day >$reorgday< reorganized keys: batinXX, batoutXX, pvrl, pvfc, con, confc, gcons, gfeedin, pvrlXX, pprlXX");
|
||
}
|
||
|
||
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 $name = $paref->{name};
|
||
my $type = $paref->{type};
|
||
my $day = $paref->{day};
|
||
my $nhour = $paref->{nhour};
|
||
my $histname = $paref->{histname};
|
||
my $val = $paref->{val};
|
||
|
||
my $hash = $defs{$name};
|
||
my $store = $hfspvh{$histname}{storname};
|
||
my ($validkey, $validval);
|
||
|
||
$data{$name}{pvhist}{$day}{$nhour}{$store} = $val;
|
||
|
||
if (defined $hfspvh{$histname}{validkey}) { # 1: bestimmter Eintrag wird intern für Prozesse (z.B. Lernprozess) berücksichtigt oder nicht (0)
|
||
$validkey = $hfspvh{$histname}{validkey};
|
||
$validval = $paref->{$validkey};
|
||
$data{$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{$name}{pvhist}{$day}}) {
|
||
next if($k eq '99');
|
||
$sum += HistoryVal ($hash, $day, $k, $store, 0);
|
||
}
|
||
|
||
$data{$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 ($sq, $h);
|
||
|
||
if ($htol eq "pvhist") {
|
||
$sq = _listDataPoolPvHist ($hash, $par);
|
||
}
|
||
|
||
if ($htol =~ /consumers|inverters|producers|strings|batteries/xs) {
|
||
$sq = _listDataPoolVarious ($hash, $htol, $par);
|
||
}
|
||
|
||
if ($htol eq "circular") {
|
||
$sq = _listDataPoolCircular ($hash, $par);
|
||
}
|
||
|
||
if ($htol eq "nexthours") {
|
||
$sq = _listDataPoolNextHours ($name, $par);
|
||
}
|
||
|
||
if ($htol eq "qualities") {
|
||
$sq = _listDataPoolQualities ($name, $par);
|
||
}
|
||
|
||
if ($htol eq "current") {
|
||
$sq = _listDataPoolCurrent ($name, $par);
|
||
}
|
||
|
||
if ($htol =~ /radiationApiData|weatherApiData|statusApiData/xs) {
|
||
$sq = _listDataPoolApiData ($name, $htol, $par);
|
||
}
|
||
|
||
if ($htol eq "aiRawData") {
|
||
$h = $data{$name}{aidectree}{airaw};
|
||
my $maxcnt = keys %{$h};
|
||
if (!$maxcnt) {
|
||
return qq{aiRawData values cache is empty.};
|
||
}
|
||
|
||
$sq = "<b>Number of datasets:</b> ".$maxcnt."\n";
|
||
|
||
for my $idx (sort keys %{$h}) {
|
||
my $hod = AiRawdataVal ($name, $idx, 'hod', '-');
|
||
my $sunalt = AiRawdataVal ($name, $idx, 'sunalt', '-');
|
||
my $sunaz = AiRawdataVal ($name, $idx, 'sunaz', '-');
|
||
my $rad1h = AiRawdataVal ($name, $idx, 'rad1h', '-');
|
||
my $wcc = AiRawdataVal ($name, $idx, 'wcc', '-');
|
||
my $rr1c = AiRawdataVal ($name, $idx, 'rr1c', '-');
|
||
my $pvrl = AiRawdataVal ($name, $idx, 'pvrl', '-');
|
||
my $temp = AiRawdataVal ($name, $idx, 'temp', '-');
|
||
my $nod = AiRawdataVal ($name, $idx, 'dayname', '-');
|
||
my $con = AiRawdataVal ($name, $idx, 'con', '-');
|
||
|
||
$sq .= "\n";
|
||
$sq .= "$idx => hod: $hod, nod: $nod, sunaz: $sunaz, sunalt: $sunalt, rad1h: $rad1h, wcc: $wcc, rr1c: $rr1c, pvrl: $pvrl, con: $con, temp: $temp";
|
||
}
|
||
}
|
||
|
||
return $sq;
|
||
}
|
||
|
||
################################################################
|
||
# Listing des pvHistory Speichers
|
||
################################################################
|
||
sub _listDataPoolPvHist {
|
||
my $hash = shift;
|
||
my $par = shift // q{};
|
||
|
||
my $name = $hash->{NAME};
|
||
|
||
my ($sq, $h, $hexp);
|
||
my $export = q{};
|
||
|
||
if ($par eq 'exportToCsv') {
|
||
$export = 'csv';
|
||
$par = q{};
|
||
}
|
||
|
||
my $sub = sub {
|
||
my $day = shift;
|
||
my $ret;
|
||
|
||
for my $key (sort {$a<=>$b} keys %{$h->{$day}}) {
|
||
my $pvrl = HistoryVal ($name, $day, $key, 'pvrl', '-');
|
||
my $pvrlvd = HistoryVal ($name, $day, $key, 'pvrlvd', '-');
|
||
my $pvfc = HistoryVal ($name, $day, $key, 'pvfc', '-');
|
||
my $gcons = HistoryVal ($name, $day, $key, 'gcons', '-');
|
||
my $con = HistoryVal ($name, $day, $key, 'con', '-');
|
||
my $confc = HistoryVal ($name, $day, $key, 'confc', '-');
|
||
my $gfeedin = HistoryVal ($name, $day, $key, 'gfeedin', '-');
|
||
my $wid = HistoryVal ($name, $day, $key, 'weatherid', '-');
|
||
my $wcc = HistoryVal ($name, $day, $key, 'wcc', '-');
|
||
my $rr1c = HistoryVal ($name, $day, $key, 'rr1c', '-');
|
||
my $temp = HistoryVal ($name, $day, $key, 'temp', undef);
|
||
my $pvcorrf = HistoryVal ($name, $day, $key, 'pvcorrf', '-');
|
||
my $dayname = HistoryVal ($name, $day, $key, 'dayname', undef);
|
||
my $rad1h = HistoryVal ($name, $day, $key, 'rad1h', '-');
|
||
my $sunaz = HistoryVal ($name, $day, $key, 'sunaz', '-');
|
||
my $sunalt = HistoryVal ($name, $day, $key, 'sunalt', '-');
|
||
my $don = HistoryVal ($name, $day, $key, 'DoN', '-');
|
||
my $conprc = HistoryVal ($name, $day, $key, 'conprice', '-');
|
||
my $feedprc = HistoryVal ($name, $day, $key, 'feedprice', '-');
|
||
|
||
if ($export eq 'csv') {
|
||
$hexp->{$day}{$key}{PVreal} = $pvrl;
|
||
$hexp->{$day}{$key}{PVrealValid} = $pvrlvd;
|
||
$hexp->{$day}{$key}{PVforecast} = $pvfc;
|
||
$hexp->{$day}{$key}{GridConsumption} = $gcons;
|
||
$hexp->{$day}{$key}{Consumption} = $con;
|
||
$hexp->{$day}{$key}{confc} = $confc;
|
||
$hexp->{$day}{$key}{GridFeedIn} = $gfeedin;
|
||
$hexp->{$day}{$key}{WeatherId} = $wid;
|
||
$hexp->{$day}{$key}{CloudCover} = $wcc;
|
||
$hexp->{$day}{$key}{TotalPrecipitation} = $rr1c;
|
||
$hexp->{$day}{$key}{Temperature} = $temp // '';
|
||
$hexp->{$day}{$key}{PVCorrectionFactor} = $pvcorrf eq '-' ? '' : (split "/", $pvcorrf)[0];
|
||
$hexp->{$day}{$key}{Quality} = $pvcorrf eq '-' ? '' : (split "/", $pvcorrf)[1];
|
||
$hexp->{$day}{$key}{DayName} = $dayname // '';
|
||
$hexp->{$day}{$key}{GlobalRadiation } = $rad1h;
|
||
$hexp->{$day}{$key}{SunAzimuth} = $sunaz;
|
||
$hexp->{$day}{$key}{SunAltitude} = $sunalt;
|
||
$hexp->{$day}{$key}{DayOrNight} = $don;
|
||
$hexp->{$day}{$key}{PurchasePrice} = $conprc;
|
||
$hexp->{$day}{$key}{FeedInPrice} = $feedprc;
|
||
}
|
||
|
||
my ($inve, $invl);
|
||
for my $in (1..MAXINVERTER) { # + alle Inverter
|
||
$in = sprintf "%02d", $in;
|
||
my $etoti = HistoryVal ($name, $day, $key, 'etotali'.$in, '-');
|
||
my $pvrli = HistoryVal ($name, $day, $key, 'pvrl'.$in, '-');
|
||
|
||
if ($export eq 'csv') {
|
||
$hexp->{$day}{$key}{"Etotal${in}"} = $etoti;
|
||
$hexp->{$day}{$key}{"PVreal${in}"} = $pvrli;
|
||
}
|
||
|
||
$inve .= ', ' if($inve);
|
||
$inve .= "etotali${in}: $etoti";
|
||
$invl .= ', ' if($invl);
|
||
$invl .= "pvrl${in}: $pvrli";
|
||
}
|
||
|
||
my ($prde, $prdl);
|
||
for my $pn (1..MAXPRODUCER) { # + alle Producer
|
||
$pn = sprintf "%02d", $pn;
|
||
my $etotp = HistoryVal ($name, $day, $key, 'etotalp'.$pn, '-');
|
||
my $pprl = HistoryVal ($name, $day, $key, 'pprl'.$pn, '-');
|
||
|
||
if ($export eq 'csv') {
|
||
$hexp->{$day}{$key}{"Etotal${pn}"} = $etotp;
|
||
$hexp->{$day}{$key}{"PPreal${pn}"} = $pprl;
|
||
}
|
||
|
||
$prde .= ', ' if($prde);
|
||
$prde .= "etotalp${pn}: $etotp";
|
||
$prdl .= ', ' if($prdl);
|
||
$prdl .= "pprl${pn}: $pprl";
|
||
}
|
||
|
||
my ($btotin, $batin, $btotout, $batout, $batmsoc, $batssoc, $batsoc);
|
||
for my $bn (1..MAXBATTERIES) { # + alle Batterien
|
||
$bn = sprintf "%02d", $bn;
|
||
my $hbtotin = HistoryVal ($name, $day, $key, 'batintotal'.$bn, '-');
|
||
my $hbtotout = HistoryVal ($name, $day, $key, 'batouttotal'.$bn, '-');
|
||
my $hbatin = HistoryVal ($name, $day, $key, 'batin'.$bn, '-');
|
||
my $hbatout = HistoryVal ($name, $day, $key, 'batout'.$bn, '-');
|
||
my $hbatmsoc = HistoryVal ($name, $day, $key, 'batmaxsoc'.$bn, '-');
|
||
my $hbatssoc = HistoryVal ($name, $day, $key, 'batsetsoc'.$bn, '-');
|
||
my $hbatsoc = HistoryVal ($name, $day, $key, 'batsoc'.$bn, '-');
|
||
|
||
if ($export eq 'csv') {
|
||
$hexp->{$day}{$key}{"BatteryInTotal${bn}"} = $hbtotin;
|
||
$hexp->{$day}{$key}{"BatteryOutTotal${bn}"} = $hbtotout;
|
||
$hexp->{$day}{$key}{"BatteryIn${bn}"} = $hbatin;
|
||
$hexp->{$day}{$key}{"BatteryOut${bn}"} = $hbatout;
|
||
$hexp->{$day}{$key}{"BatteryMaxSoc${bn}"} = $hbatmsoc;
|
||
$hexp->{$day}{$key}{"BatterySetSoc${bn}"} = $hbatssoc;
|
||
$hexp->{$day}{$key}{"BatterySoc${bn}"} = $hbatsoc;
|
||
}
|
||
|
||
$btotin .= ', ' if($btotin);
|
||
$btotin .= "batintotal${bn}: $hbtotin";
|
||
$btotout .= ', ' if($btotout);
|
||
$btotout .= "batouttotal${bn}: $hbtotout";
|
||
$batin .= ', ' if($batin);
|
||
$batin .= "batin${bn}: $hbatin";
|
||
$batout .= ', ' if($batout);
|
||
$batout .= "batout${bn}: $hbatout";
|
||
$batmsoc .= ', ' if($batmsoc);
|
||
$batmsoc .= "batmaxsoc${bn}: $hbatmsoc";
|
||
$batssoc .= ', ' if($batssoc);
|
||
$batssoc .= "batsetsoc${bn}: $hbatssoc";
|
||
$batsoc .= ', ' if($batsoc);
|
||
$batsoc .= "batsoc${bn}: $hbatsoc";
|
||
}
|
||
|
||
$ret .= "\n " if($ret);
|
||
$ret .= $key." => ";
|
||
$ret .= "pvfc: $pvfc, pvrl: $pvrl, pvrlvd: $pvrlvd, rad1h: $rad1h";
|
||
$ret .= "\n ";
|
||
$ret .= $inve if($inve && $key ne '99');
|
||
$ret .= "\n " if($inve && $key ne '99');
|
||
$ret .= $invl if($invl);
|
||
$ret .= "\n " if($invl);
|
||
$ret .= $prde if($prde && $key ne '99');
|
||
$ret .= "\n " if($prde && $key ne '99');
|
||
$ret .= $prdl if($prdl);
|
||
$ret .= "\n " if($prdl);
|
||
$ret .= "confc: $confc, con: $con, gcons: $gcons, conprice: $conprc";
|
||
$ret .= "\n ";
|
||
$ret .= "gfeedin: $gfeedin, feedprice: $feedprc";
|
||
$ret .= "\n ";
|
||
$ret .= "DoN: $don, sunaz: $sunaz, sunalt: $sunalt";
|
||
$ret .= "\n ";
|
||
|
||
$ret .= $btotin if($key ne '99');
|
||
$ret .= "\n " if($key ne '99');
|
||
$ret .= $btotout if($key ne '99');
|
||
$ret .= "\n " if($key ne '99');
|
||
$ret .= $batsoc if($key ne '99');
|
||
$ret .= "\n " if($key ne '99');
|
||
|
||
$ret .= $batin;
|
||
$ret .= "\n ";
|
||
$ret .= $batout;
|
||
$ret .= "\n ";
|
||
|
||
$ret .= $batmsoc if($key eq '99');
|
||
$ret .= "\n " if($key eq '99');
|
||
$ret .= $batssoc if($key eq '99');
|
||
$ret .= "\n " if($key eq '99');
|
||
|
||
if ($key ne '99') {
|
||
$ret .= "weatherid: $wid, ";
|
||
$ret .= "wcc: $wcc, ";
|
||
$ret .= "rr1c: $rr1c, ";
|
||
$ret .= "pvcorrf: $pvcorrf ";
|
||
}
|
||
|
||
$ret .= "temp: $temp, " if($temp);
|
||
$ret .= "dayname: $dayname, " if($dayname);
|
||
|
||
my $csm;
|
||
for my $c (1..MAXCONSUMER) { # + alle Consumer
|
||
$c = sprintf "%02d", $c;
|
||
my $nl = 0;
|
||
my $csmc = HistoryVal ($name, $day, $key, "cyclescsm${c}", undef);
|
||
my $csmt = HistoryVal ($name, $day, $key, "csmt${c}", undef);
|
||
my $csme = HistoryVal ($name, $day, $key, "csme${c}", undef);
|
||
my $csmm = HistoryVal ($name, $day, $key, "minutescsm${c}", undef);
|
||
my $csmh = HistoryVal ($name, $day, $key, "hourscsme${c}", undef);
|
||
my $csma = HistoryVal ($name, $day, $key, "avgcycmntscsm${c}", undef);
|
||
|
||
if ($export eq 'csv') {
|
||
$hexp->{$day}{$key}{"CyclesCsm${c}"} = $csmc if(defined $csmc);
|
||
$hexp->{$day}{$key}{"Csmt${c}"} = $csmt if(defined $csmt);
|
||
$hexp->{$day}{$key}{"Csme${c}"} = $csme if(defined $csme);
|
||
$hexp->{$day}{$key}{"MinutesCsm${c}"} = $csmm if(defined $csmm);
|
||
$hexp->{$day}{$key}{"HoursCsme${c}"} = $csmh if(defined $csmh);
|
||
$hexp->{$day}{$key}{"AvgCycleMinutesCsm${c}"} = $csma if(defined $csma);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
if (defined $csma) {
|
||
$csm .= ", " if($nl);
|
||
$csm .= "avgcycmntscsm${c}: $csma";
|
||
$nl = 1;
|
||
}
|
||
|
||
$csm .= "\n " if($nl);
|
||
}
|
||
|
||
if ($csm) {
|
||
$ret .= "\n ";
|
||
$ret .= $csm;
|
||
}
|
||
else {
|
||
$ret .= "\n ";
|
||
}
|
||
}
|
||
return $ret;
|
||
};
|
||
|
||
$h = $data{$name}{pvhist};
|
||
|
||
if (!keys %{$h}) {
|
||
return qq{PV cache is empty.};
|
||
}
|
||
|
||
for my $i (keys %{$h}) {
|
||
if (!isNumeric ($i)) {
|
||
delete $data{$name}{pvhist}{$i};
|
||
Log3 ($name, 2, qq{$name - INFO - invalid key "$i" was deleted from pvHistory storage});
|
||
}
|
||
}
|
||
|
||
for my $idx (sort keys %{$h}) {
|
||
next if($par && $idx ne $par);
|
||
$sq .= $idx." => ".$sub->($idx)."\n";
|
||
}
|
||
|
||
if ($export eq 'csv') {
|
||
return _writeAsCsv ($hash, $hexp, $pvhexprtcsv.$name.'.csv');
|
||
}
|
||
|
||
return $sq;
|
||
}
|
||
|
||
################################################################
|
||
# Listing des verschiedene Speicher
|
||
################################################################
|
||
sub _listDataPoolVarious {
|
||
my $hash = shift;
|
||
my $htol = shift;
|
||
my $par = shift // q{};
|
||
|
||
my $name = $hash->{NAME};
|
||
|
||
my $func = $htol eq 'consumers' ? \&ConsumerVal :
|
||
$htol eq 'inverters' ? \&InverterVal :
|
||
$htol eq 'producers' ? \&ProducerVal :
|
||
$htol eq 'strings' ? \&StringVal :
|
||
$htol eq 'batteries' ? \&BatteryVal :
|
||
'';
|
||
|
||
my $h = $data{$name}{$htol};
|
||
|
||
if (!keys %{$h}) {
|
||
return ucfirst($htol).qq{ cache is empty.};
|
||
}
|
||
|
||
for my $i (keys %{$h}) {
|
||
if ($i !~ /^[0-9]{2}$/ix && $htol ne 'strings') { # bereinigen ungültige Position, Forum: https://forum.fhem.de/index.php/topic,117864.msg1173219.html#msg1173219
|
||
delete $data{$name}{$htol}{$i};
|
||
Log3 ($name, 2, qq{$name - INFO - invalid key "$i" was deleted from }.ucfirst($htol).qq{ storage});
|
||
}
|
||
}
|
||
|
||
my $sq;
|
||
|
||
for my $idx (sort keys %{$h}) {
|
||
next if($par && $idx ne $par);
|
||
my ($cret, $s1);
|
||
my $sp1 = _ldpspaces ($idx, q{});
|
||
|
||
for my $ckey (sort keys %{$h->{$idx}}) {
|
||
if (ref $h->{$idx}{$ckey} eq 'ARRAY') {
|
||
my $aser = join " ", @{$h->{$idx}{$ckey}};
|
||
$cret .= ($s1 ? $sp1 : "").$ckey." => ".$aser."\n";
|
||
}
|
||
|
||
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 .= ($s1 ? $sp1 : "").$ckey." => ".$hk."\n";
|
||
}
|
||
else {
|
||
$cret .= ($s1 ? $sp1 : "").$ckey." => ". &{$func} ($hash, $idx, $ckey, "")."\n";
|
||
}
|
||
|
||
$s1 = 1;
|
||
}
|
||
$sq .= $idx." => ".$cret."\n";
|
||
}
|
||
|
||
return $sq;
|
||
}
|
||
|
||
################################################################
|
||
# Listing des Circular Speichers
|
||
################################################################
|
||
sub _listDataPoolCircular {
|
||
my $hash = shift;
|
||
my $par = shift // q{};
|
||
|
||
my $name = $hash->{NAME};
|
||
my $h = $data{$name}{circular};
|
||
|
||
if (!keys %{$h}) {
|
||
return qq{Circular cache is empty.};
|
||
}
|
||
|
||
my $sq;
|
||
|
||
for my $idx (sort keys %{$h}) {
|
||
next if($par && $idx ne $par);
|
||
|
||
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 $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) {
|
||
my $prdl;
|
||
for my $pn (1..MAXPRODUCER) { # alle Producer
|
||
$pn = sprintf "%02d", $pn;
|
||
my $pprl = CircularVal ($hash, $idx, 'pprl'.$pn, '-');
|
||
$prdl .= ', ' if($prdl);
|
||
$prdl .= "pprl${pn}: $pprl";
|
||
}
|
||
|
||
my ($bin, $bout);
|
||
for my $bn (1..MAXBATTERIES) { # alle Batterien
|
||
$bn = sprintf "%02d", $bn;
|
||
my $batin = CircularVal ($hash, $idx, 'batin'. $bn, '-');
|
||
my $batout = CircularVal ($hash, $idx, 'batout'.$bn, '-');
|
||
$bin .= ', ' if($bin);
|
||
$bin .= "batin${bn}: $batin";
|
||
$bout .= ', ' if($bout);
|
||
$bout .= "batout${bn}: $batout";
|
||
}
|
||
|
||
my ($pvrlnew, $pvfcnew, $conall);
|
||
my @pvrlkeys = map { $_ =~ /^pvrl_/xs ? $_ : '' } sort keys %{$h->{$idx}};
|
||
my @pvfckeys = map { $_ =~ /^pvfc_/xs ? $_ : '' } sort keys %{$h->{$idx}};
|
||
my @conakeys = map { $_ =~ /^con_all/xs ? $_ : '' } sort keys %{$h->{$idx}};
|
||
|
||
for my $prl (@pvrlkeys) {
|
||
next if(!$prl);
|
||
my $lref = CircularVal ($hash, $idx, $prl, '');
|
||
next if(!$lref);
|
||
|
||
$pvrlnew .= "\n " if($pvrlnew);
|
||
$pvrlnew .= _ldchash2val ( { pool => $h, idx => $idx, key => $prl, cval => $lref } );
|
||
}
|
||
|
||
for my $pfc (@pvfckeys) {
|
||
next if(!$pfc);
|
||
my $cref = CircularVal ($hash, $idx, $pfc, '');
|
||
next if(!$cref);
|
||
|
||
$pvfcnew .= "\n " if($pvfcnew);
|
||
$pvfcnew .= _ldchash2val ( { pool => $h, idx => $idx, key => $pfc, cval => $cref } );
|
||
}
|
||
|
||
for my $coa (@conakeys) {
|
||
next if(!$coa);
|
||
my $caref = CircularVal ($hash, $idx, $coa, '');
|
||
next if(!$caref);
|
||
|
||
$conall .= "\n " if($conall);
|
||
$conall .= _ldchash2val ( { pool => $h, idx => $idx, key => $coa, cval => $caref } );
|
||
}
|
||
|
||
$sq .= $idx." => pvapifc: $pvapifc, pvaifc: $pvaifc, pvfc: $pvfc, aihit: $aihit, pvrl: $pvrl";
|
||
$sq .= "\n $bin";
|
||
$sq .= "\n $bout";
|
||
$sq .= "\n confc: $confc, gcon: $gcons, gfeedin: $gfeedin, wcc: $wccv, rr1c: $rr1c";
|
||
$sq .= "\n temp: $temp, wid: $wid, wtxt: $wtxt";
|
||
$sq .= "\n $prdl";
|
||
$sq .= "\n pvcorrf: $pvcf";
|
||
$sq .= "\n quality: $cfq";
|
||
$sq .= "\n pvrlsum: $pvrs";
|
||
$sq .= "\n pvfcsum: $pvfs";
|
||
$sq .= "\n dnumsum: $dnus";
|
||
$sq .= "\n $conall" if($conall);
|
||
$sq .= "\n $pvrlnew" if($pvrlnew);
|
||
$sq .= "\n $pvfcnew" if($pvfcnew);
|
||
}
|
||
else {
|
||
my ($batvl1, $batvl2, $batvl3, $batvl4, $batvl5, $batvl6, $batvl7);
|
||
|
||
my $con = CircularVal ($hash, $idx, 'todayConsumption', '-');
|
||
my $gcontot = CircularVal ($hash, $idx, 'gridcontotal', '-');
|
||
my $idgcon = CircularVal ($hash, $idx, 'initdaygcon', '-');
|
||
my $idfi = CircularVal ($hash, $idx, 'initdayfeedin', '-');
|
||
my $fitot = CircularVal ($hash, $idx, 'feedintotal', '-');
|
||
my $tdayDvtn = CircularVal ($hash, $idx, 'tdayDvtn', '-');
|
||
my $ydayDvtn = CircularVal ($hash, $idx, 'ydayDvtn', '-');
|
||
my $rtaitr = CircularVal ($hash, $idx, 'runTimeTrainAI', '-');
|
||
my $fsaitr = CircularVal ($hash, $idx, 'aitrainLastFinishTs', '-');
|
||
my $airn = CircularVal ($hash, $idx, 'aiRulesNumber', '-');
|
||
my $aicts = CircularVal ($hash, $idx, 'attrInvChangedTs', '-');
|
||
|
||
for my $bn (1..MAXBATTERIES) { # + alle Batterien
|
||
$bn = sprintf "%02d", $bn;
|
||
my $idbintot = CircularVal ($hash, $idx, 'initdaybatintot'. $bn, '-');
|
||
my $idboutot = CircularVal ($hash, $idx, 'initdaybatouttot'.$bn, '-');
|
||
my $bintot = CircularVal ($hash, $idx, 'batintot'. $bn, '-');
|
||
my $boutot = CircularVal ($hash, $idx, 'batouttot'. $bn, '-');
|
||
my $lstmsr = CircularVal ($hash, $idx, 'lastTsMaxSocRchd'.$bn, '-');
|
||
my $ntsmsc = CircularVal ($hash, $idx, 'nextTsMaxSocChge'.$bn, '-');
|
||
my $dtocare = CircularVal ($hash, $idx, 'days2care'. $bn, '-');
|
||
$batvl1 .= ', ' if($batvl1);
|
||
$batvl1 .= "initdaybatintot${bn}: $idbintot";
|
||
$batvl2 .= ', ' if($batvl2);
|
||
$batvl2 .= "initdaybatouttot${bn}: $idboutot";
|
||
$batvl3 .= ', ' if($batvl3);
|
||
$batvl3 .= "batintot${bn}: $bintot";
|
||
$batvl4 .= ', ' if($batvl4);
|
||
$batvl4 .= "batouttot${bn}: $boutot";
|
||
$batvl5 .= ', ' if($batvl5);
|
||
$batvl5 .= "lastTsMaxSocRchd${bn}: $lstmsr";
|
||
$batvl6 .= ', ' if($batvl6);
|
||
$batvl6 .= "nextTsMaxSocChge${bn}: $ntsmsc";
|
||
$batvl7 .= ', ' if($batvl7);
|
||
$batvl7 .= "days2care${bn}: $dtocare";
|
||
}
|
||
|
||
$sq .= $idx." => tdayDvtn: $tdayDvtn, ydayDvtn: $ydayDvtn \n";
|
||
$sq .= " todayConsumption: $con, feedintotal: $fitot, initdayfeedin: $idfi \n";
|
||
$sq .= " gridcontotal: $gcontot, initdaygcon: $idgcon \n";
|
||
$sq .= " $batvl1\n";
|
||
$sq .= " $batvl2\n";
|
||
$sq .= " $batvl3\n";
|
||
$sq .= " $batvl4\n";
|
||
$sq .= " $batvl5\n";
|
||
$sq .= " $batvl6\n";
|
||
$sq .= " $batvl7\n";
|
||
$sq .= " runTimeTrainAI: $rtaitr, aitrainLastFinishTs: $fsaitr, aiRulesNumber: $airn \n";
|
||
$sq .= " attrInvChangedTs: $aicts \n";
|
||
}
|
||
}
|
||
|
||
return $sq;
|
||
}
|
||
|
||
################################################################
|
||
# Listing des NextHours Speicher
|
||
################################################################
|
||
sub _listDataPoolNextHours {
|
||
my $name = shift;
|
||
my $par = shift // q{};
|
||
|
||
my $h = $data{$name}{nexthours};
|
||
my $sq;
|
||
|
||
if (!keys %{$h}) {
|
||
return qq{NextHours cache is empty.};
|
||
}
|
||
|
||
for my $idx (sort keys %{$h}) {
|
||
my $nhts = NexthoursVal ($name, $idx, 'starttime', '-');
|
||
my $hod = NexthoursVal ($name, $idx, 'hourofday', '-');
|
||
my $today = NexthoursVal ($name, $idx, 'today', '-');
|
||
my $pvfc = NexthoursVal ($name, $idx, 'pvfc', '-');
|
||
my $pvapifc = NexthoursVal ($name, $idx, 'pvapifc', '-'); # PV Forecast der API
|
||
my $pvaifc = NexthoursVal ($name, $idx, 'pvaifc', '-'); # PV Forecast der KI
|
||
my $aihit = NexthoursVal ($name, $idx, 'aihit', '-'); # KI ForeCast Treffer Status
|
||
my $wid = NexthoursVal ($name, $idx, 'weatherid', '-');
|
||
my $wcc = NexthoursVal ($name, $idx, 'wcc', '-');
|
||
my $crang = NexthoursVal ($name, $idx, 'cloudrange', '-');
|
||
my $rr1c = NexthoursVal ($name, $idx, 'rr1c', '-');
|
||
my $rrange = NexthoursVal ($name, $idx, 'rainrange', '-');
|
||
my $rad1h = NexthoursVal ($name, $idx, 'rad1h', '-');
|
||
my $pvcorrf = NexthoursVal ($name, $idx, 'pvcorrf', '-');
|
||
my $temp = NexthoursVal ($name, $idx, 'temp', '-');
|
||
my $confc = NexthoursVal ($name, $idx, 'confc', '-');
|
||
my $confcex = NexthoursVal ($name, $idx, 'confcEx', '-');
|
||
my $don = NexthoursVal ($name, $idx, 'DoN', '-');
|
||
my $sunaz = NexthoursVal ($name, $idx, 'sunaz', '-');
|
||
my $sunalt = NexthoursVal ($name, $idx, 'sunalt', '-');
|
||
|
||
my ($rcdbat, $socs);
|
||
for my $bn (1..MAXBATTERIES) { # alle Batterien
|
||
$bn = sprintf "%02d", $bn;
|
||
my $rcdcharge = NexthoursVal ($name, $idx, 'rcdchargebat'.$bn, '-');
|
||
my $socxx = NexthoursVal ($name, $idx, 'soc'.$bn, '-');
|
||
$rcdbat .= ', ' if($rcdbat);
|
||
$rcdbat .= "rcdchargebat${bn}: $rcdcharge";
|
||
$socs .= ', ' if($socs);
|
||
$socs .= "soc${bn}: $socxx";
|
||
}
|
||
|
||
$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, weatherid: $wid, wcc: $wcc, rr1c: $rr1c, temp=$temp";
|
||
$sq .= "\n ";
|
||
$sq .= "rad1h: $rad1h, sunaz: $sunaz, sunalt: $sunalt";
|
||
$sq .= "\n ";
|
||
$sq .= "rrange: $rrange, crange: $crang, correff: $pvcorrf";
|
||
$sq .= "\n ";
|
||
$sq .= $socs;
|
||
$sq .= "\n ";
|
||
$sq .= $rcdbat;
|
||
}
|
||
|
||
return $sq;
|
||
}
|
||
|
||
################################################################
|
||
# Listing des Qualities Speicher
|
||
################################################################
|
||
sub _listDataPoolQualities {
|
||
my $name = shift;
|
||
my $par = shift // q{};
|
||
|
||
my $h = $data{$name}{nexthours};
|
||
my $sq;
|
||
|
||
if (!keys %{$h}) {
|
||
return qq{NextHours cache is empty.};
|
||
}
|
||
|
||
for my $idx (sort keys %{$h}) {
|
||
my $nhfc = NexthoursVal ($name, $idx, 'pvfc', undef);
|
||
next if(!$nhfc);
|
||
|
||
my $nhts = NexthoursVal ($name, $idx, 'starttime', '-');
|
||
my $pvcorrf = NexthoursVal ($name, $idx, 'pvcorrf', '-/-');
|
||
my $aihit = NexthoursVal ($name, $idx, 'aihit', '-');
|
||
my $pvfc = NexthoursVal ($name, $idx, 'pvfc', '-');
|
||
my $wcc = NexthoursVal ($name, $idx, 'wcc', '-');
|
||
my $sunalt = NexthoursVal ($name, $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: $wcc";
|
||
}
|
||
|
||
return $sq;
|
||
}
|
||
|
||
################################################################
|
||
# Listing des Current Speicher
|
||
################################################################
|
||
sub _listDataPoolCurrent {
|
||
my $name = shift;
|
||
my $par = shift // q{};
|
||
|
||
my $h = $data{$name}{current};
|
||
my $sq;
|
||
|
||
if (!keys %{$h}) {
|
||
return qq{Current values cache is empty.};
|
||
}
|
||
|
||
for my $idx (sort keys %{$h}) {
|
||
if (ref $h->{$idx} eq 'ARRAY') {
|
||
my $aser = join " ",@{$h->{$idx}};
|
||
$sq .= $idx." => ".$aser."\n";
|
||
}
|
||
elsif (ref $h->{$idx} eq 'HASH') {
|
||
my $s1;
|
||
my $sp1 = _ldpspaces ($idx, q{});
|
||
$sq .= $idx." => ";
|
||
|
||
for my $idx1 (sort keys %{$h->{$idx}}) {
|
||
if (ref $h->{$idx}{$idx1} eq 'HASH') {
|
||
my $s2;
|
||
my $sp2 = _ldpspaces ($idx1, $sp1);
|
||
$sq .= ($s1 ? $sp1 : "").$idx1." => ";
|
||
|
||
for my $idx2 (sort keys %{$h->{$idx}{$idx1}}) {
|
||
my $s3;
|
||
my $sp3 = _ldpspaces ($idx2, $sp2);
|
||
$sq .= ($s2 ? $sp2 : "").$idx2." => ";
|
||
|
||
if (ref $h->{$idx}{$idx1}{$idx2} eq 'HASH') {
|
||
for my $idx3 (sort keys %{$h->{$idx}{$idx1}{$idx2}}) {
|
||
$sq .= ($s3 ? $sp3 : "").$idx3." => ".(defined $h->{$idx}{$idx1}{$idx2}{$idx3} ? $h->{$idx}{$idx1}{$idx2}{$idx3} : '')."\n";
|
||
$s3 = 1;
|
||
}
|
||
}
|
||
else {
|
||
$sq .= (defined $h->{$idx}{$idx1}{$idx2} ? $h->{$idx}{$idx1}{$idx2} : '')."\n";
|
||
}
|
||
|
||
$s1 = 1;
|
||
$s2 = 1;
|
||
}
|
||
}
|
||
else {
|
||
$sq .= (defined $h->{$idx}{$idx1} ? $h->{$idx}{$idx1} : '')."\n";
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
$sq .= $idx." => ".(defined $h->{$idx} ? $h->{$idx} : '')."\n";
|
||
}
|
||
}
|
||
|
||
return $sq;
|
||
}
|
||
|
||
################################################################
|
||
# Listing der APiData Speicher
|
||
################################################################
|
||
sub _listDataPoolApiData {
|
||
my $name = shift;
|
||
my $htol = shift;
|
||
my $par = shift // q{};
|
||
|
||
my $h = $data{$name}{solcastapi};
|
||
$h = $data{$name}{weatherapi} if($htol eq 'weatherApiData');
|
||
$h = $data{$name}{statusapi} if($htol eq 'statusApiData');
|
||
|
||
if (!keys %{$h}) {
|
||
return qq{The API values cache is empty.};
|
||
}
|
||
|
||
my $git = sub {
|
||
my $it = shift;
|
||
my @sorted = sort { $a cmp $b } keys %$it;
|
||
my $key = shift @sorted;
|
||
|
||
my $ret = {};
|
||
$ret = { $key => $it->{$key} } if($key);
|
||
|
||
return $ret;
|
||
};
|
||
|
||
my $sq;
|
||
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};
|
||
}
|
||
}
|
||
|
||
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 (ref $pool->{$idx}{$key}{$f} eq 'ARRAY') {
|
||
my @sub_arrays = arraySplitBy (20, @{$pool->{$idx}{$key}{$f}}); # Array in Teil-Arrays zu je 20 Elemente aufteilen
|
||
my $ln0 = length $key;
|
||
my $blk0 = ' ' x 17;
|
||
my $blkadd0 = ' ' x (7 - ($ln0 > 7 ? 0 : $ln0));
|
||
|
||
my $ln1 = length $f;
|
||
my $blkadd1 = ' ' x (3 - ($ln1 > 3 ? 0 : $ln1));
|
||
|
||
for my $suaref (@sub_arrays) { # für jedes Teil-Array Join ausführen
|
||
my $suajoined = join ' ', @{$suaref};
|
||
|
||
if (!$ret) {
|
||
$ret .= $key.$blkadd0.' => ';
|
||
$ret .= $f.$blkadd1.' @ '.$suajoined;
|
||
}
|
||
else {
|
||
$ret .= "\n".$blk0;
|
||
$ret .= $f.$blkadd1.' @ '.$suajoined;
|
||
}
|
||
}
|
||
}
|
||
elsif ($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;
|
||
}
|
||
|
||
################################################################
|
||
# Export Speicherstruktur in CSV-Datei
|
||
################################################################
|
||
sub _writeAsCsv {
|
||
my $hash = shift;
|
||
my $hexp = shift;
|
||
my $outfile = shift // return "No file specified for writing data";
|
||
|
||
my @data;
|
||
|
||
## Header schreiben
|
||
#####################
|
||
my @head = qw (Day Hour);
|
||
for my $hexd (sort{$a<=>$b} keys %{$hexp}) {
|
||
for my $hexh (sort{$a<=>$b} keys %{$hexp->{$hexd}}) {
|
||
for my $hk (sort keys %{$hexp->{$hexd}{$hexh}}) {
|
||
push @head, $hk;
|
||
}
|
||
last;
|
||
}
|
||
last;
|
||
}
|
||
|
||
push @data, join(',', map { s{"}{""}g; qq{"$_"};} @head);
|
||
|
||
## Daten schreiben
|
||
####################
|
||
for my $exd (sort{$a<=>$b} keys %{$hexp}) {
|
||
for my $exh (sort{$a<=>$b} keys %{$hexp->{$exd}}) {
|
||
push my @aexp, ($exd, $exh);
|
||
|
||
for my $k (sort keys %{$hexp->{$exd}{$exh}}) {
|
||
my $val = $hexp->{$exd}{$exh}{$k};
|
||
$val =~ s/\./,/xs;
|
||
push @aexp, $val;
|
||
}
|
||
|
||
push @data, join(',', map { s{"}{""}g; qq{"$_"};} @aexp);
|
||
}
|
||
}
|
||
|
||
my $err = FileWrite ($outfile, @data);
|
||
return $err if($err);
|
||
|
||
return "The memory structure was written to the file $outfile";
|
||
}
|
||
|
||
################################################################
|
||
# 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 = AttrVal ($name, 'setupRadiationAPI', '');
|
||
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 },
|
||
'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 },
|
||
'Perl Modules' => { 'state' => $ok, 'result' => '', 'note' => '', 'info' => 0, 'warn' => 0, 'fault' => 0 },
|
||
};
|
||
|
||
my $sub = sub {
|
||
my $string = shift;
|
||
my $ret;
|
||
|
||
for my $key (sort keys %{$data{$name}{strings}{$string}}) {
|
||
$ret .= ", " if($ret);
|
||
$ret .= $key.": ".$data{$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{$name}{strings}}) {
|
||
my $sp = $sn." => ".$sub->($sn)."<br>";
|
||
|
||
$result->{'String Configuration'}{note} .= $sn." => ".$sub->($sn)."<br>";
|
||
|
||
if ($data{$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. <br>};
|
||
$result->{'String Configuration'}{state} = $warn;
|
||
$result->{'String Configuration'}{warn} = 1;
|
||
}
|
||
|
||
if (!isSolCastUsed ($hash) && !isVictronKiUsed ($hash)) {
|
||
if ($sp !~ /azimut.*?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..MAXWEATHERDEV) {
|
||
my ($valid, $fcname, $apiu) = isWeatherDevValid ($hash, 'setupWeatherDev'.$step);
|
||
next if(!$fcname && $step ne 1);
|
||
|
||
if (!$valid) {
|
||
$result->{'Weather Properties'}{state} = $nok;
|
||
|
||
if (!$fcname) {
|
||
$result->{'Weather Properties'}{result} .= qq{No DWD device is defined in attribute "setupWeatherDev$step". <br>};
|
||
}
|
||
else {
|
||
$result->{'Weather Properties'}{result} .= qq{The DWD device "$fcname" doesn't exist. <br>};
|
||
}
|
||
|
||
$result->{'Weather Properties'}{fault} = 1;
|
||
}
|
||
else {
|
||
if (!$apiu) {
|
||
$err = checkdwdattr ($name, $fcname, \@dweattrmust);
|
||
|
||
if ($err) {
|
||
$result->{'Weather Properties'}{state} = $nok;
|
||
$result->{'Weather Properties'}{result} .= $err.'<br>';
|
||
$result->{'Weather Properties'}{fault} = 1;
|
||
}
|
||
else {
|
||
$mosm = AttrVal ($fcname, 'forecastRefresh', 6) == 6 ? 'MOSMIX_L' : 'MOSMIX_S';
|
||
|
||
if ($mosm eq 'MOSMIX_L') {
|
||
$result->{'Weather Properties'}{state} = $info;
|
||
$result->{'Weather Properties'}{result} .= qq(The device "$fcname" uses "$mosm" which is only updated by DWD every 6 hours. <br>);
|
||
$result->{'Weather Properties'}{info} = 1;
|
||
}
|
||
|
||
$result->{'Weather Properties'}{result} .= $hqtxt{fulfd}{$lang}." ($hqtxt{attrib}{$lang}: setupWeatherDev$step)<br>";
|
||
}
|
||
|
||
$result->{'Weather Properties'}{note} .= qq{checked parameters and attributes of device "$fcname": <br>};
|
||
$result->{'Weather Properties'}{note} .= 'forecastProperties -> '.join (',', @dweattrmust).'<br>';
|
||
$result->{'Weather Properties'}{note} .= 'forecastRefresh '.($mosm eq 'MOSMIX_L' ? '-> set attribute to below "6" if possible' : '').'<br>';
|
||
}
|
||
else {
|
||
$result->{'Weather Properties'}{result} .= $hqtxt{fulfd}{$lang}." ($hqtxt{attrib}{$lang}: setupWeatherDev$step)<br>";
|
||
}
|
||
}
|
||
}
|
||
|
||
## Alter DWD Wetterdaten
|
||
##########################
|
||
($err, $resh) = isWeatherAgeExceeded ( {name => $name, lang => $lang} );
|
||
|
||
if (!$err && $resh->{exceed}) {
|
||
$result->{'Weather Properties'}{state} = $warn;
|
||
$result->{'Weather Properties'}{note} .= qq{The Prediction time of Weather data is older than expected when using $resh->{mosmix}. <br>};
|
||
$result->{'Weather Properties'}{note} .= qq{Data time forecast: $resh->{fctime} <br>};
|
||
$result->{'Weather Properties'}{note} .= qq{Check the DWD device(s) for proper functioning of the data retrieval. <br>};
|
||
$result->{'Weather Properties'}{warn} = 1;
|
||
}
|
||
|
||
$result->{'Weather Properties'}{note} .= '<br>';
|
||
$result->{'Weather Properties'}{note} .= qq{checked global Weather parameters: <br>};
|
||
$result->{'Weather Properties'}{note} .= 'MOSMIX variant or ICON Forecast Model, Age of Weather data. <br>';
|
||
|
||
## 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 <br>};
|
||
$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.'<br>';
|
||
$result->{'DWD Radiation Properties'}{note} .= qq{<br>Check the parameters set in device '$raname': attribute 'forecastProperties' <br>};
|
||
$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. <br>);
|
||
$result->{'DWD Radiation Properties'}{info} = 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
## Alter DWD Radiation
|
||
#######################
|
||
($err, $resh) = isRad1hAgeExceeded ( {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}. <br>};
|
||
$result->{'DWD Radiation Properties'}{note} .= qq{Data time forecast: $resh->{fctime} <br>};
|
||
$result->{'DWD Radiation Properties'}{note} .= qq{Check the DWD device '$raname' for proper functioning of the data retrieval.<br>};
|
||
$result->{'DWD Radiation Properties'}{warn} = 1;
|
||
}
|
||
|
||
if (!$result->{'DWD Radiation Properties'}{fault}) {
|
||
$result->{'DWD Radiation Properties'}{result} .= $hqtxt{fulfd}{$lang}.'<br>';
|
||
}
|
||
|
||
$result->{'DWD Radiation Properties'}{note} .= '<br>';
|
||
$result->{'DWD Radiation Properties'}{note} .= qq{checked global Radiation parameters: <br>};
|
||
$result->{'DWD Radiation Properties'}{note} .= 'MOSMIX variant, Age of Radiation data. <br>';
|
||
$result->{'DWD Radiation Properties'}{note} .= qq{<br>checked parameters and attributes device "$raname": <br>};
|
||
$result->{'DWD Radiation Properties'}{note} .= 'forecastProperties -> '.join (',', @draattrmust).'<br>';
|
||
$result->{'DWD Radiation Properties'}{note} .= 'forecastRefresh '.($mosm eq 'MOSMIX_L' ? '-> set attribute to below "6" if possible' : '').'<br>';
|
||
}
|
||
|
||
## 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 = AttrVal ($name, 'setupRoofTops', '');
|
||
|
||
if (!$rft) {
|
||
$result->{'Rooftop Settings'}{state} = $nok;
|
||
$result->{'Rooftop Settings'}{result} .= qq{No RoofTops are defined <br>};
|
||
$result->{'Rooftop Settings'}{note} .= qq{Set your Rooftops with "attr $name setupRoofTops". <br>};
|
||
$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<br>};
|
||
$result->{'Roof Ident Pair Settings'}{note} .= qq{See the "Rooftop Settings" section below. <br>};
|
||
$result->{'Roof Ident Pair Settings'}{fault} = 1;
|
||
}
|
||
else {
|
||
$result->{'Rooftop Settings'}{result} .= $hqtxt{fulfd}{$lang};
|
||
$result->{'Rooftop Settings'}{note} .= qq{Rooftops defined: }.$rft.qq{<br>};
|
||
}
|
||
|
||
my ($a,$h) = parseParams ($rft);
|
||
|
||
while (my ($is, $pk) = each %$h) {
|
||
my $rtid = StatusAPIVal ($hash, '?IdPair', '?'.$pk, 'rtid', '');
|
||
my $apikey = StatusAPIVal ($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. <br>};
|
||
my $note = qq{Set the Roof Ident Pair "$pk" with "set $name roofIdentPair". <br>};
|
||
|
||
$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'}{note} .= qq{checked "$is" Roof Ident Pair "$pk":<br>rtid=$rtid, apikey=$apikey <br>};
|
||
}
|
||
}
|
||
|
||
if (!$result->{'Roof Ident Pair Settings'}{fault}) {
|
||
$result->{'Roof Ident Pair Settings'}{result} = $hqtxt{fulfd}{$lang};
|
||
}
|
||
}
|
||
|
||
## 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. <br>};
|
||
$result->{'Common Settings'}{note} .= qq{Setting attribute 'event-on-change-reading = .*' is recommended to improve the runtime performance.<br>};
|
||
$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'. <br>};
|
||
$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.<br>};
|
||
$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. <br>};
|
||
$result->{'Common Settings'}{note} .= qq{Set the coordinates of your installation in the latitude attribute of the global device.<br>};
|
||
$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. <br>};
|
||
$result->{'Common Settings'}{note} .= qq{Set the coordinates of your installation in the longitude attribute of the global device.<br>};
|
||
$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. <br>};
|
||
$result->{'Common Settings'}{note} .= qq{Set the altitude in meters above sea level in the altitude attribute of the global device.<br>};
|
||
$result->{'Common Settings'}{fault} = 1;
|
||
}
|
||
|
||
if (!$aiprep) {
|
||
$result->{'Common Settings'}{state} = $info;
|
||
$result->{'Common Settings'}{result} .= qq{AI support for the PV forecast is not used. <br>};
|
||
$result->{'Common Settings'}{note} .= qq{$aiusemsg.<br>};
|
||
$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 <br>};
|
||
$result->{'Common Settings'}{note} .= qq{checked module: <br>};
|
||
$result->{'Common Settings'}{note} .= qq{76_SolarForecast <br>};
|
||
}
|
||
|
||
if ($cmerr) {
|
||
$result->{'Common Settings'}{state} = $warn;
|
||
$result->{'Common Settings'}{result} .= qq{$cmmsg <br>};
|
||
$result->{'Common Settings'}{note} .= qq{$cmrec <br>};
|
||
$result->{'Common Settings'}{warn} = 1;
|
||
}
|
||
|
||
if ($cmupd) {
|
||
$result->{'Common Settings'}{state} = $warn;
|
||
$result->{'Common Settings'}{result} .= qq{$cmmsg <br>};
|
||
$result->{'Common Settings'}{note} .= qq{$cmrec <br>};
|
||
$result->{'Common Settings'}{warn} = 1;
|
||
}
|
||
|
||
if ($result->{'Common Settings'}{result}) {
|
||
$result->{'Common Settings'}{result} .= '<br>';
|
||
}
|
||
|
||
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" <br>};
|
||
$result->{'Common Settings'}{note} .= qq{Set pvCorrectionFactor_Auto to "on_complex" is recommended.<br>};
|
||
}
|
||
|
||
if (!$result->{'Common Settings'}{fault}) {
|
||
$result->{'Common Settings'}{result} .= $hqtxt{fulfd}{$lang}.'<br>';
|
||
$result->{'Common Settings'}{note} .= qq{<br>checked parameters and attributes: <br>};
|
||
$result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto <br>};
|
||
}
|
||
}
|
||
|
||
if (isOpenMeteoUsed ($hash)) { # allg. Settings bei Nutzung Open-Meteo API
|
||
if ($aidtabs) {
|
||
$result->{'Common Settings'}{state} = $info;
|
||
$result->{'Common Settings'}{result} .= qq{The Perl module AI::DecisionTree is missing. <br>};
|
||
$result->{'Common Settings'}{note} .= qq{If you want use AI support, please install it with e.g. "sudo apt-get install libai-decisiontree-perl".<br>};
|
||
$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" <br>};
|
||
$result->{'Common Settings'}{note} .= qq{Set pvCorrectionFactor_Auto to "on_complex" is recommended.<br>};
|
||
}
|
||
|
||
if (!$result->{'Common Settings'}{fault}) {
|
||
$result->{'Common Settings'}{result} .= $hqtxt{fulfd}{$lang}.'<br>';
|
||
$result->{'Common Settings'}{note} .= qq{<br>checked parameters and attributes: <br>};
|
||
$result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto <br>};
|
||
}
|
||
}
|
||
|
||
if (isSolCastUsed ($hash)) { # allg. Settings bei Nutzung SolCast API
|
||
my $gdn = AttrVal ('global', 'dnsServer', '');
|
||
my $osi = AttrVal ($name, 'ctrlSolCastAPIoptimizeReq', 0);
|
||
|
||
my $lam = StatusAPIVal ($hash, 'SolCast', '?All', 'response_message', 'success');
|
||
|
||
if ($pcf !~ /on/xs) {
|
||
$result->{'Common Settings'}{state} = $info;
|
||
$result->{'Common Settings'}{result} .= qq{pvCorrectionFactor_Auto is set to "$pcf" <br>};
|
||
$result->{'Common Settings'}{note} .= qq{set pvCorrectionFactor_Auto to "on_complex" is recommended if the SolCast efficiency factor is already adjusted.<br>};
|
||
}
|
||
|
||
if (!$osi) {
|
||
$result->{'Common Settings'}{state} = $warn;
|
||
$result->{'Common Settings'}{result} .= qq{Attribute ctrlSolCastAPIoptimizeReq is set to "$osi" <br>};
|
||
$result->{'Common Settings'}{note} .= qq{set ctrlSolCastAPIoptimizeReq to "1" is recommended.<br>};
|
||
$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:<br>"$lam"<br>};
|
||
$result->{'API Access'}{note} .= qq{Wait until the next day when the limit resets.<br>};
|
||
$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:<br>"$lam"<br>};
|
||
$result->{'API Access'}{note} .= qq{Check the validity of your API key and Rooftop identificators.<br>};
|
||
$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. <br>};
|
||
$result->{'API Access'}{note} .= qq{set global attribute dnsServer to the IP Adresse of your DNS Server.<br>};
|
||
$result->{'API Access'}{fault} = 1;
|
||
}
|
||
|
||
if (!$result->{'Common Settings'}{fault}) {
|
||
$result->{'Common Settings'}{result} .= $hqtxt{fulfd}{$lang}.'<br>';
|
||
$result->{'Common Settings'}{note} .= qq{<br>checked parameters and attributes: <br>};
|
||
$result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto, ctrlSolCastAPIoptimizeReq, global dnsServer <br>};
|
||
}
|
||
}
|
||
|
||
if (isDWDUsed ($hash)) { # allg. Settings bei Nutzung DWD API
|
||
my $lam = StatusAPIVal ($hash, 'DWD', '?All', 'response_message', 'success');
|
||
|
||
if ($aidtabs) {
|
||
$result->{'Common Settings'}{state} = $info;
|
||
$result->{'Common Settings'}{result} .= qq{The Perl module AI::DecisionTree is missing. <br>};
|
||
$result->{'Common Settings'}{note} .= qq{If you want use AI support, please install it with e.g. "sudo apt-get install libai-decisiontree-perl".<br>};
|
||
$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" <br>};
|
||
$result->{'Common Settings'}{note} .= qq{Set pvCorrectionFactor_Auto to "on_complex" or "on_complex_ai" is recommended.<br>};
|
||
}
|
||
|
||
if ($lam ne 'success') {
|
||
$result->{'API Access'}{state} = $nok;
|
||
$result->{'API Access'}{result} .= qq{DWD last message:<br>"$lam"<br>};
|
||
$result->{'API Access'}{note} .= qq{Check the setup of the device "$raname". <br>};
|
||
$result->{'API Access'}{note} .= qq{It is possible that not all readings are transmitted when "$raname" is newly set up or was changed. <br>};
|
||
$result->{'API Access'}{note} .= qq{In this case, wait until tomorrow and check again.<br>};
|
||
$result->{'API Access'}{fault} = 1;
|
||
}
|
||
|
||
if (!$result->{'Common Settings'}{fault}) {
|
||
$result->{'Common Settings'}{result} .= $hqtxt{fulfd}{$lang}.'<br>';
|
||
$result->{'Common Settings'}{note} .= qq{<br>checked Perl modules: <br>};
|
||
$result->{'Common Settings'}{note} .= qq{AI::DecisionTree <br>};
|
||
$result->{'Common Settings'}{note} .= qq{<br>checked parameters and attributes: <br>};
|
||
$result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto <br>};
|
||
}
|
||
}
|
||
|
||
if (isVictronKiUsed ($hash)) { # allg. Settings bei Nutzung VictronKI-API
|
||
my $gdn = AttrVal ('global', 'dnsServer', '');
|
||
my $vrmcr = StatusAPIVal ($hash, '?VRM', '?API', 'credentials', '');
|
||
|
||
if ($pcf !~ /on/xs) {
|
||
$result->{'Common Settings'}{state} = $warn;
|
||
$result->{'Common Settings'}{result} .= qq{pvCorrectionFactor_Auto is set to "$pcf" <br>};
|
||
$result->{'Common Settings'}{note} .= qq{set pvCorrectionFactor_Auto to "on_complex" is recommended.<br>};
|
||
$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. <br>};
|
||
$result->{'API Access'}{note} .= qq{set the credentials with command "set $name vrmCredentials".<br>};
|
||
$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. <br>};
|
||
$result->{'API Access'}{note} .= qq{set global attribute dnsServer to the IP Adresse of your DNS Server.<br>};
|
||
$result->{'API Access'}{fault} = 1;
|
||
}
|
||
|
||
if (!$result->{'Common Settings'}{fault}) {
|
||
$result->{'Common Settings'}{result} .= $hqtxt{fulfd}{$lang}.'<br>';
|
||
$result->{'Common Settings'}{note} .= qq{<br>checked parameters and attributes: <br>};
|
||
$result->{'Common Settings'}{note} .= qq{pvCorrectionFactor_Auto, global dnsServer, vrmCredentials <br>};
|
||
}
|
||
}
|
||
|
||
if (!$result->{'Common Settings'}{fault}) {
|
||
$result->{'Common Settings'}{note} .= qq{global latitude, global longitude, global altitude, global language <br>};
|
||
$result->{'Common Settings'}{note} .= qq{event-on-change-reading, ctrlLanguage <br>};
|
||
}
|
||
|
||
## installierte Perl Module
|
||
#############################
|
||
if ($aidtabs) {
|
||
$result->{'Perl Modules'}{state} = $info;
|
||
$result->{'Perl Modules'}{result} .= qq{The Perl module AI::DecisionTree is missing. <br>};
|
||
$result->{'Perl Modules'}{note} .= qq{If you want use AI support, please install it with e.g. "sudo apt-get install libai-decisiontree-perl".<br>};
|
||
$result->{'Perl Modules'}{info} = 1;
|
||
}
|
||
|
||
if (!$result->{'Perl Modules'}{info} && !$result->{'Perl Modules'}{warn} && !$result->{'Perl Modules'}{fault}) {
|
||
$result->{'Perl Modules'}{result} .= $hqtxt{fulfd}{$lang}.'<br>';
|
||
$result->{'Perl Modules'}{note} .= qq{<br>checked installed Perl Modules: <br>};
|
||
$result->{'Perl Modules'}{note} .= qq{AI::DecisionTree <br>};
|
||
}
|
||
|
||
|
||
|
||
## 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.<br>};
|
||
}
|
||
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}.'<br>';
|
||
$result->{'FTUI Widget Files'}{result} .= $cmmsg.'<br>';
|
||
$result->{'FTUI Widget Files'}{note} .= qq{Update the FHEM Tablet UI Widget Files with the command: <br>};
|
||
$result->{'FTUI Widget Files'}{note} .= qq{"get $name ftuiFramefiles". <br>};
|
||
$result->{'FTUI Widget Files'}{note} .= qq{After that do the test again. If the error is permanent, please inform the maintainer.<br>};
|
||
$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: <br>};
|
||
$result->{'FTUI Widget Files'}{note} .= qq{"get $name ftuiFramefiles". <br>};
|
||
$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: <br>};
|
||
$result->{'FTUI Widget Files'}{note} .= (join ', ', @fs).qq{ <br>};
|
||
}
|
||
}
|
||
|
||
## Ausgabe
|
||
############
|
||
my $out = qq{<html>};
|
||
$out .= qq{<b>}.$hqtxt{plntck}{$lang}.qq{ - Model: $hash->{MODEL} </b> <br><br>};
|
||
|
||
$out .= qq{<table class="roomoverview" style="text-align:left; border:1px solid; padding:5px; border-spacing:5px; margin-left:auto; margin-right:auto;">};
|
||
$out .= qq{<tr style="font-weight:bold;">};
|
||
$out .= qq{<td style="text-decoration:underline; padding: 5px;"> $hqtxt{object}{$lang} </td>};
|
||
$out .= qq{<td style="text-decoration:underline;"> $hqtxt{state}{$lang} </td>};
|
||
$out .= qq{<td> </td>};
|
||
$out .= qq{<td style="text-decoration:underline;"> $hqtxt{result}{$lang} </td>};
|
||
$out .= qq{<td> </td>};
|
||
$out .= qq{<td style="text-decoration:underline;"> $hqtxt{note}{$lang} </td>};
|
||
$out .= qq{</tr>};
|
||
$out .= qq{<tr></tr>};
|
||
|
||
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{<tr>};
|
||
$out .= qq{<td style="padding: 5px; white-space:nowrap;"> <b>$key</b> </td>};
|
||
$out .= qq{<td style="padding: 5px; text-align: center"> $result->{$key}{state} </td>};
|
||
$out .= qq{<td style="padding: 5px;"> </td>};
|
||
$out .= qq{<td style="padding: 0px;"> $result->{$key}{result} </td>};
|
||
$out .= qq{<td style="padding: 0px;"> </td>};
|
||
$out .= qq{<td style="padding-right: 5px; text-align: left"> $result->{$key}{note} </td>};
|
||
$out .= qq{</tr>};
|
||
|
||
if ($hc < $hz) { # Zwischenzeile
|
||
$out .= qq{<tr>};
|
||
$out .= qq{<td> </td>};
|
||
$out .= qq{</tr>};
|
||
}
|
||
}
|
||
|
||
$out .= qq{</table>};
|
||
$out .= qq{</html>};
|
||
|
||
$out .= "<br>";
|
||
|
||
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/<I>/$info/gx;
|
||
$out =~ s/<W>/$warn/gx;
|
||
|
||
return $out;
|
||
}
|
||
|
||
#####################################################################
|
||
# Ermittelt den PV Überschuß nach verschiedenen Verfahren
|
||
# ($surpmeth). Auswertung des Schieberegisters surplusslidereg.
|
||
#
|
||
# $surpmeth = default - der aktuell gemessene Überschuß
|
||
# $surpmeth = 2 .. 20 - Durchschnitt der letzten X Messungen
|
||
# $surpmeth = median - Median der vorhandenen Überschußwerte
|
||
# $surpmeth = <Device>:<Reading> - Device/Reading Kombination
|
||
# die einen berechneten
|
||
# User spezifischen PV-Überschuß
|
||
# liefert
|
||
#
|
||
# Rückgabe: PV Überschuß
|
||
#
|
||
#####################################################################
|
||
sub determSurplus {
|
||
my $hash = shift;
|
||
my $c = shift;
|
||
|
||
my $surpmeth = ConsumerVal ($hash, $c, 'surpmeth', 'default');
|
||
my $splref = CurrentVal ($hash, 'surplusslidereg', '');
|
||
my $name = $hash->{NAME};
|
||
my $method = 'default';
|
||
|
||
my ($surplus, $fallback);
|
||
|
||
if ($surpmeth eq 'median') { # Median der Werte in surplusslidereg, !kann UNDEF sein!
|
||
$surplus = medianArray ($splref);
|
||
$method = 'median';
|
||
}
|
||
elsif ($surpmeth eq 'default') { # aktueller Energieüberschuß
|
||
$surplus = CurrentVal ($hash, 'surplus', 0);
|
||
$method = 'default';
|
||
}
|
||
elsif ($surpmeth =~ /^[2-9]$|^1[0-9]$|^20$/xs) {
|
||
$surplus = avgArray ($splref, $surpmeth); # Average Ermittlung, !kann UNDEF sein!
|
||
$method = "average:$surpmeth";
|
||
}
|
||
elsif ($surpmeth =~ /.*:.*/xs) {
|
||
my ($dv, $rd) = split ':', $surpmeth;
|
||
$method = "$dv:$rd";
|
||
my ($err) = isDeviceValid ( { name => $name, obj => $dv, method => 'string' } );
|
||
|
||
if ($err) {
|
||
$fallback = 1;
|
||
Log3 ($name, 1, qq{$name - ERROR of consumer $c key 'surpmeth': $err (fall back to default Surplus determination)});
|
||
}
|
||
else {
|
||
$surplus = ReadingsNum ($dv, $rd, '');
|
||
|
||
if (!isNumeric ($surplus)) {
|
||
$fallback = 1;
|
||
Log3 ($name, 1, qq{$name - ERROR of consumer $c key 'surpmeth': Device $dv / Reading $rd is not numeric (fall back to default Surplus determination)});
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($fallback) { # Fall Back
|
||
$surplus = CurrentVal ($hash, 'surplus', 0);
|
||
$method = $method." but fallback to 'default'";
|
||
}
|
||
|
||
return ($method, $surplus);
|
||
}
|
||
|
||
################################################################
|
||
# Array auf eine festgelegte Anzahl Elemente beschränken,
|
||
# Das älteste Element wird entfernt
|
||
#
|
||
# $aref = Referenz zum Array
|
||
# $limit = die Anzahl Elemente auf die gekürzt werden soll
|
||
# (default SLIDENUMMAX)
|
||
#
|
||
################################################################
|
||
sub limitArray {
|
||
my $aref = shift;
|
||
my $limit = shift // SLIDENUMMAX;
|
||
|
||
return if(ref $aref ne 'ARRAY');
|
||
|
||
while (scalar @{$aref} > $limit) {
|
||
shift @{$aref};
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Durchschnitt der Werte eines Array ermitteln
|
||
#
|
||
# $aref = Referenz zum Array
|
||
# $num = Anzahl der zu verwendenden Elemente
|
||
# (es MÜSSEN die num Anzahl
|
||
# Elemente im Array vorhanden sein)
|
||
#
|
||
################################################################
|
||
sub avgArray {
|
||
my $aref = shift;
|
||
my $num = shift // SLIDENUMMAX;
|
||
|
||
return undef if(ref $aref ne 'ARRAY' || scalar @{$aref} < $num);
|
||
|
||
my $sum = 0;
|
||
|
||
for my $i (0 .. $num-1) {
|
||
$sum += ${$aref}[$i];
|
||
}
|
||
|
||
my $avg = $sum / $num;
|
||
|
||
return $avg;
|
||
}
|
||
|
||
######################################################################################
|
||
# Median der Werte eines Array ermitteln
|
||
# (https://www.ionos.de/digitalguide/online-marketing/web-analyse/median-berechnen/)
|
||
#
|
||
# $aref = Referenz zum Array
|
||
#
|
||
######################################################################################
|
||
sub medianArray {
|
||
my $aref = shift;
|
||
|
||
return undef if(ref $aref ne 'ARRAY' || !scalar @{$aref});
|
||
|
||
my $enum = scalar @{$aref}; # Anzahl der Elemente im Array
|
||
my @sorted = sort { $a <=> $b } @{$aref}; # Numerisch aufsteigend
|
||
|
||
if ($enum % 2) { # Array enthält ungerade Anzahl Elemente
|
||
return $sorted[$enum/2]; # ungerade Elemente -> Median Element steht in der Mitte von @sorted
|
||
}
|
||
else {
|
||
return ($sorted[$enum/2 - 1] + $sorted[$enum/2]) / 2; # gerade Elemente -> Median ist der Durchschnitt der beiden mittleren Elemente
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# Berechnen Tag / Stunden Verschieber
|
||
# aus aktueller Stunde + lfd. Nummer
|
||
################################################################
|
||
sub calcDayHourMove {
|
||
my $chour = shift;
|
||
my $num = shift;
|
||
|
||
my $fh = $chour + $num;
|
||
my $fd = int ($fh / 24) ;
|
||
$fh = $fh - ($fd * 24);
|
||
|
||
return ($fd, $fh);
|
||
}
|
||
|
||
################################################################
|
||
# Zeit gemäß DWD_OpenData-Format
|
||
# Berechnen Tag / Stunden Verschieber ab aktuellen Tag
|
||
# Input: YYYY-MM-DD HH:MM:SS
|
||
# Output: $fd - 0 (Heute), 1 (Morgen), 2 (Übermorgen), ....
|
||
# $fh - Stunde von $fd ohne führende Null
|
||
# Return: fc${fd}_${fh}
|
||
################################################################
|
||
sub formatWeatherTimestrg {
|
||
my $date = shift // return;
|
||
|
||
my $cdate = strftime "%Y-%m-%d", localtime(time);
|
||
my $refts = timestringToTimestamp ($cdate.' 00:00:00'); # Referenztimestring
|
||
my $datts = timestringToTimestamp ($date);
|
||
my $fd = int (($datts - $refts) / 86400);
|
||
my $fh = int ((split /[ :]/, $date)[1]);
|
||
|
||
return "fc${fd}_${fh}";
|
||
}
|
||
|
||
################################################################
|
||
# Spezialfall auflösen wenn Wert von $val2 dem
|
||
# Redingwert von $val1 entspricht sofern $val1 negativ ist
|
||
################################################################
|
||
sub substSpecialCases {
|
||
my $paref = shift;
|
||
my $dev = $paref->{dev};
|
||
my $rdg = $paref->{rdg};
|
||
my $rdgf = $paref->{rdgf};
|
||
|
||
my $val1 = ReadingsNum ($dev, $rdg, 0) * $rdgf;
|
||
my $val2;
|
||
|
||
if($val1 <= 0) {
|
||
$val2 = abs($val1);
|
||
$val1 = 0;
|
||
}
|
||
else {
|
||
$val2 = 0;
|
||
}
|
||
|
||
return ($val1,$val2);
|
||
}
|
||
|
||
################################################################
|
||
# Timestrings berechnen
|
||
# gibt Zeitstring in lokaler Zeit zurück
|
||
################################################################
|
||
sub timestampToTimestring {
|
||
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);
|
||
}
|
||
|
||
################################################################
|
||
# Timestrings aus Startzeit Timestamp und gegebenen Offset (s)
|
||
# berechnen, Rückgabe als Hashreferenz
|
||
################################################################
|
||
sub timestringsFromOffset {
|
||
my $epoch = shift;
|
||
my $offset = shift // 0;
|
||
|
||
return if($epoch !~ /^-?[0-9]*(.[0-9]*)?$/xs);
|
||
|
||
if (length ($epoch) == 13) { # Millisekunden
|
||
$epoch = $epoch / 1000;
|
||
}
|
||
|
||
my @ts = localtime ($epoch + $offset); # Offset kann pos. oder negativ sein
|
||
|
||
my $dt = {
|
||
year => (strftime "%Y", (@ts)), # Jahr
|
||
month => (strftime "%m", (@ts)), # Monat
|
||
day => (strftime "%d", (@ts)), # Tag (range 01 .. 31)
|
||
date => (strftime "%Y-%m-%d", (@ts)), # Datum
|
||
hour => (strftime "%H", (@ts)), # Stunde in 24h format (00-23)
|
||
minute => (strftime "%M", (@ts)), # Minute (00-59)
|
||
dayname => (strftime "%a", (@ts)), # Wochentagsname
|
||
};
|
||
|
||
return $dt;
|
||
}
|
||
|
||
################################################################
|
||
# 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);
|
||
|
||
@da = (); # completely empty @ARRAY
|
||
|
||
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;
|
||
}
|
||
|
||
################################################################
|
||
# Zentralschleife freigeben
|
||
################################################################
|
||
sub releaseCentralTask {
|
||
my $hash = shift;
|
||
my $name = $hash->{NAME};
|
||
|
||
RemoveInternalTimer ($hash, 'FHEM::SolarForecast::releaseCentralTask');
|
||
|
||
$data{$name}{current}{ctrunning} = 0;
|
||
|
||
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, 'setupWeatherDev1', ''); # Weather forecast Device 1
|
||
($afc,$h) = parseParams ($fcdev1);
|
||
$fcdev1 = $afc->[0] // '';
|
||
|
||
my $fcdev2 = AttrVal ($name, 'setupWeatherDev2', ''); # Weather forecast Device 2
|
||
($afc,$h) = parseParams ($fcdev2);
|
||
$fcdev2 = $afc->[0] // '';
|
||
|
||
my $fcdev3 = AttrVal ($name, 'setupWeatherDev3', ''); # Weather forecast Device 3
|
||
($afc,$h) = parseParams ($fcdev3);
|
||
$fcdev3 = $afc->[0] // '';
|
||
|
||
my $radev = AttrVal ($name, 'setupRadiationAPI', ''); # Radiation forecast Device
|
||
($ara,$h) = parseParams ($radev);
|
||
$radev = $ara->[0] // '';
|
||
|
||
my $medev = AttrVal ($name, 'setupMeterDev', ''); # Meter Device
|
||
($ame,$h) = parseParams ($medev);
|
||
$medev = $ame->[0] // '';
|
||
push @cd, $medev;
|
||
|
||
for my $c (sort{$a<=>$b} keys %{$data{$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);
|
||
}
|
||
|
||
for my $bn (1..MAXBATTERIES) { # Battery Devices
|
||
$bn = sprintf "%02d", $bn;
|
||
my $badev = AttrVal ($name, "setupBatteryDev${bn}", '');
|
||
my ($aba) = parseParams ($badev);
|
||
push @cd, $aba->[0] if($aba->[0]);
|
||
}
|
||
|
||
for my $in (1..MAXINVERTER) { # Inverter Devices
|
||
$in = sprintf "%02d", $in;
|
||
my $inc = AttrVal ($name, "setupInverterDev${in}", '');
|
||
my ($ind) = parseParams ($inc);
|
||
push @cd, $ind->[0] if($ind->[0]);
|
||
}
|
||
|
||
@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, $medev;
|
||
|
||
for my $prn (1..MAXPRODUCER) { # Producer Devices
|
||
$prn = sprintf "%02d", $prn;
|
||
my $pdc = AttrVal ($name, "setupOtherProducer${prn}", "");
|
||
my ($prd) = parseParams ($pdc);
|
||
push @nd, $prd->[0] if($prd->[0]);
|
||
}
|
||
|
||
my @ndn = ();
|
||
|
||
for my $e (@nd) {
|
||
next if(grep /^$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;
|
||
}
|
||
|
||
################################################################
|
||
# Funktion liefert den Planungsmodus eines Verbrauchers
|
||
# mode kann sein:
|
||
# can
|
||
# must
|
||
################################################################
|
||
sub getConsumerPlanningMode {
|
||
my $hash = shift;
|
||
my $c = shift;
|
||
|
||
my $name = $hash->{NAME};
|
||
my $mode = ConsumerVal ($hash, $c, 'mode', DEFCMODE); # Consumer Planungsmode
|
||
|
||
if ($mode =~ /^(?:can|must)$/xs) {
|
||
return $mode;
|
||
}
|
||
|
||
## Mode kann über Device:Reading gesteuert sein
|
||
#################################################
|
||
my ($dv, $rd) = split ':', $mode;
|
||
my ($err) = isDeviceValid ( { name => $hash->{NAME}, obj => $dv, method => 'string' } );
|
||
|
||
if ($err) {
|
||
Log3 ($name, 1, qq{$name - ERROR - consumer >$c< - The device '$dv' in consumer key 'mode' doesn't exist. Fall back to 'DEFCMODE' mode.});
|
||
return DEFCMODE;
|
||
}
|
||
|
||
$err = q{};
|
||
$mode = ReadingsVal ($dv, $rd, '');
|
||
|
||
if ($mode !~ /^(?:can|must)$/xs) {
|
||
Log3 ($name, 1, qq{$name - ERROR - consumer >$c< - The reading '$rd' of device '$dv' is invalid or doesn't contain a valid mode. Fall back to 'DEFCMODE' mode.});
|
||
return DEFCMODE;
|
||
}
|
||
|
||
return $mode;
|
||
}
|
||
|
||
################################################################
|
||
# 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{$name}{consumers}{$c}{planstate};
|
||
delete $data{$name}{consumers}{$c}{planSupplement};
|
||
delete $data{$name}{consumers}{$c}{planswitchon};
|
||
delete $data{$name}{consumers}{$c}{planswitchoff};
|
||
delete $data{$name}{consumers}{$c}{plandelete};
|
||
delete $data{$name}{consumers}{$c}{ehodpieces};
|
||
|
||
deleteReadingspec ($hash, "consumer${c}.*");
|
||
|
||
Log3($name, 3, qq{$name - Consumer planning of "$calias" deleted});
|
||
|
||
return;
|
||
}
|
||
|
||
################################################################
|
||
# 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);
|
||
}
|
||
|
||
################################################################
|
||
# Internal MODEL und Model abhängige Setzungen / Löschungen
|
||
################################################################
|
||
sub setModel {
|
||
my $hash = shift;
|
||
my $name = $hash->{NAME};
|
||
|
||
my $radapi = AttrVal ($name, 'setupRadiationAPI', 'DWD');
|
||
my $wthapi = AttrVal ($name, 'setupWeatherDev1', 'DWD');
|
||
|
||
if ($radapi =~ /SolCast-/xs) { $hash->{MODEL} = 'SolCastAPI'; }
|
||
elsif ($radapi =~ /ForecastSolar-/xs) { $hash->{MODEL} = 'ForecastSolarAPI'; }
|
||
elsif ($radapi =~ /VictronKI-/xs) { $hash->{MODEL} = 'VictronKiAPI'; }
|
||
elsif ($radapi =~ /OpenMeteoDWDEnsemble-/xs) { $hash->{MODEL} = 'OpenMeteoDWDEnsembleAPI'; }
|
||
elsif ($radapi =~ /OpenMeteoDWD-/xs) { $hash->{MODEL} = 'OpenMeteoDWDAPI'; }
|
||
elsif ($radapi =~ /OpenMeteoWorld-/xs) { $hash->{MODEL} = 'OpenMeteoWorldAPI'; }
|
||
else { $hash->{MODEL} = 'DWD'; }
|
||
|
||
if ($wthapi =~ /OpenMeteoDWDEnsemble-/xs) { $hash->{WEATHERMODEL} = 'OpenMeteoDWDEnsembleAPI'; }
|
||
elsif ($wthapi =~ /OpenMeteoDWD-/xs) { $hash->{WEATHERMODEL} = 'OpenMeteoDWDAPI'; }
|
||
elsif ($wthapi =~ /OpenMeteoWorld-/xs) { $hash->{WEATHERMODEL} = 'OpenMeteoWorldAPI'; }
|
||
else { $hash->{WEATHERMODEL} = '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{$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 '<any>_ai' for switch on AI support";
|
||
}
|
||
|
||
if ($err) {
|
||
$data{$name}{current}{aicanuse} = $err;
|
||
return 0;
|
||
}
|
||
|
||
$data{$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 ($err, $cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name
|
||
|
||
if ($err) {
|
||
Log3 ($name, 1, "$name - ERROR - $err");
|
||
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 ($err, $cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name
|
||
|
||
if ($err) {
|
||
Log3 ($name, 1, "$name - ERROR - $err");
|
||
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
|
||
my ($err) = isDeviceValid ( { name => $name, obj => $cname, method => 'string' } );
|
||
|
||
if ($err) {
|
||
Log3 ($name, 1, qq{$name - ERROR - 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{$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 ($err, $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) { # nur 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 $dswoncond = ConsumerVal ($hash, $c, 'dswoncond', ''); # Device zur Lieferung einer zusätzlichen Einschaltbedingung
|
||
my ($err) = isDeviceValid ( { name => $hash->{NAME},
|
||
obj => $dswoncond,
|
||
method => 'string',
|
||
}
|
||
);
|
||
|
||
if ($dswoncond && $err) {
|
||
$err = qq{ERROR - the device "$dswoncond" doesn't exist! Check the key "swoncond" in attribute "consumer${c}"};
|
||
return (0, $info, $err);
|
||
}
|
||
|
||
$err = q{};
|
||
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 $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', '');
|
||
}
|
||
|
||
my ($err) = isDeviceValid ( { name => $hash->{NAME}, obj => $dswoffcond, method => 'string' } );
|
||
|
||
if ($dswoffcond && $err) {
|
||
$err = qq{ERROR - the device "$dswoffcond" doesn't exist! Check the key "swoffcond" or "interruptable" in attribute "consumer${c}"};
|
||
return (0, $info, $err);
|
||
}
|
||
|
||
$err = q{};
|
||
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};
|
||
}
|
||
|
||
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 $digncond = ConsumerVal ($hash, $c, 'dspignorecond', ''); # Device zur Lieferung einer "Überschuß Ignore-Bedingung"
|
||
my ($err) = isDeviceValid ( { name => $hash->{NAME},
|
||
obj => $digncond,
|
||
method => 'string',
|
||
}
|
||
);
|
||
if ($digncond && $err) {
|
||
$err = qq{ERROR - the device "$digncond" doesn't exist! Check the key "spignorecond" in attribute "consumer${c}"};
|
||
return (0, $info, $err);
|
||
}
|
||
|
||
$err = q{};
|
||
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${c}/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 $name = $paref->{name};
|
||
my $c = $paref->{consumer};
|
||
my $t = $paref->{t};
|
||
|
||
my $hash = $defs{$name};
|
||
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 $valid = 0;
|
||
|
||
for my $bn (1..MAXBATTERIES) {
|
||
$bn = sprintf "%02d", $bn;
|
||
my ($err) = isDeviceValid ( { name => $name, obj => 'setupBatteryDev'.$bn, method => 'attr' } );
|
||
next if($err);
|
||
|
||
$valid = 1;
|
||
}
|
||
|
||
return $valid;
|
||
}
|
||
|
||
################################################################
|
||
# 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${c}/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;
|
||
}
|
||
|
||
################################################################
|
||
# Prüfung auf Verwendung von Open-Meteo API als
|
||
# Lieferant für Wetterdaten
|
||
################################################################
|
||
sub isWeatherOpenMeteoUsed {
|
||
my $hash = shift;
|
||
|
||
my $ret = 0;
|
||
|
||
if (InternalVal ($hash->{NAME}, 'WEATHERMODEL', '') =~ /^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 im Ojekt übergebene Device valide ist
|
||
# input: $obj - das Objekt (Reading, Attr, String)
|
||
# method - Art des Objekts
|
||
# reading: Device ist im Reading Value enthalten
|
||
# attr: Device ist im Attr Value enthalten
|
||
# string: Device ist im Objekt-Inhalt enthalten
|
||
# return: $err - evtl. Fehler
|
||
# $a->[0] - das extrahierte Device
|
||
# $h - Hash der geparsten Entität
|
||
#####################################################################
|
||
sub isDeviceValid {
|
||
my $paref = shift;
|
||
my $name = $paref->{name};
|
||
my $obj = $paref->{obj};
|
||
my $method = $paref->{method} // 'reading';
|
||
|
||
my $err = '';
|
||
my $dev = '';
|
||
|
||
if ($method eq 'reading') {
|
||
$dev = ReadingsVal ($name, $obj, '');
|
||
return qq{Reading '$obj' is not set or is empty} if(!$dev);
|
||
}
|
||
elsif ($method eq 'attr') {
|
||
$dev = AttrVal ($name, $obj, '');
|
||
return qq{Attribute '$obj' is not set} if(!$dev);
|
||
}
|
||
elsif ($method eq 'string') {
|
||
return qq{Object '$obj' is empty} if(!$obj);
|
||
$dev = $obj;
|
||
}
|
||
|
||
my ($a, $h) = parseParams ($dev);
|
||
|
||
my ($dv, $al) = !$a->[0] ? ('', '') :
|
||
$a->[0] =~ /:/xs ? (split ':', $a->[0]) :
|
||
($a->[0], ''); # (optionalen) SF-spezifischen Alias abtrennen
|
||
|
||
if (!$dv || !$defs{$dv}) {
|
||
$dv //= '';
|
||
$err = qq{The device '$dv' doesn't exist or is not a valid device.};
|
||
$err = qq{There is no device set. Check the syntax with the command reference.} if(!$dv);
|
||
$err = qq{The device '$dv' doesn't exist anymore! Delete or change the attribute '$obj'.} if(!$defs{$dv} && $method eq 'attr' && $obj =~ /consumer/);
|
||
}
|
||
|
||
if ($err) {
|
||
Log3 ($name, 1, "$name - ERROR - $err");
|
||
}
|
||
|
||
if ($al) { # Leerzeichen im SF-Alias generieren
|
||
$al =~ s/\+/ /g;
|
||
}
|
||
|
||
return ($err, $dv, $h, $al);
|
||
}
|
||
|
||
#####################################################################
|
||
# Prüft ob das in setupWeatherDevX
|
||
# ü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
|
||
|
||
return if(!$fcname);
|
||
|
||
$valid = 1;
|
||
if (!$defs{$fcname} || $defs{$fcname}{TYPE} ne "DWD_OpenData") { $valid = '' }
|
||
|
||
my ($rapi, $wapi) = getStatusApiName ($hash); # $rapi - Radiation-API, $wapi - Weather-API
|
||
|
||
if ($wapi =~ /^OpenMeteo/xs) {
|
||
$valid = 1;
|
||
$apiu = $wapi;
|
||
}
|
||
|
||
return ($valid, $fcname, $apiu);
|
||
}
|
||
|
||
################################################################
|
||
# Inhalt des Attr graphicHeaderOwnspecValForm validieren
|
||
################################################################
|
||
sub isGhoValFormValid {
|
||
my $name = shift;
|
||
my $code = shift;
|
||
|
||
my $err = q{};
|
||
($err, $code) = checkCode ($name, $code);
|
||
|
||
return $err;
|
||
}
|
||
|
||
###################################################################
|
||
# 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 $name = $paref->{name};
|
||
my $lang = $paref->{lang};
|
||
|
||
my $hash = $defs{$name};
|
||
my $currts = int time;
|
||
my $agets = $currts;
|
||
|
||
my $resh->{agedv} = '-';
|
||
$resh->{mosmix} = '-';
|
||
$resh->{exceed} = '';
|
||
$resh->{fctime} = '-';
|
||
|
||
my ($newts, $th);
|
||
|
||
for my $step (1..MAXWEATHERDEV) {
|
||
my ($valid, $fcname, $apiu) = isWeatherDevValid ($hash, 'setupWeatherDev'.$step);
|
||
next if(!$fcname && $step ne 1);
|
||
|
||
if (!$apiu) {
|
||
if (!$fcname || !$valid) {
|
||
if (!$fcname) {
|
||
return (qq{No DWD device is defined in attribute "setupWeatherDev$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 {
|
||
my ($rapi, $wapi) = getStatusApiName ($hash);
|
||
$newts = StatusAPIVal ($hash, $wapi, '?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 $name = $paref->{name};
|
||
my $lang = $paref->{lang};
|
||
|
||
my $hash = $defs{$name};
|
||
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 "setupRadiationAPI"}, $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 ($err, $cname, $dswname) = getCDnames ($hash, $c); # Consumer und Switch Device Name
|
||
|
||
if ($err) {
|
||
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 'no Regex is provided' if(!defined $regexp);
|
||
|
||
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 "{<your own code>}"};
|
||
}
|
||
|
||
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
|
||
my ($err) = isDeviceValid ( { name => $hash->{NAME}, obj => $dswname, method => 'string' } );
|
||
$err = qq{$err Please check device names in consumer '$c' attribute} if($err);
|
||
|
||
return ($err, $cname, $dswname);
|
||
}
|
||
|
||
################################################################
|
||
# Namen der Strahlungs-API und Wetter-API ermitteln.
|
||
# Wird als Schlüssel in statusapi bzw. weatherapi verwendet.
|
||
# Return:
|
||
# $rapi - Name der Strahlungsdaten-API
|
||
# $wapi - Name der Wetter-API
|
||
################################################################
|
||
sub getStatusApiName {
|
||
my $hash = shift;
|
||
|
||
my $rapi = isOpenMeteoUsed ($hash) ? 'OpenMeteo' :
|
||
isForecastSolarUsed ($hash) ? 'ForecastSolar' :
|
||
isSolCastUsed ($hash) ? 'SolCast' :
|
||
isVictronKiUsed ($hash) ? 'VictronKi' :
|
||
isDWDUsed ($hash) ? 'DWD' :
|
||
'';
|
||
|
||
my $wapi = isWeatherOpenMeteoUsed ($hash) ? 'OpenMeteo' :
|
||
'';
|
||
|
||
return ($rapi, $wapi);
|
||
}
|
||
|
||
################################################################
|
||
# 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 :
|
||
$val > -2 ? 0 :
|
||
$val > -5 ? -5 :
|
||
$val > -7 ? -5 :
|
||
$val > -10 ? -10 :
|
||
$val > -12 ? -10 :
|
||
$val > -15 ? -15 :
|
||
$val > -17 ? -15 :
|
||
-20;
|
||
|
||
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;
|
||
}
|
||
|
||
###############################################################################
|
||
# Teilt das Original-Array in Unter-Arrays auf, die den Inhalt des
|
||
# Originals enthalten. Die Größe jedes Unterarrays ist gleich oder kleiner als
|
||
# $split_size, wobei das letzte Array in der Regel kleiner ist, wenn nicht
|
||
# genügend Elemente in @original vorhanden sind.
|
||
# (aus https://metacpan.org/dist/Array-Split/source/lib/Array/Split.pm)
|
||
#
|
||
# arraySplitBy ($split_size, @original)
|
||
###############################################################################
|
||
sub arraySplitBy {
|
||
my $split_size = shift;
|
||
|
||
$split_size = max ($split_size, 1);
|
||
my @sub_arrays;
|
||
|
||
while (@_) {
|
||
push @sub_arrays, [splice @_, 0, $split_size];
|
||
}
|
||
|
||
return @sub_arrays;
|
||
}
|
||
|
||
###############################################################################
|
||
# Teilt das angegebene Array in die Anzahl $count Unterarrays auf.
|
||
# Es wird versucht, so viele Unter-Arrays zu erstellen, wie $count angibt,
|
||
# aber es werden weniger zurückgegeben, wenn nicht genügend Elemente in
|
||
# @original vorhanden sind.
|
||
#
|
||
# Gibt eine Liste von Array-Referenzen zurück.
|
||
# (aus https://metacpan.org/dist/Array-Split/source/lib/Array/Split.pm)
|
||
#
|
||
# arraySplitInto ($count, @original)
|
||
###############################################################################
|
||
sub arraySplitInto {
|
||
my ($count, @original) = @_;
|
||
|
||
$count = max( $count, 1 );
|
||
my $size = ceil @original / $count;
|
||
|
||
return arraySplitBy ($size, @original);
|
||
}
|
||
|
||
###############################################################################
|
||
# 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 $dat = shift;
|
||
my $name = $dat->{name};
|
||
|
||
my $serial = eval { freeze ($dat)
|
||
}
|
||
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 ($s);
|
||
$mlen = $len if($len && $len > $mlen);
|
||
}
|
||
|
||
$mlen = $mlen > LPOOLLENLIM ? LPOOLLENLIM : $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 $name = $paref->{name};
|
||
my $hash = $defs{$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 or $name, $day, $hod, $key, $def)
|
||
#
|
||
# $day: Tag des Monats (01,02,...,31)
|
||
# $hod: Stunde des Tages (01,02,...,24,99)
|
||
# $key: etotaliXX - totale PV Erzeugung (Wh) des Inverters XX
|
||
# pvrlXX - realer PV Ertrag (Wh) des Inverters XX
|
||
# pvfc - PV Vorhersage
|
||
# pprlXX - Energieerzeugung des Produzenten XX
|
||
# etotalpXX - Zählerstand "Energieertrag total" (Wh) des Produzenten XX
|
||
# confc - Vorhersage Hausverbrauch (Wh)
|
||
# gcons - realer Netzbezug
|
||
# gfeedin - reale Netzeinspeisung
|
||
# batintotalXX - Gesamtladung Batterie XX (Wh) zu Beginn der Stunde
|
||
# batinXX - Ladung Batterie XX innerhalb der Stunde (Wh)
|
||
# batouttotalXX - Gesamtentladung Batterie XX (Wh)
|
||
# batoutXX - Entladung Batterie XX innerhalb der Stunde (Wh)
|
||
# batmsoc - max. SOC des Tages (%)
|
||
# batmaxsocXX - maximum SOC (%) der Batterie XX des Tages
|
||
# batsetsocXX - optimaler (berechneter) SOC (%) der Batterie XX 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 $name = shift;
|
||
my $day = shift;
|
||
my $hod = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined($data{$name}{pvhist}) &&
|
||
defined($data{$name}{pvhist}{$day}) &&
|
||
defined($data{$name}{pvhist}{$day}{$hod}) &&
|
||
defined($data{$name}{pvhist}{$day}{$hod}{$key})) {
|
||
return $data{$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 or $name, $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
|
||
# batinXX - Ladung Batterie XX (Wh)
|
||
# batoutXX - Entladung Batterie XX (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 (<Sonne Altitude>.<Cloudcover> = Faktor)
|
||
# lastTsMaxSocRchdXX - Timestamp des letzten Erreichens von SoC >= maxSoC
|
||
# nextTsMaxSocChgeXX - Timestamp bis zu dem die Batterie mindestens einmal maxSoC erreichen soll
|
||
# days2careXX - verbleibende Tage bis der Batterie XX 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)
|
||
# initdaybatintotXX - initialer Wert für Batterie intotal zu Beginn des Tages (Wh)
|
||
# batintotXX - aktuell total Batterieladung XX (Wh)
|
||
# initdaybatouttotXX - initialer Wert für Batterie outtotal zu Beginn des Tages (Wh)
|
||
# batouttotXX - aktuell total Batterieentladung (Wh)
|
||
# gridcontotal - Netzbezug total (Wh)
|
||
# aiRulesNumber - Anzahl der Regeln in der trainierten KI-Instanz
|
||
#
|
||
# $def: Defaultwert
|
||
#
|
||
#####################################################################################################
|
||
sub CircularVal {
|
||
my $name = shift;
|
||
my $hod = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if(defined($data{$name}{circular}) &&
|
||
defined($data{$name}{circular}{$hod}) &&
|
||
defined($data{$name}{circular}{$hod}{$key})) {
|
||
return $data{$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 or $name, $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 $name = shift;
|
||
my $hod = shift;
|
||
my $sabin = shift;
|
||
my $crang = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
my $corrf = $def;
|
||
my $qual = $def;
|
||
my $idx = 'simple';
|
||
|
||
if ($crang ne 'simple') {
|
||
$idx = $sabin.'.'.$crang;
|
||
}
|
||
|
||
if (defined ($data{$name}{circular}) &&
|
||
defined ($data{$name}{circular}{$hod}) &&
|
||
defined ($data{$name}{circular}{$hod}{pvcorrf}) &&
|
||
defined ($data{$name}{circular}{$hod}{pvcorrf}{$idx})) {
|
||
$corrf = $data{$name}{circular}{$hod}{pvcorrf}{$idx};
|
||
}
|
||
|
||
if (defined ($data{$name}{circular}) &&
|
||
defined ($data{$name}{circular}{$hod}) &&
|
||
defined ($data{$name}{circular}{$hod}{quality}) &&
|
||
defined ($data{$name}{circular}{$hod}{quality}{$idx})) {
|
||
$qual = $data{$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 or $name, $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 $name = shift;
|
||
my $hod = shift;
|
||
my $sabin = shift;
|
||
my $crang = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
my $pvrlsum = $def;
|
||
my $pvfcsum = $def;
|
||
my $dnumsum = $def;
|
||
my $idx = 'simple';
|
||
|
||
if ($crang ne 'simple') {
|
||
$idx = $sabin.'.'.$crang;
|
||
}
|
||
|
||
if (defined ($data{$name}{circular}) &&
|
||
defined ($data{$name}{circular}{$hod}) &&
|
||
defined ($data{$name}{circular}{$hod}{pvrlsum}) &&
|
||
defined ($data{$name}{circular}{$hod}{pvrlsum}{$idx})) {
|
||
$pvrlsum = $data{$name}{circular}{$hod}{pvrlsum}{$idx};
|
||
}
|
||
|
||
if (defined ($data{$name}{circular}) &&
|
||
defined ($data{$name}{circular}{$hod}) &&
|
||
defined ($data{$name}{circular}{$hod}{pvfcsum}) &&
|
||
defined ($data{$name}{circular}{$hod}{pvfcsum}{$idx})) {
|
||
$pvfcsum = $data{$name}{circular}{$hod}{pvfcsum}{$idx};
|
||
}
|
||
|
||
if (defined ($data{$name}{circular}) &&
|
||
defined ($data{$name}{circular}{$hod}) &&
|
||
defined ($data{$name}{circular}{$hod}{dnumsum}) &&
|
||
defined ($data{$name}{circular}{$hod}{dnumsum}{$idx})) {
|
||
$dnumsum = $data{$name}{circular}{$hod}{dnumsum}{$idx};
|
||
}
|
||
|
||
return ($pvrlsum, $pvfcsum, $dnumsum);
|
||
}
|
||
|
||
#########################################################################################
|
||
# Wert des nexthours-Hash zurückliefern
|
||
# Usage:
|
||
# NexthoursVal ($hash or $name, $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
|
||
# wcc - DWD Wolkendichte
|
||
# cloudrange - berechnete Bewölkungsrange
|
||
# rr1c - 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 $name = shift;
|
||
my $nhr = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined ($data{$name}{nexthours}) &&
|
||
defined ($data{$name}{nexthours}{$nhr}) &&
|
||
defined ($data{$name}{nexthours}{$nhr}{$key})) {
|
||
return $data{$name}{nexthours}{$nhr}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
###################################################################################################
|
||
# Wert des current-Hash zurückliefern
|
||
# Usage:
|
||
# CurrentVal ($hash or $name, $key, $def)
|
||
#
|
||
# $key: aiinitstate - Initialisierungsstatus der KI
|
||
# aitrainstate - Traisningsstatus der KI
|
||
# aiaddistate - Add Instanz Status der KI
|
||
# 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)
|
||
# surplusslidereg - Schieberegister PV Überschuß (Array)
|
||
# moonPhaseI - aktuelle Mondphase (1 .. 8)
|
||
# batsocslidereg - Schieberegister Batterie SOC (Array)
|
||
# consumption - aktueller Verbrauch (W)
|
||
# consumerdevs - alle registrierten Consumerdevices (Array)
|
||
# consumerCollected - Statusbit Consumer Attr gesammelt und ausgewertet
|
||
# gridconsumption - aktueller Netzbezug
|
||
# temp - aktuelle Außentemperatur
|
||
# surplus - aktueller PV Überschuß
|
||
# tomorrowconsumption - Verbrauch des kommenden Tages
|
||
# 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 $name = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined $data{$name}{current} &&
|
||
defined $data{$name}{current}{$key}) {
|
||
return $data{$name}{current}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
###################################################################################################
|
||
# Wert des String Hash zurückliefern
|
||
# Usage:
|
||
# StringVal ($hash or $name, $strg, $key, $def)
|
||
#
|
||
# $strg: - Name des Strings aus setupInverterStrings
|
||
# $key: peak - Peakleistung aus setupStringPeak
|
||
# tilt - Neigungswinkel der Module aus setupStringDeclination
|
||
# 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 $name = shift;
|
||
my $strg = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined $data{$name}{strings} &&
|
||
defined $data{$name}{strings}{$strg} &&
|
||
defined $data{$name}{strings}{$strg}{$key}) {
|
||
return $data{$name}{strings}{$strg}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
###################################################################################################
|
||
# Wert AI::DecisionTree Objects zurückliefern
|
||
# Usage:
|
||
# AiDetreeVal ($hash or $name, key, $def)
|
||
#
|
||
# key: object - das AI Object
|
||
# aitrained - AI trainierte Daten
|
||
# airaw - Rohdaten für AI Input = Raw Trainigsdaten
|
||
#
|
||
# $def: Defaultwert
|
||
#
|
||
###################################################################################################
|
||
sub AiDetreeVal {
|
||
my $name = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined $data{$name}{aidectree} &&
|
||
defined $data{$name}{aidectree}{$key}) {
|
||
return $data{$name}{aidectree}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
###################################################################################################
|
||
# Wert AI Raw Data zurückliefern
|
||
# Usage:
|
||
# AiRawdataVal ($hash or $name, $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 $name = shift;
|
||
my $idx = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (!$idx && !$key) {
|
||
if (defined $data{$name}{aidectree}{airaw}) {
|
||
return $data{$name}{aidectree}{airaw};
|
||
}
|
||
}
|
||
|
||
if (defined $data{$name}{aidectree}{airaw} &&
|
||
defined $data{$name}{aidectree}{airaw}{$idx} &&
|
||
defined $data{$name}{aidectree}{airaw}{$idx}{$key}) {
|
||
return $data{$name}{aidectree}{airaw}{$idx}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
###################################################################################################################
|
||
# Wert des consumer-Hash zurückliefern
|
||
# Usage:
|
||
# ConsumerVal ($hash or $name, $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
|
||
# logoffon - logischer 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
|
||
# runtimeAvgDay - durchschnittliche 'On'-Zeit an einem Tag (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 $name = shift;
|
||
my $co = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined $data{$name}{consumers} &&
|
||
defined $data{$name}{consumers}{$co} &&
|
||
defined $data{$name}{consumers}{$co}{$key}) {
|
||
return $data{$name}{consumers}{$co}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
###################################################################################################
|
||
# Wert des Batterie-Hash zurückliefern
|
||
# Usage:
|
||
# BatteryVal ($hash or $name, $bn, $key, $def)
|
||
#
|
||
# $bn: Batterie Nummer (01,02,03,...)
|
||
# $key: balias - Alias des Batterie Devices
|
||
# bname - Name des Batterie Devices
|
||
# basynchron - Asynchron Modus
|
||
# bcharge - Bat SOC in %
|
||
# binstcap - installierte Batteriekapazität in Wh
|
||
# bpowerin - Batterie momentane Ladeleistung
|
||
# bpowerout - Batterie momentane Entladeleistung
|
||
# bicon - Batterie Icons
|
||
# bshowingraph - Batterie in Balkengrafik anzeigen
|
||
#
|
||
# $def: Defaultwert
|
||
#
|
||
###################################################################################################
|
||
sub BatteryVal {
|
||
my $name = shift;
|
||
my $bn = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined $data{$name}{batteries} &&
|
||
defined $data{$name}{batteries}{$bn} &&
|
||
defined $data{$name}{batteries}{$bn}{$key}) {
|
||
return $data{$name}{batteries}{$bn}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
###################################################################################################
|
||
# Wert des Inverter-Hash zurückliefern
|
||
# Usage:
|
||
# InverterVal ($hash or $name, $in, $key, $def)
|
||
#
|
||
# $in: Inverter Nummer (01,02,03,...)
|
||
# $key: ietotal - Stand etotal des WR
|
||
# igeneration - aktuelle PV Erzeugung Inverter
|
||
# invertercap - Bemessungsleistung der Wechselrichters (max. W)
|
||
# iname - Name des Inverterdevices
|
||
# iicon - Icon des Inverters
|
||
# ialias - Alias des Inverters
|
||
#
|
||
# $def: Defaultwert
|
||
#
|
||
###################################################################################################
|
||
sub InverterVal {
|
||
my $name = shift;
|
||
my $in = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined $data{$name}{inverters} &&
|
||
defined $data{$name}{inverters}{$in} &&
|
||
defined $data{$name}{inverters}{$in}{$key}) {
|
||
return $data{$name}{inverters}{$in}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
###################################################################################################
|
||
# Wert des non-PV Producer-Hash zurückliefern
|
||
# Usage:
|
||
# ProducerVal ($hash or $name, $pn, $key, $def)
|
||
#
|
||
# $pn: Producer Nummer (01,02,03,...)
|
||
# $key: petotal - Stand etotal des Producers
|
||
# pgeneration - aktuelle Erzeugung Producers
|
||
# pname - Name des Producersdevices
|
||
# picon - Icon des Producers
|
||
# palias - Alias des Producers
|
||
#
|
||
# $def: Defaultwert
|
||
#
|
||
###################################################################################################
|
||
sub ProducerVal {
|
||
my $name = shift;
|
||
my $pn = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined $data{$name}{producers} &&
|
||
defined $data{$name}{producers}{$pn} &&
|
||
defined $data{$name}{producers}{$pn}{$key}) {
|
||
return $data{$name}{producers}{$pn}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
##########################################################################################################################################################
|
||
# Wert des solcastapi-Hash zurückliefern
|
||
# Usage:
|
||
# RadiationAPIVal ($hash or $name, $tring, $ststr, $key, $def)
|
||
#
|
||
# $tring: Stringname aus "setupInverterStrings" (?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:
|
||
# RadiationAPIVal ($hash, '?All', $ststr, 'Rad1h', $def) - Globalstrahlung mit Startzeit ohne Stringbezug
|
||
##########################################################################################################################################################
|
||
sub RadiationAPIVal {
|
||
my $name = shift;
|
||
my $string = shift;
|
||
my $ststr = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined $data{$name}{solcastapi} &&
|
||
defined $data{$name}{solcastapi}{$string} &&
|
||
defined $data{$name}{solcastapi}{$string}{$ststr} &&
|
||
defined $data{$name}{solcastapi}{$string}{$ststr}{$key}) {
|
||
return $data{$name}{solcastapi}{$string}{$ststr}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
##########################################################################################################################################################
|
||
# Wert des weatherAPI-Hash zurückliefern
|
||
# Usage:
|
||
# WeatherAPIVal ($hash or $name, $apiname, $tstr, $key, $def)
|
||
#
|
||
# $apiname: Hauptname der API gemäß setupWeatherDevX (z.B. OpenMeteo)
|
||
# $tstr: Zeitwert der Form fcX_XX (z.B. fc1_19)
|
||
# $key: Parameter (z.B. don, neff, rr1c, ...)
|
||
# $def: Defaultwert
|
||
#
|
||
##########################################################################################################################################################
|
||
sub WeatherAPIVal {
|
||
my $name = shift;
|
||
my $apiname = shift;
|
||
my $tstr = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined $data{$name}{weatherapi} &&
|
||
defined $data{$name}{weatherapi}{$apiname} &&
|
||
defined $data{$name}{weatherapi}{$apiname}{$tstr} &&
|
||
defined $data{$name}{weatherapi}{$apiname}{$tstr}{$key}) {
|
||
return $data{$name}{weatherapi}{$apiname}{$tstr}{$key};
|
||
}
|
||
|
||
return $def;
|
||
}
|
||
|
||
##########################################################################################################################################################
|
||
# Wert des StatusAPI-Hash zurückliefern
|
||
# Usage:
|
||
# StatusAPIVal ($hash or $name, $apiname, '?All', $key, $def)
|
||
#
|
||
# $apiname: Hauptname der API gemäß setupWeatherDevX (z.B. OpenMeteo)
|
||
# $tstr: default '?All'
|
||
# $key: Parameter
|
||
# $def: Defaultwert
|
||
#
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'lastretrieval_time', $def) - letzte Abfrage Zeitstring
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'lastretrieval_timestamp', $def) - letzte Abfrage Unix Timestamp
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'todayDoneAPIrequests', $def) - heute ausgeführte API Requests
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'todayDoneAPIcalls', $def) - heute ausgeführte API Calls (hat u.U. mehrere Requests)
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'todayRemainingAPIrequests $def) - heute verbleibende API Requests
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'todayRemainingAPIcalls', $def) - heute noch mögliche API Calls (ungl. Requests !)
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'currentAPIinterval', $def) - aktuelles API Request Intervall
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'response_message', $def) - letzte API Antwort
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'place', $def) - ForecastSolarAPI -> Location der Anlage
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'requests_limit', $def) - ForecastSolarAPI -> Request Limit innerhalb der Periode
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'requests_limit_period', $def) - ForecastSolarAPI -> Periode für Request Limit
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'requests_remaining', $def) - ForecastSolarAPI -> verbleibende Requests innerhalb der laufenden Periode
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'response_code', $def) - ForecastSolarAPI -> letzter Antwortcode
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'retryat_time', $def) - ForecastSolarAPI -> Zwangsverzögerung des nächsten Calls bis Uhrzeit
|
||
# StatusAPIVal ($hash, $apiname, '?All', 'retryat_timestamp', $def) - ForecastSolarAPI -> Zwangsverzögerung des nächsten Calls bis UNIX-Zeitstempel
|
||
#
|
||
# Sonderabfragen:
|
||
# StatusAPIVal ($hash, '?IdPair', '?<pk>', 'rtid', $def) - SolCast RoofTop-ID, <pk> = Paarschlüssel
|
||
# StatusAPIVal ($hash, '?IdPair', '?<pk>', 'apikey', $def) - SolCast API-Key, <pk> = Paarschlüssel
|
||
##########################################################################################################################################################
|
||
sub StatusAPIVal {
|
||
my $name = shift;
|
||
my $apiname = shift;
|
||
my $tstr = shift;
|
||
my $key = shift;
|
||
my $def = shift;
|
||
|
||
if (ref $name eq 'HASH') {
|
||
$name = $name->{NAME};
|
||
}
|
||
|
||
if (defined $data{$name}{statusapi} &&
|
||
defined $data{$name}{statusapi}{$apiname} &&
|
||
defined $data{$name}{statusapi}{$apiname}{$tstr} &&
|
||
defined $data{$name}{statusapi}{$apiname}{$tstr}{$key}) {
|
||
return $data{$name}{statusapi}{$apiname}{$tstr}{$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
|
||
|
||
<a id="SolarForecast"></a>
|
||
<h3>SolarForecast</h3>
|
||
<br>
|
||
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. <br>
|
||
|
||
To create the solar forecast, the SolarForecast module can use different services and sources: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>DWD</b> </td><td>solar forecast based on MOSMIX data of the German Weather Service </td></tr>
|
||
<tr><td> <b>SolCast-API </b> </td><td>uses forecast data of the <a href='https://toolkit.solcast.com.au/rooftop-sites/' target='_blank'>SolCast API</a> </td></tr>
|
||
<tr><td> <b>ForecastSolar-API</b> </td><td>uses forecast data of the <a href='https://doc.forecast.solar/api' target='_blank'>Forecast.Solar API</a> </td></tr>
|
||
<tr><td> <b>OpenMeteoDWD-API</b> </td><td>ICON weather models of the German Weather Service (DWD) via <a href='https://open-meteo.com/en/docs/dwd-api' target='_blank'>Open-Meteo</a> </td></tr>
|
||
<tr><td> <b>OpenMeteoDWDEnsemble-API</b> </td><td>Access to the <a href='https://www.dwd.de/DE/forschung/wettervorhersage/num_modellierung/04_ensemble_methoden/ensemble_vorhersage/ensemble_vorhersagen.html' target='_blank'>global ensemble forecast system (EPS)</a> of the DWD </td></tr>
|
||
<tr><td> <b>OpenMeteoWorld-API</b> </td><td>Seamlessly combines weather models from organizations such as NOAA, DWD, CMCC and ECMWF via <a href='https://open-meteo.com/en/docs' target='_blank'>Open-Meteo</a> </td></tr>
|
||
<tr><td> <b>VictronKI-API</b> </td><td>Victron Energy API of the <a href='https://www.victronenergy.com/blog/2023/07/05/new-vrm-solar-production-forecast-feature/' target='_blank'>VRM Portal</a> </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
The use of the mentioned API's is limited to the respective free version of the selected service. <br>
|
||
AI support can be activated depending on the model used. <br><br>
|
||
|
||
In addition to the PV generation forecast, consumption values or grid reference values are recorded and used for a
|
||
consumption forecast. <br>
|
||
The module calculates a future energy surplus from the forecast values, which is used to plan the operation of consumers.
|
||
Furthermore, the module offers <a href="#SolarForecast-Consumer">Consumer Integration</a> for integrated
|
||
planning and control of PV surplus dependent consumer circuits. Support for optimum battery
|
||
SoC management is also part of the range of functions. <br><br>
|
||
|
||
At the first definition of the module the user is supported by a Guided Procedure to make all initial entries. <br>
|
||
At the end of the process and after relevant changes to the system or device configuration, it is essential to perform a
|
||
<a href="#SolarForecast-set-plantConfiguration">set <name> plantConfiguration ceck</a>
|
||
to ensure that the system configuration is correct.
|
||
|
||
<ul>
|
||
<a id="SolarForecast-define"></a>
|
||
<b>Define</b>
|
||
<br><br>
|
||
|
||
<ul>
|
||
A SolarForecast Device is created with: <br><br>
|
||
|
||
<ul>
|
||
<b>define <name> SolarForecast </b>
|
||
</ul>
|
||
<br>
|
||
|
||
After the definition of the device, depending on the forecast sources used, it is mandatory to store additional
|
||
plant-specific information. <br>
|
||
The following set commands and attributes are used to store information that is relevant for the function of the
|
||
module: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>setupWeatherDevX</b> </td><td>DWD_OpenData Device which provides meteorological data (e.g. cloud cover) </td></tr>
|
||
<tr><td> <b>setupRadiationAPI </b> </td><td>DWD_OpenData Device or API for the delivery of radiation data. </td></tr>
|
||
<tr><td> <b>setupInverterDevXX</b> </td><td>Device which provides PV performance data </td></tr>
|
||
<tr><td> <b>setupMeterDev</b> </td><td>Device which supplies network I/O data </td></tr>
|
||
<tr><td> <b>setupBatteryDevXX</b> </td><td>Device which provides battery performance data (if available) </td></tr>
|
||
<tr><td> <b>setupInverterStrings</b> </td><td>Identifier of the existing plant strings </td></tr>
|
||
<tr><td> <b>setupStringAzimuth</b> </td><td>Azimuth of the plant strings </td></tr>
|
||
<tr><td> <b>setupStringPeak</b> </td><td>the DC peak power of the plant strings </td></tr>
|
||
<tr><td> <b>roofIdentPair</b> </td><td>the identification data (when using the SolCast API) </td></tr>
|
||
<tr><td> <b>setupRoofTops</b> </td><td>the Rooftop parameters (when using the SolCast API) </td></tr>
|
||
<tr><td> <b>setupStringDeclination</b> </td><td>the angle of inclination of the plant modules </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
In order to enable an adjustment to the personal system, correction factors can be manually fixed or automatically
|
||
applied dynamically.
|
||
<br><br>
|
||
</ul>
|
||
|
||
<a id="SolarForecast-Consumer"></a>
|
||
<b>Consumer Integration</b>
|
||
<br><br>
|
||
|
||
<ul>
|
||
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
|
||
<a href="#SolarForecast-attr-consumer">ConsumerXX attributes</a>. 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. <br>
|
||
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.
|
||
<br><br>
|
||
|
||
A dummy device according to this pattern is suitable for this purpose:
|
||
<br><br>
|
||
|
||
<ul>
|
||
define SolCastDummy dummy <br>
|
||
attr SolCastDummy userattr nomPower <br>
|
||
attr SolCastDummy alias SolarForecast Consumer Dummy <br>
|
||
attr SolCastDummy cmdIcon on:remotecontrol/black_btn_GREEN off:remotecontrol/black_btn_RED <br>
|
||
attr SolCastDummy devStateIcon off:light_light_dim_100@grey on:light_light_dim_100@darkorange <br>
|
||
attr SolCastDummy group Solarforecast <br>
|
||
attr SolCastDummy icon solar_icon <br>
|
||
attr SolCastDummy nomPower 1000 <br>
|
||
attr SolCastDummy readingList BatIn BatOut BatVal BatInTot BatOutTot bezW einW Batcharge Temp automatic <br>
|
||
attr SolCastDummy room Energy,Testroom <br>
|
||
attr SolCastDummy setList BatIn BatOut BatVal BatInTot BatOutTot bezW einW Batcharge on off Temp <br>
|
||
attr SolCastDummy userReadings actpow {ReadingsVal ($name, 'state', 'off') eq 'on' ? AttrVal ($name, 'nomPower', 100) : 0} <br>
|
||
</ul>
|
||
|
||
<br><br>
|
||
</ul>
|
||
|
||
<a id="SolarForecast-set"></a>
|
||
<b>Set</b>
|
||
<ul>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-aiDecTree"></a>
|
||
<li><b>aiDecTree </b> <br><br>
|
||
|
||
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.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="10%"> <col width="90%"> </colgroup>
|
||
<tr><td> <b>addInstances</b> </td><td>- The AI is enriched with the currently available PV, radiation and environmental data. </td></tr>
|
||
<tr><td> <b>addRawData</b> </td><td>- Relevant PV, radiation and environmental data are extracted and stored for later use. </td></tr>
|
||
<tr><td> <b>train</b> </td><td>- The AI is trained with the available data. </td></tr>
|
||
<tr><td> </td><td> Successfully generated decision data is stored in the file system. </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-batteryTrigger"></a>
|
||
<li><b>batteryTrigger <1on>=<Value> <1off>=<Value> [<2on>=<Value> <2off>=<Value> ...] </b> <br><br>
|
||
|
||
Generates triggers when the battery charge exceeds or falls below certain values (SoC in %). <br>
|
||
The SoC used is formed as the resulting SoC (sum of the current charge of all battery devices in relation to the total installed
|
||
total capacity), i.e. all batteries are considered as one cluster. <br>
|
||
If the last three SoC measurements exceed a defined <b>Xon-Bedingung</b>, the reading <b>batteryTrigger_X = on</b>
|
||
is created/set. <br>
|
||
If the last three SoC measurements fall below a defined <b>Xoff-Bedingung</b>, the reading
|
||
<b>batteryTrigger_X = off</b> is created/set. <br>
|
||
Any number of trigger conditions can be specified. Xon/Xoff conditions do not necessarily have to be defined in pairs.
|
||
<br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
set <name> batteryTrigger 1on=30 1off=10 2on=70 2off=20 3on=15 4off=90<br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-consumerNewPlanning"></a>
|
||
<li><b>consumerNewPlanning <Consumer number> </b> <br><br>
|
||
|
||
The existing planning of the specified consumer is deleted. <br>
|
||
The new planning is carried out immediately, taking into account the parameters set in the consumerXX attribute.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
set <name> consumerNewPlanning 01 <br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-consumerImmediatePlanning"></a>
|
||
<li><b>consumerImmediatePlanning <Consumer number> </b> <br><br>
|
||
|
||
Immediate switching on of the consumer at the current time is scheduled.
|
||
Any keys <b>notbefore</b>, <b>notafter</b> respectively <b>mode</b> set in the consumerXX attribute are ignored <br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
set <name> consumerImmediatePlanning 01 <br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-energyH4Trigger"></a>
|
||
<li><b>energyH4Trigger <1on>=<Value> <1off>=<Value> [<2on>=<Value> <2off>=<Value> ...] </b> <br><br>
|
||
|
||
Generates triggers on exceeding or falling below the 4-hour PV forecast (NextHours_Sum04_PVforecast). <br>
|
||
Überschreiten die letzten drei Messungen der 4-Stunden PV Vorhersagen eine definierte <b>Xon-Bedingung</b>, wird das Reading
|
||
<b>energyH4Trigger_X = on</b> erstellt/gesetzt.
|
||
If the last three measurements of the 4-hour PV predictions exceed a defined <b>Xon condition</b>,
|
||
the Reading <b>energyH4Trigger_X = off</b> is created/set. <br>
|
||
Any number of trigger conditions can be specified.
|
||
Xon/Xoff conditions do not necessarily have to be defined in pairs. <br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
set <name> energyH4Trigger 1on=2000 1off=1700 2on=2500 2off=2000 3off=1500 <br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-setupStringAzimuth"></a>
|
||
<li><b>setupStringAzimuth <Stringname1>=<dir> [<Stringname2>=<dir> <Stringname3>=<dir> ...] </b> <br><br>
|
||
|
||
Alignment <dir> of the solar modules in the string "StringnameX". The string name is a key value of the
|
||
<b>setupInverterStrings</b> attribute. <br>
|
||
The direction specification <dir> can be specified as an azimuth identifier or as an azimuth value: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="30%"> <col width="20%"> <col width="50%"> </colgroup>
|
||
<tr><td> <b>Identifier</b></td><td><b>Azimuth</b></td><td> </td></tr>
|
||
<tr><td> N </td><td>-180 </td><td>North orientation </td></tr>
|
||
<tr><td> NE </td><td>-135 </td><td>North-East orientation </td></tr>
|
||
<tr><td> E </td><td>-90 </td><td>East orientation </td></tr>
|
||
<tr><td> SE </td><td>-45 </td><td>South-east orientation </td></tr>
|
||
<tr><td> S </td><td>0 </td><td>South orientation </td></tr>
|
||
<tr><td> SW </td><td>45 </td><td>South-west orientation </td></tr>
|
||
<tr><td> W </td><td>90 </td><td>West orientation </td></tr>
|
||
<tr><td> NW </td><td>135 </td><td>North-West orientation </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
Azimuth values are integers in the range -180 to 180. Although the specified identifiers can be used,
|
||
it is recommended to specify the exact azimuth value in the attribute. This allows any intermediate values such
|
||
as 83, 48 etc. to be specified.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
set <name> setupStringAzimuth Ostdach=-85 Südgarage=S S3=132 <br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-setupStringDeclination"></a>
|
||
<li><b>setupStringDeclination <Stringname1>=<Angle> [<Stringname2>=<Angle> <Stringname3>=<Angle> ...] </b> <br><br>
|
||
|
||
Tilt angle of the solar modules. The string name is a key value of the attribute <b>setupInverterStrings</b>. <br>
|
||
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). <br><br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
set <name> setupStringDeclination eastroof=40 southgarage=60 S3=30 <br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-operatingMemory"></a>
|
||
<li><b>operatingMemory backup | save | recover-<File> </b> <br><br>
|
||
|
||
The pvHistory (PVH) and pvCircular (PVC) components of the internal cache database are stored in the file system. <br>
|
||
The target directory is "../FHEM/FhemUtils". This process is carried out regularly by the module in the background. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="17%"> <col width="83%"> </colgroup>
|
||
<tr><td> <b>backup</b> </td><td>Saves the active in-memory structures with the current timestamp. </td></tr>
|
||
<tr><td> </td><td><a href="#SolarForecast-attr-ctrlBackupFilesKeep">ctrlBackupFilesKeep</a> generations of the files are saved. Older versions are deleted. </td></tr>
|
||
<tr><td> </td><td>Files: PVH_SolarForecast_<name>_<Timestamp>, PVC_SolarForecast_<name>_<Timestamp> </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>save</b> </td><td>The active in-memory structures are saved. </td></tr>
|
||
<tr><td> </td><td>Files: PVH_SolarForecast_<name>, PVC_SolarForecast_<name> </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>recover-<File></b> </td><td>Restores the data of the selected backup file as an active in-memory structure. </td></tr>
|
||
<tr><td> </td><td>To avoid inconsistencies, the PVH.* and PVC.* files should be restored in pairs </td></tr>
|
||
<tr><td> </td><td>with the same time stamp. </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-operationMode"></a>
|
||
<li><b>operationMode </b> <br><br>
|
||
The SolarForecast device is deactivated with <b>inactive</b>. The <b>active</b> 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.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-plantConfiguration"></a>
|
||
<li><b>plantConfiguration </b> <br><br>
|
||
|
||
Depending on the selected command option, the following operations are performed: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>check</b> </td><td>Checks the current plant configuration. A plausibility check </td></tr>
|
||
<tr><td> </td><td>is performed and the result and any notes or errors are output. </td></tr>
|
||
<tr><td> <b>save</b> </td><td>Secures important parameters of the plant configuration. </td></tr>
|
||
<tr><td> </td><td>The operation is performed automatically every day shortly after 00:00. </td></tr>
|
||
<tr><td> <b>restore</b> </td><td>Restores a saved plant configuration </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-powerTrigger"></a>
|
||
<li><b>powerTrigger <1on>=<Value> <1off>=<Value> [<2on>=<Value> <2off>=<Value> ...] </b> <br><br>
|
||
|
||
Generates triggers when certain PV generation values (Current_PV) are exceeded or not reached. <br>
|
||
If the last three measurements of PV generation exceed a defined <b>Xon condition</b>, the Reading
|
||
<b>powerTrigger_X = on</b> is created/set.
|
||
If the last three measurements of the PV generation fall below a defined <b>Xoff-Bedingung</b>, the Reading
|
||
<b>powerTrigger_X = off</b> is created/set.
|
||
<br>
|
||
Any number of trigger conditions can be specified. Xon/Xoff conditions do not necessarily have to be defined in pairs.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
set <name> powerTrigger 1on=1000 1off=500 2on=2000 2off=1000 3on=1600 4off=1100<br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-pvCorrectionFactor_Auto"></a>
|
||
<li><b>pvCorrectionFactor_Auto </b> <br><br>
|
||
|
||
Switches the automatic prediction correction on/off.
|
||
The mode of operation differs depending on the selected method. <br>
|
||
(default: off)
|
||
<br><br>
|
||
|
||
<b>on_simple(_ai):</b> <br>
|
||
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
|
||
<b>not</b> additionally related to other conditions such as cloud cover or temperatures. <br>
|
||
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.
|
||
<br><br>
|
||
|
||
<b>on_complex(_ai):</b> <br>
|
||
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. <br>
|
||
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.
|
||
<br><br>
|
||
|
||
<b>Note:</b> The automatic prediction correction is learning and needs time to optimise the correction values.
|
||
After activation, optimal predictions cannot be expected immediately!
|
||
<br><br>
|
||
|
||
Below are some API-specific tips that are merely best practice recommendations.
|
||
<br><br>
|
||
|
||
<b>Model OpenMeteo...API:</b> <br>
|
||
The recommended autocorrection method is <b>on_complex</b> or <b>on_complex_ai</b>.
|
||
<br><br>
|
||
|
||
<b>Model SolCastAPI:</b> <br>
|
||
The recommended autocorrection method is <b>on_complex</b>. <br>
|
||
Before turning on autocorrection, optimise the forecast with the following steps: <br><br>
|
||
<ul>
|
||
<li>
|
||
In the RoofTop editor of the SolCast API, define the
|
||
<a href="https://articles.solcast.com.au/en/articles/2959798-what-is-the-efficiency-factor?_ga=2.119610952.1991905456.1665567573-1390691316.1665567573"><b>efficiency factor</b></a>
|
||
according to the age of the plant. <br>
|
||
With an 8-year-old plant, it would be 84 (100 - (8 x 2%)). <br>
|
||
</li>
|
||
<li>
|
||
after sunset, the Reading Today_PVdeviation is created, which shows the deviation between the forecast and the real
|
||
PV generation in percent.
|
||
</li>
|
||
</li>
|
||
<li>
|
||
according to the deviation, adjust the efficiency factor in steps until an optimum is found, i.e. the smallest
|
||
daily deviation is found
|
||
</li>
|
||
<li>
|
||
If you think you have found the optimal setting, you can set pvCorrectionFactor_Auto on*.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
Ideally, this process is carried out in a phase of stable meteorological conditions (uniform sun or cloud cover).
|
||
cloud cover).
|
||
<br><br>
|
||
|
||
<b>Model VictronKiAPI:</b> <br>
|
||
This model is based on Victron Energy's AI-supported API.
|
||
The recommended autocorrect method is <b>on_complex</b>. <br><br>
|
||
|
||
<b>Model DWD:</b> <br>
|
||
The recommended autocorrect method is <b>on_complex</b> or <b>on_complex_ai</b>. <br><br>
|
||
|
||
<b>Model ForecastSolarAPI:</b> <br>
|
||
The recommended autocorrect method is <b>on_complex</b>.
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-pvCorrectionFactor_" data-pattern="pvCorrectionFactor_.*"></a>
|
||
<li><b>pvCorrectionFactor_XX <Zahl> </b> <br><br>
|
||
|
||
Manual correction factor for hour XX of the day. <br>
|
||
(default: 1.0) <br><br>
|
||
|
||
Depending on the setting <a href="#SolarForecast-set-pvCorrectionFactor_Auto">pvCorrectionFactor_Auto </a> ('off' or 'on_.*'),
|
||
a static or dynamic default setting is made: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="10%"> <col width="90%"> </colgroup>
|
||
<tr><td> <b>off</b> </td><td>The set correction factor is not overwritten by the auto-correction. </td></tr>
|
||
<tr><td> </td><td>In the pvCorrectionFactor_XX reading, the status is signaled by the addition 'manual fix'. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>on_.*</b> </td><td>The set correction factor is overwritten by the auto-correction or AI </td></tr>
|
||
<tr><td> </td><td>if a calculated correction value is available in the system. </td></tr>
|
||
<tr><td> </td><td>In the pvCorrectionFactor_XX reading, the status is signaled by the addition 'manual flex'. </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-reset"></a>
|
||
<li><b>reset </b> <br><br>
|
||
|
||
Deletes the data source selected from the drop-down list, readings belonging to the function or other internal
|
||
data structures. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>aiData</b> </td><td>deletes an existing AI instance including all training data and reinitialises it </td></tr>
|
||
<tr><td> <b>batteryTriggerSet</b> </td><td>deletes the trigger points of the battery storage </td></tr>
|
||
<tr><td> <b>consumerPlanning</b> </td><td>deletes the planning data of all registered consumers </td></tr>
|
||
<tr><td> </td><td>To delete the planning data of only one consumer, use: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset consumerPlanning <Consumer number> </ul> </td></tr>
|
||
<tr><td> </td><td>The module carries out an automatic rescheduling of the consumer circuit. </td></tr>
|
||
<tr><td> <b>consumerMaster</b> </td><td>deletes the current and historical data of all registered consumers from the memory </td></tr>
|
||
<tr><td> </td><td>The defined consumer attributes remain and the data is collected again. </td></tr>
|
||
<tr><td> </td><td>To delete the data of only one consumer use: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset consumerMaster <Consumer number> </ul> </td></tr>
|
||
<tr><td> <b>consumption</b> </td><td>deletes the stored consumption values of the house </td></tr>
|
||
<tr><td> </td><td>To delete the consumption values of a specific day: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset consumption <Day> (e.g. set <name> reset consumption 08) </ul> </td></tr>
|
||
<tr><td> </td><td>To delete the consumption values of a specific hour of a day: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset consumption <Day> <Hour> (e.g. set <name> reset consumption 08 10) </ul> </td></tr>
|
||
<tr><td> <b>energyH4TriggerSet</b> </td><td>deletes the 4-hour energy trigger points </td></tr>
|
||
<tr><td> <b>powerTriggerSet</b> </td><td>deletes the trigger points for PV generation values </td></tr>
|
||
<tr><td> <b>pvCorrection</b> </td><td>deletes the readings pvCorrectionFactor* </td></tr>
|
||
<tr><td> </td><td>To delete all previously stored PV correction factors from the caches: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset pvCorrection cached </ul> </td></tr>
|
||
<tr><td> </td><td>To delete stored PV correction factors of a certain hour from the caches: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset pvCorrection cached <Hour> </ul> </td></tr>
|
||
<tr><td> </td><td><ul>(e.g. set <name> reset pvCorrection cached 10) </ul> </td></tr>
|
||
<tr><td> <b>pvHistory</b> </td><td>deletes the memory of all historical days (01 ... 31) </td></tr>
|
||
<tr><td> </td><td>To delete a specific historical day: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset pvHistory <Day> (e.g. set <name> reset pvHistory 08) </ul> </td></tr>
|
||
<tr><td> </td><td>To delete a specific hour of a historical day: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset pvHistory <Day> <Hour> (e.g. set <name> reset pvHistory 08 10) </ul> </td></tr>
|
||
<tr><td> <b>roofIdentPair</b> </td><td>deletes all saved SolCast API Rooftop ID / API Key pairs. </td></tr>
|
||
<tr><td> </td><td>To delete a specific pair, specify its key <pk>: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset roofIdentPair <pk> (e.g. set <name> reset roofIdentPair p1) </ul> </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-roofIdentPair"></a>
|
||
<li><b>roofIdentPair <pk> rtid=<Rooftop-ID> apikey=<SolCast API Key> </b> <br>
|
||
(only when using Model SolCastAPI) <br><br>
|
||
|
||
The retrieval of each rooftop created in
|
||
<a href='https://toolkit.solcast.com.au/rooftop-sites' target='_blank'>SolCast Rooftop Sites</a> is to be identified
|
||
by specifying a pair <b>Rooftop-ID</b> and <b>API-Key</b>. <br>
|
||
The key <pk> uniquely identifies a linked Rooftop ID / API key pair. Any number of pairs can be created
|
||
<b>one after the other</b>. In that case, a new name for "<pk>" is to be used in each case.
|
||
<br><br>
|
||
|
||
The key <pk> is assigned in the atribute <a href="#SolarForecast-attr-setupRoofTops">setupRoofTops</a> to the
|
||
Rooftops (=Strings) to be retrieved.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Examples: </b> <br>
|
||
set <name> roofIdentPair p1 rtid=92fc-6796-f574-ae5f apikey=oNHDbkKuC_eGEvZe7ECLl6-T1jLyfOgC <br>
|
||
set <name> roofIdentPair p2 rtid=f574-ae5f-92fc-6796 apikey=eGEvZe7ECLl6_T1jLyfOgC_oNHDbkKuC <br>
|
||
</ul>
|
||
|
||
<br>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-vrmCredentials"></a>
|
||
<li><b>vrmCredentials user=<Benutzer> pwd=<Paßwort> idsite=<idSite> </b> <br>
|
||
(only when using Model VictronKiAPI) <br><br>
|
||
|
||
If the Victron VRM API is used, the required access data must be stored with this set command. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="10%"> <col width="90%"> </colgroup>
|
||
<tr><td> <b>user</b> </td><td>Username for the Victron VRM Portal </td></tr>
|
||
<tr><td> <b>pwd</b> </td><td>Password for access to the Victron VRM Portal </td></tr>
|
||
<tr><td> <b>idsite</b> </td><td>idSite is the identifier "XXXXXX" in the Victron VRM Portal Dashboard URL. </td></tr>
|
||
<tr><td> </td><td>URL of the Victron VRM Dashboard: </td></tr>
|
||
<tr><td> </td><td>https://vrm.victronenergy.com/installation/<b>XXXXXX</b>/dashboard </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
To delete the stored credentials, only the argument <b>delete</b> must be passed to the command. <br><br>
|
||
|
||
<ul>
|
||
<b>Examples: </b> <br>
|
||
set <name> vrmCredentials user=john@example.com pwd=somepassword idsite=212008 <br>
|
||
set <name> vrmCredentials delete <br>
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
</ul>
|
||
<br>
|
||
|
||
<a id="SolarForecast-get"></a>
|
||
<b>Get</b>
|
||
<ul>
|
||
<ul>
|
||
<a id="SolarForecast-get-data"></a>
|
||
<li><b>data </b> <br><br>
|
||
Starts data collection to determine the solar forecast and other values.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-dwdCatalog"></a>
|
||
<li><b>dwdCatalog </b> <br><br>
|
||
The German Weather Service (DWD) provides a catalog of MOSMIX stations. <br>
|
||
The stations provide data whose meaning is explained in this
|
||
<a href='https://www.dwd.de/DE/leistungen/opendata/help/schluessel_datenformate/kml/mosmix_elemente_xls.html' target='_blank'>Overview</a>.
|
||
The DWD distinguishes between MOSMIX_L and MOSMIX_S stations, which differ in terms of update frequency
|
||
and data volume. <br>
|
||
This command reads the catalog into SolarForecast and saves it in the file
|
||
./FHEM/FhemUtils/DWDcat_SolarForecast. <br>
|
||
The catalog can be extensively filtered and saved in GPS Exchange Format (GPX).
|
||
The latitude and logitude coordinates are displayed in decimal degrees. <br>
|
||
Regex expressions in the corresponding keys are used for filtering. The Regex is enclosed in
|
||
^...$ for evaluation. <br>
|
||
The following parameters can be specified. Without parameters, the entire catalog is output: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>byID</b> </td><td>The output is sorted by station ID. (default) </td></tr>
|
||
<tr><td> <b>byName</b> </td><td>The output is sorted by station name. </td></tr>
|
||
<tr><td> <b>force</b> </td><td>The latest version of the DWD station catalog is loaded into the system. </td></tr>
|
||
<tr><td> <b>exportgpx</b> </td><td>The (filtered) stations are saved in the file ./FHEM/FhemUtils/DWDcat_SolarForecast.gpx. </td></tr>
|
||
<tr><td> </td><td>This file can be displayed in the <a href='https://www.j-berkemeier.de/ShowGPX.html' target='_blank'>GPX viewer</a>, for example. </td></tr>
|
||
<tr><td> <b>id=<Regex></b> </td><td>Filtering is carried out according to station ID. </td></tr>
|
||
<tr><td> <b>name=<Regex></b> </td><td>Filtering is carried out according to station name. </td></tr>
|
||
<tr><td> <b>lat=<Regex></b> </td><td>Filtering is carried out according to latitude. </td></tr>
|
||
<tr><td> <b>lon=<Regex></b> </td><td>Filtering is carried out according to longitude. </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
get <name> dwdCatalog byName name=ST.* exportgpx lat=(48|49|50|51|52)\..* lon=([5-9]|10|11|12|13|14|15)\..* <br>
|
||
# filters the stations largely to German locations beginning with "ST" and exports the data in GPS Exchange format
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-forecastQualities"></a>
|
||
<li><b>forecastQualities </b> <br><br>
|
||
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.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-ftuiFramefiles"></a>
|
||
<li><b>ftuiFramefiles </b> <br><br>
|
||
SolarForecast provides widgets for
|
||
<a href='https://wiki.fhem.de/wiki/FHEM_Tablet_UI' target='_blank'>FHEM Tablet UI v2 (FTUI2)</a>. <br>
|
||
If FTUI2 is installed on the system, the files for the framework can be loaded into the FTUI directory structure
|
||
with this command. <br>
|
||
The setup and use of the widgets is described in Wiki
|
||
<a href='https://wiki.fhem.de/wiki/SolarForecast_FTUI_Widget' target='_blank'>SolarForecast FTUI Widget</a>.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-html"></a>
|
||
<li><b>html </b> <br><br>
|
||
The SolarForecast graphic is retrieved and displayed as HTML code. <br>
|
||
<b>Note:</b> By the attribute <a href="#SolarForecast-attr-graphicHeaderOwnspec">graphicHeaderOwnspec</a>
|
||
generated set or attribute commands in the user-specific area of the header are generally hidden for technical
|
||
reasons. <br>
|
||
One of the following selections can be given as an argument to the command:
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="30%"> <col width="70%"> </colgroup>
|
||
<tr><td> <b>both</b> </td><td>displays the header, consumer legend, energy flow graph and forecast graph (default) </td></tr>
|
||
<tr><td> <b>both_noHead</b> </td><td>displays the consumer legend, energy flow graph and forecast graph </td></tr>
|
||
<tr><td> <b>both_noCons</b> </td><td>displays the header, energy flow and prediction graphic </td></tr>
|
||
<tr><td> <b>both_noHead_noCons</b> </td><td>displays energy flow and prediction graphs </td></tr>
|
||
<tr><td> <b>flow</b> </td><td>displays the header, the consumer legend and energy flow graphic </td></tr>
|
||
<tr><td> <b>flow_noHead</b> </td><td>displays the consumer legend and the energy flow graph </td></tr>
|
||
<tr><td> <b>flow_noCons</b> </td><td>displays the header and the energy flow graph </td></tr>
|
||
<tr><td> <b>flow_noHead_noCons</b> </td><td>displays the energy flow graph </td></tr>
|
||
<tr><td> <b>forecast</b> </td><td>displays the header, the consumer legend and the forecast graphic </td></tr>
|
||
<tr><td> <b>forecast_noHead</b> </td><td>displays the consumer legend and the forecast graph </td></tr>
|
||
<tr><td> <b>forecast_noCons</b> </td><td>displays the header and the forecast graphic </td></tr>
|
||
<tr><td> <b>forecast_noHead_noCons</b> </td><td>displays the forecast graph </td></tr>
|
||
<tr><td> <b>none</b> </td><td>displays only the header and the consumer legend </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
The graphic can be retrieved and embedded in your own code. This can be done in a simple way by defining
|
||
a weblink device: <br><br>
|
||
|
||
<ul>
|
||
define wl.SolCast5 weblink htmlCode { FHEM::SolarForecast::pageAsHtml ('SolCast5', '-', '<argument>') }
|
||
</ul>
|
||
<br>
|
||
'SolCast5' is the name of the SolarForecast device to be included. <b><argument></b> is one of the above
|
||
described selection options.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-nextHours"></a>
|
||
<li><b>nextHours </b> <br><br>
|
||
Lists the expected values for the coming hours. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>aihit</b> </td><td>delivery status of the AI for the PV forecast (0-no delivery, 1-delivery) </td></tr>
|
||
<tr><td> <b>confc</b> </td><td>expected energy consumption including the shares of registered consumers </td></tr>
|
||
<tr><td> <b>confcEx</b> </td><td>expected energy consumption without consumer shares with set key exconfc=1 </td></tr>
|
||
<tr><td> <b>crange</b> </td><td>calculated cloud area </td></tr>
|
||
<tr><td> <b>correff</b> </td><td>correction factor/quality used </td></tr>
|
||
<tr><td> </td><td><factor>/- -> no quality defined </td></tr>
|
||
<tr><td> </td><td><factor>/0..1 - quality of the PV forecast (1 = best quality) </td></tr>
|
||
<tr><td> <b>DoN</b> </td><td>sunrise and sunset status (0 - night, 1 - day) </td></tr>
|
||
<tr><td> <b>hourofday</b> </td><td>current hour of the day </td></tr>
|
||
<tr><td> <b>pvapifc</b> </td><td>expected PV generation (Wh) of the used API incl. a possible correction </td></tr>
|
||
<tr><td> <b>pvaifc</b> </td><td>expected PV generation of the AI (Wh) </td></tr>
|
||
<tr><td> <b>pvfc</b> </td><td>PV generation forecast used (Wh) </td></tr>
|
||
<tr><td> <b>rad1h</b> </td><td>predicted global radiation </td></tr>
|
||
<tr><td> <b>starttime</b> </td><td>start time of the record </td></tr>
|
||
<tr><td> <b>sunaz</b> </td><td>Azimuth of the sun (in decimal degrees) </td></tr>
|
||
<tr><td> <b>sunalt</b> </td><td>Altitude of the sun (in decimal degrees) </td></tr>
|
||
<tr><td> <b>temp</b> </td><td>predicted outdoor temperature </td></tr>
|
||
<tr><td> <b>today</b> </td><td>has value '1' if start date on current day </td></tr>
|
||
<tr><td> <b>rcdchargebatXX</b> </td><td>Charging recommendation for battery XX (1 - Yes, 0 - No) </td></tr>
|
||
<tr><td> <b>rr1c</b> </td><td>Total precipitation during the last hour kg/m2 </td></tr>
|
||
<tr><td> <b>rrange</b> </td><td>range of total rain </td></tr>
|
||
<tr><td> <b>socXX</b> </td><td>current (NextHour00) or predicted SoC of battery XX </td></tr>
|
||
<tr><td> <b>weatherid</b> </td><td>ID of the predicted weather </td></tr>
|
||
<tr><td> <b>wcc</b> </td><td>predicted degree of cloudiness </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-pvHistory"></a>
|
||
<li><b>pvHistory </b> <br><br>
|
||
Displays or exports the contents of the pvHistory data memory sorted by date and hour. <br>
|
||
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 'exportToCsv' specification exports the entire content of the pvHistory to a CSV file. <br>
|
||
|
||
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. The hour '99' contains daily values.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>batintotalXX</b> </td><td>total battery XX charge (Wh) at the beginning of the hour </td></tr>
|
||
<tr><td> <b>batinXX</b> </td><td>Charge of battery XX within the hour (Wh) </td></tr>
|
||
<tr><td> <b>batouttotalXX</b> </td><td>total battery XX discharge (Wh) at the beginning of the hour </td></tr>
|
||
<tr><td> <b>batoutXX</b> </td><td>Discharge of battery XX within the hour (Wh) </td></tr>
|
||
<tr><td> <b>batsocXX</b> </td><td>State of charge SOC (%) of battery XX at the end of the hour </td></tr>
|
||
<tr><td> <b>batmaxsocXX</b> </td><td>Maximum SOC (%) achieved by battery XX on the day </td></tr>
|
||
<tr><td> <b>batsetsocXX</b> </td><td>Optimum SOC setpoint (%) of battery XX for the day </td></tr>
|
||
<tr><td> <b>confc</b> </td><td>expected energy consumption (Wh) </td></tr>
|
||
<tr><td> <b>con</b> </td><td>real energy consumption (Wh) of the house </td></tr>
|
||
<tr><td> <b>conprice</b> </td><td>Price for the purchase of one kWh. The currency of the price is defined in the setupMeterDev. </td></tr>
|
||
<tr><td> <b>csmtXX</b> </td><td>total energy consumption of ConsumerXX </td></tr>
|
||
<tr><td> <b>csmeXX</b> </td><td>Energy consumption of ConsumerXX in the hour of the day (hour 99 = daily energy consumption) </td></tr>
|
||
<tr><td> <b>cyclescsmXX</b> </td><td>Number of active cycles of ConsumerXX of the day </td></tr>
|
||
<tr><td> <b>dayname</b> </td><td>short name of the day (locale-dependent) </td></tr>
|
||
<tr><td> <b>DoN</b> </td><td>Sunrise and sunset status (0 - night, 1 - day) </td></tr>
|
||
<tr><td> <b>etotaliXX</b> </td><td>PV meter reading “Total energy yield” (Wh) of inverter XX at the beginning of the hour </td></tr>
|
||
<tr><td> <b>etotalpXX</b> </td><td>Meter reading “Total energy yield” (Wh) of producer XX at the beginning of the hour </td></tr>
|
||
<tr><td> <b>gcons</b> </td><td>real power consumption (Wh) from the electricity grid </td></tr>
|
||
<tr><td> <b>gfeedin</b> </td><td>real feed-in (Wh) into the electricity grid </td></tr>
|
||
<tr><td> <b>feedprice</b> </td><td>Remuneration for the feed-in of one kWh. The currency of the price is defined in the setupMeterDev. </td></tr>
|
||
<tr><td> <b>hourscsmeXX</b> </td><td>total active hours of the day from ConsumerXX </td></tr>
|
||
<tr><td> <b>minutescsmXX</b> </td><td>total active minutes in the hour of ConsumerXX </td></tr>
|
||
<tr><td> <b>pprlXX</b> </td><td>Energy generation of producer XX (see attribute setupOtherProducerXX) in the hour (Wh) </td></tr>
|
||
<tr><td> <b>pvfc</b> </td><td>the predicted PV yield (Wh) </td></tr>
|
||
<tr><td> <b>pvrlXX</b> </td><td>real PV generation (Wh) of inverter XX </td></tr>
|
||
<tr><td> <b>pvrl</b> </td><td>Sum real PV generation (Wh) of all inverters </td></tr>
|
||
<tr><td> <b>pvrlvd</b> </td><td>1-'pvrl' is valid and is taken into account in the learning process, 0-'pvrl' is assessed as abnormal </td></tr>
|
||
<tr><td> <b>pvcorrf</b> </td><td>Autocorrection factor used / forecast quality achieved </td></tr>
|
||
<tr><td> <b>rad1h</b> </td><td>global radiation (kJ/m2) </td></tr>
|
||
<tr><td> <b>rr1c</b> </td><td>Total precipitation during the last hour kg/m2 </td></tr>
|
||
<tr><td> <b>sunalt</b> </td><td>Altitude of the sun (in decimal degrees) </td></tr>
|
||
<tr><td> <b>sunaz</b> </td><td>Azimuth of the sun (in decimal degrees) </td></tr>
|
||
<tr><td> <b>wid</b> </td><td>Weather identification number </td></tr>
|
||
<tr><td> <b>wcc</b> </td><td>effective cloud cover </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-pvCircular"></a>
|
||
<li><b>pvCircular </b> <br><br>
|
||
Lists the stored data for the selected hour or all existing values in the ring buffer. <br>
|
||
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. <br>
|
||
Hour 99 has a special function. <br>
|
||
The values of the keys pvcorrf, quality, pvrlsum, pvfcsum and dnumsum are coded in the form
|
||
<range sun elevation>.<cloud cover range>.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>aihit</b> </td><td>Delivery status of the AI for the PV forecast (0-no delivery, 1-delivery) </td></tr>
|
||
<tr><td> <b>attrInvChangedTs</b> </td><td>Timestamp of the last change to the inverter device definition </td></tr>
|
||
<tr><td> <b>batinXX</b> </td><td>Battery XX charge (Wh) </td></tr>
|
||
<tr><td> <b>batoutXX</b> </td><td>Battery XX discharge (Wh) </td></tr>
|
||
<tr><td> <b>batouttotXX</b> </td><td>total energy drawn from the battery XX (Wh) </td></tr>
|
||
<tr><td> <b>batintotXX</b> </td><td>current total energy charged into the battery XX (Wh) </td></tr>
|
||
<tr><td> <b>confc</b> </td><td>expected energy consumption (Wh) of the house on the current day </td></tr>
|
||
<tr><td> <b>days2careXX</b> </td><td>remaining days until the battery XX maintenance SoC (default 95%) is reached </td></tr>
|
||
<tr><td> <b>dnumsum</b> </td><td>Number of days per cloudy area over the entire term </td></tr>
|
||
<tr><td> <b>feedintotal</b> </td><td>total PV energy fed into the public grid (Wh) </td></tr>
|
||
<tr><td> <b>gcon</b> </td><td>real power drawn from the electricity grid </td></tr>
|
||
<tr><td> <b>gfeedin</b> </td><td>real power feed-in to the electricity grid </td></tr>
|
||
<tr><td> <b>gridcontotal</b> </td><td>total energy drawn from the public grid (Wh) </td></tr>
|
||
<tr><td> <b>initdayfeedin</b> </td><td>initial PV feed-in value at the beginning of the current day (Wh) </td></tr>
|
||
<tr><td> <b>initdaygcon</b> </td><td>initial grid reference value at the beginning of the current day (Wh) </td></tr>
|
||
<tr><td> <b>initdaybatintotXX</b> </td><td>initial value of the total energy charged into the battery XX at the beginning of the current day. (Wh) </td></tr>
|
||
<tr><td> <b>initdaybatouttotXX</b> </td><td>initial value of the total energy drawn from the battery XX at the beginning of the current day. (Wh) </td></tr>
|
||
<tr><td> <b>lastTsMaxSocRchdXX</b> </td><td>Timestamp of last achievement of battery XX SoC >= maxSoC (default 95%) </td></tr>
|
||
<tr><td> <b>nextTsMaxSocChgeXX</b> </td><td>Timestamp by which the battery XX should reach maxSoC at least once </td></tr>
|
||
<tr><td> <b>pvapifc</b> </td><td>expected PV generation (Wh) of the API used </td></tr>
|
||
<tr><td> <b>pvaifc</b> </td><td>PV forecast (Wh) of the AI for the next 24h from the current hour of the day </td></tr>
|
||
<tr><td> <b>pvfc</b> </td><td>PV forecast used for the next 24h from the current hour of the day </td></tr>
|
||
<tr><td> <b>pvfc_XX</b> </td><td>Array of predicted PV generation values depending on a certain degree of cloud cover (XX = altitude of the sun) </td></tr>
|
||
<tr><td> <b>pvcorrf</b> </td><td>Autocorrection factors for the hour of the day, where 'simple' is the simple correction factor. </td></tr>
|
||
<tr><td> <b>pvfcsum</b> </td><td>summary PV forecast per cloud area over the entire term </td></tr>
|
||
<tr><td> <b>pvrl</b> </td><td>real PV generation of the last 24h (Attention: pvforecast and pvreal do not refer to the same period!) </td></tr>
|
||
<tr><td> <b>pvrl_XX</b> </td><td>Array of real PV generation values generated at a certain degree of cloudiness (XX = altitude of the sun) </td></tr>
|
||
<tr><td> <b>pvrlsum</b> </td><td>summary real PV generation per cloud area over the entire term </td></tr>
|
||
<tr><td> <b>pprlXX</b> </td><td>Energy generation of producer XX (see attribute setupOtherProducerXX) in the last 24 hours (Wh) </td></tr>
|
||
<tr><td> <b>quality</b> </td><td>Quality of the autocorrection factors (0..1), where 'simple' is the quality of the simple correction factor. </td></tr>
|
||
<tr><td> <b>runTimeTrainAI</b> </td><td>Duration of the last AI training </td></tr>
|
||
<tr><td> <b>aitrainLastFinishTs</b> </td><td>Timestamp of the last successful AI training </td></tr>
|
||
<tr><td> <b>aiRulesNumber</b> </td><td>Number of rules in the trained AI instance </td></tr>
|
||
<tr><td> <b>todayConsumption</b> </td><td>real energy consumption (Wh) of the house on the current day </td></tr>
|
||
<tr><td> <b>tdayDvtn</b> </td><td>Today's deviation PV forecast/generation in % </td></tr>
|
||
<tr><td> <b>temp</b> </td><td>Outdoor temperature </td></tr>
|
||
<tr><td> <b>wcc</b> </td><td>Degree of cloud cover </td></tr>
|
||
<tr><td> <b>rr1c</b> </td><td>Total precipitation during the last hour kg/m2 </td></tr>
|
||
<tr><td> <b>wid</b> </td><td>ID of the predicted weather </td></tr>
|
||
<tr><td> <b>wtxt</b> </td><td>Description of the predicted weather </td></tr>
|
||
<tr><td> <b>ydayDvtn</b> </td><td>Deviation PV forecast/generation in % on the previous day </td></tr>
|
||
</table>
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-rooftopData"></a>
|
||
<li><b>rooftopData </b> <br><br>
|
||
The expected solar radiation data or PV generation data are retrieved from the selected API. <br>
|
||
If an API is also selected for weather data, this data is also retrieved.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-radiationApiData"></a>
|
||
<li><b>radiationApiData </b> <br><br>
|
||
|
||
Lists the radiation data saved in the context of the API call.
|
||
The forecast data supplied by the API regarding the global radiation Rad1h and the predicted PV yield (Wh)
|
||
related to a string are normalized to one hour.
|
||
The available characteristic values differ depending on the API used.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>Rad1h</b> </td><td>if available, expected global irradiation (GI) or global tilt irradiation (GTI) in kJ/m2 </td></tr>
|
||
<tr><td> <b>pv_estimateXX</b> </td><td>Expected PV generation (Wh) </td></tr>
|
||
<tr><td> <b>KI-based</b> </td><td>expected PV generation (Wh) of the VictronKI-API </td></tr>
|
||
<tr><td> <b>KI-based_co</b> </td><td>expected energy consumption (Wh) of the VictronKI-API </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-statusApiData"></a>
|
||
<li><b>statusApiData </b> <br><br>
|
||
Shows the status data of the radiation data API or weather data API used.
|
||
Only the status data of the leading weather service is output.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="37%"> <col width="63%"> </colgroup>
|
||
<tr><td> <b>currentAPIinterval</b> </td><td>the API call interval currently used in seconds </td></tr>
|
||
<tr><td> <b>lastretrieval_time</b> </td><td>Time of the last API call </td></tr>
|
||
<tr><td> <b>lastretrieval_timestamp</b> </td><td>Unix timestamp of the last API call </td></tr>
|
||
<tr><td> <b>todayDoneAPIrequests</b> </td><td>Number of API requests executed on the current day </td></tr>
|
||
<tr><td> <b>todayRemainingAPIrequests</b> </td><td>Number of remaining SolCast API requests on the current day </td></tr>
|
||
<tr><td> <b>todayDoneAPIcalls</b> </td><td>Number of API calls executed on the current day </td></tr>
|
||
<tr><td> <b>todayRemainingAPIcalls</b> </td><td>Number of SolCast API calls still possible on the current day </td></tr>
|
||
<tr><td> </td><td>(one call can execute several SolCast API requests) </td></tr>
|
||
<tr><td> <b>todayMaxAPIcalls</b> </td><td>Maximum number of possible SolCast API calls per day </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valBattery"></a>
|
||
<li><b>valBattery </b> <br><br>
|
||
Shows the operating values determined for the selected battery or all defined battery devices. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>bname </b> </td><td>Name of the device </td></tr>
|
||
<tr><td> <b>balias </b> </td><td>Alias of the device </td></tr>
|
||
<tr><td> <b>basynchron </b> </td><td>Mode of processing received battery events </td></tr>
|
||
<tr><td> <b>bcharge </b> </td><td>current SoC (State of Charge) of the battery (%) </td></tr>
|
||
<tr><td> <b>bchargewh </b> </td><td>current SoC (State of Charge) of the battery (Wh) </td></tr>
|
||
<tr><td> <b>binstcap </b> </td><td>installed battery capacity (Wh) </td></tr>
|
||
<tr><td> <b>bpowerin </b> </td><td>current charging power (W) </td></tr>
|
||
<tr><td> <b>bpowerout </b> </td><td>current discharge power (W) </td></tr>
|
||
</table>
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valConsumerMaster"></a>
|
||
<li><b>valConsumerMaster </b> <br><br>
|
||
Shows the data of the consumers currently registered in the SolarForecast Device. <br>
|
||
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.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valCurrent"></a>
|
||
<li><b>valCurrent </b> <br><br>
|
||
Lists current operating data, key figures and status.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valDecTree"></a>
|
||
<li><b>valDecTree </b> <br><br>
|
||
|
||
Display of AI-relevant data.
|
||
The available display options depend on the available and activated AI support level.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>aiRawData</b> </td><td>Display of the PV, radiation and environmental data currently stored for the AI. </td></tr>
|
||
<tr><td> </td><td>(available if the Perl module AI::DecisionTree is installed) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>aiRuleStrings</b> </td><td>Returns a list that describes the AI's decision tree in the form of rules. </td></tr>
|
||
<tr><td> </td><td><b>Note:</b> While the order of the rules is not predictable, the </td></tr>
|
||
<tr><td> </td><td>order of criteria within each rule, however, reflects the order </td></tr>
|
||
<tr><td> </td><td>in which the criteria are considered in the decision-making process. </td></tr>
|
||
<tr><td> </td><td>(available if an AI-compatible SolarForecast MODEL of the PV forecast is activated) </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valInverter"></a>
|
||
<li><b>valInverter </b> <br><br>
|
||
Shows the operating values determined for the selected inverter or all defined inverters. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>iasynchron </b> </td><td>Mode of processing received inverter events </td></tr>
|
||
<tr><td> <b>ietotal </b> </td><td>total energy generated by the inverter to date (Wh) </td></tr>
|
||
<tr><td> <b>ifeed </b> </td><td>Energy supply characteristics </td></tr>
|
||
<tr><td> <b>igeneration </b> </td><td>current PV generation (W) </td></tr>
|
||
<tr><td> <b>iicon </b> </td><td>any icons defined for displaying the device in the graphic </td></tr>
|
||
<tr><td> <b>ialias </b> </td><td>Alias of the device </td></tr>
|
||
<tr><td> <b>iname </b> </td><td>Name of the device </td></tr>
|
||
<tr><td> <b>invertercap </b> </td><td>the nominal power (W) of the inverter (if defined) </td></tr>
|
||
<tr><td> <b>istrings </b> </td><td>List of strings assigned to the inverter (if defined) </td></tr>
|
||
</table>
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valProducer"></a>
|
||
<li><b>valProducer </b> <br><br>
|
||
Shows the operating values determined for the selected non-PV generator or all defined non-PV generators. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>petotal </b> </td><td>total energy generated by the producer to date (Wh) </td></tr>
|
||
<tr><td> <b>pfeed </b> </td><td>Energy supply characteristics </td></tr>
|
||
<tr><td> <b>pgeneration </b> </td><td>current power (W) </td></tr>
|
||
<tr><td> <b>picon </b> </td><td>any icons defined for displaying the device in the graphic </td></tr>
|
||
<tr><td> <b>palias </b> </td><td>Alias of the device </td></tr>
|
||
<tr><td> <b>pname </b> </td><td>Name of the device </td></tr>
|
||
</table>
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valStrings"></a>
|
||
<li><b>valStrings </b> <br><br>
|
||
Lists the parameters of the selected or all defined strings.
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-weatherApiData"></a>
|
||
<li><b>weatherApiData </b> <br><br>
|
||
Shows the data supplied by the selected weather API.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
</ul>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr"></a>
|
||
<b>Attribute</b>
|
||
<br><br>
|
||
<ul>
|
||
<ul>
|
||
<a id="SolarForecast-attr-affectBatteryPreferredCharge"></a>
|
||
<li><b>affectBatteryPreferredCharge </b><br>
|
||
Consumers with the <b>can</b> mode are only switched on when the specified battery charge (%)
|
||
is reached. <br>
|
||
Consumers with the <b>must</b> mode do not observe the priority charging of the battery. <br>
|
||
(default: 0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-affectConsForecastInPlanning"></a>
|
||
<li><b>affectConsForecastInPlanning </b><br>
|
||
If set, the consumption forecast is also taken into account in addition to the PV forecast when scheduling the
|
||
consumer. <br>
|
||
Standard consumer planning is based on the PV forecast only. <br>
|
||
(default: 0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-affectConsForecastIdentWeekdays"></a>
|
||
<li><b>affectConsForecastIdentWeekdays </b><br>
|
||
If set, only the same weekdays (Mon..Sun) are included in the calculation of the consumption forecast. <br>
|
||
Otherwise, all weekdays are used equally for calculation. <br>
|
||
Any additional attribute
|
||
<a href="#SolarForecast-attr-affectConsForecastLastDays">affectConsForecastLastDays</a>
|
||
is also taken into account. <br>
|
||
(default: 0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-affectConsForecastLastDays"></a>
|
||
<li><b>affectConsForecastLastDays </b><br>
|
||
The specified past days (1..31) are included in the calculation of the consumption forecast. <br>
|
||
For example, with the attribute value “1” only the previous day is taken into account, with the value “14” the previous 14 days. <br>
|
||
If an additional attribute
|
||
<a href="#SolarForecast-attr-affectConsForecastIdentWeekdays">affectConsForecastIdentWeekdays</a>
|
||
is set, the specified number of past weekdays of the same type (Mon .. Sun) is taken into account. <br>
|
||
(default: all days available in pvHistory)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-affectSolCastPercentile"></a>
|
||
<li><b>affectSolCastPercentile <10 | 50 | 90> </b><br>
|
||
(only when using Model SolCastAPI) <br><br>
|
||
|
||
Selection of the probability range of the delivered SolCast data.
|
||
SolCast provides the 10 and 90 percent probability around the forecast mean (50). <br>
|
||
(default: 50)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-alias"></a>
|
||
<li><b>alias </b> <br>
|
||
In connection with "ctrlShowLink" any display name.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-consumerAdviceIcon"></a>
|
||
<li><b>consumerAdviceIcon </b><br>
|
||
Defines the type of information about the planned switching times of a consumer in the consumer legend.
|
||
<br><br>
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="18%"> <col width="82%"> </colgroup>
|
||
<tr><td> <b><Icon>@<Colour></b> </td><td>Activation recommendation is represented by icon and colour (optional) (default: clock@gold) </td></tr>
|
||
<tr><td> </td><td>(the planning data is displayed as mouse-over text) </td></tr>
|
||
<tr><td> <b>times</b> </td><td>the planning status and the planned switching times are displayed as text </td></tr>
|
||
<tr><td> <b>none</b> </td><td>no display of the planning data </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-consumerLegend"></a>
|
||
<li><b>consumerLegend </b><br>
|
||
Defines the position or display mode of the load legend if loads are registered in the SolarForecast Device.
|
||
<br>
|
||
(default: icon_top)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-consumerLink"></a>
|
||
<li><b>consumerLink </b><br>
|
||
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. <br>
|
||
(default: 1)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-consumer" data-pattern="consumer.*"></a>
|
||
<li><b>consumerXX <Device>[:<Alias>] type=<type> power=<power> [switchdev=<device>]<br>
|
||
[mode=<mode>] [icon=<Icon>[@<Color>]] [mintime=<minutes> | SunPath[:<Offset_Sunrise>:<Offset_Sunset>]] <br>
|
||
[on=<command>] [off=<command>] [swstate=<Readingname>:<on-Regex>:<off-Regex>] [asynchron=<Option>] <br>
|
||
[notbefore=<Expression>] [notafter=<Expression>] [locktime=<offlt>[:<onlt>]] <br>
|
||
[auto=<Readingname>] [pcurr=<Readingname>:<Unit>[:<Threshold>]] [etotal=<Readingname>:<Einheit>[:<Threshold>]] <br>
|
||
[swoncond=<Device>:<Reading>:<Regex>] [swoffcond=<Device>:<Reading>:<Regex>] [spignorecond=<Device>:<Reading>:<Regex>] <br>
|
||
[surpmeth=<Option>] [interruptable=<Option>] [noshow=<Option>] [exconfc=<Option>] </b><br>
|
||
<br>
|
||
|
||
Registers a consumer <Device> with the SolarForecast Device. An optional alias can be specified. <br>
|
||
In this case, <Device> is a consumer device already created in FHEM, e.g. a switchable socket.
|
||
Most of the keys are optional, but are a prerequisite for certain functionalities and are filled with
|
||
default values. <br>
|
||
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. <br><br>
|
||
|
||
With the optional key <b>swoncond</b>, an <b>additional external condition</b> 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
|
||
<b>AND-link</b> of the key swoncond with the further switch-on conditions. <br><br>
|
||
|
||
The optional key <b>swoffcond</b> defines a <b>priority switch-off condition</b> (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 (<b>OR link</b>). Further conditions such as off key and auto mode must be
|
||
be fulfilled for automatic switch-off. <br><br>
|
||
|
||
With the optional <b>interruptable</b> 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!
|
||
<br><br>
|
||
|
||
The <b>power</b> 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.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="12%"> <col width="88%"> </colgroup>
|
||
<tr><td> <b>Device</b> </td><td>Consumer device. In the simple case, the device works both as an energy meter and as a switch. </td></tr>
|
||
<tr><td> </td><td>In the optional alias, spaces must be replaced by '+' (e.g. 'Ein+toller+Alias'). </td></tr>
|
||
<tr><td> </td><td>If the consumer consists of different devices/channels (e.g. Homematic), the energy meter is defined as a <Device>. </td></tr>
|
||
<tr><td> </td><td>The associated switching device is specified with the key 'switchdev'. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>type</b> </td><td>Type of consumer. The following types are allowed: </td></tr>
|
||
<tr><td> </td><td><b>dishwasher</b> - Consumer is a dishwasher </td></tr>
|
||
<tr><td> </td><td><b>dryer</b> - Consumer is a tumble dryer </td></tr>
|
||
<tr><td> </td><td><b>washingmachine</b> - Consumer is a washing machine </td></tr>
|
||
<tr><td> </td><td><b>heater</b> - Consumer is a heating rod </td></tr>
|
||
<tr><td> </td><td><b>charger</b> - Consumer is a charging device (battery, car, bicycle, etc.) </td></tr>
|
||
<tr><td> </td><td><b>other</b> - Consumer is none of the above types </td></tr>
|
||
<tr><td> </td><td><b>noSchedule</b> - there is no scheduling or automatic switching for the consumer. </td></tr>
|
||
<tr><td> </td><td>
|
||
Display functions or manual switching are available. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>power</b> </td><td>nominal power consumption of the consumer (see data sheet) in W </td></tr>
|
||
<tr><td> </td><td>(can be set to "0") </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>switchdev</b> </td><td>The specified <device> is assigned to the consumer as a switch device (optional). Switching operations are performed with this device. </td></tr>
|
||
<tr><td> </td><td>The key is useful for consumers where energy measurement and switching is carried out with different devices </td></tr>
|
||
<tr><td> </td><td>e.g. Homematic or readingsProxy. If switchdev is specified, the keys on, off, swstate, auto, asynchronous refer to this device. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>mode</b> </td><td>Consumer planning mode (optional). Allowed are: </td></tr>
|
||
<tr><td> </td><td><b>can</b> - Scheduling takes place at the time when there is probably enough PV surplus available (default). </td></tr>
|
||
<tr><td> </td><td> The consumer is not started at the time of planning if the PV surplus is insufficient. </td></tr>
|
||
<tr><td> </td><td><b>must</b> - The consumer is optimally planned, even if there will probably not be enough PV surplus. </td></tr>
|
||
<tr><td> </td><td> The load is started even if there is insufficient PV surplus, provided that
|
||
a set "swoncond" condition is met and "swoffcond" is not met. </td></tr>
|
||
<tr><td> </td><td><b>Device:Reading</b> - Device/Reading combination to be able to change the planning mode dynamically. The reading must return 'can' or 'must'. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>icon</b> </td><td>Icon and, if applicable, its color for displaying the consumer in the overview graphic (optional) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>mintime</b> </td><td>Scheduling duration (minutes or "SunPath") of the consumer. (optional) </td></tr>
|
||
<tr><td> </td><td>By specifying <b>SunPath</b>, planning is done according to sunrise and sunset. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> </td><td><b>SunPath</b>[:<Offset_Sunrise>:<Offset_Sunset>] - scheduling takes place from sunrise to sunset. </td></tr>
|
||
<tr><td> </td><td> Optionally, a positive / negative shift (minutes) of the planning time regarding sunrise or sunset can be specified. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> </td><td>If mintime is not specified, a standard scheduling duration according to the following table is used. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> </td><td><b>Default mintime by consumer type:</b> </td></tr>
|
||
<tr><td> </td><td>- dishwasher: 180 minutes </td></tr>
|
||
<tr><td> </td><td>- dryer: 90 minutes </td></tr>
|
||
<tr><td> </td><td>- washingmachine: 120 minutes </td></tr>
|
||
<tr><td> </td><td>- heater: 240 minutes </td></tr>
|
||
<tr><td> </td><td>- charger: 120 minutes </td></tr>
|
||
<tr><td> </td><td>- other: 60 minutes </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>on</b> </td><td>Set command for switching on the consumer (optional) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>off</b> </td><td>Set command for switching off the consumer (optional) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>swstate</b> </td><td>Reading which indicates the switching status of the consumer (default: 'state'). </td></tr>
|
||
<tr><td> </td><td><b>on-Regex</b> - regular expression for the state 'on' (default: 'on') </td></tr>
|
||
<tr><td> </td><td><b>off-Regex</b> - regular expression for the state 'off' (default: 'off') </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>asynchron</b> </td><td>the type of switching status determination in the consumer device. The status of the consumer is only determined after a switching command </td></tr>.
|
||
<tr><td> </td><td>by polling within a data collection interval (synchronous) or additionally by event processing (asynchronous). </td></tr>
|
||
<tr><td> </td><td><b>0</b> - only synchronous processing of switching states (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - additional asynchronous processing of switching states through event processing </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>notbefore</b> </td><td>Schedule start time consumer not before specified time 'hour[:minute]' (optional) </td></tr>
|
||
<tr><td> </td><td>The <Expression> has the format hh[:mm] or is Perl code enclosed in {...} that returns hh[:mm]. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>notafter</b> </td><td>Schedule start time consumer not after specified time 'hour[:minute]' (optional) </td></tr>
|
||
<tr><td> </td><td>The <Expression> has the format hh[:mm] or is Perl code enclosed in {...} that returns hh[:mm]. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>auto</b> </td><td>Reading in the consumer device which enables or blocks the switching of the consumer (optional) </td></tr>
|
||
<tr><td> </td><td>If the key switchdev is given, the reading is set and evaluated in this device. </td></tr>
|
||
<tr><td> </td><td>Reading value = 1 - switching enabled (default), 0: switching blocked </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>pcurr</b> </td><td>Reading:Unit (W/kW) which provides the current energy consumption (optional) </td></tr>
|
||
<tr><td> </td><td>:<Threshold> (W) - From this power reference on, the consumer is considered active. The specification is optional (default: 0) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>etotal</b> </td><td>Reading:Unit (Wh/kWh) of the consumer device that supplies the sum of the consumed energy (optional) </td></tr>
|
||
<tr><td> </td><td>:<Threshold> (Wh) - From this energy consumption per hour, the consumption is considered valid. Optional specification (default: 0) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>swoncond</b> </td><td>Condition that must also be fulfilled in order to switch on the consumer (optional). The scheduled cycle is started. </td></tr>
|
||
<tr><td> </td><td><b>Device</b> - Device to supply the additional switch-on condition </td></tr>
|
||
<tr><td> </td><td><b>Reading</b> - Reading for delivery of the additional switch-on condition </td></tr>
|
||
<tr><td> </td><td><b>Regex</b> - regular expression that must be satisfied for a 'true' condition to be true </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>swoffcond</b> </td><td>priority condition to switch off the consumer (optional). The scheduled cycle is stopped. </td></tr>
|
||
<tr><td> </td><td><b>Device</b> - Device to supply the priority switch-off condition </td></tr>
|
||
<tr><td> </td><td><b>Reading</b> - Reading for the delivery of the priority switch-off condition </td></tr>
|
||
<tr><td> </td><td><b>Regex</b> - regular expression that must be satisfied for a 'true' condition to be true </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>surpmeth</b> </td><td>The possible options define the procedure for determining the PV surplus. (optional) </td></tr>
|
||
<tr><td> </td><td><b>default</b> - the PV surplus is read directly from the 'Current_Surplus' reading. (default) </td></tr>
|
||
<tr><td> </td><td><b>median</b> - the median of the last PV surplus measurements (max. 20) is used. </td></tr>
|
||
<tr><td> </td><td><b>2 .. 20</b> - the PV surplus used is calculated from the average of the specified number of measured values. </td></tr>
|
||
<tr><td> </td><td><b>Device:Reading</b> - Device/Reading combination that provides a numerical PV surplus value in Watt determined or calculated by the user. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>spignorecond</b> </td><td>Condition to ignore a missing PV surplus (optional). If the condition is fulfilled, the load is switched on according to </td></tr>
|
||
<tr><td> </td><td>the planning even if there is no PV surplus at the time. </td></tr>
|
||
<tr><td> </td><td><b>CAUTION:</b> Using both keys <I>spignorecond</I> and <I>interruptable</I> can lead to undesired behaviour! </td></tr>
|
||
<tr><td> </td><td><b>Device</b> - Device to deliver the condition </td></tr>
|
||
<tr><td> </td><td><b>Reading</b> - Reading which contains the condition </td></tr>
|
||
<tr><td> </td><td><b>Regex</b> - regular expression that must be satisfied for a 'true' condition to be true </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>interruptable</b> </td><td>defines the possible interruption options for the consumer after it has been started (optional) </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Load is not temporarily switched off even if the PV surplus falls below the required energy (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - Load is temporarily switched off if the PV surplus falls below the required energy </td></tr>
|
||
<tr><td> </td><td><b>Device:Reading:Regex[:Hysteresis]</b> - Load is temporarily interrupted if the value of the specified </td></tr>
|
||
<tr><td> </td><td>Device:Readings match on the regex or if is insufficient PV surplus (if power not equal to 0). </td></tr>
|
||
<tr><td> </td><td>If the value no longer matches, the interrupted load is switched on again if there is sufficient </td></tr>
|
||
<tr><td> </td><td>PV surplus provided (if power is not 0). </td></tr>
|
||
<tr><td> </td><td>If the optional <b>hysteresis</b> is specified, the hysteresis value is subtracted from the reading value and the regex is then applied. </td></tr>
|
||
<tr><td> </td><td>If this and the original reading value match, the consumer is temporarily interrupted. </td></tr>
|
||
<tr><td> </td><td>The consumer is continued if both the original and the subtracted readings value do not (or no longer) match. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>locktime</b> </td><td>Blocking times in seconds for switching the consumer (optional). </td></tr>
|
||
<tr><td> </td><td><b>offlt</b> - Blocking time in seconds after the consumer has been switched off or interrupted </td></tr>
|
||
<tr><td> </td><td><b>onlt</b> - Blocking time in seconds after the consumer has been switched on or continued </td></tr>
|
||
<tr><td> </td><td>The consumer is only switched again when the corresponding blocking time has elapsed. </td></tr>
|
||
<tr><td> </td><td><b>Note:</b> The 'locktime' switch is only effective in automatic mode. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>noshow</b> </td><td>Hide or show consumers in graphic (optional). </td></tr>
|
||
<tr><td> </td><td><b>0</b> - the consumer is displayed (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - the consumer is hidden </td></tr>
|
||
<tr><td> </td><td><b>2</b> - the consumer is hidden in the consumer legend </td></tr>
|
||
<tr><td> </td><td><b>3</b> - the consumer is hidden in the flow chart </td></tr>
|
||
<tr><td> </td><td><b>[Device:]Reading</b> - Reading in the consumer or optionally an alternative device. </td></tr>
|
||
<tr><td> </td><td>If the reading has the value 0 or is not present, the consumer is displayed. </td></tr>
|
||
<tr><td> </td><td>The effect of the possible reading values 1, 2 and 3 is as described. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>exconfc</b> </td><td>Use of the consumer's recorded energy consumption to create the consumption forecast (optional). </td></tr>
|
||
<tr><td> </td><td><b>0</b> - the consumer's historical energy consumption is used to create the consumption forecast (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - the consumer's historical energy consumption is excluded from the consumption forecast. </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Examples: </b> <br>
|
||
<b>attr <name> consumer01</b> wallplug icon=scene_dishwasher@orange type=dishwasher mode=can power=2500 on=on off=off notafter=20 etotal=total:kWh:5 <br>
|
||
<b>attr <name> consumer02</b> WPxw type=heater mode=can power=3000 mintime=180 on="on-for-timer 3600" notafter=12 auto=automatic <br>
|
||
<b>attr <name> consumer03</b> 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 <br>
|
||
<b>attr <name> consumer04</b> Shelly.shellyplug3 icon=scene_microwave_oven@ed 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 <br>
|
||
<b>attr <name> consumer05</b> 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 <br>
|
||
<b>attr <name> consumer06</b> 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 <br>
|
||
<b>attr <name> consumer07</b> 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=1 <br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlAIdataStorageDuration"></a>
|
||
<li><b>ctrlAIdataStorageDuration <Tage></b> <br>
|
||
If the corresponding prerequisites are met, training data is collected and stored for the
|
||
module-internal AI. <br>
|
||
The data is deleted when it has exceeded the specified holding period (days).<br>
|
||
(default: 1825)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlAIshiftTrainStart"></a>
|
||
<li><b>ctrlAIshiftTrainStart <1...23></b> <br>
|
||
Daily training takes place when using the internal AI.<br>
|
||
Training begins approx. 15 minutes after the hour specified in the attribute. <br>
|
||
For example, with a set value of '3', training would start at around 03:15. <br>
|
||
(default: 2)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlBackupFilesKeep"></a>
|
||
<li><b>ctrlBackupFilesKeep <Integer> </b><br>
|
||
Defines the number of generations of backup files
|
||
(see also <a href="#SolarForecast-set-operatingMemory">set <name> operatingMemory backup</a>). <br>
|
||
If ctrlBackupFilesKeep explit is set to '0', no automatic generation and cleanup of backup files takes place. <br>
|
||
Manual execution with the aforementioned set command is still possible. <br>
|
||
(default: 3)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlBatSocManagementXX" data-pattern="ctrlBatSocManagement.*"></a>
|
||
<li><b>ctrlBatSocManagementXX lowSoc=<Value> upSoC=<Value> [maxSoC=<Value>] [careCycle=<Value>] </b> <br><br>
|
||
If a battery device (setupBatteryDevXX) is installed, this attribute activates the battery SoC management for this
|
||
battery device. <br>
|
||
The <b>Battery_OptimumTargetSoC_XX</b> reading contains the optimum minimum SoC calculated by the module. <br>
|
||
The <b>Battery_ChargeRequest_XX</b> reading is set to '1' if the current SoC has fallen below the minimum SoC. <br>
|
||
In this case, the battery should be forcibly charged, possibly with mains power. <br>
|
||
The readings can be used to control the SoC (State of Charge) and to control the charging current used for the
|
||
battery. <br>
|
||
The module itself does not control the battery. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>lowSoc</b> </td><td>lower minimum SoC - The battery is not discharged lower than this value (> 0) </td></tr>
|
||
<tr><td> <b>upSoC</b> </td><td>upper minimum SoC - The usual value of the optimum SoC tends to be </td></tr>
|
||
<tr><td> </td><td>between 'lowSoC' and 'upSoC' in periods with a high PV surplus </td></tr>
|
||
<tr><td> </td><td>and between 'upSoC' and 'maxSoC' in periods with a low PV surplus </td></tr>
|
||
<tr><td> <b>maxSoC</b> </td><td>Maximum minimum SoC - SoC value that must be reached at least every 'careCycle' days </td></tr>
|
||
<tr><td> </td><td>in order to balance the charge in the storage network. </td></tr>
|
||
<tr><td> </td><td>The specification is optional (<= 100, default: 95) </td></tr>
|
||
<tr><td> <b>careCycle</b> </td><td>Maximum interval in days that may occur between two states of charge </td></tr>
|
||
<tr><td> </td><td>of at least 'maxSoC'. The specification is optional (default: 20) </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
All values are whole numbers in %. The following applies: 'lowSoc' < 'upSoC' < 'maxSoC'. <br>
|
||
The optimum SoC is determined according to the following scheme: <br><br>
|
||
|
||
<table>
|
||
<colgroup> <col width="2%"> <col width="98%"> </colgroup>
|
||
<tr><td> 1. </td><td>Starting from 'lowSoc', the minimum SoC is increased by 5% on the following day but not higher than </td></tr>
|
||
<tr><td> </td><td>'upSoC', if 'maxSoC' has not been reached on the current day. </td></tr>
|
||
<tr><td> 2. </td><td>If 'maxSoC' is reached (again), the minimum SoC is reduced by 5%, but not lower than 'lowSoc'. </td></tr>
|
||
<tr><td> 3. </td><td>Minimum SoC is reduced to the extent that the predicted PV energy for the current or following </td></tr>
|
||
<tr><td> </td><td>day can be absorbed by the battery. Minimum SoC is typically reduced to 'upSoc' and not lower than 'lowSoc'. </td></tr>
|
||
<tr><td> 4. </td><td>The module records the last point in time at the 'maxSoC' level in order to ensure a charge to 'maxSoC' </td></tr>
|
||
<tr><td> </td><td>at least every 'careCycle' days. For this purpose, the optimized SoC is changed depending on the remaining days </td></tr>
|
||
<tr><td> </td><td>until the next 'careCycle' point in such a way that 'maxSoC' is mathematically achieved by a daily 5% SoC increase </td></tr>
|
||
<tr><td> </td><td>at the 'careCycle' time point. If 'maxSoC' is reached in the meantime, the 'careCycle' period starts again. </td></tr>
|
||
</table>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
attr <name> ctrlBatSocManagement01 lowSoc=10 upSoC=50 maxSoC=99 careCycle=25 <br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlConsRecommendReadings"></a>
|
||
<li><b>ctrlConsRecommendReadings </b><br>
|
||
Readings of the form <b>consumerXX_ConsumptionRecommended</b> are created for the selected consumers (number). <br>
|
||
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. <br>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlDebug"></a>
|
||
<li><b>ctrlDebug</b><br>
|
||
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". <br>
|
||
The debug level can be combined with each other: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>aiProcess</b> </td><td>Data enrichment and training process for AI support </td></tr>
|
||
<tr><td> <b>aiData</b> </td><td>Data use AI in the forecasting process </td></tr>
|
||
<tr><td> <b>apiCall</b> </td><td>Retrieval API interface without data output </td></tr>
|
||
<tr><td> <b>apiProcess</b> </td><td>API data retrieval and processing </td></tr>
|
||
<tr><td> <b>batteryManagement</b> </td><td>Battery management control values (SoC) </td></tr>
|
||
<tr><td> <b>collectData</b> </td><td>detailed data collection </td></tr>
|
||
<tr><td> <b>consumerPlanning</b> </td><td>Consumer scheduling processes </td></tr>
|
||
<tr><td> <b>consumerSwitchingXX</b> </td><td>Operations of the internal consumer switching module of consumer XX </td></tr>
|
||
<tr><td> <b>consumption</b> </td><td>Consumption calculation, consumption forecasting and utilization </td></tr>
|
||
<tr><td> <b>consumption_long</b> </td><td>extended output of the consumption forecast Determination </td></tr>
|
||
<tr><td> <b>dwdComm</b> </td><td>Communication with the website or server of the German Weather Service (DWD) </td></tr>
|
||
<tr><td> <b>epiecesCalc</b> </td><td>Calculation of specific energy consumption per operating hour and consumer </td></tr>
|
||
<tr><td> <b>graphic</b> </td><td>Module graphic information </td></tr>
|
||
<tr><td> <b>notifyHandling</b> </td><td>Sequence of event processing in the module </td></tr>
|
||
<tr><td> <b>pvCorrectionRead</b> </td><td>Application of PV correction factors </td></tr>
|
||
<tr><td> <b>pvCorrectionWrite</b> </td><td>Calculation of PV correction factors </td></tr>
|
||
<tr><td> <b>radiationProcess</b> </td><td>Collection and processing of solar radiation data </td></tr>
|
||
<tr><td> <b>saveData2Cache</b> </td><td>Data storage in internal memory structures </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlGenPVdeviation"></a>
|
||
<li><b>ctrlGenPVdeviation </b><br>
|
||
Specifies the method for calculating the deviation between predicted and real PV generation.
|
||
The Reading <b>Today_PVdeviation</b> is created depending on this setting. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>daily</b> </td><td>Calculation and creation of Today_PVdeviation is done after sunset (default) </td></tr>
|
||
<tr><td> <b>continuously</b> </td><td>Calculation and creation of Today_PVdeviation is done continuously </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li><br>
|
||
|
||
<a id="SolarForecast-attr-ctrlInterval"></a>
|
||
<li><b>ctrlInterval <Sekunden> </b><br>
|
||
Repetition interval of the data collection. <br>
|
||
If ctrlInterval is explicitly set to “0”, no regular data collection takes place and must be started externally
|
||
with “get <name> data”. <br>
|
||
(default: 70)
|
||
<br><br>
|
||
|
||
<b>Note:</b> Regardless of the set interval (even with “0”), data is collected automatically a few seconds
|
||
before the end and after the start of a full hour. <br>
|
||
Furthermore, data is collected automatically when an event from a device defined as “asynchron”
|
||
device (consumer, meter, etc.) is received and processed.
|
||
</li><br>
|
||
|
||
<a id="SolarForecast-attr-ctrlLanguage"></a>
|
||
<li><b>ctrlLanguage <DE | EN> </b><br>
|
||
Defines the used language of the device. The language definition has an effect on the module graphics and various
|
||
reading contents. <br>
|
||
If the attribute is not set, the language is defined by setting the global attribute "language". <br>
|
||
(default: EN)
|
||
</li><br>
|
||
|
||
<a id="SolarForecast-attr-ctrlNextDayForecastReadings"></a>
|
||
<li><b>ctrlNextDayForecastReadings <01,02,..,24> </b><br>
|
||
If set, readings of the form <b>Tomorrow_Hour<hour>_PVforecast</b> are created. <br>
|
||
These readings contain the expected PV generation of the coming day. Here <hour> is the
|
||
hour of the day. <br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
attr <name> ctrlNextDayForecastReadings 09,11 <br>
|
||
# creates readings for hour 09 (08:00-09:00) and 11 (10:00-11:00) of the coming day
|
||
</ul>
|
||
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlNextHoursSoCForecastReadings"></a>
|
||
<li><b>ctrlNextHoursSoCForecastReadings <00,02,..,23> </b><br>
|
||
If set, readings of the form Battery_NextHourXX_SoCforecast_BN are created if a battery is registered
|
||
in the SolarForecast device (see <a href="#SolarForecast-attr-setupBatteryDev">attr <name> setupBatteryDevXX </a>). <br>
|
||
These readings contain the predicted SoC value (%) at the end of the selected hour. <br>
|
||
Where 'XX' is the hour in the future starting from the current hour (00) and 'BN' is the number of the registered battery.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
attr <name> ctrlNextHoursSoCForecastReadings 00,03,12,18 <br>
|
||
# creates readings for the current hour (00) and the following hours +03, +12 and +18.
|
||
</ul>
|
||
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlShowLink"></a>
|
||
<li><b>ctrlShowLink </b><br>
|
||
Display of the link to the detailed view of the device above the graphic area. <br>
|
||
(default: 1)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlSolCastAPImaxReq"></a>
|
||
<li><b>ctrlSolCastAPImaxReq </b><br>
|
||
(only when using Model SolCastAPI) <br><br>
|
||
|
||
The setting of the maximum possible daily requests to the SolCast API. <br>
|
||
This value is specified by SolCast and may change according to the SolCast
|
||
license model. <br>
|
||
(default: 50)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlSolCastAPIoptimizeReq"></a>
|
||
<li><b>ctrlSolCastAPIoptimizeReq </b><br>
|
||
(only when using Model SolCastAPI) <br><br>
|
||
|
||
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. <br>
|
||
(default: 0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlSpecialReadings"></a>
|
||
<li><b>ctrlSpecialReadings </b><br>
|
||
Readings are created for the selected key figures and indicators with the
|
||
naming scheme 'special_<indicator>'. Selectable key figures / indicators are: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>BatPowerIn_Sum</b> </td><td>the sum of the current battery charging power of all defined battery devices </td></tr>
|
||
<tr><td> <b>BatPowerOut_Sum</b> </td><td>the sum of the current battery discharge power of all defined battery devices </td></tr>
|
||
<tr><td> <b>allStringsFullfilled</b> </td><td>Fulfillment status of error-free generation of all strings </td></tr>
|
||
<tr><td> <b>conForecastTillNextSunrise</b> </td><td>Consumption forecast from current hour to the coming sunrise </td></tr>
|
||
<tr><td> <b>currentAPIinterval</b> </td><td>the current polling interval of the selected radiation data API in seconds </td></tr>
|
||
<tr><td> <b>currentRunMtsConsumer_XX</b> </td><td>the running time (minutes) of the consumer "XX" since the last switch-on. (last running cycle) </td></tr>
|
||
<tr><td> <b>dayAfterTomorrowPVforecast</b> </td><td>provides the forecast of PV generation for the day after tomorrow (if available) without autocorrection (raw data) </td></tr>
|
||
<tr><td> <b>daysUntilBatteryCare_XX</b> </td><td>Days until the next battery XX maintenance (reaching the charge 'maxSoC' from attribute ctrlBatSocManagementXX) </td></tr>
|
||
<tr><td> <b>lastretrieval_time</b> </td><td>the last retrieval time of the selected radiation data API </td></tr>
|
||
<tr><td> <b>lastretrieval_timestamp</b> </td><td>the timestamp of the last retrieval time of the selected radiation data API </td></tr>
|
||
<tr><td> <b>response_message</b> </td><td>the last status message of the selected radiation data API </td></tr>
|
||
<tr><td> <b>runTimeAvgDayConsumer_XX</b> </td><td>the average running time (minutes) of consumer "XX" on one day </td></tr>
|
||
<tr><td> <b>runTimeCentralTask</b> </td><td>the runtime of the last SolarForecast interval (total process) in seconds </td></tr>
|
||
<tr><td> <b>runTimeTrainAI</b> </td><td>the runtime of the last AI training cycle in seconds </td></tr>
|
||
<tr><td> <b>runTimeLastAPIAnswer</b> </td><td>the last response time of the radiation data API retrieval to a request in seconds </td></tr>
|
||
<tr><td> <b>runTimeLastAPIProc</b> </td><td>the last process time for processing the received radiation data API data </td></tr>
|
||
<tr><td> <b>SunMinutes_Remain</b> </td><td>the remaining minutes until sunset of the current day </td></tr>
|
||
<tr><td> <b>SunHours_Remain</b> </td><td>the remaining hours until sunset of the current day </td></tr>
|
||
<tr><td> <b>todayConsumption</b> </td><td>the energy consumption of the house on the current day </td></tr>
|
||
<tr><td> <b>todayConsumptionForecast</b> </td><td>Consumption forecast per hour of the current day (01-24) </td></tr>
|
||
<tr><td> <b>todayConForecastTillSunset</b> </td><td>Consumption forecast from current hour to hour before sunset </td></tr>
|
||
<tr><td> <b>todayDoneAPIcalls</b> </td><td>the number of radiation data API calls executed on the current day </td></tr>
|
||
<tr><td> <b>todayDoneAPIrequests</b> </td><td>the number of radiation data API requests executed on the current day </td></tr>
|
||
<tr><td> <b>todayGridConsumption</b> </td><td>the energy drawn from the public grid on the current day </td></tr>
|
||
<tr><td> <b>todayGridFeedIn</b> </td><td>PV energy fed into the public grid on the current day </td></tr>
|
||
<tr><td> <b>todayMaxAPIcalls</b> </td><td>the maximum possible number of radiation data API calls. </td></tr>
|
||
<tr><td> </td><td>A call can contain multiple API requests. </td></tr>
|
||
<tr><td> <b>todayRemainingAPIcalls</b> </td><td>the number of radiation data API calls still possible on the current day </td></tr>
|
||
<tr><td> <b>todayRemainingAPIrequests</b> </td><td>the number of radiation data API requests still possible on the current day </td></tr>
|
||
<tr><td> <b>todayBatIn_XX</b> </td><td>the energy charged into the battery XX on the current day </td></tr>
|
||
<tr><td> <b>todayBatInSum</b> </td><td>Total energy charged in all batteries on the current day </td></tr>
|
||
<tr><td> <b>todayBatOut_XX</b> </td><td>the energy taken from the battery XX on the current day </td></tr>
|
||
<tr><td> <b>todayBatOutSum</b> </td><td>Total energy drawn from all batteries on the current day </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlUserExitFn"></a>
|
||
<li><b>ctrlUserExitFn {<Code>} </b><br>
|
||
After each cycle (see the <a href="#SolarForecast-attr-ctrlInterval">ctrlInterval </a> attribute), the code given
|
||
in this attribute is executed. The code is to be enclosed in curly brackets {...}. <br>
|
||
The code is passed the variables <b>$name</b> and <b>$hash</b>, which contain the name of the SolarForecast
|
||
device and its hash. <br>
|
||
In the SolarForecast Device, readings can be created and modified using the <b>storeReading</b> function.
|
||
<br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
{ <br>
|
||
my $batdev = (split " ", AttrVal ($name, 'setupBatteryDev01', ''))[0]; <br>
|
||
my $pvfc = ReadingsNum ($name, 'RestOfDayPVforecast', 0); <br>
|
||
my $cofc = ReadingsNum ($name, 'RestOfDayConsumptionForecast', 0); <br>
|
||
my $diff = $pvfc - $cofc; <br>
|
||
<br>
|
||
storeReading ('userFn_Battery_device', $batdev); <br>
|
||
storeReading ('userFn_estimated_surplus', $diff); <br>
|
||
}
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-flowGraphicControl"></a>
|
||
<li><b>flowGraphicControl <Key1=Value1> <Key2=Value2> ... </b><br>
|
||
By optionally specifying the key=value pairs listed below, various display properties of the energy flow
|
||
graph can be influenced. <br>
|
||
The entry can be made in several lines.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="26%"> <col width="74%"> </colgroup>
|
||
<tr><td> <b>animate</b> </td><td> Animates the energy flow graphic if displayed. (<a href="#SolarForecast-attr-graphicSelect">graphicSelect</a>) </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Animation off, <b>1</b> - Animation on, default: 1 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>consumerdist</b> </td><td>Controls the distance between the consumer icons. </td></tr>
|
||
<tr><td> </td><td>Value: <b>80 ... 500</b>, default: 130 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>h2consumerdist</b> </td><td>Extension of the vertical distance between the house and the consumer icons. </td></tr>
|
||
<tr><td> </td><td>Value: <b>0 ... 999</b>, default: 0 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>shiftx</b> </td><td>Horizontal shift of the energy flow graph. </td></tr>
|
||
<tr><td> </td><td>Value: <b>-80 ... 80</b>, default: 0 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>shifty</b> </td><td>Vertical shift of the energy flow chart. </td></tr>
|
||
<tr><td> </td><td>Wert: <b>Integer</b>, default: 0 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>showconsumer</b> </td><td>Display of consumers in the energy flow chart. </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Display off, <b>1</b> - Display on, default: 1 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>showconsumerdummy</b> </td><td>Controls the display of the dummy consumer. The dummy consumer is assigned the </td></tr>
|
||
<tr><td> </td><td>energy consumption that cannot be assigned to other consumers. </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Display off, <b>1</b> - Display on, default: 1 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>showconsumerpower</b> </td><td>Controls the display of the consumers' energy consumption. </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Display off, <b>1</b> - Display on, default: 1 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>showconsumerremaintime</b> </td><td>Controls the display of the remaining running time (minutes) of the loads. </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Display off, <b>1</b> - Display on, default: 1 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>size </b> </td><td>Size of the energy flow graphic in pixels if displayed. (<a href="#SolarForecast-attr-graphicSelect">graphicSelect</a>) </td></tr>
|
||
<tr><td> </td><td>Value: <b>Integer</b>, default: 400 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>strokecolina </b> </td><td>Color of an inactive line </td></tr>
|
||
<tr><td> </td><td>Value: <b>Hex (e.g. #cc3300) or designation (e.g. red, blue)</b>, default: gray </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>strokecolsig </b> </td><td>Color of an active signal line </td></tr>
|
||
<tr><td> </td><td>Value: <b>Hex (e.g. #cc3300) or designation (e.g. red, blue)</b>, default: red </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>strokecolstd </b> </td><td>Color of an active standard line </td></tr>
|
||
<tr><td> </td><td>Value: <b>Hex (e.g. #cc3300) or designation (e.g. red, blue)</b>, default: darkorange </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>strokewidth </b> </td><td>Width of the lines </td></tr>
|
||
<tr><td> </td><td>Value: <b>Integer</b>, default: 25 </td></tr>
|
||
</table>
|
||
</ul>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
attr <name> flowGraphicControl size=300 animate=0 consumerdist=100 showconsumer=1 showconsumerdummy=0 shiftx=-20 strokewidth=15 strokecolstd=#99cc00
|
||
</ul>
|
||
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam1Color"></a>
|
||
<li><b>graphicBeam1Color </b><br>
|
||
Color selection of the primary bar of the first level. <br>
|
||
(default: FFAC63)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam1FontColor"></a>
|
||
<li><b>graphicBeam1FontColor </b><br>
|
||
Selection of the font color of the primary bar of the first level. <br>
|
||
(default: 0D0D0D)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam2Color"></a>
|
||
<li><b>graphicBeam2Color </b><br>
|
||
Color selection of the secondary bars of the first level. <br>
|
||
(default: C4C4A7)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam2FontColor"></a>
|
||
<li><b>graphicBeam2FontColor </b><br>
|
||
Selection of the font color of the secondary bars of the first level. <br>
|
||
(default: 000000)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam3Color"></a>
|
||
<li><b>graphicBeam3Color </b><br>
|
||
Color selection of the primary bars of the second level. <br>
|
||
(default: BED6C0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam3FontColor"></a>
|
||
<li><b>graphicBeam3FontColor </b><br>
|
||
Selection of the font color of the primary bars of the second level. <br>
|
||
(default: 000000)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam4Color"></a>
|
||
<li><b>graphicBeam4Color </b><br>
|
||
Color selection of the secondary bars of the second level. <br>
|
||
(default: DBDBD0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam4FontColor"></a>
|
||
<li><b>graphicBeam4FontColor </b><br>
|
||
Selection of the font color of the secondary bars of the second level. <br>
|
||
(default: 000000)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeamXContent" data-pattern="graphicBeam.*Content"></a>
|
||
<li><b>graphicBeamXContent </b><br>
|
||
Defines the content of the bars to be displayed in the bar charts.
|
||
The bar charts are available in two levels. <br>
|
||
Level 1 is preset by default.
|
||
The content is determined by the attributes graphicBeam1Content and graphicBeam2Content. <br>
|
||
Level 2 can be activated by setting the attributes graphicBeam3Content and graphicBeam4Content. <br>
|
||
The attributes graphicBeam1Content and graphicBeam3Content represent the primary beams, the attributes
|
||
graphicBeam2Content and graphicBeam4Content attributes represent the secondary beams of the
|
||
respective level.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>batsocforecast_XX</b> </td><td>the predicted and historically achieved SOC (%) of the battery XX </td></tr>
|
||
<tr><td> <b>consumption</b> </td><td>Energy consumption </td></tr>
|
||
<tr><td> <b>consumptionForecast</b> </td><td>forecasted energy consumption </td></tr>
|
||
<tr><td> <b>energycosts</b> </td><td>Cost of energy purchased from the grid. The currency is defined in the setupMeterDev, key conprice. </td></tr>
|
||
<tr><td> <b>feedincome</b> </td><td>Remuneration for feeding into the grid. The currency is defined in the setupMeterDev, key feedprice. </td></tr>
|
||
<tr><td> <b>gridconsumption</b> </td><td>Energy purchase from the public grid </td></tr>
|
||
<tr><td> <b>gridfeedin</b> </td><td>Feed into the public grid </td></tr>
|
||
<tr><td> <b>pvForecast</b> </td><td>predicted PV generation (default for graphicBeam2Content) </td></tr>
|
||
<tr><td> <b>pvReal</b> </td><td>real PV generation (default for graphicBeam1Content) </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Hinweis:</b> The selection of the parameters energycosts and feedincome only makes sense if the optional keys
|
||
conprice and feedprice are set in setupMeterDev.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeamHeightLevelX" data-pattern="graphicBeamHeightLevel.*"></a>
|
||
<li><b>graphicBeamHeightLevelX <value> </b><br>
|
||
Multiplier for determining the maximum bar height of the respective level. <br>
|
||
In conjunction with the attribute <a href=“#SolarForecast-attr-graphicHourCount”>graphicHourCount</a>
|
||
this can also be used to generate very small graphic outputs. <br>
|
||
(default: 200)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeamWidth"></a>
|
||
<li><b>graphicBeamWidth <value> </b><br>
|
||
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. <br>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicEnergyUnit"></a>
|
||
<li><b>graphicEnergyUnit <Wh | kWh> </b><br>
|
||
Defines the unit for displaying the electrical power in the graph. The kilowatt hour is rounded to one
|
||
decimal place. <br>
|
||
(default: Wh)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHeaderDetail"></a>
|
||
<li><b>graphicHeaderDetail </b><br>
|
||
Selection of the zones of the graphic header to be displayed. <br>
|
||
(default: all)
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>all</b> </td><td>all zones of the head area (default) </td></tr>
|
||
<tr><td> <b>co</b> </td><td>show consumption range </td></tr>
|
||
<tr><td> <b>pv</b> </td><td>show creation area </td></tr>
|
||
<tr><td> <b>own</b> </td><td>user zone (see <a href="#SolarForecast-attr-graphicHeaderOwnspec">graphicHeaderOwnspec</a>) </td></tr>
|
||
<tr><td> <b>status</b> </td><td>status information area </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHeaderOwnspec"></a>
|
||
<li><b>graphicHeaderOwnspec <Label>:<Reading>[@Device] <Label>:<Set>[@Device] <Label>:<Attr>[@Device] ... </b> <br><br>
|
||
|
||
Display of any readings, set commands and attributes of the device in the graphic header. <br>
|
||
Readings, set commands and attributes of other devices can be displayed by specifying the optional [@Device]. <br>
|
||
The values to be displayed are separated by spaces.
|
||
Four values (fields) are displayed per line. <br>
|
||
The input can be made in multiple lines. Values with the units "Wh" or "kWh" are converted according to the
|
||
setting of the attribute <a href="#SolarForecast-attr-graphicEnergyUnit">graphicEnergyUnit</a>.
|
||
<br><br>
|
||
|
||
Each value is to be defined by a label and the corresponding reading connected by ":". <br>
|
||
Spaces in the label are to be inserted by "&nbsp;", a line break by "<br>". <br>
|
||
An empty field in a line is created by ":". <br>
|
||
A line title can be inserted by specifying "#:<Text>", an empty title by entering "#".
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
<table>
|
||
<colgroup> <col width="33%"> <col width="67%"> </colgroup>
|
||
<tr><td> attr <name> graphicHeaderOwnspec </td><td># </td></tr>
|
||
<tr><td> </td><td>AutarkyRate:Current_AutarkyRate </td></tr>
|
||
<tr><td> </td><td>Surplus:Current_Surplus </td></tr>
|
||
<tr><td> </td><td>current&nbsp;Gridconsumption:Current_GridConsumption </td></tr>
|
||
<tr><td> </td><td>: </td></tr>
|
||
<tr><td> </td><td># </td></tr>
|
||
<tr><td> </td><td>CO&nbsp;until&nbsp;sunset:special_todayConForecastTillSunset </td></tr>
|
||
<tr><td> </td><td>PV&nbsp;Day&nbsp;after&nbsp;tomorrow:special_dayAfterTomorrowPVforecast </td></tr>
|
||
<tr><td> </td><td>: </td></tr>
|
||
<tr><td> </td><td>: </td></tr>
|
||
<tr><td> </td><td>#Battery </td></tr>
|
||
<tr><td> </td><td>in&nbsp;today:special_todayBatIn </td></tr>
|
||
<tr><td> </td><td>out&nbsp;today:special_todayBatOut </td></tr>
|
||
<tr><td> </td><td>: </td></tr>
|
||
<tr><td> </td><td>: </td></tr>
|
||
<tr><td> </td><td>#Settings </td></tr>
|
||
<tr><td> </td><td>Autocorrection:pvCorrectionFactor_Auto : : : </td></tr>
|
||
<tr><td> </td><td>Consumer<br>Replanning:consumerNewPlanning : : : </td></tr>
|
||
<tr><td> </td><td>Consumer<br>Quickstart:consumerImmediatePlanning : : : </td></tr>
|
||
<tr><td> </td><td>Weather:graphicShowWeather : : : </td></tr>
|
||
<tr><td> </td><td>History:graphicHistoryHour : : : </td></tr>
|
||
<tr><td> </td><td>ShowNight:graphicShowNight : : : </td></tr>
|
||
<tr><td> </td><td>Debug:ctrlDebug : : : </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHeaderOwnspecValForm"></a>
|
||
<li><b>graphicHeaderOwnspecValForm </b> <br><br>
|
||
|
||
The readings to be displayed with the attribute
|
||
<a href="#SolarForecast-attr-graphicHeaderOwnspec">graphicHeaderOwnspec</a> can be manipulated with sprintf and
|
||
other Perl operations. <br>
|
||
There are two basic notation options that cannot be combined with each other. <br>
|
||
The notations are always specified within two curly brackets {...}.
|
||
<br><br>
|
||
|
||
<b>Notation 1: </b> <br>
|
||
A simple formatting of readings of your own device with sprintf is carried out as shown in line
|
||
'Current_AutarkyRate' or 'Current_GridConsumption'. <br>
|
||
Other Perl operations are to be bracketed with (). The respective readings values and units are available via
|
||
the variables $VALUE and $UNIT. <br>
|
||
Readings of other devices are specified by '<Device>.<Reading>'.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td>{ </td><td> </td></tr>
|
||
<tr><td> 'Current_AutarkyRate' </td><td> => "%.1f %%", </td></tr>
|
||
<tr><td> 'Current_GridConsumption' </td><td> => "%.2f $UNIT", </td></tr>
|
||
<tr><td> 'SMA_Energymeter.Cover_RealPower' </td><td> => q/($VALUE)." W"/, </td></tr>
|
||
<tr><td> 'SMA_Energymeter.L2_Cover_RealPower' </td><td> => "($VALUE).' W'", </td></tr>
|
||
<tr><td> 'SMA_Energymeter.L1_Cover_RealPower' </td><td> => '(sprintf "%.2f", ($VALUE / 1000))." kW"', </td></tr>
|
||
<tr><td>} </td><td> </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Notation 2: </b> <br>
|
||
The manipulation of reading values and units is done via Perl If ... else structures. <br>
|
||
The device, reading, reading value and unit are available to the structure with the variables $DEVICE, $READING,
|
||
$VALUE and $UNIT. <br>
|
||
If the variables are changed, the new values are transferred to the display accordingly.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="5%"> <col width="95%"> </colgroup>
|
||
<tr><td>{ </td><td> </td></tr>
|
||
<tr><td> </td><td> if ($READING eq 'Current_AutarkyRate') { </td></tr>
|
||
<tr><td> </td><td> $VALUE = sprintf "%.1f", $VALUE; </td></tr>
|
||
<tr><td> </td><td> $UNIT = "%"; </td></tr>
|
||
<tr><td> </td><td> } </td></tr>
|
||
<tr><td> </td><td> elsif ($READING eq 'Current_GridConsumption') { </td></tr>
|
||
<tr><td> </td><td> ... </td></tr>
|
||
<tr><td> </td><td> } </td></tr>
|
||
<tr><td>} </td><td> </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHeaderShow"></a>
|
||
<li><b>graphicHeaderShow </b><br>
|
||
Show/hide the graphic table header with forecast data and certain current and
|
||
statistical values. <br>
|
||
(default: 1)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHistoryHour"></a>
|
||
<li><b>graphicHistoryHour </b><br>
|
||
Number of previous hours displayed in the bar graph. <br>
|
||
(default: 2)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHourCount"></a>
|
||
<li><b>graphicHourCount <4...24> </b><br>
|
||
Number of bars/hours in the bar graph. <br>
|
||
(default: 24)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHourStyle"></a>
|
||
<li><b>graphicHourStyle </b><br>
|
||
Format of the time in the bar graph. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="30%"> <col width="70%"> </colgroup>
|
||
<tr><td> <b>nicht gesetzt</b> </td><td>hours only without minutes (default) </td></tr>
|
||
<tr><td> <b>:00</b> </td><td>Hours as well as minutes in two digits, e.g. 10:00 </td></tr>
|
||
<tr><td> <b>:0</b> </td><td>Hours as well as minutes single-digit, e.g. 8:0 </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicLayoutType"></a>
|
||
<li><b>graphicLayoutType <single | double | diff> </b><br>
|
||
Layout of the bar graph. <br>
|
||
The content of the bars to be displayed is determined by the <b>graphicBeam1Content</b> or
|
||
<b>graphicBeam2Content</b> attributes.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="10%"> <col width="90%"> </colgroup>
|
||
<tr><td> <b>double</b> </td><td>displays the primary bar and the secondary bar (default) </td></tr>
|
||
<tr><td> <b>single</b> </td><td>displays only the primary bar </td></tr>
|
||
<tr><td> <b>diff</b> </td><td>difference display. It is valid: <Difference> = <Value primary bar> - <Value secondary bar> </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicSelect"></a>
|
||
<li><b>graphicSelect </b><br>
|
||
Selects the graphic segments of the module to be displayed.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>both</b> </td><td>displays the header, consumer legend, energy flow and prediction graph (default) </td></tr>
|
||
<tr><td> <b>flow</b> </td><td>displays the header, the consumer legend and energy flow graphic </td></tr>
|
||
<tr><td> <b>forecast</b> </td><td>displays the header, the consumer legend and the prediction graphic </td></tr>
|
||
<tr><td> <b>none</b> </td><td>displays only the header and the consumer legend </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicShowDiff"></a>
|
||
<li><b>graphicShowDiff [no | top | bottom] </b><br>
|
||
Additional display of the difference “<primary bar content> - <secondary bar content>” in the header or
|
||
footer of the bar chart. <br>
|
||
(default: no)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicShowNight"></a>
|
||
<li><b>graphicShowNight </b><br>
|
||
Display or hide the night hours in the bar chart.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="5%"> <col width="95%"> </colgroup>
|
||
<tr><td> <b>0</b> </td><td>no display of night hours if no value is to be displayed (default) </td></tr>
|
||
<tr><td> </td><td>If the selected content contains a value, these bars are still displayed. </td></tr>
|
||
<tr><td> <b>01</b> </td><td>Like ‘0’, but time synchronisation takes place between the level 1 </td></tr>
|
||
<tr><td> </td><td>and the subsequent bar chart level. </td></tr>
|
||
<tr><td> <b>1</b> </td><td>the night hours are always displayed </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicShowWeather"></a>
|
||
<li><b>graphicShowWeather </b><br>
|
||
Show/hide weather icons in the bar graph. <br>
|
||
(default: 1)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicSpaceSize"></a>
|
||
<li><b>graphicSpaceSize <value> </b><br>
|
||
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. <br>
|
||
(default: 24)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicWeatherColor"></a>
|
||
<li><b>graphicWeatherColor </b><br>
|
||
Color of the weather icons in the bar graph for the daytime hours.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicWeatherColorNight"></a>
|
||
<li><b>graphicWeatherColorNight </b><br>
|
||
Color of the weather icons for the night hours.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupBatteryDev" data-pattern="setupBatteryDev.*"></a>
|
||
<li><b>setupBatteryDevXX <Battery Device Name> pin=<Readingname>:<Unit> pout=<Readingname>:<Unit>
|
||
cap=<Option> [intotal=<Readingname>:<Unit>] [outtotal=<Readingname>:<Unit>]
|
||
[charge=<Readingname>] [asynchron=<Option>] [show=<Option>] <br>
|
||
[[icon=<recomm>@<Color>]:[<charge>@<Color>]:[<discharge>@<Color>]:[<omit>@<Color>]] </b> <br><br>
|
||
|
||
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.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>pin</b> </td><td>Reading which provides the current battery charging power </td></tr>
|
||
<tr><td> <b>pout</b> </td><td>Reading which provides the current battery discharge rate </td></tr>
|
||
<tr><td> <b>intotal</b> </td><td>Reading which provides the total battery charge as a continuous counter (optional) </td></tr>
|
||
<tr><td> <b>outtotal</b> </td><td>Reading which provides the total battery discharge as a continuous counter (optional) </td></tr>
|
||
<tr><td> <b>cap</b> </td><td>installed battery capacity. Option can be: </td></tr>
|
||
<tr><td> </td><td><b>numerical value</b> - direct specification of the battery capacity in Wh without specifying the unit! </td></tr>
|
||
<tr><td> </td><td><b><Readingname>:<unit></b> - Reading which provides the capacity and unit (Wh, kWh) </td></tr>
|
||
<tr><td> <b>charge</b> </td><td>Reading which provides the current state of charge (SOC in percent) (optional) </td></tr>
|
||
<tr><td> <b>Unit</b> </td><td>the respective unit (W,Wh,kW,kWh) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>icon</b> </td><td>Icon and/or (only) colour for displaying the battery in the bar chart (optional) </td></tr>
|
||
<tr><td> </td><td>The colour can be specified as an identifier (e.g. blue) or HEX value (e.g. #d9d9d9). </td></tr>
|
||
<tr><td> </td><td><b><recomm></b> - Charging is recommended but inactive (no charging or discharging) </td></tr>
|
||
<tr><td> </td><td><b><charge></b> - is used when the battery is currently being charged </td></tr>
|
||
<tr><td> </td><td><b><discharge></b> - is used when the battery is currently being discharged </td></tr>
|
||
<tr><td> </td><td><b><omit></b> - is used when charging is not recommended </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>show</b> </td><td>Control of the battery display in the bar graph (optional) </td></tr>
|
||
<tr><td> </td><td><b>0</b> - no display of the device (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - Display of the device in the bar chart level 1 </td></tr>
|
||
<tr><td> </td><td><b>2</b> - Display of the device in the bar chart level 2 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>asynchron</b> </td><td>Data collection mode according to the ctrlInterval setting (synchronous) or additionally by </td></tr>
|
||
<tr><td> </td><td>event processing (asynchronous). </td></tr>
|
||
<tr><td> </td><td><b>0</b> - no data collection after receiving an event from the device (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - trigger a data collection when an event is received from the device </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Special cases:</b> If the reading for pin and pout should be identical but signed,
|
||
the keys pin and pout can be defined as follows: <br><br>
|
||
<ul>
|
||
pin=-pout (a negative value of pout is used as pin) <br>
|
||
pout=-pin (a negative value of pin is used as pout)
|
||
</ul>
|
||
<br>
|
||
|
||
The unit is omitted in the particular special case. <br><br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
attr <name> setupBatteryDev01 BatDummy pin=BatVal:W pout=-pin intotal=BatInTot:Wh outtotal=BatOutTot:Wh cap=BatCap:kWh icon=measure_battery_50@#262626:@yellow:measure_battery_100@red
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Note:</b> Deleting the attribute also removes the internally corresponding data.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupInverterDev" data-pattern="setupInverterDev.*"></a>
|
||
<li><b>setupInverterDevXX <Inverter Device Name> pv=<Readingname>:<Unit> etotal=<Readingname>:<Unit>
|
||
capacity=<max. WR-Leistung> [strings=<String1>,<String2>,...] [asynchron=<Option>]
|
||
[feed=<Delivery type>] [limit=<0..100>]
|
||
[icon=<Day>[@<Color>][:<Night>[@<Color>]]] </b> <br><br>
|
||
|
||
Defines any inverter device or solar charger and its readings to supply the current PV generation values. <br>
|
||
A solar charger does not convert the energy supplied by the solar cells into alternating current,
|
||
but instead directly charges an existing battery <br>
|
||
(e.g. a Victron SmartSolar MPPT). <br>
|
||
Several devices can be defined one after the other in the setupInverterDev01..XX attributes. <br>
|
||
This can also be a dummy device with corresponding readings. <br>
|
||
The values of several inverters can be combined in a dummy device, for example, and this device can
|
||
be specified with the corresponding readings. <br>
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>pv</b> </td><td>Reading which provides the current PV generation as a positive value </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>etotal</b> </td><td>Reading which provides the total PV energy generated (a steadily increasing counter). </td></tr>
|
||
<tr><td> </td><td>If the reading violates the specification of a continuously rising counter, </td></tr>
|
||
<tr><td> </td><td>SolarForecast handles this error and reports the situation by means of a log message. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>Einheit</b> </td><td>the respective unit (W,kW,Wh,kWh) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>capacity</b> </td><td>Rated power of the inverter according to data sheet, i.e. max. possible output in Watts </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>strings</b> </td><td>Comma-separated list of the strings assigned to the inverter (optional). The string names </td></tr>
|
||
<tr><td> </td><td>are defined in the <a href=“#SolarForecast-attr-setupInverterStrings”>setupInverterStrings</a> attribute. </td></tr>
|
||
<tr><td> </td><td>If 'strings' is not specified, all defined string names are assigned to the inverter. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>feed</b> </td><td>Defines special properties of the device's energy supply (optional). </td></tr>
|
||
<tr><td> </td><td>If the key is not set, the device feeds the PV energy into the house's AC grid. </td></tr>
|
||
<tr><td> </td><td><b>bat</b> - Solar charger for direct battery charging. Any surplus is fed into the inverter/house network. </td></tr>
|
||
<tr><td> </td><td><b>grid</b> - the energy is fed exclusively into the public grid </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>limit</b> </td><td>Defines any active power limitation in % (optional). </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>icon</b> </td><td>Icon for displaying the inverter in the flow chart (optional) </td></tr>
|
||
<tr><td> </td><td><b><Day></b> - Icon and optional color for activity after sunrise </td></tr>
|
||
<tr><td> </td><td><b><Night></b> - Icon and optional color after sunset, otherwise the moon phase is displayed </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>asynchron</b> </td><td>Data collection mode according to the ctrlInterval setting (synchronous) or additionally by </td></tr>
|
||
<tr><td> </td><td>event processing (asynchronous). (optional) </td></tr>
|
||
<tr><td> </td><td><b>0</b> - no data collection after receiving an event from the device (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - trigger a data collection when an event is received from the device </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
attr <name> setupInverterDev01 STP5000 pv=total_pac:kW etotal=etotal:kWh capacity=5000 asynchron=1 strings=Garage icon=inverter@red:solar
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Note:</b> Deleting the attribute also removes the internally corresponding data.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupInverterStrings"></a>
|
||
<li><b>setupInverterStrings <Stringname1>[,<Stringname2>,<Stringname3>,...] </b> <br><br>
|
||
|
||
Designations of the active strings. These names are used as keys in the further
|
||
settings. <br>
|
||
When using an AI based API (e.g. VictronKI API) only "<b>KI-based</b>" has to be entered regardless of
|
||
which real strings exist. <br><br>
|
||
|
||
<ul>
|
||
<b>Examples: </b> <br>
|
||
attr <name> setupInverterStrings eastroof,southgarage,S3 <br>
|
||
attr <name> setupInverterStrings KI-based <br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupMeterDev"></a>
|
||
<li><b>setupMeterDev <Meter Device Name> gcon=<Readingname>:<Unit> contotal=<Readingname>:<Unit>
|
||
gfeedin=<Readingname>:<Unit> feedtotal=<Readingname>:<Unit>
|
||
[conprice=<Field>] [feedprice=<Field>] [asynchron=<Option>] </b> <br><br>
|
||
|
||
Defines any device and its readings for measuring energy into or out of the public grid.
|
||
The module assumes that the numeric value of the readings is positive.
|
||
It can also be a dummy device with corresponding readings.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>gcon</b> </td><td>Reading which supplies the power currently drawn from the grid </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>contotal</b> </td><td>Reading which provides the sum of the energy drawn from the grid (a constantly increasing meter) </td></tr>
|
||
<tr><td> </td><td>If the counter is reset to '0' at the beginning of the day (daily counter), the module handles this situation accordingly.</td></tr>
|
||
<tr><td> </td><td>In this case, a message is displayed in the log with verbose 3. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>gfeedin</b> </td><td>Reading which supplies the power currently fed into the grid </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>feedtotal</b> </td><td>Reading which provides the sum of the energy fed into the grid (a constantly increasing meter) </td></tr>
|
||
<tr><td> </td><td>If the counter is reset to '0' at the beginning of the day (daily counter), the module handles this situation accordingly.</td></tr>
|
||
<tr><td> </td><td>In this case, a message is displayed in the log with verbose 3. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>Unit</b> </td><td>the respective unit (W,kW,Wh,kWh) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>conprice</b> </td><td>Price for the purchase of one kWh (optional). The <field> can be specified in one of the following variants: </td></tr>
|
||
<tr><td> </td><td><Price>:<Currency> - Price as a numerical value and its currency </td></tr>
|
||
<tr><td> </td><td><Reading>:<Currency> - Reading of the <b>meter device</b> that contains the price : Currency </td></tr>
|
||
<tr><td> </td><td><Device>:<Reading>:<Currency> - any device and reading containing the price : Currency </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>feedprice</b> </td><td>Remuneration for the feed-in of one kWh (optional). The <field> can be specified in one of the following variants: </td></tr>
|
||
<tr><td> </td><td><Remuneration>:<Currency> - Remuneration as a numerical value and its currency </td></tr>
|
||
<tr><td> </td><td><Reading>:<Currency> - Reading of the <b>meter device</b> that contains the remuneration : Currency </td></tr>
|
||
<tr><td> </td><td><Device>:<Reading>:<Currency> - any device and reading containing the remuneration : Currency </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>asynchron</b> </td><td>Data collection mode according to the ctrlInterval setting (synchronous) or additionally by </td></tr>
|
||
<tr><td> </td><td>event processing (asynchronous). </td></tr>
|
||
<tr><td> </td><td><b>0</b> - no data collection after receiving an event from the device (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - trigger a data collection when an event is received from the device </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Special cases:</b> If the reading for gcon and gfeedin should be identical but signed,
|
||
the keys gfeedin and gcon can be defined as follows: <br><br>
|
||
<ul>
|
||
gfeedin=-gcon (a negative value of gcon is used as gfeedin) <br>
|
||
gcon=-gfeedin (a negative value of gfeedin is used as gcon)
|
||
</ul>
|
||
<br>
|
||
|
||
The unit is omitted in the particular special case. <br><br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
attr <name> setupMeterDev Meter gcon=Wirkleistung:W contotal=BezWirkZaehler:kWh gfeedin=-gcon feedtotal=EinWirkZaehler:kWh conprice=powerCost:€ feedprice=0.1269:€
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Note:</b> Deleting the attribute also removes the internally corresponding data.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupOtherProducer" data-pattern="setupOtherProducer.*"></a>
|
||
<li><b>setupOtherProducerXX <Device Name> pcurr=<Readingname>:<Unit> etotal=<Readingname>:<Unit> [icon=<Icon>[@<Color>]] </b> <br><br>
|
||
|
||
Defines any device and its readings for the delivery of other generation values
|
||
(e.g. CHP, wind generation, emergency generator). This device is not intended for PV generation.
|
||
It can also be a dummy device with corresponding readings.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>icon</b> </td><td>Icon and, if applicable, color for activity to display the producer in the flow chart (optional) </td></tr>
|
||
<tr><td> <b>pcurr</b> </td><td>Reading which returns the current generation as a positive value or a self-consumption (special case) as a negative value </td></tr>
|
||
<tr><td> <b>etotal</b> </td><td>Reading which supplies the total energy generated (a continuously ascending counter) </td></tr>
|
||
<tr><td> </td><td>If the reading violates the specification of a continuously rising counter, </td></tr>
|
||
<tr><td> </td><td>SolarForecast handles this error and reports the situation that has occurred with a log entry. </td></tr>
|
||
<tr><td> <b>Einheit</b> </td><td>the respective unit (W,kW,Wh,kWh) </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
attr <name> setupOtherProducer01 windwheel pcurr=total_pac:kW etotal=etotal:kWh icon=Ventilator_wind@darkorange
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Note:</b> Deleting the attribute also removes the internally corresponding data.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupRadiationAPI"></a>
|
||
<li><b>setupRadiationAPI </b> <br><br>
|
||
|
||
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. <br><br>
|
||
|
||
<b>Note:</b> If an OpenMeteo API is also set in the 'setupWeatherDev1' attribute, the settings of both attributes
|
||
are harmonized, whereby the setting of 'setupRadiationAPI' is leading. <br><br>
|
||
|
||
<b>OpenMeteoDWD-API</b> <br>
|
||
|
||
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
|
||
<a href='https://open-meteo.com/en/docs/dwd-api' target='_blank'>API Documentation</a> is available on
|
||
the service's website.
|
||
<br><br>
|
||
|
||
<b>OpenMeteoDWDEnsemble-API</b> <br>
|
||
|
||
This Open-Meteo API variant provides access to the DWD's global
|
||
<a href='https://www.dwd.de/DE/forschung/wettervorhersage/num_modellierung/04_ensemble_methoden/ensemble_vorhersage/ensemble_vorhersagen.html' target='_blank'>Ensemble Prediction System (EPS)</a>.
|
||
<br>
|
||
The ensemble models ICON-D2-EPS, ICON-EU-EPS and ICON-EPS are seamlessly combined. <br>
|
||
<a href='https://openmeteo.substack.com/p/ensemble-weather-forecast-api' target='_blank'>Ensemble weather forecasts</a> 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.
|
||
<br><br>
|
||
|
||
<b>OpenMeteoWorld-API</b> <br>
|
||
|
||
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.
|
||
<br><br>
|
||
|
||
<b>SolCast-API</b> <br>
|
||
|
||
API usage requires one or more API-keys (accounts) and one or more Rooftop-ID's in advance
|
||
created on the <a href='https://toolkit.solcast.com.au/rooftop-sites/' target='_blank'>SolCast</a> website.
|
||
A rooftop is equivalent to one <a href="#SolarForecast-attr-setupInverterStrings">setupInverterStrings</a>
|
||
in the SolarForecast context. <br>
|
||
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
|
||
<a href="#SolarForecast-attr-ctrlSolCastAPIoptimizeReq ">ctrlSolCastAPIoptimizeReq </a>.
|
||
<br><br>
|
||
|
||
<b>ForecastSolar-API</b> <br>
|
||
|
||
Free use of the <a href='https://doc.forecast.solar/start' target='_blank'>Forecast.Solar API</a>.
|
||
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. <br>
|
||
Note: Based on previous experience, unreliable and not recommended.
|
||
<br><br>
|
||
|
||
<b>VictronKI-API</b> <br>
|
||
|
||
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
|
||
<a href="#SolarForecast-attr-setupInverterStrings">setupInverterStrings</a>. <br>
|
||
In the Victron Energy VRM Portal, the location of the PV system must be specified as a prerequisite. <br>
|
||
See also the blog post
|
||
<a href="https://www.victronenergy.com/blog/2023/07/05/new-vrm-solar-production-forecast-feature/">Introducing Solar Production Forecast</a>.
|
||
<br><br>
|
||
|
||
<b>DWD_OpenData Device</b> <br>
|
||
|
||
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 <a href="http://fhem.de/commandref.html#DWD_OpenData">DWD_OpenData Commandref</a>). <br>
|
||
To obtain a good radiation forecast, a DWD station located near the plant site should be used. <br>
|
||
Unfortunately, not all
|
||
<a href="https://www.dwd.de/DE/leistungen/klimadatendeutschland/statliste/statlex_html.html;jsessionid=EC5F572A52EB69684D552DCF6198F290.live31092?view=nasPublication&nn=16102">DWD stations</a>
|
||
provide the required Rad1h values. <br>
|
||
Explanations of the stations are listed in
|
||
<a href="https://www.dwd.de/DE/leistungen/klimadatendeutschland/stationsliste.html">Stationslexikon</a>. <br>
|
||
At least the following attributes must be set in the selected DWD_OpenData Device: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>forecastDays</b> </td><td>1 (set it to >= 2 if you want longer prediction) </td></tr>
|
||
<tr><td> <b>forecastProperties</b> </td><td>Rad1h </td></tr>
|
||
<tr><td> <b>forecastResolution</b> </td><td>1 </td></tr>
|
||
<tr><td> <b>forecastStation</b> </td><td><Station code of the evaluated DWD station> </td></tr>
|
||
<tr><td> </td><td><b>Note:</b> The selected DWD station must provide radiation values (Rad1h Readings). </td></tr>
|
||
<tr><td> </td><td>Not all stations provide this data! </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupRoofTops"></a>
|
||
<li><b>setupRoofTops <Stringname1>=<pk> [<Stringname2>=<pk> <Stringname3>=<pk> ...] </b> <br>
|
||
(only when using Model SolCastAPI) <br><br>
|
||
|
||
The string "StringnameX" is assigned to a key <pk>. The key <pk> was created with the setter
|
||
<a href="#SolarForecast-set-roofIdentPair">roofIdentPair</a>. This is used to specify the rooftop ID and API key to
|
||
be used in the SolCast API. <br>
|
||
The StringnameX is a key value of the attribute <b>setupInverterStrings</b>.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Example: </b> <br>
|
||
attr <name> setupRoofTops eastroof=p1 southgarage=p2 S3=p3 <br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupStringPeak"></a>
|
||
<li><b>setupStringPeak <Stringname1>=<Peak> [<Stringname2>=<Peak> <Stringname3>=<Peak> ...] </b> <br><br>
|
||
|
||
The DC peak power of the string "StringnameX" in kWp. The string name is a key value of the
|
||
attribute <b>setupInverterStrings</b>. <br>
|
||
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 <b>KI-based</b>. <br><br>
|
||
|
||
<ul>
|
||
<b>Examples: </b> <br>
|
||
attr <name> setupStringPeak eastroof=5.1 southgarage=2.0 S3=7.2 <br>
|
||
attr <name> setupStringPeak KI-based=14.3 (for AI based API)<br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupWeatherDev" data-pattern="setupWeatherDev.*"></a>
|
||
<li><b>setupWeatherDevX </b> <br><br>
|
||
|
||
Specifies the device or API for providing the required weather data (cloud cover, precipitation, etc.).<br>
|
||
The attribute 'setupWeatherDev1' specifies the leading weather service and is mandatory.<br><br>
|
||
|
||
<b>Note:</b> If an OpenMeteo API is also set in the 'setupRadiationAPI' attribute, the settings of both attributes
|
||
are harmonized, whereby the setting of 'setupRadiationAPI' is leading. <br><br>
|
||
|
||
<b>OpenMeteoDWD-API</b> <br>
|
||
|
||
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
|
||
<a href='https://open-meteo.com/en/docs/dwd-api' target='_blank'>API Documentation</a> is available on
|
||
the service's website.
|
||
<br><br>
|
||
|
||
<b>OpenMeteoDWDEnsemble-API</b> <br>
|
||
|
||
This Open-Meteo API variant provides access to the DWD's global
|
||
<a href='https://www.dwd.de/DE/forschung/wettervorhersage/num_modellierung/04_ensemble_methoden/ensemble_vorhersage/ensemble_vorhersagen.html' target='_blank'>Ensemble Prediction System (EPS)</a>.
|
||
<br>
|
||
The ensemble models ICON-D2-EPS, ICON-EU-EPS and ICON-EPS are seamlessly combined. <br>
|
||
<a href='https://openmeteo.substack.com/p/ensemble-weather-forecast-api' target='_blank'>Ensemble weather forecasts</a> 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.
|
||
<br><br>
|
||
|
||
<b>OpenMeteoWorld-API</b> <br>
|
||
|
||
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.
|
||
<br><br>
|
||
|
||
<b>DWD Device</b> <br>
|
||
|
||
As an alternative to Open-Meteo, an FHEM 'DWD_OpenData' device can be used to supply the weather data.<br>
|
||
If no device of this type exists, at least one DWD_OpenData device must first be defined.
|
||
(see <a href="http://fhem.de/commandref.html#DWD_OpenData">DWD_OpenData Commandref</a>). <br>
|
||
If more than one setupWeatherDevX is specified, the average of all weather stations is determined
|
||
if the respective value was supplied and is numerical. <br>
|
||
Otherwise, the data from 'setupWeatherDev1' is always used as the leading weather device.<br>
|
||
At least these attributes must be set in the selected DWD_OpenData Device: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>forecastDays</b> </td><td>1 </td></tr>
|
||
<tr><td> <b>forecastProperties</b> </td><td>TTT,Neff,RR1c,ww,SunUp,SunRise,SunSet </td></tr>
|
||
<tr><td> <b>forecastResolution</b> </td><td>1 </td></tr>
|
||
<tr><td> <b>forecastStation</b> </td><td><Station code of the evaluated DWD station> </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Note:</b> If the latitude and longitude attributes are set in the global device, the sunrise and sunset
|
||
result from this information.
|
||
</li>
|
||
<br>
|
||
|
||
</ul>
|
||
</ul>
|
||
|
||
|
||
</ul>
|
||
=end html
|
||
=begin html_DE
|
||
|
||
<a id="SolarForecast"></a>
|
||
<h3>SolarForecast</h3>
|
||
<br>
|
||
|
||
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. <br>
|
||
|
||
Zur Erstellung der solaren Vorhersage kann das Modul SolarForecast unterschiedliche Dienste und Quellen nutzen: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="32%"> <col width="68%"> </colgroup>
|
||
<tr><td> <b>DWD</b> </td><td>solare Vorhersage basierend auf MOSMIX Daten des Deutschen Wetterdienstes </td></tr>
|
||
<tr><td> <b>SolCast-API </b> </td><td>verwendet Prognosedaten der <a href='https://toolkit.solcast.com.au/rooftop-sites/' target='_blank'>SolCast API</a> </td></tr>
|
||
<tr><td> <b>ForecastSolar-API</b> </td><td>verwendet Prognosedaten der <a href='https://doc.forecast.solar/api' target='_blank'>Forecast.Solar API</a> </td></tr>
|
||
<tr><td> <b>OpenMeteoDWD-API</b> </td><td>ICON-Wettermodelle des Deutschen Wetterdienstes (DWD) über <a href='https://open-meteo.com/en/docs/dwd-api' target='_blank'>Open-Meteo</a> </td></tr>
|
||
<tr><td> <b>OpenMeteoDWDEnsemble-API</b> </td><td>Zugang zum <a href='https://www.dwd.de/DE/forschung/wettervorhersage/num_modellierung/04_ensemble_methoden/ensemble_vorhersage/ensemble_vorhersagen.html' target='_blank'>globalen Ensemble-Vorhersagesystem (EPS)</a> des DWD </td></tr>
|
||
<tr><td> <b>OpenMeteoWorld-API</b> </td><td>vereint nahtlos Wettermodelle von Organisationen wie NOAA, DWD, CMCC und ECMWF über <a href='https://open-meteo.com/en/docs' target='_blank'>Open-Meteo</a> </td></tr>
|
||
<tr><td> <b>VictronKI-API</b> </td><td>Victron Energy API des <a href='https://www.victronenergy.com/blog/2023/07/05/new-vrm-solar-production-forecast-feature/' target='_blank'>VRM Portals</a> </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
Die Nutzung der erwähnten API's beschränkt sich auf die jeweils kostenlose Version des Dienstes. <br>
|
||
In Abhängigkeit vom verwendeten Model kann eine KI-Unterstützung aktiviert werden. <br><br>
|
||
|
||
Über die PV Erzeugungsprognose hinaus werden Verbrauchswerte bzw. Netzbezugswerte erfasst und für eine
|
||
Verbrauchsprognose verwendet. <br>
|
||
Das Modul errechnet aus den Prognosewerten einen zukünftigen Energieüberschuß der zur Betriebsplanung von Verbrauchern
|
||
genutzt wird. Weiterhin bietet das Modul eine <a href="#SolarForecast-Consumer">Consumer Integration</a> zur integrierten
|
||
Planung und Steuerung von PV Überschuß abhängigen Verbraucherschaltungen. Eine Unterstützung zum optimalen
|
||
Batterie SoC-Management gehört ebenfalls zum Funktionsumfang. <br><br>
|
||
|
||
Bei der ersten Definition des Moduls wird der Benutzer über eine Guided Procedure unterstützt um alle initial notwendigen Eingaben
|
||
vorzunehmen. <br>
|
||
Am Ende des Vorganges und nach relevanten Änderungen der Anlagen- bzw. Devicekonfiguration sollte unbedingt mit einem
|
||
<a href="#SolarForecast-set-plantConfiguration">set <name> plantConfiguration ceck</a>
|
||
die ordnungsgemäße Anlagenkonfiguration geprüft werden.
|
||
|
||
<ul>
|
||
<a id="SolarForecast-define"></a>
|
||
<b>Define</b>
|
||
<br><br>
|
||
|
||
<ul>
|
||
Ein SolarForecast Device wird erstellt mit: <br><br>
|
||
|
||
<ul>
|
||
<b>define <name> SolarForecast </b>
|
||
</ul>
|
||
<br>
|
||
|
||
Nach der Definition des Devices sind in Abhängigkeit der verwendeten Prognosequellen zwingend weitere
|
||
anlagenspezifische Angaben zu hinterlegen. <br>
|
||
Mit nachfolgenden Set-Kommandos und Attributen werden für die Funktion des Moduls maßgebliche Informationen
|
||
hinterlegt: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>setupWeatherDevX</b> </td><td>DWD_OpenData Device welches meteorologische Daten (z.B. Bewölkung) liefert </td></tr>
|
||
<tr><td> <b>setupRadiationAPI </b> </td><td>DWD_OpenData Device bzw. API zur Lieferung von Strahlungsdaten </td></tr>
|
||
<tr><td> <b>setupInverterDevXX</b> </td><td>Device welches PV Leistungsdaten liefert </td></tr>
|
||
<tr><td> <b>setupMeterDev</b> </td><td>Device welches Netz I/O-Daten liefert </td></tr>
|
||
<tr><td> <b>setupBatteryDevXX</b> </td><td>Device welches Batterie Leistungsdaten liefert (sofern vorhanden) </td></tr>
|
||
<tr><td> <b>setupInverterStrings</b> </td><td>Bezeichner der vorhandenen Anlagenstrings </td></tr>
|
||
<tr><td> <b>setupStringAzimuth</b> </td><td>Ausrichtung (Azimut) der Anlagenstrings </td></tr>
|
||
<tr><td> <b>setupStringPeak</b> </td><td>die DC-Peakleistung der Anlagenstrings </td></tr>
|
||
<tr><td> <b>roofIdentPair</b> </td><td>die Identifikationsdaten (bei Nutzung der SolCast API) </td></tr>
|
||
<tr><td> <b>setupRoofTops</b> </td><td>die Rooftop Parameter (bei Nutzung der SolCast API) </td></tr>
|
||
<tr><td> <b>setupStringDeclination</b> </td><td>die Neigungswinkel der Anlagenmodule </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
Um eine Anpassung an die persönliche Anlage zu ermöglichen, können Korrekturfaktoren manuell fest bzw. automatisiert
|
||
dynamisch angewendet werden.
|
||
<br><br>
|
||
</ul>
|
||
|
||
<a id="SolarForecast-Consumer"></a>
|
||
<b>Consumer Integration</b>
|
||
<br><br>
|
||
|
||
<ul>
|
||
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
|
||
<a href="#SolarForecast-attr-consumer">ConsumerXX-Attributen</a>. 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. <br>
|
||
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.
|
||
<br><br>
|
||
|
||
Zu diesem Zweck eignet sich ein Dummy Device nach diesem Muster:
|
||
<br><br>
|
||
|
||
<ul>
|
||
define SolCastDummy dummy <br>
|
||
attr SolCastDummy userattr nomPower <br>
|
||
attr SolCastDummy alias SolarForecast Consumer Dummy <br>
|
||
attr SolCastDummy cmdIcon on:remotecontrol/black_btn_GREEN off:remotecontrol/black_btn_RED <br>
|
||
attr SolCastDummy devStateIcon off:light_light_dim_100@grey on:light_light_dim_100@darkorange <br>
|
||
attr SolCastDummy group Solarprognose <br>
|
||
attr SolCastDummy icon solar_icon <br>
|
||
attr SolCastDummy nomPower 1000 <br>
|
||
attr SolCastDummy readingList BatIn BatOut BatVal BatInTot BatOutTot bezW einW Batcharge Temp automatic <br>
|
||
attr SolCastDummy room Energie,Testraum <br>
|
||
attr SolCastDummy setList BatIn BatOut BatVal BatInTot BatOutTot bezW einW Batcharge on off Temp <br>
|
||
attr SolCastDummy userReadings actpow {ReadingsVal ($name, 'state', 'off') eq 'on' ? AttrVal ($name, 'nomPower', 100) : 0} <br>
|
||
</ul>
|
||
|
||
<br><br>
|
||
</ul>
|
||
|
||
<a id="SolarForecast-set"></a>
|
||
<b>Set</b>
|
||
<ul>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-aiDecTree"></a>
|
||
<li><b>aiDecTree </b> <br><br>
|
||
|
||
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.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="10%"> <col width="90%"> </colgroup>
|
||
<tr><td> <b>addInstances</b> </td><td>- Die KI wird mit den aktuell vorhandenen PV-, Strahlungs- und Umweltdaten angereichert. </td></tr>
|
||
<tr><td> <b>addRawData</b> </td><td>- Relevante PV-, Strahlungs- und Umweltdaten werden extrahiert und für die spätere Verwendung gespeichert. </td></tr>
|
||
<tr><td> <b>train</b> </td><td>- Die KI wird mit den verfügbaren Daten trainiert. </td></tr>
|
||
<tr><td> </td><td> Erfolgreich generierte Entscheidungsdaten werden im Filesystem gespeichert. </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-batteryTrigger"></a>
|
||
<li><b>batteryTrigger <1on>=<Wert> <1off>=<Wert> [<2on>=<Wert> <2off>=<Wert> ...] </b> <br><br>
|
||
|
||
Generiert Trigger bei Über- bzw. Unterschreitung bestimmter Batterieladungswerte (SoC in %). <br>
|
||
Der verwendete SoC wird als resultierender SoC (Summe aktuelle Ladung aller Batterie Geräte im Verhältnis zur installierten
|
||
Gesamtkapazität) gebildet, d.h. alle Batterien werden als ein Cluster betrachtet. <br>
|
||
Überschreiten die letzten drei SoC-Messungen eine definierte <b>Xon-Bedingung</b>, wird das Reading
|
||
<b>batteryTrigger_X = on</b> erstellt/gesetzt. <br>
|
||
Unterschreiten die letzten drei SoC-Messungen eine definierte <b>Xoff-Bedingung</b>, wird das Reading
|
||
<b>batteryTrigger_X = off</b> erstellt/gesetzt. <br>
|
||
Es kann eine beliebige Anzahl von Triggerbedingungen angegeben werden. Xon/Xoff-Bedingungen müssen nicht zwingend paarweise
|
||
definiert werden. <br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
set <name> batteryTrigger 1on=30 1off=10 2on=70 2off=20 3on=15 4off=90<br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-consumerNewPlanning"></a>
|
||
<li><b>consumerNewPlanning <Verbrauchernummer> </b> <br><br>
|
||
|
||
Es wird die vorhandene Planung des angegebenen Verbrauchers gelöscht. <br>
|
||
Die Neuplanung wird unter Berücksichtigung der im consumerXX Attribut gesetzten Parameter sofort vorgenommen.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
set <name> consumerNewPlanning 01 <br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-consumerImmediatePlanning"></a>
|
||
<li><b>consumerImmediatePlanning <Verbrauchernummer> </b> <br><br>
|
||
|
||
Es wird das sofortige Einschalten des Verbrauchers zur aktuellen Zeit eingeplant.
|
||
Eventuell im consumerXX Attribut gesetzte Schlüssel <b>notbefore</b>, <b>notafter</b> bzw. <b>mode</b> werden nicht
|
||
beachtet. <br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
set <name> consumerImmediatePlanning 01 <br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-energyH4Trigger"></a>
|
||
<li><b>energyH4Trigger <1on>=<Wert> <1off>=<Wert> [<2on>=<Wert> <2off>=<Wert> ...] </b> <br><br>
|
||
|
||
Generiert Trigger bei Über- bzw. Unterschreitung der 4-Stunden PV Vorhersage (NextHours_Sum04_PVforecast). <br>
|
||
Überschreiten die letzten drei Messungen der 4-Stunden PV Vorhersagen eine definierte <b>Xon-Bedingung</b>, wird das Reading
|
||
<b>energyH4Trigger_X = on</b> erstellt/gesetzt.
|
||
Unterschreiten die letzten drei Messungen der 4-Stunden PV Vorhersagen eine definierte <b>Xoff-Bedingung</b>, wird das Reading
|
||
<b>energyH4Trigger_X = off</b> erstellt/gesetzt. <br>
|
||
Es kann eine beliebige Anzahl von Triggerbedingungen angegeben werden. Xon/Xoff-Bedingungen müssen nicht zwingend paarweise
|
||
definiert werden. <br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
set <name> energyH4Trigger 1on=2000 1off=1700 2on=2500 2off=2000 3off=1500 <br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-setupStringAzimuth"></a>
|
||
<li><b>setupStringAzimuth <Stringname1>=<dir> [<Stringname2>=<dir> <Stringname3>=<dir> ...] </b> <br><br>
|
||
|
||
Ausrichtung <dir> der Solarmodule im String "StringnameX". Der Stringname ist ein Schlüsselwert des
|
||
Attributs <b>setupInverterStrings</b>. <br>
|
||
Die Richtungsangabe <dir> kann als Azimut Kennung oder als Azimut Wert angegeben werden: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="30%"> <col width="20%"> <col width="50%"> </colgroup>
|
||
<tr><td> <b>Kennung</b> </td><td><b>Azimut</b> </td><td> </td></tr>
|
||
<tr><td> N </td><td>-180 </td><td>Nordausrichtung </td></tr>
|
||
<tr><td> NE </td><td>-135 </td><td>Nord-Ost Ausrichtung </td></tr>
|
||
<tr><td> E </td><td>-90 </td><td>Ostausrichtung </td></tr>
|
||
<tr><td> SE </td><td>-45 </td><td>Süd-Ost Ausrichtung </td></tr>
|
||
<tr><td> S </td><td>0 </td><td>Südausrichtung </td></tr>
|
||
<tr><td> SW </td><td>45 </td><td>Süd-West Ausrichtung </td></tr>
|
||
<tr><td> W </td><td>90 </td><td>Westausrichtung </td></tr>
|
||
<tr><td> NW </td><td>135 </td><td>Nord-West Ausrichtung </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
Azimut Werte sind Ganzzahlen im Bereich von -180 bis 180. Obwohl die genannten Kennungen verwendet werden können,
|
||
wird empfohlen den genauen Azimut Wert im Attribut anzugeben. Dadurch können beliebige Zwischenwerte wie 83, 48 etc.
|
||
angeben werden.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
set <name> setupStringAzimuth Ostdach=-85 Südgarage=S S3=132 <br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-setupStringDeclination"></a>
|
||
<li><b>setupStringDeclination <Stringname1>=<Winkel> [<Stringname2>=<Winkel> <Stringname3>=<Winkel> ...] </b> <br><br>
|
||
|
||
Neigungswinkel der Solarmodule. Der Stringname ist ein Schlüsselwert des Attributs <b>setupInverterStrings</b>. <br>
|
||
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). <br><br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
set <name> setupStringDeclination Ostdach=40 Südgarage=60 S3=30 <br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-operatingMemory"></a>
|
||
<li><b>operatingMemory backup | save | recover-<Datei> </b> <br><br>
|
||
|
||
Die Komponenten pvHistory (PVH) und pvCircular (PVC) der internen Cache Datenbank werden im Filesystem gespeichert. <br>
|
||
Das Zielverzeichnis ist "../FHEM/FhemUtils". Dieser Vorgang wird vom Modul regelmäßig im Hintergrund ausgeführt. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="17%"> <col width="83%"> </colgroup>
|
||
<tr><td> <b>backup</b> </td><td>Sichert die aktiven In-Memory Strukturen mit dem aktuellen Zeitstempel. </td></tr>
|
||
<tr><td> </td><td>Es werden <a href="#SolarForecast-attr-ctrlBackupFilesKeep">ctrlBackupFilesKeep</a> Generationen der Dateien gespeichert. Ältere Versionen werden gelöscht. </td></tr>
|
||
<tr><td> </td><td>Dateien: PVH_SolarForecast_<name>_<Zeitstempel>, PVC_SolarForecast_<name>_<Zeitstempel> </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>save</b> </td><td>Die aktiven In-Memory Strukturen werden gespeichert. </td></tr>
|
||
<tr><td> </td><td>Dateien: PVH_SolarForecast_<name>, PVC_SolarForecast_<name> </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>recover-<Datei></b> </td><td>Stellt die Daten der ausgewählten Sicherungsdatei als aktive In-Memory Struktur wieder her. </td></tr>
|
||
<tr><td> </td><td>Um Inkonsistenzen zu vermeiden, sollten die Dateien PVH.* und PVC.* mit dem gleichen </td></tr>
|
||
<tr><td> </td><td>Zeitstempel paarweise recovert werden. </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-operationMode"></a>
|
||
<li><b>operationMode </b> <br><br>
|
||
Mit <b>inactive</b> wird das SolarForecast Gerät deaktiviert. Die <b>active</b> 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.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-plantConfiguration"></a>
|
||
<li><b>plantConfiguration </b> <br><br>
|
||
|
||
Je nach ausgewählter Kommandooption werden folgende Operationen ausgeführt: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>check</b> </td><td>Prüft die aktuelle Anlagenkonfiguration. Es wird eine Plausibilitätsprüfung </td></tr>
|
||
<tr><td> </td><td>vorgenommen und das Ergebnis sowie eventuelle Hinweise bzw. Fehler ausgegeben. </td></tr>
|
||
<tr><td> <b>save</b> </td><td>sichert wichtige Parameter der Anlagenkonfiguration. </td></tr>
|
||
<tr><td> </td><td>Die Operation wird täglich kurz nach 00:00 Uhr automatisch ausgeführt. </td></tr>
|
||
<tr><td> <b>restore</b> </td><td>stellt eine gesicherte Anlagenkonfiguration wieder her </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-powerTrigger"></a>
|
||
<li><b>powerTrigger <1on>=<Wert> <1off>=<Wert> [<2on>=<Wert> <2off>=<Wert> ...] </b> <br><br>
|
||
|
||
Generiert Trigger bei Über- bzw. Unterschreitung bestimmter PV Erzeugungswerte (Current_PV). <br>
|
||
Überschreiten die letzten drei Messungen der PV Erzeugung eine definierte <b>Xon-Bedingung</b>, wird das Reading
|
||
<b>powerTrigger_X = on</b> erstellt/gesetzt.
|
||
Unterschreiten die letzten drei Messungen der PV Erzeugung eine definierte <b>Xoff-Bedingung</b>, wird das Reading
|
||
<b>powerTrigger_X = off</b> erstellt/gesetzt. <br>
|
||
Es kann eine beliebige Anzahl von Triggerbedingungen angegeben werden. Xon/Xoff-Bedingungen müssen nicht zwingend paarweise
|
||
definiert werden. <br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
set <name> powerTrigger 1on=1000 1off=500 2on=2000 2off=1000 3on=1600 4off=1100<br>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-pvCorrectionFactor_Auto"></a>
|
||
<li><b>pvCorrectionFactor_Auto </b> <br><br>
|
||
|
||
Schaltet die automatische Vorhersagekorrektur ein/aus.
|
||
Die Wirkungsweise unterscheidet sich je nach gewählter Methode. <br>
|
||
(default: off)
|
||
<br><br>
|
||
|
||
<b>noLearning:</b> <br>
|
||
Mit dieser Option wird die erzeugte PV Energie der aktuellen Stunde vom Lernprozess (Korrekturfaktoren
|
||
sowie KI) ausgeschlossen. <br>
|
||
Die zuvor eingestellte Autokorrekturmethode wird weiterhin angewendet.
|
||
<br><br>
|
||
|
||
<b>on_simple(_ai):</b> <br>
|
||
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 <b>nicht</b> zusätzlich mit weiteren Bedingungen wie den Bewölkungszustand oder Temperaturen in
|
||
Beziehung gesetzt.<br>
|
||
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.
|
||
<br><br>
|
||
|
||
<b>on_complex(_ai):</b> <br>
|
||
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.<br>
|
||
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.
|
||
<br><br>
|
||
|
||
<b>Hinweis:</b> Die automatische Vorhersagekorrektur ist lernend und benötigt Zeit um die Korrekturwerte zu optimieren.
|
||
Nach der Aktivierung sind nicht sofort optimale Vorhersagen zu erwarten!
|
||
<br><br>
|
||
|
||
Nachfolgend einige API-spezifische Hinweise die lediglich Best Practice Empfehlungen darstellen.
|
||
<br><br>
|
||
|
||
<b>Model OpenMeteo...API:</b> <br>
|
||
Die empfohlene Autokorrekturmethode ist <b>on_complex</b> bzw. <b>on_complex_ai</b>.
|
||
<br><br>
|
||
|
||
<b>Model SolCastAPI:</b> <br>
|
||
Die empfohlene Autokorrekturmethode ist <b>on_complex</b>. <br>
|
||
Bevor man die Autokorrektur eingeschaltet, ist die Prognose mit folgenden Schritten zu optimieren: <br><br>
|
||
<ul>
|
||
<li>
|
||
definiere im RoofTop-Editor der SolCast API den
|
||
<a href="https://articles.solcast.com.au/en/articles/2959798-what-is-the-efficiency-factor?_ga=2.119610952.1991905456.1665567573-1390691316.1665567573"><b>efficiency factor</b></a>
|
||
entsprechend dem Alter der Anlage. <br>
|
||
Bei einer 8 Jahre alten Anlage wäre er 84 (100 - (8 x 2%)). <br>
|
||
</li>
|
||
<li>
|
||
nach Sonnenuntergang wird das Reading Today_PVdeviation erstellt, welches die Abweichung zwischen Prognose und
|
||
realer PV Erzeugung in Prozent darstellt.
|
||
</li>
|
||
</li>
|
||
<li>
|
||
entsprechend der Abweichung passe den efficiency factor in Schritten an bis ein Optimum, d.h. die kleinste
|
||
Tagesabweichung gefunden ist
|
||
</li>
|
||
<li>
|
||
ist man der Auffassung die optimale Einstellung gefunden zu haben, kann pvCorrectionFactor_Auto on* gesetzt werden.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
Idealerweise wird dieser Prozess in einer Phase stabiler meteorologischer Bedingungen (gleichmäßige Sonne bzw.
|
||
Bewölkung) durchgeführt.
|
||
<br><br>
|
||
|
||
<b>Model VictronKiAPI:</b> <br>
|
||
Dieses Model basiert auf der KI gestützten API von Victron Energy.
|
||
Die empfohlene Autokorrekturmethode ist <b>on_complex</b>. <br><br>
|
||
|
||
<b>Model DWD:</b> <br>
|
||
Die empfohlene Autokorrekturmethode ist <b>on_complex</b> bzw. <b>on_complex_ai</b>. <br><br>
|
||
|
||
<b>Model ForecastSolarAPI:</b> <br>
|
||
Die empfohlene Autokorrekturmethode ist <b>on_complex</b>.
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-pvCorrectionFactor_" data-pattern="pvCorrectionFactor_.*"></a>
|
||
<li><b>pvCorrectionFactor_XX <Zahl> </b> <br><br>
|
||
|
||
Voreinstellung des Korrekturfaktors für die Stunde XX des Tages. <br>
|
||
(default: 1.0) <br><br>
|
||
|
||
In Abhängigkeit vom Setting <a href="#SolarForecast-set-pvCorrectionFactor_Auto ">pvCorrectionFactor_Auto </a> ('off' bzw. 'on_.*') erfolgt
|
||
eine statische oder dynamische Voreinstellung: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="10%"> <col width="90%"> </colgroup>
|
||
<tr><td> <b>off</b> </td><td>Der eingestellte Korrekturfaktor wird durch die Autokorrektur nicht überschrieben. </td></tr>
|
||
<tr><td> </td><td>Im Reading pvCorrectionFactor_XX wird der Status durch den Zusatz 'manual fix' signalisiert. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>on_.*</b> </td><td>Der eingestellte Korrekturfaktor wird durch die Autokorrektur bzw. KI überschrieben </td></tr>
|
||
<tr><td> </td><td>sofern ein berechneter Korrekturwert im System verfügbar ist. </td></tr>
|
||
<tr><td> </td><td>Im Reading pvCorrectionFactor_XX wird der Status durch den Zusatz 'manual flex' signalisiert. </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-reset"></a>
|
||
<li><b>reset </b> <br><br>
|
||
|
||
Löscht die aus der Drop-Down Liste gewählte Datenquelle, zu der Funktion gehörende Readings oder weitere interne
|
||
Datenstrukturen. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>aiData</b> </td><td>löscht eine vorhandene KI Instanz inklusive aller Trainingsdaten und initialisiert sie neu </td></tr>
|
||
<tr><td> <b>batteryTriggerSet</b> </td><td>löscht die Triggerpunkte des Batteriespeichers </td></tr>
|
||
<tr><td> <b>consumerPlanning</b> </td><td>löscht die Planungsdaten aller registrierten Verbraucher </td></tr>
|
||
<tr><td> </td><td>Um die Planungsdaten nur eines Verbrauchers zu löschen verwendet man: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset consumerPlanning <Verbrauchernummer> </ul> </td></tr>
|
||
<tr><td> </td><td>Das Modul führt eine automatische Neuplanung der Verbraucherschaltung durch. </td></tr>
|
||
<tr><td> <b>consumerMaster</b> </td><td>löscht die aktuellen und historischen Daten aller registrierten Verbraucher aus dem Speicher </td></tr>
|
||
<tr><td> </td><td>Die definierten Consumer Attribute bleiben bestehen und die Daten werden neu gesammelt. </td></tr>
|
||
<tr><td> </td><td>Um die Daten nur eines Verbrauchers zu löschen verwendet man: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset consumerMaster <Verbrauchernummer> </ul> </td></tr>
|
||
<tr><td> <b>consumption</b> </td><td>löscht die gespeicherten Verbrauchswerte des Hauses </td></tr>
|
||
<tr><td> </td><td>Um die Verbrauchswerte eines bestimmten Tages zu löschen: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset consumption <Tag> (z.B. set <name> reset consumption 08) </ul> </td></tr>
|
||
<tr><td> </td><td>Um die Verbrauchswerte einer bestimmten Stunde eines Tages zu löschen: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset consumption <Tag> <Stunde> (z.B. set <name> reset consumption 08 10) </ul> </td></tr>
|
||
<tr><td> <b>energyH4TriggerSet</b> </td><td>löscht die 4-Stunden Energie Triggerpunkte </td></tr>
|
||
<tr><td> <b>powerTriggerSet</b> </td><td>löscht die Triggerpunkte für PV Erzeugungswerte </td></tr>
|
||
<tr><td> <b>pvCorrection</b> </td><td>löscht die Readings pvCorrectionFactor* </td></tr>
|
||
<tr><td> </td><td>Um alle bisher gespeicherten PV Korrekturfaktoren aus den Caches zu löschen: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset pvCorrection cached </ul> </td></tr>
|
||
<tr><td> </td><td>Um gespeicherte PV Korrekturfaktoren einer bestimmten Stunde aus den Caches zu löschen: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset pvCorrection cached <Stunde> </ul> </td></tr>
|
||
<tr><td> </td><td><ul>(z.B. set <name> reset pvCorrection cached 10) </ul> </td></tr>
|
||
<tr><td> <b>pvHistory</b> </td><td>löscht den Speicher aller historischen Tage (01 ... 31) </td></tr>
|
||
<tr><td> </td><td>Um einen bestimmten historischen Tag zu löschen: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset pvHistory <Tag> (z.B. set <name> reset pvHistory 08) </ul> </td></tr>
|
||
<tr><td> </td><td>Um eine bestimmte Stunde eines historischer Tages zu löschen: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset pvHistory <Tag> <Stunde> (z.B. set <name> reset pvHistory 08 10) </ul> </td></tr>
|
||
<tr><td> <b>roofIdentPair</b> </td><td>löscht alle gespeicherten SolCast API Rooftop-ID / API-Key Paare </td></tr>
|
||
<tr><td> </td><td>Um ein bestimmtes Paar zu löschen ist dessen Schlüssel <pk> anzugeben: </td></tr>
|
||
<tr><td> </td><td><ul>set <name> reset roofIdentPair <pk> (z.B. set <name> reset roofIdentPair p1) </ul> </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-roofIdentPair"></a>
|
||
<li><b>roofIdentPair <pk> rtid=<Rooftop-ID> apikey=<SolCast API Key> </b> <br>
|
||
(nur bei Verwendung Model SolCastAPI) <br><br>
|
||
|
||
Der Abruf jedes in <a href='https://toolkit.solcast.com.au/rooftop-sites' target='_blank'>SolCast Rooftop Sites</a>
|
||
angelegten Rooftops ist mit der Angabe eines Paares <b>Rooftop-ID</b> und <b>API-Key</b> zu identifizieren. <br>
|
||
Der Schlüssel <pk> kennzeichnet eindeutig ein verbundenes Paar Rooftop-ID / API-Key. Es können beliebig viele
|
||
Paare <b>nacheinander</b> angelegt werden. In dem Fall ist jeweils ein neuer Name für "<pk>" zu verwenden.
|
||
<br><br>
|
||
|
||
Der Schlüssel <pk> wird im Attribut <a href="#SolarForecast-attr-setupRoofTops">setupRoofTops</a> dem abzurufenden
|
||
Rooftop (=String) zugeordnet.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Beispiele: </b> <br>
|
||
set <name> roofIdentPair p1 rtid=92fc-6796-f574-ae5f apikey=oNHDbkKuC_eGEvZe7ECLl6-T1jLyfOgC <br>
|
||
set <name> roofIdentPair p2 rtid=f574-ae5f-92fc-6796 apikey=eGEvZe7ECLl6_T1jLyfOgC_oNHDbkKuC <br>
|
||
</ul>
|
||
|
||
<br>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-set-vrmCredentials"></a>
|
||
<li><b>vrmCredentials user=<Benutzer> pwd=<Paßwort> idsite=<idSite> </b> <br>
|
||
(nur bei Verwendung Model VictronKiAPI) <br><br>
|
||
|
||
Wird die Victron VRM API genutzt, sind mit diesem set-Befehl die benötigten Zugangsdaten zu hinterlegen. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="10%"> <col width="90%"> </colgroup>
|
||
<tr><td> <b>user</b> </td><td>Benutzername für das Victron VRM Portal </td></tr>
|
||
<tr><td> <b>pwd</b> </td><td>Paßwort für den Zugang zum Victron VRM Portal </td></tr>
|
||
<tr><td> <b>idsite</b> </td><td>idSite ist der Bezeichner "XXXXXX" in der Victron VRM Portal Dashboard URL. </td></tr>
|
||
<tr><td> </td><td>URL des Victron VRM Dashboard ist: </td></tr>
|
||
<tr><td> </td><td>https://vrm.victronenergy.com/installation/<b>XXXXXX</b>/dashboard </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
Um die gespeicherten Credentials zu löschen, ist dem Kommando nur das Argument <b>delete</b> zu übergeben. <br><br>
|
||
|
||
<ul>
|
||
<b>Beispiele: </b> <br>
|
||
set <name> vrmCredentials user=john@example.com pwd=somepassword idsite=212008 <br>
|
||
set <name> vrmCredentials delete <br>
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
</ul>
|
||
<br>
|
||
|
||
<a id="SolarForecast-get"></a>
|
||
<b>Get</b>
|
||
<ul>
|
||
<ul>
|
||
<a id="SolarForecast-get-data"></a>
|
||
<li><b>data </b> <br><br>
|
||
Startet die Datensammlung zur Bestimmung der solaren Vorhersage und anderer Werte.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-dwdCatalog"></a>
|
||
<li><b>dwdCatalog </b> <br><br>
|
||
Der Deutsche Wetterdienst (DWD) stellt einen Katalog der MOSMIX Stationen zur Verfügung. <br>
|
||
Die Stationen liefern Daten deren Bedeutung in dieser
|
||
<a href='https://www.dwd.de/DE/leistungen/opendata/help/schluessel_datenformate/kml/mosmix_elemente_xls.html' target='_blank'>Übersicht</a>
|
||
erläutert ist. Der DWD unterscheidet dabei zwischen MOSMIX_L und MOSMIX_S Stationen die sich durch Aktualisierungfrequenz
|
||
und Datenumfang unterscheiden. <br>
|
||
Mit diesem Kommando wird der Katalog in SolarForecast eingelesen und in der Datei
|
||
./FHEM/FhemUtils/DWDcat_SolarForecast gespeichert. <br>
|
||
Der Katalog kann umfangreich gefiltert und im GPS Exchange Format (GPX) gespeichert werden.
|
||
Die Koordinaten Latitude und Logitude werden in Dezimalgrad ausgegeben. <br>
|
||
Zur Filterung werden Regex-Ausdrücke in den entsprechenden Schlüsseln verwendet. Der Regex wird zur Auswertung in
|
||
^...$ eingeschlossen. <br>
|
||
Folgende Parameter können angegeben werden. Ohne Parameter erfolgt die Ausgabe des gesamten Katalogs: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>byID</b> </td><td>Die Ausgabe erfolgt sortiert nach Stations-ID. (default) </td></tr>
|
||
<tr><td> <b>byName</b> </td><td>Die Ausgabe erfolgt sortiert nach Stations-Name. </td></tr>
|
||
<tr><td> <b>force</b> </td><td>Es wird die neueste Version des DWD Stationskatalogs in das System geladen. </td></tr>
|
||
<tr><td> <b>exportgpx</b> </td><td>Die (gefilterten) Stationen werden in der Datei ./FHEM/FhemUtils/DWDcat_SolarForecast.gpx gespeichert. </td></tr>
|
||
<tr><td> </td><td>Diese Datei kann z.B. im <a href='https://www.j-berkemeier.de/ShowGPX.html' target='_blank'>GPX-Viewer</a> dargestellt werden. </td></tr>
|
||
<tr><td> <b>id=<Regex></b> </td><td>Es erfolgt eine Filterung nach Stations-ID. </td></tr>
|
||
<tr><td> <b>name=<Regex></b> </td><td>Es erfolgt eine Filterung nach Stations-Name. </td></tr>
|
||
<tr><td> <b>lat=<Regex></b> </td><td>Es erfolgt eine Filterung nach Latitude. </td></tr>
|
||
<tr><td> <b>lon=<Regex></b> </td><td>Es erfolgt eine Filterung nach Longitude. </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
get <name> dwdCatalog byName name=ST.* exportgpx lat=(48|49|50|51|52)\..* lon=([5-9]|10|11|12|13|14|15)\..* <br>
|
||
# filtert die Stationen weitgehend auf deutsche Orte beginnend mit "ST" und exportiert die Daten im GPS Exchange Format
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-forecastQualities"></a>
|
||
<li><b>forecastQualities </b> <br><br>
|
||
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.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-ftuiFramefiles"></a>
|
||
<li><b>ftuiFramefiles </b> <br><br>
|
||
SolarForecast stellt Widgets für
|
||
<a href='https://wiki.fhem.de/wiki/FHEM_Tablet_UI' target='_blank'>FHEM Tablet UI v2 (FTUI2)</a> zur Verfügung. <br>
|
||
Ist FTUI2 auf dem System installiert, können die Dateien für das Framework mit diesem Kommando in die
|
||
FTUI-Verzeichnisstruktur geladen werden. <br>
|
||
Die Einrichtung und Verwendung der Widgets ist im Wiki
|
||
<a href='https://wiki.fhem.de/wiki/SolarForecast_FTUI_Widget' target='_blank'>SolarForecast FTUI Widget</a>
|
||
beschrieben.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-html"></a>
|
||
<li><b>html </b> <br><br>
|
||
Die SolarForecast Grafik wird als HTML-Code abgerufen und wiedergegeben. <br>
|
||
<b>Hinweis:</b> Durch das Attribut <a href="#SolarForecast-attr-graphicHeaderOwnspec ">graphicHeaderOwnspec</a>
|
||
generierte set-Kommandos oder Attribut-Befehle im Anwender spezifischen Bereich des Headers werden aus technischen
|
||
Gründen generell ausgeblendet. <br>
|
||
Als Argument kann dem Befehl eine der folgenden Selektionen mitgegeben werden:
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="30%"> <col width="70%"> </colgroup>
|
||
<tr><td> <b>both</b> </td><td>zeigt den Header, die Verbraucherlegende, Energiefluß- und Vorhersagegrafik an (default) </td></tr>
|
||
<tr><td> <b>both_noHead</b> </td><td>zeigt die Verbraucherlegende, Energiefluß- und Vorhersagegrafik an </td></tr>
|
||
<tr><td> <b>both_noCons</b> </td><td>zeigt den Header, Energiefluß- und Vorhersagegrafik an </td></tr>
|
||
<tr><td> <b>both_noHead_noCons</b> </td><td>zeigt Energiefluß- und Vorhersagegrafik an </td></tr>
|
||
<tr><td> <b>flow</b> </td><td>zeigt den Header, die Verbraucherlegende und Energieflußgrafik an </td></tr>
|
||
<tr><td> <b>flow_noHead</b> </td><td>zeigt die Verbraucherlegende und die Energieflußgrafik an </td></tr>
|
||
<tr><td> <b>flow_noCons</b> </td><td>zeigt den Header und die Energieflußgrafik an </td></tr>
|
||
<tr><td> <b>flow_noHead_noCons</b> </td><td>zeigt die Energieflußgrafik an </td></tr>
|
||
<tr><td> <b>forecast</b> </td><td>zeigt den Header, die Verbraucherlegende und die Vorhersagegrafik an </td></tr>
|
||
<tr><td> <b>forecast_noHead</b> </td><td>zeigt die Verbraucherlegende und die Vorhersagegrafik an </td></tr>
|
||
<tr><td> <b>forecast_noCons</b> </td><td>zeigt den Header und die Vorhersagegrafik an </td></tr>
|
||
<tr><td> <b>forecast_noHead_noCons</b> </td><td>zeigt die Vorhersagegrafik an </td></tr>
|
||
<tr><td> <b>none</b> </td><td>zeigt nur den Header und die Verbraucherlegende an </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
Die Grafik kann abgerufen und in eigenen Code eingebettet werden. Auf einfache Weise kann dies durch die Definition
|
||
eines weblink-Devices vorgenommen werden: <br><br>
|
||
|
||
<ul>
|
||
define wl.SolCast5 weblink htmlCode { FHEM::SolarForecast::pageAsHtml ('SolCast5', '-', '<argument>') }
|
||
</ul>
|
||
<br>
|
||
'SolCast5' ist der Name des einzubindenden SolarForecast-Device. <b><argument></b> ist eine der oben
|
||
beschriebenen Auswahlmöglichkeiten.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-nextHours"></a>
|
||
<li><b>nextHours </b> <br><br>
|
||
Listet die erwarteten Werte der kommenden Stunden auf. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>aihit</b> </td><td>Lieferstatus der KI für die PV Vorhersage (0-keine Lieferung, 1-Lieferung) </td></tr>
|
||
<tr><td> <b>confc</b> </td><td>erwarteter Energieverbrauch inklusive der Anteile registrierter Verbraucher </td></tr>
|
||
<tr><td> <b>confcEx</b> </td><td>erwarteter Energieverbrauch ohne Anteile Verbraucher mit gesetztem Schlüssel exconfc=1 </td></tr>
|
||
<tr><td> <b>crange</b> </td><td>berechneter Bewölkungsbereich </td></tr>
|
||
<tr><td> <b>correff</b> </td><td>verwendeter Korrekturfaktor/Qualität </td></tr>
|
||
<tr><td> </td><td><Faktor>/- -> keine Qualität definiert </td></tr>
|
||
<tr><td> </td><td><Faktor>/0..1 - Qualität der PV Prognose (1 = beste Qualität) </td></tr>
|
||
<tr><td> <b>DoN</b> </td><td>Sonnenauf- und untergangsstatus (0 - Nacht, 1 - Tag) </td></tr>
|
||
<tr><td> <b>hourofday</b> </td><td>laufende Stunde des Tages </td></tr>
|
||
<tr><td> <b>pvapifc</b> </td><td>erwartete PV Erzeugung (Wh) der verwendeten API inkl. einer eventuellen Korrektur </td></tr>
|
||
<tr><td> <b>pvaifc</b> </td><td>erwartete PV Erzeugung der KI (Wh) </td></tr>
|
||
<tr><td> <b>pvfc</b> </td><td>verwendete PV Erzeugungsprognose (Wh) </td></tr>
|
||
<tr><td> <b>rad1h</b> </td><td>vorhergesagte Globalstrahlung </td></tr>
|
||
<tr><td> <b>starttime</b> </td><td>Startzeit des Datensatzes </td></tr>
|
||
<tr><td> <b>sunaz</b> </td><td>Azimuth der Sonne (in Dezimalgrad) </td></tr>
|
||
<tr><td> <b>sunalt</b> </td><td>Höhe der Sonne (in Dezimalgrad) </td></tr>
|
||
<tr><td> <b>temp</b> </td><td>vorhergesagte Außentemperatur </td></tr>
|
||
<tr><td> <b>today</b> </td><td>hat Wert '1' wenn Startdatum am aktuellen Tag </td></tr>
|
||
<tr><td> <b>rcdchargebatXX</b> </td><td>Aufladeempfehlung für Batterie XX (1 - Ja, 0 - Nein) </td></tr>
|
||
<tr><td> <b>rr1c</b> </td><td>Gesamtniederschlag in der letzten Stunde kg/m2 </td></tr>
|
||
<tr><td> <b>rrange</b> </td><td>Bereich des Gesamtniederschlags </td></tr>
|
||
<tr><td> <b>socXX</b> </td><td>aktueller (NextHour00) oder prognostizierter SoC der Batterie XX </td></tr>
|
||
<tr><td> <b>weatherid</b> </td><td>ID des vorhergesagten Wetters </td></tr>
|
||
<tr><td> <b>wcc</b> </td><td>vorhergesagter Grad der Bewölkung </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-pvHistory"></a>
|
||
<li><b>pvHistory </b> <br><br>
|
||
Zeigt oder exportiert den Inhalt des pvHistory Datenspeichers sortiert nach dem Tagesdatum und Stunde. <br>
|
||
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 Angabe 'exportToCsv' exportiert den gesamten Inhalt der pvHistory in eine CSV-Datei. <br>
|
||
|
||
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. Die Stunde '99' enthält Tageswerte.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>batintotalXX</b> </td><td>Gesamtladung der Batterie XX (Wh) zu Beginn der Stunde </td></tr>
|
||
<tr><td> <b>batinXX</b> </td><td>Ladung der Batterie XX innerhalb der Stunde (Wh) </td></tr>
|
||
<tr><td> <b>batouttotalXX</b> </td><td>Gesamtentladung der Batterie XX (Wh) zu Beginn der Stunde </td></tr>
|
||
<tr><td> <b>batoutXX</b> </td><td>Entladung der Batterie XX innerhalb der Stunde (Wh) </td></tr>
|
||
<tr><td> <b>batsocXX</b> </td><td>Ladezustand SOC (%) der Batterie XX am Ende der Stunde </td></tr>
|
||
<tr><td> <b>batmaxsocXX</b> </td><td>maximal erreichter SOC (%) der Batterie XX an dem Tag </td></tr>
|
||
<tr><td> <b>batsetsocXX</b> </td><td>optimaler SOC Sollwert (%) der Batterie XX für den Tag </td></tr>
|
||
<tr><td> <b>csmtXX</b> </td><td>Energieverbrauch total von ConsumerXX </td></tr>
|
||
<tr><td> <b>csmeXX</b> </td><td>Energieverbrauch von ConsumerXX in der Stunde des Tages (Stunde 99 = Tagesenergieverbrauch) </td></tr>
|
||
<tr><td> <b>confc</b> </td><td>erwarteter Energieverbrauch (Wh) </td></tr>
|
||
<tr><td> <b>con</b> </td><td>realer Energieverbrauch (Wh) des Hauses </td></tr>
|
||
<tr><td> <b>conprice</b> </td><td>Preis für den Bezug einer kWh. Die Einheit des Preises ist im setupMeterDev definiert. </td></tr>
|
||
<tr><td> <b>cyclescsmXX</b> </td><td>Anzahl aktive Zyklen von ConsumerXX des Tages </td></tr>
|
||
<tr><td> <b>dayname</b> </td><td>Kurzname des Tages (locale-abhängig) </td></tr>
|
||
<tr><td> <b>DoN</b> </td><td>Sonnenauf- und untergangsstatus (0 - Nacht, 1 - Tag) </td></tr>
|
||
<tr><td> <b>etotaliXX</b> </td><td>PV Zählerstand "Energieertrag total" (Wh) von Inverter XX zu Beginn der Stunde </td></tr>
|
||
<tr><td> <b>etotalpXX</b> </td><td>Zählerstand "Energieertrag total" (Wh) des Produzenten XX zu Beginn der Stunde </td></tr>
|
||
<tr><td> <b>gcons</b> </td><td>realer Leistungsbezug (Wh) aus dem Stromnetz </td></tr>
|
||
<tr><td> <b>gfeedin</b> </td><td>reale Einspeisung (Wh) in das Stromnetz </td></tr>
|
||
<tr><td> <b>feedprice</b> </td><td>Vergütung für die Einpeisung einer kWh. Die Währung des Preises ist im setupMeterDev definiert. </td></tr>
|
||
<tr><td> <b>avgcycmntscsmXX</b> </td><td>durchschnittliche Dauer eines Einschaltzyklus des Tages von ConsumerXX in Minuten </td></tr>
|
||
<tr><td> <b>hourscsmeXX</b> </td><td>Summe Aktivstunden des Tages von ConsumerXX </td></tr>
|
||
<tr><td> <b>minutescsmXX</b> </td><td>Summe Aktivminuten in der Stunde von ConsumerXX </td></tr>
|
||
<tr><td> <b>pprlXX</b> </td><td>Energieerzeugung des Produzenten XX (siehe Attribut setupOtherProducerXX) in der Stunde (Wh) </td></tr>
|
||
<tr><td> <b>pvfc</b> </td><td>der prognostizierte PV Ertrag (Wh) </td></tr>
|
||
<tr><td> <b>pvrlXX</b> </td><td>reale PV Erzeugung (Wh) von Inverter XX </td></tr>
|
||
<tr><td> <b>pvrl</b> </td><td>Summe reale PV Erzeugung (Wh) aller Inverter </td></tr>
|
||
<tr><td> <b>pvrlvd</b> </td><td>1-'pvrl' ist gültig und wird im Lernprozess berücksichtigt, 0-'pvrl' ist als abnormal bewertet </td></tr>
|
||
<tr><td> <b>pvcorrf</b> </td><td>verwendeter Autokorrekturfaktor / erreichte Prognosequalität </td></tr>
|
||
<tr><td> <b>rad1h</b> </td><td>Globalstrahlung (kJ/m2) </td></tr>
|
||
<tr><td> <b>rr1c</b> </td><td>Gesamtniederschlag in der letzten Stunde kg/m2 </td></tr>
|
||
<tr><td> <b>sunalt</b> </td><td>Höhe der Sonne (in Dezimalgrad) </td></tr>
|
||
<tr><td> <b>sunaz</b> </td><td>Azimuth der Sonne (in Dezimalgrad) </td></tr>
|
||
<tr><td> <b>wid</b> </td><td>Identifikationsnummer des Wetters </td></tr>
|
||
<tr><td> <b>wcc</b> </td><td>effektive Wolkenbedeckung </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-pvCircular"></a>
|
||
<li><b>pvCircular </b> <br><br>
|
||
Listet die gespeicherten Daten der ausgewählten Stunde oder alle vorhandenen Werte im Ringspeicher auf. <br>
|
||
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. <br>
|
||
Die Stunde 99 hat eine Sonderfunktion. <br>
|
||
Die Werte der Schlüssel pvcorrf, quality, pvrlsum, pvfcsum und dnumsum sind in der Form
|
||
<Bereich Sonnenstand Höhe>.<Bewölkungsbereich> kodiert.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>aihit</b> </td><td>Lieferstatus der KI für die PV Vorhersage (0-keine Lieferung, 1-Lieferung) </td></tr>
|
||
<tr><td> <b>attrInvChangedTs</b> </td><td>Zeitstempel der letzten Änderung der Inverter Gerätedefinition </td></tr>
|
||
<tr><td> <b>batinXX</b> </td><td>Ladung der Batterie XX (Wh) </td></tr>
|
||
<tr><td> <b>batoutXX</b> </td><td>Entladung der Batterie XX (Wh) </td></tr>
|
||
<tr><td> <b>batouttotXX</b> </td><td>aktuell total aus der Batterie XX entnommene Energie (Wh) </td></tr>
|
||
<tr><td> <b>batintotXX</b> </td><td>aktuell total in die Batterie XX geladene Energie (Wh) </td></tr>
|
||
<tr><td> <b>confc</b> </td><td>erwarteter Energieverbrauch (Wh) des Hauses am aktuellen Tag </td></tr>
|
||
<tr><td> <b>days2careXX</b> </td><td>verbleibende Tage bis der Batterie XX Pflege-SoC (default 95%) erreicht sein soll </td></tr>
|
||
<tr><td> <b>dnumsum</b> </td><td>Anzahl Tage pro Bewölkungsbereich über die gesamte Laufzeit </td></tr>
|
||
<tr><td> <b>feedintotal</b> </td><td>in das öffentliche Netz total eingespeiste PV Energie (Wh) </td></tr>
|
||
<tr><td> <b>gcon</b> </td><td>realer Leistungsbezug aus dem Stromnetz </td></tr>
|
||
<tr><td> <b>gfeedin</b> </td><td>reale Leistungseinspeisung in das Stromnetz </td></tr>
|
||
<tr><td> <b>gridcontotal</b> </td><td>vom öffentlichen Netz total bezogene Energie (Wh) </td></tr>
|
||
<tr><td> <b>initdayfeedin</b> </td><td>initialer PV Einspeisewert zu Beginn des aktuellen Tages (Wh) </td></tr>
|
||
<tr><td> <b>initdaygcon</b> </td><td>initialer Netzbezugswert zu Beginn des aktuellen Tages (Wh) </td></tr>
|
||
<tr><td> <b>initdaybatintotXX</b> </td><td>initialer Wert der total in die Batterie XX geladenen Energie zu Beginn des aktuellen Tages (Wh) </td></tr>
|
||
<tr><td> <b>initdaybatouttotXX</b> </td><td>initialer Wert der total aus der Batterie XX entnommenen Energie zu Beginn des aktuellen Tages (Wh) </td></tr>
|
||
<tr><td> <b>lastTsMaxSocRchdXX</b> </td><td>Timestamp des letzten Erreichens von Batterie XX SoC >= maxSoC (default 95%) </td></tr>
|
||
<tr><td> <b>nextTsMaxSocChgeXX</b> </td><td>Timestamp bis zu dem die Batterie XX mindestens einmal maxSoC erreichen soll </td></tr>
|
||
<tr><td> <b>pvapifc</b> </td><td>erwartete PV Erzeugung (Wh) der verwendeten API </td></tr>
|
||
<tr><td> <b>pvaifc</b> </td><td>PV Vorhersage (Wh) der KI für die nächsten 24h ab aktueller Stunde des Tages </td></tr>
|
||
<tr><td> <b>pvfc</b> </td><td>verwendete PV Prognose für die nächsten 24h ab aktueller Stunde des Tages </td></tr>
|
||
<tr><td> <b>pvfc_XX</b> </td><td>Array der prognostizierten PV Erzeugungswerte abhängig von einem bestimmten Bewölkungsgrad (XX = Altitude der Sonne) </td></tr>
|
||
<tr><td> <b>pvcorrf</b> </td><td>Autokorrekturfaktoren für die Stunde des Tages, wobei 'simple' der einfach berechnete Korrekturfaktor ist. </td></tr>
|
||
<tr><td> <b>pvfcsum</b> </td><td>Summe PV Prognose pro Bewölkungsbereich über die gesamte Laufzeit </td></tr>
|
||
<tr><td> <b>pvrl</b> </td><td>reale PV Erzeugung der letzten 24h (Achtung: pvforecast und pvreal beziehen sich nicht auf den gleichen Zeitraum!) </td></tr>
|
||
<tr><td> <b>pvrl_XX</b> </td><td>Array realer PV Erzeugungswerte erzeugt bei einem bestimmten Bewölkungsgrad (XX = Altitude der Sonne) </td></tr>
|
||
<tr><td> <b>pvrlsum</b> </td><td>Summe reale PV Erzeugung pro Bewölkungsbereich über die gesamte Laufzeit </td></tr>
|
||
<tr><td> <b>pprlXX</b> </td><td>Energieerzeugung des Produzenten XX (siehe Attribut setupOtherProducerXX) der letzten 24 Stunden (Wh) </td></tr>
|
||
<tr><td> <b>quality</b> </td><td>Qualität der Autokorrekturfaktoren (0..1), wobei 'simple' die Qualität des einfach berechneten Korrekturfaktors ist. </td></tr>
|
||
<tr><td> <b>runTimeTrainAI</b> </td><td>Laufzeit des letzten KI Trainings </td></tr>
|
||
<tr><td> <b>aitrainLastFinishTs</b> </td><td>Timestamp des letzten erfolgreichen KI Trainings </td></tr>
|
||
<tr><td> <b>aiRulesNumber</b> </td><td>Anzahl der Regeln in der trainierten KI Instanz </td></tr>
|
||
<tr><td> <b>todayConsumption</b> </td><td>realer Energieverbrauch (Wh) des Hauses am aktuellen Tag </td></tr>
|
||
<tr><td> <b>tdayDvtn</b> </td><td>heutige Abweichung PV Prognose/Erzeugung in % </td></tr>
|
||
<tr><td> <b>temp</b> </td><td>Außentemperatur </td></tr>
|
||
<tr><td> <b>wcc</b> </td><td>Grad der Wolkenüberdeckung </td></tr>
|
||
<tr><td> <b>rr1c</b> </td><td>Gesamtniederschlag in der letzten Stunde kg/m2 </td></tr>
|
||
<tr><td> <b>wid</b> </td><td>ID des vorhergesagten Wetters </td></tr>
|
||
<tr><td> <b>wtxt</b> </td><td>Beschreibung des vorhergesagten Wetters </td></tr>
|
||
<tr><td> <b>ydayDvtn</b> </td><td>Abweichung PV Prognose/Erzeugung in % am Vortag </td></tr>
|
||
</table>
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-rooftopData"></a>
|
||
<li><b>rooftopData </b> <br><br>
|
||
Die erwarteten solaren Strahlungsdaten bzw. PV Erzeugungsdaten werden von der gewählten API abgerufen. <br>
|
||
Ist bezüglich Wetterdaten ebenfalls eine API gewählt, werden diese Daten ebenfalls abgerufen.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-radiationApiData"></a>
|
||
<li><b>radiationApiData </b> <br><br>
|
||
|
||
Listet die im Kontext des API-Abrufs gespeicherten Strahlungsdaten auf.
|
||
Die von der API gelieferten Vorhersagedaten bzgl. der Globalstrahlung Rad1h und des auf einen String bezogenen
|
||
prognostizierten PV Ertrag (Wh) sind auf eine Stunde normiert.
|
||
Die verfügbaren Kennwerte unterscheiden sich je nach verwendeter API.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>Rad1h</b> </td><td>wenn vorhanden, erwartete Globalstrahlung (GI) bzw. globale Schräglagenstrahlung (GTI) in kJ/m2 </td></tr>
|
||
<tr><td> <b>pv_estimateXX</b> </td><td>erwartete PV Erzeugung (Wh) </td></tr>
|
||
<tr><td> <b>KI-based</b> </td><td>erwartete PV Erzeugung (Wh) der VictronKI-API </td></tr>
|
||
<tr><td> <b>KI-based_co</b> </td><td>erwarteter Energieverbrauch (Wh) der VictronKI-API </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-statusApiData"></a>
|
||
<li><b>statusApiData </b> <br><br>
|
||
Zeigt die Statusdaten der verwendeten Strahlungsdaten-API bzw. Wetterdaten-API.
|
||
Es werden nur die Statusdaten des führenden Wetterdienstes ausgegeben.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="37%"> <col width="63%"> </colgroup>
|
||
<tr><td> <b>currentAPIinterval</b> </td><td>das aktuell verwendete API Abrufintervall in Sekunden </td></tr>
|
||
<tr><td> <b>lastretrieval_time</b> </td><td>Zeit des letzten API Abrufs </td></tr>
|
||
<tr><td> <b>lastretrieval_timestamp</b> </td><td>Unix Timestamp des letzten API Abrufs </td></tr>
|
||
<tr><td> <b>todayDoneAPIrequests</b> </td><td>Anzahl der ausgeführten API Requests am aktuellen Tag </td></tr>
|
||
<tr><td> <b>todayRemainingAPIrequests</b> </td><td>Anzahl der verbleibenden SolCast API Requests am aktuellen Tag </td></tr>
|
||
<tr><td> <b>todayDoneAPIcalls</b> </td><td>Anzahl der ausgeführten API Abrufe am aktuellen Tag </td></tr>
|
||
<tr><td> <b>todayRemainingAPIcalls</b> </td><td>Anzahl der noch möglichen SolCast API Abrufe am aktuellen Tag </td></tr>
|
||
<tr><td> </td><td>(ein Abruf kann mehrere SolCast API Requests ausführen) </td></tr>
|
||
<tr><td> <b>todayMaxAPIcalls</b> </td><td>Anzahl der maximal möglichen SolCast API Abrufe pro Tag </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valBattery"></a>
|
||
<li><b>valBattery </b> <br><br>
|
||
Zeigt die ermittelten Betriebswerte der ausgewählten Batterie oder aller definierten Batteriegeräte. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>bname </b> </td><td>Name des Gerätes </td></tr>
|
||
<tr><td> <b>balias </b> </td><td>Alias des Gerätes </td></tr>
|
||
<tr><td> <b>basynchron </b> </td><td>Modus der Verarbeitung empfangener Batterie-Events </td></tr>
|
||
<tr><td> <b>bcharge </b> </td><td>aktueller SoC (State of Charge) der Batterie (%) </td></tr>
|
||
<tr><td> <b>bchargewh </b> </td><td>aktueller SoC (State of Charge) der Batterie (Wh) </td></tr>
|
||
<tr><td> <b>binstcap </b> </td><td>installierte Batteriekapazität (Wh) </td></tr>
|
||
<tr><td> <b>bpowerin </b> </td><td>momentane Ladeleistung (W) </td></tr>
|
||
<tr><td> <b>bpowerout </b> </td><td>momentane Entladeleistung (W) </td></tr>
|
||
</table>
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valConsumerMaster"></a>
|
||
<li><b>valConsumerMaster </b> <br><br>
|
||
Zeigt die Daten der aktuell im SolarForecast Device registrierten Verbraucher. <br>
|
||
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.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valCurrent"></a>
|
||
<li><b>valCurrent </b> <br><br>
|
||
Listet aktuelle Betriebsdaten, Kennzahlen und Status auf.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valDecTree"></a>
|
||
<li><b>valDecTree </b> <br><br>
|
||
|
||
Anzeige von KI relevanten Daten.
|
||
Die verfügbaren Anzeigeoptionen sind abhängig vom verfügbaren und aktivierten KI Unterstützungslevel.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>aiRawData</b> </td><td>Anzeige der aktuell für die KI gespeicherten PV-, Strahlungs- und Umweltdaten. </td></tr>
|
||
<tr><td> </td><td>(verfügbar wenn das Perl Modul AI::DecisionTree installiert ist) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>aiRuleStrings</b> </td><td>Gibt eine Liste zurück, die den Entscheidungsbaum der KI in Form von Regeln beschreibt. </td></tr>
|
||
<tr><td> </td><td><b>Hinweis:</b> Die Reihenfolge der Regeln ist zwar nicht vorhersehbar, die </td></tr>
|
||
<tr><td> </td><td>Reihenfolge der Kriterien innerhalb jeder Regel spiegelt jedoch die Reihenfolge </td></tr>
|
||
<tr><td> </td><td>wider, in der die Kriterien bei der Entscheidungsfindung geprüft werden. </td></tr>
|
||
<tr><td> </td><td>(verfügbar wenn ein KI kompatibles SolarForecast MODEL der PV Vorhersage aktiviert ist) </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valInverter"></a>
|
||
<li><b>valInverter </b> <br><br>
|
||
Zeigt die ermittelten Betriebswerte des ausgewählten Wechselrichters oder aller definierten Wechselrichter. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>iasynchron </b> </td><td>Modus der Verarbeitung empfangener Inverter-Events </td></tr>
|
||
<tr><td> <b>ietotal </b> </td><td>Stand gesamte bisher erzeugte Energie des Wechselrichters (Wh) </td></tr>
|
||
<tr><td> <b>ifeed </b> </td><td>Eigenschaften der Energielieferung </td></tr>
|
||
<tr><td> <b>igeneration </b> </td><td>aktuelle PV Erzeugung (W) </td></tr>
|
||
<tr><td> <b>iicon </b> </td><td>die evtl. festgelegten Icons zur Darstellung des Gerätes in der Grafik </td></tr>
|
||
<tr><td> <b>ialias </b> </td><td>Alias des Gerätes </td></tr>
|
||
<tr><td> <b>iname </b> </td><td>Name des Gerätes </td></tr>
|
||
<tr><td> <b>invertercap </b> </td><td>die nominale Leistung (W) des Wechselrichters (falls definiert) </td></tr>
|
||
<tr><td> <b>istrings </b> </td><td>Liste der dem Wechselrichter zugeordneten Strings (falls definiert) </td></tr>
|
||
</table>
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valProducer"></a>
|
||
<li><b>valProducer </b> <br><br>
|
||
Zeigt die ermittelten Betriebswerte des ausgewählten nicht PV-Erzeugers oder aller definierten nicht PV-Erzeuger. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>petotal </b> </td><td>Stand gesamte bisher erzeugte Energie des Erzeugers (Wh) </td></tr>
|
||
<tr><td> <b>pfeed </b> </td><td>Eigenschaften der Energielieferung </td></tr>
|
||
<tr><td> <b>pgeneration </b> </td><td>aktuelle Leistung (W) </td></tr>
|
||
<tr><td> <b>picon </b> </td><td>die evtl. festgelegten Icons zur Darstellung des Gerätes in der Grafik </td></tr>
|
||
<tr><td> <b>palias </b> </td><td>Alias des Gerätes </td></tr>
|
||
<tr><td> <b>pname </b> </td><td>Name des Gerätes </td></tr>
|
||
</table>
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-valStrings"></a>
|
||
<li><b>valStrings </b> <br><br>
|
||
Listet die Parameter des ausgewählten oder aller definierten Strings auf.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<a id="SolarForecast-get-weatherApiData"></a>
|
||
<li><b>weatherApiData </b> <br><br>
|
||
Zeigt die gelieferten Daten der gewählten Wetter-API.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
|
||
</ul>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr"></a>
|
||
<b>Attribute</b>
|
||
<br><br>
|
||
<ul>
|
||
<ul>
|
||
<a id="SolarForecast-attr-affectBatteryPreferredCharge"></a>
|
||
<li><b>affectBatteryPreferredCharge </b><br>
|
||
Es werden Verbraucher mit dem Mode <b>can</b> erst dann eingeschaltet, wenn die angegebene Batterieladung (%)
|
||
erreicht ist. <br>
|
||
Verbraucher mit dem Mode <b>must</b> beachten die Vorrangladung der Batterie nicht. <br>
|
||
(default: 0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-affectConsForecastInPlanning"></a>
|
||
<li><b>affectConsForecastInPlanning </b><br>
|
||
Wenn gesetzt, wird bei der Einplanung der Consumer zusätzlich zur PV Prognose ebenfalls die Prognose
|
||
des Verbrauchs berücksichtigt. <br>
|
||
Die Standardplanung der Consumer erfolgt lediglich auf Grundlage der PV Prognose. <br>
|
||
(default: 0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-affectConsForecastIdentWeekdays"></a>
|
||
<li><b>affectConsForecastIdentWeekdays </b><br>
|
||
Wenn gesetzt, werden zur Berechnung der Verbrauchsprognose nur gleiche Wochentage (Mo..So) einbezogen. <br>
|
||
Anderenfalls werden alle Wochentage gleichberechtigt zur Kalkulation verwendet. <br>
|
||
Ein eventuell zusätzlich gesetztes Attribut
|
||
<a href="#SolarForecast-attr-affectConsForecastLastDays">affectConsForecastLastDays</a>
|
||
wird gleichfalls berücksichtigt. <br>
|
||
(default: 0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-affectConsForecastLastDays"></a>
|
||
<li><b>affectConsForecastLastDays </b><br>
|
||
Es werden die angegebenen vergangenen Tage (1..31) bei der Berechnung der Verbrauchsprognose einbezogen. <br>
|
||
So wird z.B. mit dem Attributwert "1" nur der vorangegangene Tag berücksichtigt, mit dem Wert "14" die vergangenen 14 Tage. <br>
|
||
Bei einem zusätzlich gesetzten Attribut
|
||
<a href="#SolarForecast-attr-affectConsForecastIdentWeekdays">affectConsForecastIdentWeekdays</a>
|
||
wird die angegebene Anzahl vergangener gleicher Wochentage (Mo .. So) berücksichtigt. <br>
|
||
(default: alle in pvHistory vorhandenen Tage)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-affectSolCastPercentile"></a>
|
||
<li><b>affectSolCastPercentile <10 | 50 | 90> </b><br>
|
||
(nur bei Verwendung Model SolCastAPI) <br><br>
|
||
|
||
Auswahl des Wahrscheinlichkeitsbereiches der gelieferten SolCast-Daten.
|
||
SolCast liefert die 10- und 90-prozentige Wahrscheinlichkeit um den Prognosemittelwert (50) herum. <br>
|
||
(default: 50)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-alias"></a>
|
||
<li><b>alias </b> <br>
|
||
In Verbindung mit "ctrlShowLink" ein beliebiger Anzeigename.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-consumerAdviceIcon"></a>
|
||
<li><b>consumerAdviceIcon </b><br>
|
||
Definiert die Art der Information über die geplanten Schaltzeiten eines Verbrauchers in der Verbraucherlegende.
|
||
<br><br>
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="18%"> <col width="82%"> </colgroup>
|
||
<tr><td> <b><Icon>@<Farbe></b> </td><td>Aktivierungsempfehlung wird durch Icon und Farbe (optional) dargestellt (default: clock@gold) </td></tr>
|
||
<tr><td> </td><td>(die Planungsdaten werden als Mouse-Over Text angezeigt) </td></tr>
|
||
<tr><td> <b>times</b> </td><td>es werden der Planungsstatus und die geplanten Schaltzeiten als Text angezeigt </td></tr>
|
||
<tr><td> <b>none</b> </td><td>keine Anzeige der Planungsdaten </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-consumerLegend"></a>
|
||
<li><b>consumerLegend </b><br>
|
||
Definiert die Lage bzw. Darstellungsweise der Verbraucherlegende sofern Verbraucher im SolarForecast Device
|
||
registriert sind. <br>
|
||
(default: icon_top)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-consumerLink"></a>
|
||
<li><b>consumerLink </b><br>
|
||
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. <br>
|
||
(default: 1)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-consumer" data-pattern="consumer.*"></a>
|
||
<li><b>consumerXX <Device>[:<Alias>] type=<type> power=<power> [switchdev=<device>]<br>
|
||
[mode=<mode>] [icon=<Icon>[@<Farbe>]] [mintime=<minutes> | SunPath[:<Offset_Sunrise>:<Offset_Sunset>]] <br>
|
||
[on=<Kommando>] [off=<Kommando>] [swstate=<Readingname>:<on-Regex>:<off-Regex>] [asynchron=<Option>] <br>
|
||
[notbefore=<Ausdruck>] [notafter=<Ausdruck>] [locktime=<offlt>[:<onlt>]] <br>
|
||
[auto=<Readingname>] [pcurr=<Readingname>:<Einheit>[:<Schwellenwert>]] [etotal=<Readingname>:<Einheit>[:<Schwellenwert>]] <br>
|
||
[swoncond=<Device>:<Reading>:<Regex>] [swoffcond=<Device>:<Reading>:<Regex>] [spignorecond=<Device>:<Reading>:<Regex>] <br>
|
||
[surpmeth=<Option>] [interruptable=<Option>] [noshow=<Option>] [exconfc=<Option>] </b><br>
|
||
<br>
|
||
|
||
Registriert einen Verbraucher <Device> beim SolarForecast Device. Ein optionaler Alias kann angegeben werden. <br>
|
||
Dabei ist <Device> ein in FHEM bereits angelegtes Verbraucher Device, z.B. eine Schaltsteckdose.
|
||
Die meisten Schlüssel sind optional, sind aber für bestimmte Funktionalitäten Voraussetzung und werden mit
|
||
default-Werten besetzt. <br>
|
||
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. <br><br>
|
||
|
||
Mit dem optionalen Schlüssel <b>swoncond</b> kann eine <b>zusätzliche externe Bedingung</b> 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
|
||
<b>UND-Verknüpfung</b> des Schlüssels swoncond mit den weiteren Einschaltbedingungen. <br><br>
|
||
|
||
Der optionale Schlüssel <b>swoffcond</b> definiert eine <b>vorrangige Ausschaltbedingung</b> (Regex). Sobald diese
|
||
Bedingung erfüllt ist, wird der Consumer ausgeschaltet auch wenn die geplante Endezeit (consumerXX_planned_stop)
|
||
noch nicht erreicht ist (<b>ODER-Verknüpfung</b>). Weitere Bedingungen wie off-Schlüssel und auto-Mode müssen
|
||
zum automatischen Ausschalten erfüllt sein. <br><br>
|
||
|
||
Mit dem optionalen Schlüssel <b>interruptable</b> 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!
|
||
<br><br>
|
||
|
||
Der Schlüssel <b>power</b> 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 <b>power=0</b> gesetzt, wird der Verbraucher unabhängig von einem ausreichend vorhandenem PV-Überschuß
|
||
wie eingeplant geschaltet.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="12%"> <col width="88%"> </colgroup>
|
||
<tr><td> <b>Device</b> </td><td>Verbraucher-Gerät. Im einfachen Fall arbeitet das Gerät sowohl als Energiemesser als auch als Schalter. </td></tr>
|
||
<tr><td> </td><td>Im optionalen Alias sind Leerzeichen durch '+' zu ersetzen (z.B. 'Ein+toller+Alias'). </td></tr>
|
||
<tr><td> </td><td>Besteht der Verbraucher aus verschiedenen Geräten/Kanäalen (z.B. Homematic), wird der Energiemesser als <Device> definiert. </td></tr>
|
||
<tr><td> </td><td>Das dazugehörige Schalt-Gerät wird mit dem Schlüssel 'switchdev' spezifiziert. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>type</b> </td><td>Typ des Verbrauchers. Folgende Typen sind erlaubt: </td></tr>
|
||
<tr><td> </td><td><b>dishwasher</b> - Verbraucher ist eine Spülmaschine </td></tr>
|
||
<tr><td> </td><td><b>dryer</b> - Verbraucher ist ein Wäschetrockner </td></tr>
|
||
<tr><td> </td><td><b>washingmachine</b> - Verbraucher ist eine Waschmaschine </td></tr>
|
||
<tr><td> </td><td><b>heater</b> - Verbraucher ist ein Heizstab </td></tr>
|
||
<tr><td> </td><td><b>charger</b> - Verbraucher ist eine Ladeeinrichtung (Akku, Auto, Fahrrad, etc.) </td></tr>
|
||
<tr><td> </td><td><b>other</b> - Verbraucher ist keiner der vorgenannten Typen </td></tr>
|
||
<tr><td> </td><td><b>noSchedule</b> - für den Verbraucher erfolgt keine Einplanung oder automatische Schaltung. </td></tr>
|
||
<tr><td> </td><td>
|
||
Anzeigefunktionen oder manuelle Schaltungen sind verfügbar. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>power</b> </td><td>nominale Leistungsaufnahme des Verbrauchers (siehe Datenblatt) in W </td></tr>
|
||
<tr><td> </td><td>(kann auf "0" gesetzt werden) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>switchdev</b> </td><td>Das angegebene <device> wird als Schalter Device dem Verbraucher zugeordnet (optional). Schaltvorgänge werden mit diesem Gerät </td></tr>
|
||
<tr><td> </td><td>ausgeführt. Der Schlüssel ist für Verbraucher nützlich bei denen Energiemessung und Schaltung mit verschiedenen Geräten vorgenommen </td></tr>
|
||
<tr><td> </td><td>wird, z.B. Homematic oder readingsProxy. Ist switchdev angegeben, beziehen sich die Schlüssel on, off, swstate, auto, asynchron auf dieses Gerät. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>mode</b> </td><td>Planungsmodus des Verbrauchers (optional). Erlaubt sind: </td></tr>
|
||
<tr><td> </td><td><b>can</b> - Die Einplanung erfolgt zum Zeitpunkt mit wahrscheinlich genügend verfügbaren PV Überschuß (default) </td></tr>
|
||
<tr><td> </td><td> Der Start des Verbrauchers zum Planungszeitpunkt unterbleibt bei ungenügendem PV-Überschuß. </td></tr>
|
||
<tr><td> </td><td><b>must</b> - der Verbraucher wird optimiert eingeplant auch wenn wahrscheinlich nicht genügend PV Überschuß vorhanden sein wird </td></tr>
|
||
<tr><td> </td><td> Der Start des Verbrauchers erfolgt auch bei ungenügendem PV-Überschuß sofern eine
|
||
gesetzte "swoncond" Bedingung erfüllt und "swoffcond" nicht erfüllt ist. </td></tr>
|
||
<tr><td> </td><td><b>Device:Reading</b> - Device/Reading Kombination um den Planungsmodus dynamisch ändern zu können. Das Reading muß 'can' oder 'must' zurückgeben. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>icon</b> </td><td>Icon und ggf. dessen Farbe zur Darstellung des Verbrauchers in der Übersichtsgrafik (optional) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>mintime</b> </td><td>Einplanungsdauer (Minuten oder "SunPath") des Verbrauchers. (optional) </td></tr>
|
||
<tr><td> </td><td>Mit der Angabe von <b>SunPath</b> erfolgt die Planung entsprechend des Sonnenauf- und untergangs. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> </td><td><b>SunPath</b>[:<Offset_Sunrise>:<Offset_Sunset>] - die Einplanung erfolgt von Sonnenaufgang bis Sonnenuntergang. </td></tr>
|
||
<tr><td> </td><td> Optional kann eine positive / negative Verschiebung (Minuten) der Planungszeit bzgl. Sonnenaufgang bzw. Sonnenuntergang angegeben werden. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> </td><td>Ist mintime nicht angegeben, wird eine Standard Einplanungsdauer gemäß nachfolgender Tabelle verwendet. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> </td><td><b>Default mintime nach Verbrauchertyp:</b> </td></tr>
|
||
<tr><td> </td><td>- dishwasher: 180 Minuten </td></tr>
|
||
<tr><td> </td><td>- dryer: 90 Minuten </td></tr>
|
||
<tr><td> </td><td>- washingmachine: 120 Minuten </td></tr>
|
||
<tr><td> </td><td>- heater: 240 Minuten </td></tr>
|
||
<tr><td> </td><td>- charger: 120 Minuten </td></tr>
|
||
<tr><td> </td><td>- other: 60 Minuten </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>on</b> </td><td>Set-Kommando zum Einschalten des Verbrauchers (optional) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>off</b> </td><td>Set-Kommando zum Ausschalten des Verbrauchers (optional) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>swstate</b> </td><td>Reading welches den Schaltzustand des Verbrauchers anzeigt (default: 'state'). </td></tr>
|
||
<tr><td> </td><td><b>on-Regex</b> - regulärer Ausdruck für den Zustand 'ein' (default: 'on') </td></tr>
|
||
<tr><td> </td><td><b>off-Regex</b> - regulärer Ausdruck für den Zustand 'aus' (default: 'off') </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>asynchron</b> </td><td>die Art der Schaltstatus Ermittlung im Verbraucher Device. Die Statusermittlung des Verbrauchers nach einem Schaltbefehl erfolgt nur </td></tr>
|
||
<tr><td> </td><td>durch Abfrage innerhalb eines Datensammelintervals (synchron) oder zusätzlich durch Eventverarbeitung (asynchron). </td></tr>
|
||
<tr><td> </td><td><b>0</b> - ausschließlich synchrone Verarbeitung von Schaltzuständen (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - zusätzlich asynchrone Verarbeitung von Schaltzuständen durch Eventverarbeitung </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>notbefore</b> </td><td>Startzeitpunkt Verbraucher nicht vor angegebener Zeit 'Stunde[:Minute]' einplanen (optional) </td></tr>
|
||
<tr><td> </td><td>Der <Ausdruck> hat das Format hh[:mm] oder ist in {...} eingeschlossener Perl-Code der hh[:mm] zurückgibt. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>notafter</b> </td><td>Startzeitpunkt Verbraucher nicht nach angegebener Zeit 'Stunde[:Minute]' einplanen (optional) </td></tr>
|
||
<tr><td> </td><td>Der <Ausdruck> hat das Format hh[:mm] oder ist in {...} eingeschlossener Perl-Code der hh[:mm] zurückgibt. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>auto</b> </td><td>Reading im Verbraucherdevice welches das Schalten des Verbrauchers freigibt bzw. blockiert (optional) </td></tr>
|
||
<tr><td> </td><td>Ist der Schlüssel switchdev angegeben, wird das Reading in diesem Device gesetzt und ausgewertet. </td></tr>
|
||
<tr><td> </td><td>Readingwert = 1 - Schalten freigegeben (default), 0: Schalten blockiert </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>pcurr</b> </td><td>Reading:Einheit (W/kW) welches den aktuellen Energieverbrauch liefert (optional) </td></tr>
|
||
<tr><td> </td><td>:<Schwellenwert> (W) - Ab diesem Leistungsbezug wird der Verbraucher als aktiv gewertet. Die Angabe ist optional (default: 0) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>etotal</b> </td><td>Reading:Einheit (Wh/kWh) des Consumer Device, welches die Summe der verbrauchten Energie liefert (optional) </td></tr>
|
||
<tr><td> </td><td>:<Schwellenwert> (Wh) - Ab diesem Energieverbrauch pro Stunde wird der Verbrauch als gültig gewertet. Optionale Angabe (default: 0) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>swoncond</b> </td><td>Bedingung die zusätzlich erfüllt sein muß um den Verbraucher einzuschalten (optional). Der geplante Zyklus wird gestartet. </td></tr>
|
||
<tr><td> </td><td><b>Device</b> - Device zur Lieferung der zusätzlichen Einschaltbedingung </td></tr>
|
||
<tr><td> </td><td><b>Reading</b> - Reading zur Lieferung der zusätzlichen Einschaltbedingung </td></tr>
|
||
<tr><td> </td><td><b>Regex</b> - regulärer Ausdruck der für eine 'wahre' Bedingung erfüllt sein muß </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>swoffcond</b> </td><td>vorrangige Bedingung um den Verbraucher auszuschalten (optional). Der geplante Zyklus wird gestoppt. </td></tr>
|
||
<tr><td> </td><td><b>Device</b> - Device zur Lieferung der vorrangigen Ausschaltbedingung </td></tr>
|
||
<tr><td> </td><td><b>Reading</b> - Reading zur Lieferung der vorrangigen Ausschaltbedingung </td></tr>
|
||
<tr><td> </td><td><b>Regex</b> - regulärer Ausdruck der für eine 'wahre' Bedingung erfüllt sein muß </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>surpmeth</b> </td><td>Die möglichen Optionen legen das Verfahren zur Ermittlung des PV-Überschusses fest. (optional) </td></tr>
|
||
<tr><td> </td><td><b>default</b> - der PV-Überschuß wird aus dem Reading 'Current_Surplus' direkt ausgelesen. (default) </td></tr>
|
||
<tr><td> </td><td><b>median</b> - es wird der Median der letzten PV-Überschuß Messungen (max. 20) verwendet. </td></tr>
|
||
<tr><td> </td><td><b>2 .. 20</b> - der verwendete PV-Überschuß wird als Durchschnitt der angegebenen Anzahl Meßwerte gebildet. </td></tr>
|
||
<tr><td> </td><td><b>Device:Reading</b> - Device/Reading-Kombination die einen vom Nutzer bestimmten bzw. berechneten numerischen PV-Überschuß in Watt liefert. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>spignorecond</b> </td><td>Bedingung um einen fehlenden PV Überschuß zu ignorieren (optional). Bei erfüllter Bedingung wird der Verbraucher entsprechend </td></tr>
|
||
<tr><td> </td><td>der Planung eingeschaltet auch wenn zu dem Zeitpunkt kein PV Überschuß vorliegt. </td></tr>
|
||
<tr><td> </td><td><b>ACHTUNG:</b> Die Verwendung beider Schlüssel <I>spignorecond</I> und <I>interruptable</I> kann zu einem unerwünschten Verhalten führen! </td></tr>
|
||
<tr><td> </td><td><b>Device</b> - Device zur Lieferung der Bedingung </td></tr>
|
||
<tr><td> </td><td><b>Reading</b> - Reading welches die Bedingung enthält </td></tr>
|
||
<tr><td> </td><td><b>Regex</b> - regulärer Ausdruck der für eine 'wahre' Bedingung erfüllt sein muß </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>interruptable</b> </td><td>definiert die möglichen Unterbrechungsoptionen für den Verbraucher nachdem er gestartet wurde (optional) </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Verbraucher wird nicht temporär ausgeschaltet auch wenn der PV Überschuß die benötigte Energie unterschreitet (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - Verbraucher wird temporär ausgeschaltet falls der PV Überschuß die benötigte Energie unterschreitet </td></tr>
|
||
<tr><td> </td><td><b>Device:Reading:Regex[:Hysterese]</b> - Verbraucher wird temporär unterbrochen wenn der Wert des angegebenen </td></tr>
|
||
<tr><td> </td><td>Device:Readings auf den Regex matched oder unzureichender PV Überschuß (wenn power ungleich 0) vorliegt. </td></tr>
|
||
<tr><td> </td><td>Matched der Wert nicht mehr, wird der unterbrochene Verbraucher wieder eingeschaltet sofern ausreichender </td></tr>
|
||
<tr><td> </td><td>PV Überschuß (wenn power ungleich 0) vorliegt. </td></tr>
|
||
<tr><td> </td><td>Ist die optionale <b>Hysterese</b> angegeben, wird der Hysteresewert vom Readingswert subtrahiert und danach der Regex angewendet. </td></tr>
|
||
<tr><td> </td><td>Matched dieser und der originale Readingswert, wird der Verbraucher temporär unterbrochen. </td></tr>
|
||
<tr><td> </td><td>Der Verbraucher wird fortgesetzt, wenn sowohl der originale als auch der substrahierte Readingswert nicht (mehr) matchen. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>locktime</b> </td><td>Sperrzeiten in Sekunden für die Schaltung des Verbrauchers (optional). </td></tr>
|
||
<tr><td> </td><td><b>offlt</b> - Sperrzeit in Sekunden nachdem der Verbraucher ausgeschaltet oder unterbrochen wurde </td></tr>
|
||
<tr><td> </td><td><b>onlt</b> - Sperrzeit in Sekunden nachdem der Verbraucher eingeschaltet oder fortgesetzt wurde </td></tr>
|
||
<tr><td> </td><td>Der Verbraucher wird erst wieder geschaltet wenn die entsprechende Sperrzeit abgelaufen ist. </td></tr>
|
||
<tr><td> </td><td><b>Hinweis:</b> Der Schalter 'locktime' ist nur im Automatik-Modus wirksam. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>noshow</b> </td><td>Verbraucher in Grafik ausblenden oder einblenden (optional). </td></tr>
|
||
<tr><td> </td><td><b>0</b> - der Verbraucher wird eingeblendet (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - der Verbraucher wird ausgeblendet </td></tr>
|
||
<tr><td> </td><td><b>2</b> - der Verbraucher wird in der Verbraucherlegende ausgeblendet </td></tr>
|
||
<tr><td> </td><td><b>3</b> - der Verbraucher wird in der Flußgrafik ausgeblendet </td></tr>
|
||
<tr><td> </td><td><b>[Device:]Reading</b> - Reading im Verbraucher oder optional einem alternativen Device. </td></tr>
|
||
<tr><td> </td><td>Hat das Reading den Wert 0 oder ist nicht vorhanden, wird der Verbraucher eingeblendet. </td></tr>
|
||
<tr><td> </td><td>Die Wirkung der möglichen Readingwerte 1, 2 und 3 ist wie beschrieben. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>exconfc</b> </td><td>Verwendung des aufgezeichneten Energieverbrauchs des Verbrauchers zur Erstellung der Verbrauchsprognose (optional). </td></tr>
|
||
<tr><td> </td><td><b>0</b> - der historische Energieverbrauch des Verbrauchers wird zur Erstellung der Verbrauchsprognose verwendet (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - der historische Energieverbrauch des Verbrauchers wird von der Verbrauchsprognose ausgeschlossen. </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiele: </b> <br>
|
||
<b>attr <name> consumer01</b> wallplug icon=scene_dishwasher@orange type=dishwasher mode=can power=2500 on=on off=off notafter=20 etotal=total:kWh:5 <br>
|
||
<b>attr <name> consumer02</b> WPxw type=heater mode=can power=3000 mintime=180 on="on-for-timer 3600" notafter=12 auto=automatic <br>
|
||
<b>attr <name> consumer03</b> 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 <br>
|
||
<b>attr <name> consumer04</b> Shelly.shellyplug3 icon=scene_microwave_oven@red 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 <br>
|
||
<b>attr <name> consumer05</b> 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 <br>
|
||
<b>attr <name> consumer06</b> 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 <br>
|
||
<b>attr <name> consumer07</b> 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=1 <br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlAIdataStorageDuration"></a>
|
||
<li><b>ctrlAIdataStorageDuration <Tage></b> <br>
|
||
Sind die entsprechenden Voraussetzungen gegeben, werden Trainingsdaten für die modulinterne KI gesammelt und
|
||
gespeichert. <br>
|
||
Die Daten werden gelöscht wenn sie die angegebene Haltedauer (Tage) überschritten haben.<br>
|
||
(default: 1825)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlAIshiftTrainStart"></a>
|
||
<li><b>ctrlAIshiftTrainStart <1...23></b> <br>
|
||
Bei Nutzung der internen KI erfolgt ein tägliches Training.<br>
|
||
Der Start des Trainings erfolgt ca. 15 Minuten nach der im Attribut festgelegten vollen Stunde. <br>
|
||
Zum Beispiel würde bei einem eingestellten Wert von '3' das Traning ca. 03:15 Uhr starten. <br>
|
||
(default: 2)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlBackupFilesKeep"></a>
|
||
<li><b>ctrlBackupFilesKeep <Ganzzahl></b><br>
|
||
Legt die Anzahl der Generationen von Sicherungsdateien
|
||
(siehe <a href="#SolarForecast-set-operatingMemory">set <name> operatingMemory backup</a>) fest. <br>
|
||
Ist ctrlBackupFilesKeep explit auf '0' gesetzt, erfolgt keine automatische Generierung und Bereinigung von Sicherungsdateien. <br>
|
||
Eine manuelle Ausführung mit dem genannten Set-Kommando ist weiterhin möglich. <br>
|
||
(default: 3)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlBatSocManagementXX" data-pattern="ctrlBatSocManagement.*"></a>
|
||
<li><b>ctrlBatSocManagementXX lowSoc=<Wert> upSoC=<Wert> [maxSoC=<Wert>] [careCycle=<Wert>] </b> <br><br>
|
||
Sofern ein Batterie Device (setupBatteryDevXX) installiert ist, aktiviert dieses Attribut das Batterie
|
||
SoC-Management für dieses Batteriegerät. <br>
|
||
Das Reading <b>Battery_OptimumTargetSoC_XX</b> enthält den vom Modul berechneten optimalen Mindest-SoC. <br>
|
||
Das Reading <b>Battery_ChargeRequest_XX</b> wird auf '1' gesetzt, wenn der aktuelle SoC unter den Mindest-SoC gefallen
|
||
ist. <br>
|
||
In diesem Fall sollte die Batterie, unter Umständen mit Netzstrom, zwangsgeladen werden. <br>
|
||
Die Readings können zur Steuerung des SoC (State of Charge) sowie zur Steuerung des verwendeten Ladestroms
|
||
der Batterie verwendet werden. <br>
|
||
Durch das Modul selbst findet keine Steuerung der Batterie statt. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>lowSoc</b> </td><td>unterer Mindest-SoC - Die Batterie wird nicht tiefer als dieser Wert entladen (> 0) </td></tr>
|
||
<tr><td> <b>upSoC</b> </td><td>oberer Mindest-SoC - Der übliche Wert des optimalen SoC bewegt sich in Perioden mit hohen </td></tr>
|
||
<tr><td> </td><td>PV-Überschuß tendenziell zwischen 'lowSoC' und 'upSoC', in Perioden mit geringem PV-Überschuß </td></tr>
|
||
<tr><td> </td><td>tendenziell zwischen 'upSoC' und 'maxSoC' </td></tr>
|
||
<tr><td> <b>maxSoC</b> </td><td>maximaler Mindest-SoC - SoC Wert der mindestens im Abstand von 'careCycle' Tagen erreicht </td></tr>
|
||
<tr><td> </td><td>werden muß um den Ladungsausgleich im Speicherverbund auszuführen. </td></tr>
|
||
<tr><td> </td><td>Die Angabe ist optional (<= 100, default: 95) </td></tr>
|
||
<tr><td> <b>careCycle</b> </td><td>maximaler Abstand in Tagen, der zwischen zwei Ladungszuständen von mindestens 'maxSoC' </td></tr>
|
||
<tr><td> </td><td>auftreten darf. Die Angabe ist optional (default: 20) </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
Alle Werte sind ganze Zahlen in %. Dabei gilt: 'lowSoc' < 'upSoC' < 'maxSoC'. <br>
|
||
Die Ermittlung des optimalen SoC erfolgt nach folgendem Schema: <br><br>
|
||
|
||
<table>
|
||
<colgroup> <col width="2%"> <col width="98%"> </colgroup>
|
||
<tr><td> 1. </td><td>Ausgehend von 'lowSoc' wird der Mindest-SoC kurz vor Sonnenuntergang um 5% inkrementiert sofern am laufenden </td></tr>
|
||
<tr><td> </td><td>Tag 'maxSoC' nicht erreicht wurde und die PV-Prognose keinen hinreichenden Ertrag des kommenden Tages vorhersagt. </td></tr>
|
||
<tr><td> 2. </td><td>Wird 'maxSoC' (wieder) erreicht, wird Mindest-SoC um 5%, aber nicht tiefer als 'lowSoc', verringert. </td></tr>
|
||
<tr><td> 3. </td><td>Mindest-SoC wird soweit verringert, dass die prognostizierte PV Energie des aktuellen bzw. des folgenden Tages </td></tr>
|
||
<tr><td> </td><td>von der Batterie aufgenommen werden kann. Mindest-SoC wird typisch auf 'upSoc' und nicht tiefer als 'lowSoc' verringert. </td></tr>
|
||
<tr><td> 4. </td><td>Das Modul erfasst den letzten Zeitpunkt am 'maxSoC'-Level, um eine Ladung auf 'maxSoC' mindestens alle 'careCycle' </td></tr>
|
||
<tr><td> </td><td>Tage zu realisieren. Zu diesem Zweck wird der optimierte SoC in Abhängigkeit der Resttage bis zum nächsten </td></tr>
|
||
<tr><td> </td><td>'careCycle' Zeitpunkt derart verändert, dass durch eine tägliche 5% SoC-Steigerung 'maxSoC' am 'careCycle' Zeitpunkt </td></tr>
|
||
<tr><td> </td><td>rechnerisch erreicht wird. Wird zwischenzeitlich 'maxSoC' erreicht, beginnt der 'careCycle' Zeitraum erneut. </td></tr>
|
||
</table>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
attr <name> ctrlBatSocManagement01 lowSoc=10 upSoC=50 maxSoC=99 careCycle=25 <br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlConsRecommendReadings"></a>
|
||
<li><b>ctrlConsRecommendReadings </b><br>
|
||
Für die ausgewählten Consumer (Nummer) werden Readings der Form <b>consumerXX_ConsumptionRecommended</b> erstellt. <br>
|
||
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 des Consumers, kann aber von dem Planungszeitraum abweichen. <br>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlDebug"></a>
|
||
<li><b>ctrlDebug</b><br>
|
||
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. <br>
|
||
Die Debug Ebenen können miteinander kombiniert werden: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>aiProcess</b> </td><td>Datenanreicherung und Trainingsprozess der KI Unterstützung </td></tr>
|
||
<tr><td> <b>aiData</b> </td><td>Datennutzung KI im Prognoseprozess </td></tr>
|
||
<tr><td> <b>apiCall</b> </td><td>Abruf API Schnittstelle ohne Datenausgabe </td></tr>
|
||
<tr><td> <b>apiProcess</b> </td><td>Abruf und Verarbeitung von API Daten </td></tr>
|
||
<tr><td> <b>batteryManagement</b> </td><td>Steuerungswerte des Batterie Managements (SoC) </td></tr>
|
||
<tr><td> <b>collectData</b> </td><td>detailliierte Datensammlung </td></tr>
|
||
<tr><td> <b>consumerPlanning</b> </td><td>Consumer Einplanungsprozesse </td></tr>
|
||
<tr><td> <b>consumerSwitchingXX</b> </td><td>Operationen des internen Consumer Schaltmodul für Verbraucher XX </td></tr>
|
||
<tr><td> <b>consumption</b> </td><td>Verbrauchskalkulation, Verbrauchsvorhersage und -nutzung </td></tr>
|
||
<tr><td> <b>consumption_long</b> </td><td>erweiterte Ausgabe der Verbrauchsvorhersage Ermittlung </td></tr>
|
||
<tr><td> <b>dwdComm</b> </td><td>Kommunikation mit Webseite oder Server des Deutschen Wetterdienst (DWD) </td></tr>
|
||
<tr><td> <b>epiecesCalc</b> </td><td>Berechnung des spezifischen Energieverbrauchs je Betriebsstunde und Verbraucher </td></tr>
|
||
<tr><td> <b>graphic</b> </td><td>Informationen der Modulgrafik </td></tr>
|
||
<tr><td> <b>notifyHandling</b> </td><td>Ablauf der Eventverarbeitung im Modul </td></tr>
|
||
<tr><td> <b>pvCorrectionRead</b> </td><td>Anwendung PV Korrekturfaktoren </td></tr>
|
||
<tr><td> <b>pvCorrectionWrite</b> </td><td>Berechnung PV Korrekturfaktoren </td></tr>
|
||
<tr><td> <b>radiationProcess</b> </td><td>Sammlung und Verarbeitung der Solarstrahlungsdaten </td></tr>
|
||
<tr><td> <b>saveData2Cache</b> </td><td>Datenspeicherung in internen Speicherstrukturen </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlGenPVdeviation"></a>
|
||
<li><b>ctrlGenPVdeviation </b><br>
|
||
Legt die Methode zur Berechnung der Abweichung von prognostizierter und realer PV Erzeugung fest.
|
||
Das Reading <b>Today_PVdeviation</b> wird in Abhängigkeit dieser Einstellung erstellt. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>daily</b> </td><td>Berechnung und Erstellung von Today_PVdeviation erfolgt nach Sonnenuntergang (default) </td></tr>
|
||
<tr><td> <b>continuously</b> </td><td>Berechnung und Erstellung von Today_PVdeviation erfolgt fortlaufend </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li><br>
|
||
|
||
<a id="SolarForecast-attr-ctrlInterval"></a>
|
||
<li><b>ctrlInterval <Sekunden> </b><br>
|
||
Wiederholungsintervall der Datensammlung. <br>
|
||
Ist ctrlInterval explizit auf "0" gesetzt, erfolgt keine regelmäßige Datensammlung und muss mit
|
||
"get <name> data" extern gestartet werden. <br>
|
||
(default: 70)
|
||
<br><br>
|
||
|
||
<b>Hinweis:</b> Unabhängig vom eingestellten Intervall (auch bei "0") erfolgt einige Sekunden vor dem Ende
|
||
sowie nach dem Beginn einer vollen Stunde eine automatische Datensammlung. <br>
|
||
Weiterhin erfolgt eine automatische Datensammlung wenn ein Event eines als "asynchron"
|
||
definierten Gerätes (Consumer, Meter, etc.) empfangen und verarbeitet wird.
|
||
</li><br>
|
||
|
||
<a id="SolarForecast-attr-ctrlLanguage"></a>
|
||
<li><b>ctrlLanguage <DE | EN> </b><br>
|
||
Legt die benutzte Sprache des Devices fest. Die Sprachendefinition hat Auswirkungen auf die Modulgrafik und
|
||
verschiedene Readinginhalte. <br>
|
||
Ist das Attribut nicht gesetzt, definiert sich die Sprache durch die Einstellung des globalen Attributs "language". <br>
|
||
(default: EN)
|
||
</li><br>
|
||
|
||
<a id="SolarForecast-attr-ctrlNextDayForecastReadings"></a>
|
||
<li><b>ctrlNextDayForecastReadings <01,02,..,24> </b><br>
|
||
Wenn gesetzt, werden Readings der Form <b>Tomorrow_Hour<hour>_PVforecast</b> erstellt. <br>
|
||
Diese Readings enthalten die voraussichtliche PV Erzeugung des kommenden Tages. Dabei ist <hour> die
|
||
Stunde des Tages. <br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
attr <name> ctrlNextDayForecastReadings 09,11 <br>
|
||
# erstellt Readings für die Stunde 09 (08:00-09:00) und 11 (10:00-11:00) des kommenden Tages
|
||
</ul>
|
||
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlNextHoursSoCForecastReadings"></a>
|
||
<li><b>ctrlNextHoursSoCForecastReadings <00,02,..,23> </b><br>
|
||
Wenn gesetzt, werden Readings der Form Battery_NextHourXX_SoCforecast_BN erstellt sofern eine Batterie im
|
||
SolarForecast-Device registriert ist (siehe <a href="#SolarForecast-attr-setupBatteryDev">attr <name> setupBatteryDevXX </a>). <br>
|
||
Diese Readings enthalten den prognostizierten SoC-Wert (%) zum Ende der ausgewählten Stunde. <br>
|
||
Dabei ist 'XX' die Stunde in der Zukunft ausgehend von der aktuellen Stunde (00) und 'BN' die Nummer der registrierten Batterie.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
attr <name> ctrlNextHoursSoCForecastReadings 00,03,12,18 <br>
|
||
# erstellt Readings für die aktuelle Stunde (00) sowie die nachfolgenden Stunden +03, +12 und +18.
|
||
</ul>
|
||
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlShowLink"></a>
|
||
<li><b>ctrlShowLink </b><br>
|
||
Anzeige des Links zur Detailansicht des Device über dem Grafikbereich <br>
|
||
(default: 1)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlSolCastAPImaxReq"></a>
|
||
<li><b>ctrlSolCastAPImaxReq </b><br>
|
||
(nur bei Verwendung Model SolCastAPI) <br><br>
|
||
|
||
Die Einstellung der maximal möglichen täglichen Requests an die SolCast API. <br>
|
||
Dieser Wert wird von SolCast vorgegeben und kann sich entsprechend des SolCast
|
||
Lizenzmodells ändern. <br>
|
||
(default: 50)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlSolCastAPIoptimizeReq"></a>
|
||
<li><b>ctrlSolCastAPIoptimizeReq </b><br>
|
||
(nur bei Verwendung Model SolCastAPI) <br><br>
|
||
|
||
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. <br>
|
||
(default: 0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlSpecialReadings"></a>
|
||
<li><b>ctrlSpecialReadings </b><br>
|
||
Für die ausgewählten Kennzahlen und Indikatoren werden Readings mit dem
|
||
Namensschema 'special_<Indikator>' erstellt. Auswählbare Kennzahlen / Indikatoren sind: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>BatPowerIn_Sum</b> </td><td>die Summe der momentanen Batterieladeleistung aller definierten Batterie Geräte </td></tr>
|
||
<tr><td> <b>BatPowerOut_Sum</b> </td><td>die Summe der momentanen Batterieentladeleistung aller definierten Batterie Geräte </td></tr>
|
||
<tr><td> <b>allStringsFullfilled</b> </td><td>Erfüllungsstatus der fehlerfreien Generierung aller Strings </td></tr>
|
||
<tr><td> <b>conForecastTillNextSunrise</b> </td><td>Verbrauchsprognose von aktueller Stunde bis zum kommenden Sonnenaufgang </td></tr>
|
||
<tr><td> <b>currentAPIinterval</b> </td><td>das aktuelle Abrufintervall der gewählten Strahlungsdaten-API in Sekunden </td></tr>
|
||
<tr><td> <b>currentRunMtsConsumer_XX</b> </td><td>die Laufzeit (Minuten) des Verbrauchers "XX" seit dem letzten Einschalten. (letzter Laufzyklus) </td></tr>
|
||
<tr><td> <b>dayAfterTomorrowPVforecast</b> </td><td>liefert die Vorhersage der PV Erzeugung für Übermorgen (sofern verfügbar) ohne Autokorrektur (Rohdaten). </td></tr>
|
||
<tr><td> <b>daysUntilBatteryCare_XX</b> </td><td>Tage bis zur nächsten Batterie XX Pflege (Erreichen der Ladung 'maxSoC' aus Attribut ctrlBatSocManagementXX) </td></tr>
|
||
<tr><td> <b>lastretrieval_time</b> </td><td>der letzte Abrufzeitpunkt der gewählten Strahlungsdaten-API </td></tr>
|
||
<tr><td> <b>lastretrieval_timestamp</b> </td><td>der Timestamp der letzen Abrufzeitpunkt der gewählten Strahlungsdaten-API </td></tr>
|
||
<tr><td> <b>response_message</b> </td><td>die letzte Statusmeldung der gewählten Strahlungsdaten-API </td></tr>
|
||
<tr><td> <b>runTimeAvgDayConsumer_XX</b> </td><td>die durchschnittliche Laufzeit (Minuten) des Verbrauchers "XX" an einem Tag </td></tr>
|
||
<tr><td> <b>runTimeCentralTask</b> </td><td>die Laufzeit des letzten SolarForecast Intervalls (Gesamtprozess) in Sekunden </td></tr>
|
||
<tr><td> <b>runTimeTrainAI</b> </td><td>die Laufzeit des letzten KI Trainingszyklus in Sekunden </td></tr>
|
||
<tr><td> <b>runTimeLastAPIAnswer</b> </td><td>die letzte Antwortzeit des Strahlungsdaten-API Abrufs auf einen Request in Sekunden </td></tr>
|
||
<tr><td> <b>runTimeLastAPIProc</b> </td><td>die letzte Prozesszeit zur Verarbeitung der empfangenen Strahlungsdaten-API Daten </td></tr>
|
||
<tr><td> <b>SunMinutes_Remain</b> </td><td>die verbleibenden Minuten bis Sonnenuntergang des aktuellen Tages </td></tr>
|
||
<tr><td> <b>SunHours_Remain</b> </td><td>die verbleibenden Stunden bis Sonnenuntergang des aktuellen Tages </td></tr>
|
||
<tr><td> <b>todayConsumption</b> </td><td>der Energieverbrauch des Hauses am aktuellen Tag </td></tr>
|
||
<tr><td> <b>todayConsumptionForecast</b> </td><td>Verbrauchsprognose pro Stunde des aktuellen Tages (01-24) </td></tr>
|
||
<tr><td> <b>todayConForecastTillSunset</b> </td><td>Verbrauchsprognose von aktueller Stunde bis Stunde vor Sonnenuntergang </td></tr>
|
||
<tr><td> <b>todayDoneAPIcalls</b> </td><td>die Anzahl der am aktuellen Tag ausgeführten Strahlungsdaten-API Calls </td></tr>
|
||
<tr><td> <b>todayDoneAPIrequests</b> </td><td>die Anzahl der am aktuellen Tag ausgeführten Strahlungsdaten-API Requests </td></tr>
|
||
<tr><td> <b>todayGridConsumption</b> </td><td>die aus dem öffentlichen Netz bezogene Energie am aktuellen Tag </td></tr>
|
||
<tr><td> <b>todayGridFeedIn</b> </td><td>die in das öffentliche Netz eingespeiste PV Energie am aktuellen Tag </td></tr>
|
||
<tr><td> <b>todayMaxAPIcalls</b> </td><td>die maximal mögliche Anzahl Strahlungsdaten-API Calls. </td></tr>
|
||
<tr><td> </td><td>Ein Call kann mehrere API Requests enthalten. </td></tr>
|
||
<tr><td> <b>todayRemainingAPIcalls</b> </td><td>die Anzahl der am aktuellen Tag noch möglichen Strahlungsdaten-API Calls </td></tr>
|
||
<tr><td> <b>todayRemainingAPIrequests</b> </td><td>die Anzahl der am aktuellen Tag noch möglichen Strahlungsdaten-API Requests </td></tr>
|
||
<tr><td> <b>todayBatIn_XX</b> </td><td>die am aktuellen Tag in die Batterie XX geladene Energie </td></tr>
|
||
<tr><td> <b>todayBatInSum</b> </td><td>Summe der am aktuellen Tag in alle Batterien geladene Energie </td></tr>
|
||
<tr><td> <b>todayBatOut_XX</b> </td><td>die am aktuellen Tag aus der Batterie XX entnommene Energie </td></tr>
|
||
<tr><td> <b>todayBatOutSum</b> </td><td>Summe der am aktuellen Tag aus allen Batterien entnommene Energie </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-ctrlUserExitFn"></a>
|
||
<li><b>ctrlUserExitFn {<Code>} </b><br>
|
||
Nach jedem Zyklus (siehe Attribut <a href="#SolarForecast-attr-ctrlInterval ">ctrlInterval </a>) wird der in diesem
|
||
Attribut abgegebene Code ausgeführt. Der Code ist in geschweifte Klammern {...} einzuschließen. <br>
|
||
Dem Code werden die Variablen <b>$name</b> und <b>$hash</b> übergeben, die den Namen des SolarForecast Device und
|
||
dessen Hash enthalten. <br>
|
||
Im SolarForecast Device können Readings über die Funktion <b>storeReading</b> erzeugt und geändert werden.
|
||
<br>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
{ <br>
|
||
my $batdev = (split " ", AttrVal ($name, 'setupBatteryDev01', ''))[0]; <br>
|
||
my $pvfc = ReadingsNum ($name, 'RestOfDayPVforecast', 0); <br>
|
||
my $cofc = ReadingsNum ($name, 'RestOfDayConsumptionForecast', 0); <br>
|
||
my $diff = $pvfc - $cofc; <br>
|
||
<br>
|
||
storeReading ('userFn_Battery_device', $batdev); <br>
|
||
storeReading ('userFn_estimated_surplus', $diff); <br>
|
||
}
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-flowGraphicControl"></a>
|
||
<li><b>flowGraphicControl <Schlüssel1=Wert1> <Schlüssel2=Wert2> ... </b><br>
|
||
Durch die optionale Angabe der nachfolgend aufgeführten Schlüssel=Wert Paare können verschiedene
|
||
Anzeigeeigenschaften der Energieflußgrafik beeinflusst werden. <br>
|
||
Die Eingabe kann mehrzeilig erfolgen.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="26%"> <col width="74%"> </colgroup>
|
||
<tr><td> <b>animate</b> </td><td> Animiert die Energieflußgrafik sofern angezeigt. (<a href="#SolarForecast-attr-graphicSelect">graphicSelect</a>) </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Animation aus, <b>1</b> - Animation an, default: 1 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>consumerdist</b> </td><td>Steuert den Abstand zwischen den Verbraucher-Icons. </td></tr>
|
||
<tr><td> </td><td>Wert: <b>80 ... 500</b>, default: 130 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>h2consumerdist</b> </td><td>Erweiterung des vertikalen Abstandes zwischen dem Haus und den Verbraucher-Icons. </td></tr>
|
||
<tr><td> </td><td>Wert: <b>0 ... 999</b>, default: 0 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>shiftx</b> </td><td>Horizontale Verschiebung der Energieflußgrafik. </td></tr>
|
||
<tr><td> </td><td>Wert: <b>-80 ... 80</b>, default: 0 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>shifty</b> </td><td>Vertikale Verschiebung der Energieflußgrafik. </td></tr>
|
||
<tr><td> </td><td>Wert: <b>Ganzzahl</b>, default: 0 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>showconsumer</b> </td><td>Anzeige der Verbraucher in der Energieflußgrafik. </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Anzeige aus, <b>1</b> - Anzeige an, default: 1 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>showconsumerdummy</b> </td><td>Steuert die Anzeige des Dummy-Verbrauchers. Dem Dummy-Verbraucher wird der </td></tr>
|
||
<tr><td> </td><td>Energieverbrauch zugewiesen der anderen Verbrauchern nicht zugeordnet werden kann. </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Anzeige aus, <b>1</b> - Anzeige an, default: 1 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>showconsumerpower</b> </td><td>Steuert die Anzeige des Energieverbrauchs der Verbraucher. </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Anzeige aus, <b>1</b> - Anzeige an, default: 1 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>showconsumerremaintime</b> </td><td>Steuert die Anzeige der Restlaufzeit (Minuten) der Verbraucher. </td></tr>
|
||
<tr><td> </td><td><b>0</b> - Anzeige aus, <b>1</b> - Anzeige an, default: 1 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>size </b> </td><td>Größe der Energieflußgrafik in Pixel sofern angezeigt. (<a href="#SolarForecast-attr-graphicSelect">graphicSelect</a>) </td></tr>
|
||
<tr><td> </td><td>Wert: <b>Ganzzahl</b>, default: 400 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>strokecolina </b> </td><td>Farbe einer inaktiven Linie </td></tr>
|
||
<tr><td> </td><td>Wert: <b>Hex (z.B. #cc3300) oder Bezeichnung (z.B. red, blue)</b>, default: gray </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>strokecolsig </b> </td><td>Farbe einer aktiven Signallinie </td></tr>
|
||
<tr><td> </td><td>Wert: <b>Hex (z.B. #cc3300) oder Bezeichnung (z.B. red, blue)</b>, default: red </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>strokecolstd </b> </td><td>Farbe einer aktiven Standardlinie </td></tr>
|
||
<tr><td> </td><td>Wert: <b>Hex (z.B. #cc3300) oder Bezeichnung (z.B. red, blue)</b>, default: darkorange </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>strokewidth </b> </td><td>Breite der Linien </td></tr>
|
||
<tr><td> </td><td>Wert: <b>Ganzzahl</b>, default: 25 </td></tr>
|
||
</table>
|
||
</ul>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
attr <name> flowGraphicControl size=300 animate=0 consumerdist=100 showconsumer=1 showconsumerdummy=0 shiftx=-20 strokewidth=15 strokecolstd=#99cc00
|
||
</ul>
|
||
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam1Color"></a>
|
||
<li><b>graphicBeam1Color </b><br>
|
||
Farbauswahl des primären Balkens der ersten Ebene. <br>
|
||
(default: FFAC63)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam1FontColor"></a>
|
||
<li><b>graphicBeam1FontColor </b><br>
|
||
Auswahl der Schriftfarbe des primären Balkens der ersten Ebene. <br>
|
||
(default: 0D0D0D)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam2Color"></a>
|
||
<li><b>graphicBeam2Color </b><br>
|
||
Farbauswahl der sekundären Balken der ersten Ebene. <br>
|
||
(default: C4C4A7)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam2FontColor"></a>
|
||
<li><b>graphicBeam2FontColor </b><br>
|
||
Auswahl der Schriftfarbe der sekundären Balken der ersten Ebene. <br>
|
||
(default: 000000)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam3Color"></a>
|
||
<li><b>graphicBeam3Color </b><br>
|
||
Farbauswahl für die primären Balken der zweiten Ebene. <br>
|
||
(default: BED6C0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam3FontColor"></a>
|
||
<li><b>graphicBeam3FontColor </b><br>
|
||
Auswahl der Schriftfarbe der primären Balken der zweiten Ebene. <br>
|
||
(default: 000000)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam4Color"></a>
|
||
<li><b>graphicBeam4Color </b><br>
|
||
Farbauswahl für die sekundären Balken der zweiten Ebene. <br>
|
||
(default: DBDBD0)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeam4FontColor"></a>
|
||
<li><b>graphicBeam4FontColor </b><br>
|
||
Auswahl der Schriftfarbe der sekundären Balken der zweiten Ebene. <br>
|
||
(default: 000000)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeamXContent" data-pattern="graphicBeam.*Content"></a>
|
||
<li><b>graphicBeamXContent </b><br>
|
||
Legt den darzustellenden Inhalt der Balken in den Balkendiagrammen fest.
|
||
Die Balkendiagramme sind in zwei Ebenen verfügbar. <br>
|
||
Die Ebene 1 ist im Standard voreingestellt.
|
||
Der Inhalt wird durch die Attribute graphicBeam1Content und graphicBeam2Content bestimmt. <br>
|
||
Die Ebene 2 kann durch Setzen der Attribute graphicBeam3Content und graphicBeam4Content aktiviert werden. <br>
|
||
Die Attribute graphicBeam1Content und graphicBeam3Content stellen die primären Balken, die Attribute
|
||
graphicBeam2Content und graphicBeam4Content die sekundären Balken der jeweiligen Ebene dar.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td> <b>batsocforecast_XX</b> </td><td>der prognostizierte und in der Vergangenheit erreichte SOC (%) der Batterie XX </td></tr>
|
||
<tr><td> <b>consumption</b> </td><td>Energieverbrauch </td></tr>
|
||
<tr><td> <b>consumptionForecast</b> </td><td>prognostizierter Energieverbrauch </td></tr>
|
||
<tr><td> <b>energycosts</b> </td><td>Kosten des Energiebezuges aus dem Netz. Die Währung ist im setupMeterDev, Schlüssel conprice, definiert. </td></tr>
|
||
<tr><td> <b>feedincome</b> </td><td>Vergütung für die Netzeinspeisung. Die Währung ist im setupMeterDev, Schlüssel feedprice, definiert. </td></tr>
|
||
<tr><td> <b>gridconsumption</b> </td><td>Energiebezug aus dem öffentlichen Netz </td></tr>
|
||
<tr><td> <b>gridfeedin</b> </td><td>Einspeisung in das öffentliche Netz </td></tr>
|
||
<tr><td> <b>pvForecast</b> </td><td>prognostizierte PV-Erzeugung (default für graphicBeam2Content) </td></tr>
|
||
<tr><td> <b>pvReal</b> </td><td>reale PV-Erzeugung (default für graphicBeam1Content) </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Hinweis:</b> Die Auswahl der Parameter energycosts und feedincome ist nur sinnvoll wenn in setupMeterDev die
|
||
optionalen Schlüssel conprice und feedprice gesetzt sind.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeamHeightLevelX" data-pattern="graphicBeamHeightLevel.*"></a>
|
||
<li><b>graphicBeamHeightLevelX <value> </b><br>
|
||
Multiplikator zur Festlegung der maximalen Balkenhöhe der jeweiligen Ebene. <br>
|
||
In Verbindung mit dem Attribut <a href="#SolarForecast-attr-graphicHourCount">graphicHourCount</a>
|
||
lassen sich damit auch recht kleine Grafikausgaben erzeugen. <br>
|
||
(default: 200)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicBeamWidth"></a>
|
||
<li><b>graphicBeamWidth <value> </b><br>
|
||
Breite der Balken der Balkengrafik in px. Ohne gesetzen Attribut wird die Balkenbreite durch das Modul
|
||
automatisch bestimmt. <br>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicEnergyUnit"></a>
|
||
<li><b>graphicEnergyUnit <Wh | kWh> </b><br>
|
||
Definiert die Einheit zur Anzeige der elektrischen Leistung in der Grafik. Die Kilowattstunde wird auf eine
|
||
Nachkommastelle gerundet. <br>
|
||
(default: Wh)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHeaderDetail"></a>
|
||
<li><b>graphicHeaderDetail </b><br>
|
||
Auswahl der anzuzeigenden Zonen des Grafik Kopfbereiches. <br>
|
||
(default: all)
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>all</b> </td><td>alle Zonen des Kopfbereiches (default) </td></tr>
|
||
<tr><td> <b>co</b> </td><td>Verbrauchsbereich anzeigen </td></tr>
|
||
<tr><td> <b>pv</b> </td><td>Erzeugungsbereich anzeigen </td></tr>
|
||
<tr><td> <b>own</b> </td><td>Nutzerzone (siehe <a href="#SolarForecast-attr-graphicHeaderOwnspec">graphicHeaderOwnspec</a>) </td></tr>
|
||
<tr><td> <b>status</b> </td><td>Bereich der Statusinformationen </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHeaderOwnspec"></a>
|
||
<li><b>graphicHeaderOwnspec <Label>:<Reading>[@Device] <Label>:<Set>[@Device] <Label>:<Attr>[@Device] ... </b> <br><br>
|
||
|
||
Anzeige beliebiger Readings, Set-Kommandos und Attribute des SolarForecast Devices im Grafikkopf. <br>
|
||
Durch Angabe des optionalen [@Device] können Readings, Set-Kommandos und Attribute anderer Devices angezeigt werden. <br>
|
||
Die anzuzeigenden Werte werden durch Leerzeichen getrennt.
|
||
Es werden vier Werte (Felder) pro Zeile dargestellt. <br>
|
||
Die Eingabe kann mehrzeilig erfolgen. Werte mit den Einheiten "Wh" bzw. "kWh" werden entsprechend der Einstellung
|
||
des Attributs <a href="#SolarForecast-attr-graphicEnergyUnit">graphicEnergyUnit</a> umgerechnet.
|
||
<br><br>
|
||
|
||
Jeder Wert ist jeweils durch ein Label und das dazugehörige Reading verbunden durch ":" zu definieren. <br>
|
||
Leerzeichen im Label sind durch "&nbsp;" einzufügen, ein Zeilenumbruch durch "<br>". <br>
|
||
Ein leeres Feld in einer Zeile wird durch ":" erzeugt. <br>
|
||
Ein Zeilentitel kann durch Angabe von "#:<Text>" eingefügt werden, ein leerer Titel durch die Eingabe von "#".
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
<table>
|
||
<colgroup> <col width="35%"> <col width="65%"> </colgroup>
|
||
<tr><td> attr <name> graphicHeaderOwnspec </td><td># </td></tr>
|
||
<tr><td> </td><td>AutarkyRate:Current_AutarkyRate </td></tr>
|
||
<tr><td> </td><td>Überschuß:Current_Surplus </td></tr>
|
||
<tr><td> </td><td>aktueller&nbsp;Netzbezug:Current_GridConsumption </td></tr>
|
||
<tr><td> </td><td>: </td></tr>
|
||
<tr><td> </td><td># </td></tr>
|
||
<tr><td> </td><td>CO&nbsp;bis&nbsp;Sonnenuntergang:special_todayConForecastTillSunset </td></tr>
|
||
<tr><td> </td><td>PV&nbsp;Übermorgen:special_dayAfterTomorrowPVforecast </td></tr>
|
||
<tr><td> </td><td>InverterRelay:gridrelay_status@MySTP_5000 </td></tr>
|
||
<tr><td> </td><td>: </td></tr>
|
||
<tr><td> </td><td>#Batterie </td></tr>
|
||
<tr><td> </td><td>in&nbsp;heute:special_todayBatIn </td></tr>
|
||
<tr><td> </td><td>out&nbsp;heute:special_todayBatOut </td></tr>
|
||
<tr><td> </td><td>: </td></tr>
|
||
<tr><td> </td><td>: </td></tr>
|
||
<tr><td> </td><td>#Settings </td></tr>
|
||
<tr><td> </td><td>Autokorrektur:pvCorrectionFactor_Auto : : : </td></tr>
|
||
<tr><td> </td><td>Consumer<br>Neuplanung:consumerNewPlanning : : : </td></tr>
|
||
<tr><td> </td><td>Consumer<br>Sofortstart:consumerImmediatePlanning : : : </td></tr>
|
||
<tr><td> </td><td>Wetter:graphicShowWeather : : : </td></tr>
|
||
<tr><td> </td><td>History:graphicHistoryHour : : : </td></tr>
|
||
<tr><td> </td><td>ShowNight:graphicShowNight : : : </td></tr>
|
||
<tr><td> </td><td>Debug:ctrlDebug : : : </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHeaderOwnspecValForm"></a>
|
||
<li><b>graphicHeaderOwnspecValForm </b> <br><br>
|
||
|
||
Die mit dem Attribut <a href="#SolarForecast-attr-graphicHeaderOwnspec">graphicHeaderOwnspec</a> anzuzeigenden
|
||
Readings können mit sprintf und anderen Perl Operationen manipuliert werden. <br>
|
||
Es stehen zwei grundsätzliche, miteinander nicht kombinierbare Möglichkeiten der Notation zur Verfügung. <br>
|
||
Die Angabe der Notationen erfolgt grundsätzlich innerhalb von zwei geschweiften Klammern {...}.
|
||
<br><br>
|
||
|
||
<b>Notation 1: </b> <br>
|
||
Eine einfache Formatierung von Readings des eigenen Devices mit sprintf erfolgt wie in Zeile
|
||
'Current_AutarkyRate' bzw. 'Current_GridConsumption' angegeben. <br>
|
||
Andere Perl Operationen sind mit () zu klammern. Die jeweiligen Readingswerte und Einheiten stehen über
|
||
die Variablen $VALUE und $UNIT zur Verfügung. <br>
|
||
Readings anderer Devices werden durch die Angabe '<Device>.<Reading>' spezifiziert.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="20%"> <col width="80%"> </colgroup>
|
||
<tr><td>{ </td><td> </td></tr>
|
||
<tr><td> 'Current_AutarkyRate' </td><td> => "%.1f %%", </td></tr>
|
||
<tr><td> 'Current_GridConsumption' </td><td> => "%.2f $UNIT", </td></tr>
|
||
<tr><td> 'SMA_Energymeter.Cover_RealPower' </td><td> => q/($VALUE)." W"/, </td></tr>
|
||
<tr><td> 'SMA_Energymeter.L2_Cover_RealPower' </td><td> => "($VALUE).' W'", </td></tr>
|
||
<tr><td> 'SMA_Energymeter.L1_Cover_RealPower' </td><td> => '(sprintf "%.2f", ($VALUE / 1000))." kW"', </td></tr>
|
||
<tr><td>} </td><td> </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Notation 2: </b> <br>
|
||
Die Manipulation von Readingwerten und Einheiten erfolgt über Perl If ... else Strukturen. <br>
|
||
Der Struktur stehen Device, Reading, Readingwert und Einheit mit den Variablen $DEVICE, $READING, $VALUE und
|
||
$UNIT zur Verfügung. <br>
|
||
Bei Änderung der Variablen werden die neuen Werte entsprechend in die Anzeige übernommen.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="5%"> <col width="95%"> </colgroup>
|
||
<tr><td>{ </td><td> </td></tr>
|
||
<tr><td> </td><td> if ($READING eq 'Current_AutarkyRate') { </td></tr>
|
||
<tr><td> </td><td> $VALUE = sprintf "%.1f", $VALUE; </td></tr>
|
||
<tr><td> </td><td> $UNIT = "%"; </td></tr>
|
||
<tr><td> </td><td> } </td></tr>
|
||
<tr><td> </td><td> elsif ($READING eq 'Current_GridConsumption') { </td></tr>
|
||
<tr><td> </td><td> ... </td></tr>
|
||
<tr><td> </td><td> } </td></tr>
|
||
<tr><td>} </td><td> </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHeaderShow"></a>
|
||
<li><b>graphicHeaderShow </b><br>
|
||
Anzeigen/Verbergen des Grafik Tabellenkopfes mit Prognosedaten sowie bestimmten aktuellen und
|
||
statistischen Werten. <br>
|
||
(default: 1)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHistoryHour"></a>
|
||
<li><b>graphicHistoryHour </b><br>
|
||
Anzahl der vorangegangenen Stunden die in der Balkengrafik dargestellt werden. <br>
|
||
(default: 2)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHourCount"></a>
|
||
<li><b>graphicHourCount <4...24> </b><br>
|
||
Anzahl der Balken/Stunden in der Balkengrafk. <br>
|
||
(default: 24)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicHourStyle"></a>
|
||
<li><b>graphicHourStyle </b><br>
|
||
Format der Zeitangabe in der Balkengrafik. <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="30%"> <col width="70%"> </colgroup>
|
||
<tr><td> <b>nicht gesetzt</b> </td><td>nur Stundenangabe ohne Minuten (default) </td></tr>
|
||
<tr><td> <b>:00</b> </td><td>Stunden sowie Minuten zweistellig, z.B. 10:00 </td></tr>
|
||
<tr><td> <b>:0</b> </td><td>Stunden sowie Minuten einstellig, z.B. 8:0 </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicLayoutType"></a>
|
||
<li><b>graphicLayoutType <single | double | diff> </b><br>
|
||
Layout der Balkengrafik. <br>
|
||
Der darzustellende Inhalt der Balken wird durch die Attribute <b>graphicBeam1Content</b> bzw.
|
||
<b>graphicBeam2Content</b> bestimmt.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="10%"> <col width="90%"> </colgroup>
|
||
<tr><td> <b>double</b> </td><td>zeigt den primären Balken und den sekundären Balken an (default) </td></tr>
|
||
<tr><td> <b>single</b> </td><td>zeigt nur den primären Balken an </td></tr>
|
||
<tr><td> <b>diff</b> </td><td>Differenzanzeige. Es gilt: <Differenz> = <Wert primärer Balken> - <Wert sekundärer Balken> </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicSelect"></a>
|
||
<li><b>graphicSelect </b><br>
|
||
Wählt die anzuzeigenden Grafiksegmente des Moduls aus.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>both</b> </td><td>zeigt den Header, die Verbraucherlegende, Energiefluß- und Vorhersagegrafik an (default) </td></tr>
|
||
<tr><td> <b>flow</b> </td><td>zeigt den Header, die Verbraucherlegende und Energieflußgrafik an </td></tr>
|
||
<tr><td> <b>forecast</b> </td><td>zeigt den Header, die Verbraucherlegende und die Vorhersagegrafik an </td></tr>
|
||
<tr><td> <b>none</b> </td><td>zeigt nur den Header und die Verbraucherlegende an </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicShowDiff"></a>
|
||
<li><b>graphicShowDiff [no | top | bottom] </b><br>
|
||
Zusätzliche Anzeige der Differenz "<primärer Balkeninhalt> - <sekundärer Balkeninhalt>" im Kopf- oder
|
||
Fußbereich der Balkengrafik. <br>
|
||
(default: no)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicShowNight"></a>
|
||
<li><b>graphicShowNight </b><br>
|
||
Anzeigen oder Verbergen der Nachtstunden in der Balkengrafik.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="5%"> <col width="95%"> </colgroup>
|
||
<tr><td> <b>0</b> </td><td>keine Anzeige der Nachtstunden sofern kein Wert anzuzeigen ist (default) </td></tr>
|
||
<tr><td> </td><td>Sofern die ausgewählten Inhalte einen Wert enthalten, werden diese Balken dennoch dargestellt. </td></tr>
|
||
<tr><td> <b>01</b> </td><td>Wie '0', es findet jedoch eine Zeitsynchronisation zwischen der Ebene 1 </td></tr>
|
||
<tr><td> </td><td>und der nachfolgenden Balkengrafikebene statt. </td></tr>
|
||
<tr><td> <b>1</b> </td><td>Nachtstunden werden immer angezeigt </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicShowWeather"></a>
|
||
<li><b>graphicShowWeather </b><br>
|
||
Wettericons in der Balkengrafik anzeigen/verbergen. <br>
|
||
(default: 1)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicSpaceSize"></a>
|
||
<li><b>graphicSpaceSize <value> </b><br>
|
||
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. <br>
|
||
(default: 24)
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicWeatherColor"></a>
|
||
<li><b>graphicWeatherColor </b><br>
|
||
Farbe der Wetter-Icons in der Balkengrafik für die Tagesstunden.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-graphicWeatherColorNight"></a>
|
||
<li><b>graphicWeatherColorNight </b><br>
|
||
Farbe der Wetter-Icons für die Nachtstunden.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupBatteryDev" data-pattern="setupBatteryDev.*"></a>
|
||
<li><b>setupBatteryDevXX <Batterie Device Name> pin=<Readingname>:<Einheit> pout=<Readingname>:<Einheit>
|
||
cap=<Option> [intotal=<Readingname>:<Einheit>] [outtotal=<Readingname>:<Einheit>]
|
||
[charge=<Readingname>] [asynchron=<Option>] [show=<Option>] <br>
|
||
[[icon=<empfohlen>@<Farbe>]:[<aufladen>@<Farbe>]:[<entladen>@<Farbe>]:[icon=<unterlassen>@<Farbe>]] </b> <br><br>
|
||
|
||
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.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>pin</b> </td><td>Reading welches die aktuelle Batterieladeleistung liefert </td></tr>
|
||
<tr><td> <b>pout</b> </td><td>Reading welches die aktuelle Batterieentladeleistung liefert </td></tr>
|
||
<tr><td> <b>intotal</b> </td><td>Reading welches die totale Batterieladung als fortlaufenden Zähler liefert (optional) </td></tr>
|
||
<tr><td> <b>outtotal</b> </td><td>Reading welches die totale Batterieentladung als fortlaufenden Zähler liefert (optional) </td></tr>
|
||
<tr><td> <b>cap</b> </td><td>installierte Batteriekapazität. Option kann sein: </td></tr>
|
||
<tr><td> </td><td><b>numerischer Wert</b> - direkte Angabe der Batteriekapazität in Wh ohne die Einheit anzugeben! </td></tr>
|
||
<tr><td> </td><td><b><Readingname>:<Einheit></b> - Reading welches die Kapazität liefert und Einheit (Wh, kWh) </td></tr>
|
||
<tr><td> <b>charge</b> </td><td>Reading welches den aktuellen Ladezustand (SOC in Prozent) liefert (optional) </td></tr>
|
||
<tr><td> <b>Einheit</b> </td><td>die jeweilige Einheit (W,Wh,kW,kWh) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>icon</b> </td><td>Icon und/oder (nur) Farbe zur Darstellung der Batterie in der Balkengrafik (optional) </td></tr>
|
||
<tr><td> </td><td>Die Farbe kann als Bezeichner (z.B. blue) oder HEX-Wert (z.B. #d9d9d9) angegeben werden. </td></tr>
|
||
<tr><td> </td><td><b><empfohlen></b> - die Aufladung ist empfohlen aber inaktiv (kein Aufladen oder Entladen) </td></tr>
|
||
<tr><td> </td><td><b><aufladen></b> - wird verwendet wenn die Batterie aktuell aufgeladen wird </td></tr>
|
||
<tr><td> </td><td><b><entladen></b> - wird verwendet wenn die Batterie aktuell entladen wird </td></tr>
|
||
<tr><td> </td><td><b><unterlassen></b> - wird verwendet wenn die Aufladung nicht empfohlen ist </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>show</b> </td><td>Steuerung der Anzeige der Batterie in der Balkengrafik (optional) </td></tr>
|
||
<tr><td> </td><td><b>0</b> - keine Anzeige des Gerätes (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - Anzeige des Gerätes in der Balkengrafik Ebene 1 </td></tr>
|
||
<tr><td> </td><td><b>2</b> - Anzeige des Gerätes in der Balkengrafik Ebene 2 </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>asynchron</b> </td><td>Modus der Datensammlung entsprechend Einstellung ctrlInterval (synchron) oder zusätzlich durch </td></tr>
|
||
<tr><td> </td><td>Eventverarbeitung (asynchron). </td></tr>
|
||
<tr><td> </td><td><b>0</b> - keine Datensammlung nach Empfang eines Events des Gerätes (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - auslösen einer Datensammlung bei Empfang eines Events des Gerätes </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Sonderfälle:</b> Sollte das Reading für pin und pout identisch, aber vorzeichenbehaftet sein,
|
||
können die Schlüssel pin und pout wie folgt definiert werden: <br><br>
|
||
<ul>
|
||
pin=-pout (ein negativer Wert von pout wird als pin verwendet) <br>
|
||
pout=-pin (ein negativer Wert von pin wird als pout verwendet)
|
||
</ul>
|
||
<br>
|
||
|
||
Die Einheit entfällt in dem jeweiligen Sonderfall. <br><br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
attr <name> setupBatteryDev01 BatDummy pin=BatVal:W pout=-pin intotal=BatInTot:Wh outtotal=BatOutTot:Wh cap=BatCap:kWh icon=measure_battery_50@#262626:@yellow:measure_battery_100@red
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Hinweis:</b> Durch Löschen des Attributes werden ebenfalls die intern korrespondierenden Daten entfernt.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupInverterDev" data-pattern="setupInverterDev.*"></a>
|
||
<li><b>setupInverterDevXX <Inverter Device Name> pv=<Readingname>:<Einheit> etotal=<Readingname>:<Einheit>
|
||
capacity=<max. WR-Leistung> [strings=<String1>,<String2>,...] [asynchron=<Option>]
|
||
[feed=<Liefertyp>] [limit=<0..100>]
|
||
[icon=<Tag>[@<Farbe>][:<Nacht>[@<Farbe>]]] </b> <br><br>
|
||
|
||
Legt ein beliebiges Wechselrichter-Gerät bzw. Solar-Ladegerät und dessen Readings zur Lieferung der aktuellen
|
||
PV Erzeugungswerte fest. <br>
|
||
Ein Solar-Ladegerät wandelt die von den Solarzellen gelieferte Energie nicht in Wechselstrom um, sondern
|
||
lädt damit direkt eine vorhandene Batterie <br>
|
||
(z.B. ein Victron SmartSolar MPPT). <br>
|
||
Es können nacheinander mehrere Geräte in den Attributen setupInverterDev01..XX definiert werden. <br>
|
||
Dabei kann es sich auch um ein Dummy Gerät mit entsprechenden Readings handeln. <br>
|
||
Die Werte mehrerer Wechselrichter kann man z.B. in einem Dummy Gerät zusammenführen und gibt dieses Gerät mit
|
||
den entsprechenden Readings an. <br>
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>pv</b> </td><td>Reading welches die aktuelle PV-Erzeugung als positiven Wert liefert </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>etotal</b> </td><td>Reading welches die gesamte erzeugte PV-Energie liefert (ein stetig aufsteigender Zähler) </td></tr>
|
||
<tr><td> </td><td>Sollte des Reading die Vorgabe eines stetig aufsteigenden Zählers verletzen, behandelt </td></tr>
|
||
<tr><td> </td><td>SolarForecast diesen Fehler und meldet die aufgetretene Situation durch einen Logeintrag. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>Einheit</b> </td><td>die jeweilige Einheit (W,kW,Wh,kWh) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>capacity</b> </td><td>Bemessungsleistung des Wechselrichters gemäß Datenblatt, d.h. max. möglicher Output in Watt </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>strings</b> </td><td>Komma getrennte Liste der dem Wechselrichter zugeordneten Strings (optional). Die Stringnamen </td></tr>
|
||
<tr><td> </td><td>werden im Attribut <a href="#SolarForecast-attr-setupInverterStrings">setupInverterStrings</a> definiert. </td></tr>
|
||
<tr><td> </td><td>Ist 'strings' nicht angegeben, werden alle definierten Stringnamen dem Wechselrichter zugeordnet. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>feed</b> </td><td>Definiert spezielle Eigenschaften der Energielieferung des Gerätes (optional). </td></tr>
|
||
<tr><td> </td><td>Ist der Schlüssel nicht gesetzt, speist das Gerät die PV-Energie in das Wechselstromnetz des Hauses ein. </td></tr>
|
||
<tr><td> </td><td><b>bat</b> - Solar-Ladegerät zur Batterie Direktladung. Ein Überschuß wird dem Inverter/Hausnetz zugeführt. </td></tr>
|
||
<tr><td> </td><td><b>grid</b> - die Energie wird ausschließlich in das öffentlich Netz eingespeist </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>limit</b> </td><td>Definiert eine eventuelle Wirkleistungsbeschränkung in % (optional). </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>icon</b> </td><td>Icon zur Darstellung des Inverters in der Flowgrafik (optional) </td></tr>
|
||
<tr><td> </td><td><b><Tag></b> - Icon und ggf. Farbe bei Aktivität nach Sonnenaufgang </td></tr>
|
||
<tr><td> </td><td><b><Nacht></b> - Icon und ggf. Farbe nach Sonnenuntergang, sonst wird die Mondphase angezeigt </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>asynchron</b> </td><td>Modus der Datensammlung entsprechend Einstellung ctrlInterval (synchron) oder zusätzlich durch </td></tr>
|
||
<tr><td> </td><td>Eventverarbeitung (asynchron). (optional) </td></tr>
|
||
<tr><td> </td><td><b>0</b> - keine Datensammlung nach Empfang eines Events des Gerätes (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - auslösen einer Datensammlung bei Empfang eines Events des Gerätes </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
attr <name> setupInverterDev01 STP5000 pv=total_pac:kW etotal=etotal:kWh capacity=5000 asynchron=1 strings=Garage icon=inverter@red:solar
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Hinweis:</b> Durch Löschen des Attributes werden ebenfalls die intern korrespondierenden Daten entfernt.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupInverterStrings"></a>
|
||
<li><b>setupInverterStrings <Stringname1>[,<Stringname2>,<Stringname3>,...] </b> <br><br>
|
||
|
||
Bezeichnungen der aktiven Strings. Diese Bezeichnungen werden als Schlüssel in den weiteren
|
||
Settings verwendet. <br>
|
||
Bei Nutzung einer KI basierenden API (z.B. VictronKI-API) ist nur "<b>KI-based</b>" einzutragen unabhängig davon
|
||
welche realen Strings existieren. <br><br>
|
||
|
||
<ul>
|
||
<b>Beispiele: </b> <br>
|
||
attr <name> setupInverterStrings Ostdach,Südgarage,S3 <br>
|
||
attr <name> setupInverterStrings KI-based <br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupMeterDev"></a>
|
||
<li><b>setupMeterDev <Meter Device Name> gcon=<Readingname>:<Einheit> contotal=<Readingname>:<Einheit>
|
||
gfeedin=<Readingname>:<Einheit> feedtotal=<Readingname>:<Einheit>
|
||
[conprice=<Feld>] [feedprice=<Feld>] [asynchron=<Option>] </b> <br><br>
|
||
|
||
Legt ein beliebiges Device und seine Readings zur Energiemessung in bzw. aus dem öffentlichen Netz fest.
|
||
Das Modul geht davon aus, dass der numerische Wert der Readings positiv ist.
|
||
Es kann auch ein Dummy Device mit entsprechenden Readings sein.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>gcon</b> </td><td>Reading welches die aktuell aus dem Netz bezogene Leistung liefert </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>contotal</b> </td><td>Reading welches die Summe der aus dem Netz bezogenen Energie liefert (ein sich stetig erhöhender Zähler) </td></tr>
|
||
<tr><td> </td><td>Wird der Zähler zu Beginn des Tages auf '0' zurückgesetzt (Tageszähler), behandelt das Modul diese Situation entsprechend. </td></tr>
|
||
<tr><td> </td><td>In diesem Fall erfolgt eine Meldung im Log mit verbose 3. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>gfeedin</b> </td><td>Reading welches die aktuell in das Netz eingespeiste Leistung liefert </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>feedtotal</b> </td><td>Reading welches die Summe der in das Netz eingespeisten Energie liefert (ein sich stetig erhöhender Zähler) </td></tr>
|
||
<tr><td> </td><td>Wird der Zähler zu Beginn des Tages auf '0' zurückgesetzt (Tageszähler), behandelt das Modul diese Situation entsprechend. </td></tr>
|
||
<tr><td> </td><td>In diesem Fall erfolgt eine Meldung im Log mit verbose 3. </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>Einheit</b> </td><td>die jeweilige Einheit (W,kW,Wh,kWh) </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>conprice</b> </td><td>Preis für den Bezug einer kWh (optional). Die Angabe <Feld> ist in einer der folgenden Varianten möglich: </td></tr>
|
||
<tr><td> </td><td><Preis>:<Währung> - Preis als numerischer Wert und dessen Währung </td></tr>
|
||
<tr><td> </td><td><Reading>:<Währung> - Reading des <b>Meter Device</b> das den Preis enthält : Währung </td></tr>
|
||
<tr><td> </td><td><Device>:<Reading>:<Währung> - beliebiges Device und Reading welches den Preis enthält : Währung </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>feedprice</b> </td><td>Vergütung für die Einspeisung einer kWh (optional). Die Angabe <Feld> ist in einer der folgenden Varianten möglich: </td></tr>
|
||
<tr><td> </td><td><Vergütung>:<Währung> - Vergütung als numerischer Wert und dessen Währung </td></tr>
|
||
<tr><td> </td><td><Reading>:<Währung> - Reading des <b>Meter Device</b> das die Vergütung enthält : Währung </td></tr>
|
||
<tr><td> </td><td><Device>:<Reading>:<Währung> - beliebiges Device und Reading welches die Vergütung enthält : Währung </td></tr>
|
||
<tr><td> </td><td> </td></tr>
|
||
<tr><td> <b>asynchron</b> </td><td>Modus der Datensammlung entsprechend Einstellung ctrlInterval (synchron) oder zusätzlich durch </td></tr>
|
||
<tr><td> </td><td>Eventverarbeitung (asynchron). </td></tr>
|
||
<tr><td> </td><td><b>0</b> - keine Datensammlung nach Empfang eines Events des Gerätes (default) </td></tr>
|
||
<tr><td> </td><td><b>1</b> - auslösen einer Datensammlung bei Empfang eines Events des Gerätes </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Sonderfälle:</b> Sollte das Reading für gcon und gfeedin identisch, aber vorzeichenbehaftet sein,
|
||
können die Schlüssel gfeedin und gcon wie folgt definiert werden: <br><br>
|
||
<ul>
|
||
gfeedin=-gcon (ein negativer Wert von gcon wird als gfeedin verwendet) <br>
|
||
gcon=-gfeedin (ein negativer Wert von gfeedin wird als gcon verwendet)
|
||
</ul>
|
||
<br>
|
||
|
||
Die Einheit entfällt in dem jeweiligen Sonderfall. <br><br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
attr <name> setupMeterDev Meter gcon=Wirkleistung:W contotal=BezWirkZaehler:kWh gfeedin=-gcon feedtotal=EinWirkZaehler:kWh conprice=powerCost:€ feedprice=0.1269:€
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Hinweis:</b> Durch Löschen des Attributes werden ebenfalls die intern korrespondierenden Daten entfernt.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupOtherProducer" data-pattern="setupOtherProducer.*"></a>
|
||
<li><b>setupOtherProducerXX <Device Name> pcurr=<Readingname>:<Einheit> etotal=<Readingname>:<Einheit> [icon=<Icon>[@<Farbe>]] </b> <br><br>
|
||
|
||
Legt ein beliebiges Device und dessen Readings zur Lieferung sonstiger Erzeugungswerte fest
|
||
(z.B. BHKW, Winderzeugung, Notstromaggregat). Dieses Device ist nicht für PV-Erzeugung vorgsehen.
|
||
Es kann auch ein Dummy Device mit entsprechenden Readings sein.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="15%"> <col width="85%"> </colgroup>
|
||
<tr><td> <b>icon</b> </td><td>Icon und ggf. Farbe bei Aktivität zur Darstellung des Producers in der Flowgrafik (optional) </td></tr>
|
||
<tr><td> <b>pcurr</b> </td><td>Reading welches die aktuelle Erzeugung als positiven Wert oder einen Eigenverbrauch (Sonderfall) als negativen Wert liefert </td></tr>
|
||
<tr><td> <b>etotal</b> </td><td>Reading welches die gesamte erzeugte Energie liefert (ein stetig aufsteigender Zähler) </td></tr>
|
||
<tr><td> </td><td>Sollte des Reading die Vorgabe eines stetig aufsteigenden Zählers verletzen, behandelt </td></tr>
|
||
<tr><td> </td><td>SolarForecast diesen Fehler und meldet die aufgetretene Situation durch einen Logeintrag. </td></tr>
|
||
<tr><td> <b>Einheit</b> </td><td>die jeweilige Einheit (W,kW,Wh,kWh) </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
attr <name> setupOtherProducer01 windwheel pcurr=total_pac:kW etotal=etotal:kWh icon=Ventilator_wind@darkorange
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Hinweis:</b> Durch Löschen des Attributes werden ebenfalls die intern korrespondierenden Daten entfernt.
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupRadiationAPI"></a>
|
||
<li><b>setupRadiationAPI </b> <br><br>
|
||
|
||
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. <br><br>
|
||
|
||
<b>Hinweis:</b> Ist im Attribut 'setupWeatherDev1' ebenfalls eine OpenMeteo API gesetzt, werden die Einstellungen
|
||
beider Attribute harmonisiert wobei die Einstellung von 'setupRadiationAPI' führend ist. <br><br>
|
||
|
||
<b>OpenMeteoDWD-API</b> <br>
|
||
|
||
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
|
||
<a href='https://open-meteo.com/en/docs/dwd-api' target='_blank'>API Dokumentation</a> verfügbar.
|
||
<br><br>
|
||
|
||
<b>OpenMeteoDWDEnsemble-API</b> <br>
|
||
|
||
Diese Open-Meteo API Variante bietet Zugang zum globalen
|
||
<a href='https://www.dwd.de/DE/forschung/wettervorhersage/num_modellierung/04_ensemble_methoden/ensemble_vorhersage/ensemble_vorhersagen.html' target='_blank'>Ensemble-Vorhersagesystem (EPS)</a>
|
||
des DWD. <br>
|
||
Es werden die Ensemble Modelle ICON-D2-EPS, ICON-EU-EPS und ICON-EPS nahtlos vereint. <br>
|
||
<a href='https://openmeteo.substack.com/p/ensemble-weather-forecast-api' target='_blank'>Ensemble-Wetterprognosen</a> 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.
|
||
<br><br>
|
||
|
||
<b>OpenMeteoWorld-API</b> <br>
|
||
|
||
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.
|
||
<br><br>
|
||
|
||
<b>SolCast-API</b> <br>
|
||
|
||
Die API-Nutzung benötigt vorab ein oder mehrere API-keys (Accounts) sowie ein oder mehrere Rooftop-ID's
|
||
die auf der <a href='https://toolkit.solcast.com.au/rooftop-sites/' target='_blank'>SolCast</a> Webseite angelegt
|
||
werden müssen.
|
||
Ein Rooftop ist im SolarForecast-Kontext mit einem <a href="#SolarForecast-attr-setupInverterStrings">setupInverterString</a>
|
||
gleichzusetzen. <br>
|
||
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
|
||
<a href="#SolarForecast-attr-ctrlSolCastAPIoptimizeReq ">ctrlSolCastAPIoptimizeReq </a>.
|
||
<br><br>
|
||
|
||
<b>ForecastSolar-API</b> <br>
|
||
|
||
Die kostenfreie Nutzung der <a href='https://doc.forecast.solar/start' target='_blank'>Forecast.Solar API</a>
|
||
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. <br>
|
||
Hinweis: Nach den bisherigen Erfahrungen unzuverlässig und nicht zu empfehlen.
|
||
<br><br>
|
||
|
||
<b>VictronKI-API</b> <br>
|
||
|
||
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 <a href="#SolarForecast-attr-setupInverterStrings">setupInverterStrings</a>
|
||
einzutragen. <br>
|
||
Im Victron Energy VRM Portal ist als Voraussetzung der Standort der PV-Anlage anzugeben. <br>
|
||
Siehe dazu auch den Blog-Beitrag
|
||
<a href="https://www.victronenergy.com/blog/2023/07/05/new-vrm-solar-production-forecast-feature/">Introducing Solar Production Forecast</a>.
|
||
<br><br>
|
||
|
||
<b>DWD_OpenData Device</b> <br>
|
||
|
||
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 <a href="http://fhem.de/commandref.html#DWD_OpenData">DWD_OpenData Commandref</a>). <br>
|
||
Um eine gute Strahlungsprognose zu erhalten, sollte eine nahe dem Anlagenstandort gelegene DWD-Station genutzt
|
||
werden. <br>
|
||
Leider liefern nicht alle
|
||
<a href="https://www.dwd.de/DE/leistungen/klimadatendeutschland/statliste/statlex_html.html;jsessionid=EC5F572A52EB69684D552DCF6198F290.live31092?view=nasPublication&nn=16102">DWD-Stationen</a>
|
||
die benötigten Rad1h-Werte. <br>
|
||
Erläuterungen zu den Stationen sind im
|
||
<a href="https://www.dwd.de/DE/leistungen/klimadatendeutschland/stationsliste.html">Stationslexikon</a> aufgeführt. <br>
|
||
Im ausgewählten DWD_OpenData Device müssen mindestens die folgenden Attribute gesetzt sein: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>forecastDays</b> </td><td>1 (auf >= 2 setzen wenn eine längere Vorhersage gewünscht ist) </td></tr>
|
||
<tr><td> <b>forecastProperties</b> </td><td>Rad1h </td></tr>
|
||
<tr><td> <b>forecastResolution</b> </td><td>1 </td></tr>
|
||
<tr><td> <b>forecastStation</b> </td><td><Stationscode der ausgewerteten DWD Station> </td></tr>
|
||
<tr><td> </td><td><b>Hinweis:</b> Die ausgewählte DWD Station muß Strahlungswerte (Rad1h Readings) liefern. </td></tr>
|
||
<tr><td> </td><td>Nicht alle Stationen liefern diese Daten! </td></tr>
|
||
</table>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupRoofTops"></a>
|
||
<li><b>setupRoofTops <Stringname1>=<pk> [<Stringname2>=<pk> <Stringname3>=<pk> ...] </b> <br>
|
||
(nur bei Verwendung Model SolCastAPI) <br><br>
|
||
|
||
Es erfolgt die Zuordnung des Strings "StringnameX" zu einem Schlüssel <pk>. Der Schlüssel <pk> wurde mit dem
|
||
Setter <a href="#SolarForecast-set-roofIdentPair">roofIdentPair</a> angelegt. Damit wird bei Abruf des Rooftops (=String)
|
||
in der SolCast API die zu verwendende Rooftop-ID sowie der zu verwendende API-Key festgelegt. <br>
|
||
Der StringnameX ist ein Schlüsselwert des Attributs <b>setupInverterStrings</b>.
|
||
<br><br>
|
||
|
||
<ul>
|
||
<b>Beispiel: </b> <br>
|
||
attr <name> setupRoofTops Ostdach=p1 Südgarage=p2 S3=p3 <br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupStringPeak"></a>
|
||
<li><b>setupStringPeak <Stringname1>=<Peak> [<Stringname2>=<Peak> <Stringname3>=<Peak> ...] </b> <br><br>
|
||
|
||
Die DC Peakleistung des Strings "StringnameX" in kWp. Der Stringname ist ein Schlüsselwert des
|
||
Attributs <b>setupInverterStrings</b>. <br>
|
||
Bei Verwendung einer KI basierenden API (z.B. Model VictronKiAPI) sind die Peakleistungen aller vorhandenen
|
||
Strings als Summe dem Stringnamen <b>KI-based</b> zuzuordnen. <br><br>
|
||
|
||
<ul>
|
||
<b>Beispiele: </b> <br>
|
||
attr <name> setupStringPeak Ostdach=5.1 Südgarage=2.0 S3=7.2 <br>
|
||
attr <name> setupStringPeak KI-based=14.3 (bei KI basierender API)<br>
|
||
</ul>
|
||
</li>
|
||
<br>
|
||
|
||
<a id="SolarForecast-attr-setupWeatherDev" data-pattern="setupWeatherDev.*"></a>
|
||
<li><b>setupWeatherDevX </b> <br><br>
|
||
|
||
Gibt das Gerät oder die API zur Lieferung der erforderlichen Wetterdaten (Wolkendecke, Niederschlag usw.) an.<br>
|
||
Das Attribut 'setupWeatherDev1' definiert den führenden Wetterdienst und ist zwingend erforderlich. <br><br>
|
||
|
||
<b>Hinweis:</b> Ist im Attribut 'setupRadiationAPI' ebenfalls eine OpenMeteo API gesetzt, werden die Einstellungen
|
||
beider Attribute harmonisiert wobei die Einstellung von 'setupRadiationAPI' führend ist. <br><br>
|
||
|
||
<b>OpenMeteoDWD-API</b> <br>
|
||
|
||
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
|
||
<a href='https://open-meteo.com/en/docs/dwd-api' target='_blank'>API Dokumentation</a> verfügbar.
|
||
<br><br>
|
||
|
||
<b>OpenMeteoDWDEnsemble-API</b> <br>
|
||
|
||
Diese Open-Meteo API Variante bietet Zugang zum globalen
|
||
<a href='https://www.dwd.de/DE/forschung/wettervorhersage/num_modellierung/04_ensemble_methoden/ensemble_vorhersage/ensemble_vorhersagen.html' target='_blank'>Ensemble-Vorhersagesystem (EPS)</a>
|
||
des DWD. <br>
|
||
Es werden die Ensemble Modelle ICON-D2-EPS, ICON-EU-EPS und ICON-EPS nahtlos vereint. <br>
|
||
<a href='https://openmeteo.substack.com/p/ensemble-weather-forecast-api' target='_blank'>Ensemble-Wetterprognosen</a> 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.
|
||
<br><br>
|
||
|
||
<b>OpenMeteoWorld-API</b> <br>
|
||
|
||
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.
|
||
<br><br>
|
||
|
||
<b>DWD Gerät</b> <br>
|
||
|
||
Alternativ zu Open-Meteo kann ein FHEM 'DWD_OpenData'-Gerät zur Lieferung der Wetterdaten dienen.<br>
|
||
Ist noch kein Gerät dieses Typs vorhanden, muß zunächst mindestens ein DWD_OpenData Gerät
|
||
definiert werden (siehe <a href="http://fhem.de/commandref.html#DWD_OpenData">DWD_OpenData Commandref</a>). <br>
|
||
Sind mehr als ein setupWeatherDevX angegeben, wird der Durchschnitt aller Wetterstationen ermittelt
|
||
sofern der jeweilige Wert geliefert wurde und numerisch ist. <br>
|
||
Anderenfalls werden immer die Daten von 'setupWeatherDev1' als führendes Wetterdevice genutzt. <br>
|
||
Im ausgewählten DWD_OpenData Gerät müssen mindestens diese Attribute gesetzt sein: <br><br>
|
||
|
||
<ul>
|
||
<table>
|
||
<colgroup> <col width="25%"> <col width="75%"> </colgroup>
|
||
<tr><td> <b>forecastDays</b> </td><td>1 </td></tr>
|
||
<tr><td> <b>forecastProperties</b> </td><td>TTT,Neff,RR1c,ww,SunUp,SunRise,SunSet </td></tr>
|
||
<tr><td> <b>forecastResolution</b> </td><td>1 </td></tr>
|
||
<tr><td> <b>forecastStation</b> </td><td><Stationscode der ausgewerteten DWD Station> </td></tr>
|
||
</table>
|
||
</ul>
|
||
<br>
|
||
|
||
<b>Hinweis:</b> Sind die Attribute latitude und longitude im global Device gesetzt, ergibt sich der
|
||
Sonnenauf- und Sonnenuntergang aus diesen Angaben.
|
||
</li>
|
||
<br>
|
||
|
||
</ul>
|
||
</ul>
|
||
|
||
</ul>
|
||
|
||
=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 <heiko.maaz@t-online.de>"
|
||
],
|
||
"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,
|
||
"FHEM::SynoModules::ErrCodes": 1.003009,
|
||
"Time::HiRes": 0,
|
||
"MIME::Base64": 0,
|
||
"Math::Trig": 0,
|
||
"List::Util": 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
|