Frage:Chart für Zustände, History Graph, uiTable für PIR Sensoren, Bewegungsmeld

Begonnen von Torxgewinde, 02 Juni 2023, 17:23:42

Vorheriges Thema - Nächstes Thema

Torxgewinde

Hi,
Was wäre geeignet um Zustände von mehreren Bewegungsmeldern oder Präsenzmeldern darzustellen?

Zur Zeit habe ich mir was zusammengebastelt aus einem SVG-Plot, aber ich bin mir sicher dass es da bessere Ideen gibt. So sieht es bisher aus:
Du darfst diesen Dateianhang nicht ansehen.

Was mir gefallen würde, wäre etwas in der Art wie bei HA: https://www.home-assistant.io/dashboards/history-graph/
Du darfst diesen Dateianhang nicht ansehen.
oder in dieser Art:
Du darfst diesen Dateianhang nicht ansehen.

Ein paar Tipps wären toll. Am SVG stört mich, dass es sich nicht so wie uiTable Elemente aktualisiert.

Damian

z. Zt. gibt´s nur col für Linien und bar für Säulen, später sollen noch Punkte dazukommen.
Programmierte FHEM-Module: DOIF-FHEM, DOIF-Perl, DOIF-uiTable, THRESHOLD, FHEM-Befehl: IF

Torxgewinde

Danke für die Rückmeldung, das motiviert mich immerhin an der Darstellung ein wenig weiter zu feilen und ich nichts offensichtlich Besseres übersehe.

Torxgewinde

Noch ist es nicht zufriedenstellend, aber einen Entwurf hätte ich hier. Interessant fand ich es mit einem CSS-grid und als Maßeinheit mir 'fr' zu arbeiten. Vielleicht ist ja die ein oder andere Idee dabei, falls nicht ist auch gut.

defmod GridTest dummy
attr GridTest readingList wert
attr GridTest setList wert tick
attr GridTest stateFormat FromMyUtils
attr GridTest userReadings history10:wert.* {\
    my $val = ReadingsVal("$name", "wert", "???");;\
    my $ts = ReadingsTimestamp("$name", "wert", undef);;\
   \
    my $this = ReadingsVal("$name", "$reading", "");;\
    my $length = ($reading =~ /(\d+)/g)[-1];;\
    my @timestampArray = split("\n", $this);;\
\
    if (scalar(@timestampArray) == 0 || $timestampArray[-1] eq "$ts: $val") { return;; }\
    #optional: statt "history10" einfach "history10:state.*" als Readingnamen wählen\
    push(@timestampArray, "$ts: $val");;\
    while ( scalar @timestampArray > $length ) { shift(@timestampArray);; }\
\
    my $serializedArray = join("\n", @timestampArray);;\
    return $serializedArray;;\
},\
historyGrid:wert.* {\
my $values = ReadingsVal("$name", "history10", "");;\
    my @values = split("\n", $values);;\
\
my $start_time = time() - (5 * 60);;\
\
#colors too choose from:\
my @colors = ("#48bb78", "#ecc94b", "#ed8936", "#e53e3e", "#805ad5");;\
#colors that have a certain mapping of value->color:\
my %color_map = ("on" => "green",\
                 "off" => "red",\
"on-old-for-timer 60" => "orange",\
"no_motion" => "green",\
"motion" => "red",\
"offline" => "red");;\
my $color_index = 0;;\
\
#go through the array, but from highest index to lowest, keep it however in the initial order\
@values = reverse map {\
    my ($fullvalue, $timestampStr, $value) = ($_, $_ =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}): (.*)$/);;\
