# $Id$ ################################################################################ # # 34_ESPEasy.pm is a FHEM Perl module to control ESP82xx/ESP32 /w ESPEasy # # Copyright 2018 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; # ------------------------------------------------------------------------------ # required perl/fhem modules # ------------------------------------------------------------------------------ use strict; use warnings; use Data::Dumper; use MIME::Base64; use TcpServerUtils; use HttpUtils; use Color; use SetExtensions; my $module_version = "2.18"; # Version of this module # ------------------------------------------------------------------------------ # modul version and required ESP Easy firmware / JSON lib version # ------------------------------------------------------------------------------ my $minEEBuild = 128; # informational my $minJsonVersion = 1.02; # checked in received data # ------------------------------------------------------------------------------ # default values # ------------------------------------------------------------------------------ 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; # do no00t resend failed http requests my $d_displayTextEncode = 1; # urlEncode Text for Displays my $d_displayTextWidth = 0; # display width, 0 => disable formating my $d_bridgePort = 8383; # bridge port if none specified my $d_disableLogin = 0; # Disable login if HTTP Code 302 my $d_maxCmdDuration = 1; # max cmd exec time, subtracted from awake time my $d_sleepReading = 'sleepState'; # Reading used for Indication of deep sleep # ------------------------------------------------------------------------------ # defaults for user defined cmds # ------------------------------------------------------------------------------ my $d_args = 0; # min number of required arguments my $d_urlPlg = "/control?cmd="; # plugin command URL my $d_urlSys = "/?cmd="; # system command URL my $d_widget = ""; # widget defaults my $d_usage = ""; # usage defaults # ------------------------------------------------------------------------------ # IP ranges that are allowed to connect to ESPEasy without attr allowedIPs set. # defined as regexp beause it's quicker than check against IP ranges... # ------------------------------------------------------------------------------ my $d_allowedIPs = "192.168.0.0/16,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12," . "fe80::/10,fc00::/7,::1"; my $d_localIPs = "^(127|192.168|172.(1[6-9]|2[0-9]|3[01])|10|169.254)\\.|" . "^(f(e[89ab]|[cd])|::1)"; # ------------------------------------------------------------------------------ # some mappings # ------------------------------------------------------------------------------ my %ee_map = ( build => { # ESP Easy build versions 1 => { type => "ESP Easy", ver => "STD" }, 5 => { type => "RPI Easy", ver => "STD" }, # https://github.com/enesbcs/rpieasy 17 => { type => "ESP Easy Mega", ver => "STD" }, 33 => { type => "ESP Easy 32", ver => "STD" }, 65 => { type => "ARDUINO Easy", ver => "STD" }, 81 => { type => "NANO Easy", ver => "STD" } }, pins => { # Arduino pin names, keys must be upper case here # ESP82xx / ESP32 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, # ESP32 TOUCH0 => 4, TOUCH1 => 0, TOUCH2 => 21, TOUCH3 => 15, TOUCH4 => 13, TOUCH5 => 12, TOUCH6 => 14, TOUCH7 => 27, TOUCH8 => 33, TOUCH9 => 32, # ESP32 ADC1_0 => 36, ADC1_1 => 37, ADC1_2 => 38, ADC1_3 => 39, ADC1_4 => 32, ADC1_5 => 33, ADC1_6 => 34, ADC1_7 => 35, ADC2_0 => 4, ADC2_1 => 0, ADC2_2 => 21, ADC2_3 => 15, ADC2_4 => 13, ADC2_5 => 12, ADC2_6 => 14, ADC2_7 => 27, ADC2_8 => 25, ADC2_9 => 26 }, rst => { # readingSwitchText => { 10 => { # vType => { 1 => { 0 => "off", 1 => "on" }, # attr_rst => {org => new, ...}, 2 => { 0 => "on", 1 => "off" } # attr_rst => {org => new, ...} } # } }, onOff => { # on/off mappings within setFn on => 1, off => 0 } ); # ------------------------------------------------------------------------------ # get commands # ------------------------------------------------------------------------------ my %ee_gets = ( bridge => { queuesize => {widget => "noArg", fn => ""}, queuecontent => {widget => "", fn => ""}, pinmap => {widget => "noArg", fn => ""}, user => {widget => "noArg", fn => ""}, pass => {widget => "noArg", fn => ""}, }, device => { pinmap => {widget => "noArg", fn => ""}, setcmds => {widget => "noArg", fn => ""}, adminpassword => {widget => "noArg", fn => ""} } ); # ------------------------------------------------------------------------------ # attributes # ------------------------------------------------------------------------------ my %ee_attr = ( all => { disable => { widget => "1,0" }, disabledForIntervals => { widget => "" }, do_not_notify => { widget => "0,1" }, }, bridge => { allowedIPs => { widget => "" }, authentication => { widget => "1,0" }, autocreate => { widget => "1,0" }, autosave => { widget => "1,0" }, combineDevices => { widget => "" }, deniedIPs => { widget => "" }, httpReqTimeout => { widget => "" }, maxQueueSize => { widget => "10,25,50,100,250,500,1000,2500,5000,10000,25000,50000,100000" }, maxHttpSessions => { widget => "0,1,2,3,4,5,6,7,8,9" }, resendFailedCmd => { widget => "" }, }, device => { adjustValue => { widget => "" }, deepsleep => { widget => "0,1" }, disableRiskyCmds => { widget => "" }, displayTextEncode => { widget => "1,0" }, displayTextWidth => { widget => "" }, IODev => { widget => "" }, Interval => { widget => "" }, mapLightCmds => { widget => "lights,nfx" }, maxCmdDuration => { widget => "slider,0,0.25,15,1" }, parseCmdResponse => { widget => "" }, pollGPIOs => { widget => "" }, presenceCheck => { widget => "1,0" }, readingPrefixGPIO => { widget => "" }, readingSuffixGPIOState => { widget => "" }, readingSwitchText => { widget => "1,0,2" }, setState => { widget => "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,25,50,100" }, userSetCmds => { widget => "textField-long" }, useSetExtensions => { widget => "0,1"}, rgbGPIOs => { widget => "" }, wwcwGPIOs => { widget => "" }, colorpicker => { widget => "RGB,HSV,HSVp" }, }, # attr_rgbGPIOs => { # colorpicker => { widget => "RGB,HSV,HSVp" }, # }, attr_wwcwGPIOs => { colorpickerCTcw => { widget => "" }, colorpickerCTww => { widget => "" }, ctCW_reducedRange => { widget => "" }, ctWW_reducedRange => { widget => "" }, wwcwMaxBri => { widget => "0,1" }, } ); # ------------------------------------------------------------------------------ # - get available set cmds based on attributes # - available cmds can be found in $data{ESPEasy}{device}{sets}... # - will be called from notifyFN() on INITIALIZED, REREADCFG and some attr changes # ------------------------------------------------------------------------------ sub ESPEasy_initDevSets($) { my ($hash) = @_; my $name = $hash->{NAME}; my $subtype = $hash->{SUBTYPE}; # define colorpickers for use below my $cp_pct = "colorpicker,BRI,0,1,100"; my $cp_bri = "colorpicker,BRI,0,1,255"; my $cp_ct = "colorpicker,CT," . AttrVal($name,"ctWW_reducedRange",AttrVal($name,"colorpickerCTww",$d_colorpickerCTww)) . ",10," . AttrVal($name,"ctCW_reducedRange",AttrVal($name,"colorpickerCTcw",$d_colorpickerCTcw)); my $cp_rgb = "colorpicker,".AttrVal($name,"colorpicker","HSVp"); my %ee_sets = ( bridge => { # bridge commands user => { args => 0, url => "", widget => "", usage => "" }, pass => { args => 0, url => "", widget => "", usage => "" }, clearqueue => { args => 0, url => "", widget => "noArg", usage => "" }, active => { args => 0, url => "", widget => "noArg", usage => "" }, inactive => { args => 0, url => "", widget => "noArg", usage => "" }, reopen => { args => 0, url => "", widget => "noArg", usage => "" } }, device => { # known ESP Easy plugin commands gpio => { args => 2, url => $d_urlPlg, widget => "", usage => " <0|1|off|on>" }, pwm => { args => 2, url => $d_urlPlg, widget => "", usage => " " }, pwmfade => { args => 3, url => $d_urlPlg, widget => "", usage => " " }, pulse => { args => 3, url => $d_urlPlg, widget => "", usage => " <0|1|off|on> " }, longpulse => { args => 3, url => $d_urlPlg, widget => "", usage => " <0|1|off|on> " }, longpulse_ms => { args => 3, url => $d_urlPlg, widget => "", usage => " <0|1|off|on> " }, servo => { args => 3, url => $d_urlPlg, widget => "", usage => " " }, lcd => { args => 3, url => $d_urlPlg, widget => "", usage => " " }, lcdcmd => { args => 1, url => $d_urlPlg, widget => "", usage => "" }, mcpgpio => { args => 2, url => $d_urlPlg, widget => "", usage => " <0|1|off|on>" }, mcppulse => { args => 3, url => $d_urlPlg, widget => "", usage => " <0|1|off|on> " }, mcplongpulse => { args => 3, url => $d_urlPlg, widget => "", usage => " <0|1|off|on> " }, oled => { args => 3, url => $d_urlPlg, widget => "", usage => " " }, oledcmd => { args => 1, url => $d_urlPlg, widget => "", usage => "" }, pcapwm => { args => 2, url => $d_urlPlg, widget => "", usage => " " }, pcfgpio => { args => 2, url => $d_urlPlg, widget => "", usage => " <0|1|off|on>" }, pcfpulse => { args => 3, url => $d_urlPlg, widget => "", usage => " <0|1|off|on> " }, pcflongpulse => { args => 3, url => $d_urlPlg, widget => "", usage => " <0|1|off|on> " }, irsend => { args => 3, url => $d_urlPlg, widget => "", usage => " | irsend " }, #_P035_IRTX.ino status => { args => 2, url => $d_urlPlg, widget => "", usage => " " }, lights => { args => 1, url => $d_urlPlg, widget => "", usage => " [color] [fading time] [pct]" }, dots => { args => 1, url => $d_urlPlg, widget => "", usage => "" }, tone => { args => 3, url => $d_urlPlg, widget => "", usage => " " }, rtttl => { args => 1, url => $d_urlPlg, widget => "", usage => "" }, dmx => { args => 1, url => $d_urlPlg, widget => "", usage => "" }, motorshieldcmd => { args => 5, url => $d_urlPlg, widget => "", usage => " " }, candle => { args => 0, url => $d_urlPlg, widget => "", usage => ":::" }, # params are splited by ":" not " " neopixel => { args => 4, url => $d_urlPlg, widget => "", usage => " " }, neopixelall => { args => 3, url => $d_urlPlg, widget => "", usage => " " }, neopixelline => { args => 5, url => $d_urlPlg, widget => "", usage => " " }, oledframedcmd => { args => 1, url => $d_urlPlg, widget => "", usage => "" }, serialsend => { args => 1, url => $d_urlPlg, widget => "", usage => "" }, #_P020_Ser2Net.ino buzzer => { args => 0, url => $d_urlPlg, widget => "", usage => "" }, inputswitchstate => { args => 0, url => $d_urlPlg, widget => "", usage => "" }, nfx => { args => 1, url => $d_urlPlg, widget => "", usage => " " }, event => { args => 1, url => $d_urlPlg, widget => "", usage => "" }, #changed url to sys-url; # rules related commands deepsleep => { args => 1, url => $d_urlSys, widget => "", usage => "" }, publish => { args => 2, url => $d_urlSys, widget => "", usage => " " }, notify => { args => 0, url => $d_urlSys, widget => "", usage => " " }, reboot => { args => 0, url => $d_urlSys, widget => "noArg", usage => "" }, rules => { args => 1, url => $d_urlSys, widget => "", usage => "<0|1|off|on>" }, #enable/disable use of rules sendto => { args => 2, url => $d_urlSys, widget => "", usage => " " }, sendtohttp => { args => 3, url => $d_urlSys, widget => "", usage => " " }, sendtoudp => { args => 3, url => $d_urlSys, widget => "", usage => " " }, nosleep => { args => 0, url => $d_urlSys, widget => "", usage => "" }, taskvalueset => { args => 3, url => $d_urlSys, widget => "", usage => " " }, taskvaluesetandrun => {args=> 3, url => $d_urlSys, widget => "", usage => " " }, taskrun => { args => 1, url => $d_urlSys, widget => "", usage => "" }, timerset => { args => 2, url => $d_urlSys, widget => "", usage => " " }, # dummies raw => { args => 1, url => $d_urlPlg, widget => "", usage => " [args]" }, rawsystem => { args => 1, url => $d_urlSys, widget => "", usage => " [args]" }, # internal cmds statusrequest => { args => 0, url => "", widget => "noArg", usage => "" }, adminpassword => { args => 0, url => "", widget => "", usage => "" }, clearreadings => { args => 0, url => "", widget => "noArg", usage => "" }, active => { args => 0, url => "", widget => "noArg", usage => "" }, inactive => { args => 0, url => "", widget => "noArg", usage => "" } }, system => { # system commands (another url) erase => { args => 0, url => $d_urlSys, widget => "noArg", usage => "" }, reset => { args => 0, url => $d_urlSys, widget => "noArg", usage => "" }, resetflashwritecounter => { args => 0, url => $d_urlSys, widget => "noArg", usage => "" }, }, attr_rgbGPIOs => { rgb => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [fadetime] [delay +/-ms]" }, pct => { args => 1, url => $d_urlPlg, widget => $cp_pct, usage => " [fadetime]" }, on => { args => 0, url => $d_urlPlg, widget => "noArg", usage => "" }, off => { args => 0, url => $d_urlPlg, widget => "noArg", usage => "" }, toggle => { args => 0, url => $d_urlPlg, widget => "noArg", usage => "" }, }, attr_wwcwGPIOs => { pct => { args => 1, url => $d_urlPlg, widget => $cp_pct, usage => " [fadetime]" }, ct => { args => 1, url => $d_urlPlg, widget => $cp_ct, usage => " [fadetime] [pct bri]" }, on => { args => 0, url => $d_urlPlg, widget => "noArg", usage => "" }, off => { args => 0, url => $d_urlPlg, widget => "noArg", usage => "" }, toggle => { args => 0, url => $d_urlPlg, widget => "noArg", usage => "" }, }, attr_lights => { # Lights rgb => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [fadetime] [delay +/-ms]" }, pct => { args => 1, url => $d_urlPlg, widget => $cp_pct, usage => " [fadetime]" }, ct => { args => 1, url => $d_urlPlg, widget => $cp_ct, usage => " [fadetime] [pct bri]" }, on => { args => 0, url => $d_urlPlg, widget => "", usage => "[fadetime]" }, off => { args => 0, url => $d_urlPlg, widget => "", usage => "[fadetime]" }, toggle => { args => 0, url => $d_urlPlg, widget => "", usage => "[fadetime]" }, }, attr_nfx => { # nfx commands - Forum #73949 rgb => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [fadetime] [delay +/-ms]" }, pct => { args => 1, url => $d_urlPlg, widget => $cp_pct, usage => " [fadetime]" }, ct => { args => 1, url => $d_urlPlg, widget => $cp_ct, usage => " [fadetime] [pct bri]" }, on => { args => 0, url => $d_urlPlg, widget => "", usage => "[fadetime] [delay +/-ms]" }, off => { args => 0, url => $d_urlPlg, widget => "", usage => "[fadetime] [delay +/-ms]" }, toggle => { args => 0, url => $d_urlPlg, widget => "", usage => "[fadetime]" }, all => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [fadetime] [delay +/-ms]" }, bgcolor => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => "" }, colorfade => { args => 2, url => $d_urlPlg, widget => "", usage => " [startpixel] [endpixel]" }, comet => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [speed +/- 0-50]" }, dim => { args => 1, url => $d_urlPlg, widget => $cp_bri, usage => "" }, dualscan => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [rrggbb background] [speed 0-50]" }, fade => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [fadetime ms] [delay +/-ms]" }, fire => { args => 0, url => $d_urlPlg, widget => "", usage => "[fps] [brightness 0-255] [cooling 20-100] [sparking 50-200]" }, fireflicker => { args => 0, url => $d_urlPlg, widget => "", usage => "[intensity 0-255] [speed 0-50]" }, kitt => { args => 1, url => $d_urlPlg, widget => "", usage => " [speed 0-50]" }, line => { args => 3, url => $d_urlPlg, widget => "", usage => " " }, one => { args => 2, url => $d_urlPlg, widget => "", usage => " " }, scan => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [rrggbb background] [speed 0-50]" }, sparkle => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [rrggbb background] [speed 0-50]" }, stop => { args => 0, url => $d_urlPlg, widget => "noArg", usage => "" }, theatre => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [rrggbb background] [speed +/- 0-50]" }, twinkle => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [rrggbb background] [speed 0-50]" }, twinklefade => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [number of pixels] [speed 0-50]" }, wipe => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [rrggbb dot] [speed +/- 0-50]" }, dualwipe => { args => 1, url => $d_urlPlg, widget => $cp_rgb, usage => " [rrggbb dot] [speed +/- 0-50]" }, faketv => { args => 0, url => $d_urlPlg, widget => "", usage => "[startpixel] [endpixel]" }, simpleclock => { args => 0, url => $d_urlPlg, widget => "", usage => "[bigtickcolor] [smalltickcolor] [hourcolor] [minutecolor] [secondcolor]" }, count => { args => 1, url => $d_urlPlg, widget => "slider,1,1,50", usage => "" }, fadedelay => { args => 1, url => $d_urlPlg, widget => "slider,-5000,10,5000", usage => "" }, fadetime => { args => 1, url => $d_urlPlg, widget => "slider,0,100,10000", usage => "" }, rainbow => { args => 0, url => $d_urlPlg, widget => "slider,-10,1,10", usage => "[speed +/- 0-50]" }, speed => { args => 1, url => $d_urlPlg, widget => "slider,-50,1,50", usage => "" }, } ); # hash %ee_sets # gather required categories my @categories; my $mapLightsCmd = lc AttrVal($name,"mapLightCmds",0); push (@categories, $subtype); if ($subtype eq "device") { push (@categories, "system") if !AttrVal("$name","disableRiskyCmds",0); push (@categories, "attr_".$mapLightsCmd) if $mapLightsCmd && defined $ee_sets{"attr_$mapLightsCmd"}; push (@categories, "attr_rgbGPIOs") if AttrVal("$name","rgbGPIOs",0); push (@categories, "attr_wwcwGPIOs") if AttrVal("$name","wwcwGPIOs",0); } # build hash of avail commands # todo: with hashref copy, see https://perlmaven.com/how-to-insert-a-hash-in-another-hash ??? my %activeSets; foreach my $cat (@categories) { foreach my $cmd (keys %{ $ee_sets{$cat} } ) { $activeSets{$cmd} = $ee_sets{$cat}{$cmd}; } } # write all mapped subcms in $hash->{helper}{mapLightCmds}, will be used in SetFn; delete $hash->{helper}{mapLightCmds}; if ($mapLightsCmd) { foreach (keys %{$ee_sets{"attr_$mapLightsCmd"}}) { $hash->{helper}{mapLightCmds}{$_} = $mapLightsCmd; } } # user cmds/maps my $userSetCmds = AttrVal($name,"userSetCmds",0); if ($userSetCmds) { my %ua = eval($userSetCmds); if ($@) { Log3 $name, 2, "An error occourred while building user defined cmds/maps: $@"; return $@; } foreach my $plugin (keys %ua) { my $p = lc($plugin); # use reverse order to be sure plugin's url is set before subcmds. my @keys = reverse sort keys %{ $ua{$plugin} }; foreach my $key (@keys) { # key is a mapped subcmd if ( ref($ua{$plugin}{$key}) eq "HASH" ) { foreach my $subcmd (keys %{ $ua{$plugin}{$key} }) { my $sc = lc($subcmd); $activeSets{$sc} = $ua{$plugin}{$key}{$subcmd}; # write all mapped subcms in $hash->{helper}{mapLightCmds}, will be used in SetFn; $hash->{helper}{mapLightCmds}{$sc} = $p; # Set defaults for mapped cmds and be sure all keys are defined in following fns $activeSets{$sc}{args} = $d_args if !defined $activeSets{$sc}{args}; $activeSets{$sc}{widget} = $d_widget if !defined $activeSets{$sc}{widget}; $activeSets{$sc}{usage} = $d_usage if !defined $activeSets{$sc}{usage}; # use plugin's url, if not defined use default $activeSets{$sc}{url} = defined $activeSets{$p}{url} ? $activeSets{$p}{url} : $d_urlPlg if !defined $activeSets{$sc}{url}; } } # key is param for plugin cmd else { $activeSets{$p}{$key} = $ua{$plugin}{$key}; } } # Set defaults for plugin cmds and be sure all keys are defined in following fns $activeSets{$p}{args} = $d_args if !defined $activeSets{$p}{args}; $activeSets{$p}{url} = $d_urlPlg if !defined $activeSets{$p}{url}; $activeSets{$p}{widget} = $d_widget if !defined $activeSets{$p}{widget}; $activeSets{$p}{usage} = $d_usage if !defined $activeSets{$p}{usage}; } } # add help command $activeSets{help} = { args => 1, widget => join(",",sort keys %activeSets), url => "", usage => "<".join(",",sort keys %activeSets).">" }; # reference to all available cmds $data{ESPEasy}{$name}{sets} = \%activeSets; Log3 $name, 4, "ESPEasy $name: Available set cmds/maps (re)initialized."; } # ------------------------------------------------------------------------------ # enable colorpicker etc. only if attrs (rgb|wwcw)GPIOs|mapLightCmds are set # called by NotifyFn # ------------------------------------------------------------------------------ sub ESPEasy_initDevAttrs($) { my ($hash) = @_; my ($name, $subtype) = ($hash->{NAME}, $hash->{SUBTYPE}); # add attr_.* categories if corresponding attr is in use my @cats = ($subtype, "all"); foreach (keys %ee_attr) { if (m/^attr_(\w+)$/) { push(@cats, "attr_".$1) if defined AttrVal($name, $1, undef) || defined AttrVal($name,"mapLightCmds",undef); } } # push attributes from selected categories in array @attrs my @attrs; foreach my $cat (@cats) { foreach my $attr (sort keys %{ $ee_attr{$cat} }) { my $w = $ee_attr{$cat}{$attr}{widget}; # push attrs with corresponding widget push(@attrs, $attr . ($w ne "" ? ":$w" : "")); } } push (@attrs, $readingFnAttributes); setDevAttrList($name, join(" ", sort @attrs)); Log3 $name, 4, "ESPEasy $name: Available attributes (re)initialized."; } # ------------------------------------------------------------------------------ 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"; $hash->{StateFn} = "ESPEasy_State"; #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} = ".+"; # add all attributes to hash, unnecessary attributes will be removed in # ESPEasy_initDevAttrs called from NotifyFn my @attr; foreach my $subtype (keys %ee_attr) { foreach my $attr ( keys %{ $ee_attr{$subtype} } ) { push ( @attr, $attr . ( $ee_attr{$subtype}{$attr}{widget} ne "" ? ":" . $ee_attr{$subtype}{$attr}{widget} : "" ) # ternary if ) # push } # foreach $attr } # foreach $subtype push (@attr, $readingFnAttributes); $hash->{AttrList} = join(" ",sort @attr); # for the next release... # $hash->{AttrRenameMap} = { "ctCW_reducedRange" => "ctCWreducedRange", # "ctWW_reducedRange" => "ctWWreducedRange", # "colorpickerCTcw" => "ctCWColorpicker" # "colorpickerCTww" => "ctWWcolorpicker" # "wwcwMaxBri" => "ctMaxBri" # }; } # ------------------------------------------------------------------------------ sub ESPEasy_Define($$) # only called when defined, not on reload. { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $usage = "\nUse 'define ESPEasy " . "\nUse 'define ESPEasy "; return "Wrong syntax: $usage" if(int(@a) < 3); my $name = $a[0]; my $type = $a[1]; my $host = $a[2]; my $port; $port = $a[3] if defined $a[3]; $port = 8383 if !defined $port && $host eq "bridge"; my $iodev = $a[4] if defined $a[4]; my $ident = $a[5] if defined $a[5]; my $ipv = $port =~ m/^IPV6:/ ? 6 : 4; return "ERROR: only 1 ESPEasy bridge can be defined!" if($host eq "bridge" && $modules{ESPEasy}{defptr}{BRIDGE}{$ipv}); return "ERROR: missing arguments for subtype device: $usage" if ($host ne "bridge" && !(defined $a[4]) && !(defined $a[5])); return "ERROR: too much arguments for a bridge: $usage" if ($host eq "bridge" && defined $a[4]); (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 (req. setDevAttrList Forum # 85868, 86010) AttrVal('global','version','') =~ m/^fhem.pl:(\d+)\/.*$/; return "ERROR: fhem.pl is too old to use $type module. " ."Version 16453/2018-03-21 is required at least." if (not(defined $1) || $1 < 16453); $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"; $hash->{IPV} = $ipv; $modules{ESPEasy}{defptr}{BRIDGE}{$ipv} = $hash; if ($init_done && !defined($hash->{OLDDEF})) { CommandAttr(undef,"$name room $type"); CommandAttr(undef,"$name group $type Bridge"); CommandAttr(undef,"$name authentication 0"); CommandAttr(undef,"$name combineDevices 0"); } $hash->{".bau"} = getKeyValue($type."_".$name."-user"); $hash->{".bap"} = getKeyValue($type."_".$name."-pass"); # only informational $hash->{MAX_HTTP_SESSIONS} = $d_maxHttpSessions; $hash->{MAX_QUEUE_SIZE} = $d_maxQueueSize; # Check OS IPv6 support if ($ipv == 6) { use constant HAS_AF_INET6 => defined eval { Socket::AF_INET6() }; Log3 $name, 2, "$type $name: WARNING: Your system seems to have no IPv6 support." if !HAS_AF_INET6; } # $hash->{MODEL} = 'Bridge v'.$module_version; } #--- DEVICE ------------------------------------------------- else { $hash->{INTERVAL} = $d_Interval; $hash->{SUBTYPE} = "device"; $hash->{sec}{admpwd} = getKeyValue($type."_".$name."-admpwd"); AssignIoPort($hash,$iodev) if !defined $hash->{IODev}; InternalTimer(gettimeofday()+5+rand(5), "ESPEasy_statusRequest", $hash); readingsSingleUpdate($hash, 'state', 'Initialized',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"; } ESPEasy_initDevSets($hash); ESPEasy_loadRequiredModules($hash); return undef; } # ------------------------------------------------------------------------------ sub ESPEasy_Get(@) { my ($hash, $name, $cmd, @args) = @_; return "argument is missing" if !$cmd; my $subtype = $hash->{SUBTYPE}; $cmd = lc $cmd; my $ret; if( !grep( m/^$cmd$/, keys %{ $ee_gets{$subtype} } ) || $cmd eq "?") { my @clist; foreach my $c ( sort keys %{ $ee_gets{$subtype} } ) { my $w = $ee_gets{$subtype}{$c}{widget} ? ":".$ee_gets{$subtype}{$c}{widget} : ""; push(@clist, $c.$w); } return "Unknown argument $cmd, choose one of ". join(" ",@clist); } # lookup sub fn to be executed or use "ESPEasy_Get_$cmd" my $fn = $ee_gets{$subtype}{$cmd}{fn}; $fn = $fn ne "" ? $fn : "ESPEasy_Get_$cmd"; # exec $fn return &{\&{ $fn }}(@_); } # ------------------------------------------------------------------------------ # GetFn subs, called by reference to $cmd name or global $gets{$cmd}{fn} # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # get username or password that is being used # ------------------------------------------------------------------------------ sub ESPEasy_Get_user(@) { my ($hash, $name, $cmd, @args) = @_; return defined $hash->{".bau"} ? $hash->{".bau"} : "username is not defined, yet."; } # ------------------------------------------------------------------------------ # get username or password that is being used # ------------------------------------------------------------------------------ sub ESPEasy_Get_pass(@) { my ($hash, $name, $cmd, @args) = @_; return defined $hash->{".bap"} ? $hash->{".bap"} : "password is not defined, yet."; } # ------------------------------------------------------------------------------ # get arduino pin mappings that can be used # ------------------------------------------------------------------------------ sub ESPEasy_Get_adminpassword(@) { my ($hash, $name, $cmd, @args) = @_; return defined $hash->{sec}{admpwd} ? $hash->{sec}{admpwd} : "password is not defined, yet."; } # ------------------------------------------------------------------------------ # get formated list of available commands # ------------------------------------------------------------------------------ sub ESPEasy_Get_setcmds(@) { my ($hash) = @_; my ($type, $name) = ($hash->{TYPE}, $hash->{NAME}); my ($args, $url, $widget, $usage); my $line = "-" x 79 . "\n"; $line .= "plugin / mapped cmd |mapped to plugin |args|url |widget |\n"; $line .= "-" x 79 . "\n"; foreach my $cmd (sort keys %{ $data{$type}{$name}{sets} }) { next if $cmd =~ m/^(help|clearreadings|statusrequest)$/; my $plugin = defined $hash->{helper}{mapLightCmds}{$cmd} ? $hash->{helper}{mapLightCmds}{$cmd} : "-"; $line .= substr( $cmd . " " x (21 - length($cmd)) ,0,21 ) ."|"; $line .= substr( $plugin . " " x (17 - length($plugin)) ,0,17 ) ."|"; my $c = $data{$type}{$name}{sets}{$cmd}; # just a little bit shorter... $line .= substr( $c->{args} . " " x(4 - length($c->{args})) ,0,4 ) ."|"; $line .= substr( $c->{url} . " " x(14 - length($c->{url})) ,0,14 ) ."|"; $line .= substr( $c->{widget} . " " x(18 - length($c->{widget})) ,0,18 ) ."|\n"; } # replace lace braces for FHEMWEB if ($hash->{CL}{TYPE} eq "FHEMWEB") { $line =~ s//>/g; } return $line; } # ------------------------------------------------------------------------------ # get arduino pin mappings that can be used # ------------------------------------------------------------------------------ sub ESPEasy_Get_pinmap(@) { my $ret .= "\nAlias => GPIO\n"; $ret .= "---------------\n"; foreach (sort keys %{$ee_map{pins}}) { $ret .= $_." " x (8-length $_ ) ."=> $ee_map{pins}{$_}\n"; } return $ret; } # ------------------------------------------------------------------------------ # simple get queue sizes # ------------------------------------------------------------------------------ sub ESPEasy_Get_queuesize(@) { my ($hash, $name, $cmd, @args) = @_; my $ret; foreach (keys %{ $hash->{helper}{queue} }) { $ret .= "$_:".scalar @{$hash->{helper}{queue}{"$_"}}." "; } return $ret ? $ret : "No queues in use."; } # ------------------------------------------------------------------------------ # get queue content of all/selected queues # ------------------------------------------------------------------------------ sub ESPEasy_Get_queuecontent(@) { my ($hash, $name, $cmd, @args) = @_; my $host = defined $args[0] ? $args[0] : ""; my $ret; my $i = 0; my $j = 0; my $mseclog = AttrVal("global","mseclog",0); if (defined $hash->{helper}{queue}) { my $Xspace = " "x ($mseclog ? 20 : 16); # different spacer if attr/global/mseclog my $Xdash = ("-"x80)."\n"; # just a few dashes; foreach my $q (sort keys %{ $hash->{helper}{queue} }) { next if $host ne "" && $q !~ m/^$host$/; $ret .= "\nQueue for host $q:\n"; $ret .= $Xdash."Time:".$Xspace."Cmd:\n".$Xdash; # queue title $i = 0; foreach my $qe (@{ $hash->{helper}{queue}{$q} }) { my ($s,$ms) = split(/\./,$qe->{ts}); # get secs + mSecs, see WriteFn my $ts = FmtDateTime($s); # format time string as FHEM does $ts .= sprintf(".%03d", $ms/1000) if $mseclog; # add .msecs if attr/global/mseclog $ret .= $ts ." " .$qe->{cmd} ." " .join(",",@{$qe->{cmdArgs}})."\n"; $i++ # single queue counter } $ret .= "=> $i entries\n"; # single queue counter $j += $i; # add single counter to overall counter } } return $ret ? $ret."\n==> Number of all requested queue entries: $j entries" : "No specified queues active."; } # ------------------------------------------------------------------------------ sub ESPEasy_Set($$@) { my ($hash, $name, $cmd, @params) = @_; my ($type, $subtype) = ($hash->{TYPE}, $hash->{SUBTYPE}); # case insensitive $cmd = lc($cmd) if $cmd && $cmd !~ m/^(attrTemplate)$/; # get current cmd list if cmd is __unknown__ my $clist = ESPEasy_isCmdAvailable($hash,$cmd); if (defined $clist) { if (AttrVal($name,"useSetExtensions",0)) { Log3 $name, 3, "$type $name: set $name $cmd ".join(" ",@params)." (use set extensions)" if $cmd =~ m/^(o(n|ff)-(for-timer|till(-overnight)?)|blink|intervals|toggle)$/ ; return SetExtensions($hash, $clist, ($name, $cmd, @params)); } return "Unknown argument $cmd, choose one of $clist"; } SetExtensionsCancel($hash); # Forum #53137 return if (IsDisabled $name) && $cmd !~ m/^(in)?active$/; # Log set command Log3 $name, 3, "$type $name: set $name $cmd ".join(" ",@params) if $cmd !~ m/^(\?|user|pass|help|active|inactive)$/; # check if there are all required arguments my $set = $data{ESPEasy}{$name}{sets}{$cmd}; if($set->{args} && scalar @params < $set->{args}) { Log3 $name, 2, "$type $name: Missing argument: 'set $name $cmd ".join(" ",@params)."'" if $cmd ne "help"; return "Missing argument: $cmd needs at least $set->{args} argument" . ($set->{args} < 2 ? "" : "s")."\n" . "Usage: 'set $name $cmd $set->{usage}'"; } # if (defined &{\&{ "ESPEasy_Set_$cmd" }}) { # return &{\&{ "ESPEasy_Set_$cmd" }}(@_); # } # Internal cmds if ($cmd eq "help") { my $usage = $data{ESPEasy}{$name}{sets}{$params[0]}{usage}; return $usage ? "Usage: set $name $params[0] $usage" : "Note: '$params[0]' is not registered as an ESPEasy command. " . "See attribute userSetCmds to register your own or unsupported commands."; } 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]); $cmd eq "user" ? $hash->{".bau"} = $params[0] : $hash->{".bap"} = $params[0]; return undef; } elsif ($cmd eq "reopen" ) { ESPEasy_TcpServer_Close($hash); ESPEasy_TcpServer_Open($hash); return undef; } elsif ($cmd =~ m/^(in)?active$/) { if (AttrVal($name, "disable", 0) == 1) { my $msg = "Set cmd '$cmd' ignored, attribute 'disable' is enabled."; Log3 $name, 3, "$type $name: $msg"; return $msg; } elsif ($cmd eq "inactive") { if ($subtype eq "bridge") { ESPEasy_TcpServer_Close($hash); } else { # device } readingsSingleUpdate($hash, "state", "inactive", 1); Log3 $name, 3, "$type $name: Device disabled"; } elsif ($cmd eq "active") { if (ReadingsVal($name, "state", "") ne "Initialized") { if ($subtype eq "bridge") { ESPEasy_TcpServer_Open($hash); } else { readingsSingleUpdate($hash, "state", "Initialized", 1); ESPEasy_setState($hash); Log3 $name, 3, "$type $name: Device enabled"; } return undef; } } return undef; } # Device cmds elsif ($cmd eq "statusrequest") { ESPEasy_statusRequest($hash); return undef; } elsif ($cmd eq "clearreadings") { ESPEasy_clearReadings($hash); return undef; } elsif ($cmd =~ m/^adminpassword$/ ) { setKeyValue($hash->{TYPE}."_".$hash->{NAME}."-admpwd", $params[0]); $hash->{sec}{admpwd} = $params[0]; return undef; } return undef if $subtype eq "bridge"; # urlEncode parameter @params = ESPEasy_urlEncodeDisplayText($hash,$cmd,@params); # pin mapping (eg. D8 -> 15), parameter my $pp = ESPEasy_paramPos($hash,$cmd,''); if ($pp && $params[$pp-1] =~ m/^[a-zA-Z]/) { Log3 $name, 5, "$type $name: Pin mapping ". uc $params[$pp-1] . " => ".$ee_map{pins}{uc $params[$pp-1]}; $params[$pp-1] = $ee_map{pins}{uc $params[$pp-1]}; } # onOff mapping (on/off -> 1/0), <0|1|off|on> parameter $pp = ESPEasy_paramPos($hash,$cmd,'<0|1|off|on>'); if ($pp) { my $ooArg = lc($params[$pp-1]); my $ooVal = defined $ee_map{onOff}{$ooArg} ? $ee_map{onOff}{$ooArg} : undef; if (defined $ooVal) { Log3 $name, 5, "$type $name: onOff mapping ". $params[$pp-1]." => $ooVal"; $params[$pp-1] = $ooVal; } } # re-map cmds if necessary if (defined $hash->{helper}{mapLightCmds} && defined $hash->{helper}{mapLightCmds}{$cmd}) { unshift @params, $cmd; $cmd = $hash->{helper}{mapLightCmds}{$cmd}; } # special handling for attrs wwcwGPIOs & rgbGPIOs 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 related 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); } } # Log device set cmd with all mappings Log3 $name, 5, "$type $name: set $name $cmd ".join(" ",@params). " (mappings done)" if $cmd !~ m/^(\?|user|pass|help)$/; Log3 $name, 5, "$type $name: IOWrite ( \$defs{$name}, \$defs{$name}, $cmd, (\"".join("\",\"",@params)."\") )"; # send cmd with required args to IO Device my $parseCmd = ESPEasy_isParseCmd($hash,$cmd); # should response be parsed and dispatched? IOWrite($hash, $hash, $parseCmd, $cmd, @params); return undef; } ## ------------------------------------------------------------------------------ #sub ESPEasy_Set_help(@) { # my ($hash, $name, $cmd, @params) = @_; # my $usage = $data{ESPEasy}{$name}{sets}{$params[0]}{usage}; # return $usage ? "Usage: set $name $params[0] $usage" # : "Note: '$params[0]' is not registered as an ESPEasy command. " # . "See attribute userSetCmds to register your own or unsupported commands."; #} # ------------------------------------------------------------------------------ sub ESPEasy_Read($) { my ($hash) = @_; #hash of temporary child instance my $name = $hash->{NAME}; my $ipv = $hash->{IPV} ? $hash->{IPV} : ($hash->{PEER} =~ m/:/ ? 6 : 4); my $bhash = $modules{ESPEasy}{defptr}{BRIDGE}{$ipv}; #hash of original instance my $bname = $bhash->{NAME}; my $btype = $bhash->{TYPE}; # Accept and create a child if( $hash->{SERVERSOCKET} ) { # Levering new TcpServerUtils security feature, use our own TcpServer_Accept() my $aRet = ESPEasy_TcpServer_Accept($hash,"ESPEasy"); return; } # use received IP instead of configured one (NAT/PAT could have modified) my $peer = $hash->{PEER}; # Read max 9000 bytes, return num of read bytes my $buf; my $ret = sysread($hash->{CD}, $buf, 9000); # accept jumbo frames # Delete temporary device if( !defined($ret ) || $ret <= 0 ) { CommandDelete( undef, $hash->{NAME} ); return; } # Check allowed IPs if ( !( ESPEasy_isPeerAllowed($peer,AttrVal($bname,"allowedIPs", $d_allowedIPs)) && !ESPEasy_isPeerAllowed($peer,AttrVal($bname,"deniedIPs",0)) ) ) { Log3 $bname, 2, "$btype $name: Peer address rejected"; return; } Log3 $bname, 4, "$btype $name: Peer address $peer accepted"; # check content-length header (Forum #87607) $hash->{PARTIAL} .= $buf; my @data = split( '\R\R', $hash->{PARTIAL} ); (my $ldata = $hash->{PARTIAL}) =~ s/Authorization: Basic [\w=]+/Authorization: Basic *****/; if(scalar @data < 2) { #header not complete Log3 $bname, 5, "$btype $name: Incomplete or no header, awaiting more data: \n$ldata"; #start timer return; } my $header = ESPEasy_header2Hash($data[0]); if(!defined $header->{"Content-Length"}) { Log3 $bname, 2, "$btype $name: Missing content-length header: \n$ldata"; ESPEasy_sendHttpClose($hash,"400 Bad Request",""); #delete temp bridge device return; } my $len = length($data[1]); if($header->{"Content-Length"} > $len) { Log3 $bname, 5, "$btype $name: Received content too small, awaiting more content: $header->{'Content-Length'}:$len \n$ldata"; #start timer return; } elsif($header->{"Content-Length"} < $len) { Log3 $bname, 2, "$btype $name: Received content too large, skip processing data: $header->{'Content-Length'}:$len \n$ldata"; ESPEasy_sendHttpClose($hash,"400 Bad Request",""); #delete temp bridge device return; } Log3 $name, 4, "$btype $name: Received content length ok"; # mask password in authorization header with **** my $logHeader = { %$header }; # public IPs if (!defined $logHeader->{Authorization} && $peer !~ m/$d_localIPs/) { Log3 $bname, 2, "$btype $name: No basic auth set while using a public IP " . "address. $peer rejected."; return; } $logHeader->{Authorization} =~ s/Basic\s.*\s/Basic ***** / if defined $logHeader->{Authorization}; # Dump logHeader Log3 $bname, 5, "$btype $name: Received header: ".ESPEasy_dumpSingleLine($logHeader) if (defined $logHeader); # Dump content Log3 $bname, 5, "$btype $name: Received content: $data[1]" if defined $data[1]; # 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"/) { # perl module JSON not installed if ( !$bhash->{helper}{pm}{JSON} ) { Log3 $bname, 2, "$btype $bname: Perl module 'JSON' is not installed. Can't process received data from $peer."; return; } # use encode_utf8 if available else replace any disturbing chars $bhash->{helper}{pm}{Encode} ? ( eval { $json = decode_json( encode_utf8($data[1]) ); 1; } ) : ( eval { $json = decode_json( $data[1] =~ s/[^\x20-\x7E]/_/gr ); 1; } ); if ($@) { Log3 $bname, 2, "$btype $name: WARNING: Invalid JSON received. " . "Check your ESP configuration ($peer).\n$@"; 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 $cd = ESPEasy_isCombineDevices($peer,$espName,AttrVal($bname,"combineDevices",0)); my $ident = $cd ? $espName ne "" ? $espName : $peer : $espName.($espName ne "" && $espDevName ne "" ? "_" : "").$espDevName; my $d0; Log3 $bname, 4, "$btype $name: Src:'$json->{data}{ESP}{name}'/'" . (!defined $json->{data}{SENSOR}{0}{deviceName} || $json->{data}{SENSOR}{0}{deviceName} eq "" ? "" : $json->{data}{SENSOR}{0}{deviceName} ) ."' => ident:$ident dev:" . ( ($d0=(devspec2array("i:IDENT=$ident:FILTER=i:TYPE=$btype"))[0]) ? $d0 : "" ) . " combinedDevice:".$cd; # push internals in @values my @values; my @intVals = qw(unit sleep build build_git build_notes version node_type_id); foreach my $intVal (@intVals) { next if !defined $json->{data}{ESP}{$intVal} || $json->{data}{ESP}{$intVal} eq ""; push(@values,"i||".$intVal."||".$json->{data}{ESP}{$intVal}."||0"); } # if ESP is awaked then yield dequeuing my $s = defined $json->{data}{ESP}{sleep} ? $json->{data}{ESP}{sleep} : undef; ESPEasy_httpReqDequeue_onAwake($bhash, $peer, $s, $espName, $ident); # 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,$dhash,$parseCmd,$cmd,@params) = @_; my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); my ($dname,$dtype) = ($dhash->{NAME},$dhash->{TYPE}); if (IsDisabled($name)) { Log3 $name, 4, "$type $name: cmd 'set $dname $cmd' ignored, bridge is disabled or inactive."; # if ($cmd ne "statusrequest"); return undef; } if ($cmd eq "cleanup") { delete $hash->{helper}{received}; return undef; } elsif ($cmd eq "statusrequest") { ESPEasy_statusRequest($hash); return undef; } $hash->{helper}{maxCmdDuration}{$dhash->{HOST}} = defined $dhash->{MAX_CMD_DURATION} ? $dhash->{MAX_CMD_DURATION} : $d_maxCmdDuration; # a hash is more easy to handle in the following subs... my $cmdHash = { name => $dhash->{NAME}, ident => $dhash->{IDENT}, port => $dhash->{PORT}, host => $dhash->{HOST}, parseCmd => $parseCmd, retry => 0, cmd => $cmd, cmdArgs => [ @params ], ts => ESPEasy_timeStamp(), authRetry => 0, admpwd => $dhash->{sec}{admpwd}, sleep => defined $dhash->{ESP_SLEEP} ? $dhash->{ESP_SLEEP} : undef, }; ESPEasy_httpReq($hash, $cmdHash); } # ------------------------------------------------------------------------------ # Global events only ( $hash->{NOTIFYDEV}=global ) # ------------------------------------------------------------------------------ sub ESPEasy_Notify($$) { my ($hash,$dev) = @_; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; return if(!grep(m/^(DELETE)?ATTR $name |^INITIALIZED$|^REREADCFG$|^DEFINED/, @{$dev->{CHANGED}})); foreach (@{$dev->{CHANGED}}) { if (m/^(DELETE)?ATTR ($name) (\w+)\s?(.*)?$/s) { # modifier 's' is important multiline attrs like userSetCmds, ... Log3 $name, 5, "$type $name: received event: $_"; if ($3 eq "disable") { # attr disable = 0 or deleted # --- enable --- if (defined $1 || (defined $4 && $4 eq "0")) { # device (trigger only if state is disabled) if ($hash->{SUBTYPE} eq "device" && ReadingsVal($name, "state", "") eq "disabled") { ESPEasy_resetTimer($hash) ; readingsSingleUpdate($hash, 'state', 'Initialized',1); Log3 $name, 3, "$type $name: Device enabled"; } # bridge (trigger only if file descriptor is defined) elsif ($hash->{SUBTYPE} eq "bridge" && !defined $hash->{FD}) { ESPEasy_TcpServer_Close($hash,1); #close silently ESPEasy_TcpServer_Open($hash); } } # --- disable --- else { # device (trigger only if state != disabled) if ($hash->{SUBTYPE} eq "device" && ReadingsVal($name, "state", "") ne "disabled") { ESPEasy_clearReadings($hash); ESPEasy_resetTimer($hash,"stop"); readingsSingleUpdate($hash, "state", "disabled",1); Log3 $name, 3, "$type $name: Device disabled"; } # bridge (trigger only if state != disabled) elsif ($hash->{SUBTYPE} eq "bridge" && ReadingsVal($name, "state", "") ne "disabled") { ESPEasy_TcpServer_Close($hash); readingsSingleUpdate($hash, "state", "disabled",1); Log3 $name, 3, "$type $name: Device disabled"; } } } 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 Initialized"); } } elsif ($3 =~ /^(mapLightCmds)$/) { ESPEasy_initDevSets($hash); ESPEasy_initDevAttrs($hash); } elsif ($3 =~ /^(rgbGPIOs|wwcwGPIOs)$/) { ESPEasy_initDevAttrs($hash); } elsif ($3 =~ /^(mapLightCmds|colorpicker(CT[cw]w)?|ct[CW]W_reducedRange|disableRiskyCmds|userSetCmds|userSetMaps|userSets)$/) { ESPEasy_initDevSets($hash); } elsif ($3 eq "deepsleep") { my $ds = defined $1 || (defined $4 && $4 == 0) ? 0 : 1; $hash->{ESP_SLEEP} = $ds; InternalTimer(0, sub() { readingsSingleUpdate($hash, $d_sleepReading, ($ds eq "1" ? "sleeping" : "awaked"), 1)}, $name) } elsif ($3 eq "maxCmdDuration") { $hash->{MAX_CMD_DURATION} = defined $1 ? $d_maxCmdDuration : $4; } else { #Log 5, "$type $name: Attribute $3 not handeled by NotifyFn "; } } # if (m/^(DELETE)?ATTR ($name) (\w+)\s?(.*)?$/s) elsif (m/^(INITIALIZED|REREADCFG|DEFINED $name)$/) { ESPEasy_initDevSets($hash); ESPEasy_initDevAttrs($hash); if ($hash->{SUBTYPE} eq "bridge") { ESPEasy_TcpServer_Open($hash); } else { $hash->{MAX_CMD_DURATION} = AttrVal($name, "maxCmdDuration", $d_maxCmdDuration); my $ds = AttrVal($name, "deepsleep", undef); if (defined $ds) { $hash->{ESP_SLEEP} = $ds; InternalTimer(0, sub(){readingsSingleUpdate($hash, $d_sleepReading, ($ds eq "1" ? "sleeping" : "awaked"), 1)}, $name); } } } 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."-admpwd",getKeyValue($type."_".$old."-admpwd")); # delete old entries setKeyValue($type."_".$old."-user",undef); setKeyValue($type."_".$old."-pass",undef); setKeyValue($type."_".$old."-firstrun",undef); setKeyValue($type."_".$old."-admpwd",undef); # sets/maps $data{$type}{$new} = $data{$type}{$old}; delete $data{$type}{$old}; # 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, $subtype) = ($hash->{TYPE}, $hash->{SUBTYPE}); my $revSubType = $subtype eq "bridge" ? "device" : "bridge"; 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"; } elsif ($aName eq "readingSwitchText") { $ret = "0,1,2" if ($cmd eq "set" && not $aVal =~ m/^(0|1|2)$/) } elsif ($aName =~ m/^(autosave|autocreate|authentication|disable|deepsleep)$/ || $aName =~ m/^(presenceCheck|displayTextEncode)$/) { $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 = "[comma separated list of] ip[/netmask] or a regexp" if $cmd eq "set" && !ESPEasy_isIPv64Range($aVal,"regexp") } 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 %{ $data{ESPEasy}{$name}{sets} }); $ret = "cmd[,cmd][...] #cmd must be a registered ESPEasy cmd" if $init_done && $cmd eq "set" && lc($aVal) !~ m/^($cmds){1}(,($cmds))*$/ } elsif ($aName eq "mapLightCmds") { my $cmds = lc join("|",keys %{ $data{ESPEasy}{$name}{sets} }); $ret = "ESPEasy cmd" if $init_done && $cmd eq "set" && lc($aVal) !~ m/^($cmds){1}(,($cmds))*$/} elsif ($aName =~ m/^(setState|resendFailedCmd)$/) { $ret = "integer" if ($cmd eq "set" && not $aVal =~ m/^(\d+)$/)} elsif ($aName eq "displayTextWidth") { $ret = "number of charaters per line" 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) } elsif ($aName eq "maxCmdDuration") { $ret = "decimal or floating point" if ($cmd eq "set" && $aVal !~ m/^\d+(\.\d)*$/); } elsif ($aName eq "userSetCmds") { $ret = ESPEasy_Attr_userSetCmds($hash, $cmd, $aName, $aVal); $ret = "a perl hash. See command reference for details.\n\n" . "Error: ".chomp($ret) . "\n\nExample:\n" . "(\n" ." plugin_X => { cmd_1 => {}, cmd_2 => {} },\n" ." plugin_Y => {\n" ." rgb => { args => 1, url => \"/myUrl\", widget => \"colorpicker,RGB\", usage => \" [fadetime]\" },\n" ." ct => { args => 1, url => \"/myUrl\", widget => \"colorpicker,CT,2000,100,4500\", usage => \"\" }\n" ." }\n" .")\n" if defined $ret; } if (!$init_done) { if ($aName =~ /^disable$/ && $aVal == 1) { readingsSingleUpdate($hash, "state", "disabled",1); } } if (defined $ret) { return "$name: Attribut '$aName' must be: $ret"; } return undef; } # ------------------------------------------------------------------------------ # check attr userSetCmds | userSetMaps # ------------------------------------------------------------------------------ sub ESPEasy_Attr_userSetCmds(@) { my ($hash, $cmd, $aName, $aVal) = @_; my %user; my $ret; if ($cmd eq "set") { my %ua = eval($aVal); return $@ if $@; foreach my $plugin (keys %ua) { foreach my $key ( keys %{ $ua{$plugin} } ) { return "Unknown key '$key' in $plugin => { $key => ... }" if ($key !~ m/^(args|url|widget|usage|cmds)$/); next if $key =~ m/^(args|url|widget|usage)$/ && !ref($ua{$plugin}{$key}); if ($key eq "cmds") { if (ref($ua{$plugin}{$key}) eq "HASH") { foreach my $subcmd (keys %{ $ua{$plugin}{$key} }) { foreach my $subkey (keys %{ $ua{$plugin}{$key}{$subcmd} }) { my $where = "$plugin => { $key => { $subcmd => { $subkey => ... } } }"; return "Unknown key '$subkey' in $where. Mistyped?" if ($subkey !~ m/^(args|url|widget|usage)$/); return "Value of '$subkey' in $where must be a string." if ref($ua{$plugin}{$key}{$subcmd}{$subkey}); } } } else { return "Value of key '$key' in $plugin => { $key => ... } must be a hash."; } } # key eq "cmds" } # foreach key } # foreach plugin } # set attr # Delete Attribute, afterwards notifyFn will build new cmdhash in $data{ESPEasy}{$name}{sets}... else { return undef; } # eval() above accepts single string expressions... my $reHash = '\s*\w+\s*=>\s*\{.*}\s*,*\s*'; return "Wrong Syntax: '$aVal'" if $aVal !~ m/^\s*\($reHash(,$reHash)*\)\s*$/s; 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 $ipv = $hash->{PEER} =~ m/:/ ? 6 : 4; my $bhash = $modules{ESPEasy}{defptr}{BRIDGE}{$ipv}; Log3 $bhash->{NAME}, 4, "$type $name: Closing tcp session."; ESPEasy_TcpServer_Close($hash); return undef }; HttpUtils_Close($hash); RemoveInternalTimer($hash); if($hash->{SUBTYPE} && $hash->{SUBTYPE} eq "bridge") { my $ipv = $hash->{IPV}; delete $modules{ESPEasy}{defptr}{BRIDGE}{$ipv} if(defined($modules{ESPEasy}{defptr}{BRIDGE}{$ipv})); ESPEasy_TcpServer_Close($hash); Log3 $name, 2, "$type $name: Socket on port tcp/$port closed"; } else { IOWrite($hash, $hash, 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) = @_; my ($name, $type) = ($hash->{NAME}, $hash->{TYPE}); # return if it is a child process for incoming http requests if (!defined $hash->{TEMPORARY}) { setKeyValue($type."_".$name."-user",undef); setKeyValue($type."_".$name."-pass",undef); setKeyValue($type."_".$name."-firstrun",undef); setKeyValue($type."_".$name."-admpwd",undef); delete $data{$type}{$name}; Log3 $hash->{NAME}, 4, "$type $name: $hash->{NAME} deleted"; } return undef; } # ------------------------------------------------------------------------------ sub ESPEasy_State($$$$) { my ($hash, $time, $reading, $val) = @_; if($reading eq "state" && $val eq "inactive") { readingsSingleUpdate($hash, "state", "inactive", 1); } 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 $ipv = $host =~ m/:/ ? 6 : 4; my $bhash = $modules{ESPEasy}{defptr}{BRIDGE}{$ipv}; 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 "" 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") { readingsBeginUpdate($hash); 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 my $rst = AttrVal($name,"readingSwitchText",1); $value = $ee_map{rst}{$vType}{$rst}{$value} if defined $ee_map{rst}{$vType} && defined $ee_map{rst}{$vType}{$rst} && defined $ee_map{rst}{$vType}{$rst}{$value} && !AttrVal($name,"rgbGPIOs",0); # special treatment if attr rgbGPIOs is set # 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; } my $genEvent = $reading ne "sleepState" ? 1 : ( AttrVal($name, "deepsleep", 0) eq 1 ? 1 : 0 ); readingsBulkUpdate($hash, $reading, $value, $genEvent); 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() if $reading ne $d_sleepReading; # 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) ) { readingsBulkUpdate($hash, "rgb", $r.$g.$b, 1); } } } # --- Internals ----------------------------------------------- elsif ($cmd eq "i") { # add human readable text to node_type_id $value = defined $ee_map{build}{$value}{type} ? $ee_map{build}{$value}{type} : $value;# if $reading eq "node_type_id"; # no value given $value = "" if !defined $value || $value eq ""; # set internal $hash->{"ESP_".uc($reading)} = $value; # add to log 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 ...."); } #readingsBulkUpdate($hash, $reading, $value, 1); } # --- Notice (just log) --------------------------------------- elsif ($cmd eq "n") { Log3 $name, $vType, "$type $name: $reading: $value"; } # --- DeleteReading ------------------------------------------- elsif ($cmd eq "dr") { CommandDeleteReading(undef, "$name $reading"); Log3 $name, 4, "$type $name: Reading $reading deleted"; } else { Log3 $name, 2, "$type $name: Unknown internal command code received via dispatch. Report to maintainer, please."; } } # foreach @v Log3 $name, 5, "$type $name: Internals: ".join(" ",@logInternals) if scalar @logInternals > 0; if (defined $hash->{ESP_BUILD}) { my $model = defined $hash->{ESP_NODE_TYPE_ID} ? $hash->{ESP_NODE_TYPE_ID} . " - Build " : "ESP Easy - Build "; $model .= $hash->{ESP_BUILD}; # $hash->{MODEL} = $model; # readingsBulkUpdate($hash, "model", $model, 0); } # Remove sleepState Reading if device does not use deep sleep CommandDeleteReading(undef, "$name $d_sleepReading") if (ReadingsVal($name, $d_sleepReading, undef) && defined $hash->{ESP_SLEEP} && $hash->{ESP_SLEEP} eq "0" ); readingsEndUpdate($hash, 1); # ESPEasy_checkPresence($hash) if ReadingsVal($name,"presence","") ne "present"; # ESPEasy_setState($hash); # yield presenceCheck and setState InternalTimer( gettimeofday(), sub() { ESPEasy_checkPresence($hash) if ReadingsVal($name,"presence","") ne "present"; ESPEasy_setState($hash); }, "$name.checkPresence.setState" ); } 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, $cmdHash) = @_; my ($name, $type) = ($hash->{NAME},$hash->{TYPE}); my ($host, $port, $ident, $dname) = ($cmdHash->{host}, $cmdHash->{port}, $cmdHash->{ident}, $cmdHash->{name}); my ($cmd, @cmdArgs) = ($cmdHash->{cmd}, @{$cmdHash->{cmdArgs}}) ; my $url; # queue http requests or continue if there are no queued cmds # command will also be queued if ESP is in deepsleep mode return undef if ESPEasy_httpReqQueue($hash, $cmdHash); $cmdHash->{retry}++; $hash->{helper}{sessions}{$host}++; # increment http session counter my $path = $data{ESPEasy}{$dname}{sets}{$cmd}{url}; # build http url # raw/rawsystem is used for commands not implemented right now if ($cmd =~ m/^raw|rawsystem$/) { $cmd = $cmdArgs[0]; splice(@cmdArgs,0,1); } if (defined $cmdHash->{dologin} && $cmdHash->{dologin} == 1) { $url = "http://$host:$port/login?password=$cmdHash->{admpwd}"; } else { my $plist = join(",",@cmdArgs); # join cmd params into a string to be used in http url $plist = ",".$plist if @cmdArgs; # add leading comma if defined $url = "http://".$host.":".$port.$path; # build base url $url .= $cmd if($data{ESPEasy}{$dname}{sets}{$cmd}{args} ne "-1"); #Forum 97301 $url .= $plist; } my $httpParams = { url => $url, timeout => AttrVal($name,"httpReqTimeout",$d_httpReqTimeout), keepalive => 0, httpversion => "1.0", hideurl => ($url =~ m/password/ ? 1 : 0), method => "GET", ignoreredirects => 1, callback => \&ESPEasy_httpReqParse, hash => $hash, # pass throght to ESPEasy_httpReqParse() cmdHash => $cmdHash # pass throght to ESPEasy_httpReqParse() }; (my $logUrl = $url) =~ s/password=.*/password=*****/; Log3 $name, 4, "$type $name: httpReq device:$dname ident:$ident timeout:$httpParams->{timeout} url:$logUrl" if ($cmd !~ m/^(status)/); HttpUtils_NonblockingGet($httpParams); return undef; } # ------------------------------------------------------------------------------ sub ESPEasy_httpReqParse($$$) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); my $cmdHash = $param->{cmdHash}; my ($host, $ident, $dname) = ($cmdHash->{host}, $cmdHash->{ident}, $cmdHash->{name}); my ($retry, $parseCmd, $cmd) = ($cmdHash->{retry}, $cmdHash->{parseCmd}, $cmdHash->{cmd}); my ($port, $pass) = ($cmdHash->{port}, $cmdHash->{admpwd}); my @cmdArgs = @{ $cmdHash->{cmdArgs} }; # used for queueing my $plist = join(",",@cmdArgs); # used in Log entries my @values; # command queue $hash->{helper}{sessions}{$host}--; if ($err ne "") { push(@values, "e||_lastError||$err||0") # dispatch $err to logical device if $cmd ne "deepsleep"; # but not if cmd == deepsleep or logical loglevel below 4. $hash->{"WARNING_$host"} = $err; # keep in helper for support reason #Log3 $name, 2, "$type $name: httpReq failed: $host $ident '$cmd $plist' "; #Log3 $name, 2, "$type $name: set $dname $cmd". ($plist ne "" ?" $plist": "")." failed: $err" ; my $ll = $cmd eq "deepsleep" ? 4 : 2; Log3 $name, $ll, "$type $name: $err [set $dname $cmd". ($plist ne ""?" $plist":"") ."]"; # unshift command back to queue (resend) if retry not reached my $maxRetry = AttrVal($name,"resendFailedCmd",$d_resendFailedCmd); if ($retry <= $maxRetry && $hash->{MAX_HTTP_SESSIONS} ) { unshift @{$hash->{helper}{queue}{$host}}, $cmdHash; Log3 $name, 4, "$type $name: Requeuing: $host $ident '$cmd $plist' (".scalar @{$hash->{helper}{queue}{$host}}.")"; } } # ESPEasy's firmware command is unknown elsif ($data =~ m/^(Unknown or restricted command)!/) { my $n = $1. ": '" .($cmd !~m/^raw(system)?/ ? $cmd : $cmdArgs[0]). "'"; push(@values, "n||Warning||$n||3"); } # Authorization not send or failed. elsif ($data =~ m/^(HTTP\/1.1 302)\s?\r\nLocation: \/login/s) { if (!defined $pass || $pass eq "") { my $n = "Command \'$cmd\' requires authentication but no adminpassword ist set."; push(@values, "n||Warning||$n||2"); } else { # queue command, send credentials if ($cmdHash->{authRetry} == 0) { my $n = "Wrong URL or authorization required for \'$cmd $plist\'. Queueing command, sending credentials first."; push(@values, "n||Notice||$n||4"); $cmdHash->{authRetry} = 1; unshift @{$hash->{helper}{queue}{$host}}, $cmdHash; my $loginHash = { name => $dname, ident => $ident, port => $port, host => $host, parseCmd => 1, retry => 0, cmd => $cmd, cmdArgs => [ ], authRetry => 0, admpwd => $pass, dologin => 1, ts => ESPEasy_timeStamp() }; unshift @{$hash->{helper}{queue}{$host}}, $loginHash; } # credentials send but still 302... else { my $n = "Authorization failed. Discarding command \'$cmd $plist\'."; push(@values, "n||Error||$n||2"); } } } # check that response from cmd should be parsed (client attr parseCmdResponse) elsif ($data ne "" && !$parseCmd) { ESPEasy_httpReqDequeue($hash, $host); return undef; } elsif ($data ne "") { # no error occurred # command queue delete $hash->{"WARNING_$host"}; (my $logData = $data) =~ s/\n//sg; Log3 $name, 5, "$type $name: http response for ident:$ident cmd:'$cmd,$plist' => '$logData'"; # This json data are response from plugin. Lights and nfx plugin use it. # Also status command (polling) send infos that will be evaluate (deprecated) if ($data =~ m/^\{/) { #it could be json... my $res; # return here if PM JSON is not installed. if ( !$hash->{helper}{pm}{JSON} ) { Log3 $name, 2, "$type $name: Perl module JSON missing, can't process data."; return undef; } $hash->{helper}{pm}{Encode} # use encode_utf8 if available else replace any disturbing chars ? ( eval { $res = decode_json( encode_utf8($data) ); 1; } ) : ( eval { $res = decode_json( $data =~ s/[^\x20-\x7E]/_/gr ); 1; } ); # is there an json decode error? if ($@) { Log3 $name, 2, "$type $name: WARNING: deformed JSON data received from $host requested by $ident."; Log3 $name, 2, "$type $name: $@"; push(@values, "n||Error||$@||2"); } # json decode worked fine... else { # maps plugin type (answer for set state/gpio) to SENSOR_TYPE_SWITCH (vType:10) my $vType = (defined $res->{plugin} && $res->{plugin} eq "1") ? "10" : "0"; # Plugins lights:123 nfx:124 if (defined $res->{plugin} && $res->{plugin} =~ m/^(123|124)$/) { foreach my $key (keys %{ $res }) { push @values, "r||$key||".$res->{$key}."||".$vType if $res->{$key} ne "" && $key ne "plugin"; } } # all other plugins... else { 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 ""; } } # json decode worked fine... } #if ($data =~ m/^\{/) # no json returned => unknown state else { Log3 $name, 5, "$type $name: No json fmt: ident:$ident $cmd $plist => $data"; if (defined $param->{cmd} && $param->{cmd} eq "status" && defined $param->{plist} && $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 "") else { } ESPEasy_dispatch($hash,$ident,$host,@values); ESPEasy_httpReqDequeue($hash, $host); return undef; } # ------------------------------------------------------------------------------ # Queue cmd if max_sessions reached and queueSize is not reached, # else discard cmd # ------------------------------------------------------------------------------ sub ESPEasy_httpReqQueue(@) { my ($hash, $cmdHash) = @_; my ($name, $type) = ($hash->{NAME}, $hash->{TYPE}); my $cmd = $cmdHash->{cmd}; my @cmdArgs = @{ $cmdHash->{cmdArgs} }; my $cmdArgs = join(",",@cmdArgs); my $host = $cmdHash->{host}; my $queueSize = defined $hash->{helper}{queue} && defined $hash->{helper}{queue}{$host} ? scalar @{$hash->{helper}{queue}{$host}} : 0; $hash->{helper}{sessions}{$host} = 0 if !defined $hash->{helper}{sessions}{$host}; # is queueing enabled? if ($hash->{MAX_HTTP_SESSIONS}) { # queue if max sessions are already in use or if ESP is deepsleep mode if ($hash->{helper}{sessions}{$host} >= $hash->{MAX_HTTP_SESSIONS} # ESP already send data, so we know if and wheen it goes to sleep || ( defined $hash->{helper}{awaked} && defined $hash->{helper}{awaked}{$host} && $hash->{helper}{awaked}{$host} - gettimeofday() < $hash->{helper}{maxCmdDuration}{$host} ) # ESP did not send any data right now, but device has defined $hash->{ESP_SLEEP} || ( !(defined $hash->{helper}{awaked} && defined $hash->{helper}{awaked}{$host}) && defined $cmdHash->{sleep} && $cmdHash->{sleep} > 1 ) ) { # max queue size reached if ($queueSize < $hash->{MAX_QUEUE_SIZE}) { push(@{$hash->{helper}{queue}{$host}}, $cmdHash); Log3 $name, 4, "$type $name: Queuing: $host $cmdHash->{ident} cmd:" . "'$cmd $cmdArgs' queueSize:".($queueSize +1)." reason:" . ($hash->{helper}{sessions}{$host} >= $hash->{MAX_HTTP_SESSIONS} ? "maxSessions" : "deepsleep"); return 1; } else { Log3 $name, 2, "$type $name: set $cmd $cmdArgs (skipped due to max queue size exceeded: $hash->{MAX_QUEUE_SIZE})"; return 1; } } } return 0; } # ------------------------------------------------------------------------------ # De-Queue set cmds and delete $hash->{helper}{queue}.. if empty # ------------------------------------------------------------------------------ sub ESPEasy_httpReqDequeue($$) { my ($hash,$host) = @_; my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); if ( defined $hash->{helper}{queue} && defined $hash->{helper}{queue}{$host} && scalar @{$hash->{helper}{queue}{$host}} ) { # ESP will be go into deep sleep soon. stop queueing. if (defined $hash->{helper}{awaked} && defined $hash->{helper}{awaked}{$host} && ($hash->{helper}{awaked}{$host} - gettimeofday()) < $hash->{helper}{maxCmdDuration}{$host} ) { Log3 $name, 4, "$type $name: $host is going into deep sleep in <= " . $hash->{helper}{maxCmdDuration}{$host} . "s. De-queueing stopped. " . scalar @{$hash->{helper}{queue}{$host}}. " outstanding commands left."; return undef; } my $cmdHash = shift @{ $hash->{helper}{queue}{$host} }; Log3 $name, 4, "$type $name: Dequeuing: $host $cmdHash->{ident} " . "'$cmdHash->{cmd} " . join(",",@{$cmdHash->{cmdArgs}})."'" . " queuesize:".scalar @{$hash->{helper}{queue}{$host}}; # delete queue if empty delete $hash->{helper}{queue}{$host} if defined $hash->{helper}{queue} && defined $hash->{helper}{queue}{$host} && scalar @{$hash->{helper}{queue}{$host}} == 0; delete $hash->{helper}{queue} if defined $hash->{helper}{queue} && scalar keys %{ $hash->{helper}{queue} } == 0; ESPEasy_httpReq($hash, $cmdHash); } return undef; } # ------------------------------------------------------------------------------ # Mark a peer as being in deep sleep or in awaked state and de-queue. # called within ReadFn. # ------------------------------------------------------------------------------ # $hash->{helper}{awaked}{$host} > 0 : awaked, value is next sleep time # $hash->{helper}{awaked}{$host} = 0 : sleeping # $hash->{helper}{awaked}{$host} = -1 : sleep awaited # $hash->{helper}{awaked}{$host} ! defined : no sleep mode used by peer # ------------------------------------------------------------------------------ sub ESPEasy_httpReqDequeue_onAwake($$$$$) { my ($hash, $host, $sleep, $espName, $ident) = @_; my ($name, $type) = ($hash->{NAME}, $hash->{TYPE}); if (defined $sleep && $sleep > 1) { if ( ( !defined $hash->{helper}{awaked} || !defined $hash->{helper}{awaked}{$host} ) || ( defined $hash->{helper}{awaked} && defined $hash->{helper}{awaked}{$host} && $hash->{helper}{awaked}{$host} == 0 ) ) { $hash->{helper}{awaked}{$host} = gettimeofday() + $sleep; # --- schedule dispatch "sleeping" ------------------------------------ InternalTimer( $hash->{helper}{awaked}{$host}, sub() { $hash->{helper}{awaked}{$host} = 0; my @value = ("r||$d_sleepReading||sleeping||0"); ESPEasy_dispatch($hash,$ident,$host,@value); }, "$type.$name.$host.sleepStarted" ); # --- schedule dispatch "sleep awaited" ------------------------------- my $mcd = defined $hash->{helper}{maxCmdDuration} && defined $hash->{helper}{maxCmdDuration}{$host} ? $hash->{helper}{maxCmdDuration}{$host} : $d_maxCmdDuration; my $awaited = $hash->{helper}{awaked}{$host} - $mcd; my @dpsa = ("r||$d_sleepReading||sleep awaited in ".$mcd."s: $hash->{helper}{awaked}{$host}||0"); InternalTimer( $awaited, sub() { ESPEasy_dispatch($hash, $ident, $host, @dpsa); $hash->{helper}{awaked}{$host} = -1; }, "$type.$name.$host.sleepAwaited" ); # --- dispatch "awaked" ----------------------------------------------- # eg. 2018.12.21 10:35:03.479 4: ESPEasy em1: sleepState: awaked for 25s (-5s): [2018-12-21 10:35:23] my @dpa = ("r||$d_sleepReading||awaked for ".$sleep."s (-".$mcd."s): ".$awaited."||0"); ESPEasy_dispatch($hash, $ident, $host, @dpa); # --- yield dequeuing ------------------------------------------------- InternalTimer( gettimeofday(), sub() { ESPEasy_httpReqDequeue($hash, $host); }, "$type.$name.$host.dequeue" ); return 1; } # if !defined... } # if (defined $sleep # Peer did not send $sleep or $sleep == 0 elsif (!$sleep) { delete $hash->{helper}{awaked}{$host} if defined $hash->{helper}{awaked} && $hash->{helper}{awaked}{$host}; } return 0; } # ------------------------------------------------------------------------------ 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." => $ee_map{pins}{uc $gpio}"; $gpio = $ee_map{pins}{uc $gpio}; } Log3 $name, 5, "$type $name: IOWrite(\$defs{$name}, $hash, 1, status, gpio,".$gpio.")"; IOWrite($hash, $hash, 1, "status", "gpio,".$gpio); } } return undef; } # ------------------------------------------------------------------------------ sub ESPEasy_resetTimer($;$) { my ($hash,$sig) = @_; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; $sig = "" if !$sig; if ($init_done == 1) { RemoveInternalTimer($hash, "ESPEasy_statusRequest"); } 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); } return undef; } # ------------------------------------------------------------------------------ # Loaned from TcpServerUtils, but no IPv4 fallback, modified logging and state sub ESPEasy_TcpServer_Open($) { my ($hash) = @_; my ($name, $type) = ($hash->{NAME}, $hash->{TYPE}); my $port = $hash->{PORT}; my $dp = "[TCP:".($hash->{IPV}==4?'IPV4:':'')."$port]"; if($port =~ m/^IPV6:(\d+)$/i) { $port = $1; eval "require IO::Socket::INET6; use Socket6;"; if($@) { readingsSingleUpdate($hash, 'state', 'error', 1); Log3 $hash, 1, "$type $name: Error: Can't load INET6"; Log3 $hash, 1, "$type $name: $@"; return "error"; } else { $hash->{IPV6} = 1; } } my @opts = ( Domain => ($hash->{IPV6} ? AF_INET6() : AF_UNSPEC), # Linux bug LocalHost => undef, LocalPort => $port, Listen => 32, # For Windows Blocking => ($^O =~ /Win/ ? 1 : 0), # Needed for .WRITEBUFFER@darwin ReuseAddr => 1 ); $hash->{SERVERSOCKET} = $hash->{IPV6} ? IO::Socket::INET6->new(@opts) : IO::Socket::INET->new(@opts); if(!$hash->{SERVERSOCKET}) { Log3 $hash, 1, "$type $name: Error: Can't open server port $dp"; Log3 $hash, 1, "$type $name: $!"; readingsSingleUpdate($hash, 'state', 'error', 1); return "error"; } $hash->{FD} = $hash->{SERVERSOCKET}->fileno(); $hash->{PORT} = $hash->{SERVERSOCKET}->sockport(); $selectlist{"$name.$port"} = $hash; readingsSingleUpdate($hash, 'state', 'Initialized', 1); Log3 $hash, 3, "$type $name: Bridge v$module_version port $dp opened."; return undef; } # ------------------------------------------------------------------------------ sub ESPEasy_TcpServer_Close($;$) # 1:hash 2:silent { my ($hash, $silent) = @_; TcpServer_Close($hash); if (!defined $hash->{TEMPORARY} && !$silent) { my ($name, $type) = ($hash->{NAME}, $hash->{TYPE}); my $dp = "[TCP:".($hash->{IPV}==4?'IPV4:':'').$hash->{PORT}."]"; Log3 $hash, 3, "$type $name: Bridge v$module_version port $dp closed."; } return undef; } # ------------------------------------------------------------------------------ # Duplicated sub from TcpServerUtils as a workaround for new security feature: # https://forum.fhem.de/index.php/topic,72717.0.html sub ESPEasy_TcpServer_Accept($$) { my ($hash, $type) = @_; my $name = $hash->{NAME}; my @clientinfo = $hash->{SERVERSOCKET}->accept(); if(!@clientinfo) { Log3 $name, 1, "Accept failed ($name: $!)" if($! != EAGAIN); return undef; } $hash->{CONNECTS}++; my ($port, $iaddr) = $hash->{IPV6} ? sockaddr_in6($clientinfo[1]) : sockaddr_in($clientinfo[1]); my $caddr = $hash->{IPV6} ? inet_ntop(AF_INET6(), $iaddr) : inet_ntoa($iaddr); # ------------------------------------------------------------------------------ # Removed from sub because we have our own access control that works in a more # readable and flexible way (network ranges with allow/deny and regexps). # Our new allowed ranges default are also now: # 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7,fe80::/10,::1 # ------------------------------------------------------------------------------ # # my $af = $attr{$name}{allowfrom}; # if(!$af) { # my $re = "^(127|192.168|172.(1[6-9]|2[0-9]|3[01])|10|169.254)\\.|". # "^(fe[89ab]|::1)"; # if($caddr !~ m/$re/) { # my %empty; # $hash->{SNAME} = $hash->{NAME}; # my $auth = Authenticate($hash, \%empty); # delete $hash->{SNAME}; # if($auth == 0) { # Log3 $name, 1, # "Connection refused from the non-local address $caddr:$port, ". # "as there is no working allowed instance defined for it"; # close($clientinfo[0]); # return undef; # } # } # } # # if($af) { # if($caddr !~ m/$af/) { # my $hostname = gethostbyaddr($iaddr, AF_INET); # if(!$hostname || $hostname !~ m/$af/) { # Log3 $name, 1, "Connection refused from $caddr:$port"; # close($clientinfo[0]); # return undef; # } # } # } #$clientinfo[0]->blocking(0); # Forum #24799 if($hash->{SSL}) { # Forum #27565: SSLv23:!SSLv3:!SSLv2', #35004: TLSv12:!SSLv3 my $sslVersion = AttrVal($hash->{NAME}, "sslVersion", AttrVal("global", "sslVersion", "TLSv12:!SSLv3")); # Certs directory must be in the modpath, i.e. at the same level as the # FHEM directory my $mp = AttrVal("global", "modpath", "."); my $ret = IO::Socket::SSL->start_SSL($clientinfo[0], { SSL_server => 1, SSL_key_file => "$mp/certs/server-key.pem", SSL_cert_file => "$mp/certs/server-cert.pem", SSL_version => $sslVersion, SSL_cipher_list => 'HIGH:!RC4:!eNULL:!aNULL', Timeout => 4, }); my $err = $!; if( !$ret && $err != EWOULDBLOCK && $err ne "Socket is not connected") { $err = "" if(!$err); $err .= " ".($SSL_ERROR ? $SSL_ERROR : IO::Socket::SSL::errstr()); Log3 $name, 1, "$type SSL/HTTPS error: $err" if($err !~ m/error:00000000:lib.0.:func.0.:reason.0./); #Forum 56364 close($clientinfo[0]); return undef; } } my $cname = "${name}_${caddr}_${port}"; my %nhash; $nhash{NR} = $devcount++; $nhash{NAME} = $cname; $nhash{PEER} = $caddr; $nhash{PORT} = $port; $nhash{FD} = $clientinfo[0]->fileno(); $nhash{CD} = $clientinfo[0]; # sysread / close won't work on fileno $nhash{TYPE} = $type; $nhash{SSL} = $hash->{SSL}; $nhash{STATE} = "Connected"; $nhash{SNAME} = $name; $nhash{TEMPORARY} = 1; # Don't want to save it $nhash{BUF} = ""; $attr{$cname}{room} = "hidden"; $defs{$cname} = \%nhash; $selectlist{$nhash{NAME}} = \%nhash; my $ret = $clientinfo[0]->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1); Log3 $name, 4, "Connection accepted from $nhash{NAME}"; return \%nhash; } # ------------------------------------------------------------------------------ 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_isCmdAvailable($$@) { my ($hash,$cmd) = @_; my $name = $hash->{NAME}; if (!defined $data{ESPEasy}{$name}{sets}{$cmd}) { my $clist; foreach my $c (sort keys %{ $data{ESPEasy}{$name}{sets} } ) { $clist .= $c . ($data{ESPEasy}{$name}{sets}{$c}{widget} eq "" ? " " : ":$data{ESPEasy}{$name}{sets}{$c}{widget} "); } return $clist; } 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_IsPeerAwaked($$) { my ($hash, $host) = @_; return undef if !defined defined $hash->{awaked}{$host}; return 0 if ($hash->{awaked}{$host} == 0); return 1 if ($hash->{awaked}{$host} > 0); } # ------------------------------------------------------------------------------ sub ESPEasy_sendHttpClose($$$) { my ($hash,$code,$response) = @_; my ($name,$type,$con) = ($hash->{NAME},$hash->{TYPE},$hash->{CD}); my $ipv = $hash->{PEER} =~ m/:/ ? 6 : 4; my $bhash = $modules{ESPEasy}{defptr}{BRIDGE}{$ipv}; 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 ($hash,$cmd,$search) = @_; my $name = $hash->{NAME}; my @usage = split(" ",$data{ESPEasy}{$name}{sets}{$cmd}{usage}); my $pos = 0; my $i = 0; foreach (@usage) { if ($_ eq $search) { $pos = $i+1; 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}}) { CommandDeleteReading(undef, "$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|$d_sleepReading)$/; 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", "Initialized"); 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 = $ee_map{pins}{uc $rg} if defined $ee_map{pins}{uc $rg}; $gg = $ee_map{pins}{uc $gg} if defined $ee_map{pins}{uc $gg}; $bg = $ee_map{pins}{uc $bg} if defined $ee_map{pins}{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 = $ee_map{pins}{uc $gww} if defined $ee_map{pins}{uc $gww}; $gcw = $ee_map{pins}{uc $gcw} if defined $ee_map{pins}{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); 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 = $ee_map{pins}{uc $gr} if defined $ee_map{pins}{uc $gr}; $gg = $ee_map{pins}{uc $gg} if defined $ee_map{pins}{uc $gg}; $gb = $ee_map{pins}{uc $gb} if defined $ee_map{pins}{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)); } # ------------------------------------------------------------------------------ # 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 for 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_urlEncodeDisplayText($$@) { my ($hash, $cmd, @params) = @_; my $name = $hash->{NAME}; my $enc = AttrVal($name, "displayTextEncode", $d_displayTextEncode); my $pp = ESPEasy_paramPos($hash,$cmd,''); if ($enc && $pp) { my (@p, @t); my $c = scalar @params; # leading parameters for (my $i=0; $i<$pp-1; $i++) { push( @p, $params[$i] ) } # collect all texts parameters for (my $i=$pp-1; $i<$c; $i++) { $params[$i] =~ s/,/./g; # comma is ESPEasy parameter splitter, can't be used push @t, $params[$i]; } my $text = join(" ", @t); # fill line with leading/trailing spaces my $width = AttrVal($name,"displayTextWidth", $d_displayTextWidth); if ($width) { $text = " " x ($p[1]-1) .$text. " " x ($width - length($text) - $p[1]+1); $text = substr($text, 0, $width); $p[1] = 1; } push(@p, urlEncode($text)); return @p; } return @params; } # ------------------------------------------------------------------------------ sub ESPEasy_loadRequiredModules($) { my ($hash) = @_; foreach ("JSON", "Encode") { eval "use $_; 1;"; if (!$@) { $hash->{helper}{pm}{$_} = 1; } else { $hash->{helper}{pm}{$_} = 0; if ($init_done || $hash->{SUBTYPE} eq "bridge") { my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); Log3 $name, 1, "$type $name: WARNING: Perl module $_ is not installed. " . "Reduced functionality!"; Log3 $name, 2, "$type $name: $@" if $init_done; } } } 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_isAuthenticated($$) { my ($hash,$ah) = @_; my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); my $ipv = $hash->{PEER} =~ m/:/ ? 6 : 4; my $bhash = $modules{ESPEasy}{defptr}{BRIDGE}{$ipv}; my ($bname,$btype) = ($bhash->{NAME},$bhash->{TYPE}); my $u = $bhash->{".bau"}; my $p = $bhash->{".bap"}; 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_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 (or a regexp if opt. argument # $regexChk is set) # - space or comma separated list of above. # ------------------------------------------------------------------------------ sub ESPEasy_isIPv64Range($;$) { my ($addr,$regexChk) = @_; 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); } elsif (defined $regexChk && !defined $nm) { return 0 if $ip =~ m/^\*/ || $ip =~ m/^\d+\.\d+\.\d+\.\d+$/; # faulty regexp/ip eval { "Hallo" =~ m/^$ip$/ }; return $@ ? 0 : 1; } 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) { return 1 if $peer =~ m/^$_$/; # a regexp is been used 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 seems to be 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}(?

