diff --git a/fhem/CHANGED b/fhem/CHANGED index 185911634..0adaee3c5 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,8 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: 73_NUKIBridge 74_NUKIDevice: new modul structure and rewrite code, + add support for new smartlock 3.0 and 3.0 pro, + version 2.0 pre-release - feature: 93_DbRep: diffValue - recognize if diff is 0 or no value available - change: 72_FB_CALLLIST: reading "count-missed-calls" only counts incoming missed calls diff --git a/fhem/FHEM/73_NUKIBridge.pm b/fhem/FHEM/73_NUKIBridge.pm index 4c6d1a700..2434332fe 100644 --- a/fhem/FHEM/73_NUKIBridge.pm +++ b/fhem/FHEM/73_NUKIBridge.pm @@ -35,1235 +35,44 @@ # ################################ - -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); +require FHEM::Devices::Nuki::Bridge; -# 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; -}; +sub ::NUKIBridge_Initialize { goto &Initialize } -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($) { +sub Initialize { my ($hash) = @_; # Provider - $hash->{WriteFn} = 'FHEM::NUKIBridge::Write'; + $hash->{WriteFn} = \&FHEM::Devices::Nuki::Bridge::Write; $hash->{Clients} = ':NUKIDevice:'; $hash->{MatchList} = { '1:NUKIDevice' => '^{.*}$' }; my $webhookFWinstance = - join( ",", devspec2array('TYPE=FHEMWEB:FILTER=TEMPORARY!=1') ); + 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->{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 ' + 'disable:1 ' . 'port ' . 'webhookFWinstance:' . $webhookFWinstance . ' ' . 'webhookHttpHostname ' - . $readingFnAttributes; + . $::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 @@ -1432,8 +241,8 @@ sub ParseJSON($$) { ], "release_status": "stable", "license": "GPL_2", - "version": "v1.9.50", - "x_apiversion": "1.9", + "version": "v2.0.0", + "x_apiversion": "1.12.3", "author": [ "Marko Oldenburg " ], diff --git a/fhem/FHEM/74_NUKIDevice.pm b/fhem/FHEM/74_NUKIDevice.pm index ad99e94c1..4462195f1 100644 --- a/fhem/FHEM/74_NUKIDevice.pm +++ b/fhem/FHEM/74_NUKIDevice.pm @@ -24,604 +24,39 @@ # $Id$ # ############################################################################### - -package main; - -use strict; -use warnings; - package FHEM::NUKIDevice; use strict; use warnings; + use FHEM::Meta; -use GPUtils qw(GP_Import GP_Export); +require FHEM::Devices::Nuki::Device; 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 ::NUKIDevice_Initialize { goto &Initialize } 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->{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:opener,smartlock ' + . 'model:smartlock,opener,smartdoor,smartlock3 ' . 'disable:1 ' - . $readingFnAttributes; + . $::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 @@ -769,7 +204,7 @@ sub WriteReadings($$) { ], "release_status": "stable", "license": "GPL_2", - "version": "v1.9.50", + "version": "v2.0.0", "author": [ "Marko Oldenburg " ], diff --git a/fhem/lib/FHEM/Devices/Nuki/Bridge.pm b/fhem/lib/FHEM/Devices/Nuki/Bridge.pm new file mode 100644 index 000000000..541a46d58 --- /dev/null +++ b/fhem/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/fhem/lib/FHEM/Devices/Nuki/Device.pm b/fhem/lib/FHEM/Devices/Nuki/Device.pm new file mode 100644 index 000000000..1a20598c2 --- /dev/null +++ b/fhem/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;