sqlpostgresqlsolar

Solar-Heating: Data analytics for Grafana, advanced query


I would need some help with a very specific use case I have for my homelab. I do have some solar panels on my roof, and I do extract a lot of data points to my server. I am using a specific app for that, making it easy to consume and automate stuff for that data (iobroker). The data I do save into a progres database. (No questions please why not Influx or TimescaleDB, postgres is what I need to live with...) I use everything on docker right now, works perfectly. While I was able to create numerous dashboard on Grafana, display everything I like there, there is one specific "thing" I was unable to do, and after month of trying to get it done I finally ask for help. I do have a device supporting my heating from generated power to warm up the water. The device is using energy that we would normally feed back to the grid. The device is updating the power it pushes to the heating device pretty much every second. I am pulling the data from the device also every second. However I do have the logging configured in the way, that is only logs data when there is a difference to the previous datapoint. One example:

Time consumption in W
2018-02-21 12:00:00 3500
2018-02-21 12:00:01 1470
2018-02-21 12:00:02 1470
2018-02-21 12:00:03 1470
2018-02-21 12:00:00 1600

The second and third entry with the value of "1470" would not exist! So first issue I have is a missing data point(s). What I would like to achieve is to have a calculation showing the consumption by individual day, month, year and all-time.

This does not need to happen inside Grafana, and I don't think Grafana can do this at all. There are options to do similar things in Grafana, but they do not provide an accurate result ($__unixEpochGroupAlias(ts,1s,previous)). I do have every option that is needed to create the data, so there should not be any obstacle in your ideas, and store it again inside the DB.

The data is polled/stored every 1000ms, so every second. Idea is to use Ws (Watt-seconds) to easily calculate with accurate numbers, as well as to display them better in Wh or kWh.

The DB can be only queried with SQL - but as mentioned if calculations needs to be done in a different language or so, then this is also fine.

Tried everything I could think of. SQL queries, searching numerous posts, all avaialble SQL based Grafana options. Guess I need custom code, but that above my skillset.

Anything more you'd need to know? Let me know. Thanks in advance!

The data structure looks the following:

id=entry for the application to identify the datapoint ts=timestamp val=value in Ws

The other values are not important, but I wanted to show them for completeness.

 id |      ts       | val  | ack | _from | q
