diff --git a/fhem/CHANGED b/fhem/CHANGED index d256202ac..054c0159d 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. + - new: 34_ESPEasy.pm: initial check in - bugfix: 38_netatmo.pm: fixed error crash - update: 32_withings.pm: added unknown values for body scale - update: 98_DOIFtools: add CSRF-Token, add DOIFtoolsLogDir diff --git a/fhem/FHEM/34_ESPEasy.pm b/fhem/FHEM/34_ESPEasy.pm new file mode 100644 index 000000000..2792f3cc5 --- /dev/null +++ b/fhem/FHEM/34_ESPEasy.pm @@ -0,0 +1,2980 @@ +# $Id$ +################################################################################ +# +# 34_ESPEasy.pm is a FHEM Perl module to control ESP8266 /w ESPEasy +# +# Copyright 2017 by dev0 +# FHEM forum: https://forum.fhem.de/index.php?action=profile;u=7465 +# +# 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 . +# +################################################################################ + +package main; + +use strict; +use warnings; +use Data::Dumper; +use MIME::Base64; +use TcpServerUtils; +use HttpUtils; +use Color; + +# ------------------------------------------------------------------------------ +# global/default values +# ------------------------------------------------------------------------------ +my $module_version = 1.00; # Version of this module +my $minEEBuild = 128; # informational +my $minJsonVersion = 1.02; # checked in received data + +my $d_Interval = 300; # interval +my $d_httpReqTimeout = 10; # timeout http req +my $d_colorpickerCTww = 2000; # color temp for ww (kelvin) +my $d_colorpickerCTcw = 6000; # color temp for cw (kelvin) + +my $d_maxHttpSessions = 3; # concurrent connects to a single esp +my $d_maxQueueSize = 250; # max queue size, +my $d_resendFailedCmd = 0; # resend failed http requests by default? + +# ------------------------------------------------------------------------------ +# "setCmds" => "min. number of parameters" +# ------------------------------------------------------------------------------ +my %ESPEasy_setCmds = ( + "gpio" => "2", + "pwm" => "2", + "pwmfade" => "3", + "pulse" => "3", + "longpulse" => "3", + "servo" => "3", + "lcd" => "3", + "lcdcmd" => "1", + "mcpgpio" => "2", + "oled" => "3", + "oledcmd" => "1", + "pcapwm" => "2", + "pcfgpio" => "2", + "pcfpulse" => "3", + "pcflongpulse" => "3", + "irsend" => "3", + "status" => "2", + "raw" => "1", + "reboot" => "0", + "erase" => "0", + "reset" => "0", + "statusrequest" => "0", + "clearreadings" => "0", + "help" => "1", + "lights" => "1", + "dots" => "1", +); + +# ------------------------------------------------------------------------------ +# "setCmds" => "syntax", ESPEasy_paramPos() will parse for some <.*> positions +# ------------------------------------------------------------------------------ +my %ESPEasy_setCmdsUsage = ( + "gpio" => "gpio <0|1|off|on>", + "pwm" => "pwm ", + "pulse" => "pulse <0|1|off|on> ", + "longpulse" => "longpulse <0|1|off|on> ", + "servo" => "servo ", + "lcd" => "lcd ", + "lcdcmd" => "lcdcmd ", + "mcpgpio" => "mcpgpio <0|1|off|on>", + "oled" => "oled ", + "oledcmd" => "oledcmd ", + "pcapwm" => "pcapwm ", + "pcfgpio" => "pcfgpio <0|1|off|on>", + "pcfpulse" => "pcfpulse <0|1|off|on> ", #missing docu + "pcflongpulse" => "pcflongPulse <0|1|off|on> ",#missing docu + "status" => "status ", + #https://forum.fhem.de/index.php/topic,55728.msg480966.html#msg480966 + "pwmfade" => "pwmfade ", + #https://forum.fhem.de/index.php/topic,55728.msg530220.html#msg530220 + "irsend" => "irsend ", + "raw" => "raw <...>", + "reboot" => "reboot", + "erase" => "erase", + "reset" => "reset", + "statusrequest" => "statusRequest", + "clearreadings" => "clearReadings", + "help" => "help <".join("|", sort keys %ESPEasy_setCmds).">", + "lights" => "light [color] [fading time] [pct]", + "dots" => "dots ", + + #Lights + "rgb" => "rgb [fading time]", + "pct" => "pct [fading time]", + "ct" => "ct [fading time] [pct bri]", + "on" => "on [fading time]", + "off" => "off [fading time]", + "toggle" => "toggle [fading time]" + +); + +# ------------------------------------------------------------------------------ +# Bridge "setCmds" => "min. number of parameters" +# ------------------------------------------------------------------------------ +my %ESPEasy_setBridgeCmds = ( + "user" => "0", + "pass" => "0", + "clearqueue" => "0", + "help" => "1" +); + +# ------------------------------------------------------------------------------ +# "setBridgeCmds" => "syntax", ESPEasy_paramPos() parse for some <.*> positions +# ------------------------------------------------------------------------------ +my %ESPEasy_setBridgeCmdsUsage = ( + "user" => "user ", + "pass" => "pass ", + "clearqueue" => "clearqueue", + "help" => "help <".join("|", sort keys %ESPEasy_setBridgeCmds).">" +); + +# ------------------------------------------------------------------------------ +# pin names can be used instead of gpio numbers. +# ------------------------------------------------------------------------------ +my %ESPEasy_pinMap = ( + "D0" => 16, + "D1" => 5, + "D2" => 4, + "D3" => 0, + "D4" => 2, + "D5" => 14, + "D6" => 12, + "D7" => 13, + "D8" => 15, + "D9" => 3, + "D10" => 1, + + "RX" => 3, + "TX" => 1, + "SD2" => 9, + "SD3" => 10 +); + +# ------------------------------------------------------------------------------ +#grep ^sub 34_ESPEasy.pm | awk '{print $1" "$2";"}' + +# ------------------------------------------------------------------------------ +sub ESPEasy_Initialize($) +{ + my ($hash) = @_; + + #common + $hash->{DefFn} = "ESPEasy_Define"; + $hash->{GetFn} = "ESPEasy_Get"; + $hash->{SetFn} = "ESPEasy_Set"; + $hash->{AttrFn} = "ESPEasy_Attr"; + $hash->{UndefFn} = "ESPEasy_Undef"; + $hash->{ShutdownFn} = "ESPEasy_Shutdown"; + $hash->{DeleteFn} = "ESPEasy_Delete"; + $hash->{RenameFn} = "ESPEasy_Rename"; + $hash->{NotifyFn} = "ESPEasy_Notify"; + + #provider + $hash->{ReadFn} = "ESPEasy_Read"; #ESP http request will be parsed here + $hash->{WriteFn} = "ESPEasy_Write"; #called from logical module's IOWrite + $hash->{Clients} = ":ESPEasy:"; #used by dispatch,$hash->{TYPE} of receiver + my %matchList = ( "1:ESPEasy" => ".*" ); + $hash->{MatchList} = \%matchList; + + #consumer + $hash->{ParseFn} = "ESPEasy_dispatchParse"; + $hash->{Match} = ".+"; + + $hash->{AttrList} = "allowedIPs " + ."authentication:1,0 " + ."autocreate:1,0 " + ."autosave:1,0 " + ."colorpicker:RGB,HSV,HSVp " + ."deniedIPs " + ."disable:1,0 " + ."do_not_notify:0,1 " + ."httpReqTimeout " + ."IODev " + ."Interval " + ."adjustValue " + ."parseCmdResponse " + ."pollGPIOs " + ."presenceCheck:1,0 " + ."readingPrefixGPIO " + ."readingSuffixGPIOState " + ."readingSwitchText:1,0 " + ."setState:0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,100 " + ."combineDevices " + ."rgbGPIOs " + ."maxQueueSize:10,25,50,100,250,500,1000,2500,5000,10000,25000,50000,100000 " + ."maxHttpSessions:0,1,2,3,4,5,6,7,8,9 " + ."resendFailedCmd:0,1 " + ."mapLightCmds " + ."colorpickerCTww " + ."colorpickerCTcw " +# ."wwcwGPIOs " +# ."wwcwMaxBri:0,1 " +# ."ctWW_reducedRange " +# ."ctCW_reducedRange " + .$readingFnAttributes; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_Define($$) # only called when defined, not on reload. +{ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + my $usg = "\nUse 'define ESPEasy ". + "\nUse 'define ESPEasy "; + return "Wrong syntax: $usg" if(int(@a) < 3); + + my $name = $a[0]; + my $type = $a[1]; + my $host = $a[2]; + my $port = $a[3] if defined $a[3]; + my $iodev = $a[4] if defined $a[4]; + my $ident = $a[5] if defined $a[5]; + + return "ERROR: only 1 ESPEasy bridge can be defined!" + if($host eq "bridge" && $modules{ESPEasy}{defptr}{BRIDGE}); + return "ERROR: missing arguments for subtype device: $usg" + if ($host ne "bridge" && !(defined $a[4]) && !(defined $a[5])); + return "ERROR: too much arguments for a bridge: $usg" + if ($host eq "bridge" && defined $a[4]); + return "ERROR: perl module JSON is not installed" + if (ESPEasy_isPmInstalled($hash,"JSON")); + + (ESPEasy_isIPv4($host) || ESPEasy_isFqdn($host) || $host eq "bridge") + ? $hash->{HOST} = $host + : return "ERROR: invalid IPv4 address, fqdn or keyword bridge: '$host'"; + + # check fhem.pl version (internalTimer modifications are required) + # https://forum.fhem.de/index.php/topic,55728.msg497094.html#msg497094 + AttrVal('global','version','') =~ m/^fhem.pl:(\d+)\/.*$/; + return "ERROR: fhem.pl is too old to use $type module." + ." Version 11000/2016-03-05 is required at least." + if (not(defined $1) || $1 < 11000); + + $hash->{PORT} = $port if defined $port; + $hash->{IDENT} = $ident if defined $ident; + $hash->{VERSION} = $module_version; + $hash->{NOTIFYDEV} = "global"; + + #--- BRIDGE ------------------------------------------------- + if ($hash->{HOST} eq "bridge") { + $hash->{SUBTYPE} = "bridge"; + $modules{ESPEasy}{defptr}{BRIDGE} = $hash; + Log3 $hash->{NAME}, 2, "$type $name: Opening bridge on port tcp/$port (v$module_version)"; + ESPEasy_tcpServerOpen($hash); + if ($init_done && !defined($hash->{OLDDEF})) { + #if (not defined getKeyValue($type."_".$name."-firstrun")) { + CommandAttr(undef,"$name room $type"); + CommandAttr(undef,"$name group $type Bridge"); + CommandAttr(undef,"$name authentication 0"); + CommandAttr(undef,"$name combineDevices 0"); + setKeyValue($type."_".$name."-firstrun","done"); + } + # only informational + my $u = getKeyValue($type."_".$name."-user"); + $hash->{USER} = (defined $u) ? $u : "not defined yet !!!"; + my $p = getKeyValue($type."_".$name."-pass"); + $hash->{PASS} = (defined $p) ? "*" x length($p) : "not defined yet !!!"; + + $hash->{MAX_HTTP_SESSIONS} = $d_maxHttpSessions; + $hash->{MAX_QUEUE_SIZE} = $d_maxQueueSize; + + ESPEasy_removeGit($hash); + } + + #--- DEVICE ------------------------------------------------- + else { + $hash->{INTERVAL} = $d_Interval; + $hash->{SUBTYPE} = "device"; + $modules{$type}{defptr}{$ident} = $hash; + AssignIoPort($hash,$iodev) if(not defined $hash->{IODev}); + InternalTimer(gettimeofday()+5+rand(5), "ESPEasy_statusRequest", $hash); + readingsSingleUpdate($hash, 'state', 'opened',1); + my $io = (defined($hash->{IODev}{NAME})) ? $hash->{IODev}{NAME} : "none"; + Log3 $hash->{NAME}, 4, "$type $name: Opened for $ident $host:$port using bridge $io"; + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_Get($@) +{ + my ($hash, @a) = @_; + return "argument is missing" if(int(@a) != 2); + + my $reading = $a[1]; + my $ret; + if ($reading =~ m/^pinmap$/i && $hash->{SUBTYPE} eq "device") { + $ret .= "\nName => GPIO\n"; + $ret .= "------------\n"; + foreach (sort keys %ESPEasy_pinMap) { + $ret .= $_." " x (5-length $_ ) ."=> $ESPEasy_pinMap{$_}\n"; + } + return $ret; + + } elsif (lc $reading =~ m/^user|pass$/i && $hash->{SUBTYPE} eq "bridge") { + $ret .= getKeyValue($hash->{TYPE}."_".$hash->{NAME}."-".lc $reading); + return $ret; + + } elsif (lc $reading =~ m/^queueSize$/i && $hash->{SUBTYPE} eq "bridge") { + foreach (keys %{ $hash->{helper}{queue} }) { + $ret .= "$_:".scalar @{$hash->{helper}{queue}{"$_"}}." "; + } + return $ret; + + } elsif (exists($hash->{READINGS}{$reading})) { + return defined($hash->{READINGS}{$reading}) + ? $hash->{READINGS}{$reading}{VAL} + : "reading $reading exists but has no value defined"; + + } else { + $ret = "unknown argument $reading, choose one of"; + foreach my $reading (sort keys %{$hash->{READINGS}}) { + $ret .= " $reading:noArg"; + } + + return ($hash->{SUBTYPE} eq "bridge") + ? $ret . " user:noArg pass:noArg queueSize:noArg" + : $ret . " pinMap:noArg"; + } +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_Set($$@) +{ + my ($hash, $name, $cmd, @params) = @_; + my ($type,$self) = ($hash->{TYPE},ESPEasy_whoami()); + $cmd = lc($cmd) if $cmd; + + return if (IsDisabled $name); + + Log3 $name, 3, "$type $name: set $name $cmd ".join(" ",@params) + if $cmd !~ m/^(\?|user|pass)$/; + + # ----- BRDIGE ---------------------------------------------- + if ($hash->{SUBTYPE} eq "bridge") { + + # are there all required argumets? + if($ESPEasy_setBridgeCmds{$cmd} + && scalar @params < $ESPEasy_setBridgeCmds{$cmd}) { + Log3 $name, 2, "$type $name: Missing argument: 'set $name $cmd " + .join(" ",@params)."'"; + return "Missing argument: $cmd needs at least " + ."$ESPEasy_setBridgeCmds{$cmd} parameter(s)\n" + ."Usage: 'set $name $ESPEasy_setBridgeCmdsUsage{$cmd}'"; + } + + # handle unknown cmds + if(!exists $ESPEasy_setBridgeCmds{$cmd}) { + my @cList = sort keys %ESPEasy_setBridgeCmds; + my $clist = join(" ", @cList); + my $hlist = join(",", @cList); + $clist =~ s/help/help:$hlist/; # add all cmds as params to help cmd + return "Unknown argument $cmd, choose one of ". $clist; + } + + if ($cmd eq "help") { + my $usage = $ESPEasy_setBridgeCmdsUsage{$params[0]}; + $usage =~ s/Note:/\nNote:/g; + return "Usage: set $name $usage"; + } + + elsif ($cmd =~ m/^clearqueue$/i) { + delete $hash->{helper}{queue}; + Log3 $name, 3, "$type $name: Queues erased."; + return undef; + } + + elsif ($cmd =~ m/^user|pass$/ ) { + setKeyValue($hash->{TYPE}."_".$hash->{NAME}."-".$cmd,$params[0]); + # only informational + if (defined $params[0]) { + $hash->{uc($cmd)} = ($cmd eq "user") ? $params[0] + : "*" x length($params[0]); + } else { + $hash->{uc($cmd)} = "not defined yet !!!"; + } + } + } + + # ----- DEVICE ---------------------------------------------- + else { + # cmds are included in hash + ESPEasy_adjustSetCmds($hash); + + # are there all required argumets? + if($ESPEasy_setCmds{$cmd} && scalar @params < $ESPEasy_setCmds{$cmd}) { + Log3 $name, 2, "$type $name: Missing argument: " + ."'set $name $cmd ".join(" ",@params)."'"; + return "Missing argument: $cmd needs at least $ESPEasy_setCmds{$cmd} ". + "parameter(s)\n"."Usage: 'set $name $ESPEasy_setCmdsUsage{$cmd}'"; + } + + + #Lights Plugin + if (defined AttrVal($name,"mapLightCmds",undef) && $cmd =~ m/^(ct|pct|rgb|on|off|toggle)$/i) { + unshift @params, $cmd; + $cmd = lc AttrVal($name,"mapLightCmds",""); +# Log 1, "cmd: $cmd params: ".join(",",@params); + } + else { + # enable ct|pct commands if attr wwcwGPIOs is set + if (AttrVal($name,"wwcwGPIOs",0) && $cmd =~ m/^(ct|pct)$/i) { + my $ret = ESPEasy_setCT($hash,$cmd,@params); + return $ret if ($ret); + } + # enable rgb commands if attr rgbGPIOs is set + if (AttrVal($name,"rgbGPIOs",0) && $cmd =~ m/^(rgb|on|off|toggle)$/i) { + my $ret = ESPEasy_setRGB($hash,$cmd,@params); + return $ret if ($ret); + } + } #else + + # handle unknown cmds + if (!exists $ESPEasy_setCmds{$cmd}) { + my @cList = sort keys %ESPEasy_setCmds; + my $clist = join(" ", @cList); + my $hlist = join(",", @cList); + foreach (@cList) {$clist =~ s/ $_/ $_:noArg/ if $ESPEasy_setCmds{$_} == 0} + # expand rgb + my $cp = AttrVal($name,"colorpicker","HSVp"); + $clist =~ s/rgb/rgb:colorpicker,$cp/; # add colorPicker if rgb cmd is available + # expand ct + my $ct = "ct:colorpicker,CT," + .AttrVal($name,"ctWW_reducedRange",AttrVal($name,"colorpickerCTww",$d_colorpickerCTww)) + .",10," + .AttrVal($name,"ctCW_reducedRange",AttrVal($name,"colorpickerCTcw",$d_colorpickerCTcw)); + $clist =~ s/ct /$ct /; + # expand pct + my $pct = "pct:colorpicker,BRI,0,1,100"; + $clist =~ s/pct /$pct /; + # expand help + $clist =~ s/help/help:$hlist/; + Log3 $name, 2, "$type $name: Unknown set command $cmd" if $cmd ne "?"; + return "Unknown argument $cmd, choose one of ". $clist; + } + + # pin mapping (eg. D8 -> 15) + my $pp = ESPEasy_paramPos($cmd,''); + if ($pp && $params[$pp-1] =~ m/^[a-zA-Z]/) { + Log3 $name, 5, "$type $name: Pin mapping ". uc $params[$pp-1] . + " => $ESPEasy_pinMap{uc $params[$pp-1]}"; + $params[$pp-1] = $ESPEasy_pinMap{uc $params[$pp-1]}; + } + + # onOff mapping (on/off -> 1/0) + $pp = ESPEasy_paramPos($cmd,'<0|1|off|on>'); + if ($pp && not($params[$pp-1] =~ m/^(0|1)$/)) { + my $state; + if ($params[$pp-1] =~ m/^off$/i) { + $state = 0; + } + elsif ($params[$pp-1] =~ m/^on$/i) { + $state = 1; + } + else { + Log3 $name, 2, "$type $name: $cmd ".join(" ",@params)." => unknown argument: '$params[$pp-1]'"; + return undef; + } + Log3 $name, 5, "$type $name: onOff mapping ". $params[$pp-1]." => $state"; + $params[$pp-1] = $state; + } + + if ($cmd eq "help") { + my $usage = $ESPEasy_setCmdsUsage{$params[0]}; + $usage =~ s/Note:/\nNote:/g; + return "Usage: set $name $usage"; + } + + if ($cmd eq "statusrequest") { + ESPEasy_statusRequest($hash); + return undef; + } + + if ($cmd eq "clearreadings") { + ESPEasy_clearReadings($hash); + return undef; + } + + Log3 $name, 5, "$type $name: IOWrite(\$defs{$hash->{NAME}}, $hash->{HOST}, $hash->{PORT}, ". + "$hash->{IDENT}, $cmd, ".join(",",@params).")"; + + Log3 $name, 2, "$type $name: Device seems to be in sleep mode, sending command nevertheless." + if (defined $hash->{SLEEP} && $hash->{SLEEP} ne "0"); + + my $parseCmd = ESPEasy_isParseCmd($hash,$cmd); # should response be parsed and dispatched + IOWrite($hash, $hash->{HOST}, $hash->{PORT}, $hash->{IDENT}, $parseCmd, $cmd, @params); + + } # DEVICE + +return undef +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_Read($) { + + my ($hash) = @_; #hash of temporary child instance + my $name = $hash->{NAME}; + my $bhash = $modules{ESPEasy}{defptr}{BRIDGE}; #hash of original instance + my $bname = $bhash->{NAME}; + my $btype = $bhash->{TYPE}; + $Data::Dumper::Indent = 0; + $Data::Dumper::Terse = 1; + + # Accept and create a child + if( $hash->{SERVERSOCKET} ) { + my $aRet = TcpServer_Accept($hash,"ESPEasy"); + return; + } + + # use received IP instead of configured one (NAT/PAT could have modified) + my $peer = $hash->{PEER}; + + # Read 1024 byte of data + my $buf; + my $ret = sysread($hash->{CD}, $buf, 1024); + + # If there is an error in connection return + if( !defined($ret ) || $ret <= 0 ) { + CommandDelete( undef, $hash->{NAME} ); + return; + } + + $bhash->{SESSIONS} = scalar devspec2array("TYPE=$btype:FILTER=TEMPORARY=1")-1; + + # Check attr disabled + return if (IsDisabled $bname); + + # Check allowed IPs + if ( !( ESPEasy_isPeerAllowed($peer,AttrVal($bname,"allowedIPs",1)) && + !ESPEasy_isPeerAllowed($peer,AttrVal($bname,"deniedIPs",0)) ) ) { + Log3 $bname, 2, "$btype $name: Peer address rejected"; + return; + } + Log3 $bname, 4, "$btype $name: Peer address accepted"; + + my @data = split( '\R\R', $buf ); + my $header = ESPEasy_header2Hash($data[0]); + + # mask password in authorization header with **** + my $logHeader = { %$header }; + $logHeader->{Authorization} =~ s/Basic\s.*\s/Basic ***** / if defined $logHeader->{Authorization}; + # Dump logHeader + Log3 $bname, 5, "$btype $name: Received header: ".Dumper($logHeader) if defined $logHeader; + # Dump content + Log3 $bname, 5, "$btype $name: Received content: $data[1]" if defined $data[1]; + + # Check content length if defined + if (defined $header->{'Content-Length'} + && $header->{'Content-Length'} != length($data[1])) { + Log3 $bname, 2, "$btype $name: Invalid content length ". + "($header->{'Content-Length'} != ".length($data[1]).")"; + Log3 $bname, 2, "$btype $name: Received content: $data[1]" + if defined $data[1]; + ESPEasy_sendHttpClose($hash,"400 Bad Request",""); + return; + } + + # check authorization + if (!defined ESPEasy_isAuthenticated($hash,$header->{Authorization})) { + ESPEasy_sendHttpClose($hash,"401 Unauthorized",""); + return; + } + + # No error occurred, send http respose OK to ESP + ESPEasy_sendHttpClose($hash,"200 OK",""); #if !grep(/"sleep":1/, $data[1]); + + # JSON received... + my $json; + if (defined $data[1] && $data[1] =~ m/"module":"ESPEasy"/) { + + # remove illegal chars but keep JSON relevant chars. + $data[1] =~ s/[^A-Za-z\d_\.\-\/\{}:,"]/_/g; + + eval {$json = decode_json($data[1]);1;}; + if ($@) { + Log3 $bname, 2, "$btype $name: WARNING: deformed JSON data, check your ESP config ($peer)"; + Log3 $bname, 2, "$btype $name: $@"; + return; + } + + # check that ESPEasy software is new enough + return if ESPEasy_checkVersion($bhash,$peer,$json->{data}{ESP}{build},$json->{version}); + + # should never happen, but who knows what some JSON module versions do... + $json->{data}{ESP}{name} = "" if !defined $json->{data}{ESP}{name}; + $json->{data}{SENSOR}{0}{deviceName} = "" if !defined $json->{data}{SENSOR}{0}{deviceName}; + + # remove illegal chars from ESP name for further processing and assign to new var + (my $espName = $json->{data}{ESP}{name}) =~ s/[^A-Za-z\d_\.]/_/g; + (my $espDevName = $json->{data}{SENSOR}{0}{deviceName}) =~ s/[^A-Za-z\d_\.]/_/g; + + # check that 'ESP name' or 'device name' is set + if ($espName eq "" && $espDevName eq "") { + Log3 $bname, 2, "$btype $name: WARNIING 'ESP name' and 'device name' " + ."missing ($peer). Check your ESP config. Skip processing data."; + Log3 $bname, 2, "$btype $name: Data: $data[1]"; + return; + } + + my $ident = ESPEasy_isCombineDevices($peer,$espName,AttrVal($bname,"combineDevices",0)) + ? $espName ne "" ? $espName : $peer + : $espName.($espName ne "" && $espDevName ne "" ? "_" : "").$espDevName; + + # push internals in @values (and in bridge helper for support reason, only) + my @values; + my @intVals = qw(unit sleep build); + foreach my $intVal (@intVals) { + push(@values,"i||".$intVal."||".$json->{data}{ESP}{$intVal}."||0"); + $bhash->{helper}{received}{$peer}{$intVal} = $json->{data}{ESP}{$intVal}; + } + $bhash->{helper}{received}{$peer}{espName} = $espName; + + # push sensor value in @values + foreach my $vKey (keys %{$json->{data}{SENSOR}}) { + if(ref $json->{data}{SENSOR}{$vKey} eq ref {} + && exists $json->{data}{SENSOR}{$vKey}{value}) { + # remove illegal chars + $json->{data}{SENSOR}{$vKey}{valueName} =~ s/[^A-Za-z\d_\.\-\/]/_/g; + my $dmsg = "r||".$json->{data}{SENSOR}{$vKey}{valueName} + ."||".$json->{data}{SENSOR}{$vKey}{value} + ."||".$json->{data}{SENSOR}{$vKey}{type}; + if ($dmsg =~ m/(\|\|\|\|)|(\|\|$)/) { #detect an empty value + Log3 $bname, 2, "$btype $name: WARNING: value name or value is " + ."missing ($peer). Skip processing this value."; + Log3 $bname, 2, "$btype $name: Data: $data[1]"; + next; #skip further processing for this value only + } + push(@values,$dmsg); + } + } + + ESPEasy_dispatch($hash,$ident,$peer,@values); + + } #$data[1] =~ m/"module":"ESPEasy"/ + + else { + Log3 $bname, 2, "$btype $name: WARNING: Wrong controller configured or " + ."ESPEasy Version is too old."; + Log3 $bname, 2, "$btype $name: WARNING: ESPEasy version R" + .$minEEBuild." or later required."; + } + + # session will not be close immediately if ESP goes to sleep after http send + # needs further investigation? + if ($hash->{TEMPORARY} && $json->{data}{ESP}{sleep}) { + CommandDelete(undef, $name); + } + return; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_Write($$$$$@) #called from logical's IOWrite (end of SetFn) +{ + my ($hash,$ip,$port,$ident,$parseCmd,$cmd,@params) = @_; + my ($name,$type,$self) = ($hash->{NAME},$hash->{TYPE},ESPEasy_whoami()."()"); + + if ($cmd eq "cleanup") { + delete $hash->{helper}{received}; + return undef; + } + + elsif ($cmd eq "statusrequest") { + ESPEasy_statusRequest($hash); + return undef; + } + + ESPEasy_httpReq($hash, $ip, $port, $ident, $parseCmd, $cmd, @params); +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_Notify($$) +{ + my ($hash,$dev) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + # $hash->{NOTIFYDEV} = "global" set in DefineFn + return if(!grep(m/^(DELETE)?ATTR $name /, @{$dev->{CHANGED}})); + + foreach (@{$dev->{CHANGED}}) { + if (m/^(DELETE)?ATTR ($name) (\w+)\s?(\d+)?$/) { + Log3 $name, 5, "$type $name: received event: $_"; + + if ($3 eq "disable") { + if (defined $1 || (defined $4 && $4 eq "0")) { + Log3 $name, 4,"$type $name: Device enabled"; + ESPEasy_resetTimer($hash) if ($hash->{SUBTYPE} eq "device"); + readingsSingleUpdate($hash, 'state', 'opened',1); + } + else { + Log3 $name, 3,"$type $name: Device disabled"; + ESPEasy_clearReadings($hash) if $hash->{SUBTYPE} eq "device"; + ESPEasy_resetTimer($hash,"stop"); + readingsSingleUpdate($hash, "state", "disabled",1) + } + } + + elsif ($3 eq "Interval") { + if (defined $1) { + $hash->{INTERVAL} = $d_Interval; + } + elsif (defined $4 && $4 eq "0") { + $hash->{INTERVAL} = "disabled"; + ESPEasy_resetTimer($hash,"stop"); + CommandDeleteReading(undef, "$name presence") + if defined $hash->{READINGS}{presence}; + } + else { # Interval > 0 + $hash->{INTERVAL} = $4; + ESPEasy_resetTimer($hash); + } + } + + elsif ($3 eq "setState") { + if (defined $1 || (defined $4 && $4 > 0)) { + ESPEasy_setState($hash); + } + else { #setState == 0 + CommandSetReading(undef,"$name state opened"); + } + } + + else { + #Log 5, "$type $name: Attribute $3 not handeled by NotifyFn "; + } + + } #main if + else { #should never be reached + #Log 5, "$type $name: WARNING: unexpected event received by NotifyFn: $_"; + } + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_Rename() { + my ($new,$old) = @_; + my $i = 0; + my $type = $defs{"$new"}->{TYPE}; + my $name = $defs{"$new"}->{NAME}; + my $subtype = $defs{"$new"}->{SUBTYPE}; + my @am; + + # copy values from old to new device + setKeyValue($type."_".$new."-user",getKeyValue($type."_".$old."-user")); + setKeyValue($type."_".$new."-pass",getKeyValue($type."_".$old."-pass")); + setKeyValue($type."_".$new."-firstrun",getKeyValue($type."_".$old."-firstrun")); + + # delete old entries + setKeyValue($type."_".$old."-user",undef); + setKeyValue($type."_".$old."-pass",undef); + setKeyValue($type."_".$old."-firstrun",undef); + + # replace IDENT in devices if bridge name changed + if ($subtype eq "bridge") { + foreach my $ldev (devspec2array("TYPE=$type")) { + my $dhash = $defs{$ldev}; + my $dsubtype = $dhash->{SUBTYPE}; + next if ($dsubtype eq "bridge"); + my $dname = $dhash->{NAME}; + my $ddef = $dhash->{DEF}; + my $oddef = $dhash->{DEF}; + $ddef =~ s/ $old / $new /; + if ($oddef ne $ddef){ + $i = $i+2; + CommandModify(undef, "$dname $ddef"); + CommandAttr(undef,"$dname IODev $new"); + push (@am,$dname); + } + } + } + Log3 $name, 2, "$type $name: Device $old renamed to $new"; + Log3 $name, 2, "$type $name: Attribute IODev set to '$name' in these " + ."devices: ".join(", ",@am) if $subtype eq "bridge"; + + if (AttrVal($name,"autosave",AttrVal("global","autosave",1)) && $i>0) { + CommandSave(undef,undef); + Log3 $type, 2, "$type $name: $i structural changes saved " + ."(autosave is enabled)"; + } + elsif ($i>0) { + Log3 $type, 2, "$type $name: There are $i structural changes. " + ."Don't forget to save chages."; + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_Attr(@) +{ + my ($cmd,$name,$aName,$aVal) = @_; + my $hash = $defs{$name}; + my $type = $hash->{TYPE}; + my $ret; + + if ($cmd eq "set" && !defined $aVal) { + Log3 $name, 2, "$type $name: attr $name $aName '': value must not be empty"; + return "$name: attr $aName: value must not be empty"; + } + + # device attributes + if (defined $hash->{SUBTYPE} && $hash->{SUBTYPE} eq "bridge" + && ($aName =~ m/^(Interval|pollGPIOs|IODev|setState|readingSwitchText)$/ + || $aName =~ m/^(readingPrefixGPIO|readingSuffixGPIOState|adjustValue)$/ + || $aName =~ m/^(presenceCheck|parseCmdResponse|rgbGPIOs|colorpicker)$/ + || $aName =~ m/^(wwcwGPIOs|colorpickerCTww|colorpickerCTcw|mapLightCmds)$/)) { + Log3 $name, 2, "$type $name: Attribut '$aName' can not be used by bridge"; + return "$type: attribut '$aName' cannot be used by bridge device"; + } + # bridge attributes + elsif (defined $hash->{SUBTYPE} && $hash->{SUBTYPE} eq "device" + && ($aName =~ m/^(autocreate|autosave|authentication|httpReqTimeout)$/ + || $aName =~ m/^(maxHttpSessions|maxQueueSize|resendFailedCmd)$/ + || $aName =~ m/^(allowedIPs|deniedIPs|combineDevices)$/ )) { + Log3 $name, 2, "$type $name: Attribut '$aName' can be used with " + ."bridge device, only"; + return "$type: attribut '$aName' can be used with the bridge device, only"; + } + + elsif ($aName =~ m/^(autosave|autocreate|authentication|disable)$/ + || $aName =~ m/^(presenceCheck|readingSwitchText|resendFailedCmd)$/) { + $ret = "0,1" if ($cmd eq "set" && not $aVal =~ m/^(0|1)$/)} + + elsif ($aName eq "combineDevices") { + $ret = "0 | 1 | ESPname | ip[/netmask][,ip[/netmask]][,...]" + if $cmd eq "set" && !(ESPEasy_isAttrCombineDevices($aVal) || $aVal =~ m/^[01]$/ )} + + elsif ($aName =~ m/^(allowedIPs|deniedIPs)$/) { + $ret = "ip[/netmask][,ip[/netmask]][,...]" + if $cmd eq "set" && !ESPEasy_isIPv64Range($aVal)} + + elsif ($aName =~ m/^(pollGPIOs|rgbGPIOs|wwcwGPIOs)$/) { + $ret = "GPIO_No[,GPIO_No][...]" + if $cmd eq "set" && $aVal !~ m/^[a-zA-Z]{0,2}[0-9]+(,[a-zA-Z]{0,2}[0-9]+)*$/} + + elsif ($aName eq "colorpicker") { + $ret = "RGB | HSV | HSVp" + if ($cmd eq "set" && not $aVal =~ m/^(RGB|HSV|HSVp)$/)} + + elsif ($aName =~ m/^(colorpickerCTww|colorpickerCTcw)$/) { + $ret = "1000..10000" + if $cmd eq "set" && ($aVal < 1000 || $aVal > 10000)} + + elsif ($aName eq "parseCmdResponse") { + my $cmds = lc join("|",keys %ESPEasy_setCmdsUsage); + $ret = "cmd[,cmd][...]" + if $cmd eq "set" && lc($aVal) !~ m/^($cmds){1}(,($cmds))*$/} + + elsif ($aName eq "mapLightCmds") { + my $cmds = lc join("|",keys %ESPEasy_setCmdsUsage); + $ret = "ESPEasy cmd" + if $cmd eq "set" && lc($aVal) !~ m/^($cmds){1}(,($cmds))*$/} + + elsif ($aName eq "setState") { + $ret = "integer" + if ($cmd eq "set" && not $aVal =~ m/^(\d+)$/)} + + elsif ($aName eq "readingPrefixGPIO") { + $ret = "[a-zA-Z0-9._-/]+" + if ($cmd eq "set" && $aVal !~ m/^[A-Za-z\d_\.\-\/]+$/)} + + elsif ($aName eq "readingSuffixGPIOState") { + $ret = "[a-zA-Z0-9._-/]+" + if ($cmd eq "set" && $aVal !~ m/^[A-Za-z\d_\.\-\/]+$/)} + + elsif ($aName eq "httpReqTimeout") { + $ret = "3..60 (default: $d_httpReqTimeout)" + if $cmd eq "set" && ($aVal < 3 || $aVal > 60)} + + elsif ($aName eq "maxHttpSessions") { + ($cmd eq "set" && ($aVal !~ m/^[0-9]+$/)) + ? $ret = ">= 0 (default: $d_maxHttpSessions, 0: disable queuing)" + : $hash->{MAX_HTTP_SESSIONS} = $aVal; + if ($cmd eq "del") {$hash->{MAX_HTTP_SESSIONS} = $d_maxHttpSessions} + } + + elsif ($aName eq "maxQueueSize") { + ($cmd eq "set" && ($aVal !~ m/^[1-9][0-9]+$/)) + ? $ret = ">10 (default: $d_maxQueueSize)" + : $hash->{MAX_QUEUE_SIZE} = $aVal; + if ($cmd eq "del") {$hash->{MAX_QUEUE_SIZE} = $d_maxQueueSize} + } + + elsif ($aName eq "Interval") { + ($cmd eq "set" && ($aVal !~ m/^(\d)+$/ || $aVal <10 && $aVal !=0)) + ? $ret = "0 or >=10" + : $hash->{INTERVAL} = $aVal} + + if (!$init_done) { + if ($aName =~ /^disable$/ && $aVal == 1) { + readingsSingleUpdate($hash, "state", "disabled",1); + } + } + + if (defined $ret) { + Log3 $name, 2, "$type $name: attr $name $aName '$aVal' != '$ret'"; + return "$name: $aName must be: $ret"; + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +#UndefFn: called while deleting device (delete-command) or while rereadcfg +sub ESPEasy_Undef($$) +{ + my ($hash, $arg) = @_; + my ($name,$type,$port) = ($hash->{NAME},$hash->{TYPE},$hash->{PORT}); + + # close server and return if it is a child process for incoming http requests + if (defined $hash->{TEMPORARY} && $hash->{TEMPORARY} == 1) { + my $bhash = $modules{ESPEasy}{defptr}{BRIDGE}; + Log3 $bhash->{NAME}, 4, "$type $name: Closing tcp session."; + TcpServer_Close($hash); + return undef + }; + + HttpUtils_Close($hash); + RemoveInternalTimer($hash); + + if($hash->{SUBTYPE} && $hash->{SUBTYPE} eq "bridge") { + delete $modules{ESPEasy}{defptr}{BRIDGE} + if(defined($modules{ESPEasy}{defptr}{BRIDGE})); + TcpServer_Close( $hash ); + Log3 $name, 2, "$type $name: Socket on port tcp/$port closed"; + } + else { + IOWrite($hash, $hash->{HOST}, undef, undef, undef, "cleanup", undef ); + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +#ShutdownFn: called before fhem's shutdown command +sub ESPEasy_Shutdown($) +{ + my ($hash) = @_; + HttpUtils_Close($hash); + Log3 $hash->{NAME}, 4, "$hash->{TYPE} $hash->{NAME}: Shutdown requested"; + return undef; +} + + +# ------------------------------------------------------------------------------ +#DeleteFn: called while deleting device (delete-command) but after UndefFn +sub ESPEasy_Delete($$) +{ + my ($hash, $arg) = @_; + #return if it is a child process for incoming http requests + if (not defined $hash->{TEMPORARY}) { + setKeyValue($hash->{TYPE}."_".$hash->{NAME}."-user",undef); + setKeyValue($hash->{TYPE}."_".$hash->{NAME}."-pass",undef); + setKeyValue($hash->{TYPE}."_".$hash->{NAME}."-firstrun",undef); + + Log3 $hash->{NAME}, 4, "$hash->{TYPE} $hash->{NAME}: $hash->{NAME} deleted"; + } + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_dispatch($$$@) #called by bridge -> send to logical devices +{ + my($hash,$ident,$host,@values) = @_; + my $name = $hash->{NAME}; + return if (IsDisabled $name); + + my $type = $hash->{TYPE}; + my $bhash = $modules{ESPEasy}{defptr}{BRIDGE}; + my $bname = $bhash->{NAME}; + + my $ui = 1; #can be removed later + my $as = (AttrVal($bname,"autosave",AttrVal("global","autosave",1))) ? 1 : 0; + my $ac = (AttrVal($bname,"autocreate",AttrVal("global","autoload_undefined_devices",1))) ? 1 : 0; + my $msg = $ident."::".$host."::".$ac."::".$as."::".$ui."::".join("|||",@values); + + Log3 $bname, 5, "$type $name: Dispatch: $msg"; + Dispatch($bhash, $msg, undef); + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_dispatchParse($$$) # called by logical device (defined by +{ # $hash->{ParseFn}) + # we are called from dispatch() from the ESPEasy bridge device + # we never come here if $msg does not match $hash->{MATCH} in the first place + my ($IOhash, $msg) = @_; # IOhash points to the ESPEasy bridge, not device + my $IOname = $IOhash->{NAME}; + my $type = $IOhash->{TYPE}; + + # 1:ident 2:ip 3:autocreate 4:autosave 5:uniqIDs 6:value(s) + my ($ident,$ip,$ac,$as,$ui,$v) = split("::",$msg); +# return undef if !$ident || $ident eq ""; + return "" if !$ident || $ident eq ""; + + my $name; + my @v = split("\\|\\|\\|",$v); + + # look in each $defs{$d}{IDENT} for $ident to get device name. + foreach my $d (keys %defs) { + next if($defs{$d}{TYPE} ne "ESPEasy"); + if (InternalVal($defs{$d}{NAME},"IDENT","") eq "$ident") { + $name = $defs{$d}{NAME} ; + last; + } + } + + # autocreate device if no device has $ident asigned. + if (!($name) && $ac eq "1") { + $name = ESPEasy_autocreate($IOhash,$ident,$ip,$as); + # cleanup helper + delete $IOhash->{helper}{autocreate}{$ident} + if defined $IOhash->{helper}{autocreate}{$ident}; + delete $IOhash->{helper}{autocreate} + if scalar keys %{$IOhash->{helper}{autocreate}} == 0; + } + # autocreate is disabled + elsif (!($name) && $ac eq "0") { + Log3 $IOname, 2, "$type $IOname: autocreate is disabled (ident: $ident)" + if not defined $IOhash->{helper}{autocreate}{$ident}; + $IOhash->{helper}{autocreate}{$ident} = "disabled"; + return $ident; + } + + return $name if (IsDisabled $name); + my $hash = $defs{$name}; + + Log3 $name, 5, "$type $name: Received: $msg"; + + if (defined $hash && $hash->{TYPE} eq "ESPEasy" && $hash->{SUBTYPE} eq "device") { + my @logInternals; + foreach (@v) { + my ($cmd,$reading,$value,$vType) = split("\\|\\|",$_); + + # reading prefix replacement (useful if we poll values) + my $replace = '"'.AttrVal($name,"readingPrefixGPIO","GPIO").'"'; + $reading =~ s/^GPIO/$replace/ee; + + # --- setReading ---------------------------------------------- + if ($cmd eq "r") { + # reading suffix replacement only for setreading + $replace = '"'.AttrVal($name,"readingSuffixGPIOState","").'"'; + $reading =~ s/_state$/$replace/ee; + + # map value to on/off if device is a switch + $value = ($value eq "1") ? "on" : "off" + if ($vType == 10 && AttrVal($name,"readingSwitchText",1) && !AttrVal($name,"rgbGPIOs",0) + && $value =~ /^(0|1)$/); + + # delete ignored reading and helper + if (defined ReadingsVal($name,".ignored_$reading",undef)) { + delete $hash->{READINGS}{".ignored_$reading"}; + delete $hash->{helper}{received}{".ignored_$reading"}; + } + + # delete warning if there is any (send from httpRequestParse before) + if (exists ($hash->{"WARNING"})) { + if (defined $hash->{"WARNING"}) { + Log3 $name, 2, "$type $name: RESOLVED: ".$hash->{"WARNING"}; + } + delete $hash->{"WARNING"}; + } + + # attr adjustValue + my $orgVal = $value; + $value = ESPEasy_adjustValue($hash,$reading,$value); + if (!defined $value) { + Log3 $name, 4, "$type $name: $reading: $orgVal [ignored]"; + $reading = ".ignored_$reading"; + $value = $orgVal; + } + + readingsSingleUpdate($hash, $reading, $value, 1); + my $adj = ($orgVal ne $value) ? " [adjusted]" : ""; + Log3 $name, 4, "$type $name: $reading: $value".$adj + if defined $value && $reading !~ m/^\./; #no leading dot + + # used for presence detection + $hash->{helper}{received}{$reading} = time(); + + # recalc RGB reading if a PWM channel has changed + if (AttrVal($name,"rgbGPIOs",0) && $reading =~ m/\d$/i) { + my ($r,$g,$b) = ESPEasy_gpio2RGB($hash); +# if (($r ne "" && uc ReadingsVal($name,"rgb","") ne uc $r.$g.$b) || ReadingsAge($name,"rgb",0) > 5 ) { + if (($r ne "" && uc ReadingsVal($name,"rgb","") ne uc $r.$g.$b) ) { + readingsSingleUpdate($hash, "rgb", $r.$g.$b, 1); + } + } + + } + + # --- setInternal --------------------------------------------- + elsif ($cmd eq "i") { + $hash->{"ESP_".uc($reading)} = $value; + push(@logInternals,"$reading:$value"); + } + + # --- Error --------------------------------------------------- + elsif ($cmd eq "e") { + if (!defined $hash->{"WARNING"} || $hash->{"WARNING"} ne $value) { + Log3 $name, 2, "$type $name: WARNING: $value"; + $hash->{"WARNING"} = $value; + # CommandTrigger(undef, "$name ...."); + } + #readingsSingleUpdate($hash, $reading, $value, 1); + } + + # --- DeleteReading ------------------------------------------- + elsif ($cmd eq "dr") { + CommandDeleteReading(undef, "$name $reading"); + Log3 $name, 4, "$type $name: Reading $reading deleted"; + } + + else { + Log3 $name, 2, "$type $name: Unknown command received via dispatch"; + } + } # foreach @v + + Log3 $name, 5, "$type $name: Internals: ".join(" ",@logInternals) + if scalar @logInternals > 0; + + ESPEasy_checkPresence($hash) if ReadingsVal($name,"presence","") ne "present"; + ESPEasy_setState($hash); + + } + + else { #autocreate failed + Log3 undef, 2, "ESPEasy: Device $name not defined"; + } + + return $name; # must be != undef. else msg will processed further -> help me! +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_autocreate($$$$) +{ + my ($IOhash,$ident,$ip,$autosave) = @_; + my $IOname = $IOhash->{NAME}; + my $IOtype = $IOhash->{TYPE}; + + my $devname = "ESPEasy_".$ident; + my $define = "$devname ESPEasy $ip 80 $IOhash->{NAME} $ident"; + Log3 undef, 2, "$IOtype $IOname: Autocreate $define"; + + my $cmdret= CommandDefine(undef,$define); + if(!$cmdret) { + $cmdret= CommandAttr(undef, "$devname room $IOhash->{TYPE}"); + $cmdret= CommandAttr(undef, "$devname group $IOhash->{TYPE} Device"); + $cmdret= CommandAttr(undef, "$devname setState 3"); + $cmdret= CommandAttr(undef, "$devname Interval $d_Interval"); + $cmdret= CommandAttr(undef, "$devname presenceCheck 1"); + $cmdret= CommandAttr(undef, "$devname readingSwitchText 1"); + if (AttrVal($IOname,"autosave",AttrVal("global","autosave",1))) { + CommandSave(undef,undef); + Log3 undef, 2, "$IOtype $IOname: Structural changes saved."; + } + else { + Log3 undef, 2, "$IOtype $IOname: Autosave is disabled: " + ."Do not forget to save changes."; + } + } + else { + Log3 undef, 1, "$IOtype $IOname: WARNING: an error occurred " + ."while creating device for $ident: $cmdret"; + } + + return $devname; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_httpReq($$$$$$@) +{ + my ($hash, $host, $port, $ident, $parseCmd, $cmd, @params) = @_; + my ($name,$type,$self) = ($hash->{NAME},$hash->{TYPE},ESPEasy_whoami()."()"); + + # command queue (in) + return undef if ESPEasy_httpReqQueue(@_); + + # increment http session counter + $hash->{helper}{sessions}{$host}++; + + my $orgParams = join(",",@params); + my $orgCmd = $cmd; + + # raw is used for command not implemented right now + if ($cmd eq "raw") { + $cmd = $params[0]; + splice(@params,0,1); + } + + $params[0] = ",".$params[0] if defined $params[0]; + my $plist = join(",",@params); + + my $path = ($cmd =~ m/(reboot|reset|erase)/i) ? "/?cmd=" : "/control?cmd="; + my $url = "http://".$host.":".$port.$path.$cmd.$plist; + + # there is already a log entry with verbose 3 from device + Log3 $name, 4, "$type $name: Send $cmd$plist to $host for ident $ident" if ($cmd !~ m/^(status)/); + + my $timeout = AttrVal($name,"httpReqTimeout",$d_httpReqTimeout); + my $httpParams = { + url => $url, + timeout => $timeout, + keepalive => 0, + httpversion => "1.0", + hideurl => 0, + method => "GET", + hash => $hash, + cmd => $orgCmd, # passthrought to parseFn + plist => $orgParams, # passthrought to parseFn + host => $host, # passthrought to parseFn + port => $port, # passthrought to parseFn + ident => $ident, # passthrought to parseFn + parseCmd => $parseCmd, # passthrought to parseFn (attr parseCmdResponse => (0)|1) + callback => \&ESPEasy_httpReqParse + }; + Log3 $name, 5, "$type $name: NonblockingGet for ident:$ident => $url"; + HttpUtils_NonblockingGet($httpParams); + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_httpReqParse($$$) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); + my @values; + + # command queue + $hash->{helper}{sessions}{$param->{host}}--; + + if ($err ne "") { + #dispatch $err to logical device + @values = ("e||_lastError||$err||0"); + + # keep in helper for support reason + $hash->{"WARNING_$param->{host}"} = $err; + + # logqueue + Log3 $name, 2, "$type $name: httpReq failed: $param->{host} $param->{ident} " + ."'$param->{cmd} $param->{plist}' "; + + # unshift command back to queue (resend) + if (AttrVal($name,"resendFailedCmd",$d_resendFailedCmd) + && $hash->{MAX_HTTP_SESSIONS}) { + my @p = ($param->{hash}, $param->{host}, $param->{port}, $param->{ident}, + $param->{parseCmd}, $param->{cmd}, $param->{plist}); + unshift @{$hash->{helper}{queue}{$param->{host}}}, \@p; + # logqueue + Log3 $name, 5, "$type $name: Requeuing: $param->{host} $param->{ident} " + ."'$param->{cmd} $param->{plist}' " + ."(".scalar @{$hash->{helper}{queue}{$param->{host}}}.")"; + } + } + + # check that response from cmd should be parsed (client attr parseCmdResponse) + elsif ($data ne "" && !$param->{parseCmd}) { + ESPEasy_httpReqDequeue($hash, $param->{host}); + return undef; + } + + elsif ($data ne "") { # no error occurred + # command queue + delete $hash->{"WARNING_$param->{host}"}; + + (my $logData = $data) =~ s/\n//sg; + Log3 $name, 5, "$type $name: http response for ident:$param->{ident} " + ."cmd:'$param->{cmd},$param->{plist}' => '$logData'"; + if ($data =~ m/^{/) { #it could be json... + my $res; + eval {$res = decode_json($data);1;}; + if ($@) { + Log3 $name, 2, "$type $name: WARNING: deformed JSON data received " + ."from $param->{host} requested by $param->{ident}."; + Log3 $name, 2, "$type $name: $@"; + @values = ("e||_lastError||$@||0"); + return undef; + } + + # maps plugin type (answer for set state/gpio) to SENSOR_TYPE_SWITCH + # 10 = SENSOR_TYPE_SWITCH + my $vType = (defined $res->{plugin} && $res->{plugin} eq "1") ? "10" : "0"; + if (defined $res->{plugin} && $res->{plugin} eq "123") { + # Lights plugin +# Log 1, Dumper $res; + foreach (keys %{ $res }) { + push @values, "r||$_||".$res->{$_}."||".$vType + if $res->{$_} ne "" && $_ ne "plugin"; + } + } + else { + # push values/cmds in @values + push @values, "r||GPIO".$res->{pin}."_mode||".$res->{mode}."||".$vType; + push @values, "r||GPIO".$res->{pin}."_state||".$res->{state}."||".$vType; + push @values, "r||_lastAction"."||".$res->{log}."||".$vType if $res->{log} ne ""; + } + } #it is json... + + else { # no json returned => unknown state + Log3 $name, 5, "$type $name: No json fmt: ident:$param->{ident} ". + "$param->{cmd} $param->{plist} => $data"; + if ($param->{cmd} eq "status" && $param->{plist} =~ m/^gpio,(\d+)$/i) { + # push values/cmds in @values + if (defined $1) { + push @values, "r||GPIO".$1."_mode||"."?"."||0"; + push @values, "r||GPIO".$1."_state||".$data."||0"; + } + } + } + } # ($data ne "") + + ESPEasy_dispatch($hash,$param->{ident},$param->{host},@values); + ESPEasy_httpReqDequeue($hash, $param->{host}); + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_httpReqQueue(@) +{ + my ($hash, $host, $port, $ident, $parseCmd, $cmd, @params) = @_; + my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); + + $hash->{helper}{sessions}{$host} = 0 if !defined $hash->{helper}{sessions}{$host}; + # is queueing enabled? + if ($hash->{MAX_HTTP_SESSIONS}) { + # do queueing if max sessions are already in use + if ($hash->{helper}{sessions}{$host} >= $hash->{MAX_HTTP_SESSIONS} ) { + # max queue size reached + if (!defined $hash->{helper}{queue}{"$host"} + || scalar @{$hash->{helper}{queue}{"$host"}} < $hash->{MAX_QUEUE_SIZE}) { + push(@{$hash->{helper}{queue}{"$host"}}, \@_); + # logqueue + Log3 $name, 5, "$type $name: Queuing: $host $ident '$cmd ".join(",",@params)."' (". scalar @{$hash->{helper}{queue}{"$host"}} .")"; + return 1; + } + else { + # logqueue + Log3 $name, 2, "$type $name: set $cmd ".join(",",@params)." (skipped " + ."due to queue size exceeded: $hash->{MAX_QUEUE_SIZE})"; +# if ($cmd ne "status"); +# ESPEasy_httpReqDequeue($hash,$host); + return 1; + } + } + } + + return 0; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_httpReqDequeue($$) +{ + my ($hash,$host) = @_; + my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); + + if (defined $hash->{helper}{queue}{"$host"} && scalar @{$hash->{helper}{queue}{"$host"}}) { + my $p = shift @{$hash->{helper}{queue}{"$host"}}; + my ($dhash, $dhost, $port, $ident, $parseCmd, $cmd, @params) = @{ $p }; + # logqueue + Log3 $name, 5, "$type $name: Dequeuing: $host $ident '$cmd ".join(",",@params)."' (". scalar @{$hash->{helper}{queue}{"$host"}} .")"; + ESPEasy_httpReq($dhash, $dhost, $port, $ident, $parseCmd, $cmd, @params); + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_statusRequest($) #called by device +{ + my ($hash) = @_; + my ($name, $type) = ($hash->{NAME},$hash->{TYPE}); + + unless (IsDisabled $name) { + Log3 $name, 4, "$type $name: set statusRequest"; + ESPEasy_pollGPIOs($hash); + ESPEasy_checkPresence($hash); + ESPEasy_setState($hash); + } + ESPEasy_resetTimer($hash); + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_pollGPIOs($) #called by device +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + my $sleep = $hash->{SLEEP}; + my $a = AttrVal($name,'pollGPIOs',undef); + + if (!defined $a) { + # do nothing, just return + } + elsif (defined $sleep && $sleep eq "1") { + Log3 $name, 2, "$type $name: Polling of GPIOs is not possible as long as deep sleep mode is active."; + } + + else { + my @gpios = split(",",$a); + foreach my $gpio (@gpios) { + if ($gpio =~ m/^[a-zA-Z]/) { # pin mapping (eg. D8 -> 15) + Log3 $name, 5, "$type $name: Pin mapping ".uc $gpio." => $ESPEasy_pinMap{uc $gpio}"; + $gpio = $ESPEasy_pinMap{uc $gpio}; + } + Log3 $name, 5, "$type $name: IOWrite(\$defs{$name}, $hash->{HOST}, $hash->{PORT}, $hash->{IDENT}, 1, status, gpio,".$gpio.")"; + IOWrite($hash, $hash->{HOST}, $hash->{PORT}, $hash->{IDENT}, 1, "status", "gpio,".$gpio); + } #foreach + } #else + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_resetTimer($;$) +{ + my ($hash,$sig) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + $sig = "" if !$sig; + + if ($init_done == 1) { + #Log3 $name, 5, "$type $name: RemoveInternalTimer ESPEasy_statusRequest"; + RemoveInternalTimer($hash, "ESPEasy_statusRequest"); + delete $hash->{helper}{intAt}; + } + + if ($sig eq "stop") { + Log3 $name, 5, "$type $name: internalTimer stopped"; + return undef; + } + return undef if AttrVal($name,"Interval",$d_Interval) == 0; + + unless(IsDisabled($name)) { + my $s = AttrVal($name,"Interval",$d_Interval) + rand(5); + my $ts = $s + gettimeofday(); + Log3 $name, 5, "$type $name: Start internalTimer +".int($s)." => ".FmtDateTime($ts); + InternalTimer($ts, "ESPEasy_statusRequest", $hash); + ESPEasy_intAt2helper($hash); + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_intAt2helper($) { + my ($hash) = @_; + + my $i = 1; + delete $hash->{helper}{intAt}; + foreach my $a (keys %intAt) { + my $arg = $intAt{$a}{ARG}; + my $nam = (ref($arg) eq "HASH" ) ? $arg->{NAME} : ""; + if (defined $nam && $nam eq $hash->{NAME}) { + $hash->{helper}{intAt}{$i}{TRIGGERTIME} = strftime('%d.%m.%Y %H:%M:%S', + localtime($intAt{$a}{TRIGGERTIME})); + $hash->{helper}{intAt}{$i}{INTERVAL} = round($intAt{$a}{TRIGGERTIME}-time(),0); + $hash->{helper}{intAt}{$i}{FN} = $intAt{$a}{FN}; + $i++ + } + } +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_tcpServerOpen($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $port = ($hash->{PORT}) ? $hash->{PORT} : 8383; + + my $ret = TcpServer_Open( $hash, $port, "global" ); + exit(1) if ($ret && !$init_done); + readingsSingleUpdate ( $hash, "state", "initialized", 1 ); + + return $ret; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_header2Hash($) { + my ($string) = @_; + my %header = (); + + foreach my $line (split("\r\n", $string)) { + my ($key,$value) = split(": ", $line,2); + next if !$value; + + $value =~ s/^ //; + $header{$key} = $value; + } + + return \%header; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_isAuthenticated($$) +{ + my ($hash,$ah) = @_; + my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); + + my $bhash = $modules{ESPEasy}{defptr}{BRIDGE}; + my ($bname,$btype) = ($bhash->{NAME},$bhash->{TYPE}); + + my $u = getKeyValue($btype."_".$bname."-user"); + my $p = getKeyValue($btype."_".$bname."-pass"); + my $attr = AttrVal($bname,"authentication",0); + + if (!defined $u || !defined $p || $attr == 0) { + if (defined $ah){ + Log3 $bname, 2, "$type $name: No basic authentication active but ". + "credentials received"; + } + else { + Log3 $bname, 4, "$type $name: No basic authentication required"; + } + return "not required"; + } + + elsif (defined $ah) { + my ($a,$v) = split(" ",$ah); + if ($a eq "Basic" && decode_base64($v) eq $u.":".$p) { + Log3 $bname, 4, "$type $name: Basic authentication accepted"; + return "accepted"; + } + else { + Log3 $bname, 2, "$type $name: Basic authentication rejected"; + } + } + + else { + Log3 $bname, 2, "$type $name: Basic authentication active but ". + "no credentials received"; + } + +return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_isParseCmd($$) #called by device +{ + my ($hash,$cmd) = @_; + my $name = $hash->{NAME}; + my $doParse = 0; + + my @cmds = split(",",AttrVal($name,"parseCmdResponse","status")); + foreach (@cmds) { + if (lc($_) eq lc($cmd)) { + $doParse = 1; + last; + } + } + return $doParse; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_sendHttpClose($$$) { + my ($hash,$code,$response) = @_; + my ($name,$type,$con) = ($hash->{NAME},$hash->{TYPE},$hash->{CD}); + + my $bhash = $modules{ESPEasy}{defptr}{BRIDGE}; + my $bname = $bhash->{NAME}; + + print $con "HTTP/1.1 ".$code."\r\n", + "Content-Type: text/plain\r\n", + "Connection: close\r\n", + "Content-Length: ".length($response)."\r\n\r\n", + $response; + Log3 $bname, 4, "$type $name: Send http close '$code'"; + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_paramPos($$) +{ + my ($cmd,$search) = @_; + my @usage = split(" ",$ESPEasy_setCmdsUsage{$cmd}); + my $pos = 0; + my $i = 0; + + foreach (@usage) { + if ($_ eq $search) { + $pos = $i; + last; + } + $i++; + } + + return $pos; # return 0 if no match, else position +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_paramCount($) +{ + return () = $_[0] =~ m/\s/g # count \s in a string +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_clearReadings($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + my @dr; + foreach (keys %{$hash->{READINGS}}) { +# next if $_ =~ m/^(presence)$/; + CommandDeleteReading(undef, "$name $_"); +# fhem("deletereading $name $_"); + push(@dr,$_); + } + + if (scalar @dr >= 1) { + delete $hash->{helper}{received}; + delete $hash->{helper}{fpc}; # used in checkPresence + Log3 $name, 3, "$type $name: Readings [".join(",",@dr)."] wiped out"; + } + + ESPEasy_setState($hash); + + return undef +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_checkVersion($$$$) +{ + my ($hash,$dev,$ve,$vj) = @_; + my ($type,$name) = ($hash->{TYPE},$hash->{NAME}); + my $ov = "_OUTDATED_ESP_VER_$dev"; + + if ($vj < $minJsonVersion) { + $hash->{$ov} = "R".$ve."/J".$vj; + Log3 $name, 2, "$type $name: WARNING: no data processed. ESPEasy plugin " + ."'FHEM HTTP' is too old [$dev: R".$ve." J".$vj."]. ". + "Use ESPEasy R$minEEBuild at least."; + return 1; + } + else{ + delete $hash->{$ov} if exists $hash->{$ov}; + return 0; + } +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_checkPresence($) +{ + my ($hash,$isPresent) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $interval = AttrVal($name,'Interval',$d_Interval); + my $addTime = 10; # if there is extreme heavy system load + + return undef if AttrVal($name,'presenceCheck',1) == 0; + return undef if $interval == 0; + + my $presence = "absent"; + # check each received reading + foreach my $reading (keys %{$hash->{helper}{received}}) { + if (ReadingsAge($name,$reading,0) < $interval+$addTime) { + #dev is present if any reading is newer than INTERVAL+$addTime + $presence = "present"; + last; + } + } + + # update presence only if FirstPrecenceCheck is $interval seconds ago. + $hash->{helper}{fpc} = time() if (!defined $hash->{helper}{fpc}); + if ($presence eq "present" || (time() - $hash->{helper}{fpc}) > $interval) { + readingsSingleUpdate($hash,"presence",$presence,1); + Log3 $name, 4, "$type $name: presence: $presence"; + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_setState($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + return undef if not AttrVal($name,"setState",1); + + if (AttrVal($name,"rgbGPIOs",0)) { + my ($r,$g,$b) = ESPEasy_gpio2RGB($hash); + if ($r ne "") { + readingsSingleUpdate($hash,"state", "R: $r G: $g B: $b", 1) + } + } + + else { + my $interval = AttrVal($name,"Interval",$d_Interval); + my $addTime = 3; + my @ret; + foreach my $reading (sort keys %{$hash->{helper}{received}}) { + next if $reading =~ m/^(\.ignored_.*|state|presence|_lastAction|_lastError|\w+_mode)$/; + next if $interval && ReadingsAge($name,$reading,1) > $interval+$addTime; + push(@ret, substr($reading,0,AttrVal($name,"setState",3)) + .": ".ReadingsVal($name,$reading,"")); + } + + my $oState = ReadingsVal($name, "state", ""); + my $presence = ReadingsVal($name, "presence", "opened"); + + if ($presence eq "absent" && $oState ne "absent") { + readingsSingleUpdate($hash,"state","absent", 1 ); + delete $hash->{helper}{received}; + } + else { + my $nState = (scalar @ret >= 1) ? join(" ",@ret) : $presence; + readingsSingleUpdate($hash,"state",$nState, 1 ); # if ($oState ne $nState); + } + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_setRGB($$@) +{ + my ($hash,$cmd,@p) = @_; + my ($type,$name) = ($hash->{TYPE},$hash->{NAME}); + my ($rg,$gg,$bg) = split(",",AttrVal($name,"rgbGPIOs","")); + my ($r,$g,$b); + + my $rgb = $p[0] if $cmd =~ m/^rgb$/i; +# return undef if !defined $rgb; + + $rg = $ESPEasy_pinMap{uc $rg} if defined $ESPEasy_pinMap{uc $rg}; + $gg = $ESPEasy_pinMap{uc $gg} if defined $ESPEasy_pinMap{uc $gg}; + $bg = $ESPEasy_pinMap{uc $bg} if defined $ESPEasy_pinMap{uc $bg}; + + if ($cmd =~ m/^(1|on)$/ || ($cmd =~ m/^rgb$/i && $rgb =~ m/^(1|on)$/)) { + $rgb = "FFFFFF" } + elsif ($cmd =~ m/^(0|off)$/ || ($cmd =~ m/^rgb$/i && $rgb =~ m/^(0|off)$/)) { + $rgb = "000000" } + elsif ($cmd =~ m/^toggle$/i || ($cmd =~ m/^rgb$/i && $rgb =~ m/^toggle$/i)) { + $rgb = ReadingsVal($name,"rgb","000000") ne "000000" ? "000000" : "FFFFFF" + } + + if ($rgb =~ m/^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/) { + ($r,$g,$b) = (hex($1), hex($2), hex($3)); + } + else { + Log3 $name, 2, "$type $name: set $name $cmd $rgb: " + ."'$rgb' is not a valid RGB value."; + return "'$rgb' is not a valid RGB value."; + } + ESPEasy_Set($hash, $name, "pwm", ("$rg", $r*4)); + ESPEasy_Set($hash, $name, "pwm", ("$gg", $g*4)); + ESPEasy_Set($hash, $name, "pwm", ("$bg", $b*4)); + readingsSingleUpdate($hash, "rgb", uc $rgb, 1); + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_setCT($$@) +{ + my ($hash,$cmd,@p) = @_; + my ($type,$name) = ($hash->{TYPE},$hash->{NAME}); + my ($gww,$gcw) = split(",",AttrVal($name,"wwcwGPIOs","")); + my ($ww,$cw); + my ($pct,$ct); + my $ctWW = AttrVal($name,"colorpickerCTww",$d_colorpickerCTww); + my $ctCW = AttrVal($name,"colorpickerCTcw",$d_colorpickerCTcw); + my $ctWW_lim = AttrVal($name,"ctWW_reducedRange",undef); + my $ctCW_lim = AttrVal($name,"ctCW_reducedRange",undef); + + $gww = $ESPEasy_pinMap{uc $gww} if defined $ESPEasy_pinMap{uc $gww}; + $gcw = $ESPEasy_pinMap{uc $gcw} if defined $ESPEasy_pinMap{uc $gcw}; + + readingsSingleUpdate($hash, $cmd, $p[0], 1); + + if ($cmd eq "ct") { + $ct = $p[0]; + $pct = ReadingsVal($name,"pct",50); + } + elsif ($cmd eq "pct") { + $pct = $p[0]; + $ct = ReadingsVal($name,"ct",3000); + } + + # are we out of range? + $pct = 100 if $pct > 100; + $pct = 0 if $pct < 0; + $ct = $ctWW if $ct < $ctWW; + $ct = $ctCW if $ct > $ctCW; + + #Log 1, "pct:$pct ct:$ct ctWW:$ctWW ctCW:$ctCW ctWW_lim:$ctWW_lim ctCW_lim:$ctCW_lim"; + + my $wwcwMaxBri = AttrVal($name,"wwcwMaxBri",0); + my ($fww,$fcw) = ESPEasy_ct2wwcw($ct, $ctWW, $ctCW, $wwcwMaxBri, $ctWW_lim, $ctCW_lim); + #my ($fww,$fcw) = ESPEasy_ct2wwcw($ct, $ctWW, $ctCW); + + ESPEasy_Set($hash, $name, "pwm", ($gww, int $pct*10.23*$fww)); + ESPEasy_Set($hash, $name, "pwm", ($gcw, int $pct*10.23*$fcw)); + + + return undef; +} + + +# ------------------------------------------------------------------------------ +# ct2wwcw with constant brightness over temp range (or max bri if $maxBri == 1). +# "used range" can be set to reduce temp range to get a lighter leds with constant +# bri over reduced temp range. +# 1: temp to set 2:led-ww-temp 3:led-cw-temp 4:maxBri 5:used range ww 6:used range cw +sub ESPEasy_ct2wwcw($$$;$$$) +{ + my ($t,$tww,$tcw,$maxBri,$tww_ur,$tcw_ur) = @_; + my $maxBriFactor; + + $tcw -= $tww; + $t -= $tww; + my $fcw = $t / $tcw; + my $fww = 1 - $fcw; + + if ($maxBri // $maxBri) { + $maxBriFactor = ($fcw > $fww) ? 1/$fcw : 1/$fww; + #Log 1, "maxBriFactor: $maxBriFactor (maxBri)"; + } + else { + $tww_ur = $tww if !(defined $tww_ur) || $tww_ur < $tww || $tww_ur >= $tcw; + $tcw_ur = $tcw if !(defined $tcw_ur) || $tcw_ur > $tcw || $tcw_ur <= $tww; + + $tww_ur -= $tww; + $tcw_ur -= $tww; + my $t = ($tww_ur < $tcw - $tcw_ur) ? $tww_ur : $tcw - $tcw_ur; + my $fcw = $t / $tcw; + my $fww = 1 - $fcw; + $maxBriFactor = ($fcw > $fww) ? 1/$fcw : 1/$fww; + #Log 1, "maxBriFactor: $maxBriFactor (constBri)"; + } + + return ( $fww * $maxBriFactor, $fcw * $maxBriFactor ); +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_gpio2RGB($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my ($r,$g,$b,$rgb); + my $a = AttrVal($name,"rgbGPIOs",undef); + return undef if !defined $a; + my ($gr,$gg,$gb) = split(",",AttrVal($name,"rgbGPIOs","")); + + $gr = $ESPEasy_pinMap{uc $gr} if defined $ESPEasy_pinMap{uc $gr}; + $gg = $ESPEasy_pinMap{uc $gg} if defined $ESPEasy_pinMap{uc $gg}; + $gb = $ESPEasy_pinMap{uc $gb} if defined $ESPEasy_pinMap{uc $gb}; + + my $rr = AttrVal($name,"readingPrefixGPIO","GPIO").$gr; + my $rg = AttrVal($name,"readingPrefixGPIO","GPIO").$gg; + my $rb = AttrVal($name,"readingPrefixGPIO","GPIO").$gb; + + $r = ReadingsVal($name,$rr,undef); + $g = ReadingsVal($name,$rg,undef); + $b = ReadingsVal($name,$rb,undef); + + return ("","","") if !defined $r || !defined $g || !defined $b + || $r !~ m/^\d+$/ || $g !~ m/^\d+$/i || $b !~ m/^\d+$/i; + return (sprintf("%2.2X",$r/4), sprintf("%2.2X",$g/4), sprintf("%2.2X",$b/4)); +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_adjustSetCmds($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + delete $ESPEasy_setCmds{rgb}; + delete $ESPEasy_setCmds{ct}; + delete $ESPEasy_setCmds{pct}; + delete $ESPEasy_setCmds{on}; + delete $ESPEasy_setCmds{off}; + delete $ESPEasy_setCmds{toggle}; + + if (defined AttrVal($name,"mapLightCmds",undef)) { + $ESPEasy_setCmds{rgb} = 1; + $ESPEasy_setCmds{ct} = 1; + $ESPEasy_setCmds{pct} = 1; + $ESPEasy_setCmds{on} = 0; + $ESPEasy_setCmds{off} = 0; + $ESPEasy_setCmds{toggle} = 0; + } + if (defined AttrVal($name,"rgbGPIOs",undef)) { + $ESPEasy_setCmds{rgb} = 1; + $ESPEasy_setCmds{on} = 0; + $ESPEasy_setCmds{off} = 0; + $ESPEasy_setCmds{toggle} = 0; + } + if (defined AttrVal($name,"wwcwGPIOs",undef)) { + $ESPEasy_setCmds{ct} = 1; + $ESPEasy_setCmds{pct} = 1; + $ESPEasy_setCmds{on} = 0; + $ESPEasy_setCmds{off} = 0; + $ESPEasy_setCmds{toggle} = 0; + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +# attr devStateIcon { ESPEasy_devStateIcon($name) } +sub ESPEasy_devStateIcon($) +{ + my $ret = Color::devStateIcon($_[0],"rgb","rgb"); + $ret =~ m/^.*:on@#(..)(..)(..):toggle$/; + return undef if !defined $1; + my $symP = int((hex($1)+hex($2)+hex($3))/76.5)*10; + $symP = "00" if $symP == 0; + my $icon = "light_light_dim_".$symP; + $ret =~ s/:on@#/:$icon@#/; + + return $ret; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_adjustValue($$$) +{ + my ($hash,$r,$v) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + my $a = AttrVal($name,"adjustValue",undef); + return $v if !defined $a; + + my ($VALUE,$READING,$NAME) = ($v,$r,$name); #capital vars für use in attribute + my @a = split(" ",$a); + foreach (@a) { + my ($regex,$formula) = split(":",$_); + if ($r =~ m/^$regex$/) { + no warnings; + my $adjVal = $formula =~ m/\$VALUE/ ? eval($formula) : eval($v.$formula); + use warnings; + if ($@) { + Log3 $name, 2, "$type $name: WARNING: attribute 'adjustValue': " + ."mad expression '$formula'"; + Log3 $name, 2, "$type $name: $@"; + } + else { + my $rText = (defined $adjVal) ? $adjVal : "'undef'"; + Log3 $name, 5, "$type $name: Adjusted reading $r: $v => $formula = $rText"; + return $adjVal; + } + #last; #disabled to be able to match multiple readings + } + } + + return $v; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_isPmInstalled($$) +{ + my ($hash,$pm) = @_; + my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); + if (not eval "use $pm;1") + { + Log3 $name, 1, "$type $name: perl modul missing: $pm. Install it, please."; + $hash->{MISSING_MODULES} .= "$pm "; + return "failed: $pm"; + } + + return undef; +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_isAttrCombineDevices($) +{ + return 0 if !defined $_[0]; + my @ranges = split(/,| /,$_[0]); + foreach (@ranges) { + if (!($_ =~ m/^([A-Za-z0-9_\.]|[A-Za-z0-9_\.][A-Za-z0-9_\.]*[A-Za-z0-9\._])$/ + || ESPEasy_isIPv64Range($_))) + { + return 0 + } + } + + return 1; +} + + +# ------------------------------------------------------------------------------ +# check if $peer is covered by $allowed (eg. 10.1.2.3 is included in 10.0.0.0/8) +# 1:peer address 2:allowed range +# ------------------------------------------------------------------------------ +sub ESPEasy_isCombineDevices($$$) +{ + my ($peer,$espName,$allowed) = @_; + return $allowed if $allowed =~ m/^[01]$/; + + my @allowed = split(/,| /,$allowed); + foreach (@allowed) { return 1 if $espName eq $_ } + return 1 if ESPEasy_isPeerAllowed($peer,$allowed); + return 0; +} + + +# ------------------------------------------------------------------------------ +# check param to be a valid ip64 address or fqdn or hostname +# ------------------------------------------------------------------------------ +sub ESPEasy_isValidPeer($) +{ + my ($addr) = @_; + return 0 if !defined $addr; + my @ranges = split(/,| /,$addr); + foreach (@ranges) { + return 0 if !( ESPEasy_isIPv64Range($_) + || ESPEasy_isFqdn($_) || ESPEasy_isHostname($_) ); + } + + return 1; +} + + +# ------------------------------------------------------------------------------ +# check if given ip or ip range is guilty +# argument can be: +# - ipv4, ipv4/CIDR, ipv4/dotted, ipv6, ipv6/CIDR +# - space or comma separated list of above. +# ------------------------------------------------------------------------------ +sub ESPEasy_isIPv64Range($) +{ + my ($addr) = @_; + return 0 if !defined $addr; + my @ranges = split(/,| /,$addr); + foreach (@ranges) { + my ($ip,$nm) = split("/",$_); + if (ESPEasy_isIPv4($ip)) { + return 0 if defined $nm && !( ESPEasy_isNmDotted($nm) + || ESPEasy_isNmCIDRv4($nm) ); + } + elsif (ESPEasy_isIPv6($ip)) { + return 0 if defined $nm && !ESPEasy_isNmCIDRv6($nm); + } + else { + return 0; + } + } + + return 1; +} + + +# ------------------------------------------------------------------------------ +# check if $peer is covered by $allowed (eg. 10.1.2.3 is included in 10.0.0.0/8) +# 1:peer address 2:allowed range +# ------------------------------------------------------------------------------ +sub ESPEasy_isPeerAllowed($$) +{ + my ($peer,$allowed) = @_; + return $allowed if $allowed =~ m/^[01]$/; + #return 1 if $allowed =~ /^0.0.0.0\/0(.0.0.0)?$/; # not necessary but faster + + my $binPeer = ESPEasy_ip2bin($peer); + my @a = split(/,| /,$allowed); + foreach (@a) { + next if !ESPEasy_isIPv64Range($_); # needed for combinedDevices + my ($addr,$ip,$mask) = ESPEasy_addrToCIDR($_); + return 0 if !defined $ip || !defined $mask; # return if ip or mask !guilty + my $binAllowed = ESPEasy_ip2bin($addr); + my $binPeerCut = substr($binPeer,0,$mask); + return 1 if ($binAllowed eq $binPeerCut); + } + + return 0; +} + + +# ------------------------------------------------------------------------------ +# convert IPv64 address to binary format and return network part of binary, only +# ------------------------------------------------------------------------------ +sub ESPEasy_ip2bin($) +{ + my ($addr) = @_; + my ($ip,$mask) = split("/",$addr); + my @bin; + + if (ESPEasy_isIPv4($ip)) { + $mask = 32 if !defined $mask; + @bin = map substr(unpack("B32",pack("N",$_)),-8), split(/\./,$ip); + } + elsif (ESPEasy_isIPv6($ip)) { + $ip = ESPEasy_expandIPv6($ip); + $mask = 128 if !defined $mask; + @bin = map {unpack('B*',pack('H*',$_))} split(/:/, $ip); + } + else { + return undef; + } + + my $bin = join('', @bin); + my $binMask = substr($bin, 0, $mask); + + return $binMask; # return network part of $bin +} + + +# ------------------------------------------------------------------------------ +# expand IPv6 address to 8 full blocks +# Advantage of IO::Socket: already installed and it is the fastest way +# http://stackoverflow.com/questions/4800691/perl-ipv6-address-expansion-parsing +# ------------------------------------------------------------------------------ +sub ESPEasy_expandIPv6($) +{ + my ($ipv6) = @_; + use Socket qw(inet_pton AF_INET6); + return join(":", unpack("H4H4H4H4H4H4H4H4",inet_pton(AF_INET6, $ipv6))); +} + + +# ------------------------------------------------------------------------------ +# convert IPv64 address or range into CIDR notion +# return undef if addreess or netmask is not valid +# ------------------------------------------------------------------------------ +sub ESPEasy_addrToCIDR($) +{ + my ($addr) = @_; + my ($ip,$mask) = split("/",$addr); + # no nm specified + return (ESPEasy_isIPv4($ip) ? ("$ip/32",$ip,32) : ("$ip/128",$ip,128)) if !defined $mask; + # netmask is already in CIDR format and all values are valid + return ("$ip/$mask",$ip,$mask) + if (ESPEasy_isIPv4($ip) && ESPEasy_isNmCIDRv4($mask)) + || (ESPEasy_isIPv6($ip) && ESPEasy_isNmCIDRv6($mask)); + $mask = ESPEasy_dottedNmToCIDR($mask); + return (undef,undef,undef) if !defined $mask; + + return ("$ip/$mask",$ip,$mask); +} + + +# ------------------------------------------------------------------------------ +# convert dotted decimal netmask to CIDR format +# return undef if nm is not in dotted decimal format +# ------------------------------------------------------------------------------ +sub ESPEasy_dottedNmToCIDR($) +{ + my ($mask) = @_; + return undef if !ESPEasy_isNmDotted($mask); + + # dotted decimal to CIDR + my ($byte1, $byte2, $byte3, $byte4) = split(/\./, $mask); + my $num = ($byte1 * 16777216) + ($byte2 * 65536) + ($byte3 * 256) + $byte4; + my $bin = unpack("B*", pack("N", $num)); + my $count = ($bin =~ tr/1/1/); + + return $count; # return number of netmask bits +} + + +# ------------------------------------------------------------------------------ +sub ESPEasy_isIPv4($) +{ + return 0 if !defined $_[0]; + return 1 if($_[0] + =~ m/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/); + return 0; +} + +# ------------------------------------------------------------------------------ +sub ESPEasy_isIPv6($) +{ + return 0 if !defined $_[0]; + return 1 if ($_[0] + =~ m/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/); + return 0; +} + +# ------------------------------------------------------------------------------ +sub ESPEasy_isIPv64($) +{ + return 0 if !defined $_[0]; + return 1 if ESPEasy_isIPv4($_[0]) || ESPEasy_isIPv6($_[0]); + return 0; +} + +# ------------------------------------------------------------------------------ +sub ESPEasy_isNmDotted($) +{ + return 0 if !defined $_[0]; + return 1 if ($_[0] + =~ m/^(255|254|252|248|240|224|192|128|0)\.0\.0\.0|255\.(255|254|252|248|240|224|192|128|0)\.0\.0|255\.255\.(255|254|252|248|240|224|192|128|0)\.0|255\.255\.255\.(255|254|252|248|240|224|192|128|0)$/); + return 0; +} + +# ------------------------------------------------------------------------------ +sub ESPEasy_isNmCIDRv4($) +{ + return 0 if !defined $_[0]; + return 1 if ($_[0] =~ m/^([0-2]?[0-9]|3[0-2])$/); + return 0; +} + +# ------------------------------------------------------------------------------ +sub ESPEasy_isNmCIDRv6($) +{ + return 0 if !defined $_[0]; + return 1 if ($_[0] =~ m/^([0-9]?[0-9]|1([0-1][0-9]|2[0-8]))$/); + return 0; +} + +# ------------------------------------------------------------------------------ +sub ESPEasy_isFqdn($) +{ + return 0 if !defined $_[0]; + return 1 if ($_[0] + =~ m/^(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?); + close(FH); + + if ($ret =~ m/controls_ESPEasy.txt/) { + my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); + Log3 $name, 1, ""; + Log3 $name, 1, "================================================================================"; + Log3 $name, 1, ""; + Log3 $name, 1, "ESPEasy is part of the official FHEM distribution."; + Log3 $name, 1, ""; + Log3 $name, 1, "Please remove ESPEasy Github repository from FHEM update by using the following"; + Log3 $name, 1, "FHEM command: "; + Log3 $name, 1, ""; + Log3 $name, 1, "update delete https://raw.githubusercontent.com/ddtlabs/ESPEasy/master/controls_ESPEasy.txt"; + Log3 $name, 1, ""; + Log3 $name, 1, "================================================================================"; + Log3 $name, 1, ""; + } + } + + return undef; +} + + +1; + +=pod +=item device +=item summary Control and access to ESPEasy (Espressif ESP8266 WLAN-SoC) +=item summary_DE Steuerung und Zugriff auf ESPEasy (Espressif ESP8266 WLAN-SoC) +=begin html + + +

