mod-NUKI/74_NUKIDevice.pm

597 lines
18 KiB
Perl
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

###############################################################################
#
# Developed with Kate
#
# (c) 2016 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$
#
###############################################################################
package main;
use strict;
use warnings;
use JSON;
#use Time::HiRes qw(gettimeofday);
my $version = "0.3.36";
sub NUKIDevice_Initialize($) {
my ($hash) = @_;
$hash->{SetFn} = "NUKIDevice_Set";
$hash->{DefFn} = "NUKIDevice_Define";
$hash->{UndefFn} = "NUKIDevice_Undef";
$hash->{AttrFn} = "NUKIDevice_Attr";
my $webhookFWinstance = join( ",", devspec2array('TYPE=FHEMWEB:FILTER=TEMPORARY!=1') );
$hash->{AttrList} = "IODev ".
"disable:1 ".
"webhookFWinstance:$webhookFWinstance ".
"webhookHttpHostname ".
"webhookPort ".
$readingFnAttributes;
foreach my $d(sort keys %{$modules{NUKIDevice}{defptr}}) {
my $hash = $modules{NUKIDevice}{defptr}{$d};
$hash->{VERSION} = $version;
}
}
sub NUKIDevice_Define($$) {
my ( $hash, $def ) = @_;
my @a = split( "[ \t]+", $def );
splice( @a, 1, 1 );
my $iodev;
my $i = 0;
foreach my $param ( @a ) {
if( $param =~ m/IODev=([^\s]*)/ ) {
$iodev = $1;
splice( @a, $i, 3 );
last;
}
$i++;
}
return "too few parameters: define <name> NUKIDevice <nukiId>" if( @a < 2 );
my ($name,$nukiId) = @a;
$hash->{NUKIID} = $nukiId;
$hash->{VERSION} = $version;
$hash->{STATE} = 'Initialized';
AssignIoPort($hash,$iodev) if( !$hash->{IODev} );
if(defined($hash->{IODev}->{NAME})) {
Log3 $name, 3, "$name: I/O device is " . $hash->{IODev}->{NAME};
} else {
Log3 $name, 1, "$name: no I/O device";
}
$iodev = $hash->{IODev}->{NAME};
my $code = $hash->{NUKIID};
$code = $iodev ."-". $code if( defined($iodev) );
my $d = $modules{NUKIDevice}{defptr}{$code};
return "NUKIDevice device $hash->{NUKIID} on NUKIBridge $iodev already defined as $d->{NAME}."
if( defined($d)
&& $d->{IODev} == $hash->{IODev}
&& $d->{NAME} ne $name );
$modules{NUKIDevice}{defptr}{$code} = $hash;
Log3 $name, 3, "NUKIDevice ($name) - defined with Code: $code";
$attr{$name}{room} = "NUKI" if( !defined( $attr{$name}{room} ) );
if( $init_done ) {
InternalTimer( gettimeofday()+int(rand(10)), "NUKIDevice_GetUpdate", $hash, 0 );
} else {
InternalTimer( gettimeofday()+15+int(rand(5)), "NUKIDevice_GetUpdate", $hash, 0 );
}
return undef;
}
sub NUKIDevice_Undef($$) {
my ( $hash, $arg ) = @_;
my $nukiId = $hash->{NUKIID};
my $name = $hash->{NAME};
RemoveInternalTimer($hash);
my $code = $hash->{NUKIID};
$code = $hash->{IODev}->{NAME} ."-". $code if( defined($hash->{IODev}->{NAME}) );
Log3 $name, 3, "NUKIDevice ($name) - undefined with Code: $code";
delete($modules{NUKIDevice}{defptr}{$code});
return undef;
}
sub NUKIDevice_Attr(@) {
my ( $cmd, $name, $attrName, $attrVal ) = @_;
my $hash = $defs{$name};
my $token = $hash->{IODev}->{TOKEN};
if( $attrName eq "disable" ) {
if( $cmd eq "set" ) {
if( $attrVal eq "0" ) {
RemoveInternalTimer( $hash );
InternalTimer( gettimeofday()+2, "NUKIDevice_GetUpdateInternalTimer", $hash, 0 );
readingsSingleUpdate ( $hash, "state", "Initialized", 1 );
Log3 $name, 3, "NUKIDevice ($name) - enabled";
} else {
readingsSingleUpdate ( $hash, "state", "disabled", 1 );
RemoveInternalTimer( $hash );
Log3 $name, 3, "NUKIDevice ($name) - disabled";
}
} else {
RemoveInternalTimer( $hash );
InternalTimer( gettimeofday()+2, "NUKIDevice_GetUpdateInternalTimer", $hash, 0 );
readingsSingleUpdate ( $hash, "state", "Initialized", 1 );
Log3 $name, 3, "NUKIDevice ($name) - enabled";
}
}
######################
#### webhook #########
return "Invalid value for attribute $attrName: can only by FQDN or IPv4 or IPv6 address" if ( $attrVal && $attrName eq "webhookHttpHostname" && $attrVal !~ /^([A-Za-z_.0-9]+\.[A-Za-z_.0-9]+)|[0-9:]+$/ );
return "Invalid value for attribute $attrName: needs to be different from the defined name/address of your Smartlock, we need to know how Smartlock can connect back to FHEM here!" if ( $attrVal && $attrName eq "webhookHttpHostname" && $attrVal eq $hash->{DeviceName} );
return "Invalid value for attribute $attrName: FHEMWEB instance $attrVal not existing" if ( $attrVal && $attrName eq "webhookFWinstance" && ( !defined( $defs{$attrVal} ) || $defs{$attrVal}{TYPE} ne "FHEMWEB" ) );
return "Invalid value for attribute $attrName: needs to be an integer value" if ( $attrVal && $attrName eq "webhookPort" && $attrVal !~ /^\d+$/ );
if ( $attrName =~ /^webhook.*/ ) {
my $webhookHttpHostname = ( $attrName eq "webhookHttpHostname" ? $attrVal : AttrVal( $name, "webhookHttpHostname", "" ) );
my $webhookFWinstance = ( $attrName eq "webhookFWinstance" ? $attrVal : AttrVal( $name, "webhookFWinstance", "" ) );
$hash->{WEBHOOK_URI} = "%2F" . AttrVal( $webhookFWinstance, "webname", "fhem" ) . "%2FNUKIDevice";
$hash->{WEBHOOK_PORT} = ( $attrName eq "webhookPort" ? $attrVal : AttrVal( $name, "webhookPort", InternalVal( $webhookFWinstance, "PORT", "" )) );
$hash->{WEBHOOK_URL} = "";
$hash->{WEBHOOK_COUNTER} = "0";
if ( $webhookHttpHostname ne "" && $hash->{WEBHOOK_PORT} ne "" ) {
$hash->{WEBHOOK_URL} = "http://" . $webhookHttpHostname . ":" . $hash->{WEBHOOK_PORT} . $hash->{WEBHOOK_URI};
my $url = "http%3A%2F%2F$webhookHttpHostname" . "%3A" . $hash->{WEBHOOK_PORT} . $hash->{WEBHOOK_URI} . "-" . $hash->{NUKIID};
Log3 $name, 3, "NUKIDevice ($name) - URL ist: $url";
NUKIDevice_ReadFromNUKIBridge($hash,"callback/add",$url,undef ) if( $init_done );
$hash->{WEBHOOK_REGISTER} = "sent";
} else {
$hash->{WEBHOOK_REGISTER} = "incomplete_attributes";
}
}
return undef;
}
sub NUKIDevice_Set($$@) {
my ($hash, $name, @aa) = @_;
my ($cmd, @args) = @aa;
my $lockAction;
if( $cmd eq 'statusRequest' ) {
return "usage: statusRequest" if( @args != 0 );
NUKIDevice_GetUpdate($hash);
return undef;
} elsif( $cmd eq 'lock' ) {
$lockAction = $cmd;
} elsif( $cmd eq 'unlock' ) {
$lockAction = $cmd;
} elsif( $cmd eq 'unlatch' ) {
$lockAction = $cmd;
} elsif( $cmd eq 'locknGo' ) {
$lockAction = $cmd;
} elsif( $cmd eq 'locknGoWithUnlatch' ) {
$lockAction = $cmd;
} else {
my $list = "statusRequest:noArg unlock:noArg lock:noArg unlatch:noArg locknGo:noArg locknGoWithUnlatch:noArg";
return "Unknown argument $cmd, choose one of $list";
}
$hash->{helper}{lockAction} = $lockAction;
NUKIDevice_ReadFromNUKIBridge($hash,"lockAction",$lockAction,$hash->{NUKIID} );
return undef;
}
sub NUKIDevice_GetUpdateTimer($) {
my ($hash) = @_;
my $name = $hash->{NAME};
RemoveInternalTimer($hash);
if( !IsDisabled($name) ) {
NUKIDevice_ReadFromNUKIBridge($hash, "lockState", undef, $hash->{NUKIID} );
Log3 $name, 5, "NUKIDevice ($name) - NUKIDevice_GetUpdateTimer Call NUKIDevice_ReadFromNUKIBridge";
InternalTimer( gettimeofday()+12+int(rand(18)), "NUKIDevice_GetUpdateTimer", $hash, 1 );
}
return undef;
}
sub NUKIDevice_GetUpdate($) {
my ($hash) = @_;
my $name = $hash->{NAME};
RemoveInternalTimer($hash);
NUKIDevice_ReadFromNUKIBridge($hash, "lockState", undef, $hash->{NUKIID} ) if( !IsDisabled($name) );
Log3 $name, 5, "NUKIDevice ($name) - NUKIDevice_GetUpdate Call NUKIDevice_ReadFromNUKIBridge" if( !IsDisabled($name) );
return undef;
}
sub NUKIDevice_ReadFromNUKIBridge($@) {
my ($hash,@a) = @_;
my $name = $hash->{NAME};
Log3 $name, 4, "NUKIDevice ($name) - NUKIDevice_ReadFromNUKIBridge check Bridge connected";
return "IODev $hash->{IODev} is not connected" if( ReadingsVal($hash->{IODev}->{NAME},"state","not connected") eq "not connected" );
no strict "refs";
my $ret;
unshift(@a,$name);
Log3 $name, 4, "NUKIDevice ($name) - NUKIDevice_ReadFromNUKIBridge Bridge is connected call IOWrite";
$ret = IOWrite($hash,$hash,@a);
use strict "refs";
return $ret;
return if(IsDummy($name) || IsIgnored($name));
my $iohash = $hash->{IODev};
if(!$iohash ||
!$iohash->{TYPE} ||
!$modules{$iohash->{TYPE}} ||
!$modules{$iohash->{TYPE}}{ReadFn}) {
Log3 $name, 3, "No I/O device or ReadFn found for $name";
return;
}
no strict "refs";
unshift(@a,$name);
$ret = &{$modules{$iohash->{TYPE}}{ReadFn}}($iohash, @a);
use strict "refs";
return $ret;
}
sub NUKIDevice_Parse($$) {
my($hash,$result) = @_;
my $name = $hash->{NAME};
#########################################
####### Errorhandling #############
if( $result =~ /\d{3}/ ) {
if( $result eq 400 ) {
readingsSingleUpdate( $hash, "state", "action is undefined", 1 );
Log3 $name, 3, "NUKIDevice ($name) - action is undefined";
return;
}
if( $result eq 404 ) {
readingsSingleUpdate( $hash, "state", "nukiId is not known", 1 );
Log3 $name, 3, "NUKIDevice ($name) - nukiId is not known";
return;
}
}
#########################################
#### verarbeiten des JSON Strings #######
my $decode_json = decode_json($result);
if( ref($decode_json) ne "HASH" ) {
Log3 $name, 2, "NUKIDevice ($name) - got wrong status message for $name: $decode_json";
return undef;
}
Log3 $name, 5, "NUKIDevice ($name) - parse status message for $name";
NUKIDevice_WriteReadings($hash,$decode_json);
}
sub NUKIDevice_WriteReadings($$) {
my ($hash,$decode_json) = @_;
my $name = $hash->{NAME};
############################
#### Status des Smartlock
readingsBeginUpdate($hash);
my $battery;
if( defined($decode_json->{batteryCritical}) ) {
if( $decode_json->{batteryCritical} eq "false" ) {
$battery = "ok";
} elsif ( $decode_json->{batteryCritical} eq "true" ) {
$battery = "low";
} else {
$battery = "parseError";
}
}
if( defined($hash->{helper}{lockAction}) ) {
my ($state,$lockState);
$state = $hash->{helper}{lockAction} if( $decode_json->{success} eq "true" );
$state = "error" if( $decode_json->{success} eq "false" );
$lockState = $hash->{helper}{lockAction} if( $decode_json->{success} eq "true" );
readingsBulkUpdate( $hash, "state", $state );
readingsBulkUpdate( $hash, "lockState", $lockState );
readingsBulkUpdate( $hash, "success", $decode_json->{success} );
readingsBulkUpdate( $hash, "batteryCritical", $decode_json->{batteryCritical} );
readingsBulkUpdate( $hash, "battery", $battery );
delete $hash->{helper}{lockAction};
Log3 $name, 5, "NUKIDevice ($name) - readings set for $name";
} else {
readingsBulkUpdate( $hash, "batteryCritical", $decode_json->{batteryCritical} );
readingsBulkUpdate( $hash, "lockState", $decode_json->{stateName} );
readingsBulkUpdate( $hash, "state", $decode_json->{stateName} );
readingsBulkUpdate( $hash, "battery", $battery );
readingsBulkUpdate( $hash, "success", $decode_json->{success} );
Log3 $name, 5, "NUKIDevice ($name) - readings set for $name";
}
readingsEndUpdate( $hash, 1 );
return undef;
}
sub NUKIDevice_CGI() {
my ($request) = @_;
# data received
if ( defined( $FW_httpheader{UUID} ) ) {
if ( defined( $modules{NUKIDevice}{defptr} ) ) {
while ( my ( $key, $value ) = each %{ $modules{NUKIDevice}{defptr} } ) {
my $uuid = ReadingsVal( $key, "uuid", undef );
next if ( !$uuid || $uuid ne $FW_httpheader{UUID} );
$defs{$key}{WEBHOOK_COUNTER}++;
$defs{$key}{WEBHOOK_LAST} = TimeNow();
Log3 $key, 4, "NUKIDevice ($key) - Received webhook for matching UUID at device $key";
my $delay = undef;
# we need some delay as to the Robo seems to send webhooks but it's status does
# not really reflect the change we'd expect to get here already so give 'em some
# more time to think about it...
$delay = "2" if ( defined( $defs{$key}{LAST_COMMAND} ) && time() - time_str2num( $defs{$key}{LAST_COMMAND} ) < 3 );
#Hier muß die NUKIDevice_Parse Funktion aufgerufen werden
last;
}
}
return ( undef, undef );
}
# no data received
else {
Log3 undef, 4, "NUKIDevice - received malformed request\n$request";
}
return ( "text/plain; charset=utf-8", "Call failure: " . $request );
}
sub NUKIDevice_time2sec($) {
my ($timeString) = @_;
my @time = split /:/, $timeString;
return $time[0] * 3600 + $time[1] * 60;
}
1;
=pod
=item device
=item summary Modul to control the Nuki Smartlock's
=item summary_DE Modul zur Steuerung des Nuki Smartlocks.
=begin html
<a name="NUKIDevice"></a>
<h3>NUKIDevice</h3>
<ul>
<u><b>NUKIDevice - Controls the Nuki Smartlock</b></u>
<br>
The Nuki module connects FHEM over the Nuki Bridge with a Nuki Smartlock. After that, it´s possible to lock and unlock the Smartlock.<br>
Normally the Nuki devices are automatically created by the bridge module.
<br><br>
<a name="NUKIDevicedefine"></a>
<b>Define</b>
<ul><br>
<code>define &lt;name&gt; NUKIDevice &lt;Nuki-Id&gt; &lt;IODev-Device&gt;</code>
<br><br>
Example:
<ul><br>
<code>define Frontdoor NUKIDevice 1 NBridge1</code><br>
</ul>
<br>
This statement creates a NUKIDevice with the name Frontdoor, the NukiId 1 and the IODev device NBridge1.<br>
After the device has been created, the current state of the Smartlock is automatically read from the bridge.
</ul>
<br><br>
<a name="NUKIDevicereadings"></a>
<b>Readings</b>
<ul>
<li>state - Status of the Smartlock or error message if any error.</li>
<li>lockState - current lock status uncalibrated, locked, unlocked, unlocked (lock n go), unlatched, locking, unlocking, unlatching, motor blocked, undefined.</li>
<li>succes - true, false Returns the status of the last closing command. Ok or not Ok.</li>
<li>batteryCritical - Is the battery in a critical state? True, false</li>
<li>battery - battery status, ok / low</li>
</ul>
<br><br>
<a name="NUKIDeviceset"></a>
<b>Set</b>
<ul>
<li>statusRequest - retrieves the current state of the smartlock from the bridge.</li>
<li>lock - lock</li>
<li>unlock - unlock</li>
<li>unlatch - unlock / open Door</li>
<li>locknGo - lock when gone</li>
<li>locknGoWithUnlatch - lock after the door has been opened</li>
<br>
</ul>
<br><br>
<a name="NUKIDeviceattribut"></a>
<b>Attributes</b>
<ul>
<li>disable - disables the Nuki device</li>
<br>
</ul>
</ul>
=end html
=begin html_DE
<a name="NUKIDevice"></a>
<h3>NUKIDevice</h3>
<ul>
<u><b>NUKIDevice - Steuert das Nuki Smartlock</b></u>
<br>
Das Nuki Modul verbindet FHEM über die Nuki Bridge mit einem Nuki Smartlock. Es ist dann m&ouml;glich das Schloss zu ver- und entriegeln.<br>
In der Regel werden die Nuki Devices automatisch durch das Bridgemodul angelegt.
<br><br>
<a name="NUKIDevicedefine"></a>
<b>Define</b>
<ul><br>
<code>define &lt;name&gt; NUKIDevice &lt;Nuki-Id&gt; &lt;IODev-Device&gt;</code>
<br><br>
Beispiel:
<ul><br>
<code>define Haust&uuml;r NUKIDevice 1 NBridge1</code><br>
</ul>
<br>
Diese Anweisung erstellt ein NUKIDevice mit Namen Haust&uuml;r, der NukiId 1 sowie dem IODev Device NBridge1.<br>
Nach dem anlegen des Devices wird automatisch der aktuelle Zustand des Smartlocks aus der Bridge gelesen.
</ul>
<br><br>
<a name="NUKIDevicereadings"></a>
<b>Readings</b>
<ul>
<li>state - Status des Smartlock bzw. Fehlermeldung von Fehler vorhanden.</li>
<li>lockState - aktueller Schlie&szlig;status uncalibrated, locked, unlocked, unlocked (lock n go), unlatched, locking, unlocking, unlatching, motor blocked, undefined.</li>
<li>succes - true, false Gibt des Status des letzten Schlie&szlig;befehles wieder. Geklappt oder nicht geklappt.</li>
<li>batteryCritical - Ist die Batterie in einem kritischen Zustand? true, false</li>
<li>battery - Status der Batterie, ok/low</li>
</ul>
<br><br>
<a name="NUKIDeviceset"></a>
<b>Set</b>
<ul>
<li>statusRequest - ruft den aktuellen Status des Smartlocks von der Bridge ab.</li>
<li>lock - verschlie&szlig;en</li>
<li>unlock - aufschlie&szlig;en</li>
<li>unlatch - entriegeln/Falle &ouml;ffnen.</li>
<li>locknGo - verschlie&szlig;en wenn gegangen</li>
<li>locknGoWithUnlatch - verschlie&szlig;en nach dem die Falle ge&ouml;ffnet wurde.</li>
<br>
</ul>
<br><br>
<a name="NUKIDeviceattribut"></a>
<b>Attribute</b>
<ul>
<li>disable - deaktiviert das Nuki Device</li>
<br>
</ul>
</ul>
=end html_DE
=cut