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 thef7-card
and not directly to theoh-chart
. The chart family of components are not defined using vue and so will not work with thekey
property. To use this method for refreshing a chart you must have the chart as the child of a component that does accept thekey
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. Thekey
property can be used in the pageconfig
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:
With the rule enabled and running, the chart widget above will automatically refresh every 5 minutes.