From eae6ebcf1c73259d2b57c1e2c7261ee20fb9ffb1 Mon Sep 17 00:00:00 2001 From: vbs2 <> Date: Thu, 28 Jan 2016 17:41:14 +0000 Subject: [PATCH] 98_STOCKQUOTES: added new module git-svn-id: https://svn.fhem.de/fhem/trunk@10652 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/98_STOCKQUOTES.pm | 641 ++++++++++++++++++++++++++++++++++++ fhem/MAINTAINER.txt | 1 + 2 files changed, 642 insertions(+) create mode 100644 fhem/FHEM/98_STOCKQUOTES.pm diff --git a/fhem/FHEM/98_STOCKQUOTES.pm b/fhem/FHEM/98_STOCKQUOTES.pm new file mode 100644 index 000000000..38807f051 --- /dev/null +++ b/fhem/FHEM/98_STOCKQUOTES.pm @@ -0,0 +1,641 @@ +# $Id$ + +package main; + +use strict; +use warnings; +use Blocking; +use Finance::Quote; +use Encode qw(decode encode); + +sub STOCKQUOTES_Initialize($) +{ + my ($hash) = @_; + + $hash->{DefFn} = "STOCKQUOTES_Define"; + $hash->{UndefFn} = "STOCKQUOTES_Undefine"; + $hash->{SetFn} = "STOCKQUOTES_Set"; + $hash->{GetFn} = "STOCKQUOTES_Get"; + $hash->{AttrFn} = "STOCKQUOTES_Attr"; + $hash->{AttrList} = "pollInterval queryTimeout defaultSource sources stocks currency $main::readingFnAttributes"; +} + +sub STOCKQUOTES_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + if (scalar(@a) != 2) { + return "Invalid arguments! Define as 'define STOCKQUOTES'"; + } + + $attr{$hash->{NAME}}{"pollInterval"} = 300; + $attr{$hash->{NAME}}{"queryTimeout"} = 120; + $attr{$hash->{NAME}}{"defaultSource"} = "europe"; + $attr{$hash->{NAME}}{"currency"} = "EUR"; + + $hash->{QUOTER} = Finance::Quote->new; + $hash->{QUOTER}->timeout(300); # Cancel fetch operation if it takes + + readingsSingleUpdate($hash, "state", "Initialized",1); + + STOCKQUOTES_UpdateCurrency($hash); + STOCKQUOTES_QueueTimer($hash, 5); + + return undef; +} + +sub STOCKQUOTES_Attr(@) +{ + my ($cmd,$name,$aName,$aVal) = @_; + my $hash = $defs{$name}; + if($aName eq "currency") { + return STOCKQUOTES_UpdateCurrency($hash, $aVal); + } + elsif($aName eq "sources") { + return STOCKQUOTES_ClearReadings($hash); + } + + return undef; +} + +sub STOCKQUOTES_UpdateCurrency($;$) +{ + my ($hash, $cur) = @_; + $cur = AttrVal($hash->{NAME}, "currency", "") if not defined $cur; + Log3 $hash->{NAME}, 4, "STOCKQUOTES_UpdateCurrency to $cur"; + $hash->{QUOTER}->set_currency($cur); + + # delete all readings for the previous currency + STOCKQUOTES_DeleteReadings($hash, undef); + + return undef; +} + +sub STOCKQUOTES_Undefine($$) +{ + my ($hash, $arg) = @_; + + RemoveInternalTimer($hash); + BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID})); + + return undef; +} + +sub STOCKQUOTES_SetStockHashes($$) +{ + my ($hash, $stocks) = @_; + + my $str = ""; + my $first = 1; + foreach my $ex (keys %{ $stocks }) { + $str .= "," unless $first; + $first = 0; + Log3 $hash->{NAME}, 4, "KEY: $ex"; + + $str .= $ex . ":" . $stocks->{$ex}[0] . ":" . $stocks->{$ex}[1]; + } + + Log3 $hash->{NAME}, 5, "STOCKQUOTES_SetStockHashes: $str"; + $attr{$hash->{NAME}}{"stocks"} = $str; + + return undef; +} + +sub STOCKQUOTES_GetStockHashes($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + my @stocks = split ',', AttrVal($name, "stocks", ""); + + my %stockHash = (); + + foreach my $stock (@stocks) { + my @toks = split ":", $stock; + $stockHash{$toks[0]} = [$toks[1], $toks[2]]; + } + return \%stockHash; +} + +sub STOCKQUOTES_ClearReadings($) +{ + my ($hash, $stockName) = @_; + delete $hash->{READINGS}; + return undef; +} + +sub STOCKQUOTES_DeleteReadings($$) +{ + my ($hash, $prefix) = @_; + + my $delStr = defined($prefix) ? ".*" . $prefix . "_.*" : ".*"; + fhem("deletereading $hash->{NAME} $delStr", 1); + return undef; +} + +sub STOCKQUOTES_RemoveStock($$) +{ + my ($hash, $stockName) = @_; + + my $stocks = STOCKQUOTES_GetStockHashes($hash); + + if (not exists $stocks->{$stockName}) { + return "There is no stock named '$stockName' to delete!"; + } + + Log3 $hash->{NAME}, 3, "STOCKQUOTES_RemoveStock: Removing $stockName"; + delete $stocks->{$stockName}; + if (not exists $stocks->{$stockName}) { + Log3 $hash->{NAME}, 3, "DELETED"; + } + + STOCKQUOTES_SetStockHashes($hash, $stocks); + + STOCKQUOTES_DeleteReadings($hash, $stockName); + + return undef; +} + +sub STOCKQUOTES_ChangeAmount($$$$) +{ + my ($hash, $stockName, $amount, $price) = @_; + + my $stocks = STOCKQUOTES_GetStockHashes($hash); + + if (exists $stocks->{$stockName}) { + $stocks->{$stockName}->[0] += $amount; + $stocks->{$stockName}->[0] = 0 if ($stocks->{$stockName}[0] < 0); + $stocks->{$stockName}->[1] += $price; + + if ($stocks->{$stockName}->[0] == 0) + { + Log3 $hash->{NAME}, 3, "STOCKQUOTES_ChangeAmount: Amount set to 0. Removing stock: $stockName"; + delete $stocks->{$stockName}; + STOCKQUOTES_DeleteReadings($hash, $stockName); + } + } + else { + $stocks->{$stockName}->[0] = $amount; + $stocks->{$stockName}->[1] = $price; + } + + STOCKQUOTES_SetStockHashes($hash, $stocks); + + STOCKQUOTES_QueueTimer($hash, 0); + + return undef; +} + +sub STOCKQUOTES_Set($@) +{ + my ($hash, $name, $cmd, @args) = @_; + if($cmd eq "buy" or $cmd eq "sell") { + if (scalar(@args) != 3) { + return "Invalid arguments! Usage 'set $name $cmd "; + } + my $stockName = $args[0]; + my $amount = $args[1]; + my $price = $args[2]; + my $fac = ($cmd eq "buy") ? 1 : -1; + my $str = STOCKQUOTES_ChangeAmount($hash, $stockName, $fac * $amount, $fac * $price); + STOCKQUOTES_QueueTimer($hash, 0); + return $str; + } + elsif($cmd eq "add") { + if (scalar(@args) != 1) { + return "Invalid arguments! Usage 'set $name add "; + } + return STOCKQUOTES_ChangeAmount($hash, $args[0], 0 ,0); + } + elsif($cmd eq "remove") { + if (scalar(@args) != 1) { + return "Invalid arguments! Usage 'set $name remove "; + } + return STOCKQUOTES_RemoveStock($hash, $args[0]); + } + elsif($cmd eq "update") { + return STOCKQUOTES_QueueTimer($hash, 0); + } + elsif($cmd eq "clearReadings") { + return STOCKQUOTES_ClearReadings($hash); + } + + my $res = "Unknown argument " . $cmd . ", choose one of " . + "update buy sell add remove clearReadings"; + return $res ; +} + +sub STOCKQUOTES_Get($@) +{ + my ($hash, $name, $cmd, @args) = @_; + if($cmd eq "sources") { + if (scalar(@args) != 0) { + return "Invalid arguments! Usage 'get $name $cmd'"; + } + return "Available sources: " . join("\n", $hash->{QUOTER}->sources()); + } + elsif($cmd eq "currency") { + if (scalar(@args) != 1) { + return "Invalid arguments! Usage 'get $name $cmd '"; + } + my $currs = $hash->{QUOTER}->currency_lookup( name => $args[0] ); + return "Found currencies: " . join(",", keys %{ $currs }); + } + + my $res = "Unknown argument " . $cmd . ", choose one of " . + "sources currency"; + return $res ; +} + +sub STOCKQUOTES_QueueTimer($$) +{ + my ($hash, $pollInt) = @_; + Log3 $hash->{NAME}, 4, "STOCKQUOTES_QueueTimer: $pollInt seconds"; + + RemoveInternalTimer($hash); + delete($hash->{helper}{RUNNING_PID}); + InternalTimer(time() + $pollInt, "STOCKQUOTES_QueryQuotes", $hash, 0); + + return undef; +} + +sub STOCKQUOTES_QueryQuotes($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + if (not exists($hash->{helper}{RUNNING_PID})) { + Log3 $hash->{NAME}, 4, 'STOCKQUOTES: Start blocking query'; + readingsSingleUpdate($hash, "state", "Updating",1); + $hash->{helper}{RUNNING_PID} = BlockingCall("STOCKQUOTES_QueryQuotesBlocking", + $hash, + "STOCKQUOTES_QueryQuotesFinished", + AttrVal($hash, "queryTimeout", 120), + "STOCKQUOTES_QueryQuotesAbort", + $hash); + } + else { + Log3 $hash->{NAME}, 4, 'STOCKQUOTES_QueryQuotes: Blocking not started because still one running'; + } + + return undef; +} + +# return the source that should be used for a stock +sub STOCKQUOTES_GetSource($$) +{ + my ($hash, $stock) = @_; + my $name = $hash->{NAME}; + my @exs = split ",", AttrVal($name, "sources", ""); + + my %exHash = (); + foreach my $ex (@exs) { + my @tok = split ":", $ex; + $exHash{$tok[0]} = $tok[1]; + } + + if (exists($exHash{$stock})) { + return $exHash{$stock}; + } + + return AttrVal($name, "defaultSource", "europe"); +} + +sub STOCKQUOTES_QueryQuotesBlocking($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 4, 'STOCKQUOTES_QueryQuotesBlocking'; + + my $stocks = STOCKQUOTES_GetStockHashes($hash); + + my %sources = (); + foreach my $symbol (keys %{ $stocks }) { + my @toks = split ':', $symbol; + my $symbName = $toks[0]; + + my $targetSource = STOCKQUOTES_GetSource($hash, $symbName); + if (not exists $sources{$targetSource}) { + $sources{$targetSource} = (); + } + push(@{$sources{$targetSource}}, $symbName); + + Log3 $name, 4, "STOCKQUOTES_QueryQuotesBlocking: Query stockname: $symbName from source $targetSource"; + } + + my $ret = $hash->{NAME}; + foreach my $srcKey (keys %sources) { + Log3 $name, 4, "STOCKQUOTES_QueryQuotesBlocking: Fetching from source: $srcKey"; + my %quotes = $hash->{QUOTER}->fetch($srcKey, @{$sources{$srcKey}}); + + foreach my $tag (keys %quotes) { + my @keys = split($;, $tag); + + next if $quotes{$keys[0], 'success'} != 1; + + my $val = $quotes{$keys[0], $keys[1]}; + next if (not defined $val); + + $ret .= "|" . join("&", @keys) . "&"; + $val = encode('UTF-8', $val, Encode::FB_CROAK) if ($keys[1] eq "name"); + $ret .= $val; + } + } + + Log3 $name, 4, 'STOCKQUOTES_QueryQuotesBlocking Return value: ' . $ret; + + #$ret = "myC|A0M16S¤cy&EUR|A0M16S&last&125.94|A0M16S&errormsg&|A0M16S&symbol&LU0321021155|A0M16S&time&17:52|A0M16S&isodate&2015-02-16|A0M16S&name&VERMöGENSMANAGEMENT BALANCE A€|A0M16S&source&VWD|A0M16S&price&125.94|A0M16S&date&02/16/2015|A0M16S&success&1"; + return $ret; +} + +sub STOCKQUOTES_QueryQuotesAbort($$$) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 $name, 3, 'STOCKQUOTES_QueryQuotesAbort: Blocking call aborted due to timeout!'; + readingsSingleUpdate($hash, "state", "Update aborted",1); + + delete($hash->{helper}{RUNNING_PID}); + STOCKQUOTES_QueueTimer($hash, AttrVal($name, "pollInterval", 300)); + + return undef; +} + +sub STOCKQUOTES_QueryQuotesFinished($) +{ + my ($string) = @_; + + return unless(defined($string)); + + my @a = split("\\|",$string); + my $name = $a[0]; + my $hash = $defs{$name}; + Log3 $name, 4, 'STOCKQUOTES_QueryQuotesFinished'; + delete($hash->{helper}{RUNNING_PID}); + + my $stocks = STOCKQUOTES_GetStockHashes($hash); + + my %stockState = (); + readingsBeginUpdate($hash); + for my $i (1 .. $#a) + { + my @toks = split '&',$a[$i]; + + # HACK: replace "3.2%" with "3.2" since we dont want units + chop $toks[2] if ($toks[1] eq "p_change" and $toks[2] =~ /%$/); + + readingsBulkUpdate($hash, $toks[0] . "_" . $toks[1], $toks[2]); + + # build a hash filled with current values + $stockState{$toks[0]}{$toks[1]} = $toks[2]; + } + readingsEndUpdate($hash, 1); + + # build depot status + readingsBeginUpdate($hash); + + foreach my $i (keys %stockState) { + # we assume that every stockname is also in our stocks-hash. Otherwise something went terribly wrong + my $stockCount = $stocks->{$i}->[0]; + my $stockBuyPrice = $stocks->{$i}->[1]; + my $last = (exists $stockState{$i}{"last"}) ? $stockState{$i}{"last"} : undef; + my $previous = (exists $stockState{$i}{"previous"}) ? $stockState{$i}{"previous"} : undef; + my $stockValue = (defined $last) ? $stockCount * $last : undef; + my $stockValuePrev = (defined $previous) ? $stockCount * $previous : undef; + + # statics + readingsBulkUpdate($hash, $i . "_d_stockcount", $stockCount); + readingsBulkUpdate($hash, $i . "_d_buy_value_total", $stockBuyPrice); + readingsBulkUpdate($hash, $i . "_d_buy_quote", ($stockCount == 0) ? 0 : sprintf("%.2f", $stockBuyPrice / $stockCount)); + # end + + if (defined($stockValue) && defined($stockValuePrev)) + { + readingsBulkUpdate($hash, $i . "_d_cur_value_total", sprintf("%.2f", $stockValue)) if defined $stockValue; + readingsBulkUpdate($hash, $i . "_d_prev_value_total", sprintf("%.2f", $stockValuePrev)) if defined $stockValuePrev; + readingsBulkUpdate($hash, $i . "_d_value_diff_total", sprintf("%.2f", $stockValue - $stockBuyPrice)) if defined $stockValue; + readingsBulkUpdate($hash, $i . "_d_p_change_total", ($stockBuyPrice == 0) ? 0 : sprintf("%.2f", 100.0 * (($stockValue / $stockBuyPrice) - 1 ))) if defined $stockValue; + + my $valueDiff = (defined $previous and defined $last) ? $stockCount * ($last - $previous) : undef; + readingsBulkUpdate($hash, $i . "_d_value_diff", sprintf("%.2f", $valueDiff)) if defined $valueDiff; + } + } + + # update depot data + my %depotSummary = (); + $depotSummary{"depot_cur_value_total"} = 0; + $depotSummary{"depot_prev_value_total"} = 0; + $depotSummary{"depot_value_diff"} = 0; + $depotSummary{"depot_buy_value_total"} = 0; + foreach my $i (keys %stockState) { + $depotSummary{"depot_buy_value_total"} += ReadingsVal($name, $i . "_d_buy_value_total", 0); + $depotSummary{"depot_cur_value_total"} += ReadingsVal($name, $i . "_d_cur_value_total", 0); + $depotSummary{"depot_prev_value_total"} += ReadingsVal($name, $i . "_d_prev_value_total", 0); + $depotSummary{"depot_value_diff"} += ReadingsVal($name, $i . "_d_value_diff", 0); + } + + readingsBulkUpdate($hash, "depot_buy_value_total", $depotSummary{"depot_buy_value_total"}); + readingsBulkUpdate($hash, "depot_cur_value_total", $depotSummary{"depot_cur_value_total"}); + readingsBulkUpdate($hash, "depot_value_diff_total", sprintf("%.2f", $depotSummary{"depot_cur_value_total"} - $depotSummary{"depot_buy_value_total"})); + readingsBulkUpdate($hash, "depot_value_diff", sprintf("%.2f", $depotSummary{"depot_value_diff"})); + + my $depot_p_change = 0.0; + if ($depotSummary{"depot_prev_value_total"} > 0.0) { + $depot_p_change = sprintf("%.2f", 100.0 * (($depotSummary{"depot_cur_value_total"} / $depotSummary{"depot_prev_value_total"}) - 1 )); + } + readingsBulkUpdate($hash, "depot_p_change", $depot_p_change); + + my $depot_p_change_total = 0.0; + if ($depotSummary{"depot_buy_value_total"} > 0.0) { + $depot_p_change_total = sprintf("%.2f", 100.0 * (($depotSummary{"depot_cur_value_total"} / $depotSummary{"depot_buy_value_total"}) - 1 )); + } + readingsBulkUpdate($hash, "depot_p_change_total", $depot_p_change_total); + + my $now = gettimeofday(); + my $fmtDateTime = FmtDateTime($now); + readingsBulkUpdate($hash, "state", $fmtDateTime); + readingsEndUpdate($hash, 1); + + STOCKQUOTES_QueueTimer($hash, AttrVal($name, "pollInterval", 300)); + + return undef; +} + +1; + +=pod +=begin html + + +

