############################################################################### # $Id$ # Maintained by igami since 02-2018 # # TODO # - document how to include powerMap for other module maintainers # (see 50_HP1000) # package main; use strict; use warnings; use Data::Dumper; use Unit; # module hashes ############################################################### my %powerMap_tmpl = ( # Format example for devices w/ model support: # # '' => { # '(|)' => { # '' => { # # # This is the actual powerMap definition # '' => { # '' => '', # }, # }, # }, # # # # This is w/ user attributes # # '(|)' => { # '' => { # 'attribute1' => 'value1', # 'attribute2' => 'value2', # # # This is the actual powerMap definition # 'map' => { # '' => { # '' => '', # }, # }, # }, # }, # }, # Format example for devices w/o model support: # # '' => { # # # This is the actual powerMap definition # '' => { # '' => '', # }, # }, # Format example for mapping table and user attributes: # # '' => { # 'attribute1' => 'value1', # 'attribute2' => 'value2', # # # This is the actual powerMap definition # 'map' => { # '' => { # '' => '', # }, # }, # }, # TYPE alias to mirror values # # '' => '', # FS20 => { state => { 0 => 0.5, 100 => 60, }, }, HMCCU => { state => { '*' => 7.5, }, }, HMCCUCHN => "HMCCUDEV", # alias / forward to other TYPE HMCCUDEV => { ccutype => { 'HM-LC-Dim1TPBU-FM' => { hmstate => { unreachable => 0, working => 101, up => 101, down => 101, 0 => 1.0, 100 => 101, }, }, 'HM-LC-Dim1T-FM' => { hmstate => { unreachable => 0, working => 23.5, up => 23.5, down => 23.5, 0 => 1.0, 100 => 23.5, }, }, 'HM-LC-Sw2-PB-FM' => { hmstate => { unreachable => 0, off => 0.25, on => 100.25, }, }, 'HM-LC-Bl1PBU-FM' => { hmstate => { unreachable => 0, working => 121, up => 121, down => 121, '*' => 0.5, }, }, 'HM-LC-Bl1-SM' => { hmstate => { unreachable => 0, working => 121, up => 121, down => 121, '*' => 0.4, }, }, }, }, HUEBridge => { model => 'modelid', modelid => { BSB001 => { rname_E => 'energy', rname_P => 'consumption', map => { state => { 0 => 0, '*' => 1.669, }, }, }, BSB002 => { rname_E => 'energy', rname_P => 'consumption', map => { state => { 0 => 0, '*' => 1.669, }, }, }, }, }, HUEDevice => { model => 'modelid', modelid => { # Hue Bulb LCT001 => { rname_E => 'energy', rname_P => 'consumption', map => { pct => { 0 => 0.4, 100 => 8.5, }, state => { unreachable => 0, '*' => 'pct', }, }, }, # Hue Spot BR30 LCT002 => {}, # Hue Spot GU10 LCT003 => {}, # Hue Bulb V2 LCT007 => { rname_E => 'energy', rname_P => 'consumption', map => { pct => { 0 => 0.4, 100 => 10, }, state => { unreachable => 0, '*' => 'pct', }, }, }, # Hue Bulb V3 LCT010 => { rname_E => 'energy', rname_P => 'consumption', map => { pct => { 0 => 0.4, 100 => 8.5, }, state => { unreachable => 0, '*' => 'pct', }, }, }, # Hue BR30 LCT011 => {}, # Hue Bulb V3 LCT014 => {}, # Living Colors G2 LLC001 => {}, # Living Colors Bloom LLC005 => {}, # Living Colors Gen3 Iris LLC006 => {}, # Living Colors Gen3 Bloom LLC007 => {}, # Living Colors Iris LLC010 => {}, # Living Colors Bloom LLC011 => {}, # Living Colors Bloom LLC012 => {}, # Disney Living Colors LLC013 => {}, # Living Colors Aura LLC014 => {}, # Hue Go LLC020 => {}, # Hue LightStrip LST001 => { rname_E => 'energy', rname_P => 'consumption', map => { pct => { 0 => 0.4, 100 => 12, }, state => { unreachable => 0, '*' => 'pct', }, }, }, # Hue LightStrip Plus LST002 => { rname_E => 'energy', rname_P => 'consumption', map => { pct => { 0 => 0.4, 100 => 20.5, }, state => { unreachable => 0, '*' => 'pct', }, }, }, # Living Whites Bulb LWB001 => { rname_E => 'energy', rname_P => 'consumption', map => { pct => { 0 => 0.4, 10 => 1.2, 20 => 1.7, 30 => 1.9, 40 => 2.3, 50 => 2.7, 60 => 3.4, 70 => 4.7, 80 => 5.9, 90 => 7.5, 100 => 9.2, }, state => { unreachable => 0, '*' => 'pct', }, }, }, # Living Whites Bulb LWB003 => { rname_E => 'energy', rname_P => 'consumption', map => { pct => { 0 => 0.4, 10 => 1.2, 20 => 1.7, 30 => 1.9, 40 => 2.3, 50 => 2.7, 60 => 3.4, 70 => 4.7, 80 => 5.9, 90 => 7.5, 100 => 9.2, }, state => { unreachable => 0, '*' => 'pct', }, }, }, # Hue Lux LWB004 => {}, # Hue Lux LWB006 => {}, # Hue Lux LWB007 => {}, # Hue A19 White Ambience LTW001 => {}, # Hue A19 White Ambience LTW004 => {}, # Hue GU10 White Ambience LTW013 => {}, # Hue GU10 White Ambience LTW014 => {}, # Color Light Module LLM001 => {}, # Color Temperature Module LLM010 => {}, # Color Temperature Module LLM011 => {}, # Color Temperature Module LLM012 => {}, # LivingWhites Outlet LWL001 => {}, # Hue Dimmer Switch RWL020 => {}, # Hue Dimmer Switch RWL021 => {}, # Hue Tap ZGPSWITCH => {}, # dresden elektronik FLS-H lp 'FLS-H3' => {}, # dresden elektronik FLS-PP lp 'FLS-PP3' => {}, # LIGHTIFY Flex RGBW 'Flex RGBW' => {}, # LIGHTIFY Classic A60 RGBW 'Classic A60 RGBW' => {}, # LIGHTIFY Gardenspot Mini RGB 'Gardenspot RGB' => {}, # LIGHTIFY Surface light tunable white 'Surface Light TW' => {}, # LIGHTIFY Classic A60 tunable white 'Classic A60 TW' => {}, # LIGHTIFY Classic B40 tunable white 'Classic B40 TW' => {}, # LIGHTIFY PAR16 50 tunable white 'PAR16 50 TW' => {}, # LIGHTIFY Plug 'Plug - LIGHTIFY' => {}, # LIGHTIFY Plug 'Plug 01' => {}, # Busch-Jaeger ZigBee Light Link Relais 'RM01' => {}, # Busch-Jaeger ZigBee Light Link Dimmer 'DM01' => {}, }, }, netatmo => { model => { NAMain => { temperature => { '*' => 5, }, }, }, }, Panstamp => { 'Pumpe_Heizkreis' => { 'off' => "0,Pumpe_Boiler,Brenner", 'on' => "30,Pumpe_Boiler,Brenner", }, 'Pumpe_Boiler' => { 'off' => "0,Pumpe_Heizkreis,Brenner", 'on' => "30,Pumpe_Heizkreis,Brenner", }, 'Brenner' => { 'off' => "0,Pumpe_Heizkreis,Pumpe_Boiler", 'on' => "40,Pumpe_Heizkreis,Pumpe_Boiler", }, }, SONOSPLAYER => { model => { Sonos_S6 => { stateAV => { disappeared => 0, off => 2.2, mute => 2.2, pause => 2.2, on => 14.5, }, }, Sonos_S5 => { stateAV => { disappeared => 0, off => 8.3, mute => 8.3, pause => 8.3, on => 14.5, }, }, Sonos_S3 => { stateAV => { disappeared => 0, off => 4.4, mute => 4.4, pause => 4.4, on => 11.3, }, }, Sonos_S1 => { stateAV => { disappeared => 0, off => 3.8, mute => 3.8, pause => 3.8, on => 5.2, }, }, }, }, ); # initialize ################################################################## sub powerMap_Initialize($) { my ($hash) = @_; my $TYPE = "powerMap"; $hash->{DefFn} = $TYPE . "_Define"; $hash->{UndefFn} = $TYPE . "_Undefine"; $hash->{SetFn} = $TYPE . "_Set"; $hash->{GetFn} = $TYPE . "_Get"; $hash->{AttrFn} = $TYPE . "_Attr"; $hash->{NotifyFn} = $TYPE . "_Notify"; $hash->{AttrList} = "disable:1,0 disabledForIntervals do_not_notify:1,0 " . $TYPE . "_gridV:230,110 " . $TYPE . "_eventChainWarnOnly:1,0 " . $readingFnAttributes; addToAttrList( $TYPE . "_noEnergy:1,0", 'powerMap' ); addToAttrList( $TYPE . "_noPower:1,0" , 'powerMap' ); addToAttrList( $TYPE . "_interval" , 'powerMap' ); addToAttrList( $TYPE . "_rname_P:textField" , 'powerMap' ); addToAttrList( $TYPE . "_rname_E:textField" , 'powerMap' ); addToAttrList( $TYPE . ":textField-long" , 'powerMap' ); $hash->{NotifyOrderPrefix} = '30-'; } # regular Fn ################################################################## sub powerMap_Define($$) { my ( $hash, $def ) = @_; my ( $name, $type, $rest ) = split( /[\s]+/, $def, 3 ); my $TYPE = $hash->{TYPE}; my $d = $modules{$TYPE}{defptr}; return "Usage: define $TYPE" if ($rest); return "$TYPE device already defined as $d->{NAME}" if ( defined($d) ); my $interval = AttrVal( $name, $TYPE . "_interval", 900 ); $interval = 900 unless ( looks_like_number($interval) ); $interval = 30 if ( $interval < 30 ); $modules{$TYPE}{defptr} = $hash; $hash->{INTERVAL} = $interval; $hash->{STATE} = "Initialized"; return; } sub powerMap_Undefine($$) { my ( $hash, $arg ) = @_; my $name = $hash->{NAME}; my $TYPE = $hash->{TYPE}; delete $modules{$TYPE}{defptr}; # terminate powerMap for each device foreach ( devspec2array("i:pM_update=.+") ) { RemoveInternalTimer("$name|$_"); delete $defs{$_}{pM_update}; delete $defs{$_}{pM_interval}; } return; } sub powerMap_Set($@) { my ( $hash, @a ) = @_; return "Missing argument" if ( @a < 2 ); my $TYPE = $hash->{TYPE}; my $name = shift @a; my $argument = shift @a; my $value = join( " ", @a ) if (@a); my $assign; my $maps = powerMap_findPowerMaps($name); foreach ( sort keys %{$maps} ) { $assign .= "," if ($assign); $assign .= $_; } my %powerMap_sets = ( "assign" => "assign:$assign", ); return "Unknown argument $argument, choose one of " . join( " ", values %powerMap_sets ) unless ( exists( $powerMap_sets{$argument} ) ); my $ret; if ( $argument eq "assign" ) { my @devices = devspec2array($value); return "No matching device found." unless (@devices); foreach my $d (@devices) { next unless ( ref( $maps->{$d}{map} ) eq "HASH" && keys %{ $maps->{$d}{map} } ); # write attributes $Data::Dumper::Terse = 1; $Data::Dumper::Deepcopy = 1; $Data::Dumper::Sortkeys = 1; foreach ( sort keys %{ $maps->{$d} } ) { my $n = $_; $n = $TYPE if ( $_ eq "map" ); $n = $TYPE . "_" . $_ unless ( $n =~ /^$TYPE/ ); my $txt = $maps->{$d}{$_}; $txt = Dumper( $maps->{$d}{$_} ) if ( $_ eq "map" ); $ret .= CommandAttr( undef, "$d $n $txt" ); $ret .= "$d - Added attribute $n\n" if ( @devices > 1 ); } $Data::Dumper::Terse = 0; $Data::Dumper::Deepcopy = 0; $Data::Dumper::Sortkeys = 0; } } return $ret; } sub powerMap_Get($@) { my ( $hash, @a ) = @_; return "Missing argument" if ( @a < 2 ); my $TYPE = $hash->{TYPE}; my $name = shift @a; my $argument = shift @a; my $value = join( " ", @a ) if (@a); my %powerMap_gets = ( "devices" => "devices:noArg", ); return "Unknown argument $argument, choose one of " . join( " ", values %powerMap_gets ) unless ( exists( $powerMap_gets{$argument} ) ); my $ret; if ( $argument eq "devices" ) { my $pmdevs = powerMap_findPowerMaps( $name, ":PM_ENABLED" ); return keys %{$pmdevs} ? join( "\n", sort keys %{$pmdevs} ) : "No powerMap enabled devices found."; } return $ret; } sub powerMap_Attr(@) { my ( $cmd, $name, $attribute, $value ) = @_; my $hash = $defs{$name}; my $TYPE = $hash->{TYPE}; if ( $attribute eq "disable" ) { readingsSingleUpdate( $hash, "state", "disabled", 1 ) if ( $value and $value == 1 ); readingsSingleUpdate( $hash, "state", "enabled", 1 ) if ( $cmd eq "del" or !$value ); } return if ( IsDisabled($name) ); if ( $attribute eq $TYPE . "_interval" ) { my $interval = $cmd eq "set" ? $value : 900; $interval = 900 unless ( looks_like_number($interval) ); $interval = 30 if ( $interval < 30 ); $hash->{INTERVAL} = $interval; } return; } sub powerMap_Notify($$) { my ( $hash, $dev_hash ) = @_; my $name = $hash->{NAME}; my $dev = $dev_hash->{NAME}; my $TYPE = $hash->{TYPE}; return if ( !$init_done or IsDisabled($name) or IsDisabled($dev) or $name eq $dev # do not process own events or powerMap_AttrVal( $name, $dev, "noPower", 0 ) or ( !$modules{ $defs{$dev}{TYPE} }{$TYPE} and !$defs{$dev}{$TYPE} and $dev ne "global" ) ); my $events = deviceEvents( $dev_hash, 1 ); return unless ($events); Log3 $name, 5, "$TYPE: Entering powerMap_Notify() for $dev"; # global events if ( $dev eq "global" ) { foreach my $event ( @{$events} ) { next unless ( defined($event) ); # initialize or terminate powerMap for each device if ( $event =~ /^(INITIALIZED|REREADCFG|SHUTDOWN)$/ ) { foreach ( keys %{ powerMap_findPowerMaps( $name, ":PM_$1" ) } ) { next if ( $_ eq "global" or $_ eq $name or powerMap_AttrVal( $name, $_, $TYPE . "_noEnergy", 0 ) ); powerMap_update("$name|$dev") if ( $1 eq "SHUTDOWN" ); next unless ( $1 eq "SHUTDOWN" || powerMap_load( $name, $_, undef, 1 ) ); Log3 $name, 4, "$TYPE: $1 for $_"; } } # device attribute deleted elsif ( $event =~ m/^(DELETEATTR)\s(.*)\s($TYPE)(\s+(.*))?/ ) { powerMap_unload( $name, $2 ); } # device attribute changed elsif ( $event =~ m/^(ATTR|DELETEATTR)\s(.*)\s($TYPE[a-zA-Z_]*)(\s+(.*))?/ ) { next unless ( powerMap_load( $name, $2 ) ); Log3 $name, 4, "$TYPE: UPDATED for $2"; } # device was deleted elsif ( $event =~ m/^(DELETED)\s(.*)/ ) { powerMap_unload( $name, $2 ); } # device was newly defined, modified or renamed elsif ( $event =~ m/^(DEFINED|MODIFIED|RENAMED)\s(.*)/ ) { next unless ( powerMap_load( $name, $2 ) ); Log3 $name, 4, "$TYPE: INITIALIZED for $2"; } } return; } my $rname_e = powerMap_AttrVal( $name, $dev, "rname_E", "pM_energy" ); my $rname_p = powerMap_AttrVal( $name, $dev, "rname_P", "pM_consumption" ); my $powerRecalcDone; # foreign device events foreach my $event ( @{$events} ) { next if (!$event or $event =~ /^($rname_e|$rname_p): / or $event !~ /: / ); # only recalculate once no matter # how many events we get at once unless ($powerRecalcDone) { my $power = powerMap_power( $name, $dev, $event ); if ( defined($power) ) { $powerRecalcDone = 1; powerMap_update( "$name|$dev", $power ); # recalculate CHANGEDWITHSTATE # for target device in deviceEvents() $dev_hash->{CHANGEDWITHSTATE} = []; last; } } } readingsSingleUpdate( $hash, "state", "Last device: $dev", 1 ) if ($powerRecalcDone); return undef; } # module Fn #################################################################### sub powerMap_AttrVal($$$$) { my ( $p, $d, $n, $default ) = @_; my $TYPE = $defs{$p}{TYPE}; Log3 $p, 6, "$TYPE: Entering powerMap_AttrVal() for $d"; return $default if ( !$TYPE ); # device attribute # my $da = AttrVal( $d, $TYPE . "_" . $n, AttrVal( $d, $n, undef ) ); return $da if ( defined($da) ); # device INTERNAL # # $defs{device}{TYPE}{attribute} return $defs{$d}{$TYPE}{$n} if ( $d && IsDevice($d) && defined( $defs{$d}{$TYPE} ) && defined( $defs{$d}{$TYPE}{$n} ) ); # $defs{device}{.TYPE}{attribute} return $defs{$d}{".$TYPE"}{$n} if ( $d && IsDevice($d) && defined( $defs{$d}{".$TYPE"} ) && defined( $defs{$d}{".$TYPE"}{$n} ) ); # $defs{device}{TYPE_attribute} return $defs{$d}{ $TYPE . "_" . $n } if ( $d && IsDevice($d) && defined( $defs{$d}{ $TYPE . "_" . $n } ) ); # $defs{device}{attribute} return $defs{$d}{$n} if ( $d && IsDevice($d) && defined( $defs{$d}{$n} ) ); # $defs{device}{.TYPE_attribute} return $defs{$d}{ "." . $TYPE . "_" . $n } if ( $d && IsDevice($d) && defined( $defs{$d}{ "." . $TYPE . "_" . $n } ) ); # $defs{device}{.attribute} return $defs{$d}{".$n"} if ( $d && IsDevice($d) && defined( $defs{$d}{".$n"} ) ); # module HASH # my $t = GetType($d); # $modules{module}{TYPE}{attribute} return $modules{$t}{$TYPE}{$n} if ( $t && defined( $modules{$t} ) && defined( $modules{$t}{$TYPE} ) && defined( $modules{$t}{$TYPE}{$n} ) ); # $modules{module}{TYPE}{TYPE_attribute} return $modules{$t}{$TYPE}{ $TYPE . "_" . $n } if ( $t && defined( $modules{$t} ) && defined( $modules{$t}{$TYPE} ) && defined( $modules{$t}{$TYPE}{ $TYPE . "_" . $n } ) ); # module attribute # return AttrVal( $p, $TYPE . "_" . $n, AttrVal( $p, $n, $default ) ); } sub powerMap_load($$;$$) { my ( $name, $dev, $unload, $modSupport ) = @_; my $dev_hash = $defs{$dev}; my $TYPE = $defs{$name}{TYPE}; Log3 $name, 5, "$TYPE: Entering powerMap_load() for $dev"; unless ($dev_hash) { RemoveInternalTimer("$name|$dev"); delete $dev_hash->{pM_update} if ( defined( $dev_hash->{pM_update} ) ); delete $dev_hash->{pM_interval} if ( defined( $dev_hash->{pM_interval} ) ); return; } my $powerMap = $unload ? undef : AttrVal( $dev, $TYPE, undef ); my $rname_e = powerMap_AttrVal( $name, $dev, "rname_E", "pM_energy" ); my $rname_p = powerMap_AttrVal( $name, $dev, "rname_P", "pM_consumption" ); # Support for Unit.pm $dev_hash->{readingsDesc}{$rname_e} = { rtype => 'whr', }; $dev_hash->{readingsDesc}{$rname_p} = { rtype => 'w', }; # Enable Unit.pm for DbLog if ( $modules{ $dev_hash->{TYPE} }{DbLog_splitFn} or $dev_hash->{DbLog_splitFn} or $dev_hash->{'.DbLog_splitFn'} ) { Log3 $name, 5, "$TYPE: $dev has defined it's own DbLog_splitFn; " . "won't enable unit support with DbLog but rather " . "let this to the module itself"; } else { Log3 $name, 4, "$TYPE: Enabled unit support for $dev"; $dev_hash->{'.DbLog_splitFn'} = "Unit_DbLog_split"; } # restore original powerMap from module if ( defined( $dev_hash->{$TYPE}{map} ) and defined( $dev_hash->{$TYPE}{'map.module'} ) ) { Log3 $dev, 5, "$TYPE $dev: Updated device hash with module mapping table"; delete $dev_hash->{$TYPE}{map}; $dev_hash->{$TYPE}{map} = $dev_hash->{$TYPE}{'map.module'}; delete $dev_hash->{$TYPE}{'map.module'}; } # delete device specific map elsif ( $unload && defined( $dev_hash->{$TYPE}{map} ) ) { delete $dev_hash->{$TYPE}{map}; } unless ($powerMap) { return powerMap_update("$name|$dev") if ($modSupport); RemoveInternalTimer("$name|$dev"); delete $dev_hash->{pM_update} if ( defined( $dev_hash->{pM_update} ) ); delete $dev_hash->{pM_interval} if ( defined( $dev_hash->{pM_interval} ) ); return; } if ( $powerMap =~ m/=>/ and $powerMap !~ m/\$/ ) { $powerMap = "{" . $powerMap . "}" if ( $powerMap !~ m/^{.*}$/s ); my $map = eval $powerMap; if ($@) { Log3 $dev, 3, "$TYPE $dev: Unable to evaluate attribute $TYPE: " . $@; } elsif ( ref($map) ne "HASH" ) { Log3 $dev, 3, "$TYPE $dev: Attribute $TYPE was not defined in HASH format"; } else { # backup any pre-existing definitions from module if ( defined( $dev_hash->{$TYPE}{map} ) ) { Log3 $dev, 4, "$TYPE $dev: Updated device hash with user mapping table"; $dev_hash->{$TYPE}{'map.module'} = $dev_hash->{$TYPE}{map}; delete $dev_hash->{$TYPE}{map}; } else { Log3 $dev, 4, "$TYPE $dev: Updated device hash with mapping table"; } $dev_hash->{$TYPE}{map} = $map; powerMap_verifyEventChain( $name, $dev, $map ); return powerMap_update("$name|$dev"); } } else { Log3 $dev, 3, "$TYPE $dev: Illegal format for attribute $TYPE"; } return 0; } sub powerMap_unload($$) { my ( $n, $d ) = @_; return powerMap_load( $n, $d, 1 ); } sub powerMap_findPowerMaps($;$) { my ( $name, $dev ) = @_; my %maps; # directly return any existing device specific definition if ( $dev && $dev !~ /^:/ ) { return {} unless ( IsDevice($dev) ); return $defs{$dev}{powerMap}{map} if ( $defs{$dev}{powerMap}{map} && ref( $defs{$dev}{powerMap}{map} ) eq "HASH" && keys %{ $defs{$dev}{powerMap}{map} } && powerMap_verifyEventChain( $name, $dev, $defs{$dev}{powerMap}{map} ) ); } # get all devices with direct powerMap definitions else { foreach ( devspec2array("i:powerMap=.+") ) { $maps{$_}{map} = $defs{$_}{powerMap}{map} if ( $defs{$_}{powerMap}{map} && ref( $defs{$_}{powerMap}{map} ) eq "HASH" && keys %{ $defs{$_}{powerMap}{map} } ); } # during initialization, also find devices where we # need to load their custom attribute into the hash if ( $dev && $dev eq ":PM_INITIALIZED" ) { foreach ( devspec2array("a:powerMap=.+") ) { $maps{$_}{map} = {} if ( !$maps{$_}{map} ); } } } # search templates from modules foreach my $TYPE ( $dev && $dev !~ /^:/ ? $defs{$dev}{TYPE} : keys %modules ) { next unless ( $modules{$TYPE}{powerMap} && keys %{ $modules{$TYPE}{powerMap} } ); my $t = $modules{$TYPE}{powerMap}; my $modelSupport = 0; # modules w/ model support unless ( $t->{map} ) { foreach my $ta ( keys %{$t} ) { my $a = $t->{$ta}; $a = $t->{ $t->{$ta} } if ( !ref( $t->{$ta} ) && $t->{ $t->{$ta} } ); next unless ( ref($a) eq "HASH" && !$a->{map} ); foreach my $tm ( keys %{$a} ) { my $m = $a->{$tm}; $m = $a->{ $a->{$tm} } if ( !ref( $a->{$tm} ) && $a->{ $a->{$tm} } ); next unless ( ref($m) eq "HASH" ); $modelSupport = 1; foreach ( devspec2array("TYPE=$TYPE:FILTER=$ta=$tm") ) { next if ( $maps{$_} ); if ( $m->{map} ) { next unless ( keys %{ $m->{map} } ); $maps{$_} = $m; } else { next unless ( keys %{$m} ); $maps{$_}{map} = $m; } } } } } # modules w/o model support unless ($modelSupport) { foreach ( devspec2array("TYPE=$TYPE") ) { next if ( $maps{$_} ); if ( $t->{map} ) { next unless ( keys %{ $t->{map} } ); $maps{$_} = $t; } else { next unless ( keys %{$t} ); $maps{$_}{map} = $t; } } } } # find possible template for each Fhem device unless ($dev) { foreach my $TYPE ( keys %powerMap_tmpl ) { next unless ( $modules{$TYPE} ); my $t = $powerMap_tmpl{$TYPE}; $t = $powerMap_tmpl{ $powerMap_tmpl{$TYPE} } if ( !ref( $powerMap_tmpl{$TYPE} ) && $powerMap_tmpl{ $powerMap_tmpl{$TYPE} } ); my $modelSupport = 0; # modules w/ model support foreach my $ta ( keys %{$t} ) { my $a = $t->{$ta}; $a = $t->{ $t->{$ta} } if ( !ref( $t->{$ta} ) && $t->{ $t->{$ta} } ); next unless ( ref($a) eq "HASH" ); foreach my $m ( keys %{$a} ) { next unless ( ref( $a->{$m} ) eq "HASH" && !$a->{map} ); $modelSupport = 1; foreach ( devspec2array("TYPE=$TYPE:FILTER=$ta=$m") ) { next if ( $maps{$_} ); if ( $a->{$m}{map} ) { next unless ( keys %{ $a->{$m}{map} } ); $maps{$_} = $a->{$m}; } else { next unless ( keys %{ $a->{$m} } ); $maps{$_}{map} = $a->{$m}; } } } } # modules w/o model support unless ($modelSupport) { foreach ( devspec2array("TYPE=$TYPE") ) { next if ( $maps{$_} ); if ( $t->{map} ) { next unless ( keys %{ $t->{map} } ); $maps{$_} = $t; } else { next unless ( keys %{$t} ); $maps{$_}{map} = $t; } } } } } foreach my $d ( keys %maps ) { # filter devices where no reading exists unless ( $dev && $dev eq ":PM_INITIALIZED" ) { if ( !$maps{$d}{map} || ref( $maps{$d}{map} ) ne "HASH" ) { delete $maps{$d}; next; } my $verified = 0; foreach ( keys %{ $maps{$d}{map} } ) { if ( ReadingsVal( $d, $_, undef ) ) { $verified = 1; last; } } delete $maps{$d} unless ($verified); } powerMap_verifyEventChain( $name, $d, $maps{$d}{map} ); } return {} if ( $dev && $dev !~ /^:/ && !defined( $maps{$dev} ) ); return $maps{$dev}{map} if ( $dev && $dev !~ /^:/ ); return \%maps; } sub powerMap_verifyEventChain($$$) { my ( $name, $dev, $map ) = @_; my $TYPE = $defs{$name}{TYPE}; my %filter; return 0 unless ( ref($map) eq "HASH" ); my $attrminint = AttrVal( $dev, "event-min-interval", undef ); if ($attrminint) { my @a = split( /,/, $attrminint ); $filter{attrminint} = \@a; } my $attraggr = AttrVal( $dev, "event-aggregator", undef ); if ($attraggr) { my @a = split( /,/, $attraggr ); $filter{attraggr} = \@a; } my $attreocr = AttrVal( $dev, "event-on-change-reading", undef ); if ($attreocr) { my @a = split( /,/, $attreocr ); $filter{attreocr} = \@a; } my $attreour = AttrVal( $dev, "event-on-update-reading", undef ); if ($attreour) { my @a = split( /,/, $attreour ); $filter{attreour} = \@a; } my $attrtocr = AttrVal( $dev, "timestamp-on-change-reading", undef ); if ($attrtocr) { my @a = split( /,/, $attrtocr ); $filter{attrtocr} = \@a; } return 1 unless ( keys %filter ); my $leocr = ""; foreach my $reading ( keys %{$map} ) { # verify reocr + reour if ( $filter{attreocr} || $filter{attreour} ) { my $eocr = $filter{attreocr} && ( my @eocrv = grep { my $l = $_; $l =~ s/:.*//; ( $reading =~ m/^$l$/ ) ? $_ : undef } @{ $filter{attreocr} } ); my $eour = $filter{attreour} && grep( $reading =~ m/^$_$/, @{ $filter{attreour} } ); unless ($eour) { if ( !$eocr || ( $eocrv[0] =~ m/.*:(.*)/ && ( !looks_like_number($1) || $1 > 0 ) ) ) { $leocr .= "," if ( $leocr ne "" ); $leocr .= $reading; } if ( $filter{attrtocr} && grep( $reading =~ m/^$_$/, @{ $filter{attrtocr} } ) ) { Log3 $dev, 2, "$TYPE $dev: WARNING - Attribute " . "timestamp-on-change-reading is not compatible " . "when using $TYPE with reading '$reading'"; } } } # verify min-interval my @v = grep { my $l = $_; $l =~ s/:.*//; ( $reading =~ m/^$l$/ ) ? $_ : undef } @{ $filter{attrminint} }; if (@v) { Log3 $dev, 2, "$TYPE $dev: WARNING - Attribute " . "event-min-interval is not compatible " . "when using $TYPE with reading '$reading'"; } # verify aggregator my @v2 = grep { my $l = $_; $l =~ s/:.*//; ( $reading =~ m/^$l$/ ) ? $_ : undef } @{ $filter{attraggr} }; if (@v2) { Log3 $dev, 2, "$TYPE $dev: WARNING - Attribute " . "event-aggregator is not compatible " . "when using $TYPE with reading '$reading'"; } } if ( $leocr ne "" ) { if ( powerMap_AttrVal( $name, $dev, "eventChainWarnOnly", 0 ) ) { Log3 $dev, 2, "$TYPE $dev: ERROR: Broken event chain - Attributes " . "event-on-change-reading or event-on-update-reading " . "need to contain reading(s) '$leocr'"; } else { Log3 $dev, 2, "$TYPE $dev: NOTE - Attribute " . "event-on-change-reading adjusted " . "to fulfill event chain for reading(s) '$leocr'"; $attreocr .= "," if ($attreocr); $attreocr .= $leocr; CommandAttr( undef, "$dev event-on-change-reading $attreocr" ); } } return 1; } sub powerMap_power($$$;$); sub powerMap_power($$$;$) { my ( $name, $dev, $event, $loop ) = @_; my $hash = $defs{$name}; my $TYPE = $hash->{TYPE}; my $power = 0; my $powerMap = powerMap_findPowerMaps( $name, $dev ); return unless ( defined($powerMap) and ref($powerMap) eq "HASH" ); if ( $event =~ /^([A-Za-z\d_\.\-\/]+):\s+(.*)$/ ) { my ( $reading, $val ) = ( $1, $2 ); my $num = ( $val =~ /(-?\d+(\.\d+)?)/ ? $1 : $val ); my $valueAliases = { initialized => '0', unavailable => '0', disappeared => '0', absent => '0', disabled => '0', disconnected => '0', off => '0', on => '100', connected => '100', enabled => '100', present => '100', appeared => '100', available => '100', }; $num = $valueAliases->{ lc($val) } if ( defined( $valueAliases->{ lc($val) } ) and looks_like_number( $valueAliases->{ lc($val) } ) ); # no power consumption defined for this reading return unless ( defined( $powerMap->{$reading} ) ); Log3 $name, 5, "$TYPE: Entering powerMap_power() for $dev:$reading"; Log3 $dev, 5, "$TYPE $dev: $reading: val=$val num=$num"; # direct assigned power consumption (value) if ( defined( $powerMap->{$reading}{$val} ) ) { $power = $powerMap->{$reading}{$val}; } # valueAliases mapping elsif ( defined( $valueAliases->{ lc($val) } ) and defined( $powerMap->{$reading}{ $valueAliases->{ lc($val) } } ) ) { $power = $powerMap->{$reading}{ $valueAliases->{ lc($val) } }; } # direct assigned power consumption (numeric) elsif ( defined( $powerMap->{$reading}{$num} ) ) { $power = $powerMap->{$reading}{$num}; } # value interpolation elsif ( looks_like_number($num) ) { my ( $val1, $val2 ); foreach ( sort { $a cmp $b } keys( %{ $powerMap->{$reading} } ) ) { next unless ( looks_like_number($_) ); $val1 = $_ if ( $_ < $num ); $val2 = $_ if ( $_ > $num ); last if ( defined($val2) ); } if ($val2) { Log3 $dev, 5, "$TYPE $dev: $reading: Interpolating power value " . "between $val1 and $val2"; my $y1 = $powerMap->{$reading}{$val1}; $y1 =~ s/^([-\.\d]+)(.*)/$1/g; my $y1t = $2; $y1 = 0 unless ( looks_like_number($y1) ); my $y2 = $powerMap->{$reading}{$val2}; $y2 =~ s/^([-\.\d]+)(.*)/$1/g; my $y2t = $2; $y2 = 0 unless ( looks_like_number($y2) ); my $m = ( ($y2) - ($y1) ) / ( ($val2) - ($val1) ); my $b = ( ( ($val2) * ($y1) ) - ( ($val1) * ($y2) ) ) / ( ($val2) - ($val1) ); my $powerFormat = powerMap_AttrVal( $name, $dev, "format_P", undef ); if ($powerFormat) { $power = sprintf( $powerFormat, ( ($m) * ($num) ) + ($b) ); } else { $power = ( ($m) * ($num) ) + ($b); } if ( !$loop && $power - $y1 < $y2 - $power ) { $power .= $y1t; } elsif ( !$loop ) { $power .= $y2t; } } elsif ( defined( $powerMap->{$reading}{'*'} ) ) { $power = $powerMap->{$reading}{'*'}; } else { Log3 $dev, 3, "$TYPE $dev: Power value interpolation failed"; } } elsif ( defined( $powerMap->{$reading}{'*'} ) ) { $power = $powerMap->{$reading}{'*'}; } # consider additional readings if desired unless ( looks_like_number($power) ) { my $sum = 0; my $rlist = join( ",", keys %{$powerMap} ); $power =~ s/\*/$rlist/; foreach ( split( ",", $power ) ) { next if ( $reading eq $_ ); if ( looks_like_number($_) ) { $sum += $_; last if ($loop); } elsif ( defined( $powerMap->{$_} ) && !$loop ) { Log3 $dev, 5, "$TYPE $dev: $_: Adding to total"; my $ret = powerMap_power( $name, $dev, "$_: " . ReadingsVal( $dev, $_, "" ), 1 ); $sum += $ret if ( looks_like_number($ret) ); } } $power = $sum; } } return "?" unless ( looks_like_number($power) ); return $power; } sub powerMap_energy($$;$) { my ( $name, $dev, $P1 ) = @_; my $hash = $defs{$name}; my $dev_hash = $defs{$dev}; my $TYPE = $hash->{TYPE}; my $rname_e = powerMap_AttrVal( $name, $dev, "rname_E", "pM_energy" ); my $rname_p = powerMap_AttrVal( $name, $dev, "rname_P", "pM_consumption" ); Log3 $name, 5, "$TYPE: Entering powerMap_energy() for $dev"; my $E0 = ReadingsVal( $dev, $rname_e, 0 ); my $P0 = ReadingsVal( $dev, $rname_p, 0 ); $P0 = 0 unless ( looks_like_number($P0) ); $P1 = $P0 unless ( defined($P1) ); $P1 = 0 unless ( looks_like_number($P1) ); my $Dt = ReadingsAge( $dev, $rname_e, 0 ) / 3600; my $DE = $P0 * $Dt; my $E1 = $E0 + $DE; Log3( $dev, 4, "$TYPE $dev: energy calculation results:\n" . " energyOld : $E0 Wh\n" . " powerOld : $P0 W\n" . " power : $P1 W\n" . " timeframe : $Dt h\n" . " energyDiff: $DE Wh\n" . " energy : $E1 Wh" ); return ( $E1, $P1 ); } sub powerMap_update($;$) { my ( $name, $dev ) = split( "\\|", shift ); my ($power) = @_; my $hash = $defs{$name}; my $dev_hash = $defs{$dev}; my $TYPE = $hash->{TYPE}; RemoveInternalTimer("$name|$dev"); delete $dev_hash->{pM_update} if ( defined( $dev_hash->{pM_update} ) ); delete $dev_hash->{pM_interval} if ( defined( $dev_hash->{pM_interval} ) ); return unless ( !IsDisabled($name) and defined($hash) and defined($dev_hash) ); Log3 $name, 5, "$TYPE: Entering powerMap_update() for $dev"; my $rname_e = powerMap_AttrVal( $name, $dev, "rname_E", "pM_energy" ); my $rname_p = powerMap_AttrVal( $name, $dev, "rname_P", "pM_consumption" ); readingsBeginUpdate($dev_hash); unless ( powerMap_AttrVal( $name, $dev, "noEnergy", 0 ) ) { my ( $energy, $P1 ) = powerMap_energy( $name, $dev, $power ); readingsBulkUpdate( $dev_hash, $rname_e . "_begin", time() ) unless ( ReadingsVal( $dev, $rname_e, undef ) ); readingsBulkUpdate( $dev_hash, $rname_e, $energy ); if ($P1) { $dev_hash->{pM_interval} = powerMap_AttrVal( $name, $dev, $TYPE . "_interval", $hash->{INTERVAL} ); $dev_hash->{pM_interval} = 900 unless ( looks_like_number( $dev_hash->{pM_interval} ) ); $dev_hash->{pM_interval} = 30 if ( $dev_hash->{pM_interval} < 30 ); my $next = gettimeofday() + $dev_hash->{pM_interval}; $dev_hash->{pM_update} = FmtDateTime($next); Log3 $dev, 5, "$TYPE $dev: next update in " . $dev_hash->{pM_interval} . " s at " . $dev_hash->{pM_update}; InternalTimer( $next, "powerMap_update", "$name|$dev" ); } else { Log3 $dev, 5, "$TYPE $dev: no power consumption, update paused"; } } readingsBulkUpdate( $dev_hash, $rname_p, $power ) if ( defined($power) ); readingsEndUpdate( $dev_hash, 1 ); return 1; } 1; __END__ # commandref ################################################################## =pod =encoding utf8 =item helper =item summary maps power and calculates energy (as Readings) =item summary_DE leitet Leistung ab und berechnet Energie (als Readings) =begin html

