2019-03-03 20:08:04 +00:00
#########################################################################################################################
2019-03-16 06:48:59 +00:00
# $Id: 76_SMAPortal.pm 00000 2019-03-14 20:21:11Z DS_Starter $
2019-03-03 20:08:04 +00:00
#########################################################################################################################
# 76_SMAPortal.pm
#
# (c) 2019 by Heiko Maaz
# e-mail: Heiko dot Maaz at t-online dot de
#
2019-03-10 07:34:29 +00:00
# This module can be used to get data from SMA Portal https://www.sunnyportal.com/Templates/Start.aspx .
2019-03-03 20:08:04 +00:00
#
# 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/>.
#
2019-05-30 07:44:31 +00:00
# Credits (Thanks to all!):
# Brun von der Gönne <brun at goenne dot de> : author of 98_SHM.pm
# BerndArnold : author of 98_SHMForecastRelative.pm
# Wzut/XGuide : creation of SMAPortal graphics
2019-03-03 20:08:04 +00:00
#
# FHEM Forum: http://forum.fhem.de/index.php/topic,27667.0.html
#
#########################################################################################################################
#
# Definition: define <name> SMAPortal
#
#########################################################################################################################
package main ;
use strict ;
use warnings ;
2019-03-25 19:19:05 +00:00
eval "use FHEM::Meta;1" ;
2019-03-03 20:08:04 +00:00
###############################################################
# SMAPortal Initialize
2019-03-16 06:48:59 +00:00
# Da ich mit package arbeite müssen für die jeweiligen hashFn
# Funktionen der Funktionsname und davor mit :: getrennt der
# eigentliche package Name des Modules
2019-03-03 20:08:04 +00:00
###############################################################
sub SMAPortal_Initialize ($) {
my ( $ hash ) = @ _ ;
2019-03-21 21:39:30 +00:00
$ hash - > { DefFn } = "FHEM::SMAPortal::Define" ;
$ hash - > { UndefFn } = "FHEM::SMAPortal::Undefine" ;
$ hash - > { DeleteFn } = "FHEM::SMAPortal::Delete" ;
$ hash - > { AttrFn } = "FHEM::SMAPortal::Attr" ;
$ hash - > { SetFn } = "FHEM::SMAPortal::Set" ;
$ hash - > { GetFn } = "FHEM::SMAPortal::Get" ;
$ hash - > { DbLog_splitFn } = "FHEM::SMAPortal::DbLog_split" ;
$ hash - > { AttrList } = "cookieLocation " .
"cookielifetime " .
"detailLevel:1,2,3,4 " .
"disable:0,1 " .
"getDataRetries:1,2,3,4,5,6,7,8,9,10 " .
"interval " .
"showPassInLog:1,0 " .
"timeout " .
"userAgent " .
$ readingFnAttributes ;
2019-03-24 17:58:54 +00:00
eval { FHEM::Meta:: InitMod ( __FILE__ , $ hash ) } ; # für Meta.pm (https://forum.fhem.de/index.php/topic,97589.0.html)
2019-03-16 06:48:59 +00:00
return ;
2019-03-11 22:54:31 +00:00
}
2019-03-03 20:08:04 +00:00
2019-03-16 06:48:59 +00:00
###############################################################
# Begin Package
###############################################################
package FHEM::SMAPortal ;
use strict ;
use warnings ;
use GPUtils qw( :all ) ; # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt
use POSIX ;
2019-03-25 19:19:05 +00:00
eval "use FHEM::Meta;1" or my $ modMetaAbsent = 1 ;
2019-03-16 06:48:59 +00:00
use Data::Dumper ;
use Blocking ;
use Time::HiRes qw( gettimeofday ) ;
2019-03-24 15:51:33 +00:00
use Time::Local ;
2019-03-16 06:48:59 +00:00
use LWP::UserAgent ;
use HTTP::Cookies ;
use JSON qw( decode_json ) ;
use MIME::Base64 ;
2019-03-24 15:51:33 +00:00
use Encode ;
2019-03-16 06:48:59 +00:00
# Run before module compilation
BEGIN {
# Import from main::
GP_Import (
qw(
attr
AttrVal
2019-05-30 07:44:31 +00:00
AttrNum
2019-03-16 06:48:59 +00:00
addToDevAttrList
addToAttrList
BlockingCall
BlockingKill
CommandAttr
2019-04-29 22:07:15 +00:00
CommandDefine
2019-03-16 06:48:59 +00:00
CommandDeleteAttr
CommandDeleteReading
CommandSet
defs
delFromDevAttrList
delFromAttrList
devspec2array
deviceEvents
Debug
FmtDateTime
FmtTime
2019-05-01 14:30:09 +00:00
FW_makeImage
2019-03-16 06:48:59 +00:00
fhemTimeGm
getKeyValue
gettimeofday
genUUID
init_done
InternalTimer
IsDisabled
Log
Log3
modules
readingsSingleUpdate
readingsBulkUpdate
readingsBulkUpdateIfChanged
readingsBeginUpdate
2019-03-26 17:40:00 +00:00
readingsDelete
2019-03-16 06:48:59 +00:00
readingsEndUpdate
2019-04-29 22:07:15 +00:00
ReadingsNum
2019-06-03 23:00:53 +00:00
ReadingsTimestamp
2019-03-16 06:48:59 +00:00
ReadingsVal
RemoveInternalTimer
setKeyValue
2019-03-19 18:11:34 +00:00
sortTopicNum
2019-03-16 06:48:59 +00:00
TimeNow
Value
2019-05-30 07:44:31 +00:00
json2nameValue
2019-06-03 23:00:53 +00:00
FW_directNotify
FW_ME
FW_subdir
FW_room
FW_detail
FW_wname
2019-03-16 06:48:59 +00:00
)
) ;
}
# Versions History intern
our % vNotesIntern = (
2019-06-03 23:00:53 +00:00
"2.0.0" = > "03.06.2019 designed for SMAPortalSPG graphics device" ,
2019-05-30 07:44:31 +00:00
"1.8.0" = > "27.05.2019 redesign of SMAPortal graphics by Wzut/XGuide " ,
2019-05-01 20:38:02 +00:00
"1.7.1" = > "01.05.2019 PortalAsHtml: use of colored svg-icons possible " ,
2019-05-01 14:30:09 +00:00
"1.7.0" = > "01.05.2019 code change of PortalAsHtml, new attributes \"portalGraphicColor\" and \"portalGraphicStyle\" " ,
2019-04-29 22:07:15 +00:00
"1.6.0" = > "29.04.2019 function PortalAsHtml " ,
2019-04-22 18:01:58 +00:00
"1.5.5" = > "22.04.2019 fix readings for BattryOut and BatteryIn " ,
2019-04-07 06:45:06 +00:00
"1.5.4" = > "26.03.2019 delete L1_InfoMessages if no info occur " ,
2019-03-26 17:40:00 +00:00
"1.5.3" = > "26.03.2019 delete L1_ErrorMessages, L1_WarningMessages if no errors or warnings occur " ,
2019-03-25 19:19:05 +00:00
"1.5.2" = > "25.03.2019 prevent module from deactivation in case of unavailable Meta.pm " ,
2019-03-24 20:55:35 +00:00
"1.5.1" = > "24.03.2019 fix \$VAR1 problem Forum: #27667.msg922983.html#msg922983 " ,
2019-03-24 15:51:33 +00:00
"1.5.0" = > "23.03.2019 add consumer data " ,
"1.4.0" = > "22.03.2019 add function extractPlantData, DbLog_split, change L2 Readings " ,
"1.3.0" = > "18.03.2019 change module to use package FHEM::SMAPortal and Meta.pm, new sub setVersionInfo " ,
2019-03-16 06:48:59 +00:00
"1.2.3" = > "12.03.2019 make ready for 98_Installer.pm " ,
"1.2.2" = > "11.03.2019 new Errormessage analyze added, make ready for Meta.pm " ,
"1.2.1" = > "10.03.2019 behavior of state changed, commandref revised " ,
"1.2.0" = > "09.03.2019 integrate weather data, minor fixes " ,
"1.1.0" = > "09.03.2019 make get data more stable, new attribute \"getDataRetries\" " ,
"1.0.0" = > "03.03.2019 initial "
) ;
2019-06-03 23:00:53 +00:00
# Web instance
2019-03-16 06:48:59 +00:00
2019-03-03 20:08:04 +00:00
###############################################################
# SMAPortal Define
###############################################################
2019-03-16 06:48:59 +00:00
sub Define ($$) {
2019-03-03 20:08:04 +00:00
my ( $ hash , $ def ) = @ _ ;
my @ a = split ( /\s+/ , $ def ) ;
return "Wrong syntax: use \"define <name> SMAPortal\" " if ( int ( @ a ) < 1 ) ;
2019-03-25 19:19:05 +00:00
$ hash - > { HELPER } { MODMETAABSENT } = 1 if ( $ modMetaAbsent ) ; # Modul Meta.pm nicht vorhanden
2019-05-30 07:44:31 +00:00
setVersionInfo ( $ hash ) ; # Versionsinformationen setzen
getcredentials ( $ hash , 1 ) ; # Credentials lesen und in RAM laden ($boot=1)
CallInfo ( $ hash ) ; # Start Daten Abrufschleife
delcookiefile ( $ hash ) ; # Start Schleife regelmäßiges Löschen Cookiefile
2019-03-03 20:08:04 +00:00
return undef ;
}
###############################################################
# SMAPortal Undefine
###############################################################
2019-03-16 06:48:59 +00:00
sub Undefine ($$) {
2019-03-03 20:08:04 +00:00
my ( $ hash , $ arg ) = @ _ ;
RemoveInternalTimer ( $ hash ) ;
BlockingKill ( $ hash - > { HELPER } { RUNNING_PID } ) if ( $ hash - > { HELPER } { RUNNING_PID } ) ;
return undef ;
}
###############################################################
# SMAPortal Delete
###############################################################
2019-03-16 06:48:59 +00:00
sub Delete ($$) {
2019-03-03 20:08:04 +00:00
my ( $ hash , $ arg ) = @ _ ;
my $ index = $ hash - > { TYPE } . "_" . $ hash - > { NAME } . "_credentials" ;
my $ name = $ hash - > { NAME } ;
# gespeicherte Credentials löschen
setKeyValue ( $ index , undef ) ;
return undef ;
}
###############################################################
# SMAPortal Set
###############################################################
2019-03-16 06:48:59 +00:00
sub Set ($@) {
2019-03-03 20:08:04 +00:00
my ( $ hash , @ a ) = @ _ ;
return "\"set X\" needs at least an argument" if ( @ a < 2 ) ;
my $ name = $ a [ 0 ] ;
my $ opt = $ a [ 1 ] ;
my $ prop = $ a [ 2 ] ;
my $ prop1 = $ a [ 3 ] ;
my ( $ setlist , $ success ) ;
return if ( IsDisabled ( $ name ) ) ;
if ( ! $ hash - > { CREDENTIALS } ) {
# initiale setlist für neue Devices
$ setlist = "Unknown argument $opt, choose one of " .
"credentials "
;
} else {
# erweiterte Setlist wenn Credentials gesetzt
$ setlist = "Unknown argument $opt, choose one of " .
2019-04-29 22:07:15 +00:00
"credentials " .
2019-05-30 07:44:31 +00:00
"createPortalGraphic:Generation,Consumption,Generation_Consumption,Differential "
2019-03-03 20:08:04 +00:00
;
}
if ( $ opt eq "credentials" ) {
return "Credentials are incomplete, use username password" if ( ! $ prop || ! $ prop1 ) ;
2019-03-16 06:48:59 +00:00
( $ success ) = setcredentials ( $ hash , $ prop , $ prop1 ) ;
2019-03-03 20:08:04 +00:00
if ( $ success ) {
2019-03-16 06:48:59 +00:00
CallInfo ( $ hash ) ;
2019-03-03 20:08:04 +00:00
return "Username and Password saved successfully" ;
} else {
return "Error while saving Username / Password - see logfile for details" ;
}
2019-04-29 22:07:15 +00:00
} elsif ( $ opt eq "createPortalGraphic" ) {
if ( ! $ hash - > { CREDENTIALS } ) { return "Credentials of $name are not set - make sure you've set it with \"set $name credentials username password\"" ; }
2019-05-30 07:44:31 +00:00
my ( $ htmldev , $ ret , $ c , $ type , $ color2 ) ;
2019-04-29 22:07:15 +00:00
2019-05-30 07:44:31 +00:00
if ( $ prop eq "Generation" ) {
2019-05-31 10:53:56 +00:00
$ htmldev = "SPG1.$name" ; # Grafiktyp Generation (Erzeugung)
2019-05-30 07:44:31 +00:00
$ type = 'pv' ;
2019-05-31 10:53:56 +00:00
$ c = "SMA Sunny Portal Graphics - Forecast Generation" ;
$ color2 = "000000" ; # zweite Farbe als schwarz setzen
2019-05-30 07:44:31 +00:00
} elsif ( $ prop eq "Consumption" ) {
2019-05-31 10:53:56 +00:00
$ htmldev = "SPG2.$name" ; # Grafiktyp Consumption (Verbrauch)
2019-05-30 07:44:31 +00:00
$ type = 'co' ;
2019-05-31 10:53:56 +00:00
$ c = "SMA Sunny Portal Graphics - Forecast Consumption" ;
$ color2 = "000000" ; # zweite Farbe als schwarz setzen
2019-05-30 07:44:31 +00:00
} elsif ( $ prop eq "Generation_Consumption" ) {
2019-05-31 10:53:56 +00:00
$ htmldev = "SPG3.$name" ; # Grafiktyp Generation_Consumption (Erzeugung und Verbrauch)
2019-05-30 07:44:31 +00:00
$ type = 'pvco' ;
2019-05-31 10:53:56 +00:00
$ c = "SMA Sunny Portal Graphics - Forecast Generation & Consumption" ;
$ color2 = "FF5C82" ; # zweite Farbe als rot setzen
2019-05-30 07:44:31 +00:00
} elsif ( $ prop eq "Differential" ) {
2019-05-31 10:53:56 +00:00
$ htmldev = "SPG4.$name" ; # Grafiktyp Differential (Differenzanzeige)
2019-05-30 07:44:31 +00:00
$ type = 'diff' ;
2019-05-31 10:53:56 +00:00
$ c = "SMA Sunny Portal Graphics - Forecast Differential" ;
$ color2 = "FF5C82" ; # zweite Farbe als rot setzen
2019-05-30 07:44:31 +00:00
} else {
return "Invalid portal graphic devicetype ! Use one of \"Generation\", \"Consumption\", \"Generation_Consumption\", \"Differential\". "
}
2019-06-03 23:00:53 +00:00
$ ret = CommandDefine ( $ hash - > { CL } , "$htmldev SMAPortalSPG {FHEM::SMAPortal::PortalAsHtml ('$name','$htmldev')}" ) ;
2019-04-29 22:07:15 +00:00
return $ ret if ( $ ret ) ;
2019-05-31 10:53:56 +00:00
CommandAttr ( $ hash - > { CL } , "$htmldev alias $c" ) ; # Alias setzen
2019-05-30 07:44:31 +00:00
$ c = "This device provides a praphical output of SMA Sunny Portal values.\n" .
"The device needs to set attribute \"detailLevel\" in device \"$name\" to level \"4\"" ;
CommandAttr ( $ hash - > { CL } , "$htmldev comment $c" ) ;
# es muß nicht unbedingt jedes der möglichen userattr unbedingt vorbesetzt werden
# bzw muß überhaupt hier etwas vorbesetzt werden ?
# alle Werte enstprechen eh den AttrVal/ AttrNum default Werten
2019-06-03 23:00:53 +00:00
CommandAttr ( $ hash - > { CL } , "$htmldev hourCount 24" ) ;
CommandAttr ( $ hash - > { CL } , "$htmldev suggestIcon on" ) ;
CommandAttr ( $ hash - > { CL } , "$htmldev showHeader 1" ) ;
CommandAttr ( $ hash - > { CL } , "$htmldev showLink 1" ) ;
CommandAttr ( $ hash - > { CL } , "$htmldev spaceSize 24" ) ;
CommandAttr ( $ hash - > { CL } , "$htmldev showWeather 1" ) ;
CommandAttr ( $ hash - > { CL } , "$htmldev layoutType $type" ) ; # Anzeigetyp setzen
2019-05-30 07:44:31 +00:00
# eine mögliche Startfarbe steht beim installiertem f18 Style direkt zur Verfügung
# ohne vorhanden f18 Style bestimmt später tr.odd aus der Style css die Anfangsfarbe
my $ color = % { json2nameValue ( AttrVal ( 'WEB' , 'styleData' , undef ) ) } { 'f18_cols.header' } ;
if ( defined ( $ color ) ) {
2019-06-03 23:00:53 +00:00
CommandAttr ( $ hash - > { CL } , "$htmldev beamColor $color" ) ;
2019-05-30 07:44:31 +00:00
}
# zweite Farbe setzen
2019-06-03 23:00:53 +00:00
CommandAttr ( $ hash - > { CL } , "$htmldev beamColor2 $color2" ) ;
2019-05-30 07:44:31 +00:00
my $ room = AttrVal ( $ name , "room" , "SMAPortal" ) ;
2019-04-29 22:07:15 +00:00
CommandAttr ( $ hash - > { CL } , "$htmldev room $room" ) ;
CommandAttr ( $ hash - > { CL } , "$name detailLevel 4" ) ;
2019-05-30 07:44:31 +00:00
2019-04-29 22:07:15 +00:00
return "SMA Portal Graphics device \"$htmldev\" created and assigned to room \"$room\"." ;
2019-03-03 20:08:04 +00:00
} else {
return "$setlist" ;
}
return ;
}
2019-03-21 21:39:30 +00:00
###############################################################
# SMAPortal DbLog_splitFn
###############################################################
sub DbLog_split ($$) {
my ( $ event , $ device ) = @ _ ;
my $ devhash = $ defs { $ device } ;
my ( $ reading , $ value , $ unit ) ;
if ( $ event =~ m/L2_PlantPeakPower/ ) {
$ event =~ /^L2_PlantPeakPower:\s(.*)\s(.*)/ ;
$ reading = "L2_PlantPeakPower" ;
$ value = $ 1 ;
$ unit = $ 2 ;
}
if ( $ event =~ m/L1_.*_Temperature/ ) {
$ event =~ /^L1_(.*)_Temperature:\s(.*)\s(.*)/ ;
$ reading = "L1_$1_Temperature" ;
$ value = $ 2 ;
$ unit = $ 3 ;
}
2019-03-22 14:46:36 +00:00
if ( $ event =~ m/summary/ ) {
$ event =~ /summary:\s(.*)\s(.*)/ ;
$ reading = "summary" ;
$ value = $ 1 ;
$ unit = $ 2 ;
}
2019-03-21 21:39:30 +00:00
return ( $ reading , $ value , $ unit ) ;
}
2019-03-03 20:08:04 +00:00
######################################################################################
# Username / Paßwort speichern
######################################################################################
2019-03-16 06:48:59 +00:00
sub setcredentials ($@) {
2019-03-03 20:08:04 +00:00
my ( $ hash , @ credentials ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my ( $ success , $ credstr , $ index , $ retcode ) ;
my ( @ key , $ len , $ i ) ;
$ credstr = encode_base64 ( join ( ':' , @ credentials ) ) ;
# Beginn Scramble-Routine
@ key = qw( 1 3 4 5 6 3 2 1 9 ) ;
$ len = scalar @ key ;
$ i = 0 ;
$ credstr = join "" ,
map { $ i = ( $ i + 1 ) % $ len ;
chr ( ( ord ( $ _ ) + $ key [ $ i ] ) % 256 ) } split // , $ credstr ;
# End Scramble-Routine
$ index = $ hash - > { TYPE } . "_" . $ hash - > { NAME } . "_credentials" ;
$ retcode = setKeyValue ( $ index , $ credstr ) ;
if ( $ retcode ) {
Log3 ( $ name , 1 , "$name - Error while saving the Credentials - $retcode" ) ;
$ success = 0 ;
} else {
2019-03-16 06:48:59 +00:00
getcredentials ( $ hash , 1 ) ; # Credentials nach Speicherung lesen und in RAM laden ($boot=1)
2019-03-03 20:08:04 +00:00
$ success = 1 ;
}
return ( $ success ) ;
}
######################################################################################
# Username / Paßwort abrufen
######################################################################################
2019-03-16 06:48:59 +00:00
sub getcredentials ($$) {
2019-03-03 20:08:04 +00:00
my ( $ hash , $ boot ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my ( $ success , $ username , $ passwd , $ index , $ retcode , $ credstr ) ;
my ( @ key , $ len , $ i ) ;
if ( $ boot ) {
# mit $boot=1 Credentials von Platte lesen und als scrambled-String in RAM legen
$ index = $ hash - > { TYPE } . "_" . $ hash - > { NAME } . "_credentials" ;
( $ retcode , $ credstr ) = getKeyValue ( $ index ) ;
if ( $ retcode ) {
Log3 ( $ name , 2 , "$name - Unable to read password from file: $retcode" ) ;
$ success = 0 ;
}
if ( $ credstr ) {
# beim Boot scrambled Credentials in den RAM laden
$ hash - > { HELPER } { CREDENTIALS } = $ credstr ;
# "Credentials" wird als Statusbit ausgewertet. Wenn nicht gesetzt -> Warnmeldung und keine weitere Verarbeitung
$ hash - > { CREDENTIALS } = "Set" ;
$ success = 1 ;
}
} else {
# boot = 0 -> Credentials aus RAM lesen, decoden und zurückgeben
$ credstr = $ hash - > { HELPER } { CREDENTIALS } ;
if ( $ credstr ) {
# Beginn Descramble-Routine
@ key = qw( 1 3 4 5 6 3 2 1 9 ) ;
$ len = scalar @ key ;
$ i = 0 ;
$ credstr = join "" ,
map { $ i = ( $ i + 1 ) % $ len ;
chr ( ( ord ( $ _ ) - $ key [ $ i ] + 256 ) % 256 ) }
split // , $ credstr ;
# Ende Descramble-Routine
( $ username , $ passwd ) = split ( ":" , decode_base64 ( $ credstr ) ) ;
my $ logpw = AttrVal ( $ name , "showPassInLog" , "0" ) == 1 ? $ passwd : "********" ;
Log3 ( $ name , 3 , "$name - Credentials read from RAM: $username $logpw" ) ;
} else {
Log3 ( $ name , 1 , "$name - Credentials not set in RAM !" ) ;
}
2019-05-30 07:44:31 +00:00
2019-03-03 20:08:04 +00:00
$ success = ( defined ( $ passwd ) ) ? 1 : 0 ;
}
return ( $ success , $ username , $ passwd ) ;
}
###############################################################
# SMAPortal Get
###############################################################
2019-03-16 06:48:59 +00:00
sub Get ($$) {
2019-03-03 20:08:04 +00:00
my ( $ hash , @ a ) = @ _ ;
return "\"get X\" needs at least an argument" if ( @ a < 2 ) ;
my $ name = shift @ a ;
my $ opt = shift @ a ;
my $ getlist = "Unknown argument $opt, choose one of " .
"storedCredentials:noArg " .
"data:noArg " ;
return "module is disabled" if ( IsDisabled ( $ name ) ) ;
if ( $ opt eq "data" ) {
2019-03-16 06:48:59 +00:00
CallInfo ( $ hash ) ;
2019-03-03 20:08:04 +00:00
} elsif ( $ opt eq "storedCredentials" ) {
if ( ! $ hash - > { CREDENTIALS } ) { return "Credentials of $name are not set - make sure you've set it with \"set $name credentials <username> <password>\"" ; }
# Credentials abrufen
2019-03-16 06:48:59 +00:00
my ( $ success , $ username , $ password ) = getcredentials ( $ hash , 0 ) ;
2019-03-03 20:08:04 +00:00
unless ( $ success ) { return "Credentials couldn't be retrieved successfully - see logfile" } ;
return "Stored Credentials to access SMA Portal:\n" .
"========================================\n" .
"Username: $username, Password: $password\n" .
"\n" ;
} else {
return "$getlist" ;
}
2019-05-30 07:44:31 +00:00
2019-03-03 20:08:04 +00:00
return undef ;
}
###############################################################
# SMAPortal Attr
###############################################################
2019-03-16 06:48:59 +00:00
sub Attr ($$$$) {
2019-03-03 20:08:04 +00:00
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" ) ;
if ( $ do ) {
2019-03-16 06:48:59 +00:00
delread ( $ hash ) ;
2019-03-03 20:08:04 +00:00
delete $ hash - > { MODE } ;
RemoveInternalTimer ( $ hash ) ;
2019-03-16 06:48:59 +00:00
delcookiefile ( $ hash , 1 ) ;
2019-03-03 20:08:04 +00:00
} else {
2019-03-16 06:48:59 +00:00
InternalTimer ( gettimeofday ( ) + 1.0 , "FHEM::SMAPortal::CallInfo" , $ hash , 0 ) ;
InternalTimer ( gettimeofday ( ) + 5.0 , "FHEM::SMAPortal::delcookiefile" , $ hash , 0 ) ;
2019-03-03 20:08:04 +00:00
}
readingsBeginUpdate ( $ hash ) ;
readingsBulkUpdate ( $ hash , "state" , $ val ) ;
readingsEndUpdate ( $ hash , 1 ) ;
2019-06-03 23:00:53 +00:00
InternalTimer ( gettimeofday ( ) + 2.0 , "FHEM::SMAPortal::SPGRefresh" , "$name,0,1" , 0 ) ;
2019-03-03 20:08:04 +00:00
}
if ( $ cmd eq "set" ) {
if ( $ aName =~ m/timeout|interval/ ) {
unless ( $ aVal =~ /^\d+$/ ) { return " The Value for $aName is not valid. Use only figures 0-9 !" ; }
}
if ( $ aName =~ m/interval/ ) {
2019-05-30 07:44:31 +00:00
$ _ [ 3 ] = 120 if ( $ aVal > 0 && $ aVal < 120 ) ;
2019-03-16 06:48:59 +00:00
InternalTimer ( gettimeofday ( ) + 1.0 , "FHEM::SMAPortal::CallInfo" , $ hash , 0 ) ;
2019-03-03 20:08:04 +00:00
}
}
return undef ;
}
################################################################
## Hauptschleife BlockingCall
################################################################
2019-03-16 06:48:59 +00:00
sub CallInfo ($) {
2019-03-03 20:08:04 +00:00
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ timeout = AttrVal ( $ name , "timeout" , 30 ) ;
my $ interval = AttrVal ( $ name , "interval" , 300 ) ;
my $ new ;
2019-03-16 06:48:59 +00:00
RemoveInternalTimer ( $ hash , "FHEM::SMAPortal::CallInfo" ) ;
2019-03-03 20:08:04 +00:00
if ( $ init_done == 1 ) {
if ( ! $ hash - > { CREDENTIALS } ) {
Log3 ( $ name , 1 , "$name - Credentials not set. Set it with \"set $name credentials <username> <password>\"" ) ;
2019-03-09 08:32:50 +00:00
readingsSingleUpdate ( $ hash , "state" , "Credentials not set" , 1 ) ;
2019-03-03 20:08:04 +00:00
return ;
}
if ( ! $ interval ) {
$ hash - > { MODE } = "Manual" ;
} else {
$ new = gettimeofday ( ) + $ interval ;
2019-03-16 06:48:59 +00:00
InternalTimer ( $ new , "FHEM::SMAPortal::CallInfo" , $ hash , 0 ) ;
2019-03-03 20:08:04 +00:00
$ hash - > { MODE } = "Automatic - next polltime: " . FmtTime ( $ new ) ;
}
return if ( IsDisabled ( $ name ) ) ;
if ( $ hash - > { HELPER } { RUNNING_PID } ) {
BlockingKill ( $ hash - > { HELPER } { RUNNING_PID } ) ;
delete ( $ hash - > { HELPER } { RUNNING_PID } ) ;
}
2019-03-09 08:32:50 +00:00
$ hash - > { HELPER } { RETRIES } = AttrVal ( $ name , "getDataRetries" , 3 ) ;
2019-03-16 06:48:59 +00:00
$ hash - > { HELPER } { RUNNING_PID } = BlockingCall ( "FHEM::SMAPortal::GetData" , $ name , "FHEM::SMAPortal::ParseData" , $ timeout , "FHEM::SMAPortal::ParseAborted" , $ hash ) ;
2019-03-03 20:08:04 +00:00
$ hash - > { HELPER } { RUNNING_PID } { loglevel } = 5 if ( $ hash - > { HELPER } { RUNNING_PID } ) ; # Forum #77057
} else {
2019-03-16 06:48:59 +00:00
InternalTimer ( gettimeofday ( ) + 5 , "FHEM::SMAPortal::CallInfo" , $ hash , 0 ) ;
2019-03-03 20:08:04 +00:00
}
return ;
}
################################################################
## Datenabruf SMA-Portal
################################################################
2019-03-16 06:48:59 +00:00
sub GetData ($) {
2019-03-03 20:08:04 +00:00
my ( $ name ) = @ _ ;
my $ hash = $ defs { $ name } ;
my ( $ livedata_content ) ;
my $ login_state = 0 ;
2019-03-09 08:32:50 +00:00
my ( $ forecast_content , $ weatherdata_content ) = ( "" , "" ) ;
2019-03-03 20:08:04 +00:00
my $ useragent = AttrVal ( $ name , "userAgent" , "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)" ) ;
my $ cookieLocation = AttrVal ( $ name , "cookieLocation" , "./log/mycookies.txt" ) ;
2019-03-16 06:48:59 +00:00
Log3 $ name , 5 , "$name - Start BlockingCall GetData with CookieLocation: $cookieLocation and UserAgent: $useragent" ;
2019-03-03 20:08:04 +00:00
my $ ua = LWP::UserAgent - > new ;
# Define user agent type
$ ua - > agent ( "$useragent" ) ;
# Cookies
$ ua - > cookie_jar ( HTTP::Cookies - > new ( file = > "$cookieLocation" ,
ignore_discard = > 1 ,
autosave = > 1
)
) ;
# Sunny Home Manager Seite abfragen
my $ livedata = $ ua - > get ( 'https://www.sunnyportal.com/homemanager' ) ;
if ( ( $ livedata - > content =~ m/FeedIn/i ) && ( $ livedata - > content !~ m/expired/i ) ) {
Log3 $ name , 4 , "$name - Login to SMA-Portal succesful" ;
# JSON Live Daten
$ livedata_content = $ livedata - > content ;
$ login_state = 1 ;
Log3 $ name , 4 , "$name - Getting live data now" ;
Log3 $ name , 5 , "$name - Data received:\n" . Dumper decode_json ( $ livedata_content ) ;
2019-03-09 08:32:50 +00:00
# JSON Wetterdaten
Log3 $ name , 4 , "$name - Getting weather data now" ;
my $ weatherdata = $ ua - > get ( 'https://www.sunnyportal.com/Dashboard/Weather' ) ;
$ weatherdata_content = $ weatherdata - > content ;
Log3 $ name , 5 , "$name - Data received:\n" . Dumper decode_json ( $ weatherdata_content ) ;
2019-03-03 20:08:04 +00:00
# JSON Forecast Daten
my $ dl = AttrVal ( $ name , "detailLevel" , 1 ) ;
if ( $ dl > 1 ) {
2019-03-24 15:51:33 +00:00
Log3 $ name , 4 , "$name - Getting forecast data now" ;
2019-03-03 20:08:04 +00:00
my $ forecast_page = $ ua - > get ( 'https://www.sunnyportal.com/HoMan/Forecast/LoadRecommendationData' ) ;
Log3 $ name , 5 , "$name - Return Code: " . $ forecast_page - > code ;
if ( $ forecast_page - > content =~ m/ForecastChartDataPoint/i ) {
$ forecast_content = $ forecast_page - > content ;
2019-03-21 21:39:30 +00:00
Log3 $ name , 5 , "$name - Forecast Data received:\n" . Dumper decode_json ( $ forecast_content ) ;
2019-03-03 20:08:04 +00:00
}
}
} else {
my $ usernameField = "ctl00\$ContentPlaceHolder1\$Logincontrol1\$txtUserName" ;
my $ passwordField = "ctl00\$ContentPlaceHolder1\$Logincontrol1\$txtPassword" ;
my $ loginField = "__EVENTTARGET" ;
my $ loginButton = "ctl00\$ContentPlaceHolder1\$Logincontrol1\$LoginBtn" ;
Log3 $ name , 3 , "$name - not logged in. Try again ..." ;
# Credentials abrufen
2019-03-16 06:48:59 +00:00
my ( $ success , $ username , $ password ) = getcredentials ( $ hash , 0 ) ;
2019-03-03 20:08:04 +00:00
unless ( $ success ) {
Log3 ( $ name , 1 , "$name - Credentials couldn't be retrieved successfully - make sure you've set it with \"set $name credentials <username> <password>\"" ) ;
$ login_state = 0 ;
} else {
my $ loginp = $ ua - > post ( 'https://www.sunnyportal.com/Templates/Start.aspx' , [ $ usernameField = > $ username , $ passwordField = > $ password , "__EVENTTARGET" = > $ loginButton ] ) ;
Log3 $ name , 4 , "$name -> " . $ loginp - > code ;
Log3 $ name , 5 , "$name -> Login-Page return: " . $ loginp - > content ;
if ( $ loginp - > content =~ /Logincontrol1_ErrorLabel/i ) {
Log3 $ name , 1 , "$name - Error: login to SMA-Portal failed" ;
$ livedata_content = "{\"Login-Status\":\"failed\"}" ;
} else {
2019-03-09 08:32:50 +00:00
Log3 $ name , 3 , "$name - login to SMA-Portal successful ... " ;
2019-03-03 20:08:04 +00:00
$ livedata_content = '{"Login-Status":"successful", "InfoMessages":["login to SMA-Portal successful but get data with next data cycle."]}' ;
$ login_state = 1 ;
}
my $ shmp = $ ua - > get ( 'https://www.sunnyportal.com/FixedPages/HoManLive.aspx' ) ;
Log3 $ name , 5 , "$name -> " . $ shmp - > code ;
}
}
2019-03-16 06:48:59 +00:00
my ( $ reread , $ retry ) = analivedat ( $ hash , $ livedata_content ) ;
2019-03-09 08:32:50 +00:00
2019-03-03 20:08:04 +00:00
# Daten müssen als Einzeiler zurückgegeben werden
2019-03-09 18:58:54 +00:00
$ livedata_content = encode_base64 ( $ livedata_content , "" ) ;
$ forecast_content = encode_base64 ( $ forecast_content , "" ) if ( $ forecast_content ) ;
$ weatherdata_content = encode_base64 ( $ weatherdata_content , "" ) if ( $ weatherdata_content ) ;
2019-03-03 20:08:04 +00:00
2019-03-09 18:58:54 +00:00
return "$name|$livedata_content|$forecast_content|$weatherdata_content|$login_state|$reread|$retry" ;
2019-03-03 20:08:04 +00:00
}
################################################################
## Verarbeitung empfangene Daten, setzen Readings
################################################################
2019-03-16 06:48:59 +00:00
sub ParseData ($) {
2019-03-03 20:08:04 +00:00
my ( $ string ) = @ _ ;
my @ a = split ( "\\|" , $ string ) ;
2019-03-09 18:58:54 +00:00
my $ hash = $ defs { $ a [ 0 ] } ;
my $ name = $ hash - > { NAME } ;
my $ ld_response = decode_base64 ( $ a [ 1 ] ) ;
my $ fd_response = decode_base64 ( $ a [ 2 ] ) if ( $ a [ 2 ] ) ;
my $ wd_response = decode_base64 ( $ a [ 3 ] ) if ( $ a [ 3 ] ) ;
my $ login_state = $ a [ 4 ] ;
my $ reread = $ a [ 5 ] ;
my $ retry = $ a [ 6 ] ;
my $ livedata_content = decode_json ( $ ld_response ) ;
my $ forecast_content = decode_json ( $ fd_response ) if ( $ fd_response ) ;
my $ weatherdata_content = decode_json ( $ wd_response ) if ( $ wd_response ) ;
2019-03-03 20:08:04 +00:00
2019-03-10 07:34:29 +00:00
my $ state = "ok" ;
2019-03-09 08:32:50 +00:00
my $ timeout = AttrVal ( $ name , "timeout" , 30 ) ;
if ( $ reread ) {
# login war erfolgreich, aber Daten müssen jetzt noch gelesen werden
delete ( $ hash - > { HELPER } { RUNNING_PID } ) ;
readingsSingleUpdate ( $ hash , "L1_Login-Status" , "successful" , 1 ) ;
2019-05-30 07:44:31 +00:00
$ hash - > { HELPER } { oldlogintime } = gettimeofday ( ) ;
$ hash - > { HELPER } { RUNNING_PID } = BlockingCall ( "FHEM::SMAPortal::GetData" , $ name , "FHEM::SMAPortal::ParseData" , $ timeout , "FHEM::SMAPortal::ParseAborted" , $ hash ) ;
2019-03-09 08:32:50 +00:00
$ hash - > { HELPER } { RUNNING_PID } { loglevel } = 5 if ( $ hash - > { HELPER } { RUNNING_PID } ) ; # Forum #77057
return ;
}
if ( $ retry && $ hash - > { HELPER } { RETRIES } ) {
# Livedaten konnte nicht gelesen werden, neuer Versuch zeitverzögert
delete ( $ hash - > { HELPER } { RUNNING_PID } ) ;
$ hash - > { HELPER } { RETRIES } -= 1 ;
2019-03-16 06:48:59 +00:00
InternalTimer ( gettimeofday ( ) + 5 , "FHEM::SMAPortal::retrygetdata" , $ hash , 0 ) ;
2019-03-09 08:32:50 +00:00
return ;
}
2019-03-03 20:08:04 +00:00
my $ dl = AttrVal ( $ name , "detailLevel" , 1 ) ;
2019-03-16 06:48:59 +00:00
delread ( $ hash , $ dl + 1 ) ;
2019-03-03 20:08:04 +00:00
readingsBeginUpdate ( $ hash ) ;
2019-03-09 19:40:21 +00:00
my ( $ FeedIn_done , $ GridConsumption_done , $ PV_done , $ AutarkyQuote_done , $ SelfConsumption_done ) = ( 0 , 0 , 0 , 0 , 0 ) ;
2019-04-07 06:45:06 +00:00
my ( $ SelfConsumptionQuote_done , $ SelfSupply_done , $ errMsg , $ warnMsg , $ infoMsg ) = ( 0 , 0 , 0 , 0 , 0 ) ;
2019-04-22 18:01:58 +00:00
my ( $ batteryin , $ batteryout ) ;
2019-03-03 20:08:04 +00:00
for my $ k ( keys %$ livedata_content ) {
2019-03-09 11:11:53 +00:00
my $ new_val = "" ;
2019-03-03 20:08:04 +00:00
if ( defined $ livedata_content - > { $ k } ) {
if ( ( $ livedata_content - > { $ k } =~ m/ARRAY/i ) || ( $ livedata_content - > { $ k } =~ m/HASH/i ) ) {
2019-03-24 15:51:33 +00:00
Log3 $ name , 4 , "$name - Livedata content \"$k\": " . ( $ livedata_content - > { $ k } ) ;
2019-03-03 20:08:04 +00:00
if ( $ livedata_content - > { $ k } =~ m/ARRAY/i ) {
2019-03-24 20:55:35 +00:00
my $ hd0 = $ livedata_content - > { $ k } [ 0 ] ;
2019-04-22 18:01:58 +00:00
if ( ! defined $ hd0 ) {
2019-03-03 20:08:04 +00:00
next ;
}
chomp $ hd0 ;
$ hd0 =~ s/[;']//g ;
2019-03-24 20:55:35 +00:00
$ hd0 = encode ( "utf8" , $ hd0 ) ;
2019-03-24 15:51:33 +00:00
Log3 $ name , 4 , "$name - Livedata \"$k\": $hd0" ;
2019-03-03 20:08:04 +00:00
$ new_val = $ hd0 ;
}
} else {
$ new_val = $ livedata_content - > { $ k } ;
}
if ( $ new_val && $ k !~ /__type/i ) {
2019-03-09 08:32:50 +00:00
Log3 $ name , 4 , "$name -> $k - $new_val" ;
2019-03-03 20:08:04 +00:00
readingsBulkUpdate ( $ hash , "L1_$k" , $ new_val ) ;
2019-03-26 17:40:00 +00:00
$ FeedIn_done = 1 if ( $ k =~ /^FeedIn$/ ) ;
$ GridConsumption_done = 1 if ( $ k =~ /^GridConsumption$/ ) ;
$ PV_done = 1 if ( $ k =~ /^PV$/ ) ;
$ AutarkyQuote_done = 1 if ( $ k =~ /^AutarkyQuote$/ ) ;
$ SelfConsumption_done = 1 if ( $ k =~ /^SelfConsumption$/ ) ;
2019-03-09 19:40:21 +00:00
$ SelfConsumptionQuote_done = 1 if ( $ k =~ /^SelfConsumptionQuote$/ ) ;
2019-03-26 17:40:00 +00:00
$ SelfSupply_done = 1 if ( $ k =~ /^SelfSupply$/ ) ;
$ errMsg = 1 if ( $ k =~ /^ErrorMessages$/ ) ;
$ warnMsg = 1 if ( $ k =~ /^WarningMessages$/ ) ;
2019-04-07 06:45:06 +00:00
$ infoMsg = 1 if ( $ k =~ /^InfoMessages$/ ) ;
2019-04-22 18:01:58 +00:00
$ batteryin = 1 if ( $ k =~ /^BatteryIn$/ ) ;
$ batteryout = 1 if ( $ k =~ /^BatteryOut$/ ) ;
2019-03-03 20:08:04 +00:00
}
}
}
2019-03-09 11:11:53 +00:00
readingsBulkUpdate ( $ hash , "L1_FeedIn" , 0 ) if ( ! $ FeedIn_done ) ;
readingsBulkUpdate ( $ hash , "L1_GridConsumption" , 0 ) if ( ! $ GridConsumption_done ) ;
readingsBulkUpdate ( $ hash , "L1_PV" , 0 ) if ( ! $ PV_done ) ;
2019-03-09 19:40:21 +00:00
readingsBulkUpdate ( $ hash , "L1_AutarkyQuote" , 0 ) if ( ! $ AutarkyQuote_done ) ;
readingsBulkUpdate ( $ hash , "L1_SelfConsumption" , 0 ) if ( ! $ SelfConsumption_done ) ;
readingsBulkUpdate ( $ hash , "L1_SelfConsumptionQuote" , 0 ) if ( ! $ SelfConsumptionQuote_done ) ;
readingsBulkUpdate ( $ hash , "L1_SelfSupply" , 0 ) if ( ! $ SelfSupply_done ) ;
2019-04-22 18:01:58 +00:00
if ( defined $ batteryin || defined $ batteryout ) {
readingsBulkUpdate ( $ hash , "L1_BatteryIn" , 0 ) if ( ! $ batteryin ) ;
readingsBulkUpdate ( $ hash , "L1_BatteryOut" , 0 ) if ( ! $ batteryout ) ;
}
2019-03-03 20:08:04 +00:00
readingsEndUpdate ( $ hash , 1 ) ;
2019-03-26 17:40:00 +00:00
readingsDelete ( $ hash , "L1_ErrorMessages" ) if ( ! $ errMsg ) ;
readingsDelete ( $ hash , "L1_WarningMessages" ) if ( ! $ warnMsg ) ;
2019-04-07 06:45:06 +00:00
readingsDelete ( $ hash , "L1_InfoMessages" ) if ( ! $ infoMsg ) ;
2019-03-26 17:40:00 +00:00
2019-03-03 20:08:04 +00:00
if ( $ forecast_content && $ forecast_content !~ m/undefined/i ) {
# Auswertung der Forecast Daten
2019-03-16 06:48:59 +00:00
extractForecastData ( $ hash , $ forecast_content ) ;
2019-03-21 21:39:30 +00:00
extractPlantData ( $ hash , $ forecast_content ) ;
2019-03-24 15:51:33 +00:00
extractConsumerData ( $ hash , $ forecast_content ) ;
2019-03-03 20:08:04 +00:00
}
2019-03-09 18:58:54 +00:00
if ( $ weatherdata_content && $ weatherdata_content !~ m/undefined/i ) {
# Auswertung Wetterdaten
2019-03-16 06:48:59 +00:00
extractWeatherData ( $ hash , $ weatherdata_content ) ;
2019-03-09 18:58:54 +00:00
}
2019-03-03 20:08:04 +00:00
my $ pv = ReadingsVal ( $ name , "L1_PV" , 0 ) ;
my $ fi = ReadingsVal ( $ name , "L1_FeedIn" , 0 ) ;
my $ gc = ReadingsVal ( $ name , "L1_GridConsumption" , 0 ) ;
2019-03-10 07:34:29 +00:00
my $ sum = $ fi - $ gc ;
if ( ! $ hash - > { HELPER } { RETRIES } && ! $ pv && ! $ fi && ! $ gc ) {
# keine Anlagendaten vorhanden
2019-03-21 21:39:30 +00:00
$ state = "Data can't be retrieved from SMA-Portal. Reread at next scheduled cycle." ;
2019-03-10 07:34:29 +00:00
Log3 $ name , 2 , "$name - $state" ;
}
2019-03-03 20:08:04 +00:00
readingsBeginUpdate ( $ hash ) ;
if ( $ login_state ) {
readingsBulkUpdate ( $ hash , "state" , $ state ) ;
2019-03-22 14:46:36 +00:00
readingsBulkUpdate ( $ hash , "summary" , "$sum W" ) ;
2019-03-03 20:08:04 +00:00
}
readingsEndUpdate ( $ hash , 1 ) ;
delete ( $ hash - > { HELPER } { RUNNING_PID } ) ;
2019-06-03 23:00:53 +00:00
SPGRefresh ( $ hash , 0 , 1 ) ;
return ;
2019-03-03 20:08:04 +00:00
}
################################################################
## Timeout BlockingCall
################################################################
2019-03-16 06:48:59 +00:00
sub ParseAborted ($) {
2019-03-03 20:08:04 +00:00
my ( $ hash , $ cause ) = @ _ ;
my $ name = $ hash - > { NAME } ;
$ cause = $ cause ? $ cause: "Timeout: process terminated" ;
Log3 ( $ name , 1 , "$name -> BlockingCall $hash->{HELPER}{RUNNING_PID}{fn} pid:$hash->{HELPER}{RUNNING_PID}{pid} $cause" ) ;
delete ( $ hash - > { HELPER } { RUNNING_PID } ) ;
2019-06-03 23:00:53 +00:00
return ;
2019-03-03 20:08:04 +00:00
}
################################################################
## regelmäßig Cookie-Datei löschen
################################################################
2019-03-16 06:48:59 +00:00
sub delcookiefile ($;$) {
2019-03-03 20:08:04 +00:00
my ( $ hash , $ must ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my ( $ validperiod , $ cookieLocation , $ oldlogintime , $ delfile ) ;
2019-03-16 06:48:59 +00:00
RemoveInternalTimer ( $ hash , "FHEM::SMAPortal::delcookiefile" ) ;
2019-03-03 20:08:04 +00:00
# Gültigkeitsdauer Cookie in Sekunden
$ validperiod = AttrVal ( $ name , "cookielifetime" , 3000 ) ;
$ cookieLocation = AttrVal ( $ name , "cookieLocation" , "./log/mycookies.txt" ) ;
if ( $ must ) {
# Cookie Zwangslöschung
$ delfile = unlink ( $ cookieLocation ) ;
}
$ oldlogintime = $ hash - > { HELPER } { oldlogintime } ? $ hash - > { HELPER } { oldlogintime } : 0 ;
if ( $ init_done == 1 ) {
# Abfrage ob gettimeofday() größer ist als gettimeofday()+$validperiod
if ( gettimeofday ( ) > $ oldlogintime + $ validperiod ) {
$ delfile = unlink ( $ cookieLocation ) ;
}
}
if ( $ delfile ) {
Log3 $ name , 3 , "$name - cookie file deleted: $cookieLocation" ;
}
return if ( IsDisabled ( $ name ) ) ;
2019-03-16 06:48:59 +00:00
InternalTimer ( gettimeofday ( ) + 30 , "FHEM::SMAPortal::delcookiefile" , $ hash , 0 ) ;
2019-03-03 20:08:04 +00:00
return ;
}
################################################################
## Auswertung Forecast Daten
################################################################
2019-03-16 06:48:59 +00:00
sub extractForecastData ($$) {
2019-03-03 20:08:04 +00:00
my ( $ hash , $ forecast ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ dl = AttrVal ( $ name , "detailLevel" , 1 ) ;
if ( $ dl <= 1 ) {
return ;
}
my ( $ sec , $ min , $ hour , $ mday , $ mon , $ year , $ wday , $ yday , $ isdst ) = localtime ( time ) ;
$ year += 1900 ;
$ mon += 1 ;
my $ today = "$year-" . sprintf ( "%02d" , $ mon ) . "-" . sprintf ( "%02d" , $ mday ) . "T" ;
my $ PV_sum = 0 ;
my $ consum_sum = 0 ;
my $ sum = 0 ;
readingsBeginUpdate ( $ hash ) ;
my $ plantOid = $ forecast - > { 'ForecastTimeframes' } - > { 'PlantOid' } ;
# Counter for forecast objects
my $ obj_nr = 0 ;
# The next few hours...
my % nextFewHoursSum = ( "PV" = > 0 , "Consumption" = > 0 , "Total" = > 0 , "ConsumpRcmd" = > 0 ) ;
# Rest of the day...
my % restOfDaySum = ( "PV" = > 0 , "Consumption" = > 0 , "Total" = > 0 , "ConsumpRcmd" = > 0 ) ;
# Tomorrow...
my % tomorrowSum = ( "PV" = > 0 , "Consumption" = > 0 , "Total" = > 0 , "ConsumpRcmd" = > 0 ) ;
# Get the current day (for 2016-02-26, this is 26)
my $ current_day = ( localtime ) [ 3 ] ;
# Loop through all forecast objects
2019-03-24 15:51:33 +00:00
# Energie wird als "J" geliefert, Wh = J / 3600
2019-03-03 20:08:04 +00:00
foreach my $ fc_obj ( @ { $ forecast - > { 'ForecastSeries' } } ) {
2019-05-30 07:44:31 +00:00
my $ fc_datetime = $ fc_obj - > { 'TimeStamp' } - > { 'DateTime' } ; # Example for DateTime: 2016-02-15T23:00:00
my $ tkind = $ fc_obj - > { 'TimeStamp' } - > { 'Kind' } ; # Zeitart: Unspecified, Utc
2019-03-03 20:08:04 +00:00
# Calculate Unix timestamp (month begins at 0, year at 1900)
my ( $ fc_year , $ fc_month , $ fc_day , $ fc_hour ) = $ fc_datetime =~ /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):00:00$/ ;
my $ fc_uts = POSIX:: mktime ( 0 , 0 , $ fc_hour , $ fc_day , $ fc_month - 1 , $ fc_year - 1900 ) ;
my $ fc_diff_seconds = $ fc_uts - time + 3600 ; # So we go above 0 for the current hour
my $ fc_diff_hours = int ( $ fc_diff_seconds / 3600 ) ;
2019-05-30 07:44:31 +00:00
# Use also old data to integrate daily PV and Consumption
if ( $ current_day == $ fc_day ) {
$ PV_sum += int ( $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } ) ; # integrator of daily PV
$ consum_sum += int ( $ fc_obj - > { 'ConsumptionForecast' } - > { 'Amount' } / 3600 ) ; # integrator of daily Consumption forecast
}
2019-03-03 20:08:04 +00:00
# Don't use old data
next if $ fc_diff_seconds < 0 ;
# Sum up for the next few hours (4 hours total, this is current hour plus the next 3 hours)
if ( $ obj_nr < 4 ) {
$ nextFewHoursSum { 'PV' } += $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } ;
$ nextFewHoursSum { 'Consumption' } += $ fc_obj - > { 'ConsumptionForecast' } - > { 'Amount' } / 3600 ;
$ nextFewHoursSum { 'Total' } += $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } - $ fc_obj - > { 'ConsumptionForecast' } - > { 'Amount' } / 3600 ;
$ nextFewHoursSum { 'ConsumpRcmd' } += $ fc_obj - > { 'IsConsumptionRecommended' } ? 1 : 0 ;
}
# If data is for the rest of the current day
if ( $ current_day == $ fc_day ) {
$ restOfDaySum { 'PV' } += $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } ;
$ restOfDaySum { 'Consumption' } += $ fc_obj - > { 'ConsumptionForecast' } - > { 'Amount' } / 3600 ;
$ restOfDaySum { 'Total' } += $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } - $ fc_obj - > { 'ConsumptionForecast' } - > { 'Amount' } / 3600 ;
$ restOfDaySum { 'ConsumpRcmd' } += $ fc_obj - > { 'IsConsumptionRecommended' } ? 1 : 0 ;
2019-05-30 07:44:31 +00:00
}
2019-03-03 20:08:04 +00:00
# If data is for the next day (quick and dirty: current day different from this object's day)
# Assuming only the current day and the next day are returned from Sunny Portal
if ( $ current_day != $ fc_day ) {
$ tomorrowSum { 'PV' } += $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } if ( exists ( $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } ) ) ;
$ tomorrowSum { 'Consumption' } += $ fc_obj - > { 'ConsumptionForecast' } - > { 'Amount' } / 3600 ;
$ tomorrowSum { 'Total' } += $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } - $ fc_obj - > { 'ConsumptionForecast' } - > { 'Amount' } / 3600 if ( $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } ) ;
$ tomorrowSum { 'ConsumpRcmd' } += $ fc_obj - > { 'IsConsumptionRecommended' } ? 1 : 0 ;
}
# Update values in Fhem if less than 24 hours in the future
2019-05-30 07:44:31 +00:00
# TimeStamp Kind: "Unspecified"
2019-03-22 14:46:36 +00:00
if ( $ dl >= 2 ) {
2019-03-03 20:08:04 +00:00
if ( $ obj_nr < 24 ) {
my $ time_str = "ThisHour" ;
$ time_str = "NextHour" . sprintf ( "%02d" , $ obj_nr ) if ( $ fc_diff_hours > 0 ) ;
2019-03-22 14:46:36 +00:00
if ( $ time_str =~ /NextHour/ && $ dl >= 4 ) {
2019-05-30 07:44:31 +00:00
readingsBulkUpdate ( $ hash , "L4_${time_str}_Time" , TimeAdjust ( $ hash , $ fc_obj - > { 'TimeStamp' } - > { 'DateTime' } , $ tkind ) ) ;
2019-03-22 14:46:36 +00:00
readingsBulkUpdate ( $ hash , "L4_${time_str}_PvMeanPower" , int ( $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } ) ) ;
readingsBulkUpdate ( $ hash , "L4_${time_str}_Consumption" , int ( $ fc_obj - > { 'ConsumptionForecast' } - > { 'Amount' } / 3600 ) ) ;
readingsBulkUpdate ( $ hash , "L4_${time_str}_IsConsumptionRecommended" , ( $ fc_obj - > { 'IsConsumptionRecommended' } ? "yes" : "no" ) ) ;
2019-05-30 07:44:31 +00:00
# Rundungsfehler möglich -> nicht int((x-y)/3600) besser ist: int(x/3600) - int(y/3600)
# readingsBulkUpdate( $hash, "L4_${time_str}", int( $fc_obj->{'PvMeanPower'}->{'Amount'} - $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 ) );
readingsBulkUpdate ( $ hash , "L4_${time_str}" , int ( ( $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } /3600) - ($fc_obj->{'ConsumptionForecast'}->{'Amount'}/ 3600 ) ) ) ;
# add WeatherId Helper to show weather icon
$ hash - > { HELPER } { "L4_" . $ { time_str } . "_WeatherId" } = int ( $ fc_obj - > { 'WeatherId' } ) ;
2019-03-22 14:46:36 +00:00
}
if ( $ time_str =~ /ThisHour/ && $ dl >= 2 ) {
2019-05-30 07:44:31 +00:00
readingsBulkUpdate ( $ hash , "L2_${time_str}_Time" , TimeAdjust ( $ hash , $ fc_obj - > { 'TimeStamp' } - > { 'DateTime' } , $ tkind ) ) ;
2019-03-22 14:46:36 +00:00
readingsBulkUpdate ( $ hash , "L2_${time_str}_PvMeanPower" , int ( $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } ) ) ;
readingsBulkUpdate ( $ hash , "L2_${time_str}_Consumption" , int ( $ fc_obj - > { 'ConsumptionForecast' } - > { 'Amount' } / 3600 ) ) ;
readingsBulkUpdate ( $ hash , "L2_${time_str}_IsConsumptionRecommended" , ( $ fc_obj - > { 'IsConsumptionRecommended' } ? "yes" : "no" ) ) ;
2019-05-30 07:44:31 +00:00
# readingsBulkUpdate( $hash, "L2_${time_str}", int( $fc_obj->{'PvMeanPower'}->{'Amount'} - $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 ) );
readingsBulkUpdate ( $ hash , "L2_${time_str}" , int ( ( $ fc_obj - > { 'PvMeanPower' } - > { 'Amount' } /3600) - ($fc_obj->{'ConsumptionForecast'}->{'Amount'}/ 3600 ) ) ) ;
# add WeatherId Helper to show weather icon
$ hash - > { HELPER } { "L2_" . $ { time_str } . "_WeatherId" } = int ( $ fc_obj - > { 'WeatherId' } ) ;
2019-03-22 14:46:36 +00:00
}
2019-03-03 20:08:04 +00:00
}
}
# Increment object counter
$ obj_nr + + ;
}
if ( $ dl >= 2 ) {
readingsBulkUpdate ( $ hash , "L2_Next04Hours-Consumption" , int ( $ nextFewHoursSum { 'Consumption' } ) ) ;
readingsBulkUpdate ( $ hash , "L2_Next04Hours-PV" , int ( $ nextFewHoursSum { 'PV' } ) ) ;
readingsBulkUpdate ( $ hash , "L2_Next04Hours-Total" , int ( $ nextFewHoursSum { 'Total' } ) ) ;
readingsBulkUpdate ( $ hash , "L2_Next04Hours-IsConsumptionRecommended" , int ( $ nextFewHoursSum { 'ConsumpRcmd' } ) ) ;
readingsBulkUpdate ( $ hash , "next04hours_state" , int ( $ nextFewHoursSum { 'PV' } ) ) ;
2019-05-01 14:30:09 +00:00
readingsBulkUpdate ( $ hash , "L2_Forecast-Today-Consumption" , $ consum_sum ) ; # publish consumption forecast values
readingsBulkUpdate ( $ hash , "L2_Forecast-Today-PV" , $ PV_sum ) ; # publish integrated PV
2019-03-03 20:08:04 +00:00
}
if ( $ dl >= 3 ) {
readingsBulkUpdate ( $ hash , "L3_RestOfDay-Consumption" , int ( $ restOfDaySum { 'Consumption' } ) ) ;
readingsBulkUpdate ( $ hash , "L3_RestOfDay-PV" , int ( $ restOfDaySum { 'PV' } ) ) ;
readingsBulkUpdate ( $ hash , "L3_RestOfDay-Total" , int ( $ restOfDaySum { 'Total' } ) ) ;
readingsBulkUpdate ( $ hash , "L3_RestOfDay-IsConsumptionRecommended" , int ( $ restOfDaySum { 'ConsumpRcmd' } ) ) ;
readingsBulkUpdate ( $ hash , "L3_Tomorrow-Consumption" , int ( $ tomorrowSum { 'Consumption' } ) ) ;
readingsBulkUpdate ( $ hash , "L3_Tomorrow-PV" , int ( $ tomorrowSum { 'PV' } ) ) ;
readingsBulkUpdate ( $ hash , "L3_Tomorrow-Total" , int ( $ tomorrowSum { 'Total' } ) ) ;
readingsBulkUpdate ( $ hash , "L3_Tomorrow-IsConsumptionRecommended" , int ( $ tomorrowSum { 'ConsumpRcmd' } ) ) ;
}
if ( $ dl >= 4 ) {
readingsBulkUpdate ( $ hash , "L4_plantOid" , $ plantOid ) ;
}
readingsEndUpdate ( $ hash , 1 ) ;
return ;
}
2019-03-09 18:58:54 +00:00
################################################################
## Auswertung Wetterdaten
################################################################
2019-03-16 06:48:59 +00:00
sub extractWeatherData ($$) {
2019-03-09 18:58:54 +00:00
my ( $ hash , $ weather ) = @ _ ;
my $ name = $ hash - > { NAME } ;
2019-03-21 21:39:30 +00:00
my ( $ tsymbol , $ ttoday , $ ttomorrow ) ;
2019-03-09 18:58:54 +00:00
my $ dl = AttrVal ( $ name , "detailLevel" , 1 ) ;
readingsBeginUpdate ( $ hash ) ;
for my $ k ( keys %$ weather ) {
my $ new_val = "" ;
if ( defined $ weather - > { $ k } ) {
Log3 $ name , 4 , "$name - Weatherdata content \"$k\": " . ( $ weather - > { $ k } ) ;
if ( $ weather - > { $ k } =~ m/HASH/i ) {
my $ ih = $ weather - > { $ k } ;
for my $ i ( keys %$ ih ) {
2019-03-24 20:55:35 +00:00
my $ hd0 = $ weather - > { $ k } { $ i } ;
2019-03-09 18:58:54 +00:00
if ( ! $ hd0 ) {
next ;
}
chomp $ hd0 ;
$ hd0 =~ s/[;']//g ;
$ hd0 = ( $ hd0 =~ /^undef$/ ) ? "none" : $ hd0 ;
2019-03-24 20:55:35 +00:00
$ hd0 = encode ( "utf8" , $ hd0 ) ;
2019-03-24 15:51:33 +00:00
Log3 $ name , 4 , "$name - Weatherdata \"$k $i\": $hd0" ;
2019-03-09 18:58:54 +00:00
next if ( $ i =~ /^WeatherIcon$/ ) ;
$ new_val = $ hd0 ;
2019-03-24 20:55:35 +00:00
2019-03-09 18:58:54 +00:00
if ( $ new_val ) {
2019-03-21 21:39:30 +00:00
if ( $ i =~ /^TemperatureSymbol$/ ) {
$ tsymbol = $ new_val ;
next ;
}
if ( $ i =~ /^Temperature$/ ) {
if ( $ k =~ /^today$/ ) {
$ ttoday = sprintf ( "%.1f" , $ new_val ) ;
}
if ( $ k =~ /^tomorrow$/ ) {
$ ttomorrow = sprintf ( "%.1f" , $ new_val ) ;
}
next ;
}
2019-03-09 18:58:54 +00:00
Log3 $ name , 4 , "$name -> ${k}_${i} - $new_val" ;
readingsBulkUpdate ( $ hash , "L1_${k}_${i}" , $ new_val ) ;
}
}
}
}
}
2019-03-21 21:39:30 +00:00
readingsBulkUpdate ( $ hash , "L1_today_Temperature" , "$ttoday $tsymbol" ) if ( $ ttoday && $ tsymbol ) ;
readingsBulkUpdate ( $ hash , "L1_tomorrow_Temperature" , "$ttomorrow $tsymbol" ) if ( $ ttomorrow && $ tsymbol ) ;
2019-03-09 18:58:54 +00:00
readingsEndUpdate ( $ hash , 1 ) ;
return ;
}
2019-03-03 20:08:04 +00:00
################################################################
2019-03-21 21:39:30 +00:00
## Auswertung Anlagendaten
################################################################
sub extractPlantData ($$) {
my ( $ hash , $ forecast ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my ( $ amount , $ unit ) ;
my $ dl = AttrVal ( $ name , "detailLevel" , 1 ) ;
if ( $ dl <= 1 ) {
return ;
}
readingsBeginUpdate ( $ hash ) ;
my $ ppp = $ forecast - > { 'PlantPeakPower' } ;
if ( $ ppp && $ dl >= 2 ) {
$ amount = $ forecast - > { 'PlantPeakPower' } { 'Amount' } ;
$ unit = $ forecast - > { 'PlantPeakPower' } { 'StandardUnit' } { 'Symbol' } ;
2019-03-24 15:51:33 +00:00
Log3 $ name , 4 , "$name - Plantdata \"PlantPeakPower Amount\": $amount" ;
Log3 $ name , 4 , "$name - Plantdata \"PlantPeakPower Symbol\": $unit" ;
2019-03-21 21:39:30 +00:00
}
readingsBulkUpdate ( $ hash , "L2_PlantPeakPower" , "$amount $unit" ) ;
readingsEndUpdate ( $ hash , 1 ) ;
return ;
}
2019-03-24 15:51:33 +00:00
################################################################
## Auswertung Consumer Data
################################################################
sub extractConsumerData ($$) {
my ( $ hash , $ forecast ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my % consumers ;
my ( $ key , $ val ) ;
my $ dl = AttrVal ( $ name , "detailLevel" , 1 ) ;
if ( $ dl <= 1 ) {
return ;
}
readingsBeginUpdate ( $ hash ) ;
# Schleife über alle Consumer Objekte
my $ i = 0 ;
foreach my $ c ( @ { $ forecast - > { 'Consumers' } } ) {
$ consumers { "${i}_ConsumerName" } = encode ( "utf8" , $ c - > { 'ConsumerName' } ) ;
$ consumers { "${i}_ConsumerOid" } = $ c - > { 'ConsumerOid' } ;
$ i + + ;
}
if ( % consumers && $ forecast - > { 'ForecastTimeframes' } ) {
# es sind Vorhersagen zu geplanten Verbraucherschaltzeiten vorhanden
2019-05-30 07:44:31 +00:00
# TimeFrameStart/End Kind: "Utc"
2019-03-24 15:51:33 +00:00
foreach my $ c ( @ { $ forecast - > { 'ForecastTimeframes' } { 'PlannedTimeFrames' } } ) {
2019-05-30 07:44:31 +00:00
my $ tkind = $ c - > { 'TimeFrameStart' } - > { 'Kind' } ; # Zeitart: Unspecified, Utc
2019-03-24 15:51:33 +00:00
my $ deviceOid = $ c - > { 'DeviceOid' } ;
2019-05-30 07:44:31 +00:00
my $ timeFrameStart = TimeAdjust ( $ hash , $ c - > { 'TimeFrameStart' } { 'DateTime' } , $ tkind ) ; # wandele UTC
my $ timeFrameEnd = TimeAdjust ( $ hash , $ c - > { 'TimeFrameEnd' } { 'DateTime' } , $ tkind ) ; # wandele UTC
2019-03-24 15:51:33 +00:00
my $ tz = $ c - > { 'TimeFrameStart' } { 'Kind' } ;
foreach my $ k ( keys ( % consumers ) ) {
$ val = $ consumers { $ k } ;
if ( $ val eq $ deviceOid ) {
$ k =~ /^(\d+)_.*$/ ;
my $ lfn = $ 1 ;
# $consumer = $consumers{"${lfn}_ConsumerName"};
$ consumers { "${lfn}_PlannedOpTimeStart" } = $ timeFrameStart ;
$ consumers { "${lfn}_PlannedOpTimeEnd" } = $ timeFrameEnd ;
}
}
}
}
if ( % consumers ) {
foreach my $ key ( keys ( % consumers ) ) {
Log3 $ name , 4 , "$name - Consumer data \"$key\": " . $ consumers { $ key } ;
if ( $ key =~ /ConsumerName/ && $ dl >= 3 ) {
$ key =~ /^(\d+)_.*$/ ;
my $ lfn = $ 1 ;
my $ cn = $ consumers { "${lfn}_ConsumerName" } ; # Verbrauchername
2019-03-25 22:41:54 +00:00
$ cn = substUmlauts ( $ cn ) ; # evtl. Umlaute im Verbrauchernamen ersetzen
2019-03-24 15:51:33 +00:00
my $ pos = $ consumers { "${lfn}_PlannedOpTimeStart" } ; # geplanter Start
my $ poe = $ consumers { "${lfn}_PlannedOpTimeEnd" } ; # geplantes Ende
my $ rb = "L3_${cn}_PlannedOpTimeBegin" ;
my $ re = "L3_${cn}_PlannedOpTimeEnd" ;
my $ rp = "L3_${cn}_Planned" ;
if ( $ pos ) {
readingsBulkUpdate ( $ hash , $ rb , $ pos ) ;
readingsBulkUpdate ( $ hash , $ rp , "yes" ) ;
} else {
readingsBulkUpdate ( $ hash , $ rb , "undefined" ) ;
readingsBulkUpdate ( $ hash , $ rp , "no" ) ;
}
if ( $ poe ) {
readingsBulkUpdate ( $ hash , $ re , $ poe ) ;
} else {
readingsBulkUpdate ( $ hash , $ re , "undefined" ) ;
}
}
}
}
readingsEndUpdate ( $ hash , 1 ) ;
return ;
}
2019-03-21 21:39:30 +00:00
################################################################
2019-03-03 20:08:04 +00:00
# sortiert eine Liste von Versionsnummern x.x.x
# Schwartzian Transform and the GRT transform
# Übergabe: "asc | desc",<Liste von Versionsnummern>
################################################################
2019-03-16 06:48:59 +00:00
sub sortVersionNum (@) {
2019-03-03 20:08:04 +00:00
my ( $ sseq , @ versions ) = @ _ ;
my @ sorted = map { $ _ - > [ 0 ] }
sort { $ a - > [ 1 ] cmp $ b - > [ 1 ] }
map { [ $ _ , pack "C*" , split /\./ ] } @ versions ;
@ sorted = map { join "." , unpack "C*" , $ _ }
sort
map { pack "C*" , split /\./ } @ versions ;
if ( $ sseq eq "desc" ) {
@ sorted = reverse @ sorted ;
}
return @ sorted ;
}
2019-03-16 06:48:59 +00:00
################################################################
# Versionierungen des Moduls setzen
# Die Verwendung von Meta.pm und Packages wird berücksichtigt
################################################################
sub setVersionInfo ($) {
2019-03-18 23:51:42 +00:00
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
2019-03-19 18:11:34 +00:00
my $ v = ( sortTopicNum ( "desc" , keys % vNotesIntern ) ) [ 0 ] ;
my $ type = $ hash - > { TYPE } ;
2019-03-18 23:51:42 +00:00
$ hash - > { HELPER } { PACKAGE } = __PACKAGE__ ;
2019-03-19 18:11:34 +00:00
$ hash - > { HELPER } { VERSION } = $ v ;
2019-03-25 19:19:05 +00:00
if ( $ modules { $ type } { META } { x_prereqs_src } && ! $ hash - > { HELPER } { MODMETAABSENT } ) {
2019-03-16 06:48:59 +00:00
# META-Daten sind vorhanden
2019-03-19 18:11:34 +00:00
$ modules { $ type } { META } { version } = "v" . $ v ; # Version aus META.json überschreiben, Anzeige mit {Dumper $modules{SMAPortal}{META}}
2019-03-16 06:48:59 +00:00
if ( $ modules { $ type } { META } { x_version } ) { # {x_version} ( nur gesetzt wenn $Id: ... $ im Kopf komplett! vorhanden )
2019-03-19 18:11:34 +00:00
$ modules { $ type } { META } { x_version } =~ s/1.1.1/$v/g ;
2019-03-16 06:48:59 +00:00
} else {
2019-03-19 18:11:34 +00:00
$ modules { $ type } { META } { x_version } = $ v ;
2019-03-16 06:48:59 +00:00
}
return $@ unless ( FHEM::Meta:: SetInternals ( $ hash ) ) ; # FVERSION wird gesetzt ( nur gesetzt wenn $Id: ... $ im Kopf komplett! vorhanden )
2019-03-25 19:19:05 +00:00
if ( __PACKAGE__ eq "FHEM::$type" || __PACKAGE__ eq $ type ) {
2019-03-16 06:48:59 +00:00
# es wird mit Packages gearbeitet -> Perl übliche Modulversion setzen
2019-03-19 18:11:34 +00:00
# mit {<Modul>->VERSION()} im FHEMWEB kann Modulversion abgefragt werden
2019-03-16 06:48:59 +00:00
use version 0.77 ; our $ VERSION = FHEM::Meta:: Get ( $ hash , 'version' ) ;
}
} else {
# herkömmliche Modulstruktur
2019-03-19 18:11:34 +00:00
$ hash - > { VERSION } = $ v ;
2019-03-16 06:48:59 +00:00
}
return ;
}
2019-03-03 20:08:04 +00:00
################################################################
# delete Readings
# $dl = detailLevel ab dem das Reading gelöscht werden soll
################################################################
2019-03-16 06:48:59 +00:00
sub delread ($;$) {
2019-03-03 20:08:04 +00:00
my ( $ hash , $ dl ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my @ allrds = keys % { $ defs { $ name } { READINGS } } ;
if ( $ dl ) {
# Readings ab dem angegebenen Detail-Level löschen
foreach my $ key ( @ allrds ) {
$ key =~ m/^L(\d)_.*$/ ;
if ( $ 1 && $ 1 >= $ dl ) {
delete ( $ defs { $ name } { READINGS } { $ key } ) ;
}
}
return ;
}
foreach my $ key ( @ allrds ) {
delete ( $ defs { $ name } { READINGS } { $ key } ) if ( $ key ne "state" ) ;
}
return ;
}
2019-03-09 08:32:50 +00:00
################################################################
# analysiere Livedaten
################################################################
2019-03-16 06:48:59 +00:00
sub analivedat ($$) {
2019-03-09 08:32:50 +00:00
my ( $ hash , $ lc ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my ( $ reread , $ retry ) = ( 0 , 0 ) ;
my $ livedata_content = decode_json ( $ lc ) ;
for my $ k ( keys %$ livedata_content ) {
my $ new_val = "" ;
if ( defined $ livedata_content - > { $ k } ) {
if ( ( $ livedata_content - > { $ k } =~ m/ARRAY/i ) || ( $ livedata_content - > { $ k } =~ m/HASH/i ) ) {
if ( $ livedata_content - > { $ k } =~ m/ARRAY/i ) {
my $ hd0 = Dumper ( $ livedata_content - > { $ k } [ 0 ] ) ;
if ( ! $ hd0 ) {
next ;
}
chomp $ hd0 ;
$ hd0 =~ s/[;']//g ;
$ hd0 = ( $ hd0 =~ /^undef$/ ) ? "none" : $ hd0 ;
2019-03-24 15:51:33 +00:00
# Log3 $name, 4, "$name - livedata ARRAY content \"$k\": $hd0";
2019-03-09 08:32:50 +00:00
$ new_val = $ hd0 ;
}
} else {
$ new_val = $ livedata_content - > { $ k } ;
}
if ( $ new_val && $ k !~ /__type/i ) {
if ( $ k =~ /InfoMessages/ && $ new_val =~ /.*login to SMA-Portal successful.*/ ) {
# Login war erfolgreich, Daten neu lesen
Log3 $ name , 3 , "$name - get data again" ;
$ reread = 1 ;
}
if ( $ k =~ /ErrorMessages/ && $ new_val =~ /.*The current data cannot be retrieved from the PV system. Check the cabling and configuration of the following energy meters.*/ ) {
# Energiedaten konnten nicht ermittelt werden, Daten neu lesen mit Zeitverzögerung
2019-03-09 18:58:54 +00:00
Log3 $ name , 3 , "$name - The current data cannot be retrieved from PV system, get data again." ;
2019-03-09 08:32:50 +00:00
$ retry = 1 ;
}
2019-03-11 22:54:31 +00:00
if ( $ k =~ /ErrorMessages/ && $ new_val =~ /.*Communication with the Sunny Home Manager is currently not possible.*/ ) {
# Energiedaten konnten nicht ermittelt werden, Daten neu lesen mit Zeitverzögerung
Log3 $ name , 3 , "$name - Communication with the Sunny Home Manager currently impossible, get data again." ;
$ retry = 1 ;
}
2019-03-09 08:32:50 +00:00
}
}
}
return ( $ reread , $ retry ) ;
}
################################################################
# Restart get Data
################################################################
2019-03-16 06:48:59 +00:00
sub retrygetdata ($) {
2019-03-09 08:32:50 +00:00
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ timeout = AttrVal ( $ name , "timeout" , 30 ) ;
2019-03-16 06:48:59 +00:00
$ hash - > { HELPER } { RUNNING_PID } = BlockingCall ( "FHEM::SMAPortal::GetData" , $ name , "FHEM::SMAPortal::ParseData" , $ timeout , "FHEM::SMAPortal::ParseAborted" , $ hash ) ;
2019-03-09 08:32:50 +00:00
$ hash - > { HELPER } { RUNNING_PID } { loglevel } = 5 if ( $ hash - > { HELPER } { RUNNING_PID } ) ; # Forum #77057
return ;
}
2019-03-24 15:51:33 +00:00
################################################################
2019-05-30 07:44:31 +00:00
# Timestamp korrigieren
2019-03-24 15:51:33 +00:00
################################################################
2019-05-30 07:44:31 +00:00
sub TimeAdjust ($$$) {
my ( $ hash , $ t , $ tkind ) = @ _ ;
$ t =~ s/T/ / ;
2019-03-24 15:51:33 +00:00
my ( $ datehour , $ rest ) = split ( /:/ , $ t , 2 ) ;
my ( $ year , $ month , $ day , $ hour ) = $ datehour =~ /(\d+)-(\d\d)-(\d\d)\s+(\d\d)/ ;
2019-05-30 07:44:31 +00:00
# Time::timegm - a UTC version of mktime()
2019-03-24 15:51:33 +00:00
# proto: $time = timegm($sec,$min,$hour,$mday,$mon,$year);
2019-05-30 07:44:31 +00:00
my $ epoch = timegm ( 0 , 0 , $ hour , $ day , $ month - 1 , $ year ) ;
2019-03-24 15:51:33 +00:00
# proto: ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
2019-05-01 14:30:09 +00:00
my ( undef , undef , undef , undef , undef , undef , undef , undef , $ isdst ) = localtime ( time ) ;
2019-05-30 07:44:31 +00:00
if ( lc ( $ tkind ) =~ /unspecified/ ) {
if ( $ isdst ) {
$ epoch = $ epoch - 7200 ;
} else {
$ epoch = $ epoch - 3600 ;
}
2019-05-01 14:30:09 +00:00
}
# proto: ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
2019-05-30 07:44:31 +00:00
my ( $ lyear , $ lmonth , $ lday , $ lhour ) = ( localtime ( $ epoch ) ) [ 5 , 4 , 3 , 2 ] ;
2019-03-24 15:51:33 +00:00
$ lyear += 1900 ; # year is 1900 based
$ lmonth + + ; # month number is zero based
2019-05-01 14:30:09 +00:00
2019-03-24 15:51:33 +00:00
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) {
return ( sprintf ( "%02d.%02d.%04d %02d:%s" , $ lday , $ lmonth , $ lyear , $ lhour , $ rest ) ) ;
} else {
return ( sprintf ( "%04d-%02d-%02d %02d:%s" , $ lyear , $ lmonth , $ lday , $ lhour , $ rest ) ) ;
}
}
2019-03-25 22:41:54 +00:00
###############################################################################
# Umlaute für Readingerstellung ersetzen
###############################################################################
sub substUmlauts ($) {
my ( $ txt ) = @ _ ;
$ txt =~ s/ß/ss/g ;
$ txt =~ s/ä/ae/g ;
$ txt =~ s/ö/oe/g ;
$ txt =~ s/ü/ue/g ;
$ txt =~ s/Ä/Ae/g ;
$ txt =~ s/Ö/Oe/g ;
$ txt =~ s/Ü/Ue/g ;
return ( $ txt ) ;
}
2019-04-29 22:07:15 +00:00
###############################################################################
2019-05-30 07:44:31 +00:00
# Subroutine für Portalgrafik
2019-04-29 22:07:15 +00:00
###############################################################################
2019-05-30 07:44:31 +00:00
sub PortalAsHtml ($$) {
my ( $ name , $ wlname ) = @ _ ;
my $ hash = $ defs { $ name } ;
2019-06-03 23:00:53 +00:00
my $ ret = "" ;
2019-05-01 14:30:09 +00:00
2019-06-03 23:00:53 +00:00
my ( $ i , $ icon , $ colorv , $ colorc , $ maxhours , $ hourstyle , $ header , $ legend , $ legend_txt , $ legend_style ) ;
2019-05-30 07:44:31 +00:00
my ( $ val , $ height , $ fsize , $ html_start , $ html_end , $ wlalias , $ weather , $ colorw , $ maxVal , $ show_night , $ type , $ kw ) ;
my ( $ maxDif , $ minDif , $ maxCon , $ v , $ z2 , $ z3 , $ z4 , $ show_diff , $ width , $ w ) ;
my $ he ; # Balkenhöhe
my ( % pv , % is , % t , % we , % di , % co ) ;
my @ pgCDev ;
2019-06-03 23:00:53 +00:00
# Kontext des aufrufenden SMAPortalSPG-Devices speichern für Refresh
$ hash - > { HELPER } { SPGDEV } = $ wlname ; # 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 $ dl = AttrVal ( $ name , "detailLevel" , 1 ) ;
my $ pv0 = ReadingsNum ( $ name , "L2_ThisHour_PvMeanPower" , undef ) ;
my $ pv1 = ReadingsNum ( $ name , "L4_NextHour01_PvMeanPower" , undef ) ;
if ( ! $ hash || ! defined ( $ defs { $ wlname } ) || $ dl != 4 || ! defined $ pv0 || ! defined $ pv1 ) {
$ height = AttrNum ( $ wlname , 'beamHeight' , 200 ) ;
$ ret . = "<table class='roomoverview'>" ;
$ ret . = "<tr style='height:" . $ height . "px'>" ;
$ ret . = "<td>" ;
if ( ! $ hash ) {
$ ret . = "Device \"$name\" doesn't exist !" ;
} elsif ( ! defined ( $ defs { $ wlname } ) ) {
$ ret . = "Graphic device \"$wlname\" doesn't exist !" ;
} elsif ( $ dl != 4 ) {
$ ret . = "The attribute \"detailLevel\" of device \"$name\" has to be set to level \"4\" !" ;
} elsif ( ! defined $ pv0 ) {
# Vorschlag : Das und level 4 data weiter unten stehen so verloren auf der Seite
# man könnte die beiden Meldungen noch in eine mini <table class=roomoverview> packen
# und mit dem Wert aus attr beamHeight in etwa auf die spätere Ausǵabe vorformatieren
$ ret . = "Awaiting level 2 data ..." ;
} elsif ( ! defined $ pv1 ) {
$ ret . = "Awaiting level 4 data ..." ;
}
$ ret . = "</td>" ;
$ ret . = "</tr>" ;
$ ret . = "</table>" ;
return $ ret ;
}
2019-05-30 07:44:31 +00:00
2019-06-03 23:00:53 +00:00
@ pgCDev = split ( ',' , AttrVal ( $ wlname , "consumerList" , "" ) ) ; # definierte Verbraucher ermitteln
( $ legend_style , $ legend ) = split ( '_' , AttrVal ( $ wlname , 'consumerLegend' , 'icon_top' ) ) ;
2019-05-30 07:44:31 +00:00
$ legend = '' if ( ( $ legend_style eq 'none' ) || ( ! int ( @ pgCDev ) ) ) ;
if ( $ legend ) {
foreach ( @ pgCDev ) {
my ( $ txt , $ im ) = split ( ':' , $ _ ) ;
if ( $ legend_style eq 'icon' ) { # mögliche Umbruchstellen mit normalen Blanks vorsehen !
$ legend_txt . = $ txt . ' ' . FW_makeImage ( $ im ) . ' ' ;
} else {
my ( undef , $ co ) = split ( '\@' , $ im ) ;
2019-06-03 23:00:53 +00:00
$ co = '#cccccc' if ( ! $ co ) ; # Farbe per default
2019-05-30 07:44:31 +00:00
$ legend_txt . = '<font color=\'' . $ co . '\'>' . $ txt . '</font> ' ; # hier auch Umbruch erlauben
}
}
}
# Parameter f. Anzeige extrahieren
2019-06-03 23:00:53 +00:00
$ maxhours = AttrNum ( $ wlname , 'hourCount' , 24 ) ;
$ hourstyle = AttrVal ( $ wlname , 'hourStyle' , undef ) ;
$ colorv = AttrVal ( $ wlname , 'beamColor' , undef ) ;
$ colorc = AttrVal ( $ wlname , 'beamColor2' , '000000' ) ; # schwarz wenn keine Userauswahl;
$ icon = AttrVal ( $ wlname , 'suggestIcon' , undef ) ;
$ html_start = AttrVal ( $ wlname , 'htmlStart' , undef ) ; # beliebige HTML Strings die vor der Grafik ausgegeben werden
$ html_end = AttrVal ( $ wlname , 'htmlEnd' , undef ) ; # beliebige HTML Strings die nach der Grafik ausgegeben werden
$ type = AttrVal ( $ wlname , 'layoutType' , 'pv' ) ;
$ kw = AttrVal ( $ wlname , 'W/kW' , 'W' ) ;
$ height = AttrNum ( $ wlname , 'beamHeight' , 200 ) ;
$ width = AttrNum ( $ wlname , 'beamWidth' , 6 ) ; # zu klein ist nicht problematisch
$ w = $ width * $ maxhours ; # gesammte Breite der Ausgabe , WetterIcon braucht ca. 34px
$ fsize = AttrNum ( $ wlname , 'spaceSize' , 24 ) ;
$ maxVal = AttrNum ( $ wlname , 'maxPV' , 0 ) ; # dyn. Anpassung der Balkenhöhe oder statisch ?
$ show_night = AttrNum ( $ wlname , 'showNight' , 0 ) ; # alle Balken (Spalten) anzeigen ?
$ show_diff = AttrVal ( $ wlname , 'showDiff' , 'no' ) ; # zusätzliche Anzeige $di{} in allen Typen
$ weather = AttrNum ( $ wlname , 'showWeather' , 1 ) ;
$ colorw = AttrVal ( $ wlname , 'weatherColor' , undef ) ;
$ wlalias = AttrVal ( $ wlname , 'alias' , $ wlname ) ;
$ header = ( AttrNum ( $ wlname , 'showHeader' , 1 ) ) ? 1 : undef ;
2019-05-30 07:44:31 +00:00
2019-05-01 20:38:02 +00:00
# Icon Erstellung, mit @<Farbe> ergänzen falls einfärben
# Beispiel mit Farbe: $icon = FW_makeImage('light_light_dim_100.svg@green');
2019-05-30 07:44:31 +00:00
$ icon = FW_makeImage ( $ icon ) if ( defined ( $ icon ) ) ;
my $ co4h = ReadingsNum ( $ name , "L2_Next04Hours-Consumption" , 0 ) ;
my $ coRe = ReadingsNum ( $ name , "L3_RestOfDay-Consumption" , 0 ) ;
my $ coTo = ReadingsNum ( $ name , "L3_Tomorrow-Consumption" , 0 ) ;
my $ pv4h = ReadingsNum ( $ name , "L2_Next04Hours-PV" , 0 ) ;
my $ pvRe = ReadingsNum ( $ name , "L3_RestOfDay-PV" , 0 ) ;
my $ pvTo = ReadingsNum ( $ name , "L3_Tomorrow-PV" , 0 ) ;
if ( $ kw eq 'kW' ) {
$ co4h = sprintf ( "%.1f" , $ co4h / 1000 ) . " kW" ;
$ coRe = sprintf ( "%.1f" , $ coRe / 1000 ) . " kW" ;
$ coTo = sprintf ( "%.1f" , $ coTo / 1000 ) . " kW" ;
$ pv4h = sprintf ( "%.1f" , $ pv4h / 1000 ) . " kW" ;
$ pvRe = sprintf ( "%.1f" , $ pvRe / 1000 ) . " kW" ;
$ pvTo = sprintf ( "%.1f" , $ pvTo / 1000 ) . " kW" ;
} else {
$ co4h . = " W" ;
$ coRe . = " W" ;
$ coTo . = " W" ;
$ pv4h . = " W" ;
$ pvRe . = " W" ;
$ pvTo . = " W" ;
}
2019-05-01 14:30:09 +00:00
# Headerzeile generieren
2019-05-30 07:44:31 +00:00
my $ alias = AttrVal ( $ name , "alias" , "SMA Sunny Portal" ) ; # Linktext als Aliasname oder "SMA Sunny Portal"
2019-05-01 14:30:09 +00:00
my $ dlink = "<a href=\"/fhem?detail=$name\">$alias</a>" ;
2019-06-03 23:00:53 +00:00
my $ lup = ReadingsTimestamp ( $ name , "state" , "0000-00-00 00:00:00" ) ; # letzte Updatezeit
2019-05-30 07:44:31 +00:00
# Da der Header relativ viele Zeichen hat, müssen Stellen erlaubt werden an denen automatisch umgebrochen werden kann.
# Sonst sind schmale Ausgaben nicht von den Balken bzw. deren Anzahl abhängig, sondern allein durch die Breite des Headers bestimmt
if ( $ header ) {
my ( $ h1 , $ h2 ) ;
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) {
2019-06-03 23:00:53 +00:00
$ h1 = "Prognose [pv] - nächste 4 Stunden: $pv4h/h / Rest des Tages: $pvRe/h / Morgen: $pvTo/h" ;
$ h2 = "Prognose [co] - nächste 4 Stunden: $co4h/h / Rest des Tages: $coRe/h / Morgen: $coTo/h" ;
my ( $ year , $ month , $ day , $ hour , $ min , $ sec ) = $ lup =~ /(\d+)-(\d\d)-(\d\d)\s+(.*)/ ;
$ lup = "$3.$2.$1 $4" ;
2019-05-30 07:44:31 +00:00
} else {
2019-06-03 23:00:53 +00:00
$ h1 = "forecast data [pv] - next 4 hours: $pv4h/h / rest of day: $pvRe / tomorrow: $pvTo/h" ;
$ h2 = "forecast data [co] - next 4 hours: $co4h/h / rest of day: $coRe / tomorrow: $coTo/h" ;
2019-05-30 07:44:31 +00:00
}
2019-06-03 23:00:53 +00:00
$ lup = " (last update: $lup)" ;
2019-05-30 07:44:31 +00:00
if ( $ type eq 'pv' ) {
2019-06-03 23:00:53 +00:00
$ header = $ dlink . ' ' . $ lup . ' <br/>' . $ h1 ;
2019-05-30 07:44:31 +00:00
} elsif ( $ type eq 'co' ) {
2019-06-03 23:00:53 +00:00
$ header = $ dlink . ' ' . $ lup . ' <br/>' . $ h2 ;
2019-05-30 07:44:31 +00:00
} else {
2019-06-03 23:00:53 +00:00
$ header = $ dlink . ' ' . $ lup . ' <br/>' . $ h1 . '<br/>' . $ h2 ;
2019-05-30 07:44:31 +00:00
}
2019-05-01 14:30:09 +00:00
}
2019-05-30 07:44:31 +00:00
2019-05-01 14:30:09 +00:00
# Werte aktuelle Stunde
2019-05-30 07:44:31 +00:00
$ pv { 0 } = ReadingsNum ( $ name , "L2_ThisHour_PvMeanPower" , 0 ) ;
$ co { 0 } = ReadingsNum ( $ name , "L2_ThisHour_Consumption" , 0 ) ;
#$di{0} = ReadingsNum($name,"L2_ThisHour", 0); # kann wieder verwendet werden -> Rundungsfehler ist beseitigt
$ di { 0 } = $ pv { 0 } - $ co { 0 } ;
$ is { 0 } = ( ReadingsVal ( $ name , "L2_ThisHour_IsConsumptionRecommended" , 'no' ) eq 'yes' ) ? $ icon : undef ;
$ we { 0 } = $ hash - > { HELPER } { L2_ThisHour_WeatherId } if ( $ weather ) ; # für Wettericons
2019-05-31 06:25:38 +00:00
$ we { 0 } = $ we { 0 } ? $ we { 0 } : 0 ;
2019-05-30 07:44:31 +00:00
2019-05-01 14:30:09 +00:00
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) {
( undef , undef , undef , $ t { 0 } ) = ReadingsVal ( $ name , "L2_ThisHour_Time" , '0' ) =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/ ;
} else {
2019-06-03 23:00:53 +00:00
( undef , undef , undef , $ t { 0 } ) = ReadingsVal ( $ name , "L2_ThisHour_Time" , '0' ) =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/ ;
2019-05-01 14:30:09 +00:00
}
2019-05-30 07:44:31 +00:00
$ t { 0 } = int ( $ t { 0 } ) ; # zum Rechnen Integer ohne führende Null
###########################################################
# get consumer list and display it in portalGraphics
foreach ( @ pgCDev ) {
my ( $ itemName , undef ) = split ( ':' , $ _ ) ;
$ itemName =~ s/^\s+|\s+$//g ; #trim it, if blanks were used
$ _ =~ s/^\s+|\s+$//g ; #trim it, if blanks were used
#check if listed device is planned
if ( ReadingsVal ( $ name , "L3_" . $ 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
2019-05-01 14:30:09 +00:00
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) {
2019-05-30 07:44:31 +00:00
( undef , undef , undef , $ start ) = ReadingsVal ( $ name , "L3_" . $ itemName . "_PlannedOpTimeBegin" , '00.00.0000 24' ) =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/ ;
( undef , undef , undef , $ end ) = ReadingsVal ( $ name , "L3_" . $ itemName . "_PlannedOpTimeEnd" , '00.00.0000 24' ) =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/ ;
2019-05-01 14:30:09 +00:00
} else {
2019-05-30 07:44:31 +00:00
( undef , undef , undef , $ start ) = ReadingsVal ( $ name , "L3_" . $ itemName . "_PlannedOpTimeBegin" , '0000-00-00 24' ) =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/ ;
( undef , undef , undef , $ end ) = ReadingsVal ( $ name , "L3_" . $ itemName . "_PlannedOpTimeEnd" , '0000-00-00 24' ) =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/ ;
}
$ start = int ( $ start ) ;
$ end = int ( $ end ) ;
#correct the hour for accurate display
2019-06-03 23:00:53 +00:00
if ( $ start < $ t { 0 } ) { # consumption seems to be tomorrow
2019-05-30 07:44:31 +00:00
$ start = 23 - $ t { 0 } + $ start ;
} else {
$ start -= $ t { 0 } ;
}
2019-06-03 23:00:53 +00:00
if ( $ end < $ t { 0 } ) { # consumption seems to be tomorrow
2019-05-30 07:44:31 +00:00
$ end = 23 - $ t { 0 } + $ end ;
} else {
$ end -= $ t { 0 } ;
}
$ _ . = ":" . $ start . ":" . $ end ;
} else {
$ _ . = ":24:24" ;
}
}
$ maxVal = ( ! $ maxVal ) ? $ pv { 0 } : $ maxVal ; # Startwert wenn kein Wert bereits via attr vorgegeben ist
$ maxCon = $ co { 0 } ; # für Typ co
2019-06-03 23:00:53 +00:00
$ maxDif = $ di { 0 } ; # für Typ diff
$ minDif = $ di { 0 } ; # für Typ diff
2019-05-30 07:44:31 +00:00
foreach $ i ( 1 .. $ maxhours - 1 ) {
$ pv { $ i } = ReadingsNum ( $ name , "L4_NextHour" . sprintf ( "%02d" , $ i ) . "_PvMeanPower" , 0 ) ; # Erzeugung
$ co { $ i } = ReadingsNum ( $ name , "L4_NextHour" . sprintf ( "%02d" , $ i ) . "_Consumption" , 0 ) ; # Verbrauch
# $di{$i} = ReadingsNum($name,"L4_NextHour".sprintf("%02d",$i),0); # kann wieder verwendet werden -> Rundungsfehler ist beseitigt
$ 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 , "L4_NextHour" . sprintf ( "%02d" , $ i ) . "_IsConsumptionRecommended" , 'no' ) eq 'yes' ) ? $ icon : undef ;
$ we { $ i } = $ hash - > { HELPER } { "L4_NextHour" . sprintf ( "%02d" , $ i ) . "_WeatherId" } if ( $ weather ) ; # für Wettericons
2019-05-31 06:25:38 +00:00
$ we { $ i } = $ we { $ i } ? $ we { $ i } : 0 ;
2019-05-30 07:44:31 +00:00
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) {
( undef , undef , undef , $ t { $ i } ) = ReadingsVal ( $ name , "L4_NextHour" . sprintf ( "%02d" , $ i ) . "_Time" , '0' ) =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/ ;
} else {
( undef , undef , undef , $ t { $ i } ) = ReadingsVal ( $ name , "L4_NextHour" . sprintf ( "%02d" , $ i ) . "_Time" , '0' ) =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/ ;
}
2019-05-31 07:55:21 +00:00
$ t { $ i } = int ( $ t { $ i } ) ; # keine führende 0
2019-05-30 07:44:31 +00:00
}
######################################
# 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
2019-06-03 23:00:53 +00:00
$ ret = "<html>" ;
$ ret . = $ html_start if ( defined ( $ html_start ) ) ;
2019-05-30 07:44:31 +00:00
$ ret . = "<style>TD.smaportal {text-align: center; padding-left:1px; padding-right:1px; margin:0px;}</style>" ;
2019-06-03 23:00:53 +00:00
$ ret . = "<table class='roomoverview' width='$w' style='width:" . $ w . "px'><tr class='devTypeTr'></tr>" ;
2019-05-30 07:44:31 +00:00
$ ret . = "<tr><td class='smaportal'>" ;
2019-06-03 23:00:53 +00:00
$ ret . = "\n<table class='block'>" ; # das \n erleichtert das Lesen der debug Quelltextausgabe
2019-05-30 07:44:31 +00:00
if ( $ header ) { # Header ausgeben
$ ret . = "<tr class='odd'>" ;
# mit einem extra <td></td> ein wenig mehr Platz machen, ergibt i.d.R. weniger als ein Zeichen
$ ret . = "<td colspan='" . ( $ maxhours + 2 ) . "' align='center' style='word-break: normal'>$header</td></tr>" ;
}
if ( $ legend_txt && ( $ legend eq 'top' ) ) {
$ ret . = "<tr class='odd'>" ;
$ ret . = "<td colspan='" . ( $ maxhours + 2 ) . "' align='center' style='word-break: normal'>$legend_txt</td></tr>" ;
}
if ( $ weather ) {
$ ret . = "<tr class='even'><td class='smaportal'></td>" ; # freier Platz am Anfang
foreach $ 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 SMA Portal 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 = '<b>???<b/>' if ( $ val eq $ icon_name ) ; # passendes Icon beim User nicht vorhanden ! ( attr web iconPath falsch/prüfen/update ? )
$ ret . = "<td class='smaportal' width='$width' style='margin:1px; vertical-align:middle align:center; padding-bottom:1px;'>$val</td>" ;
} else { # Kein Ertrag oder show_night = 0
$ ret . = "<td></td>" ; $ we { $ i } = undef ;
}
# mit $we{$i} = undef kann man unten leicht feststellen ob für diese Spalte bereits ein Icon ausgegeben wurde oder nicht
2019-05-01 14:30:09 +00:00
}
2019-05-30 07:44:31 +00:00
$ ret . = "<td class='smaportal'></td></tr>" ; # freier Platz am Ende der Icon Zeile
}
2019-05-01 14:30:09 +00:00
2019-05-30 07:44:31 +00:00
if ( $ show_diff eq 'top' ) { # Zusätzliche Zeile Ertrag - Verbrauch
$ ret . = "<tr class='even'><td class='smaportal'></td>" ; # freier Platz am Anfang
foreach $ i ( 0 .. $ maxhours - 1 ) {
$ val = formatVal6 ( $ di { $ i } , $ kw , $ we { $ i } ) ;
2019-06-03 23:00:53 +00:00
$ val = ( $ di { $ i } < 0 ) ? '<b>' . $ val . '<b/>' : '+' . $ val ; # negativ Zahlen in Fettschrift
2019-05-30 07:44:31 +00:00
$ ret . = "<td class='smaportal' style='vertical-align:middle; text-align:center;'>$val</td>" ;
}
$ ret . = "<td class='smaportal'></td></tr>" ; # freier Platz am Ende
}
2019-05-01 14:30:09 +00:00
2019-05-30 07:44:31 +00:00
$ ret . = "<tr class='even'><td class='smaportal'></td>" ; # Neue Zeile mit freiem Platz am Anfang
2019-05-01 14:30:09 +00:00
2019-05-30 07:44:31 +00:00
foreach $ 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
2019-06-03 23:00:53 +00:00
$ height = 200 if ( ! $ height ) ; # Fallback, sollte eigentlich nicht vorkommen, außer der User setzt es auf 0
2019-05-30 07:44:31 +00:00
$ maxVal = 1 if ( ! $ maxVal ) ;
$ maxCon = 1 if ( ! $ maxCon ) ;
2019-05-01 14:30:09 +00:00
2019-05-30 07:44:31 +00:00
# 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 } ;
2019-05-01 14:30:09 +00:00
}
2019-05-30 07:44:31 +00:00
$ he = int ( ( $ maxVal - $ z2 ) / $ maxVal * $ height ) ;
$ z2 = int ( ( $ z2 - $ z3 ) / $ maxVal * $ height ) ;
2019-05-01 14:30:09 +00:00
2019-05-30 07:44:31 +00:00
$ 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 ;
}
2019-05-01 14:30:09 +00:00
2019-05-30 07:44:31 +00:00
} else { # Typ dif
# 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
2019-06-03 23:00:53 +00:00
if ( $ minDif >= 0 ) { # keine negativen Balken vorhanden, die Positiven bekommen den gesammten Raum
2019-05-30 07:44:31 +00:00
$ 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
2019-06-03 23:00:53 +00:00
} else { # keine positiven Balken vorhanden, die Negativen bekommen den gesammten Raum
2019-05-30 07:44:31 +00:00
$ px_neg = $ height ;
$ px_pos = 0 ;
}
}
}
if ( $ di { $ i } >= 0 ) { # Zone 2 & 3 mit ihren direkten Werten vorbesetzen
$ z2 = $ di { $ i } ;
$ z3 = abs ( $ minDif ) ;
2019-05-01 14:30:09 +00:00
} else {
2019-05-30 07:44:31 +00:00
$ z2 = $ maxDif ;
$ z3 = abs ( $ di { $ i } ) ; # Nur Betrag ohne Vorzeichen
2019-05-01 14:30:09 +00:00
}
2019-05-30 07:44:31 +00:00
# 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 ) ;
2019-05-01 14:30:09 +00:00
2019-05-30 07:44:31 +00:00
$ 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
2019-05-01 14:30:09 +00:00
}
2019-05-30 07:44:31 +00:00
# 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" ;
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 . = "<table width='100%' height='100%'>" ; # mit width=100% etwas bessere Füllung der Balken
$ ret . = "<tr class='even' style='height:" . $ he . "px'>" ;
$ ret . = "<td class='smaportal' style='vertical-align:bottom'>" . $ val . "</td></tr>" ;
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;" ;
2019-06-03 23:00:53 +00:00
$ style . = ( defined ( $ colorv ) ) ? " background-color:#$colorv\"" : '"' ; # Syntaxhilight
2019-05-30 07:44:31 +00:00
$ ret . = "<tr class='odd' style='height:" . $ z3 . "px;'>" ;
$ ret . = "<td align='center' class='smaportal' " . $ style . ">" ;
##################################
# inject the new icon if defined
my $ show = 0 ; # wurde bereits für diese Stunde ein Geräte Icon ausgegeben ?
foreach ( @ pgCDev ) {
if ( $ _ ) {
my ( undef , $ im , $ start , $ end ) = split ( ':' , $ _ ) ;
if ( $ im && ( $ i >= $ start ) && ( $ i <= $ end ) ) {
$ ret . = FW_makeImage ( $ im ) ;
# $show = 1; # nachher dann kein normales Icon mehr anzeigen, oder doch ?
# oder noch ein extra Attr machen zum auswählen ?
# eventuell den Block wieder nach unten schieben und den normalen Birnen
# Vorrang geben
}
}
}
$ ret . = $ is { $ i } if ( defined ( $ is { $ i } ) && ! $ show ) ;
$ ret . = "</td></tr>" ;
}
} elsif ( $ type eq 'pvco' ) {
my ( $ color1 , $ color2 , $ style1 , $ style2 ) ;
$ ret . = "<table width='100%' height='100%'>\n" ; # mit width=100% etwas bessere Füllung der Balken
if ( $ he ) { # der Freiraum oben kann beim größten Balken ganz entfallen
$ ret . = "<tr class='even' style='height:" . $ he . "px'><td class='smaportal'></td></tr>" ;
}
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 = $ colorv ;
$ 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 = $ colorv ;
$ style2 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;" ;
$ style2 . = ( defined ( $ color2 ) ) ? " background-color:#$color2\"" : '"' ;
}
}
$ ret . = "<tr class='odd' style='height:" . $ z2 . "px'>" ;
$ ret . = "<td align='center' class='smaportal' " . $ style1 . ">$val" ;
$ ret . = $ is { $ i } if ( defined $ is { $ i } ) ;
$ 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='smaportal' " . $ style2 . ">$v</td></tr>" ;
}
2019-05-01 14:30:09 +00:00
2019-05-30 07:44:31 +00:00
} else { # Type dif
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 = ( $ di { $ i } >= 0 ) ? formatVal6 ( $ di { $ i } , $ kw , $ we { $ i } ) : '' ;
$ val = ' 0 ' if ( $ di { $ i } == 0 ) ; # Sonderfall , hier wird die 0 gebraucht !
if ( $ val ) {
$ ret . = "<tr class='even' style='height:" . $ he . "px'>" ;
$ ret . = "<td class='smaportal' style='vertical-align:bottom'>" . $ val . "</td></tr>" ;
}
if ( $ di { $ i } >= 0 ) { # mit Farbe 1 colorv füllen
$ style . = ( defined ( $ colorv ) ) ? " background-color:#$colorv\"" : '"' ;
$ z2 = 1 if ( $ di { $ i } == 0 ) ; # Sonderfall , 1px dünnen Strich ausgeben
$ ret . = "<tr class='odd' style='height:" . $ z2 . "px'>" ;
$ ret . = "<td align='center' class='smaportal' " . $ style . ">" ;
$ ret . = $ is { $ i } if ( defined $ is { $ i } ) ;
$ ret . = "</td></tr>" ;
} 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 . = "<tr class='even' style='height:" . $ z2 . "px'>" ;
$ ret . = "<td class='smaportal'></td></tr>" ;
}
}
if ( $ di { $ i } < 0 ) { # Negativ Balken anzeigen ?
$ style . = ( defined ( $ colorc ) ) ? " background-color:#$colorc\"" : '"' ; # mit Farbe 2 colorc füllen
$ ret . = "<tr class='odd' style='height:" . $ z3 . "px'>" ;
$ ret . = "<td align='center' class='smaportal' " . $ style . "></td></tr>" ;
} elsif ( $ z3 ) { # ohne Farbe
$ ret . = "<tr class='even' style='height:" . $ z3 . "px'>" ;
$ ret . = "<td class='smaportal'></td></tr>" ;
}
if ( $ z4 ) { # kann entfallen wenn auch z3 0 ist
$ val = ( $ di { $ i } < 0 ) ? formatVal6 ( $ di { $ i } , $ kw , $ we { $ i } ) : ' ' ;
$ ret . = "<tr class='even' style='height:" . $ z4 . "px'>" ;
$ ret . = "<td class='smaportal' style='vertical-align:top'>" . $ val . "</td></tr>" ;
}
}
if ( $ show_diff eq 'bottom' ) { # zusätzliche diff Anzeige
$ val = formatVal6 ( $ di { $ i } , $ kw , $ we { $ i } ) ;
$ val = ( $ di { $ i } < 0 ) ? '<b>' . $ val . '<b/>' : '+' . $ val ; # Kommentar siehe oben bei show_diff eq top
$ ret . = "<tr class='even'><td class='smaportal' style='vertical-align:middle; text-align:center;'>$val</td></tr>" ;
}
$ ret . = "<tr class='even'><td class='smaportal' style='vertical-align:bottom; text-align:center;'>" ;
$ t { $ i } = $ t { $ i } . $ hourstyle if ( defined ( $ hourstyle ) ) ; # z.B. 10:00 statt 10
$ ret . = $ t { $ i } . "</td></tr></table></td>" ; # Stundenwerte ohne führende 0
2019-05-01 14:30:09 +00:00
}
2019-05-30 07:44:31 +00:00
$ ret . = "<td class='smaportal'></td></tr>" ;
###################
# Legende unten
if ( $ legend_txt && ( $ legend eq 'bottom' ) ) {
$ ret . = "<tr class='odd'>" ;
$ ret . = "<td colspan='" . ( $ maxhours + 2 ) . "' align='center' style='word-break: normal'>" ;
$ ret . = "$legend_txt</td></tr>" ;
}
$ ret . = "</table></td></tr></table>" ;
$ ret . = $ html_end if ( defined ( $ html_end ) ) ;
2019-06-03 23:00:53 +00:00
$ ret . = "</html>" ;
2019-05-30 07:44:31 +00:00
2019-05-01 14:30:09 +00:00
return $ ret ;
2019-04-29 22:07:15 +00:00
}
2019-05-30 07:44:31 +00:00
###############################################################################
# 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 'kW' ) { # bei Anzeige in kW 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 ' ' ; } # 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 = (
'0' = > 'weather_sun' , # Sonne (klar) # vorhanden
'1' = > 'weather_cloudy_light' , # leichte Bewölkung (1/3) # vorhanden
'2' = > 'weather_cloudy' , # mittlere Bewölkung (2/3) # vorhanden
'3' = > '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
) ;
return $ weather_ids { $ id } if ( defined ( $ weather_ids { $ id } ) ) ;
return 'unknown' ;
}
2019-06-03 23:00:53 +00:00
######################################################################################################
# Refresh eines Raumes aus $hash->{HELPER}{SPGROOM}
# bzw. Longpoll von SSCam bzw. eines SMAPortalSPG Devices wenn $hash->{HELPER}{SPGDEV} gefüllt
# $hash, $pload (1=Page reload), SMAPortalSPG-Event (1=Event)
######################################################################################################
sub SPGRefresh ($$$) {
my ( $ hash , $ pload , $ lpollspg ) = @ _ ;
my $ name ;
if ( ref $ hash ne "HASH" ) {
( $ name , $ pload , $ lpollspg ) = split "," , $ hash ;
$ hash = $ defs { $ name } ;
} else {
$ name = $ hash - > { NAME } ;
}
my $ fpr = 0 ;
# Kontext des SMAPortalSPG-Devices speichern für Refresh
my $ sd = $ hash - > { HELPER } { SPGDEV } ? $ hash - > { HELPER } { SPGDEV } : "\"n.a.\"" ; # Name des aufrufenden SMAPortalSPG-Devices
my $ sr = $ hash - > { HELPER } { SPGROOM } ? $ hash - > { HELPER } { SPGROOM } : "\"n.a.\"" ; # Raum aus dem das SMAPortalSPG-Device die Funktion aufrief
my $ sl = $ hash - > { HELPER } { SPGDETAIL } ? $ hash - > { HELPER } { SPGDETAIL } : "\"n.a.\"" ; # Name des SMAPortalSPG-Devices (wenn Detailansicht)
$ fpr = AttrVal ( $ hash - > { HELPER } { SPGDEV } , "forcePageRefresh" , 0 ) if ( $ hash - > { HELPER } { SPGDEV } ) ;
Log3 ( $ name , 4 , "$name - Refresh - caller: $sd, callerroom: $sr, detail: $sl, pload: $pload, forcePageRefresh: $fpr, event_Spgdev: $lpollspg" ) ;
# Page-Reload
if ( $ pload && ( $ hash - > { HELPER } { SPGROOM } && ! $ hash - > { HELPER } { SPGDETAIL } && ! $ fpr ) ) {
# trifft zu wenn in einer Raumansicht
my @ rooms = split ( "," , $ hash - > { HELPER } { SPGROOM } ) ;
foreach ( @ rooms ) {
my $ room = $ _ ;
{ map { FW_directNotify ( "FILTER=room=$room" , "#FHEMWEB:$_" , "location.reload('true')" , "" ) } devspec2array ( "TYPE=FHEMWEB" ) }
}
} elsif ( $ pload && ( ! $ hash - > { HELPER } { SPGROOM } || $ hash - > { HELPER } { SPGDETAIL } ) ) {
# trifft zu bei Detailansicht oder im FLOORPLAN bzw. Dashboard oder wenn Seitenrefresh mit dem
# SMAPortalSPG-Attribut "forcePageRefresh" erzwungen wird
{ map { FW_directNotify ( "#FHEMWEB:$_" , "location.reload('true')" , "" ) } devspec2array ( "TYPE=FHEMWEB" ) }
} else {
if ( $ fpr ) {
{ map { FW_directNotify ( "#FHEMWEB:$_" , "location.reload('true')" , "" ) } devspec2array ( "TYPE=FHEMWEB" ) }
}
}
# parentState des SMAPortalSPG-Device updaten
my @ spgs = devspec2array ( "TYPE=SMAPortalSPG" ) ;
my $ st = ReadingsVal ( $ name , "state" , "initialized" ) ;
foreach ( @ spgs ) {
if ( $ defs { $ _ } { PARENT } eq $ name ) {
next if ( IsDisabled ( $ defs { $ _ } { NAME } ) ) ;
readingsBeginUpdate ( $ defs { $ _ } ) ;
readingsBulkUpdate ( $ defs { $ _ } , "parentState" , $ st ) ;
readingsBulkUpdate ( $ defs { $ _ } , "state" , "updated" ) ;
readingsEndUpdate ( $ defs { $ _ } , 1 ) ;
}
}
return ;
}
2019-03-03 20:08:04 +00:00
1 ;
= pod
2019-03-11 22:54:31 +00:00
= encoding utf8
2019-05-30 07:44:31 +00:00
= item summary Module for communication with the SMA Sunny Portal
= item summary_DE Modul zur Kommunikation mit dem SMA Sunny Portal
2019-03-03 20:08:04 +00:00
= begin html
< a name = "SMAPortal" > </a>
<h3> SMAPortal </h3>
<ul>
<br>
Is coming soon ...
</ul>
= end html
= begin html_DE
< a name = "SMAPortal" > </a>
<h3> SMAPortal </h3>
<ul>
2019-06-03 23:00:53 +00:00
Mit diesem Modul können Daten aus dem < a href = "https://www.sunnyportal.com" > SMA Sunny Portal </a> abgerufen werden .
2019-03-03 20:08:04 +00:00
Momentan sind es: <br> <br>
<ul>
<ul>
<li> Live - Daten ( Verbrauch und PV - Erzeugung ) </li>
2019-04-29 22:07:15 +00:00
<li> Batteriedaten ( In /Out) </ li >
2019-03-10 07:34:29 +00:00
<li> Wetter - Daten von SMA für den Anlagenstandort </li>
2019-03-03 20:08:04 +00:00
<li> Prognosedaten ( Verbrauch und PV - Erzeugung ) inklusive Verbraucherempfehlung </li>
2019-04-29 22:07:15 +00:00
<li> die durch den Sunny Home Manager geplanten Schaltzeiten von Verbrauchern ( sofern vorhanden ) </li>
2019-03-03 20:08:04 +00:00
</ul>
</ul>
<br>
<b> Vorbereitung </b> <br> <br>
<ul>
Dieses Modul nutzt das Perl - Modul JSON welches üblicherweise nachinstalliert werden muss . <br>
Auf Debian - Linux basierenden Systemen kann es installiert werden mit: <br> <br>
<code> sudo apt - get install libjson - perl </code> <br> <br>
Überblick über die Perl - Module welche von SMAPortal genutzt werden: <br> <br>
POSIX <br>
JSON <br>
Data:: Dumper <br>
Time:: HiRes <br>
2019-03-25 19:46:08 +00:00
Time:: Local <br>
2019-03-03 20:08:04 +00:00
Blocking ( FHEM - Modul ) <br>
2019-03-25 19:46:08 +00:00
GPUtils ( FHEM - Modul ) <br>
FHEM:: Meta ( FHEM - Modul ) <br>
2019-03-03 20:08:04 +00:00
LWP:: UserAgent <br>
2019-03-25 19:46:08 +00:00
HTTP:: Cookies <br>
MIME:: Base64 <br>
Encode <br>
2019-03-03 20:08:04 +00:00
<br> <br>
</ul>
< a name = "SMAPortalDefine" > </a>
<b> Definition </b>
<ul>
<br>
Ein SMAPortal - Device wird definiert mit: <br> <br>
<ul>
<b> <code> define & lt ; Name & gt ; SMAPortal </code> </b> <br> <br>
</ul>
Nach der Definition des Devices müssen noch die Zugangsparameter für das SMA - Portal gespeichert werden .
Das geschieht mit dem Befehl: <br> <br>
<ul>
set & lt ; Name & gt ; credentials & lt ; Username & gt ; & lt ; Passwort & gt ;
</ul>
</ul>
<br> <br>
< a name = "SMAPortalSet" > </a>
<b> Set </b>
<ul>
<br>
<ul>
2019-05-30 07:44:31 +00:00
<li> <b> set & lt ; name & gt ; createPortalGraphic & lt ; Generation | Consumption | Generation_Consumption | Differential & gt ; </b> </li>
Erstellt Devices zur grafischen Anzeige der SMA Sunny Portal Prognosedaten in verschiedenen Layouts .
2019-05-01 14:30:09 +00:00
Das Attribut "detailLevel" muss auf den Level 4 gesetzt sein . Der Befehl setzt dieses Attribut automatisch auf
den benötigten Wert . <br>
2019-06-03 23:00:53 +00:00
Mit den < a href = "#SMAPortalSPGattr" > "Attributen des Grafikdevices" </a> können Erscheinungsbild und
Farbgebung der Prognosedaten in den erstellten Grafik - Devices angepasst werden .
2019-03-03 20:08:04 +00:00
</ul>
2019-05-01 14:30:09 +00:00
<br>
2019-04-29 22:07:15 +00:00
<ul>
2019-05-01 14:30:09 +00:00
<li> <b> set & lt ; name & gt ; credentials & lt ; username & gt ; & lt ; password & gt ; </b> </li>
Setzt Username / Passwort für den Zugriff zum SMA - Portal .
2019-04-29 22:07:15 +00:00
</ul>
2019-03-03 20:08:04 +00:00
</ul>
<br> <br>
< a name = "SMAPortalGet" > </a>
<b> Get </b>
<ul>
<br>
<ul>
<li> <b> get & lt ; name & gt ; data </b> </li>
Mit diesem Befehl werden die Daten aus dem SMA - Portal manuell abgerufen .
</ul>
<br>
<ul>
<li> <b> get & lt ; name & gt ; storedCredentials </b> </li>
Die gespeicherten Anmeldeinformationen ( Credentials ) werden in einem Popup als Klartext angezeigt .
</ul>
</ul>
<br> <br>
< a name = "SMAPortalAttr" > </a>
<b> Attribute </b>
<ul>
<br>
<ul>
< a name = "cookielifetime" > </a>
<li> <b> cookielifetime & lt ; Sekunden & gt ; </b> <br>
Gültigkeitszeitraum für einen empfangenen Cookie ( Default: 3000 Sekunden ) .
</li> <br>
< a name = "cookieLocation" > </a>
<li> <b> cookieLocation & lt ; Pfad /File> </ b > <br>
Angabe von Pfad und Datei zur Abspeicherung des empfangenen Cookies ( Default: . /log/m ycookies . txt ) .
<br> <br>
<ul>
<b> Beispiel: </b> <br>
attr & lt ; name & gt ; cookieLocation . /log/coo kies . txt <br>
</ul>
</li> <br>
< a name = "detailLevel" > </a>
<li> <b> detailLevel </b> <br>
Es wird der Umfang der zu generierenden Daten eingestellt .
<br> <br>
<ul>
<table>
<colgroup> < col width = 5 % > < col width = 95 % > < / colgroup >
2019-03-10 07:34:29 +00:00
<tr> <td> <b> L1 </b> </td> <td> - nur Live - Daten und Wetter - Daten werden generiert . </td> </tr>
2019-05-01 14:30:09 +00:00
<tr> <td> <b> L2 </b> </td> <td> - wie L1 und zusätzlich Prognose der aktuellen und nächsten 4 Stunden sowie PV - Erzeugung und Verbrauch des aktuellen Tages </td> </tr>
2019-03-24 15:51:33 +00:00
<tr> <td> <b> L3 </b> </td> <td> - wie L2 und zusätzlich Prognosedaten des Resttages , des Folgetages und der geplanten Schaltzeiten für Verbraucher </td> </tr>
2019-03-03 20:08:04 +00:00
<tr> <td> <b> L4 </b> </td> <td> - wie L3 und zusätzlich die detaillierte Prognose der nächsten 24 Stunden </td> </tr>
</table>
</ul>
<br>
</li> <br>
< a name = "disable" > </a>
<li> <b> disable </b> <br>
Deaktiviert das Device . </li> <br>
2019-03-09 08:32:50 +00:00
< a name = "getDataRetries" > </a>
<li> <b> getDataRetries & lt ; Anzahl & gt ; </b> <br>
Anzahl der Wiederholungen ( get data ) im Fall dass keine Live - Daten vom SMA - Portal geliefert
wurden ( default: 3 ) . </li> <br>
2019-03-03 20:08:04 +00:00
< a name = "interval" > </a>
<li> <b> interval & lt ; Sekunden & gt ; </b> <br>
Zeitintervall zum kontinuierlichen Datenabruf aus dem SMA - Portal ( Default: 300 Sekunden ) . <br>
Ist "interval = 0" gesetzt , erfolgt kein automatischer Datenabruf und muss mit "get <name> data" manuell
2019-05-30 07:44:31 +00:00
erfolgen . Wird ein Wert kleiner 120 Sekunden ( und > 0 ) angegeben , wird der Wert auf 120 Sekunden korrigiert . <br> <br>
<b> Hinweis: </b>
Das Abfrageintervall sollte nicht kleiner 120 Sekunden sein . Nach bisherigen Erfahrungen toleriert SMA ein
Intervall von 120 Sekunden obwohl lt . SMA AGB der automatische Datenabruf untersagt ist .
</li> <br>
2019-03-03 20:08:04 +00:00
< a name = "showPassInLog" > </a>
<li> <b> showPassInLog </b> <br>
Wenn gesetzt , wird das verwendete Passwort im Logfile angezeigt .
( default = 0 ) </li> <br>
< a name = "timeout" > </a>
<li> <b> timeout & lt ; Sekunden & gt ; </b> <br>
Timeout - Wert für HTTP - Aufrufe zum SMA - Portal ( Default: 30 Sekunden ) .
</li> <br>
< a name = "userAgent" > </a>
<li> <b> userAgent & lt ; Kennung & gt ; </b> <br>
Es kann die User - Agent - Kennung zur Identifikation gegenüber dem Portal angegeben werden .
<br> <br>
<ul>
<b> Beispiel: </b> <br>
attr & lt ; name & gt ; userAgent Mozilla /5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/ 20100101 Firefox / 65.0 <br>
</ul>
</li> <br>
2019-05-30 07:44:31 +00:00
</ul>
</ul>
2019-06-03 23:00:53 +00:00
<br>
2019-03-03 20:08:04 +00:00
2019-03-12 22:17:06 +00:00
</ul>
= end html_DE
= for : application / json ; q = META . json 76 _SMAPortal . pm
{
2019-05-30 07:44:31 +00:00
"abstract" : "Module for communication with the SMA Sunny Portal" ,
2019-03-12 22:17:06 +00:00
"x_lang" : {
"de" : {
2019-05-30 07:44:31 +00:00
"abstract" : "Modul zur Kommunikation mit dem SMA Sunny Portal"
2019-03-12 22:17:06 +00:00
}
} ,
"keywords" : [
"sma" ,
"photovoltaik" ,
"electricity" ,
"portal" ,
"smaportal"
] ,
2019-03-18 23:51:42 +00:00
"version" : "v1.1.1" ,
2019-03-12 22:17:06 +00:00
"release_status" : "testing" ,
"author" : [
2019-05-30 07:44:31 +00:00
"Heiko Maaz <heiko.maaz@t-online.de>" ,
"Wzut" ,
"XGuide" ,
null
2019-03-12 22:17:06 +00:00
] ,
"x_fhem_maintainer" : [
"DS_Starter"
] ,
"x_fhem_maintainer_github" : [
"nasseeder1"
] ,
"prereqs" : {
"runtime" : {
"requires" : {
"FHEM" : 5.00918799 ,
"perl" : 5.014 ,
"JSON" : 0 ,
2019-03-24 15:51:33 +00:00
"Encode" : 0 ,
2019-03-12 22:17:06 +00:00
"POSIX" : 0 ,
"Data::Dumper" : 0 ,
2019-03-16 06:48:59 +00:00
"Blocking" : 0 ,
"GPUtils" : 0 ,
"Time::HiRes" : 0 ,
2019-03-24 15:51:33 +00:00
"Time::Local" : 0 ,
2019-03-16 06:48:59 +00:00
"LWP" : 0 ,
"HTTP::Cookies" : 0 ,
"MIME::Base64" : 0
2019-03-12 22:17:06 +00:00
} ,
"recommends" : {
2019-03-25 19:19:05 +00:00
"FHEM::Meta" : 0
2019-03-12 22:17:06 +00:00
} ,
"suggests" : {
}
}
}
}
= end : application / json ; q = META . json
= cut