diff --git a/fhem/contrib/DS_Starter/76_SolarForecast.pm b/fhem/contrib/DS_Starter/76_SolarForecast.pm new file mode 100644 index 000000000..f75b1b8ca --- /dev/null +++ b/fhem/contrib/DS_Starter/76_SolarForecast.pm @@ -0,0 +1,2237 @@ +######################################################################################################################## +# $Id: 76_SolarForecast.pm 21735 2020-04-20 20:53:24Z DS_Starter $ +######################################################################################################################### +# 76_SolarForecast.pm +# +# (c) 2020 by Heiko Maaz e-mail: Heiko dot Maaz at t-online dot de +# +# This Module is used by module 76_SMAPortal to create graphic devices. +# It can't be used standalone without any SMAPortal-Device. +# +# 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 . +# +######################################################################################################################### +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); +eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' +use Encode; +use utf8; + +# Run before module compilation +BEGIN { + # Import from main:: + GP_Import( + qw( + AnalyzePerlCommand + AttrVal + AttrNum + defs + delFromDevAttrList + delFromAttrList + devspec2array + deviceEvents + Debug + FmtDateTime + FmtTime + FW_makeImage + getKeyValue + init_done + InternalTimer + IsDisabled + Log + Log3 + modules + readingsSingleUpdate + readingsBulkUpdate + readingsBulkUpdateIfChanged + readingsBeginUpdate + readingsDelete + readingsEndUpdate + ReadingsNum + ReadingsTimestamp + ReadingsVal + RemoveInternalTimer + readingFnAttributes + setKeyValue + sortTopicNum + FW_cmd + FW_directNotify + FW_ME + FW_subdir + FW_room + FW_detail + FW_wname + ) + ); + + # 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 + ) + ); + +} + +# Versions History intern +my %vNotesIntern = ( + "0.1.0" => "09.12.2020 initial Version " +); + +# Voreinstellungen + +my %hset = ( # Hash der Set-Funktion + forecastDevice => { fn => \&_setforecastDevice }, + moduleArea => { fn => \&_setmoduleArea }, + moduleEfficiency => { fn => \&_setmoduleEfficiency }, + inverterEfficiency => { fn => \&_setinverterEfficiency }, + inverterDevice => { fn => \&_setinverterDevice }, + meterDevice => { fn => \&_setmeterDevice }, + 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 }, +); + +my @chours = (5..21); # Stunden des Tages mit möglichen Korrekturwerten +my $defpvme = 16.52; # default Wirkungsgrad Solarmodule +my $definve = 98.3; # default Wirkungsgrad Wechselrichter +my $kJtokWh = 0.00027778; # Umrechnungsfaktor kJ in kWh + +################################################################ +# Init Fn +################################################################ +sub Initialize { + my ($hash) = @_; + + my $fwd = join(",",devspec2array("TYPE=FHEMWEB:FILTER=STATE=Initialized")); + + $hash->{DefFn} = \&Define; + $hash->{GetFn} = \&Get; + $hash->{SetFn} = \&Set; + $hash->{FW_summaryFn} = \&FwFn; + $hash->{FW_detailFn} = \&FwFn; + $hash->{AttrFn} = \&Attr; + $hash->{NotifyFn} = \&Notify; + $hash->{AttrList} = "autoRefresh:selectnumbers,120,0.2,1800,0,log10 ". + "autoRefreshFW:$fwd ". + "beamColor:colorpicker,RGB ". + "beamColor2:colorpicker,RGB ". + "beamHeight ". + "beamWidth ". + # "consumerList ". + # "consumerLegend:none,icon_top,icon_bottom,text_top,text_bottom ". + # "consumerAdviceIcon ". + "disable:1,0 ". + "forcePageRefresh:1,0 ". + "headerAlignment:center,left,right ". + "headerDetail:all,co,pv,pvco,statusLink ". + "hourCount:slider,4,1,24 ". + "hourStyle ". + "maxPV ". + "htmlStart ". + "htmlEnd ". + "showDiff:no,top,bottom ". + "showHeader:1,0 ". + "showLink:1,0 ". + "showNight:1,0 ". + "showWeather:1,0 ". + "spaceSize ". + "layoutType:pv,co,pvco,diff ". + "Wh/kWh:Wh,kWh ". + "weatherColor:colorpicker,RGB ". + $readingFnAttributes; + + $hash->{FW_hideDisplayName} = 1; # Forum 88667 + + # $hash->{FW_addDetailToSummary} = 1; + # $hash->{FW_atPageEnd} = 1; # wenn 1 -> kein Longpoll ohne informid in HTML-Tag + + eval { FHEM::Meta::InitMod( __FILE__, $hash ) }; # für Meta.pm (https://forum.fhem.de/index.php/topic,97589.0.html) + +return; +} + +############################################################### +# SolarForecast Define +############################################################### +sub Define { + my ($hash, $def) = @_; + + my @a = split(/\s+/x, $def); + + $hash->{HELPER}{MODMETAABSENT} = 1 if($modMetaAbsent); # Modul Meta.pm nicht vorhanden + + setVersionInfo ($hash); # Versionsinformationen setzen + createNotifyDev ($hash); + + readingsSingleUpdate($hash, "state", "initialized", 1); + +return; +} + +############################################################### +# SolarForecast Set +############################################################### +sub Set { + my ($hash, @a) = @_; + return "\"set 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 $prop = shift @a; + my $prop1 = shift @a; + + my ($setlist,@fcdevs,@indevs,@medevs,@cfs); + my ($fcd,$ind,$med,$cf) = ("","","",""); + + return if(IsDisabled($name)); + + @fcdevs = devspec2array("TYPE=DWD_OpenData"); + $fcd = join ",", @fcdevs if(@fcdevs); + + @indevs = devspec2array("TYPE=SMAInverter"); + $ind = join ",", @indevs if(@indevs); + + @medevs = devspec2array("TYPE=SMAEM"); + $med = join ",", @medevs if(@medevs); + + for my $h (@chours) { + push @cfs, "pvCorrectionFactor_".sprintf("%02d",$h); + } + $cf = join " ", @cfs if(@cfs); + + $setlist = "Unknown argument $opt, choose one of ". + "forecastDevice:$fcd ". + "moduleArea ". + "moduleEfficiency ". + "inverterDevice:$ind ". + "inverterEfficiency ". + "meterDevice:$med ". + $cf + ; + + my $params = { + hash => $hash, + name => $name, + opt => $opt, + prop => $prop, + prop1 => $prop1 + }; + + if($hset{$opt} && defined &{$hset{$opt}{fn}}) { + my $ret = q{}; + $ret = &{$hset{$opt}{fn}} ($params); + return $ret; + } + +return "$setlist"; +} + +################################################################ +# Setter forecastDevice +################################################################ +sub _setforecastDevice { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $prop = $paref->{prop} // return qq{no PV forecast device specified}; + + if(!$defs{$prop} || $defs{$prop}{TYPE} ne "DWD_OpenData") { + return qq{Forecast device "$prop" doesn't exist or has no TYPE "DWD_OpenData"}; + } + + readingsSingleUpdate($hash, "currentForecastDev", $prop, 1); + createNotifyDev ($hash); + +return; +} + +################################################################ +# Setter inverterDevice +################################################################ +sub _setinverterDevice { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $prop = $paref->{prop} // return qq{no inverter device specified}; + + if(!$defs{$prop} || $defs{$prop}{TYPE} ne "SMAInverter") { + return qq{Inverter device "$prop" doesn't exist or has no TYPE "SMAInverter"}; + } + + readingsSingleUpdate($hash, "currentInverterDev", $prop, 1); + createNotifyDev ($hash); + +return; +} + +################################################################ +# Setter meterDevice +################################################################ +sub _setmeterDevice { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $prop = $paref->{prop} // return qq{no meter device specified}; + + if(!$defs{$prop} || $defs{$prop}{TYPE} ne "SMAEM") { + return qq{Meter device "$prop" doesn't exist or has no TYPE "SMAEM"}; + } + + readingsSingleUpdate($hash, "currentMeterDev", $prop, 1); + createNotifyDev ($hash); + +return; +} + +################################################################ +# Setter moduleArea +################################################################ +sub _setmoduleArea { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $prop = $paref->{prop} // return qq{no PV module area specified}; + + if($prop !~ /[0-9,.]/x) { + return qq{The module area must be specified by numbers and optionally with decimal places}; + } + + $prop =~ s/,/./x; + + readingsSingleUpdate($hash, "moduleArea", $prop, 1); + +return; +} + +################################################################ +# Setter moduleEfficiency +################################################################ +sub _setmoduleEfficiency { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $prop = $paref->{prop} // return qq{no PV module efficiency specified}; + + if($prop !~ /[0-9,.]/x) { + return qq{The module efficiency must be specified by numbers and optionally with decimal places}; + } + + $prop =~ s/,/./x; + + readingsSingleUpdate($hash, "moduleEfficiency", $prop, 1); + +return; +} + +################################################################ +# Setter inverterEfficiency +################################################################ +sub _setinverterEfficiency { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $prop = $paref->{prop} // return qq{no inverter efficiency specified}; + + if($prop !~ /[0-9,.]/x) { + return qq{The inverter efficiency must be specified by numbers and optionally with decimal places}; + } + + $prop =~ s/,/./x; + + readingsSingleUpdate($hash, "inverterEfficiency", $prop, 1); + +return; +} + +################################################################ +# Setter pvCorrectionFactor +################################################################ +sub _setpvCorrectionFactor { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $opt = $paref->{opt}; + my $prop = $paref->{prop} // return qq{no correction value specified}; + + if($prop !~ /[0-9,.]/x) { + return qq{The correction value must be specified by numbers and optionally with decimal places}; + } + + $prop =~ s/,/./x; + + readingsSingleUpdate($hash, $opt, $prop, 1); + + my @da; + my $t = time; # aktuelle Unix-Zeit + my $chour = strftime "%H", localtime($t); # aktuelle Stunde + my $fcdev = ReadingsVal($name, "currentForecastDev", ""); # aktuelles Forecast Device + + my $params = { + myHash => $hash, + myName => $name, + devName => $fcdev, + t => $t, + chour => $chour, + daref => \@da + }; + + _transferForecastValues ($params); + + if(@da) { + push @da, "state:updated"; # Abschluß state + createReadings ($hash, \@da); + } + +return; +} + +############################################################### +# SolarForecast Get +############################################################### +sub Get { + my ($hash, @a) = @_; + return "\"get X\" needs at least an argument" if ( @a < 2 ); + my $name = shift @a; + my $cmd = shift @a; + + my $getlist = "Unknown argument $cmd, choose one of ". + "html:noArg ". + "ftui:noArg "; + + if ($cmd eq "html") { + return pageAsHtml($hash); + } + + if ($cmd eq "ftui") { + return pageAsHtml($hash,"ftui"); + } + +return; +} + +################################################################ +sub Attr { + my ($cmd,$name,$aName,$aVal) = @_; + my $hash = $defs{$name}; + my ($do,$val); + + # $cmd can be "del" or "set" + # $name is device name + # aName and aVal are Attribute name and value + + if($aName eq "disable") { + if($cmd eq "set") { + $do = ($aVal) ? 1 : 0; + } + $do = 0 if($cmd eq "del"); + $val = ($do == 1 ? "disabled" : "initialized"); + readingsSingleUpdate($hash, "state", $val, 1); + + if($do == 1) { + my @allrds = keys%{$defs{$name}{READINGS}}; + foreach my $key(@allrds) { + delete($defs{$name}{READINGS}{$key}) if($key ne "state"); + } + } + } + + if($aName eq "icon") { + $_[2] = "consumerAdviceIcon"; + } + +return; +} + +################################################################################### +# Eventverarbeitung +################################################################################### +sub Notify { + # Es werden nur die Events von Geräten verarbeitet die im Hash $hash->{NOTIFYDEV} gelistet sind (wenn definiert). + # Dadurch kann die Menge der Events verringert werden. In sub DbRep_Define angeben. + my $myHash = shift; + my $dev_hash = shift; + my $myName = $myHash->{NAME}; # Name des eigenen Devices + my $devName = $dev_hash->{NAME}; # Device welches Events erzeugt hat + + return if(IsDisabled($myName) || !$myHash->{NOTIFYDEV}); + + my $events = deviceEvents($dev_hash, 1); + return if(!$events); + + my @da; + my $t = time; # aktuelle Unix-Zeit + my $chour = strftime "%H", localtime($t); # aktuelle Stunde + + my $params = { + myHash => $myHash, + myName => $myName, + devName => $devName, + t => $t, + chour => $chour, + daref => \@da + }; + + my $fcdev = ReadingsVal($myName, "currentForecastDev", ""); # aktuelles Forecast Device + my $indev = ReadingsVal($myName, "currentInverterDev", ""); # aktuelles Inverter Device + my $medev = ReadingsVal($myName, "currentMeterDev", ""); # aktuelles Meter Device + + return if($devName eq $medev); # Energymeter erst auslesen wenn Inverter Device Event ausgelöst hat ! (innerhalb _transferInverterValues) + + for my $event (@{$events}) { + $event = "" if(!defined($event)); + my ($r, $v) = split(/\s+/x, $event); + + if($devName eq $fcdev && $r =~ /^state/x && $v =~ /^forecast\supdated/x) { # Forecast Werte übertragen wenn fertig + _transferForecastValues ($params); + last; + } + + if($devName eq $indev && $r =~ /^state/x) { # WR Werte übertragen + _transferInverterValues ($params); + last; + } + } + + if(@da) { + createReadings ($myHash, \@da); + } + + sumNextHours ($myHash, $chour, \@da); # Zusammenfassung nächste 4 Stunden erstellen + + readingsSingleUpdate($myHash, "state", "updated", 1); # Abschluß state + +return; +} + +################################################################ +# Werte Forecast Device ermitteln und übertragen +################################################################ +sub _transferForecastValues { + my $paref = shift; + my $myHash = $paref->{myHash}; + my $myName = $paref->{myName}; + my $devName = $paref->{devName}; + my $t = $paref->{t}; + my $chour = $paref->{chour}; + my $daref = $paref->{daref}; + + my $fcdev = ReadingsVal($myName, "currentForecastDev", ""); # aktuelles Forecast Device + return if(!$fcdev || !$defs{$fcdev} || $fcdev ne $devName); + + my ($time_str,$epoche,$fd,$v); + + my $fc0_SunRise = (split ":", ReadingsVal($devName, "fc0_SunRise", "00:00"))[0]; # Sonnenaufgang heute (hh) + my $fc0_SunSet = (split ":", ReadingsVal($devName, "fc0_SunSet", "00:00"))[0]; # Sonnenuntergang heute (hh) + my $fc1_SunRise = (split ":", ReadingsVal($devName, "fc1_SunRise", "00:00"))[0]; # Sonnenaufgang morgen (hh) + my $fc1_SunSet = (split ":", ReadingsVal($devName, "fc1_SunSet", "00:00"))[0]; # Sonnenuntergang morgen (hh) + + push @$daref, "Today_HourSunRise:". $fc0_SunRise; + push @$daref, "Today_HourSunSet:". $fc0_SunSet; + push @$daref, "Tomorrow_HourSunRise:".$fc1_SunRise; + push @$daref, "Tomorrow_HourSunSet:". $fc1_SunSet; + + for my $num (0..23) { + my $fh = $chour + $num; + + if($fh < 24) { + $fd = 0; + } + else { + $fd = 1; + $fh = $fh-24; + } + + $v = ReadingsVal($devName, "fc${fd}_${fh}_Rad1h", 0); + + ## PV Forecast + ############### + if($num == 0) { + $time_str = "ThisHour"; + $epoche = $t; # Epoche Zeit + } + else { + $time_str = "NextHour".sprintf "%02d", $num; + $epoche = $t + (3600*$num); + } + + my $calcpv = calcPVforecast ($myName, $v, $fh); # Vorhersage gewichtet kalkulieren + + push @$daref, "${time_str}_PVforecast:".$calcpv; + push @$daref, "${time_str}_Time:" .TimeAdjust ($epoche); # Zeit fortschreiben + + $myHash->{HELPER}{"fc${fd}_".sprintf("%02d",$fh)."_PVforecast"} = $calcpv; # Vorhersagedaten zur Berechnung Korrekturfaktor in Helper speichern + + ## Wetter Forecast + ################### + + my $wid = ReadingsVal($devName, "fc${fd}_${fh}_ww", 0); + $wid = sprintf "%02d", $wid; # führende 0 einfügen wenn nötig + my $fhstr = sprintf "%02d", $fh; + + if($fd == 0 && ($fhstr lt $fc0_SunRise || $fhstr gt $fc0_SunSet)) { # Zeit vor Sonnenaufgang oder nach Sonnenuntergang heute + $wid = "1".$wid; # "1" der WeatherID voranstellen wenn Nacht + } + elsif ($fd == 1 && ($fhstr lt $fc1_SunRise || $fhstr gt $fc1_SunSet)) { # Zeit vor Sonnenaufgang oder nach Sonnenuntergang morgen + $wid = "1".$wid; # "1" der WeatherID voranstellen wenn Nacht + } + + $myHash->{HELPER}{"${time_str}_WeatherId"} = $wid; + } + +return; +} + +################################################################ +# Werte Inverter Device ermitteln und übertragen +################################################################ +sub _transferInverterValues { + my $paref = shift; + my $myHash = $paref->{myHash}; + my $myName = $paref->{myName}; + my $devName = $paref->{devName}; + my $t = $paref->{t}; + my $chour = $paref->{chour}; + my $daref = $paref->{daref}; + + my $indev = ReadingsVal($myName, "currentInverterDev", ""); + return if(!$indev || !$defs{$indev} || $indev ne $devName); + + my $tlim = 23; # Stunde 23 -> bestimmte Aktionen + + if($chour == $tlim) { + my @allrds = keys %{$myHash->{READINGS}}; + for my $key(@allrds) { + readingsDelete($myHash, $key) if($key =~ m/^Hour_\d{2}_PVreal$/x); + } + } + + ## aktuelle PV-Erzeugung + ######################### + my $pvspot = ReadingsNum ($indev, "SPOT_PACTOT", 0) / 1000; # aktuelle Erzeugung (kW) wenn attr SBFSpotComp = 0 + my $pv = ReadingsNum ($indev, "total_pac", $pvspot) * 1000; # aktuelle Erzeugung (W) wenn attr SBFSpotComp = 1 + + push @$daref, "Current_PV:". $pv." W"; + + my $etodayspot = ReadingsNum ($indev, "SPOT_ETODAY", 0) / 1000; # aktuelle Erzeugung (kWh) wenn attr SBFSpotComp = 0 + my $etoday = ReadingsNum ($indev, "etoday", $etodayspot) * 1000; # aktuelle Erzeugung (Wh) wenn attr SBFSpotComp = 1 + + my $edaypast = 0; + for my $h (0..int($chour)-1) { # alle bisherigen Erzeugungen des Tages summieren + $edaypast += ReadingsNum ($myName, "Hour_".sprintf("%02d",$h)."_PVreal", 0); + } + + my $ethishour = $etoday - $edaypast; + + push @$daref, "Hour_${chour}_PVreal:". $ethishour." Wh" if($chour != $tlim); # nicht setzen wenn Stunde 23 des Tages + + ## Meter extrahieren + ######################### + my $medev = ReadingsVal($myName, "currentMeterDev", ""); # aktuelles Meter Device + $paref->{devName} = $medev; + _transferMeterValues ($paref); + + ## Forecast extrahieren + ######################### + my $fcdev = ReadingsVal($myName, "currentForecastDev", ""); # aktuelles Forecast Device + $paref->{devName} = $fcdev; + _transferForecastValues ($paref); + +return; +} + +################################################################ +# Werte Meter Device ermitteln und übertragen +################################################################ +sub _transferMeterValues { + my $paref = shift; + my $myHash = $paref->{myHash}; + my $myName = $paref->{myName}; + my $devName = $paref->{devName}; + my $t = $paref->{t}; + my $chour = $paref->{chour}; + my $daref = $paref->{daref}; + + my $medev = ReadingsVal($myName, "currentMeterDev", ""); # aktuelles Meter device + return if(!$medev || !$defs{$medev} || $medev ne $devName); + + ## aktuelle Consumption + ######################### + my $co = ReadingsNum ($medev, "state", 0); # aktueller Bezug (-) oder Einspeisung + $co = 0 if($co > 0); + + push @$daref, "Current_GridConsumption:".(abs $co)." W"; + +return; +} + +################################################################ +# FHEMWEB Fn +################################################################ +sub FwFn { + my ($FW_wname, $d, $room, $pageHash) = @_; # pageHash is set for summaryFn. + my $hash = $defs{$d}; + my $height; + + RemoveInternalTimer($hash); + $hash->{HELPER}{FW} = $FW_wname; + + my $link = forecastGraphic ($d); + + my $alias = AttrVal($d, "alias", $d); # Linktext als Aliasname oder Devicename setzen + my $dlink = "$alias"; + + my $ret = ""; + if(IsDisabled($d)) { + $height = AttrNum($d, 'beamHeight', 200); + $ret .= ""; + $ret .= ""; + $ret .= ""; + $ret .= ""; + $ret .= "
"; + $ret .= "Solar forecast graphic device $d is disabled"; + $ret .= "
"; + } + else { + $ret .= "$dlink
" if(AttrVal($d,"showLink",0)); + $ret .= $link; + } + + # Autorefresh nur des aufrufenden FHEMWEB-Devices + my $al = AttrVal($d, "autoRefresh", 0); + if($al) { + InternalTimer(gettimeofday()+$al, \&pageRefresh, $hash, 0); + Log3($d, 5, "$d - next start of autoRefresh: ".FmtDateTime(gettimeofday()+$al)); + } + +return $ret; +} + +################################################################ +sub pageRefresh { + my ($hash) = @_; + my $d = $hash->{NAME}; + + # Seitenrefresh festgelegt durch SolarForecast-Attribut "autoRefresh" und "autoRefreshFW" + my $rd = AttrVal($d, "autoRefreshFW", $hash->{HELPER}{FW}); + { map { FW_directNotify("#FHEMWEB:$_", "location.reload('true')", "") } $rd } + + my $al = AttrVal($d, "autoRefresh", 0); + if($al) { + InternalTimer(gettimeofday()+$al, \&pageRefresh, $hash, 0); + Log3($d, 5, "$d - next start of autoRefresh: ".FmtDateTime(gettimeofday()+$al)); + } + else { + RemoveInternalTimer($hash); + } + +return; +} + +############################################################################################# +# Versionierungen des Moduls setzen +# Die Verwendung von Meta.pm und Packages wird berücksichtigt +############################################################################################# +sub setVersionInfo { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $v = (sortTopicNum("desc",keys %vNotesIntern))[0]; + my $type = $hash->{TYPE}; + $hash->{HELPER}{PACKAGE} = __PACKAGE__; + $hash->{HELPER}{VERSION} = $v; + + if($modules{$type}{META}{x_prereqs_src} && !$hash->{HELPER}{MODMETAABSENT}) { + # META-Daten sind vorhanden + $modules{$type}{META}{version} = "v".$v; # Version aus META.json überschreiben, Anzeige mit {Dumper $modules{SMAPortal}{META}} + if($modules{$type}{META}{x_version}) { # {x_version} ( nur gesetzt wenn $Id: 76_SolarForecast.pm 21735 2020-04-20 20:53:24Z DS_Starter $ im Kopf komplett! vorhanden ) + $modules{$type}{META}{x_version} =~ s/1\.1\.1/$v/g; + } + else { + $modules{$type}{META}{x_version} = $v; + } + return $@ unless (FHEM::Meta::SetInternals($hash)); # FVERSION wird gesetzt ( nur gesetzt wenn $Id: 76_SolarForecast.pm 21735 2020-04-20 20:53:24Z DS_Starter $ im Kopf komplett! vorhanden ) + + if(__PACKAGE__ eq "FHEM::$type" || __PACKAGE__ eq $type) { + # es wird mit Packages gearbeitet -> Perl übliche Modulversion setzen + # mit {->VERSION()} im FHEMWEB kann Modulversion abgefragt werden + use version 0.77; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); + } + } + else { # herkömmliche Modulstruktur + $hash->{VERSION} = $v; + } + +return; +} + +################################################################ +# Grafik als HTML zurück liefern (z.B. für Widget) +################################################################ +sub pageAsHtml { + my ($hash,$ftui) = @_; + my $name = $hash->{NAME}; + my $height; + + my $link = forecastGraphic ($name, $ftui); + + my $alias = AttrVal($name, "alias", $name); # Linktext als Aliasname oder Devicename setzen + my $dlink = "$alias"; + + my $ret = ""; + if(IsDisabled($name)) { + $height = AttrNum($name, 'beamHeight', 200); + $ret .= ""; + $ret .= ""; + $ret .= ""; + $ret .= ""; + $ret .= "
"; + $ret .= "SMA Portal graphic device $name is disabled"; + $ret .= "
"; + } + else { + $ret .= "$dlink
" if(AttrVal($name,"showLink",0)); + $ret .= $link; + } + $ret .= ""; + +return $ret; +} + +############################################################################### +# Subroutine für Vorhersagegrafik +############################################################################### +sub forecastGraphic { ## no critic 'complexity' + my $name = shift; + my $ftui = shift // ""; + + my $hash = $defs{$name}; + my $ret = ""; + + my ($val,$height); + my ($z2,$z3,$z4); + my $he; # Balkenhöhe + my (%pv,%is,%t,%we,%di,%co); + my @pgCDev; + + ########################################################## + # Kontext des SolarForecast-Devices speichern für Refresh + $hash->{HELPER}{SPGDEV} = $name; # Name des aufrufenden SMAPortalSPG-Devices + $hash->{HELPER}{SPGROOM} = $FW_room ? $FW_room : ""; # Raum aus dem das SMAPortalSPG-Device die Funktion aufrief + $hash->{HELPER}{SPGDETAIL} = $FW_detail ? $FW_detail : ""; # Name des SMAPortalSPG-Devices (wenn Detailansicht) + + my $fcdev = ReadingsVal($name, "currentForecastDev", ""); # aktuelles Forecast Device + my $indev = ReadingsVal($name, "currentInverterDev", ""); # aktuelles Inverter Device + my $cclv = "L05"; + + my $pv0 = ReadingsNum ($name, "ThisHour_PVforecast", undef); + my $ma = ReadingsNum ($name, "moduleArea", 0); # Solar Modulfläche (qm) + + if(!$fcdev || !$ma || !defined $pv0) { + $height = AttrNum($name, 'beamHeight', 200); + $ret .= ""; + $ret .= ""; + $ret .= ""; + $ret .= ""; + $ret .= "
"; + + if(!$fcdev) { + $ret .= qq{Please select a Solar Forecast device of Type "DWD_OpenData" with "set $name forecastDevice"}; + } + elsif(!$indev) { + $ret .= qq{Please select an Inverter device with "set $name inverterDevice"}; + } + elsif(!$ma) { + $ret .= qq{Please specify the total module area with "set $name moduleArea"}; + } + elsif(!defined $pv0) { + $ret .= qq{Awaiting data from selected Solar Forecast device ...}; + } + + $ret .= "
"; + return $ret; + } + + @pgCDev = split(',',AttrVal($name,"consumerList","")); # definierte Verbraucher ermitteln + my ($legend_style, $legend) = split('_',AttrVal($name,'consumerLegend','icon_top')); + + $legend = '' if(($legend_style eq 'none') || (!int(@pgCDev))); + + ################################### + # Verbraucherlegende und Steuerung + ################################### + my $legend_txt; + if ($legend) { + for (@pgCDev) { + my($txt,$im) = split(':',$_); # $txt ist der Verbrauchername + my $cmdon = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name $txt on')\""; + my $cmdoff = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name $txt off')\""; + my $cmdauto = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name $txt auto')\""; + + if ($ftui eq "ftui") { + $cmdon = "\"ftui.setFhemStatus('set $name $txt on')\""; + $cmdoff = "\"ftui.setFhemStatus('set $name $txt off')\""; + $cmdauto = "\"ftui.setFhemStatus('set $name $txt auto')\""; + } + + my $swstate = ReadingsVal($name,"${cclv}_".$txt."_Switch", "undef"); + my $swicon = ""; + + if($swstate eq "off") { + $swicon = ""; + } + elsif ($swstate eq "on") { + $swicon = ""; + } + elsif ($swstate =~ /off.*automatic.*/ix) { + $swicon = ""; + } + + if ($legend_style eq 'icon') { # mögliche Umbruchstellen mit normalen Blanks vorsehen ! + $legend_txt .= $txt.' '.FW_makeImage($im).' '.$swicon.'  '; + } + else { + my (undef,$co) = split('\@',$im); + $co = '#cccccc' if (!$co); # Farbe per default + $legend_txt .= ''.$txt.' '.$swicon.'  '; # hier auch Umbruch erlauben + } + } + } + + ################################### + # Parameter f. Anzeige extrahieren + ################################### + my $maxhours = AttrNum ($name, 'hourCount', 24 ); + my $hourstyle = AttrVal ($name, 'hourStyle', undef ); + my $colorfc = AttrVal ($name, 'beamColor', undef ); + my $colorc = AttrVal ($name, 'beamColor2', 'C4C4A7'); + my $icon = AttrVal ($name, 'consumerAdviceIcon', undef ); + my $html_start = AttrVal ($name, 'htmlStart', undef ); # beliebige HTML Strings die vor der Grafik ausgegeben werden + my $html_end = AttrVal ($name, 'htmlEnd', undef ); # beliebige HTML Strings die nach der Grafik ausgegeben werden + + my $type = AttrVal ($name, 'layoutType', 'pv' ); + my $kw = AttrVal ($name, 'Wh/kWh', 'Wh' ); + + $height = AttrNum ($name, 'beamHeight', 200 ); + my $width = AttrNum ($name, 'beamWidth', 6 ); # zu klein ist nicht problematisch + my $w = $width*$maxhours; # gesammte Breite der Ausgabe , WetterIcon braucht ca. 34px + my $fsize = AttrNum ($name, 'spaceSize', 24 ); + my $maxVal = AttrNum ($name, 'maxPV', 0 ); # dyn. Anpassung der Balkenhöhe oder statisch ? + + my $show_night = AttrNum ($name, 'showNight', 0 ); # alle Balken (Spalten) anzeigen ? + my $show_diff = AttrVal ($name, 'showDiff', 'no' ); # zusätzliche Anzeige $di{} in allen Typen + my $weather = AttrNum ($name, 'showWeather', 1 ); + my $colorw = AttrVal ($name, 'weatherColor', undef ); + + my $wlalias = AttrVal ($name, 'alias', $name ); + my $header = AttrNum ($name, 'showHeader', 1 ); + my $hdrAlign = AttrVal ($name, 'headerAlignment', 'center' ); # ermöglicht per attr die Ausrichtung der Tabelle zu setzen + my $hdrDetail = AttrVal ($name, 'headerDetail', 'all' ); # ermöglicht den Inhalt zu begrenzen, um bspw. passgenau in ftui einzubetten + + # Icon Erstellung, mit @ ergänzen falls einfärben + # Beispiel mit Farbe: $icon = FW_makeImage('light_light_dim_100.svg@green'); + + $icon = FW_makeImage($icon) if (defined($icon)); + my $co4h = ReadingsNum ($name,"Next04Hours_Consumption", 0); + my $coRe = ReadingsNum ($name,"RestOfDay_Consumption", 0); + my $coTo = ReadingsNum ($name,"Tomorrow_Consumption", 0); + my $coCu = ReadingsNum ($name,"Current_GridConsumption", 0); + + my $pv4h = ReadingsNum ($name,"Next04Hours_PV", 0); + my $pvRe = ReadingsNum ($name,"RestOfDay_PV", 0); + my $pvTo = ReadingsNum ($name,"Tomorrow_PV", 0); + my $pvCu = ReadingsNum ($name,"Current_PV", 0); + + if ($kw eq 'kWh') { + $co4h = sprintf("%.1f" , $co4h/1000)." kWh"; + $coRe = sprintf("%.1f" , $coRe/1000)." kWh"; + $coTo = sprintf("%.1f" , $coTo/1000)." kWh"; + $coCu = sprintf("%.1f" , $coCu/1000)." kW"; + $pv4h = sprintf("%.1f" , $pv4h/1000)." kWh"; + $pvRe = sprintf("%.1f" , $pvRe/1000)." kWh"; + $pvTo = sprintf("%.1f" , $pvTo/1000)." kWh"; + $pvCu = sprintf("%.1f" , $pvCu/1000)." kW"; + } + else { + $co4h .= " Wh"; + $coRe .= " Wh"; + $coTo .= " Wh"; + $coCu .= " W"; + $pv4h .= " Wh"; + $pvRe .= " Wh"; + $pvTo .= " Wh"; + $pvCu .= " W"; + } + + ########################## + # Headerzeile generieren + ########################## + if ($header) { + my $lang = AttrVal ("global", "language", "EN" ); + my $alias = AttrVal ($name, "alias", $name ); # Linktext als Aliasname + + my $dlink = "$alias"; + my $lup = ReadingsTimestamp($name, "ThisHour_PVforecast", "0000-00-00 00:00:00"); # letzter Forecast Update + + my $lupt = "last update:"; + my $lblPv4h = "next 4h:"; + my $lblPvRe = "remain today:"; + my $lblPvTo = "tomorrow:"; + my $lblPvCu = "actual"; + + if(AttrVal("global", "language", "EN") eq "DE") { # Header globales Sprachschema Deutsch + $lupt = "Stand:"; + $lblPv4h = encode("utf8", "nächste 4h:"); + $lblPvRe = "Rest heute:"; + $lblPvTo = "morgen:"; + $lblPvCu = "aktuell"; + } + + $header = ""; + + ######################################### + # Header Link + Status + Update Button + if($hdrDetail eq "all" || $hdrDetail eq "statusLink") { + my ($year, $month, $day, $time) = $lup =~ /(\d{4})-(\d{2})-(\d{2})\s+(.*)/x; + + if(AttrVal("global","language","EN") eq "DE") { + $lup = "$day.$month.$year $time"; + } + else { + $lup = "$year-$month-$day $time"; + } + + my $cmdupdate = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=get $fcdev forecast')\""; # Update Button generieren + + if ($ftui eq "ftui") { + $cmdupdate = "\"ftui.setFhemStatus('get $fcdev forecast')\""; + } + + my $upstate = ReadingsVal($name, "state", ""); + my $upicon; + + if ($upstate =~ /updated/ix) { + $upicon = ""; + } + elsif ($upstate =~ /updating/ix) { + $upicon = ""; + } + elsif ($upstate =~ /initialized/ix) { + $upicon = ""; + } + else { + $upicon = ""; + } + + $header .= ""; + } + + ######################## + # Header Information pv + if($hdrDetail eq "all" || $hdrDetail eq "pv" || $hdrDetail eq "pvco") { + $header .= ""; + $header .= ""; + $header .= ""; + $header .= ""; + $header .= ""; + $header .= ""; + $header .= ""; + } + + ######################## + # Header Information co + if($hdrDetail eq "all" || $hdrDetail eq "co" || $hdrDetail eq "pvco") { + $header .= ""; + $header .= ""; + $header .= ""; + $header .= ""; + $header .= ""; + $header .= ""; + $header .= ""; + } + + $header .= "
".$dlink."(".$lupt." ".$lup.") ".$upicon."
PV =>$lblPvCu $pvCu$lblPv4h $pv4h$lblPvRe $pvRe$lblPvTo $pvTo
CO =>$lblPvCu $coCu$lblPv4h $co4h$lblPvRe $coRe$lblPvTo $coTo
"; + } + + ########################## + # Werte aktuelle Stunde + ########################## + $pv{0} = ReadingsNum($name, "ThisHour_PVforecast", 0); + $co{0} = ReadingsNum($name, "ThisHour_Consumption", 0); + $di{0} = $pv{0} - $co{0}; + $is{0} = (ReadingsVal($name,"ThisHour_IsConsumptionRecommended",'no') eq 'yes' ) ? $icon : undef; + $we{0} = $hash->{HELPER}{"ThisHour_WeatherId"} if($weather); # für Wettericons + $we{0} = $we{0} // 999; + + if(AttrVal("global","language","EN") eq "DE") { + (undef,undef,undef,$t{0}) = ReadingsVal($name, "ThisHour_Time", '00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; + } + else { + (undef,undef,undef,$t{0}) = ReadingsVal($name, "ThisHour_Time", '0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; + } + + $t{0} = int($t{0}); # zum Rechnen Integer ohne führende Null + + ########################################################### + # get consumer list and display it in portalGraphics + ########################################################### + for (@pgCDev) { + my ($itemName, undef) = split(':',$_); + $itemName =~ s/^\s+|\s+$//gx; #trim it, if blanks were used + $_ =~ s/^\s+|\s+$//gx; #trim it, if blanks were used + + ################################## + #check if listed device is planned + if (ReadingsVal($name, $itemName."_Planned", "no") eq "yes") { + #get start and end hour + my ($start, $end); # werden auf Balken Pos 0 - 23 umgerechnet, nicht auf Stunde !!, Pos = 24 -> ungültige Pos = keine Anzeige + + if(AttrVal("global","language","EN") eq "DE") { + (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 < $t{0}) { # consumption seems to be tomorrow + $start = 24-$t{0}+$start; + $flag = 1; + } + else { + $start -= $t{0}; + } + + if ($flag) { # consumption seems to be tomorrow + $end = 24-$t{0}+$end; + } + else { + $end -= $t{0}; + } + + $_ .= ":".$start.":".$end; + } + else { + $_ .= ":24:24"; + } + Log3($name, 4, "$name - Consumer planned data: $_"); + } + + $maxVal = !$maxVal ? $pv{0} : $maxVal; # Startwert wenn kein Wert bereits via attr vorgegeben ist + my $maxCon = $co{0}; # für Typ co + my $maxDif = $di{0}; # für Typ diff + my $minDif = $di{0}; # für Typ diff + + for my $i (1..$maxhours-1) { + $pv{$i} = ReadingsNum($name, "NextHour".sprintf("%02d",$i)."_PVforecast", 0); # Erzeugung + $co{$i} = ReadingsNum($name, "NextHour".sprintf("%02d",$i)."_Consumption", 0); # Verbrauch + $di{$i} = $pv{$i} - $co{$i}; + + $maxVal = $pv{$i} if ($pv{$i} > $maxVal); + $maxCon = $co{$i} if ($co{$i} > $maxCon); + $maxDif = $di{$i} if ($di{$i} > $maxDif); + $minDif = $di{$i} if ($di{$i} < $minDif); + + $is{$i} = (ReadingsVal($name,"NextHour".sprintf("%02d",$i)."_IsConsumptionRecommended",'no') eq 'yes') ? $icon : undef; + $we{$i} = $hash->{HELPER}{"NextHour". sprintf("%02d",$i)."_WeatherId"} if($weather); # für Wettericons + $we{$i} = $we{$i} // 999; + + if(AttrVal("global","language","EN") eq "DE") { + (undef,undef,undef,$t{$i}) = ReadingsVal($name,"NextHour".sprintf("%02d",$i)."_Time", '00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; + } + else { + (undef,undef,undef,$t{$i}) = ReadingsVal($name,"NextHour".sprintf("%02d",$i)."_Time", '0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; + } + + $t{$i} = int($t{$i}); # keine führende 0 + } + + ###################################### + # Tabellen Ausgabe erzeugen + ###################################### + + # 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 + + $ret = ""; + $ret .= $html_start if (defined($html_start)); + $ret .= ""; + $ret .= ""; + $ret .= "
"; + $ret .= "\n"; # das \n erleichtert das Lesen der debug Quelltextausgabe + + if ($header) { # Header ausgeben + $ret .= ""; + # mit einem extra ein wenig mehr Platz machen, ergibt i.d.R. weniger als ein Zeichen + $ret .= ""; + } + + if ($legend_txt && ($legend eq 'top')) { + $ret .= ""; + $ret .= ""; + } + + if ($weather) { + $ret .= ""; # freier Platz am Anfang + + for my $i (0..$maxhours-1) { # keine Anzeige bei Null Ertrag bzw. in der Nacht , Typ pcvo & diff haben aber immer Daten in der Nacht + if ($pv{$i} || $show_night || ($type eq 'pvco') || ($type eq 'diff')) { # FHEM Wetter Icons (weather_xxx) , Skalierung und Farbe durch FHEM Bordmittel + my $icon_name = weather_icon($we{$i}); # unknown -> FHEM Icon Fragezeichen im Kreis wird als Ersatz Icon ausgegeben + Log3($name, 3,"$name - unknown weather id: ".$we{$i}.", please inform the maintainer") if($icon_name eq 'unknown'); + + $icon_name .='@'.$colorw if (defined($colorw)); + $val = FW_makeImage($icon_name); + + $val ='???' if ($val eq $icon_name); # passendes Icon beim User nicht vorhanden ! ( attr web iconPath falsch/prüfen/update ? ) + $ret .= ""; + + } + else { # Kein Ertrag oder show_night = 0 + $ret .= ""; $we{$i} = undef; + } + # mit $we{$i} = undef kann man unten leicht feststellen ob für diese Spalte bereits ein Icon ausgegeben wurde oder nicht + } + + $ret .= ""; # freier Platz am Ende der Icon Zeile + } + + if($show_diff eq 'top') { # Zusätzliche Zeile Ertrag - Verbrauch + $ret .= ""; # freier Platz am Anfang + + for my $i (0..$maxhours-1) { + $val = formatVal6($di{$i},$kw,$we{$i}); + $val = ($di{$i} < 0) ? ''.$val.'' : '+'.$val; # negative Zahlen in Fettschrift + $ret .= ""; + } + $ret .= ""; # freier Platz am Ende + } + + $ret .= ""; # Neue Zeile mit freiem Platz am Anfang + + for my $i (0..$maxhours-1) { + # Achtung Falle, Division by Zero möglich, + # maxVal kann gerade bei kleineren maxhours Ausgaben in der Nacht leicht auf 0 fallen + $height = 200 if (!$height); # Fallback, sollte eigentlich nicht vorkommen, außer der User setzt es auf 0 + $maxVal = 1 if (!int $maxVal); + $maxCon = 1 if (!$maxCon); + + # Der zusätzliche Offset durch $fsize verhindert bei den meisten Skins + # dass die Grundlinie der Balken nach unten durchbrochen wird + if($type eq 'co') { + $he = int(($maxCon-$co{$i})/$maxCon*$height) + $fsize; # he - freier der Raum über den Balken. + $z3 = int($height + $fsize - $he); # Resthöhe + } + elsif($type eq 'pv') { + $he = int(($maxVal-$pv{$i}) / $maxVal*$height) + $fsize; + $z3 = int($height + $fsize - $he); + } + elsif($type eq 'pvco') { + # Berechnung der Zonen + # he - freier der Raum über den Balken. fsize wird nicht verwendet, da bei diesem Typ keine Zahlen über den Balken stehen + # z2 - der Ertrag ggf mit Icon + # z3 - der Verbrauch , bei zu kleinem Wert wird der Platz komplett Zone 2 zugeschlagen und nicht angezeigt + # z2 und z3 nach Bedarf tauschen, wenn der Verbrauch größer als der Ertrag ist + + $maxVal = $maxCon if ($maxCon > $maxVal); # wer hat den größten Wert ? + + if ($pv{$i} > $co{$i}) { # pv oben , co unten + $z2 = $pv{$i}; $z3 = $co{$i}; + } + else { # tauschen, Verbrauch ist größer als Ertrag + $z3 = $pv{$i}; $z2 = $co{$i}; + } + + $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; + } + } + else { # Typ diff + # Berechnung der Zonen + # he - freier der Raum über den Balken , Zahl positiver Wert + fsize + # z2 - positiver Balken inkl Icon + # z3 - negativer Balken + # z4 - Zahl negativer Wert + fsize + + my ($px_pos,$px_neg); + my $maxPV = 0; # ToDo: maxPV noch aus Attribut maxPV ableiten + + if ($maxPV) { # Feste Aufteilung +/- , jeder 50 % bei maxPV = 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 ($di{$i} >= 0) { # Zone 2 & 3 mit ihren direkten Werten vorbesetzen + $z2 = $di{$i}; + $z3 = abs($minDif); + } + else { + $z2 = $maxDif; + $z3 = abs($di{$i}); # Nur Betrag ohne Vorzeichen + } + + # Alle vorbesetzen Werte umrechnen auf echte Ausgabe px + $he = (!$px_pos) ? 0 : int(($maxDif-$z2)/$maxDif*$px_pos); # Teilung durch 0 vermeiden + $z2 = ($px_pos - $he) ; + + $z4 = (!$px_neg) ? 0 : int((abs($minDif)-$z3)/abs($minDif)*$px_neg); # Teilung durch 0 unbedingt vermeiden + $z3 = ($px_neg - $z4); + + # Beiden Zonen die Werte ausgeben könnten muß fsize als zusätzlicher Raum zugeschlagen werden ! + $he += $fsize; + $z4 += $fsize if ($z3); # komplette Grafik ohne negativ Balken, keine Ausgabe von z3 & z4 + } + + # das style des nächsten TD bestimmt ganz wesentlich das gesammte Design + # das \n erleichtert das lesen des Seitenquelltext beim debugging + # vertical-align:bottom damit alle Balken und Ausgaben wirklich auf der gleichen Grundlinie sitzen + + $ret .=""; # Stundenwerte ohne führende 0 + } + + $ret .= ""; + + ################### + # Legende unten + if ($legend_txt && ($legend eq 'bottom')) { + $ret .= ""; + $ret .= ""; + } + + $ret .= "
$header
$legend_txt
$val
$val
\n"; + + my $v; + if (($type eq 'pv') || ($type eq 'co')) { + $v = ($type eq 'co') ? $co{$i} : $pv{$i} ; + $v = 0 if (($type eq 'co') && !$pv{$i} && !$show_night); # auch bei type co die Nacht ggf. unterdrücken + $val = formatVal6($v,$kw,$we{$i}); + + $ret .=""; # mit width=100% etwas bessere Füllung der Balken + $ret .=""; + $ret .=""; + + if ($v || $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 $colorfc ? " background-color:#$colorfc\"" : '"'; # Syntaxhilight + + $ret .= ""; + $ret .= ""; + } + } + elsif ($type eq 'pvco') { + my ($color1, $color2, $style1, $style2); + + $ret .="
".$val."
"; + + my $sicon = 1; + $ret .= $is{$i} if (defined ($is{$i}) && $sicon); + + ################################## + # inject the new icon if defined + $ret .= consinject($hash,$i,@pgCDev) if($ret); + + $ret .= "
\n"; # mit width=100% etwas bessere Füllung der Balken + + if($he) { # der Freiraum oben kann beim größten Balken ganz entfallen + $ret .=""; + } + + if($pv{$i} > $co{$i}) { # wer ist oben, co pder pv ? Wert und Farbe für Zone 2 & 3 vorbesetzen + $val = formatVal6($pv{$i},$kw,$we{$i}); + $color1 = $colorfc; + $style1 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; + $style1 .= (defined($color1)) ? " background-color:#$color1\"" : '"'; + + if($z3) { # die Zuweisung können wir uns sparen wenn Zone 3 nachher eh nicht ausgegeben wird + $v = formatVal6($co{$i},$kw,$we{$i}); + $color2 = $colorc; + $style2 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; + $style2 .= (defined($color2)) ? " background-color:#$color2\"" : '"'; + } + } + else { + $val = formatVal6($co{$i},$kw,$we{$i}); + $color1 = $colorc; + $style1 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; + $style1 .= (defined($color1)) ? " background-color:#$color1\"" : '"'; + + if($z3) { + $v = formatVal6($pv{$i},$kw,$we{$i}); + $color2 = $colorfc; + $style2 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; + $style2 .= (defined($color2)) ? " background-color:#$color2\"" : '"'; + } + } + + $ret .= ""; + $ret .= ""; + + if($z3) { # die Zone 3 lassen wir bei zu kleinen Werten auch ganz weg + $ret .= ""; + $ret .= ""; + } + } + else { # Type diff + my $style = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; + $ret .= "
$val"; + $ret .= $is{$i} if (defined $is{$i}); + + ################################## + # inject the new icon if defined + $ret .= consinject($hash,$i,@pgCDev) if($ret); + + $ret .= "
$v
\n"; # Tipp : das nachfolgende border=0 auf 1 setzen hilft sehr Ausgabefehler zu endecken + $val = ($di{$i} >= 0) ? formatVal6($di{$i},$kw,$we{$i}) : ''; + $val = '   0  ' if ($di{$i} == 0); # Sonderfall , hier wird die 0 gebraucht ! + + if($val) { + $ret .= ""; + $ret .= ""; + } + + if($di{$i} >= 0) { # mit Farbe 1 colorfc füllen + $style .= defined $colorfc ? " background-color:#$colorfc\"" : '"'; + $z2 = 1 if ($di{$i} == 0); # Sonderfall , 1px dünnen Strich ausgeben + $ret .= ""; + $ret .= ""; + } + else { # ohne Farbe + $z2 = 2 if ($di{$i} == 0); # Sonderfall, hier wird die 0 gebraucht ! + if ($z2 && $val) { # z2 weglassen wenn nicht unbedigt nötig bzw. wenn zuvor he mit val keinen Wert hatte + $ret .= ""; + $ret .= ""; + } + } + + if($di{$i} < 0) { # Negativ Balken anzeigen ? + $style .= (defined($colorc)) ? " background-color:#$colorc\"" : '"'; # mit Farbe 2 colorc füllen + $ret .= ""; + $ret .= ""; + } + elsif($z3) { # ohne Farbe + $ret .= ""; + $ret .= ""; + } + + if($z4) { # kann entfallen wenn auch z3 0 ist + $val = ($di{$i} < 0) ? formatVal6($di{$i},$kw,$we{$i}) : ' '; + $ret .= ""; + $ret .= ""; + } + } + + if ($show_diff eq 'bottom') { # zusätzliche diff Anzeige + $val = formatVal6($di{$i},$kw,$we{$i}); + $val = ($di{$i} < 0) ? ''.$val.'' : '+'.$val; # Kommentar siehe oben bei show_diff eq top + $ret .= ""; + } + + $ret .= "
".$val."
"; + $ret .= $is{$i} if (defined $is{$i}); + $ret .= "
".$val."
$val
"; + $t{$i} = $t{$i}.$hourstyle if(defined($hourstyle)); # z.B. 10:00 statt 10 + $ret .= $t{$i}."
"; + $ret .= "$legend_txt
"; + $ret .= $html_end if (defined($html_end)); + $ret .= ""; + +return $ret; +} + +################################################################ +# Inject consumer icon +################################################################ +sub consinject { + my ($hash,$i,@pgCDev) = @_; + my $name = $hash->{NAME}; + my $ret = ""; + + for (@pgCDev) { + if ($_) { + my ($cons,$im,$start,$end) = split (':', $_); + Log3($name, 4, "$name - Consumer to show -> $cons, relative to current time -> start: $start, end: $end") if($i<1); + + if ($im && ($i >= $start) && ($i <= $end)) { + $ret .= FW_makeImage($im); + } + } + } + +return $ret; +} + +############################################################################### +# Balkenbreite normieren +# +# Die Balkenbreite wird bestimmt durch den Wert. +# Damit alle Balken die gleiche Breite bekommen, müssen die Werte auf +# 6 Ausgabezeichen angeglichen werden. +# "align=center" gleicht gleicht es aus, alternativ könnte man sie auch +# komplett rechtsbündig ausgeben. +# Es ergibt bei fast allen Styles gute Ergebnisse, Ausnahme IOS12 & 6, da diese +# beiden Styles einen recht großen Font benutzen. +# Wird Wetter benutzt, wird die Balkenbreite durch das Icon bestimmt +# +############################################################################### +sub formatVal6 { + my ($v,$kw,$w) = @_; + 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 $id = shift; + + my %weather_ids = ( + '00' => 'weather_sun', # Sonne (klar) # vorhanden + '01' => 'weather_cloudy_light', # leichte Bewölkung (1/3) # vorhanden + '02' => 'weather_cloudy', # mittlere Bewölkung (2/3) # vorhanden + '03' => 'weather_cloudy_heavy', # starke Bewölkung (3/3) # vorhanden + '10' => 'weather_fog', # Nebel # neu + '11' => 'weather_rain_fog', # Nebel mit Regen # neu + '20' => 'weather_rain_heavy', # Regen (viel) # vorhanden + '21' => 'weather_rain_snow_heavy', # Regen (viel) mit Schneefall # neu + '30' => 'weather_rain_light', # leichter Regen (1 Tropfen) # vorhanden + '31' => 'weather_rain', # leichter Regen (2 Tropfen) # vorhanden + '32' => 'weather_rain_heavy', # leichter Regen (3 Tropfen) # vorhanden + '40' => 'weather_rain_snow_light', # leichter Regen mit Schneefall (1 Tropfen) # neu + '41' => 'weather_rain_snow', # leichter Regen mit Schneefall (3 Tropfen) # neu + '50' => 'weather_snow_light', # bewölkt mit Schneefall (1 Flocke) # vorhanden + '51' => 'weather_snow', # bewölkt mit Schneefall (2 Flocken) # vorhanden + '52' => 'weather_snow_heavy', # bewölkt mit Schneefall (3 Flocken) # vorhanden + '60' => 'weather_rain_light', # Sonne, Wolke mit Regen (1 Tropfen) # vorhanden + '61' => 'weather_rain', # Sonne, Wolke mit Regen (2 Tropfen) # vorhanden + '62' => 'weather_rain_heavy', # Sonne, Wolke mit Regen (3 Tropfen) # vorhanden + '70' => 'weather_snow_light', # Sonne, Wolke mit Schnee (1 Flocke) # vorhanden + '71' => 'weather_snow_heavy', # Sonne, Wolke mit Schnee (3 Flocken) # vorhanden + '80' => 'weather_thunderstorm', # Wolke mit Blitz # vorhanden + '81' => 'weather_storm', # Wolke mit Blitz und Starkregen # vorhanden + '90' => 'weather_sun', # Sonne (klar) # vorhanden + '91' => 'weather_sun', # Sonne (klar) wie 90 # vorhanden + '100' => 'weather_night', # Mond - Nacht # neu + '101' => 'weather_night_cloudy_light', # Mond mit Wolken - # neu + '102' => 'weather_night_cloudy', # Wolken mittel (2/2) - Nacht # neu + '103' => 'weather_night_cloudy_heavy', # Wolken stark (3/3) - Nacht # neu + '110' => 'weather_night_fog', # Nebel - Nacht # neu + '111' => 'weather_night_rain_fog', # Nebel mit Regen (3 Tropfen) - Nacht # neu + '120' => 'weather_night_rain_heavy', # Regen (viel) - Nacht # neu + '121' => 'weather_night_snow_rain_heavy', # Regen (viel) mit Schneefall - Nacht # neu + '130' => 'weather_night_rain_light', # leichter Regen (1 Tropfen) - Nacht # neu + '131' => 'weather_night_rain', # leichter Regen (2 Tropfen) - Nacht # neu + '132' => 'weather_night_rain_heavy', # leichter Regen (3 Tropfen) - Nacht # neu + '140' => 'weather_night_snow_rain_light', # leichter Regen mit Schneefall (1 Tropfen) - Nacht # neu + '141' => 'weather_night_snow_rain_heavy', # leichter Regen mit Schneefall (3 Tropfen) - Nacht # neu + '150' => 'weather_night_snow_light', # bewölkt mit Schneefall (1 Flocke) - Nacht # neu + '151' => 'weather_night_snow', # bewölkt mit Schneefall (2 Flocken) - Nacht # neu + '152' => 'weather_night_snow_heavy', # bewölkt mit Schneefall (3 Flocken) - Nacht # neu + '160' => 'weather_night_rain_light', # Mond, Wolke mit Regen (1 Tropfen) - Nacht # neu + '161' => 'weather_night_rain', # Mond, Wolke mit Regen (2 Tropfen) - Nacht # neu + '162' => 'weather_night_rain_heavy', # Mond, Wolke mit Regen (3 Tropfen) - Nacht # neu + '170' => 'weather_night_snow_rain', # Mond, Wolke mit Schnee (1 Flocke) - Nacht # neu + '171' => 'weather_night_snow_heavy', # Mond, Wolke mit Schnee (3 Flocken) - Nacht # neu + '180' => 'weather_night_thunderstorm_light', # Wolke mit Blitz - Nacht # neu + '181' => 'weather_night_thunderstorm', # Wolke mit Blitz und Starkregen - Nacht # neu + '999' => '1px-spacer' # Dummy - keine Anzeige Wettericon # vorhanden + ); + +return $weather_ids{$id} if(defined($weather_ids{$id})); +return 'unknown'; +} + +################################################################ +# Timestamp berechnen +################################################################ +sub TimeAdjust { + my $epoch = shift; + + my ($lyear,$lmonth,$lday,$lhour) = (localtime($epoch))[5,4,3,2]; + + $lyear += 1900; # year is 1900 based + $lmonth++; # month number is zero based + + if(AttrVal("global","language","EN") eq "DE") { + return (sprintf("%02d.%02d.%04d %02d:%s", $lday,$lmonth,$lyear,$lhour,"00:00")); + } + else { + return (sprintf("%04d-%02d-%02d %02d:%s", $lyear,$lmonth,$lday,$lhour,"00:00")); + } +} + +################################################################################################## +# PV Forecast Rad1h in kWh / Wh +# Für die Umrechnung in einen kWh/Wh-Wert benötigt man einen entsprechenden Faktorwert: +# +# * 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: 83% wegen Ost/West und Schatten (Iteration) +# +# Die Formel wäre dann: +# Ertrag in kWh = Rad1h * 0.00027778 * 31,04 qm * 16,52% * 98,3% * 100% +# +# Damit ergibt sich ein Umrechnungsfaktor von: 0,00140019 für kWh / 1,40019 für Wh +# +# Bei einem Rad1h-Wert von 500 ergibt dies bei mir also 0,700095 kWh / 700,095 Wh +################################################################################################## +sub calcPVforecast { + my $name = shift; + my $rad = shift; # Nominale Strahlung aus DWD Device + my $fh = shift; # Stunde des Tages + + my $ma = ReadingsNum ($name, "moduleArea", 0 ); # Solar Modulfläche (gesamt) + my $me = ReadingsNum ($name, "moduleEfficiency", $defpvme ); # Solar Modul Wirkungsgrad (%) + my $ie = ReadingsNum ($name, "inverterEfficiency", $definve ); # Solar Inverter Wirkungsgrad (%) + my $hc = ReadingsNum ($name, "pvCorrectionFactor_".sprintf("%02d",$fh), 1 ); # Korrekturfaktor für die Stunde des Tages + + my $pv = sprintf "%.1f", ($rad * $kJtokWh * $ma * $me/100 * $ie/100 * $hc * 1000); + +return $pv." Wh"; +} + +################################################################ +# Zusammenfassungen erstellen +################################################################ +sub sumNextHours { + my $hash = shift; + my $chour = shift; # aktuelle Stunde + my $daref = shift; + + my $name = $hash->{NAME}; + + my $next4HoursSum = { "PV" => 0, "Consumption" => 0, "Total" => 0, "ConsumpRcmd" => 0 }; + my $restOfDaySum = { "PV" => 0, "Consumption" => 0, "Total" => 0, "ConsumpRcmd" => 0 }; + my $tomorrowSum = { "PV" => 0, "Consumption" => 0, "Total" => 0, "ConsumpRcmd" => 0 }; + + my $rdh = 24 - $chour - 1; # verbleibende Anzahl Stunden am Tag beginnend mit 00 (abzüglich aktuelle Stunde) + my $thforecast = ReadingsNum ($name, "ThisHour_PVforecast", 0); + $next4HoursSum->{PV} = $thforecast; + $restOfDaySum->{PV} = $thforecast; + + for my $h (1..23) { + $next4HoursSum->{PV} += ReadingsNum ($name, "NextHour".(sprintf "%02d", $h)."_PVforecast", 0) if($h <= 3); + $restOfDaySum->{PV} += ReadingsNum ($name, "NextHour".(sprintf "%02d", $h)."_PVforecast", 0) if($h <= $rdh); + $tomorrowSum->{PV} += ReadingsNum ($name, "NextHour".(sprintf "%02d", $h)."_PVforecast", 0) if($h > $rdh); + } + + push @$daref, "Next04Hours_PV:". (int $next4HoursSum->{PV})." Wh"; + push @$daref, "RestOfDay_PV:". (int $restOfDaySum->{PV}). " Wh"; + push @$daref, "Tomorrow_PV:". (int $tomorrowSum->{PV}). " Wh"; + + createReadings ($hash, $daref); + +return; +} + +################################################################ +# Readings erstellen +################################################################ +sub createReadings { + my $hash = shift; + my $daref = shift; + + readingsBeginUpdate($hash); + + for my $elem (@$daref) { + my ($rn,$rval) = split ":", $elem, 2; + readingsBulkUpdate($hash, $rn, $rval); + } + + readingsEndUpdate($hash, 1); + +return; +} + +###################################################################################### +# NOTIFYDEV erstellen +###################################################################################### +sub createNotifyDev { + my $hash = shift; + my $name = $hash->{NAME}; + + RemoveInternalTimer($hash, "FHEM::SolarForecast::createNotifyDev"); + + if($init_done == 1) { + my @nd; + + my $fcdev = ReadingsVal($name, "currentForecastDev", ""); # Forecast device + my $indev = ReadingsVal($name, "currentInverterDev", ""); # Inverter device + my $medev = ReadingsVal($name, "currentMeterDev", ""); # Meter device + + push @nd, $fcdev; + push @nd, $indev; + push @nd, $medev; + + $hash->{NOTIFYDEV} = join ",", @nd if(@nd); + } + else { + InternalTimer(gettimeofday()+3, "FHEM::SolarForecast::createNotifyDev", $hash, 0); + } + +return; +} + +1; + +=pod +=item summary Visualization of solar predictions for PV systems +=item summary_DE Visualisierung von solaren Vorhersagen für PV Anlagen + +=begin html + + +=end html +=begin html_DE + + +

SolarForecast

+ +
+Das Modul SolarForecast erstellt auf Grundlage der Werte aus Modulen vom Typ DWD_OpenData, SMAInverter und SMAEM eine Vorhersage +für den solaren Ertrag und weitere Informationen als Grundlage für abhängige Steuerungen.
+Die Solargrafik kann ebenfalls in FHEM Tablet UI mit dem +"SolarForecast Widget" integriert werden.

+ +Die solare Vorhersage basiert auf der durch den Deutschen Wetterdienst (DWD) prognostizierten Globalstrahlung am +Anlagenstandort. Im zugeordneten DWD_OpenData Device ist die passende Wetterstation mit dem Attribut "forecastStation" +festulegen um eine Prognose für diesen Standort zu erhalten.
+Abhängig von der physikalischen Anlagengestaltung (Ausrichtung, Winkel, Aufteilung in mehrere Strings, u.a.) wird die +verfügbare Globalstrahlung ganz spezifisch in elektrische Energie umgewandelt. +Um eine Anpassung an die persönliche Anlage zu ermöglichen, können Korrekturfaktoren manuell +(set <name> pvCorrectionFactor_XX) oder automatisiert (set <name> pvCorrectionFactor_Auto) eingefügt werden. + +
    + + Define +

    + +
      + Ein SolarForecast Device wird einfach erstellt mit:

      + +
        + define <name> SolarForecast +
      +
      + + Nach der Definition des Devices ist zwingend ein Vorhersage-Device des Typs DWD_OpenData zuzuordnen sowie weitere + anlagenspezifische Angaben mit dem entsprechenden set-Kommando vorzunehmen. Empfohlen wird ebenfalls ein Inverter-Device + des Typs SMAInverter zuzuordnen um eine automatische Vorhersagekorrektur zu ermöglichen (Auto Learning Mode). + +

      +
    + + + Set +
      +
        + +
      • forecastDevice
        + Legt das Device (Typ DWD_OpenData) fest, welches die Daten der solaren Vorhersage liefert. Ist noch kein Device dieses Typs + vorhanden, muß es manuell definiert werden (siehe DWD_OpenData Commandref).
        + Im ausgewählten DWD_OpenData Device müssen mindestens diese Attribute gesetzt sein:

        + +
          + + + + + + +
          forecastDays 1
          forecastProperties Rad1h,TTT,Neff,R600,ww,SunUp,SunRise,SunSet
          forecastResolution 1
          forecastStation <Stationscode der ausgewerteten DWD Station>
          +
        +
        + + Hinweis: Das Device muß mindestens für das "state" Reading Events erzeugen. +
      • +
      +
      + +
        + +
      • inverterDevice
        + Legt das Device (Typ SMAInverter) zur Lieferung der aktuellen PV Erzeugungswerte fest. + Ist noch kein Device dieses Typs vorhanden, muß es manuell definiert werden + (siehe SMAInverter Commandref). +

        + + Hinweis: Das Device muß mindestens für das "state" Reading Events erzeugen. +
      • +
      +
      + +
        + +
      • inverterEfficiency <Zahl>
        + Wirkungsgrad des Wechselrichters (inverterDevice) in %.
        + (default: 98.3) +
      • +
      +
      + +
        + +
      • meterDevice
        + Legt das Device (Typ SMAEM) zur Messung des aktuellen Energiebezugs fest. + Ist noch kein Device dieses Typs vorhanden, muß es manuell definiert werden + (siehe SMAEM Commandref). +
      • +
      +
      + +
        + +
      • moduleArea <Zahl>
        + Gesamte installierte Solarmodulfläche in qm. +
      • +
      +
      + +
        + +
      • moduleEfficiency <Zahl>
        + Wirkungsgrad der Solarmodule in %.
        + (default: 16.52) +
      • +
      +
      + +
        + +
      • pvCorrectionFactor_XX <Zahl>
        + Manueller Korrekturfaktor für die Stunde XX des Tages zur Anpassung der Vorhersage an die individuelle Anlage.
        + (default: 1.0) +
      • +
      +
      + +
    +
    + + + Get +
      +
        + +
      • html
        + Die Solar Grafik wird als HTML-Code abgerufen und wiedergegeben. +
      • +
      +
      +
    +
    + + + Attribute +

    +
      +
        + +
      • alias
        + In Verbindung mit "showLink" ein beliebiger Abzeigename. +
      • +
        + + +
      • autoRefresh
        + Wenn gesetzt, werden aktive Browserseiten des FHEMWEB-Devices welches das SolarForecast-Device aufgerufen hat, nach der + eingestellten Zeit (Sekunden) neu geladen. Sollen statt dessen Browserseiten eines bestimmten FHEMWEB-Devices neu + geladen werden, kann dieses Device mit dem Attribut "autoRefreshFW" festgelegt werden. +
      • +
        + + +
      • autoRefreshFW
        + Ist "autoRefresh" aktiviert, kann mit diesem Attribut das FHEMWEB-Device bestimmt werden dessen aktive Browserseiten + regelmäßig neu geladen werden sollen. +
      • +
        + + +
      • beamColor
        + Farbauswahl der primären Balken. +
      • +
        + + +
      • beamColor2
        + Farbauswahl der sekundären Balken. Die zweite Farbe ist nur sinnvoll für den Anzeigedevice-Typ "Generation_Consumption" + (pvco) und "Differential" (diff). +
      • +
        + + +
      • beamHeight <value>
        + Höhe der Balken in px und damit Bestimmung der gesammten Höhe. + In Verbindung mit "hourCount" lassen sich damit auch recht kleine Grafikausgaben erzeugen.
        + (default: 200) +
      • +
        + + +
      • beamWidth <value>
        + Breite der Balken in px.
        + (default: 6 (auto)) +
      • +
        + + +
      • consumerList <Verbraucher1>:<Icon>@<Farbe>,<Verbraucher2>:<Icon>@<Farbe>,...
        + Komma getrennte Liste der am SMA Sunny Home Manager angeschlossenen Geräte.
        + Sobald die Aktivierung einer der angegebenen Verbraucher geplant ist, wird der geplante Zeitraum in der Grafik + angezeigt. + Der Name des Verbrauchers muss dabei dem Namen im Reading "L3_<Verbrauchername>_Planned" entsprechen.

        + + Beispiel:
        + attr <name> consumerList Trockner:scene_clothes_dryer@yellow,Waschmaschine:scene_washing_machine@lightgreen,Geschirrspueler:scene_dishwasher@orange +
        +
      • +
        + + +
      • consumerLegend <none | icon_top | icon_bottom | text_top | text_bottom>
        + Lage bzw. Art und Weise der angezeigten Verbraucherlegende. +
      • +
        + + +
      • disable
        + Aktiviert/deaktiviert das Device. +
      • +
        + + +
      • forcePageRefresh
        + Das Attribut wird durch das SMAPortal-Device ausgewertet.
        + Wenn gesetzt, wird ein Reload aller Browserseiten mit aktiven FHEMWEB-Verbindungen nach dem Update des + Eltern-SMAPortal-Devices erzwungen. +
      • +
        + + +
      • headerAlignment <center | left | right>
        + Ausrichtung der Kopfzeilen.
        + (default: center) +
      • +
        + + +
      • headerDetail <all | co | pv | pvco | statusLink>
        + Detailiierungsgrad der Kopfzeilen.
        + (default: all) + +
          + + + + + + + +
          all Anzeige Erzeugung (PV), Verbrauch (CO), Link zur Device Detailanzeige + Aktualisierungszeit (default)
          co nur Verbrauch (CO)
          pv nur Erzeugung (PV)
          pvco Erzeugung (PV) und Verbrauch (CO)
          statusLink Link zur Device Detailanzeige + Aktualisierungszeit
          +
        +
      • +
        + + +
      • hourCount <4...24>
        + Anzahl der Balken/Stunden.
        + (default: 24) +
      • +
        + + +
      • hourStyle
        + Format der Zeitangabe.

        + +
          + + + + + +
          nicht gesetzt - nur Stundenangabe ohne Minuten (default)
          :00 - Stunden sowie Minuten zweistellig, z.B. 10:00
          :0 - Stunden sowie Minuten einstellig, z.B. 8:0
          +
        +
      • +
        + + +
      • maxPV <0...val>
        + Maximaler Ertrag in einer Stunde zur Berechnung der Balkenhöhe.
        + (default: 0 -> dynamisch) +
      • +
        + + +
      • htmlStart <HTML-String>
        + Angabe eines beliebigen HTML-Strings der vor dem Grafik-Code ausgeführt wird. +
      • +
        + + +
      • htmlEnd <HTML-String>
        + Angabe eines beliebigen HTML-Strings der nach dem Grafik-Code ausgeführt wird. +
      • +
        + + +
      • showDiff <no | top | bottom>
        + Zusätzliche Anzeige der Differenz "Ertrag - Verbrauch" wie beim Anzeigetyp Differential (diff).
        + (default: no) +
      • +
        + + +
      • showHeader
        + Anzeige der Kopfzeile mit Prognosedaten, Rest des aktuellen Tages und des nächsten Tages
        + (default: 1) +
      • +
        + + +
      • showLink
        + Anzeige des Detail-Links über dem Grafik-Device
        + (default: 1) +
      • +
        + + +
      • showNight
        + Die Nachtstunden (ohne Ertragsprognose) werden mit angezeigt.
        + (default: 0) +
      • +
        + + +
      • showWeather
        + Wettericons anzeigen.
        + (default: 1) +
      • +
        + + +
      • spaceSize <value>
        + Legt fest wieviel Platz in px über oder unter den Balken (bei Anzeigetyp Differential (diff)) zur Anzeige der + Werte freigehalten wird. Bei Styles mit große Fonts kann der default-Wert zu klein sein bzw. rutscht ein + Balken u.U. über die Grundlinie. In diesen Fällen bitte den Wert erhöhen.
        + (default: 24) +
      • +
        + + +
      • consumerAdviceIcon
        + Setzt das Icon zur Darstellung der Zeiten mit Verbraucherempfehlung. + Dazu kann ein beliebiges Icon mit Hilfe der Standard "Select Icon"-Funktion (links unten im FHEMWEB) direkt ausgewählt + werden. +
      • +
        + + +
      • layoutType <pv | co | pvco | diff>
        + Layout der Portalgrafik.
        + (default: pv) +

        + +
          + + + + + + +
          pv - Erzeugung
          co - Verbrauch
          pvco - Erzeugung und Verbrauch
          diff - Differenz von Erzeugung und Verbrauch
          +
        +
      • +
        + + +
      • Wh/kWh <Wh | kWh>
        + Definiert die Anzeigeeinheit in Wh oder in kWh auf eine Nachkommastelle gerundet.
        + (default: W) +
      • +
        + + +
      • weatherColor
        + Farbe der Wetter-Icons. +
      • +
        + +
      +
    + +
+ +=end html_DE + +=for :application/json;q=META.json 76_SolarForecast.pm +{ + "abstract": "Visualization of solar predictions for PV systems", + "x_lang": { + "de": { + "abstract": "Visualisierung von solaren Vorhersagen für PV Anlagen" + } + }, + "keywords": [ + "sma", + "photovoltaik", + "electricity", + "portal", + "smaportal", + "graphics", + "longpoll", + "refresh" + ], + "version": "v1.1.1", + "release_status": "testing", + "author": [ + "Heiko Maaz " + ], + "x_fhem_maintainer": [ + "DS_Starter" + ], + "x_fhem_maintainer_github": [ + "nasseeder1" + ], + "prereqs": { + "runtime": { + "requires": { + "FHEM": 5.00918799, + "perl": 5.014, + "Time::HiRes": 0 + }, + "recommends": { + "FHEM::Meta": 0 + }, + "suggests": { + } + } + }, + "resources": { + "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