my $timestamp = time_str2num($timestampStr);;\
if (not exists $color_map{$value}) {\
$color_map{$value} = $colors[$color_index];;\
$color_index = ($color_index + 1) % scalar(@colors);;\
}\
\
    {\
'timestamp' => $timestamp,\
'outdated' => ($timestamp < $start_time)?1:0,\
#'timestampStr' => $timestampStr,\
'value' => $value,\
#'fullvalue' => $fullvalue,\
'color' => $color_map{$value}\
}\
} reverse @values;;\
\
#iterate from last to earlier items\
my $i;;\
for ($i = $#values;; $i > 0;; $i--) {\
last if ($values[$i]->{outdated});;\
\
my $duration=0;;\
if ($values[$i-1]->{'outdated'}) {\
#calculate duration from start of this timeframe\
$duration = $values[$i]->{'timestamp'} - $start_time;;\
} else {\
#calculate the duration the state was valid for\
$duration = $values[$i]->{'timestamp'} - $values[$i-1]->{'timestamp'};;\
}\
    $values[$i-1]->{'duration'} = round($duration, 0);;\
}\
#special handling of most recent item, always assign 1s\
#$values[-1]->{'duration'} = time() - $values[-1]->{'timestamp'};;\
$values[-1]->{'duration'} = 1;;\
\
#just work with the values from the interval\
my @valuesToKeep = grep { $_->{'outdated'} == 0 } @values;;\
#add the last outdated element to the front\
unshift(@valuesToKeep, $values[$i]);;\
\
#return Data::Dumper::Dumper(\@valuesToKeep);;\
\
my $grid = "<div style=\"border: 1px solid white;;".\
"border-radius: 10px;; overflow: hidden;; display: grid;;".\
"color: white;; text-align: center;; align-items: center;;".\
"grid-template-columns:";;\
foreach my $item (@valuesToKeep) {\
$grid .= "$item->{duration}fr ";;\
}\
$grid .= ";;\">";;\
\
# Generate <div> elements with inline CSS style\
my $grid_elements = '';;\
foreach my $entry (@valuesToKeep) {\
    $grid_elements .= "<div style=\"background: $entry->{'color'};;".\
"border-right: white 1px solid;; height: 100%;;\">$entry->{'value'}</div>";;\
}\
\
return "<html>$grid".$grid_elements."</div></html>";;\
},\
FromMyUtils:(wert|tick).* {\
my $values = ReadingsVal("$name", "history10", "");;\
my $start_time = time() - (5 * 60);;\
return historyToHTML($values, $start_time);;\
}

setstate GridTest <html><div style="border: 1px solid white;;border-radius: 0px;; overflow: hidden;; display: grid;; width: 100%;;color: white;; text-align: center;; align-items: center;;grid-template-columns:334fr ;;"><div style="background: red;;border-right: white 1px solid;; height: 100%;;">&nbsp;;</div></div></html>
setstate GridTest 2023-06-24 23:34:52 FromMyUtils <html><div style="border: 1px solid white;;border-radius: 0px;; overflow: hidden;; display: grid;; width: 100%;;color: white;; text-align: center;; align-items: center;;grid-template-columns:334fr ;;"><div style="background: red;;border-right: white 1px solid;; height: 100%;;">&nbsp;;</div></div></html>
setstate GridTest 2023-06-24 23:29:19 history10 2023-06-24 23:17:07: 456\
2023-06-24 23:17:12: off\
2023-06-24 23:19:08: motion\
2023-06-24 23:19:34: no_motion\
2023-06-24 23:20:14: 999\
2023-06-24 23:20:27: off\
2023-06-24 23:22:29: on\
2023-06-24 23:28:01: off\
2023-06-24 23:28:20: on\
2023-06-24 23:29:19: off
setstate GridTest 2023-06-24 23:29:19 historyGrid <html><div style="border: 1px solid white;;border-radius: 10px;; overflow: hidden;; display: grid;;color: white;; text-align: center;; align-items: center;;grid-template-columns:222fr 19fr 59fr 1fr ;;"><div style="background: green;;border-right: white 1px solid;; height: 100%;;">on</div><div style="background: red;;border-right: white 1px solid;; height: 100%;;">off</div><div style="background: green;;border-right: white 1px solid;; height: 100%;;">on</div><div style="background: red;;border-right: white 1px solid;; height: 100%;;">off</div></div></html>
setstate GridTest 2023-06-24 23:34:52 state tick
setstate GridTest 2023-06-24 23:29:19 wert off


