# $Id$ ################################################################ # # Copyright notice # # (c) 2015 mike3436 (mike3436@online.de) # # 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 # (at your option) 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. # # This copyright notice MUST APPEAR in all copies of the script! # ################################################################ # $Id: 26_tahoma.pm # # 2014-08-01 V 0100 first Version using XML Interface # 2015-08-16 V 0200 communication to server changes from xml to json # 2015-08-16 V 0201 some standard requests after login which are not neccessary disabled (so the actual requests are not equal to flow of iphone app) # 2016-02-14 V 0202 bugs forcing some startup warning messages fixed # 2016-02-20 V 0203 perl exception while parsing json string captured # 2016-02-24 V 0204 commands open,close,my,stop and setClosure added # 2016-04-24 V 0205 commands taken from setup # 2016-06-16 V 0206 updateDevices called for devices created before setup has been read # 2016-11-15 V 0207 BLOCKING=0 can be used, all calls asynchron, attribut levelInvert inverts RollerShutter position # 2016-11-29 V 0208 HttpUtils used instead of LWP::UserAgent, BLOCKING=0 set as default, umlaut can be used in Tahoma names package main; use strict; use warnings; use utf8; use Encode qw(decode_utf8); use JSON; #use Data::Dumper; use Time::HiRes qw(time); use HttpUtils; sub tahoma_parseGetSetupPlaces($$); sub tahoma_UserAgent_NonblockingGet($); sub tahoma_encode_utf8($); my $hash_; sub tahoma_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "tahoma_Define"; $hash->{NOTIFYDEV} = "global"; $hash->{NotifyFn} = "tahoma_Notify"; $hash->{UndefFn} = "tahoma_Undefine"; $hash->{SetFn} = "tahoma_Set"; $hash->{GetFn} = "tahoma_Get"; $hash->{AttrFn} = "tahoma_Attr"; $hash->{AttrList} = "IODev ". "blocking ". "debug:1 ". "disable:1 ". "interval ". "logfile ". "url ". "placeClasses ". "levelInvert ". "userAgent "; $hash->{AttrList} .= $readingFnAttributes; } ##################################### sub tahoma_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $ModuleVersion = "0208"; my $subtype; my $name = $a[0]; if( $a[2] eq "DEVICE" && @a == 4 ) { $subtype = "DEVICE"; my $device = $a[3]; my $fid = (split "/", $device)[-1]; $hash->{device} = $device; $hash->{fid} = $fid; $hash->{INTERVAL} = 2; my $d = $modules{$hash->{TYPE}}{defptr}{"$fid"}; return "device $device already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); $modules{$hash->{TYPE}}{defptr}{"$fid"} = $hash; } elsif( $a[2] eq "PLACE" && @a == 4 ) { $subtype = "PLACE"; my $oid = $a[@a-1]; my $fid = (split "-", $oid)[0]; $hash->{oid} = $oid; $hash->{fid} = $fid; $hash->{INTERVAL} = 0; my $d = $modules{$hash->{TYPE}}{defptr}{"$fid"}; return "place oid $oid already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); $modules{$hash->{TYPE}}{defptr}{"$fid"} = $hash; } elsif( $a[2] eq "SCENE" && @a == 4 ) { $subtype = "SCENE"; my $oid = $a[@a-1]; my $fid = (split "-", $oid)[0]; $hash->{oid} = $oid; $hash->{fid} = $fid; $hash->{INTERVAL} = 0; my $d = $modules{$hash->{TYPE}}{defptr}{"$fid"}; return "scene oid $oid already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); $modules{$hash->{TYPE}}{defptr}{"$fid"} = $hash; } elsif( $a[2] eq "ACCOUNT" && @a == 5 ) { $subtype = "ACCOUNT"; my $username = $a[@a-2]; my $password = $a[@a-1]; $hash->{Clients} = ":tahoma:"; $hash->{username} = $username; $hash->{password} = $password; $hash->{BLOCKING} = 0; $hash->{INTERVAL} = 2; $hash->{VERSION} = $ModuleVersion; } else { return "Usage: define tahoma device\ define tahoma ACCOUNT username password\ define tahoma DEVICE id\ define tahoma SCENE oid username password\ define tahoma PLACE oid" if(@a < 4 || @a > 5); } $hash->{NAME} = $name; $hash->{SUBTYPE} = $subtype; $hash->{STATE} = "Initialized"; if( $init_done ) { tahoma_connect($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" ); tahoma_initDevice($hash) if( $hash->{SUBTYPE} eq "DEVICE" ); tahoma_initDevice($hash) if( $hash->{SUBTYPE} eq "PLACE" ); tahoma_initDevice($hash) if( $hash->{SUBTYPE} eq "SCENE" ); } return undef; } sub tahoma_Notify($$) { my ($hash,$dev) = @_; return if($dev->{NAME} ne "global"); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); tahoma_connect($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" ); tahoma_initDevice($hash) if( $hash->{SUBTYPE} eq "DEVICE" ); tahoma_initDevice($hash) if( $hash->{SUBTYPE} eq "PLACE" ); tahoma_initDevice($hash) if( $hash->{SUBTYPE} eq "SCENE" ); } sub tahoma_Undefine($$) { my ($hash, $arg) = @_; delete( $modules{$hash->{TYPE}}{defptr}{"$hash->{device}"} ) if( $hash->{SUBTYPE} eq "DEVICE" ); delete( $modules{$hash->{TYPE}}{defptr}{"$hash->{oid}"} ) if( $hash->{SUBTYPE} eq "PLACE" ); delete( $modules{$hash->{TYPE}}{defptr}{"$hash->{oid}"} ) if( $hash->{SUBTYPE} eq "SCENE" ); return undef; } sub tahoma_login($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 3, "$name: tahoma_login"; $hash->{logged_in} = undef; $hash->{startup_run} = undef; $hash->{startup_done} = undef; $hash->{url} = "https://www.tahomalink.com/enduser-mobile-web/externalAPI/json/"; $hash->{url} = $attr{$name}{url} if (defined $attr{$name}{url}); $hash->{userAgent} = "TaHoma/7845 CFNetwork/758.3.15 Darwin/15.4.0"; $hash->{userAgent} = $attr{$name}{userAgent} if (defined $attr{$name}{userAgent}); $hash->{timeout} = 10; $hash->{HTTPCookies} = undef; Log3 $name, 2, "$name: login start"; tahoma_UserAgent_NonblockingGet({ timeout => 10, noshutdown => 1, hash => $hash, page => 'login', data => {'userId' => $hash->{username} , 'userPassword' => $hash->{password}}, callback => \&tahoma_dispatch, nonblocking => 1, }); } my @startup_pages = ( 'getEndUser', 'getSetup', 'getActionGroups', #'getWeekPlanning', #'getCalendarDayList', #'getCalendarRuleList', #'getScheduledExecutions', #'getHistory', #'getSetupTriggers', #'getUserPreferences', #'getSetupOptions', #'getAvailableProtocolsType', #'getActiveProtocolsType', '../../enduserAPI/setup/gateways', 'getCurrentExecutions', 'refreshAllStates' ); sub tahoma_startup($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_startup"; return if (!$hash->{logged_in}); return if ($hash->{startup_done}); if (!defined($hash->{startup_run})) { $hash->{startup_run} = 0; } else { $hash->{startup_run}++; if ($hash->{startup_run} >= scalar @startup_pages) { $hash->{startup_done} = 1; return; } } my $page = $startup_pages[$hash->{startup_run}]; my $subpage = ""; $subpage = '?gatewayId='.$hash->{gatewayId} if (substr($page, -13) eq 'ProtocolsType'); $subpage = '?quotaId=smsCredit' if ($page eq 'getSetupQuota'); $subpage = '/'.$hash->{gatewayId}.'/version' if (substr($page, -8) eq 'gateways'); tahoma_UserAgent_NonblockingGet({ timeout => 10, noshutdown => 1, hash => $hash, page => $page, subpage => $subpage, callback => \&tahoma_dispatch, nonblocking => 1, }); } sub tahoma_refreshAllStates($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_refreshAllStates"; tahoma_UserAgent_NonblockingGet({ timeout => 10, noshutdown => 1, hash => $hash, page => 'refreshAllStates', callback => \&tahoma_dispatch, nonblocking => 1, }); } sub tahoma_getEvents($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_getEvents"; return if(!$hash->{logged_in} && !$hash->{startup_done}); tahoma_UserAgent_NonblockingGet({ timeout => 10, noshutdown => 1, hash => $hash, page => 'getEvents', callback => \&tahoma_dispatch, nonblocking => 1, }); } sub tahoma_readStatusTimer($) { my $timestart = time; my $timeinfo = "tahoma_readStatusTimer"; my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); my ($seconds) = gettimeofday(); $hash->{refreshStateTimer} = $seconds + 10 if ( (!defined($hash->{refreshStateTimer})) || (!$hash->{logged_in}) ); if( $hash->{request_active} ) { Log3 $name, 3, "$name: request active"; if( ($timestart - $hash->{request_time}) > 10) { Log3 $name, 2, "$name: timeout, close ua"; $hash->{socket} = undef; $hash->{request_active} = 0; $hash->{logged_in} = 0; $hash->{startup_done} = 0; } } elsif( !$hash->{logged_in} ) { tahoma_login($hash); $timeinfo = "tahoma_login"; } elsif( !$hash->{startup_done} ) { tahoma_startup($hash); $timeinfo = "tahoma_startup"; if ( $hash->{startup_done} ) { tahoma_getStates($hash) ; $hash->{refreshStateTimer} = $seconds + 300; $timeinfo = "tahoma_getStates"; } } elsif( $seconds < $hash->{refreshStateTimer} ) { Log3 $name, 4, "$name: refreshing event"; tahoma_getEvents($hash); $timeinfo = "tahoma_getEvents"; } else { Log3 $name, 4, "$name: refreshing state"; tahoma_refreshAllStates($hash); tahoma_getStates($hash); $hash->{refreshStateTimer} = $seconds + 300; $timeinfo = "tahoma_refreshAllStates tahoma_getStates"; } my $timedelta = time -$timestart; if ($timedelta > 0.5) { $timedelta *= 1000; Log3 $name, 3, "$name: $timeinfo took $timedelta ms" } InternalTimer(gettimeofday()+2, "tahoma_readStatusTimer", $hash, 0); } sub tahoma_connect($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 3, "$name: tahoma_connect"; RemoveInternalTimer($hash); tahoma_login($hash); my ($seconds) = gettimeofday(); $hash->{refreshStateTimer} = $seconds + 10; tahoma_readStatusTimer($hash); } sub tahoma_initDevice($) { my ($hash) = @_; my $name = $hash->{NAME}; my $subtype = $hash->{SUBTYPE}; AssignIoPort($hash); if(defined($hash->{IODev}->{NAME})) { Log3 $name, 3, "$name: I/O device is " . $hash->{IODev}->{NAME}; } else { Log3 $name, 1, "$name: no I/O device"; } my $device; if( defined($hash->{device}) ) { $device = tahoma_getDeviceDetail( $hash, $hash->{device} ); #Log3 $name, 4, Dumper($device); } elsif( defined($hash->{oid}) ) { $device = tahoma_getDeviceDetail( $hash, $hash->{oid} ); #Log3 $name, 4, Dumper($device); } if( defined($device) && ($subtype eq 'DEVICE') ) { Log3 $name, 4, "$name: I/O device is label=".$device->{label}; $hash->{inType} = $device->{type}; $hash->{inLabel} = $device->{label}; $hash->{inControllable} = $device->{controllableName}; $hash->{inPlaceOID} = $device->{placeOID}; $hash->{inClass} = $device->{uiClass}; $device->{levelInvert} = $attr{$hash->{NAME}}{levelInvert} if (defined $attr{$hash->{NAME}}{levelInvert}); } elsif( defined($device) && ($subtype eq 'PLACE') ) { Log3 $name, 4, "$name: I/O device is label=".$device->{label}; $hash->{inType} = $device->{type}; $hash->{inLabel} = $device->{label}; $hash->{inOID} = $device->{oid}; $hash->{inClass} = 'RollerShutter'; $hash->{inClass} = $attr{$hash->{NAME}}{placeClasses} if (defined $attr{$hash->{NAME}}{placeClasses}); } elsif( defined($device) && ($subtype eq 'SCENE') ) { Log3 $name, 4, "$name: I/O device is label=".$device->{label}; $hash->{inLabel} = $device->{label}; $hash->{inOID} = $device->{oid}; } else { Log3 $name, 3, "$name: unknown device=$hash->{device}, subtype=$subtype"; } my $state_format; if( $device->{states} ) { delete($hash->{dataTypes}); delete($hash->{helper}{dataTypes}); my @reading_names = (); foreach my $type (@{$device->{states}}) { $hash->{dataTypes} = "" if ( !defined($hash->{dataTypes}) ); $hash->{dataTypes} .= "," if ( $hash->{dataTypes} ); $hash->{dataTypes} .= $type->{name}; #Log3 $name, 4, "state=$type->{name}"; push @reading_names, lc($type); if( $type->{name} eq "core:ClosureState" ) { $state_format .= " " if( $state_format ); $state_format .= "Closure: ClosureState"; } elsif( $type->{name} eq "core:OpenClosedState" ) { $state_format .= " " if( $state_format ); $state_format .= "Closed: OpenClosedState"; } } $hash->{helper}{readingNames} = \@reading_names; } #$attr{$name}{stateFormat} = $state_format if( !defined( $attr{$name}{stateFormat} ) && defined($state_format) ); } sub tahoma_updateDevices($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 3, "$name: tahoma_updateDevices"; return undef if( !$hash->{helper}{devices} ) ; $hash = $hash->{IODev} if( defined($hash->{IODev}) ); foreach my $module (keys %{$modules{$hash->{TYPE}}{defptr}}) { my $def = $modules{$hash->{TYPE}}{defptr}{"$module"}; my $subtype = $def->{SUBTYPE}; if (defined($def->{oid}) && !defined($def->{inType})) { Log3 $name, 3, "$name: updateDevices oid=$def->{oid}"; my $device = tahoma_getDeviceDetail( $hash, $def->{oid} ); if( defined($device) && ($subtype eq 'PLACE') ) { Log3 $name, 4, "$name: I/O device is label=".$device->{label}; $def->{inType} = $device->{type}; $def->{inLabel} = $device->{label}; $def->{inOID} = $device->{oid}; $def->{inClass} = 'RollerShutter'; $def->{inClass} = $attr{$def->{NAME}}{placeClasses} if (defined $attr{$def->{NAME}}{placeClasses}); } elsif( defined($device) && ($subtype eq 'SCENE') ) { Log3 $name, 4, "$name: I/O device is label=".$device->{label}; $def->{inLabel} = $device->{label}; $def->{inOID} = $device->{oid}; } } elsif (defined($def->{device}) && !defined($def->{inType})) { Log3 $name, 3, "$name: updateDevices device=$def->{device}"; my $device = tahoma_getDeviceDetail( $hash, $def->{device} ); if( defined($device) && ($subtype eq 'DEVICE') ) { Log3 $name, 4, "$name: I/O device is label=".$device->{label}; $def->{inType} = $device->{type}; $def->{inLabel} = $device->{label}; $def->{inControllable} = $device->{controllableName}; $def->{inPlaceOID} = $device->{placeOID}; $def->{inClass} = $device->{uiClass}; $device->{levelInvert} = $attr{$def->{NAME}}{levelInvert} if (defined $attr{$def->{NAME}}{levelInvert}); } } } return undef; } sub tahoma_getDevices($$) { my ($hash,$nonblocking) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_getDevices"; tahoma_UserAgent_NonblockingGet({ timeout => 10, noshutdown => 1, hash => $hash, page => 'getSetup', callback => \&tahoma_dispatch, nonblocking => $nonblocking, }); return $hash->{helper}{devices}; } sub tahoma_getDeviceDetail($$) { my ($hash,$id) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_getDeviceDetails $id"; $hash = $hash->{IODev} if( defined($hash->{IODev}) ); foreach my $device (@{$hash->{helper}{devices}}) { return $device if( defined($device->{deviceURL}) && ($device->{deviceURL} eq $id) ); return $device if( defined($device->{oid}) && ($device->{oid} eq $id) ); } Log3 $name, 4, "$name: getDeviceDetails $id not found"; return undef; } sub tahoma_getStates($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_getStates"; my $data = '['; foreach my $device (@{$hash->{helper}{devices}}) { if( defined($device->{deviceURL}) && defined($device->{states}) ) { $data .= ',' if (substr($data, -1) eq '}'); $data .= '{"deviceURL":"'.$device->{deviceURL}.'","states":['; foreach my $state (@{$device->{states}}) { $data .= ',' if (substr($data, -1) eq '}'); $data .= '{"name":"' . $state->{name} . '"}'; } $data .= ']}'; } } $data .= ']'; Log3 $name, 5, "$name: tahoma_getStates data=".$data; tahoma_UserAgent_NonblockingGet({ timeout => 10, noshutdown => 1, hash => $hash, page => 'getStates', data => tahoma_encode_utf8($data), callback => \&tahoma_dispatch, nonblocking => 1, }); } sub tahoma_getDeviceList($$$$); sub tahoma_getDeviceList($$$$) { my ($hash,$oid,$placeClasses,$deviceList) = @_; #print "tahoma_getDeviceList oid=$oid devices=".scalar @{$deviceList}."\n"; my @classes = split(' ',$placeClasses); my $devices = $hash->{helper}{devices}; foreach my $device (@{$devices}) { if ( defined($device->{deviceURL}) && defined($device->{placeOID}) && defined($device->{uiClass}) ) { if (( grep { $_ eq $device->{uiClass}} @classes ) && ($device->{placeOID} eq $oid)) { push ( @{$deviceList}, { device => $device->{deviceURL}, class => $device->{uiClass}, levelInvert => $device->{levelInvert} } ) ; #print "tahoma_getDeviceList url=$device->{deviceURL} devices=".scalar @{$deviceList}."\n"; } } elsif ( defined($device->{oid}) && defined($device->{place}) ) { if ($device->{oid} eq $oid) { foreach my $place (@{$device->{place}}) { tahoma_getDeviceList($hash,$place->{oid},$placeClasses,$deviceList); } } } } } sub tahoma_checkCommand($$$$) { my ($hash,$device,$command,$value) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_checkCommand"; if (($command eq 'setClosure') && (defined ($device->{levelInvert}))) { $value = 100 - $value if ($device->{levelInvert} && ($value >= 0) && ($value <= 100)); } return ($command,$value); } sub tahoma_applyRequest($$$) { my ($hash,$command,$value) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_applyRequest"; if ( !defined($hash->{IODev}) || !(defined($hash->{device}) || defined($hash->{oid})) || !defined($hash->{inLabel}) || !defined($hash->{inClass}) ) { Log3 $name, 4, "$name: tahoma_applyRequest failed - define error"; return; } my @devices = (); if ( defined($hash->{device}) ) { push ( @devices, { device => $hash->{device}, class => $hash->{inClass}, commands => $hash->{COMMANDS}, levelInvert => $attr{$hash->{NAME}}{levelInvert} } ); } else { tahoma_getDeviceList($hash->{IODev},$hash->{oid},$hash->{inClass},\@devices); } Log3 $name, 4, "$name: tahoma_applyRequest devices=".scalar @devices; foreach my $dev (@devices) { Log3 $name, 4, "$name: tahoma_applyRequest devices=$dev->{device} class=$dev->{class}"; } return if (scalar @devices < 1); my $data = ''; $value = '' if (!defined($value)); foreach my $device (@devices) { my ($commandChecked, $valueChecked) = tahoma_checkCommand($hash,$device,$command,$value); if (defined($commandChecked) && defined($valueChecked)) { $data .= ',' if substr($data, -1) eq '}'; $data .= '{"deviceURL":"'.$device->{device}.'",'; $data .= '"commands":[{"name":"'.$commandChecked.'","parameters":['.$valueChecked.']}]}'; } } return if (length $data < 20); my $dataHead = '{"label":"' . $hash->{inLabel}; $dataHead .= ' - Positionieren auf '.$value.' % - iPhone","actions":[' if ($command eq 'setClosure'); $dataHead .= ' - $command $value - iPhone","actions":[' if ($command ne 'setClosure'); $data = $dataHead . $data . ']}'; Log3 $name, 3, "$name: tahoma_applyRequest data=".$data; tahoma_UserAgent_NonblockingGet({ timeout => 10, noshutdown => 1, hash => $hash->{IODev}, page => 'apply', data => tahoma_encode_utf8($data), callback => \&tahoma_dispatch, nonblocking => 1, }); } sub tahoma_scheduleActionGroup($$) { my ($hash,$delay) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_scheduleActionGroup"; if ( !defined($hash->{IODev}) || !defined($hash->{oid}) ) { Log3 $name, 3, "$name: tahoma_scheduleActionGroup failed - define error"; return; } $delay = 0 if(!defined($delay)); tahoma_UserAgent_NonblockingGet({ timeout => 10, noshutdown => 1, hash => $hash->{IODev}, page => 'scheduleActionGroup', subpage => '?oid='.$hash->{oid}.'&delay='.$delay, callback => \&tahoma_dispatch, nonblocking => 1, }); } sub tahoma_dispatch($$$) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if (!$hash->{logged_in}) { tahoma_GetCookies($hash,$param->{httpheader}); } if( $err ) { Log3 $name, 2, "$name: tahoma_dispatch http request failed: $err"; $hash->{request_active} = 0; $hash->{logged_in} = 0; } elsif( $data ) { $hash->{request_active} = 0; $data =~ tr/\r\n//d; $data =~ s/\h+/ /g; $data =~ s/\\\//\//g; Log3 $name, (length $data > 120)?4:5, "$name: tahoma_dispatch data=".decode_utf8($data); # perl exception while parsing json string captured my $json = {}; eval { $json = JSON->new->utf8(0)->decode($data); }; if ($@) { Log3 $name, 3, "$name: tahoma_dispatch json string is faulty"; $hash->{lastError} = 'json string is faulty'; $hash->{logged_in} = 0; return; } if( (ref $json ne 'ARRAY') && ($json->{errorResponse}) ) { $hash->{lastError} = $json->{errorResponse}{message}; $hash->{logged_in} = 0; return; } if( $param->{page} eq 'getEvents' ) { tahoma_parseGetEvents($hash,$json); } elsif( $param->{page} eq 'apply' ) { tahoma_parseApplyRequest($hash,$json); } elsif( $param->{page} eq 'getSetup' ) { tahoma_parseGetSetup($hash,$json); } elsif( $param->{page} eq 'refreshAllStates' ) { tahoma_parseRefreshAllStates($hash,$json); } elsif( $param->{page} eq 'getStates' ) { tahoma_parseGetStates($hash,$json); } elsif( $param->{page} eq 'login' ) { tahoma_parseLogin($hash,$json); } elsif( $param->{page} eq 'getActionGroups' ) { tahoma_parseGetActionGroups($hash,$json); } elsif( $param->{page} eq '../../enduserAPI/setup/gateways' ) { tahoma_parseEnduserAPISetupGateways($hash,$json); } elsif( $param->{page} eq 'getCurrentExecutions' ) { tahoma_parseGetCurrentExecutions($hash,$json); } elsif( $param->{page} eq 'scheduleActionGroup' ) { tahoma_parseScheduleActionGroup($hash,$json); } } } sub tahoma_autocreate($) { my($hash) = @_; my $name = $hash->{NAME}; if( !$hash->{helper}{devices} ) { tahoma_getDevices($hash,1); return undef; } foreach my $d (keys %defs) { next if($defs{$d}{TYPE} ne "autocreate"); return undef if(AttrVal($defs{$d}{NAME},"disable",undef)); } Log3 $name, 2, "$name: tahoma_autocreate begin"; my $autocreated = 0; my $devices = $hash->{helper}{devices}; foreach my $device (@{$devices}) { my ($id, $fid, $devname, $define); if ($device->{deviceURL}) { $id = $device->{deviceURL}; $fid = (split("/",$id))[-1]; $devname = "tahoma_". $fid; $define = "$devname tahoma DEVICE $id"; if( defined($modules{$hash->{TYPE}}{defptr}{"$fid"}) ) { Log3 $name, 4, "$name: device '$fid' already defined"; next; } } elsif ( $device->{oid} ) { $id = $device->{oid}; $fid = (split("-",$id))[0]; $devname = "tahoma_". $fid; $define = "$devname tahoma PLACE $id" if (!defined $device->{actions}); $define = "$devname tahoma SCENE $id" if (defined $device->{actions}); if( defined($modules{$hash->{TYPE}}{defptr}{"$fid"}) ) { Log3 $name, 4, "$name: device '$fid' already defined"; next; } } Log3 $name, 3, "$name: create new device '$devname' for device '$id'"; my $cmdret= CommandDefine(undef,$define); if($cmdret) { Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret"; } else { $cmdret= CommandAttr(undef,"$devname alias room ".$device->{label}) if( defined($device->{label}) && defined($device->{oid}) && !defined($device->{actions}) ); $cmdret= CommandAttr(undef,"$devname alias scene ".$device->{label}) if( defined($device->{label}) && defined($device->{oid}) && defined($device->{actions}) ); $cmdret= CommandAttr(undef,"$devname alias $device->{uiClass} ".$device->{label}) if( defined($device->{label}) && defined($device->{states}) ); $cmdret= CommandAttr(undef,"$devname room tahoma"); $cmdret= CommandAttr(undef,"$devname IODev $name"); $cmdret= CommandAttr(undef,"$devname webCmd dim") if( defined($device->{uiClass}) && ($device->{uiClass} eq "RollerShutter") ); $autocreated++; } } CommandSave(undef,undef) if( $autocreated && AttrVal( "autocreate", "autosave", 1 ) ); Log3 $name, 2, "$name: tahoma_autocreate end, new=$autocreated"; } sub tahoma_defineCommands($) { my($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_defineCommands"; my $devices = $hash->{helper}{devices}; foreach my $device (@{$devices}) { my ($id, $fid, $devname, $define); if ($device->{deviceURL}) { $id = $device->{deviceURL}; $fid = (split("/",$id))[-1]; $devname = "tahoma_". $fid; $define = "$devname tahoma DEVICE $id"; if( defined $device->{definition}{commands}[0]{commandName} ) { my $commandlist = "dim:slider,0,1,100"; foreach my $command (@{$device->{definition}{commands}}) { $commandlist .= " " . $command->{commandName}; $commandlist .= ":noArg" if ($command->{nparams} == 0); } if( defined($modules{$hash->{TYPE}}{defptr}{"$fid"}) ) { $modules{$hash->{TYPE}}{defptr}{"$fid"}{COMMANDS} = $commandlist; Log3 $name, 4, "$name: tahoma_defineCommands fid=$fid commandlist=$commandlist"; } } } } } sub tahoma_parseLogin($$) { my($hash, $json) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_parseLogin"; if (defined $json->{errorResponse}) { $hash->{logged_in} = 0; $hash->{STATE} = $json->{errorResponse}{message}; } else { $hash->{inVersion} = $json->{version}; $hash->{logged_in} = 1; } Log3 $name, 2, "$name: login end, logged_in=".$hash->{logged_in}; } sub tahoma_parseGetEvents($$) { my($hash, $json) = @_; my $name = $hash->{NAME}; Log3 $name, 5, "$name: tahoma_parseGetEvent"; $hash->{refresh_event} = $json; if( $hash->{logged_in} ) { $hash->{STATE} = "Connected"; } else { $hash->{STATE} = "Disconnected"; } if( ref $json eq 'ARRAY' ) { #print Dumper($json); foreach my $devices ( @{$json} ) { if( defined($devices->{deviceURL}) ) { #print "\nDevice=$devices->{deviceURL} found\n"; my $id = $devices->{deviceURL}; my $fid = (split("/",$id))[-1]; my $devname = "tahoma_". $fid; my $d = $modules{$hash->{TYPE}}{defptr}{"$fid"}; if( defined($d) )# && $d->{NAME} eq $devname ) { #print "\nDevice=$devices->{deviceURL} updated\n"; readingsBeginUpdate($d); foreach my $state (@{$devices->{deviceStates}}) { #print "$devname $state->{name} = $state->{value}\n"; next if (!defined($state->{name}) || !defined($state->{value})); if ($state->{name} eq "core:ClosureState") { $state->{value} = 100 - $state->{value} if ($attr{$d->{NAME}}{levelInvert}); readingsBulkUpdate($d, "state", "dim".$state->{value}); } elsif ($state->{name} eq "core:OpenClosedState") { readingsBulkUpdate($d, "devicestate", $state->{value}); } readingsBulkUpdate($d, (split(":",$state->{name}))[-1], $state->{value}); } my ($seconds) = gettimeofday(); readingsBulkUpdate( $d, ".lastupdate", $seconds, 0 ); readingsEndUpdate($d,1); } } } } } sub tahoma_parseApplyRequest($$) { my($hash, $json) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_parseApplyRequest"; if (defined($json->{applyResponse}{apply}{execId})) { $hash->{InExecId} = $json->{applyResponse}{apply}{execId}; } else { $hash->{InExecId} = "undefined"; } } sub tahoma_parseGetSetup($$) { my($hash, $json) = @_; my $name = $hash->{NAME}; $hash->{gatewayId} = $json->{setup}{gateways}[0]{gatewayId}; my @devices = (); foreach my $device (@{$json->{setup}{devices}}) { Log3 $name, 4, "$name: tahoma_parseGetSetup device = $device->{label}"; push( @devices, $device ); } $hash->{helper}{devices} = \@devices; if ($json->{setup}{place}) { my $places = $json->{setup}{place}; #Log3 $name, 4, "$name: tahoma_parseGetSetup places= " . Dumper($places); tahoma_parseGetSetupPlaces($hash, $places); } tahoma_autocreate($hash); tahoma_updateDevices($hash); tahoma_defineCommands($hash); } sub tahoma_parseGetSetupPlaces($$) { my($hash, $places) = @_; my $name = $hash->{NAME}; #Log3 $name, 4, "$name: tahoma_parseGetSetupPlaces " . Dumper($places); my $devices = $hash->{helper}{devices}; if (ref $places eq 'ARRAY') { #Log3 $name, 4, "$name: tahoma_parseGetSetupPlaces isArray"; foreach my $place (@{$places}) { push( @{$devices}, $place ); my $placesNext = $place->{place}; tahoma_parseGetSetupPlaces($hash, $placesNext ) if ($placesNext); } } else { #Log3 $name, 4, "$name: tahoma_parseGetSetupPlaces isScalar"; push( @{$devices}, $places ); my $placesNext = $places->{place}; tahoma_parseGetSetupPlaces($hash, $placesNext) if ($placesNext); } } sub tahoma_parseGetActionGroups($$) { my($hash, $json) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_parseGetActionGroups"; my $devices = $hash->{helper}{devices}; foreach my $action (@{$json->{actionGroups}}) { push( @{$devices}, $action ); } tahoma_autocreate($hash); tahoma_updateDevices($hash); tahoma_defineCommands($hash); } sub tahoma_parseRefreshAllStates($$) { my($hash, $json) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_parseRefreshAllStates"; } sub tahoma_parseGetStates($$) { my($hash, $states) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_parseGetStates"; if( defined($states->{devices}) ) { foreach my $devices ( @{$states->{devices}} ) { if( defined($devices->{deviceURL}) ) { my $id = $devices->{deviceURL}; my $fid = (split("/",$id))[-1]; my $devname = "tahoma_". $fid; my $d = $modules{$hash->{TYPE}}{defptr}{"$fid"}; if( defined($d) )# && $d->{NAME} eq $devname ) { readingsBeginUpdate($d); foreach my $state (@{$devices->{states}}) { next if (!defined($state->{name}) || !defined($state->{value})); if ($state->{name} eq "core:ClosureState") { $state->{value} = 100 - $state->{value} if ($attr{$d->{NAME}}{levelInvert}); readingsBulkUpdate($d, "state", "dim".$state->{value}); } elsif ($state->{name} eq "core:OpenClosedState") { readingsBulkUpdate($d, "devicestate", $state->{value}); } readingsBulkUpdate($d, (split(":",$state->{name}))[-1], $state->{value}); } my ($seconds) = gettimeofday(); readingsBulkUpdate( $d, ".lastupdate", $seconds, 0 ); readingsEndUpdate($d,1); } } } } } sub tahoma_parseEnduserAPISetupGateways($$) { my($hash, $json) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_parseEnduserAPISetupGateways"; eval { $hash->{inGateway} = $json->{result}; }; eval { $hash->{inGateway} = $json->[0]{gatewayId}; }; } sub tahoma_parseGetCurrentExecutions($$) { my($hash, $json) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_parseGetCurrentExecutions"; } sub tahoma_parseScheduleActionGroup($$) { my($hash, $json) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: tahoma_parseScheduleActionGroup"; } sub tahoma_Get($$@) { my ($hash, $name, $cmd) = @_; my $list; if( $hash->{SUBTYPE} eq "DEVICE" ) { $list = "updateAll:noArg"; if( $cmd eq "updateAll" ) { my ($seconds) = gettimeofday(); $hash->{refreshStateTimer} = $seconds; return undef; } } elsif( $hash->{SUBTYPE} eq "SCENE" || $hash->{SUBTYPE} eq "PLACE" ) { $list = ""; } elsif( $hash->{SUBTYPE} eq "ACCOUNT" ) { $list = "devices:noArg reset:noArg"; if( $cmd eq "devices" ) { my $devices = tahoma_getDevices($hash,0); my $ret; foreach my $device (@{$devices}) { $ret .= "$device->{deviceURL}\t".$device->{label}."\t$device->{uiClass}\t$device->{controllable}\t\n" if ($device->{deviceURL}); $ret .= "$device->{oid}\t".$device->{label}."\n" if ($device->{oid}); } $ret = "id\t\t\t\tname\t\t\tuiClass\t\tcontrollable\n" . $ret if( $ret ); $ret = "no devices found" if( !$ret ); return $ret; } elsif( $cmd eq "reset" ) { HttpUtils_Close($hash); $hash->{logged_in} = undef; return "connection closed"; } } return "Unknown argument $cmd, choose one of $list"; } sub tahoma_Set($$@) { my ($hash, $name, $cmd, $val) = @_; my $list = ""; if( $hash->{SUBTYPE} eq "DEVICE" || $hash->{SUBTYPE} eq "PLACE" ) { $list = "dim:slider,0,1,100 setClosure open:noArg close:noArg my:noArg stop:noArg"; $list = $hash->{COMMANDS} if (defined $hash->{COMMANDS}); $cmd = "setClosure" if( $cmd eq "dim" ); my @commands = split(" ",$list); foreach my $command (@commands) { if( $cmd eq (split(":",$command))[0]) { tahoma_applyRequest($hash,$cmd,$val); return undef; } } } if( $hash->{SUBTYPE} eq "SCENE") { $list = "start:noArg startAt"; if( $cmd eq "start" ) { tahoma_scheduleActionGroup($hash,0); return undef; } if( $cmd eq "startAt" ) { tahoma_scheduleActionGroup($hash,$val); return undef; } } return "Unknown argument $cmd, choose one of $list"; } sub tahoma_Attr($$$) { my ($cmd, $name, $attrName, $attrVal) = @_; my $orig = $attrVal; $attrVal = int($attrVal) if($attrName eq "interval"); $attrVal = 60*5 if($attrName eq "interval" && $attrVal < 60*5 && $attrVal != 0); if( $attrName eq "interval" ) { my $hash = $defs{$name}; $hash->{INTERVAL} = $attrVal; $hash->{INTERVAL} = 60*5 if( !$attrVal ); } elsif( $attrName eq "disable" ) { my $hash = $defs{$name}; RemoveInternalTimer($hash); if( $cmd eq "set" && $attrVal ne "0" ) { } else { $attr{$name}{$attrName} = 0; } } elsif( $attrName eq "blocking" ) { my $hash = $defs{$name}; $hash->{BLOCKING} = $attrVal; } elsif( $attrName eq "placeClasses" ) { my $hash = $defs{$name}; $hash->{inClass} = $attrVal if $hash->{SUBTYPE} eq "PLACE"; } elsif ( $attrName eq "levelInvert" ) { my $hash = $defs{$name}; my $device = tahoma_getDeviceDetail( $hash, $hash->{device} ); $device->{levelInvert} = $attrVal if (defined $device); } if( $cmd eq "set" ) { if( $orig ne $attrVal ) { $attr{$name}{$attrName} = $attrVal; return $attrName ." set to ". $attrVal; } } return; } sub tahoma_UserAgent_NonblockingGet($) { my ($param) = @_; my ($hash) = $param->{hash}; my $name = $hash->{NAME}; Log3 $name, 5, "$name: tahoma_UserAgent_NonblockingGet page=$param->{page}"; #headers,343:29:4:Host,18:www.tahomalink.com,]34:16:Proxy-Connection,10:keep-alive,]36:15:Accept-Encoding,13:gzip, deflate,]53:12:Content-Type,33:application/x-www-form-urlencoded,]23:14:Content-Length,2:49,]27:15:Accept-Language,5:de-de,]15:6:Accept,3:*/*,]28:10:Connection,10:keep-alive,]62:10:User-Agent,44:TaHoma/7845 CFNetwork/758.3.15 Darwin/15.4.0,]] $param->{header} = {'User-Agent' => $hash->{userAgent} }; #, 'Accept-Language' => "de-de", 'Accept-Encoding' => "gzip, deflate"}; $param->{header}{Cookie} = $hash->{HTTPCookies} if ($hash->{HTTPCookies}); $param->{compress} = 1; $param->{keepalive} = 1; $param->{url} = $hash->{url} . $param->{page}; $param->{url} .= $param->{subPage} if ($param->{subPage}); $hash->{request_active} = 1; $hash->{request_time} = time; if ($param->{blocking}) { my($err,$data) = HttpUtils_BlockingGet($param); $param->{callback}($param, $err, $data, length $data) if($param->{callback}); } else { my($err,$data) = HttpUtils_NonblockingGet($param); } } sub tahoma_encode_utf8($) { my ($text) = @_; $text =~ s/Ä/Ae/g; $text =~ s/Ö/Oe/g; $text =~ s/Ü/Ue/g; $text =~ s/ä/ae/g; $text =~ s/ö/oe/g; $text =~ s/ü/ue/g; $text =~ s/ß/ss/g; return $text; } sub tahoma_GetCookies($$) { my ($hash, $header) = @_; my $name = $hash->{NAME}; Log3 $name, 5, "$name: tahoma_GetCookies looking for Cookies in header"; foreach my $cookie ($header =~ m/set-cookie: ?(.*)/gi) { Log3 $name, 5, "$name: Set-Cookie: $cookie"; $cookie =~ /([^,; ]+)=([^,; ]+)[;, ]*(.*)/; Log3 $name, 4, "$name: Cookie: $1 Wert $2 Rest $3"; $hash->{HTTPCookieHash}{$1}{Value} = $2; $hash->{HTTPCookieHash}{$1}{Options} = ($3 ? $3 : ""); } $hash->{HTTPCookies} = join ("; ", map ($_ . "=".$hash->{HTTPCookieHash}{$_}{Value}, sort keys %{$hash->{HTTPCookieHash}})); } 1; =pod =item summary commumication modul for io-homecontrol® gateway TaHoma® =item summary_DE Kommunicationsmodul füer io-homecontrol® Gateway TaHoma® =begin html

