diff --git a/20211130NukiBridgeAPI1_13_0.pdf b/20211130NukiBridgeAPI1_13_0.pdf new file mode 100644 index 0000000..e9d1884 Binary files /dev/null and b/20211130NukiBridgeAPI1_13_0.pdf differ diff --git a/73_NUKIBridge.pm b/73_NUKIBridge.pm deleted file mode 100755 index 4c6d1a7..0000000 --- a/73_NUKIBridge.pm +++ /dev/null @@ -1,1464 +0,0 @@ -############################################################################### -# -# Developed with Kate -# -# (c) 2016-2021 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) -# 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$ -# -############################################################################### - -################################# -######### Wichtige Hinweise und Links ################# - -## Beispiel für Logausgabe -# https://forum.fhem.de/index.php/topic,55756.msg508412.html#msg508412 - -## -# - -################################ - -package main; - -use strict; -use warnings; - -package FHEM::NUKIBridge; - -use strict; -use warnings; -use HttpUtils; -use FHEM::Meta; -use GPUtils qw(GP_Import GP_Export); - -# 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 { - - # 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; - } - } - } - } -} - -## Import der FHEM Funktionen -#-- Run before package compilation -BEGIN { - - # Import from main context - GP_Import( - qw( - readingsSingleUpdate - readingsBulkUpdate - readingsBeginUpdate - readingsEndUpdate - readingFnAttributes - defs - modules - Log3 - CommandAttr - AttrVal - IsDisabled - deviceEvents - init_done - gettimeofday - InternalTimer - InternalVal - ReadingsVal - RemoveInternalTimer - HttpUtils_NonblockingGet - asyncOutput - data - TimeNow - devspec2array - Dispatch) - ); -} - -#-- Export to main context with different name -GP_Export( - qw( - GetCheckBridgeAlive - Initialize - CGI - BridgeCall - ) -); - -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 -); - -sub Initialize($) { - my ($hash) = @_; - - # Provider - $hash->{WriteFn} = 'FHEM::NUKIBridge::Write'; - $hash->{Clients} = ':NUKIDevice:'; - $hash->{MatchList} = { '1:NUKIDevice' => '^{.*}$' }; - - my $webhookFWinstance = - join( ",", devspec2array('TYPE=FHEMWEB:FILTER=TEMPORARY!=1') ); - - # Consumer - $hash->{SetFn} = 'FHEM::NUKIBridge::Set'; - $hash->{GetFn} = 'FHEM::NUKIBridge::Get'; - $hash->{DefFn} = 'FHEM::NUKIBridge::Define'; - $hash->{UndefFn} = 'FHEM::NUKIBridge::Undef'; - $hash->{NotifyFn} = 'FHEM::NUKIBridge::Notify'; - $hash->{AttrFn} = 'FHEM::NUKIBridge::Attr'; - $hash->{AttrList} = - 'disable:1 ' - . 'webhookFWinstance:' - . $webhookFWinstance . ' ' - . 'webhookHttpHostname ' - . $readingFnAttributes; - - return FHEM::Meta::InitMod( __FILE__, $hash ); -} - -sub Define($$) { - my ( $hash, $def ) = @_; - - my @a = split( "[ \t][ \t]*", $def ); - - return $@ unless ( FHEM::Meta::SetInternals($hash) ); - use version 0.60; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); - - return ('too few parameters: define NUKIBridge ') - if ( @a != 4 ); - - 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->{NOTIFYDEV} = 'global,' . $name; - $hash->{VERSION} = version->parse($VERSION)->normal; - $hash->{BRIDGEAPI} = FHEM::Meta::Get( $hash, 'x_apiversion' ); - $hash->{helper}->{actionQueue} = []; - $hash->{helper}->{iowrite} = 0; - my $infix = 'NUKIBridge'; - - 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' ); - - if ( addExtension( $name, 'NUKIBridge_CGI', $infix . "-" . $host ) ) { - $hash->{fhem}{infix} = $infix; - } - - $hash->{WEBHOOK_REGISTER} = "unregistered"; - - readingsSingleUpdate( $hash, 'state', 'Initialized', 1 ); - - RemoveInternalTimer($hash); - - $modules{NUKIBridge}{defptr}{ $hash->{HOST} } = $hash; - - return undef; -} - -sub Undef($$) { - my ( $hash, $arg ) = @_; - - my $host = $hash->{HOST}; - my $name = $hash->{NAME}; - - if ( defined( $hash->{fhem}{infix} ) ) { - removeExtension( $hash->{fhem}{infix} ); - } - - RemoveInternalTimer($hash); - delete $modules{NUKIBridge}{defptr}{ $hash->{HOST} }; - - return undef; -} - -sub Attr(@) { - my ( $cmd, $name, $attrName, $attrVal ) = @_; - - my $hash = $defs{$name}; - my $orig = $attrVal; - - 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" ); - } - } - - 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" ); -# Write( $hash, 'callback/add', $url, undef, undef ) - Write( $hash, 'callback/add', '{"param":"' . $url . '"}' ) - if ($init_done); - $hash->{WEBHOOK_REGISTER} = 'sent'; - } - else { - $hash->{WEBHOOK_REGISTER} = 'incomplete_attributes'; - } - } - - return undef; -} - -sub Notify($$) { - - my ( $hash, $dev ) = @_; - my $name = $hash->{NAME}; - return if ( IsDisabled($name) ); - - my $devname = $dev->{NAME}; - my $devtype = $dev->{TYPE}; - my $events = deviceEvents( $dev, 1 ); - return if ( !$events ); - - FirstRun($hash) - if ( - ( - grep /^INITIALIZED$/, - @{$events} - or grep /^REREADCFG$/, - @{$events} - or grep /^MODIFIED.$name$/, - @{$events} - or grep /^DEFINED.$name$/, - @{$events} - ) - and $devname eq 'global' - and $init_done - ); - - return; -} - -sub 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 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 Set($@) { - my ( $hash, $name, $cmd, @args ) = @_; - - my ( $arg, @params ) = @args; - my $endpoint; - - if ( lc($cmd) eq 'getdevicelist' ) { - return 'usage: getDeviceList' if ( @args != 0 ); - - $endpoint = 'list'; - } - elsif ( $cmd eq 'info' ) { - return 'usage: statusRequest' if ( @args != 0 ); - - $endpoint = 'info'; - } - elsif ( lc($cmd) eq 'fwupdate' ) { - return 'usage: fwUpdate' if ( @args != 0 ); - - $endpoint = 'fwupdate'; - } - elsif ( $cmd eq 'reboot' ) { - return 'usage: reboot' if ( @args != 0 ); - - $endpoint = 'reboot'; - } - elsif ( lc($cmd) eq 'clearlog' ) { - return 'usage: clearLog' if ( @args != 0 ); - - $endpoint = 'clearlog'; - } - elsif ( lc($cmd) eq 'factoryreset' ) { - return 'usage: clearLog' if ( @args != 0 ); - - $endpoint = 'factoryReset'; - } - elsif ( lc($cmd) eq 'callbackremove' ) { - return 'usage: callbackRemove' if ( @args > 1 ); - - my $id = ( @args > 0 ? join( ' ', @args ) : 0 ); - -# Write( $hash, 'callback/remove', $id, undef, undef ) - Write( $hash, 'callback/remove', '{"param":"' . $id . '"}' ) - if ( !IsDisabled($name) ); - - return undef; - } - else { - my $list = ''; - $list .= 'info:noArg getDeviceList: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 ); - } - - Write( $hash, $endpoint, undef ) - if ( !IsDisabled($name) ); -} - -sub Get($@) { - my ( $hash, $name, $cmd, @args ) = @_; - - my ( $arg, @params ) = @args; - my $endpoint; - - if ( lc($cmd) eq 'logfile' ) { - return 'usage: logFile' if ( @args != 0 ); - - $endpoint = 'log'; - } - elsif ( lc($cmd) eq 'callbacklist' ) { - return 'usage: callbackList' if ( @args != 0 ); - - $endpoint = 'callback/list'; - } - else { - my $list = ''; - $list .= 'callbackList:noArg '; - $list .= 'logFile:noArg' - if ( ReadingsVal( $name, 'bridgeType', 'Software' ) eq 'Hardware' ); - - return 'Unknown argument ' . $cmd . ', choose one of ' . $list; - } - - Write( $hash, $endpoint, undef ) -} - -sub GetCheckBridgeAlive($) { - my ($hash) = @_; - - my $name = $hash->{NAME}; - - RemoveInternalTimer($hash); - Log3( $name, 4, "NUKIBridge ($name) - GetCheckBridgeAlive" ); - - if ( !IsDisabled($name) - and $hash->{helper}->{iowrite} == 0 ) - { - - Write( $hash, 'info', undef); - - Log3( $name, 4, "NUKIBridge ($name) - run Write" ); - } - - InternalTimer( gettimeofday() + 30, - 'NUKIBridge_GetCheckBridgeAlive', $hash ); - - Log3( $name, 4, - "NUKIBridge ($name) - Call InternalTimer for GetCheckBridgeAlive" ); -} - -sub FirstRun($) { - my ($hash) = @_; - - my $name = $hash->{NAME}; - - RemoveInternalTimer($hash); - Write( $hash, 'list', undef ) - if ( !IsDisabled($name) ); - InternalTimer( gettimeofday() + 5, - 'NUKIBridge_GetCheckBridgeAlive', $hash ); - - return undef; -} - -sub Write($@) { - my ( $hash, $endpoint, $json ) = @_; - - my $decode_json = eval { decode_json($json) } - if ( defined($json) ); - - my $nukiId = $decode_json->{nukiId} // undef; - my $deviceType = $decode_json->{deviceType} // undef; - my $param = $decode_json->{param} // undef; - - my $obj = { - endpoint => $endpoint, - param => $param, - nukiId => $nukiId, - deviceType => $deviceType - }; - - $hash->{helper}->{lastDeviceAction} = $obj - if ( - ( defined($param) and $param ) - or ( defined($nukiId) - and $nukiId ) - ); - - unshift( @{ $hash->{helper}->{actionQueue} }, $obj ); - - BridgeCall($hash); -} - -sub CreateUri($$) { - my ( $hash, $obj ) = @_; - - my $name = $hash->{NAME}; - 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) ); - - if ( defined($param) - and defined($deviceType) ) - { - $uri .= '&action=' . $lockActionsSmartLock{$param} - if ( $endpoint ne 'callback/add' - and ($deviceType == 0 - || $deviceType == 4) ); - - $uri .= '&action=' . $lockActionsOpener{$param} - if ( $endpoint ne 'callback/add' - and $deviceType == 2 ); - } - - $uri .= '&id=' . $param - if ( defined($param) - and $endpoint eq 'callback/remove' ); - - $uri .= '&url=' . $param - if ( defined($param) - and $endpoint eq 'callback/add' ); - - $uri .= '&nukiId=' . $nukiId - if ( defined($nukiId) ); - $uri .= '&deviceType=' . $deviceType - if ( defined($deviceType) ); - - Log3( $name, 4, "NUKIBridge ($name) - created uri: $uri" ); - return $uri; -} - -sub BridgeCall($) { - 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 = CreateUri( $hash, $obj ); - - if ( defined($uri) and $uri ) { - $hash->{helper}->{iowrite} = 1; - - my $param = { - url => $uri, - timeout => 30, - hash => $hash, - nukiId => $nukiId, - endpoint => $endpoint, - header => 'Accept: application/json', - method => 'GET', - callback => \&Distribution, - }; - - $param->{cl} = $hash->{CL} - if ( - ( - $endpoint eq 'callback/list' - or $endpoint eq 'log' - ) - and ref( $hash->{CL} ) eq 'HASH' - ); - - HttpUtils_NonblockingGet($param); - 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 Distribution($$$) { - my ( $param, $err, $json ) = @_; - - 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, 4, "NUKIBridge ($name) - Response JSON: $json" ); - Log3( $name, 4, "NUKIBridge ($name) - Response ERROR: $err" ); - Log3( $name, 4, "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' ); - Log3( $name, 5, "NUKIBridge ($name) - Bridge ist offline" ); - } - - 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 ); - - asyncOutput( $param->{cl}, "Request Error: $err\r\n" ) - if ( $param->{cl} && $param->{cl}{canAsyncOutput} ); - - return $err; - } - } - - 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 ); - - if ( defined( $hash->{helper}->{lastDeviceAction} ) - and $hash->{helper}->{lastDeviceAction} ) - { - push( - @{ $hash->{helper}->{actionQueue} }, - $hash->{helper}->{lastDeviceAction} - ); - - InternalTimer( gettimeofday() + 1, - 'NUKIBridge_BridgeCall', $hash ); - } - - asyncOutput( $param->{cl}, "Request Error: $err\r\n" ) - if ( $param->{cl} && $param->{cl}{canAsyncOutput} ); - - return; - } - - 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' ); - - asyncOutput( $param->{cl}, "Request Error: $err\r\n" ) - if ( $param->{cl} && $param->{cl}{canAsyncOutput} ); - } - - 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 ); - - readingsEndUpdate( $hash, 1 ); - - asyncOutput( $param->{cl}, "Request Error: $err\r\n" ) - if ( $param->{cl} && $param->{cl}{canAsyncOutput} ); - - return $param->{code}; - } - - delete $hash->{helper}->{lastDeviceAction} - if ( defined( $hash->{helper}->{lastDeviceAction} ) - and $hash->{helper}->{lastDeviceAction} ); - - readingsEndUpdate( $hash, 1 ); - - readingsSingleUpdate( $hash, 'state', 'connected', 1 ); - Log3( $name, 5, "NUKIBridge ($name) - Bridge ist online" ); - - if ( $param->{endpoint} eq 'callback/list' ) { - getCallbackList( $param, $json ); - return undef; - } - elsif ( $param->{endpoint} eq 'log' ) { - getLogfile( $param, $json ); - return undef; - } - - if ( $hash == $dhash ) { - 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() + 3, 'NUKIBridge_BridgeCall', $hash ) - if ( defined( $hash->{helper}->{actionQueue} ) - and scalar( @{ $hash->{helper}->{actionQueue} } ) > 0 ); - - return undef; -} - -sub ResponseProcessing($$$) { - my ( $hash, $json, $endpoint ) = @_; - - my $name = $hash->{NAME}; - my $decode_json; - - 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" ); - return undef; - } - 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: $@" ); - return; - } - - 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) eq 'HASH' - and 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 ) = 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 ) = ParseJSON( $hash, $tail ); - - Log3( $name, 5, - "NUKIBridge ($name) - Nach Sub: Laenge JSON: " - . length($json) - . " Content: " - . $json - . " Tail: " - . $tail ); - } - } - - WriteReadings( $hash, $decode_json ) - if ( $endpoint eq 'info' ); - } - else { - Log3( - $name, 5, "NUKIBridge ($name) - Rückgabe Path nicht korrekt: -$json" - ); - return; - } - - return undef; -} - -sub CGI() { - my ($request) = @_; - - my $hash; - my $name; - while ( my ( $key, $value ) = each %{ $modules{NUKIBridge}{defptr} } ) { - $hash = $modules{NUKIBridge}{defptr}{$key}; - $name = $hash->{NAME}; - } - - return ('NUKIBridge WEBHOOK - No IODev found') - unless ( defined($hash) and defined($name) ); - - my $json = ( split( '&', $request, 2 ) )[1]; - - if ( !$json ) { - Log3( $name, 3, "NUKIBridge WEBHOOK ($name) - empty message received" ); - return undef; - } - 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"); - } - - 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 WriteReadings($$) { - my ( $hash, $decode_json ) = @_; - - my $name = $hash->{NAME}; - - my $nukiId; - my $scanResults; - my %response_hash; - my $dname; - my $dhash; - - 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, 'currentGMTime', $decode_json->{currentTime} ); - readingsBulkUpdate( $hash, 'serverConnected', - $decode_json->{serverConnected} ); - readingsEndUpdate( $hash, 1 ); -} - -sub getLogfile($$) { - my ( $param, $json ) = @_; - - my $hash = $param->{hash}; - my $name = $hash->{NAME}; - - my $decode_json = eval { decode_json($json) }; - if ($@) { - Log3( $name, 3, "NUKIBridge ($name) - JSON error while request: $@" ); - return; - } - - Log3( $name, 4, - "NUKIBridge ($name) - Log data are collected and processed" ); - - if ( $param->{cl} and $param->{cl}->{TYPE} eq 'FHEMWEB' ) { - if ( ref($decode_json) eq 'ARRAY' and scalar( @{$decode_json} ) > 0 ) { - Log3( $name, 4, - "NUKIBridge ($name) - created Table with log file" ); - - my $header = '' - . '
Log List
'; - - my $ret = $header.''; - $ret .= '
'; - $ret .= ''; - - foreach my $logs ( @{$decode_json} ) { - $ret .= ''; - - if ( $logs->{timestamp} ) { - $ret .= ''; - $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 .= ''; - $ret .= ''; - } - - $ret .= ''; - } - - $ret .= '
timestamp: ' . $logs->{timestamp} . ' type: ' . $logs->{type} . ' ' . $d . ': ' . $logs->{$d} . '
'; - - asyncOutput( $param->{cl}, $ret ) - if ( $param->{cl} and $param->{cl}{canAsyncOutput} ); - return; - } - } -} - -sub getCallbackList($$) { - my ( $param, $json ) = @_; - - my $hash = $param->{hash}; - my $name = $hash->{NAME}; - - my $decode_json = eval { decode_json($json) }; - if ($@) { - Log3( $name, 3, "NUKIBridge ($name) - JSON error while request: $@" ); - return; - } - - Log3( $name, 4, - "NUKIBridge ($name) - Callback data are collected and processed" ); - - if ( $param->{cl} and $param->{cl}->{TYPE} eq 'FHEMWEB' ) { - if ( ref( $decode_json->{callbacks} ) eq 'ARRAY' ) { - Log3( $name, 4, - "NUKIBridge ($name) - created Table with log file" ); - - my $space = ' '; - my $aHref; - my $header = '' - . '
Callback List
'; - - my $ret = $header.''; - $ret .= '
'; - $ret .= ''; - $ret .= ''; - $ret .= ''; - $ret .= ''; - $ret .= ''; - - if ( scalar( @{ $decode_json->{callbacks} } ) > 0 ) { - foreach my $cb ( @{ $decode_json->{callbacks} } ) { - $aHref = - "{host} - . "/fhem?cmd=set+" - . $name - . "+callbackRemove+" - . $cb->{id} - . $::FW_CSRF - . "\">X"; - - $ret .= ''; - $ret .= ''; - $ret .= ''; - } - } - else { - $ret .= ''; - $ret .= ''; - $ret .= ''; - $ret .= ''; - } - - $ret .= '
URLRemove
' . $cb->{url} . ''.$aHref.'
nonenone
'; - - asyncOutput( $param->{cl}, $ret ) - if ( $param->{cl} and $param->{cl}{canAsyncOutput} ); - return; - } - } -} - -sub getCallbackList2($$) { - my ( $param, $json ) = @_; - - my $hash = $param->{hash}; - my $name = $hash->{NAME}; - - my $decode_json = eval { decode_json($json) }; - if ($@) { - Log3( $name, 3, "NUKIBridge ($name) - JSON error while request: $@" ); - return; - } - - Log3( $name, 4, - "NUKIBridge ($name) - Callback data are collected and processed" ); - - if ( $param->{cl} and $param->{cl}->{TYPE} eq 'FHEMWEB' ) { - if ( ref( $decode_json->{callbacks} ) eq 'ARRAY' ) { - Log3( $name, 4, - "NUKIBridge ($name) - created Table with Callback List" ); - - - my $j1 = ''; - -# FW_cmd(FW_root+"?cmd="+type+" "+dev+ -# (params[0]=="state" ? "":" "+params[0])+" "+arg+"&XHR=1"); - - - my $header = ''; - my $footer = ''; - - my $ret = '
Callback List
' - . ''; - $ret .= '
' - . '' - . '' - . '' - . '' - . ''; - - if ( scalar( @{ $decode_json->{callbacks} } ) > 0 ) { - foreach my $cb ( @{ $decode_json->{callbacks} } ) { - $ret .= ''; - $ret .= ""; - $ret .= ''; - } - } - else { - $ret .= ''; - $ret .= ''; - $ret .= ''; - $ret .= ''; - } - - $ret .= '
URLRemove
' . $cb->{url} . '
nonenone
'; - - Log3( $name, 4, - "NUKIBridge ($name) - Callback List Table created and call asyncOutput Fn" ); - - asyncOutput( $param->{cl}, $header . $ret . $j1 . $footer ) - if ( $param->{cl} and $param->{cl}{canAsyncOutput} ); - return; - } - } -} - -sub 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. -=item summary_DE Modul zur Steuerung des Nuki Smartlock über die Nuki Bridge. - -=begin html - - -

