Animated SVG in MainUI Widget

Hi @ all and a happy new year!

after diving deeper into pages, custom widget and YAML. I want to display a power flow sheme. I created first an svg in inkscape and then added some item values. That’s how it looks like:

I want the orange circle going to the battery if it is charged and the green on whenn the battery provides power to the inverter. Unfortunately both are allways visible and moving. I found a lot of similar project. But as far as I see and undestand them it should work. This is the Code. Maybe someone has a sharp eye and sees what’s my issue. Many Thanks in advance:

uid: widget_solarthermie
tags:
  - diagram
  - temperature
  - ventilation
props:
  parameters:
    - description: Widget title
      label: Title
      name: title
      required: false
      type: TEXT
    - context: item
      description: Solarthermie S1 Vorlauf
      label: Solarthermie S1 Vorlauf
      name: Solarthermie_S1_Vorlauf
      required: false
      type: TEXT
    - context: item
      description: Temperatura Za GWC
      label: Za GWC Item
      name: zagwc
      required: false
      type: TEXT
    - context: item
      description: Temperatura Wyrzutnia
      label: Wyrzutnia Item
      name: wyrzutnia
      required: false
      type: TEXT
    - context: item
      description: Pufferspeicher 1-2
      label: Pufferspeicher 1-2 Item
      name: Pufferspeicher_1_2
      required: false
      type: TEXT
    - context: item
      description: Temperatura Wywiew
      label: Wywiew Item
      name: wywiew
      required: false
      type: TEXT
timestamp: Jan 5, 2026, 6:06:32 PM
component: f7-card
config:
  style:
    --f7-card-header-font-size: 16px
    --f7-card-header-font-weight: bold
    margin: 15 px
    min-height: 300px
  title: "=props.title ? props.title : 'Maschinenraum - Status'"
slots:
  default:
    - component: f7-block
      config:
        style:
          align-items: center
          display: flex
          justify-content: center
          margin: 0
          padding: 0
          position: relative
          width: 100%
      slots:
        default:
          - component: div
            config:
              style:
                max-width: 600px
                position: relative
                width: 100%
            slots:
              default:
                - component: oh-image
                  config:
                    style:
                      display: block
                      height: auto
                      width: 100%
                    url: /static/Solarthermie-Heizung_new.svg
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 6%
                      padding: 2px 6px
                      position: absolute
                      top: 5%
                      transform: translate(-50%, -50%)
                    text: =items.Solarthermie_S1_Vorlauf.state
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 23%
                      padding: 2px 6px
                      position: absolute
                      top: 5%
                      transform: translate(-50%, -50%)
                    text: =items.Solarthermie_S2_Ruecklauf.state
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 12%
                      padding: 2px 6px
                      position: absolute
                      top: 54%
                      transform: translate(-50%, -50%)
                    text: =items.Pufferspeicher_1_1.state
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 12%
                      padding: 2px 6px
                      position: absolute
                      top: 63%
                      transform: translate(-50%, -50%)
                    text: =items.Pufferspeicher_1_2.state
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 12%
                      padding: 2px 6px
                      position: absolute
                      top: 72%
                      transform: translate(-50%, -50%)
                    text: =items.Pufferspeicher_1_3.state
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 12%
                      padding: 2px 6px
                      position: absolute
                      top: 81%
                      transform: translate(-50%, -50%)
                    text: =items.Pufferspeicher_1_4.state
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 39.5%
                      padding: 2px 6px
                      position: absolute
                      top: 54%
                      transform: translate(-50%, -50%)
                    text: =items.Pufferspeicher_2_1.state
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 39.5%
                      padding: 2px 6px
                      position: absolute
                      top: 63%
                      transform: translate(-50%, -50%)
                    text: =items.Pufferspeicher_2_2.state
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 39.5%
                      padding: 2px 6px
                      position: absolute
                      top: 72%
                      transform: translate(-50%, -50%)
                    text: =items.Pufferspeicher_2_3.state
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 39.5%
                      padding: 2px 6px
                      position: absolute
                      top: 81%
                      transform: translate(-50%, -50%)
                    text: =items.Pufferspeicher_2_4.state
                - component: Label
                  config:
                    style:
                      background: rgba(0, 0, 0, 0)
                      border: 1px solid
                      border-radius: 4px
                      box-shadow: 0 2px 4px rgba(0,0,0,0.15)
                      color: rgba(106, 176, 212, 100)
                      font-size: 12px
                      font-weight: bold
                      left: 93.1%
                      padding: 2px 6px
                      position: absolute
                      top: 90%
                      transform: translate(-50%, -50%)
                    text: =Math.round(items.Batteriestatus_1_Kapazitaet.state) + '%'
                - component: div
                  config:
                    style:
                      background: rgba(120, 160, 120, 1)
                      border-radius: 4px
                      bottom: 16.5%
                      height: =Math.max(0, (items.Batteriestatus_1_Kapazitaet.state / 100) * 90) +
                        'px'
                      left: 89.5%
                      position: absolute
                      width: 43px
                      z-index: 10
                # --- Animation als Overlay ---
                - component: svg
                  config:
                    style:
                      position: absolute
                      top: 0
                      left: 0
                      width: 100%
                      height: 100%
                      pointer-events: none
                      z-index: 20
                    viewBox: "0 0 192.0397 121.61422"
                    xmlns: "http://www.w3.org/2000/svg"
                  slots:
                    default:
                      # --- Pfad für die Animation ---
                      - component: path
                        config:
                          id: inverterToBattery
                          d: "m 150.846,60.884988 7.18151,3e-6 V 85.36738 l 11.51017,-2e-6"
                          fill: none
                          stroke: orange
                          stroke-width: 1
                      - component: circle
                        config:
                          r: 4
                          fill: orange
                          visible: =(items[props.batteryStatus].state) == 'Laden' ? false:true
                        slots:
                          default:
                            - component: animateMotion
                              config:
                                dur: 3s
                                repeatCount: indefinite
                                keyPoints: 0;1
                                keyTimes: 0;1
                                calcMode: linear
                              slots:
                                default:
                                  - component: mpath
                                    config:
                                      xlink:href: "#inverterToBattery"
                      - component: circle
                        config:
                          r: 4
                          fill: green
                          visible: =(items[props.batteryStatus].state) == 'Entladen' ? false:true
                        slots:
                          default:
                            - component: animateMotion
                              config:
                                dur: 3s
                                repeatCount: indefinite
                                keyPoints: 1;0
                                keyTimes: 0;1
                                calcMode: linear
                              slots:
                                default:
                                  - component: mpath
                                    config:
                                      xlink:href: "#inverterToBattery"
                                  
                                  
                             

