From 9c57c4f08aa795bf293d19c2578eb4f78a4c68af Mon Sep 17 00:00:00 2001 From: rapster <> Date: Sun, 23 Aug 2015 00:13:22 +0000 Subject: [PATCH] 74_Unifi: New module for the Ubiquiti Networks (UBNT) - Unifi Controller. (Forum: #40287) git-svn-id: https://svn.fhem.de/fhem/trunk@9112 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/74_Unifi.pm | 396 ++++++++++++++++++++++++++++++++++++++++++ fhem/HISTORY | 3 + fhem/MAINTAINER.txt | 1 + 4 files changed, 401 insertions(+) create mode 100644 fhem/FHEM/74_Unifi.pm diff --git a/fhem/CHANGED b/fhem/CHANGED index c4d88d6a0..a09f07bec 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # 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. + - added: 74_Unifi.pm for the Ubiquiti Networks (UBNT) - Unifi Controller. - change: 70_VolumeLink: - Changed vol/mute-RegexPattern modifier to /si - Changed default timeout to 0.5 - Fixed bug while storing RegEx from Attr to hash diff --git a/fhem/FHEM/74_Unifi.pm b/fhem/FHEM/74_Unifi.pm new file mode 100644 index 000000000..13947b69f --- /dev/null +++ b/fhem/FHEM/74_Unifi.pm @@ -0,0 +1,396 @@ +############################################################################### +# $Id: 74_Unifi.pm 2015-08-23 01:00 - rapster - rapster at x0e dot de $ + +package main; +use strict; +use warnings; +use HttpUtils; +use POSIX qw(strftime); +use JSON qw(decode_json); +############################################################################### + +sub Unifi_Initialize($$) { + my ($hash) = @_; + $hash->{DefFn} = "Unifi_Define"; + $hash->{UndefFn} = "Unifi_Undef"; + $hash->{SetFn} = "Unifi_Set"; + $hash->{GetFn} = "Unifi_Get"; + $hash->{AttrList} = $readingFnAttributes; +} +############################################################################### + +sub Unifi_Define($$) { + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + return "Wrong syntax: use define Unifi [ [ []]]" if(int(@a) < 6); + return "Wrong syntax: is not a number!" if(!looks_like_number($a[3])); + return "Wrong syntax: is not a number!" if($a[6] && !looks_like_number($a[6])); + return "Wrong syntax: too small, must be at least 10" if($a[6] && $a[6] < 10); + return "Wrong syntax: is not a valid number! Must be 3 or 4." if($a[8] && (!looks_like_number($a[8]) || $a[8] !~ /3|4/)); + + my $name = $a[0]; + %$hash = ( %$hash, + url => "https://".$a[2].(($a[3] != 443) ? ':'.$a[3] : '').'/', + interval => $a[6] || 30, + siteID => $a[7] || 'default', + version => $a[8] || 4, + ); + $hash->{httpParams} = { + hash => $hash, + timeout => 5, + method => "POST", + noshutdown => 0, + ignoreredirects => 1, + loglevel => 5, + sslargs => { SSL_verify_mode => 'SSL_VERIFY_NONE' }, + header => "Content-Type: application/json;charset=UTF-8" + }; + $hash->{loginParams} = { + %{$hash->{httpParams}}, + url => $hash->{url}."api/login", + data => "{'username':'".$a[4]."', 'password':'".$a[5]."'}", + cookies => '', + callback => \&Unifi_Login_Receive + }; + + readingsSingleUpdate($hash,"state","initialized",0); + Log3 $name, 5, "$name: Defined with url:$hash->{url}, interval:$hash->{interval}, siteID:$hash->{siteID}, version:$hash->{version}"; + + RemoveInternalTimer($hash); + Unifi_DoUpdate($hash); + + return undef; +} +############################################################################### + +sub Unifi_Undef($$) { + my ($hash,$arg) = @_; + + RemoveInternalTimer($hash); + return undef; +} +############################################################################### + +sub Unifi_Set($@) { + my ($hash,@a) = @_; + return "\"set $hash->{NAME}\" needs at least an argument" if ( @a < 2 ); + + my ($name,$setName,$setVal) = @a; + + if (AttrVal($name, "disable", 0)) { + Log3 $name, 5, "$name: set called with $setName but device is disabled" if ($setName ne "?"); + return undef; + } + Log3 $name, 5, "$name: set called with $setName " . ($setVal ? $setVal : "") if ($setName ne "?"); + + if($setName !~ /update|clear/) { + return "Unknown argument $setName, choose one of update:noArg clear:all,readings,clientData"; + } else { + Log3 $name, 4, "$name: set $setName"; + + if ($setName eq 'update') { + Unifi_DoUpdate($hash,1); + } + elsif ($setName eq 'clear') { + if ($setVal eq 'readings' || $setVal eq 'all') { + for (keys %{$hash->{READINGS}}) { + delete $hash->{READINGS}->{$_} if($_ ne 'state'); + } + } + if ($setVal eq 'clientData' || $setVal eq 'all') { + undef $hash->{clients}; + } + } + } + return undef; +} +############################################################################### + +sub Unifi_Get($@) { + my ($hash,@a) = @_; + return "\"get $hash->{NAME}\" needs at least one argument" if ( @a < 2 ); + my ($name,$getName,$getVal) = @a; + + if($getName !~ /clientDetails/) { + return "Unknown argument $getName, choose one of clientDetails:noArg"; + } + elsif ($getName eq 'clientDetails') { + my $clientDetails = ''; + for my $client (sort keys %{$hash->{clients}}) { + for (sort keys %{$hash->{clients}->{$client}}) { + $clientDetails .= "$_ = $hash->{clients}->{$client}->{$_}\n"; + } + $clientDetails .= "============================================\n"; + } + return $clientDetails if($clientDetails ne ''); + } + return undef; +} +############################################################################### + +sub Unifi_DoUpdate($@) { + my ($hash,$manual) = @_; + my $name = $hash->{NAME}; + Log3 $name, 5, "$name: DoUpdate - executed."; + + if ( $hash->{STATE} ne 'connected' ) { + Unifi_Login_Send($hash); + } else { + Unifi_GetClients_Send($hash); + if($manual) { + Log3 $name, 5, "$name: DoUpdate - Manual updated executed."; + } else { + InternalTimer(time()+$hash->{interval}, 'Unifi_DoUpdate', $hash, 0); + } + } + return undef; +} +############################################################################### + +sub Unifi_Login_Send($) { + my ($hash) = @_; + Log3 $hash->{NAME}, 5, "$hash->{NAME}: Login_Send - executed."; + + HttpUtils_NonblockingGet($hash->{loginParams}); + return undef; +} +sub Unifi_Login_Receive($) { + my ($param, $err, $data) = @_; + my $name = $param->{hash}->{NAME}; + Log3 $name, 5, "$name: Login_Receive - executed."; + + if ($err ne "") { + Log3 $name, 5, "$name: Login_Receive - Error while requesting ".$param->{url}." - $err"; + } + elsif ($data ne "") { + if ($param->{code} == 200 || $param->{code} == 400) { + eval { + $data = decode_json($data); + 1; + } or do { + my $e = $@; + Log3 $name, 5, "$name: Login_Receive - Failed to decode returned json object! Will try again after interval... - error:$e"; + + InternalTimer(time()+$param->{hash}->{interval}, 'Unifi_Login_Send', $param->{hash}, 0); + return undef; + }; + Log3 $name, 5, "$name: Login_Receive - state:'$data->{meta}->{rc}'"; + if ($data->{meta}->{rc} eq "ok") { + Log3 $name, 5, "$name: Login_Receive - Login successfully!"; + $param->{cookies} = ''; + for (split("\r\n",$param->{httpheader})) { + if(/^Set-Cookie/) { + s/Set-Cookie:\s(.*?);.*/Cookie: $1/; + $param->{cookies} .= $_.'\r\n'; + } + } + Log3 $name, 5, "$name: Login_Receive - Received-cookies:$param->{cookies}"; + readingsSingleUpdate($param->{hash},"state","connected",1); + + Unifi_DoUpdate($param->{hash}); + return undef; + } + else { + $param->{cookies} = ''; + if (defined($data->{meta}->{msg})) { + my $loglevel = ($data->{meta}->{msg} eq 'api.err.Invalid') ? 1 : 5; + Log3 $name, $loglevel, "$name: Login_Receive - Login Failed! - state:'$data->{meta}->{rc}' - msg:'$data->{meta}->{msg}'"; + } else { + Log3 $name, 5, "$name: Login_Receive - Login Failed (without message)!"; + } + readingsSingleUpdate($param->{hash},"state","disconnected",1) if($param->{hash}->{READINGS}->{state}->{VAL} ne "disconnected"); + } + } else { + readingsSingleUpdate($param->{hash},"state","disconnected",1) if($param->{hash}->{READINGS}->{state}->{VAL} ne "disconnected"); + Log3 $name, 5, "$name: Login_Receive - Failed with HTTP Code $param->{code}!"; + } + } + Log3 $name, 5, "$name: Login_Receive - Connect/Login to Unifi-Controller failed! Will try again after interval..."; + readingsSingleUpdate($param->{hash},"state","disconnected",1) if($param->{hash}->{READINGS}->{state}->{VAL} ne "disconnected"); + InternalTimer(time()+$param->{hash}->{interval}, 'Unifi_Login_Send', $param->{hash}, 0); + return undef; +} +############################################################################### + +sub Unifi_GetClients_Send($) { + my ($hash) = @_; + Log3 $hash->{NAME}, 5, "$hash->{NAME}: GetClients_Send - executed."; + my $param = { + %{$hash->{httpParams}}, + url => $hash->{url}."api/s/$hash->{siteID}/stat/sta", + header => $hash->{loginParams}->{cookies}.$hash->{httpParams}->{header}, + callback => \&Unifi_GetClients_Receive + }; + HttpUtils_NonblockingGet($param); + return undef; +} +sub Unifi_GetClients_Receive($) { + my ($param, $err, $data) = @_; + my $name = $param->{hash}->{NAME}; + Log3 $name, 5, "$name: GetClients_Receive - executed."; + + if ($err ne "") { + Log3 $name, 5, "$name: GetClients_Receive - Error while requesting ".$param->{url}." - $err"; + } + elsif ($data ne "") { + if ($param->{code} == 200 || $param->{code} == 401 || $param->{code} == 400) { + eval { + $data = decode_json($data); + 1; + } or do { + my $e = $@; + Log3 $name, 5, "$name: GetClients_Receive - Failed to decode returned json object! - error:$e"; + return undef; + }; + Log3 $name, 5, "$name: GetClients_Receive - state:'$data->{meta}->{rc}'"; + if ($data->{meta}->{rc} eq "ok") { + Log3 $name, 5, "$name: GetClients_Receive - Data received successfully!"; + + readingsBeginUpdate($param->{hash}); + my $connectedClientIDs = {}; + my $i = 1; + for my $h (@{$data->{data}}) { + $param->{hash}->{clients}->{$h->{user_id}} = $h; + $connectedClientIDs->{$h->{user_id}} = 1; + readingsBulkUpdate($param->{hash},$h->{user_id}."_hostname",$h->{hostname}); + readingsBulkUpdate($param->{hash},$h->{user_id}."_last_seen",strftime "%Y-%m-%d %H:%M:%S",localtime($h->{last_seen})); + readingsBulkUpdate($param->{hash},$h->{user_id}."_essid",$h->{essid}); + readingsBulkUpdate($param->{hash},$h->{user_id}."_ip",$h->{ip}); + readingsBulkUpdate($param->{hash},$h->{user_id}."_uptime",$h->{uptime}); + readingsBulkUpdate($param->{hash},$h->{user_id},'connected'); + } + for my $clientID (keys %{$param->{hash}->{clients}}) { + if (!defined($connectedClientIDs->{$clientID}) && $param->{hash}->{READINGS}->{$clientID}->{VAL} ne 'disconnected') { + readingsBulkUpdate($param->{hash},$clientID,'disconnected'); + Log3 $name, 5, "$name: GetClients_Receive - Client '$clientID' previously connected is now disconnected."; + } + } + readingsEndUpdate($param->{hash},1); + } + else { + if (defined($data->{meta}->{msg})) { + Log3 $name, 5, "$name: GetClients_Receive - Failed! - state:'$data->{meta}->{rc}' - msg:'$data->{meta}->{msg}'"; + if($data->{meta}->{msg} eq 'api.err.LoginRequired') { + readingsSingleUpdate($param->{hash},"state","disconnected",1) if($param->{hash}->{READINGS}->{state}->{VAL} ne "disconnected"); + Log3 $name, 5, "$name: GetClients_Receive - LoginRequired detected. Set state to disconnected..."; + } + } else { + Log3 $name, 5, "$name: GetClients_Receive - Failed (without message)!"; + } + } + } + else { + Log3 $name, 5, "$name: GetClients_Receive - Failed with HTTP Code $param->{code}!"; + } + } + return undef; +} +############################################################################### + +### KNOWN RESPONSES ### +# { "data" : [ ] , "meta" : { "msg" : "api.err.Invalid" , "rc" : "error"}} +# { "data" : [ ] , "meta" : { "rc" : "ok"}} +# { "data" : [ ] , "meta" : { "msg" : "api.err.NoSiteContext" , "rc" : "error"}} +# { "data" : [ ] , "meta" : { "msg" : "api.err.LoginRequired" , "rc" : "error"}} +############################################################################### + + +1; + +=pod +=begin html + + +

Unifi

+
    + +Unifi is the fhem module for the Ubiquiti Networks (UBNT) - Unifi Controller.

    +This module is very new, therefore it supports only a limited function selection of the unifi-controller.

    +At the moment you can use the 'PRESENCE' function, which will tell you if a device is connected to your WLAN (even in PowerSave Mode!) and get some informations.
    +Immediately after connecting to your WLAN it will set the device-reading to 'connected' and about 5 minutes after leaving your WLAN it will set the reading to 'disconnected'.
    +The device will be still connected, even it is in PowerSave-Mode. (In this mode the devices are not pingable, but the connection to the unifi-controller does not break off.) +

    + +

    Define

    +
      + define <name> Unifi <ip> <port> <username> <password> [<interval> [<siteID> [<version>]]] +

      +
      + <name>: +
        + The FHEM device name for the device.
        +
      + <ip>: +
        + The ip of your unifi-controller.
        +
      + <port>: +
        + The port of your unifi-controller. Normally it's 8443 or 443.
        +
      + <username>: +
        + The Username to log on.
        +
      + <password>: +
        + The password to log on.
        +
      + [<interval>]: +
        + optional: interval to fetch the information from the unifi-api.
        + default: 30 seconds

        +
      + [<siteID>]: +
        + optional: You can find the site-ID by selecting the site in the UniFi web interface.
        + e.g. (https://localhost:8443/manage/s/foobar) siteId = 'foobar'.
        + default: 'default'

        +
      + [<version>]: +
        + optional: Your unifi-controller version.
        + This is not used at the moment, both v3.x and v4.x controller are supported.
        + default: 4

        +
      +
    +

    Example

    +
      + define my_unifi_controller Unifi 192.168.1.15 443 user password
      +
    + +

    Set

    +
      +
    • set <name> update
      + Makes immediately a manual update.
    • +

    +
      +
    • set <name> clear <readings|clientData|all>
      + Clears the readings, clientData or both.
    • +
    + + +

    Get

    +
      +
    • get <name> clientDetails
      + Show more details about each client.
    • +
    + + +

    Attributes

    +
      +
    • attr verbose 5
      + Unifi itself has no attributes, but this attribute will help you if something does not work as espected.
    • +
    + +

    Readings

    +
      +
    • Each device has multiple readings.
    • +
    • The unifi-device reading 'state' represents the connections-state to the unifi-controller.
    • +
    +
    + +
+ +=end html +=cut diff --git a/fhem/HISTORY b/fhem/HISTORY index dceb589ea..560125f5e 100644 --- a/fhem/HISTORY +++ b/fhem/HISTORY @@ -633,3 +633,6 @@ - Mon Aug 17 2015 (rapster) - added new module 70_VolumeLink to link the volume-level and mute-state of a physical-device with a fhem-device + +- Mon Aug 23 2015 (rapster) + - added new module 74_Unifi for the Ubiquiti Networks (UBNT) - Unifi Controller. diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index f58556f8f..fceb1fff6 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -223,6 +223,7 @@ FHEM/72_FRITZBOX.pm tupol http://forum.fhem.de FRITZBOX FHEM/73_km200.pm sailor http://forum.fhem.de Heizungssteuerung/Raumklima FHEM/73_PRESENCE.pm markusbloch http://forum.fhem.de Unterstuetzende Dienste FHEM/73_MPD.pm Wzut http://forum.fhem.de Multimedia +FHEM/74_Unifi.pm rapster http://forum.fhem.de Automatisierung FHEM/75_MSG.pm gandy http://forum.fhem.de Automatisierung FHEM/76_MSGFile.pm gandy http://forum.fhem.de Automatisierung FHEM/76_MSGMail.pm gandy http://forum.fhem.de Automatisierung