NUKIBridge

-
    - NUKIBridge - controls the Nuki Smartlock over the Nuki Bridge -
    - The Nuki Bridge module connects FHEM to the Nuki Bridge and then reads all the smartlocks available on the bridge. Furthermore, the detected Smartlocks are automatically created as independent devices. -

    - - Define -

      - define <name> NUKIBridge <HOST> <API-TOKEN> -

      - Example: -

        - define NBridge1 NUKIBridge 192.168.0.23 F34HK6
        -
      -
      - This statement creates a NUKIBridge device with the name NBridge1 and the IP 192.168.0.23 as well as the token F34HK6.
      - After the bridge device is created, all available Smartlocks are automatically placed in FHEM. -
    -

    - - Readings -
      -
    • bridgeAPI - API Version of bridge
    • -
    • bridgeType - Hardware bridge / Software bridge
    • -
    • currentTime - Current timestamp
    • -
    • firmwareVersion - Version of the bridge firmware
    • -
    • hardwareId - Hardware ID
    • -
    • lastError - Last connected error
    • -
    • serverConnected - Flag indicating whether or not the bridge is connected to the Nuki server
    • -
    • serverId - Server ID
    • -
    • uptime - Uptime of the bridge in seconds
    • -
    • wifiFirmwareVersion- Version of the WiFi modules firmware
    • -
      - The preceding number is continuous, starts with 0 und returns the properties of one Smartlock. -
    -

    - - Set -
      -
    • 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)
    • -
    • fwUpdate - Immediately checks for a new firmware update and installs it (only hardwarebridge)
    • -
    • info - Returns all Smart Locks in range and some device information of the bridge itself
    • -
    • reboot - reboots the bridge (only hardwarebridge)
    • -
      -
    -

    - - Get -
      -
    • callbackList - List of register url callbacks.
    • -
    • logFile - Retrieves the log of the Bridge
    • -
      -
    -

    - - Attributes -
      -
    • disable - disables the Nuki Bridge
    • -
    • webhookFWinstance - Webinstanz of the Callback
    • -
    • webhookHttpHostname - IP or FQDN of the FHEM Server Callback
    • -
      -
    -
- -=end html -=begin html_DE - - -

NUKIBridge

-
    - 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. -

    - - Define -

      - define <name> NUKIBridge <HOST> <API-TOKEN> -

      - Beispiel: -

        - define NBridge1 NUKIBridge 192.168.0.23 F34HK6
        -
      -
      - Diese Anweisung erstellt ein NUKIBridge Device mit Namen NBridge1 und der IP 192.168.0.23 sowie dem Token F34HK6.
      - Nach dem anlegen des Bridge Devices werden alle zur verfügung stehende Smartlock automatisch in FHEM an gelegt. -
    -

    - - Readings -
      -
    • bridgeAPI - API Version der Bridge
    • -
    • bridgeType - Hardware oder Software/App Bridge
    • -
    • currentTime - aktuelle Zeit auf der Bridge zum zeitpunkt des Info holens
    • -
    • firmwareVersion - aktuell auf der Bridge verwendete Firmwareversion
    • -
    • hardwareId - ID der Hardware Bridge
    • -
    • lastError - gibt die letzte HTTP Errormeldung wieder
    • -
    • serverConnected - true/false gibt an ob die Hardwarebridge Verbindung zur Nuki-Cloude hat.
    • -
    • serverId - gibt die ID des Cloudeservers wieder
    • -
    • uptime - Uptime der Bridge in Sekunden
    • -
    • wifiFirmwareVersion- Firmwareversion des Wifi Modules der Bridge
    • -
      - Die vorangestellte Zahl ist forlaufend und gibt beginnend bei 0 die Eigenschaften Eines Smartlocks wieder. -
    -

    - - Set -
      -
    • 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
    • -
      -
    -

    - - Get -
      -
    • callbackList - Gibt die Liste der eingetragenen Callback URL's wieder.
    • -
    • logFile - Zeigt das Logfile der Bridge an
    • -
      -
    -

    - - 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
    • -
      -
    -