ESPEasy

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

    Notes:
    • You have to define a bridge device before any logical device can be (automatically) defined.
    • You have to configure your ESP to use "FHEM HTTP" controller protocol. Furthermore ESP Easy controller IP must match FHEM's IP address. ESP controller port and the FHEM ESPEasy bridge port must be the same.
    • Max. 2 ESPEasy bridges can be defined in the same FHEM instance: 1 for IPv4 and 1 for IPv6
    • Further information about this module is available here: Forum #55728 or in this wiki article.
    • For security reasons: if one or more of your ESPEasy device uses a public IP address then you have to enable this explicitly or the device(s) will be ignored/rejected:

    Requirements:
    • ESPEasy build >= R128 (self compiled) or an ESPEasy precompiled image >= R140_RC3
    • ESPEasy Mega with option to set sleep awake time (Config -> Sleep Mode -> Sleep awake time) is required to control ESP Easy nodes in deep sleep. Receiving sensor values works with all other supported versions.
    • 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 <[IPV6:]port>

    • <name>
      Specifies a device name of your choise.
      example: ESPBridge

    • <port>
      Specifies TCP port for incoming ESPEasy http requests. This port must not be used by any other application or daemon on your system and must be in the range 1024..65535 unless you run your FHEM installation with root permissions (not recommanded).
      If you want to define an IPv4 and an IPv6 bridge on the same TCP port (recommanded) then it might be necessary on (some?) Linux distributions to activate IPV6_V6ONLY socket option. Use "echo 1>/proc/sys/net/ipv6/bindv6only" or systemctl for that purpose.
      eg. 8383
      eg. IPV6:8383
      Example:
      define ESPBridge ESPEasy bridge 8383


    Get (bridge)

    • <reading>
      returns the value of the specified reading

    • queueSize
      returns number of entries for currently used queue.

    • queueContent
      returns queues content.
      • arguments: IP address (can be a regex or omitted to display all queues)

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

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


    Set (bridge)

    • active
      Activates the current device if it was set inactive before. Set active/inactive will be mostly used in scripts without the side effect that the 'red question mark' will be displayed in FHEMWEB that indicates unsaved configuration changes. If attribute disabled is enabled (set to '1') then this set command will be ignored.

    • inactive
      Opposite of set command activate

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

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

    • pass
      Specifies password used by basic authentication for incoming requests.
      Note that attribute authentication must be set to enable basic authentication, too.
      required value: <password>
      eg. : set ESPBridge pass secretpass

    • reopen
      Reopen TCP/IP server port for incoming connections from ESPs.

    • user
      Specifies username used by basic authentication for incoming requests.
      Note that attribute authentication must be set to enable basic authentication, too.
      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 IP, IP/netmask, regexp or a comma separated list of these values. Netmask can be written as bitmask or dotted decimal. Domain names for dns lookups are not supported.
      Possible values: IPv64 address, IPv64/netmask, regexp
      Default: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fe80::/10, fc00::/7, ::1

      Examles:
      10.68.30.147 => IPv4 single address
      10.68.30.0/25 => IPv4 CIDR network 192.168.30.0-127
      10.68.30.8/255.255.248.0 => IPv4 CIDR network 192.168.30.8-15
      192.168.30.1([0-4][0-9]|50) => IPv4 range w/ regexp: 192.168.30.100-150
      2001:1a59:50a9::aaaa => IPv6 single address
      2001:1a59:50a9::/48 => IPv6 network 2001:1a59:50a9::/48
      2001:1a59:50a9::01[0-4][0-9] => IPv6 range w/ regexp: 2001:1a59:50a9::0100-0149
      Note that short IPv6 notation (::) must be used in conjunction with regexps.

    • 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.
      Global autocreate setting will be detected by global attribut 'autoload_undefined_devices'
      Possible values: 0,1
      Default: 0 (disabled)

    • autosave
      Used to overwrite global autosave setting.
      Global autosave setting will be detected by global attribut 'autosave'.
      Possible values: 0,1
      Default: 0 (disabled)

    • 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. inactive state will be overwritten.
      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 define number of command resends to the ESP if there is an error in transmission on network layer (eg. unreachable wifi device).
      Possible values: a positive number
      Default: 0 (disabled: no resending of commands)

    • uniqIDs
      This attribute has been removed.

    • readingFnAttributes

    ESPEasy Device

    Define (logical device)

      Note 1: 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 and presence detection.

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

    • <name>
      Specifies a device name of your choise.
      example: ESPxx

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

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

    • <IODev>
      Specifies your ESP bridge device. See above.
      example: 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 bridge 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
      example: ESPxx_DHT22
      example: ESPxx

    • Example:
      define ESPxx ESPEasy 172.16.4.100 80 ESPBridge EspXX_SensorXX


    Get (logical device)

    • adminPassword
      returns the admin password. For details see set adminPassword

    • pinMap
      returns possible alternative pin names that can be used in commands

    • setCmds
      returns formatted table of registered ESP commands/mappings.


    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

      ESPEasy module internal commands:

    • active
      Works in the same way as bridge set command active.

    • inactive
      Works in the same way as bridge set command inactive.

    • adminPassword
      The ESP Easy 'Admin Password" is used to protect some ESP Easy commands against unauthorized access. When this feature is enabled on your ESPs you should deposit this password. If an ESP Easy command will require this authorization the password will be sent to the ESP. Keep in mind that this feature works quite slow on your ESP Easy nodes.

    • attrTemplate
      See global attrTemplate. Attribute useSetExtensions must be activated or the command is unavailable.

    • clearReadings
      Delete all readings that are auto created by received sensor values since last FHEM restart.
      • arguments: none
      • example: set <esp> clearReadings

    • help
      Shows set command usage.
      • arguments: <a valid set command>
      • example: set <esp> help gpio

    • 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. Used URL is: "/control?cmd="
      • arguments: raw <cmd> [<arg1>] [<arg2>] [<...>]
      • example: set <esp> raw myCommand p1 p2 p3

    • rawsystem
      The same as set command raw but this command uses the URL "/?cmd=" (command.ino) instead of "/control?cmd=" (ESPEasy plugins).

    • statusRequest
      Trigger a statusRequest for configured GPIOs (see attribut pollGPIOs) and do a presence check
      • arguments: n/a
      • example: set <esp> statusRequest


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

      ESP Easy generic I/O commands:

    • GPIO
      Switch output pins to high/low
      • arguments: <pin> <0|1|off|on>
      • example: set <esp> gpio 14 on

    • PWM
      Direct PWM control of output pins
      • arguments: <pin> <level>
      • example: set <esp> pwm 14 512

    • PWMFADE
      Fade output pins to a pwm value
      • arguments: <pin> <target pwm> <duration 1-30s>
      • example: set <esp> pwmfade 14 1023 10

    • Pulse
      Direct pulse control of output pins
      • arguments: <pin> <0|1|off|on> <duration>
      • example: set <esp> pulse 14 on 10

    • LongPulse
      Direct pulse control of output pins (duration in s)
      • arguments: <pin> <0|1|off|on> <duration>
      • example: set <esp> longpulse 14 on 10

    • LongPulse_ms
      Direct pulse control of output pins (duration in ms)
      • arguments: <pin> <0|1|off|on> <duration>
      • example: set <esp> longpulse_ms 14 on 10000

    • PCFGpio
      Control PCF8574 (8-bit I/O expander for I2C-bus)
      • arguments: <port> <0|1|off|on>
      • example: set <esp> PCFGpio 128 on
      Port numbering see: ESPEasy Wiki PCF8574

    • PCFPulse
      Control PCF8574 (8-bit I/O expander for I2C-bus)
      • arguments: <port> <0|1|off|on> <duration>
      • example: set <esp> PCFPulse 128 on 10
      Port numbering see: ESPEasy Wiki PCF8574

    • PCFLongPulse
      Control on PCF8574 (8-bit I/O expander for I2C-bus)
      • arguments: <port> <0|1|off|on> <duration>
      • example: set <esp> PCFLongPulse 128 on 10
      Port numbering see: ESPEasy Wiki PCF8574

    • MCPGPIO
      Control MCP23017 output pins (16-Bit I/O Expander with Serial Interface)
      • arguments: <port> <0|1|off|on>
      • example: set <esp> MCPGPIO 48 on
      Port numbering see: ESPEasy Wiki MCP23017

    • MCPPulse
      Pulse control on MCP23017 output pins (duration in ms)
      • arguments: <port> <0|1|off|on> <duration>
      • example: set <esp> MCPPulse 48 on 100

    • MCPLongPulse
      Longpulse control on MCP23017 output pins (duration in s)
      • arguments: <port> <0|1|off|on> <duration>
      • example: set <esp> MCPLongPulse 48 on 2

    • pcapwm
      Control PCA9685 (16-channel / 12-bit PWM I2C-bus controller)
      • arguments: <pin 0-15> <level 0-4095>
      • example: set <esp> pcapwm 15 4095

    • ESP Easy motor control commands:

    • Servo
      Direct control of servo motors
      • arguments: <servoNo> <pin> <position>
      • example: set <esp> servo 1 14 100

    • MotorShieldCMD
      Control a DC motor or stepper
      • arguments: DCMotor <motornumber> <forward|backward|release> <speed>
        arguments: Stepper <motornumber> <forward|backward|release> <steps> <single|double|interleave|microstep>
      • example: set <esp> MotorShieldCMD DCMotor 1 forward 10
        example: set <esp> MotorShieldCMD Stepper 1 backward 25 single

    • ESP Easy display related commands:

    • lcd
      Write text messages to LCD screen
      Pay attention to attributes displayTextEncode and displayTextWidth.
      • arguments: <row> <col> <text>
      • example: set <esp> lcd 1 1 Test a b c

    • lcdcmd
      Control LCD screen
      • arguments: <on|off|clear>
      • example: set <esp> lcdcmd clear

    • oled
      Write text messages to OLED screen
      Pay attention to attributes displayTextEncode and displayTextWidth.
      • arguments: <row> <col> <text>
      • example: set <esp> oled 1 1 Test a b c

    • oledcmd
      Control OLED screen
      • arguments: <on|off|clear>
      • example: set <esp> oledcmd clear

    • oledframedcmd
      Switch oledframed on/off
      • arguments: <on|off>
      • example: set <esp> oledframedcmd on

    • ESP Easy DMX related commands:

    • dmx
      Send DMX commands to a device
      • arguments: <on|off|log|value|channel=value[,value][...]>
      • example: set <esp> dmx 1=255,2=127

    • ESP Easy LED/Lights related commands:

    • Lights (plugin can be found here)
      Control a rgb or ct light
      • arguments: <rgb|ct|pct|on|off|toggle> [<hex-rgb|color-temp|pct-value>] [<fading time>]
      • examples:
        set <esp> lights rgb aa00aa
        set <esp> lights rgb aa00aa 10
        set <esp> lights ct 3200
        set <esp> lights ct 3200 10
        set <esp> lights pct 50
        set <esp> lights on
        set <esp> lights off
        set <esp> lights toggle

    • nfx (plugin can be found here)
      Control nfx plugin. Note: To use FHEMWEB's colorpicker and slider widgets you have to set Attribut mapLightCmds to nfx.
      • arguments:
        all <rrggbb> [fadetime] [delay +/-ms]
        bgcolor <rrggbb>
        ct <ct> [fadetime] [pct bri]
        colorfade <rrggbb_start> <rrggbb_end> [startpixel] [endpixel]
        comet <rrggbb> [speed +/- 0-50]
        count <value>
        dim <value 0-255>
        dualscan <rrggbb> [rrggbb background] [speed 0-50]
        dualwipe <rrggbb> [rrggbb dot] [speed +/- 0-50]
        fade <rrggbb> [fadetime ms] [delay +/-ms]
        fadedelay <value in +/-ms>
        fadetime <value in ms>
        faketv [startpixel] [endpixel]
        fire [fps] [brightness 0-255] [cooling 20-100] [sparking 50-200]
        fireflicker [intensity 0-255] [speed 0-50]
        kitt <rrggbb> [speed 0-50]
        line <startpixel> <endpixel> <rrggbb>
        off [fadetime] [delay +/-ms]
        on [fadetime] [delay +/-ms]
        one <pixel> <rrggbb>
        pct <pct> [fadetime]
        rainbow [speed +/- 0-50]
        rgb <rrggbb> [fadetime] [delay +/-ms]
        scan <rrggbb> [rrggbb background] [speed 0-50]
        simpleclock [bigtickcolor] [smalltickcolor] [hourcolor] [minutecolor] [secondcolor] [backgroundcolor]
        sparkle <rrggbb> [rrggbb background] [speed 0-50]
        speed <value 0-50>
        stop
        theatre <rrggbb> [rrggbb background] [speed +/- 0-50]
        toggle [fadetime]
        twinkle <rrggbb> [rrggbb background] [speed 0-50]
        twinklefade <rrggbb> [number of pixels] [speed 0-50]
        wipe <rrggbb> [rrggbb dot] [speed +/- 0-50]
      • examples:
        set <esp> nfx all 00ff00 100
        set <esp> nfx rgb aa00ff 1000 10
        set <esp> nfx line 0 100 f0f0f0c
      • examples with attribut mapLightCmds set to nfx:
        set <esp> all 00ff00 100
        set <esp> rgb aa00ff 1000 10
        set <esp> line 0 100 f0f0f0c

    • candle
      Control candle rgb plugin
      • arguments: CANDLE:<FlameType>:<Color>:<Brightness>
      • example: set <esp> CANDLE:4:FF0000:200

    • neopixel
      Control neopixel plugin (single LED)
      • arguments: <led nr> <red 0-255> <green 0-255> <blue 0-255>
      • example: set <esp> neopixel 1 255 255 255

    • neopixelall
      Control neopixel plugin (all together)
      • arguments: <red 0-255> <green 0-255> <blue 0-255>
      • example: set <esp> neopixelall 255 255 255

    • neopixelline
      Control neopixel plugin (line)
      • arguments: <start led no> <stop led no> <red 0-255> <green 0-255> <blue 0-255>
      • example: set <esp> neopixelline 1 5 0 127 255

    • ESP Easy sound related commands:

    • tone
      Play a tone on a pin via a speaker or piezo element (ESPEasy >= 2.0.0-dev6)
      • arguments: <pin> <freq Hz> <duration s>
      • example: set <esp> tone 14 4000 1

    • rtttl
      Play melodies via RTTTL (ESPEasy >= 2.0.0-dev6)
      • arguments: <rtttl> <pin>:<rtttl codes>
      • example: set <esp> rtttl 14:d=10,o=6,b=180,c,e,g

    • buzzer
      Beep a short time
      • arguments: none
      • example: set <esp> buzzer

    • ESP Easy miscellaneous commands:

    • 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 #1 and IR Transmitter thread #61.
      • arguments: <NEC|JVC|RC5|RC6|SAMSUNG|SONY|PANASONIC> <hex code> <bit length>
        arguments: <RAW> <B32 raw> <frequenz> <pulse length> <blank length>
      • example: set <esp> irsend NEC 7E81542B 32
        example: set <esp> irsend RAW 3U0GGL8AGGK588A22K58ALALALAGL1A22LAK45ALALALALALALALALAL1AK5 38 512 256

    • reboot
      Used to reboot your ESP
      • arguments: none
      • example: set <esp> reboot

    • serialsend
      Used for ser2net plugin
      • arguments: <string>
      • example: set <esp> serialsend test

    • ESP Easy administrative commands (be careful !!!):

    • erase
      Wipe out ESP flash memory
      • arguments: none
      • example: set <esp> erase

    • reset
      Do a factory reset on the ESP
      • arguments: none
      • example: set <esp> reset

    • resetflashwritecounter
      Used to reset flash write counter
      • arguments: none
      • example: set <esp> resetflashwritecounter

    • ESP Easy rules related commands (Note: These commands may be protected with the ESP Easy 'Admin Passsword'. See set adminpassword for details.)

    • deepsleep
      Ask ESP to go into deepsleep mode.
      • arguments: <duration in is>

    • event
      Trigger an ESP event. Such events can be used in ESP Easy rules.
      • arguments: <string>
      • example: set <esp> event testevent

    • notify
      Send a notify message
      • arguments: <notify nr> <message>

    • publish
      Publish a value via MQTT
      • arguments: <topic> <value>

    • rules
      Enable/disable rule processing
      • arguments: <0|1|off|on>

    • sendto
      Send a command to another ESP
      • arguments: <unit nr> <command>

    • sendtohttp
      Used to tigger a HTTP URL call
      • arguments: none

    • sendtoudp
      Used to tigger a UDP call
      • arguments: <ip> <port> <url>

    • taskrun
      Used trigger a taskrun command
      • arguments: <task/device nr>

    • taskvalueset
      Used to set taskvalueset
      • arguments: <task/device nr> <value nr> <value/formula>

    • taskvaluesetandrun
      Used to set taskvaluesetandrun
      • arguments: <task/device nr> <value nr> <value/formula>

    • timerset
      Set an ESP Easy timer
      • arguments: <timer nr> <duration in s>

    • ESP Easy experimental commands: (The following commands can be changed or removed at any time)

    • rgb
      Used to control a rgb light wo/ an ESPEasy plugin.
      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.
      • arguments: <rrggbb>|on|off|toggle
      • examples:
        set <esp> rgb 00FF00
        set <esp> rgb on
        set <esp> rgb off
        set <esp> 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

    • ESP Easy deprecated commands: (will be removed in a later version)

    • status
      Request esp device status (eg. gpio)
      See attributes: parseCmdResponse, readingPrefixGPIO, readingSuffixGPIOState
      • arguments: <pin>
      • example: set <esp> status 14


    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

    • deepsleep
      This attribut defines the default deep sleep state that is assumed if the ESP has not sent its status to FHEM. Eg. directly after a FHEM restart. If the ESP has sent its status, this value is ignored. Useful if you want to be sure that a set command would be queued and sent when the ESP awakes after a restart/rereadcfg of FHEM.
      Furthermore events for reading sleepState are generated if enabled. ESPEasy Mega with option to set sleep awake time (Config -> Sleep Mode -> Sleep awake time) is required to use this feature.
      Possible values: 0,1
      Default: 0

    • disableRiskyCmds
      Used to disable supposed dangerous set cmds: erase, reset, resetflashwritecounter
      Possible values: 0,1
      Default: 0

    • displayTextEncode
      Used to disable url encoding for text that is send to oled/lcd displays. Useful if you want to encode the text by yourself.
      Possible values: 0,1
      Default: 1 (enabled)

    • displayTextWidth
      Used to specify number of characters per display line.
      If set then all characters before and after the text on the same line will be overwritten with spaces. Attribute displayTextEncode must not be disabled to use this feature. A 128x64px display has 16 characters per line if you are using a 8px font.
      Possible values: integer
      Default: 0 (disabled)

    • 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, dim, line, one, all, fade, colorfade, rainbow, kitt, comet, theatre, scan, dualscan, twinkle, twinklefade, sparkle, wipe, fire, stop, fadetime, fadedelay, count, speed, bgcolor. Ask the ESPEasy maintainer to add more if required.
      Needed to use FHEM's colorpicker or slider widgets to control a rgb/ct/effect/... plugin.
      required values: a valid set command
      eg. attr <esp> mapLightCmds Lights

    • maxCmdDuration
      Only used if an ESP Easy node works in deep sleep mode. This attribut defines the amount of seconds your ESP node needs to work off a single command. In other words: This value defines how much awake time must be left to send a command to your ESP node before it goes into deep sleep mode. Commands that are not send will be queued und worked off when the node awakes again.
      ESPEasy Mega with option to set sleep awake time (Config -> Sleep Mode -> Sleep awake time) is required to use this feature.
      Possible values: secs >= 0, but < awake time
      Default: 1

    • 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 then 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. Therefore the ESP must send the corresponding data regularly (ESP device option "delay").
      Possible values: 0,1
      Default: 1 (enabled)

    • readingFnAttributes

    • readingSwitchText
      Map values for readings to on/off instead 0/1 if ESP device is a switch.
      Possible values:
      0: disable mapping.
      1: enable mapping 0->off / 1->on
      2: enable inverse mapping 0->on / 1->off
      Default: 1

    • 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

    • setState
      Summarize received values in state reading.
      A positive number determines the number of characters used for abbreviated 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)

    • userSetCmds
      Can be used to:
      • Define new, own or unconsidered ESP Easy commands. Note: alternatively the set commands raw or rawsystem can also be used to it.
      • Mapping of secondary commands as primary ones to be able to use FHEM widgets or FHEM's set extentions.
      • Redefine built-in commands.

      Argument must be a perl hash. The following hash keys can be used. An omitted key will be replaced with the appropriate default value.
      • args: minimum number of required arguments for set cmd. Default: 0, no additional arguments required.
        [Special case: if set to -1 then <FHEM cmd> will not be added to <ESP Easy cmd>. Useful if <FHEM cmd> differs from <ESP Easy cmd>. <ESP Easy cmd> must then be part of url hash key. See Forum or example myCmd4 below.]
      • url: ESPEasy URL to be called. Default: "/control?cmd="
      • widget: FHEM widget to be used for this set command. Default: none
      • cmds: Sub command(s) of specified plugin that will be mapped as regular command(s). Must also be a perl hash. Default: none
      • usage: Possible command line arguments. Used in help command and syntax check. Required arguments should be enclosed in curly brackets, optional arguments in square brackets. Both should be separated by spaces. Default: none
      • The following usage strings have a special meaning and effect:
        • <0|1|off|on>: "on" or "off" will be replaced with "0" or "1" in commands send to the ESPEasy device. See attribute readingSwitchText for details.
        • <pin>: GPIO pin numbers can also be written as Arduino/NodeMCU pin names. See get pinMap command.
        • <text>: Text will be encoded for use with oled/lcd commands to be able to use special characters.

      Define new commands:
      • ( myCmd1 => {} )
      • ( myCmd1 => {}, myCmd2 => {} )
      • ( myCmd3 => {args => 2, url => "/?cmd=", widget=> "", usage => "<param1> <param2>"} )
      • ( myCmd4 => {url =>"/control?cmd=event,myevent", args => -1} )

      Define new commands with mapped sub commands:
      This example registers the new commands plugin_a and plugin_b. Both commands can be used like any other ESP Easy command (eg. set dev plugin_b on). Sub commands rgb, ct, on, off and bri can also be used as regular commands. The advantage is that FHEM's widgets and/or set extentions can be used for these sub commands right now.
      • (
        plugin_a => {
            args  => 2,
            url   => "/control?cmd=",
            usage => "<rgb|ct> ",
            cmds  => {
               rgb => { args => 1, usage => "<rrggbb>", widget => "colorpicker,HSV" },
               ct  => { args => 1, usage => "<colortemp>", widget => "colorpicker,CT,2000,10,4000" }
            }
          },
        plugin_b => {
            args  => 1,
            url   => "/foo?bar",
            usage => "<on|off|bri> [bri_value]",
            cmds  => {
               on  => { widget => "noArg" },
               off => { widget => "noArg" },
               bri => { widget => "knob,min:1,max:100,step:1,linecap:round", usage => "<0..255>", args => 1 }
            }
          }
        )
    • useSetExtensions
      If set to 1 and on/off commands are available (use userSetCmds or eventMap if not) then the set extensions are supported.
      Default: 0 (disabled)
      Eg. attr ESPxx useSetExtensions 1

    • Deprecated attributes:

    • parseCmdResponse (deprecated, may be removed in later versions)
      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 like STATUS, PWM, ...
      Possible values: <set cmd>[,<set cmd>][,...]
      Default: status
      Eg. attr ESPxx parseCmdResponse status,pwm

    • pollGPIOs (deprecated, may be removed in later versions)
      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 (deprecated, may be removed in later versions)
      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 (deprecated, may be removed in later versions)
      Specifies a suffix for the state-reading of GPIOs (see Attribute pollGPIOs)
      Possible Values: string
      Default: no suffix
      Eg. attr ESPxx readingSuffixGPIOState _state

=end html =cut