From 4026a7f9196cae944da3e53e82e8a4590eafbd75 Mon Sep 17 00:00:00 2001 From: Otto123 <> Date: Sat, 22 Jan 2022 21:12:58 +0000 Subject: [PATCH] mqtt2.template contrib/99_valetudoUtils.pm:new valetudo Template version git-svn-id: https://svn.fhem.de/fhem/trunk@25545 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/lib/AttrTemplate/mqtt2.template | 60 ++++---- fhem/contrib/AttrTemplate/99_valetudoUtils.pm | 131 +++++++++++++++--- 2 files changed, 144 insertions(+), 47 deletions(-) diff --git a/fhem/FHEM/lib/AttrTemplate/mqtt2.template b/fhem/FHEM/lib/AttrTemplate/mqtt2.template index 549ffd046..4669c6f2d 100644 --- a/fhem/FHEM/lib/AttrTemplate/mqtt2.template +++ b/fhem/FHEM/lib/AttrTemplate/mqtt2.template @@ -4567,52 +4567,58 @@ attr DEVICE event-on-change-reading .* attr DEVICE model roborockRE setreading DEVICE attrTemplateVersion 20210510 -# new valtudo Template contributed by Otto123, should replace the old valetudo/rockrobo Template +# new valetudo Template contributed by Otto123, should replace the old valetudo/rockrobo Template name:valetudo filter:TYPE=MQTT2_DEVICE -desc:use this to control a rooted vacuum with valetudo. For further details visit https://github.com/Hypfer/Valetudo

NOTE: Initial version +desc:use this to control a rooted vacuum with valetudo V2. For further details visit Hypfer/Valetudo

