Generic energy flow widget v2

I own a solar plant for a week now (Fronius Gen24). So I started digging into how to visualize it in OH. I quickly found the fronius inspired widget by @pwoehrer . It sure looks nice (really nice!), but for me it has 2 issues: It uses way too much real estate for little information, and extending it to include my EV and heatpump is far beyond my CSS and f7 framework capabilities.

After some more research, I discovered a promising widget by @james_dinniss which I then adopted to my needs and wishes. This meant:

  • incorporating the dot resize function of the fronius widget (kudos for that one!) as well as the W/kW auto formatting function
  • reworking the parameters
  • reworking the visual appearance

I did not touch the basic layout so far, though.

I tried to my best to get the code somewhat organized, but to me it still looks like a total mess :slight_smile: Since it is hard to simulate all operating conditions of a PV/EV/Heatpump system, it might as well still have logical errors. For instance, I cannot test which data I get when the heatpump goes to defrosting mode. Through trial and error I discovered that in this state, it still reports its overall power consumption, but power for heating and drinking water both drop to 0. The Modbus binding for Kermi does not report the actual state directly, so I calculate it from the 3 power consumption readings.

Requirements

Unlike a lot of other energy flow widgets, it has no dependencies despite relying on oh-context (available from OH 4.2.1 onwards). No icon files used.

Usage

The widget is based on my own configuration, which determines the available items/properties.

  • Fronius Gen24 inverter/smart meter
  • Kermi Heatpump (Modbus TCP binding)
  • Tesla EV

Based on the readings I can get from these devices, the widget supports the following data sources.

  • PV inverter
    • Total solar power
    • MPPT1/MPPT2 power (individual MPPT readings due to the solar plant being a West/East-configuration)
    • House power load
    • Solar battery power
    • Solar battery SOC
    • Grid power
    • Grid autonomy
  • EV
    • Charging power
    • Charging state
    • Connection state
    • EV battery SOC
  • Heat pump
    • Electric power
    • Electric power - heating (required to determine current operating state)
    • Electric power - drinking water (required to determine current operating state)
    • Water temperature - pick any value you like, I chose the hot water flow from the pump
    • Overall COP

In addition, you can customize the maximum power values for your solar plant, your grid from/to and your heat pump. And you can configure CSS color names to polish up the colors of the 6 boxes (you know all 140 CSS colors by name, don’t you? :sweat_smile: ). Defaults are chosen to be readable on both dark and light themes.

Credits

Honorable mentions and a lot of thanks go to the creators of the 2 widgets mentioned before.

  • @pwoehrer for his fronius widget which also gave me some insight on how to use CSS in widgets
  • @james_dinniss for providing the basic layout and widget logic I could then built upon

Changelog

Version 0.1

  • initial release

Resources

