Auto-refresh charts and widgets in MainUI

As people migrate over to OH3 and the MainUI from other UIs, many are surprised to find that MainUI charts are static on a loaded page and won’t refresh unless the page is reloaded. Here I’m providing a fairly simple solution that allows you to set up timed, automatic refreshes for charts on a page (or another widget that you have configured with custom content that doesn’t refresh).


When Do Widgets Refresh?

So many of the standard widgets and components do respond to changes in item states that users come to expect all widgets to respond to any changes they can imagine. Which features refresh and which don’t becomes a little more understandable if you consider the UI is subject to the same event restrictions as everything else. The MainUI gathers available information from the core OH through the event stream and then most additional two-way communication is only as necessary through calls to the API.

Changes in an item’s state produce events, and these changes are then easy for the UI to mirror in the items dictionary available to the widgets. Those changes are readily visible to the widget builder and the widget gets refreshed as expected. So any widget using a property that includes the state or displayState from the items dictionary, such as,

- component: Label
  config:
    text: =items.OutsideTemp.displayState

will refresh accordingly.

Changes to things outside the OH system, however, do not cause events nor give MainUI any other possible trigger for a refresh. Perhaps the most common example of this that appears in forum questions is image files. Many of the widgets can accept a url for an image to use as a background and often users have a system where that image is updated regularly by some other process (security camera, for example). MainUI does not have a built-in folder monitoring service and so cannot know when that image changes and therefore a widget such as

- component: oh-image-card
  config:
    url: /static/FrontCamera.jpg

can’t refresh even when your security camera saves a new image to that file.

Changes to information that the UI only gets through API calls also won’t result in widget updates because the UI doesn’t know that information has changed without constant polling of the API, an unnecessary waste of bandwidth 99.9% of the time. This kind of widget information is most often found in an oh-repeater, such as:

  • the metadata collected via the fetchMetadata property
  • the list of a group’s member items
  • the list of items with a particular tag

The other place where API calls from within a widget are common is charts and trendlines. The data for those are obtained via calls to the persistence API. MainUI simply has no way of knowing that a persistence table has changed other than being manually forced (via page refresh) to rebuild the widget and make a new API call.

The following solution can be used to address any of these situations (image file, repeater, or persistence) and many other related ones, but for this example we’ll use a chart.

Force A Refresh

Most of the available components for custom widgets use a vue template as their definition. The vue renderer, in order to optimize performance, tends to avoid re-rendering any element it finds to be static. In order to be able to manually supersede this priority, vue has a key property for elements. Whenever this property changes in an element it forces vue to re-render that element. We can use this to our advantage by adding the key property to any component we want to force to refresh and making sure we control how/when the value of that key property changes.

Before we do this, we should take one more factor into account: the vue docs specify:

Children of the same common parent must have unique keys . Duplicate keys will cause render errors.

This is easy within the widget code because we have access to the Math.random() method which will generate a random number. As long as we combine that random number with some item state, we have generated a unique key property that will force a widget component to refresh whenever we change the state of that item.

Here’s an example of a widget that contains a basic chart of the outside temperature over the last hour:

uid: demo:chart_refresh
tags: []
props:
  parameters: []
  parameterGroups: []
component: f7-card
config:
  title: Refreshing oh-chart
  key: =Math.random() + items.UI_Refresh_Timer.state
slots:
  default:
    - component: oh-chart
      config:
        label: Outside Temperature
        period: h
      slots:
        grid:
          - component: oh-chart-grid
            config: {}
        xAxis:
          - component: oh-time-axis
            config:
              gridIndex: 0
        yAxis:
          - component: oh-value-axis
            config:
              gridIndex: 0
        series:
          - component: oh-time-series
            config:
              name: Temperature
              gridIndex: 0
              xAxisIndex: 0
              yAxisIndex: 0
              type: line
              item: OutsideTemp
              animation: false

The f7-card that forms the base for this simple chart widget has a key configured as described above. We can see that the item it is linked to is named UI_Refresh_Timer.