NOTE: tested with Dreame L10pro, Xiaomi Vacuum Gen1, details: Forum Thread order:X_03b -par:BASE_TOPIC;BASE_TOPIC typically is valetudo;{ AttrVal("DEVICE","readingList","") =~ m,(valetudo)[/].*:, ? $1 : undef } -par:DEV_ID;DEV_ID is random by Firmware;{ AttrVal("DEVICE","readingList","") =~ m,valetudo[/]([^/]+)[/].*:, ? $1 : undef } -{ Svn_GetFile("contrib/AttrTemplate/99_valetudoUtils.pm", "FHEM/99_valetudoUtils.pm", sub(){CommandReload(undef, "99_valetudoUtils")}) } -deletereading -q DEVICE (?!associatedWith|IODev).* +par:BASE_TOPIC;BASE_TOPIC typically is valetudo;{ AttrVal("DEVICE","devicetopic",AttrVal("DEVICE","readingList","")) =~ m,([^\/:]+)[\/].*, ? $1 : undef } +par:DEV_ID;DEV_ID is random by Firmware;{ AttrVal("DEVICE","devicetopic",AttrVal("DEVICE","readingList","")) =~ m,[^\/]+\/([^\/:]+).*, ? $1 : undef } +{ $data{f}='99_valetudoUtils.pm';\ +$data{u}="https://svn.fhem.de/trac/browser/trunk/fhem/contrib/AttrTemplate/$data{f}?format=txt";\ +qx(wget -qO FHEM/$data{f} $data{u}); CommandReload(undef, $data{f});\ +delete $data{f};delete $data{u};''} attr DEVICE alias DEV_ID attr DEVICE devicetopic BASE_TOPIC/DEV_ID attr DEVICE icon vacuum_top attr DEVICE readingList \ $\DEVICETOPIC/\x24state:.* _state\ - $\DEVICETOPIC/AttachmentStateAttribute/dustbin:.* dustbin\ - $\DEVICETOPIC/AttachmentStateAttribute/.* { $TOPIC =~ m,.*\/(.*),; {"$1"=>$EVENT} }\ + $\DEVICETOPIC/(Att.*|Basic.*|Consum.*|Curr.*|Loc.*|Wifi.*)/[a-zA-Z\-_]+:.* { $TOPIC =~ m,$\DEVICETOPIC\/.*\/([a-zA-Z\-_]+),; $1 eq 'ips'? {"ip4"=> (split ',',$EVENT)[0]}:{"$1"=>$EVENT} }\ $\DEVICETOPIC/BatteryStateAttribute/level:.* batteryPercent\ $\DEVICETOPIC/BatteryStateAttribute/status:.* batteryState\ - $\DEVICETOPIC/BasicControlCapability/operation:.* operation\ - $\DEVICETOPIC/CurrentStatisticsCapability/area:.* area\ - $\DEVICETOPIC/CurrentStatisticsCapability/time:.* time\ - $\DEVICETOPIC/ConsumableMonitoringCapability/.* { $TOPIC =~ m,.*\/(.*),; {"$1"=>$EVENT} }\ $\DEVICETOPIC/FanSpeedControlCapability/preset:.* fanSpeed\ - $\DEVICETOPIC/LocateCapability/locate:.* locate\ + $\DEVICETOPIC/GoToLocationCapability/presets:.* .locationsPresets\ + $\DEVICETOPIC/GoToLocationCapability/go:.* {}\ $\DEVICETOPIC/MapData/map-data:.* {}\ - $\DEVICETOPIC/MapData/segments:.* segments\ - $\DEVICETOPIC/StatusStateAttribute/status:.* state\ + $\DEVICETOPIC/MapData/segments:.* .segments\ + $\DEVICETOPIC/StatusStateAttribute/status:.* { {"state"=>$EVENT,"cleanerState"=>$EVENT} }\ $\DEVICETOPIC/StatusStateAttribute/detail:.* stateDetail\ $\DEVICETOPIC/StatusStateAttribute/error:.* stateError\ $\DEVICETOPIC/WaterUsageControlCapability/preset:.* waterUsage\ - $\DEVICETOPIC/WifiConfigurationCapability/.* { $TOPIC =~ m,.*\/(.*),; {"$1"=>$EVENT} }\ - $\DEVICETOPIC/ZoneCleaningCapability/presets:.* presets\ - $\DEVICETOPIC/ZoneCleaningCapability/start:.* start + $\DEVICETOPIC/ZoneCleaningCapability/presets:.* .zonesPresets\ + $\DEVICETOPIC/ZoneCleaningCapability/start:.* {} attr DEVICE setList \ - operation:PAUSE,START,STOP,HOME $\DEVICETOPIC/BasicControlCapability/operation/set $EVTPART1\ - clean_segment:{"multiple-strict,".valetudo_w($name,"segments")} { valetudo_c($NAME,$EVENT) }\ - clean_zone:{valetudo_w($name,"presets")} { valetudo_c($NAME,$EVENT) }\ - fanSpeed:off,min,low,medium,high,turbo,max $\DEVICETOPIC/FanSpeedControlCapability/preset/set $EVTPART1\ - waterUsage:off,min,low,medium,high,turbo,max $\DEVICETOPIC/WaterUsageControlCapability/preset/set $EVTPART1\ + pause:noArg $\DEVICETOPIC/BasicControlCapability/operation/set PAUSE\ + start:noArg $\DEVICETOPIC/BasicControlCapability/operation/set START\ + stop:noArg $\DEVICETOPIC/BasicControlCapability/operation/set STOP\ + charge:noArg $\DEVICETOPIC/BasicControlCapability/operation/set HOME\ + clean_zone:{valetudo_w($name,'zones')} { valetudo_c($NAME,$EVENT) }\ + fanSpeed:off,min,low,medium,high,turbo,max $\DEVICETOPIC/FanSpeedControlCapability/preset/set $EVTPART1\ + waterUsage:off,min,low,medium,high,turbo,max $\DEVICETOPIC/WaterUsageControlCapability/preset/set $EVTPART1\ locate:PERFORM $\DEVICETOPIC/LocateCapability/locate/set $EVTPART1\ x_raw_payload:textField { valetudo_c($NAME,$EVENT) } -attr DEVICE userReadings ip4:ips:.* {(split ',',ReadingsVal($name,'ips','error'))[0]} attr DEVICE event-on-change-reading .* -attr DEVICE setStateList operation clean_segment clean_zone fanSpeed waterUsage x_raw_payload -attr DEVICE model valetudo -setreading DEVICE attrTemplateVersion 20220109 or prior +attr DEVICE timestamp-on-change-reading .* +attr DEVICE setStateList operation clean_segment clean_zone goto fanSpeed waterUsage locate x_raw_payload +attr DEVICE model valetudoV2 +attr DEVICE stateFormat state +setreading DEVICE attrTemplateVersion 20220121 +option:{valetudo_f('DEVICE','MapSegmentation')} +{ CommandAttr_multiline( 'DEVICE','setList',q( clean_segment:{'multiple-strict,'.valetudo_w($name,'segments')} { valetudo_c($NAME,$EVENT) }) ) } +option:{valetudo_f('DEVICE','GoToLocation')} +{ CommandAttr_multiline( 'DEVICE','setList',q( goto:{valetudo_w($name,'locations')} { valetudo_c($NAME,$EVENT) }) ) } +option:{1} +deletereading -q DEVICE (?!associatedWith|IODev).* # contributed by Otto123, source: https://forum.fhem.de/index.php/topic,94495.msg1062312.html#msg1062312 name:worx_landroid diff --git a/fhem/contrib/AttrTemplate/99_valetudoUtils.pm b/fhem/contrib/AttrTemplate/99_valetudoUtils.pm index 7b2d2bac2..044faec6a 100644 --- a/fhem/contrib/AttrTemplate/99_valetudoUtils.pm +++ b/fhem/contrib/AttrTemplate/99_valetudoUtils.pm @@ -8,7 +8,7 @@ package main; use strict; use warnings; -use JSON; +use JSON;Dumper; sub valetudoUtils_Initialize { @@ -17,23 +17,25 @@ valetudoUtils_Initialize { } # Enter you functions below _this_ line. -# valetudo_w return a string for dynamic selection setList +####### +# return a string for dynamic selection setList (widgets) sub valetudo_w { my $NAME = shift; my $setter = shift; # this part reads segments, it's only filled if Provide map data is enabled in connectivity if ($setter eq 'segments') { - my $json = ReadingsVal($NAME,'segments','select'); + my $json = ReadingsVal($NAME,'.segments','{}'); + if ($json eq '{}') {$json = '{"1":"no_Segment_or_not_supported"}'}; my $decoded = decode_json($json); return join ',', sort values %$decoded } -# this part read preset which contains a full json for preset zones -if ($setter eq 'presets') { - my $json = ReadingsVal($NAME,'presets','select'); +# this part read presets which contains a full json for preset zones or locations +if ($setter eq 'zones' or $setter eq 'locations') { + my $json = ReadingsVal($NAME,'.'.$setter.'Presets',''); my $decoded = decode_json($json); - my %t; - for (keys %$decoded) { $t{$decoded->{$_}->{'name'}} = $_ } # build a new hash only with zone id and zone name - return join ',', sort keys %t + my @array; + for (keys %$decoded) { push @array, $decoded->{$_}->{'name'} } + return join',',@array } # this part is for study purpose to read the full json segments with the REST API like # setreading alias=DreameL10pro json_segments {(qx(wget -qO - http://192.168.90.21/api/v2/robot/capabilities/MapSegmentationCapability))} @@ -46,8 +48,8 @@ if ($setter eq 'json_segments') { return join ',', sort keys %t } } -############################################################## -# valetudo_w return a complete string for setList right part +####### +# valetudo_c return a complete string for setList right part sub valetudo_c { my $NAME = shift; my $EVENT = shift; @@ -62,22 +64,28 @@ if ($cmd eq 'x_raw_payload') {$ret=$devicetopic.$load} # this part return an array of segment id's according to selected Names from segments (simple json) if ($cmd eq 'clean_segment') { my @rooms = split',', $load; - my $json = ReadingsVal($NAME,'segments',''); + my $json = ReadingsVal($NAME,'.segments',''); my $decoded = decode_json($json); - my %t = reverse %$decoded; my @ids; - for ( @rooms ) {push @ids, $t{$_}} + for ( @rooms ) { push @ids,{reverse %$decoded}->{$_} } my %Hcmd = ( clean_segment => {segment_ids => \@ids,iterations => 1,customOrder => 'true' } ); $ret = $devicetopic.'/MapSegmentationCapability/clean/set '.toJSON $Hcmd{$cmd} } -# this part return the zone id according to the selected Name from presets (zones) (more complex json) +# this part return the zone/location id according to the selected Name from presets (zones/locations) (more complex json) if ($cmd eq 'clean_zone') { - my $json = ReadingsVal($NAME,'presets',''); + my $json = ReadingsVal($NAME,'.zonesPresets',''); my $decoded = decode_json($json); - my %t; - for (keys %$decoded) { $t{$decoded->{$_}->{'name'}} = $_ } - $ret = $devicetopic.'/ZoneCleaningCapability/start/set '.$t{$load} + for (keys %$decoded) { + if ( $decoded->{$_}->{'name'} eq $load ) {$ret = $devicetopic.'/ZoneCleaningCapability/start/set '.$_ } + } + } +if ($cmd eq 'goto') { + my $json = ReadingsVal($NAME,'.locationsPresets',''); + my $decoded = decode_json($json); + for (keys %$decoded) { + if ( $decoded->{$_}->{'name'} eq $load ) {$ret = $devicetopic.'/GoToLocationCapability/go/set '.$_ } + } } # this part is for study purpose to read the full json segments with the REST API @@ -95,9 +103,92 @@ if ($cmd eq 'clean_segment_j') { my %Hcmd = ( clean_segment => {segment_ids => \@ids,iterations => 1,customOrder => 'true' } ); $ret = $devicetopic.'/MapSegmentationCapability/clean/set '.toJSON $Hcmd{$cmd} } - return $ret } +####### +# ask the robot via REST API for Featurelist and feature and return true false +sub valetudo_f { +my $NAME = shift; # Devicename of the robot +my $substr = shift; # requested Feature like GoToLocation or MapSegmentation +my $ip=ReadingsVal($NAME,'ip4',(split ',',ReadingsVal($NAME,'ips','error'))[0]); +my $string = GetHttpFile($ip, '/api/v2/robot/capabilities'); +index($string, $substr) == -1 ? '0':'1'; +} +####### +# add a line to multiline Attribute setList or regList +# CommandAttr_multiline( 'MQTT2_valetudo_xxx','setList',q( clean_segment:{"multiple-strict,".valetudo_w($name,"segments")} { valetudo_c($NAME,$EVENT) }) ) +sub CommandAttr_multiline { +my $NAME = shift; +my $attr = shift; +my $item = shift; +if ($attr ne 'setList' and $attr ne 'readingList') {return 'use only for multiline attrib'} +my $val = AttrVal($NAME,$attr,'')."\n".$item; +CommandAttr(undef, "$NAME $attr $val"); +} + +################ +# is never used, was in a first version used to preread the json in valetudo_c +# return simpel json pairs from presets format of valetudo +sub valetudo_r { +my $setter = shift; +my $payload = shift; +my $ret = 'error'; +my %t; +if ($setter eq 'presets') { + my $decoded = decode_json($payload); + for (keys %$decoded) { $t{$decoded->{$_}->{'name'}} = $_ } # build a new hash only with names and ids pairs + $ret = toJSON(\%t); # result is sorted + } + return $ret +} + + +####### Aus dem Forum funktioniert aber nicht +# Zeigt aber wie man Readings zurück gibt +sub +valetudo2svg($$$) +{ + my ($reading, $d, $filename) = @_; + my %ret; + + if(!open FD,">$filename") { + $ret{$reading} = "ERROR: $filename: $!"; + return \%ret; + } + print FD $d; + close(FD); + $ret{$reading} = "Wrote $filename"; + return \%ret; + + if($d !~ m/height":(\d+),"width":(\d+).*?floor":\[(.*\])\]/) { + $ret{$reading} = "ERROR: Unknown format"; + return \%ret; + } + my ($w,$h,$nums) = ($1, $2, $3); + + my $svg=<<"EOD"; + + + + + +EOD + + $nums =~ s/\[(\d+),(\d+)\]/ + $svg .= "\n"; + "" + /xge; + $svg .= ""; + + if(!open FD,">$filename") { + $ret{$reading} = "ERROR: $filename: $!"; + return \%ret; + } + print FD $svg; + close(FD); + $ret{$reading} = "Wrote $filename"; + return \%ret; +} 1; =pod