diff --git a/fhem/contrib/98_dev_proxy.pm b/fhem/contrib/98_dev_proxy.pm
new file mode 100644
index 000000000..c6363362e
--- /dev/null
+++ b/fhem/contrib/98_dev_proxy.pm
@@ -0,0 +1,623 @@
+##############################################################################
+#
+# 98_dev_proxy.pm
+# Copyright by A. Schulz
+# e-mail:
+#
+# 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 .
+#
+##############################################################################
+# $Id: $
+package main;
+
+use strict;
+use warnings;
+use List::Util qw[min max];
+use Data::Dumper;
+
+#####################################
+sub dev_proxy_setDefaultObservedReadings($);
+sub dev_proxy_setObservedReading($@);
+sub dev_proxy_addDevice($$);
+sub dev_proxy_updateReadings($$);
+sub dev_proxy_computeCombReading($$);
+sub dev_proxy_mapDeviceReadingValueDefultMap($$$$$);
+sub dev_proxy_mapDeviceReadingValue($$$$$);
+sub dev_proxy_mapValue($$$);
+sub dev_proxy_eval_map_readings($$);
+sub dev_proxy_remap_reading($$$);
+sub dev_proxy_cleanup_readings($);
+#####################################
+sub dev_proxy_Initialize($) {
+ my ($hash) = @_;
+
+ $hash->{DefFn} = "dev_proxy_Define";
+ $hash->{UndefFn} = "dev_proxy_Undef";
+ $hash->{NotifyFn} = "dev_proxy_Notify";
+ $hash->{SetFn} = "dev_proxy_Set";
+ $hash->{AttrFn} = "dev_proxy_Attr";
+ $hash->{AttrList} = "observedReadings setList mapValues mapReadings ". "disable disabledForIntervals ". $readingFnAttributes;
+
+}
+
+sub dev_proxy_Define($$) {
+my ($hash, $def) = @_;
+ my @a = split("[ \t][ \t]*", $def);
+
+ my $u = "wrong syntax: define dev_proxy [device ...]*";
+ return $u if(int(@a) < 3);
+
+ my $devname = shift(@a);
+ my $modname = shift(@a);
+
+ $hash->{CHANGEDCNT} = 0;
+ my $or = AttrVal($devname, "observedReadings", undef);
+ if(defined($or)) {
+ dev_proxy_setObservedReading($or);
+ } else {
+ dev_proxy_setDefaultObservedReadings($hash);
+ }
+
+ my %list;
+ $hash->{CONTENT} = \%list;
+ foreach my $a (@a) {
+ foreach my $d (devspec2array($a)) {
+ dev_proxy_addDevice($hash, $d);
+ }
+ }
+
+ my $valuesMap = AttrVal($devname,'mapValues',undef);
+ if (defined $valuesMap) {
+ $hash->{DEV_READING_VALUE_MAP} = eval($valuesMap);
+ } else {
+ $hash->{DEV_READING_VALUE_MAP} = undef;
+ }
+ dev_proxy_eval_map_readings($hash, AttrVal($devname,'mapReadings',undef));
+ dev_proxy_updateReadings($hash, undef);
+
+ return undef;
+}
+
+sub dev_proxy_setDefaultObservedReadings($) {
+ my ($hash) = @_;
+ #$hash->{OBSERVED_READINGS} = ["state","dim", "position"];
+ $hash->{OBSERVED_READINGS} = {};
+ $hash->{OBSERVED_READINGS} ->{"state"}=1;
+ $hash->{OBSERVED_READINGS} ->{"dim"}=1;
+ $hash->{OBSERVED_READINGS} ->{"position"}=1;
+}
+
+sub dev_proxy_setObservedReading($@) {
+ my ($hash, @list) = @_;
+
+ $hash->{OBSERVED_READINGS} = {};
+ foreach my $a (@list) {
+ $hash->{OBSERVED_READINGS} -> {$a} = 1;
+ }
+}
+
+sub dev_proxy_addDevice($$) {
+ my ($hash, $d) = @_;
+ if($defs{$d}) {
+ $hash->{CONTENT}{$d} = 1;
+ }
+}
+
+sub dev_proxy_Undef($$) {
+ my ($hash, $def) = @_;
+ return undef;
+}
+
+sub dev_proxy_Notify($$) {
+ my ($hash, $dev) = @_;
+ my $name = $hash->{NAME};
+
+ if( $dev->{NAME} eq "global" ) {
+ my $max = int(@{$dev->{CHANGED}});
+ for (my $i = 0; $i < $max; $i++) {
+ my $s = $dev->{CHANGED}[$i];
+ $s = "" if(!defined($s));
+ if($s =~ m/^RENAMED ([^ ]*) ([^ ]*)$/) {
+ my ($old, $new) = ($1, $2);
+ if( exists($hash->{CONTENT}{$old}) ) {
+
+ $hash->{DEF} =~ s/(\s+)$old(\s*)/$1$new$2/;
+
+ delete( $hash->{CONTENT}{$old} );
+ $hash->{CONTENT}{$new} = 1;
+ }
+ } elsif($s =~ m/^DELETED ([^ ]*)$/) {
+ my ($name) = ($1);
+
+ if( exists($hash->{CONTENT}{$name}) ) {
+
+ $hash->{DEF} =~ s/(\s+)$name(\s*)/ /;
+ $hash->{DEF} =~ s/^ //;
+ $hash->{DEF} =~ s/ $//;
+
+ delete $hash->{CONTENT}{$name};
+ delete $hash->{".cachedHelp"};
+ }
+ }
+ }
+ }
+
+ return "" if(IsDisabled($name));
+
+ # pruefen ob Devices welches das notify ausgeloest hat Mitglied dieser Gruppe ist
+ return "" if (! exists $hash->{CONTENT}->{$dev->{NAME}});
+
+ dev_proxy_updateReadings($hash, $dev);
+
+ readingsSingleUpdate($hash, "LastDevice", $dev->{NAME}, 0);
+
+ return undef;
+}
+
+sub dev_proxy_updateReadings($$) {
+ my ($hash,$dev) = @_;
+ my $name = $hash->{NAME};
+
+ if($hash->{INNTFY}) {
+ Log3 $name, 1, "ERROR: endless loop detected in composite_Notify $name";
+ return "";
+ }
+ $hash->{INNTFY} = 1;
+
+# my $nrmap;
+# foreach my $or (keys %{ $hash->{OBSERVED_READINGS}} ) {
+# my $map;
+# foreach my $d (keys %{ $hash->{CONTENT}} ) {
+# next if(!$defs{$d});
+# my $or_mapped = dev_proxy_remap_reading($hash, $d, $or);
+# my $devReadings = ReadingsVal($d,$or_mapped,undef);
+# if(defined($devReadings)) {
+# ($devReadings) = dev_proxy_mapDeviceReadingValueDefultMap($hash,$d,$or,$devReadings,1);
+# $map->{$d}=$devReadings;
+# }
+# }
+# my $newReading = dev_proxy_computeCombReading($or, $map);
+# if(defined($newReading)) {
+# $nrmap->{$or}=$newReading;
+# }
+# }
+
+ my $map;
+ foreach my $or (keys %{ $hash->{OBSERVED_READINGS}} ) {
+ foreach my $d (keys %{ $hash->{CONTENT}} ) {
+ next if(!$defs{$d});
+ my $or_mapped = dev_proxy_remap_reading($hash, $d, $or);
+ my $devReadings = ReadingsVal($d,$or_mapped,undef);
+ if(defined($devReadings)) {
+ my $nReading;
+ ($devReadings, $nReading) = dev_proxy_mapDeviceReadingValueDefultMap($hash,$d,$or,$devReadings,1);
+ # Nur wenn nicht ueberschrieben wurde
+ if(!defined($map->{$or}->{$d})) {
+ $map->{$or}->{$d}=$devReadings;
+ }
+ # falls umgemappt werden soll, den neuen Wert auch aufnehmen (ueberschreibt den eigentlichen Wert für das andere Reading)
+ if($or ne $nReading) {
+ $map->{$nReading}->{$d}=$devReadings;
+ }
+ }
+ }
+ }
+
+ # jetzt gesammelten Werte kombinieren / zusammenrechnen
+ my $nrmap;
+ foreach my $or (keys %{ $map } ) {
+ my $newReading = dev_proxy_computeCombReading($or, $map->{$or});
+ if(defined($newReading)) {
+ $nrmap->{$or}=$newReading;
+ }
+ }
+
+ readingsBeginUpdate($hash);
+ foreach my $d (sort keys %{ $nrmap }) {
+ my $newState = $nrmap->{$d};
+ my $dd = defined($dev)?" because device $dev->{NAME} has changed":"";
+ Log3 ($name, 5, "Update composite '$name' reading $d to $newState $dd");
+ readingsBulkUpdate($hash, $d, $newState);
+ }
+ readingsEndUpdate($hash, 1);
+
+ $hash->{CHANGEDCNT}++;
+ delete($hash->{INNTFY});
+
+ dev_proxy_cleanup_readings($hash);
+}
+
+sub dev_proxy_computeCombReading($$) {
+ my ($rName, $map) = @_;
+ my $size = keys %{$map};
+ if($size<1) {
+ return undef;
+ }
+
+ my @values = values %{$map};
+
+ if($rName eq 'state') {
+ my $tm;
+ foreach my $d (@values) {
+ $tm->{$d}=1;
+ }
+ return join(" ", keys %{ $tm });
+ }
+
+ my $maxV = max(@values);
+ my $minV = min(@values);
+ #if($maxV-$minV<10) {
+ return $minV+($maxV-$minV)/2;
+ #}
+
+ return $maxV;
+
+ return undef;
+}
+
+sub dev_proxy_mapDeviceReadingValueDefultMap($$$$$) {
+ my ($hash, $dev, $reading, $val, $incoming) = @_;
+ return dev_proxy_mapDeviceReadingValue($hash->{DEV_READING_VALUE_MAP}, $dev, $reading, $val,$incoming);
+}
+
+# Definition: map {'dev:reading'=>{'val'=>'valnew',.},..}
+# Priority: zuerst richtungsspezifische (in/out):
+# in:dev:reading, in:dev:*, in:*:reading, in:*:* (or in:*),
+# dann Standard: dev:reading, dev:*, *:reading, *:* (or *)
+# Nur bei out-Richtung (also set) relevant:
+# Moeglichkeit, Zielreading umzudefinieren.
+# Dafuer soll der Zielwert in Form WERT:NEWREADINGNAME geliefert werden:
+# ...{'val'=>'valnew:newreading',..}...
+sub dev_proxy_mapDeviceReadingValue($$$$$) {
+ my ($map, $dev, $reading, $val, $incoming) = @_;
+
+ return ($val, $reading) unless defined $map;
+ my $nval;
+ my $selectedMap;
+ # zuerst richtungsspeziefische Map (in/out) ausprobieren
+ my $prefix = $incoming ? 'in:' : 'out:';
+ $selectedMap = $map->{$prefix.$dev.':'.$reading};
+ $selectedMap = $map->{$prefix.$dev.':*'} unless defined $selectedMap;
+ $selectedMap = $map->{$prefix.'*:'.$reading} unless defined $selectedMap;
+ $selectedMap = $map->{$prefix.'*:*'} unless defined $selectedMap;
+ $selectedMap = $map->{$prefix.'*'} unless defined $selectedMap;
+ # falls keine passende Map vorhanden ist, oder sie keine passende Regel
+ # enthaelt, dann Standardmap verwenden
+ if(defined $selectedMap) {
+ $nval = dev_proxy_mapValue($selectedMap, $val, $incoming);
+ if(defined $nval) {
+ my ($nval, @areading) = split(/:/, $nval);
+ my $nreading = @areading ? join(':',@areading) : $reading;
+ return ($nval, $nreading);
+ }
+ }
+
+ $selectedMap = $map->{$dev.':'.$reading};
+ $selectedMap = $map->{$dev.':*'} unless defined $selectedMap;
+ $selectedMap = $map->{'*:'.$reading} unless defined $selectedMap;
+ $selectedMap = $map->{'*:*'} unless defined $selectedMap;
+ $selectedMap = $map->{'*'} unless defined $selectedMap;
+ # Originalwert, falls kein passendes Map
+ return ($val, $reading) unless defined $selectedMap;
+
+ $nval = dev_proxy_mapValue($selectedMap, $val, $incoming);
+ return ($nval, $reading) if defined $nval;
+ # Originalwert, falls keine Entsprechung im Map
+ return ($val, $reading);
+}
+
+sub dev_proxy_mapValue($$$) {
+ my ($map, $val, $incoming) = @_;
+
+ my $nv=$map->{$val};
+ if(!defined($nv)) {
+ $nv=$map->{'*'};
+ }
+
+ return undef unless(defined($nv)) ;
+
+ if($nv=~/^{/) {
+ $nv = eval($nv);
+ }
+
+ return $nv;
+}
+
+sub dev_proxy_Set($@){
+ my ($hash,$name,$command,@values) = @_;
+
+ return "no set value specified" if(!defined($command));
+
+ if ($command eq '?') {
+ my $setList = AttrVal($name, "setList", undef);
+ if(!defined $setList) {
+ $setList = "";
+ foreach my $n (sort keys %{ $hash->{READINGS} }) {
+ next if($n eq 'LastDevice' || $n eq 'state');
+ $setList.=$n;
+ if($n eq 'position' || $n eq 'dim' ) {
+ $setList.=":slider,0,1,100";
+ }
+ $setList.=" ";
+ }
+ }
+ $setList =~ s/\n/ /g;
+ return "Unknown argument $command, choose one of $setList";
+ }
+
+ if(int(@values)>0 && !defined($hash->{READINGS}->{$command})) {
+ return "Unknown reading $command";
+ }
+
+ my $ret;
+ my @devList = keys %{$hash->{CONTENT}};
+ foreach my $d (@devList) {
+ my $val;
+ if(int(@values)<1) {
+ # state
+ my $cmd = "state";
+ ($val, $cmd) = dev_proxy_mapDeviceReadingValueDefultMap($hash, $d, "state", $command,0);
+ $cmd = dev_proxy_remap_reading($hash, $d, $cmd);
+ my $cmdstr;
+ if($cmd ne "state") {
+ $cmdstr = join(" ", ($d, $cmd, $val));
+ } else {
+ $cmdstr = join(" ", ($d, $val));
+ }
+ #Log3 $hash, 1, "SET: >>> ".$cmdstr;
+ $ret .= CommandSet(undef, $cmdstr);
+ } else {
+ # benannte readings
+ my $cmd = $command;
+ ($val, $cmd) = dev_proxy_mapDeviceReadingValueDefultMap($hash, $d, $command, join(" ", @values),0);
+ $cmd = dev_proxy_remap_reading($hash, $d, $cmd);
+ my $cmdstr;
+ if($cmd ne "state") {
+ $cmdstr = join(" ", ($d, $cmd, $val));
+ } else {
+ $cmdstr = join(" ", ($d, $val));
+ }
+ #Log3 $hash, 1, "SET: >>> ".$cmdstr;
+ $ret .= CommandSet(undef, $cmdstr);
+ }
+ }
+ Log3 $hash, 5, "SET: $ret" if($ret);
+
+
+ return undef;
+}
+
+sub dev_proxy_Attr($@){
+ my ($type, $name, $attrName, $attrVal) = @_;
+ my %ignore = (
+ alias=>1,
+ devStateIcon=>1,
+ disable=>1,
+ disabledForIntervals=>1,
+ group=>1,
+ icon=>1,
+ room=>1,
+ stateFormat=>1,
+ webCmd=>1,
+ userattr=>1
+ );
+
+ return undef if($ignore{$attrName});
+
+ my $hash = $defs{$name};
+
+ if($attrName eq "observedReadings") {
+ if($type eq "del") {
+ dev_proxy_setDefaultObservedReadings($hash);
+ } else {
+ my @a=split("[ \t][ \t]*",$attrVal);
+ dev_proxy_setObservedReading($hash, @a);
+ }
+ } elsif($attrName eq "mapValues") {
+ if($type ne "del") {
+ $hash->{DEV_READING_VALUE_MAP} = eval($attrVal);
+ } else {
+ $hash->{DEV_READING_VALUE_MAP} = undef;
+ }
+ } elsif($attrName eq "mapReadings") {
+ if($type ne "del") {
+ dev_proxy_eval_map_readings($hash, $attrVal);
+ } else {
+ $hash->{READING_NAME_MAP} = undef;
+ }
+ }
+
+ dev_proxy_updateReadings($hash, undef);
+
+ Log3 $name, 4, "dev_proxy attr $type";
+ return undef;
+}
+
+sub dev_proxy_eval_map_readings($$) {
+ my ($hash, $attrVal) = @_;
+ $hash->{READING_NAME_MAP} = undef unless defined $attrVal;
+ my $map;
+ if(defined $attrVal) {
+ my @list = split("[ \t][ \t]*", $attrVal);
+ foreach (@list) {
+ my($devName, $devReading, $newReading) = split(/:/, $_);
+ $map->{$devName} -> {$newReading} = $devReading;
+ }
+ }
+ $hash->{READING_NAME_MAP} = $map;
+}
+
+# Readings remappen, die von hier in die Richtung anderen Devices gesendet werden
+sub dev_proxy_remap_reading($$$) {
+ my ($hash, $devName, $readingName) = @_;
+ my $map = $hash->{READING_NAME_MAP};
+ return $readingName unless defined $map;
+
+ my $t = $map->{$devName};
+ $t = $map->{"*"} unless defined $t;
+ my $newReadingName = $t->{$readingName} if defined $t;
+
+ return $readingName unless defined $newReadingName;
+ return $newReadingName;
+}
+
+sub dev_proxy_cleanup_readings($) {
+ my ($hash) = @_;
+ my $name = $hash->{NAME};
+
+ my $map = $hash->{OBSERVED_READINGS};
+ return unless defined $map;
+
+ foreach my $aName (keys %{$defs{$name}{READINGS}}) {
+ if(!defined $map->{$aName} && ($aName ne "LastDevice") && ($aName ne "state")) {
+ delete $defs{$name}{READINGS}{$aName};
+ }
+ }
+}
+
+1;
+
+=pod
+=item helper
+=item summary organize devices and readings, remap / rename readings
+=item summary_DE mehrere Geräte zu einem zusammenfassen, Readings umbenennen / umrechnen
+=begin html
+
+
+dev_proxy
+
+=end html
+=begin html_DE
+
+
+dev_proxy
+
+
+
+ Define
+
+ define <name> dev_proxy <dev1> <dev2> ...
+
+ Mit diesem virtuellem Gerät können ausgewählte Readings eines anderen oder mehreren Geräte
+ an einer Stelle zusammengefasst werden. Diese können dabei ggf. umbenannt
+ und / oder umgerechnet werden.
+
Beispiel:
+ defmod testdev_proxy dev_proxy MQ_DG_WZ_O_Rollo1 MQ_DG_WZ_O_Rollo2
+
+
+
+
+ Set
+
+ Die hier angegebenen Werte werden an die Originalgeräte weitergeleitet.
+ Definierte Umbenennungen und Umrechnungen werden berücksichtigt.
+
+
+
+
+ Get
+
+
+
+
+ Attributes
+
+ -
+ observedReadings: bestimmt zu überwachende Readings (durch Leerzeichen separierte liste)
+ (wenn dieses Attrubut nicht angegeben wird, werden 'state', 'dim'und 'position' überwacht)
+
Beispiel:
+ attr <name> observedReadings state level
+
+
+ -
+ setList: Durch Leerzeichen getrennte Liste der Werte für Set-Befehl.
+ Diese Liste wird bei "set name ?" ausgegeben.
+ Damit kann das FHEMWEB-Frontend Auswahl-Menüs oder Schalter erzeugen.
+
Die gesetzten Werte werden an die entsprechende Readings der Geräte weitergereicht.
+ Dabei wird im mapReadings definierte Umsetzungsregel beachtet.
+
Es wird jedoch nicht geprüfft, ob angegebene Reading in observed_reading vorhanden ist.
+ Ggf. wird einfach 'blind' weitergereicht.
+ Beispiel:
+ attr <name> setList opens:noArg closes:noArg stop:noArg up:noArg down:noArg position:slider,0,1,100
+
+
+ -
+ mapValues: Erlaubt Änderungen/Umrechnungen an den Werte der Readings
+ ggf. abhängig von den jeweiligen Device- und Readingsnamen.
+ Umrechnungstabellen können je nach Richtung ('in' aus Notify oder 'out' für set) getrennt definiert werden.
+ Falls die Definition mit dem Richtungsprefix nicht existiert oder kein Ergebnis liefert,
+ werden Standartdefinitionen (die parallel angegeben werden können) verwendet.
+ Für ausgehende Werte kann die Ziel-Reading auch umdefiniert werden, dieser wird im Zielwert nach dem ':' angegeben.
+ Die Angabe ist auch bei 'in:' möglich, dann wird dieser Wert den Wert der angegebenen Reading (bei dem selben Device) ersetzen.
+ Das kann nützlich sein, um spezielle Werte an andere Readings umzuleiten.
+
+ Die Werte müssen in als eine Hash-Map angegeben werden.
+ <STATE> soll als state angesprochen werden.
+ Form: {'<device>:<reading>'=>{'<value>'=>'new value',..},..}
+ Oder mit Richtungsprefix: {'out:<device>:<reading>'=>{'<value>'=>'new value[:new reading]',..},..}
+ <device>, <reading> und <value> können auch mit * angegeben werden.
+ Diese Angabe wird als 'Default' verwendet, wenn keine andere gepasst haben.
+ Prioritätenreihenfolge für die <device>:<reading>-Paaren: <device>:<reading>, <device>:*, *:<reading>, *:* (oder auch *).
+ Für die Umrechnung steht das Originalvalue als $val-Variable zur Verfügung.
+ Falls Richtung (von Original-Gerät (bei Notify) oder zu dem Original-Gerät (bei set))
+ wichtig ist, kann diese durch Abfrage der Variable $incoming
+ (jeweils 1 oder 0) abgefragt werden.
+
Beispiel:
+ attr <name> mapValues {'*:position'=>{'*'=>'{100-$val}','down'=>'100', 'closed'=>'100', 'up'=>'0', 'open'=>'0', 'open_ack'=>'0', 'off'=>'0', 'on'=>'100'}}
+
+
+ -
+ mapReadings: Erlaubt Geräte-Readings unter anderem Namen verwenden.
+ * kann als Default anstatt Gerätenamen verwendet werden. <STATE> soll als state angesprochen werden.
+
+ attr <name> mapReadings <device>:<original reading>:<hier zu verwendende reading> ...
+
+ Beispiel:
+ attr <name> mapReadings Rollo1:pct:position Rollo2:pct:position
+
+
+
+ Beispiele:
+
+ -
+ Zusammenfassung zweier Rollläden, Steuerung über die Reading 'position', Umkehrung der Prozentwerte.
+
+
+ defmod test1 dev_proxy Rollo1 Rollo2
+ attr test1 mapValues {'*:position'=>{'*'=>'{100-$val}','down'=>'100', 'closed'=>'100', 'up'=>'0', 'open'=>'0', 'open_ack'=>'0', 'off'=>'0', 'on'=>'100'}}
+ attr test1 setList opens:noArg closes:noArg stop:noArg up:noArg down:noArg position:slider,0,1,100
+ attr test1 webCmd opens:closes:stop:position
+
+
+ -
+ Abbildung für ein Rollladen, Umbenennung der Original-Reading 'position' in 'pos', Umkehrung der Prozentwerte.
+
+
+ defmod test2 dev_proxy Rollo1
+ attr test2 observed_readings pos state
+ attr test2 mapValues {'*:pos'=>{'*'=>'{100-$val}','down'=>'100', 'closed'=>'100', 'up'=>'0', 'open'=>'0', 'open_ack'=>'0', 'off'=>'0', 'on'=>'100'}}
+ attr test2 setList opens:noArg closes:noArg stop:noArg up:noArg down:noArg pos:slider,0,1,100
+ attr test2 mapReadings *:position:pos
+ attr test2 webCmd up:down:stop:pos
+
+
+
+
+
+=end html_DE
+=cut