- -=end html_DE - -=for :application/json;q=META.json 73_NUKIBridge.pm -{ - "abstract": "Modul to control the Nuki Smartlock's over the Nuki Bridge", - "x_lang": { - "de": { - "abstract": "Modul to control the Nuki Smartlock's over the Nuki Bridge" - } - }, - "keywords": [ - "fhem-mod-device", - "fhem-core", - "Smartlock", - "Nuki", - "Control" - ], - "release_status": "stable", - "license": "GPL_2", - "version": "v1.9.50", - "x_apiversion": "1.9", - "author": [ - "Marko Oldenburg " - ], - "x_fhem_maintainer": [ - "CoolTux" - ], - "x_fhem_maintainer_github": [ - "LeonGaultier" - ], - "prereqs": { - "runtime": { - "requires": { - "FHEM": 5.00918799, - "perl": 5.016, - "Meta": 0, - "JSON": 0, - "Date::Parse": 0 - }, - "recommends": { - }, - "suggests": { - } - } - } -} -=end :application/json;q=META.json - -=cut diff --git a/74_NUKIDevice.pm b/74_NUKIDevice.pm deleted file mode 100755 index ad99e94..0000000 --- a/74_NUKIDevice.pm +++ /dev/null @@ -1,800 +0,0 @@ -############################################################################### -# -# Developed with Kate -# -# (c) 2016-2021 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) -# 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; - -package FHEM::NUKIDevice; - -use strict; -use warnings; -use FHEM::Meta; -use GPUtils qw(GP_Import GP_Export); - -main::LoadModule('NUKIBridge'); - -# 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 { - - # 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; - } - } - } - } -} - -## Import der FHEM Funktionen -#-- Run before package compilation -BEGIN { - - # Import from main context - GP_Import( - qw( - readingsSingleUpdate - readingsBulkUpdate - readingsBeginUpdate - readingsEndUpdate - readingFnAttributes - makeDeviceName - defs - modules - Log3 - CommandAttr - AttrVal - IsDisabled - deviceEvents - init_done - InternalVal - ReadingsVal - AssignIoPort - IOWrite - data) - ); -} - -#-- Export to main context with different name -GP_Export( - qw( - Initialize - ) -); - -my %deviceTypes = ( - 0 => 'smartlock', - 2 => 'opener', - 4 => 'smartlockNG' -); - -my %modes = ( - 2 => { - 0 => 'door mode', - 2 => 'door mode' - }, - 3 => { - 0 => '-', - 2 => ' continuous mode' - } -); - -my %lockStates = ( - 0 => { - 0 => 'uncalibrated', - 2 => 'untrained', - 4 => 'uncalibrated' - }, - 1 => { - 0 => 'locked', - 2 => 'online', - 4 => 'locked' - }, - 2 => { - 0 => 'unlocking', - 2 => '-', - 4 => 'unlocking' - }, - 3 => { - 0 => 'unlocked', - 2 => 'rto active', - 4 => 'unlocked' - }, - 4 => { - 0 => 'locking', - 2 => '-', - 4 => 'locking' - }, - 5 => { - 0 => 'unlatched', - 2 => 'open', - 4 => 'unlatched' - }, - 6 => { - 0 => 'unlocked (lock ‘n’ go)', - 2 => '-', - 4 => 'unlocked (lock ‘n’ go)' - }, - 7 => { - 0 => 'unlatching', - 2 => 'opening', - 4 => 'unlatching' - }, - 253 => { - 0 => '-', - 2 => 'boot run', - 4 => '-' - }, - 254 => { - 0 => 'motor blocked', - 2 => '-', - 4 => 'motor blocked' - }, - 255 => { - 0 => 'undefined', - 2 => 'undefined', - 4 => 'undefined' - } -); - -my %deviceTypeIds = reverse(%deviceTypes); - -sub Initialize($) { - my ($hash) = @_; - - $hash->{Match} = '^{.*}$'; - - $hash->{SetFn} = 'FHEM::NUKIDevice::Set'; - $hash->{DefFn} = 'FHEM::NUKIDevice::Define'; - $hash->{UndefFn} = 'FHEM::NUKIDevice::Undef'; - $hash->{NotifyFn} = 'FHEM::NUKIDevice::Notify'; - $hash->{AttrFn} = 'FHEM::NUKIDevice::Attr'; - $hash->{ParseFn} = 'FHEM::NUKIDevice::Parse'; - - $hash->{AttrList} = - 'IODev ' - . 'model:opener,smartlock ' - . 'disable:1 ' - . $readingFnAttributes; - - return FHEM::Meta::InitMod( __FILE__, $hash ); -} - -sub Define($$) { - my ( $hash, $def ) = @_; - my @a = split( '[ \t][ \t]*', $def ); - - return $@ unless ( FHEM::Meta::SetInternals($hash) ); - use version 0.60; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); - - return 'too few parameters: define NUKIDevice ' - if ( @a != 4 ); - - 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->parse($VERSION)->normal; - $hash->{STATE} = 'Initialized'; - $hash->{NOTIFYDEV} = 'global,autocreate,' . $name; - - 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}; - - $hash->{BRIDGEAPI} = $defs{$iodev}->{BRIDGEAPI}; - - my $d = $modules{NUKIDevice}{defptr}{$nukiId}; - - return - 'NUKIDevice device ' - . $name - . ' on NUKIBridge ' - . $iodev - . ' already defined.' - if ( defined($d) - and $d->{IODev} == $hash->{IODev} - and $d->{NAME} ne $name ); - - 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' ); - - $modules{NUKIDevice}{defptr}{$nukiId} = $hash; - - GetUpdate($hash) - if ( ReadingsVal( $name, 'success', 'none' ) eq 'none' - and $init_done ); - - return undef; -} - -sub Undef($$) { - my ( $hash, $arg ) = @_; - - my $nukiId = $hash->{NUKIID}; - my $name = $hash->{NAME}; - - Log3( $name, 3, "NUKIDevice ($name) - undefined with NukiId: $nukiId" ); - delete( $modules{NUKIDevice}{defptr}{$nukiId} ); - - return undef; -} - -sub Attr(@) { - my ( $cmd, $name, $attrName, $attrVal ) = @_; - - my $hash = $defs{$name}; - my $token = $hash->{IODev}->{TOKEN}; - - 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 ( $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 ( $attrName eq 'model' ) { - if ( $cmd eq 'set' ) { - Log3( $name, 3, "NUKIDevice ($name) - change model" ); - $hash->{DEVICETYPE} = $deviceTypeIds{$attrVal}; - } - } - - return undef; -} - -sub Notify($$) { - - my ( $hash, $dev ) = @_; - my $name = $hash->{NAME}; - return if ( IsDisabled($name) ); - - my $devname = $dev->{NAME}; - my $devtype = $dev->{TYPE}; - my $events = deviceEvents( $dev, 1 ); - return if ( !$events ); - - GetUpdate($hash) - if ( - ( - grep /^INITIALIZED$/, - @{$events} - or grep /^REREADCFG$/, - @{$events} - or grep /^MODIFIED.$name$/, - @{$events} - or grep /^DEFINED.$name$/, - @{$events} - ) - and $devname eq 'global' - and $init_done - ); - - return; -} - -sub Set($$@) { - my ( $hash, $name, @aa ) = @_; - - my ( $cmd, @args ) = @aa; - my $lockAction; - - if ( lc($cmd) eq 'statusrequest' ) { - return ('usage: statusRequest') if ( @args != 0 ); - - GetUpdate($hash); - return undef; - } - 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; - - } - else { - my $list = ''; - $list = -'statusRequest:noArg unlock:noArg lock:noArg unlatch:noArg locknGo:noArg locknGoWithUnlatch:noArg unpair:noArg' - if ( $hash->{DEVICETYPE} == 0 - || $hash->{DEVICETYPE} == 4 ); - $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; - -# IOWrite( $hash, 'lockAction', $lockAction, $hash->{NUKIID}, -# $hash->{DEVICETYPE} ); - - IOWrite( $hash, 'lockAction', '{"param":"' . $lockAction - . '","nukiId":' . $hash->{NUKIID} . ',"deviceType":' . $hash->{DEVICETYPE} . '}' ); - - return undef; -} - -sub GetUpdate($) { - my $hash = shift; - - my $name = $hash->{NAME}; - - if ( !IsDisabled($name) ) { -# IOWrite( $hash, 'lockState', undef, $hash->{NUKIID}, -# $hash->{DEVICETYPE} ); - - IOWrite( $hash, 'lockState', '{"nukiId":' . $hash->{NUKIID} - . ',"deviceType":' . $hash->{DEVICETYPE} . '}' ); - - Log3( $name, 2, "NUKIDevice ($name) - GetUpdate Call IOWrite" ); - } - - return undef; -} - -sub Parse($$) { - my ( $hash, $json ) = @_; - - my $name = $hash->{NAME}; - - Log3( $name, 5, "NUKIDevice ($name) - Parse with result: $json" ); - ######################################### - ####### Errorhandling ############# - - if ( $json !~ m/^[\[{].*[}\]]$/ ) { - Log3( $name, 3, "NUKIDevice ($name) - invalid json detected: $json" ); - return "NUKIDevice ($name) - invalid json detected: $json"; - } - - ######################################### - #### 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) ne 'HASH' ) { - Log3( $name, 2, -"NUKIDevice ($name) - got wrong status message for $name: $decode_json" - ); - return undef; - } - - my $nukiId = $decode_json->{nukiId}; - - if ( my $hash = $modules{NUKIDevice}{defptr}{$nukiId} ) { - my $name = $hash->{NAME}; - - 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 $hash->{NAME}; - } - else { - Log3( $name, 4, - "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}"; - } - - Log3( $name, 5, "NUKIDevice ($name) - parse status message for $name" ); - - WriteReadings( $hash, $decode_json ); -} - -sub 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} ) - - IOWrite( $hash, 'lockState', '{"nukiId":' . $hash->{NUKIID} - . ',"deviceType":' . $hash->{DEVICETYPE} . '}' ) - 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} ); - IOWrite( $hash, 'lockState', '{"nukiId":' . $hash->{NUKIID} - . ',"deviceType":' . $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 -=item summary_DE Modul zur Steuerung des Nuki Smartlocks. - -=begin html - - -

NUKIDevice

-
    - NUKIDevice - Controls the Nuki 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> <Device-Type> -

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

      - Example: -

        - define Frontdoor NUKIDevice 1 NBridge1 0
        -
      -
      - This statement creates a NUKIDevice with the name Frontdoor, the NukiId 1 and the IODev device NBridge1.
      - After the device has been created, the current state of the Smartlock is automatically read from the bridge. -
    -

    - - Readings -
      -
    • state - Status of the Smartlock or error message if any error.
    • -
    • lockState - current lock status uncalibrated, locked, unlocked, unlocked (lock ‘n’ go), unlatched, locking, unlocking, unlatching, motor blocked, undefined.
    • -
    • name - name of the device
    • -
    • paired - paired information false/true
    • -
    • rssi - value of rssi
    • -
    • succes - true, false Returns the status of the last closing command. Ok or not Ok.
    • -
    • batteryCritical - Is the battery in a critical state? True, false
    • -
    • batteryState - battery status, ok / low
    • -
    -

    - - Set -
      -
    • statusRequest - retrieves the current state of the smartlock from the bridge.
    • -
    • lock - lock
    • -
    • unlock - unlock
    • -
    • unlatch - unlock / open Door
    • -
    • unpair - Removes the pairing with a given Smart Lock
    • -
    • locknGo - lock when gone
    • -
    • locknGoWithUnlatch - lock after the door has been opened
    • -
      -
    -

    - - Attributes -
      -
    • disable - disables the Nuki device
    • -
      -
    -
- -=end html -=begin html_DE - - -

NUKIDevice