----+---------------+------+-----+-------+---
 23 | 1661439981910 | 1826 | t   |     3 | 0
 23 | 1661439982967 | 1830 | t   |     3 | 0
 23 | 1661439984027 | 1830 | t   |     3 | 0
 23 | 1661439988263 | 1828 | t   |     3 | 0
 23 | 1661439985088 | 1829 | t   |     3 | 0
 23 | 1661439987203 | 1829 | t   |     3 | 0
 23 | 1661439989322 | 1831 | t   |     3 | 0
 23 | 1661439990380 | 1830 | t   |     3 | 0
 23 | 1661439991439 | 1827 | t   |     3 | 0
 23 | 1661439992498 | 1829 | t   |     3 | 0
 23 | 1661440021097 | 1911 | t   |     3 | 0
 23 | 1661439993558 | 1830 | t   |     3 | 0
 23 | 1661440022156 | 1924 | t   |     3 | 0
 23 | 1661439994624 | 1830 | t   |     3 | 0
 23 | 1661440023214 | 1925 | t   |     3 | 0
 23 | 1661439995683 | 1828 | t   |     3 | 0
 23 | 1661440024273 | 1924 | t   |     3 | 0
 23 | 1661439996739 | 1830 | t   |     3 | 0
 23 | 1661440025332 | 1925 | t   |     3 | 0
 23 | 1661440052900 | 1694 | t   |     3 | 0
 23 | 1661439997797 | 1831 | t   |     3 | 0
 23 | 1661440026391 | 1927 | t   |     3 | 0
 23 | 1661439998855 | 1831 | t   |     3 | 0
 23 | 1661440027450 | 1925 | t   |     3 | 0
 23 | 1661439999913 | 1828 | t   |     3 | 0
 23 | 1661440028509 | 1925 | t   |     3 | 0
 23 | 1661440029569 | 1927 | t   |     3 | 0
 23 | 1661440000971 | 1830 | t   |     3 | 0
 23 | 1661440030634 | 1926 | t   |     3 | 0
 23 | 1661440002030 | 1838 | t   |     3 | 0
 23 | 1661440031694 | 1925 | t   |     3 | 0
 23 | 1661440053955 | 1692 | t   |     3 | 0
 23 | 1659399542399 |    0 | t   |     3 | 0
 23 | 1659399543455 |    1 | t   |     3 | 0
 23 | 1659399544511 |    0 | t   |     3 | 0
 23 | 1663581880895 | 2813 | t   |     3 | 0
 23 | 1663581883017 | 2286 | t   |     3 | 0
 23 | 1663581881952 | 2646 | t   |     3 | 0
 23 | 1663581884074 | 1905 | t   |     3 | 0
 23 | 1661440004144 | 1838 | t   |     3 | 0
 23 | 1661440032752 | 1926 | t   |     3 | 0
 23 | 1661440005202 | 1839 | t   |     3 | 0
 23 | 1661440034870 | 1924 | t   |     3 | 0
 23 | 1661440006260 | 1840 | t   |     3 | 0
 23 | 1661440035929 | 1922 | t   |     3 | 0
 23 | 1661440007318 | 1840 | t   |     3 | 0
 23 | 1661440036987 | 1918 | t   |     3 | 0
 23 | 1661440008377 | 1838 | t   |     3 | 0
 23 | 1661440038045 | 1919 | t   |     3 | 0
 23 | 1661440009437 | 1839 | t   |     3 | 0
 23 | 1661440039104 | 1900 | t   |     3 | 0
 23 | 1661440010495 | 1839 | t   |     3 | 0
 23 | 1661440040162 | 1877 | t   |     3 | 0
 23 | 1661440011556 | 1838 | t   |     3 | 0
 23 | 1661440041220 | 1862 | t   |     3 | 0
 23 | 1661440012629 | 1840 | t   |     3 | 0
 23 | 1661440042279 | 1847 | t   |     3 | 0
 23 | 1661440013687 | 1840 | t   |     3 | 0
 23 | 1661440043340 | 1829 | t   |     3 | 0
 23 | 1661440014746 | 1833 | t   |     3 | 0
 23 | 1661440044435 | 1817 | t   |     3 | 0
 23 | 1661440015804 | 1833 | t   |     3 | 0
 23 | 1661440045493 | 1789 | t   |     3 | 0
 23 | 1661440046551 | 1766 | t   |     3 | 0
 23 | 1661440016862 | 1846 | t   |     3 | 0
 23 | 1661440047610 | 1736 | t   |     3 | 0
 23 | 1661440048670 | 1705 | t   |     3 | 0
 23 | 1661440017920 | 1863 | t   |     3 | 0
 23 | 1661440049726 | 1694 | t   |     3 | 0
 23 | 1661440050783 | 1694 | t   |     3 | 0
 23 | 1661440018981 | 1876 | t   |     3 | 0
 23 | 1661440051840 | 1696 | t   |     3 | 0
 23 | 1661440055015 | 1692 | t   |     3 | 0
 23 | 1661440056071 | 1693 | t   |     3 | 0
 23 | 1661440322966 | 1916 | t   |     3 | 0
 23 | 1661440325082 | 1916 | t   |     3 | 0
 23 | 1661440326142 | 1926 | t   |     3 | 0
 23 | 1661440057131 | 1693 | t   |     3 | 0
 23 | 1661440327199 | 1913 | t   |     3 | 0
 23 | 1661440058189 | 1692 | t   |     3 | 0
 23 | 1661440328256 | 1915 | t   |     3 | 0
 23 | 1661440059247 | 1691 | t   |     3 | 0
 23 | 1661440329315 | 1923 | t   |     3 | 0
 23 | 1661440060306 | 1692 | t   |     3 | 0
 23 | 1661440330376 | 1912 | t   |     3 | 0
 23 | 1661440061363 | 1676 | t   |     3 | 0
 23 | 1661440331470 | 1913 | t   |     3 | 0
 23 | 1661440062437 | 1664 | t   |     3 | 0
 23 | 1663581885133 | 1678 | t   |     3 | 0
 23 | 1661440332530 | 1923 | t   |     3 | 0
 23 | 1661440064552 | 1667 | t   |     3 | 0
 23 | 1661440334647 | 1915 | t   |     3 | 0
 23 | 1661440335708 | 1913 | t   |     3 | 0
 23 | 1661440065608 | 1665 | t   |     3 | 0
 23 | 1661440066665 | 1668 | t   |     3 | 0
 23 | 1661440336763 | 1912 | t   |     3 | 0
 23 | 1661440337822 | 1913 | t   |     3 | 0
 23 | 1661440338879 | 1911 | t   |     3 | 0
 23 | 1661440068780 | 1664 | t   |     3 | 0
 23 | 1661440339939 | 1912 | t   |     3 | 0
