Grafana chart with time ranges

I haven’t used grafana for over a year but does this still work?

Yes, it works. At least on OH 3.4.4.

On 4.0.1 is doesn’t work, same hex code in the widget library, but when adding it, nothing happens

1 Like

For others it works on OH 4.01.

Maybe try creating the widget manually by copying the YAML from the first post?

Okay so i just spent 2 hours sorting through this and finally found out what my problem was. I’m posting this here to hopefully save someone else the trouble.

When editing grafana.ini you need to remove the “;” from the beginning of each line that you change. Else the change WILL NOT apply. also dont forget to restart the Grafana service.

That’s why I like using vim editor as both the # and ; in the grafana.ini would have been blue coloured text meaning they are comments. If they are not comments then they are white text.
It makes it easier to look for un-commented statements. :grinning:

I would like to share the following extension:

In addition to different time ranges, different grafana pages can be selected as well:

Code:

uid: Grafana_charts_with_timeranges_MikeTheTux
tags: []
props:
  parameters:
    - description: Title of the chart
      label: Title
      name: title
      required: false
      type: TEXT
    - description: Grafana URL with "{period}" and "{panel}" placeholder. Example: "http://nas:3000/d-solo/VCS_VtN4k/openhab?orgId=1&{period}&panelId={panel}"
      label: Grafana source URL
      name: URL
      required: true
      type: TEXT
    - default: from=now-6h&to=now,6h;from=now-12h&to=now,12h;from=now-1d&to=now,24h;from=now/d&to=now/d,Day;from=now-3d&to=now,Last 3 Days;from=now-7d&to=now,Last 7 Days;from=now-14d&to=now,Last 14 Days
      description: List of time ranges (separated with semicolon). Example: "from=now-6h&to=now,-6h;from=now-1d/d&to=now-1d/d,yesterday" for past "6h" and "yesterday". First entry is default.
      label: Grafana time range options
      name: timerange
      required: true
      type: TEXT
    - default: 27,PV Status;22,PV Forecast
      description: List of panels (separated with semicolon). Example: "27,PV Status;22,PV Forecast" for two panels with ID "27" and "22". First entry is default.
      label: Grafana Panel IDs
      name: panel
      required: true
      type: TEXT
    - description: Height of the Frame (empty = default)
      label: Height
      name: height
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Oct 29, 2023, 9:59:50 AM
component: f7-card
config:
  title: =props.title
  outline: true
  style:
    --f7-card-margin-horizontal: 10px
    --f7-card-margin-vertical: 3px
    --f7-card-padding-horizontal: 10px
    --f7-card-padding-vertical: 100px
    margin-top: 10px
    margin-bottom: 10px
    noShadow: false
    border-radius: var(--f7-card-expandable-border-radius)
    box-shadow: 5px 5px 10px 1px rgba(0,0,0,0.3)
