Neueste Beiträge

#1
Homematic / Aw: Aus CCU3 wird OpenCCU
Letzter Beitrag von Ralli - 19 Juli 2025, 15:29:06
Da, wo es geht und sinnvoll ist, mache ich das auch und setze auf bspw. Shellies. Wofür ich aber bislang keine echten Alternativen gefunden habe, das sind die in eines der renomierten Schalterprogramme integrierbaren Aktoren - beispielsweise die Rolladenaktoren oder die "einfachen" Schalter.

Wir können ja einmal eine Liste von Aktoren und Sensoren mit der Aufzählung von Alternativen anfangen.
#2
Codeschnipsel / Aw: [Mini-Chart-Card] Schätzei...
Letzter Beitrag von schwatter - 19 Juli 2025, 15:28:23
Tag,

ich habe die PerlChartCardLine umgebaut. Jetzt hat sie eine Mobile und Desktopansicht.
Dadurch werden auch bei Desktop mehr Werte übergeben. Den Ringbuffer aus Post #1 sollte man auch auf min. 120 erhöhen.

Hier jetzt als sub für die 99_myUtils.pm.

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>
  };
}

Der Aufruf in dem Device eurer Wahl funktioniert dann so:
{
  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
  );
}

Neu hinzugekommen ist eine Glättung (Average) der Werte.

Gruß schwatter
#3
Solaranlagen / Aw: PowerFlow [animiertes SVG,...
Letzter Beitrag von schwatter - 19 Juli 2025, 15:18:14
Nah jut, dann werd ich das rückbauen  :)

Gruß schwatter
#4
Solaranlagen / Aw: 76_SolarForecast - Informa...
Letzter Beitrag von hugomckinley - 19 Juli 2025, 15:06:31
Und noch eine:
Ist es bei surpmeth=median auch möglich eine Anzahl anzugeben? Wenn ja, wie?
#5
Solaranlagen / Aw: 76_SolarForecast - Informa...
Letzter Beitrag von hugomckinley - 19 Juli 2025, 14:46:19
Einige Fragen die sich mir gerade stellen:
Angenommen ich habe 3 Consumer mit 1500W, 600W und 300W mit interupptable=1.
Die PV Leistung geht zurück und es werden 1800W bezogen.

Welche Consumer, werden in welcher Reihenfolge abgeschaltet?
Gibt es da ein Timing? Oder eine Priorisierung?

Etwas OT, aber so ist mir das in den Kopf gekommen:
Meine Anforderung wäre, dass die Verbraucher in einer gewissen Reihenfolge abgeschaltet werden. (unabhängig von der Leistung)

Angedachte Lösung: Meine Überlegung wäre jetzt, dass ich interruptable vom jeweils vorherigen Verbraucher abhängig mache.
@Heiko: Wenn das alles fertig ist und so funktioniert wie gewollt, schreibe ich es als Beispiel ins Wiki

Grüße,
Hugo
#6
Solaranlagen / Aw: neues Modul (contribute) 7...
Letzter Beitrag von Aekschn - 19 Juli 2025, 14:36:03
Ich habe das Problem gefunden:
Der EZHI akzeptiert keine Anfragen mit HTTP-Version 1.0, ich habe das auf 1.1 geändert und das Modul läuft :)

Schönes Wochenende!
#7
Solaranlagen / Aw: neues Modul (contribute) 7...
Letzter Beitrag von Aekschn - 19 Juli 2025, 13:45:05
Hi,

ich habe ich mal daran gemacht komme aber nicht weiter und weiß nicht so recht wo es hängt. Das Modul geht auf Error und im Log steht nichts was weiter hilft.Könntest du mal drüber gucken ob dir was ins Auge sticht?

Danke vorab!


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;
#8
FHEMWEB / Aw: Gerätename als StatusIcon?
Letzter Beitrag von Virsacer - 19 Juli 2025, 13:42:41
Ich hab das Gerät in FHEM halt so genannt, wie der Name auf der Verpackung steht.
Und als ich dann ein Icon dafür gemacht habe, hab ich das natürlich genauso genannt.
#9
Solaranlagen / Aw: PowerFlow [animiertes SVG,...
Letzter Beitrag von Virsacer - 19 Juli 2025, 13:36:29
Hi,

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 :D
Der unzuordbare Rest-Verbrauch sozusagen...

Gruß Virsacer
#10
Homematic / Aw: Aus CCU3 wird OpenCCU
Letzter Beitrag von zap - 19 Juli 2025, 12:32:00
Da bin ich auch gespannt, v.a. im Hinblick auf zukünftige Kompatibilität.

Da mir die aktuelle Entwicklung bei EQ-3 eher weniger gefällt und mir die Hochpreispolitik für Aktoren und Sensoren schon lange ein Dorn im Auge ist, ersetze ich schon seit einiger Zeit defekte Homematik Komponenten durch Zigbee oder Matter Geräte. Gerade für Zigbee gibt es zig Hersteller mit guten Aktoren/Sensoren, die die Hälfte Kosten wie Homematic und teilweise mehr Funktionalität bieten.