70_SolarView.pm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. ##############################################################################
  2. #
  3. # 70_SolarView.pm
  4. #
  5. # A FHEM module to read power/energy values from solarview.
  6. #
  7. # written 2012 by Tobe Toben <fhem@toben.net>
  8. #
  9. # $Id: 70_SolarView.pm 2106 2012-11-10 16:38:07Z borisneubert $
  10. #
  11. ##############################################################################
  12. #
  13. # SolarView is a powerful ;) datalogger for photovoltaic systems that runs on
  14. # an AVM Fritz!Box (and also on x86 systems). For details see the SV homepage:
  15. # http://www.solarview.info
  16. #
  17. # SV supports many different inverters. To read the SV power values using
  18. # this module, a TCP-Server must be enabled for SV by adding the parameter
  19. # "-TCP <port>" to the startscript (see the SV manual).
  20. #
  21. # usage:
  22. # define <name> SolarView <host> <port> [wr<i> wr...] [<interval> [<timeout>]]
  23. #
  24. # example:
  25. # define sv SolarView fritz.box 15000 wr1 wr2 60
  26. #
  27. # If <interval> is positive, new values are read every <interval> seconds.
  28. # If <interval> is 0, new values are read whenever a get request is called
  29. # on <name>. The default for <interval> is 300 (i.e. 5 minutes).
  30. #
  31. # The parameters wr<i> specify the number(s) of the inverter(s) to be read.
  32. # When omitted, the sum of all inverters is read. If more than one inverter
  33. # is specified, the names of the readings are prefixed with the inverter
  34. # number, e.g. 'wr2_currentPower'.
  35. #
  36. # get <name> [wr<i>_]<key>
  37. #
  38. # where <key> is one of currentPower, totalEnergy, totalEnergyDay,
  39. # totalEnergyMonth, totalEnergyYear, UDC, IDC, UDCB, IDCB, UDCC, IDCC,
  40. # gridVoltage, gridCurrent and temperature.
  41. #
  42. ##############################################################################
  43. #
  44. # Copyright notice
  45. #
  46. # (c) 2012 Tobe Toben <fhem@toben.net>
  47. #
  48. # This script is free software; you can redistribute it and/or modify
  49. # it under the terms of the GNU General Public License as published by
  50. # the Free Software Foundation; either version 2 of the License, or
  51. # (at your option) any later version.
  52. #
  53. # The GNU General Public License can be found at
  54. # http://www.gnu.org/copyleft/gpl.html.
  55. #
  56. # This script is distributed in the hope that it will be useful,
  57. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  58. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  59. # GNU General Public License for more details.
  60. #
  61. # This copyright notice MUST APPEAR in all copies of the script!
  62. #
  63. ##############################################################################
  64. package main;
  65. use strict;
  66. use warnings;
  67. use IO::Socket::INET;
  68. my @gets = ('totalEnergyDay', # kWh
  69. 'totalEnergyMonth', # kWh
  70. 'totalEnergyYear', # kWh
  71. 'totalEnergy', # kWh
  72. 'currentPower', # W
  73. 'UDC', 'IDC', 'UDCB', # V, A, V
  74. 'IDCB', 'UDCC', 'IDCC', # A, V, A
  75. 'gridVoltage', 'gridCurrent', # V, A
  76. 'temperature'); # oC
  77. sub
  78. SolarView_Initialize($)
  79. {
  80. my ($hash) = @_;
  81. $hash->{DefFn} = "SolarView_Define";
  82. $hash->{UndefFn} = "SolarView_Undef";
  83. $hash->{GetFn} = "SolarView_Get";
  84. $hash->{AttrList} = "loglevel:0,1,2,3,4,5 event-on-update-reading event-on-change-reading";
  85. }
  86. sub
  87. SolarView_Define($$)
  88. {
  89. my ($hash, $def) = @_;
  90. my @args = split("[ \t]+", $def);
  91. if (int(@args) < 4)
  92. {
  93. return "SolarView_Define: too few arguments. Usage:\n" .
  94. "define <name> SolarView <host> <port> [wr<i> wr...] [<interval> [<timeout>]]";
  95. }
  96. $hash->{Host} = $args[2];
  97. $hash->{Port} = $args[3];
  98. # collect the set of inverters which are to be read
  99. @{$hash->{Inverters}} = (0);
  100. while ((int(@args) >= 5) && ($args[4] =~ /^[Ww][Rr](\d+)$/))
  101. {
  102. push @{$hash->{Inverters}}, $1 if int($1);
  103. splice(@args, 4, 1);
  104. }
  105. # remove WR0 if exactly one inverter has been specified
  106. shift @{$hash->{Inverters}} if (int(@{$hash->{Inverters}}) == 2);
  107. $hash->{Interval} = int(@args) >= 5 ? int($args[4]) : 300;
  108. $hash->{Timeout} = int(@args) >= 6 ? int($args[5]) : 4;
  109. # config variables
  110. $hash->{Invalid} = -1; # default value for invalid readings
  111. $hash->{Debounce} = 50; # minimum level for debouncing (0 to disable)
  112. $hash->{NightOff} = 'yes'; # skip reading at night? No sun, no power :-/
  113. $hash->{UseSVNight} = 'yes'; # use the on/off timings from SV (else: SUNRISE_EL)
  114. $hash->{STATE} = 'Initializing';
  115. readingsBeginUpdate($hash);
  116. # initialization
  117. for my $wr (@{$hash->{Inverters}})
  118. {
  119. $hash->{SolarView_WR($hash, 'Debounced', $wr)} = 0;
  120. for my $get (@gets)
  121. {
  122. readingsBulkUpdate($hash, SolarView_WR($hash, $get, $wr), $hash->{Invalid});
  123. }
  124. }
  125. readingsEndUpdate($hash, $init_done);
  126. SolarView_Update($hash);
  127. Log 2, "$hash->{NAME} will read from solarview at $hash->{Host}:$hash->{Port} " .
  128. ($hash->{Interval} ? "every $hash->{Interval} seconds" : "for every 'get $hash->{NAME} <key>' request");
  129. return undef;
  130. }
  131. sub
  132. SolarView_Update($)
  133. {
  134. my ($hash) = @_;
  135. if ($hash->{Interval} > 0) {
  136. InternalTimer(gettimeofday() + $hash->{Interval}, "SolarView_Update", $hash, 0);
  137. }
  138. # if NightOff is set and there has been a successful
  139. # reading before, then skip this update "at night"
  140. #
  141. if ($hash->{NightOff} and SolarView_IsNight($hash) and
  142. $hash->{READINGS}{currentPower}{VAL} != $hash->{Invalid})
  143. {
  144. $hash->{STATE} = '0 W, '.$hash->{READINGS}{totalEnergyDay}{VAL}.' kWh (Night)';
  145. return undef;
  146. }
  147. Log 4, "$hash->{NAME} tries to contact solarview at $hash->{Host}:$hash->{Port}";
  148. my $success = 0;
  149. # loop over all inverters
  150. for my $wr (@{$hash->{Inverters}})
  151. {
  152. my %readings = ();
  153. my $retries = 2;
  154. eval {
  155. local $SIG{ALRM} = sub { die 'timeout'; };
  156. alarm $hash->{Timeout};
  157. READ_SV:
  158. my $socket = IO::Socket::INET->new(PeerAddr => $hash->{Host},
  159. PeerPort => $hash->{Port},
  160. Timeout => $hash->{Timeout});
  161. if ($socket and $socket->connected())
  162. {
  163. $socket->autoflush(1);
  164. printf $socket "%02d*\r\n", int($wr);
  165. my $res = <$socket>;
  166. close($socket);
  167. if ($res and $res =~ /^\{(\d\d,[^\}]+)\},/)
  168. {
  169. my @vals = split(/,/, $1);
  170. readingsBeginUpdate($hash);
  171. # parse the result from SV to dedicated values
  172. for my $i (6..19)
  173. {
  174. if (defined($vals[$i]))
  175. {
  176. $readings{$gets[$i - 6]} = 0 + $vals[$i];
  177. }
  178. }
  179. # need to retry?
  180. if ($retries > 0 and $readings{currentPower} == 0)
  181. {
  182. sleep(1);
  183. $retries = $retries - 1;
  184. goto READ_SV;
  185. }
  186. # if Debounce is enabled (>0), then skip one! drop of
  187. # currentPower from 'greater than Debounce' to 'Zero'
  188. #
  189. if ($hash->{Debounce} > 0 and
  190. $hash->{Debounce} < $hash->{READINGS}{SolarView_WR($hash, 'currentPower', $wr)}{VAL} and
  191. $readings{currentPower} == 0 and not $hash->{SolarView_WR($hash, 'Debounced', $wr)})
  192. {
  193. # revert to the previous value
  194. $readings{currentPower} = $hash->{READINGS}{SolarView_WR($hash, 'currentPower', $wr)}{VAL};
  195. $hash->{SolarView_WR($hash, 'Debounced', $wr)} = 1;
  196. } else {
  197. $hash->{SolarView_WR($hash, 'Debounced', $wr)} = 0;
  198. }
  199. # update Readings
  200. for my $get (@gets)
  201. {
  202. readingsBulkUpdate($hash, SolarView_WR($hash, $get, $wr), $readings{$get});
  203. }
  204. readingsEndUpdate($hash, $init_done);
  205. alarm 0;
  206. $success = 1;
  207. } # res okay
  208. } # socket okay
  209. }; # eval
  210. alarm 0;
  211. } # wr loop
  212. $hash->{STATE} = $hash->{READINGS}{currentPower}{VAL}.' W, '.$hash->{READINGS}{totalEnergyDay}{VAL}.' kWh';
  213. if ($success) {
  214. Log 4, "$hash->{NAME} got fresh values from solarview";
  215. } else {
  216. $hash->{STATE} .= ' (Fail)';
  217. Log 4, "$hash->{NAME} was unable to get fresh values from solarview";
  218. }
  219. return undef;
  220. }
  221. sub
  222. SolarView_Get($@)
  223. {
  224. my ($hash, @args) = @_;
  225. return 'SolarView_Get needs two arguments' if (@args != 2);
  226. SolarView_Update($hash) unless $hash->{Interval};
  227. my $get = $args[1];
  228. my $val = $hash->{Invalid};
  229. if (defined($hash->{READINGS}{$get})) {
  230. $val = $hash->{READINGS}{$get}{VAL};
  231. } else {
  232. return "SolarView_Get: no such reading: $get";
  233. }
  234. Log 3, "$args[0] $get => $val";
  235. return $val;
  236. }
  237. sub
  238. SolarView_Undef($$)
  239. {
  240. my ($hash, $args) = @_;
  241. RemoveInternalTimer($hash) if $hash->{Interval};
  242. return undef;
  243. }
  244. sub
  245. SolarView_IsNight($)
  246. {
  247. my ($hash) = @_;
  248. my $isNight = 0;
  249. my ($sec,$min,$hour,$mday,$mon) = localtime(time);
  250. # reset totalEnergyX at midnight
  251. if ($hour == 0)
  252. {
  253. readingsBeginUpdate($hash);
  254. for my $wr (@{$hash->{Inverters}})
  255. {
  256. readingsBulkUpdate($hash, SolarView_WR($hash, 'totalEnergyDay', $wr), 0);
  257. }
  258. if ($mday == 1)
  259. {
  260. for my $wr (@{$hash->{Inverters}})
  261. {
  262. readingsBulkUpdate($hash, SolarView_WR($hash, 'totalEnergyMonth', $wr), 0);
  263. }
  264. if ($mon == 0)
  265. {
  266. for my $wr (@{$hash->{Inverters}})
  267. {
  268. readingsBulkUpdate($hash, SolarView_WR($hash, 'totalEnergyYear', $wr), 0);
  269. }
  270. }
  271. }
  272. readingsEndUpdate($hash, $init_done);
  273. }
  274. if ($hash->{UseSVNight})
  275. {
  276. # These are the on/off timings from Solarview, see
  277. # http://www.amhamberg.de/solarview-fb_Installieren.pdf
  278. #
  279. if ($mon == 0) { # Jan
  280. $isNight = ($hour < 7 or $hour > 17);
  281. } elsif ($mon == 1) { # Feb
  282. $isNight = ($hour < 7 or $hour > 18);
  283. } elsif ($mon == 2) { # Mar
  284. $isNight = ($hour < 6 or $hour > 19);
  285. } elsif ($mon == 3) { # Apr
  286. $isNight = ($hour < 5 or $hour > 20);
  287. } elsif ($mon == 4) { # May
  288. $isNight = ($hour < 5 or $hour > 21);
  289. } elsif ($mon == 5) { # Jun
  290. $isNight = ($hour < 5 or $hour > 21);
  291. } elsif ($mon == 6) { # Jul
  292. $isNight = ($hour < 5 or $hour > 21);
  293. } elsif ($mon == 7) { # Aug
  294. $isNight = ($hour < 5 or $hour > 21);
  295. } elsif ($mon == 8) { # Sep
  296. $isNight = ($hour < 6 or $hour > 20);
  297. } elsif ($mon == 9) { # Oct
  298. $isNight = ($hour < 7 or $hour > 19);
  299. } elsif ($mon == 10) { # Nov
  300. $isNight = ($hour < 7 or $hour > 17);
  301. } elsif ($mon == 11) { # Dec
  302. $isNight = ($hour < 8 or $hour > 16);
  303. }
  304. } else { # we use SUNRISE_EL
  305. $isNight = not isday();
  306. }
  307. return $isNight;
  308. }
  309. # prefix the reading name with inverter number
  310. sub
  311. SolarView_WR($$$)
  312. {
  313. my ($hash, $reading, $wr) = @_;
  314. if ((int(@{$hash->{Inverters}}) > 1) && (int($wr) > 0))
  315. {
  316. return sprintf("wr%s_%s", $wr, $reading);
  317. }
  318. else
  319. {
  320. return $reading;
  321. }
  322. }
  323. 1;