############################################################################### # # Developed with Kate # # (c) 2017 Copyright: Marko Oldenburg (leongaultier at gmail dot com) # All rights reserved # # 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 # 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. # # # $Id$ # ############################################################################### ## ## ## Das JSON Modul immer in einem eval aufrufen # $data = eval{decode_json($data)}; # # if($@){ # Log3($SELF, 2, "$TYPE ($SELF) - error while request: $@"); # # readingsSingleUpdate($hash, "state", "error", 1); # # return; # } # ####### ####### # URLs zum Abrufen diverser Daten # http:///api/system_status/soe # http:///api/meters/aggregates # http:///api/site_info # http:///api/sitemaster # http:///api/powerwalls # http:///api/networks # http:///api/system/networks # http:///api/operation # ## ## package main; my $missingModul = ""; use strict; use warnings; use HttpUtils; eval "use JSON;1" or $missingModul .= "JSON "; my $version = "0.4.0"; # Declare functions sub TeslaPowerwall2AC_Attr(@); sub TeslaPowerwall2AC_Define($$); sub TeslaPowerwall2AC_Initialize($); sub TeslaPowerwall2AC_Get($@); sub TeslaPowerwall2AC_Notify($$); sub TeslaPowerwall2AC_GetData($); sub TeslaPowerwall2AC_Undef($$); sub TeslaPowerwall2AC_ResponseProcessing($$$); sub TeslaPowerwall2AC_ReadingsProcessing_Aggregates($$); sub TeslaPowerwall2AC_ReadingsProcessing_Powerwalls($$); sub TeslaPowerwall2AC_ErrorHandling($$$); sub TeslaPowerwall2AC_WriteReadings($$$); sub TeslaPowerwall2AC_Timer_GetData($); my %paths = ( 'statussoe' => 'system_status/soe', 'aggregates' => 'meters/aggregates', 'siteinfo' => 'site_info', 'sitemaster' => 'sitemaster', 'powerwalls' => 'powerwalls', 'registration' => 'customer/registration', 'status' => 'status' ); sub TeslaPowerwall2AC_Initialize($) { my ($hash) = @_; # Consumer $hash->{GetFn} = "TeslaPowerwall2AC_Get"; $hash->{DefFn} = "TeslaPowerwall2AC_Define"; $hash->{UndefFn} = "TeslaPowerwall2AC_Undef"; $hash->{NotifyFn} = "TeslaPowerwall2AC_Notify"; $hash->{AttrFn} = "TeslaPowerwall2AC_Attr"; $hash->{AttrList} = "interval ". "disable:1 ". $readingFnAttributes; foreach my $d(sort keys %{$modules{TeslaPowerwall2AC}{defptr}}) { my $hash = $modules{TeslaPowerwall2AC}{defptr}{$d}; $hash->{VERSION} = $version; } } sub TeslaPowerwall2AC_Define($$) { my ( $hash, $def ) = @_; my @a = split( "[ \t][ \t]*", $def ); return "too few parameters: define TeslaPowerwall2AC " if( @a != 3); return "Cannot define a TeslaPowerwall2AC device. Perl modul $missingModul is missing." if ( $missingModul ); my $name = $a[0]; my $host = $a[2]; $hash->{HOST} = $host; $hash->{INTERVAL} = 300; $hash->{PORT} = 80; $hash->{VERSION} = $version; $hash->{NOTIFYDEV} = "global"; $hash->{actionQueue} = []; $attr{$name}{room} = "Tesla" if( !defined( $attr{$name}{room} ) ); Log3 $name, 3, "TeslaPowerwall2AC ($name) - defined TeslaPowerwall2AC Device with Host $host, Port $hash->{PORT} and Interval $hash->{INTERVAL}"; $modules{TeslaPowerwall2AC}{defptr}{HOST} = $hash; return undef; } sub TeslaPowerwall2AC_Undef($$) { my ( $hash, $arg ) = @_; my $name = $hash->{NAME}; Log3 $name, 3, "TeslaPowerwall2AC ($name) - Device $name deleted"; delete $modules{TeslaPowerwall2AC}{defptr}{HOST} if( defined($modules{TeslaPowerwall2AC}{defptr}{HOST}) and $hash->{HOST} ); return undef; } sub TeslaPowerwall2AC_Attr(@) { my ( $cmd, $name, $attrName, $attrVal ) = @_; my $hash = $defs{$name}; if( $attrName eq "disable" ) { if( $cmd eq "set" and $attrVal eq "1" ) { RemoveInternalTimer($hash); readingsSingleUpdate ( $hash, "state", "disabled", 1 ); Log3 $name, 3, "TeslaPowerwall2AC ($name) - disabled"; } elsif( $cmd eq "del" ) { Log3 $name, 3, "TeslaPowerwall2AC ($name) - enabled"; } } if( $attrName eq "disabledForIntervals" ) { if( $cmd eq "set" ) { return "check disabledForIntervals Syntax HH:MM-HH:MM or 'HH:MM-HH:MM HH:MM-HH:MM ...'" unless($attrVal =~ /^((\d{2}:\d{2})-(\d{2}:\d{2})\s?)+$/); Log3 $name, 3, "TeslaPowerwall2AC ($name) - disabledForIntervals"; readingsSingleUpdate ( $hash, "state", "disabled", 1 ); } elsif( $cmd eq "del" ) { Log3 $name, 3, "TeslaPowerwall2AC ($name) - enabled"; readingsSingleUpdate( $hash, "state", "active", 1 ); } } if( $attrName eq "interval" ) { if( $cmd eq "set" ) { if( $attrVal < 30 ) { Log3 $name, 3, "TeslaPowerwall2AC ($name) - interval too small, please use something >= 30 (sec), default is 300 (sec)"; return "interval too small, please use something >= 30 (sec), default is 300 (sec)"; } else { RemoveInternalTimer($hash); $hash->{INTERVAL} = $attrVal; Log3 $name, 3, "TeslaPowerwall2AC ($name) - set interval to $attrVal"; TeslaPowerwall2AC_Timer_GetData($hash); } } elsif( $cmd eq "del" ) { RemoveInternalTimer($hash); $hash->{INTERVAL} = 300; Log3 $name, 3, "TeslaPowerwall2AC ($name) - set interval to default"; TeslaPowerwall2AC_Timer_GetData($hash); } } return undef; } sub TeslaPowerwall2AC_Notify($$) { my ($hash,$dev) = @_; my $name = $hash->{NAME}; return if (IsDisabled($name)); my $devname = $dev->{NAME}; my $devtype = $dev->{TYPE}; my $events = deviceEvents($dev,1); return if (!$events); TeslaPowerwall2AC_Timer_GetData($hash) if( grep /^INITIALIZED$/,@{$events} or grep /^DELETEATTR.$name.disable$/,@{$events} or grep /^DELETEATTR.$name.interval$/,@{$events} or (grep /^DEFINED.$name$/,@{$events} and $init_done) ); return; } sub TeslaPowerwall2AC_Get($@) { my ($hash, $name, $cmd) = @_; my $arg; if( $cmd eq 'statusSOE' ) { $arg = lc($cmd); } elsif( $cmd eq 'aggregates' ) { $arg = lc($cmd); } elsif( $cmd eq 'siteinfo' ) { $arg = lc($cmd); } elsif( $cmd eq 'powerwalls' ) { $arg = lc($cmd); } elsif( $cmd eq 'sitemaster' ) { $arg = lc($cmd); } elsif( $cmd eq 'registration' ) { $arg = lc($cmd); } elsif( $cmd eq 'status' ) { $arg = lc($cmd); } else { my $list = 'statusSOE:noArg aggregates:noArg siteinfo:noArg sitemaster:noArg powerwalls:noArg registration:noArg status:noArg'; return "Unknown argument $cmd, choose one of $list"; } return 'There are still path commands in the action queue' if( defined($hash->{actionQueue}) and scalar(@{$hash->{actionQueue}}) > 0 ); unshift( @{$hash->{actionQueue}}, $arg ); TeslaPowerwall2AC_GetData($hash); return undef; } sub TeslaPowerwall2AC_Timer_GetData($) { my $hash = shift; my $name = $hash->{NAME}; if( defined($hash->{actionQueue}) and scalar(@{$hash->{actionQueue}}) == 0 ) { if( not IsDisabled($name) ) { while( my $obj = each %paths ) { unshift( @{$hash->{actionQueue}}, $obj ); } TeslaPowerwall2AC_GetData($hash); } else { readingsSingleUpdate($hash,'state','disabled',1); } } InternalTimer( gettimeofday()+$hash->{INTERVAL}, 'TeslaPowerwall2AC_Timer_GetData', $hash ); Log3 $name, 4, "TeslaPowerwall2AC ($name) - Call InternalTimer TeslaPowerwall2AC_Timer_GetData"; } sub TeslaPowerwall2AC_GetData($) { my ($hash) = @_; my $name = $hash->{NAME}; my $host = $hash->{HOST}; my $port = $hash->{PORT}; my $path = pop( @{$hash->{actionQueue}} ); my $uri = $host . ':' . $port . '/api/' . $paths{$path}; readingsSingleUpdate($hash,'state','fetch data - ' . scalar(@{$hash->{actionQueue}}) . ' entries in the Queue',1); HttpUtils_NonblockingGet( { url => "http://" . $uri, timeout => 5, method => 'GET', hash => $hash, setCmd => $path, doTrigger => 1, callback => \&TeslaPowerwall2AC_ErrorHandling, } ); Log3 $name, 4, "TeslaPowerwall2AC ($name) - Send with URI: http://$uri"; } sub TeslaPowerwall2AC_ErrorHandling($$$) { my ($param,$err,$data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; ### Begin Error Handling if( defined( $err ) ) { if( $err ne "" ) { readingsBeginUpdate( $hash ); readingsBulkUpdate( $hash, 'state', $err, 1); readingsBulkUpdate( $hash, 'lastRequestError', $err, 1 ); readingsEndUpdate( $hash, 1 ); Log3 $name, 3, "TeslaPowerwall2AC ($name) - RequestERROR: $err"; $hash->{actionQueue} = []; return; } } if( $data eq "" and exists( $param->{code} ) && $param->{code} ne 200 ) { readingsBeginUpdate( $hash ); readingsBulkUpdate( $hash, 'state', $param->{code}, 1 ); readingsBulkUpdate( $hash, 'lastRequestError', $param->{code}, 1 ); Log3 $name, 3, "TeslaPowerwall2AC ($name) - RequestERROR: ".$param->{code}; readingsEndUpdate( $hash, 1 ); Log3 $name, 5, "TeslaPowerwall2AC ($name) - RequestERROR: received http code ".$param->{code}." without any data after requesting"; $hash->{actionQueue} = []; return; } if( ( $data =~ /Error/i ) and exists( $param->{code} ) ) { readingsBeginUpdate( $hash ); readingsBulkUpdate( $hash, 'state', $param->{code}, 1 ); readingsBulkUpdate( $hash, "lastRequestError", $param->{code}, 1 ); readingsEndUpdate( $hash, 1 ); Log3 $name, 3, "TeslaPowerwall2AC ($name) - statusRequestERROR: http error ".$param->{code}; $hash->{actionQueue} = []; return; ### End Error Handling } TeslaPowerwall2AC_GetData($hash) if( defined($hash->{actionQueue}) and scalar(@{$hash->{actionQueue}}) > 0 ); Log3 $name, 4, "TeslaPowerwall2AC ($name) - Recieve JSON data: $data"; TeslaPowerwall2AC_ResponseProcessing($hash,$param->{setCmd},$data); } sub TeslaPowerwall2AC_ResponseProcessing($$$) { my ($hash,$path,$json) = @_; my $name = $hash->{NAME}; my $decode_json; my $readings; $decode_json = eval{decode_json($json)}; if($@){ Log3 $name, 4, "TeslaPowerwall2AC ($name) - error while request: $@"; readingsBeginUpdate($hash); readingsBulkUpdate($hash, 'JSON Error', $@); readingsBulkUpdate($hash, 'state', 'JSON error'); readingsEndUpdate($hash,1); return; } #### Verarbeitung der Readings zum passenden Path if( $path eq 'aggregates') { $readings = TeslaPowerwall2AC_ReadingsProcessing_Aggregates($hash,$decode_json); } elsif( $path eq 'powerwalls') { $readings = TeslaPowerwall2AC_ReadingsProcessing_Powerwalls($hash,$decode_json); } else { $readings = $decode_json; } TeslaPowerwall2AC_WriteReadings($hash,$path,$readings); } sub TeslaPowerwall2AC_WriteReadings($$$) { my ($hash,$path,$readings) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "TeslaPowerwall2AC ($name) - Write Readings"; readingsBeginUpdate($hash); while( my ($r,$v) = each %{$readings} ) { readingsBulkUpdate($hash,$path.'-'.$r,$v); } readingsBulkUpdate($hash,'batteryLevel',sprintf("%.1f",$readings->{percentage})) if( defined($readings->{percentage}) ); readingsBulkUpdate($hash,'batteryPower',sprintf("%.1f",(ReadingsVal($name,'siteinfo-nominal_system_energy_kWh',0)/100) * ReadingsVal($name,'statussoe-percentage',0) ) ); readingsBulkUpdateIfChanged($hash,'actionQueue',scalar(@{$hash->{actionQueue}}) . ' entries in the Queue'); readingsBulkUpdateIfChanged($hash,'state',(defined($hash->{actionQueue}) and scalar(@{$hash->{actionQueue}}) == 0 ? 'ready' : 'fetch data - ' . scalar(@{$hash->{actionQueue}}) . ' paths in actionQueue')); readingsEndUpdate($hash,1); } sub TeslaPowerwall2AC_ReadingsProcessing_Aggregates($$) { my ($hash,$decode_json) = @_; my $name = $hash->{NAME}; my %readings; if( ref($decode_json) eq "HASH" ) { while( my $obj = each %{$decode_json} ) { while( my ($r,$v) = each %{$decode_json->{$obj}} ) { $readings{$obj.'-'.$r} = $v; } } } else { $readings{'error'} = 'aggregates response is not a Hash'; } return \%readings; } sub TeslaPowerwall2AC_ReadingsProcessing_Powerwalls($$) { my ($hash,$decode_json) = @_; my $name = $hash->{NAME}; my %readings; if( ref($decode_json->{powerwalls}) eq "ARRAY" and scalar(@{$decode_json->{powerwalls}}) > 0 ) { my $i = 0; foreach my $powerwall (@{$decode_json->{powerwalls}}) { if( ref($powerwall) eq "HASH" ) { while( my ($r,$v) = each %{$powerwall} ) { $readings{'wall_'.$i.'_'.$r} = $v; } $i++; } } $readings{'numberOfWalls'} = $i; } else { $readings{'error'} = 'aggregates response is not a Array'; } return \%readings; } 1; =pod =item device =item summary Modul to retrieves data from a Tesla Powerwall 2AC =item summary_DE =begin html