optisch geht es dann in diese Richtung:
Du darfst diesen Dateianhang nicht ansehen.

Als Funktion für eine 99_myUtils.pm:
  ###############################################################################
  #
  #  Consume a string with lines like: Timestamp: value
  #  return HTML to show the history visually
  #
  ###############################################################################
  sub historyToHTML($$) {
    my ($values, $start_time) = @_;
    my @values = split("\n", $values);

#colors too choose from:
my @colors = ("#48bb78", "#ecc94b", "#ed8936", "#e53e3e", "#805ad5");
#colors that have a certain mapping of value->color:
my %color_map = ("on" => "green",
                 "off" => "red",
"on-old-for-timer 60" => "orange",
"no_motion" => "green",
"motion" => "red",
"offline" => "red");
my $color_index = 0;

#go through the array, but from highest index to lowest, keep it however in the initial order
@values = reverse map {
    my ($fullvalue, $timestampStr, $value) = ($_, $_ =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}): (.*)$/);
my $timestamp = time_str2num($timestampStr);
if (not exists $color_map{$value}) {
$color_map{$value} = $colors[$color_index];
$color_index = ($color_index + 1) % scalar(@colors);
}

    {
'timestamp' => $timestamp,
'outdated' => ($timestamp < $start_time)?1:0,
#'timestampStr' => $timestampStr,
'value' => $value,
#'fullvalue' => $fullvalue,
'color' => $color_map{$value}
}
} reverse @values;

#iterate from last to earlier items
my $i;
for ($i = $#values; $i > 0; $i--) {
last if ($values[$i]->{outdated});

my $duration=0;
if ($values[$i-1]->{'outdated'}) {
#calculate duration from start of this timeframe
$duration = $values[$i]->{'timestamp'} - $start_time;
} else {
#calculate the duration the state was valid for
$duration = $values[$i]->{'timestamp'} - $values[$i-1]->{'timestamp'};
}
    $values[$i-1]->{'duration'} = round($duration, 0);
}
#special handling of most recent item, always assign 1s
$values[-1]->{'duration'} = time() - $values[-1]->{'timestamp'};
$values[-1]->{'duration'} = round($values[-1]->{'duration'}, 0);
#$values[-1]->{'duration'} = 1;

#just work with the values from the interval
my @valuesToKeep = grep { $_->{'outdated'} == 0 } @values;
#add the last outdated element to the front
unshift(@valuesToKeep, $values[$i]);

#return Data::Dumper::Dumper(\@valuesToKeep);

my $grid = "<div style=\"border: 1px solid white;".
"border-radius: 0px; overflow: hidden; display: grid; width: 100%;".
"color: white; text-align: center; align-items: center;".
"grid-template-columns:";
foreach my $item (@valuesToKeep) {
$grid .= "$item->{duration}fr ";
}
$grid .= ";\">";

# Generate <div> elements with inline CSS style
my $grid_elements = '';
foreach my $entry (@valuesToKeep) {
    #$grid_elements .= "<div style=\"background: $entry->{'color'};".
#"border-right: white 1px solid; height: 100%;\">$entry->{'value'}</div>";
$grid_elements .= "<div style=\"background: $entry->{'color'};".
"border-right: white 1px solid; height: 100%;\">&nbsp;</div>";
}

return "<html>$grid".$grid_elements."</div></html>";
}

Im State kann man das sich dann auch anschauen, das säh dann so aus:
Du darfst diesen Dateianhang nicht ansehen.

wo ich Probleme habe, ist das Aktualisieren. Mit einem 'at' klappt es, aber so richtig gut finde ich das noch nicht:
defmod GridTestTimer3 at +*00:00:10 set GridTest tick

Damian

Ich habe die Schaltvorgänge einfach über die Card-Funktion abgebildet, siehe https://wiki.fhem.de/wiki/DOIF/Automatisierung#Steuerung_von_Raumthermostaten_f%C3%BCr_mehrere_R%C3%A4ume_mit_GUI