A couple of important notes here:

  • It doesn’t matter at all what type of item you use or the state that it changes to. It could be a Switch Item that toggles, or a Number Item that increments, or a DateTime Item that gets set to the current time, etc. All that matters is that it is an item that changes to a new state.

  • The key property has been applied to the f7-card and not directly to the oh-chart. The chart family of components are not defined using vue and so will not work with the key property. To use this method for refreshing a chart you must have the chart as the child of a component that does accept the key property. This doesn’t make a significant difference because refreshing any component will refresh the children components as well. This can be a benefit if, for example, you have one page with several charts. The key property can be used in the page config and all the charts will be updated.

  • At the moment this chart does not auto-refresh, it only refreshes whenever someone manually changes the state of UI_Refresh_Timer.

Auto-refresh

To upgrade from a manual refresh to an automatic refresh, all we have to do is create a simple rule. We want a rule that runs periodically (cron trigger) and changes the state of the item in our key property (item action or script action). For this example I’ve set UI_Refresh_Timer as DateTime Item and every 5 minutes a rule will change the state of this item to the current time. Here’s the sample rule with the cron setting for every 5 minutes:

The script (in this case, ECMAScript-2021 from the JSscripting add-on) is:

var runtime = require('@runtime');
var ZonedDateTime = Java.type("java.time.ZonedDateTime");
var now = ZonedDateTime.now();

runtime.events.sendCommand('UI_Refresh_Timer', now.toLocalDateTime().toString());
DSL version
UI_Refresh_Timer.sendCommand(now.toLocalDateTime().toString())
Blockly version

At the time of this writing, I don’t believe Blockly has a block that efficiently converts the current time into a format that can be passed to a DateTime item. However, remember, the choice of item type is arbitrary so a similar solution would be to make UI_Refresh_Timer a String Item instead and then a roughly equivalent Blockly script would look like this:
image

With the rule enabled and running, the chart widget above will automatically refresh every 5 minutes.

11 Likes

Based on you UI_Refresh_Timer, I was trying to refresh a (webcam) image. But seems it’s not refreshing in my case. Guess it has something to do with the ‘child of a component’ in the f7-card? But can’t figure out how I can make it work. Maybe an extra component or so? :blush:

uid: widget_refresh
tags: []
props:
  parameters: []
  parameterGroups: []
timestamp: Feb 16, 2022, 9:17:16 AM
component: f7-card
config:
  title: Widget refreshing
  key: =Math.random() + items.UI_Refresh_Timer.state
slots:
  default:      
    - component: f7-card
      slots:
        default:
          - component: oh-image
            config:
              noBorder: true
              url: =props.thumbnailURL

My timer on a 5sec refresh:

2022-02-16 09:14:20.876 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'UI_Refresh_Timer' received command 2022-02-16T08:56:58.444686
2022-02-16 09:14:25.875 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'UI_Refresh_Timer' received command 2022-02-16T08:56:58.444686
2022-02-16 09:14:30.875 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'UI_Refresh_Timer' received command 2022-02-16T08:56:58.444686

The problem is your timer item. There is a rule running every 5 seconds, yes, but it is sending the same command every time (note that in the command the seconds and microseconds aren’t changing). If the item state isn’t changing then the key value isn’t changing and no refresh is being forced.

Can you post the rule you’re using?

You’re right, the rule didn’t change anything. I’ve just updated it a bit so it updates the string.

New rule:

rule "TEST --- Refresh images"
when
        Time cron "0/5 * * * * ?"
then
        var UInow = ZonedDateTime.now()
        logInfo("ELEC", "TIJD091")
        UI_Refresh_Timer.sendCommand(UInow.toLocalDateTime().toString())
end

log:

2022-02-16 16:56:30.169 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'UI_Refresh_Timer' changed from 2022-02-16T16:56:25.166862 to 2022-02-16T16:56:30.167774
2022-02-16 16:56:35.172 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'UI_Refresh_Timer' received command 2022-02-16T16:56:35.171360
2022-02-16 16:56:35.173 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'UI_Refresh_Timer' changed from 2022-02-16T16:56:30.167774 to 2022-02-16T16:56:35.171360
2022-02-16 16:56:40.172 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'UI_Refresh_Timer' received command 2022-02-16T16:56:40.171496
2022-02-16 16:56:40.173 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'UI_Refresh_Timer' changed from 2022-02-16T16:56:35.171360 to 2022-02-16T16:56:40.171496