uid: generic_energy_flow_v2
tags: []
props:
  parameters:
    - default: "10000"
      description: Maximum solar inverter power in watt. Used to calculate usage
        percent background.
      label: Maximum inverter power
      name: power_solar_max
      required: true
      type: INTEGER
      min: 1000
      groupName: design
    - default: "22000"
      description: Maximum power to and from grid in watt. Used to calculate from/to
        grid percent background.
      label: Maximum grid power
      name: power_grid_max
      required: false
      type: INTEGER
      min: 1
      groupName: design
      advanced: true
    - default: "8000"
      description: Maximum electric power the heatpump can consume. Used to calculate
        heatpump power percent background.
      label: Maximum heatpump power
      name: power_heatpump_max
      required: false
      type: INTEGER
      min: 1000
      groupName: design
      advanced: true
    - default: gold
      description: CSS color name for the solar plant
      label: Solar plant color
      name: color_solar
      required: false
      type: TEXT
      groupName: design
      advanced: true
    - default: green
      description: CSS color name for the house
      label: House color
      name: color_house
      required: false
      type: TEXT
      groupName: design
      advanced: true
    - default: seagreen
      description: CSS color name for the solar battery
      label: Battery color
      name: color_battery
      required: false
      type: TEXT
      groupName: design
      advanced: true
    - default: royalblue
      description: CSS color name for the electric vehicle
      label: EV color
      name: color_ev
      required: false
      type: TEXT
      groupName: design
      advanced: true
    - default: red
      description: CSS color name for the heat pump
      label: Heat pump color
      name: color_heatpump
      required: false
      type: TEXT
      groupName: design
      advanced: true
    - default: gray
      description: CSS color name for the grid
      label: Grid color
      name: color_grid
      required: false
      type: TEXT
      groupName: design
      advanced: true
    - context: item
      description: Current solar power in Watts. Typically this would be the solar
        yield item for a Fronius inverter.
      label: Solar power
      name: power_solar
      required: false
      type: TEXT
      groupName: data_sources_pv
    - context: item
      description: Current solar power of MPPT1 in Watts.
      label: MPPT 1
      name: power_solar1
      required: false
      type: TEXT
      groupName: data_sources_pv
    - context: item
      description: Current solar power of MPPT2 in Watts
      label: MPPT 2
      name: power_solar2
      required: false
      type: TEXT
      groupName: data_sources_pv
    - context: item
      description: Current energy autonomy Smartmeter.
      label: Autonomy
      name: power_autonomy
      required: true
      type: TEXT
      groupName: data_sources_pv
    - context: item
      description: Power the grid delivers or receives
      label: Grid power
      name: power_grid
      required: false
      type: TEXT
      groupName: data_sources_pv
    - context: item
      description: Power the solar battery delivers or consumes
      label: Battery power
      name: power_battery
      required: false
      type: TEXT
      groupName: data_sources_pv
    - context: item
      description: State of charge of the solar battery
      label: Solar battery SOC
      name: soc_battery
      required: false
      type: TEXT
      groupName: data_sources_pv
    - context: item
      description: Power the heating system consumes for heating and drinking water.
      label: Heating system power consumption
      name: power_heatpump
      required: false
      type: TEXT
      groupName: data_sources_heating
    - context: item
      description: Heating system power consumption for heating. Only used to
        determine heating system state.
      label: Heating system power consumption for heating
      name: power_heating
      required: false
      type: TEXT
      groupName: data_sources_heating
    - context: item
      description: Heating system power consumption for drinking water. Only used to
        determine heating system state.
      label: Heating system power - drinking water
      name: power_drinking
      required: false
      type: TEXT
      groupName: data_sources_heating
    - context: item
      description: Heating system water temperature - pre-flow temp of heat pump or
        whatever value suits your needs.
      label: Heating system water temperature
      name: temp_heatpump
      required: false
      type: TEXT
      groupName: data_sources_heating
    - context: item
      description: Heating system efficiency (Coefficient of power, COP)
      label: Heating system COP
      name: status_heatpump
      required: false
      type: TEXT
      groupName: data_sources_heating
    - context: item
      description: Current charging power of the EV battery
      label: EV battery charging power
      name: power_ev
      required: false
      type: TEXT
      groupName: data_sources_ev
    - context: item
      description: EV battery state of charge
      label: EV battery percent
      name: soc_ev
      required: false
      type: TEXT
      groupName: data_sources_ev
    - context: item
      description: EV driving range estimated
      label: EV range
      name: range_ev
      required: false
      type: TEXT
      groupName: data_sources_ev
    - context: item
      description: Current charging state of the EV
      label: EV charging state
      name: chargestate_ev
      required: false
      type: TEXT
      groupName: data_sources_ev
    - context: item
      description: Current connection state of the EV
      label: EV connection state
      name: connected_ev
      required: false
      type: TEXT
      groupName: data_sources_ev
    - context: item
      description: Current house power consumption
      label: House power
      name: power_house
      required: false
      type: TEXT
      groupName: data_sources_pv
  parameterGroups:
    - name: design
      label: Design elements
      description: Group of parameters which determine the visual appearance of the widget.
    - name: data_sources_pv
      label: Data sources (solar plant)
      description: Group of parameters which deliver data for solar power production.
    - name: data_sources_ev
      label: Data sources (electric vehicle)
      description: Group of parameters which deliver data for electric vehicles.
    - name: data_sources_heating
      label: Data sources (heating system)
      description: Group of parameters which deliver data for heating and drinking water.