powerMap

    powerMap will help to determine current power consumption and calculates energy consumption either when power changes or within regular interval.
    These new values may be used to collect energy consumption for devices w/o power meter (e.g. fridge, lighting or FHEM server) and for further processing using module ElectricityCalculator.
    Note: As this module will derive some additional readings when readings are changed (with event) in the supervised FHEM devices to prepare for further processing with other event handlers, this will happen on a rather early stage of the entire event processing. So do not expect powerMap to work properly when setting own values (e.g. using notify or DOIF in combination with "setreading" commands).

    Define

      define <name> powerMap
      You may only define one single instance of powerMap.

    Set

    • assign <devspec>
      Adds pre-defined powerMap attributes to one or more devices for further customization.

    Get

    • devices
      Lists all devices having set an attribute named 'powerMap'.

    Readings


      Device specific readings:
      • pM_energy
        A counter for consumed energy in Wh.
        Hint: In order to have the calculation working, attribute timestamp-on-change-reading may not be set for reading pM_energy!

      • pM_energy_begin
        Unix timestamp when collection started and device started to consume energy for the very first time.

      • pM_consumption
        Current power consumption of device in W.

    Attributes

    • disable 1
      No readings will be created or calculated by this module.

    • do_not_notify
    • powerMap_eventChainWarnOnly <1>
      When set, event chain will NOT be repaired automatically if readings were found to be required for powerMap but their events are currently suppressed because they are either missing from attributes event-on-change-reading or event-on-update-reading. Instead, manual intervention is required.

    • powerMap_interval <seconds>
      Interval in seconds to calculate energy.
      Default value is 900 seconds.

    • powerMap_noEnergy 1
      No energy consumption will be calculated for that device.

    • powerMap_noPower 1
      No power consumption will be determined for that device and consequently no energy consumption at all.

    • powerMap_rname_E
      Sets reading name for energy consumption.
      Default value is 'pM_energy'.

    • powerMap_rname_P
      Sets reading name for power consumption.
      Default value is 'pM_consumption'.

    • powerMap
              {
                '<reading>' => {
                  '<value>' => <power>,
                  '<value>' => <power>,
                   ...
                },
      
                '<reading>' {
                  '<value>' => <power>,
                  '<value>' => <power>,
                   ...
                },
      
                ...
              }
      (device specific)
      A Hash containing event(=reading) names and possible values of it. Each value can be assigned a corresponding power consumption.
      For devices with dimming capability intemediate values will be linearly interpolated. For this to work two separate numbers will be sufficient.

      Text values will automatically get any numbers extracted from it and be used for interpolation. (example: dim50% will automatically be interpreted as 50).
      In addition "off" and "on" will be translated to 0 and 100 respectively.
      If the value cannot be interpreted in any way, 0 power consumption will be assumed.
      Explicitly set definitions in powerMap attribute always get precedence.

      In case several power values need to be summarized, the name of other readings may be added after number value, separated by comma. The current status of that reading will then be considered for total power calculcation. To consider all readings powerMap knows, just add an *.

      Example for FS20 socket:
                  'state' => {
                    '0' => 0,
                    '100' => 60,
                  },
                  


      Example for HUE white light bulb:
                  'pct' => {
                    '0' => 0.4,
                    '10' => 1.2,
                    '20' => 1.7,
                    '30' => 1.9,
                    '40' => 2.3,
                    '50' => 2.7,
                    '60' => 3.4,
                    '70' => 4.7,
                    '80' => 5.9,
                    '90' => 7.5,
                    '100' => 9.2,
                  },
                  'state' => {
                    'unreachable' => 0,
                    '*' => 'pct',
                  },
                  

