############################################################################## # # 70_SolarView.pm # # A FHEM module to read power/energy values from solarview. # # written 2012 by Tobe Toben # # $Id$ # ############################################################################## # # SolarView is a powerful ;) datalogger for photovoltaic systems that runs on # an AVM Fritz!Box (and also on x86 systems). For details see the SV homepage: # http://www.solarview.info # # SV supports many different inverters. To read the SV power values using # this module, a TCP-Server must be enabled for SV by adding the parameter # "-TCP " to the startscript (see the SV manual). # # usage: # define SolarView [wr wr...] [ []] # # example: # define sv SolarView fritz.box 15000 wr1 wr2 60 # # If is positive, new values are read every seconds. # If is 0, new values are read whenever a get request is called # on . The default for is 300 (i.e. 5 minutes). # # The parameters wr specify the number(s) of the inverter(s) to be read. # When omitted, the sum of all inverters is read. If more than one inverter # is specified, the names of the readings are prefixed with the inverter # number, e.g. 'wr2_currentPower'. # # get [wr_] # # where is one of currentPower, totalEnergy, totalEnergyDay, # totalEnergyMonth, totalEnergyYear, UDC, IDC, UDCB, IDCB, UDCC, IDCC, # gridVoltage, gridCurrent and temperature. # ############################################################################## # # Copyright notice # # (c) 2012 Tobe Toben # # This script 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. # # The GNU General Public License can be found at # http://www.gnu.org/copyleft/gpl.html. # # This script 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. # # This copyright notice MUST APPEAR in all copies of the script! # ############################################################################## package main; use strict; use warnings; use IO::Socket::INET; my @gets = ('totalEnergyDay', # kWh 'totalEnergyMonth', # kWh 'totalEnergyYear', # kWh 'totalEnergy', # kWh 'currentPower', # W 'UDC', 'IDC', 'UDCB', # V, A, V 'IDCB', 'UDCC', 'IDCC', # A, V, A 'gridVoltage', 'gridCurrent', # V, A 'temperature'); # oC sub SolarView_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "SolarView_Define"; $hash->{UndefFn} = "SolarView_Undef"; $hash->{GetFn} = "SolarView_Get"; $hash->{AttrList} = "loglevel:0,1,2,3,4,5 event-on-update-reading event-on-change-reading"; } sub SolarView_Define($$) { my ($hash, $def) = @_; my @args = split("[ \t]+", $def); if (int(@args) < 4) { return "SolarView_Define: too few arguments. Usage:\n" . "define SolarView [wr wr...] [ []]"; } $hash->{Host} = $args[2]; $hash->{Port} = $args[3]; # collect the set of inverters which are to be read @{$hash->{Inverters}} = (0); while ((int(@args) >= 5) && ($args[4] =~ /^[Ww][Rr](\d+)$/)) { push @{$hash->{Inverters}}, $1 if int($1); splice(@args, 4, 1); } # remove WR0 if exactly one inverter has been specified shift @{$hash->{Inverters}} if (int(@{$hash->{Inverters}}) == 2); $hash->{Interval} = int(@args) >= 5 ? int($args[4]) : 300; $hash->{Timeout} = int(@args) >= 6 ? int($args[5]) : 4; # config variables $hash->{Invalid} = -1; # default value for invalid readings $hash->{Debounce} = 50; # minimum level for debouncing (0 to disable) $hash->{NightOff} = 'yes'; # skip reading at night? No sun, no power :-/ $hash->{UseSVNight} = 'yes'; # use the on/off timings from SV (else: SUNRISE_EL) $hash->{STATE} = 'Initializing'; readingsBeginUpdate($hash); # initialization for my $wr (@{$hash->{Inverters}}) { $hash->{SolarView_WR($hash, 'Debounced', $wr)} = 0; for my $get (@gets) { readingsBulkUpdate($hash, SolarView_WR($hash, $get, $wr), $hash->{Invalid}); } } readingsEndUpdate($hash, $init_done); SolarView_Update($hash); Log 2, "$hash->{NAME} will read from solarview at $hash->{Host}:$hash->{Port} " . ($hash->{Interval} ? "every $hash->{Interval} seconds" : "for every 'get $hash->{NAME} ' request"); return undef; } sub SolarView_Update($) { my ($hash) = @_; if ($hash->{Interval} > 0) { InternalTimer(gettimeofday() + $hash->{Interval}, "SolarView_Update", $hash, 0); } # if NightOff is set and there has been a successful # reading before, then skip this update "at night" # if ($hash->{NightOff} and SolarView_IsNight($hash) and $hash->{READINGS}{currentPower}{VAL} != $hash->{Invalid}) { $hash->{STATE} = '0 W, '.$hash->{READINGS}{totalEnergyDay}{VAL}.' kWh (Night)'; return undef; } Log 4, "$hash->{NAME} tries to contact solarview at $hash->{Host}:$hash->{Port}"; my $success = 0; # loop over all inverters for my $wr (@{$hash->{Inverters}}) { my %readings = (); my $retries = 2; eval { local $SIG{ALRM} = sub { die 'timeout'; }; alarm $hash->{Timeout}; READ_SV: my $socket = IO::Socket::INET->new(PeerAddr => $hash->{Host}, PeerPort => $hash->{Port}, Timeout => $hash->{Timeout}); if ($socket and $socket->connected()) { $socket->autoflush(1); printf $socket "%02d*\r\n", int($wr); my $res = <$socket>; close($socket); if ($res and $res =~ /^\{(\d\d,[^\}]+)\},/) { my @vals = split(/,/, $1); readingsBeginUpdate($hash); # parse the result from SV to dedicated values for my $i (6..19) { if (defined($vals[$i])) { $readings{$gets[$i - 6]} = 0 + $vals[$i]; } } # need to retry? if ($retries > 0 and $readings{currentPower} == 0) { sleep(1); $retries = $retries - 1; goto READ_SV; } # if Debounce is enabled (>0), then skip one! drop of # currentPower from 'greater than Debounce' to 'Zero' # if ($hash->{Debounce} > 0 and $hash->{Debounce} < $hash->{READINGS}{SolarView_WR($hash, 'currentPower', $wr)}{VAL} and $readings{currentPower} == 0 and not $hash->{SolarView_WR($hash, 'Debounced', $wr)}) { # revert to the previous value $readings{currentPower} = $hash->{READINGS}{SolarView_WR($hash, 'currentPower', $wr)}{VAL}; $hash->{SolarView_WR($hash, 'Debounced', $wr)} = 1; } else { $hash->{SolarView_WR($hash, 'Debounced', $wr)} = 0; } # update Readings for my $get (@gets) { readingsBulkUpdate($hash, SolarView_WR($hash, $get, $wr), $readings{$get}); } readingsEndUpdate($hash, $init_done); alarm 0; $success = 1; } # res okay } # socket okay }; # eval alarm 0; } # wr loop $hash->{STATE} = $hash->{READINGS}{currentPower}{VAL}.' W, '.$hash->{READINGS}{totalEnergyDay}{VAL}.' kWh'; if ($success) { Log 4, "$hash->{NAME} got fresh values from solarview"; } else { $hash->{STATE} .= ' (Fail)'; Log 4, "$hash->{NAME} was unable to get fresh values from solarview"; } return undef; } sub SolarView_Get($@) { my ($hash, @args) = @_; return 'SolarView_Get needs two arguments' if (@args != 2); SolarView_Update($hash) unless $hash->{Interval}; my $get = $args[1]; my $val = $hash->{Invalid}; if (defined($hash->{READINGS}{$get})) { $val = $hash->{READINGS}{$get}{VAL}; } else { return "SolarView_Get: no such reading: $get"; } Log 3, "$args[0] $get => $val"; return $val; } sub SolarView_Undef($$) { my ($hash, $args) = @_; RemoveInternalTimer($hash) if $hash->{Interval}; return undef; } sub SolarView_IsNight($) { my ($hash) = @_; my $isNight = 0; my ($sec,$min,$hour,$mday,$mon) = localtime(time); # reset totalEnergyX at midnight if ($hour == 0) { readingsBeginUpdate($hash); for my $wr (@{$hash->{Inverters}}) { readingsBulkUpdate($hash, SolarView_WR($hash, 'totalEnergyDay', $wr), 0); } if ($mday == 1) { for my $wr (@{$hash->{Inverters}}) { readingsBulkUpdate($hash, SolarView_WR($hash, 'totalEnergyMonth', $wr), 0); } if ($mon == 0) { for my $wr (@{$hash->{Inverters}}) { readingsBulkUpdate($hash, SolarView_WR($hash, 'totalEnergyYear', $wr), 0); } } } readingsEndUpdate($hash, $init_done); } if ($hash->{UseSVNight}) { # These are the on/off timings from Solarview, see # http://www.amhamberg.de/solarview-fb_Installieren.pdf # if ($mon == 0) { # Jan $isNight = ($hour < 7 or $hour > 17); } elsif ($mon == 1) { # Feb $isNight = ($hour < 7 or $hour > 18); } elsif ($mon == 2) { # Mar $isNight = ($hour < 6 or $hour > 19); } elsif ($mon == 3) { # Apr $isNight = ($hour < 5 or $hour > 20); } elsif ($mon == 4) { # May $isNight = ($hour < 5 or $hour > 21); } elsif ($mon == 5) { # Jun $isNight = ($hour < 5 or $hour > 21); } elsif ($mon == 6) { # Jul $isNight = ($hour < 5 or $hour > 21); } elsif ($mon == 7) { # Aug $isNight = ($hour < 5 or $hour > 21); } elsif ($mon == 8) { # Sep $isNight = ($hour < 6 or $hour > 20); } elsif ($mon == 9) { # Oct $isNight = ($hour < 7 or $hour > 19); } elsif ($mon == 10) { # Nov $isNight = ($hour < 7 or $hour > 17); } elsif ($mon == 11) { # Dec $isNight = ($hour < 8 or $hour > 16); } } else { # we use SUNRISE_EL $isNight = not isday(); } return $isNight; } # prefix the reading name with inverter number sub SolarView_WR($$$) { my ($hash, $reading, $wr) = @_; if ((int(@{$hash->{Inverters}}) > 1) && (int($wr) > 0)) { return sprintf("wr%s_%s", $wr, $reading); } else { return $reading; } } 1;