STOCKQUOTES

+(en | de) +
    + + Fetching actual stock quotes from various sources
    + Preliminary
    + Perl module Finance::Quote must be installed:
    + cpan install Finance::Quote or sudo apt-get install libfinance-quote-perl

    + + Define +
      + define Depot STOCKQUOTES

      +
    + + + Set +
      + <Symbol> depends on source. May also an WKN.

      +
    • set <name> buy <Symbol> <Amount> <Value of amount>
      + Add a stock exchange security. If stock exchange security already exists, new values will be added to old values.

      +
    • +
    • set <name> sell <Symbol> <Amount> <Value of amount>
      + Remove a stock exchange security (or an part of it).

      +
    • +
    • set <name> add <Symbol>
      + Watch only

      +
    • +
    • set <name> remove <Symbol>
      + Remove watched stock exchange security.

      +
    • +
    • set <name> clearReadings
      + Clears all readings.

      +
    • +
    • set <name> update
      + Refresh all readings.

      +
    • +
    + + + Get +
      +
    • get <name> sources
      + Lists all avaiable data sources.

      +
    • +
    • get <name> currency <Symbol>
      + Get currency of stock exchange securities

      +
    • +
    + + + Attributes +
      +
    • currency
      + All stock exchange securities will shown in this currency.
      + Default: EUR

      +
    • +
    • defaultSource
      + Default source for stock exchange securities values.
      + Default: europe, valid values: from get <name> sources

      +
    • +
    • queryTimeout
      + Fetching timeout in seconds.
      + Standard: 120, valid values: Number

      +
    • +
    • pollInterval
      + Refresh interval in seconds.
      + Standard: 300, valid values: Number

      +
    • +
    • sources
      + An individual data source can be set for every single stock exchange securities.
      + Data sources can be fetched with: get <name> sources.
      + Format: <Symbol>:<Source>[,<Symbol>:<Source>...]
      + Example: A0M16S:vwd,532669:unionfunds,849104:unionfunds
      + Stock exchange securities not listed in sources will be updated from defaultSource.

      +
    • +
    • stocks
      + Will be created/modified via buy/sell/add/remove
      + Contains stock exchange securities informations in format: <Symbol>:<Anzahl>:<Einstandswert>[,<Symbol>:<Anzahl>:<Einstandswert>...]

      +
    • +

    +
