sub myLineChart {
my (
$name,
$value_01, $value_02, $value_03,
$label_01, $label_02, $label_03,
$unit_01, $unit_02, $unit_03,
$numValues_mobile,
$window_mobile,
$numValues_desktop,
$window_desktop
) = @_;
$label_01 //= "Currently";
$label_02 //= "Today";
$label_03 //= "Total out";
$unit_01 //= "W";
$unit_02 //= "Wh";
$unit_03 //= "kWh";
$numValues_mobile //= 40;
$window_mobile //= 3;
$numValues_desktop //= 120;
$window_desktop //= 3;
my @all_values = grep { defined && $_ ne '' } split /,/, ReadingsVal($name,'chartArray','');
# ---------------- MOBILE --------------------
my @v = @all_values[-$numValues_mobile..-1];
my @v_sma;
for my $i (0 .. $#v) {
my $start = $i - $window_mobile + 1;
$start = 0 if $start < 0;
my $count = $i - $start + 1;
my $sum = 0;
$sum += $v[$_] for $start .. $i;
push @v_sma, $sum / $count;
}
@v = @v_sma;
my ($vmin, $vmax) = (sort { $a <=> $b } @v)[0, -1];
$vmin //= 0; $vmax //= 0;
my $min = $vmin < 0 ? $vmin : 0;
my $max = $vmax > 0 ? $vmax : 0;
my $range = $max - $min || 1;
my $width = 155;
my $svg_total_height = 70;
my $padding_top = 2;
my $padding_bottom = 2;
my $height = $svg_total_height - $padding_top - $padding_bottom;
my $w = $width / (@v - 1 || 1);
my $zeroY = $padding_top + ($height - ((0 - $min) / $range * $height));
my @points = map {
my $x = $_ * $w;
my $y = $padding_top + ($height - (($v[$_] - $min) / $range * $height));
[$x, $y];
} 0 .. $#v;
my $path_d = "M" . join(" L", map { sprintf("%.2f,%.2f", @$_) } @points);
my $area_path_d = $path_d . sprintf(" L%.2f,%.2f L0,%.2f Z", $width, $zeroY, $zeroY);
my $gradient_def = '';
if ($vmin < 0 && $vmax > 0) {
$gradient_def = qq{
<linearGradient id="${name}_gradientFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.2" />
<stop offset="50%" stop-color="#3b82f6" stop-opacity="0" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.2" />
</linearGradient>
};
} elsif ($vmin >= 0) {
$gradient_def = qq{
<linearGradient id="${name}_gradientFill" x1="0" y1="1" x2="0" y2="0">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.6" />
</linearGradient>
};
} else {
$gradient_def = qq{
<linearGradient id="${name}_gradientFillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.6" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0" />
</linearGradient>
};
}
(my $gradient_def_d = $gradient_def) =~ s/_gradientFill/_gradientFillDesktop/g;
# ---------------- DESKTOP --------------------
my @v_d = @all_values[-$numValues_desktop..-1];
my @v_sma_d;
for my $i (0 .. $#v_d) {
my $start = $i - $window_desktop + 1;
$start = 0 if $start < 0;
my $count = $i - $start + 1;
my $sum = 0;
$sum += $v_d[$_] for $start .. $i;
push @v_sma_d, $sum / $count;
}
@v_d = @v_sma_d;
my ($vmin_d, $vmax_d) = (sort { $a <=> $b } @v_d)[0, -1];
$vmin_d //= 0; $vmax_d //= 0;
my $min_d = $vmin_d < 0 ? $vmin_d : 0;
my $max_d = $vmax_d > 0 ? $vmax_d : 0;
my $range_d = $max_d - $min_d || 1;
my $width_d = 360;
my $height_d = $svg_total_height - $padding_top - $padding_bottom;
my $w_d = $width_d / (@v_d - 1 || 1);
my $zeroY_d = $padding_top + ($height_d - ((0 - $min_d) / $range_d * $height_d));
my @points_d = map {
my $x = $_ * $w_d;
my $y = $padding_top + ($height_d - (($v_d[$_] - $min_d) / $range_d * $height_d));
[$x, $y];
} 0 .. $#v_d;
my $path_d_d = "M" . join(" L", map { sprintf("%.2f,%.2f", @$_) } @points_d);
my $area_path_d_d = $path_d_d . sprintf(" L%.2f,%.2f L0,%.2f Z", $width_d, $zeroY_d, $zeroY_d);
return qq{
<style>
div#$name-mobile { display:block; }
div#$name-desktop { display:none; }
div#$name { pointer-events:none; }
@{[ chr(64) ]}media screen and (min-width: 900px) {
div#$name-mobile { display:none !important; }
div#$name-desktop { display:block !important; }
}
</style>
<!-- Mobile -->
<div id="$name-mobile" style="width:365px; padding:10px; border:1px solid #ddd; border-radius:6px; font-family:sans-serif; box-shadow:0 2px 4px rgba(0,0,0,0.1); box-sizing:border-box; height:90px; position:relative;">
<div style="position:absolute; top:10px; left:10px; width:90px; font-size:14px; text-align:left; line-height:26px;">
<div>${label_01}</div><div>${label_02}</div><div>${label_03}</div>
</div>
<div style="position:absolute; top:10px; left:80px; width:120px; font-size:14px; font-weight:bold; text-align:center; line-height:26px; display:flex; flex-direction:column;">
<div>${value_01} ${unit_01}</div><div>${value_02} ${unit_02}</div><div>${value_03} ${unit_03}</div>
</div>
<div style="position:absolute; top:10px; right:10px; width:160px; height:70px; overflow:hidden;">
<svg style="width:160px!important; height:70px!important; display:block;" viewBox="0 0 160 70" preserveAspectRatio="xMidYMid meet">
<defs>$gradient_def</defs>
<path d="$area_path_d" fill="url(#${name}_gradientFill)" />
<line x1="0" y1="@{[sprintf '%.2f', $zeroY]}" x2="$width" y2="@{[sprintf '%.2f', $zeroY]}" stroke="white" stroke-dasharray="2,2" stroke-width="1" />
<path d="$path_d" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</div>
<!-- Desktop -->
<div id="$name-desktop" style="width:700px; padding:10px; border:1px solid #ddd; border-radius:6px; font-family:sans-serif; box-shadow:0 2px 4px rgba(0,0,0,0.1); box-sizing:border-box; height:90px; position:relative;">
<div style="position:absolute; top:10px; left:10px; width:100px; font-size:14px; text-align:left; line-height:26px;">
<div>${label_01}</div><div>${label_02}</div><div>${label_03}</div>
</div>
<div style="position:absolute; top:10px; left:90px; width:130px; font-size:14px; font-weight:bold; text-align:center; line-height:26px; display:flex; flex-direction:column;">
<div>${value_01} ${unit_01}</div><div>${value_02} ${unit_02}</div><div>${value_03} ${unit_03}</div>
</div>
<div style="position:absolute; top:10px; right:10px; width:${width_d}px; height:70px; overflow:hidden;">
<svg style="width:${width_d}px!important; height:70px!important; display:block;" viewBox="0 0 ${width_d} 70" preserveAspectRatio="xMidYMid meet">
<defs>$gradient_def_d</defs>
<path d="$area_path_d_d" fill="url(#${name}_gradientFillDesktop)" />
<line x1="0" y1="@{[sprintf '%.2f', $zeroY_d]}" x2="$width_d" y2="@{[sprintf '%.2f', $zeroY_d]}" stroke="white" stroke-dasharray="2,2" stroke-width="1" />
<path d="$path_d_d" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</div>
};
}
{
my $val1 = ReadingsVal($name, 'total.Power.v', '--');
my $val2 = ReadingsVal($name, 'total.YieldDay.v', '--');
my $val3 = ReadingsVal($name, 'total.YieldTotal.v', '--');
myLineChart(
$name,
$val1, $val2, $val3,
'Currently', 'Today', 'Total out',
'W', 'Wh', 'kWh',
40, 3, # mobile: 40 Werte, Average 3
120, 3 # desktop: 120 Werte, Average 3
);
}
package FHEM::EZHI;
my $cvsid = '$Id: 74_EZHI.pm 29928 2025-07-10 08:22:04Z $';
use strict;
use warnings;
use POSIX;
use GPUtils qw(:all);
use Time::HiRes qw(gettimeofday);
use Time::Local;
my $EMPTY = q{};
my $missingModul = $EMPTY;
## no critic (ProhibitConditionalUseStatements)
eval { use Readonly; 1 } or $missingModul .= 'Readonly ';
Readonly my $APIPORT => '80';
Readonly my $SPACE => q{ };
Readonly $EMPTY => q{};
Readonly my $MAIN_INTERVAL => 30;
Readonly my $LONG_INTERVAL => 600;
Readonly my $YEARSEC => 31536000;
eval { use JSON; 1 } or $missingModul .= 'JSON ';
## use critic
require HttpUtils;
BEGIN {
GP_Import(
qw(
AttrVal
FW_ME
SVG_FwFn
getKeyValue
InternalTimer
InternalVal
IsDisabled
Log3
Log
attr
defs
init_done
readingFnAttributes
readingsBeginUpdate
readingsBulkUpdate
readingsBulkUpdateIfChanged
readingsDelete
readingsEndUpdate
ReadingsNum
readingsSingleUpdate
ReadingsVal
RemoveInternalTimer
)
);
}
GP_Export(
qw(
Initialize
)
);
##############################################################
sub Initialize() {
my ($hash) = @_;
$hash->{DefFn} = \&Define;
$hash->{UndefFn} = \&Undefine;
$hash->{DeleteFn} = \&Delete;
$hash->{SetFn} = \&Set;
$hash->{FW_detailFn} = \&FW_detailFn;
$hash->{AttrFn} = \&Attr;
$hash->{AttrList} =
'disable:1,0 ' .
'disabledForIntervals ' .
'SVG_PlotsToShow:textField-long ' .
$::readingFnAttributes;
return;
}
#########################
sub Define{
my ( $hash, $def ) = @_;
my @val = split( "[ \t]+", $def );
my $name = $val[0];
my $type = $val[1];
my $iam = "$type $name Define:";
my $ip = '';
my $tod = gettimeofday();
return "$iam Cannot define $type device. Perl modul $missingModul is missing." if ( $missingModul );
return "$iam too few parameters: define <NAME> $type <email>" if( @val < 3 );
$ip = $val[2];
%$hash = (%$hash,
helper => {
cmds => {
getOutputData => {
lasttime => 0,
interval => $MAIN_INTERVAL * 10,
readings => {
batS => 'battery_Status',
batSoc => 'battery_state_of_charge',
pvP => 'pv_input_power',
batP => 'battery_power',
ogP => 'on-grid_power',
ofgP => 'off-grid_power'
},
},
getDeviceInfo => {
lasttime => 0,
interval => $YEARSEC,
readings => {
deviceId => 'inverter_Id',
type => 'inverter_type',
ssid => 'inverter_SSID',
ip => 'inverter_IpAddress'
},
},
getPower => {
lasttime => 0,
interval => $YEARSEC,
readings => {
Power => 'on-grid_power-setting'
},
},
setPower => {
lasttime => 0,
interval => $YEARSEC,
quantity => 'p',
readings => {
Power => 'on-grid_power-setting'
},
},
},
timeout_ => 2,
retry_count => 0,
retry_max => 6,
call_delay => 3,
callStack => [
'getOutputData',
'getDeviceInfo',
'getPower'
],
url => "http://${ip}:${APIPORT}/",
inverter => {
start => $tod,
stop => $tod,
duration => 0
}
}
);
# $attr{$name}{disabledForIntervals} = '{sunset_abs("HORIZON=0")}-24 00-{sunrise_abs("HORIZON=0")}' if( !defined( $attr{$name}{disabledForIntervals} ) );
$attr{$name}{stateFormat} = 'Power' if( !defined( $attr{$name}{stateFormat} ) );
$attr{$name}{room} = 'APsystemsEZHI' if( !defined( $attr{$name}{room} ) );
$attr{$name}{icon} = 'inverter' if( !defined( $attr{$name}{icon} ) );
( $hash->{VERSION} ) = $cvsid =~ /\.pm (.*)Z/;
readingsSingleUpdate( $hash, '.associatedWith', $attr{$name}{SVG_PlotsToShow}, 0 ) if ( defined $attr{$name}{SVG_PlotsToShow} );
RemoveInternalTimer($hash);
InternalTimer( gettimeofday() + 2, \&callAPI, $hash, 1);
readingsSingleUpdate( $hash, 'state', 'defined', 1 );
return;
}
#########################
sub FW_detailFn { ## no critic (ProhibitExcessComplexity [complexity core maintenance])
my ($FW_wname, $name, $room, $pageHash) = @_; # pageHash is set for summaryFn.
my $hash = $defs{$name};
my $type = $hash->{TYPE};
my $iam = "$type $name FW_detailFn:";
return if ( !AttrVal( $name, 'SVG_PlotsToShow', 0 ) || AttrVal( $name, 'disable', 0 ) || !$init_done || !$FW_ME );
my @plots = split( " ", AttrVal( $name, 'SVG_PlotsToShow', 0 ) );
my $ret = "<div id='${type}_${name}_plots' ><table>";
for my $plot ( @plots ) {
$ret .= '<tr><td>' . SVG_FwFn( $FW_wname, $plot, "", {} ) . '</td></tr>';
}
$ret .= '</table></div>';
return $ret;
}
#########################
sub callAPI {
my ( $hash, $update ) = @_;
my $name = $hash->{NAME};
my $type = $hash->{TYPE};
my $iam = "$type $name callAPI:";
my @states = ('undefined', 'disabled', 'temporarily disabled', 'inactive' );
my $tod = gettimeofday();
if( IsDisabled( $name ) ) {
readingsSingleUpdate( $hash, 'state', $states[ IsDisabled( $name ) ], 1 ) if ( ReadingsVal( $name, 'state', '' ) !~ /disabled|inactive/ );
RemoveInternalTimer( $hash );
InternalTimer( $tod + $MAIN_INTERVAL, \&callAPI, $hash, 0 );
return;
}
if ( scalar @{ $hash->{helper}{callStack} } == 0 ) {
my @cmds = qw( getDeviceInfo getPower getOutputData );
for ( @cmds ) {
push @{ $hash->{helper}{callStack} }, $_ if ( ( $hash->{helper}{cmds}{$_}{lasttime} + $hash->{helper}{cmds}{$_}{interval} ) < $tod );
}
if ( scalar @{ $hash->{helper}{callStack} } == 0 ) {
RemoveInternalTimer( $hash, \&callAPI );
InternalTimer( gettimeofday() + $MAIN_INTERVAL, \&callAPI, $hash, 0 );
return;
}
}
if ( !$update && $::init_done ) {
readingsSingleUpdate( $hash, 'state', 'initialized', 1 ) if ( $hash->{READINGS}{state}{VAL} !~ /initialized|connected/ );
my $url = $hash->{helper}{url};
my $timeout = $hash->{helper}{timeout_api};
my $command = $hash->{helper}{callStack}[0];
Log3 $name, 1, join(" | ", @{ $hash->{helper}{callStack} } ) . "\n$url$command";
::HttpUtils_NonblockingGet( {
url => $url . $command,
timeout => $timeout,
hash => $hash,
method => 'GET',
callback => \&APIresponse,
t_begin => $tod
} );
} else {
RemoveInternalTimer( $hash, \&callAPI );
InternalTimer( gettimeofday() + $MAIN_INTERVAL, \&callAPI, $hash, 0 );
}
return;
}
#########################
sub APIresponse {
my ($param, $err, $data) = @_;
my $hash = $param->{hash};
my $name = $hash->{NAME};
my $type = $hash->{TYPE};
my $statuscode = $param->{code} // '';
my $call_delay = $hash->{helper}{call_delay};
my $iam = "$type $name APIresponse:";
my $tod = gettimeofday();
my $duration = sprintf( "%.2f", ( $tod - $param->{t_begin} ) );
Log3 $name, 4, "$iam response time ". $duration . ' s';
Log3 $name, 4, "$iam \$statuscode >$statuscode< \$err >$err< \$param->url $param->{url}\n\$data >$data<\n";
if ( !$err && $statuscode == 200 && $data ) {
my $result = eval { decode_json( $data ) };
if ($@) {
Log3 $name, 2, "$iam JSON error [ $@ ]";
readingsSingleUpdate( $hash, 'state', 'error JSON', 1 );
} else {
if ( $hash->{READINGS}{state}{VAL} ne 'connected' ) {
$hash->{helper}{inverter}{start} = $tod;
readingsSingleUpdate( $hash, 'state', "connected", 1 );
}
$hash->{helper}{inverter}{duration} = $tod - $hash->{helper}{inverter}{start};
my $cmd = $hash->{helper}{callStack}[0];
$cmd = $1 if ( $cmd =~ /(.*)\?/ );
$hash->{helper}->{response}{$cmd} = $result;
if ( $result->{message} eq 'SUCCESS' ) {
$hash->{helper}{cmds}{$cmd}{lasttime} = $tod;
readingsBeginUpdate($hash);
for my $ky ( keys %{ $result->{data} } ) {
if ( "$cmd$ky" =~ /getOutputDatap(1|2)|Power/ ) {
readingsBulkUpdateIfChanged( $hash, $hash->{helper}{cmds}{$cmd}{readings}{$ky}, $result->{data}{$ky} );
} elsif ( "$cmd$ky" =~ /getOutputDatae(1|2)/ ) {
readingsBulkUpdateIfChanged( $hash, $hash->{helper}{cmds}{$cmd}{readings}{$ky}, sprintf( "%.1f", $result->{data}{$ky} ) );
} elsif ( "$cmd$ky" =~ /getOutputDatate(1|2)/ ) {
readingsBulkUpdate( $hash, $hash->{helper}{cmds}{$cmd}{readings}{$ky}, sprintf( "%.1f", $result->{data}{$ky} ), 0 );
} else {
readingsBulkUpdate( $hash, $hash->{helper}{cmds}{$cmd}{readings}{$ky}, $result->{data}{$ky}, 0 );
}
}
# if ( $hash->{helper}{callStack}[0] eq 'getOutputData' ) {
# readingsBulkUpdate( $hash, 'inverter_OnlineTime', int( $hash->{helper}{inverter}{duration} /3600 ) , 0 );
# readingsBulkUpdateIfChanged( $hash, 'Power', $result->{data}{p1} + $result->{data}{p2} );
# readingsBulkUpdateIfChanged( $hash, 'Energy', sprintf( "%.1f", $result->{data}{e1} + $result->{data}{e2} ) );
# readingsBulkUpdateIfChanged( $hash, 'LifeTimeEnergy', sprintf( "%.1f", $result->{data}{te1} + $result->{data}{te2} ) );
# readingsBulkUpdate( $hash, 'PowerDensity', int( ( $result->{data}{p1} + $result->{data}{p2} ) / AttrVal( $name, 'activeArea', 4.10592 ) ), 0 ) if ( AttrVal( $name, 'activeArea', 0 ) );
# }
readingsEndUpdate($hash, 1);
shift @{ $hash->{helper}{callStack} };
if ( scalar @{ $hash->{helper}{callStack} } ) {
RemoveInternalTimer( $hash, \&calAPI );
InternalTimer( $tod + $call_delay, \&callAPI, $hash, 0 );
return;
}
RemoveInternalTimer( $hash, \&calAPI );
InternalTimer( $tod + $MAIN_INTERVAL, \&callAPI, $hash, 0 );
return;
}
$hash->{helper}{retry_count}++;
if ( $hash->{helper}{retry_count} > $hash->{helper}{retry_max} - 1 ) {
shift @{ $hash->{helper}{callStack} };
$hash->{helper}{retry_count} = 0;
if ( scalar @{ $hash->{helper}{callStack} } ) {
RemoveInternalTimer( $hash, \&calAPI );
InternalTimer( $tod + $call_delay, \&callAPI, $hash, 0 );
return;
}
RemoveInternalTimer( $hash, \&calAPI );
InternalTimer( $tod + ( $result->{data}{batS} < 1 ? $LONG_INTERVAL : $MAIN_INTERVAL ), \&callAPI, $hash, 0 );
return;
}
}
} elsif ( !$statuscode && !$data && $err =~ /\(113\)$|timed out/ ) {
if ( $hash->{READINGS}{state}{VAL} ne 'disconnected' ) {
$hash->{helper}{inverter}{stop} = $tod;
$hash->{helper}{inverter}{duration} = $tod - $hash->{helper}{inverter}{start};
readingsSingleUpdate( $hash, 'state', "disconnected", 1 );
}
RemoveInternalTimer( $hash, \&callAPI );
InternalTimer( $tod + $LONG_INTERVAL, \&callAPI, $hash, 0 );
return;
}
readingsSingleUpdate( $hash, 'state', "error", 1 );
Log3 $name, 1, "$iam \$statuscode >$statuscode< \$err >$err< \$param->url $param->{url}\n\$data >$data<\n";
$hash->{helper}{retry_count}++;
if ( $hash->{helper}{retry_count} > $hash->{helper}{retry_max} ) {
CommandAttr( $hash, "$name disable 1" );
$hash->{helper}{retry_count} = 0;
}
RemoveInternalTimer( $hash, \&callAPI );
InternalTimer( $tod + $MAIN_INTERVAL, \&callAPI, $hash, 0 );
my $txt = AttrVal( $name, 'disable', $EMPTY ) ? "$iam: Device is disabled now." : "$iam failed, retry in $MAIN_INTERVAL seconds.";
Log3 $name, 1, $txt;
return;
}
#########################
sub Set {
my ($hash,@val) = @_;
my $type = $hash->{TYPE};
my $name = $hash->{NAME};
my $iam = "$type $name Set:";
return "$iam: needs at least one argument" if ( @val < 2 );
return "Unknown argument, $iam is disabled, choose one of none:noArg" if ( IsDisabled( $name ) );
my ($pname,$setName,$setVal,$setVal2,$setVal3) = @val;
Log3 $name, 4, "$iam called with $setName";
my $minpow = ReadingsNum( $name, 'minPower', -1200 );
my $maxpow = ReadingsNum( $name, 'maxPower', 1200 );
$setVal = 0 if ( defined( $setVal ) && $setVal eq 'on' );
$setVal = 1 if ( defined( $setVal ) && $setVal eq 'off' );
if ( $setName eq 'setOnOff' && ( $setVal == 0 || $setVal == 1 ) || $setName eq 'setPower' && $setVal >= $minpow && $setVal <= $maxpow ) {
my $cmd = $setName . '?' . $hash->{helper}{cmds}{$setName}{quantity} . '=' . $setVal;
unshift @{ $hash->{helper}{callStack} }, $cmd;
# Log3 $name, 1, "$iam called with $cmd | ".join(" | ", @{ $hash->{helper}{callStack} } );
return;
} elsif ( $setName eq 'getUpdate') {
my @cmds = qw( getDeviceInfo getPower getOutputData );
push @{ $hash->{helper}{callStack} }, @cmds;
return;
}
my $ret = ' setPower:selectnumbers,' . $minpow . ',10,' . $maxpow . ',0,lin setOnOff:on,off getUpdate:noArg ';
return "Unknown argument $setName, choose one of".$ret;
}
#########################
sub Undefine {
my ( $hash, $arg ) = @_;
my $name = $hash->{NAME};
my $type = $hash->{TYPE};
RemoveInternalTimer( $hash );
readingsSingleUpdate( $hash, 'state', 'undefined', 1 );
return;
}
##########################
sub Delete {
my ( $hash, $arg ) = @_;
my $name = $hash->{NAME};
my $type = $hash->{TYPE};
my $iam ="$type $name Delete: ";
Log3( $name, 5, "$iam called" );
return;
}
##########################
sub Attr {
my ( $cmd, $name, $attrName, $attrVal ) = @_;
my $hash = $defs{$name};
my $type = $hash->{TYPE};
my $iam = "$type $name Attr:";
##########
if ( $attrName eq 'disable' ) {
if( $cmd eq "set" and $attrVal eq "1" ) {
readingsSingleUpdate( $hash,'state','disabled',1);
Log3 $name, 3, "$iam $cmd $attrName disabled";
} elsif( $cmd eq "del" or $cmd eq 'set' and !$attrVal ) {
RemoveInternalTimer( $hash, \&callAPI);
InternalTimer( gettimeofday() + 1, \&callAPI, $hash, 0 );
Log3 $name, 3, "$iam $cmd $attrName enabled";
}
return;
##########
} elsif ( $attrName eq 'SVG_PlotsToShow' ) {
readingsSingleUpdate( $hash, '.associatedWith', $attrVal, 0 ) if ( $cmd eq 'set' && $attrVal );
delete $hash->{READINGS}{'.associatedWith'} if ( $cmd eq 'del' );
return;
##########
} elsif ( $attrName eq 'intervalMultiplier' ) {
if( $cmd eq 'set' ) {
my @ivals = split( /\R/, $attrVal );
for my $ival (@ivals) {
if ( my ($cm, $val) = $ival =~ /^(getDeviceInfo|getPower|getOutputData)=(\d+)$/ ) {
$hash->{helper}{cmds}{$cm}{interval} = $MAIN_INTERVAL * $val;
} else {
Log3 $name, 1, "$iam $cmd $attrName wrong syntax for $ival, default is used.";
}
}
} elsif( $cmd eq 'del' ) {
my @ivals = qw( getDeviceInfo=1051200 getPower=1051200 getOutputData=10 );
for my $ival ( @ivals ) {
if ( my ($cm, $val) = $ival =~ /^(getDeviceInfo|getPower|getOutputData)=(\d+)$/ ) {
$hash->{helper}{cmds}{$cm}{interval} = $MAIN_INTERVAL * $val;
}
}
}
Log3 $name, 3, "$iam $cmd $attrName to default interval multiplier.";
return;
}
return;
}
1;
Zitat von: schwatter am 19 Juli 2025, 12:00:58- Hausring hat kein freien Wert. Da habe ich jetzt sofortige Autarkieanzeige in % eingebaut. Das so lassen?Genau da wollte ich den Wattverbrauch abzüglich den der Custom-Ringe hinschreiben