From 016b9f7c6860e0fefeece03c43520be0d9b78931 Mon Sep 17 00:00:00 2001 From: LeonGaultier Date: Sat, 11 Jan 2020 13:30:09 +0000 Subject: [PATCH] 73_NUKIBridge: 74_NUKIDevice change Code to 2 step modul and Dispatch Fn git-svn-id: https://svn.fhem.de/fhem/trunk@20938 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 2 + fhem/FHEM/73_NUKIBridge.pm | 1400 +++++++++++++++++++++++------------- fhem/FHEM/74_NUKIDevice.pm | 829 ++++++++++----------- 3 files changed, 1279 insertions(+), 952 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index e5c147ef2..8b9bb3600 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: 73_NUKIBridge: 74_NUKIDevice change Code to 2 step modul + and Dispatch Fn, make Bridge robust - bugfix: 74_XiaomiBTLESens: fix thermoHydroSens loop bug - bugfix: 73_AutoShuttersControl: fix little Resident home bug - feature: 70_VIERA: new commands, direct access to apps, commands had diff --git a/fhem/FHEM/73_NUKIBridge.pm b/fhem/FHEM/73_NUKIBridge.pm index 0a405f278..409c6f86e 100644 --- a/fhem/FHEM/73_NUKIBridge.pm +++ b/fhem/FHEM/73_NUKIBridge.pm @@ -1,8 +1,8 @@ ############################################################################### -# +# # Developed with Kate # -# (c) 2016-2017 Copyright: Marko Oldenburg (leongaultier at gmail dot com) +# (c) 2016-2020 Copyright: Marko Oldenburg (leongaultier at gmail dot com) # All rights reserved # # This script is free software; you can redistribute it and/or modify @@ -34,31 +34,108 @@ ## # - ################################ - package main; use strict; use warnings; -use JSON; - use HttpUtils; -my $version = "0.6.4"; -my $bridgeapi = "1.6"; +# try to use JSON::MaybeXS wrapper +# for chance of better performance + open code +eval { + require JSON::MaybeXS; + import JSON::MaybeXS qw( decode_json encode_json ); + 1; +}; +if ($@) { + $@ = undef; + # try to use JSON wrapper + # for chance of better performance + eval { -my %lockActions = ( - 'unlock' => 1, - 'lock' => 2, - 'unlatch' => 3, - 'locknGo' => 4, - 'locknGoWithUnlatch' => 5 + # JSON preference order + local $ENV{PERL_JSON_BACKEND} = + 'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP' + unless ( defined( $ENV{PERL_JSON_BACKEND} ) ); + + require JSON; + import JSON qw( decode_json encode_json ); + 1; + }; + + if ($@) { + $@ = undef; + + # In rare cases, Cpanel::JSON::XS may + # be installed but JSON|JSON::MaybeXS not ... + eval { + require Cpanel::JSON::XS; + import Cpanel::JSON::XS qw(decode_json encode_json); + 1; + }; + + if ($@) { + $@ = undef; + + # In rare cases, JSON::XS may + # be installed but JSON not ... + eval { + require JSON::XS; + import JSON::XS qw(decode_json encode_json); + 1; + }; + + if ($@) { + $@ = undef; + + # Fallback to built-in JSON which SHOULD + # be available since 5.014 ... + eval { + require JSON::PP; + import JSON::PP qw(decode_json encode_json); + 1; + }; + + if ($@) { + $@ = undef; + + # Fallback to JSON::backportPP in really rare cases + require JSON::backportPP; + import JSON::backportPP qw(decode_json encode_json); + 1; + } + } + } + } +} + +my $version = '1.8.0'; +my $bridgeapi = '1.9'; + +my %bridgeType = ( + '1' => 'Hardware', + '2' => 'Software' ); +my %lockActionsSmartLock = ( + 'unlock' => 1, + 'lock' => 2, + 'unlatch' => 3, + 'locknGo' => 4, + 'locknGoWithUnlatch' => 5 +); + +my %lockActionsOpener = ( + 'activateRto' => 1, + 'deactivateRto' => 2, + 'electricStrikeActuation' => 3, + 'activateContinuousMode' => 4, + 'deactivateContinuousMode' => 5 +); # Declare functions sub NUKIBridge_Initialize ($); @@ -66,713 +143,1000 @@ sub NUKIBridge_Define ($$); sub NUKIBridge_Undef ($$); sub NUKIBridge_Read($@); sub NUKIBridge_Attr(@); +sub NUKIBridge_addExtension($$$); +sub NUKIBridge_removeExtension($); sub NUKIBridge_Set($@); sub NUKIBridge_Get($@); sub NUKIBridge_GetCheckBridgeAlive($); sub NUKIBridge_firstRun($); -sub NUKIBridge_Call($$$$$); +sub NUKIBridge_Write($@); +sub NUKIBridge_Call($); sub NUKIBridge_Distribution($$$); sub NUKIBridge_ResponseProcessing($$$); +sub NUKIBridge_CGI(); sub NUKIBridge_Autocreate($$;$); sub NUKIBridge_InfoProcessing($$); sub NUKIBridge_getLogfile($); sub NUKIBridge_getCallbackList($); -sub NUKIBridge_CallBlocking($$$); - - - - +sub NUKIBridge_CallBlocking($@); sub NUKIBridge_Initialize($) { - my ($hash) = @_; - + # Provider - $hash->{ReadFn} = "NUKIBridge_Read"; - $hash->{WriteFn} = "NUKIBridge_Read"; - $hash->{Clients} = ":NUKIDevice:"; + $hash->{WriteFn} = 'NUKIBridge_Write'; + $hash->{Clients} = ':NUKIDevice:'; + $hash->{MatchList} = { '1:NUKIDevice' => '^{.*}$' }; + + my $webhookFWinstance = + join( ",", devspec2array('TYPE=FHEMWEB:FILTER=TEMPORARY!=1') ); - # Consumer - $hash->{SetFn} = "NUKIBridge_Set"; - $hash->{GetFn} = "NUKIBridge_Get"; - $hash->{DefFn} = "NUKIBridge_Define"; - $hash->{UndefFn} = "NUKIBridge_Undef"; - $hash->{AttrFn} = "NUKIBridge_Attr"; - $hash->{AttrList} = "disable:1 ". - $readingFnAttributes; + $hash->{SetFn} = 'NUKIBridge_Set'; + $hash->{GetFn} = 'NUKIBridge_Get'; + $hash->{DefFn} = 'NUKIBridge_Define'; + $hash->{UndefFn} = 'NUKIBridge_Undef'; + $hash->{AttrFn} = 'NUKIBridge_Attr'; + $hash->{AttrList} = + 'disable:1 ' + . 'webhookFWinstance:' + . $webhookFWinstance . ' ' + . 'webhookHttpHostname ' + . $readingFnAttributes; - - foreach my $d(sort keys %{$modules{NUKIBridge}{defptr}}) { + foreach my $d ( sort keys %{ $modules{NUKIBridge}{defptr} } ) { my $hash = $modules{NUKIBridge}{defptr}{$d}; - $hash->{VERSION} = $version; + $hash->{VERSION} = $version; } } -sub NUKIBridge_Read($@) { - - my ($hash,$chash,$name,$path,$lockAction,$nukiId)= @_; - NUKIBridge_Call($hash,$chash,$path,$lockAction,$nukiId ); - -} - sub NUKIBridge_Define($$) { - my ( $hash, $def ) = @_; - + my @a = split( "[ \t][ \t]*", $def ); - - return "too few parameters: define NUKIBridge " if( @a != 4 ); - + return ('too few parameters: define NUKIBridge ') + if ( @a != 4 ); + my $name = $a[0]; + my $host = $a[2]; + my $token = $a[3]; + my $port = 8080; - my $name = $a[0]; - my $host = $a[2]; - my $token = $a[3]; - my $port = 8080; + $hash->{HOST} = $host; + $hash->{PORT} = $port; + $hash->{TOKEN} = $token; + $hash->{VERSION} = $version; + $hash->{BRIDGEAPI} = $bridgeapi; + $hash->{helper}->{aliveCount} = 0; + $hash->{helper}->{actionQueue} = []; + $hash->{helper}->{iowrite} = 0; + my $infix = 'NUKIBridge'; - $hash->{HOST} = $host; - $hash->{PORT} = $port; - $hash->{TOKEN} = $token; - $hash->{VERSION} = $version; - $hash->{BRIDGEAPI} = $bridgeapi; - $hash->{helper}{aliveCount} = 0; - + Log3( $name, 3, +"NUKIBridge ($name) - defined with host $host on port $port, Token $token" + ); + CommandAttr( undef, $name . ' room NUKI' ) + if ( AttrVal( $name, 'room', 'none' ) eq 'none' ); - Log3 $name, 3, "NUKIBridge ($name) - defined with host $host on port $port, Token $token"; - - $attr{$name}{room} = "NUKI" if( !defined( $attr{$name}{room} ) ); - readingsSingleUpdate($hash, 'state', 'Initialized', 1 ); - - RemoveInternalTimer($hash); - - if( $init_done ) { - NUKIBridge_firstRun($hash) if( ($hash->{HOST}) and ($hash->{TOKEN}) ); - } else { - InternalTimer( gettimeofday()+15, 'NUKIBridge_firstRun', $hash, 0 ) if( ($hash->{HOST}) and ($hash->{TOKEN}) ); + if ( + NUKIBridge_addExtension( + $name, 'NUKIBridge_CGI', $infix . "-" . $host + ) + ) + { + $hash->{fhem}{infix} = $infix; } - $modules{NUKIBridge}{defptr}{$hash->{HOST}} = $hash; - + $hash->{WEBHOOK_REGISTER} = "unregistered"; + + readingsSingleUpdate( $hash, 'state', 'Initialized', 1 ); + + RemoveInternalTimer($hash); + + if ($init_done) { + NUKIBridge_firstRun($hash) + if ( ( $hash->{HOST} ) and ( $hash->{TOKEN} ) ); + } + else { + InternalTimer( gettimeofday() + 15, 'NUKIBridge_firstRun', $hash ) + if ( ( $hash->{HOST} ) and ( $hash->{TOKEN} ) ); + } + + $modules{NUKIBridge}{defptr}{ $hash->{HOST} } = $hash; + return undef; } sub NUKIBridge_Undef($$) { - my ( $hash, $arg ) = @_; - + my $host = $hash->{HOST}; my $name = $hash->{NAME}; - - RemoveInternalTimer( $hash ); - - delete $modules{NUKIBridge}{defptr}{$hash->{HOST}}; - + + if ( defined( $hash->{fhem}{infix} ) ) { + NUKIBridge_removeExtension( $hash->{fhem}{infix} ); + } + + RemoveInternalTimer($hash); + delete $modules{NUKIBridge}{defptr}{ $hash->{HOST} }; + return undef; } sub NUKIBridge_Attr(@) { - my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $hash = $defs{$name}; - my $orig = $attrVal; - - if( $attrName eq "disable" ) { - if( $cmd eq "set" and $attrVal eq "1" ) { - readingsSingleUpdate ( $hash, "state", "disabled", 1 ); - Log3 $name, 3, "NUKIBridge ($name) - disabled"; + if ( $attrName eq 'disable' ) { + if ( $cmd eq 'set' and $attrVal == 1 ) { + readingsSingleUpdate( $hash, 'state', 'disabled', 1 ); + Log3( $name, 3, "NUKIBridge ($name) - disabled" ); } - - elsif( $cmd eq "del" ) { - readingsSingleUpdate ( $hash, "state", "active", 1 ); - Log3 $name, 3, "NUKIBridge ($name) - enabled"; + elsif ( $cmd eq 'del' ) { + readingsSingleUpdate( $hash, 'state', 'active', 1 ); + Log3( $name, 3, "NUKIBridge ($name) - enabled" ); } } - - if( $attrName eq "disabledForIntervals" ) { - if( $cmd eq "set" ) { - Log3 $name, 3, "NUKIBridge ($name) - enable disabledForIntervals"; - readingsSingleUpdate ( $hash, "state", "Unknown", 1 ); - } - elsif( $cmd eq "del" ) { - readingsSingleUpdate ( $hash, "state", "active", 1 ); - Log3 $name, 3, "NUKIBridge ($name) - delete disabledForIntervals"; + if ( $attrName eq 'disabledForIntervals' ) { + if ( $cmd eq 'set' ) { + Log3( $name, 3, + "NUKIBridge ($name) - enable disabledForIntervals" ); + readingsSingleUpdate( $hash, 'state', 'Unknown', 1 ); + } + elsif ( $cmd eq 'del' ) { + readingsSingleUpdate( $hash, 'state', 'active', 1 ); + Log3( $name, 3, + "NUKIBridge ($name) - delete disabledForIntervals" ); + } + } + + ###################### + #### webhook ######### + + return ( +"Invalid value for attribute $attrName: can only by FQDN or IPv4 or IPv6 address" + ) + if ( $attrVal + and $attrName eq 'webhookHttpHostname' + and $attrVal !~ /^([A-Za-z_.0-9]+\.[A-Za-z_.0-9]+)|[0-9:]+$/ ); + + return ( +"Invalid value for attribute $attrName: FHEMWEB instance $attrVal not existing" + ) + if ( $attrVal + and $attrName eq 'webhookFWinstance' + and + ( !defined( $defs{$attrVal} ) or $defs{$attrVal}{TYPE} ne 'FHEMWEB' ) ); + + return ( + "Invalid value for attribute $attrName: needs to be an integer value") + if ( $attrVal and $attrName eq 'webhookPort' and $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} = '/' + . AttrVal( $webhookFWinstance, 'webname', 'fhem' ) + . '/NUKIBridge' . '-' + . $hash->{HOST}; + $hash->{WEBHOOK_PORT} = ( + $attrName eq 'webhookPort' ? $attrVal : AttrVal( + $name, 'webhookPort', + InternalVal( $webhookFWinstance, 'PORT', '' ) + ) + ); + + $hash->{WEBHOOK_URL} = ''; + $hash->{WEBHOOK_COUNTER} = 0; + + if ( $webhookHttpHostname ne '' and $hash->{WEBHOOK_PORT} ne '' ) { + + $hash->{WEBHOOK_URL} = + 'http://' + . $webhookHttpHostname . ':' + . $hash->{WEBHOOK_PORT} + . $hash->{WEBHOOK_URI}; + my $url = + 'http://' + . $webhookHttpHostname . ':' + . $hash->{WEBHOOK_PORT} + . $hash->{WEBHOOK_URI}; + + Log3( $name, 3, "NUKIBridge ($name) - URL ist: $url" ); + NUKIBridge_Write( $hash, 'callback/add', $url, undef, undef ) + if ($init_done); + $hash->{WEBHOOK_REGISTER} = 'sent'; + } + else { + $hash->{WEBHOOK_REGISTER} = 'incomplete_attributes'; } } return undef; } +sub NUKIBridge_addExtension($$$) { + my ( $name, $func, $link ) = @_; + + my $url = '/' . $link; + + Log3( $name, 2, + "NUKIBridge ($name) - Registering NUKIBridge for webhook URI $url ..." + ); + + $data{FWEXT}{$url}{deviceName} = $name; + $data{FWEXT}{$url}{FUNC} = $func; + $data{FWEXT}{$url}{LINK} = $link; + + return 1; +} + +sub NUKIBridge_removeExtension($) { + my ($link) = @_; + + my $url = '/' . $link; + my $name = $data{FWEXT}{$url}{deviceName}; + + Log3( $name, 2, + "NUKIBridge ($name) - Unregistering NUKIBridge for webhook URL $url..." + ); + delete $data{FWEXT}{$url}; +} + sub NUKIBridge_Set($@) { + my ( $hash, $name, $cmd, @args ) = @_; - my ($hash, $name, $cmd, @args) = @_; - my ($arg, @params) = @args; + my ( $arg, @params ) = @args; - - if($cmd eq 'autocreate') { - return "usage: autocreate" if( @args != 0 ); - - NUKIBridge_Call($hash,$hash,"list",undef,undef) if( !IsDisabled($name) ); + if ( lc($cmd) eq 'getdevicelist' ) { + return 'usage: getDeviceList' if ( @args != 0 ); + NUKIBridge_Write( $hash, "list", undef, undef, undef ) + if ( !IsDisabled($name) ); return undef; + } + elsif ( $cmd eq 'info' ) { + return 'usage: statusRequest' if ( @args != 0 ); - } elsif($cmd eq 'info') { - return "usage: statusRequest" if( @args != 0 ); - - NUKIBridge_Call($hash,$hash,"info",undef,undef) if( !IsDisabled($name) ); - + NUKIBridge_Write( $hash, "info", undef, undef, undef ) + if ( !IsDisabled($name) ); return undef; - - } elsif($cmd eq 'fwUpdate') { - return "usage: fwUpdate" if( @args != 0 ); - - NUKIBridge_CallBlocking($hash,"fwupdate",undef) if( !IsDisabled($name) ); - + } + elsif ( lc($cmd) eq 'fwupdate' ) { + return 'usage: fwUpdate' if ( @args != 0 ); + + NUKIBridge_CallBlocking( $hash, "fwupdate", undef ) + if ( !IsDisabled($name) ); return undef; - - } elsif($cmd eq 'reboot') { - return "usage: reboot" if( @args != 0 ); - - NUKIBridge_CallBlocking($hash,"reboot",undef) if( !IsDisabled($name) ); - + } + elsif ( $cmd eq 'reboot' ) { + return 'usage: reboot' if ( @args != 0 ); + + NUKIBridge_CallBlocking( $hash, "reboot", undef ) + if ( !IsDisabled($name) ); return undef; - - } elsif($cmd eq 'clearLog') { - return "usage: clearLog" if( @args != 0 ); - - NUKIBridge_CallBlocking($hash,"clearlog",undef) if( !IsDisabled($name) ); - - } elsif($cmd eq 'factoryReset') { - return "usage: clearLog" if( @args != 0 ); - - NUKIBridge_CallBlocking($hash,"factoryReset",undef) if( !IsDisabled($name) ); - - } elsif($cmd eq 'callbackRemove') { - return "usage: callbackRemove" if( @args != 1 ); - my $id = "id=" . join( " ", @args ); - - my $resp = NUKIBridge_CallBlocking($hash,"callback/remove",$id) if( !IsDisabled($name) ); - if( $resp->{success} eq "true" and !IsDisabled($name) ) { - return "Success Callback $id removed"; - } else { - return "remove Callback failed"; + } + elsif ( lc($cmd) eq 'clearlog' ) { + return 'usage: clearLog' if ( @args != 0 ); + + NUKIBridge_CallBlocking( $hash, "clearlog", undef ) + if ( !IsDisabled($name) ); + } + elsif ( lc($cmd) eq 'factoryreset' ) { + return 'usage: clearLog' if ( @args != 0 ); + + NUKIBridge_CallBlocking( $hash, "factoryReset", undef ) + if ( !IsDisabled($name) ); + } + elsif ( lc($cmd) eq 'callbackremove' ) { + return 'usage: callbackRemove' if ( @args > 1 ); + + my $id = "id=" . ( @args > 0 ? join( ' ', @args ) : 0 ); + my $resp = NUKIBridge_CallBlocking( $hash, 'callback/remove', $id ) + if ( !IsDisabled($name) ); + + if ( + ( + $resp->{success} eq 'true' + or $resp->{success} == 1 + ) + and !IsDisabled($name) + ) + { + return ( 'Success Callback ' . $id . ' removed' ); } - - } else { - my $list = ""; - $list .= "info:noArg autocreate:noArg callbackRemove:0,1,2 "; - $list .= "clearLog:noArg fwUpdate:noArg reboot:noArg factoryReset:noArg" if( ReadingsVal($name,'bridgeType','Software') eq 'Hardware' ); - return "Unknown argument $cmd, choose one of $list"; + else { + return ('remove Callback failed'); + } + } + else { + my $list = ''; + $list .= 'info:noArg getDeviceList:noArg callbackRemove:noArg '; + $list .= 'clearLog:noArg fwUpdate:noArg reboot:noArg factoryReset:noArg' + if ( ReadingsVal( $name, 'bridgeType', 'Software' ) eq 'Hardware' ); + return ( 'Unknown argument ' . $cmd . ', choose one of ' . $list ); } } sub NUKIBridge_Get($@) { + my ( $hash, $name, $cmd, @args ) = @_; - my ($hash, $name, $cmd, @args) = @_; - my ($arg, @params) = @args; - - if($cmd eq 'logFile') { - return "usage: logFile" if( @args != 0 ); + my ( $arg, @params ) = @args; - NUKIBridge_getLogfile($hash) if( !IsDisabled($name) ); - - } elsif($cmd eq 'callbackList') { - return "usage: callbackList" if( @args != 0 ); + if ( lc($cmd) eq 'logfile' ) { + return 'usage: logFile' if ( @args != 0 ); - NUKIBridge_getCallbackList($hash) if( !IsDisabled($name) ); - - } else { - my $list = ""; - $list .= "callbackList:noArg "; - $list .= "logFile:noArg" if( ReadingsVal($name,'bridgeType','Software') eq 'Hardware' ); - return "Unknown argument $cmd, choose one of $list"; + NUKIBridge_getLogfile($hash) if ( !IsDisabled($name) ); + } + elsif ( lc($cmd) eq 'callbacklist' ) { + return 'usage: callbackList' if ( @args != 0 ); + + NUKIBridge_getCallbackList($hash) if ( !IsDisabled($name) ); + } + else { + my $list = ''; + $list .= 'callbackList:noArg '; + $list .= 'logFile:noArg' + if ( ReadingsVal( $name, 'bridgeType', 'Software' ) eq 'Hardware' ); + + return 'Unknown argument ' . $cmd . ', choose one of ' . $list; } } sub NUKIBridge_GetCheckBridgeAlive($) { - my ($hash) = @_; - my $name = $hash->{NAME}; - - RemoveInternalTimer($hash); - Log3 $name, 4, "NUKIBridge ($name) - NUKIBridge_GetCheckBridgeAlive"; - - if( !IsDisabled($name) ) { - NUKIBridge_Call($hash,$hash,'info',undef,undef); - - Log3 $name, 4, "NUKIBridge ($name) - run NUKIBridge_Call"; + my $name = $hash->{NAME}; + + RemoveInternalTimer($hash); + Log3( $name, 4, "NUKIBridge ($name) - NUKIBridge_GetCheckBridgeAlive" ); + + if ( !IsDisabled($name) + and $hash->{helper}->{iowrite} == 0 ) { + + NUKIBridge_Write( $hash, 'info', undef, undef, undef ); + + Log3( $name, 4, "NUKIBridge ($name) - run NUKIBridge_Write" ); } - - InternalTimer( gettimeofday()+15+int(rand(15)), 'NUKIBridge_GetCheckBridgeAlive', $hash, 1 ); - - Log3 $name, 4, "NUKIBridge ($name) - Call InternalTimer for NUKIBridge_GetCheckBridgeAlive"; + +# InternalTimer( gettimeofday() + 15 + int( rand(15) ), +# 'NUKIBridge_GetCheckBridgeAlive', $hash ); + InternalTimer( gettimeofday() + 10, + 'NUKIBridge_GetCheckBridgeAlive', $hash ); + + Log3( $name, 4, +"NUKIBridge ($name) - Call InternalTimer for NUKIBridge_GetCheckBridgeAlive" + ); } sub NUKIBridge_firstRun($) { - my ($hash) = @_; + my $name = $hash->{NAME}; - - + RemoveInternalTimer($hash); - NUKIBridge_Call($hash,$hash,'list',undef,undef) if( !IsDisabled($name) ); - InternalTimer( gettimeofday()+15, 'NUKIBridge_GetCheckBridgeAlive', $hash, 1 ); + NUKIBridge_Write( $hash, 'list', undef, undef, undef ) + if ( !IsDisabled($name) ); + InternalTimer( gettimeofday() + 15, + 'NUKIBridge_GetCheckBridgeAlive', $hash ); return undef; } -sub NUKIBridge_Call($$$$$) { +sub NUKIBridge_Write($@) { + my ( $hash, $endpoint, $param, $nukiId, $deviceType ) = @_; - my ($hash,$chash,$path,$lockAction,$nukiId) = @_; - - my $name = $hash->{NAME}; - my $host = $hash->{HOST}; - my $port = $hash->{PORT}; - my $token = $hash->{TOKEN}; - - - my $uri = "http://" . $hash->{HOST} . ":" . $port; - $uri .= "/" . $path if( defined $path); - $uri .= "?token=" . $token if( defined($token) ); - $uri .= "&action=" . $lockActions{$lockAction} if( defined($lockAction) and $path ne "callback/add" ); - $uri .= "&url=" . $lockAction if( defined($lockAction) and $path eq "callback/add" ); - $uri .= "&nukiId=" . $nukiId if( defined($nukiId) ); + my $obj = { + endpoint => $endpoint, + param => $param, + nukiId => $nukiId, + deviceType => $deviceType + }; + $hash->{helper}->{lastDeviceAction} = $obj + if ( defined($param) + and $param ); - HttpUtils_NonblockingGet( - { - url => $uri, - timeout => 30, - hash => $hash, - chash => $chash, - endpoint => $path, - header => "Accept: application/json", - method => "GET", - callback => \&NUKIBridge_Distribution, + unshift( @{ $hash->{helper}->{actionQueue} }, $obj ); + + NUKIBridge_Call($hash); +} + +sub NUKIBridge_CreateUri($$) { + my ( $hash, $obj ) = @_; + + my $host = $hash->{HOST}; + my $port = $hash->{PORT}; + my $token = $hash->{TOKEN}; + my $endpoint = $obj->{endpoint}; + my $param = $obj->{param}; + my $nukiId = $obj->{nukiId}; + my $deviceType = $obj->{deviceType}; + + my $uri = 'http://' . $host . ':' . $port; + $uri .= '/' . $endpoint if ( defined $endpoint ); + $uri .= '?token=' . $token if ( defined($token) ); + $uri .= '&action=' . $lockActionsSmartLock{$param} + if ( defined($param) + and $param ne 'callback/add' + and $deviceType == 0 ); + + $uri .= '&action=' . $lockActionsOpener{$param} + if ( defined($param) + and $param ne 'callback/add' + and $deviceType == 2 ); + + $uri .= '&url=' . $param + if ( defined($param) + and $param eq 'callback/add' ); + + $uri .= '&nukiId=' . $nukiId + if ( defined($nukiId) ); + $uri .= '&deviceType=' . $deviceType + if ( defined($deviceType) ); + + return $uri; +} + +sub NUKIBridge_Call($) { + my $hash = shift; + + my $name = $hash->{NAME}; + my $obj = pop( @{ $hash->{helper}->{actionQueue} } ); + my $endpoint = $obj->{endpoint}; + my $nukiId = $obj->{nukiId}; + + if ( $hash->{helper}->{iowrite} == 0 ) { + my $uri = NUKIBridge_CreateUri( $hash, $obj ); + + if ( defined($uri) and $uri ) { + $hash->{helper}->{iowrite} = 1; + + HttpUtils_NonblockingGet( + { + url => $uri, + timeout => 30, + hash => $hash, + nukiId => $nukiId, + endpoint => $endpoint, + header => 'Accept: application/json', + method => 'GET', + callback => \&NUKIBridge_Distribution, + } + ); + + Log3( $name, 4, "NUKIBridge ($name) - Send HTTP POST with URL $uri" ); } - ); - - Log3 $name, 4, "NUKIBridge ($name) - Send HTTP POST with URL $uri"; + } + else { + push( @{ $hash->{helper}->{actionQueue} },$obj ) + if ( defined($endpoint) + and $endpoint eq 'lockAction' ); + } } sub NUKIBridge_Distribution($$$) { - my ( $param, $err, $json ) = @_; - my $hash = $param->{hash}; - my $doTrigger = $param->{doTrigger}; - my $name = $hash->{NAME}; - my $host = $hash->{HOST}; - - - Log3 $name, 5, "NUKIBridge ($name) - Response JSON: $json"; - Log3 $name, 5, "NUKIBridge ($name) - Response ERROR: $err"; - Log3 $name, 5, "NUKIBridge ($name) - Response CODE: $param->{code}" if( defined($param->{code}) and ($param->{code}) ); - + + my $hash = $param->{hash}; + my $doTrigger = $param->{doTrigger}; + my $name = $hash->{NAME}; + my $host = $hash->{HOST}; + + my $dhash = $hash; + + $dhash = $modules{NUKIDevice}{defptr}{ $param->{'nukiId'} } + unless ( not defined( $param->{'nukiId'} ) ); + + my $dname = $dhash->{NAME}; + + Log3( $name, 5, "NUKIBridge ($name) - Response JSON: $json" ); + Log3( $name, 5, "NUKIBridge ($name) - Response ERROR: $err" ); + Log3( $name, 5, "NUKIBridge ($name) - Response CODE: $param->{code}" ) + if ( defined( $param->{code} ) and ( $param->{code} ) ); + + $hash->{helper}->{iowrite} = 0 + if ( $hash->{helper}->{iowrite} == 1 ); + readingsBeginUpdate($hash); - - if( defined( $err ) ) { - if ( $err ne "" ) { - if ($param->{endpoint} eq "info") { - readingsBulkUpdate( $hash, "state", "not connected") if( $hash->{helper}{aliveCount} > 1 ); - Log3 $name, 5, "NUKIBridge ($name) - Bridge ist offline"; + + if ( defined($err) ) { + if ( $err ne '' ) { + if ( $param->{endpoint} eq 'info' ) { + readingsBulkUpdate( $hash, 'state', 'not connected' ) + if ( $hash->{helper}{aliveCount} > 1 ); + Log3( $name, 5, "NUKIBridge ($name) - Bridge ist offline" ); $hash->{helper}{aliveCount} = $hash->{helper}{aliveCount} + 1; } - - readingsBulkUpdate( $hash, "lastError", $err ) if( ReadingsVal($name,"state","not connected") eq "not connected" ); - Log3 $name, 4, "NUKIBridge ($name) - error while requesting: $err"; + + readingsBulkUpdate( $hash, 'lastError', $err ) + if ( ReadingsVal( $name, 'state', 'not connected' ) eq + 'not connected' ); + + Log3( $name, 4, + "NUKIBridge ($name) - error while requesting: $err" ); readingsEndUpdate( $hash, 1 ); return $err; } } - if( $json eq "" and exists( $param->{code} ) and $param->{code} ne 200 ) { - - if( $param->{code} eq 503 ) { - NUKIDevice_Parse($param->{chash},$param->{code}) if( $hash != $param->{chash} ); - Log3 $name, 4, "NUKIBridge ($name) - smartlock is offline"; + if ( ( $json eq '' or $json =~ /Unavailable/i ) + and exists( $param->{code} ) + and $param->{code} != 200 ) + { + if ( $param->{code} == 503 and $json eq 'HTTP 503 Unavailable' ) { + Log3( $name, 4, +"NUKIBridge ($name) - Response from Bridge: $param->{code}, $json" + ); readingsEndUpdate( $hash, 1 ); - return "received http code ".$param->{code}.": smartlock is offline"; + + if ( defined( $hash->{helper}->{lastDeviceAction} ) + and $hash->{helper}->{lastDeviceAction} ) + { + push( + @{ $hash->{helper}->{actionQueue} }, + $hash->{helper}->{lastDeviceAction} + ); + delete $hash->{helper}->{lastDeviceAction}; + } + + return; } - - readingsBulkUpdate( $hash, "lastError", "Internal error, " .$param->{code} ); - Log3 $name, 4, "NUKIBridge ($name) - received http code " .$param->{code}." without any data after requesting"; + + readingsBulkUpdate( $hash, 'lastError', + 'Internal error, ' . $param->{code} ); + Log3( $name, 4, + "NUKIBridge ($name) - received http code " + . $param->{code} + . " without any data after requesting" ); readingsEndUpdate( $hash, 1 ); - return "received http code ".$param->{code}." without any data after requesting"; + return ('received http code ' + . $param->{code} + . ' without any data after requesting' ); } - if( ( $json =~ /Error/i ) and exists( $param->{code} ) ) { - - readingsBulkUpdate( $hash, "lastError", "invalid API token" ) if( $param->{code} eq 401 ); - readingsBulkUpdate( $hash, "lastError", "action is undefined" ) if( $param->{code} eq 400 and $hash == $param->{chash} ); - - - ###### Fehler bei Antwort auf Anfrage eines logischen Devices ###### - NUKIDevice_Parse($param->{chash},$param->{code}) if( $param->{code} eq 404 ); - NUKIDevice_Parse($param->{chash},$param->{code}) if( $param->{code} eq 400 and $hash != $param->{chash} ); - - - - Log3 $name, 4, "NUKIBridge ($name) - invalid API token" if( $param->{code} eq 401 ); - Log3 $name, 4, "NUKIBridge ($name) - nukiId is not known" if( $param->{code} eq 404 ); - Log3 $name, 4, "NUKIBridge ($name) - action is undefined" if( $param->{code} eq 400 and $hash == $param->{chash} ); + if ( ( $json =~ /Error/i ) and exists( $param->{code} ) ) { + readingsBulkUpdate( $hash, 'lastError', 'invalid API token' ) + if ( $param->{code} == 401 ); + readingsBulkUpdate( $hash, 'lastError', 'action is undefined' ) + if ( $param->{code} == 400 and $hash == $dhash ); + + Log3( $name, 4, "NUKIBridge ($name) - invalid API token" ) + if ( $param->{code} == 401 ); + Log3( $name, 4, "NUKIBridge ($name) - nukiId is not known" ) + if ( $param->{code} == 404 ); + Log3( $name, 4, "NUKIBridge ($name) - action is undefined" ) + if ( $param->{code} == 400 and $hash == $dhash ); - ######### Zum testen da ich kein Nuki Smartlock habe ############ - #if ( $param->{code} eq 404 ) { - # if( defined($param->{chash}->{helper}{lockAction}) ) { - # Log3 $name, 3, "NUKIBridge ($name) - Test JSON String for lockAction"; - # $json = '{"success": true, "batteryCritical": false}'; - # } else { - # Log3 $name, 3, "NUKIBridge ($name) - Test JSON String for lockState"; - # $json = '{"state": 1, "stateName": "locked", "batteryCritical": false, "success": "true"}'; - # } - # NUKIDevice_Parse($param->{chash},$json); - #} - - readingsEndUpdate( $hash, 1 ); return $param->{code}; } - - if( $hash == $param->{chash} ) { - - # zum testen da ich kein Nuki Smartlock habe - #$json = '[{"nukiId": 1,"name": "Home","lastKnownState": {"state": 1,"stateName": "locked","batteryCritical": false,"timestamp": "2016-10-03T06:49:00+00:00"}},{"nukiId": 2,"name": "Grandma","lastKnownState": {"state": 3,"stateName": "unlocked","batteryCritical": false,"timestamp": "2016-10-03T06:49:00+00:00"}}]' if( $param->{endpoint} eq "list" ); - - #$json= '{"bridgeType":2,"ids":{"serverId":142667440},"versions":{"appVersion":"0.2.14"},"uptime":1527,"currentTime":"2017-01-17T04:55:58Z","serverConnected":true,"scanResults":[{"nukiId": 1,"name": "Home","rssi": -87,"paired": true},{"nukiId": 2,"name": "Grandma","rssi": -93,"paired": false}]}' if( $param->{endpoint} eq "info" ); - - NUKIBridge_ResponseProcessing($hash,$json,$param->{endpoint}); - - } else { - - NUKIDevice_Parse($param->{chash},$json); - } - + + delete $hash->{helper}->{lastDeviceAction} + if ( defined( $hash->{helper}->{lastDeviceAction} ) + and $hash->{helper}->{lastDeviceAction} ); + readingsEndUpdate( $hash, 1 ); + + if ( $hash == $dhash ) { + NUKIBridge_ResponseProcessing( $hash, $json, $param->{endpoint} ); + } + else { + my $decode_json = eval { decode_json($json) }; + if ($@) { + Log3( $name, 3, + "NUKIBridge ($name) - JSON error while request: $@" ); + return; + } + + $decode_json->{nukiId} = $param->{nukiId}; + $json = encode_json($decode_json); + Dispatch( $hash, $json, undef ); + } + + InternalTimer( gettimeofday() + 1, 'NUKIBridge_Call', $hash ) + if ( defined( $hash->{helper}->{actionQueue} ) + and scalar( @{ $hash->{helper}->{actionQueue} } ) > 0 ); + return undef; } sub NUKIBridge_ResponseProcessing($$$) { + my ( $hash, $json, $endpoint ) = @_; - my ($hash,$json,$path) = @_; my $name = $hash->{NAME}; my $decode_json; - - - if( !$json ) { - Log3 $name, 3, "NUKIBridge ($name) - empty answer received"; + + if ( !$json ) { + Log3( $name, 3, "NUKIBridge ($name) - empty answer received" ); return undef; - } elsif( $json =~ m'HTTP/1.1 200 OK' ) { - Log3 $name, 4, "NUKIBridge ($name) - empty answer received"; + } + elsif ( $json =~ m'HTTP/1.1 200 OK' ) { + Log3( $name, 4, "NUKIBridge ($name) - empty answer received" ); return undef; - } elsif( $json !~ m/^[\[{].*[}\]]$/ ) { - Log3 $name, 3, "NUKIBridge ($name) - invalid json detected: $json"; - return "NUKIBridge ($name) - invalid json detected: $json"; + } + elsif ( $json !~ m/^[\[{].*[}\]]$/ ) { + Log3( $name, 3, "NUKIBridge ($name) - invalid json detected: $json" ); + return ("NUKIBridge ($name) - invalid json detected: $json"); } - $decode_json = eval{decode_json($json)}; - if($@){ - Log3 $name, 3, "NUKIBridge ($name) - JSON error while request: $@"; + $decode_json = eval { decode_json($json) }; + if ($@) { + Log3( $name, 3, "NUKIBridge ($name) - JSON error while request: $@" ); return; } - - if( ref($decode_json) eq "ARRAY" and scalar(@{$decode_json}) > 0 and $path eq "list" ) { - NUKIBridge_Autocreate($hash,$decode_json); - NUKIBridge_Call($hash,$hash,"info",undef,undef) if( !IsDisabled($name) ); - } - - elsif( $path eq "info" ) { - readingsBeginUpdate( $hash ); - readingsBulkUpdate( $hash, "state", "connected" ); - Log3 $name, 5, "NUKIBridge ($name) - Bridge ist online"; - - readingsEndUpdate( $hash, 1 ); + if ( $endpoint eq 'list' + or $endpoint eq 'info' ) + { + if ( + ( + ref($decode_json) eq 'ARRAY' + and scalar( @{$decode_json} ) > 0 + and $endpoint eq 'list' + ) + or ( ref( $decode_json->{scanResults} ) eq 'ARRAY' + and scalar( @{ $decode_json->{scanResults} } ) > 0 + and $endpoint eq 'info' ) + ) + { + my @buffer; + @buffer = split( '\[', $json ) + if ( $endpoint eq 'list' ); + @buffer = split( '"scanResults": \[', $json ) + if ( $endpoint eq 'info' ); + + my ( $json, $tail ) = NUKIBridge_ParseJSON( $hash, $buffer[1] ); + + while ($json) { + Log3( $name, 5, + "NUKIBridge ($name) - Decoding JSON message. Length: " + . length($json) + . " Content: " + . $json ); + + Log3( $name, 5, + "NUKIBridge ($name) - Vor Sub: Laenge JSON: " + . length($json) + . " Content: " + . $json + . " Tail: " + . $tail ); + + Dispatch( $hash, $json, undef ) + unless ( not defined($tail) and not($tail) ); + + ( $json, $tail ) = NUKIBridge_ParseJSON( $hash, $tail ); + + Log3( $name, 5, + "NUKIBridge ($name) - Nach Sub: Laenge JSON: " + . length($json) + . " Content: " + . $json + . " Tail: " + . $tail ); + } + } + + NUKIBridge_InfoProcessing( $hash, $decode_json ) + if ( $endpoint eq 'info' ); + + readingsSingleUpdate( $hash, 'state', 'connected', 1 ); + Log3( $name, 5, "NUKIBridge ($name) - Bridge ist online" ); + $hash->{helper}{aliveCount} = 0; - - NUKIBridge_InfoProcessing($hash,$decode_json); - - } else { - Log3 $name, 5, "NUKIBridge ($name) - Rückgabe Path nicht korrekt: $json"; + } + else { + Log3( + $name, 5, "NUKIBridge ($name) - Rückgabe Path nicht korrekt: +$json" + ); return; } - + return undef; } -sub NUKIBridge_Autocreate($$;$) { +sub NUKIBridge_CGI() { + my ($request) = @_; - my ($hash,$decode_json,$force)= @_; - my $name = $hash->{NAME}; - - if( !$force ) { - foreach my $d (keys %defs) { - next if($defs{$d}{TYPE} ne "autocreate"); - return undef if(AttrVal($defs{$d}{NAME},"disable",undef)); - } + my $hash; + my $name; + while ( my ( $key, $value ) = each %{ $modules{NUKIBridge}{defptr} } ) { + $hash = $modules{NUKIBridge}{defptr}{$key}; + $name = $hash->{NAME}; } - my $autocreated = 0; - my $nukiSmartlock; - my $nukiId; - my $nukiName; - - readingsBeginUpdate($hash); - - foreach $nukiSmartlock (@{$decode_json}) { - - $nukiId = $nukiSmartlock->{nukiId}; - $nukiName = $nukiSmartlock->{name}; - - - my $code = $name ."-".$nukiId; - if( defined($modules{NUKIDevice}{defptr}{$code}) ) { - Log3 $name, 3, "NUKIDevice ($name) - NukiId '$nukiId' already defined as '$modules{NUKIDevice}{defptr}{$code}->{NAME}'"; - next; - } - - my $devname = "NUKIDevice" . $nukiId; - my $define= "$devname NUKIDevice $nukiId IODev=$name"; - Log3 $name, 3, "NUKIDevice ($name) - create new device '$devname' for address '$nukiId'"; + return ('NUKIBridge WEBHOOK - No IODev found') + unless ( defined($hash) and defined($name) ); - my $cmdret= CommandDefine(undef,$define); - if($cmdret) { - Log3 $name, 3, "NUKIDevice ($name) - Autocreate: An error occurred while creating device for nukiId '$nukiId': $cmdret"; - } else { - $cmdret= CommandAttr(undef,"$devname alias $nukiName"); - $cmdret= CommandAttr(undef,"$devname room NUKI"); - $cmdret= CommandAttr(undef,"$devname IODev $name"); - } + my $json = ( split( '&', $request, 2 ) )[1]; - $defs{$devname}{helper}{fromAutocreate} = 1 ; - - readingsBulkUpdate( $hash, "${autocreated}_nukiId", $nukiId ); - readingsBulkUpdate( $hash, "${autocreated}_name", $nukiName ); - - $autocreated++; - - readingsBulkUpdate( $hash, "smartlockCount", $autocreated ); + if ( !$json ) { + Log3( $name, 3, "NUKIBridge WEBHOOK ($name) - empty message received" ); + return undef; } - - readingsEndUpdate( $hash, 1 ); - - - if( $autocreated ) { - Log3 $name, 2, "NUKIDevice ($name) - autocreated $autocreated devices"; - CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) ); + elsif ( $json =~ m'HTTP/1.1 200 OK' ) { + Log3( $name, 4, "NUKIBridge WEBHOOK ($name) - empty answer received" ); + return undef; + } + elsif ( $json !~ m/^[\[{].*[}\]]$/ ) { + Log3( $name, 3, + "NUKIBridge WEBHOOK ($name) - invalid json detected: $json" ); + return ("NUKIBridge WEBHOOK ($name) - invalid json detected: $json"); } - return "created $autocreated devices"; + Log3( $name, 5, + "NUKIBridge WEBHOOK ($name) - Webhook received with JSON: $json" ); + + if ( $json =~ m/^\{.*\}$/ ) { + $hash->{WEBHOOK_COUNTER}++; + $hash->{WEBHOOK_LAST} = TimeNow(); + + Log3( + $name, 4, "NUKIBridge WEBHOOK ($name) - Received webhook for +matching NukiId at device $name" + ); + + Dispatch( $hash, $json, undef ); + + return ( undef, undef ); + } + + # no data received + else { + Log3( $name, 4, + "NUKIBridge WEBHOOK - received malformed request\n$request" ); + } + + return ( 'text/plain; charset=utf-8', 'Call failure: ' . $request ); } sub NUKIBridge_InfoProcessing($$) { + my ( $hash, $decode_json ) = @_; + + my $name = $hash->{NAME}; - my ($hash,$decode_json) = @_; - my $name = $hash->{NAME}; - my $nukiId; my $scanResults; my %response_hash; my $dname; my $dhash; - - my %bridgeType = ( - '1' => 'Hardware', - '2' => 'Software' - ); - - + readingsBeginUpdate($hash); - readingsBulkUpdate($hash,"appVersion",$decode_json->{versions}->{appVersion}); - readingsBulkUpdate($hash,"firmwareVersion",$decode_json->{versions}->{firmwareVersion}); - readingsBulkUpdate($hash,"wifiFirmwareVersion",$decode_json->{versions}->{wifiFirmwareVersion}); - readingsBulkUpdate($hash,"bridgeType",$bridgeType{$decode_json->{bridgeType}}); - readingsBulkUpdate($hash,"hardwareId",$decode_json->{ids}{hardwareId}); - readingsBulkUpdate($hash,"serverId",$decode_json->{ids}{serverId}); - readingsBulkUpdate($hash,"uptime",$decode_json->{uptime}); - readingsBulkUpdate($hash,"currentTime",$decode_json->{currentTime}); - readingsBulkUpdate($hash,"serverConnected",$decode_json->{serverConnected}); - readingsEndUpdate($hash,1); - - - foreach $scanResults (@{$decode_json->{scanResults}}) { - if( ref($scanResults) eq "HASH" ) { - if ( defined( $modules{NUKIDevice}{defptr} ) ) { - while ( my ( $key, $value ) = each %{ $modules{NUKIDevice}{defptr} } ) { - - $dhash = $modules{NUKIDevice}{defptr}{$key}; - $dname = $dhash->{NAME}; - $nukiId = InternalVal( $dname, "NUKIID", undef ); - next if ( !$nukiId or $nukiId ne $scanResults->{nukiId} ); - - Log3 $name, 4, "NUKIDevice ($dname) - Received scanResults for matching NukiID $nukiId at device $dname"; - - %response_hash = ('name'=>$scanResults->{name}, 'rssi'=>$scanResults->{rssi},'paired'=>$scanResults->{paired}); - - NUKIDevice_Parse($dhash,encode_json \%response_hash); - } - } - } - } + readingsBulkUpdate( $hash, 'appVersion', + $decode_json->{versions}->{appVersion} ); + readingsBulkUpdate( $hash, 'firmwareVersion', + $decode_json->{versions}->{firmwareVersion} ); + readingsBulkUpdate( $hash, 'wifiFirmwareVersion', + $decode_json->{versions}->{wifiFirmwareVersion} ); + readingsBulkUpdate( $hash, 'bridgeType', + $bridgeType{ $decode_json->{bridgeType} } ); + readingsBulkUpdate( $hash, 'hardwareId', $decode_json->{ids}{hardwareId} ); + readingsBulkUpdate( $hash, 'serverId', $decode_json->{ids}{serverId} ); + readingsBulkUpdate( $hash, 'uptime', $decode_json->{uptime} ); + readingsBulkUpdate( $hash, 'currentTime', $decode_json->{currentTime} ); + readingsBulkUpdate( $hash, 'serverConnected', + $decode_json->{serverConnected} ); + readingsEndUpdate( $hash, 1 ); } sub NUKIBridge_getLogfile($) { + my ($hash) = @_; - my ($hash) = @_; - my $name = $hash->{NAME}; + my $name = $hash->{NAME}; + my $decode_json = NUKIBridge_CallBlocking( $hash, 'log', undef ); + + Log3( $name, 4, + "NUKIBridge ($name) - Log data are collected and processed" ); + + if ( ref($decode_json) eq 'ARRAY' and scalar( @{$decode_json} ) > 0 ) { + Log3( $name, 4, "NUKIBridge ($name) - created Table with log file" ); - - my $decode_json = NUKIBridge_CallBlocking($hash,"log",undef); - - Log3 $name, 4, "NUKIBridge ($name) - Log data are collected and processed"; - - - if( ref($decode_json) eq "ARRAY" and scalar(@{$decode_json}) > 0 ) { - Log3 $name, 4, "NUKIBridge ($name) - created Table with log file"; - my $ret = ''; $ret .= '
'; $ret .= ''; - - foreach my $logs (@{$decode_json}) { + + foreach my $logs ( @{$decode_json} ) { $ret .= ''; - - if($logs->{timestamp}) { - $ret .= ""; - $ret .= ""; + + if ( $logs->{timestamp} ) { + $ret .= ''; + $ret .= ''; $ret .= ''; } - - if($logs->{type}) { - $ret .= ""; - $ret .= ""; + + if ( $logs->{type} ) { + $ret .= ''; + $ret .= ''; $ret .= ''; } - - foreach my $d (reverse sort keys %{$logs}) { - next if( $d eq "type" ); - next if( $d eq "timestamp" ); - - $ret .= ""; - $ret .= ""; + + foreach my $d ( reverse sort keys %{$logs} ) { + next if ( $d eq 'type' ); + next if ( $d eq 'timestamp' ); + + $ret .= ''; + $ret .= ''; $ret .= ''; } - + $ret .= ''; } - + $ret .= '
timestamp: $logs->{timestamp}timestamp: ' . $logs->{timestamp} . ' type: $logs->{type}type: ' . $logs->{type} . ' $d: $logs->{$d}' . $d . ': ' . $logs->{$d} . '
'; - + return $ret; } } sub NUKIBridge_getCallbackList($) { + my ($hash) = @_; - my ($hash) = @_; - my $name = $hash->{NAME}; + my $name = $hash->{NAME}; + my $decode_json = NUKIBridge_CallBlocking( $hash, 'callback/list', undef ); - - my $decode_json = NUKIBridge_CallBlocking($hash,"callback/list",undef); return unless ( ref($decode_json) eq 'HASH' ); - - Log3 $name, 4, "NUKIBridge ($name) - Callback data is collected and processed"; - - if( ref($decode_json->{callbacks}) eq "ARRAY" and scalar(@{$decode_json->{callbacks}}) > 0 ) { - Log3 $name, 4, "NUKIBridge ($name) - created Table with log file"; - + + Log3( + $name, 4, "NUKIBridge ($name) - Callback data is collected and +processed" + ); + + if ( ref( $decode_json->{callbacks} ) eq 'ARRAY' + and scalar( @{ $decode_json->{callbacks} } ) > 0 ) + { + Log3( $name, 4, "NUKIBridge ($name) - created Table with log file" ); + my $ret = ''; $ret .= '
'; $ret .= ''; - $ret .= ''; - $ret .= ""; - $ret .= ""; - $ret .= ""; - $ret .= ''; - - foreach my $cb (@{$decode_json->{callbacks}}) { - - $ret .= ""; - $ret .= ""; - $ret .= ""; + $ret .= ''; + $ret .= ''; + $ret .= ''; + $ret .= ''; + $ret .= ''; + + foreach my $cb ( @{ $decode_json->{callbacks} } ) { + + $ret .= ''; + $ret .= ''; + $ret .= ''; $ret .= ''; } - + $ret .= '
Callback-ID Callback-URL
$cb->{id} $cb->{url}
Callback-ID Callback-URL
' . $cb->{id} . ' ' . $cb->{url} . '
'; - return $ret; } - + return "No callback data available or error during processing"; } -sub NUKIBridge_CallBlocking($$$) { +sub NUKIBridge_CallBlocking($@) { + my ( $hash, $endpoint, $obj ) = @_; - my ($hash,$path,$obj) = @_; - my $name = $hash->{NAME}; - my $host = $hash->{HOST}; - my $port = $hash->{PORT}; - my $token = $hash->{TOKEN}; - - - my $url = "http://" . $hash->{HOST} . ":" . $port; - $url .= "/" . $path if( defined $path); - $url .= "?token=" . $token if( defined($token) ); - $url .= "&" . $obj if( defined($obj) ); - - - my($err,$data) = HttpUtils_BlockingGet({ - url => $url, - timeout => 3, - method => "GET", - header => "Content-Type: application/json", - }); + my $name = $hash->{NAME}; + my $host = $hash->{HOST}; + my $port = $hash->{PORT}; + my $token = $hash->{TOKEN}; + my $url = 'http://' . $hash->{HOST} . ':' . $port; + $url .= '/' . $endpoint + if ( defined $endpoint ); + $url .= '?token=' . $token + if ( defined($token) ); + $url .= '&' . $obj + if ( defined($obj) ); - if( !$data ) { - Log3 $name, 3, "NUKIDevice ($name) - empty answer received for $url"; + my ( $err, $data ) = HttpUtils_BlockingGet( + { + url => $url, + timeout => 3, + method => "GET", + header => "Content-Type: application/json", + } + ); + + if ( !$data ) { + Log3( $name, 3, "NUKIDevice ($name) - empty answer received for $url" ); return undef; - } elsif( $data =~ m'HTTP/1.1 200 OK' ) { - Log3 $name, 4, "NUKIDevice ($name) - empty answer received for $url"; + } + elsif ( $data =~ m'HTTP/1.1 200 OK' ) { + Log3( $name, 4, "NUKIDevice ($name) - empty answer received for $url" ); return undef; - } elsif( $data !~ m/^[\[{].*[}\]]$/ and $path ne "log" ) { - Log3 $name, 3, "NUKIDevice ($name) - invalid json detected for $url: $data"; - return "NUKIDevice ($name) - invalid json detected for $url: $data"; + } + elsif ( $data !~ m/^[\[{].*[}\]]$/ and $endpoint ne "log" ) { + Log3( $name, 3, + "NUKIDevice ($name) - invalid json detected for $url: $data" ); + return ("NUKIDevice ($name) - invalid json detected for $url: $data"); } - - my $decode_json = eval{decode_json($data)}; - if($@){ - Log3 $name, 3, "NUKIBridge ($name) - JSON error while request: $@"; + my $decode_json = eval { decode_json($data) }; + if ($@) { + Log3( $name, 3, "NUKIBridge ($name) - JSON error while request: $@" ); return; } - - return undef if( !$decode_json ); - - Log3 $name, 5, "NUKIBridge ($name) - Data: $data"; - Log3 $name, 4, "NUKIBridge ($name) - Blocking HTTP Query finished"; + + return undef if ( !$decode_json ); + + Log3( $name, 5, "NUKIBridge ($name) - Data: $data" ); + Log3( $name, 4, "NUKIBridge ($name) - Blocking HTTP Query finished" ); return ($decode_json); } +sub NUKIBridge_ParseJSON($$) { + my ( $hash, $buffer ) = @_; + my $name = $hash->{NAME}; + my $open = 0; + my $close = 0; + my $msg = ''; + my $tail = ''; + if ($buffer) { + foreach my $c ( split //, $buffer ) { + if ( $open == $close and $open > 0 ) { + $tail .= $c; + Log3( $name, 5, + "NUKIBridge ($name) - $open == $close and $open > 0" ); + } + elsif ( ( $open == $close ) and ( $c ne '{' ) ) { + Log3( $name, 5, + "NUKIBridge ($name) - Garbage character before message: " + . $c ); + } + else { + + if ( $c eq '{' ) { + + $open++; + + } + elsif ( $c eq '}' ) { + + $close++; + } + + $msg .= $c; + } + } + + if ( $open != $close ) { + + $tail = $msg; + $msg = ''; + } + } + + Log3( $name, 5, "NUKIBridge ($name) - return msg: $msg and tail: $tail" ); + return ( $msg, $tail ); +} 1; - =pod =item device =item summary Modul to control the Nuki Smartlock's over the Nuki Bridge. @@ -804,9 +1168,6 @@ sub NUKIBridge_CallBlocking($$$) { Readings
    -
  • 0_nukiId - ID of the first found Nuki Smartlock
  • -
  • 0_name - Name of the first found Nuki Smartlock
  • -
  • smartlockCount - number of all found Smartlocks
  • bridgeAPI - API Version of bridge
  • bridgeType - Hardware bridge / Software bridge
  • currentTime - Current timestamp
  • @@ -824,7 +1185,7 @@ sub NUKIBridge_CallBlocking($$$) { Set
      -
    • autocreate - Prompts to re-read all Smartlocks from the bridge and if not already present in FHEM, create the autimatic.
    • +
    • getDeviceList - Prompts to re-read all devices from the bridge and if not already present in FHEM, create the automatically.
    • callbackRemove - Removes a previously added callback
    • clearLog - Clears the log of the Bridge (only hardwarebridge)
    • factoryReset - Performs a factory reset (only hardwarebridge)
    • @@ -837,7 +1198,7 @@ sub NUKIBridge_CallBlocking($$$) { Get
        -
      • callbackList - List of register url callbacks. The Bridge register up to 3 url callbacks.
      • +
      • callbackList - List of register url callbacks.
      • logFile - Retrieves the log of the Bridge

      @@ -846,6 +1207,8 @@ sub NUKIBridge_CallBlocking($$$) { Attributes
      • disable - disables the Nuki Bridge
      • +
      • webhookFWinstance - Webinstanz of the Callback
      • +
      • webhookHttpHostname - IP or FQDN of the FHEM Server Callback

    @@ -858,7 +1221,7 @@ sub NUKIBridge_CallBlocking($$$) {
      NUKIBridge - Steuert das Nuki Smartlock über die Nuki Bridge
      - Das Nuki Bridge Modul verbindet FHEM mit der Nuki Bridge und liest dann alle auf der Bridge verfügbaren Smartlocks ein. Desweiteren werden automatisch die erkannten Smartlocks als eigenständige Devices an gelegt. + Das Nuki Bridge Modul verbindet FHEM mit der Nuki Bridge und liest dann alle auf der Bridge verfügbaren Smartlocks ein. Desweiteren werden automatisch die erkannten Smartlocks als eigenständige Devices an gelegt.

      Define @@ -877,9 +1240,6 @@ sub NUKIBridge_CallBlocking($$$) { Readings
        -
      • 0_nukiId - ID des ersten gefundenen Nuki Smartlocks
      • -
      • 0_name - Name des ersten gefunden Nuki Smartlocks
      • -
      • smartlockCount - Anzahl aller gefundenen Smartlock
      • bridgeAPI - API Version der Bridge
      • bridgeType - Hardware oder Software/App Bridge
      • currentTime - aktuelle Zeit auf der Bridge zum zeitpunkt des Info holens
      • @@ -897,19 +1257,19 @@ sub NUKIBridge_CallBlocking($$$) { Set
          -
        • autocreate - Veranlasst ein erneutes Einlesen aller Smartlocks von der Bridge und falls noch nicht in FHEM vorhanden das autimatische anlegen.
        • -
        • callbackRemove - Löschen einer Callback Instanz auf der Bridge. Die Instanz ID kann mittels get callbackList ermittelt werden
        • -
        • clearLog - löscht das Logfile auf der Bridge
        • +
        • getDeviceList - Veranlasst ein erneutes Einlesen aller Devices von der Bridge und falls noch nicht in FHEM vorhanden das automatische anlegen.
        • +
        • callbackRemove - Löschen der Callback Instanz auf der Bridge.
        • +
        • clearLog - löscht das Logfile auf der Bridge
        • fwUpdate - schaut nach einer neueren Firmware und installiert diese sofern vorhanden
        • -
        • info - holt aktuellen Informationen über die Bridge
        • -
        • reboot - veranlässt ein reboot der Bridge
        • +
        • info - holt aktuellen Informationen über die Bridge
        • +
        • reboot - veranlässt ein reboot der Bridge



        Get
          -
        • callbackList - Gibt die Liste der eingetragenen Callback URL's wieder. Die Bridge nimmt maximal 3 auf.
        • +
        • callbackList - Gibt die Liste der eingetragenen Callback URL's wieder.
        • logFile - Zeigt das Logfile der Bridge an

        @@ -918,6 +1278,8 @@ sub NUKIBridge_CallBlocking($$$) { Attribute
        • disable - deaktiviert die Nuki Bridge
        • +
        • webhookFWinstance - zu verwendene Webinstanz für den Callbackaufruf
        • +
        • webhookHttpHostname - IP oder FQDN vom FHEM Server für den Callbackaufruf

      diff --git a/fhem/FHEM/74_NUKIDevice.pm b/fhem/FHEM/74_NUKIDevice.pm index d3e0ab2af..3c4905310 100644 --- a/fhem/FHEM/74_NUKIDevice.pm +++ b/fhem/FHEM/74_NUKIDevice.pm @@ -1,8 +1,8 @@ ############################################################################### -# +# # Developed with Kate # -# (c) 2016-2017 Copyright: Marko Oldenburg (leongaultier at gmail dot com) +# (c) 2016-2020 Copyright: Marko Oldenburg (leongaultier at gmail dot com) # All rights reserved # # This script is free software; you can redistribute it and/or modify @@ -25,558 +25,521 @@ # ############################################################################### - package main; use strict; use warnings; -use JSON; +# try to use JSON::MaybeXS wrapper +# for chance of better performance + open code +eval { + require JSON::MaybeXS; + import JSON::MaybeXS qw( decode_json encode_json ); + 1; +}; -my $version = "0.6.4"; +if ($@) { + $@ = undef; + # try to use JSON wrapper + # for chance of better performance + eval { + # JSON preference order + local $ENV{PERL_JSON_BACKEND} = + 'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP' + unless ( defined( $ENV{PERL_JSON_BACKEND} ) ); + require JSON; + import JSON qw( decode_json encode_json ); + 1; + }; + + if ($@) { + $@ = undef; + + # In rare cases, Cpanel::JSON::XS may + # be installed but JSON|JSON::MaybeXS not ... + eval { + require Cpanel::JSON::XS; + import Cpanel::JSON::XS qw(decode_json encode_json); + 1; + }; + + if ($@) { + $@ = undef; + + # In rare cases, JSON::XS may + # be installed but JSON not ... + eval { + require JSON::XS; + import JSON::XS qw(decode_json encode_json); + 1; + }; + + if ($@) { + $@ = undef; + + # Fallback to built-in JSON which SHOULD + # be available since 5.014 ... + eval { + require JSON::PP; + import JSON::PP qw(decode_json encode_json); + 1; + }; + + if ($@) { + $@ = undef; + + # Fallback to JSON::backportPP in really rare cases + require JSON::backportPP; + import JSON::backportPP qw(decode_json encode_json); + 1; + } + } + } + } +} + +my $version = '1.8.0'; # Declare functions sub NUKIDevice_Initialize($); sub NUKIDevice_Define($$); sub NUKIDevice_Undef($$); sub NUKIDevice_Attr(@); -sub NUKIDevice_addExtension($$$); -sub NUKIDevice_removeExtension($); sub NUKIDevice_Set($$@); sub NUKIDevice_GetUpdate($); -sub NUKIDevice_ReadFromNUKIBridge($@); sub NUKIDevice_Parse($$); sub NUKIDevice_WriteReadings($$); -sub NUKIDevice_CGI(); +my %deviceTypes = ( + 0 => 'smartlock', + 2 => 'opener' +); +my %modes = ( + 2 => { + 0 => 'door mode', + 2 => 'door mode' + }, + 3 => { + 0 => '-', + 2 => ' continuous mode' + } +); +my %lockStates = ( + 0 => { + 0 => 'uncalibrated', + 2 => 'untrained' + }, + 1 => { + 0 => 'locked', + 2 => 'online' + }, + 2 => { + 0 => 'unlocking', + 2 => '-' + }, + 3 => { + 0 => 'unlocked', + 2 => 'rto active' + }, + 4 => { + 0 => 'locking', + 2 => '-' + }, + 5 => { + 0 => 'unlatched', + 2 => 'open' + }, + 6 => { + 0 => 'unlocked (lock ‘n’ go)', + 2 => '-' + }, + 7 => { + 0 => 'unlatching', + 2 => 'opening' + }, + 253 => { + 0 => '-', + 2 => 'boot run' + }, + 254 => { + 0 => 'motor blocked', + 2 => '-' + }, + 255 => { + 0 => 'undefined', + 2 => 'undefined' + } +); +my %deviceTypeIds = reverse(%deviceTypes); 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 ". - $readingFnAttributes; + $hash->{Match} = '^{.*}$'; + $hash->{SetFn} = 'NUKIDevice_Set'; + $hash->{DefFn} = 'NUKIDevice_Define'; + $hash->{UndefFn} = 'NUKIDevice_Undef'; + $hash->{AttrFn} = 'NUKIDevice_Attr'; + $hash->{ParseFn} = 'NUKIDevice_Parse'; + $hash->{AttrList} = + 'IODev ' + . 'model:opener,smartlock ' + . 'disable:1 ' + . $readingFnAttributes; - foreach my $d(sort keys %{$modules{NUKIDevice}{defptr}}) { + foreach my $d ( sort keys %{ $modules{NUKIDevice}{defptr} } ) { my $hash = $modules{NUKIDevice}{defptr}{$d}; - $hash->{VERSION} = $version; + $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++; - } + my @a = split( '[ \t][ \t]*', $def ); - return "too few parameters: define NUKIDevice " if( @a < 2 ); + return 'too few parameters: define NUKIDevice ' + if ( @a != 4 ); - my ($name,$nukiId) = @a; + my $name = $a[0]; + my $nukiId = $a[2]; + my $deviceType = ( defined $a[3] ) ? $a[3] : 0; $hash->{NUKIID} = $nukiId; + $hash->{DEVICETYPE} = ( defined $deviceType ) ? $deviceType : 0; $hash->{VERSION} = $version; $hash->{STATE} = 'Initialized'; - my $infix = "NUKIDevice"; - - - AssignIoPort($hash,$iodev) if( !$hash->{IODev} ); - - if(defined($hash->{IODev}->{NAME})) { - - Log3 $name, 3, "NUKIDevice ($name) - I/O device is " . $hash->{IODev}->{NAME}; - } else { - - Log3 $name, 1, "NUKIDevice ($name) - no I/O device"; + + my $iodev = AttrVal( $name, 'IODev', 'none' ); + + AssignIoPort( $hash, $iodev ) if ( !$hash->{IODev} ); + + if ( defined( $hash->{IODev}->{NAME} ) ) { + Log3( $name, 3, + "NUKIDevice ($name) - I/O device is " . $hash->{IODev}->{NAME} ); } - + else { + Log3( $name, 1, "NUKIDevice ($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 ); + my $d = $modules{NUKIDevice}{defptr}{$nukiId}; - $modules{NUKIDevice}{defptr}{$code} = $hash; - - - Log3 $name, 3, "NUKIDevice ($name) - defined with Code: $code"; - Log3 $name, 1, "NUKIDevice ($name) - reading battery a deprecated and will be remove in future"; + return + 'NUKIDevice device ' + . $name + . ' on NUKIBridge ' + . $iodev + . ' already defined.' + if ( defined($d) + and $d->{IODev} == $hash->{IODev} + and $d->{NAME} ne $name ); - $attr{$name}{room} = "NUKI" if( !defined( $attr{$name}{room} ) ); - - if ( NUKIDevice_addExtension( $name, "NUKIDevice_CGI", $infix ) ) { - $hash->{fhem}{infix} = $infix; + Log3( $name, 3, "NUKIDevice ($name) - defined with NukiId: $nukiId" ); + + CommandAttr( undef, $name . ' room NUKI' ) + if ( AttrVal( $name, 'room', 'none' ) eq 'none' ); + CommandAttr( undef, $name . ' model ' . $deviceTypes{$deviceType} ) + if ( AttrVal( $name, 'model', 'none' ) eq 'none' ); + + if ($init_done) { + InternalTimer( gettimeofday() + int( rand(10) ), + "NUKIDevice_GetUpdate", $hash ); + } + else { + InternalTimer( gettimeofday() + 15 + int( rand(5) ), + "NUKIDevice_GetUpdate", $hash ); } - $hash->{WEBHOOK_REGISTER} = "unregistered"; - - - - if( $init_done ) { - InternalTimer( gettimeofday()+int(rand(10)), "NUKIDevice_GetUpdate", $hash, 0 ); - } else { - InternalTimer( gettimeofday()+15+int(rand(5)), "NUKIDevice_GetUpdate", $hash, 0 ); - } + $modules{NUKIDevice}{defptr}{$nukiId} = $hash; return undef; } sub NUKIDevice_Undef($$) { - my ( $hash, $arg ) = @_; - + my $nukiId = $hash->{NUKIID}; - my $name = $hash->{NAME}; - - - if ( defined( $hash->{fhem}{infix} ) ) { - NUKIDevice_removeExtension( $hash->{fhem}{infix} ); - } - + 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}); + Log3( $name, 3, "NUKIDevice ($name) - undefined with NukiId: $nukiId" ); + delete( $modules{NUKIDevice}{defptr}{$nukiId} ); return undef; } sub NUKIDevice_Attr(@) { - my ( $cmd, $name, $attrName, $attrVal ) = @_; - my $hash = $defs{$name}; + + my $hash = $defs{$name}; my $token = $hash->{IODev}->{TOKEN}; - if( $attrName eq "disable" ) { - if( $cmd eq "set" and $attrVal eq "1" ) { - readingsSingleUpdate ( $hash, "state", "disabled", 1 ); - Log3 $name, 3, "NUKIDevice ($name) - disabled"; + if ( $attrName eq 'disable' ) { + if ( $cmd eq 'set' and $attrVal == 1 ) { + readingsSingleUpdate( $hash, 'state', 'disabled', 1 ); + Log3( $name, 3, "NUKIDevice ($name) - disabled" ); } - elsif( $cmd eq "del" ) { - readingsSingleUpdate ( $hash, "state", "active", 1 ); - Log3 $name, 3, "NUKIDevice ($name) - enabled"; + elsif ( $cmd eq 'del' ) { + readingsSingleUpdate( $hash, 'state', 'active', 1 ); + Log3( $name, 3, "NUKIDevice ($name) - enabled" ); } } - - if( $attrName eq "disabledForIntervals" ) { - if( $cmd eq "set" ) { - Log3 $name, 3, "NUKIDevice ($name) - enable disabledForIntervals"; - readingsSingleUpdate ( $hash, "state", "Unknown", 1 ); + elsif ( $attrName eq 'disabledForIntervals' ) { + if ( $cmd eq 'set' ) { + Log3( $name, 3, + "NUKIDevice ($name) - enable disabledForIntervals" ); + readingsSingleUpdate( $hash, 'state', 'Unknown', 1 ); } - elsif( $cmd eq "del" ) { - readingsSingleUpdate ( $hash, "state", "active", 1 ); - Log3 $name, 3, "NUKIDevice ($name) - delete disabledForIntervals"; + elsif ( $cmd eq 'del' ) { + readingsSingleUpdate( $hash, 'state', 'active', 1 ); + Log3( $name, 3, + "NUKIDevice ($name) - delete disabledForIntervals" ); } } - - ###################### - #### 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: 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} = "/" . AttrVal( $webhookFWinstance, "webname", "fhem" ) . "/NUKIDevice"; - $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} . "-" . $hash->{NUKIID}; - my $url = "http://$webhookHttpHostname" . ":" . $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"; + elsif ( $attrName eq 'model' ) { + if ( $cmd eq 'set' ) { + Log3( $name, 3, "NUKIDevice ($name) - change model" ); + $hash->{DEVICETYPE} = $deviceTypeIds{$attrVal}; } } - + return undef; } -sub NUKIDevice_addExtension($$$) { - - my ( $name, $func, $link ) = @_; - my $url = "/$link"; - - - return 0 if ( defined( $data{FWEXT}{$url} ) && $data{FWEXT}{$url}{deviceName} ne $name ); - - Log3 $name, 2, "NUKIDevice ($name) - Registering NUKIDevice for webhook URI $url ..."; - - $data{FWEXT}{$url}{deviceName} = $name; - $data{FWEXT}{$url}{FUNC} = $func; - $data{FWEXT}{$url}{LINK} = $link; - - return 1; -} - -sub NUKIDevice_removeExtension($) { - - my ($link) = @_; - - my $url = "/$link"; - my $name = $data{FWEXT}{$url}{deviceName}; - - Log3 $name, 2, "NUKIDevice ($name) - Unregistering NUKIDevice for webhook URL $url..."; - delete $data{FWEXT}{$url}; -} - sub NUKIDevice_Set($$@) { - - my ($hash, $name, @aa) = @_; - my ($cmd, @args) = @aa; + my ( $hash, $name, @aa ) = @_; + my ( $cmd, @args ) = @aa; my $lockAction; - - if( $cmd eq 'statusRequest' ) { - return "usage: statusRequest" if( @args != 0 ); + if ( lc($cmd) eq 'statusrequest' ) { + return ('usage: statusRequest') if ( @args != 0 ); NUKIDevice_GetUpdate($hash); return undef; - - } elsif( $cmd eq 'lock' ) { + } + elsif ($cmd eq 'lock' + or lc($cmd) eq 'deactivaterto' + or $cmd eq 'unlock' + or lc($cmd) eq 'activaterto' + or $cmd eq 'unlatch' + or lc($cmd) eq 'electricstrikeactuation' + or lc($cmd) eq 'lockngo' + or lc($cmd) eq 'activatecontinuousmode' + or lc($cmd) eq 'lockngowithunlatch' + or lc($cmd) eq 'deactivatecontinuousmode' + or $cmd eq 'unpair' ) + { + return ( 'usage: ' . $cmd ) + if ( @args != 0 ); $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; - - } elsif( $cmd eq 'unpair' ) { - - NUKIDevice_ReadFromNUKIBridge($hash,"$cmd",undef,$hash->{NUKIID} ) if( !IsDisabled($name) ); - return undef; - - } else { - my $list = "statusRequest:noArg unlock:noArg lock:noArg unlatch:noArg locknGo:noArg locknGoWithUnlatch:noArg unpair:noArg"; - return "Unknown argument $cmd, choose one of $list"; } - + else { + my $list = ''; + $list = +'statusRequest:noArg unlock:noArg lock:noArg unlatch:noArg locknGo:noArg locknGoWithUnlatch:noArg unpair:noArg' + if ( $hash->{DEVICETYPE} == 0 ); + $list = +'statusRequest:noArg activateRto:noArg deactivateRto:noArg electricStrikeActuation:noArg activateContinuousMode:noArg deactivateContinuousMode:noArg unpair:noArg' + if ( $hash->{DEVICETYPE} == 2 ); + + return ( 'Unknown argument ' . $cmd . ', choose one of ' . $list ); + } + $hash->{helper}{lockAction} = $lockAction; - NUKIDevice_ReadFromNUKIBridge($hash,"lockAction",$lockAction,$hash->{NUKIID} ) if( !IsDisabled($name) ); - + + IOWrite( $hash, 'lockAction', $lockAction, $hash->{NUKIID}, + $hash->{DEVICETYPE} ); + return undef; } sub NUKIDevice_GetUpdate($) { + my $hash = shift; - 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) ); + + IOWrite( $hash, 'lockState', undef, $hash->{NUKIID}, $hash->{DEVICETYPE} ) + if ( !IsDisabled($name) ); + Log3( $name, 5, "NUKIDevice ($name) - NUKIDevice_GetUpdate Call IOWrite" ) + 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, "NUKIDevice ($name) - 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, $json ) = @_; - my($hash,$result) = @_; my $name = $hash->{NAME}; - Log3 $name, 5, "NUKIDevice ($name) - Parse with result: $result"; + Log3( $name, 5, "NUKIDevice ($name) - Parse with result: $json" ); ######################################### ####### Errorhandling ############# - - if( !$result ) { - Log3 $name, 3, "NUKIDevice ($name) - empty answer received"; - return undef; - } elsif( $result =~ m'HTTP/1.1 200 OK' ) { - Log3 $name, 4, "NUKIDevice ($name) - empty answer received"; - return undef; - } elsif( $result !~ m/^[\[{].*[}\]]$/ ) { - Log3 $name, 3, "NUKIDevice ($name) - invalid json detected: $result"; - return "NUKIDevice ($name) - invalid json detected: $result"; - } - - 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; - } - - if( $result eq 503 ) { - readingsSingleUpdate( $hash, "state", "smartlock is offline", 1 ); - Log3 $name, 3, "NUKIDevice ($name) - smartlock is offline"; - return; - } - } - - - ######################################### - #### verarbeiten des JSON Strings ####### - my $decode_json = eval{decode_json($result)}; - if($@){ - Log3 $name, 3, "NUKIDevice ($name) - JSON error while request: $@"; - return; - } - - - 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 - - my $battery; - if( defined($decode_json->{batteryCritical}) ) { - if( $decode_json->{batteryCritical} eq "false" or $decode_json->{batteryCritical} == 0 ) { - $battery = "ok"; - } elsif ( $decode_json->{batteryCritical} eq "true" or $decode_json->{batteryCritical} == 1 ) { - $battery = "low"; - } - } - - - readingsBeginUpdate($hash); - - if( defined($hash->{helper}{lockAction}) ) { - - my ($state,$lockState); - - - if( defined($decode_json->{success}) and ($decode_json->{success} eq "true" or $decode_json->{success} eq "1") ) { - - $state = $hash->{helper}{lockAction}; - $lockState = $hash->{helper}{lockAction}; - NUKIDevice_ReadFromNUKIBridge($hash, "lockState", undef, $hash->{NUKIID} ) if( ReadingsVal($hash->{IODev}->{NAME},'bridgeType','Software') eq 'Software' ); - - } elsif ( defined($decode_json->{success}) and ($decode_json->{success} eq "false" or $decode_json->{success} eq "0") ) { - - $state = "error"; - NUKIDevice_ReadFromNUKIBridge($hash, "lockState", undef, $hash->{NUKIID} ); - } - - readingsBulkUpdate( $hash, "state", $state ); - readingsBulkUpdate( $hash, "lockState", $lockState ); - readingsBulkUpdate( $hash, "success", $decode_json->{success} ); - - - delete $hash->{helper}{lockAction}; - Log3 $name, 5, "NUKIDevice ($name) - lockAction 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, "batteryState", $battery ); - readingsBulkUpdate( $hash, "success", $decode_json->{success} ); - - readingsBulkUpdate( $hash, "name", $decode_json->{name} ); - readingsBulkUpdate( $hash, "rssi", $decode_json->{rssi} ); - readingsBulkUpdate( $hash, "paired", $decode_json->{paired} ); - - Log3 $name, 5, "NUKIDevice ($name) - readings set for $name"; - } - - readingsEndUpdate( $hash, 1 ); - - - return undef; -} - -sub NUKIDevice_CGI() { - - my ($request) = @_; - - my $hash; - my $name; - my $nukiId; - - - # data received - # Testaufruf: - # curl --data '{"nukiId": 123456, "state": 1,"stateName": "locked", "batteryCritical": false}' http://10.6.6.20:8083/fhem/NUKIDevice-123456 - # wget --post-data '{"nukiId": 123456, "state": 1,"stateName": "locked", "batteryCritical": false}' http://10.6.6.20:8083/fhem/NUKIDevice-123456 - - - my $header = join("\n", @FW_httpheader); - - my ($first,$json) = split("&",$request,2); - - if( !$json ) { - Log3 $name, 3, "NUKIDevice ($name) - empty answer received"; - return undef; - } elsif( $json =~ m'HTTP/1.1 200 OK' ) { - Log3 $name, 4, "NUKIDevice ($name) - empty answer received"; - return undef; - } elsif( $json !~ m/^[\[{].*[}\]]$/ ) { - Log3 $name, 3, "NUKIDevice ($name) - invalid json detected: $json"; + if ( $json !~ m/^[\[{].*[}\]]$/ ) { + Log3( $name, 3, "NUKIDevice ($name) - invalid json detected: $json" ); return "NUKIDevice ($name) - invalid json detected: $json"; } - my $decode_json = eval{decode_json($json)}; - if($@){ - Log3 $name, 3, "NUKIDevice ($name) - JSON error while request: $@"; + ######################################### + #### verarbeiten des JSON Strings ####### + my $decode_json = eval { decode_json($json) }; + if ($@) { + Log3( $name, 3, "NUKIDevice ($name) - JSON error while request: $@" ); return; } - - - if( ref($decode_json) eq "HASH" ) { - if ( defined( $modules{NUKIDevice}{defptr} ) ) { - while ( my ( $key, $value ) = each %{ $modules{NUKIDevice}{defptr} } ) { - $hash = $modules{NUKIDevice}{defptr}{$key}; - $name = $hash->{NAME}; - $nukiId = InternalVal( $name, "NUKIID", undef ); - next if ( !$nukiId or $nukiId ne $decode_json->{nukiId} ); + if ( ref($decode_json) ne 'HASH' ) { + Log3( $name, 2, +"NUKIDevice ($name) - got wrong status message for $name: $decode_json" + ); + return undef; + } - $hash->{WEBHOOK_COUNTER}++; - $hash->{WEBHOOK_LAST} = TimeNow(); + my $nukiId = $decode_json->{nukiId}; - Log3 $name, 4, "NUKIDevice ($name) - Received webhook for matching NukiId at device $name"; - - NUKIDevice_Parse($hash,$json); - } + if ( my $hash = $modules{NUKIDevice}{defptr}{$nukiId} ) { + my $name = $hash->{NAME}; + + NUKIDevice_WriteReadings( $hash, $decode_json ); + Log3( $name, 4, + "NUKIDevice ($name) - find logical device: $hash->{NAME}" ); + + ################## + ## Zwischenlösung so für die Umstellung, kann später gelöscht werden + if ( AttrVal( $name, 'model', '' ) eq '' ) { + CommandDefMod( undef, + $name + . ' NUKIDevice ' + . $hash->{NUKIID} . ' ' + . $decode_json->{deviceType} ); + CommandAttr( undef, + $name + . ' model ' + . $deviceTypes{ $decode_json->{deviceType} } ); + Log3( $name, 2, "NUKIDevice ($name) - redefined Defmod" ); } - - return ( undef, undef ); + + return $hash->{NAME}; } - - # no data received else { - - Log3 undef, 4, "NUKIDevice - received malformed request\n$request"; + Log3( $name, 3, + "NUKIDevice ($name) - autocreate new device " + . makeDeviceName( $decode_json->{name} ) + . " with nukiId $decode_json->{nukiId}, model $decode_json->{deviceType}" + ); + return + 'UNDEFINED ' + . makeDeviceName( $decode_json->{name} ) + . " NUKIDevice $decode_json->{nukiId} $decode_json->{deviceType}"; } - return ( "text/plain; charset=utf-8", "Call failure: " . $request ); + 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 + if ( defined( $hash->{helper}{lockAction} ) ) { + my $state; + if ( + defined( $decode_json->{success} ) + and ( $decode_json->{success} eq 'true' + or $decode_json->{success} == 1 ) + ) + { + $state = $hash->{helper}{lockAction}; + IOWrite( $hash, 'lockState', undef, $hash->{NUKIID} ) + if ( + ReadingsVal( $hash->{IODev}->{NAME}, 'bridgeType', 'Software' ) + eq 'Software' ); + } + elsif ( + defined( $decode_json->{success} ) + and ( $decode_json->{success} eq 'false' + or $decode_json->{success} == 0 ) + ) + { + + $state = $deviceTypes{ $hash->{DEVICETYPE} } . ' response error'; + IOWrite( $hash, 'lockState', undef, $hash->{NUKIID}, + $hash->{DEVICETYPE} ); + } + + $decode_json->{'state'} = $state; + delete $hash->{helper}{lockAction}; + } + + readingsBeginUpdate($hash); + + my $t; + my $v; + + if ( defined( $decode_json->{lastKnownState} ) + and ref( $decode_json->{lastKnownState} ) eq 'HASH' ) + { + while ( ( $t, $v ) = each %{ $decode_json->{lastKnownState} } ) { + $decode_json->{$t} = $v; + } + + delete $decode_json->{lastKnownState}; + } + + while ( ( $t, $v ) = each %{$decode_json} ) { + readingsBulkUpdate( $hash, $t, $v ) + unless ( $t eq 'state' + or $t eq 'mode' + or $t eq 'deviceType' + or $t eq 'paired' + or $t eq 'batteryCritical' + or $t eq 'timestamp' ); + readingsBulkUpdate( $hash, $t, + ( $v =~ m/^[0-9]$/ ? $lockStates{$v}{ $hash->{DEVICETYPE} } : $v ) ) + if ( $t eq 'state' ); + readingsBulkUpdate( $hash, $t, $modes{$v}{ $hash->{DEVICETYPE} } ) + if ( $t eq 'mode' ); + readingsBulkUpdate( $hash, $t, $deviceTypes{$v} ) + if ( $t eq 'deviceType' ); + readingsBulkUpdate( $hash, $t, ( $v == 1 ? 'true' : 'false' ) ) + if ( $t eq 'paired' ); + readingsBulkUpdate( $hash, 'batteryState', + ( ( $v eq 'true' or $v == 1 ) ? 'low' : 'ok' ) ) + if ( $t eq 'batteryCritical' ); + } + + readingsEndUpdate( $hash, 1 ); + + Log3( $name, 5, "NUKIDevice ($name) - lockAction readings set for $name" ); + + return undef; +} 1; - - - =pod =item device =item summary Modul to control the Nuki Smartlock's @@ -589,17 +552,19 @@ sub NUKIDevice_CGI() {
        NUKIDevice - Controls the Nuki Smartlock
        - The Nuki module connects FHEM over the Nuki Bridge with a Nuki Smartlock. After that, it´s possible to lock and unlock the Smartlock.
        + The Nuki module connects FHEM over the Nuki Bridge with a Nuki Smartlock or Nuki Opener. After that, it´s possible to lock and unlock the Smartlock.
        Normally the Nuki devices are automatically created by the bridge module.

        Define

          - define <name> NUKIDevice <Nuki-Id> <IODev-Device> + define <name> NUKIDevice <Nuki-Id> <IODev-Device> <Device-Type> +

          + Device-Type is 0 for the Smartlock and 2 for the Opener.

          Example:

            - define Frontdoor NUKIDevice 1 NBridge1
            + define Frontdoor NUKIDevice 1 NBridge1 0

          This statement creates a NUKIDevice with the name Frontdoor, the NukiId 1 and the IODev device NBridge1.
          @@ -636,8 +601,6 @@ sub NUKIDevice_CGI() { Attributes
          • disable - disables the Nuki device
          • -
          • webhookFWinstance - Webinstanz of the Callback
          • -
          • webhookHttpHostname - IP or FQDN of the FHEM Server Callback

        @@ -650,17 +613,19 @@ sub NUKIDevice_CGI() {
          NUKIDevice - Steuert das Nuki Smartlock
          - Das Nuki Modul verbindet FHEM über die Nuki Bridge mit einem Nuki Smartlock. Es ist dann möglich das Schloss zu ver- und entriegeln.
          + Das Nuki Modul verbindet FHEM über die Nuki Bridge mit einem Nuki Smartlock oder Nuki Opener. Es ist dann möglich das Schloss zu ver- und entriegeln.
          In der Regel werden die Nuki Devices automatisch durch das Bridgemodul angelegt.

          Define

            - define <name> NUKIDevice <Nuki-Id> <IODev-Device> + define <name> NUKIDevice <Nuki-Id> <IODev-Device> <Device-Type> +

            + Device-Type ist 0 für das Smartlock und 2 f&üuml;r den Opener.

            Beispiel:

              - define Haustür NUKIDevice 1 NBridge1
              + define Haustür NUKIDevice 1 NBridge1 0

            Diese Anweisung erstellt ein NUKIDevice mit Namen Haustür, der NukiId 1 sowie dem IODev Device NBridge1.
            @@ -697,8 +662,6 @@ sub NUKIDevice_CGI() { Attribute
            • disable - deaktiviert das Nuki Device
            • -
            • webhookFWinstance - zu verwendene Webinstanz für den Callbackaufruf
            • -
            • webhookHttpHostname - IP oder FQDN vom FHEM Server für den Callbackaufruf