# $Id$ ############################################################################## # # 83_IOhomecontrol.pm # Copyright by Dr. Boris Neubert # e-mail: omega at online dot de # # This file is part of fhem. # # Fhem 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 # (at your option) any later version. # # Fhem 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. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## =for comment The interaction with the interface has three layers: - a FHEM get or set command enqueues a job consisting of one or several API calls combined with a callback to be executed on the result - the queues takes care of logging in, running the API calls and executing the callbacks, and logging out again - the core communication handles the REST API calls to the interface Commands ======== A get or set command enqueues an API call with callback and then runs the queue. When the queue has executed the API call, the callback is executed and handles the reply from the API call according to the specific get or set command. Queue ===== Processing the queue logs into the interface,executes the single API calls from the get or set commands in the queue, and then logs out from the interface. A function to reenter queue processing is added to the callback list for each call. Communication ============= The communication with the KLF200 interface is effected through the IOhomecontrol_Call and IOhomecontrol_Callback functions. IOhomecontrol_Call makes a REST API call to the interface. In the non-blocking mode, the call is asynchronous. On completion, IOhomecontrol_Callback is called. The blocking mode is not used. Among others, IOhomecontrol_Call is called with the parameters api, action and params. These can take the following values: api action params description ------------------------------------------------------------------------------ auth login log into the interface, return token auth logout log out from the interface scenes get get the scenes scenes run id run the scene given by id settings getLog The callbacks parameter of IOhomecontrol_Call is an array of function references that are called in order with the following parameters: hash: the hash reference to the FHEM device httpParams: the hash reference used for making the API call err: the error message from the call, if any, else undefined result: the JSON decoded to a hash reference, undefined in case of error Note: err is undefined if the call succeeded and a result has been returned even if that result contains error messages. Aforementioned evaluation and execution of callback function references is done in IOhomecontrol_Callback. =cut package main; use strict; use warnings; use HttpUtils; use JSON; #use Data::Dumper; use constant MESSAGES => <<'JSON'; { "e401": "Falsches Passwort", "e402": "Unbekannter Fehler, Anmeldung fehlgeschlagen", "e403": "KLF 200 wird von einem anderen Benutzer konfiguriert. Bitte versuchen Sie die Anmeldung später.", "e404": "Fehler bei der Abmeldung!", "e405": "Sitzung abgelaufen!", "e406": "Ungültiges Zeichen!", "e407": "Passwort entspricht nicht den Erfordernissen!", "e408": "Login not possible, while reset to factory settings!", "e999": "Unbekannter Fehler", "e998": "Das Gerät ist ausgelastet!", "e997": "Das Gerät befindet sich im Fehlerzustand!", "e996": "Ein kritischer Fehler ist aufgetreten. Das Gerät kann nicht mehr im normalen Modus betrieben werden!", "e995": "Abrufen der gelöschten Programmliste fehlgeschlagen", "e994": "Abrufen der gelöschten Eingangsliste fehlgeschlagen", "e993": "Programm \"{{name}}\" wurde aufgrund gelöschter Produkte gelöscht", "e992": "Die Verbindung \"{{name}}\" wurde aufgrund gelöschter Produkte oder Programme gelöscht", "e991": "Ein GW_ERROR_NTF wurde empfangen.", "e449": "Nicht unterstützter Modus", "e450": "Wechsel in den Interface-Modus fehlgeschlagen", "e451": "Wechsel in den Repeater-Modus fehlgeschlagen", "e452": "Wechsel in die Werkseinstellungen fehlgeschlagen", "e485": "Modus-Wechsel fehlgeschlagen - allgemeiner Fehler", "e101": "Produktempfang fehlgeschlagen", "e102": "Produktlöschung fehlgeschlagen", "e103": "Empfangvorgang von Produkt(en) fehlgeschlagen", "e104": "Kurze Bewegungen/Aufleuchten fehlgeschlagen", "e105": "Kopiervorgang von Produkt(en) fehlgeschlagen", "e106": "Suchvorgang von Produkt(en) fehlgeschlagen", "e107": "Produktumbenennung fehlgeschlagen", "e108": "Länge des Namens darf 32 Zeichen nicht überschreiten", "e109": "Dieser Name wird bereits verwendet. Bitte wählen Sie einen anderen Namen", "e110": "Stornieren des Kopierens von Produkten fehlgeschlagen", "e111": "Das Datenfeld darf nicht leer sein", "e112": "Kopieren von Produkt(en) fehlgeschlagen!", "e113": "Produktsuche fehlgeschlagen!", "e114": "Unbekannter Fehler!", "e115": "Das Auffinden einer anderen Bedienung im Konfigurationsempfangmodus fehlgeschlagen.", "e116": "DTS nicht bereit", "e117": "DTS-Fehler. Werkseinstellungen müssen aufgrund eines Systemfehlers der Bedienung hergestellt werden.", "e118": "Konfiguration nicht bereit.", "e119": "Die Datenübertragung zu oder von der Bedienung unterbrochen.", "e120": "Konfigurationsempfang wurde in der Bedienung abgebrochen.", "e121": "Unterbrechung.", "e201": "Programmumbenennung fehlgeschlagen", "e202": "Wechsel in den Flüstermodus fehlgeschlagen", "e203": "Programmlöschen fehlgeschlagen", "e204": "Programmempfang fehlgeschlagen", "e205": "Länge des Namens darf 32 Zeichen nicht überschreiten", "e206": "Dieser Name wird bereits verwendet. Bitte wählen Sie einen anderen Namen", "e207": "Programmausführung fehlgeschlagen", "e208": "Start der Programmerstellung fehlgeschlagen", "e209": "Stornieren der Programmerstellung fehlgeschlagen", "e210": "Programmerstellung fehlgeschlagen. Versuchen Sie es noch einmal", "e211": "Das Datenfeld darf nicht leer sein", "e212": "Unbekannte Rückmeldung", "e213": "Keine Kommunikation zum Netzknoten", "e214": "Manuell betätigt durch einen Benutzer", "e215": "Netzknoten durch ein Objekt blockiert", "e216": "Netzknoten enthält einen falschen Systemschlüssel", "e217": "Netzknoten auf dieser Prioritätsstufe gesperrt", "e218": "Netzknoten in einer anderen Position gestoppt als erwartet", "e219": "Während der Ausführung des Befehls ist ein Fehler aufgetreten", "e220": "Keine Bewegung des Netzknoten-Parameters", "e221": "Netzknoten kalibriert die Parameter", "e222": "Netzknoten-Energieverbrauch ist zu hoch", "e223": "Netzknoten-Energieverbrauch ist zu niedrig", "e224": "Türschloss-Fehler. Türverriegelung fehlgeschlagen.", "e225": "Zielposition nicht rechtzeitig erreicht", "e226": "Netzknoten ist in den Temperaturschutzmodus gegangen", "e227": "Netzknoten ohne Funktion", "e228": "Filter muss gewartet werden", "e229": "Batteriestand ist niedrig", "e230": "Netzknoten hat den Zielwert des Befehls modifiziert", "e231": "Netzknoten unterstützt nicht den empfangenen Modus", "e232": "Netzknoten ist nicht in der Lage, die richtige Richtung durchzuführen", "e233": "Verriegelung ist während des Entriegelungbefehls manuell gesperrt", "e234": "Verriegelungsfehler", "e235": "Netzknoten ist in den automatischen Zyklus-Modus gegangen", "e236": "Falsche Last am Netzknoten", "e237": "Netzknoten ist nicht in der Lage, den empfangenen Farbcode zu erreichen", "e238": "Netzknoten ist nicht in der Lage, die empfangene Zielposition zu erreichen", "e239": "io-Protokoll hat einen ungültigen Index empfangen", "e240": "Befehl durch einen neuen Befehl außer Kraft gesetzt", "e241": "Netzknoten wartet auf Energierückmeldung", "e242": "Es können keine weiteren Programme gespeichert werden", "e243": "Programmerstellung nicht gestartet", "e244": "Keine io-homecontrol® Produkte wurden aktiviert", "e301": "Verbindung konnte nicht hinzugefügt werden", "e304": "Empfang von Verbindungen fehlgeschlagen", "e305": "Speichern von Ausgang fehlgeschlagen", "e306": "Löschen von Ausgang fehlgeschlagen", "e308": "Löschen von Eingang fehlgeschlagen", "e486": "Erstellung eines neuen Systemschlüssels fehlgeschlagen. Versuchen Sie es später noch einmal.", "e498": "Nicht alle Netzknoten in der Tabelle haben ihrem Systemschlüssel geändert. Die Schlüssel in diesen Knoten reparieren oder sie aus der Tabelle löschen.", "e499": "Nicht alle Produkte wurden gelöscht", "e500": "Schlüsselerstellung wird in der Werkseinstellungs- oder Repeater-Modus nicht unterstützt", "e501": "Die Verbindung mit einigen Netzknoten in der Tabelle ging verloren. Die Verbindung reparieren oder diese Knoten aus der Tabelle entfernen. ", "log.100": "KLF 200 application started", "log.101": "EFM chip start", "log.102": "EFM library start", "log.103": "Initial web content unpacking", "log.104": "STM restore to factory settings request", "log.105": "Firmware image - uploading started", "log.106": "Firmware image - creating file", "log.107": "Firmware image - receiving data", "log.108": "Firmware image - validating data", "log.109": "Firmware image - uploading finished", "log.110": "Reset to factory settings", "log.200": "WiFi module initialized", "log.201": "WiFi is running as access point ", "log.202": "WiFi is running as a client", "log.203": "WiFi access point stopped", "log.204": "WiFi client stopped", "log.205": "Reading WiFi settings", "log.206": "Setting WiFi SSID value", "log.300": "LAN module initialized", "log.301": "LAN module stopped", "log.400": "51200 port server started over LAN", "log.401": "51200 port server started over WiFi", "log.402": "51200 port server over LAN stopped", "log.403": "51200 port server over WiFi stopped", "log.500": "HTTP server started", "log.501": "HTTP server stopped", "log.600": "EFM GW_ERROR_NTF error", "log.601": "EFM GW_CS_DISCOVER_NODES_NTF error", "log.602": "EFM GW_CS_CONTROLLER_COPY_CFM in TCM mode error", "log.603": "EFM GW_CS_CONTROLLER_COPY_CFM in RCM mode error", "log.604": "EFM GW_INITIALIZE_SCENE_NTF error", "log.605": "EFM GW_RECORD_SCENE_NTF error", "log.606": "EFM GW_ACTIVATE_SCENE_NTF error", "log.607": "EFM GW_CS_KEY_CHANGE_CFM error", "log.608": "EFM GW_CS_GENERATE_NEW_KEY_NTF error", "log.609": "EFM GW_CS_REPAIR_KEY_NTF error", "log.620": "EFM is not responding", "log.650": "EFM was externally rebooted", "log.651": "EFM was externally reset to factory settings", "log.652": "EFM restore to factory settings request", "log.700": "FW update start phase", "log.701": "FW update validate binary image phase", "log.702": "FW update set version phase", "log.703": "FW update unpack binary phase", "log.704": "FW update move EFM chain phase", "log.705": "FW update update EFM phase", "log.706": "FW update move STM chain phase", "log.707": "FW update schedule STM update phase", "log.708": "FW update finished phase", "log.709": "FW update unknown phase", "log.710": "FW update web content update phase", "log.711": "FW update schedule KLF update phase", "log.712": "FW update move STM chain back phase", "log.713": "FW update webcontent rollback phase", "log.714": "FW update move EFM chain back phase", "log.715": "FW update EFM rollback phase", "log.800": "Token generation failed during login", "log.801": "Too many attempts to login with incorrect password", "log.802": "Error during logout", "log.803": "Unknown request received to login endpoint", "e460": "Validierung fehlgeschlagen.", "e461": "Die Installation der neuen Softwareversion ist fehlgeschlagen.", "e462": "Entpacken fehlgeschlagen.", "e463": "Verschiebung der EFM-Datei fehlgeschlagen", "e464": "EFM Aktualisierung fehlgeschlagen.", "e465": "Verschiebung der STM-Datei fehlgeschlagen.", "e466": "STM Aktualisierung fehlgeschlagen.", "e467": "Zeitpunkt für die Aktualisierung des KLF 200 fehlgeschlagen.", "e468": "WEB Aktualisierung fehlgeschlagen.", "e469": "Unbekannter Fehler beim Aktualisieren.", "e473": "Passwort muss aus mindestens 8 Zeichen mit Buchstaben und Zahlen bestehen", "e474": "Passwortwiederholung falsch", "e475": "Veraltete Software", "e476": "Keine KLF 200 Software", "e477": "Fehler beim Abrufen", "e481": "Fehler beim Ermitteln der Softwareversion", "e482": "Die Datei ist zu groß. Unbekannt Software", "e483": "Aktualisierungs-Start fehlgeschlagen", "e488": "Falsches Passwort", "e489": "Änderung des Passworts fehlgeschlagen", "e490": "Wi-Fi-Datenempfang fehlgeschlagen", "e491": "Empfang der Energieverwaltungseinstellungen fehlgeschlagen", "e494": "Ungültige Energiekonfiguration ", "e492": "Änderung der Energieverwaltungseinstellungen fehlgeschlagen", "e493": "Empfang der Systemprotokollmeldungen fehlgeschlagen", "e495": "Löschen des Systemprotokolls fehlgeschlagen", "e496": "Übertragung fehlgeschlagen", "e497": "Das Gerät führt die Formatierung des Flash-Speichers durch. Versuchen Sie es in 10 Minuten erneut.", "e430": "Empfang der LAN-Einstellungen fehlgeschlagen", "e431": "Aktualisierung der LAN-Einstellung fehlgeschlagen", "e432": "Falsche LAN-Einstellungen", "e433": "Ungültige IP-Adresse", "e434": "Ungültige Subnetzmaske", "e436": "Ungültiges Gateway", "e435": "LAN-Konfiguration ist nicht übereinstimmend: IP-Adresse und Gateway gehören nicht zum gleichen, durch die Netzwerk-Subnetzmaske definierten Netzwerk ", "e437": "IP-Adresse und Standard-Gateway-Adresse müssen unterschiedlich sein", "e438": "IP-Adresse und Standard-Gateway können mit der ersten, durch die Netzwerk-Subnetzmaske definierten Netzwerk Adresse nicht identisch sein", "e439": "IP-Adresse und Standard-Gateway-Adresse können mit der ersten, durch die Netzwerk-Subnetzmaske definierten Netzwerk Adresse nicht identisch sein", "e440": "IP-Adresse ist nicht im gültigen IP-Bereich", "e441": "Standard-Gateway ist nicht im gültigen IP-Bereich" } JSON our $messages = decode_json(MESSAGES); # https://github.com/MiSchroe/klf-200-api/tree/master/src ##################################### # Initialize, Define, Undefine ##################################### sub IOhomecontrol_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "IOhomecontrol_Define"; $hash->{UndefFn} = "IOhomecontrol_Undef"; $hash->{GetFn} = "IOhomecontrol_Get"; $hash->{SetFn} = "IOhomecontrol_Set"; $hash->{NotifyFn} = "IOhomecontrol_Notify"; $hash->{parseParams} = 1; #$hash->{AttrFn} = "IOhomecontrol_Attr"; $hash->{AttrList} = "setCmds logTraffic " . $readingFnAttributes; } ##################################### sub IOhomecontrol_getPassword($) { my $hash = shift; my $pwfile = $hash->{"pwfile"}; if ( open( my $fh, "<", $pwfile ) ) { my @contents = <$fh>; close($fh); return unless @contents; my $password = $contents[0]; chomp $password; return $password; } else { return; } } sub IOhomecontrol_Define($$) { # define IOhomecontrol my ( $hash, $argref, undef ) = @_; my @def = @{$argref}; if ( $#def != 4 ) { my $msg = "wrong syntax: define IOhomecontrol "; Log 2, $msg; return $msg; } my $name = $def[0]; my $model = $def[2]; my $host = $def[3]; my $pwfile = $def[4]; if ( $model ne "KLF200" ) { my $msg = "unsupported model $model, allowed models: KLF200"; Log 2, $msg; return $msg; } $hash->{NOTIFYDEV} = "global"; $hash->{"host"} = $host; $hash->{"pwfile"} = $pwfile; $hash->{fhem}{".password"} = IOhomecontrol_getPassword($hash); $hash->{fhem}{".token"} = undef; $hash->{fhem}{".scenes"} = undef; $hash->{fhem}{".running"} = 0; $hash->{fhem}{".log"} = []; IOhomecontrol_updateStateReadings( $hash, "Initialized" ); return; } ################################### sub IOhomecontrol_Notify($$) { my ( $hash, $dev ) = @_; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; return if ( $dev->{NAME} ne "global" ); return if ( !grep( m/^INITIALIZED|REREADCFG$/, @{ $dev->{CHANGED} } ) ); return if ( $attr{$name} && $attr{$name}{disable} ); Log3 $hash, 5, "IOhomecontrol $name: FHEM initialization or rereadcfg triggered getting scenes"; # update readings IOhomecontrol_updateLoggedInReadings($hash); IOhomecontrol_clearQueue($hash); IOhomecontrol_getScenes($hash); return undef; } ##################################### sub IOhomecontrol_Undef($$) { my ( $hash, $arg ) = @_; # we should call HttpUtils_Close here # for all pending non blocking gets # todo! return; } ##################################### # Core communication with interface # Knows nothing about business logic ##################################### sub IOhomecontrol_setDeviceResult($$) { my ( $hash, $result ) = @_; my $name = $hash->{NAME}; my $errorsref = $result->{errors}; my @errors = @{$errorsref}; my $err = ""; if (@errors) { $err = join( " ", @errors ); Log3 $hash, 2, "IOhomecontrol $name: device has errors ($err)."; } else { $err = ""; } my $r = $result->{result}; readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "deviceStatus", $result->{deviceStatus} ); readingsBulkUpdate( $hash, "deviceErrors", $err ); readingsBulkUpdate( $hash, "deviceResult", $r ); readingsEndUpdate( $hash, 1 ); } sub IOhomecontrol_Call($$$$$) { my ( $hash, $api, $action, $params, $callbacks ) = @_; my $name = $hash->{NAME}; my $host = $hash->{"host"}; my $token = $hash->{fhem}{".token"}; # build url my $url = "http://$host/api/v1/$api"; # build header my $header = { "Accept" => "application/json", "Content-Type" => "application/json;charset=utf-8", }; if ( defined($token) ) { $header->{"Authorization"} = "Bearer $token"; } # build payload my $payload = { "action" => $action, "params" => $params, }; my $json = encode_json $payload; # https://wiki.fhem.de/wiki/HttpUtils # build HTTP request my $httpParams = { url => $url, timeout => 120, # it takes long to close the shutters method => "POST", noshutdown => 1, keepalive => 0, httpversion => "1.1", header => $header, data => $json, callback => \&IOhomecontrol_Callback, # additional data hash => $hash, api => $api, action => $action, params => $params, callbacks => $callbacks, }; Log3 $hash, 5, "IOhomecontrol $name: $url, calling"; readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "callState", "calling" ); readingsBulkUpdate( $hash, "callURL", $url ); if ( AttrVal( $name, "logTraffic", 0 ) ) { Log3 $hash, 5, "IOhomecontrol $name: request $json"; readingsBulkUpdate( $hash, "callRequest", $json ); } readingsBulkUpdate( $hash, "callResult", "pending" ); readingsEndUpdate( $hash, 1 ); HttpUtils_NonblockingGet($httpParams); } sub IOhomecontrol_Callback($$$) { my ( $httpParams, $err, $data ) = @_; my $hash = $httpParams->{hash}; my $name = $hash->{NAME}; my $url = $httpParams->{url}; my $api = $httpParams->{api}; my $json = $httpParams->{data}; my $action = $httpParams->{action}; my $callbacks = $httpParams->{callbacks}; $err = undef if ( $err eq "" ); $data = undef if ( $data eq "" ); my $result = undef; readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "callState", "completed" ); # process error if ( defined($err) ) { Log3 $hash, 5, "IOhomecontrol $name: $url, call failed"; readingsBulkUpdate( $hash, "callResult", "error ($err)" ); if ( AttrVal( $name, "logTraffic", 0 ) ) { readingsBulkUpdate( $hash, "callReply", "" ); } } # decode data if ( defined($data) ) { # strip junk from the beginning $data =~ s/^\)\]\}\',\n*//; if ( AttrVal( $name, "logTraffic", 0 ) ) { Log3 $hash, 5, "IOhomecontrol $name: reply $data"; readingsBulkUpdate( $hash, "callReply", $data ); } eval { $result = decode_json $data }; if ($@) { $err = "malformed JSON string"; $data = undef; Log3 $hash, 5, "IOhomecontrol $name: $url, call returned malformed JSON"; readingsBulkUpdate( $hash, "callResult", "error ($err)" ); } else { Log3 $hash, 5, "IOhomecontrol $name: $url, call succeeded"; readingsBulkUpdate( $hash, "callResult", "success" ); IOhomecontrol_setDeviceResult( $hash, $result ); } } readingsEndUpdate( $hash, 1 ); # callbacks foreach my $callback ( @{$callbacks} ) { $callback->( ( $hash, $httpParams, $err, $result ) ); } } ##################################### # Command Queue ##################################### sub IOhomecontrol_queueLength($) { my ($hash) = @_; return scalar @{ $hash->{fhem}{".queue"} }; } sub IOhomecontrol_loggedIn($) { my ($hash) = @_; return defined( $hash->{fhem}{".token"} ); } sub IOhomecontrol_updateStateReadings($$) { my ( $hash, $msg ) = @_; readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "state", $msg ); readingsEndUpdate( $hash, 1 ); } sub IOhomecontrol_updateQueueReadings($) { my ($hash) = @_; readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "queueLength", IOhomecontrol_queueLength($hash) ); readingsEndUpdate( $hash, 1 ); } sub IOhomecontrol_updateLoggedInReadings($) { my ($hash) = @_; readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "loggedIn", IOhomecontrol_loggedIn($hash) ? "yes" : "no" ); readingsEndUpdate( $hash, 1 ); } sub IOhomecontrol_enqueue($$$$$) { my ( $hash, $api, $action, $params, $callbacks ) = @_; # if a single function reference is passed, put it in an array # this is for the developer's convenience if ( ref($callbacks) ne 'ARRAY' ) { my @callbacks = ($callbacks); $callbacks = \@callbacks; } my $name = $hash->{NAME}; my %job = ( api => $api, action => $action, params => $params, callbacks => $callbacks ); push @{ $hash->{fhem}{".queue"} }, \%job; Log3 $hash, 5, "IOhomecontrol $name: job enqueued, API $api, action $action"; IOhomecontrol_updateQueueReadings($hash); } sub IOhomecontrol_dequeue($) { my ($hash) = @_; my $name = $hash->{NAME}; my $jobref = shift @{ $hash->{fhem}{".queue"} }; my $api = $jobref->{api}; my $action = $jobref->{action}; Log3 $hash, 5, "IOhomecontrol $name: job dequeued, API $api, action $action"; IOhomecontrol_updateQueueReadings($hash); return $jobref; } sub IOhomecontrol_clearQueue($) { my ($hash) = @_; my $name = $hash->{NAME}; my @queue = (); $hash->{fhem}{".queue"} = \@queue; Log3 $hash, 5, "IOhomecontrol $name: queue cleared"; IOhomecontrol_updateQueueReadings($hash); } sub IOhomecontrol_runJob($$$) { my ( $hash, $jobref, $callback ) = @_; my $name = $hash->{NAME}; my $callbacks = $jobref->{callbacks}; push @{$callbacks}, $callback; IOhomecontrol_Call( $hash, $jobref->{api}, $jobref->{action}, $jobref->{params}, $callbacks ); } sub IOhomecontrol_Login($$) { my ( $hash, $callback ) = @_; my $name = $hash->{NAME}; Log3 $hash, 5, "IOhomecontrol $name: Logging in..."; my $password = $hash->{fhem}{".password"}; my $params = { "password" => $password }; my @callbacks = ( \&IOhomecontrol_LoginCallback, $callback ); IOhomecontrol_Call( $hash, "auth", "login", $params, \@callbacks ); } sub IOhomecontrol_LoginCallback($$$$) { my ( $hash, $httpParams, $err, $result ) = @_; my $name = $hash->{NAME}; if ( defined($err) ) { Log3 $hash, 2, "IOhomecontrol $name: Login failed ($err)"; } else { my $token = $result->{token}; if ( defined($token) ) { Log3 $hash, 5, "IOhomecontrol $name: Login successful"; $hash->{fhem}{".token"} = $token; } else { Log3 $hash, 2, "IOhomecontrol $name: Login failed (no token received)"; IOhomecontrol_clearQueue($hash); # forget Log3 $hash, 2, "IOhomecontrol $name: pending commands cancelled"; } } IOhomecontrol_updateLoggedInReadings($hash); } sub IOhomecontrol_Logout($$) { my ( $hash, $callback ) = @_; my $name = $hash->{NAME}; Log3 $hash, 5, "IOhomecontrol $name: Logging out..."; my $params = {}; my @callbacks = ( \&IOhomecontrol_LogoutCallback, $callback ); IOhomecontrol_Call( $hash, "auth", "logout", $params, \@callbacks ); } sub IOhomecontrol_LogoutCallback($$$$) { my ( $hash, $httpParams, $err, $result ) = @_; my $name = $hash->{NAME}; if ( defined($err) ) { Log3 $hash, 2, "IOhomecontrol $name: Logout failed ($err)"; } else { $hash->{fhem}{".token"} = undef; } IOhomecontrol_updateLoggedInReadings($hash); } sub IOhomecontrol_runQueue($) { my ($hash) = @_; return if ( $hash->{fhem}{".running"} ); $hash->{fhem}{".running"} = 1; IOhomecontrol_updateStateReadings( $hash, "running" ); IOhomecontrol_processQueue($hash); } sub IOhomecontrol_processQueue($) { my ($hash) = @_; if ( IOhomecontrol_queueLength($hash) > 0 ) { if ( IOhomecontrol_loggedIn($hash) ) { # already logged in => run jon IOhomecontrol_runJob( $hash, IOhomecontrol_dequeue($hash), \&IOhomecontrol_processQueue ); } else { # not yet logged on => login IOhomecontrol_Login( $hash, \&IOhomecontrol_processQueue ); } } else { # queue empty if ( IOhomecontrol_loggedIn($hash) ) { # still logged in => logout IOhomecontrol_Logout( $hash, \&IOhomecontrol_processQueue ); } else { # processing ended => idle $hash->{fhem}{".running"} = 0; IOhomecontrol_updateStateReadings( $hash, "idle" ); } } } ##################################### # Get, Set ##################################### ### scenes sub IOhomecontrol_makeScenes($$) { my ( $hash, $data ) = @_; my $sc = {}; if ( defined($data) ) { #Debug "data: " . Dumper $data; foreach my $item ( @{$data} ) { #Debug "data item: " . Dumper $item; my $name = $item->{name}; my $id = $item->{id}; #Debug "$id: $name"; $sc->{$id} = $name; } my $sns = ""; foreach my $id ( sort keys %{$sc} ) { $sns .= "," if ($sns); $sns .= sprintf( "%d: %s", $id, $sc->{$id} ); } readingsSingleUpdate( $hash, "scenes", $sns, 1 ); } $hash->{fhem}{".scenes"} = $sc; return $sc; # a hash reference to id => name } sub IOhomecontrol_getScenes($) { my $hash = shift; IOhomecontrol_enqueue( $hash, "scenes", "get", {}, \&IOhomecontrol_getScenesCallback ); IOhomecontrol_runQueue($hash); } sub IOhomecontrol_getScenesCallback($$$$) { my ( $hash, $httpParams, $err, $result ) = @_; my $name = $hash->{NAME}; if ( defined($err) ) { Log3 $hash, 2, "IOhomecontrol $name: getting scenes failed ($err)"; } else { $hash->{fhem}{".scenes"} = IOhomecontrol_makeScenes( $hash, $result->{data} ); } } sub IOhomecontrol_runSceneById($$$;$) { my ( $hash, $id, $sn, $callback ) = @_; my $name = $hash->{NAME}; Log3 $hash, 5, "IOhomecontrol $name: running scene id $id, name $sn"; my @callbacks= (\&IOhomecontrol_runSceneByIdCallback); push @callbacks, $callback if(defined($callback)); IOhomecontrol_enqueue( $hash, "scenes", "run", { id => $id }, \@callbacks ); IOhomecontrol_runQueue($hash); } sub IOhomecontrol_runSceneByIdCallback($$$$) { my ( $hash, $httpParams, $err, $result ) = @_; my $name = $hash->{NAME}; my $id= $httpParams->{params}{id}; my $sn= $hash->{fhem}{".scenes"}->{$id}; if ( defined($err) ) { Log3 $hash, 2, "IOhomecontrol $name: running scene id $id, name $sn, failed ($err)"; } else { Log3 $hash, 5, "IOhomecontrol $name: running scene id $id, name $sn, completed"; my $id= $httpParams->{params}{id}; readingsSingleUpdate( $hash, "lastScene", $id, 1 ); } } ### log sub IOhomecontrol_logLength($) { my ($hash) = @_; return scalar @{ $hash->{fhem}{".log"} }; } sub IOhomecontrol_updateLogReadings($) { my ($hash) = @_; readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "logLength", IOhomecontrol_logLength($hash) ); readingsEndUpdate( $hash, 1 ); } sub IOhomecontrol_getLog($) { my $hash = shift; IOhomecontrol_enqueue( $hash, "settings", "getLog", {}, \&IOhomecontrol_getLogCallback ); IOhomecontrol_runQueue($hash); } sub IOhomecontrol_logEntry($) { my ($e) = @_; # log entry as hash reference return sprintf( "%s: %s, %s %s", $e->{time}, $e->{type}, $messages->{ $e->{text} }, $e->{opt} ); } sub IOhomecontrol_getLogCallback($$$$) { my ( $hash, $httpParams, $err, $result ) = @_; my $name = $hash->{NAME}; if ( defined($err) ) { Log3 $hash, 2, "IOhomecontrol $name: getting log failed ($err)"; } else { $hash->{fhem}{".log"} = $result->{data}; } } ##################################### sub IOhomecontrol_Get($@) { my ( $hash, $argsref, undef ) = @_; my @a = @{$argsref}; return "get needs at least one parameter" if ( @a < 2 ); my $name = $a[0]; my $cmd = $a[1]; my $arg = ( $a[2] ? $a[2] : "" ); my @args = @a; shift @args; shift @args; my $answer = ""; if ( $cmd eq "sceneList" ) { my $sc = $hash->{fhem}{".scenes"}; if ( defined($sc) ) { foreach my $id ( sort keys %{$sc} ) { $answer .= "\n" if ($answer); $answer .= sprintf( "%2d: %s", $id, $sc->{$id} ); } } } elsif ( $cmd eq "scenes" ) { IOhomecontrol_getScenes($hash); } elsif ( $cmd eq "log" ) { IOhomecontrol_getLog($hash); } elsif ( $cmd eq "showLog" ) { my @log = @{ $hash->{fhem}{".log"} }; my $log = join( "\n", map( IOhomecontrol_logEntry($_), @log ) ); return $log; } elsif ( $cmd eq "password" ) { $hash->{fhem}{".password"} = IOhomecontrol_getPassword($hash); return "password read from file " . $hash->{fhem}{".pwfile"}; } else { return "Unknown argument $cmd, choose one of scenes:noArg sceneList:noArg log:noArg showLog:noArg password:noArg"; } return $answer; } ##################################### # sub IOhomecontrol_Attr($@) { # # my @a = @_; # my $hash= $defs{$a[1]}; # my $name= $hash->{NAME}; # # if($a[0] eq "set") { # if($a[2] eq "") { # } # } # # return undef; # } ##################################### sub IOhomecontrol_getSetCmds($) { my $hash = shift; my $name = $hash->{NAME}; my $attr = AttrVal( $name, "setCmds", "" ); my ( undef, $setCmds ) = parseParams( $attr, "," ); return $setCmds; } sub IOhomecontrol_setScene($$;$) { my ( $hash, $id, $callback ) = @_; my $sc = $hash->{fhem}{".scenes"}; return "No scenes available." unless ( defined($sc) ); if ( $id !~ /^\d+$/ ) { my %cs = reverse %{$sc}; $id = $cs{$id}; } my $sn = $sc->{$id}; if ( defined($sn) ) { IOhomecontrol_runSceneById( $hash, $id, $sn, $callback ); } else { return "No such scene $id"; } } sub IOhomecontrol_Set($$$) { my ( $hash, $argsref, undef ) = @_; my @a = @{$argsref}; return "set needs at least one parameter" if ( @a < 2 ); my $name = shift @a; my $cmd = shift @a; my $setCmds = IOhomecontrol_getSetCmds($hash); my $usage = "Unknown argument $cmd, choose one of scene " . join( " ", ( keys %{$setCmds} ) ); if ( exists( $setCmds->{$cmd} ) ) { readingsSingleUpdate( $hash, "state", $cmd, 1 ); my $subst = $setCmds->{$cmd}; Log3 $hash, 5, "IOhomecontrol $name: substitute set command $cmd by $subst"; ( $argsref, undef ) = parseParams($subst); @a = @{$argsref}; $cmd = shift @a; } if ( $cmd eq "scene" ) { if ($#a) { return "Command scene needs exactly one argument."; } else { my $id = $a[0]; IOhomecontrol_setScene( $hash, $id ); } } else { return $usage; } return undef; } ##################################### 1; =pod =item device =item summary control IOhomecontrol devices via REST API =item summary_DE IOhomecontrol-Geräte mittels REST-API steuern =begin html