-
    - NUKIDevice - Steuert das Nuki Smartlock -
    - 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> <Device-Type> -

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

      - Beispiel: -

        - define Haustür NUKIDevice 1 NBridge1 0
        -
      -
      - Diese Anweisung erstellt ein NUKIDevice mit Namen Haustür, der NukiId 1 sowie dem IODev Device NBridge1.
      - Nach dem anlegen des Devices wird automatisch der aktuelle Zustand des Smartlocks aus der Bridge gelesen. -
    -

    - - Readings -
      -
    • state - Status des Smartlock bzw. Fehlermeldung von Fehler vorhanden.
    • -
    • lockState - aktueller Schließstatus uncalibrated, locked, unlocked, unlocked (lock ‘n’ go), unlatched, locking, unlocking, unlatching, motor blocked, undefined.
    • -
    • name - Name des Smart Locks
    • -
    • paired - pairing Status des Smart Locks
    • -
    • rssi - rssi Wert des Smart Locks
    • -
    • succes - true, false Gibt des Status des letzten Schließbefehles wieder. Geklappt oder nicht geklappt.
    • -
    • batteryCritical - Ist die Batterie in einem kritischen Zustand? true, false
    • -
    • batteryState - Status der Batterie, ok/low
    • -
    -

    - - Set -
      -
    • statusRequest - ruft den aktuellen Status des Smartlocks von der Bridge ab.
    • -
    • lock - verschließen
    • -
    • unlock - aufschließen
    • -
    • unlatch - entriegeln/Falle öffnen.
    • -
    • unpair - entfernt das pairing mit dem Smart Lock
    • -
    • locknGo - verschließen wenn gegangen
    • -
    • locknGoWithUnlatch - verschließen nach dem die Falle geöffnet wurde.
    • -
      -
    -

    - - Attribute -
      -
    • disable - deaktiviert das Nuki Device
    • -
      -
    -
- -=end html_DE - -=for :application/json;q=META.json 74_NUKIDevice.pm -{ - "abstract": "Modul to control the Nuki Smartlock's over the Nuki Bridge", - "x_lang": { - "de": { - "abstract": "Modul to control the Nuki Smartlock's over the Nuki Bridge" - } - }, - "keywords": [ - "fhem-mod-device", - "fhem-core", - "Smartlock", - "Nuki", - "Control" - ], - "release_status": "stable", - "license": "GPL_2", - "version": "v1.9.50", - "author": [ - "Marko Oldenburg " - ], - "x_fhem_maintainer": [ - "CoolTux" - ], - "x_fhem_maintainer_github": [ - "LeonGaultier" - ], - "prereqs": { - "runtime": { - "requires": { - "FHEM": 5.00918799, - "perl": 5.016, - "Meta": 0, - "JSON": 0, - "Date::Parse": 0 - }, - "recommends": { - }, - "suggests": { - } - } - } -} -=end :application/json;q=META.json - -=cut diff --git a/Bridge-API-v1.10.pdf b/Bridge-API-v1.10.pdf deleted file mode 100644 index 3eb07cb..0000000 Binary files a/Bridge-API-v1.10.pdf and /dev/null differ diff --git a/Bridge-API-v1.9.pdf b/Bridge-API-v1.9.pdf deleted file mode 100644 index f84cb86..0000000 Binary files a/Bridge-API-v1.9.pdf and /dev/null differ diff --git a/FHEM/73_NUKIBridge.pm b/FHEM/73_NUKIBridge.pm new file mode 100644 index 0000000..2434332 --- /dev/null +++ b/FHEM/73_NUKIBridge.pm @@ -0,0 +1,273 @@ +############################################################################### +# +# Developed with Kate +# +# (c) 2016-2021 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) +# 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$ +# +############################################################################### + +################################# +######### Wichtige Hinweise und Links ################# + +## Beispiel für Logausgabe +# https://forum.fhem.de/index.php/topic,55756.msg508412.html#msg508412 + +## +# + +################################ +package FHEM::NUKIBridge; + +use strict; +use warnings; + +use FHEM::Meta; +require FHEM::Devices::Nuki::Bridge; + +sub ::NUKIBridge_Initialize { goto &Initialize } + +sub Initialize { + my ($hash) = @_; + + # Provider + $hash->{WriteFn} = \&FHEM::Devices::Nuki::Bridge::Write; + $hash->{Clients} = ':NUKIDevice:'; + $hash->{MatchList} = { '1:NUKIDevice' => '^{.*}$' }; + + my $webhookFWinstance = + join( ",", ::devspec2array('TYPE=FHEMWEB:FILTER=TEMPORARY!=1') ); + + # Consumer + $hash->{SetFn} = \&FHEM::Devices::Nuki::Bridge::Set; + $hash->{GetFn} = \&FHEM::Devices::Nuki::Bridge::Get; + $hash->{DefFn} = \&FHEM::Devices::Nuki::Bridge::Define; + $hash->{UndefFn} = \&FHEM::Devices::Nuki::Bridge::Undef; + $hash->{NotifyFn} = \&FHEM::Devices::Nuki::Bridge::Notify; + $hash->{AttrFn} = \&FHEM::Devices::Nuki::Bridge::Attr; + $hash->{AttrList} = + 'disable:1 ' . 'port ' + . 'webhookFWinstance:' + . $webhookFWinstance . ' ' + . 'webhookHttpHostname ' + . $::readingFnAttributes; + + return FHEM::Meta::InitMod( __FILE__, $hash ); +} + +1; + +=pod +=item device +=item summary Modul to control the Nuki Smartlock's over the Nuki Bridge. +=item summary_DE Modul zur Steuerung des Nuki Smartlock über die Nuki Bridge. + +=begin html + + +

NUKIBridge

+
    + NUKIBridge - controls the Nuki Smartlock over the Nuki Bridge +
    + The Nuki Bridge module connects FHEM to the Nuki Bridge and then reads all the smartlocks available on the bridge. Furthermore, the detected Smartlocks are automatically created as independent devices. +

    + + Define +

      + define <name> NUKIBridge <HOST> <API-TOKEN> +

      + Example: +

        + define NBridge1 NUKIBridge 192.168.0.23 F34HK6
        +
      +
      + This statement creates a NUKIBridge device with the name NBridge1 and the IP 192.168.0.23 as well as the token F34HK6.
      + After the bridge device is created, all available Smartlocks are automatically placed in FHEM. +
    +

    + + Readings +
      +
    • bridgeAPI - API Version of bridge
    • +
    • bridgeType - Hardware bridge / Software bridge
    • +
    • currentTime - Current timestamp
    • +
    • firmwareVersion - Version of the bridge firmware
    • +
    • hardwareId - Hardware ID
    • +
    • lastError - Last connected error
    • +
    • serverConnected - Flag indicating whether or not the bridge is connected to the Nuki server
    • +
    • serverId - Server ID
    • +
    • uptime - Uptime of the bridge in seconds
    • +
    • wifiFirmwareVersion- Version of the WiFi modules firmware
    • +
      + The preceding number is continuous, starts with 0 und returns the properties of one Smartlock. +
    +

    + + Set +
      +
    • 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)
    • +
    • fwUpdate - Immediately checks for a new firmware update and installs it (only hardwarebridge)
    • +
    • info - Returns all Smart Locks in range and some device information of the bridge itself
    • +
    • reboot - reboots the bridge (only hardwarebridge)
    • +
      +
    +

    + + Get +
      +
    • callbackList - List of register url callbacks.
    • +
    • logFile - Retrieves the log of the Bridge
    • +
      +
    +

    + + Attributes +
      +
    • disable - disables the Nuki Bridge
    • +
    • webhookFWinstance - Webinstanz of the Callback
    • +
    • webhookHttpHostname - IP or FQDN of the FHEM Server Callback
    • +
      +
    +
+ +=end html +=begin html_DE + + +

NUKIBridge

+
    + 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. +

    + + Define +

      + define <name> NUKIBridge <HOST> <API-TOKEN> +

      + Beispiel: +

        + define NBridge1 NUKIBridge 192.168.0.23 F34HK6
        +
      +
      + Diese Anweisung erstellt ein NUKIBridge Device mit Namen NBridge1 und der IP 192.168.0.23 sowie dem Token F34HK6.
      + Nach dem anlegen des Bridge Devices werden alle zur verfügung stehende Smartlock automatisch in FHEM an gelegt. +
    +

    + + Readings +
      +
    • bridgeAPI - API Version der Bridge
    • +
    • bridgeType - Hardware oder Software/App Bridge
    • +
    • currentTime - aktuelle Zeit auf der Bridge zum zeitpunkt des Info holens
    • +
    • firmwareVersion - aktuell auf der Bridge verwendete Firmwareversion
    • +
    • hardwareId - ID der Hardware Bridge
    • +
    • lastError - gibt die letzte HTTP Errormeldung wieder
    • +
    • serverConnected - true/false gibt an ob die Hardwarebridge Verbindung zur Nuki-Cloude hat.
    • +
    • serverId - gibt die ID des Cloudeservers wieder
    • +
    • uptime - Uptime der Bridge in Sekunden
    • +
    • wifiFirmwareVersion- Firmwareversion des Wifi Modules der Bridge
    • +
      + Die vorangestellte Zahl ist forlaufend und gibt beginnend bei 0 die Eigenschaften Eines Smartlocks wieder. +
    +

    + + Set +
      +
    • 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
    • +
      +
    +

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

    + + 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
    • +
      +
    +
+ +=end html_DE + +=for :application/json;q=META.json 73_NUKIBridge.pm +{ + "abstract": "Modul to control the Nuki Smartlock's over the Nuki Bridge", + "x_lang": { + "de": { + "abstract": "Modul to control the Nuki Smartlock's over the Nuki Bridge" + } + }, + "keywords": [ + "fhem-mod-device", + "fhem-core", + "Smartlock", + "Nuki", + "Control" + ], + "release_status": "stable", + "license": "GPL_2", + "version": "v2.0.0", + "x_apiversion": "1.12.3", + "author": [ + "Marko Oldenburg " + ], + "x_fhem_maintainer": [ + "CoolTux" + ], + "x_fhem_maintainer_github": [ + "LeonGaultier" + ], + "prereqs": { + "runtime": { + "requires": { + "FHEM": 5.00918799, + "perl": 5.016, + "Meta": 0, + "JSON": 0, + "Date::Parse": 0 + }, + "recommends": { + }, + "suggests": { + } + } + } +} +=end :application/json;q=META.json + +=cut diff --git a/FHEM/74_NUKIDevice.pm b/FHEM/74_NUKIDevice.pm new file mode 100644 index 0000000..4462195 --- /dev/null +++ b/FHEM/74_NUKIDevice.pm @@ -0,0 +1,235 @@ +############################################################################### +# +# Developed with Kate +# +# (c) 2016-2021 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) +# 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 FHEM::NUKIDevice; + +use strict; +use warnings; + +use FHEM::Meta; +require FHEM::Devices::Nuki::Device; + +main::LoadModule('NUKIBridge'); + +sub ::NUKIDevice_Initialize { goto &Initialize } + +sub Initialize($) { + my ($hash) = @_; + + $hash->{Match} = '^{.*}$'; + + $hash->{SetFn} = \&FHEM::Devices::Nuki::Device::Set; + $hash->{DefFn} = \&FHEM::Devices::Nuki::Device::Define; + $hash->{UndefFn} = \&FHEM::Devices::Nuki::Device::Undef; + $hash->{NotifyFn} = \&FHEM::Devices::Nuki::Device::Notify; + $hash->{AttrFn} = \&FHEM::Devices::Nuki::Device::Attr; + $hash->{ParseFn} = \&FHEM::Devices::Nuki::Device::Parse; + + $hash->{AttrList} = + 'IODev ' + . 'model:smartlock,opener,smartdoor,smartlock3 ' + . 'disable:1 ' + . $::readingFnAttributes; + + return FHEM::Meta::InitMod( __FILE__, $hash ); +} + +1; + +=pod +=item device +=item summary Modul to control the Nuki Smartlock's +=item summary_DE Modul zur Steuerung des Nuki Smartlocks. + +=begin html + + +

NUKIDevice

+
    + NUKIDevice - Controls the Nuki 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> <Device-Type> +

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

      + Example: +

        + define Frontdoor NUKIDevice 1 NBridge1 0
        +
      +
      + This statement creates a NUKIDevice with the name Frontdoor, the NukiId 1 and the IODev device NBridge1.
      + After the device has been created, the current state of the Smartlock is automatically read from the bridge. +
    +

    + + Readings +
      +
    • state - Status of the Smartlock or error message if any error.
    • +
    • lockState - current lock status uncalibrated, locked, unlocked, unlocked (lock ‘n’ go), unlatched, locking, unlocking, unlatching, motor blocked, undefined.
    • +
    • name - name of the device
    • +
    • paired - paired information false/true
    • +
    • rssi - value of rssi
    • +
    • succes - true, false Returns the status of the last closing command. Ok or not Ok.
    • +
    • batteryCritical - Is the battery in a critical state? True, false
    • +
    • batteryState - battery status, ok / low
    • +
    +

    + + Set +
      +
    • statusRequest - retrieves the current state of the smartlock from the bridge.
    • +
    • lock - lock
    • +
    • unlock - unlock
    • +
    • unlatch - unlock / open Door
    • +
    • unpair - Removes the pairing with a given Smart Lock
    • +
    • locknGo - lock when gone
    • +
    • locknGoWithUnlatch - lock after the door has been opened
    • +
      +
    +

    + + Attributes +
      +
    • disable - disables the Nuki device
    • +
      +
    +
