🏺Letzte Werte als Reading und Balkendiagramm, altn. ReadingsHistory, oldReadingsVal

Begonnen von Torxgewinde, 07 Juni 2023, 21:23:22

Vorheriges Thema - Nächstes Thema

Torxgewinde

Hallo,
Als kleine Alternative zu der vorzüglichen und umfangreicheren ReadingsHistory hier ein userReading der letzen N-Werte eines Readings des Devices:

Beispiel, die letzten 10 Werte des State des Devices:
history10 {
    my $val = ReadingsVal("$name", "state", "???");
    my $ts = ReadingsTimestamp("$name", "state", undef);
   
    my $this = ReadingsVal("$name", "$reading", "");
    my $length = ($reading =~ /(\d+)/g)[-1];
    my @timestampArray = split("\n", $this);

    #optional: if ($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;
}

Wie lang die History sein soll, das wird mit der Zahl im userReadings Namen festgelegt. Der Output sieht dann bei einem PIR Sensor des FS20 Systems zum Beispiel so aus:
2023-06-07 20:06:37: off
2023-06-07 20:06:48: on-old-for-timer 60
2023-06-07 20:07:48: off
2023-06-07 20:25:16: on-old-for-timer 60
2023-06-07 20:25:46: on-old-for-timer 60
2023-06-07 20:26:46: off
2023-06-07 20:27:46: on-old-for-timer 60
2023-06-07 20:28:46: off
2023-06-07 20:39:53: on-old-for-timer 60
2023-06-07 20:40:53: off

Wenn man mehr will, sollte man sich ReadingsHistory oder OldReadingsVal bzw. oldreadings anschauen.

So säh' es als ganzes Raw-device aus:
defmod ALARM.PIR2.1 FS20 1234 00
attr ALARM.PIR2.1 IODev FHZ1300PC
attr ALARM.PIR2.1 follow-on-for-timer 1
attr ALARM.PIR2.1 icon motion_detector
attr ALARM.PIR2.1 model fs20piri
attr ALARM.PIR2.1 userReadings history10 {\
    my $val = ReadingsVal("$name", "state", "???");;\
    my $ts = ReadingsTimestamp("$name", "state", undef);;\
    \
    my $this = ReadingsVal("$name", "$reading", "");;\
    my $length = ($reading =~ /(\d+)/g)[-1];;\
    my @timestampArray = split("\n", $this);;\
    \
    push(@timestampArray, "$ts: $val");;\
    while ( scalar @timestampArray > $length ) { shift(@timestampArray);; }\
    \
    my $serializedArray = join("\n", @timestampArray);;\
    return $serializedArray;;\
}

Du darfst diesen Dateianhang nicht ansehen.

betateilchen

Zitat von: Torxgewinde am 07 Juni 2023, 21:23:22Wenn man mehr will, sollte man sich ReadingsHistory oder OldReadingsVal bzw. oldreadings anschauen.

... oder die letzten 10 Werte einfach aus dem Log auslesen

get <DbLogDevice> retrieve last <deviceName> <readingName> "" "" "" 10
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

Torxgewinde

Hat man einmal die Historie, kann man diese nun auch animiert darstellen. Eine DB oder Logdatei braucht man dabei nicht.

In diesem Post habe ich beschrieben wie es geht: https://forum.fhem.de/index.php?topic=133819.msg1279910#msg1279910

Als Motivation ein paar Screenshots:
Du darfst diesen Dateianhang nicht ansehen.Du darfst diesen Dateianhang nicht ansehen. 

FHEMAN

Die Lösung finde ich schön easy. Die Visualisierung ist auch cool.
Ich nutze es, um zu tracken, welches Gerät einen bestimmten Dummy triggert, und speichere daher auch den State ab:

triggerHistory10:trigger.* {
    my $state = ReadingsVal("$name", "state", "?");
    my $val = ReadingsVal("$name", "trigger", "?");
    my $ts = ReadingsTimestamp("$name", "trigger", undef); 
    my $this = ReadingsVal("$name", "$reading", "");
    my $length = ($reading =~ /(\d+)/g)[-1];
    my @timestampArray = split("\n", $this);
    push(@timestampArray, "$ts: $val: $state");
    while ( scalar @timestampArray > $length ) { shift(@timestampArray); }
    my $serializedArray = join("\n", @timestampArray);
    return $serializedArray;
}


(Geänderte Zeilen fett markiert)
ergibt:

triggerHistory10  2023-07-04 10:36:18: Nebeneingang: on
                  2023-07-04 10:42:09: Eingang: off

Bei mehr als einem Dummy sollte es aber besser in die myUtils.
NUC7i5 | PROXMOX | FHEM 6.2 | 1 HMLAND | 2 UART | HM | LMS | HIFIBERRY | DOORBIRD | BLINK | BUDERUS | HUE | ALEXA | MILIGHT | LUFTDATENINFO | MQTT| ZIGBEE2MQTT | INDEGO | ROBOROCK | SMA | APC | OPENWB

Torxgewinde