Dort habe ich on/off auf 1/0 abgebildet. Das Sammeln der Werte mit ihren Zeitstempeln wird bereits im DOIF realisiert. Der Anwender muss dann nur noch die Funktion mit gewünschten Parametern und mit dem entsprechenden Trigger angeben.
Programmierte FHEM-Module: DOIF-FHEM, DOIF-Perl, DOIF-uiTable, THRESHOLD, FHEM-Befehl: IF

Torxgewinde

Oja, das sieht auch gut aus und die Werte aktualisieren sich auch so wie man es sich wünscht. Danke!

Torxgewinde

#6
Ich habe es jetzt doch nicht ruhen lassen können und möchte nun den aktuellen Stand posten:

Man kann mit folgendem Code die Werte aus dem "UserReading-Log" darstellen. Da die Zeit vergeht, wird die Balkenbreite mittels Javascript jede Sekunde angepasst bzw. animiert. Beim Aufruf aus dem UserReading kann man entscheiden, ob das ganze Log dargestellt werden soll, oder nur die letzten Sekunden/Stunden/Tage. Der allerneuste Werte aus dem Log wird mit einer Mindestbreite dargestellt, da er der relevanteste Wert ist. Um erkennbar zum machen, dass der Balken dann nicht im gleichen Größenverhältnis ist, ist er mit einer gestrichelten Linie abgetrennt. Diese verschwindet wieder, wenn die Zeit voranschreitet und die Balkenbreite wieder stimmt. Passt auf den Balken die Beschriftung ist sie sichtbar, passt der Text nicht ist nur eine Farbe zu sehen. Die gewählte Farbe ist je nach Wert immer die gleiche Farbe. Verweilt man mit der Maus über einer Markierung wird als Tooltip der Logeintrag eingeblendet. Zu dem was bei HA gemacht wird, fehlt eigentlich nur noch die Skala.

Ein paar Bilder:
Du darfst diesen Dateianhang nicht ansehen.Du darfst diesen Dateianhang nicht ansehen. 

Was muss man tun um es zu nutzen:
- 99_myUtils.pm mit der Funktion historyToHTML() erweitern.
- zwei UserReadings bei dem zu plottendem Device anlegen:
-- Eins für die Historie
-- Eins für die Darstellung

Wenn man will, kann man die Darstellung auch im StateFormat nutzen, das macht sich in der Übersicht dann ganz gut.