IOhomecontrol

    Define

      define <name> IOhomecontrol <model> <host> <pwfile>

      Defines an IOhomecontrol interface device (gateway) to communicate with IOhomecontrol devices. <model> is a placeholder for future amendments. Currently only the Velux Integra KLF200 Interface model KLF200 is supported as a gateway. <host> is the IP address or hostname of the IOhomecontrol interface device (gateway). <pwfile> is a file that contains the password to log into the device.

      Example:
        define myKLF200 IOhomecontrol KLF200 192.168.0.91 /opt/fhem/etc/veluxpw.txt


    Set

      set <name> scene <id>

      Runs the scene identified by <id> which can be either the numeric id of the scene or the scene's name.

      Examples:
        set velux scene 1
        set velux scene "all shutters down"

      Scene names with blanks must be enclosed in double quotes.

    Get

      get <name> scenes

      Retrieves the ids and names of the scenes from the device. This is done automatically after FHEM is initialized. So you should need this only if you have altered scenes in the interface device.

      Example:
        get myKLF200 scenes


      get <name> sceneList

      Displays the scenes.

      get <name> log

      Retrieves the event log from the device.

      get <name> showLog

      Displays the event log.

      get <name> password

      Reads the password from the password file <pwfile>. This is done automatically after FHEM is initialized. So you should need this only if you have altered the password in the file.


    Attributes

    • setCmds: a comma-separated list of set command definitions. Every definition is of the form <shorthand>=<command>. This defines a new single-word command <shorthand> as a substitute for <command>.
      Example: attr velux setCmds evening=scene "close all",morning=scene "open all"

    • logTraffic: if set to a nonzero value, request and reply JSON strings are logged with log level 5 and stored in the callRequest and callReply readings. Use with caution because the password is transmitted in plain text in the authentication request.

    • readingFnAttributes


=end html =cut