Tabular and historical data – do you have any ideas on how to solve this?

Tabular and historical data – do you have any ideas on how to solve this?

I will explain what I mean using a use-case example – I would like to record the difference between electricity consumption and electricity production over the last 7 days to see if consumption has exceeded production.

My idea is as follows: create 7 (one for each day of the week) sets of 4 items:
a) at 00:01 – energy consumption status
b) at 00:01 – energy production status
c) at 11:59 p.m. - energy consumption status
d) at 11:59 p.m. - energy production status

but it adds up to a total of 28 items + 2 items summary for week summary = total of 30 items(!).

That’s a lot :frowning:

Do you have any ideas for a different solution? Maybe using persistence would make it possible to implement my idea?

You can get the information for use in a rule from persistence. But to show this information in a UI you will need the Items.

But you can create the Items programmatically pretty easily from a script or rule.

You probably only need one Item for the delta, since its daily value can be calculated once per day and also persisted (at say 23:59) and then the chart would show the time series for that.

Right, delta will be enaugh, a discribed in the simple words my idea to better understand the issue, but absolutelly right - only delta is ok.

Could you help me how to use persistance?

Something as following?

Difficult topic with persistance….

You need to use the "get " block to query for the states stored in persistence.

There are tons of options but you’ll mostly need the “sum between” option.

You’ll need to supply whether you want a plain number or a quantity, which Item to sum the states for and the start and stop timestamps to sum between.

And then just post the result as an update to the Item you want to store the sum.

1 Like

The energy meter displays cumulative consumption, so I can’t add up the total because the reading itself is a total. Even if I take a very short time interval—1 minute—it shows absurd values ​​(like a log), and the meter logs consumption approximately every 3 minutes. So, the interval total doesn’t work, and it doesn’t work even over a very short time interval.

04.03.2026 08:19	 [INFO ]	 [tomation.jsscripting.rule.3e9725c5c9] - 	State sum between: 49182552000.00

OK, then get a historic value at the time you want it instead of a sum or an average. Like I said, there are many options.

Hohohoho! It works! Thank you.

Previously I tried to use ‘persisted state’, but there was “String” instead of “Number” and I got error “.toFixed is not a function in” that’s why I published the post with ask to help me.

And now I changed it to the “Number” and it works perfectly!

Once again THANK YOU SO MUCH MY MASTER @rlkoshak of OH!

All best!

Recently I also wanted to display some historic data in a table. I wanted to display complete history of my precipitation measurement (monthly values + yearly sum).

Here is how I did it (for those that are interested):

  • I created a rule (plain javascript) that calculates same set of history data for items tagged with particular tag (#historic-data)
  • it obtains values back in time until there are no more values, using persistence and persistedState at the end of each month
  • it then serializes all values into JSON and save it to another item (same item name + “History” suffix)
  • then created widget that uses the JSON from history item and renders table for all measured values

I used that widget in expanded cell in my overview page. Currently just for Rain and Electricity consumption, but I plan to extend it.

Here is how it looks like:

Min and max values for a year are marked red and blue. At the end there is a sum for year (Celkem) and average for year (Průměr)

Hello @djal !

Very interesting solution. Congratulations!

Are the minimum and maximum values ​​included in the JSON data or are they calculated in the widget?

Is it possible to share the widget used?

Lol! Very good job!

Could you share the source of the javascrip if is it not a problem?

Min and max are part of the JSON data.

Here is the widget code (I translated the month names to english)

uid: historyTableYear
tags: []
props:
  parameters:
    - context: item
      description: Historic Item
      label: Historic Item
      name: historicItem
      required: true
      type: TEXT
    - description: Table title
      label: Table title
      name: title
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Feb 17, 2026, 10:27:03 AM
component: div
config:
  class:
    - card
    - data-table
    - data-table-collapsible
    - data-table-init
  style:
    --f7-table-body-cell-height: 25px
slots:
  default:
    - component: table
      config: {}
      slots:
        default:
          - component: thead
            slots:
              default:
                - component: tr
                  slots:
                    default:
                      - component: th
                        config:
                          class:
                            - label-cell
                          style:
                            text-align: right
                          content: =props.title
                      - component: oh-repeater
                        config:
                          for: hist
                          fragment: true
                          in: =JSON.parse(@@(props.historicItem)).years
                        slots:
                          default:
                            - component: th
                              config:
                                class:
                                  - label-cell
                                style:
                                  text-align: right
                                content: =loop.hist
          - component: tbody
            slots:
              default:
                - component: oh-repeater
                  config:
                    for: month
                    fragment: true
                    in:
                      - idx: 1
                        label: January
                      - idx: 2
                        label: February
                      - idx: 3
                        label: March
                      - idx: 4
                        label: April
                      - idx: 5
                        label: May
                      - idx: 6
                        label: June
                      - idx: 7
                        label: July
                      - idx: 8
                        label: August
                      - idx: 9
                        label: September
                      - idx: 10
                        label: October
                      - idx: 11
                        label: November
                      - idx: 12
                        label: December
                      - idx: sum
                        label: Total
                      - idx: avg
                        label: Average
                  slots:
                    default:
                      - component: tr
                        slots:
                          default:
                            - component: td
                              config:
                                class:
                                  - label-cell
                                style:
                                  background: rgb(246, 246, 247)
                                  text-align: right
                                  font-weight: =loop.month.idx=='sum'?'bold':'normal'
                                content: =loop.month.label
                            - component: oh-repeater
                              config:
                                for: yr
                                fragment: true
                                in: =JSON.parse(@@(props.historicItem)).history
                              slots:
                                default:
                                  - component: td
                                    config:
                                      style:
                                        font-weight: =loop.month.idx=='sum'?'bold':'normal'
                                        background: =(loop.month.idx=='sum' || loop.month.idx=='avg')?'rgb(246, 246,
                                          247)':''
                                        color: =loop.month.idx===loop.yr.values['minMonth']?'blue':loop.month.idx===loop.yr.values['maxMonth']?'red':''
                                        text-align: right
                                      content: =Math.round(loop.yr.values[loop.month.idx])<0?'':Math.round(loop.yr.values[loop.month.idx])
                                      data-collapsible-title: =loop.yr.year