(100 rows)```


iobroker=# \d ts_number
             Table "public.ts_number"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 id     | integer |           | not null |
 ts     | bigint  |           | not null |
 val    | real    |           |          |
 ack    | boolean |           |          |
 _from  | integer |           |          |
 q      | integer |           |          |
Indexes:
    "ts_number_pkey" PRIMARY KEY, btree (id, ts)



Solution

  • You can do this with a mix of generate_series() and some window functions.

    First we use generate_series() to get all the second timestamps in a desired range. Then we join to our readings to find what consumption values we have. Group nulls with their most recent non-null reading. Then set the consumption the same for the whole group.

    So: if we have readings like this:

    richardh=> SELECT * FROM readings;
     id |           ts           | consumption 
    ----+------------------------+-------------
      1 | 2023-02-16 20:29:13+00 |         900
      2 | 2023-02-16 20:29:16+00 |        1000
      3 | 2023-02-16 20:29:20+00 |         925
    (3 rows)
    

    We can get all of the seconds we might want like this:

    richardh=> SELECT generate_series(timestamptz '2023-02-16 20:29:13+00', timestamptz '2023-02-16 20:29:30+00', interval '1 second');
        generate_series     
    ------------------------
     2023-02-16 20:29:13+00
     2023-02-16 20:29:14+00
    ...etc...
     2023-02-16 20:29:29+00
     2023-02-16 20:29:30+00
    (18 rows)
    

    Then we join our complete set of timestamps to our readings:

    WITH wanted_timestamps (ts) AS (
        SELECT generate_series(timestamptz '2023-02-16 20:29:13+00', timestamptz '2023-02-16 20:29:30+00', interval '1 second')
    )
    SELECT 
        wt.ts
        , r.consumption
        , sum(CASE WHEN r.consumption IS NOT NULL THEN 1 ELSE 0 END)
          OVER (ORDER BY ts) AS group_num 
    FROM
        wanted_timestamps  wt
        LEFT JOIN readings r  USING (ts)
    ORDER BY wt.ts;
    
               ts           | consumption | group_num 
    ------------------------+-------------+-----------
     2023-02-16 20:29:13+00 |         900 |         1
     2023-02-16 20:29:14+00 |             |         1
     2023-02-16 20:29:15+00 |             |         1
     2023-02-16 20:29:16+00 |        1000 |         2
     2023-02-16 20:29:17+00 |             |         2
     2023-02-16 20:29:18+00 |             |         2
     2023-02-16 20:29:19+00 |             |         2
     2023-02-16 20:29:20+00 |         925 |         3
     2023-02-16 20:29:21+00 |             |         3
     2023-02-16 20:29:22+00 |             |         3
     2023-02-16 20:29:23+00 |             |         3
     2023-02-16 20:29:24+00 |             |         3
     2023-02-16 20:29:25+00 |             |         3
     2023-02-16 20:29:26+00 |             |         3
     2023-02-16 20:29:27+00 |             |         3
     2023-02-16 20:29:28+00 |             |         3
     2023-02-16 20:29:29+00 |             |         3
     2023-02-16 20:29:30+00 |             |         3
    (18 rows)
    

    Finally, fill in the missing consumption values:

    WITH wanted_timestamps (ts) AS (
        SELECT generate_series(timestamptz '2023-02-16 20:29:13+00', timestamptz '2023-02-16 20:29:30+00', interval '1 second')
    ), grouped_values AS (
        SELECT
            wt.ts
            , r.consumption
            , sum(CASE WHEN r.consumption IS NOT NULL THEN 1 ELSE 0 END)
            OVER (ORDER BY ts) AS group_num
        FROM wanted_timestamps wt
        LEFT JOIN readings r USING (ts)
    )
    SELECT
        gv.ts
        , first_value(gv.consumption) OVER (PARTITION BY group_num) 
            AS consumption
    FROM
        grouped_values gv 
    ORDER BY ts;
    
               ts           | consumption 
    ------------------------+-------------
     2023-02-16 20:29:13+00 |         900
     2023-02-16 20:29:14+00 |         900
     2023-02-16 20:29:15+00 |         900
     2023-02-16 20:29:16+00 |        1000
     2023-02-16 20:29:17+00 |        1000
     2023-02-16 20:29:18+00 |        1000
     2023-02-16 20:29:19+00 |        1000
     2023-02-16 20:29:20+00 |         925
     2023-02-16 20:29:21+00 |         925
     2023-02-16 20:29:22+00 |         925
     2023-02-16 20:29:23+00 |         925
     2023-02-16 20:29:24+00 |         925
     2023-02-16 20:29:25+00 |         925
     2023-02-16 20:29:26+00 |         925
     2023-02-16 20:29:27+00 |         925
     2023-02-16 20:29:28+00 |         925
     2023-02-16 20:29:29+00 |         925
     2023-02-16 20:29:30+00 |         925
    (18 rows)