tahoma

    The module realizes the communication with io-homecontrol® Devices e.g. from Somfy® or Velux®
    A registered TaHoma® Connect gateway from Overkiz® sold by Somfy® which is continously connected to the internet is necessary for the module.


    Notes:
    Define
      define <name> tahoma ACCOUNT <username> <password>
      define <name> tahoma DEVICE <DeviceURL>
      define <name> tahoma PLACE <oid>
      define <name> tahoma SCENE <oid>


      A definition is only necessary for a tahoma device:
      define <name> tahoma ACCOUNT <username> <password>
      If a tahoma device of the type ACCOUNT is created, all other devices acessable by the tahoma gateway are automaticaly created!
      If the account is valid, the setup will be read from the server.
      All registrated devices are automatically created with name tahoma_12345 (device number 12345 is used from setup)
      All defined rooms will be are automatically created.
      Also all defined scenes will be automatically created.


      global Attributes for ACCOUNT:
        If autocreate is disabled, no devices, places and scenes will be created automatically:
        attr autocreate disable

      local Attributes for ACCOUNT:
        Normally, the web commands will be send asynchron, and this can be forced to wait of the result by blocking=1
        attr tahoma1 blocking 1

      local Attributes for DEVICE:
        If the closure value 0..100 should be 100..0, the level can be inverted:
        attr tahoma_23234545 levelInvert 1

      local Attributes for PLACE:
        The commands in a room will only affect the devices in the room with inClass=RollerShutter.
        This can be extend or changed by setting the placeClasses attribut:
        attr tahoma_abc12345 placeClasses RollerShutter ExteriorScreen Window

      Examples:
        define tahoma1 tahoma ACCOUNT abc@test.com myPassword
        attr tahoma1 blocking 0
        attr tahoma1 room tahoma


        Automatic created device e.g.:
        define tahoma_23234545 tahoma DEVICE io://0234-5678-9012/23234545
        attr tahoma_23234545 IODev tahoma1
        attr tahoma_23234545 alias RollerShutter Badezimmer
        attr tahoma_23234545 room tahoma
        attr tahoma_23234545 webCmd dim


        Automatic created place e.g.:
        define tahoma_abc12345 tahoma PLACE abc12345-0a23-0b45-0c67-d5e6f7a1b2c3
        attr tahoma_abc12345 IODev tahoma1
        attr tahoma_abc12345 alias room Wohnzimmer
        attr tahoma_abc12345 room tahoma


        Automatic created scene e.g.:
        define tahoma_4ef30a23 tahoma SCENE 4ef30a23-0b45-0c67-d5e6-f7a1b2c32e3f
        attr tahoma_4ef30a23 IODev tahoma1
        attr tahoma_4ef30a23 alias scene Rolladen Südfenster zu
        attr tahoma_4ef30a23 room tahoma

=end html =cut