But with my current widget, still no updates of the image. Everytime I refresh the page manual, it works.

ps the image is an url, not sure if that can be an issue?

  thumbnailURL: http://URL:PORT/cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=USER&pwd=PWD

I don’t think so, in my testing I had a url working. On the other hand that was a url to a static resource that got written over by another service, so we can’t rule out the possibility that something like this call interferes somehow.

OK, I was going to ask that as well because the widget code you posted above won’t work. It calls a property that’s not actually defined, so I assume that you didn’t post the actual widget code if you can verify that the base widget does in fact work.

What happens if you move the key parameter into the oh-image configuration instead?

Doesn’t seem to do much.

Can I use that oh-image in a f7-card without problems? :blush:

Yes, that shouldn’t be a problem. That said, I’m not sure where the problem is in this case.

Now, the oh-image component does come with it’s own refresh setting:

refreshInterval Refresh Interval INTEGER

Refresh interval in milliseconds

have you tried setting that instead of using this workaround? That might tell us if the problem is the key property or the access to your url.

I thought this option didn’t work anymore.
That’s why I hoped that your way would bring a solution to show a ‘live’ webcam image.

The access to the url seems to be working. Each time I refresh the page, I’m getting nicely the ‘new’ image.

I haven’t heard any plans to depreciate it and it’s still in the oh-image definition, so it should still work.

The oh-image docs point out (for the reasons outlined in the original post), that if you have an image that is going to change the best thing to do is to capture that image as an Image Item because then it will be very easy to be responsive to changes in the image content. Recognizing, however, that this is not always trivial (it’s always possible, but it may require quite a bit of code depending on the image and the source) the refreshInterval property is included for exactly this kid of scenario.

There’s really only one way to find out if it still works, however. Give it a try.