slots:
  default:
    - component: oh-webframe-card
      config:
        borders: false
        noBorder: false
        noShadow: true
        height: =props.height
        src: =props.URL.replace('{period}', vars.selectedPeriod || [props.timerange.split(';')[0].split(',')[0]]).replace('{panel}', vars.selectedPanel || [props.panel.split(';')[0].split(',')[0]])
        class:
          - display-block
    - component: f7-segmented
      config:
        round: false
        outline: false
        class:
          - padding-bottom-half
        style:
          margin-left: 10px
          margin-right: 10px
          --f7-button-font-size: 14px
          --f7-button-text-color: "=themeOptions.dark === 'light' ? 'black' : 'white'"
          --f7-button-text-transform: none
          --f7-button-border-radius: 4px
          --f7-button-outline-border-width: 1px
          --f7-button-font-weight: 300
          --f7-button-padding-vertical: 0px
          --f7-button-padding-horizontal: 0px
      slots:
        default:
          - component: oh-repeater
            config:
              sourceType: range
              for: size
              fragment: true
            slots:
              default:
                - component: oh-repeater
                  config:
                    fragment: true
                    for: period
                    in: =[props.timerange.split(";")[loop.size].split(",")[1]]
                  slots:
                    default:
                      - component: oh-button
                        config:
                          text: =loop.period
                          fill: "=(([props.timerange.split(';')[loop.size].split(',')[0]] == vars.selectedPeriod) || (props.timerange.split(';')[0].split(',')[1] === loop.period) && !vars.selectedPeriod) ? true : false"
                          round: false
                          outline: true
                          style:
                            --f7-button-border-color: var(--f7-card-outline-border-color)
                          action: variable
                          actionVariable: selectedPeriod
                          actionVariableValue: =props.timerange.split(";")[loop.size].split(",")[0]
    - component: f7-segmented
      config:
        round: false
        outline: false
        class:
          - padding-bottom-half
        style:
          margin-left: 10px
          margin-right: 10px
          --f7-button-font-size: 14px
          --f7-button-text-color: "=themeOptions.dark === 'light' ? 'black' : 'white'"
          --f7-button-text-transform: none
          --f7-button-border-radius: 4px
          --f7-button-outline-border-width: 1px
          --f7-button-font-weight: 300
          --f7-button-padding-vertical: 0px
          --f7-button-padding-horizontal: 0px
      slots:
        default:
          - component: oh-repeater
            config:
              sourceType: range
              for: size
              fragment: true
            slots:
              default:
                - component: oh-repeater
                  config:
                    fragment: true
                    for: panel
                    in: =[props.panel.split(";")[loop.size].split(",")[1]]
                  slots:
                    default:
                      - component: oh-button
                        config:
                          text: =loop.panel
                          fill: "=(([props.panel.split(';')[loop.size].split(',')[0]] == vars.selectedPanel) || (props.panel.split(';')[0].split(',')[1] === loop.panel) && !vars.selectedPanel) ? true : false"
                          round: false
                          outline: true
                          style:
                            --f7-button-border-color: var(--f7-card-outline-border-color)
                          action: variable
                          actionVariable: selectedPanel
                          actionVariableValue: =props.panel.split(";")[loop.size].split(",")[0]

Have fun!

1 Like

I have also made my own adjustments to this widget by adding another row of buttons that allows going back several days/hours… but not changing the overall duration of the displayed range.

There are some additional tweaks to the styling for better visual integration in block layouts.

uid: vas_grafana_picker
tags: []
props:
  parameters:
    - description: Title of the chart
      label: Title
      name: title
      required: false
      type: TEXT
    - description: URL to show in the frame
      label: Source URL
      name: URL
      required: true
      type: TEXT
    - default: from=now-{fcount}h&to=now-{tcount}h;6h;6,from=now-{fcount}h&to=now-{tcount}h;12h;12,from=now-{fcount}h&to=now-{tcount}h;24h;24,from=now-{tcount}d/d&to=now-{tcount}d/d;Day;1,from=now-{fcount}d&to=now-{tcount}d;Last 3 Days;3,from=now-{fcount}d&to=now-{tcount}d;Last 7 Days;7,from=now-{fcount}d&to=now-{tcount}d;Last 14 Days;14
      description: Comma-separated List of options. Example "from=now-6h&to=now;-6h,from=now-1d/d&to=now-1d/d;yesterday" for past "6h" and "yesterday". First entry is default.
      label: Time range options
      name: timerange
      required: true
      type: TEXT
    - default: Previous,Now,Next
      description: Labels for previous, now and next buttons as comma separated list
      label: Second line buttons labels
      name: prevNowNext
      required: false
      type: TEXT
    - default: "0"
      description: Index of default time range
      label: Time range default
      name: rangeDefault
      required: false
    - description: Height of the Frame (empty = default)
      label: Height
      name: height
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Nov 30, 2023, 12:20:38 AM
component: f7-card
config:
  title: =props.title
  outline: false
  style:
    --f7-card-margin-horizontal: 10px
    --f7-card-margin-vertical: 10px
    --f7-card-padding-horizontal: 10px
    --f7-card-padding-vertical: 100px
    margin-top: 10px
    margin-bottom: 10px
    padding-top: 1px
    box-shadow: var(--f7-card-box-shadow)