=end html =begin html_DE

powerMap

    powerMap ermittelt die aktuelle Leistungsaufnahme eines Geräts und berechnet den Energieverbrauch bei Änderung oder in einem regelmäßigen Intervall.
    Diese neuen Werte können genutzt werden, um den Stromverbrauch für Geräte ohne Zähler (z.B. Kühlschrank, Beleuchtung oder FHEM-Server) zu erfassen und mit dem Modul ElectricityCalculator weiter zu verarbeiten.
    Zur Beachtung: Da powerMap dazu gedacht ist, zusätzliche Readings zu erzeugen, greift es zu einem frühen Zeitpunkt in der gesamten Eventverarbeitung auf (triggernde) Änderungen zu. Dies ist v.a. dann zu beachten, wenn versucht werden sollte, eigene Readings oder Werte mit anderen Eventhandlern (z.B. notify oder DOIF, per "setreading" Kommando) zu setzen.

    Define

      define <name> powerMap
      Es kann immer nur eine powerMap Instanz definiert sein.

    Set
    • assign <devspec>
      Weist einem oder mehreren Geräten vordefinierte powerMap Attribute zu, um diese anschließend anpassen zu können.

    Get

    • devices
      Listet alle Geräte auf, die das Attribut 'powerMap' gesetzt haben.

    Readings


      Gerätespezifische Readings:
      • pM_energy
        Ein Zähler für die bisher bezogene Energie in Wh.
        Hinweis: Für eine korrekte Berechnung darf das Attribut timestamp-on-change-reading nicht für das Reading pM_energy gesetzt sein!

      • pM_energy_begin
        Unix Timestamp, an dem die Aufzeichnung begonnen wurde und das Gerät erstmalig Energie verbraucht hat.

      • pM_consumption
        Die aktuelle Leistungsaufnahme des Gerätes in W.

    Attribute

    • disable 1
      Es werden keine Readings mehr durch das Modul erzeugt oder berechnet.

    • do_not_notify
    • Sofern gesetzt, wird die Ereigniskette NICHT automatisch repariert, falls Readings zwar als für powerMap notwendig identifiziert wurden, ihre Events jedoch derzeit dadurch unterdrückt werden, weil sie nicht in einem der Attribute event-on-change-reading oder event-on-update-reading enthalten sind. Stattdessen ist ein manueller Eingriff erforderlich.

    • powerMap_interval <seconds>
      Intervall in Sekunden, in dem neue Werte für die Energie berechnet werden.
      Der Vorgabewert ist 900 Sekunden.

    • powerMap_noEnergy 1
      Für das Gerät wird kein Energieverbrauch berechnet.

    • powerMap_noPower 1
      Für das Gerät wird keine Leistungsaufnahme abgeleitet und daher auch kein Energieverbrauch berechnet.

    • powerMap_rname_E
      Definiert den Reading Namen, in dem der Zähler für die bisher bezogene Energie gespeichert wird.
      Der Vorgabewert ist 'pM_energy'.

    • powerMap_rname_P
      Definiert den Reading Namen, in dem die aktuelle Leistungsaufnahme des Gerätes gespeichert wird.
      Der Vorgabewert ist 'pM_consumption'.

    • powerMap
              {
                '<reading>' => {
                  '<value>' => <power>,
                  '<value>' => <power>,
                   ...
                },
      
                '<reading>' {
                  '<value>' => <power>,
                  '<value>' => <power>,
                   ...
                },
      
                ...
              }
      (gerätespezifisch)
      Ein Hash mit den Event(=Reading) Namen und seinen möglichen Werten, um diesen die dazugehörige Leistungsaufnahme zuzuordnen.
      Bei dimmbaren Geräten wird für die Zwischenschritte der Wert durch eine lineare Interpolation ermittelt, so dass mindestens zwei Zahlenwerte ausreichen.

      Aus Textwerten, die eine Zahl enthalten, wird automatisch die Zahl extrahiert und für die Interpolation verwendet (Beispiel: dim50% wird automatisch als 50 interpretiert).
      Außerdem werden "off" und "on" automatisch als 0 respektive 100 interpretiert.
      Nicht interpretierbare Werte führen dazu, dass eine Leistungsaufnahme von 0 angenommen wird.
      Explizit in powerMap enthaltene Definitionen haben immer vorrang.

      Für den Fall, dass mehrere Verbrauchswerte addiert werden sollen, kann der Name von anderen Readings direkt hinter dem eigentliche Wert mit einem Komma abgetrennt angegeben werden. Der aktuelle Status dieses Readings wird dann bei der Berechnung des Gesamtverbrauchs ebenfalls ber&uumL;cksichtigt. Sollen alle in powerMap bekannten Readings berücksichtigt werden, kann auch einfach ein * angegeben werden.

      Beispiel für einen FS20 Stecker:
                  'state' => {
                    '0' => 0,
                    '100' => 60,
                  },
                  


      Beispiel für eine HUE white Glühlampe:
                  'pct' => {
                    '0' => 0.4,
                    '10' => 1.2,
                    '20' => 1.7,
                    '30' => 1.9,
                    '40' => 2.3,
                    '50' => 2.7,
                    '60' => 3.4,
                    '70' => 4.7,
                    '80' => 5.9,
                    '90' => 7.5,
                    '100' => 9.2,
                  },
                  'state' => {
                    'unreachable' => 0,
                    '*' => 'pct',
                  },
                  

=end html_DE =cut