Dear Openhabians,
I have been experimenting with some widgets and cards and what I was looking for is a widget that combines both a simple look and featuring more detailed controls when needed.
Please note that I am an absolute layman in terms of CSS or webdesign, all I know is what I’ve learned so far since the release of OH3.
Today I want to share my recently created widget that consists of a cell-like card with a background image, labels in the center content and chips in the footer, additionally, control buttons can be unfolded for quick actions.
To configure all of these in a versatile way, data is entered as JSON. With a separate switch item, more detailed controls in the form of f7-icon buttons can be unfolded by a click of the title or through changing the detail switch item to ON.
The current limitations are:
- Label content: 3 labels with icons
- Chip content: 10 chips with icons
- Control lines 1&2: 6 buttons with icons each
Of course, you can change the ranges on the different oh-repeater components, but you might be hitting size limitations.
The items are aligned using a flex box.
Known issues & work in progress:
- Labels and their icons get separated on line wrap
- Chip text and icon colors might differ when using plain text colors (like “orange”)
- Only call JSON.parse once per repeater as suggested by @JimT
Done:
- Margin on both sides might not match the other Openhab cells - maybe somebody has advice on that? FIxed in v0.4, cards now have full width as expected
- Dark mode looks ugly Fixed in v0.4
Updates
- v0.4: Card width fixed, dark mode fixed, new modify option “static”: Display a static chip text instead of the item state
How To Use
How to create the JSON inputs:
If you don’t use any of the options (e.g. you don’t need Line 2), just fill it with
{}
That way, it won’t come up as undefined in the widget but will be hidden.
Chip Data
The chip JSON is shown with the following examples:
Example 1
{
"0": {
"item": "TTS_Temperatur",
"icon": "thermometer",
"textcolor": "orange"
},
"1": {
"item": "H_Irrigation",
"icon": "drop_triangle_fill",
"textcolor": "blue",
"visibleState": "ON"
},
"2": {
"item": "MQ_PLUG1",
"icon": "drop",
"textcolor": "blue",
"visibleState": "ON"
},
"3": {
"item": "H_Auto_Irrigation_Active",
"icon": "arrow_counterclockwise_circle_fill",
"textcolor": "red",
"visibleState": "ON"
}
}
Example 2
{
"0": {
"item": "LocalSun_Rise_Start",
"modify": "fromNow",
"icon": "sunrise",
"textcolor": "purple"
},
"1": {
"item": "LocalSun_Set_Start",
"modify": "fromNow",
"icon": "sunset",
"textcolor": "darkorange"
},
"2": {
"item": "MC_Integrity",
"icon": "exclamationmark_triangle",
"textcolor": "red",
"visibleState": "OFF",
"modify": "static",
"staticText": "ManCave"
}
}
The function is described as follows:
- item: Item state to display
- icon: f7(!)-icon to display
- textcolor: chip text & icon color
- visibleState (optional): Only display the chip when it’s state matches this string (no above/below function yet)
- modify: only two functions available up to now: dayjs.fromNow transformation for timestamps or static text
Label Data
This one is pretty self-explanatory:
{
"0": {
"item": "AqaraMQ_THPS3_TEMP",
"icon": "thermometer"
},
"1": {
"item": "AqaraMQ_THPS3_HUMIDITY",
"icon": "drop"
},
"2": {
"item": "AqaraMQ_THPS3_PRESSURE",
"icon": "gauge"
}
}
Control Button Line 1 & 2
There are two main options of what a button can do:
- Send a command to an item
- Open a page as popup (as shown in entry “2”)
{
"0": {
"item": "GF_LivingRoom_RollerShutter_BlindsControl ",
"icon": "chevron_down",
"oncolor": "orange",
"action": "command",
"actionCommand": "DOWN"
},
"1": {
"item": "GF_Dining_RollerShutter_BlindsControl",
"icon": "chevron_down",
"oncolor": "orange",
"action": "command",
"actionCommand": "DOWN"
},
"2": {
"item": "",
"icon": "dial",
"oncolor": "orange",
"action": "popup",
"actionModal": "page:Heat"
},
"3": {
"item": "DachlukeBad_Switch",
"icon": "square_arrow_up_fill",
"oncolor": "purple",
"action": "command",
"actionCommand": "TOGGLE"
}
}
Please keep in mind, that this is an experimental widget of someone who is trying to find his way through unknown territory (at least for me), so feel free to improve it and adapt it where you want.
I’ll probably start a github if this widget is something people would want.
Resources
Finally, here’s the code:
uid: ImageCardFold_v0.4
tags: []
props:
parameters:
- description: image for display
label: Background Image
name: image
required: true
type: TEXT
- description: visible
label: Visible
name: visible
required: false
type: TEXT
- context: item
description: Detail item switch to expand controls
label: Detail Switch Item
name: detailItem
required: false
type: TEXT
- description: Title Text
label: Title
name: ti
required: false
type: TEXT
- description: Text Color
label: Text Color
name: tc
required: false
type: TEXT
- description: Label as JSON
label: Label Data
name: labeljson
required: true
type: TEXT
- description: Chip Data as JSON
label: Chip Data
name: chipjson
required: true
type: TEXT
- description: Chip Background Color
label: Chip Background
name: cbg
required: false
type: TEXT
- description: Line 1 Icons as JSON
label: Line 1 Data
name: l1json
required: true
type: TEXT
- description: Line 2 Icons as JSON
label: Line 2 Data
name: l2json
required: true
type: TEXT
parameterGroups: []
timestamp: Mar 12, 2021, 10:36:38 AM
component: f7-block
config:
visible: =props.visible
style:
height: auto
margin: 0
padding: 0
slots:
default:
- component: f7-card
config:
style:
background-image: ='url(' + props.image + ')'
background-size: cover
background-position: center
slots:
header:
- component: f7-block
config:
style:
background: transparent
width: 100%
height: auto
margin: 0 auto
padding: 0 auto
slots:
default:
- component: oh-link
config:
style:
background: transparent
color: =props.tc
font-size: 200%
text: =props.ti
action: command
actionItem: =props.detailItem
actionCommand: TOGGLE
default:
- component: f7-card-content
config:
style:
background: transparent
min-height: 40px
height: auto
display: flex
flex-direction: row
flex-wrap: wrap
flex-grow: 2
flex-shrink: 1
justify-content: space-around
align-items: center
slots:
default:
- component: oh-repeater
config:
visible: "=(props.labeljson === undefined) ? false : true"
sourceType: range
for: i
rangeStart: 0
rangeStop: 2
fragment: true
slots:
default:
- component: f7-icon
config:
visible: "=(JSON.parse(props.labeljson)[loop.i]['icon']) ? true : false"
f7: =JSON.parse(props.labeljson)[loop.i]["icon"]
style:
background: transparent
color: =props.tc
font-size: 200%
- component: Label
config:
visible: "=(JSON.parse(props.labeljson)[loop.i]) ? true : false"
style:
background: transparent
color: =props.tc
font-size: 200%
font-weight: bold
text: '=(items[(JSON.parse(props.labeljson)[loop.i]["item"])].displayState) ? items[(JSON.parse(props.labeljson)[loop.i]["item"])].displayState : items[(JSON.parse(props.labeljson)[loop.i]["item"])].state'
- component: f7-card-content
config:
visible: "=(props.l1json === undefined) ? false : true"
style:
background: var(--f7-card-bg-color)
box-shadow: inset 0px 3px 3px rgba(0,0,0,0.5)
-webkit-transition: all 0.75s ease-in-out
height: '=(items[props.detailItem].state == "ON")? "50px" : "0px"'
margin: 0px
padding: 0px
overflow: hidden
visibility: visible
transform-origin: top
transform: '=(items[props.detailItem].state == "ON")? "perspective(500px) rotateX(-0deg)" : "perspective(500px) rotateX(-90deg)"'
display: flex
flex-direction: row
flex-wrap: wrap
flex-grow: 2
flex-shrink: 1
justify-content: space-around
align-items: center
slots:
default:
- component: oh-repeater
config:
sourceType: range
for: i
rangeStart: 0
rangeStop: 5
fragment: true
slots:
default:
- component: f7-block
config:
visible: "=(JSON.parse(props.l1json)[loop.i]) ? true : false"
style:
background: lightgray
-webkit-transition: all 0.75s ease-in-out
margin: 0 auto
padding: 0 auto
display: flex
flex-direction: row
align-items: center
justify-content: center
text-align: center
border-radius: 50%
width: 35px
height: 35px
border: '=(items[(JSON.parse(props.l1json)[loop.i]["item"])].state == "ON") ? "solid 2px " + JSON.parse(props.l1json)[loop.i]["oncolor"] : "solid 2px lightgray"'
box-shadow: '=(items[(JSON.parse(props.l1json)[loop.i]["item"])].state == "ON") ? "1px 1px 5px rgba(0,0,0,0.5), inset 0px 0px 12px -5px " + JSON.parse(props.l1json)[loop.i]["oncolor"] : "1px 1px 10px rgba(0,0,0,0.5)"'
slots:
default:
- component: oh-link
config:
visible: "=(JSON.parse(props.l1json)[loop.i]) ? true : false"
iconOnly: false
iconF7: =(JSON.parse(props.l1json)[loop.i]["icon"])
iconColor: '=(items[(JSON.parse(props.l1json)[loop.i]["item"])].state == "ON") ? JSON.parse(props.l1json)[loop.i]["oncolor"] : "gray"'
action: =JSON.parse(props.l1json)[loop.i]["action"]
actionModal: =JSON.parse(props.l1json)[loop.i]["actionModal"]
actionItem: =JSON.parse(props.l1json)[loop.i]["item"]
actionCommand: =JSON.parse(props.l1json)[loop.i]["actionCommand"]
iconSize: 20
style:
position: absolute
- component: f7-card-content
config:
visible: "=(props.l2json === undefined) ? false : true"
style:
background: var(--f7-card-bg-color)
box-shadow: =(items.WidgetDetail.state == "OFF")? "inset 0px 0px 0px rgba(0,0,0,0.5)":"inset 0px 1px 5px rgba(0,0,0,0.5)"
-webkit-transition: all 0.75s ease-in-out
height: '=(items[props.detailItem].state == "ON")? "50px" : "0px"'
margin: 0px
padding: 0px
transform-origin: bottom
transform: '=(items[props.detailItem].state == "ON")? "perspective(500px) rotateX(0deg)" : "perspective(500px) rotateX(90deg)"'
display: flex
flex-direction: row
flex-wrap: wrap
flex-grow: 2
flex-shrink: 1
justify-content: space-around
align-items: center
slots:
default:
- component: oh-repeater
config:
sourceType: range
for: i
rangeStart: 0
rangeStop: 5
fragment: true
slots:
default:
- component: f7-block
config:
visible: "=(JSON.parse(props.l2json)[loop.i]) ? true : false"
style:
background: lightgray
-webkit-transition: all 0.75s ease-in-out
margin: 0 auto
padding: 0 auto
display: flex
flex-direction: row
align-items: center
justify-content: center
text-align: center
border-radius: 50%
width: 35px
height: 35px
border: '=(items[(JSON.parse(props.l2json)[loop.i]["item"])].state == "ON") ? "solid 2px " + JSON.parse(props.l2json)[loop.i]["oncolor"] : "solid 2px lightgray"'
box-shadow: '=(items[(JSON.parse(props.l2json)[loop.i]["item"])].state == "ON") ? "1px 1px 5px rgba(0,0,0,0.5), inset 0px 0px 5px " + JSON.parse(props.l2json)[loop.i]["oncolor"] : "1px 1px 10px rgba(0,0,0,0.5)"'
slots:
default:
- component: oh-link
config:
visible: "=(JSON.parse(props.l2json)[loop.i]) ? true : false"
iconOnly: false
iconF7: =(JSON.parse(props.l2json)[loop.i]["icon"])
iconColor: '=(items[(JSON.parse(props.l2json)[loop.i]["item"])].state == "ON") ? JSON.parse(props.l2json)[loop.i]["oncolor"] : "gray"'
action: =JSON.parse(props.l2json)[loop.i]["action"]
actionModal: =JSON.parse(props.l2json)[loop.i]["actionModal"]
actionItem: =JSON.parse(props.l2json)[loop.i]["item"]
actionCommand: =JSON.parse(props.l2json)[loop.i]["actionCommand"]
iconSize: 20
style:
position: absolute
- component: f7-card-footer
config:
style:
background: transparent
display: flex
flex-direction: row
flex-wrap: wrap
flex-grow: 2
flex-shrink: 1
justify-content: space-between
-webkit-transition: all 0.75s ease-in
slots:
default:
- component: oh-repeater
config:
sourceType: range
for: i
rangeStart: 0
rangeStop: 9
fragment: true
slots:
default:
- component: f7-chip
config:
visible: '=(JSON.parse(props.chipjson)[loop.i]) ? ((JSON.parse(props.chipjson)[loop.i]["visibleState"]) ? ((items[(JSON.parse(props.chipjson)[loop.i]["item"])].state == JSON.parse(props.chipjson)[loop.i]["visibleState"]) ? true : false) : true) : false'
text: '=(JSON.parse(props.chipjson)[loop.i]["modify"] == "fromNow") ? dayjs(items[(JSON.parse(props.chipjson)[loop.i]["item"])].state).fromNow() : ((JSON.parse(props.chipjson)[loop.i]["modify"] == "static") ? (JSON.parse(props.chipjson)[loop.i]["staticText"]) : items[(JSON.parse(props.chipjson)[loop.i]["item"])].state)'
iconF7: =(JSON.parse(props.chipjson)[loop.i]["icon"])
iconColor: '=(JSON.parse(props.chipjson)[loop.i]["textcolor"]) ? (JSON.parse(props.chipjson)[loop.i]["textcolor"]) : "white"'
style:
font-weight: bold
color: '=(JSON.parse(props.chipjson)[loop.i]["textcolor"]) ? (JSON.parse(props.chipjson)[loop.i]["textcolor"]) : "white"'
background: '=(JSON.parse(props.chipjson)[loop.i]["background"]) ? (JSON.parse(props.chipjson)[loop.i]["background"]) : props.cbg'
I hope it’ll work for you, too - i’d be happy to see some of those widgets combined with your great ideas!