Tesla Powerwall 2 AC

    TeslaPowerwall2AC - Retrieves data from a Tesla Powerwall 2AC System
    With this module it is possible to read the data from a Tesla Powerwall 2AC and to set it as reading.

    Define

      define <name> TeslaPowerwall2AC <HOST>

      Example:

        define myPowerWall TeslaPowerwall2AC 192.168.1.34

      This statement creates a Device with the name myPowerWall and the Host IP 192.168.1.34.
      After the device has been created, the current data of Powerwall is automatically read from the device.


    Readings
    • actionQueue - information about the entries in the action queue
    • aggregates-* - readings of the /api/meters/aggregates response
    • batteryLevel - battery level in percent
    • batteryPower - battery capacity in kWh
    • powerwalls-* - readings of the /api/powerwalls response
    • registration-* - readings of the /api/customer/registration response
    • siteinfo-* - readings of the /api/site_info response
    • sitemaster-* - readings of the /api/sitemaster response
    • state - information about internel modul processes
    • status-* - readings of the /api/status response
    • statussoe-* - readings of the /api/system_status/soe response
    get
    • aggregates - fetch data from url path /api/meters/aggregates
    • powerwalls - fetch data from url path /api/powerwalls
    • registration - fetch data from url path /api/customer/registration
    • siteinfo - fetch data from url path /api/site_info
    • sitemaster - fetch data from url path /api/sitemaster
    • status - fetch data from url path /api/status
    • statussoe - fetch data from url path /api/system_status/soe
    Attribute
    • interval - interval in seconds for automatically fetch data (default 300)
=end html =begin html_DE

Tesla Powerwall 2 AC

=end html_DE =cut