timestamp: Mar 30, 2026, 6:01:39 PM
component: f7-block
config:
  style:
    width: 700px
slots:
  default:
    - component: oh-context
      config:
        comment: "'resize_dot' adjusts the size of the energy flow dots in relation to
          the highest value in the system. The value correlates to the area of
          the dot, not its radius to give a better visual feedback.
          'switch_magnitude' switches between W an kW depending on the input
          value."
        functions:
          resize_dot: "=value => Math.abs(value) < 10 ? 0 :
            `${Math.pow(Math.max(Math.abs(value) / Math.max(
            (Math.abs(#props.power_house) + (#props.power_grid > 0 ?
            #props.power_grid : 0)), Math.abs(#props.power_grid),
            Math.abs(#props.power_solar), Math.abs(#props.power_battery),
            Math.abs(#props.power_ev), Math.abs(#props.power_heatpump)), 0.05),
            0.5) * 7}px`"
          switch_magnitude: "=value => Math.abs(value) > 999 ? `${(Math.abs(value) /
            1000).toFixed(Math.abs(value) > 9999 ? 1 : 2)} kW` :
            `${Math.round(Math.abs(value)) | 0} W`"
      slots:
        default:
          - component: f7-row
            config:
              style:
                height: 90px
                display: flex
            slots:
              default:
                - component: f7-col
                  config:
                    style:
                      width: 5%
                      height: 70px
                - component: f7-col
                  config:
                    comment: box - solar plant
                    align: center
                    style:
                      width: 15%
                      height: 100%
                      display: flex
                      align-items: center
                      justify-content: space-around
                      flex-direction: column
                      border: ='1px solid '+props.color_solar
                      border-radius: 10px
                      background: ='linear-gradient(to top, transparent 0%, '+props.color_solar+' '+
                        (#props.power_solar*100/props.power_solar_max)+'%,
                        transparent '+
                        (#props.power_solar*100/props.power_solar_max)+'% )'
                      fill: none
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          icon: =(#props.power_solar) > (props.power_solar_max/20) ?
                            'f7:sun_max_fill':'f7:sun_max'
                      - component: Label
                        config:
                          text: ="Total " + fn.switch_magnitude(#props.power_solar)
                          style:
                            font-size: 12px
                            font-weight: bold
                      - component: Label
                        config:
                          text: ="MPPT1 " + fn.switch_magnitude(#props.power_solar1)
                          style:
                            font-size: 12px
                      - component: Label
                        config:
                          text: ="MPPT2 " + fn.switch_magnitude(#props.power_solar2)
                          style:
                            font-size: 12px
                - component: f7-col
                  config:
                    style:
                      width: 25%
                      height: 100%
                  slots:
                    default:
                      - component: svg
                        config:
                          comment: energy flow - solar plant
                          viewBox: 0 0 170 50
                          xlmns: http://www.w3.org/2000/svg
                          style:
                            height: 100%
                            width: 100%
                        slots:
                          default:
                            - component: path
                              config:
                                id: energyflowsolarplant
                                d: M 0 35 L 121 35 A 35 35 0 0 1 156 70
                                stroke: =props.color_solar
                                stroke-width: 1
                                fill: none
                            - component: oh-repeater
                              config:
                                comment: Sets up the circles, indicating the energy flow.
                                for: offset
                                fragment: true
                                rangeStart: 0
                                rangeStep: 1
                                rangeStop: 3
                                sourceType: range
                              slots:
                                default:
                                  - component: circle
                                    config:
                                      fill: =props.color_solar
                                      r: =fn.resize_dot(#props.power_solar)
                                    slots:
                                      default:
                                        - component: animateMotion
                                          config:
                                            begin: =`${loop.offset}s`
                                            calcMode: linear
                                            dur: 4s
                                            repeatCount: indefinite
                                          slots:
                                            default:
                                              - component: mpath
                                                config:
                                                  xlink:href: "#energyflowsolarplant"
                - component: f7-col
                  config:
                    align: center
                    style:
                      width: 10%
                      height: 100%
                      fill: none
                - component: f7-col
                  config:
                    style:
                      width: 25%
                      height: 100%
                  slots:
                    default:
                      - component: svg
                        config:
                          comment: energy flow - EV
                          viewBox: 0 0 170 50
                          xlmns: http://www.w3.org/2000/svg
                          style:
                            height: 100%
                            width: 100%
                        slots:
                          default:
                            - component: path
                              config:
                                id: energyflowev
                                d: M 0 70 A 35 35 0 0 1 35 35 L 170 35
                                stroke: =props.color_ev
                                stroke-width: 1
                                visible: =(@props.connected_ev) == 'disconnected' ? false:true
                                fill: none
                            - component: oh-repeater
                              config:
                                comment: Sets up the circles, indicating the energy flow.
                                for: offset
                                fragment: true
                                rangeStart: 0
                                rangeStep: 1
                                rangeStop: 3
                                sourceType: range
                              slots:
                                default:
                                  - component: circle
                                    config:
                                      fill: =props.color_ev
                                      r: =fn.resize_dot(#props.power_ev)
                                    slots:
                                      default:
                                        - component: animateMotion
                                          config:
                                            begin: =`${loop.offset}s`
                                            calcMode: linear
                                            dur: 4s
                                            repeatCount: indefinite
                                          slots:
                                            default:
                                              - component: mpath
                                                config:
                                                  xlink:href: "#energyflowev"
                - component: f7-col
                  config:
                    comment: box - EV
                    align: center
                    style:
                      width: 15%
                      height: 100%
                      display: flex
                      align-items: center
                      justify-content: space-around
                      flex-direction: column
                      border: ='1px solid '+props.color_ev
                      border-radius: 10px
                      background: ='linear-gradient(to top, transparent 0%, '+props.color_ev+' '+#props.soc_ev + '%,
                        transparent '+#props.soc_ev+'% )'
                      fill: none
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          icon: f7:car_fill
                      - component: Label
                        config:
                          text: =@props.chargestate_ev
                          style:
                            font-size: 12px
                            font-weight: bold
                      - component: Label
                        config:
                          text: =fn.switch_magnitude(#props.power_ev)
                          style:
                            font-size: 12px
                            font-weight: bold
                      - component: Label
                        config:
                          text: =#props.soc_ev+" % | "+@props.range_ev
                          style:
                            font-size: 12px
                - component: f7-col
                  config:
                    style:
                      width: 5%
                      height: 70px
          - component: f7-row
            config:
              style:
                width: 100%
                height: 90px
            slots:
              default:
                - component: f7-col
                  config:
                    align: center
                    style:
                      width: 20%
                      height: 100%
                      display: flex
                      align-items: center
                      justify-content: space-around
                      flex-direction: column
                - component: f7-col
                  config:
                    align: center
                    style:
                      width: 20%
                      height: 100%
                - component: f7-col
                  config:
                    comment: box - house
                    align: center
                    style:
                      width: 20%
                      height: 100%
                      display: flex
                      align-items: center
                      justify-content: space-around
                      flex-direction: column
                      border: ='1px solid '+props.color_house
                      border-radius: 20px
                      background: ='linear-gradient(to top, transparent 0%, '+props.color_house+' '+
                        (#props.power_autonomy*100)+'%, transparent '+
                        (#props.power_autonomy*100)+'% )'
                      fill: none
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          icon: f7:house
                      - component: Label
                        config:
                          text: ="Consumption"
                          style:
                            font-size: 12px
                            font-weight: bold
                      - component: Label
                        config:
                          text: =fn.switch_magnitude(#props.power_house)
                          style:
                            font-size: 12px
                            font-weight: bold
                      - component: Label
                        config:
                          text: ="Autonomy "+@props.power_autonomy
                          style:
                            font-size: 12px
                - component: f7-col
                  config:
                    comment: energy flow - battery
                    style:
                      width: 20%
                      height: 100%
                  slots:
                    default:
                      - component: svg
                        config:
                          viewBox: 0 0 156 70
                          xlmns: http://www.w3.org/2000/svg
                          style:
                            height: 100%
                            width: 100%
                        slots:
                          default:
                            - component: path
                              config:
                                id: energyflowbattery
                                d: '=(#props.power_battery) > 0 ? "M 156 35 L 0 35" : "M 0 35 L 156 35"'
                                stroke: =props.color_battery
                                stroke-width: 1
                                fill: none
                            - component: oh-repeater
                              config:
                                comment: Sets up the circles, indicating the energy flow.
                                for: offset
                                fragment: true
                                rangeStart: 0
                                rangeStep: 1
                                rangeStop: 3
                                sourceType: range
                              slots:
                                default:
                                  - component: circle
                                    config:
                                      fill: =props.color_battery
                                      r: =fn.resize_dot(#props.power_battery)
                                    slots:
                                      default:
                                        - component: animateMotion
                                          config:
                                            begin: =`${loop.offset}s`
                                            calcMode: linear
                                            dur: 4s
                                            repeatCount: indefinite
                                          slots:
                                            default:
                                              - component: mpath
                                                config:
                                                  xlink:href: "#energyflowbattery"
                - component: f7-col
                  config:
                    comment: box - battery
                    align: center
                    style:
                      width: 15%
                      height: 100%
                      border: ='1px solid '+props.color_battery
                      border-radius: 10px
                      background: ='linear-gradient(to top, transparent 0%, '+props.color_battery+'
                        '+(#props.soc_battery*100)+ '%, transparent
                        '+(#props.soc_battery*100)+'% )'
                      fill: none
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          icon: =(#props.soc_battery) > 0.5 ? 'f7:battery_100':'f7:battery_25'
                      - component: Label
                        config:
                          text: =(#props.power_battery) > 0 ? "Discharging":"Charging"
                          style:
                            font-size: 12px
                            font-weight: bold
                      - component: Label
                        config:
                          text: =fn.switch_magnitude(#props.power_battery)
                          style:
                            font-size: 12px
                            font-weight: bold
                      - component: Label
                        config:
                          text: =@props.soc_battery
                          style:
                            font-size: 12px
                - component: f7-col
                  config:
                    style:
                      width: 5%
                      height: 70px
          - component: f7-row
            config:
              style:
                width: 100%
                height: 90px
            slots:
              default:
                - component: f7-col
                  config:
                    align: center
                    style:
                      width: 5%
                      height: 100%
                      display: flex
                      align-items: center
                      justify-content: space-around
                      flex-direction: column
                - component: f7-col
                  config:
                    comment: box - grid
                    align: center
                    style:
                      width: 15%
                      height: 100%
                      display: flex
                      align-items: center
                      justify-content: space-around
                      flex-direction: column
                      border: ='1px solid '+props.color_grid
                      border-radius: 10px
                      background: ='linear-gradient('+(#props.power_grid > 0 ? "to top":"to
                        bottom")+', transparent 0%, '+props.color_grid+' '+
                        Math.abs(#props.power_grid*100/props.power_grid_max)+'%,
                        transparent '+
                        Math.abs(#props.power_grid*100/props.power_grid_max)+'%
                        )'
                      fill: none
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          icon: if:mdi:transmission-tower
                      - component: Label
                        config:
                          text: ="Power "+(#props.power_grid > 0 ? "from":"to")+" grid"
                          style:
                            font-size: 12px
                            font-weight: bold
                      - component: Label
                        config:
                          text: =fn.switch_magnitude(#props.power_grid)
                          style:
                            font-size: 12px
                            font-weight: bold
                - component: f7-col
                  config:
                    style:
                      width: 25%
                      height: 100%
                  slots:
                    default:
                      - component: svg
                        config:
                          comment: energy path - grid
                          viewBox: 0 0 170 90
                          xlmns: http://www.w3.org/2000/svg
                          style:
                            height: 100%
                            width: 100%
                        slots:
                          default:
                            - component: path
                              config:
                                id: energyflowgrid
                                d: '=(#props.power_grid) < 0 ? "M 156 0 A 35 35 0 0 1 121 35 L 0 35 " : "M 0 35
                                  L 121 35 A 35 35 0 0 0 156 0"'
                                stroke: =props.color_grid
                                stroke-width: 1
                                fill: none
                            - component: oh-repeater
                              config:
                                comment: Sets up the circles, indicating the energy flow.
                                for: offset
                                fragment: true
                                rangeStart: 0
                                rangeStep: 1
                                rangeStop: 3
                                sourceType: range
                              slots:
                                default:
                                  - component: circle
                                    config:
                                      fill: =props.color_grid
                                      r: =fn.resize_dot(#props.power_grid)
                                    slots:
                                      default:
                                        - component: animateMotion
                                          config:
                                            begin: =`${loop.offset}s`
                                            calcMode: linear
                                            dur: 4s
                                            repeatCount: indefinite
                                          slots:
                                            default:
                                              - component: mpath
                                                config:
                                                  xlink:href: "#energyflowgrid"
                - component: f7-col
                  config:
                    align: center
                    style:
                      width: 10%
                      height: 100%
                      fill: none
                - component: f7-col
                  config:
                    comment: energy path - heatpump
                    style:
                      width: 25%
                      height: 100%
                  slots:
                    default:
                      - component: svg
                        config:
                          viewBox: 0 0 170 90
                          xlmns: http://www.w3.org/2000/svg
                          style:
                            height: 100%
                            width: 100%
                        slots:
                          default:
                            - component: path
                              config:
                                id: energyflowheatpump
                                d: M 0 0 A 35 35 0 0 0 35 35 L 170 35
                                stroke: =props.color_heatpump
                                stroke-width: 1
                                fill: none
                            - component: oh-repeater
                              config:
                                comment: Sets up the circles, indicating the energy flow.
                                for: offset
                                fragment: true
                                rangeStart: 0
                                rangeStep: 1
                                rangeStop: 3
                                sourceType: range
                              slots:
                                default:
                                  - component: circle
                                    config:
                                      fill: =props.color_heatpump
                                      r: =fn.resize_dot(#props.power_heatpump)
                                    slots:
                                      default:
                                        - component: animateMotion
                                          config:
                                            begin: =`${loop.offset}s`
                                            calcMode: linear
                                            dur: 4s
                                            repeatCount: indefinite
                                          slots:
                                            default:
                                              - component: mpath
                                                config:
                                                  xlink:href: "#energyflowheatpump"
                - component: f7-col
                  config:
                    comment: box - heatpump
                    align: center
                    style:
                      width: 15%
                      height: 100%
                      display: flex
                      align-items: center
                      justify-content: space-around
                      flex-direction: column
                      border: ='1px solid '+props.color_heatpump
                      border-radius: 10px
                      background: ='linear-gradient(to top, transparent 0%, '+props.color_heatpump+' '+
                        (#props.power_heatpump*100/props.power_heatpump_max)+'%,
                        transparent '+
                        (#props.power_heatpump*100/props.power_heatpump_max)+'%
                        )'
                      fill: none
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          icon: f7:thermometer
                      - component: Label
                        config:
                          text: =(#props.power_heating) > 0 ? "Heating":(#props.power_drinking) > 0 ?
                            "Drinking water":(#props.power_heatpump) > 0 ? "Defrosting":"Standby"
                          style:
                            font-size: 12px
                            font-weight: bold
                      - component: Label
                        config:
                          text: =fn.switch_magnitude(#props.power_heatpump)
                          style:
                            font-size: 12px
                            font-weight: bold
                      - component: Label
                        config:
                          text: ="COP " + (#props.status_heatpump) + " | "  + (@props.temp_heatpump)
                          style:
                            font-size: 12px
                - component: f7-col
                  config:
                    style:
                      width: 5%
                      height: 70px

4 Likes

Did you modify your Corvette C3? (I recognized your name from another forum) :grinning_face:

I put a 87kWh battery in it :rofl: you remember correctly.

1 Like

Hi Martin,

glad my code was of any help, great widget!

Cheers

Peter