ESPEasy

+ +
    +

    Provides access and control to Espressif ESP8266 WLAN-SoC w/ ESPEasy

    + + Notes: +
      +
    • You have to define a bridge device before any logical device can be + defined. +
    • +
    • You have to configure your ESP to use "FHEM HTTP" controller protocol. + Furthermore the ESP controller port and the FHEM ESPEasy bridge port must + be the same, of cause. +
    • +
      +
    + + Requirements: +
      +
    • ESPEasy build >= R128 (self compiled) or an ESPEasy precompiled image >= + R140_RC3
      +
    • +
    • perl module JSON
      + Use "cpan install JSON" or operating system's package manager to install + Perl JSON Modul. Depending on your os the required package is named: + libjson-perl or perl-JSON. +
    • +
    + +

    ESPEasy Bridge

    + + + Define (bridge)

    + +
      + define <name> ESPEasy bridge <port>

      + +
    • + <name>
      + Specifies a device name of your choise.
      + eg. ESPBridge

    • + +
    • + <port>
      + Specifies tcp port for incoming http requests. This port must not + be used by any other application or daemon on your system and must be in + the range 1025..65535
      + eg. 8383 (ESPEasy FHEM HTTP plugin default)

    • + +
    • + Example:
      + define ESPBridge ESPEasy bridge 8383

    • +
    + +
    + Get (bridge)

    + +
      +
    • <reading>
      + returns the value of the specified reading
    • +
      + +
    • queueSize
      + returns number of entries for each queue (:number [:number] + [...]). +

    • + +
    • user
      + returns username used by basic authentication for incoming requests. +

    • + +
    • pass
      + returns password used by basic authentication for incoming requests. +

    • +
    + +
    + Set (bridge)

    + +
      +
    • help
      + Shows set command usage
      + required values: help|pass|user

    • + +
    • clearQueue
      + Used to erase command queues.
      + required value: <none>
      + eg. : set ESPBridge clearQueue

    • + +
    • pass
      + Specifies password used by basic authentication for incoming requests.
      + required value: <password>
      + eg. : set ESPBridge pass secretpass

    • + +
    • user
      + Specifies username used by basic authentication for incoming requests.
      + required value: <username>
      + eg. : set ESPBridge user itsme

    • +
    + +
    + Attributes (bridge)

    + +
      +
    • allowedIPs
      + Used to limit IPs or IP ranges of ESPs which are allowed to commit data. +
      + Specify comma separated list of IPs or IP ranges. Netmask can be written + as bitmask or dotted decimal. Domain names for dns lookups are not + supported.
      + Possible values: IPv64 address, IPv64/netmask
      + Default: 0.0.0.0/0 (all IPs are allowed)
      + Eg. 10.68.30.147
      + Eg. 10.68.30.0/24,10.68.31.0/255.255.248.0
      + Eg. fe80::/10,2001:1a59:50a9::/48,2002:1a59:50a9::,2003:1a59:50a9:acdc::36 +

    • + +
    • authentication
      + Used to enable basic authentication for incoming requests.
      + Note that user, pass and authentication attribute must be set to activate + basic authentication
      + Possible values: 0,1
      + Default: 0

    • + +
    • autocreate
      + Used to overwrite global autocreate setting.
      + Possible values: 0,1
      + Default: not set

    • + +
    • autosave
      + Used to overwrite global autosave setting.
      + Possible values: 0,1
      + Default: not set

    • + +
    • combineDevices
      + Used to gather all ESP devices of a single ESP into 1 FHEM device even if + different ESP devices names are used.
      + Possible values: 0, 1, IPv64 address, IPv64/netmask, ESPname or a comma + separated list consisting of these values.
      + Netmasks can be written as bitmask or dotted decimal. Domain names for dns + lookups are not supported.
      + Default: 0 (disabled for all ESPs)
      + Eg. 1 (globally enabled)
      + Eg. ESP01,ESP02
      + Eg. 10.68.30.1,10.69.0.0/16,ESP01,2002:1a59:50a9::/48

    • + +
    • deniedIPs
      + Used to define IPs or IP ranges of ESPs which are denied to commit data. +
      + Syntax see allowedIPs.
      + This attribute will overwrite any IP or range defined by + allowedIPs.
      + Default: none (no IPs are denied)

    • + +
    • disable
      + Used to disable device.
      + Possible values: 0,1
      + Default: 0 (eanble)

    • + +
    • httpReqTimeout
      + Specifies seconds to wait for a http answer from ESP8266 device.
      + Possible values: 4..60
      + Default: 10 seconds

    • + +
    • maxHttpSessions
      + Limit maximal concurrent outgoing http sessions to a single ESP.
      + Set to 0 to disable this feature. At the moment (ESPEasy R147) it seems + to be possible to send 5 "concurrent" requests if nothing else keeps the + esp working.
      + Possible values: 0..9
      + Default: 3

    • + +
    • maxQueueSize
      + Limit maximal queue size (number of commands in queue) for outgoing http + requests.
      + If command queue size is reached (eg. ESP is offline) any further + command will be ignored and discard.
      + Possible values: >10
      + Default: 250

    • + +
    • resendFailedCmd
      + Used to resend commands when http request returned an error
      + Possible values: 0,1
      + Default: 0 (disabled)

    • + +
    • uniqIDs
      + This attribute has been removed.

    • + +
    • readingFnAttributes +

    • +
    + +

    ESPEasy Device

    + + + Define (logical device)

    + +
      + Notes: Logical devices will be created automatically if any values are + received by the bridge device and autocreate is not disabled. If you + configured your ESP in a way that no data is send independently then you + have to define logical devices. At least wifi rssi value could be defined + to use autocreate.

      + + define <name> ESPEasy <ip|fqdn> <port> + <IODev> <identifier>

      + +
    • + <name>
      + Specifies a device name of your choise.
      + eg. ESPxx

    • + +
    • + <ip|fqdn>
      + Specifies ESP IP address or hostname.
      + eg. 172.16.4.100
      + eg. espxx.your.domain.net

    • + +
    • + <port>
      + Specifies http port to be used for outgoing request to your ESP. Should + be: 80
      + eg. 80

    • + +
    • + <IODev>
      + Specifies your ESP bridge device. See above.
      + eg. ESPBridge

    • + +
    • + <identifier>
      + Specifies an identifier that will bind your ESP to this device.
      + This identifier must be specified in this form: + <esp name>_<esp device name>.
      If attribute + combineDevices is used then + <esp name> is used, only.
      + ESP name and device name can be found here:
      + <esp name>: => ESP GUI => Config => Main Settings => + Name
      + <esp device name>: => ESP GUI => Devices => Edit => + Task Settings => Name
      + eg. ESPxx_DHT22
      + eg. ESPxx

    • + +
    • Example:
      + define ESPxx ESPEasy 172.16.4.100 80 ESPBridge EspXX_SensorXX +

    • +
    + +
    + Get (logical device)

    + +
      +
    • <reading>
      + returns the value of the specified reading

    • + +
    • pinMap
      + returns possible alternative pin names that can be used in commands
    • +
      +
    + +
    + Set (logical device)

    + +
      + Notes:
      + - Commands are case insensitive.
      + - Users of Wemos D1 mini or NodeMCU can use Arduino pin names instead of + GPIO numbers:
      +   D1 => GPIO5, D2 => GPIO4, ...,TX => GPIO1 (see: get + pinMap)
      + - low/high state can be written as 0/1 or on/off +

      + +
    • clearReadings
      + Delete all readings that are auto created by received sensor values + since last FHEM restart.
      + required values: <none>

    • + +
    • help
      + Shows set command usage.
      + required values: a valid set command

    • + +
    • raw
      + Can be used for own ESP plugins or new ESPEasy commands that are not + considered by this module at the moment. Any argument will be sent + directly to the ESP.
      + Usage: raw <cmd> <param1> <param2> <...>
      + eg: raw myCommand 3 1 2

    • + +
    • statusRequest
      + Trigger a statusRequest for configured GPIOs (see attribut pollGPIOs) + and do a presence check
      + required values: <none>

    • + +
      + Note: The following commands are built-in ESPEasy Software commands + that are send directly to the ESP after passing a syntax check. A detailed + description can be found here: + ESPEasy Command Reference

      + +
    • Event
      + Create an event. Such events can be used in ESP rules.
      + required value: <string>

    • + +
    • GPIO
      + Direct control of output pins (on/off)
      + required arguments: <pin> <0,1>
      +

    • + +
    • PWM
      + Direct PWM control of output pins
      + required arguments: <pin> <level>
      +

    • + +
    • PWMFADE
      + PWMFADE control of output pins
      + required arguments: <pin> <target> <duration> +
      + pin: 0-3 (0=r,1=g,2=b,3=w), target: 0-1023, duration: 1-30 seconds. +

    • + +
    • Lights (plugin can be found here)
      + Control a rgb or ct light
      + required arguments: <cmd> <color> <fading time> +
      + cmd: rgb, ct, pct, on, off, toggle
      + color: rrggbb (if rgb) or color temperature in Kelvin (if ct)
      + fading time: time in seconds
      + eg. set <esp> lights rgb aa00aa
      + eg. set <esp> lights ct 3200
      + eg. set <esp> lights pct 50
      + eg. set <esp> lights on
      + eg. set <esp> lights off
      + eg. set <esp> lights toggle
      +

    • + +
    • Pulse
      + Direct pulse control of output pins
      + required arguments: <pin> <0,1> <duration> +
      +

    • + +
    • LongPulse
      + Direct pulse control of output pins
      + required arguments: <pin> <0,1> <duration> +
      +

    • + +
    • Servo
      + Direct control of servo motors
      + required arguments: <servoNo> <pin> <position> +
      +

    • + +
    • lcd
      + Write text messages to LCD screen
      + required arguments: <row> <col> <text>
      +

    • + +
    • lcdcmd
      + Control LCD screen
      + required arguments: <on|off|clear>
      +

    • + +
    • mcpgpio
      + Control MCP23017 output pins
      + required arguments: <pin> <0,1>
      +

    • + +
    • oled
      + Write text messages to OLED screen
      + required arguments: <row> <col> <text>
      +

    • + +
    • oledcmd
      + Control OLED screen
      + required arguments: <on|off|clear>
      +

    • + +
    • pcapwm
      + Control PCA9685 pwm pins
      + required arguments: <pin> <level>
      +

    • + +
    • PCFLongPulse
      + Long pulse control on PCF8574 output pins
      +

    • + +
    • PCFPulse
      + Pulse control on PCF8574 output pins
      +

    • + +
    • pcfgpio
      + Control PCF8574 output pins
      +

    • + +
    • irsend
      + Send ir codes via "Infrared Transmit" Plugin
      + Supported protocols are: NEC, JVC, RC5, RC6, SAMSUNG, SONY, PANASONIC at + the moment. As long as official documentation is missing you can find + some details here: + + IR Transmitter thread
      + required arguments: <protocol> <hex code> <bit length + of hex code> +
      + eg. irsend NEC 7E81542B 32 +

    • + +
    • status
      + Request esp device status (eg. gpio)
      + required values: <device> <pin>
      + eg: gpio 13 +

    • + + Administrative commands (be careful):

      + +
    • erase
      + Wipe out ESP flash memory
      + required values: none
      +

    • + +
    • reboot
      + Used to reboot your ESP
      + required values: none
      +

    • + +
    • reset
      + Do a factory reset on the ESP
      + required values: none
      +

    • + + Experimental commands (The following commands can be changed or + removed at any time):

      + +
    • rgb
      + EXPERIMENTAL, may be removed in later versions if a usable rgb plugin is + available.
      + Used to control a rgb light.
      + You have to set attribute rgbGPIOs to enable this feature. Default + colorpicker mode is HSVp but can be adjusted with help of attribute + colorpicker to HSV or RGB. Set + attribute webCmd to rgb to display a colorpicker + in FHEMWEB room view and on detail page.
      + required argument: <rrggbb>|on|off|toggle +
      + eg. rgb 00FF00
      + eg. rgb on
      + eg. rgb off
      + eg. rgb toggle
      +
      + Full featured example:
      + attr <ESP> colorpicker HSVp
      + attr <ESP> devStateIcon { ESPEasy_devStateIcon($name) }
      + attr <ESP> Interval 30
      + attr <ESP> parseCmdResponse status,pwm
      + attr <ESP> pollGPIOs D6,D7,D8
      + attr <ESP> rgbGPIOs D6,D7,D8
      + attr <ESP> webCmd rgb:rgb ff0000:rgb 00ff00:rgb 0000ff:toggle:on:off +
      +

    • + +
    + +
    + Attributes (logical device)

    + +
      +
    • adjustValue
      + Used to adjust sensor values
      + Must be a space separated list of <reading>:<formula>. + Reading can be a regexp. Formula can be an arithmetic expression like + 'round(($VALUE-32)*5/9,2)'. + If $VALUE is omitted in formula then it will be added to the beginning of + the formula. So you can simple write 'temp:+20' or '.*:*4'
      + Modified or ignored values are marked in the system log (verbose 4). Use + verbose 5 logging to see more details.
      + If the used sub function returns 'undef' then the value will be ignored. +
      + The following variables can be used if necessary: +
        +
      • $VALUE contains the original value
      • +
      • $READING contains the reading name
      • +
      • $NAME contains the device name
      • +
      + Default: none
      + Eg. attr ESPxx adjustValue humidity:+0.1 + temperature+*:($VALUE-32)*5/9
      + Eg. attr ESPxx adjustValue + .*:my_OwnFunction($NAME,$READING,$VALUE)
      +
      + Sample function to ignore negative values:
      + + sub my_OwnFunction($$$) {
      +   my ($name,$reading,$value) = @_;
      +   return ($value < 0) ? undef : $value;
      + }
      +

    • + +
    • colorpicker
      + Used to select colorpicker mode
      + Possible values: RGB,HSV,HSVp
      + Default: HSVp

    • + +
    • colorpickerCTcw
      + Used to select ct colorpicker's cold white color temperature in Kelvin
      + Possible values: > colorpickerCTww
      + Default: 6000

    • + +
    • colorpickerCTww
      + Used to select ct colorpicker's warm white color temperature in Kelvin
      + Possible values: < colorpickerCTcw
      + Default: 2000

    • + +
    • disable
      + Used to disable device
      + Possible values: 0,1
      + Default: 0

    • + +
    • Interval
      + Used to set polling interval for presence check and GPIOs polling in + seconds. 0 will disable this feature.
      + Possible values: secs > 10.
      + Default: 300

    • + +
    • IODev
      + Used to select I/O device (ESPEasy Bridge). +

    • + +
    • mapLightCmds
      + Enable the following commands and map them to the specified ESPEasy + command: rgb, ct, pct, on, off, toggle
      + Needed if you want to use FHEM's colorpickers to control a rgb/ct ESPEasy + plugin. + required values: a valid set command
      + eg. attr <esp> mapLightCmds Lights

    • + +
    • presenceCheck
      + Used to enable/disable presence check for ESPs
      + Presence check determines the presence of a device by readings age. If any + reading of a device is newer than interval + seconds than it is marked as being present. This kind of check works for + ESP devices in deep sleep too but require at least 1 reading that is + updated regularly.
      + Possible values: 0,1
      + Default: 1 (enabled)

    • + +
    • readingSwitchText
      + Use on,off instead of 1,0 for readings if ESP device is a switch.
      + Possible values: 0,1
      + Default: 1 (enabled)

    • + +
    • setState
      + Summarize received values in state reading.
      + A positive number determines the number of characters used for reading + names. Only readings with an age less than + interval will be considered. If your are + not satisfied with format or behavior of setState then disable this + attribute (set to 0) and use global attributes userReadings and/or + stateFormat to get what you want.
      + Possible values: integer >=0
      + Default: 3 (enabled with 3 characters abbreviation)

    • + + The following two attributes should only be use in cases where ESPEasy + software do not send data on status changes and no rule/dummy can be used + to do that. Useful for commands like PWM, STATUS, ... +

      + +
    • parseCmdResponse
      + Used to parse response of commands like GPIO, PWM, STATUS, ...
      + Specify a module command or comma separated list of commands as argument. + Commands are case insensitive.
      + Only necessary if ESPEasy software plugins do not send their data + independently. Useful for commands line STATUS, PWM, ...
      + Possible values: <set cmd>[,<set cmd>][,...]
      + Default: status
      + Eg. attr ESPxx parseCmdResponse status,pwm

    • + +
    • pollGPIOs
      + Used to enable polling for GPIOs status. This polling will do same as + command 'set ESPxx status <device> <pin>'
      + Possible values: GPIO number or comma separated GPIO number list
      + Default: none
      + Eg. attr ESPxx pollGPIOs 13,D7,D2

    • + + The following two attributes control naming of readings that are + generated by help of parseCmdResponse and pollGPIOs (see above) +

      + +
    • readingPrefixGPIO
      + Specifies a prefix for readings based on GPIO numbers. For example: + "set ESPxx pwm 13 512" will switch GPIO13 into pwm mode and set pwm to + 512. If attribute readingPrefixGPIO is set to PIN and attribut + parseCmdResponse contains pwm + command then the reading name will be PIN13.
      + Possible Values: string
      + Default: GPIO

    • + +
    • readingSuffixGPIOState
      + Specifies a suffix for the state-reading of GPIOs (see Attribute + pollGPIOs)
      + Possible Values: string
      + Default: no suffix
      + Eg. attr ESPxx readingSuffixGPIOState _state

    • + +
    • readingFnAttributes +

    • + + Experimental (The following attributes can be changed or removed at + any time):

      + +
    • rgbGPIOs
      + Use to define GPIOs your lamp is conneted to. Must be set to be able to + use rgb set command.
      + Possible values: Comma separated tripple of ESP pin numbers or arduino pin + names
      + Eg: 12,13,15
      + Eg: D6,D7,D8
      + Default: none

    • + +
    +
+ +=end html +=cut