+ +=end html +=begin html_DE + + +

NUKIDevice

+
    + NUKIDevice - Steuert das Nuki Smartlock +
    + 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> <Device-Type> +

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

      + Beispiel: +

        + define Haustür NUKIDevice 1 NBridge1 0
        +
      +
      + Diese Anweisung erstellt ein NUKIDevice mit Namen Haustür, der NukiId 1 sowie dem IODev Device NBridge1.
      + Nach dem anlegen des Devices wird automatisch der aktuelle Zustand des Smartlocks aus der Bridge gelesen. +
    +

    + + Readings +
      +
    • state - Status des Smartlock bzw. Fehlermeldung von Fehler vorhanden.
    • +
    • lockState - aktueller Schließstatus uncalibrated, locked, unlocked, unlocked (lock ‘n’ go), unlatched, locking, unlocking, unlatching, motor blocked, undefined.
    • +
    • name - Name des Smart Locks
    • +
    • paired - pairing Status des Smart Locks
    • +
    • rssi - rssi Wert des Smart Locks
    • +
    • succes - true, false Gibt des Status des letzten Schließbefehles wieder. Geklappt oder nicht geklappt.
    • +
    • batteryCritical - Ist die Batterie in einem kritischen Zustand? true, false
    • +
    • batteryState - Status der Batterie, ok/low
    • +
    +

    + + Set +
      +
    • statusRequest - ruft den aktuellen Status des Smartlocks von der Bridge ab.
    • +
    • lock - verschließen
    • +
    • unlock - aufschließen
    • +
    • unlatch - entriegeln/Falle öffnen.
    • +
    • unpair - entfernt das pairing mit dem Smart Lock
    • +
    • locknGo - verschließen wenn gegangen
    • +
    • locknGoWithUnlatch - verschließen nach dem die Falle geöffnet wurde.
    • +
      +
    +

    + + Attribute +
      +
    • disable - deaktiviert das Nuki Device
    • +
      +
    +