#4
Das stimmt, hier wäre die passende Funktion für eine 99_myUtils.pm:
  ###############################################################################
  #
  #  push new value to list of type "timestamp: value\n", keep length by shifting
  #
  ###############################################################################
  sub pushTimestampValueArray($$$;$) {
    my ($values, $newTimestamp, $newValue, $length) = @_;
   
    $length = $length // 10;

    my @timestampArray = split("\n", $values);

    # Do not add to history if not new info
    if ($timestampArray[-1] eq "$newTimestamp: $newValue") {
        return;
    }
    if ($newValue eq ($timestampArray[-1] =~ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}: (.*)$/)[0]) {
        return;
    }

    push(@timestampArray, "$newTimestamp: $newValue");
    while (scalar @timestampArray > $length) {
        shift(@timestampArray);
    }

    my $serializedArray = join("\n", @timestampArray);
    return $serializedArray;
  }

Die Funktion nimmt Werte in die History nur auf, wenn sich der Wert geändert hat und wenn der neue Eintrag nicht identisch zu dem zuletzt gespeicherten Eintrag ist.


Angewendet auf dein Wunschformat, müsste es dann so lauten:
triggerHistory10:trigger.* {
    my $state = ReadingsVal("$name", "state", "?");
    my $val = ReadingsVal("$name", "trigger", "?");
    my $ts = ReadingsTimestamp("$name", "trigger", undef);
    my $this = ReadingsVal("$name", "$reading", "");
    my $length = ($reading =~ /(\d+)/g)[-1];

    return pushTimestampValueArray($this, $ts, "$val: $state", $length);
}

Bei der Funktion "historyToHTML()", habe ich noch ein paar CSS Styles ergänzt, dies wäre meine aktuellste Version (07.07.2023):

  ###############################################################################
  #
  #  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; 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; min-width: 1px;\" ".
        "class=\"zoom\" ".
        "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>
        var styleContent = `
                .zoom {
                    transition: min-width .5s ease-in, color .5s ease-in !important;
                }
                .zoom:hover {
                    min-width: max-content !important;
                    color: white !important;
                }
                .zoom:hover + div {
                    min-width: 16px !important;
                }
                .historyGrid:hover > :not(.zoom:hover, .zoom:hover ~ .zoom) {
                    min-width: 2px !important;
                } 
        `;

        /* add or update our CSS to the head */
        var styleElement = document.getElementById("historyGridCSS");
        if (!styleElement) {
            styleElement = document.createElement('style');
            styleElement.id = "historyGridCSS";
            document.head.appendChild(styleElement);
        }
        styleElement.textContent = styleContent;
   
        /* 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';
                       
                        /* if 0fr then unset min-width inline style of the div */
                        if (columns[j] === '0fr') {
                            thisEntry.style.minWidth = '';
                        }
                    }
                }
               
                /* put 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 noch zwei animierte GIFs.
Du darfst diesen Dateianhang nicht ansehen.
Wenn man es für mehrere Bewegungsmelder und Briefkastenmelder verwendet, kann es so aussehen:
Du darfst diesen Dateianhang nicht ansehen.


FHEMAN

Top! Um die Auslagerung in die myUtils ganz sauber zu haben, würde ich nur den Funktionsaufruf ins Userattribut packen, also in der Art:

attr GridTest userReadings history:wert.* { pushTimestampValueArray("$name", "wert", 10); }
NUC7i5 | PROXMOX | FHEM 6.2 | 1 HMLAND | 2 UART | HM | LMS | HIFIBERRY | DOORBIRD | BLINK | BUDERUS | HUE | ALEXA | MILIGHT | LUFTDATENINFO | MQTT| ZIGBEE2MQTT | INDEGO | ROBOROCK | SMA | APC | OPENWB

Torxgewinde

#7
Na klar, nicht jeder mag langen Perlcode einfügen, hier wäre mein Vorschlag für eine ergänzte Funktion für die 99_myUtils.pm:

  ###############################################################################
  #
  #  push new Reading Value to list of type "timestamp: value\n", keep length by shifting
  #
  #  Additional Info:
  #  https://forum.fhem.de/index.php?topic=133885.msg1281371#msg1281371
  #
  ###############################################################################
  sub pushReadingToHistory($$$;$) {
    my ($deviceName, $historyReadingName, $ReadingName, $length) = @_;
    $length = $length // ($historyReadingName =~ /(\d+)/g)[-1] // 10;

    my $val = ReadingsVal($deviceName, $ReadingName, "???");
    my $ts = ReadingsTimestamp($deviceName, $ReadingName, undef);
    my $history = ReadingsVal($deviceName, $historyReadingName, "");
   
    return pushTimestampValueArray($history, $ts, $val, $length);
  }

Der Aufruf in einem UserReading wäre dann fast so wie gewünscht. Ich wollte den Namen der Historie nicht hardcoden, deswegen ist der Parameter hinzugekommen:
attr GridTest userReadings history:wert.* { pushReadingToHistory($name, $reading, "wert", 10); }
Was auch geht wäre:
attr GridTest userReadings history5:wert.* { pushReadingToHistory($name, $reading, "wert"); }
Wenn es gefällt, kommt es mit in das Wiki.