Just to confirm that this isn’t working. :’(
When I manually refresh the page, the image is refreshed.


component: oh-image-card
config:
  url: http://URL/cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=USER&pwd=PWD
  refreshInterval: 5000
slots: null

I’ll try to figure out how I can put this url in a item (thing)…

That’s strange. Then the problem must have something to do with the how the url is fetched, but I can’t for the life me of imagine why.

refreshInterval does work. The following simple widget:

uid: refresh:url
props:
  parameterGroups: []
  parameters: []
tags: []
component: oh-image-card
config:
  title: Refresh Url
  url: 'https://picsum.photos/200/200'
  refreshInterval: 1000

produces
refreshurl

If you want to try to debug this method further, then you should look in your browser tools and see if there are any errors that show up when trying to use refreshInterval.

Get it working a little bit.
On a server with OH3.0 it fails, on a testing OH3.2 it works?

Also found out that in some cases, it works better with firefox. Chrome is spitting out a ‘err_connection_reset’. Seems that google is trying to get an https instead of the http. Seems he thinks he knows it better. But I don’t think that these webcams will be able to provide https… :cry:

Very cool, thanks for this idea! I can refresh trendlines on my tablet now automatically.

I’d wish that OH had a setting to refresh trendlines (or charts) globally every X minutes or seconds.

Spoke too soon. It does not work when I set the key on the page config, which is a bummer.
It does work when I set it on the widget, but only for custom widgets, not for OH standard widgets.
Seems like I have to set it for F7 components.
For example:

component: f7-card
config:
  key: =Math.random() + items.UI_Refresh_Timer.state

Bummer! When I add “key” to my custom widget, then scoped stylesheets do not work anymore.
So, if I add this to my widget on my page, it is not being used anymore:

config:
  stylesheet: |
    .card-header {
      min-height: 30px;
      height: 30px;
      max-height: 30px;
      line-height: 1;      
    }

In my tests it does work with the page config, so I’m not sure what’s going on. This example works for me, properly updating the time in both the card labels:

config:
  label: Refresh Test
  key: =Math.random() + items.UI_Refresh_Timer.state
blocks:
  - component: oh-block
    config: {}
    slots:
      default:
        - component: oh-grid-cells
          config: {}
          slots:
            default:
              - component: oh-label-card
                config:
                  title: Static Time 1
                  label: =dayjs().format('HH:mm:ss')
              - component: oh-label-card
                config:
                  title: Static Time 2
                  label: =dayjs().format('HH:mm:ss')
masonry: null
grid: []
canvas: []

It should work for most OH widgets. I don’t know which ones you tried. But, for most OH widgets the config parameter is passed on directly to the underlying f7 widget so there should be no difference in they key behavior (as far as I know). Just a simple modification of the test above shows that it does work for oh-label-cards:

config:
  label: Refresh Test
blocks:
  - component: oh-block
    config: {}
    slots:
      default:
        - component: oh-grid-cells
          config: {}
          slots:
            default:
              - component: oh-label-card
                config:
                  key: =Math.random() + items.UI_Refresh_Timer.state
                  title: Static Time 1
                  label: =dayjs().format('HH:mm:ss')
              - component: oh-label-card
                config:
                  title: Static Time 2
                  label: =dayjs().format('HH:mm:ss')
masonry: null
grid: []
canvas: []

The above code causes the first time to update, but the second time remains static as expected.

I have never looked into this before, so I don’t know if there are tricks or special circumstances, but again, my quick and dirty test shows at least that the basics are working. I can get blue time labels that update with:

config:
  label: Refresh Test
  key: =Math.random() + items.UI_Refresh_Timer.state
  stylesheet: >
    .item-inner {
      color: blue
    }
blocks:
  - component: oh-block
    config: {}
    slots:
      default:
        - component: oh-grid-cells
          config: {}
          slots:
            default:
              - component: oh-label-card
                config:
                  title: Static Time 1
                  label: =dayjs().format('HH:mm:ss')
              - component: oh-label-card
                config:
                  title: Static Time 2
                  label: =dayjs().format('HH:mm:ss')
masonry: null
grid: []
canvas: []

There must be some other things going on with your system or configuration. Off the top of my head, I have no idea what the issue(s) may be, but if you post the whole code you’re trying to use from the widgets or the pages, I’ll take a look and see if I can find anything.

Thanks for your reply. For testing, I set my rule to refresh the item every 10s and I check the network tab of the browser console if the UI requests the persisted data (of course, you also see the trendline redraw).
I know that it is basically working, because I got it working under the mentioned restrictions.

So, this is my page example. I have a page with a grid. I switch from the UI preview to code and changed the code like this:

config:
  colNum: 9
  fixedType: grid
  hideNavbar: true
  label: Main
  layoutType: fixed
  screenHeight: 800
  screenWidth: 1280
  sidebar: true
  order: "1"
  key: =Math.random() + items.UI_Refresh_Timer.state
blocks: []
masonry: null
grid:
  - component: oh-grid-item
[...]

Then I save and load the page in non-edit mode.
Nothing in the network tab every 10s.

Next example: oh-label-card added to the page with this code:

component: oh-label-card
config:
  action: navigate
  actionModal: page:page_810a3b86e5
  actionModalConfig: {}
  actionPage: page:page_810a3b86e5
  item: CO2
  trendItem: CO2
  key: =Math.random() + items.UI_Refresh_Timer.state

No calls to persisted state every 10s.

Now my custom widget with key set and it is working (persisted state is fetched every 10s and trendline is being redrawn):

uid: LabelCardNoPadding
tags: []
props:
  parameters:
    - description: Title
      label: Title
      name: title
      required: false
      type: TEXT
    - description: Font size of the label
      label: Font size
      name: fontSize
      required: false
      type: TEXT
    - context: item
      description: An item to control
      label: Item
      name: item
      required: false
      type: TEXT
    - description: Group to show on click
      label: Group
      name: group
      required: false
      type: TEXT
    - description: Page to navigate on click
      label: Page
      name: page
      required: false
      type: TEXT
    - context: item
      description: Analyze items on click
      label: Analyze items
      name: analyzeItems
      required: false
      type: TEXT
      multiple: true
    - description: No unit
      label: No unit
      name: noUnit
      required: false
      type: BOOLEAN
    - description: No padding
      label: No padding
      name: noPadding
      required: false
      type: BOOLEAN
    - context: item
      description: Trend item
      label: Trend item
      name: trendItem
      required: false
      type: TEXT
    - description: Line Height
      label: Line Height
      name: lineHeight
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Jun 20, 2022, 1:55:50 PM
component: f7-card
config:
  noBorder: true
  noShadow: true
  outline: false
  padding: false
  key: "=(props.trendItem) ? Math.random() + items.UI_Refresh_Timer.state : ''"
  style:
    --f7-safe-area-left: 0
    --f7-safe-area-right: 0
    --widget-line-height: =props.lineHeight || 'unset'
    --widget-padding: "=(props.noPadding) ? '0': '1rem'"
  title: =props.title
slots:
  content:
    - component: oh-label-card
      config:
        action: "=(props.group) ? 'group' : (props.page) ? 'navigate' : (props.analyzeItems) ? 'analyzer': ''"
        actionGroupPopupItem: "=(props.group) ? props.group : ''"
        actionPage: "=(props.page) ? 'page:' + props.page : ''"
        actionAnalyzerItems: =props.analyzeItems
        fontSize: =props.fontSize
        itemX: =props.item
        label: "=(props.noUnit) ? items[props.item].state.split(' ')[0] : items[props.item].displayState || items[props.item].state"
        noShadow: true
        stylesheet: >
          .item-content {
            padding: var(--widget-padding);
            line-heightX: 1;
            line-height: var(--widget-line-height);
            white-space: nowrap;
          } .label-card-content .trend {
            opacity: .4;
          }
        titleX: =props.title
        trendItem: =props.trendItem

I add it to the page with this code:

component: widget:LabelCardNoPadding
config:
  fontSize: 35px
  item: HM_WandthermostatWohnzimmer_1_ActualTemperature
  lineHeight: "1"
  noPadding: false
  stylesheet: >
    .card-header {
      min-height: 30px;
      height: 30px;
      line-height: 1;      
    }
  title: Wohnzimmer
  trendItem: HM_WandthermostatWohnzimmer_1_ActualTemperature
  analyzeItems:
    - HM_WandthermostatWohnzimmer_1_ActualTemperature

After adding the key to my custom widget, the stylesheet in this code has no effect anymore.
When I inspect the HTML code, it is simply not there like it is without the key.

Interesting. Then I’m guessing many of these issues have something to do with the fixed layout pages. I don’t use the fixed layout pages much at all, so I don’t have a lot of advice here, alas.

I do know that the fixed layout page has some fundamental differences to the responsive layout page, so it is entirely possible that the fixed layout pages don’t support some of these things.

Do the stylesheets inside the widget still apply properly in this case? You could try moving that particular stylesheet up to the page config (you might need to add a class to the widget and increase the specificity of the selector).

Late to the party but I’ve been reminded just today that the oh-time-series and oh-data-series actually support expressions. This was introduced in:

So I’ve toyed around with this and it appears to be quite powerful as you can use it even in chart pages to change series on the fly (there’s a pending PR opened today to extend this to axes and potentially other chart components as well: Configuration of axis with support for expressions. by splatch · Pull Request #1419 · openhab/openhab-webui · GitHub)

So with a time series defined like this:

  series:
    - component: oh-time-series
      config:
        id: =items.aaa_Sw.state
        name: "=items.aaa_Sw.state === 'ON' ? 'Temperature' : 'Light'"
        gridIndex: 0
        xAxisIndex: 0
        yAxisIndex: 0
        type: line
        item: "=items.aaa_Sw.state === 'ON' ? 'FlowerCare_Temperature' : 'FlowerCare_Light'"
        color: "=items.aaa_Sw.state === 'ON' ? 'red' : 'yellow'"

You can actually change the item to chart along with other options.

chrome_2022-06-20_21-00-06

I’m not 100% positive but I believe if any option is changed (for example id) the series will be redrawn and therefore refresh with the latest data, which achieves the use case of this thread at least for charts. Hope support for other series & components will be coming soon.

2 Likes

Yes, the stylesheets inside the custom widget code still work. It looks like stylesheets are disabled only on the level above where I add the “key” property or something like this. Maybe those scoped stylesheets need the “key” property for themselves.
Moving the stylesheet for the card header height to the page level is a great idea. This works together with the “key” property in the custom widget code and it also makes it unnecessary to add this to every widget.