If both are always visible then both your visible statements must be true:

This just means that items[props.batteryStatus].state returns a value that is neither Laden nor Entladen.

To check what state items[props.batteryStatus].state does return you could add a temporary label to the widget with text: items[props.batteryStatus].state, but in this case that’s not going help a lot because the result is just going to be -. When the items object returns - that’s an indication that the item you are attempting to reference doesn’t exist.

So, the ultimate problem is with the props.batteryStatus part of the expression. At the top of your widget, you have no parameter with name: batteryStatus so props.batteryStatus is most likely undefined (I say most likely, because I assume you are still testing this widget in the widget editor and not on a page which could possibly lead to a different issue).

On a related note: your expression (items[props.batteryStatus].state) == 'Entladen' ? false:true is unnecessarily complicated. The ternary statement (test) ? (if true) : (if false) starts with a boolean test. So, putting boolean results in the if true and if false sections is redundant. There’s a much simpler method for “if this is true then false”, that’s just the NOT operator. So a more readable and maintainable expression would just be:

visible: =!(items[props.batteryStatus].state == 'Entladen')

Tanks @JustinG for your replay. You’re absolutely rigth i recognized this in the mean time by my own and change the corresponding lines to e.g.

visible: =(items.Batteriestatus_1_Status.state) == 'Laden' ? false:true

I think the title of this post needs to change. This is MainUI widget, not a sitemap.

1 Like

I tryed to user another item:

visible: =(items.Battery_Power_1.state) < 0 ? true:false

If the batter power is below zero the battery is charging and the now pale blue dot should be visible.
But it is allways visible and the green dot too, which has the contrary condition.
To check if the item has a value I used a text label, as @JustinG recommended (below the green dot). I now have the assumption that the problem is maybe the unit W which is also provided by the item (I did not add it) because the item is defined as “Number:Power”. How can only the value be extracted?

Edit: I tried to define the item only as Number (the unit W disappeared) but both dots are still allways visible.

Edit 2: After saving the widget and refreshing the browser the behaviour is as expected. The problem seems to be the item definition “Number:Power” without power it works. Now there are two possible solutions:
1: creating a second auxillary item without power
2: some one can point me how to handle the item in the condition check defined as “Number:Power”

It doesn’t matter what type the Item is, the result of items[item_name].state is always a string version of the state. So, the problem with (items.Battery_Power_1.state) < 0 is not just that it could be "-538 W" > 0 but that "-538" > 0 is also not a true numerical comparison.

You can set the item to either be a plain Number or Numper:Power, it doesn’t matter. In widget expressions, you still have to convert the state from a string to a number. Fortunately, the items object can do that for you. Instead of accessing .state use the .numericState property: (items.Battery_Power_1.numericState) < 0.

In fact, this is such a common need that there is a handy shortcut syntax for it: you can just use #"Battery_Power_1" to get the numeric state of your item instead of the full items.Battery_Power_1.numericState.

1 Like

Perfect!

=(items.Battery_Power_1.numericState) < 0 ? true:false

now works with the item defined as following:

Number:Power Battery_Power_1 "Batterieleistung [%.1f %unit%]" (g_PV_und_Netzleistung_und_Verbrauch) {channel="mqtt:topic:chisage:Mars14:data:Battery_Power1"}

the solves additionally a second issue. I tried to make the speed of the moving dot dependent on the the battery power as following:

dur: =(items.Battery_Power_1.numericState * 0.0065) + 20.655

which means a duration of the animation of 1s @ 3000W and 20s @ 100W. This was also working with item defined without “Power” but the animation was always restating when the value change. Now the dot is moving with constant velocity depending on the battery power.

Again many thanks @JustinG

Edit: The animation is now not reseting just jumping a little bit back or forward. Is there a way to update the duration only at the restart of the animation?

Unfortunately there is not. The expression will produce the new value as soon as the item state updates and CSS will adjust the animation accordingly. There is no way for OH to be aware of the current timing of the CSS animation, and there is no way to delay the uptake of the new value.

Ok there are much worst things :smiley: . By the way the equation for the duration has to be corrected:

dur: =(items.Battery_Power_1.numericState * -0.0065) + 20.655

otherwise the dot is running faster the less power is present. If you want it faster with increasing power add the negativ sign to the gain.