+ +=end html_DE + +=for :application/json;q=META.json 74_NUKIDevice.pm +{ + "abstract": "Modul to control the Nuki Smartlock's over the Nuki Bridge", + "x_lang": { + "de": { + "abstract": "Modul to control the Nuki Smartlock's over the Nuki Bridge" + } + }, + "keywords": [ + "fhem-mod-device", + "fhem-core", + "Smartlock", + "Nuki", + "Control" + ], + "release_status": "stable", + "license": "GPL_2", + "version": "v2.0.0", + "author": [ + "Marko Oldenburg " + ], + "x_fhem_maintainer": [ + "CoolTux" + ], + "x_fhem_maintainer_github": [ + "LeonGaultier" + ], + "prereqs": { + "runtime": { + "requires": { + "FHEM": 5.00918799, + "perl": 5.016, + "Meta": 0, + "JSON": 0, + "Date::Parse": 0 + }, + "recommends": { + }, + "suggests": { + } + } + } +} +=end :application/json;q=META.json + +=cut diff --git a/README.md b/README.md new file mode 100644 index 0000000..6346598 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# FHEM::NUKIBridge +# FHEM::NUKIDevice + +## Used branching model +* Master branch: Production version (copy of [fhem/fhem-mirror/blob/master/fhem/FHEM/73_NUKIBridge.pm](https://github.com/fhem/fhem-mirror/blob/master/fhem/FHEM/73_NUKIBridge.pm)) +* Master branch: Production version (copy of [fhem/fhem-mirror/blob/master/fhem/FHEM/74_NUKIBDevice.pm](https://github.com/fhem/fhem-mirror/blob/master/fhem/FHEM/74_NUKIDevice.pm +* Devel branch: Latest development version + +## Community support +The FHEM Forum is available [here](https://forum.fhem.de/) for general support. +In case you have a specific question about this module, it is recommended to find the right sub-forum. +It can either be found from the module info card using the FHEM Installer (e.g. using command `search `) or it can be determined from the [MAINTAINER.txt](https://github.com/fhem/fhem-mirror/blob/master/fhem/MAINTAINER.txt) file. + +## Bug reports and feature requests +Identified bugs and feature requests are tracked using [Github Issues](https://github.com/fhem/Nuki/issues). + +## Pull requests / How to participate into development +You are invited to send pull requests to the devel branch whenever you think you can contribute with some useful improvements to the module. The module maintainer will review you code and decide whether it is going to be part of the module in a future release. diff --git a/controls_NukiSmart.txt b/controls_NukiSmart.txt new file mode 100644 index 0000000..feb063f --- /dev/null +++ b/controls_NukiSmart.txt @@ -0,0 +1,4 @@ +UPD 2021-12-05_12:10:05 9217 FHEM/73_NUKIBridge.pm +UPD 2021-12-05_12:10:05 7569 FHEM/74_NUKIDevice.pm +UPD 2021-12-05_12:14:27 40875 lib/FHEM/Devices/Nuki/Bridge.pm +UPD 2021-12-05_12:10:05 15802 lib/FHEM/Devices/Nuki/Device.pm diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..4d23534 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,53 @@ +#!/usr/bin/perl -w + +use File::Basename; +use POSIX qw(strftime); +use strict; + +my @filenames = ( 'FHEM/73_AutoShuttersControl.pm', + 'lib/FHEM/Automation/ShuttersControl.pm', + 'lib/FHEM/Automation/ShuttersControl/Dev.pm', + 'lib/FHEM/Automation/ShuttersControl/Roommate.pm', + 'lib/FHEM/Automation/ShuttersControl/Shutters.pm', + 'lib/FHEM/Automation/ShuttersControl/Shading.pm', + 'lib/FHEM/Automation/ShuttersControl/EventProcessingFunctions.pm', + 'lib/FHEM/Automation/ShuttersControl/Helper.pm', + 'lib/FHEM/Automation/ShuttersControl/Window.pm', + 'lib/FHEM/Automation/ShuttersControl/Dev/Attr.pm', + 'lib/FHEM/Automation/ShuttersControl/Dev/Readings.pm', + 'lib/FHEM/Automation/ShuttersControl/Shutters/Attr.pm', + 'lib/FHEM/Automation/ShuttersControl/Shutters/Readings.pm', + 'lib/FHEM/Automation/ShuttersControl/Window/Attr.pm', + 'lib/FHEM/Automation/ShuttersControl/Window/Readings.pm', + 'lib/FHEM/Automation/ShuttersControl/Rainprotection.pm' + ); + +my $controlsfile = 'controls_AutoShuttersControl.txt'; + +open(FH, ">$controlsfile") || return("Can't open $controlsfile: $!"); + +for my $filename (@filenames) { + my @statOutput = stat($filename); + + if (scalar @statOutput != 13) { + printf 'error: stat has unexpected return value for ' . $filename . "\n"; + next; + } + + my $mtime = $statOutput[9]; + my $date = POSIX::strftime("%Y-%m-%d", localtime($mtime)); + my $time = POSIX::strftime("%H:%M:%S", localtime($mtime)); + my $filetime = $date."_".$time; + + my $filesize = $statOutput[7]; + + printf FH 'UPD ' . $filetime . ' ' . $filesize . ' ' .$filename . "\n"; +} + +close(FH); + +system("git add $controlsfile"); + +print 'Create controls File succesfully' . "\n"; + +exit 0; diff --git a/lib/FHEM/Devices/Nuki/Bridge.pm b/lib/FHEM/Devices/Nuki/Bridge.pm new file mode 100644 index 0000000..541a46d --- /dev/null +++ b/lib/FHEM/Devices/Nuki/Bridge.pm @@ -0,0 +1,1438 @@ +############################################################################### +# +# Developed with Kate +# +# (c) 2016-2021 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) +# 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$ +# +############################################################################### + +################################# +######### Wichtige Hinweise und Links ################# + +## Beispiel für Logausgabe +# https://forum.fhem.de/index.php/topic,55756.msg508412.html#msg508412 + +## +# + +################################ +package FHEM::Devices::Nuki::Bridge; + +use strict; +use warnings; + +use FHEM::Meta; +use HttpUtils; + +# 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 { + + # 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; + } + } + } + } +} + +######## Begin Bridge + +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 +); + +sub Define { + my $hash = shift; + my $def = shift // return; + + return $@ unless ( FHEM::Meta::SetInternals($hash) ); + use version 0.60; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); + + my ( $name, undef, $host, $token ) = split( m{\s+}xms, $def ); + + my $port = ::AttrVal( $name, 'port', 8080 ); + my $infix = 'NUKIBridge'; + $hash->{HOST} = $host // 'discover'; + $hash->{PORT} = $port; + $hash->{TOKEN} = $token // 'discover'; + $hash->{NOTIFYDEV} = 'global,' . $name; + $hash->{VERSION} = version->parse($VERSION)->normal; + $hash->{BRIDGEAPI} = FHEM::Meta::Get( $hash, 'x_apiversion' ); + $hash->{helper}->{actionQueue} = []; + $hash->{helper}->{iowrite} = 0; + + ::CommandAttr( undef, $name . ' room NUKI' ) + if ( ::AttrVal( $name, 'room', 'none' ) eq 'none' ); + + $hash->{WEBHOOK_REGISTER} = "unregistered"; + + ::readingsSingleUpdate( $hash, 'state', 'Initialized', 1 ); + + ::RemoveInternalTimer($hash); + + return BridgeDiscover( $hash, 'discover' ) + if ( $hash->{HOST} eq 'discover' + && $hash->{TOKEN} eq 'discover' ); + + ::Log3( $name, 3, +"NUKIBridge ($name) - defined with host $host on port $port, Token $token" + ); + + if ( + addExtension( + $name, + \&FHEM::Devices::Nuki::Bridge::CGI, + $infix . "-" . $host + ) + ) + { + $hash->{fhem}{infix} = $infix; + } + + $::modules{NUKIBridge}{defptr}{ $hash->{HOST} } = $hash; + + return; +} + +sub Undef { + my $hash = shift; + + my $host = $hash->{HOST}; + my $name = $hash->{NAME}; + + if ( defined( $hash->{fhem}{infix} ) ) { + removeExtension( $hash->{fhem}{infix} ); + } + + ::RemoveInternalTimer($hash); + delete $::modules{NUKIBridge}{defptr}{ $hash->{HOST} }; + + return; +} + +sub Attr { + my $cmd = shift; + my $name = shift; + my $attrName = shift; + my $attrVal = shift; + + my $hash = $::defs{$name}; + my $orig = $attrVal; + + if ( $attrName eq 'disable' ) { + if ( $cmd eq 'set' && $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" ); + } + } + + if ( $attrName eq 'port' ) { + if ( $cmd eq 'set' ) { + $hash->{PORT} = $attrVal; + ::Log3( $name, 3, "NUKIBridge ($name) - change bridge port" ); + } + elsif ( $cmd eq 'del' ) { + $hash->{PORT} = 8080; + ::Log3( $name, 3, + "NUKIBridge ($name) - set bridge port to default" ); + } + } + + 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 + && $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' && defined($attrVal) + ? $attrVal + : ::AttrVal( $name, 'webhookHttpHostname', '' ) + ); + + my $webhookFWinstance = ( + $attrName eq 'webhookFWinstance' && defined($attrVal) + ? $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 '' && $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" ); + + # Write( $hash, 'callback/add', $url, undef, undef ) + Write( $hash, 'callback/add', '{"param":"' . $url . '"}' ) + if ($::init_done); + $hash->{WEBHOOK_REGISTER} = 'sent'; + } + else { + $hash->{WEBHOOK_REGISTER} = 'incomplete_attributes'; + } + } + + return; +} + +sub Notify { + + my $hash = shift; + my $dev = shift // return; + my $name = $hash->{NAME}; + + return if ( ::IsDisabled($name) ); + + my $devname = $dev->{NAME}; + my $devtype = $dev->{TYPE}; + my $events = ::deviceEvents( $dev, 1 ); + return if ( !$events ); + + FirstRun($hash) + if ( + ( + grep /^INITIALIZED$/, + @{$events} + or grep /^REREADCFG$/, + @{$events} + or grep /^MODIFIED.$name$/, + @{$events} + or grep /^DEFINED.$name$/, + @{$events} + ) + && $hash->{HOST} ne 'discover' + && $hash->{TOKEN} ne 'discover' + && $devname eq 'global' + && $::init_done + ); + + return; +} + +sub addExtension { + my $name = shift; + my $func = shift; + my $link = shift; + + 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 removeExtension { + my $link = shift; + + my $url = '/' . $link; + my $name = $::data{FWEXT}{$url}{deviceName}; + + ::Log3( $name, 2, + "NUKIBridge ($name) - Unregistering NUKIBridge for webhook URL $url..." + ) if ( defined($name) ); + + delete $::data{FWEXT}{$url}; + + return; +} + +sub Set { + my $hash = shift; + my $name = shift; + my $cmd = shift // return "set $name needs at least one argument !"; + my $arg = shift // ''; + + my $endpoint; + my $param; + + if ( lc($cmd) eq 'getdevicelist' ) { + return 'usage: getDeviceList' if ($arg); + $endpoint = 'list'; + } + elsif ( $cmd eq 'info' ) { + $endpoint = 'info'; + } + elsif ( lc($cmd) eq 'fwupdate' ) { + $endpoint = 'fwupdate'; + } + elsif ( $cmd eq 'reboot' ) { + return 'usage: reboot' if ( defined($arg) ); + + $endpoint = 'reboot'; + } + elsif ( lc($cmd) eq 'clearlog' ) { + return 'usage: clearLog' if ( defined($arg) ); + + $endpoint = 'clearlog'; + } + elsif ( lc($cmd) eq 'factoryreset' ) { + return 'usage: clearLog' if ( defined($arg) ); + + $endpoint = 'factoryReset'; + } + elsif ( lc($cmd) eq 'callbackremove' ) { + return 'usage: callbackRemove' if ( split( m{\s+}xms, $arg ) > 1 ); + + my $id = ( defined($arg) ? $arg : 0 ); + $endpoint = 'callback/remove'; + $param = '{"param":"' . $id . '"}'; + } + elsif ( lc($cmd) eq 'configauth' ) { + return 'usage: configAuth' if ( split( m{\s+}xms, $arg ) > 1 ); + + my $configAuth = 'enable=' . ( $arg eq 'enable' ? 1 : 0 ); + $endpoint = 'configAuth'; + $param = '{"param":"' . $configAuth . '"}'; + } + else { + my $list = ''; + $list .= 'info:noArg getDeviceList:noArg '; + $list .= +'clearLog:noArg fwUpdate:noArg reboot:noArg factoryReset:noArg configAuth:enable,disable' + if ( ::ReadingsVal( $name, 'bridgeType', 'Software' ) eq 'Hardware' ); + return ( 'Unknown argument ' . $cmd . ', choose one of ' . $list ); + } + + Write( $hash, $endpoint, $param ) + if ( !::IsDisabled($name) ); + + return; +} + +sub Get { + my $hash = shift; + my $name = shift; + my $cmd = shift // return "set $name needs at least one argument !"; + my $arg = shift; + + my $endpoint; + + if ( lc($cmd) eq 'logfile' ) { + return 'usage: logFile' if ( defined($arg) ); + + $endpoint = 'log'; + } + elsif ( lc($cmd) eq 'callbacklist' ) { + return 'usage: callbackList' if ( defined($arg) ); + + $endpoint = 'callback/list'; + } + else { + my $list = ''; + $list .= 'callbackList:noArg '; + $list .= 'logFile:noArg' + if ( ::ReadingsVal( $name, 'bridgeType', 'Software' ) eq 'Hardware' ); + + return 'Unknown argument ' . $cmd . ', choose one of ' . $list; + } + + return Write( $hash, $endpoint, undef ); +} + +sub GetCheckBridgeAlive { + my $hash = shift; + my $name = $hash->{NAME}; + + ::RemoveInternalTimer($hash); + ::Log3( $name, 4, "NUKIBridge ($name) - GetCheckBridgeAlive" ); + + if ( !::IsDisabled($name) + && $hash->{helper}->{iowrite} == 0 ) + { + Write( $hash, 'list', undef ); + ::Log3( $name, 4, "NUKIBridge ($name) - run Write" ); + } + + ::Log3( $name, 4, + "NUKIBridge ($name) - Call InternalTimer for GetCheckBridgeAlive" ); + + return ::InternalTimer( ::gettimeofday() + 30, + \&FHEM::Devices::Nuki::Bridge::GetCheckBridgeAlive, $hash ); +} + +sub FirstRun { + my $hash = shift; + my $name = $hash->{NAME}; + + ::RemoveInternalTimer($hash); + Write( $hash, 'list', undef ) + if ( !::IsDisabled($name) ); + + ::readingsSingleUpdate( $hash, 'configAuthSuccess', 'unknown', 0 ) + if ( ::ReadingsVal( $name, 'configAuthSuccess', 'none' ) eq 'none' ); + + return ::InternalTimer( ::gettimeofday() + 5, + \&FHEM::Devices::Nuki::Bridge::GetCheckBridgeAlive, $hash ); +} + +sub Write { + my $hash = shift; + my $endpoint = shift // return; + my $json = shift; + + my $decode_json = eval { decode_json($json) } + if ( defined($json) ); + + my $nukiId = $decode_json->{nukiId} // undef; + my $deviceType = $decode_json->{deviceType} // undef; + my $param = $decode_json->{param} // undef; + + my $obj = { + endpoint => $endpoint, + param => $param, + nukiId => $nukiId, + deviceType => $deviceType + }; + + $hash->{helper}->{lastDeviceAction} = $obj + if ( + ( defined($param) && $param ) + || ( defined($nukiId) + && $nukiId ) + ); + + unshift( @{ $hash->{helper}->{actionQueue} }, $obj ); + + return BridgeCall($hash); +} + +sub CreateUri { + my $hash = shift; + my $obj = shift; + + my $name = $hash->{NAME}; + 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) ); + + if ( defined($param) + && defined($deviceType) ) + { + $uri .= '&action=' + . $lockActionsSmartLock{$param} + if ( + $endpoint ne 'callback/add' + && ( $deviceType == 0 + || $deviceType == 4 ) + ); + + $uri .= '&action=' . $lockActionsOpener{$param} + if ( $endpoint ne 'callback/add' + && $deviceType == 2 ); + } + + $uri .= '&' . $param + if ( defined($param) + && $endpoint eq 'configAuth' ); + + $uri .= '&id=' . $param + if ( defined($param) + && $endpoint eq 'callback/remove' ); + + $uri .= '&url=' . $param + if ( defined($param) + && $endpoint eq 'callback/add' ); + + $uri .= '&nukiId=' . $nukiId + if ( defined($nukiId) ); + $uri .= '&deviceType=' . $deviceType + if ( defined($deviceType) ); + + ::Log3( $name, 4, "NUKIBridge ($name) - created uri: $uri" ); + + return $uri; +} + +sub BridgeCall { + 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 = CreateUri( $hash, $obj ); + + if ( defined($uri) && $uri ) { + $hash->{helper}->{iowrite} = 1; + + my $param = { + url => $uri, + timeout => 30, + hash => $hash, + nukiId => $nukiId, + endpoint => $endpoint, + header => 'Accept: application/json', + method => 'GET', + callback => \&Distribution, + }; + + $param->{cl} = $hash->{CL} + if ( ( $endpoint eq 'callback/list' || $endpoint eq 'log' ) + && ref( $hash->{CL} ) eq 'HASH' ); + + ::HttpUtils_NonblockingGet($param); + ::Log3( $name, 4, + "NUKIBridge ($name) - Send HTTP POST with URL $uri" ); + } + } + else { + push( @{ $hash->{helper}->{actionQueue} }, $obj ) + if ( defined($endpoint) + && $endpoint eq 'lockAction' ); + } + + return; +} + +sub Distribution { + my $param = shift; + my $err = shift; + my $json = shift; + + 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'} } + if ( defined( $param->{'nukiId'} ) ); + + my $dname = $dhash->{NAME}; + + ::Log3( $name, 4, "NUKIBridge ($name) - Response JSON: $json" ); + ::Log3( $name, 4, "NUKIBridge ($name) - Response ERROR: $err" ); + ::Log3( $name, 4, "NUKIBridge ($name) - Response CODE: $param->{code}" ) + if ( defined( $param->{code} ) + && $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' ); + ::Log3( $name, 5, "NUKIBridge ($name) - Bridge ist offline" ); + } + + ::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 ); + + ::asyncOutput( $param->{cl}, "Request Error: $err\r\n" ) + if ( $param->{cl} && $param->{cl}{canAsyncOutput} ); + + return $err; + } + } + + if ( ( $json eq '' || $json =~ /Unavailable/i ) + && exists( $param->{code} ) + && $param->{code} != 200 ) + { + + if ( $param->{code} == 503 + && $json eq 'HTTP 503 Unavailable' ) + { + ::Log3( $name, 4, +"NUKIBridge ($name) - Response from Bridge: $param->{code}, $json" + ); + + ::readingsEndUpdate( $hash, 1 ); + + if ( defined( $hash->{helper}->{lastDeviceAction} ) + && $hash->{helper}->{lastDeviceAction} ) + { + push( + @{ $hash->{helper}->{actionQueue} }, + $hash->{helper}->{lastDeviceAction} + ); + + ::InternalTimer( ::gettimeofday() + 1, + \&FHEM::Devices::Nuki::Bridge::BridgeCall, $hash ); + } + + ::asyncOutput( $param->{cl}, "Request Error: $err\r\n" ) + if ( $param->{cl} && $param->{cl}{canAsyncOutput} ); + + return; + } + + ::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 ); + + ::asyncOutput( $param->{cl}, "Request Error: $err\r\n" ) + if ( $param->{cl} && $param->{cl}{canAsyncOutput} ); + + return ('received http code ' + . $param->{code} + . ' without any data after requesting' ); + } + + if ( ( $json =~ /Error/i ) + && exists( $param->{code} ) ) + { + + ::readingsBulkUpdate( $hash, 'lastError', 'invalid API token' ) + if ( $param->{code} == 401 ); + ::readingsBulkUpdate( $hash, 'lastError', 'action is undefined' ) + if ( $param->{code} == 400 && $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 && $hash == $dhash ); + + ::readingsEndUpdate( $hash, 1 ); + + ::asyncOutput( $param->{cl}, "Request Error: $err\r\n" ) + if ( $param->{cl} && $param->{cl}{canAsyncOutput} ); + + return $param->{code}; + } + + delete $hash->{helper}->{lastDeviceAction} + if ( defined( $hash->{helper}->{lastDeviceAction} ) + && $hash->{helper}->{lastDeviceAction} ); + + ::readingsEndUpdate( $hash, 1 ); + + ::readingsSingleUpdate( $hash, 'state', 'connected', 1 ); + ::Log3( $name, 5, "NUKIBridge ($name) - Bridge ist online" ); + + if ( $param->{endpoint} eq 'callback/list' ) { + getCallbackList( $param, $json ); + return; + } + elsif ( $param->{endpoint} eq 'log' ) { + getLogfile( $param, $json ); + return; + } + + if ( $hash == $dhash ) { + 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() + 3, + \&FHEM::Devices::Nuki::Bridge::BridgeCall, $hash ) + if ( defined( $hash->{helper}->{actionQueue} ) + && scalar( @{ $hash->{helper}->{actionQueue} } ) > 0 ); + + return; +} + +sub ResponseProcessing { + my $hash = shift; + my $json = shift; + my $endpoint = shift; + + my $name = $hash->{NAME}; + my $decode_json; + + if ( !$json ) { + ::Log3( $name, 3, "NUKIBridge ($name) - empty answer received" ); + return; + } + elsif ( $json =~ m'HTTP/1.1 200 OK' ) { + ::Log3( $name, 4, "NUKIBridge ($name) - empty answer received" ); + return; + } + 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: $@" ); + return; + } + + if ( $endpoint eq 'list' + || $endpoint eq 'info' ) + { + if ( + ( + ref($decode_json) eq 'ARRAY' + && scalar( @{$decode_json} ) > 0 + && $endpoint eq 'list' + ) + || ( ref($decode_json) eq 'HASH' + && ref( $decode_json->{scanResults} ) eq 'ARRAY' + && scalar( @{ $decode_json->{scanResults} } ) > 0 + && $endpoint eq 'info' ) + ) + { + my @buffer; + @buffer = split( '\[', $json ) + if ( $endpoint eq 'list' ); + @buffer = split( '"scanResults": \[', $json ) + if ( $endpoint eq 'info' ); + + my ( $json, $tail ) = 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 ) + if ( defined($tail) + && $tail ); + + ( $json, $tail ) = ParseJSON( $hash, $tail ); + + ::Log3( $name, 5, + "NUKIBridge ($name) - Nach Sub: Laenge JSON: " + . length($json) + . " Content: " + . $json + . " Tail: " + . $tail ); + } + } + + WriteReadings( $hash, $decode_json, $endpoint ) + if ( $endpoint eq 'info' ); + + return; + } + elsif ( $endpoint eq 'configAuth' ) { + WriteReadings( $hash, $decode_json, $endpoint ); + } + else { + + return ::Log3( $name, 5, + "NUKIBridge ($name) - Rückgabe Path nicht korrekt: $json" ); + } +} + +sub CGI() { + my $request = shift; + + my $hash; + my $name; + + while ( my ( $key, $value ) = each %{ $::modules{NUKIBridge}{defptr} } ) { + $hash = $::modules{NUKIBridge}{defptr}{$key}; + $name = $hash->{NAME}; + } + + return ('NUKIBridge WEBHOOK - No IODev found') + if ( !defined($hash) + && !defined($name) ); + + my $json = ( split( '&', $request, 2 ) )[1]; + + if ( !$json ) { + ::Log3( $name, 3, + "NUKIBridge WEBHOOK ($name) - empty message received" ); + return; + } + elsif ( $json =~ m'HTTP/1.1 200 OK' ) { + ::Log3( $name, 4, + "NUKIBridge WEBHOOK ($name) - empty answer received" ); + return; + } + elsif ( $json !~ m/^[\[{].*[}\]]$/ ) { + ::Log3( $name, 3, + "NUKIBridge WEBHOOK ($name) - invalid json detected: $json" ); + return ("NUKIBridge WEBHOOK ($name) - invalid json detected: $json"); + } + + ::Log3( $name, 5, + "NUKIBridge WEBHOOK ($name) - Webhook received with JSON: $json" ); + + if ( $json =~ m/^\{.*\}$/ ) { + $hash->{WEBHOOK_COUNTER}++; + $hash->{WEBHOOK_LAST} = ::TimeNow(); + + ::Log3( $name, 3, +"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 WriteReadings { + my $hash = shift; + my $decode_json = shift; + my $endpoint = shift; + + my $name = $hash->{NAME}; + + my $nukiId; + my $scanResults; + my %response_hash; + my $dname; + my $dhash; + + ::readingsBeginUpdate($hash); + + if ( $endpoint eq 'configAuth' ) { + ::readingsBulkUpdate( $hash, 'configAuthSuccess', + $decode_json->{success} ); + } + else { + ::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, 'currentGMTime', + $decode_json->{currentTime} ); + ::readingsBulkUpdate( $hash, 'serverConnected', + $decode_json->{serverConnected} ); + ::readingsBulkUpdate( $hash, 'wlanConnected', + $decode_json->{wlanConnected} ); + } + + ::readingsEndUpdate( $hash, 1 ); + return; +} + +sub getLogfile { + my $param = shift; + my $json = shift; + + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + my $decode_json = eval { decode_json($json) }; + if ($@) { + ::Log3( $name, 3, "NUKIBridge ($name) - JSON error while request: $@" ); + return; + } + + ::Log3( $name, 4, + "NUKIBridge ($name) - Log data are collected and processed" ); + + if ( $param->{cl} + && $param->{cl}->{TYPE} eq 'FHEMWEB' ) + { + + if ( ref($decode_json) eq 'ARRAY' + && scalar( @{$decode_json} ) > 0 ) + { + ::Log3( $name, 4, + "NUKIBridge ($name) - created Table with log file" ); + + my $header = '' . '
Log List
'; + + my $ret = $header . ''; + $ret .= '
'; + $ret .= ''; + + for my $logs ( @{$decode_json} ) { + $ret .= ''; + + if ( $logs->{timestamp} ) { + $ret .= ''; + $ret .= ''; + $ret .= ''; + } + + if ( $logs->{type} ) { + $ret .= ''; + $ret .= ''; + $ret .= ''; + } + + for my $d ( reverse sort keys %{$logs} ) { + next if ( $d eq 'type' ); + next if ( $d eq 'timestamp' ); + + $ret .= ''; + $ret .= ''; + $ret .= ''; + } + + $ret .= ''; + } + + $ret .= '
timestamp: ' . $logs->{timestamp} . ' type: ' . $logs->{type} . ' ' . $d . ': ' . $logs->{$d} . '
'; + + ::asyncOutput( $param->{cl}, $ret ) + if ( $param->{cl} + && $param->{cl}{canAsyncOutput} ); + } + } + + return; +} + +sub getCallbackList { + my $param = shift; + my $json = shift; + + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + my $decode_json = eval { decode_json($json) }; + if ($@) { + ::Log3( $name, 3, "NUKIBridge ($name) - JSON error while request: $@" ); + return; + } + + ::Log3( $name, 4, + "NUKIBridge ($name) - Callback data are collected and processed" ); + + if ( $param->{cl} + && $param->{cl}->{TYPE} eq 'FHEMWEB' ) + { + + if ( ref( $decode_json->{callbacks} ) eq 'ARRAY' ) { + ::Log3( $name, 4, + "NUKIBridge ($name) - created Table with log file" ); + + my $space = ' '; + my $aHref; + my $header = + '' . '
Callback List
'; + + my $ret = $header . ''; + $ret .= '
'; + $ret .= ''; + $ret .= ''; + $ret .= ''; + $ret .= ''; + $ret .= ''; + + if ( scalar( @{ $decode_json->{callbacks} } ) > 0 ) { + for my $cb ( @{ $decode_json->{callbacks} } ) { + $aHref = "{host} + . "/fhem?cmd=set+" + . $name + . "+callbackRemove+" + . $cb->{id} + . $::FW_CSRF + . "\">X"; + + $ret .= ''; + $ret .= ''; + $ret .= ''; + } + } + else { + $ret .= ''; + $ret .= ''; + $ret .= ''; + $ret .= ''; + } + + $ret .= '
URLRemove
' . $cb->{url} . '' . $aHref . '
nonenone
'; + + ::asyncOutput( $param->{cl}, $ret ) + if ( $param->{cl} + && $param->{cl}{canAsyncOutput} ); + } + } + + return; +} + +sub getCallbackList2 { + my $param = shift; + my $json = shift; + + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + my $decode_json = eval { decode_json($json) }; + if ($@) { + ::Log3( $name, 3, "NUKIBridge ($name) - JSON error while request: $@" ); + return; + } + + ::Log3( $name, 4, + "NUKIBridge ($name) - Callback data are collected and processed" ); + + if ( $param->{cl} + && $param->{cl}->{TYPE} eq 'FHEMWEB' ) + { + + if ( ref( $decode_json->{callbacks} ) eq 'ARRAY' ) { + ::Log3( $name, 4, + "NUKIBridge ($name) - created Table with Callback List" ); + + my $j1 = + ''; + + # FW_cmd(FW_root+"?cmd="+type+" "+dev+ + # (params[0]=="state" ? "":" "+params[0])+" "+arg+"&XHR=1"); + + my $header = ''; + my $footer = ''; + + my $ret = + '
Callback List
' + . ''; + $ret .= '
' + . '' + . '' + . '' + . '' . ''; + + if ( scalar( @{ $decode_json->{callbacks} } ) > 0 ) { + for my $cb ( @{ $decode_json->{callbacks} } ) { + $ret .= ''; + $ret .= +""; + $ret .= ''; + } + } + else { + $ret .= ''; + $ret .= ''; + $ret .= ''; + $ret .= ''; + } + + $ret .= '
URLRemove
' . $cb->{url} . '
nonenone
'; + + ::Log3( $name, 4, +"NUKIBridge ($name) - Callback List Table created and call asyncOutput Fn" + ); + + ::asyncOutput( $param->{cl}, $header . $ret . $j1 . $footer ) + if ( $param->{cl} + && $param->{cl}{canAsyncOutput} ); + } + } + + return; +} + +sub ParseJSON { + my $hash = shift; + my $buffer = shift; + + my $name = $hash->{NAME}; + my $open = 0; + my $close = 0; + my $msg = ''; + my $tail = ''; + + if ($buffer) { + for my $c ( split //, $buffer ) { + + if ( $open == $close + && $open > 0 ) + { + $tail .= $c; + ::Log3( $name, 5, + "NUKIBridge ($name) - $open == $close and $open > 0" ); + + } + elsif ($open == $close + && $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 ); +} + +sub BridgeDiscover { + my $hash = shift; + my $endpoint = shift; + my $bridge = shift; + my $name = $hash->{NAME}; + my $url = ( + $endpoint eq 'discover' && !defined($bridge) + ? 'https://api.nuki.io/discover/bridges' + : 'http://' . $bridge->{'ip'} . ':' . $bridge->{'port'} . '/auth' + ); + my $timeout = ( + $endpoint eq 'discover' && !defined($bridge) + ? 5 + : 35 + ); + + if ( $endpoint eq 'discover' ) { + ::Log3( $name, 3, + "NUKIBridge ($name) - Bridge device defined. run discover mode" ); + + ::readingsSingleUpdate( $hash, 'state', 'run discovery', 1 ); + } + elsif ( $endpoint eq 'getApiToken' ) { + + ::Log3( $name, 3, +"NUKIBridge ($name) - Enables the api (if not yet enabled) and get the api token." + ); + } + + ::HttpUtils_NonblockingGet( + { + url => $url, + timeout => $timeout, + hash => $hash, + header => 'Accept: application/json', + endpoint => $endpoint, + host => $bridge->{'ip'}, + port => $bridge->{'port'}, + method => 'GET', + callback => \&BridgeDiscoverRequest, + } + ); + + ::Log3( $name, 3, + "NUKIBridge ($name) - Send Discover request to Nuki Cloud" ) + if ( $endpoint eq 'discover' ); + + ::Log3( $name, 3, "NUKIBridge ($name) - get API Token from the Bridge" ) + if ( $endpoint eq 'getApiToken' ); + + return; +} + +sub BridgeDiscoverRequest { + my $param = shift; + my $err = shift; + my $json = shift; + + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + if ( defined($err) + && $err ne '' ) + { + return ::Log3( $name, 3, "NUKIBridge ($name) - Error: $err" ); + } + elsif ( exists( $param->{code} ) + && $param->{code} != 200 ) + { + return ::Log3( $name, 3, + "NUKIBridge ($name) - HTTP error Code present. Code: $param->{code}" + ); + } + + my $decode_json; + $decode_json = eval { decode_json($json) }; + if ($@) { + ::Log3( $name, 3, "NUKIBridge ($name) - JSON error while request: $@" ); + return; + } + + if ( $param->{endpoint} eq 'discover' ) { + + return ::readingsSingleUpdate( $hash, 'state', 'no bridges discovered', + 1 ) + if ( scalar( @{ $decode_json->{bridges} } ) == 0 + && $decode_json->{errorCode} == 0 ); + + return BridgeDiscover_getAPIToken( $hash, $decode_json ); + } + elsif ( $param->{endpoint} eq 'getApiToken' ) { + ::readingsSingleUpdate( $hash, 'state', + 'modefined bridge device in progress', 1 ); + + $decode_json->{host} = $param->{host}; + $decode_json->{port} = $param->{port}; + + return ModefinedBridgeDevices( $hash, $decode_json ) + if ( $decode_json->{success} == 1 ); + + return ::readingsSingleUpdate( $hash, 'state', 'get api token failed', + 1 ); + } + + return; +} + +sub BridgeDiscover_getAPIToken { + my $hash = shift; + my $decode_json = shift; + my $name = $hash->{NAME}; + + my $pullApiKeyMessage = + 'When issuing this API-call the bridge turns on its LED for 30 seconds. +The button of the bridge has to be pressed within this timeframe. Otherwise the bridge returns a negative success and no token.'; + + ::readingsSingleUpdate( $hash, 'state', $pullApiKeyMessage, 1 ); + + for ( @{ $decode_json->{bridges} } ) { + + BridgeDiscover( $hash, 'getApiToken', $_ ); + } + + return; +} + +sub ModefinedBridgeDevices { + my $hash = shift; + my $decode_json = shift; + my $name = $hash->{NAME}; + + ::CommandAttr( undef, $name . ' port ' . $decode_json->{port} ) + if ( $decode_json->{port} != 8080 ); + ::CommandDefMod( undef, + $name + . ' NUKIBridge ' + . $decode_json->{host} . ' ' + . $decode_json->{token} ); + + return; +} + +1; diff --git a/lib/FHEM/Devices/Nuki/Device.pm b/lib/FHEM/Devices/Nuki/Device.pm new file mode 100644 index 0000000..1a20598 --- /dev/null +++ b/lib/FHEM/Devices/Nuki/Device.pm @@ -0,0 +1,598 @@ +############################################################################### +# +# Developed with Kate +# +# (c) 2016-2021 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) +# 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 FHEM::Devices::Nuki::Device; + +use strict; +use warnings; + +use FHEM::Meta; + +# 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 { + + # 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; + } + } + } + } +} + +######## Begin Device + +my %deviceTypes = ( + 0 => 'smartlock', + 2 => 'opener', + 3 => 'smartdoor', + 4 => 'smartlock3' +); + +my %deviceTypeIds = reverse(%deviceTypes); + +my %modes = ( + 2 => { + 0 => 'door mode', + 2 => 'door mode' + }, + 3 => { + 0 => '-', + 2 => ' continuous mode' + } +); + +my %lockStates = ( + 0 => { + 0 => 'uncalibrated', + 2 => 'untrained', + 4 => 'uncalibrated' + }, + 1 => { + 0 => 'locked', + 2 => 'online', + 4 => 'locked' + }, + 2 => { + 0 => 'unlocking', + 2 => '-', + 4 => 'unlocking' + }, + 3 => { + 0 => 'unlocked', + 2 => 'rto active', + 4 => 'unlocked' + }, + 4 => { + 0 => 'locking', + 2 => '-', + 4 => 'locking' + }, + 5 => { + 0 => 'unlatched', + 2 => 'open', + 4 => 'unlatched' + }, + 6 => { + 0 => 'unlocked (lock ‘n’ go)', + 2 => '-', + 4 => 'unlocked (lock ‘n’ go)' + }, + 7 => { + 0 => 'unlatching', + 2 => 'opening', + 4 => 'unlatching' + }, + 253 => { + 0 => '-', + 2 => 'boot run', + 4 => '-' + }, + 254 => { + 0 => 'motor blocked', + 2 => '-', + 4 => 'motor blocked' + }, + 255 => { + 0 => 'undefined', + 2 => 'undefined', + 4 => 'undefined' + } +); + +my %doorsensorStates = ( + 1 => 'deactivated', + 2 => 'door closed', + 3 => 'door opened', + 4 => 'door state unknown', + 5 => 'calibrating' +); + +sub Define { + my $hash = shift; + my $def = shift // return; + + return $@ unless ( FHEM::Meta::SetInternals($hash) ); + use version 0.60; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); + + my ( $name, undef, $nukiId, $deviceType ) = split( m{\s+}xms, $def ); + return 'too few parameters: define NUKIDevice ' + if ( !defined($nukiId) + || !defined($name) ); + + $deviceType = + defined($deviceType) + ? $deviceType + : 0; + + $hash->{NUKIID} = $nukiId; + $hash->{DEVICETYPEID} = $deviceType; + $hash->{VERSION} = version->parse($VERSION)->normal; + $hash->{STATE} = 'Initialized'; + $hash->{NOTIFYDEV} = 'global,autocreate,' . $name; + + 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}; + + $hash->{BRIDGEAPI} = $::defs{$iodev}->{BRIDGEAPI} + if ( defined($iodev) + && $iodev ); + + my $d = $::modules{NUKIDevice}{defptr}{$nukiId}; + + return + 'NUKIDevice device ' + . $name + . ' on NUKIBridge ' + . $iodev + . ' already defined.' + if ( defined($d) + && $d->{IODev} == $hash->{IODev} + && $d->{NAME} ne $name ); + + ::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' ); + + $::modules{NUKIDevice}{defptr}{$nukiId} = $hash; + + GetUpdate($hash) + if ( ::ReadingsVal( $name, 'success', 'none' ) eq 'none' + && $::init_done ); + + return; +} + +sub Undef { + my $hash = shift; + + my $nukiId = $hash->{NUKIID}; + my $name = $hash->{NAME}; + + ::Log3( $name, 3, "NUKIDevice ($name) - undefined with NukiId: $nukiId" ); + delete( $::modules{NUKIDevice}{defptr}{$nukiId} ); + + return; +} + +sub Attr { + my $cmd = shift; + my $name = shift; + my $attrName = shift; + my $attrVal = shift; + + my $hash = $::defs{$name}; + my $token = $hash->{IODev}->{TOKEN}; + + if ( $attrName eq 'disable' ) { + if ( $cmd eq 'set' && $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 ( $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 ( $attrName eq 'model' ) { + if ( $cmd eq 'set' ) { + ::Log3( $name, 3, "NUKIDevice ($name) - change model" ); + $hash->{DEVICETYPEID} = $deviceTypeIds{$attrVal}; + } + } + + return; +} + +sub Notify { + my $hash = shift; + my $dev = shift // return; + + my $name = $hash->{NAME}; + return if ( ::IsDisabled($name) ); + + my $devname = $dev->{NAME}; + my $devtype = $dev->{TYPE}; + my $events = ::deviceEvents( $dev, 1 ); + + return if ( !$events ); + + GetUpdate($hash) + if ( + ( + grep /^INITIALIZED$/, + @{$events} + or grep /^REREADCFG$/, + @{$events} + or grep /^MODIFIED.$name$/, + @{$events} + or grep /^DEFINED.$name$/, + @{$events} + ) + && $devname eq 'global' + && $::init_done + ); + + return; +} + +sub Set { + my $hash = shift; + my $name = shift; + my $cmd = shift // return "set $name needs at least one argument !"; + + my $lockAction; + + if ( lc($cmd) eq 'statusrequest' ) { + + GetUpdate($hash); + return; + } + elsif ($cmd eq 'lock' + || lc($cmd) eq 'deactivaterto' + || $cmd eq 'unlock' + || lc($cmd) eq 'activaterto' + || $cmd eq 'unlatch' + || lc($cmd) eq 'electricstrikeactuation' + || lc($cmd) eq 'lockngo' + || lc($cmd) eq 'activatecontinuousmode' + || lc($cmd) eq 'lockngowithunlatch' + || lc($cmd) eq 'deactivatecontinuousmode' + || $cmd eq 'unpair' ) + { + $lockAction = $cmd; + } + else { + my $list = ''; + $list = +'statusRequest:noArg unlock:noArg lock:noArg unlatch:noArg locknGo:noArg locknGoWithUnlatch:noArg unpair:noArg' + if ( $hash->{DEVICETYPEID} == 0 + || $hash->{DEVICETYPEID} == 4 ); + $list = +'statusRequest:noArg activateRto:noArg deactivateRto:noArg electricStrikeActuation:noArg activateContinuousMode:noArg deactivateContinuousMode:noArg unpair:noArg' + if ( $hash->{DEVICETYPEID} == 2 ); + + return ( 'Unknown argument ' . $cmd . ', choose one of ' . $list ); + } + + $hash->{helper}{lockAction} = $lockAction; + + ::IOWrite( $hash, 'lockAction', + '{"param":"' + . $lockAction + . '","nukiId":' + . $hash->{NUKIID} + . ',"deviceType":' + . $hash->{DEVICETYPEID} + . '}' ); + + return; +} + +sub GetUpdate { + my $hash = shift; + my $name = $hash->{NAME}; + + if ( !::IsDisabled($name) ) { + ::IOWrite( $hash, 'lockState', + '{"nukiId":' + . $hash->{NUKIID} + . ',"deviceType":' + . $hash->{DEVICETYPEID} + . '}' ); + + ::Log3( $name, 2, "NUKIDevice ($name) - GetUpdate Call IOWrite" ); + } + + return; +} + +sub Parse { + my $hash = shift; + my $json = shift // return; + my $name = $hash->{NAME}; + + ::Log3( $name, 5, "NUKIDevice ($name) - Parse with result: $json" ); + + ######################################### + ####### Errorhandling ############# + + if ( $json !~ m/^[\[{].*[}\]]$/ ) { + ::Log3( $name, 3, "NUKIDevice ($name) - invalid json detected: $json" ); + return "NUKIDevice ($name) - invalid json detected: $json"; + } + + ######################################### + #### 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) ne 'HASH' ) { + ::Log3( $name, 2, +"NUKIDevice ($name) - got wrong status message for $name: $decode_json" + ); + + return; + } + + my $nukiId = $decode_json->{nukiId}; + if ( my $hash = $::modules{NUKIDevice}{defptr}{$nukiId} ) { + my $name = $hash->{NAME}; + + WriteReadings( $hash, $decode_json ); + ::Log3( $name, 4, + "NUKIDevice ($name) - find logical device: $hash->{NAME}" ); + + return $hash->{NAME}; + } + else { + ::Log3( $name, 4, + "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}"; + } + + ::Log3( $name, 5, "NUKIDevice ($name) - parse status message for $name" ); + + WriteReadings( $hash, $decode_json ); +} + +sub WriteReadings { + my $hash = shift; + my $decode_json = shift; + my $name = $hash->{NAME}; + + ############################ + #### Status des Smartlock + + if ( defined( $hash->{helper}{lockAction} ) ) { + my $state; + + if ( + defined( $decode_json->{success} ) + && ( $decode_json->{success} eq 'true' + || $decode_json->{success} == 1 ) + ) + { + $state = $hash->{helper}{lockAction}; + + ::IOWrite( $hash, 'lockState', + '{"nukiId":' + . $hash->{NUKIID} + . ',"deviceType":' + . $hash->{DEVICETYPEID} + . '}' ) + if ( + ::ReadingsVal( $hash->{IODev}->{NAME}, + 'bridgeType', 'Software' ) eq 'Software' + ); + + } + elsif ( + defined( $decode_json->{success} ) + && ( $decode_json->{success} eq 'false' + || $decode_json->{success} == 0 ) + ) + { + + $state = $deviceTypes{ $hash->{DEVICETYPEID} } . ' response error'; + + ::IOWrite( $hash, 'lockState', + '{"nukiId":' + . $hash->{NUKIID} + . ',"deviceType":' + . $hash->{DEVICETYPEID} + . '}' ); + } + + $decode_json->{'state'} = $state; + delete $hash->{helper}{lockAction}; + } + + ::readingsBeginUpdate($hash); + + my $t; + my $v; + + if ( defined( $decode_json->{lastKnownState} ) + && 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 ) + if ( $t ne 'state' + && $t ne 'mode' + && $t ne 'deviceType' + && $t ne 'paired' + && $t ne 'batteryCritical' + && $t ne 'batteryChargeState' + && $t ne 'batteryCharging' + && $t ne 'timestamp' + && $t ne 'doorsensorState' + && $t ne 'doorsensorStateName' ); + + ::readingsBulkUpdate( + $hash, $t, + ( + $v =~ m/^[0-9]$/ + ? $lockStates{$v}{ $hash->{DEVICETYPEID} } + : $v + ) + ) if ( $t eq 'state' ); + + ::readingsBulkUpdate( $hash, $t, $modes{$v}{ $hash->{DEVICETYPEID} } ) + if ( $t eq 'mode' ); + + ::readingsBulkUpdate( $hash, $t, $deviceTypes{$v} ) + if ( $t eq 'deviceType' ); + + ::readingsBulkUpdate( $hash, $t, $doorsensorStates{$v} ) + if ( $t eq 'doorsensorState' ); + + ::readingsBulkUpdate( $hash, $t, ( $v == 1 ? 'true' : 'false' ) ) + if ( $t eq 'paired' ); + + ::readingsBulkUpdate( $hash, $t, ( $v == 1 ? 'true' : 'false' ) ) + if ( $t eq 'batteryCharging' ); + + ::readingsBulkUpdate( $hash, 'batteryState', + ( $v == 1 ? 'low' : 'ok' ) ) + if ( $t eq 'batteryCritical' ); + + ::readingsBulkUpdate( $hash, 'batteryPercent', $v ) + if ( $t eq 'batteryChargeState' ); + } + + ::readingsEndUpdate( $hash, 1 ); + + ::Log3( $name, 5, + "NUKIDevice ($name) - lockAction readings set for $name" ); + + return; +} + +1;