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/FHEM/73_NUKIBridge.pm b/FHEM/73_NUKIBridge.pm old mode 100755 new mode 100644 index 8024e87..a4234ee --- a/FHEM/73_NUKIBridge.pm +++ b/FHEM/73_NUKIBridge.pm @@ -43,14 +43,13 @@ use warnings; use FHEM::Meta; require FHEM::Devices::Nuki::Bridge; - sub ::NUKIBridge_Initialize { goto &Initialize } sub Initialize { my ($hash) = @_; # Provider - $hash->{WriteFn} = \&Write; + $hash->{WriteFn} = \&FHEM::Devices::Nuki::Bridge::Write; $hash->{Clients} = ':NUKIDevice:'; $hash->{MatchList} = { '1:NUKIDevice' => '^{.*}$' }; @@ -74,9 +73,6 @@ sub Initialize { return FHEM::Meta::InitMod( __FILE__, $hash ); } - - - 1; =pod @@ -245,8 +241,8 @@ sub Initialize { ], "release_status": "stable", "license": "GPL_2", - "version": "v1.9.30", - "x_apiversion": "1.9", + "version": "v2.0.0", + "x_apiversion": "1.12.3", "author": [ "Marko Oldenburg " ], diff --git a/FHEM/74_NUKIDevice.pm b/FHEM/74_NUKIDevice.pm new file mode 100644 index 0000000..36bee32 --- /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:opener,smartlock ' + . '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/lib/FHEM/Devices/Nuki/Bridge.pm b/lib/FHEM/Devices/Nuki/Bridge.pm index e094b4f..3d27c59 100644 --- a/lib/FHEM/Devices/Nuki/Bridge.pm +++ b/lib/FHEM/Devices/Nuki/Bridge.pm @@ -43,7 +43,6 @@ use warnings; use FHEM::Meta; use HttpUtils; - # try to use JSON::MaybeXS wrapper # for chance of better performance + open code eval { @@ -139,30 +138,27 @@ my %lockActionsOpener = ( ); sub Define { - my ( $hash, $def ) = @_; - - my @a = split( "[ \t][ \t]*", $def ); + 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 ); return ('too few parameters: define NUKIBridge ') - if ( @a != 4 ); + if ( !defined($host) + || !defined($token) ); - 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'; + my $infix = 'NUKIBridge'; + $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; ::Log3( $name, 3, "NUKIBridge ($name) - defined with host $host on port $port, Token $token" @@ -171,7 +167,14 @@ sub Define { ::CommandAttr( undef, $name . ' room NUKI' ) if ( ::AttrVal( $name, 'room', 'none' ) eq 'none' ); - if ( addExtension( $name, 'NUKIBridge_CGI', $infix . "-" . $host ) ) { + if ( + addExtension( + $name, + \&FHEM::Devices::Nuki::Bridge::CGI, + $infix . "-" . $host + ) + ) + { $hash->{fhem}{infix} = $infix; } @@ -187,7 +190,7 @@ sub Define { } sub Undef { - my ( $hash, $arg ) = @_; + my $hash = shift; my $host = $hash->{HOST}; my $name = $hash->{NAME}; @@ -203,7 +206,10 @@ sub Undef { } sub Attr { - my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $cmd = shift; + my $name = shift; + my $attrName = shift; + my $attrVal = shift; my $hash = $::defs{$name}; my $orig = $attrVal; @@ -245,30 +251,31 @@ sub Attr { return ( "Invalid value for attribute $attrName: FHEMWEB instance $attrVal not existing" ) - if ( $attrVal + if ( + $attrVal && $attrName eq 'webhookFWinstance' - && - ( !defined( $::defs{$attrVal} ) || $::defs{$attrVal}{TYPE} ne 'FHEMWEB' ) ); + && ( !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' + if ( $attrVal + && $attrName eq 'webhookPort' && $attrVal !~ /^\d+$/ ); if ( $attrName =~ /^webhook.*/ ) { my $webhookHttpHostname = ( - $attrName eq 'webhookHttpHostname' - && defined($attrVal) - ? $attrVal - : ::AttrVal( $name, 'webhookHttpHostname', '' ) + $attrName eq 'webhookHttpHostname' && defined($attrVal) + ? $attrVal + : ::AttrVal( $name, 'webhookHttpHostname', '' ) ); my $webhookFWinstance = ( - $attrName eq 'webhookFWinstance' - && defined($attrVal) - ? $attrVal - : ::AttrVal( $name, 'webhookFWinstance', '' ) + $attrName eq 'webhookFWinstance' && defined($attrVal) + ? $attrVal + : ::AttrVal( $name, 'webhookFWinstance', '' ) ); $hash->{WEBHOOK_URI} = '/' @@ -299,7 +306,8 @@ sub Attr { . $hash->{WEBHOOK_URI}; ::Log3( $name, 3, "NUKIBridge ($name) - URL ist: $url" ); -# Write( $hash, 'callback/add', $url, undef, undef ) + + # Write( $hash, 'callback/add', $url, undef, undef ) Write( $hash, 'callback/add', '{"param":"' . $url . '"}' ) if ($::init_done); $hash->{WEBHOOK_REGISTER} = 'sent'; @@ -314,8 +322,10 @@ sub Attr { sub Notify { - my ( $hash, $dev ) = @_; + my $hash = shift; + my $dev = shift // return; my $name = $hash->{NAME}; + return if ( ::IsDisabled($name) ); my $devname = $dev->{NAME}; @@ -335,15 +345,17 @@ sub Notify { or grep /^DEFINED.$name$/, @{$events} ) - && $devname eq 'global' - && $::init_done + && $devname eq 'global' + && $::init_done ); return; } sub addExtension { - my ( $name, $func, $link ) = @_; + my $name = shift; + my $func = shift; + my $link = shift; my $url = '/' . $link; @@ -359,7 +371,7 @@ sub addExtension { } sub removeExtension { - my ($link) = @_; + my $link = shift; my $url = '/' . $link; my $name = $::data{FWEXT}{$url}{deviceName}; @@ -368,56 +380,52 @@ sub removeExtension { "NUKIBridge ($name) - Unregistering NUKIBridge for webhook URL $url..." ); delete $::data{FWEXT}{$url}; - + return; } sub Set { - my ( $hash, $name, $cmd, @args ) = @_; + my $hash = shift; + my $name = shift; + my $cmd = shift // return "set $name needs at least one argument !"; + my $arg = shift // ''; - my ( $arg, @params ) = @args; my $endpoint; if ( lc($cmd) eq 'getdevicelist' ) { - return 'usage: getDeviceList' if ( @args != 0 ); - + return 'usage: getDeviceList' if ($arg); $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 ); + return 'usage: reboot' if ( defined($arg) ); $endpoint = 'reboot'; } elsif ( lc($cmd) eq 'clearlog' ) { - return 'usage: clearLog' if ( @args != 0 ); + return 'usage: clearLog' if ( defined($arg) ); $endpoint = 'clearlog'; } elsif ( lc($cmd) eq 'factoryreset' ) { - return 'usage: clearLog' if ( @args != 0 ); + return 'usage: clearLog' if ( defined($arg) ); $endpoint = 'factoryReset'; } elsif ( lc($cmd) eq 'callbackremove' ) { - return 'usage: callbackRemove' if ( @args > 1 ); + return 'usage: callbackRemove' if ( split( m{\s+}xms, $arg ) > 1 ); - my $id = ( @args > 0 ? join( ' ', @args ) : 0 ); + my $id = ( defined($arg) ? $arg : 0 ); -# Write( $hash, 'callback/remove', $id, undef, undef ) Write( $hash, 'callback/remove', '{"param":"' . $id . '"}' ) if ( !::IsDisabled($name) ); - - return undef; + + return; } else { my $list = ''; @@ -426,26 +434,28 @@ sub Set { if ( ::ReadingsVal( $name, 'bridgeType', 'Software' ) eq 'Hardware' ); return ( 'Unknown argument ' . $cmd . ', choose one of ' . $list ); } - + Write( $hash, $endpoint, undef ) - if ( !::IsDisabled($name) ); - + if ( !::IsDisabled($name) ); + return; } sub Get { - my ( $hash, $name, $cmd, @args ) = @_; + my $hash = shift; + my $name = shift; + my $cmd = shift // return "set $name needs at least one argument !"; + my $arg = shift; - my ( $arg, @params ) = @args; my $endpoint; if ( lc($cmd) eq 'logfile' ) { - return 'usage: logFile' if ( @args != 0 ); + return 'usage: logFile' if ( defined($arg) ); $endpoint = 'log'; } elsif ( lc($cmd) eq 'callbacklist' ) { - return 'usage: callbackList' if ( @args != 0 ); + return 'usage: callbackList' if ( defined($arg) ); $endpoint = 'callback/list'; } @@ -457,55 +467,55 @@ sub Get { return 'Unknown argument ' . $cmd . ', choose one of ' . $list; } - - return Write( $hash, $endpoint, undef ) + + return Write( $hash, $endpoint, undef ); } sub GetCheckBridgeAlive { - my ($hash) = @_; - + my $hash = shift; my $name = $hash->{NAME}; ::RemoveInternalTimer($hash); ::Log3( $name, 4, "NUKIBridge ($name) - GetCheckBridgeAlive" ); if ( !::IsDisabled($name) - && $hash->{helper}->{iowrite} == 0 ) + && $hash->{helper}->{iowrite} == 0 ) { - Write( $hash, 'info', undef); + Write( $hash, 'info', 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) = @_; - + my $hash = shift; my $name = $hash->{NAME}; ::RemoveInternalTimer($hash); Write( $hash, 'list', undef ) if ( !::IsDisabled($name) ); - + return ::InternalTimer( ::gettimeofday() + 5, \&FHEM::Devices::Nuki::Bridge::GetCheckBridgeAlive, $hash ); } sub Write { - my ( $hash, $endpoint, $json ) = @_; - + 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 $nukiId = $decode_json->{nukiId} // undef; my $deviceType = $decode_json->{deviceType} // undef; - my $param = $decode_json->{param} // undef; + my $param = $decode_json->{param} // undef; my $obj = { endpoint => $endpoint, @@ -518,7 +528,7 @@ sub Write { if ( ( defined($param) && $param ) || ( defined($nukiId) - && $nukiId ) + && $nukiId ) ); unshift( @{ $hash->{helper}->{actionQueue} }, $obj ); @@ -527,7 +537,8 @@ sub Write { } sub CreateUri { - my ( $hash, $obj ) = @_; + my $hash = shift; + my $obj = shift; my $name = $hash->{NAME}; my $host = $hash->{HOST}; @@ -539,16 +550,19 @@ sub CreateUri { my $deviceType = $obj->{deviceType}; my $uri = 'http://' . $host . ':' . $port; - $uri .= '/' . $endpoint if ( defined $endpoint ); + $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=' + . $lockActionsSmartLock{$param} + if ( + $endpoint ne 'callback/add' + && ( $deviceType == 0 + || $deviceType == 4 ) + ); $uri .= '&action=' . $lockActionsOpener{$param} if ( $endpoint ne 'callback/add' @@ -569,7 +583,7 @@ sub CreateUri { if ( defined($deviceType) ); ::Log3( $name, 4, "NUKIBridge ($name) - created uri: $uri" ); - + return $uri; } @@ -599,13 +613,8 @@ sub BridgeCall { }; $param->{cl} = $hash->{CL} - if ( - ( - $endpoint eq 'callback/list' - || $endpoint eq 'log' - ) - && ref( $hash->{CL} ) eq 'HASH' - ); + if ( ( $endpoint eq 'callback/list' || $endpoint eq 'log' ) + && ref( $hash->{CL} ) eq 'HASH' ); ::HttpUtils_NonblockingGet($param); ::Log3( $name, 4, @@ -617,12 +626,14 @@ sub BridgeCall { if ( defined($endpoint) && $endpoint eq 'lockAction' ); } - + return; } sub Distribution { - my ( $param, $err, $json ) = @_; + my $param = shift; + my $err = shift; + my $json = shift; my $hash = $param->{hash}; my $doTrigger = $param->{doTrigger}; @@ -639,7 +650,7 @@ sub Distribution { ::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}) + if ( defined( $param->{code} ) && $param->{code} ); $hash->{helper}->{iowrite} = 0 @@ -669,22 +680,22 @@ sub Distribution { } } - if ( ( $json eq '' - || $json =~ /Unavailable/i ) + if ( ( $json eq '' || $json =~ /Unavailable/i ) && exists( $param->{code} ) && $param->{code} != 200 ) { - if ( $param->{code} == 503 - && $json eq 'HTTP 503 Unavailable' ) + if ( $param->{code} == 503 + && $json eq 'HTTP 503 Unavailable' ) { ::Log3( $name, 4, -"NUKIBridge ($name) - Response from Bridge: $param->{code}, $json" ); +"NUKIBridge ($name) - Response from Bridge: $param->{code}, $json" + ); ::readingsEndUpdate( $hash, 1 ); if ( defined( $hash->{helper}->{lastDeviceAction} ) - && $hash->{helper}->{lastDeviceAction} ) + && $hash->{helper}->{lastDeviceAction} ) { push( @{ $hash->{helper}->{actionQueue} }, @@ -709,7 +720,7 @@ sub Distribution { . " without any data after requesting" ); ::readingsEndUpdate( $hash, 1 ); - + ::asyncOutput( $param->{cl}, "Request Error: $err\r\n" ) if ( $param->{cl} && $param->{cl}{canAsyncOutput} ); @@ -718,8 +729,8 @@ sub Distribution { . ' without any data after requesting' ); } - if ( ($json =~ /Error/i ) - && exists($param->{code}) ) + if ( ( $json =~ /Error/i ) + && exists( $param->{code} ) ) { ::readingsBulkUpdate( $hash, 'lastError', 'invalid API token' ) @@ -753,11 +764,11 @@ sub Distribution { if ( $param->{endpoint} eq 'callback/list' ) { getCallbackList( $param, $json ); - return undef; + return; } elsif ( $param->{endpoint} eq 'log' ) { getLogfile( $param, $json ); - return undef; + return; } if ( $hash == $dhash ) { @@ -777,7 +788,8 @@ sub Distribution { ::Dispatch( $hash, $json, undef ); } - ::InternalTimer( ::gettimeofday() + 3, \&FHEM::Devices::Nuki::Bridge::BridgeCall, $hash ) + ::InternalTimer( ::gettimeofday() + 3, + \&FHEM::Devices::Nuki::Bridge::BridgeCall, $hash ) if ( defined( $hash->{helper}->{actionQueue} ) && scalar( @{ $hash->{helper}->{actionQueue} } ) > 0 ); @@ -785,7 +797,9 @@ sub Distribution { } sub ResponseProcessing { - my ( $hash, $json, $endpoint ) = @_; + my $hash = shift; + my $json = shift; + my $endpoint = shift; my $name = $hash->{NAME}; my $decode_json; @@ -870,14 +884,13 @@ sub ResponseProcessing { } else { - return ::Log3( - $name, 5, "NUKIBridge ($name) - Rückgabe Path nicht korrekt: $json" - ); + return ::Log3( $name, 5, + "NUKIBridge ($name) - Rückgabe Path nicht korrekt: $json" ); } } sub CGI() { - my ($request) = @_; + my $request = shift; my $hash; my $name; @@ -894,11 +907,13 @@ sub CGI() { my $json = ( split( '&', $request, 2 ) )[1]; if ( !$json ) { - ::Log3( $name, 3, "NUKIBridge WEBHOOK ($name) - empty message received" ); + ::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" ); + ::Log3( $name, 4, + "NUKIBridge WEBHOOK ($name) - empty answer received" ); return; } elsif ( $json !~ m/^[\[{].*[}\]]$/ ) { @@ -912,16 +927,15 @@ sub CGI() { if ( $json =~ m/^\{.*\}$/ ) { $hash->{WEBHOOK_COUNTER}++; - $hash->{WEBHOOK_LAST} = TimeNow(); + $hash->{WEBHOOK_LAST} = ::TimeNow(); - ::Log3( - $name, 3, "NUKIBridge WEBHOOK ($name) - Received webhook for -matching NukiId at device $name" + ::Log3( $name, 3, +"NUKIBridge WEBHOOK ($name) - Received webhook for matching NukiId at device $name" ); ::Dispatch( $hash, $json, undef ); - return(undef,undef); + return ( undef, undef ); } # no data received @@ -930,11 +944,12 @@ matching NukiId at device $name" "NUKIBridge WEBHOOK - received malformed request\n$request" ); } - ::return ( 'text/plain; charset=utf-8', 'Call failure: ' . $request ); + ::return( 'text/plain; charset=utf-8', 'Call failure: ' . $request ); } sub WriteReadings { - my ( $hash, $decode_json ) = @_; + my $hash = shift; + my $decode_json = shift; my $name = $hash->{NAME}; @@ -953,21 +968,23 @@ sub WriteReadings { $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, '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, $json ) = @_; + my $param = shift; + my $json = shift; my $hash = $param->{hash}; my $name = $hash->{NAME}; @@ -981,20 +998,19 @@ sub getLogfile { ::Log3( $name, 4, "NUKIBridge ($name) - Log data are collected and processed" ); - if ( $param->{cl} - && $param->{cl}->{TYPE} eq 'FHEMWEB' ) + if ( $param->{cl} + && $param->{cl}->{TYPE} eq 'FHEMWEB' ) { if ( ref($decode_json) eq 'ARRAY' - && scalar( @{$decode_json} ) > 0 ) + && scalar( @{$decode_json} ) > 0 ) { ::Log3( $name, 4, "NUKIBridge ($name) - created Table with log file" ); - my $header = '' - . '
Log List
'; + my $header = '' . '
Log List
'; - my $ret = $header.'
'; + my $ret = $header . '
'; $ret .= ''; for my $logs ( @{$decode_json} ) { @@ -1032,12 +1048,13 @@ sub getLogfile { && $param->{cl}{canAsyncOutput} ); } } - + return; } sub getCallbackList { - my ( $param, $json ) = @_; + my $param = shift; + my $json = shift; my $hash = $param->{hash}; my $name = $hash->{NAME}; @@ -1051,8 +1068,8 @@ sub getCallbackList { ::Log3( $name, 4, "NUKIBridge ($name) - Callback data are collected and processed" ); - if ( $param->{cl} - && $param->{cl}->{TYPE} eq 'FHEMWEB' ) + if ( $param->{cl} + && $param->{cl}->{TYPE} eq 'FHEMWEB' ) { if ( ref( $decode_json->{callbacks} ) eq 'ARRAY' ) { @@ -1061,10 +1078,10 @@ sub getCallbackList { my $space = ' '; my $aHref; - my $header = '' - . '
Callback List
'; + my $header = + '' . '
Callback List
'; - my $ret = $header.'
'; + my $ret = $header . '
'; $ret .= ''; $ret .= ''; $ret .= ''; @@ -1075,16 +1092,16 @@ sub getCallbackList { for my $cb ( @{ $decode_json->{callbacks} } ) { $aHref = "{host} - . "/fhem?cmd=set+" - . $name - . "+callbackRemove+" - . $cb->{id} - . $::FW_CSRF - . "\">X"; +# . $::FW_httpheader->{host} + . "/fhem?cmd=set+" + . $name + . "+callbackRemove+" + . $cb->{id} + . $::FW_CSRF + . "\">X"; $ret .= ''; - $ret .= ''; + $ret .= ''; $ret .= ''; } } @@ -1103,12 +1120,13 @@ sub getCallbackList { && $param->{cl}{canAsyncOutput} ); } } - + return; } sub getCallbackList2 { - my ( $param, $json ) = @_; + my $param = shift; + my $json = shift; my $hash = $param->{hash}; my $name = $hash->{NAME}; @@ -1122,38 +1140,39 @@ sub getCallbackList2 { ::Log3( $name, 4, "NUKIBridge ($name) - Callback data are collected and processed" ); - if ( $param->{cl} - && $param->{cl}->{TYPE} eq 'FHEMWEB' ) + 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 = + ''; - my $j1 = ''; - -# FW_cmd(FW_root+"?cmd="+type+" "+dev+ -# (params[0]=="state" ? "":" "+params[0])+" "+arg+"&XHR=1"); - + # FW_cmd(FW_root+"?cmd="+type+" "+dev+ + # (params[0]=="state" ? "":" "+params[0])+" "+arg+"&XHR=1"); my $header = ''; my $footer = ''; - my $ret = '
Callback List
' - . '
URL' . $cb->{url} . ''.$aHref.'' . $aHref . '
' - . '' - . '' - . '' - . '' - . ''; + my $ret = + '
Callback List
' + . '
URLRemove
'; $ret .= '
' + . '' + . '' + . '' + . '' . ''; if ( scalar( @{ $decode_json->{callbacks} } ) > 0 ) { for my $cb ( @{ $decode_json->{callbacks} } ) { $ret .= ''; - $ret .= ""; + $ret .= +""; $ret .= ''; } } @@ -1166,21 +1185,23 @@ sub getCallbackList2 { $ret .= '
URLRemove
' . $cb->{url} . '
'; - + ::Log3( $name, 4, - "NUKIBridge ($name) - Callback List Table created and call asyncOutput Fn" ); +"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, $buffer ) = @_; + my $hash = shift; + my $buffer = shift; my $name = $hash->{NAME}; my $open = 0; @@ -1191,16 +1212,16 @@ sub ParseJSON { if ($buffer) { for my $c ( split //, $buffer ) { - if ( $open == $close - && $open > 0 ) + if ( $open == $close + && $open > 0 ) { $tail .= $c; ::Log3( $name, 5, "NUKIBridge ($name) - $open == $close and $open > 0" ); } - elsif ( $open == $close - && $c ne '{' ) + elsif ($open == $close + && $c ne '{' ) { ::Log3( $name, 5, "NUKIBridge ($name) - Garbage character before message: " @@ -1230,7 +1251,4 @@ sub ParseJSON { return ( $msg, $tail ); } - - - 1; diff --git a/lib/FHEM/Devices/Nuki/Device.pm b/lib/FHEM/Devices/Nuki/Device.pm new file mode 100644 index 0000000..681852f --- /dev/null +++ b/lib/FHEM/Devices/Nuki/Device.pm @@ -0,0 +1,584 @@ +############################################################################### +# +# 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', + 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 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}; + + 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}" ); + + ################## + ## 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 = 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 'timestamp' ); + + ::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, ( $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; +} + +1;