############################################################################### # # Developed with Kate # # (c) 2016-2020 Copyright: Marko Oldenburg (leongaultier at gmail dot com) # All rights reserved # # This script is free software; you can redistribute it and/or modify # 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; # 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; } } } } } my $version = '0.7.27'; # Declare functions sub NUKIDevice_Initialize($); sub NUKIDevice_Define($$); sub NUKIDevice_Undef($$); sub NUKIDevice_Attr(@); sub NUKIDevice_Set($$@); sub NUKIDevice_GetUpdate($); sub NUKIDevice_Parse($$); sub NUKIDevice_WriteReadings($$); my %deviceTypes = ( 0 => 'smartlock', 2 => 'opener' ); my %modes = ( 2 => { 0 => 'door mode', 2 => 'door mode' }, 3 => { 0 => '-', 2 => ' continuous mode' } ); my %lockStates = ( 0 => { 0 => 'uncalibrated', 2 => 'untrained' }, 1 => { 0 => 'locked', 2 => 'online' }, 2 => { 0 => 'unlocking', 2 => '-' }, 3 => { 0 => 'unlocked', 2 => 'rto active' }, 4 => { 0 => 'locking', 2 => '-' }, 5 => { 0 => 'unlatched', 2 => 'open' }, 6 => { 0 => 'unlocked (lock ‘n’ go)', 2 => '-' }, 7 => { 0 => 'unlatching', 2 => 'opening' }, 253 => { 0 => '-', 2 => 'boot run' }, 254 => { 0 => 'motor blocked', 2 => '-' }, 255 => { 0 => 'undefined', 2 => 'undefined' } ); my %deviceTypeIds = reverse(%deviceTypes); sub NUKIDevice_Initialize($) { my ($hash) = @_; $hash->{Match} = '^{.*}$'; $hash->{SetFn} = 'NUKIDevice_Set'; $hash->{DefFn} = 'NUKIDevice_Define'; $hash->{UndefFn} = 'NUKIDevice_Undef'; $hash->{AttrFn} = 'NUKIDevice_Attr'; $hash->{ParseFn} = 'NUKIDevice_Parse'; $hash->{AttrList} = 'IODev ' . 'model:opener,smartlock ' . 'disable:1 ' . $readingFnAttributes; foreach my $d ( sort keys %{ $modules{NUKIDevice}{defptr} } ) { my $hash = $modules{NUKIDevice}{defptr}{$d}; $hash->{VERSION} = $version; } } sub NUKIDevice_Define($$) { my ( $hash, $def ) = @_; my @a = split( '[ \t][ \t]*', $def ); 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; $hash->{STATE} = 'Initialized'; my $iodev = AttrVal( $name, 'IODev', 'none' ); AssignIoPort( $hash, $iodev ) if ( !$hash->{IODev} ); if ( defined( $hash->{IODev}->{NAME} ) ) { Log3( $name, 3, "NUKIDevice ($name) - I/O device is " . $hash->{IODev}->{NAME} ); } else { Log3( $name, 1, "NUKIDevice ($name) - no I/O device" ); } $iodev = $hash->{IODev}->{NAME}; my $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' ); if ($init_done) { InternalTimer( gettimeofday() + int( rand(10) ), "NUKIDevice_GetUpdate", $hash ); } else { InternalTimer( gettimeofday() + 15 + int( rand(5) ), "NUKIDevice_GetUpdate", $hash ); } $modules{NUKIDevice}{defptr}{$nukiId} = $hash; return undef; } sub NUKIDevice_Undef($$) { my ( $hash, $arg ) = @_; my $nukiId = $hash->{NUKIID}; my $name = $hash->{NAME}; RemoveInternalTimer($hash); Log3( $name, 3, "NUKIDevice ($name) - undefined with NukiId: $nukiId" ); delete( $modules{NUKIDevice}{defptr}{$nukiId} ); return undef; } sub NUKIDevice_Attr(@) { my ( $cmd, $name, $attrName, $attrVal ) = @_; my $hash = $defs{$name}; my $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 NUKIDevice_Set($$@) { my ( $hash, $name, @aa ) = @_; my ( $cmd, @args ) = @aa; my $lockAction; if ( lc($cmd) eq 'statusrequest' ) { return ('usage: statusRequest') if ( @args != 0 ); NUKIDevice_GetUpdate($hash); return undef; } elsif ( $cmd eq 'unpair' ) { return ('usage: unpair') if ( @args != 0 ); if ( !IsDisabled($name) ) { $hash->{IODev}->{helper}->{iowrite} = 1 if ( $hash->{IODev}->{helper}->{iowrite} == 0 ); IOWrite( $hash, $cmd, undef, $hash->{NUKIID}, $hash->{DEVICETYPE} ); } 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' ) { 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 ); $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; $hash->{IODev}->{helper}->{iowrite} = 1 if ( $hash->{IODev}->{helper}->{iowrite} == 0 ); IOWrite( $hash, "lockAction", $lockAction, $hash->{NUKIID}, $hash->{DEVICETYPE} ); return undef; } sub NUKIDevice_GetUpdate($) { my $hash = shift; my $name = $hash->{NAME}; RemoveInternalTimer($hash); IOWrite( $hash, 'lockState', undef, $hash->{NUKIID}, $hash->{DEVICETYPE} ) if ( !IsDisabled($name) ); Log3( $name, 5, "NUKIDevice ($name) - NUKIDevice_GetUpdate Call IOWrite" ) if ( !IsDisabled($name) ); return undef; } sub NUKIDevice_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}; NUKIDevice_WriteReadings( $hash, $decode_json ); Log3( $name, 4, "NUKIDevice ($name) - find logical device: $hash->{NAME}" ); ################## ## Zwischenlösung so für die Umstellung, kann später gelöscht werden if ( AttrVal( $name, 'model', '' ) eq '' ) { CommandDefMod( undef, $name . ' NUKIDevice ' . $hash->{NUKIID} . ' ' . $decode_json->{deviceType} ); CommandAttr( undef, $name . ' model ' . $deviceTypes{ $decode_json->{deviceType} } ); Log3( $name, 2, "NUKIDevice ($name) - redefined Defmod" ); } return $hash->{NAME}; } else { Log3( $name, 3, "NUKIDevice ($name) - autocreate new device " . makeDeviceName( $decode_json->{name} ) . " with nukiId $decode_json->{nukiId}, model $decode_json->{deviceType}" ); return 'UNDEFINED ' . makeDeviceName( $decode_json->{name} ) . " NUKIDevice $decode_json->{nukiId} $decode_json->{deviceType}"; } Log3( $name, 5, "NUKIDevice ($name) - parse status message for $name" ); NUKIDevice_WriteReadings( $hash, $decode_json ); } sub NUKIDevice_WriteReadings($$) { my ( $hash, $decode_json ) = @_; my $name = $hash->{NAME}; ############################ #### Status des Smartlock if ( defined( $hash->{helper}{lockAction} ) ) { my $state; if ( defined( $decode_json->{success} ) and ( $decode_json->{success} eq 'true' or $decode_json->{success} == 1 ) ) { $state = $hash->{helper}{lockAction}; IOWrite( $hash, 'lockState', undef, $hash->{NUKIID} ) if ( ReadingsVal( $hash->{IODev}->{NAME}, 'bridgeType', 'Software' ) eq 'Software' ); } elsif ( defined( $decode_json->{success} ) and ( $decode_json->{success} eq 'false' or $decode_json->{success} == 0 ) ) { $state = $deviceTypes{ $hash->{DEVICETYPE} } . ' response error'; IOWrite( $hash, 'lockState', undef, $hash->{NUKIID}, $hash->{DEVICETYPE} ); } $decode_json->{'state'} = $state; delete $hash->{helper}{lockAction}; } readingsBeginUpdate($hash); my $t; my $v; if ( defined( $decode_json->{lastKnownState} ) and ref( $decode_json->{lastKnownState} ) eq 'HASH' ) { while ( ( $t, $v ) = each %{ $decode_json->{lastKnownState} } ) { $decode_json->{$t} = $v; } delete $decode_json->{lastKnownState}; } while ( ( $t, $v ) = each %{$decode_json} ) { readingsBulkUpdate( $hash, $t, $v ) unless ( $t eq 'state' or $t eq 'mode' or $t eq 'deviceType' or $t eq 'paired' or $t eq 'batteryCritical' ); 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 =cut