mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-01-31 06:39:11 +00:00
30ee0927a1
git-svn-id: https://svn.fhem.de/fhem/trunk@28690 2b470e98-0d58-463d-a4d8-8e2adae1ed80
574 lines
18 KiB
Perl
574 lines
18 KiB
Perl
# $Id$
|
|
####################################################################################################
|
|
#
|
|
# 12_OilFox.pm
|
|
#
|
|
# Copyright: Stephan Eisler
|
|
# Email: stephan@eisler.de
|
|
#
|
|
# This file is part of fhem.
|
|
#
|
|
# Fhem 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.
|
|
#
|
|
# Fhem 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.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with fhem. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
####################################################################################################
|
|
|
|
package FHEM::OilFox;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use Time::Local;
|
|
use JSON;
|
|
use HttpUtils;
|
|
use Blocking;
|
|
use Data::Dumper;
|
|
use GPUtils qw(GP_Import);
|
|
|
|
use constant API => "https://api.oilfox.io/";
|
|
|
|
BEGIN {
|
|
GP_Import(
|
|
qw(
|
|
readingFnAttributes
|
|
readingsSingleUpdate
|
|
readingsBeginUpdate
|
|
readingsEndUpdate
|
|
readingsBulkUpdate
|
|
deviceEvents
|
|
defs
|
|
HttpUtils_NonblockingGet
|
|
modules
|
|
attr
|
|
AttrVal
|
|
AttrNum
|
|
InternalTimer
|
|
RemoveInternalTimer
|
|
Log3
|
|
strftime)
|
|
);
|
|
}
|
|
|
|
sub _Export {
|
|
no strict qw/refs/;
|
|
my $pkg = caller(0);
|
|
my $main = $pkg;
|
|
$main =~ s/^(?:.+::)?([^:]+)$/main::$1\_/g;
|
|
foreach (@_) {
|
|
*{ $main . $_ } = *{ $pkg . '::' . $_ };
|
|
}
|
|
}
|
|
|
|
_Export(
|
|
qw(
|
|
Initialize
|
|
)
|
|
);
|
|
|
|
|
|
|
|
sub Initialize($) {
|
|
my ($hash) = @_;
|
|
|
|
$hash->{SetFn} = "FHEM::OilFox::Set";
|
|
$hash->{DefFn} = "FHEM::OilFox::Define";
|
|
$hash->{UndefFn} = "FHEM::OilFox::Undef";
|
|
$hash->{NotifyFn} = "FHEM::OilFox::Notify";
|
|
$hash->{AttrFn} = "FHEM::OilFox::Attr";
|
|
$hash->{AttrList} = "email " .
|
|
"password " .
|
|
"oilfox " .
|
|
"interval " .
|
|
"timeout " .
|
|
$readingFnAttributes;
|
|
}
|
|
|
|
sub Define($$){
|
|
my ( $hash, $def ) = @_;
|
|
my @a = split( "[ \t]+", $def );
|
|
my $name = $a[0];
|
|
|
|
return "too few parameters: define <NAME> OilFox" if( @a < 1 ) ;
|
|
|
|
%$hash = (%$hash,
|
|
NOTIFYDEV => "global,$name",
|
|
OilFox => {
|
|
CONNECTED => 0,
|
|
oilfox => 0,
|
|
oilfox_id => '',
|
|
token => '',
|
|
oilfox_name => '',
|
|
oilfox_hwid => '',
|
|
oilfox_tankVolume => 0,
|
|
oilfox_metering_value => 0,
|
|
oilfox_metering_fillingPercentage => 0,
|
|
oilfox_metering_liters => 0,
|
|
oilfox_metering_currentOilHeight => 0,
|
|
oilfox_metering_battery => 0,
|
|
oilfox_currentMeteringAt => '',
|
|
oilfox_nextMeteringAt => '',
|
|
oilfox_daysReach => 0,
|
|
oilfox_quantityUnit => '',
|
|
email => '',
|
|
password => '',
|
|
interval => 300,
|
|
access_token_expires => time(),
|
|
validationError => '',
|
|
},
|
|
);
|
|
|
|
$attr{$name}{room} = "OilFox" if( !defined( $attr{$name}{room} ) );
|
|
|
|
CONNECTED($hash,'initialized');
|
|
|
|
DoUpdate($hash);
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
sub Set($@) {
|
|
my ($hash,@a) = @_;
|
|
return "\"set $hash->{NAME}\" needs at least an argument" if ( @a < 2 );
|
|
my ($name,$setName,$setVal,$setVal2,$setVal3) = @a;
|
|
|
|
Log3 $name, 3, "$name: set called with $setName " . ($setVal ? $setVal : "") if ($setName ne "?");
|
|
|
|
if (CONNECTED($hash) eq 'disabled' && $setName !~ /clear/) {
|
|
return "Unknown argument $setName, choose one of clear:all,readings";
|
|
Log3 $name, 3, "$name: set called with $setName but device is disabled!" if ($setName ne "?");
|
|
return undef;
|
|
}
|
|
|
|
if ($setName !~ /update/) {
|
|
return "Unknown argument $setName, choose one of update";
|
|
} else {
|
|
Log3 $name, 3, "$name: set $setName";
|
|
}
|
|
|
|
if ($setName eq 'update') {
|
|
RemoveInternalTimer($hash);
|
|
DoUpdate($hash);
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub Notify($$) {
|
|
|
|
my ($hash,$dev) = @_;
|
|
my ($name) = ($hash->{NAME});
|
|
|
|
if (AttrVal($name, "disable", 0)) {
|
|
Log3 $name, 5, "Device '$name' is disabled, do nothing...";
|
|
CONNECTED($hash,'disabled');
|
|
return undef;
|
|
}
|
|
|
|
my $devname = $dev->{NAME};
|
|
my $devtype = $dev->{TYPE};
|
|
my $events = deviceEvents($dev,1);
|
|
return if (!$events);
|
|
|
|
Log3 $name, 5, "OilFox ($name) - Notify: " . Dumper $events;
|
|
|
|
$hash->{OilFox}->{updateStartTime} = time();
|
|
|
|
if ( $devtype eq 'Global') {
|
|
if (
|
|
grep /^INITIALIZED$/,@{$events}
|
|
or grep /^REREADCFG$/,@{$events}
|
|
or grep /^DEFINED.$name$/,@{$events}
|
|
or grep /^MODIFIED.$name$/,@{$events}
|
|
) {
|
|
APIAuth($hash);
|
|
}
|
|
}
|
|
|
|
if ( $devtype eq 'OilFox') {
|
|
if ( grep(/^state:.authenticated$/, @{$events}) ) {
|
|
get($hash);
|
|
}
|
|
|
|
if ( grep(/^state:.disconnected$/, @{$events}) ) {
|
|
Log3 $name, 3, "Reconnecting...";
|
|
APIAuth($hash);
|
|
}
|
|
|
|
if ( grep(/^state:.connected$/, @{$events}) ) {
|
|
DoUpdate($hash);
|
|
}
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub 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", "disable", 1 );
|
|
Log3 $name, 3, "$name - disabled";
|
|
}
|
|
|
|
elsif( $cmd eq "del" ) {
|
|
readingsSingleUpdate ( $hash, "state", "active", 1 );
|
|
Log3 $name, 3, "$name - enabled";
|
|
}
|
|
}
|
|
|
|
elsif( $attrName eq "email" ) {
|
|
if( $cmd eq "set" ) {
|
|
$hash->{OilFox}->{email} = $attrVal;
|
|
Log3 $name, 3, "$name - email set to " . $hash->{OilFox}->{email};
|
|
}
|
|
}
|
|
|
|
elsif( $attrName eq "password" ) {
|
|
if( $cmd eq "set" ) {
|
|
$hash->{OilFox}->{password} = $attrVal;
|
|
Log3 $name, 3, "$name - password set to " . $hash->{OilFox}->{password};
|
|
}
|
|
}
|
|
|
|
elsif( $attrName eq "oilfox" ) {
|
|
if( $cmd eq "set" ) {
|
|
$hash->{OilFox}->{oilfox} = $attrVal;
|
|
Log3 $name, 3, "$name - oilfox set to " . $hash->{OilFox}->{oilfox};
|
|
}
|
|
elsif( $cmd eq "del" ) {
|
|
$hash->{OilFox}->{oilfox} = 0;
|
|
Log3 $name, 3, "$name - deleted oilfox and set to default: 0";
|
|
}
|
|
}
|
|
|
|
elsif( $attrName eq "interval" ) {
|
|
if( $cmd eq "set" ) {
|
|
return "Interval must be greater than 0"
|
|
unless($attrVal > 0);
|
|
$hash->{OilFox}->{interval} = $attrVal;
|
|
RemoveInternalTimer($hash);
|
|
InternalTimer( time() + $hash->{OilFox}->{interval}, "FHEM::OilFox::DoUpdate", $hash);
|
|
Log3 $name, 3, "$name - set interval: $attrVal";
|
|
}
|
|
|
|
elsif( $cmd eq "del" ) {
|
|
$hash->{OilFox}->{interval} = 300;
|
|
RemoveInternalTimer($hash);
|
|
InternalTimer( time() + $hash->{OilFox}->{interval}, "FHEM::OilFox::DoUpdate", $hash);
|
|
Log3 $name, 3, "$name - deleted interval and set to default: 300";
|
|
}
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
|
|
sub Undef($$){
|
|
my ( $hash, $arg ) = @_;
|
|
my $name = $hash->{NAME};
|
|
my $deviceId = $hash->{DEVICEID};
|
|
delete $modules{OilFox}{defptr}{$deviceId};
|
|
RemoveInternalTimer($hash);
|
|
return undef;
|
|
}
|
|
|
|
|
|
sub APIAuth($) {
|
|
my ($hash, $def) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
my $email = $hash->{OilFox}->{email};
|
|
my $password = $hash->{OilFox}->{password};
|
|
|
|
my $header = "Content-Type: application/json\r\nAccept: application/json";
|
|
my $json = '{
|
|
"email" : "' . $email. '",
|
|
"password" : "' . $password. '"
|
|
}';
|
|
|
|
HttpUtils_NonblockingGet({
|
|
url => API . "customer-api/v1/login",
|
|
timeout => AttrNum($name, "timeout", 45),
|
|
hash => $hash,
|
|
method => "POST",
|
|
header => $header,
|
|
data => $json,
|
|
callback => \&APIAuthResponse,
|
|
});
|
|
|
|
}
|
|
|
|
sub APIAuthResponse($) {
|
|
my ($param, $err, $data) = @_;
|
|
my $hash = $param->{hash};
|
|
my $name = $hash->{NAME};
|
|
|
|
if($err ne "") {
|
|
CONNECTED($hash,'error');
|
|
Log3 $name, 2, "error while requesting ".$param->{url}." - $err";
|
|
|
|
} elsif($data ne "") {
|
|
|
|
my $result = eval { decode_json($data) };
|
|
if ($@) {
|
|
Log3( $name, 2, " - JSON error while request: $@");
|
|
return;
|
|
}
|
|
|
|
if ($result->{errors}) {
|
|
CONNECTED($hash,'error');
|
|
Log3 $name, 2, "Error: " . $result->{errors}[0]->{detail};
|
|
|
|
} else {
|
|
Log3 $name, 2, "$data";
|
|
|
|
$hash->{OilFox}->{token} = $result->{access_token};
|
|
$hash->{OilFox}->{expires} = time() + 300; # TODO Read exp from JWT
|
|
|
|
readingsBeginUpdate($hash);
|
|
readingsBulkUpdate($hash,'token',$hash->{OilFox}->{token} );
|
|
|
|
my $expire_date = strftime("%Y-%m-%d %H:%M:%S", localtime($hash->{OilFox}->{expires}));
|
|
readingsBulkUpdate($hash,'access_token_expires',$expire_date );
|
|
readingsEndUpdate($hash, 1);
|
|
|
|
CONNECTED($hash,'authenticated');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sub CONNECTED($@) {
|
|
my ($hash,$set) = @_;
|
|
if ($set) {
|
|
$hash->{OilFox}->{CONNECTED} = $set;
|
|
%{$hash->{updateDispatch}} = ();
|
|
if (!defined($hash->{READINGS}->{state}->{VAL}) || $hash->{READINGS}->{state}->{VAL} ne $set) {
|
|
readingsSingleUpdate($hash,"state",$set,1);
|
|
}
|
|
return undef;
|
|
} else {
|
|
if ($hash->{OilFox}->{CONNECTED} eq 'disabled') {
|
|
return 'disabled';
|
|
}
|
|
elsif ($hash->{OilFox}->{CONNECTED} eq 'connected') {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
sub DoUpdate($) {
|
|
my ($hash) = @_;
|
|
my ($name) = $hash->{NAME};
|
|
|
|
Log3 $name, 5, "doUpdate() called.";
|
|
|
|
if (CONNECTED($hash) eq "disabled") {
|
|
Log3 $name, 3, "$name - Device is disabled.";
|
|
return undef;
|
|
}
|
|
|
|
if (time() >= $hash->{OilFox}->{expires} ) {
|
|
Log3 $name, 2, "LOGIN TOKEN MISSING OR EXPIRED";
|
|
CONNECTED($hash,'disconnected');
|
|
|
|
}
|
|
elsif ($hash->{OilFox}->{CONNECTED} eq 'connected') {
|
|
Log3 $name, 4, "Update with device: " . $hash->{OilFox}->{oilfox_hwid} . " Interval:". $hash->{OilFox}->{interval};
|
|
get($hash);
|
|
InternalTimer( time() + $hash->{OilFox}->{interval}, "FHEM::OilFox::DoUpdate", $hash);
|
|
}
|
|
|
|
}
|
|
|
|
sub get($) {
|
|
my ($hash) = @_;
|
|
my ($name) = $hash->{NAME};
|
|
|
|
my $token = $hash->{OilFox}->{token};
|
|
my $header = "Content-Type: application/json\r\nAccept: application/json\r\nAuthorization: Bearer " . $token;
|
|
|
|
HttpUtils_NonblockingGet({
|
|
url => API . "customer-api/v1/device",
|
|
timeout => AttrNum($name, "timeout", 45),
|
|
hash => $hash,
|
|
method => "GET",
|
|
header => $header,
|
|
callback => \&getResponse,
|
|
});
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub getResponse($) {
|
|
|
|
my ($param, $err, $data) = @_;
|
|
my $hash = $param->{hash};
|
|
my $name = $hash->{NAME};
|
|
|
|
if($err ne "") {
|
|
Log3 $name, 2, "error while requesting ".$param->{url}." - $err";
|
|
|
|
} elsif($data ne "") {
|
|
|
|
if ($data eq '{"errorCode":401}') {
|
|
Log3 $name, 2, "Please register OilFox first";
|
|
$hash->{OilFox}->{oilfox_hwid} = "none";
|
|
|
|
CONNECTED($hash,'connected');
|
|
|
|
} else {
|
|
|
|
Log3 $name, 5, "OilFox(s) found";
|
|
Log3 $name, 5, $data;
|
|
|
|
my $result = eval { decode_json($data) };
|
|
if ($@) {
|
|
Log3( $name, 2, " - JSON error while request: $@");
|
|
return;
|
|
}
|
|
|
|
my $oilfox = $hash->{OilFox}->{oilfox};
|
|
Log3 $name, 5, $result->{'items'}->[$oilfox]->{'hwid'};
|
|
|
|
my $myoilfox = $result->{'items'}->[$oilfox];
|
|
$hash->{OilFox}->{oilfox_hwid} = $myoilfox->{'hwid'};
|
|
$hash->{OilFox}->{oilfox_currentMeteringAt} = $myoilfox->{'currentMeteringAt'};
|
|
$hash->{OilFox}->{oilfox_nextMeteringAt} = $myoilfox->{'nextMeteringAt'};
|
|
$hash->{OilFox}->{oilfox_daysReach} = $myoilfox->{'daysReach'};
|
|
$hash->{OilFox}->{oilfox_metering_fillingPercentage} = $myoilfox->{'fillLevelPercent'};
|
|
$hash->{OilFox}->{oilfox_metering_liters} = $myoilfox->{'fillLevelQuantity'};
|
|
$hash->{OilFox}->{oilfox_metering_battery} = $myoilfox->{'batteryLevel'};
|
|
$hash->{OilFox}->{oilfox_quantityUnit} = $myoilfox->{'quantityUnit'};
|
|
$hash->{OilFox}->{validationError} = $myoilfox->{'validationError'};
|
|
|
|
CONNECTED($hash,'connected');
|
|
|
|
}
|
|
|
|
readingsBeginUpdate($hash);
|
|
readingsBulkUpdate($hash, "oilfox_hwid", $hash->{OilFox}->{oilfox_hwid});
|
|
readingsBulkUpdate($hash, "oilfox_currentMeteringAt", $hash->{OilFox}->{oilfox_currentMeteringAt});
|
|
readingsBulkUpdate($hash, "oilfox_nextMeteringAt", $hash->{OilFox}->{oilfox_nextMeteringAt});
|
|
readingsBulkUpdate($hash, "oilfox_daysReach", $hash->{OilFox}->{oilfox_daysReach});
|
|
readingsBulkUpdate($hash, "oilfox_metering_fillingPercentage", $hash->{OilFox}->{oilfox_metering_fillingPercentage});
|
|
readingsBulkUpdate($hash, "oilfox_metering_liters", $hash->{OilFox}->{oilfox_metering_liters});
|
|
readingsBulkUpdate($hash, "oilfox_metering_battery", $hash->{OilFox}->{oilfox_metering_battery});
|
|
readingsBulkUpdate($hash, "oilfox_quantityUnit", $hash->{OilFox}->{oilfox_quantityUnit});
|
|
readingsBulkUpdate($hash, "validationError", $hash->{OilFox}->{validationError});
|
|
readingsEndUpdate($hash, 1);
|
|
|
|
}
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
1;
|
|
|
|
=pod
|
|
=item device
|
|
=item summary support for OilFox
|
|
=begin html
|
|
|
|
<a id="OilFox"></a>
|
|
<h3>OilFox</h3>
|
|
<ul>
|
|
<u><b>Requirements</b></u>
|
|
<br><br>
|
|
<ul>
|
|
<li>This module allows the communication between the Oilfox Cloud and FHEM.</li>
|
|
</ul>
|
|
<br>
|
|
|
|
<a id="OilFoxDefine"></a>
|
|
<b>Define</b>
|
|
<ul>
|
|
<code>define <name> OilFox</code>
|
|
<br><br>
|
|
Example:
|
|
<ul><br>
|
|
<code>define myOilFox OilFox<br>
|
|
attr myOilFox email YOUR_EMAIL<br>
|
|
attr myOilFox password YOUR_PASSWORD
|
|
</code><br>
|
|
</ul>
|
|
<br><br>
|
|
You must set both attributes <b>email</b> and <b>password</b>. These are the same that you use to login via the OilFox App.
|
|
</ul>
|
|
<br>
|
|
|
|
|
|
<a id="OilFox-attr"></a>
|
|
<b>Attributes</b>
|
|
<ul>
|
|
<li>email - Email that is used in OilFox App</li>
|
|
<li>password - Password that is used in OilFox App</li>
|
|
</ul>
|
|
<br>
|
|
|
|
<b>Optional attributes</b>
|
|
<ul>
|
|
<li>oilfox - ID of OilFox, if more that one is registered. Default: 0</li>
|
|
<li>timeout - Connection timeout</li>
|
|
<li>interval - Time in seconds that is used to get new data from OilFox Cloud. Default: 300</li>
|
|
</ul>
|
|
<br>
|
|
|
|
<a name="OilFoxReadings"></a>
|
|
<b>Readings</b>
|
|
<ul>
|
|
<li>access_token_expires - date when session of OilFox Cloud expires</li>
|
|
<li>oilfox_hwid - Id of the OilFox</li>
|
|
<li>oilfox_name - Name of the OilFox</li>
|
|
<li>oilfox_daysReach - Estimated days until the storage runs empty</li>
|
|
<li>oilfox_tankVolume - Tank Volume in liters</li>
|
|
<li>oilfox_metering_battery - enum of the battery level</li>
|
|
<ul>
|
|
<li>FULL - Full battery level</li>
|
|
<li>GOOD - Good battery level</li>
|
|
<li>MEDIUM - Medium battery level</li>
|
|
<li>WARNING - Low battery level</li>
|
|
<li>CRITICAL - Critical battery level</li>
|
|
</ul>
|
|
<li>oilfox_metering_value - Tank Value</li>
|
|
<li>oilfox_metering_fillingPercentage - Tank filling Percentage</li>
|
|
<li>oilfox_metering_liters - Tank filling liters</li>
|
|
<li>oilfox_metering_currentOilHeight - Tank current Oil Height</li>
|
|
<li>oilfox_currentMeteringAt - Timestamp of measurement</li>
|
|
<li>oilfox_nextMeteringAt - Timestamp of next measurement</li>
|
|
<li>validationError - enum with errors regarding the measurement</li>
|
|
<ul>
|
|
<li>NO_METERING - No measurement yet</li>
|
|
<li>EMPTY_METERING - Incorrect Measurement</li>
|
|
<li>NO_EXTRACTED_VALUE - No fill level detected</li>
|
|
<li>SENSOR_CONFIG - Faulty measurement</li>
|
|
<li>MISSING_STORAGE_CONFIG - Storage configuration missing</li>
|
|
<li>INVALID_STORAGE_CONFIG - Incorrect storage configuration</li>
|
|
<li>DISTANCE_TOO_SHORT - Measured distance too small</li>
|
|
<li>ABOVE_STORAGE_MAX - Storage full</li>
|
|
<li>BELOW_STORAGE_MIN - Calculated filling level implausible</li>
|
|
</ul>
|
|
</ul>
|
|
</ul>
|
|
|
|
=end html
|