2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-02-25 03:44:52 +00:00
fhem-mirror/fhem/FHEM/95_Shares.pm
2024-04-20 16:30:32 +00:00

1283 lines
45 KiB
Perl

########################################################################################
#
# Shares.pm
#
# FHEM module for display of stock market shares (stocks, ETF, funds)
#
# Prof. Dr. Peter A. Henning
# based on the module 98_STOCKQUOTES.pm by vbs
#
# $Id$
#
########################################################################################
#
# This programm 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.
# A copy is found in the textfile GPL.txt and important notices to the license
# from the author is found in LICENSE.txt distributed with these scripts.
#
# 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.
#
#########################################################################################
# TODO: in 716 und 818 & Trennzeichen kollidiert mit & in Namen.
# buy/sell/remove mit Liste der Stocks
package main;
use strict;
use warnings;
use Blocking;
use Finance::Quote;
use Encode qw(decode encode);
#-- global variables
my $version = "1.2";
my %shares_transtable_DE = (
"symbol" => "Symbol",
"share" => "Wertpapier",
"value" => "Wert",
"change" => "Änderung",
"trend" => "Trend",
"rate" => "Kurs",
"count" => "Anzahl",
"total" => "Total",
"category" => "Kategorie",
"automotive" => "Auto",
"bio" => "Bio",
"chemistry" => "Chemie",
"commodity" => "Rohstoff",
"energy" => "Energie",
"finance" => "Finanz",
"h2" => "Wasserstoff",
"health" => "Gesundheit",
"mobility" => "Mobilität",
"pharma" => "Pharma",
"realestate" => "Immo",
"sales" => "Handel",
"semiconductor" => "Halbleiter",
"software" => "Software",
"tech" => "Technologie"
);
my %shares_transtable_EN = (
"symbol" => "Symbol",
"share" => "Stock",
"value" => "Value",
"change" => "Change",
"trend" => "Trend",
"rate" => "Rate",
"count" => "Count",
"total" => "Total",
"category" => "Category",
"automotive" => "Auto",
"bio" => "Bio",
"chemistry" => "Chemistry",
"commodity" => "Commodity",
"energy" => "Energy",
"finance" => "Finance",
"h2" => "Hydrogen",
"health" => "Health",
"mobility" => "Mobility",
"pharma" => "Pharma",
"realestate" => "RelaEstate",
"sales" => "Sales",
"semiconductor" => "Semiconductor",
"software" => "Software",
"tech" => "Technology"
);
my $shares_tt;
#########################################################################################
#
# Shares_Initialize
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_Initialize($)
{
my ($hash) = @_;
$hash->{DefFn} = "Shares_Define";
$hash->{UndefFn} = "Shares_Undefine";
$hash->{SetFn} = "Shares_Set";
$hash->{GetFn} = "Shares_Get";
$hash->{AttrFn} = "Shares_Attr";
my $attr = "pollInterval queryTimeout arrows colors headercolor depotCurrency shareCurrency altSymbol defaultSource sources sourcesLinks stocks:textField-long ".
"shareFurtherReadings:multiple,open,close,last,return,high,low,value_entry,value_prev,div_yield,eps,volume,year_range ".
"$main::readingFnAttributes";
$hash->{AttrList} = $attr;
if( !defined($shares_tt) ){
#-- in any attribute redefinition readjust language
my $lang = AttrVal("global","language","EN");
if( $lang eq "DE"){
$shares_tt = \%shares_transtable_DE;
}else{
$shares_tt = \%shares_transtable_EN;
}
}
}
#########################################################################################
#
# Shares_Define
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_Define($$){
my ($hash, $def) = @_;
my @a = split("[ \t][ \t]*", $def);
my $name = $hash->{NAME};
$attr{$name}{"pollInterval"} = 1800;
$attr{$name}{"queryTimeout"} = 120;
$attr{$name}{"defaultSource"} = "xetra";
$attr{$name}{"depotCurrency"} = "EUR:€";
$attr{$name}{"shareCurrency"} = "EUR:€";
#-- readjust language
if( !defined($shares_tt) ){
#-- in any attribute redefinition readjust language
my $lang = AttrVal("global","language","EN");
if( $lang eq "DE"){
$shares_tt = \%shares_transtable_DE;
}else{
$shares_tt = \%shares_transtable_EN;
}
}
$hash->{QUOTER} = Finance::Quote->new(currency_rates => {order => ['ECB','AlphaVantage'], 'alphavantage' => {API_KEY => 'VCJX1KJV1260XUOD'}});
$hash->{QUOTER}->timeout(300); # Cancel fetch operation if it takes too long
Shares_UpdateCurrency($hash);
Shares_QueueTimer($hash, 5);
readingsSingleUpdate($hash, "state", "Initialized",1);
return undef;
}
#########################################################################################
#
# Shares_Attr
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_Attr(@){
my ($cmd,$name,$aName,$aVal) = @_;
my $hash = $defs{$name};
if( $aName =~/.*Currency/) {
return Shares_UpdateCurrency($hash);
}
elsif($aName eq "sources") {
return Shares_ClearReadings($hash);
}
return undef;
}
#########################################################################################
#
# Shares_UpdateCurrency
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_UpdateCurrency($){
my ($hash) = @_;
my $name = $hash->{NAME};
my $cur = (split(':',AttrVal($name, "shareCurrency", "")))[0];
my $depcur = (split(':',AttrVal($name, "depotCurrency", "")))[0];
if( !defined($hash->{QUOTER})){
Log3 $name, 1, "[Shares_UpdateCurrency] no quoter defined for depot $name";
return 1;
}
Shares_DeleteReadings($hash, undef);
$hash->{QUOTER}->set_currency($cur);
my $exr = $hash->{QUOTER}->currency($cur,"EUR");
readingsSingleUpdate($hash,"exchangerate",$exr." $depcur/$cur",1);
Log3 $name, 4, "[Shares_UpdateCurrency] exchangerate = $exr for $depcur/$cur in depot $name";
return undef;
}
#########################################################################################
#
# Shares_Undefine
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_Undefine($$){
my ($hash, $arg) = @_;
RemoveInternalTimer($hash);
BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID}));
return undef;
}
#########################################################################################
#
# Helper functions
#
#########################################################################################
sub shares_round($){
my ($num) = @_;
return sprintf("%.2f",$num)
}
sub shares_storekey {
my ($hash,$keyname,$keyvalue) = @_;
my $name = $hash->{NAME};
my $index = $hash->{TYPE} ."_". $name ."_". $keyname;
my $key = getUniqueId() . $index;
my $enc_key = "";
if ( eval "use Digest::MD5;1" ) {
$key = Digest::MD5::md5_hex( unpack "H*", $key );
$key .= Digest::MD5::md5_hex($key);
}
for my $char ( split //, $keyvalue ) {
my $encode = chop($key);
$enc_key .= sprintf( "%.2x", ord($char) ^ ord($encode) );
$key = $encode . $key;
}
my $err = setKeyValue( $index, $enc_key );
if ( defined($err) ){
return "[shares_storekey] error while saving the value for key $keyname - $err" ;
}else{
return "[shares_storekey] key $keyname successfully saved";
}
}
sub shares_readkey {
my ($hash,$keyname) = @_;
my $name = $hash->{NAME};
my $index = $hash->{TYPE} ."_". $name ."_". $keyname;
my $key = getUniqueId() . $index;
Log3 $name,1,"[shares_readkey] reading value for key $keyname from file";
my ($err, $keyvalue ) = getKeyValue($index);
if ( defined($err) ) {
Log3 $name,1,"[shares_readkey] unable to read value for key $keyname from file";
return;
}
if ( defined($keyvalue) ) {
if ( eval "use Digest::MD5;1" ) {
$key = Digest::MD5::md5_hex( unpack "H*", $key );
$key .= Digest::MD5::md5_hex($key);
}
my $dec_key = '';
for my $char ( map { pack( 'C', hex($_) ) } ( $keyvalue =~ /(..)/g ) ) {
my $decode = chop($key);
$dec_key .= chr( ord($char) ^ ord($decode) );
$key = $decode . $key;
}
return $dec_key;
}else{
Log3 $name, 1, "[shares_readkey] no value for key $keyname in file";
return;
}
return;
}
#########################################################################################
#
# Shares_GetLinkHashes
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_GetLinkHashes($){
my ($hash) = @_;
my $name = $hash->{NAME};
my $sstring = AttrVal($name, "sourcesLinks", "");
my @links = split (',',$sstring);
my %linkHash = ();
foreach my $link (@links) {
if ($link !~ /^.*\:https?\:/){
Log3 $name,1,"[Shares_GetLinkHashes] missing protocol part missing in link $link";
}
my @toks = split ":", $link;
#Log 1,"========> setting link for source ".$toks[0]." to ".$toks[1].":".$toks[2];
$linkHash{$toks[0]} = $toks[1].":".$toks[2]; $toks[2];
}
return \%linkHash;
}
#########################################################################################
#
# Shares_SetStockHashes
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_SetStockHashes($$){
my ($hash, $stocks) = @_;
my $name = $hash->{NAME};
#-- attribute stocks contains share informations in format:
# <Symbol>:<Count>:<Value_entry>:<Category>
# IMPORTANT
# <Symbol> may have the format <Symbol1>[|<SymbolN>]*
# <Value_entry> in attribute is in depot currency
# <Value_entry> in hash is in share currency
my $cur = (split(':',AttrVal($name, "shareCurrency", "")))[0];
my $depcur = (split(':',AttrVal($name, "depotCurrency", "")))[0];
my $exr = ReadingsNum($name, "exchangerate", 1);
if( $exr == 0 ){
Log3 $name,1,"[Shares_SetStockHashes] error: exchangerate is zero ";
return
}
my $buyval;
my $str = "";
my $first = 1;
foreach my $stock (sort keys %{ $stocks }) {
$str .= ",\n" unless $first;
$first = 0;
#-- buy value
if( $depcur eq $cur ){
$buyval = $stocks->{$stock}[1];
}else{
$buyval = shares_round($stocks->{$stock}[1]*$exr);
Log3 $name,5,"[Shares_SetStockHashes] share ".$stock." transforming buy value ".$stocks->{$stock}[1]." $cur into $buyval $depcur";
}
#-- [0] is number, [1] is buyval (modified), [2] is catgory, [3] is long name
$str .= $stocks->{$stock}[3].":".$stocks->{$stock}[0].":".$buyval.":".lc($stocks->{$stock}[2]);
}
Log3 $name, 4, "[Shares_SetStockHashes] setting stocks attribute to $str";
$attr{$name}{"stocks"} = $str;
return undef;
}
#########################################################################################
#
# Shares_GetStockHashes
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_GetStockHashes($){
my ($hash) = @_;
my $name = $hash->{NAME};
#-- attribute stocks contains share informations in format:
# <Symbol>:<Count>:<Value_entry>:<Category>
# IMPORTANT
# <Value_entry> in attribute is in depot currency
# <Value_entry> in hash is in share currency
my $sstring = AttrVal($name, "stocks", "");
$sstring =~ s/[\#\n]//g;
my @stocks = split (',',$sstring);
my $cur = (split(':',AttrVal($name, "shareCurrency", "")))[0];
my $depcur = (split(':',AttrVal($name, "depotCurrency", "")))[0];
my $altsym = AttrVal($name, "altSymbol",0);
my $exr = ReadingsNum($name, "exchangerate", 1);
my ($buyval,$category);
my %stockHash = ();
foreach my $stock (@stocks) {
my @toks = split ":", $stock;
#-- check if alternate symbol is needed when first source does not work
my @stocksyms = split '\|', $toks[0];
my $stocksym = $stocksyms[$altsym];
if( !defined($stocksym) ){
$stocksym=$stocksyms[0];
Log3 $name,1,"[Shares_GetStockHashes] missing alternate symbol $altsym, using $stocksym instead";
}
#-- buy value
if( $depcur eq $cur ){
$buyval = $toks[2];
}else{
$buyval = shares_round($toks[2]/$exr);
Log3 $name,5,"[Shares_GetStockHashes] share ".$toks[0]." transforming buy value ".$toks[2]." $depcur into $buyval $cur";
}
#-- category
if( !defined($toks[3])){
Log3 $name,4,"[Shares_GetStockHashes] share $stocksym does not have a category assigned";
$category = "";
}else{
$category = $toks[3];
}
$stockHash{$stocksym} = [$toks[1], $buyval, $category,$toks[0]];
}
return \%stockHash;
}
#########################################################################################
#
# Shares_ClearReadings
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_ClearReadings($){
my ($hash, $stockName) = @_;
delete $hash->{READINGS};
return undef;
}
#########################################################################################
#
# Shares_DeleteReadings
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_DeleteReadings($$){
my ($hash, $prefix) = @_;
my $delStr = defined($prefix) ? ".*".$prefix."_.*" : ".*";
fhem("deletereading $hash->{NAME} $delStr", 1);
return undef;
}
#########################################################################################
#
# Shares_RemoveStock
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_RemoveStock($$){
my ($hash, $stockName) = @_;
my $stocks = Shares_GetStockHashes($hash);
if (not exists $stocks->{$stockName}) {
return "[Shares_RemoveStock] error: no share named '$stockName' to delete!";
}
Log3 $hash->{NAME}, 3, "[Shares_RemoveStock] removing share $stockName";
delete $stocks->{$stockName};
if (not exists $stocks->{$stockName}) {
Log3 $hash->{NAME}, 3, "DELETED";
}
Shares_SetStockHashes($hash, $stocks);
Shares_DeleteReadings($hash, $stockName);
return undef;
}
#########################################################################################
#
# Shares_ChangeAmount
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_ChangeAmount($$$$){
my ($hash, $stockName, $count, $buyval) = @_;
my $name = $hash->{NAME};
# IMPORTANT: parameter $buyval is in depot currency
# <Value_entry> in hash is in share currency
my $stocks = Shares_GetStockHashes($hash);
my $cur = (split(':',AttrVal($name, "shareCurrency", "")))[0];
my $depcur = (split(':',AttrVal($name, "depotCurrency", "")))[0];
my $exr = ReadingsNum($name, "exchangerate", 1);
if( $exr == 0 ){
Log3 $name,1,"[Shares_ChangeAmount] error: exchangerate is zero ";
return
}
#-- buy value
if( $depcur ne $cur ){
$buyval = shares_round($buyval/$exr);
Log3 $name,5,"[Shares_ChangeAmount] share ".$stockName." transforming buy value from $depcur into $cur";
}
if (exists $stocks->{$stockName}) {
$stocks->{$stockName}->[0] += $count;
$stocks->{$stockName}->[0] = 0 if ($stocks->{$stockName}[0] < 0);
Log3 $name, 5, "[Shares_ChangeAmount] previous buy value = ".$stocks->{$stockName}->[1].
" $cur will be increased by ".$buyval." $cur" ;
$stocks->{$stockName}->[1] += $buyval;
if ($stocks->{$stockName}->[0] == 0)
{
Log3 $hash->{NAME}, 3, "[Shares_ChangeAmount] removing share: $stockName";
delete $stocks->{$stockName};
Shares_DeleteReadings($hash, $stockName);
}
}
else {
$stocks->{$stockName}->[0] = $count;
$stocks->{$stockName}->[1] = $buyval;
}
Shares_SetStockHashes($hash, $stocks);
Shares_QueueTimer($hash, 0);
return undef;
}
#########################################################################################
#
# Shares_ChangeCategory
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_ChangeCategory($$$){
my ($hash, $stockName, $category) = @_;
my $stocks = Shares_GetStockHashes($hash);
if (not exists $stocks->{$stockName}) {
return "[Shares_ChangeCategory] error: no share named '$stockName' to change!";
}
$stocks->{$stockName}->[2] = $category;
Shares_SetStockHashes($hash, $stocks);
Shares_QueueTimer($hash, 0);
return undef;
}
#########################################################################################
#
# Shares_Set
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_Set($@){
my ($hash, $name, $cmd, @args) = @_;
if($cmd eq "buy" or $cmd eq "sell") {
my $depcur = AttrVal($name,"depotCurrency","");
if (int(@args) != 3) {
return "[Shares_Set] invalid arguments, usage 'set $name $cmd <sharename> <count> <price in $depcur>";
}
my $stockName = $args[0];
my $count = $args[1];
my $price = $args[2];
my $fac = ($cmd eq "buy") ? 1 : -1;
my $str = Shares_ChangeAmount($hash, $stockName, $fac * $count, $fac * $price);
Shares_QueueTimer($hash, 0);
return $str;
}
elsif($cmd eq "add") {
if (int(@args) != 1) {
return "[Shares_Set] invalid arguments, usage 'set $name add <sharename>";
}
return Shares_ChangeAmount($hash, $args[0], 0 ,0);
}
elsif($cmd eq "remove") {
if (int(@args) != 1) {
return "[Shares_Set] invalid arguments, usage 'set $name remove <sharename>";
}
return Shares_RemoveStock($hash, $args[0]);
}
if($cmd eq "update") {
return Shares_QueueTimer($hash, 0);
}
elsif($cmd eq "category") {
if (int(@args) != 2) {
return "[Shares_Set] invalid arguments, usage 'set $name category <sharename> <comma separated list of categories>";
}
return Shares_ChangeCategory($hash, $args[0], $args[1]);
}
elsif($cmd eq "clearReadings") {
return Shares_ClearReadings($hash);
}elsif($cmd eq "alphavantagekey") {
return Shares_ClearReadings($hash);
}elsif($cmd eq "sparkasseID") {
return Shares_ClearReadings($hash);
}
my $res = "Unknown argument ".$cmd.", choose one of update:noArg clearReadings:noArg buy sell add remove";
return $res ;
}
#########################################################################################
#
# Shares_Get
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_Get($@){
my ($hash, $name, $cmd, @args) = @_;
if($cmd eq "sources") {
if (int(@args) != 0) {
return "Invalid arguments, usage 'get $name $cmd'";
}
return "Available sources: ".join("\n", $hash->{QUOTER}->sources());
}
elsif($cmd eq "currencies") {
if (int(@args) != 0) {
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 currencies";
return $res ;
}
#########################################################################################
#
# Shares_QueueTimer
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_QueueTimer($$){
my ($hash, $pollInt) = @_;
Log3 $hash->{NAME}, 4, "[Shares_QueueTimer] $pollInt seconds";
RemoveInternalTimer($hash);
delete($hash->{helper}{RUNNING_PID});
InternalTimer(time() + $pollInt, "Shares_QueryQuotes", $hash, 0);
return undef;
}
#########################################################################################
#
# Shares_QueryQuotes
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_QueryQuotes($){
my ($hash) = @_;
my $name = $hash->{NAME};
if (not exists($hash->{helper}{RUNNING_PID})) {
Log3 $hash->{NAME}, 4, '[Shares_QueryQuotes] start blocking query';
readingsSingleUpdate($hash, "state", "Updating",1);
$hash->{helper}{RUNNING_PID} = BlockingCall("Shares_QueryQuotesBlocking",
$hash,
"Shares_QueryQuotesFinished",
AttrVal($hash, "queryTimeout", 120),
"Shares_QueryQuotesAbort",
$hash);
}
else {
Log3 $hash->{NAME}, 4, '[Shares_QueryQuotes] blocking not started because one running already';
}
return undef;
}
#########################################################################################
#
# Shares_GetSource
#
# return the source that should be used for a share
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_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};
}
#-- check if alternate src is needed when first source does not work
my $altsym = AttrVal($name, "altSymbol",0);
my @altsrcs = split '\|',AttrVal($name, "defaultSource", "europe");
my $src = $altsrcs[$altsym];
if( !defined($src) ){
$src=$altsrcs[0];
Log3 $name,1,"[Shares_GetSources] missing alternate source $altsym, using $src instead";
}
return $src;
}
#########################################################################################
#
# Shares_QueryQuotesBlocking
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_QueryQuotesBlocking($){
my ($hash) = @_;
my $name = $hash->{NAME};
Log3 $name, 4, '[Shares_QueryQuotesBlocking]';
my $stocks = Shares_GetStockHashes($hash);
my %sources = ();
foreach my $symbol (keys %{ $stocks }) {
my @toks = split ':', $symbol;
my $symbName = $toks[0];
my $targetSource = Shares_GetSource($hash, $symbName);
if (not exists $sources{$targetSource}) {
$sources{$targetSource} = ();
}
push(@{$sources{$targetSource}}, $symbName);
Log3 $name, 4, "[Shares_QueryQuotesBlocking] query share: $symbName from source $targetSource";
}
my $ret = $hash->{NAME};
foreach my $srcKey (keys %sources) {
Log3 $name, 4, "[Shares_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");
$val =~ s/\&//g;
$ret .= $val;
}
}
Log3 $name, 4, '[Shares_QueryQuotesBlocking] return value: '.$ret;
return $ret;
}
#########################################################################################
#
# Shares_QueryQuotesAbort
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_QueryQuotesAbort($$$){
my ($hash) = @_;
my $name = $hash->{NAME};
Log3 $name, 3, '[Shares_QueryQuotesAbort] blocking call aborted due to timeout!';
readingsSingleUpdate($hash, "state", "Update aborted",1);
delete($hash->{helper}{RUNNING_PID});
Shares_QueueTimer($hash, AttrVal($name, "pollInterval", 300));
return undef;
}
#########################################################################################
#
# Shares_QueryQuotesFinished
#
# Parameter string = long string with all data obtained from sources
# first piece = name of depot
#
#########################################################################################
sub Shares_QueryQuotesFinished($){
my ($string) = @_;
return unless(defined($string));
#-- split into array
my @a = split("\\|",$string);
my $name = $a[0];
my $hash = $defs{$name};
Log3 $name, 4, '[Shares_QueryQuotesFinished]';
delete($hash->{helper}{RUNNING_PID});
#-- update exchange rate
Shares_UpdateCurrency($hash);
#-- depot data
my %depotSummary = ();
$depotSummary{"depot_value"} = 0;
$depotSummary{"depot_diff"} = 0;
$depotSummary{"depot_value_prev"} = 0;
$depotSummary{"depot_value_entry"} = 0;
my %depotCategories = ();
#$depotCategories{$j}{"depot_value"} = 0;
#$depotCategories{$j}{"depot_value_entry"} = 0;
#$depotCategories{$j}{"depot_value_prev"} = 0;
my $exrate = ReadingsNum("$name","exchangerate",1);
my $cur = (split(':',AttrVal($name, "shareCurrency", "")))[0];
my $depcur = (split(':',AttrVal($name, "depotCurrency", "")))[0];
my $stocks = Shares_GetStockHashes($hash);
#-- We get lots of data here
# needed and present only locally: count[0], value_entry[1]
# needed: name,last,close
# calculated value,diff,change,diff_daily,change_daily,value_prev,
# optionally displayed: value_entry,quote_entry,open,close,div_yield,high,low,eps,volume,year_range
# not needed: currency,date,isodate,exchange,method,success,symbol,type
my $fread = AttrVal($name,"shareFurtherReadings",undef);
if( $fread){
$fread = "((".$fread."))";
$fread =~ s/,/\)\|\(/g
}else{
$fread = "empty"
}
#-- Categories
my $cread = AttrVal($name,"categories",undef);
my @acread = ();
if( $cread ){
@acread = split(',',$cread);
}
#-- First run through all shares: take long string apart
my %stockState = ();
my ($symb,$sname);
readingsBeginUpdate($hash);
for my $i (1 .. $#a){
my @toks = split '&',$a[$i];
#-- take out symbol from name
if( $toks[1] eq "name"){
$symb = $toks[0];
$sname = $toks[2];
$sname =~ /$symb\s\((.*)\)/;
$sname = $1
if( $1 && $1 ne "" );
readingsBulkUpdate($hash, $toks[0]."_name" , $sname);
$stockState{$toks[0]}{"name"} = $toks[2];
}elsif( $toks[1] eq "return"){
readingsBulkUpdate($hash, $toks[0]."_return" , $toks[2]);
$stockState{$toks[0]}{"return"} = $toks[2];
}elsif( $toks[1] eq "price"){
readingsBulkUpdate($hash, $toks[0]."_price" , $toks[2]);
$stockState{$toks[0]}{"price"} = $toks[2];
}elsif( $toks[1] eq "last"){
readingsBulkUpdate($hash, $toks[0]."_last" , $toks[2]);
$stockState{$toks[0]}{"last"} = $toks[2];
}elsif( $toks[1] eq "close"){
#-- not yet in readings, need to process after last is defined
### TEMP correction
readingsBulkUpdate($hash, $toks[0]."_close" , $toks[2]);
$stockState{$toks[0]}{"close"} = $toks[2];
#-- take out p from change
}elsif( $toks[1] eq "p_change"){
chop $toks[2] if ($toks[2] =~ /%$/);
readingsBulkUpdate($hash, $toks[0]."_change", $toks[2]);
$stockState{$toks[0]}{"change"} = $toks[2];
#-- leave out exchange by default
}elsif( $toks[1] eq "exchange"){
readingsBulkUpdate($hash, $toks[0]."_exchange", $toks[2])
if( "exch" =~ /$fread/ );
$stockState{$toks[0]}{"exchange"} = $toks[2];
#-- all others as they are
}else{
readingsBulkUpdate($hash, $toks[0]."_".$toks[1], $toks[2])
if( $toks[1] =~ /$fread/ );
$stockState{$toks[0]}{$toks[1]} = $toks[2];
}
}
readingsEndUpdate($hash, 1);
#-- Second run through all shares: derived values
my ($count,$last,$value,$close,$value_close,$value_entry,$value_diff);
readingsBeginUpdate($hash);
foreach my $stock (keys %stockState) {
$count = $stocks->{$stock}->[0];
readingsBulkUpdate($hash, $stock."_count", $count);
#-- this is in stock currency, remember !
if( exists $stockState{$stock}{"last"} ){
$last = $stockState{$stock}{"last"};
#-- already in readings, but in case we have a return value this must be corrected
if( $stockState{$stock}{"return"} ){
$last = $stockState{$stock}{"return"};
Log3 $name, 1, "[Shares_QueryQuotesFinished] reading \"last\" replaced by \"return\" for $stock";
readingsBulkUpdate($hash, $stock."_last" , $last);
}
$value = $count * $last;
$depotSummary{"depot_value"} += $value*$exrate;
readingsBulkUpdate($hash, $stock."_value", shares_round($value));
}else{
$last = undef;
#$value = ??
}
#-- this is in stock currency, remember !
if( exists $stockState{$stock}{"close"} ){
$close = $stockState{$stock}{"close"};
#-- in London, close value is sometimes 100*close
if( $last && ($last != 0 ) && (abs($close/$last) > 5)){
Log3 $name, 1, "[Shares_QueryQuotesFinished] reading \"close\" rescaled by 0.01 for $stock";
$close *= 0.01;
}
}else{
Log3 $name, 1, "[Shares_QueryQuotesFinished] reading \"close\" replaced by \"last\" for $stock";
$close = $last;
}
readingsBulkUpdate($hash, $stock."_close" , $close)
if( "close" =~ /$fread/ );
$value_close = $count * $close;
readingsBulkUpdate($hash, $stock."_value_prev", shares_round( $value_close))
if( "value_prev" =~ /$fread/ );
$depotSummary{"depot_value_prev"} += $value_close*$exrate;
#--
if( defined($last) ){
$value_diff = $count * ($last - $close);
readingsBulkUpdate($hash, $stock."_diff_day", shares_round($value_diff))
if( "diff_day" =~ /$fread/ );
readingsBulkUpdate($hash, $stock."_change_day", ($close != 0)?shares_round( 100.0 * ( $last / $close -1)):0.0)
if( "change_day" =~ /$fread/ );
}
#-- entry value has been transformed to stock currency, remember !
$value_entry = $stocks->{$stock}->[1];
readingsBulkUpdate($hash, $stock."_diff", shares_round($value - $value_entry));
readingsBulkUpdate($hash, $stock."_change", ($value_entry == 0) ? 0 : shares_round( 100.0 * (($value / $value_entry) - 1 )));
readingsBulkUpdate($hash, $stock."_value_entry", $value_entry)
if( "value_entry" =~ /$fread/ );
readingsBulkUpdate($hash, $stock."_quote_entry", ($count == 0) ? 0 : shares_round( $value_entry / $count))
if( "quote_entry" =~ /$fread/ );
$depotSummary{"depot_value_entry"} += $value_entry*$exrate;
#-- category
my $category = $stocks->{$stock}->[2];
readingsBulkUpdate($hash, $stock."_category", $category);
$depotCategories{$category}{"depot_value"} += $value*$exrate
if( defined($value));
$depotCategories{$category}{"depot_value_entry"} += $value_entry*$exrate
if( defined($value_entry));
$depotCategories{$category}{"depot_value_prev"} += $value_close*$exrate
if( defined($value_close));
}
#-- cleanup readings
$depotSummary{"depot_value"} = shares_round($depotSummary{"depot_value"});
$depotSummary{"depot_value_entry"} = shares_round($depotSummary{"depot_value_entry"});
$depotSummary{"depot_diff_day"} = shares_round($depotSummary{"depot_value"} - $depotSummary{"depot_value_prev"});
$depotSummary{"depot_diff"} = shares_round($depotSummary{"depot_value"} - $depotSummary{"depot_value_entry"});
readingsBulkUpdate($hash, "depot_value", $depotSummary{"depot_value"});
readingsBulkUpdate($hash, "depot_value_entry", $depotSummary{"depot_value_entry"});
readingsBulkUpdate($hash, "depot_diff_day", $depotSummary{"depot_diff_day"});
readingsBulkUpdate($hash, "depot_diff", $depotSummary{"depot_diff"});
foreach my $category (keys %depotCategories){
$depotCategories{$category}{"depot_value"} = shares_round($depotCategories{$category}{"depot_value"});
$depotCategories{$category}{"depot_value_entry"} = shares_round($depotCategories{$category}{"depot_value_entry"});
$depotCategories{$category}{"depot_value_prev"} = shares_round($depotCategories{$category}{"depot_value_prev"});
}
$hash->{DATA}{"categories"} = \%depotCategories;
my $depot_change_day = 0.0;
if ($depotSummary{"depot_value_prev"} > 0.0) {
$depot_change_day = shares_round(100.0 * (($depotSummary{"depot_value"} / $depotSummary{"depot_value_prev"}) - 1 ));
}
readingsBulkUpdate($hash, "depot_change_day", $depot_change_day);
my $depot_change = 0.0;
if ($depotSummary{"depot_value_entry"} > 0.0) {
$depot_change = shares_round(100.0 * (($depotSummary{"depot_value"} / $depotSummary{"depot_value_entry"}) - 1 ));
}
readingsBulkUpdate($hash, "depot_change", $depot_change);
#-- todo replace by TIME from reading
my $now = gettimeofday();
my $fmtDateTime = FmtDateTime($now);
readingsBulkUpdate($hash,"state",$depotSummary{"depot_value"}." $depcur ( ".$depot_change." % = ".$depotSummary{"depot_diff"}." $depcur) ".$fmtDateTime);
readingsEndUpdate($hash, 1);
Shares_QueueTimer($hash, AttrVal($name, "pollInterval", 300));
return undef;
}
#########################################################################################
#
# Shares_MakeTable
#
# Parameter hash = hash of device addressed
#
#########################################################################################
sub Shares_MakeTable($){
my ($hash) = @_;
my $name = $hash->{NAME};
my $exrate = ReadingsNum("$name","exchangerate",1);
my $cur = (split(':',AttrVal($name, "shareCurrency", "")))[0];
my $depcur = (split(':',AttrVal($name, "depotCurrency", "")))[0];
my @colors = split(',',AttrVal($name, "colors","green,seagreen,black,orangered,red"));
my @arrows = split(',',AttrVal($name, "arrows","&#129153;,&#129157;,&#129154;,&#129158;,&#129155;"));
my $bgcolor= AttrVal($name,"headercolor","#aaaaff;");
my $stocks = Shares_GetStockHashes($hash);
my $links = Shares_GetLinkHashes($hash);
my ($estyle,$oddeven,$source,$stock,$estock,$count,$sname,$value,$change,$changest,
$diff,$trend,$trendf,$trendst,$rate,$erate,$frate,$category);
my $smidleft = "border-left:1px solid gray;border-radius:0px;";
my $smidright= "border-right:1px solid gray;border-radius:0px;";
$change = exists($hash->{READINGS}{"depot_change"}{VAL}) ? $hash->{READINGS}{"depot_change"}{VAL} : 0.0;
$changest = 'style="text-align:right;color:'.(($change>1.0)?$colors[0]:(($change>0.1)?$colors[1]:(($change>=-0.1)?$colors[2]:(($change>=-1.0)?$colors[3]:$colors[4])))).'"';
$trend = exists($hash->{READINGS}{"depot_change_day"}{VAL}) ? $hash->{READINGS}{"depot_change_day"}{VAL} : 0.0;
$trendf = $trend."% ".(($trend>1.0)?$arrows[0]:(($trend>0.1)?$arrows[1]:(($trend>=-0.1)?$arrows[2]:(($trend>=-1.0)?$arrows[3]:$arrows[4]))));
$trendst = 'style="text-align:right;color:'.(($trend>1.0)?$colors[0]:(($trend>0.1)?$colors[1]:(($trend>=-0.1)?$colors[2]:(($trend>=-1.0)?$colors[3]:$colors[4])))).'"';
my $table = "<tr class=\"odd\" style=\"background-color:$bgcolor;font-weight:bold;text-align:right\">".
"<td style=\"$smidleft\"><a href=\"/fhem?detail=$name\">$name</a></td><td colspan=\"2\" style=\"text-align:right\">".$hash->{READINGS}{"depot_value"}{VAL}.
"€</td><td $changest>$change".
"%</td><td $changest>".$hash->{READINGS}{"depot_diff"}{VAL}.
"€</td><td $trendst>$trendf".
"</td><td colspan=\"3\" style=\"text-align:right;$smidright\">".$hash->{READINGS}{"depot_value"}{TIME}.
"</td></tr>\n";
##my $td_style = 'style="padding-left:6px;padding-right:6px;"';
$oddeven = 0;
foreach $stock (sort {lc $hash->{READINGS}{$a."_name"}{VAL} cmp lc $hash->{READINGS}{$b."_name"}{VAL} } keys %{$stocks}) {
#-- link defined?
my $source = Shares_GetSource($hash, $stock);
if (not exists $links->{$source}) {
$estock = $stock;
}else{
$source = $links->{$source};
$source =~ s/(https?)\/\//$1\:\/\//;
$source =~ s/\$SYMBOL/$stock/g;
$estock = "<a href=\"".$source."\" target=\"_blank\">$stock</a>";
}
$estyle = ($oddeven == 1)?" class=\"odd\"":" class=\"even\"";
$oddeven = 1- $oddeven;
$sname = $hash->{READINGS}{$stock."_name"}{VAL};
$value = exists($hash->{READINGS}{$stock."_value"}{VAL}) ? shares_round($hash->{READINGS}{$stock."_value"}{VAL}*$exrate) : 0.0;
$change = exists($hash->{READINGS}{$stock."_change"}{VAL}) ? $hash->{READINGS}{$stock."_change"}{VAL} : 0.0;
$changest = 'style="text-align:right;color:'.(($change>1.0)?$colors[0]:(($change>0.1)?$colors[1]:(($change>=-0.1)?$colors[2]:(($change>=-1.0)?$colors[3]:$colors[4])))).'"';
$diff = exists($hash->{READINGS}{$stock."_diff"}{VAL}) ? shares_round($hash->{READINGS}{$stock."_diff"}{VAL}*$exrate) : 0.0;
$trend = exists($hash->{READINGS}{$stock."_change_day"}{VAL}) ? $hash->{READINGS}{$stock."_change_day"}{VAL} : 0.0;
$trendf = $trend."% ".(($trend>1.0)?$arrows[0]:(($trend>0.1)?$arrows[1]:(($trend>=-0.1)?$arrows[2]:(($trend>=-1.0)?$arrows[3]:$arrows[4]))));
$trendst = 'style="text-align:right;color:'.(($trend>1.0)?$colors[0]:(($trend>0.1)?$colors[1]:(($trend>=-0.1)?$colors[2]:(($trend>=-1.0)?$colors[3]:$colors[4])))).'"';
$rate = exists($hash->{READINGS}{$stock."_last"}{VAL}) ? $hash->{READINGS}{$stock."_last"}{VAL} : 0.0;
$erate = shares_round($rate*$exrate);
$frate = ($cur ne $depcur)?"(".shares_round($rate)." $cur)":"";
$count = $stocks->{$stock}->[0];
$category = $shares_tt->{$stocks->{$stock}->[2]};
$table .= "<tr $estyle><td style=\"$smidleft\">$estock</td><td>$sname</td><td>$value€</td><td ".$changest.">$change%</td><td ".$changest.
">$diff€</td><td ".$trendst.">$trendf</td><td>$erate€ $frate</td><td>$count</td><td style=\"$smidright\">$category</td></tr>\n";
}
return $table;
}
1;
=pod
=item device
=item summary Acquisition and listing of share values
=item summary_DE Beschaffung und Listing der Kursdaten von Wertpapieren
=begin html
<a name="Shares"></a>
<h3>Shares</h3>
(en | <a href="commandref_DE.html#Shares">de</a>)
<ul>
<a name="Shares"></a>
Acquire and display share values<br>
<b>Define</b>
<ul>
<code>define Depot Shares</code><br><br>
</ul>
<a name="Sharesset"></a>
Notes: <ul>
<li>This FHEM module uses the Perl module Finance::Quote, which acquires share data from different sources (see attribute sources).
Since the suppliers of data change their policies from time to time, it may occur that from one day to the next no data is available.
Therefore this FHEM module implements a flexible approach that allows to change the default data source for all shares with a single attribute altSymbol.</li>
<li>The &lt;Symbol&gt; for a particular share depends very much on the source! Therefore, each share <i>can</i> be represented
by a '|'-separated list of symbols, example: The well known NASDAQ ETF is represented by NADQ.DE|LU2197908721|..., where the first symbol is used by Yahoo,
the second symbol is the ISIN used by Deutsche Börse etc. See attributes defaultSource, altSymbol, stocks.</li>
<li>This module uses the global attribute <code>language</code> to determine its output data<br/>
(default: EN=english). For German output set <code>attr global language DE</code>.</li>
<li>This module needs the package Finance::Quote. Install with
<code>cpan Finance::Quote</code> or <code>sudo apt-get install libfinance-quote-perl</code><br><br></li>
</ul>
<b>Set</b>
<ul>
<li><code>set &lt;name&gt; buy &lt;Symbol&gt; &lt;Count&gt; &lt;Value <i>in depotCurrency</i>&gt;</code><br>
Buy some shares. If this particular share already exists, new values will be added to old values.<br><br>
</li>
<li><code>set &lt;name&gt; sell &lt;Symbol&gt; &lt;Count&gt; &lt;Value <i>in depotCurrency</i>&gt;</code><br>
Sell some shares<br><br>
</li>
<li><code>set &lt;name&gt; add &lt;Symbol&gt;</code><br>
Watch this share<br><br>
</li>
<li><code>set &lt;name&gt; remove &lt;Symbol&gt;</code><br>
Remove share from watch list<br><br>
</li>
<li><code>set &lt;name&gt; clearReadings</code><br>
Clear all readings.<br><br>
</li>
<li><code>set &lt;name&gt; update</code><br>
Refresh all readings.<br><br>
</li>
</ul>
<a name="Sharesget"></a>
<b>Get</b>
<ul>
<li><code>get &lt;name&gt; sources</code><br>
Lists all avaialble data sources.<br><br>
</li>
<li><code>get &lt;name&gt; currencies</code><br>
Lists all available currencies.<br><br>
</li>
</ul>
<a name="Sharesattr"></a>
<b>Attributes</b>
<ul>
<li>shareCurrency<br>
Individual shares will be shown in this currency. <br>
Format: <code><ISO-Code for currency>[:<Currency Symbol>]</code><br>
Default: EUR:€<br><br>
</li>
<li>depotCurrency<br>
The total depot value will be shown in this currency, also the buy values in the <i>stocks</i> attribute are given in this currency<br>
Format: <code><ISO-Code for currency>[:<Currency Symbol>]</code><br>
Default: EUR:€<br><br>
</li>
<li>queryTimeout<br>
Fetching timeout in seconds.<br>
Standard: 120, valid values: Number<br><br>
</li>
<li>pollInterval<br>
Refresh interval in seconds.<br>
Standard: 300, valid values: Number<br><br>
</li>
<li>defaultSource<br>
'|'-separated list of default sources for share values. See altSymbol attribute<br>
Default: yahoo_json (single source), valid values: from <code>get &lt;name&gt; sources</code><br><br>
</li>
<li>sources<br>
An individual data source can be set for every single symbol.<br>
Data sources can be fetched with: <code>get &lt;name&gt; sources</code>.<br>
Format: &lt;Symbol&gt;:&lt;Source&gt;[,&lt;Symbol&gt;:&lt;Source&gt;...]<br>
Shares not listed in sources will be updated from defaultSource.<br><br>
</li>
<li>sourcesLinks<br>
An individual link to obtain details and graphical data can be set for every source.<br>
The string <code>$SYMBOL</code> in each link will be replaced by the symbol for a share.<br>
Format: &lt;Source&gt;:&lt;Link&gt;[,&lt;Source&gt;:&lt;Link&gt;...]<br>
Example: yahoo_json:de.finance.yahoo.com/quote/$SYMBOL<br><br> </li>
<li>arroWs<br>
Comma-separated list of arrow symbols (in HTML-Format) for denoting change in stock value, boundary values are >1%,1%,0%,-1%,<-1%
Default: "&#129153;,&#129157;,&#129154;,&#129158;,&#129155;" </li>
<li>colors<br>
Comma-separated list of color specs (in HTML-Format) for coloring change in stock value, boundary values are >1%,1%,0%,-1%,<-1%
Default: "green,seagreen,black,orangered,red" </li>
<li>headercolor<br>
color spec (in HTML-Format) for coloring the header line in a depot table. Default #aaaaff;
</li>
<li>altSymbol<br>
Numerical value, the index for the list of symbols representing each share in the stocks attribute. The default value for the
altSymbol is 0, which means that the first symbol is used for querying Finance::Quote. A value of 1 means to use the second symbol, etc.
</li>
<li>stocks<br>
Will be created/modified via buy/sell/add/remove<br>
Contains share informations in the format: &lt;Symbol0[|Symbol1|...]&gt;:&lt;Number&gt;:&lt;Buy value in depotCurrency&gt;:&lt;Category&gt;<br><br>
</li>
</ul><br>
</ul>
=end html
=begin html_DE
<a name="Shares"></a>
<h3>Shares</h3>
<ul>
<a href="https://wiki.fhem.de/wiki/Modul_Shares">Deutsche Dokumentation im Wiki</a> vorhanden, die englische Version gibt es hier: <a href="commandref.html#Shares">Shares</a>
</ul>
=end html_DE
=cut