slots:
  default:
    - component: oh-webframe-card
      config:
        borders: false
        noBorder: false
        noShadow: true
        height: =props.height
        src: = props.URL.replace('{period}', (vars.sP?.period || props.timerange.split(',')[props.rangeDefault].split(';')[0]) .replaceAll('{fcount}', (vars.sP?.count || props.timerange.split(',')[props.rangeDefault].split(';')[2])*((vars.pIdx||0)+1)) .replaceAll('{tcount}', (vars.sP?.count || props.timerange.split(',')[props.rangeDefault].split(';')[2])*(vars.pIdx||0)))
        class:
          - display-block
    - component: f7-segmented
      config:
        round: false
        outline: false
        class:
          - padding-bottom-half
        style:
          margin-left: 10px
          margin-right: 10px
          --f7-button-font-size: 14px
          --f7-button-text-color: "=themeOptions.dark === 'light' ? 'black' : 'white'"
          --f7-button-text-transform: none
          --f7-button-border-radius: 4px
          --f7-button-outline-border-width: 1px
          --f7-button-font-weight: 300
          --f7-button-padding-vertical: 0px
          --f7-button-padding-horizontal: 0px
      slots:
        default:
          - component: oh-button
            config:
              text: =props.prevNowNext.split(',')[0]
              round: false
              outline: true
              style:
                --f7-button-border-color: var(--f7-card-outline-border-color)
              action: variable
              actionVariable: pIdx
              actionVariableValue: =(vars.pIdx || 0)+1
          - component: oh-button
            config:
              text: =props.prevNowNext.split(',')[1]
              fill: = (vars.pIdx == undefined || vars.pIdx == 0 )
              round: false
              outline: true
              style:
                --f7-button-border-color: var(--f7-card-outline-border-color)
              action: variable
              actionVariable: pIdx
              actionVariableValue: 0
          - component: oh-button
            config:
              text: =props.prevNowNext.split(',')[2]
              round: false
              outline: true
              disabled: = (vars.pIdx == undefined || vars.pIdx == 0 )
              style:
                --f7-button-border-color: var(--f7-card-outline-border-color)
              action: variable
              actionVariable: pIdx
              actionVariableValue: =(vars.pIdx || 0)-1
    - component: f7-segmented
      config:
        round: false
        outline: false
        class:
          - padding-bottom-half
        style:
          margin-left: 10px
          margin-right: 10px
          --f7-button-font-size: 14px
          --f7-button-text-color: "=themeOptions.dark === 'light' ? 'black' : 'white'"
          --f7-button-text-transform: none
          --f7-button-border-radius: 4px
          --f7-button-outline-border-width: 1px
          --f7-button-font-weight: 300
          --f7-button-padding-vertical: 0px
          --f7-button-padding-horizontal: 0px
      slots:
        default:
          - component: oh-repeater
            config:
              sourceType: range
              for: size
              fragment: true
            slots:
              default:
                - component: oh-repeater
                  config:
                    fragment: true
                    for: period
                    in: =[props.timerange.split(",")[loop.size].split(";")[1]]
                  slots:
                    default:
                      - component: oh-button
                        config:
                          text: =loop.period
                          fill: "=((loop.size == vars.sP?.index) || loop.size == (props.rangeDefault || 0) && !vars.sP) ? true : false"
                          round: false
                          outline: true
                          style:
                            --f7-button-border-color: var(--f7-card-outline-border-color)
                          action: variable
                          actionVariable: sP
                          actionVariableValue: "= {'period': props.timerange.split(',')[loop.size].split(';')[0], 'count': props.timerange.split(',')[loop.size].split(';')[2], index: loop.size }"

Here’s what it looks like in context:

Hi @tarag
I’m working on the same widget with a similar concept, only I’m trying to use arrows to go forwards/backwards in time.

I’m trying to use your code as a startingpoint, but I’m struggling to understand 2 things:

  1. What is the purpose of “rangeDefault” and its use?

  2. When generating the URL, how is this line read:

vars.sP?.count || props.timerange.split(',')[props.rangeDefault].split(';')[2])

in particular what do the OR in this mean, is it bit-wise, or in case sP?.count is not specified then do the split thing?
Also how is the code on the right side of the OR statement read? I dont understand

[props.rangeDefault].split(';')[2])

when rangeDefault is set to “0” in the props

Thank you

Sorry I did not explain much. The computation of the url for the web card is a 2 step process.

  • Computing the from and to query string parameters using {fcount} and/or {tcount} tags in the selected timerange prop by multiplying the count parameter (3rd of each timerange), by the current position (0 upon display)
  • Replacing the {period} tag in the URL prop by the computed timerange in the previous step.

