Neueste Beiträge

#11
Solaranlagen / Aw: PowerFlow [animiertes SVG,...
Letzter Beitrag von schwatter - 19 Juli 2025, 16:50:14
Ok,

Update in Post #1. Housering Autarkie entfernt und dafür freies Feld hinzugefügt.

edit:
Ich habe im Post #1 im Bereich notify ein Beispiel dazu gepackt um Autarkie zu berechnen. Oder man nimmt das Reading von SolarForecast.

Gruß schwatter
#12
Multimedia / Aw: Audio-Ausgabe in (fast) be...
Letzter Beitrag von Prof. Dr. Peter Henning - 19 Juli 2025, 16:02:00
So, ich habe noch neue Versionen meiner Dateien erstellt.
1. Wenn der Browser einen Shared Worker unterstützt, und mehrere Tabs für die Webseite (z.B. FHEM) offen sind, wird nur EIN Worker gestartet - das verhindert, dass die MP3-Datei mehrfach parallel abgespielt wird.
2. Die Shell-Skripte habe ich noch mit einem ordentlichen Logging für Fehler ausgestattet, und ein paar Timeouts eingebaut.

Der Aufruf aus FHEM heraus mit   }elsif( $dadtype eq "Browser" ){
    #Log 1,"===========> [speakDAD] for Browser name=$name daddev=$daddev dadtype=$dadtype text=$text\n";
    #-- start TTS server, 2 seconds delay
    my $cmd = "/opt/fhem/tts_run.sh ".$ttsDir.$ttsTarget.".mp3 ".$ttsDur." 2 &";
    Log 1,"===========> cmd= $cmd";
    system($cmd);
funktioniert jetzt astrein, getestet unter Firefox und Chrome.

LG

pah
#13
Solaranlagen / Aw: 76_SolarForecast - Informa...
Letzter Beitrag von DS_Starter - 19 Juli 2025, 15:57:19
Hallo Hugo,

ZitatWelche Consumer, werden in welcher Reihenfolge abgeschaltet?
Gibt es da ein Timing? Oder eine Priorisierung?
Ja, in gewisser Weise. Die Consumer werden bei jeden Zyklus sequentiell aufsteigend entsprechend ihrer Nummer 01, 02, 03 ... behandelt.

ZitatEtwas 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.
Das ist quasi eine Chain die aufgebaut werden soll. Kann man machen. Dazu kann man z.B. das auto-Reading benutzen.
Ausgangszustand: - auto für alle Consumer "on"
-> sind alle Consumer eingeschaltet? -> Ausschalten "auto" für alle Consumer außer dem "Master"
-> ist der Master beendet ("finished") ? -> "auto" für den nächsten Consumer wieder "on" schalten
-> ist dieser Consumer "finished" ? ->  "auto" für den nächsten Consumer wieder "on" schalten
-> usw. ....
-> wenn der letzte Consumer "finished" ist alle wieder auto "on"

Eine solche Logik kann man im ctrlUserExitFn einbauen.

ZitatIst es bei surpmeth=median auch möglich eine Anzahl anzugeben? Wenn ja, wie?
Nein. Macht auch keinen Sinn. Median verwendet den mittleren Wert eines nach Größe geordneten Arrays aller gespeicherten Werte. Bei z.B. 7 vorhandenen Werten ist es der Wert an der 4. Stelle (Mitte bei ungeraden Zahlen). Bei gerader Anzahl der Werte ist es der Durchschnitt der beiden mittleren Werte, also z.B. Durchschnitt Wert 5 und 6 bei 10 vorhandenen Werten.

LG,
Heiko




#14
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.
#15
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
#16
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
#17
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?
#18
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
#19
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!
#20
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;