mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-03-04 11:26:55 +00:00
1036 lines
32 KiB
Perl
1036 lines
32 KiB
Perl
# $Id: 23_KOSTALPIKO.pm 6805 2014-10-24 18:00:00Z john $
|
|
####################################################################################################
|
|
#
|
|
# 23_KOSTALPIKO.pm
|
|
#
|
|
# This modul supports the KOSTAL Piko Inverter.
|
|
# All Value of Piko's Home-page are captured.
|
|
#
|
|
# Futhermore the Global-Radion value is captured from http://www.proplanta.de/Wetter/<city>-Wetter-Heute.html
|
|
# so the expected energy ca be estimated
|
|
#
|
|
# 2013-06-28 john : added some snippets for getting all readings
|
|
# : added UndefFn
|
|
# 2013-06-28 john : global radiation support; updated hourly; needs attribute GR.Link
|
|
# the link must have the form of: http://www.proplanta.de/Wetter/<city>-Wetter-Heute.html
|
|
# take a look to the site http://www.proplanta.de
|
|
# you can calculate the expected daily power by using userReadings
|
|
# Daily.Energy.Last is updated once at the hour 23
|
|
# 2013-06-28 john : Delay.Counter added
|
|
# will be decremented until 0,
|
|
# if not 0, then only AC.Power is scanned, otherwise alle Values are scanned
|
|
# 2013-07-02 john : AC.Power.Fast added
|
|
# 2013-07-14 john : some fixes with minor priority
|
|
# 2014-06-01 john V2.00 : adaption to common developer standards
|
|
# attribute changes
|
|
# - verbose is supported instead of loglevel
|
|
# - disable is supported
|
|
# - new attribute : GRIntervall : intervall for capturing global radiation
|
|
# new software-design
|
|
# - non-blocking calls for capturing and parsing of html-pages
|
|
# - reducing side-effects for other devices due timeouts
|
|
# 2014-06-29 john V2.01 : supporting sensor values for http://<name>/Info.fhtml
|
|
# 2014-06-05 john V2.02 : supporting UV-Index and sunshine duration
|
|
# 2014-07-05 john V2.03 : fix: value extraction was faulty
|
|
# 2014-09-08 john V2.04 : fix: device name with dot made trouble (checked against Kostal Pikos Firmware 10.1)
|
|
# adjusting KOSTALPIKO_Log
|
|
# Inital Checkin to FHEM ; docu revised
|
|
####################################################################################################
|
|
|
|
# --------------------------------------------
|
|
# parser for the site http://<ip-kostal>/index.fhtml
|
|
package MyParser;
|
|
use base qw(HTML::Parser);
|
|
our @texte = ();
|
|
my $isTD = 0;
|
|
my $takeNext = 0;
|
|
|
|
|
|
# is called if a text content is detected
|
|
# results in an array of string with alternating description / value
|
|
sub text
|
|
{
|
|
my ( $self, $text ) = @_;
|
|
if ( $isTD == 1 ) # if we are inside a TD-Tag
|
|
{
|
|
$text =~ s/^\s+//; # trim string
|
|
$text =~ s/\s+$//;
|
|
if ( $takeNext == 1 ) # first text is description, next text is value
|
|
{
|
|
$takeNext = 0;
|
|
push( @texte, $text );
|
|
}
|
|
|
|
# filter only interesting captions
|
|
if ( $text eq "aktuell"
|
|
|| $text eq "Gesamtenergie"
|
|
|| $text eq "Tagesenergie"
|
|
|| $text eq "Status"
|
|
|| $text eq "Spannung"
|
|
|| $text eq "Strom"
|
|
|| $text eq "Leistung" )
|
|
{
|
|
$takeNext = 1; # expect next tag as value
|
|
push( @texte, $text );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
# callback, if start tag is detected
|
|
sub start
|
|
{
|
|
my ( $self, $tagname, $attr, $attrseq, $origtext ) = @_;
|
|
|
|
# we are only interested on TD-Tags
|
|
if ( $tagname eq 'td' )
|
|
{
|
|
$isTD = 1;
|
|
} else
|
|
{
|
|
$isTD = 0;
|
|
}
|
|
}
|
|
|
|
# after end-tag reset TD-marker
|
|
sub end
|
|
{
|
|
$isTD = 0;
|
|
}
|
|
|
|
###############################################
|
|
# parser for the global radiation
|
|
package MyRadiationParser;
|
|
use base qw(HTML::Parser);
|
|
our @texte = ();
|
|
my $lookupTag = "span";
|
|
my $curTag = "";
|
|
my $takeNext = 0;
|
|
|
|
# here HTML::text/start/end are overridden
|
|
sub text
|
|
{
|
|
my ( $self, $text ) = @_;
|
|
if ( $curTag eq $lookupTag )
|
|
{
|
|
$text =~ s/^\s+//; # trim string
|
|
$text =~ s/\s+$//;
|
|
if ( $takeNext == 1 )
|
|
{
|
|
$takeNext = 0;
|
|
push( @texte, $text );
|
|
}
|
|
if ( $text eq "Globalstrahlung" )
|
|
{
|
|
$takeNext = 1;
|
|
push( @texte, $text );
|
|
}
|
|
elsif ( $text eq "UV-Index" )
|
|
{
|
|
$takeNext = 1;
|
|
push( @texte, $text );
|
|
}
|
|
elsif ( $text eq "rel. Sonnenscheindauer" )
|
|
{
|
|
$takeNext = 1;
|
|
push( @texte, $text );
|
|
}
|
|
}
|
|
}
|
|
|
|
sub start
|
|
{
|
|
my ( $self, $tagname, $attr, $attrseq, $origtext ) = @_;
|
|
$curTag = $tagname;
|
|
}
|
|
|
|
sub end
|
|
{
|
|
$curTag = "";
|
|
}
|
|
|
|
##############################################
|
|
# parser for the site http://<kostal-piko-ip>/Info.fhtml with sensor values
|
|
package MyInfoParser;
|
|
use base qw(HTML::Parser);
|
|
our @texte = ();
|
|
my $isTD = 0;
|
|
my $isBold = 0;
|
|
my $takeNext = 0;
|
|
|
|
|
|
# is called if a text content is detected
|
|
sub text
|
|
{
|
|
my ( $self, $text ) = @_;
|
|
if ( $isTD == 1 ) # if we are inside a TD-Tag
|
|
{
|
|
# filter only interesting captions
|
|
if ( $text =~ m/.*Eingang.*/)
|
|
{
|
|
$takeNext = 1; # expect next tag as value
|
|
push( @texte, $text );
|
|
}
|
|
}
|
|
|
|
if ( $isBold == 1 && $takeNext==1)
|
|
{
|
|
$takeNext = 0;
|
|
$text =~ s/^\s+//; # trim string
|
|
$text =~ s/\s+$//;
|
|
$text=~ m/([0-9]+\.[0-9]+)/; # find substring 0.00V : 0.00
|
|
my $value=$1;
|
|
push( @texte, $value );
|
|
}
|
|
}
|
|
|
|
# callback, if start tag is detected
|
|
sub start
|
|
{
|
|
my ( $self, $tagname, $attr, $attrseq, $origtext ) = @_;
|
|
|
|
# we are only interested on TD-Tags
|
|
$isTD = 0;
|
|
$isBold = 0;
|
|
if ( $tagname eq 'td' )
|
|
{
|
|
$isTD = 1;
|
|
}
|
|
|
|
if ( $tagname eq 'b' )
|
|
{
|
|
$isBold = 1;
|
|
}
|
|
}
|
|
|
|
# after end-tag reset TD-marker
|
|
sub end
|
|
{
|
|
$isTD = 0;
|
|
}
|
|
|
|
##############################################
|
|
package main;
|
|
use strict;
|
|
use feature qw/say switch/;
|
|
use warnings;
|
|
use Data::Dumper;
|
|
use LWP::UserAgent;
|
|
use HTTP::Request;
|
|
require 'Blocking.pm';
|
|
require 'HttpUtils.pm';
|
|
use vars qw($readingFnAttributes);
|
|
|
|
use vars qw(%defs);
|
|
my $MODUL = "KOSTALPIKO";
|
|
my $KOSTAL_VERSION = "2.04";
|
|
|
|
|
|
########################################
|
|
sub KOSTALPIKO_Log($$$)
|
|
{
|
|
my ( $hash, $loglevel, $text ) = @_;
|
|
my $xline = ( caller(0) )[2];
|
|
|
|
my $xsubroutine = ( caller(1) )[3];
|
|
my $sub = ( split( ':', $xsubroutine ) )[2];
|
|
$sub =~ s/KOSTALPIKO_//;
|
|
|
|
my $instName = ( ref($hash) eq "HASH" ) ? $hash->{NAME} : $MODUL;
|
|
Log3 $hash, $loglevel, "$MODUL $instName: $sub.$xline " . $text;
|
|
}
|
|
###################################
|
|
sub KOSTALPIKO_Initialize($)
|
|
{
|
|
my ($hash) = @_;
|
|
$hash->{DefFn} = "KOSTALPIKO_Define";
|
|
$hash->{UndefFn} = "KOSTALPIKO_Undef";
|
|
$hash->{SetFn} = "KOSTALPIKO_Set";
|
|
$hash->{AttrList} = "delay " . "delayCounter " . "GR.Link " . "GR.Interval " . "disable:0,1 " . $readingFnAttributes;
|
|
}
|
|
###################################
|
|
sub KOSTALPIKO_Define($$)
|
|
{
|
|
my ( $hash, $def ) = @_;
|
|
my $name = $hash->{NAME};
|
|
my @a = split( "[ \t][ \t]*", $def );
|
|
my $host = $a[2];
|
|
my $user = $a[3];
|
|
my $pass = $a[4];
|
|
if ( int(@a) < 5 )
|
|
{
|
|
return "Wrong syntax: use define <name> KOSTALPIKO <ip-address> <user> <pass>";
|
|
}
|
|
$hash->{VERSION} = $KOSTAL_VERSION;
|
|
$hash->{helper}{Host} = $host;
|
|
$hash->{helper}{User} = $user;
|
|
$hash->{helper}{Pass} = $pass;
|
|
$hash->{helper}{GRHour} = 25;
|
|
$hash->{helper}{TimerStatus} = $name . ".STATUS"; # like "Kostal.STATUS"
|
|
$hash->{helper}{TimerGR} = $name . ".GR";
|
|
InternalTimer( gettimeofday() + 10, "KOSTALPIKO_StatusTimer", $hash->{helper}{TimerStatus}, 0 );
|
|
InternalTimer( gettimeofday() + 20, "KOSTALPIKO_GrTimer", $hash->{helper}{TimerGR}, 0 );
|
|
return undef;
|
|
}
|
|
#####################################
|
|
sub KOSTALPIKO_Undef($$)
|
|
{
|
|
my ( $hash, $arg ) = @_;
|
|
|
|
RemoveInternalTimer( $hash->{helper}{TimerStatus} );
|
|
RemoveInternalTimer( $hash->{helper}{TimerGR} );
|
|
BlockingKill( $hash->{helper}{RUNNING_STATUS} ) if ( defined( $hash->{helper}{RUNNING_STATUS} ) );
|
|
BlockingKill( $hash->{helper}{RUNNING_GR} ) if ( defined( $hash->{helper}{RUNNING_GR} ) );
|
|
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
return undef;
|
|
}
|
|
#####################################
|
|
sub KOSTALPIKO_Set($@)
|
|
{
|
|
my ( $hash, @a ) = @_;
|
|
my $name = $hash->{NAME};
|
|
my $reUINT = '^([\\+]?\\d+)$';
|
|
my $usage = "Unknown argument $a[1], choose one of captureKostalData:noArg ";
|
|
my $URL = AttrVal( $name, 'GR.Link', "" );
|
|
if ($URL)
|
|
{
|
|
$usage .= "captureGlobalRadiation:noArg ";
|
|
}
|
|
|
|
# for debugging issues
|
|
# $usage .= "test:noArg sleeper ";
|
|
|
|
return $usage if ( @a < 2 );
|
|
|
|
my $cmd = lc( $a[1] );
|
|
given ($cmd)
|
|
{
|
|
when ("?")
|
|
{
|
|
return $usage;
|
|
}
|
|
when ("capturekostaldata")
|
|
{
|
|
KOSTALPIKO_Log $hash, 3, "set command: " . $a[1] . " para:" . $hash->{helper}{TimerStatus};
|
|
KOSTALPIKO_StatusStart($hash);
|
|
}
|
|
when ("captureglobalradiation")
|
|
{
|
|
KOSTALPIKO_Log $hash, 3, "set command: " . $a[1];
|
|
KOSTALPIKO_GrStart($hash);
|
|
}
|
|
when ("test")
|
|
{
|
|
KOSTALPIKO_Log $hash, 3, "set command: " . $a[1];
|
|
KOSTALPIKO_GrStart($hash);
|
|
}
|
|
|
|
when ("sleeper")
|
|
{
|
|
return "Set sleeper needs a <value> parameter"
|
|
if ( @a != 3 );
|
|
my $value=$a[2];
|
|
$value=($value=~ m/$reUINT/) ? $1:undef;
|
|
return "value ".$a[2]." is not a number"
|
|
if (!defined($value));
|
|
|
|
KOSTALPIKO_Log $hash, 3, "set command: " . $a[1]." value:". $a[2];
|
|
$hash->{helper}{Sleeper} = $a[2];
|
|
}
|
|
default
|
|
{
|
|
return $usage;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
#############################################
|
|
# get hour as number, input is a serial date
|
|
sub KOSTAL_GetHourSD($)
|
|
{
|
|
my @t = localtime(shift);
|
|
return $t[2];
|
|
}
|
|
#############################################
|
|
# current datetime round off to current hour
|
|
sub KOSTAL_GetDateTrunc($)
|
|
{
|
|
my @t = localtime(shift);
|
|
return sprintf( "%04d-%02d-%02d %02d:%02d:%02d", $t[5] + 1900, $t[4] + 1, $t[3], $t[2], 0, 0 );
|
|
}
|
|
#############################################
|
|
# converts string-datetime to serial-datetime
|
|
# input: datetime as string
|
|
# output: serial datetime
|
|
sub KOSTAL_DateStr2Serial($)
|
|
{
|
|
my $datestr = shift;
|
|
my ( $yyyy, $mm, $dd, $hh, $mi, $ss ) = $datestr =~ /(\d+)-(\d+)-(\d+) (\d+)[:](\d+)[:](\d+)/;
|
|
|
|
# months are zero based
|
|
my $t2 = fhemTimeLocal( $ss, $mi, $hh, $dd, $mm - 1, $yyyy - 1900 );
|
|
return $t2;
|
|
}
|
|
|
|
#####################################
|
|
# acquires the sensor html page of kostalpiko
|
|
sub KOSTALPIKO_SensorHtmlAcquire($)
|
|
{
|
|
my ($hash) = @_;
|
|
return unless (defined($hash->{NAME}));
|
|
|
|
my $err_log = '';
|
|
|
|
my $URL =
|
|
"http://" . $hash->{helper}{User} . ":" . $hash->{helper}{Pass} . "\@" . $hash->{helper}{Host} . "/Info.fhtml";
|
|
|
|
KOSTALPIKO_Log $hash, 4, "$URL";
|
|
my $agent = LWP::UserAgent->new( env_proxy => 1, keep_alive => 1, timeout => 3 );
|
|
my $header = HTTP::Request->new( GET => $URL );
|
|
my $request = HTTP::Request->new( 'GET', $URL, $header );
|
|
my $response = $agent->request($request);
|
|
$err_log .= "Can't get $URL -- " . $response->status_line
|
|
unless $response->is_success;
|
|
|
|
if ( $err_log ne "" )
|
|
{
|
|
KOSTALPIKO_Log $hash, 1, $err_log;
|
|
return "";
|
|
}
|
|
return $response->content;
|
|
}
|
|
|
|
#####################################
|
|
# acquires the html page of kostalpiko
|
|
sub KOSTALPIKO_StatusHtmlAcquire($)
|
|
{
|
|
my ($hash) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
return unless (defined($hash->{NAME}));
|
|
|
|
my $err_log = '';
|
|
|
|
my $URL =
|
|
"http://" . $hash->{helper}{User} . ":" . $hash->{helper}{Pass} . "\@" . $hash->{helper}{Host} . "/index.fhtml";
|
|
|
|
KOSTALPIKO_Log $hash, 4, "$URL";
|
|
my $agent = LWP::UserAgent->new( env_proxy => 1, keep_alive => 1, timeout => 3 );
|
|
my $header = HTTP::Request->new( GET => $URL );
|
|
my $request = HTTP::Request->new( 'GET', $URL, $header );
|
|
my $response = $agent->request($request);
|
|
$err_log .= "Can't get $URL -- " . $response->status_line
|
|
unless $response->is_success;
|
|
|
|
if ( $err_log ne "" )
|
|
{
|
|
KOSTALPIKO_Log $hash, 1, $err_log;
|
|
return "";
|
|
}
|
|
return $response->content;
|
|
}
|
|
|
|
#####################################
|
|
sub KOSTALPIKO_StatusStart($)
|
|
{
|
|
my ($hash) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
return unless (defined($hash->{NAME}));
|
|
|
|
my $err_log = '';
|
|
my $sdCurTime = gettimeofday();
|
|
my $hour = KOSTAL_GetHourSD($sdCurTime);
|
|
my $disable = AttrVal( $name, "disable", 0 );
|
|
my $delay = AttrVal( $name, "delay", 300 );
|
|
|
|
while (1)
|
|
{
|
|
KOSTALPIKO_Log $hash, 3, "--- started ---";
|
|
|
|
# check disable attribute
|
|
if ( $disable == 1 )
|
|
{
|
|
KOSTALPIKO_Log $hash, 3, "disabled";
|
|
last;
|
|
}
|
|
|
|
if (!defined($hash->{helper}{delayCounter}))
|
|
{
|
|
$hash->{helper}{delayCounter}=AttrVal( $name, "delayCounter", "0" );
|
|
}
|
|
|
|
# wenn delayCounter aktiv
|
|
if ( $hash->{helper}{delayCounter} > 0 )
|
|
{
|
|
$hash->{helper}{delayCounter}--;
|
|
}
|
|
|
|
$hash->{helper}{RUNNING_STATUS} =
|
|
BlockingCall(
|
|
"KOSTALPIKO_StatusRun", # callback worker task
|
|
$name, # name of the device
|
|
"KOSTALPIKO_StatusDone", # callback result method
|
|
50, # timeout seconds
|
|
"KOSTALPIKO_StatusAborted", # callback for abortion
|
|
$hash ); # parameter for abortion
|
|
|
|
last;
|
|
}
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
}
|
|
|
|
#####################################
|
|
sub KOSTALPIKO_StatusRun($)
|
|
{
|
|
my ($string) = @_;
|
|
my ( $name, $server ) = split( "\\|", $string );
|
|
my $level = 5;
|
|
|
|
return unless ( defined($name) );
|
|
|
|
my $hash = $defs{$name};
|
|
return unless (defined($hash->{NAME}));
|
|
|
|
KOSTALPIKO_Log $hash, 3, "--- started ---";
|
|
# acquire the html-page
|
|
my $response = KOSTALPIKO_StatusHtmlAcquire($hash);
|
|
|
|
# perform parsing
|
|
#KOSTALPIKO_Log $hash, $level, "before parsing of response-Len:".length($response);
|
|
my $parser = MyParser->new;
|
|
@MyParser::texte = ();
|
|
|
|
# parsing the complete html-page-response, needs some time
|
|
# only <td> tags will be regarded
|
|
$parser->parse($response);
|
|
|
|
# for testing issues
|
|
if (defined($hash->{helper}{Sleeper}))
|
|
{
|
|
my $sleep = $hash->{helper}{Sleeper};
|
|
$hash->{helper}{Sleeper}=0;
|
|
sleep($sleep) if ($sleep>0);
|
|
}
|
|
|
|
# pack the results in a single string
|
|
my $ptext=$name;
|
|
foreach my $text (@MyParser::texte)
|
|
{
|
|
$ptext = $ptext . "|".$text;
|
|
}
|
|
|
|
#---------------------------- Sensor values
|
|
$response = KOSTALPIKO_SensorHtmlAcquire($hash);
|
|
$parser = MyInfoParser->new;
|
|
@MyInfoParser::texte = ();
|
|
$parser->parse($response);
|
|
foreach my $text (@MyInfoParser::texte)
|
|
{
|
|
$ptext = $ptext . "|".$text;
|
|
}
|
|
|
|
#------------------------------ aquire is finished
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
return $ptext;
|
|
}
|
|
#####################################
|
|
# assyncronous callback by blocking
|
|
sub KOSTALPIKO_StatusDone($)
|
|
{
|
|
my ($string) = @_;
|
|
return unless ( defined($string) );
|
|
|
|
# all term are separated by "|" , the first ist the name of the instance
|
|
my ( $name, @values ) = split( "\\|", $string );
|
|
my $hash = $defs{$name};
|
|
return unless (defined($hash->{NAME}));
|
|
KOSTALPIKO_Log $hash, 3, "--- started ---";
|
|
|
|
# show the values
|
|
KOSTALPIKO_Log $hash, 5, "values:".join(', ', @values);
|
|
|
|
# delete the marker for running process
|
|
delete( $hash->{helper}{RUNNING_STATUS} );
|
|
|
|
#------------------
|
|
while (1)
|
|
{
|
|
my $tag = ""; # der Name des parameters in der web site
|
|
my $index = 0; # laufindex von 1..4 f. String x und Lx
|
|
my $strang = 1;
|
|
my $rdName = ""; # name for reading
|
|
my $rdValue; # value for reading
|
|
my %hashValues = (); # hash for name,value
|
|
my $sdCurTime = gettimeofday();
|
|
my $hour = KOSTAL_GetHourSD($sdCurTime);
|
|
|
|
foreach my $text (@values)
|
|
{
|
|
if ( $text eq "aktuell"
|
|
|| $text eq "Gesamtenergie"
|
|
|| $text eq "Tagesenergie"
|
|
|| $text eq "Status"
|
|
|| $text =~ m/.*analoger Eingang.*/
|
|
)
|
|
{
|
|
$tag = $text; # remember the identifier
|
|
}
|
|
elsif ( $text eq "Spannung" || $text eq "Strom" || $text eq "Leistung" )
|
|
{
|
|
$index++;
|
|
if ( $index > 4 )
|
|
{
|
|
$strang++;
|
|
$index = 1;
|
|
}
|
|
$tag = $text; # remember the identifier
|
|
}
|
|
else
|
|
{
|
|
if ( $tag ne "" ) # last text was a identifier, so we expect a value
|
|
{
|
|
$rdValue = $text;
|
|
# translate the identifier of the html.page to internal identifiers
|
|
$rdName = "AC.Power" if ( $tag eq "aktuell" );
|
|
$rdName = "Total.Energy" if ( $tag eq "Gesamtenergie" );
|
|
$rdName = "Daily.Energy" if ( $tag eq "Tagesenergie" );
|
|
$rdName = "Mode" if ( $tag eq "Status" );
|
|
$rdName = "generator.$strang.voltage" if ( $tag eq "Spannung" && $index == 1 );
|
|
$rdName = "output.$strang.voltage" if ( $tag eq "Spannung" && $index == 2 );
|
|
$rdName = "generator.$strang.current" if ( $tag eq "Strom" );
|
|
$rdName = "output.$strang.power" if ( $tag eq "Leistung" );
|
|
$rdName = "sensor.1" if ( $tag eq "1. analoger Eingang:" );
|
|
$rdName = "sensor.2" if ( $tag eq "2. analoger Eingang:" );
|
|
$rdName = "sensor.3" if ( $tag eq "3. analoger Eingang:" );
|
|
$rdName = "sensor.4" if ( $tag eq "4. analoger Eingang:" );
|
|
|
|
# set 0, if "x x x" is given
|
|
$rdValue = 0 if ( index( $rdValue, "x x x" ) != -1 );
|
|
|
|
# add the pair of identifier and value to the hash
|
|
$hashValues{$rdName} = $rdValue;
|
|
|
|
#special treatment for fast value
|
|
$hashValues{ $rdName . ".Fast" } = $rdValue if ( $rdName eq "AC.Power" );
|
|
$tag = ""; # next text will be an identifier
|
|
$rdName = "";
|
|
}
|
|
}
|
|
} # foreach
|
|
|
|
# add the state for reading update
|
|
$rdValue = "W: " . $hashValues{"AC.Power"} . " - " . $hashValues{"Mode"};
|
|
$hashValues{state} = $rdValue;
|
|
|
|
# set the ModeNum
|
|
my $NMode = 9;
|
|
$rdValue = $hashValues{"Mode"};
|
|
$NMode = 0 if ( $rdValue eq "Aus" );
|
|
$NMode = 1 if ( $rdValue eq "Leerlauf" );
|
|
$NMode = 2 if ( $rdValue eq "Einspeisen MPP" );
|
|
$hashValues{ModeNum} = $NMode;
|
|
|
|
# Daily.Energy.Last, remember the last value of dayly energy
|
|
# check from 23 hour
|
|
if ( defined( $hash->{READINGS}{"Daily.Energy"} ) && $hour == 23 )
|
|
{
|
|
my $ss = KOSTAL_GetDateTrunc($sdCurTime); # string date rounded to hour
|
|
my $sdDateTrunc = KOSTAL_DateStr2Serial($ss); # string date to serial date
|
|
$ss = ReadingsTimestamp( $name, "Daily.Energy.Last", $ss ); # determine reading timestamp
|
|
my $sdEnergyLast = KOSTAL_DateStr2Serial($ss); # serial format
|
|
KOSTALPIKO_Log $hash, 5, "DateTrunc : $ss sdDateTrunc: $sdDateTrunc sdEnergyLast:$sdEnergyLast";
|
|
if ( $sdEnergyLast <= $sdDateTrunc )
|
|
{
|
|
KOSTALPIKO_Log $hash, 4, "update Daily.Energy.Last with " . $hash->{READINGS}{"Daily.Energy"}{VAL};
|
|
readingsSingleUpdate( $hash, "Daily.Energy.Last", $hash->{READINGS}{"Daily.Energy"}{VAL}, 1 );
|
|
}
|
|
}
|
|
|
|
# update readings
|
|
my $upd;
|
|
readingsBeginUpdate($hash);
|
|
foreach my $xxx ( sort keys %hashValues )
|
|
{
|
|
$upd = 0;
|
|
# update if reading not exists or if new/old value differs
|
|
if ( !defined( $hash->{READINGS}{$xxx}{VAL} ) || $hash->{READINGS}{$xxx}{VAL} ne $hashValues{$xxx} )
|
|
{
|
|
# AC.Power.FAst will every time updated, the others only, if delaycount is 0
|
|
if ( $xxx eq "AC.Power.Fast" || $hash->{helper}{delayCounter} == 0 )
|
|
{
|
|
readingsBulkUpdate( $hash, $xxx, $hashValues{$xxx} );
|
|
$upd = 1;
|
|
}
|
|
}
|
|
KOSTALPIKO_Log $hash, 4, "$xxx: $hashValues{ $xxx } upd:$upd";
|
|
}
|
|
readingsEndUpdate( $hash, 1 );
|
|
last;
|
|
}
|
|
|
|
# wir arbeiten mit delay counter
|
|
if ( AttrVal( $name, "delayCounter", "0" ) ne "0" && $hash->{helper}{delayCounter}==0)
|
|
{
|
|
$hash->{helper}{delayCounter} = AttrVal( $name, "delayCounter", "0" );
|
|
KOSTALPIKO_Log $hash, 3, "delayCounter restarted";
|
|
}
|
|
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
|
|
}
|
|
#####################################
|
|
sub KOSTALPIKO_StatusAborted($)
|
|
{
|
|
my ($hash) = @_;
|
|
delete( $hash->{helper}{RUNNING_STATUS} );
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
}
|
|
#####################################
|
|
sub KOSTALPIKO_StatusTimer($)
|
|
{
|
|
my ($timerpara) = @_;
|
|
#my ( $name, $func ) = split( /\./, $timerpara );
|
|
my $index = rindex($timerpara,"."); # rechter punkt
|
|
my $func = substr $timerpara,$index+1,length($timerpara); # function extrahieren
|
|
my $name = substr $timerpara,0,$index; # name extrahieren
|
|
my $hash = $defs{$name};
|
|
|
|
#KOSTALPIKO_Log "", 3, "--- started --- name:$name";
|
|
return unless (defined($hash->{NAME}));
|
|
KOSTALPIKO_Log $hash, 3, "--- started ---";
|
|
|
|
KOSTALPIKO_StatusStart($hash);
|
|
$hash->{helper}{TimerInterval} = AttrVal( $name, "delay", 60 );
|
|
# setup timer
|
|
RemoveInternalTimer( $hash->{helper}{TimerStatus} );
|
|
|
|
InternalTimer(
|
|
gettimeofday() + $hash->{helper}{TimerInterval},
|
|
"KOSTALPIKO_StatusTimer",
|
|
$hash->{helper}{TimerStatus},
|
|
0 );
|
|
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
}
|
|
|
|
#####################################
|
|
# acquires the html page of Global radiation
|
|
sub KOSTALPIKO_GrHtmlAcquire($)
|
|
{
|
|
my ($hash) = @_;
|
|
my $name = $hash->{NAME};
|
|
return unless (defined($hash->{NAME}));
|
|
|
|
my $URL = AttrVal( $name, 'GR.Link', "" );
|
|
|
|
# abbrechen, wenn wichtig parameter nicht definiert sind
|
|
return "" if ( !defined($URL) );
|
|
return "" if ( $URL eq "" );
|
|
|
|
my $err_log = "";
|
|
my $agent = LWP::UserAgent->new( env_proxy => 1, keep_alive => 1, timeout => 3 );
|
|
my $header = HTTP::Request->new( GET => $URL );
|
|
my $request = HTTP::Request->new( 'GET', $URL, $header );
|
|
my $response = $agent->request($request);
|
|
$err_log = "Can't get $URL -- " . $response->status_line
|
|
unless $response->is_success;
|
|
|
|
if ( $err_log ne "" )
|
|
{
|
|
KOSTALPIKO_Log $hash, 1, "Error: $err_log";
|
|
return "";
|
|
}
|
|
|
|
return $response->content;
|
|
}
|
|
|
|
|
|
#####################################
|
|
sub KOSTALPIKO_GrStart($)
|
|
{
|
|
my ($hash) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
return unless (defined($hash->{NAME}));
|
|
|
|
return if (AttrVal( $name, 'GR.Link', "" ) eq "");
|
|
|
|
while (1)
|
|
{
|
|
KOSTALPIKO_Log $hash, 3, "--- started ---";
|
|
|
|
|
|
$hash->{helper}{RUNNING_GR} =
|
|
BlockingCall(
|
|
"KOSTALPIKO_GrRun", # callback worker task
|
|
$name, # name of the device
|
|
"KOSTALPIKO_GrDone", # callback result method
|
|
50, # timeout seconds
|
|
"KOSTALPIKO_GrAborted", # callback for abortion
|
|
$hash ); # parameter for abortion
|
|
|
|
last;
|
|
}
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
}
|
|
|
|
#####################################
|
|
sub KOSTALPIKO_GrRun($)
|
|
{
|
|
my ($string) = @_;
|
|
my ( $name, $server ) = split( "\\|", $string );
|
|
my $ptext=$name;
|
|
|
|
return unless ( defined($name) );
|
|
|
|
my $hash = $defs{$name};
|
|
return unless (defined($hash->{NAME}));
|
|
|
|
KOSTALPIKO_Log $hash, 3, "--- started ---";
|
|
while (1)
|
|
{
|
|
# acquire the html-page
|
|
my $response = KOSTALPIKO_GrHtmlAcquire($hash);
|
|
last if ($response eq "");
|
|
|
|
my $parser = MyRadiationParser->new;
|
|
@MyRadiationParser::texte = ();
|
|
# parsing the complete html-page-response, needs some time
|
|
# only <td> tags will be regarded
|
|
$parser->parse($response);
|
|
KOSTALPIKO_Log $hash, 4, "parsed terms:" . @MyRadiationParser::texte;
|
|
|
|
# pack the results in a single string
|
|
foreach my $text (@MyRadiationParser::texte)
|
|
{
|
|
$ptext = $ptext . "|".$text;
|
|
}
|
|
|
|
last;
|
|
}
|
|
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
return $ptext;
|
|
}
|
|
#####################################
|
|
# assyncronous callback by blocking
|
|
sub KOSTALPIKO_GrDone($)
|
|
{
|
|
my ($string) = @_;
|
|
return unless ( defined($string) );
|
|
|
|
# all term are separated by "|" , the first ist the name of the instance
|
|
my ( $name, @values ) = split( "\\|", $string );
|
|
my $hash = $defs{$name};
|
|
return unless (defined($hash->{NAME}));
|
|
|
|
KOSTALPIKO_Log $hash, 3, "--- started ---";
|
|
|
|
# show the values
|
|
KOSTALPIKO_Log $hash, 5, "values:".join(', ', @values);
|
|
|
|
# delete the marker for running process
|
|
delete( $hash->{helper}{RUNNING_GR} );
|
|
|
|
my $tag = "";
|
|
my $rdName = "";
|
|
my $rdValue = "";
|
|
my %hashValues = ();
|
|
|
|
# nach myRadiation suchen
|
|
foreach my $text (@values)
|
|
{
|
|
if ( $text eq "Globalstrahlung" || $text eq "UV-Index" || $text eq "rel. Sonnenscheindauer" )
|
|
{
|
|
$tag = $text;
|
|
}
|
|
else
|
|
{
|
|
if ( $tag ne "" )
|
|
{
|
|
$rdValue = $text;
|
|
$rdValue =~ tr/,/./; # komma gegen punkt tauschen
|
|
$rdValue=~ m/([-,\+]?\d+\.?\d*)/; # zahl extrahieren
|
|
$rdValue=$1;
|
|
$rdName = $tag;
|
|
$rdName = "Global.Radiation" if ( $tag eq "Globalstrahlung" );
|
|
$rdName = "UV.Index" if ( $tag eq "UV-Index" );
|
|
$rdName = "sunshine.duration" if ( $tag eq "rel. Sonnenscheindauer" );
|
|
$hashValues{$rdName} = $rdValue;
|
|
$tag = "";
|
|
KOSTALPIKO_Log $hash, 5, "tag:$rdName value:$rdValue";
|
|
}
|
|
}
|
|
}
|
|
my $upd = 1;
|
|
|
|
# hash sortieren und ausgeben, immer updaten, damit kurve angezeigt wird
|
|
readingsBeginUpdate($hash);
|
|
foreach my $xxx ( sort keys %hashValues ) # alle schluessel abfragen
|
|
{
|
|
readingsBulkUpdate( $hash, $xxx, $hashValues{$xxx} ); # alten zustand merken
|
|
KOSTALPIKO_Log $hash, 5, "$xxx: $hashValues{ $xxx } upd:$upd";
|
|
}
|
|
readingsEndUpdate( $hash, 1 );
|
|
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
}
|
|
#####################################
|
|
sub KOSTALPIKO_GrAborted($)
|
|
{
|
|
my ($hash) = @_;
|
|
delete( $hash->{helper}{RUNNING_GR} );
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
}
|
|
|
|
#####################################
|
|
sub KOSTALPIKO_GrTimer($)
|
|
{
|
|
my ($timerpara) = @_;
|
|
# my ( $name, $func ) = split( /\./, $timerpara );
|
|
my $index = rindex($timerpara,"."); # rechter punkt
|
|
my $func = substr $timerpara,$index+1,length($timerpara); # function extrahieren
|
|
my $name = substr $timerpara,0,$index; # name extrahieren
|
|
my $hash = $defs{$name};
|
|
|
|
return unless (defined($hash->{NAME}));
|
|
KOSTALPIKO_Log $hash, 3, "--- started ---";
|
|
|
|
$hash->{helper}{TimerGRInterval} = AttrVal( $name, "GR.Interval", 3600 );
|
|
|
|
KOSTALPIKO_GrStart($hash);
|
|
|
|
# setup timer
|
|
RemoveInternalTimer( $hash->{helper}{TimerGR} );
|
|
|
|
InternalTimer(
|
|
gettimeofday() + $hash->{helper}{TimerGRInterval},
|
|
"KOSTALPIKO_GrTimer",
|
|
$hash->{helper}{TimerGR},
|
|
0 );
|
|
|
|
KOSTALPIKO_Log $hash, 3, "--- done ---";
|
|
}
|
|
|
|
#####################################
|
|
1;
|
|
|
|
=pod
|
|
=begin html
|
|
|
|
<a name="KOSTALPIKO"></a>
|
|
<h3>KOSTALPIKO</h3>
|
|
<ul>
|
|
<a name="KOSTALPIKOdefine"></a>
|
|
<b>Define</b>
|
|
<ul>
|
|
<br/>
|
|
<code>define <name> KOSTALPIKO <ip-address> <user> <password></code>
|
|
<br/>
|
|
<br/>
|
|
The module reads the current values from web page of a Kostal Piko inverter.<br/>
|
|
It can also be used, to capture the values of global radiation, UV-index and sunshine duration<br/>
|
|
from a special web-site (proplanta) regardless of the existence of the inverter.<br/>
|
|
<br/>
|
|
|
|
<b>Parameters:</b><br/>
|
|
<ul>
|
|
<li><b><ip-address></b> - the ip address of the inverter</li>
|
|
<li><b><user></b> - the login-user for the inverter's web page</li>
|
|
<li><b><password></b> - the login-password for the inverter's web page</li>
|
|
</ul>
|
|
|
|
<br/><br/>
|
|
<b>Example:</b><br/>
|
|
<ul>
|
|
<code>define Kostal KOSTALPIKO 192.168.2.4 pvserver pvwr</code><br>
|
|
</ul>
|
|
</ul>
|
|
<br>
|
|
|
|
<a name="KOSTALPIKOset"></a>
|
|
<b>Set-Commands</b>
|
|
<ul>
|
|
|
|
<br/>
|
|
<code>set <name> captureGlobalRadiation</code>
|
|
<br/>
|
|
<ul>
|
|
The values for global radiation, UV-index and sunshine duration are immediately polled.
|
|
</ul><br/>
|
|
|
|
<br/>
|
|
<code>set <name> captureKostalData</code>
|
|
<br/>
|
|
<ul>
|
|
All values of the inverter are immediately polled.
|
|
</ul><br/>
|
|
</ul>
|
|
|
|
<a name="KOSTALPIKOattr"></a>
|
|
<b>Attributes</b><br/><br/>
|
|
<ul>
|
|
<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
|
|
<br/>
|
|
<li><b>GR.Interval</b> - poll interval for global radiation in seconds</li>
|
|
<li><b>GR.Link</b> - regionalised link the to the proplanta web page (global radiation, UV-index and sunshine duration)<br/>
|
|
(see Wiki for further information)
|
|
</li>
|
|
<li><b>delay</b> - poll interval for the values of the inverter in seconds</li>
|
|
<li><b>delayCounter</b> - delay counter for poll of invert's values beside AC.Power; <br/>
|
|
needed for fast acquisition scenarios to limit the log-output. </li>
|
|
<li><b>disable</b> - if disable=1, the poll of inverter's values is disabled,<br/>
|
|
but not the the poll of proplanta-values </li>
|
|
</ul>
|
|
<br/><br/>
|
|
|
|
<a name="KOSTALPIKOreading"></a>
|
|
<b>Generated Readings/Events</b><br/><br/>
|
|
<ul>
|
|
<li><b>AC.Power</b> - the current power, captured only if the internal delayCounter = 0</li>
|
|
<li><b>AC.Power.Fast</b> - the current power, on each poll cycle; used for fast acquisition scenarios </li>
|
|
<li><b>Daily.Energie</b> - the current procduced energie of the day </li>
|
|
<li><b>Daily.Energie.Last</b> - the value of daily energy at 23:00 clock</li>
|
|
<li><b>Global.Radiation</b> - the value of global radiation (proplanta);useful for determing the expected energy amount of the day</li>
|
|
<li><b>ModeNum</b> - the current processing state of the inverter (1=off 2=idle 3=active)</li>
|
|
<li><b>Mode</b> - the german term for the current ModeNum</li>
|
|
<li><b>Total.Energy</b> - the total produced energie</li>
|
|
|
|
<li><b>generator.1.current</b> - the electrical current at string 1 </li>
|
|
<li><b>generator.2.current</b> - the electrical current at string 2 </li>
|
|
<li><b>generator.3.current</b> - the electrical current at string 3 </li>
|
|
<li><b>generator.1.voltage</b> - the voltage at string 1 </li>
|
|
<li><b>generator.2.voltage</b> - the voltage at string 2 </li>
|
|
<li><b>generator.3.voltage</b> - the voltage at string 3 </li>
|
|
|
|
<li><b>output.1.voltage</b> - the voltage at output 1 </li>
|
|
<li><b>output.2.voltage</b> - the voltage at output 2 </li>
|
|
<li><b>output.3.voltage</b> - the voltage at output 3 </li>
|
|
<li><b>output.1.power</b> - the power at output 1 </li>
|
|
<li><b>output.2.power</b> - the power at output 2 </li>
|
|
<li><b>output.3.power</b> - the power at output 3 </li>
|
|
|
|
<li><b>sensor.1</b> - the voltage at analog input 1 </li>
|
|
<li><b>sensor.2</b> - the voltage at analog input 2 </li>
|
|
<li><b>sensor.3</b> - the voltage at analog input 3 </li>
|
|
<li><b>sensor.4</b> - the voltage at analog input 4 </li>
|
|
|
|
<li><b>UV.Index</b> - the UV Index (proplanta) </li>
|
|
<li><b>sunshine.duration</b> - the sunshine duration (proplanta) </li>
|
|
|
|
</ul>
|
|
<br/><br/>
|
|
|
|
<b>Additional information</b><br/><br/>
|
|
<ul>
|
|
<li><a href="http://forum.fhem.de/index.php/topic,24409.msg175253.html#msg175253">Discussion in FHEM forum</a></li><br/>
|
|
<li><a href="http://www.fhemwiki.de/wiki/KostalPiko#FHEM-Modul">Information in FHEM Wiki</a></li><br/>
|
|
</ul>
|
|
|
|
</ul>
|
|
|
|
=end html
|
|
=cut
|