The rangeDefault prop, should rather be called defaultRange. It is the 0-starting index of the default selected range among all timerange prop values. I did not want it to be 0 as in the topic widget version (bu the default value of rangeDefaut in 0…). This is written in the prop description :wink:

vars.sP is the selected period, an object with period and count properties, when initialised. But since one can’t initialise vars in widgets, I have to take this case into account.

So the vars.sP?.count || props.timerange.split(',')[props.rangeDefault].split(';')[2]) means:
if vars.sP?.count is defined (when at least one of the range buttons has been selected by the user), use it, else use the count value from the timerange property, by taking the field at the third position (index 2) of the range at the rangeDefault index.

This line is buggy if count is 0, but there is no point in having count at 0 so I left it this way. Not being able to initialise vars in widgets is a real pain…

And it does work!

Screen Recording 2023-12-01 at 18.21.55

…with some nice query btw to get historical values one year before current ones

Awesome - thanks for your explanation, that was really helpful! :beers:

I’ve finished my widget with the help of your inputs, if I want to share the functioning of my widget, with a nice animation like yours, how can I do this, as it explains much better than trying to describe in words.

On my Mac, I record part of the screen using built-in function via Cmd-Shift-5, and then use Gifski app to convert to animated gif.

Cheers.

All done! Here is my widget.

OH_Grafana

Lots of thanks to maxmaximax for making the base widget and tarag for getting me to the finish line.

Like tarags this widget is made to handle historic data that is preformatted in duration for days, weeks, months and years, and you can go backwards/forwards in those steps by using the arrows.

It requires that you have a single grafana dashboard with 4 panels, one for each duration, and that the data in each panel is controlled by setting the absolute timerange. It does not matter what the range is set to in the dashboard - the widget takes care of setting the time ranges.

Here is the code:

Remember to change the panel numbers in the rangePanels property to match those of your grafana dashboard!

uid: Grafana chart with timeranges v2
tags: []
props:
  parameters:
    - description: Title of the chart
      label: Title
      name: title
      required: false
      type: TEXT
    - default: http://<your.ip.here>/<your.base.url.to.grafana.dashboard.here.ends.with.something.like."orgId=1&refresh=5m".{period}
      description: URL to show in the frame
      label: Source URL
      name: URL
      required: true
      type: TEXT
    - default: from=now%2Fd-{frange}h&to=now%2Fd-{trange}h&viewPanel=10&kiosk;Day;24,from=now%2Fw-{frange}d&to=now%2Fw-{trange}d&viewPanel=9&kiosk;Week;7,from=now%2FM-{trange}M-1d&to=now%2FM-{trange}M&viewPanel=11&kiosk;Month;1,from=now%2Fy-{frange}M&to=now%2Fy-{trange}M&viewPanel=12&kiosk;Year;12
      description: Comma-separated List of available panels and timeranges. Example "viewPanel=3&kiosk;Day,viewPanel=6&kiosk;Week," for past "Day" and "Week". First entry is default.
      label: Available Range Panels
      name: rangePanels
      required: true
      type: TEXT
    - default: "0"
      description: Index of default time range
      label: Time range default
      name: rangeDefault
      required: false
    - description: Height of the Frame (empty = default)
      label: Height
      name: height
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Dec 6, 2023, 9:00:26 PM
component: f7-card
config:
  style:
    --f7-card-margin-horizontal: 0px
    --f7-card-margin-vertical: 5px
    --f7-card-padding-horizontal: 0px
    --f7-card-padding-vertical: 0px
    box-shadow: 5px 5px 10px 1px rgba(0,0,0,0.3)
    margin-bottom: 5px
    margin-top: 5px
    noShadow: false
    padding-right: 0px
    padding-left: 0px
    padding-top: 0px
    padding-bottom: 0px
  title: =props.title
