| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241 |
- # $Id: 23_KOSTALPIKO.pm 15368 2017-10-31 19:00:55Z 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
- # 2014-09-08 john V2.05 : support of battery option; developed by jannik_78
- # 2014-12-22 john V2.06 : checked HTML
- # 2015-01-25 john V2.07 : adjusted argument agent for http-request of proplanta (thanks to framller)
- # 2016-02-25 john V2.08 : support of Piko 7 with only 2 strings instead of 3 (thanks to erwin)
- # 2016-02-25 john V2.09 : substitution of term given/when with if/then
- # 2017-19-26 john V2.10 : support of https
- ####################################################################################################
- # --------------------------------------------
- # 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 site http://<ip-kostal>/BA.fhtml
- package MyBatteryParser;
- use base qw(HTML::Parser);
- our @texte = ();
- my $isTD = 0;
- my $isBold = 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
- {
- # filter only interesting captions
- if ( $text eq "Ladezustand:"
- || $text eq "Spannung:"
- || $text eq "Ladestrom:"
- || $text eq "Temperatur:"
- || $text eq "Zyklenanzahl:"
- || $text eq "Solargenerator:"
- || $text eq "Batterie:"
- || $text eq "Netz:"
- || $text eq "Phase 1:"
- || $text eq "Phase 2:"
- || $text eq "Phase 3:" )
- {
- $takeNext = 1; # expect next tag as value
- push( @texte, $text );
- }
- }
- if ( $isBold == 1 && $takeNext == 1 )
- {
- $takeNext = 0;
- $text =~ s/[^0-9\.]//g;
- push( @texte, $text );
- }
- }
- # 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;
- }
- ###############################################
- # 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.10";
- ########################################
- 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 " . "BAEnable: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] );
- if ( $cmd eq "?" )
- {
- return $usage;
- } elsif ( $cmd eq "capturekostaldata" )
- {
- KOSTALPIKO_Log $hash, 3, "set command: " . $a[1] . " para:" . $hash->{helper}{TimerStatus};
- KOSTALPIKO_StatusStart($hash);
- } elsif ( $cmd eq "captureglobalradiation" )
- {
- KOSTALPIKO_Log $hash, 3, "set command: " . $a[1];
- KOSTALPIKO_GrStart($hash);
- } elsif ( $cmd eq "test" )
- {
- KOSTALPIKO_Log $hash, 3, "set command: " . $a[1];
- KOSTALPIKO_GrStart($hash);
- } elsif ( $cmd eq "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];
- } else
- {
- 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 battery html page of kostalpiko
- sub KOSTALPIKO_BatteryHtmlAcquire($)
- {
- my ($hash) = @_;
- return unless ( defined( $hash->{NAME} ) );
- my $err_log = '';
- my $URL =
- "http://" . $hash->{helper}{User} . ":" . $hash->{helper}{Pass} . "\@" . $hash->{helper}{Host} . "/BA.fhtml";
- # $URL = "http://192.168.178.20/XBA.html"; # for testing only uncomment
- 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;
- }
- #---------------------------- battery values
- if ( AttrVal( $name, 'BAEnable', 0 ) == 1 )
- {
- $response = KOSTALPIKO_BatteryHtmlAcquire($hash);
- $parser = MyBatteryParser->new;
- @MyBatteryParser::texte = ();
- $parser->parse($response);
- foreach my $text (@MyBatteryParser::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) );
- # need to do this before split !!!
- my @nVoltages = $string =~ m/Spannung/g; ##MH how often did we find the word Spannung?
- my $strangCount = int( @nVoltages / 2 ); # the number of strings
- # 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 --- with numStrings:' . $strangCount;
- # 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; # gruppe String<n>/ L<n>
- 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.*/
- || $text eq "Ladezustand:"
- || $text eq "Spannung:"
- || $text eq "Ladestrom:"
- || $text eq "Temperatur:"
- || $text eq "Zyklenanzahl:"
- || $text eq "Solargenerator:"
- || $text eq "Batterie:"
- || $text eq "Netz:"
- || $text eq "Phase 1:"
- || $text eq "Phase 2:"
- || $text eq "Phase 3:" )
- {
- $tag = $text; # remember the identifier
- } elsif ( $text eq "Spannung" || $text eq "Strom" || $text eq "Leistung" )
- {
- $index++;
- # there are max 4 values per group
- 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" );
- # MH change for PIKO7 (2 Strings only / should work for 3 string PIKO's
- if ( $tag eq "Spannung" )
- {
- $rdName = "output.$strang.voltage" if ( $index == 2 );
- if ( $index == 1 )
- {
- if ( $strang <= $strangCount )
- {
- $rdName = "generator.$strang.voltage";
- } else
- {
- # useful for PIKO7 with 2 Strings only
- $rdName = "output.$strang.voltage";
- }
- }
- }
- $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:" );
- # BA.fhtml
- $rdName = "Battery.StateOfCharge" if ( $tag eq "Ladezustand:" );
- $rdName = "Battery.Voltage" if ( $tag eq "Spannung:" );
- $rdName = "Battery.ChargeCurrent" if ( $tag eq "Ladestrom:" );
- $rdName = "Battery.Temperature" if ( $tag eq "Temperatur:" );
- $rdName = "Battery.CycleCount" if ( $tag eq "Zyklenanzahl:" );
- $rdName = "Power.Solar" if ( $tag eq "Solargenerator:" );
- $rdName = "Power.Battery" if ( $tag eq "Batterie:" );
- $rdName = "Power.Net" if ( $tag eq "Netz:" );
- $rdName = "Power.Phase1" if ( $tag eq "Phase 1:" );
- $rdName = "Power.Phase2" if ( $tag eq "Phase 2:" );
- $rdName = "Power.Phase3" if ( $tag eq "Phase 3:" );
- # 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 $agent = LWP::UserAgent->new(
- env_proxy => 1,
- keep_alive => 1,
- protocols_allowed => ['http','https'],
- timeout => 10,
- agent => "Mozilla/5.0 (Windows NT 5.1) [de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4]"
- );
- 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
- =item summary Module for Kostal Piko Inverter
- =begin html
- <a name="KOSTALPIKO"></a>
- <h3>KOSTALPIKO</h3>
- <div>
- <a name="KOSTALPIKOdefine" id="KOSTALPIKOdefine"></a> <b>Define</b>
- <div>
- <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 />
- <div>
- <code>define Kostal KOSTALPIKO 192.168.2.4 pvserver pvwr</code><br />
- </div>
- </div><br />
- <a name="KOSTALPIKOset" id="KOSTALPIKOset"></a> <b>Set-Commands</b>
- <div>
- <br />
- <code>set <name> captureGlobalRadiation</code><br />
- <div>
- The values for global radiation, UV-index and sunshine duration are immediately polled.
- </div><br />
- <br />
- <code>set <name> captureKostalData</code><br />
- <div>
- All values of the inverter are immediately polled.
- </div><br />
- </div><a name="KOSTALPIKOattr" id="KOSTALPIKOattr"></a> <b>Attributes</b><br />
- <br />
- <ul>
- <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
- <li><b>BAEnable</b> - if 1, data from ../BA.fhtml site is captured</li>
- <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 /> ut not the the poll of proplanta-values</li>
- </ul><br />
- <br />
- <a name="KOSTALPIKOreading" id="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 />
- <b>Additional Readings/Events, if BAEnable=1</b><br />
- <br />
- <ul>
- <li><b>Battery.CycleCount</b> - count of charge cycles</li>
- <li><b>Battery.StateOfCharge</b> - State of charge for the battery in percent</li>
- <li><b>Battery.Voltage</b> - the voltage of the battery</li>
- <li><b>Battery.ChargeCurrent</b> - the charge current of the battery</li>
- <li><b>Battery.Temperature</b> - the temperature of the battery</li>
- <li><b>Power.Solar</b> - the sum of the power produced by the solarinverter</li>
- <li><b>Power.Battery</b> - the power drawn from the battery</li>
- <li><b>Power.Net</b> - the power drawn from the main</li>
- <li><b>Power.Phase1</b> - the power used on phase L1</li>
- <li><b>Power.Phase2</b> - the power used on phase L2</li>
- <li><b>Power.Phase3</b> - the power used on phase L3</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>
- <li><a href="http://www.fhemwiki.de/wiki/KostalPiko#FHEM-Modul">Information in FHEM Wiki</a></li>
- </ul>
- </div>
- =end html
- =cut
|