+ +=end html + +=begin html_DE + + +

STOCKQUOTES

+(en | de) +
    + + Wertpapierdaten von verschiedenen Quellen holen
    + Vorbereitung
    + Perl Modul Finance::Quote muss installiert werden:
    + cpan install Finance::Quote oder sudo apt-get install libfinance-quote-perl

    + + Define +
      + define <name> STOCKQUOTES

      +
    + + + Set +
      + <Symbol> hängt von den jeweiligen Quellen ab. Kann auch eine WKN sein. Hier muss ggf. experimentiert werden.

      +
    • set <name> buy <Symbol> <Menge> <Gesamtpreis>
      + Wertpapier in Depot einbuchen. Wenn dieses Wertpapier bereits vorhanden ist, werden die Neuen einfach dazuaddiert.

      +
    • +
    • set <name> sell <Symbol> <Menge> <Gesamtpreis>
      + Wertpapier (auch Teilmenge) wieder ausbuchen.

      +
    • +
    • set <name> add <Symbol>
      + Wertpapier nur beobachten

      +
    • +
    • set <name> remove <Symbol>
      + Entferne Wertpapier das nur beobachtet wird.

      +
    • +
    • set <name> clearReadings
      + Alle Readings löschen.

      +
    • +
    • set <name> update
      + Alle Readings aktualisieren.

      +
    • +
    + + + Get +
      +
    • get <name> sources
      + Verfügbare Datenquellen auflisten. Diese werden für die Attribute defaultSource und sources benötigt

      +
    • +
    • get <name> currency <Symbol>
      + Wertpapierwährung ermitteln

      +
    • +
    + + + Attribute +
      +
    • currency
      + Währung, in der die Wertpapiere angezeigt werden.
      + Default: EUR, gültige Werte: Währungskürzel

      +
    • +
    • defaultSource
      + Standardquelle für die Wertpapierdaten.
      + Default: europe, gültige Werte: alles was get <name> sources ausgibt.

      +
    • +
    • queryTimeout
      + Timeout beim holen der Daten in Sekunden.
      + Standard: 120, gültige Werte: Zahl

      +
    • +
    • pollInterval
      + Aktualisierungsintervall in Sekunden.
      + Standard: 300, gültige Werte: Zahl

      +
    • +
    • sources
      + Für jedes Wertpapier kann eine eigene Datenquelle definiert werden.
      + Die Datenquellen können über get <name> sources angefragt werden.
      + Format: <Symbol>:<Source>[,<Symbol>:<Source>...]
      + Beispiel: A0M16S:vwd,532669:unionfunds,849104:unionfunds
      + Alle nicht aufgeführten Werpapiere werden über die defaultSource abgefragt.

      +
    • +
    • stocks
      + Wird über buy/sell/add/remove angelegt/modifiziert
      + Enthält die Werpapiere im Format <Symbol>:<Anzahl>:<Einstandswert>[,<Symbol>:<Anzahl>:<Einstandswert>...]

      +
    • +

    +
+ +=end html_DE +=cut \ No newline at end of file diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index bad5c9edb..e6b1c0777 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -337,6 +337,7 @@ FHEM/98_update.pm rudolfkoenig http://forum.fhem.de Sonstiges FHEM/98_version.pm markusbloch http://forum.fhem.de Sonstiges FHEM/98_weblink.pm rudolfkoenig http://forum.fhem.de Frontends FHME/98_weekprofile.pm risiko http://forum.fhem.de Frontends +FHEM/98_STOCKQUOTES.pm vbs http://forum.fhem.de Unterstuetzende Dienste FHEM/99_SUNRISE_EL.pm rudolfkoenig http://forum.fhem.de Automatisierung FHEM/99_Utils.pm rudolfkoenig http://forum.fhem.de Automatisierung FHEM/Blocking.pm rudolfkoenig http://forum.fhem.de Automatisierung