Dies in die 99_myUtils.pm packen:
  ###############################################################################
  #
  #  Consume a string with lines like: Timestamp: value
  #  return HTML to show the history visually
  #
  ###############################################################################
  sub historyToHTML($;$) {
    my ($values, $interval) = @_;
    my @values = split("\n", $values);
    return "Could not split values" unless @values;
   
    #if no interval given, use whole history, do not "shift" out old values
    my $start_time;
    if (not defined $interval) {
        my ($timestampStr) = ($values[0] =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}): .*$/)[0];
        return "Could not parse timestamp" unless defined $timestampStr;
        $start_time = time_str2num($timestampStr);
    } else {
        $start_time = time() - $interval;
    }

    #colors too choose from:
    my @colors = (
        '#f44336', '#f44336', '#2196f3', '#03a9f4', '#00bcd4',
        '#009688', '#4caf50', '#8bc34a', '#cddc39', '#ffeb3b',
        '#ffc107', '#ff9800', '#ff5722', '#9e9e9e', '#2196f3',
        '#607d8b', '#9c27b0', '#673ab7', '#3f51b5', '#e91e63',
        '#00e676', '#ff5722', '#ffeb3b', '#00bcd4', '#8bc34a',
        '#607d8b', '#9e9e9e', '#795548'
    );
    #colors that should have a certain mapping of value->color:
    my %color_map = (
        "on" => "#a4c639",               # Yellowish or Green
        "off" => "#3c3c3c",              # Darker, Matted Toned
        "on-old-for-timer 60" => "#ffa500",  # Orange
        "no_motion" => "#4caf50",        # Green
        "motion" => "#f44336",           # Red
        "offline" => "#f44336"           # Red
    );

    #go through the array, but from highest index to lowest, keep it however in the initial order
    @values = reverse map {
        my ($fullvalue, $timestampStr, $value) = ($_, $_ =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}): (.*)$/);
        my $timestamp = time_str2num($timestampStr);

        #choose color
        if (not exists $color_map{$value}) {
            my $color_index = join('', map { ord($_) } split('', $value)) % scalar(@colors);
            $color_map{$value} = $colors[$color_index];
        }

        #fill hash with values
        {
            'timestamp' => $timestamp,
            'outdated' => ($timestamp < $start_time)?1:0,
            'timestampStr' => $timestampStr,
            'value' => $value,
            'fullvalue' => $fullvalue,
            'color' => $color_map{$value}
        }
    } reverse @values;
   
    #iterate from last to earlier items
    my $i;
    for ($i = $#values; $i > 0; $i--) {
        last if ($values[$i]->{outdated});
       
        my $duration=0;
        if ($values[$i-1]->{'outdated'}) {
            #calculate duration from start of this timeframe
            $duration = $values[$i]->{'timestamp'} - $start_time;
        } else {
            #calculate the duration the state was valid for
            $duration = $values[$i]->{'timestamp'} - $values[$i-1]->{'timestamp'};
        }
        $values[$i-1]->{'duration'} = round($duration, 0);
    }
    #special handling of most recent item, always assign 1s
    $values[-1]->{'duration'} = time() - $values[-1]->{'timestamp'};
    $values[-1]->{'duration'} = round($values[-1]->{'duration'}, 0);
    #$values[-1]->{'duration'} = 1;
    $values[-1]->{'mostRecentItem'} = 1;
   
    #just work with the values from the interval
    my @valuesToKeep = grep { $_->{'outdated'} == 0 } @values;
    #add the last outdated element to the front
    unshift(@valuesToKeep, $values[$i]) if (scalar(@valuesToKeep) != scalar(@values));
   
    #return Data::Dumper::Dumper(\@valuesToKeep);
   
    my $grid = "<div class=\"historyGrid\" style=\"border: 1px solid white;".
    "border-radius: 5px; overflow: hidden; display: grid;".
    "color: white; text-align: center; align-items: center;".
    "width: 500px; grid-template-columns:";
    foreach my $item (@valuesToKeep) {
        $grid .= "$item->{duration}fr ";
    }
    $grid .= ";\" ".
    "data-startTime=$start_time ".
    "data-interval=". ($interval // "NaN") .">";
   
    # Generate <div> elements with inline CSS style
    my $grid_elements = '';
    foreach my $entry (@valuesToKeep) {
        $grid_elements .= "<div style=\"background: $entry->{'color'};".
        "border-right: white 0px solid; height: 100%; overflow: hidden;".
        "white-space: nowrap; text-overflow: clip;\"".
        "data-timestamp=". round($entry->{'timestamp'}, 0) ." ".
        "title=\"$entry->{'fullvalue'}\">".
        "$entry->{'value'}<br>($entry->{'timestampStr'})</div>";
    }
   
    #Script to animate from the current time onwards
    my $script = <<"JS";
    <script>
        /* just keep one timer for our purposes */
        if (window.historyGridIntervalTimer) {
            clearInterval(window.historyGridIntervalTimer);
        }

        /* this function adjusts the div sizes based on time */
        var updateValue = function() {
            /* there might be several copies of this element, work with classes */
            var historyGridItems = document.getElementsByClassName('historyGrid');
            var currentTimestamp = Date.now() / 1000;

            /* work on each individual historyGrid */
            for (var i = 0; i < historyGridItems.length; i++) {
                var historyGrid = historyGridItems[i];
                var columns = historyGrid.style.gridTemplateColumns.split(' ');

                /* 1 fr equals 1 second here, adjust the most recent item */
                var timestampLastItem = historyGrid.lastChild.dataset.timestamp;
                columns[columns.length - 1] = (currentTimestamp - timestampLastItem) + 'fr';

                /* adjust the other item sizes if not whole history is to be shown */
                if (historyGrid.dataset.interval != "NaN") {
                    var start_at = currentTimestamp - parseInt(historyGrid.dataset.interval);

                    /* adjust the first and next child-div, the last has been adjusted above */
                    for (var j = 0; j < columns.length-1; j++) {
                        var thisEntry = historyGrid.children[j];
                        var nextEntry = historyGrid.children[j+1];

                        var thisTimestamp = Math.max(0, thisEntry.dataset.timestamp-start_at);
                        var nextTimestamp = Math.max(0, nextEntry.dataset.timestamp-start_at);

                        /* 0fr is OK for outdated items, item is still in the DOM, but 0-sized */
                        columns[j] = Math.max(0, nextTimestamp-thisTimestamp) + 'fr';
                    }
                }
               
                /* but together the grid-size-info again and apply */
                historyGrid.style.gridTemplateColumns = columns.join(' ');
            }

            /* if the text does not fit, hide the text without deleting it */
            var divElements = document.querySelectorAll('.historyGrid > div');
            divElements.forEach(function(element) {
                if (element.scrollWidth > element.clientWidth) {
                    element.style.color = 'transparent';
                } else {
                    element.style.color = '';
                }
            });

            /* the most recent log entry (the last) is too small to be seen,
               so, make it larger and maintain aspect ratio of 1:1
             */
            var elements = document.querySelectorAll('.historyGrid > div:last-child');
            elements.forEach(function(element) {
              element.style.minWidth = element.offsetHeight + 'px';
              if (element.offsetWidth > parseInt(element.style.minWidth) + 2) {
                element.style.borderLeft = '0px solid white';
              } else {
                element.style.borderLeft = '2px dashed white';
              }
            });
        };

        /* immiditaly update sizes right now and set intervalTimer */
        updateValue();
        window.historyGridIntervalTimer = setInterval(updateValue, 1000);
    </script>
JS
    #remove newlines, because this confuses FHEMs handling of HTML content
    $script =~ s/\n//g;

    return "<html>$grid".$grid_elements."</div>".$script."</html>";
    }

