FHEM Forum

FHEM => Codeschnipsel => Thema gestartet von: Torxgewinde am 07 Juni 2023, 21:23:22

Titel: 🏺Letzte Werte als Reading und Balkendiagramm, altn. ReadingsHistory, oldReadingsVal
Beitrag von: Torxgewinde am 07 Juni 2023, 21:23:22
Hallo,
Als kleine Alternative zu der vorzüglichen und umfangreicheren ReadingsHistory (https://wiki.fhem.de/wiki/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 (https://wiki.fhem.de/wiki/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;;\
}

Bildschirmfoto_2023-06-10_10-54-03.png
Titel: Aw: 🏺Letzte Werte auflisten, kleine Alternative zu ReadingsHistory, oldReadingsVal
Beitrag von: betateilchen am 07 Juni 2023, 22:38:17
Zitat von: Torxgewinde am 07 Juni 2023, 21:23:22Wenn man mehr will, sollte man sich ReadingsHistory (https://wiki.fhem.de/wiki/ReadingsHistory) oder OldReadingsVal bzw. oldreadings anschauen.

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

get <DbLogDevice> retrieve last <deviceName> <readingName> "" "" "" 10
Titel: Aw: 🏺Letzte Werte auflisten, kleine Alternative zu ReadingsHistory, oldReadingsVal
Beitrag von: Torxgewinde am 26 Juni 2023, 22:27:59
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 (https://forum.fhem.de/index.php?topic=133819.msg1279910#msg1279910)

Als Motivation ein paar Screenshots:
Bildschirmfoto_2023-06-26_21-51-38.pngBildschirmfoto_2023-06-26_21-55-05.png 
Titel: Aw: 🏺Letzte Werte auflisten, kleine Alternative zu ReadingsHistory, oldReadingsVal
Beitrag von: FHEMAN am 04 Juli 2023, 11:06:03
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.
Titel: Aw: 🏺Letzte Werte auflisten, kleine Alternative zu ReadingsHistory, oldReadingsVal
Beitrag von: Torxgewinde am 07 Juli 2023, 18:37:20
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.
Unbenannt-converted.gif
Wenn man es für mehrere Bewegungsmelder und Briefkastenmelder verwendet, kann es so aussehen:
Bewegungsmelder-converted.gif
Titel: Aw: 🏺Letzte Werte als Reading und Balkendiagramm, altn. ReadingsHistory, oldReadingsVal
Beitrag von: Torxgewinde am 11 Juli 2023, 18:36:52
Ich habe einen Wikieintrag zu diesem Thema gemacht: https://wiki.fhem.de/wiki/Letzte_Werte_als_Reading_und_Balkendiagramm (https://wiki.fhem.de/wiki/Letzte_Werte_als_Reading_und_Balkendiagramm)
Titel: Aw: 🏺Letzte Werte als Reading und Balkendiagramm, altn. ReadingsHistory, oldReadingsVal
Beitrag von: FHEMAN am 13 Juli 2023, 13:32:57
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); }
Titel: Aw: 🏺Letzte Werte als Reading und Balkendiagramm, altn. ReadingsHistory, oldReadingsVal
Beitrag von: Torxgewinde am 13 Juli 2023, 21:24:46
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.