Grafana chart with time ranges

Iā€™ve just upgraded to OH 3.4.4-2, and this problem still occurs.

Ok so I found your widget code in that discussion you posted about (somewhere at the bottom of the conversation):

I copied and pasted that and created a widget locally, and it worked. So I believe the problem is the marketplace, or how/what is being downloaded. I donā€™t know if you can at least check that the widget in the marketplace is not corrupt or something?

I have installed other marketplace widgets, all successfully. So this hopefully points you in the right direction.

Thank you for troubleshootingā€¦

I will make a note in the first post suggesting this workaround until the issue with the marketplace is fixed.

This looks stellar Rob!
Any chance youā€™ll share the yaml for it?
I love this.

Hi @maxmaximax thanks for sharing your work!

[UPDATE] [SOLVED]: from the in-depth post on this subject:

So far, this is really close to being 100% in OH4.01 except for one minor irritance: the frame shows the full dashboard controls.

I made sure to copy the link only to the panel, but in my widget, it continually gives me the dashboard controls.

Any idea what I might be doing incorrectly?

Thanks again, great stuff!

Glad you like the widget :grinning:
Worth noting that I have not yet upgraded to OH4 - and will not upgrade any time soon due lack of time.

Have you tried this?

Hope this works for you.

1 Like

Thanks @maxmaximax
I should have updated my post. I was using an external link instead of ā€˜share embedā€™, which is what was causing the controls issue.
After using the proper link, it works fine (I do have to log back into grafana via the widget from time to time, as I could not find a way for it to store cookies, but thatā€™s a different issue).

Works great, thanks, and I do, I really like it!

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

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.