2 Likes

Here is the javascript rule that gathers data into JSON

configuration: {}
triggers:
  - id: "2"
    label: Every 6 hours 2 minutes before midnight
    configuration:
      cronExpression: 49 58 23/6 * * ? *
    type: timer.GenericCronTrigger
conditions: []
actions:
  - inputs: {}
    id: "1"
    configuration:
      type: application/javascript
      script: >
        /* Example of history data JSON

        {
        	"years": [
        		2026,
        		2025,
            ...
        	],
        	"history": [
        		{
        			"year": 2026,
        			"values": {
        				"1": 297.1,
        				"2": 147.7,
        				"3": -1,
                ...
        				"12": -1,
        				"max": 297.0985,
        				"maxMonth": 1,
        				"min": 147.6747,
        				"minMonth": 2,
        				"sum": 444.8,
        				"avg": 222.4
        			}
        		},
            ...
          ]
        }

        */

        let calcHistoryForYear = (calcYear, calcIt) => {
          let res = {
            year: calcYear,
            values: {}
          };
          
          let calcDate = time.toZDT(calcYear+'-01-01');
          let sum = 0;
          let cnt = 0;

          for (let m = 1; m <= 12; m++) {
            // folowing doesn't work for my very old historic data, so I used alternative way
            //const st = calcIt.persistence.persistedState(calcDate.plusMonths(1).minusSeconds(1));
            const st = calcIt.persistence.maximumBetween(calcDate.plusMonths(1).minusMinutes(65), calcDate.plusMonths(1));
            const calcMonth = calcDate.monthValue();
            let val = 0;
            if (st) {
              val = st.numericState;
            }
            if (calcDate.isAfter(time.toZDT())) {
              val = -1;
            } else {
              cnt++;
              sum += val;
            
              if (val > res.values['max'] || m == 1) {
                res.values['max'] = val;
                res.values['maxMonth'] = calcMonth;
              }
          
              if (val < res.values['min'] || m == 1) {
                res.values['min'] = val;
                res.values['minMonth'] = calcMonth;
              }
            }

            res.values[calcMonth] = Number(val.toFixed(1));
            
            calcDate = calcDate.plusMonths(1);
          }
          
          res.values['sum'] = Number(sum.toFixed(1));
          res.values['avg'] = cnt > 0 ? Number((sum / cnt).toFixed(1)) : 0;
            
          return res;
        };


        // calculate complete history for one given item

        let calcHistoryForItem = (calcItem, histItem) => {
          let hist = {
            years: [],
            history: []
          }
          
          var calcYear = time.toZDT().year();
          let cnt = 100; // safety counter to avoid infinite loop
          while (cnt > 0) {
            let result = calcHistoryForYear(calcYear, calcItem);
            if (result.values['sum'] == 0) break;
            hist.years.push(calcYear);
            hist.history.push(result);
            calcYear--;
            cnt--;
          }

          console.log("Hisoric data for %s (stored to %s):\n %s", calcItem.name, histItem.name, JSON.stringify(hist));

          histItem.postUpdate(JSON.stringify(hist));
        }


        // find all tagged items and check if history item exists, if yes then
        calculate history data

        items.getItemsByTag("historic-data").forEach((it)=>{
          let histIt = items.getItem(it.name + "History", true);
          if (histIt) {
            calcHistoryForItem(it, histIt);
          }
        });
    type: script.ScriptAction

I should notice that this rule works for item that contains measured values for one month (so it gets reset to zero at the beginning of each month). If you have item with continuous counter you would have to change the part that gets monthly value.

2 Likes

The approach you’re describing with 28 separate items would technically work, but you’re right, that’s a lot of overhead and maintenance. There’s definitely a cleaner way using persistence.

What you actually want is to capture two specific readings each day: the meter values at midnight and just before midnight. Then you can calculate the daily difference without storing every single day’s readings as separate items.

With persistence set up, you can simply query historic values. For example, you’d have one consumption item and one production item that get updated in real time. Then in your rule or script, you’d ask the persistence service for the state of those items at 00:01 and at 23:59. Subtract one from the other and you’ve got your daily totals.
Persistence is exactly the solution here. No need for dozens of items, just smart queries against your time series data.

But you cannot query persistence from the UI.

Thank you so much @djal !

Fantastic job!

1 Like