slots:
  default:
    - component: f7-block
      config:
        style:
          display: flex
          justify-content: center
          align-items: center
          padding-right: 0px
          padding-left: 0px
          padding-top: 0px
          padding-bottom: 0px
      slots:
        default:
          - component: oh-button
            config:
              action: variable
              actionVariable: sP
              actionVariableValue: = {'period':vars.sP.period, 'count':vars.sP.count, index:vars.sP.index, pIdx:((vars.sP.pIdx || 0)+1) }
              iconMaterial: chevron_left
              iconSize: 30
              style:
                display: flex
                justify-content: center
                align-self: center
                color: black
                width: 3%
                padding-right: 0px
                padding-left: 0px
                padding-top: 0px
                padding-bottom: 0px
                margin-right: 0px
          - component: f7-block
            config:
              borders: false
              noBorder: false
              noShadow: true
              style:
                width: 155%
                margin-left: 0px
                margin-right: 0px
                padding-right: 0px
                padding-left: 0px
                padding-top: 0px
                padding-bottom: 0px
            slots:
              default:
                - component: oh-webframe-card
                  config:
                    height: =props.height
                    src: = props.URL.replace('{period}', (vars.sP?.period || props.rangePanels.split(',')[props.rangeDefault].split(';')[0]) .replaceAll('{frange}', ((((vars.sP?.count || 0)*(vars.sP.pIdx || 0))+1) || 1)) .replaceAll('{trange}', (((vars.sP?.count || 0)*(vars.sP.pIdx || 0)) || 0)))
                    borders: false
                    noBorder: false
                    noShadow: true
          - component: oh-button
            config:
              visible: =(vars.sP.pIdx > 0) == true
              action: variable
              actionVariable: sP
              actionVariableValue: = {'period':vars.sP.period, 'count':vars.sP.count, index:vars.sP.index, pIdx:((vars.sP.pIdx || 0)-1) }
              iconMaterial: chevron_right
              iconSize: 30
              style:
                display: flex
                justify-content: center
                align-self: center
                color: black
                width: 3%
                padding-right: 0px
                padding-left: 0px
                padding-top: 0px
                padding-bottom: 0px
                margin-left: 0px
                margin-right: 0px
    - component: f7-segmented
      config:
        class:
          - padding-bottom-half
        outline: false
        round: false
        style:
          --f7-button-border-radius: 4px
          --f7-button-font-size: 14px
          --f7-button-font-weight: 300
          --f7-button-outline-border-width: 1px
          --f7-button-padding-horizontal: 0px
          --f7-button-padding-vertical: 0px
          --f7-button-text-color: "=themeOptions.dark === 'light' ? 'black' : 'white'"
          --f7-button-text-transform: none
          margin-left: 10px
          margin-right: 10px
      slots:
        default:
          - component: oh-repeater
            config:
              for: size
              fragment: true
              sourceType: range
            slots:
              default:
                - component: oh-repeater
                  config:
                    for: period
                    fragment: true
                    in: =[props.rangePanels.split(",")[loop.size].split(";")[1]]
                  slots:
                    default:
                      - component: oh-button
                        config:
                          action: variable
                          actionVariable: sP
                          actionVariableValue: = {'period':props.rangePanels.split(',')[loop.size].split(';')[0], 'count':props.rangePanels.split(',')[loop.size].split(';')[2], index:loop.size, pIdx:0 }
                          fill: "=((loop.size == vars.sP?.index) || loop.size == (props.rangeDefault || 0) && !vars.sP) ? true : false"
                          outline: true
                          round: false
                          style:
                            --f7-button-border-color: var(--f7-card-outline-border-color)
                          text: =loop.period

Hope this inspires someone, as tarags and maxmaximaxs did me :call_me_hand:

1 Like

Great to see how you improve the widget further!
Sad I cannot dedicate more time to this subject right now…

maxmaximax

Hello everyone,

I’m using InfluxDB and Grafana on my QNAP NAS (running in containers). This NAS is on the same local network as my Raspberry Pi 5 running openHAB. I’ve managed to get the Grafana widget working locally within my network after following the instructions and making some adjustments to the grafana.ini file.

However, when I try to access openHAB via the myopenHAB cloud service, the content of the Grafana widget is not displayed. I suspect this is related to accessing the widget from a HTTPS context (myopenHAB) but the Grafana instance being accessible via HTTP on the local network.

Has anyone successfully managed to display a Grafana widget in openHAB when accessing it through the myopenHAB cloud service? If so, could you please share your configuration and any specific steps you took to make it work?

Any help or suggestions would be greatly appreciated!

Thanks in advance.