In two previous widget building tutorials, I’ve gone step-by-step through my process for creating custom widgets. Those tutorials have omitted one important part of the process which should be obvious to anyone who has ever tried to code/script/command line anything: It never goes right the first time, and now you have to fix it. So, let’s fix some widgets that have gone wrong.
This is the third in a series of planned tutorials walking through many aspects of widget creation. This tutorial is going to go over some common pain points during widget creation and the most important tools at your disposal to get past those points.
Custom widget tutorials
- How To Build A Custom Widget
- How To Build An Advanced Custom Widget
- How To Troubleshoot Custom Widgets
Why does my widget not look right?
As discussed in the previous tutorials, there is a direct pathway from the YAML configuration for a widget to the HTML of the page being displayed. If the widget doesn’t look right, at the most basic level, that means the webpage code, either the HTML or the CSS, didn’t come out as expected. Debugging complex widgets doesn’t require mastery level skill with either HTML or CSS, but it does require a familiarity with both to the point where you can at least find (or guess) the place where the code doesn’t look right, even if you don’t know what “right” should be or how to fix it just by looking at it. This also means that, when your widget doesn’t look right, some of the best tools for troubleshooting the problem are the tools for troubleshooting HTML and CSS.
The ultimate source of the HTML or CSS problem can come from different places, but there are a few most common ones:
-
Component selection: Even with just the OH family of widgets there’s quite a selection of options, some of which overlap in function. Then, when you include the F7 components and general HTML tags that selection becomes overwhelming. Sometimes a widget doesn’t come out right just because you picked one version of a component that has some extra quirk you didn’t know about.
-
Component configuration: Each of the widget components has many different properties and it is easy to use the wrong property either through misunderstanding or just plain typo. It is also not uncommon to set a property to the wrong value. The context help in the widget editor gives some hints but not always enough to know exactly what you need to put for the value of that property.
-
Widget expressions: Widget expressions give OH widgets most of their extraordinary power and flexibility, but that also means they come with the most ways to go wrong. Keeping track of what expressions can and can’t do coupled with trying to make clean code in the widget editor is a tall order even for experienced widget creators.
-
CSS collisions: Sometimes you just want your widget to look a certain way, but that doesn’t play well with the visual styles set by OH and its underlying F7 library. You can throw CSS at certain parts of your widgets all day long and not get the results you have in mind if the F7 CSS is interfering. These can be some of the most difficult errors to diagnose and fixing them in a robust way can be even more difficult sometimes.
So, in order to go through several examples of fixing errors in widgets, I need a series of widgets with errors. To get widgets with errors I’m going to use various AI coders to produce some examples of common widgets. I’m not dumping on AIs or AI use here. Whatever may come in the near or not so near future, LLMs right now still don’t have the best track record with OH widgets (a training set problem to be sure and one that is rapidly being fixed). More than that, however, it is clear that many users are already relying heavily on AI created widgets and this is only going to increase as time goes on. In that case, what better way to review troubleshooting steps then to review AI code.
Now Playing - Where’s my text?
Let’s start with a very common widget: one that shows information about some media that’s currently playing. I prompted Gemini for this widget
Gemini 3 Thinking model prompt
Give me the full yaml configuration for for an openHAB MainUI widget that is a card which shows the album art, artist name, and song name of a song that is currently playing. Keep the widget to 400 pixels in width and make the card configurable with widget properties named albumArt, artistName, and songName.uid: music_card_widget
tags: []
props:
parameters:
- context: item
description: Item that holds the album art URL or Image data
label: Album Art Item
name: albumArt
placeholder: e.g., Player_Image
type: TEXT
- context: item
description: Item that holds the artist name
label: Artist Name Item
name: artistName
placeholder: e.g., Player_Artist
type: TEXT
- context: item
description: Item that holds the song name
label: Song Name Item
name: songName
placeholder: e.g., Player_Song
type: TEXT
parameterGroups: []
component: f7-card
config:
style:
--f7-card-margin-horizontal: 0px
border-radius: var(--f7-card-expandable-border-radius)
box-shadow: var(--f7-card-content-shadow)
overflow: hidden
width: 400px
slots:
default:
- component: oh-image
config:
item: =(props.albumArt)
style:
aspect-ratio: 1 / 1
display: block
object-fit: cover
width: 400px
- component: f7-card-content
slots:
default:
- component: f7-block
config:
style:
display: flex
flex-direction: column
margin: 0
padding: 0
slots:
default:
- component: span
config:
style:
font-size: 1.2rem
font-weight: 700
line-height: 1.2
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
text: =items[props.artistName].displayState || items[props.artistName].state
- component: span
config:
style:
font-size: 0.9rem
opacity: 0.7
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
text: =items[props.songName].displayState || items[props.songName].state
Looking at the YAML, we could be picky about several minor details, but overall it did a pretty good job. However, when I populate the widget parameters there is a problem: I see the album art, but the artist name and song name are nowhere to be found.
The first thing to check, of course, is where in the widget these text items are being rendered, and that’s pretty easy: it’s the two span components at the very end of the widget. We know what doesn’t work, now we just have to figure out why.
The inspector
Nine times out of ten, the first tool that I’m going to reach for when widget troubleshooting is the browser’s page inspector. This tool opens up a panel full of useful development and troubleshooting tools and allows you to quickly target the specific area of a page where you think the problem is. In this case, it’s pretty clear that my problem is in the white area below the image. So, I’m going to right click on that white area specifically and select the inspect option at the bottom of the context menu.
Browsers
I use mostly Chromium-based broswers, so all the examples will use Chrome’s terminology and short-cuts. I occasionally use FireFox (especially when testing if a problem is restricted to Chromium or not) and know that these features are all pretty much identical in that browser. I cannot say what terms or specific functions are the equivalents in other broswers, but I would guess most of what I’m discussing in this tutorial is pretty well standardized.
The panel that opens when I inspect that white space shows the html of each element on the page and highlights the element I last interacted with. In this case, however, it thinks the last element I interacted with (where I right clicked) is the base card element, and I have to expand that two levels before I see the span elements I’m looking for. Hovering over the first span element HTML will then highlight that element on the page and show an overlay with the element’s dimensions.
In this case, my span element is 368 pixels wide and 0 pixels tall (0 pixels tall, incidentally is why I couldn’t right click on the span and wound up clicking on the card background instead). An element can really only be 0 pixels tall under two circumstances: 1) CSS setting it 0 pixels, or 2) The height is automatically calculated and there is just no content inside the element. Looking at the expanded span HTML in the panel we can see that in this case it is the second option. <!-- opens an HTML comment and --> closes it. So, in this case <!----> is just an empty HTML comment. There is no text inside the span only a comment which renders to nothing.
The item state that we have linked to this element in the widget YAML is clearly functioning because we can see that the span has a text attribute with the correct string value. Why didn’t the string in the text config become text inside the span element? Checking the doc page that introduces HTML components, we see that the correct configuration for setting text content in an HTML components is content not text. This is a case of component mis-configuration. All we have to do is change:
text: =items[props.artistName].displayState || items[props.artistName].state
to
content: =items[props.artistName].displayState || items[props.artistName].state
(and do the same for the second span as well) and now our widget functions as expected.
More than one solution
HTML and CSS are very flexible and you can arrive at the same (or nearly the same) visual result through many different solutions. Troubleshooting is no different. In this case a second equally viable solution is to treat this problem as a case of improper component selection. If you change the
spancomponents toLabelcomponents, you also arrive at a functional widget (becausetextis the correct property for aLabel’s text). The resulting HTML would not be 100% identical, but there would be no functional difference and your users certainly would not be able to tell.
Fan Control - Why isn’t anything happening?
Let’s increase the complexity a little bit by including interactivity in a widget. I prompted Claude for a widget to control my ceiling fans and include some fancy animations while we’re at it.
Claude Sonnet 4.6 prompt
I want the full yaml configuration for an openHAB MainUI widget. The widget should be a card that I can click to turn my ceiling fan on and off. The title of the card should be a configurable property. The content of the card should only be an icon of a fan. I want the icon to rotate slowly when the fan is on. The fan item should also be a configurable property.uid: fan_control_card
tags: []
props:
parameters:
- name: fanItem
label: Fan Item
description: The openHAB item that controls the ceiling fan
type: TEXT
required: true
context: item
- name: title
label: Card Title
description: The title displayed on the card
type: TEXT
required: false
default: Ceiling Fan
component: f7-card
config:
title: =props.title
style:
cursor: pointer
action: toggle
actionItem: =props.fanItem
slots:
default:
- component: f7-card-content
config:
style:
display: flex
justify-content: center
align-items: center
padding: 16px
slots:
default:
- component: f7-block
config:
innerHTML: >
=('<style>@keyframes fan-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }</style>
<span style="display:flex; justify-content:center; align-items:center;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96" height="96"
style="animation:' + (items[props.fanItem].state === 'ON' ? 'fan-spin 2s linear infinite' : 'none') + '">
<path fill="currentColor" d="M12 11C11.45 11 11 11.45 11 12S11.45 13 12 13 13 12.55 13 12 12.55 11 12 11M12
2C10.07 2 8.35 2.62 7 3.67L10.77 7.44C11.14 7.17 11.55 7 12 7S12.86 7.17 13.23
7.44L17 3.67C15.65 2.62 13.93 2 12 2M2 12C2 13.93 2.62 15.65 3.67 17L7.44
13.23C7.17 12.86 7 12.45 7 12S7.17 11.14 7.44 10.77L3.67 7C2.62 8.35 2 10.07 2
12M22 12C22 10.07 21.38 8.35 20.33 7L16.56 10.77C16.83 11.14 17 11.55 17
12S16.83 12.86 16.56 13.23L20.33 17C21.38 15.65 22 13.93 22 12M12 17C11.55 17
11.14 16.83 10.77 16.56L7 20.33C8.35 21.38 10.07 22 12 22S15.65 21.38 17
20.33L13.23 16.56C12.86 16.83 12.45 17 12 17Z"/>
</svg></span>')
There’s some intimidating looking text at the end of this, but let’s see if it works (spoiler alert…it doesn’t).
Not bad. Claude got the widget parameters correct, I can set the item and the title. It seems that the big ugly block of text at the bottom is a custom made ceiling fan SVG graphic; we can work with that for now. There’s a pretty big issue, however: when I click on the card nothing happens.
Because there is only one thing that should happen (and therefore only one configured action for the widget) we can at least locate the source of the error pretty quickly:
component: f7-card
config:
title: =props.title
style:
cursor: pointer
action: toggle
actionItem: =props.fanItem
I know that toggle is a valid value for the action configuration, and actionItem correctly uses a widget expression to set the item name. The detailed action configuration examples are in the oh-button reference page, so it’s time to head to that page. The toggle action text says:
action: toggleis used to change any item that supports two states e.g. a lamp that is either ON or OFF or blinds that are OPEN or CLOSED. UseactionCommandandactionCommandAltto specify the commands to switch between.
So, this action configuration is incomplete. When using the toggle action you must also specify the two commands to toggle between. This is actually a very common error and it occurs with enough frequency in forum questions that I’m not surprised at all that Claude fell for it. Many users assume that if no command or alt-command are specified the toggle action defaults to ON and OFF commands, but that is not the case, the two commands must always be specified. That, at least, is an easy fix:
component: f7-card
config:
title: =props.title
style:
cursor: pointer
action: toggle
actionItem: =props.fanItem
actionCommand: ON
actionCommandAlt: OFF
And now…clicking on the card still does not do anything.
Looking again, I see there’s a second very common mistake:
component: f7-card
The action configurations are specific to the oh- family of components. Nothing is happening because the f7-card completely ignores the action. There is an oh-card which is based on the f7-card and the oh-card does allow for actions so I just have to change the component:
component: oh-card
config:
title: =props.title
style:
cursor: pointer
action: toggle
actionItem: =props.fanItem
actionCommand: ON
actionCommandAlt: OFF
Uh oh! When I change from the f7 to the oh based card, the fan icon in the main section of the card disappears:

The only reason I can think of that would explain why the components inside the card just changed is if the f7-card and the oh-card use different slots. Back to the component reference pages. The oh-card page says:
Slots
You need to define either one of these two slots:
content: The default slot for content, which will then be rendered inside a f7-card-content element.content-root: The slot for content that should be rendered directly inside the card, without any additional wrapping element. If this slot is defined, the content slot will be ignored, and you have to implement the handling of config.contentStyle and config.contentClass yourself.
It looks like the default slot that works with the f7-card does not work with the oh-card so I need to update that as well. When I change the slot name to content I see the complete fan card, and it even works:

Not only does the widget control my fan, but the animation does show a spinning icon. Drat! I was hoping Claude would mess up the CSS animation and we could work on debugging that, but that animation looks pretty good. It’s not how I would have done it and it’s “thought” process for how it arrived at this solution was not 100% correct. It’s doesn’t seem to know about the stylesheet configuration option which is not surprising as that is not well documented still and so not likely in the model’s training data. This does highlight one weak point in using LLM’s like this, but if it works it’s good enough for now.
I could consider this done now and use it as is, but I’m not 100% satisfied still. There’s a lot of extra blank space above the fan icon that I’d like to eliminate.
Fan Control 2 - Where’s that space coming from?
Perhaps the most common question variety regarding custom widgets is “Why is X here instead of there?” Sizing and positioning is one of the main functions of CSS and of the many rules some are intuitive, some are understandable, and a few sometimes just feel like they were put in place to bedevil future generations. Fortunately, most of the time with custom widgets we are going to encounter only the first and second kind. Even more encouraging, there are some very useful tools at our disposal.
Here’s the current state of the fan control widget code:
uid: fan_control_card
tags: []
props:
parameters:
- name: fanItem
label: Fan Item
description: The openHAB item that controls the ceiling fan
type: TEXT
required: true
context: item
- name: title
label: Card Title
description: The title displayed on the card
type: TEXT
required: false
default: Ceiling Fan
component: oh-card
config:
title: =props.title
style:
cursor: pointer
action: toggle
actionItem: =props.fanItem
actionCommand: ON
actionCommandAlt: OFF
slots:
content:
- component: f7-card-content
config:
style:
display: flex
justify-content: center
align-items: center
padding: 16px
slots:
default:
- component: f7-block
config:
innerHTML: >
=('<style>@keyframes fan-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }</style>
<span style="display:flex; justify-content:center; align-items:center;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96" height="96"
style="animation:' + (items[props.fanItem].state === 'ON' ? 'fan-spin 2s linear infinite' : 'none') + '">
<path fill="currentColor" d="M12 11C11.45 11 11 11.45 11 12S11.45 13 12 13 13 12.55 13 12 12.55 11 12 11M12
2C10.07 2 8.35 2.62 7 3.67L10.77 7.44C11.14 7.17 11.55 7 12 7S12.86 7.17 13.23
7.44L17 3.67C15.65 2.62 13.93 2 12 2M2 12C2 13.93 2.62 15.65 3.67 17L7.44
13.23C7.17 12.86 7 12.45 7 12S7.17 11.14 7.44 10.77L3.67 7C2.62 8.35 2 10.07 2
12M22 12C22 10.07 21.38 8.35 20.33 7L16.56 10.77C16.83 11.14 17 11.55 17
12S16.83 12.86 16.56 13.23L20.33 17C21.38 15.65 22 13.93 22 12M12 17C11.55 17
11.14 16.83 10.77 16.56L7 20.33C8.35 21.38 10.07 22 12 22S15.65 21.38 17
20.33L13.23 16.56C12.86 16.83 12.45 17 12 17Z"/>
</svg></span>')
When trying to track down position and spacing issues, again the very first thing I’m going to do is to open up the element inspector and look at the resultant HTML to see if I can spot anything strange there. Here’s the HTML for the elements on the card:
<div class="card oh-card" style="cursor: pointer;">
<!---->
<!---->
<!---->
<div class="card-header">
<div>Claude's Ceiling Fan Card</div>
</div>
<div class="card-content card-content-padding card-link padding">
<div class="card-content card-content-padding" style="display: flex; justify-content: center; align-items: center; padding: 16px;">
<div class="block">
<style>
@keyframes fan-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
<span style="display:flex; justify-content:center; align-items:center;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="96" height="96" style="animation:none">
<path fill="currentColor" d="M12 11C11.45 11 11 11.45 11 12S11.45 13 12 13 13 12.55 13 12 12.55 11 12 11M12 2C10.07 2 8.35 2.62 7 3.67L10.77 7.44C11.14 7.17 11.55 7 12 7S12.86 7.17 13.23
7.44L17 3.67C15.65 2.62 13.93 2 12 2M2 12C2 13.93 2.62 15.65 3.67 17L7.44
13.23C7.17 12.86 7 12.45 7 12S7.17 11.14 7.44 10.77L3.67 7C2.62 8.35 2 10.07 2 12M22 12C22 10.07 21.38 8.35 20.33 7L16.56 10.77C16.83 11.14 17 11.55 17 12S16.83 12.86 16.56 13.23L20.33 17C21.38 15.65 22 13.93 22 12M12 17C11.55 17 11.14 16.83 10.77 16.56L7 20.33C8.35 21.38 10.07 22 12 22S15.65 21.38 17 20.33L13.23 16.56C12.86 16.83 12.45 17 12 17Z">
</path>
</svg>
</span>
</div>
</div>
</div>
<!---->
</div>
Again, we don’t need to understand every level of that HTML. In the previous tutorials, I’ve mentioned that every container element (elements that just hold some other element without any direct content of their own) should be simple, explainable, and not redundant. Here’s a great example.
There are two div elements that have a class which indicates that element holds the card’s content (card-content). Where do these come from? The style directive on the second one matches the style object on the f7-card-content in the widget code so that’s one that I have explicitly put there. To figure out where the first one came from, I have to go back to the oh-card docs. The answer is in the quote from the docs above:
content: The default slot for content, which will then be rendered inside a f7-card-content element.
When Claude created the widget, the f7-card root component needed an f7-card-content child component, but when I switched to the oh-card it seems that the content component is added automatically, behind the scenes. Let’s remove the second card content component and see if that fixes the spacing issue:
That’s better, but still not right. There is much less extra space around the card icon, but there’s still some extra space above it and as a result it’s not centered in the card content area. To find where that last bit of empty space is coming from, I’m going to use another feature of the browser’s inspector.
At the top of the browser panel that opens for the developer’s tools, there’s a small button that looks like this:
. When you press that button and move the cursor around the displayed page, every element you hover over will get an overlay showing information about the element including its padding and margins.

Another way to see the same information is to click on the text of each element in the elements panel; the size and spacing information will be displayed on a graphic at the top of the lower “Computed” styles panel.
In the animation above, you can see that the card content element adds a reasonable space around everything in the content area in the form of a 16px padding (green highlight) on all sides. The next, element, the block element, however, then adds additional spacing, including a top margin (orange highlight) of 16px, and this results in the offset of the svg content. So, the problem is the f7-block.
Because of the way that Claude chose to build the widget using the f7-block’s innerHTML property we can’t get rid of the block without completely rebuilding the widget. Instead we’ll just have to remove the margin (and while we’re at it the padding just to be safe) from the block.
- component: f7-block
config:
style:
margin: 0
padding: 0
innerHTML: >
...
That’s better yet, but it feels likes there’s still just a little too much space. Looking at the animation above, you can see that, actually there’s some space around the drawn areas built right into the icon. I probably don’t need any additional space from the containers because of the way the SVG is constructed. Let’s also remove the padding from the card content element. This content element, recall, is added automatically by the oh-card so there’s no component in the YAML that I can add a style to. Fortunately, the oh-card comes with special style properties. From the card docs:
Style
`oh-card` provides full control over the style of itself as well as the header, content and footer.
The card itself can be styled through the
styleandclassconfig properties, while the header, content and footer can be styled through theheaderStyle,headerClass,contentStyle,contentClass,footerStyleandfooterClassproperties.
So here I just need to use the contentStyle instead of the regular style property:
component: oh-card
config:
title: =props.title
style:
cursor: pointer
action: toggle
actionItem: =props.fanItem
actionCommand: ON
actionCommandAlt: OFF
contentStyle:
padding: 0
When I try that, nothing changes. What did I do wrong? To find out if I correctly applied the padding style, I’m going back to the browser inspector once again. I’m going to click on the card content element and flip over from the Computed lower panel to the Styles lower panel which should show me, in order of precedence, all the CSS styles applied to that element.
This can look a little overwhelming at first, but there’s a lot of helpful information here. For now, I’m just looking at all the hints related to “padding”. First off, the element text itself shows that I did properly declare 0 padding for this element: style="padding: 0px;". However, it also shows that when the oh-card creates the card content container it adds a padding class to the container.
Below that, in the list of styles I see my padding: 0px listed right at the top, but it’s crossed out. The next place that padding is shown explains why: the padding class adds a padding style with the !important CSS keyword. This keyword will prevent any other styles from overriding the “important” style. To override an “important” style another style declaration also has use the !important keyword.
So, to set the padding to 0 instead of padding: 0, I need to use padding: 0 !important to override the previous padding setting. When I do that, I get exactly what I’m looking for.
House Temperatures - Where did my items go?
I haven’t had to debug any expressions yet because my requests have not required any significant data manipulation. Let’s see if I can fix that by working with a repeater and some Number:Temperature items. So, I prompted Claude for a list of temperatures around my house.
Claude Sonnet 4.6 prompt
I want the full yaml configuration for an openHAB MainUI widget. The widget should be a card with the title "House temperatures". The card should then list all the temperature values from sensors around the house. Temperature items have the tags "Measurement" and "Temperature". Each row should display the label of the temperature item and the temperature. Round the temperature value to the nearest integer and keep the unit. Ignore any items with "Pool", "Outside", "Air", or "Synology" in the labels.uid: house_temperatures_card
tags: []
props:
parameters: []
component: f7-card
config:
title: House Temperatures
slots:
default:
- component: oh-list
config: {}
slots:
default:
- component: oh-repeater
config:
for: item
sourceType: itemsWithTags
tags: Measurement,Temperature
filter: >
=!items[item].label.includes('Pool') &&
!items[item].label.includes('Outside') &&
!items[item].label.includes('Air') &&
!items[item].label.includes('Synology')
slots:
default:
- component: oh-list-item
config:
title: =items[item].label
after: =Math.round(parseFloat(items[item].displayState)) + ' ' + (items[item].displayState.match(/[°a-zA-Z%]+/) || [''])[0]
That configuration is pretty concise. There’s just one problem:

It doesn’t work at all. None of the list items that I expect are visible.
You can probably guess where my first stop is going to be…yep, let’s check out the HTML:
<div class="card">
<div class="card-header">House Temperatures </div>
<!---->
<!---->
<div class="list oh-list">
<!---->
<ul>
<div class=""></div>
</ul>
<!---->
<!---->
</div>
</div>
The big clue here is I’m expecting 10 - 12 items in this list and there is a single, malformed div.
That tells me the problem lies with the repeater. A quick check of the repeater’s doc page and the problem is clear: the correct property name to list the tags is itemTags not just tags. So, changing the config to:
itemTags: Measurement,Temperature
Should get me my list:
…or at least a list.
Well, I got my wish. There’s definitely an error in one of the expressions in this widget. This is a pretty easy one to start with because Error: 'parseFloat' is not a function is quite descriptive.
The error here is that you cannot call parseFloat() directly, it is a method of the javascript Number object, so it has to be Number.parseFloat(...) instead.
We’re making progress. The full error text which is cut off in the screenshot is TypeError: Cannot read properties of undefined (reading 'match').
This is a common javascript error. It means that instead of [object].match(...) some part of the expression evaluates to undefined.match(...) (and of, course there can’t be any methods or properties of something that is undefined). The problem must be here:
items[item].displayState.match(/[°a-zA-Z%]+/)
displayState should be a String (which does have a match method). items[...] is the correct way to access the item information.
That makes the problem [item], and although item is the correct repeater variable name it must be referenced via the loop object and we must pass the name String not the entire object to items.
It seems Claude made this mistake repeatedly, and I have several instances of items[item] that I have to change to items[loop.item.name].
What? It seems that some instances of displayState work and others are undefined. Aha! Claude fell victim to one of the classic blunders!
MainUI will save bandwidth by sending only the state of an item if the state and the displayState are equal. You should almost never just rely on the displayState for exactly this reason, and most widgets use an expression that will return the displayState but fall back to the state if required. In fact, this is such a common requirement, that there’s a shortcut for this behavior @itemName.
So, in the after expression I need to change both the displayState references to use the @ shortcut:
after: =Math.round(Number.parseFloat(@loop.item.name)) + ' ' + ((@loop.item.name).match(/[°a-zA-Z%]+/) || [''])[0]
And with that, all my nice round temperatures are available.
Still can’t see which items those are though…alright, let’s look at the title expression. This one is short and sweet:
title: =items[loop.item.name].label
I know that items[loop.item.name] works because it worked in the after expression (before I upgraded to the shortcut). So the problem must be the .label. This is an excellent chance to use the expression tester.
In the developer tools side panel (shift + alt + d) the third tool is the widget expression tester. You can put an expression (with the leading =) in the top box and see the output in the bottom box.
Now, the tester does not have access to the current widget context (even if the widget editor is open) so we can’t use the loop.item variable, but we can just replace that with one of the item names we know it is using. If we put =items["Sensor_Garage2_Temperature"] in the top box, the bottom just says [object object] which is the standard javascript output for an object, and not very helpful here. If we want that in more readable terms we have to “stringify” it:
and when we do that, we see that the items object just doesn’t return the item’s label. For the bandwidth reason mentioned above, the items object is very restricted in its information and items[...].label will never work. Fortunately, because this is the child of a repeater, the loop.item variable is a complete object for the item including the label. So we can just change the expression to title: =loop.item.label.
Now that I can see what the items in the list are, it is clear that the repeater is not properly filtering the output list. I can see several “Air” and “Pool” items and the temp of my Synology drive (without it’s °C, for some reason I’ll have to look into later). Claude made the filter expression a pretty standard sequence of boolean tests (in YAML > is just an indicator of a multi-line string, so everything that follows is the same expression):
filter: >
=!items[loop.item.name].label.includes('Pool') &&
!items[loop.item.name].label.includes('Outside') &&
!items[loop.item.name].label.includes('Air') &&
!items[loop.item.name].label.includes('Synology')
Well, right away I see the same label issue as above, so all those need to be changed to just loop.item.label. But that doesn’t actually fix the problem.
The rest looks correct to me so it’s time to go back to the repeater’s doc page:
filterFilter expression TEXTSpecify an expression WITHOUT THE = PREFIX to filter the resulting array
There’s the problem in big capital letters: Claude added the = to the front of the filter value. There’s a good technical reason for this requirement, but for now we can just accept that the = needs to go. When it does, the widget is complete and correct:
A Note About LLMs
I still do not want this topic to turn into a referendum on the use of LLMs in OH or coding in general, but I feel I should add this note. I intended to use one example from each Gemini, Claude, and ChatGPT for this tutorial, but you’ll note there is no ChatGPT example.
Gemini actually fully succeeded a couple of times with functional code on the first try except for the first example above.
Claude turned out widget code with lots of errors but close enough to be worth troubleshooting.
ChatGPT simply failed. All attempts to prompt ChatGPT to produce functional OH widgets resulted in gibberish YAML that didn’t even follow the correct basic structure (I suspect, although I didn’t look too carefully, that it actually produced Home Assisant YAML stuctures). Any attempt to rescue the YAML produced by ChatGPT would have been just rewriting, not troubleshooting.
There are some limitations to this observation:
- I’m not using a paid tier for any of these so maybe the more advanced ChatGPT available to subscribers can do better.
- All responses used here were first attempts. I made no effort to refine the code with follow-up prompts (because I wanted errors to fix).
That concludes my three-part series on custom widgets. I hope some of you found these useful.
Resources In this Tutorial
| OH Help Docs | |
|---|---|
| Creating personal widgets | Introduction to the technical details of widget creation and the types of components available |
| Widget Action Examples | Detailed descriptions and yaml configuration examples for the different OH actions |