Hier ist ein Dummy zum Testen:
defmod GridTest dummy
attr GridTest readingList wert
attr GridTest setList wert
attr GridTest stateFormat FromMyUtils
attr GridTest userReadings history10:wert.* {\
    my $val = ReadingsVal("$name", "wert", "???");;\
    my $ts = ReadingsTimestamp("$name", "wert", undef);;\
    \
    my $this = ReadingsVal("$name", "$reading", "");;\
    my $length = ($reading =~ /(\d+)/g)[-1];;\
    my @timestampArray = split("\n", $this);;\
    \
    #do not add to history if not new info\
    if ($timestampArray[-1] eq "$ts: $val") { return;; }\
    if ($val eq ($timestampArray[-1] =~ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}: (.*)$/)[0]) { return;; }\
    #optional: statt "history10" einfach "history10:state.*" als Readingnamen wählen\
    \
    push(@timestampArray, "$ts: $val");;\
    while ( scalar @timestampArray > $length ) { shift(@timestampArray);; }\
    \
    my $serializedArray = join("\n", @timestampArray);;\
    return $serializedArray;;\
},\
FromMyUtils:wert.* {\
    my $values = ReadingsVal("$name", "history10", "");;\
    \
    #zeige nur die letzten 5 Minuten (=300s)\
    return historyToHTML($values, 5*60);;\
    \
    #zeige all Einträge aus der Historie\
    #return historyToHTML($values);;\
}

Um zu der eigentliche Motivation, der Darstellung von den letzten 24h von Bewegungsmeldern und Briefkastenmeldern zurückzukommen: Das sieht nun wie folgt aus:
Du darfst diesen Dateianhang nicht ansehen.

Die aktuelleste Version des Quelltextes findet man in dem Thread:
https://forum.fhem.de/index.php?topic=133885.msg1280863#msg1280863