[OH 4.2] Custom Fronius like solar widget [4.2.1;5.0.0]

solarload

There are a lot of Energy Flow Widgets for Fronius (and other brands) around, which try to imitate the ones seen on the manufacturer’s apps. Though there are some very nice ones out there, none of them quite matches what I had in mind.

I also found many of them - while working fine - much too convoluted and complicated. They try and wring some functionality from standard components, which are clearly not intended for such use and are rather limited in their application, when trying to replicate a very specific look.

So this is my first attempt at one of the gauges. Eventually it is intended to be used with others (for battery, load, inverter and grid), but also works as standalone widget. It scales freely to the desired size (determined by the parent component) and is built almost completely using SVG, which makes for a much shallower document tree and provides a lot more freedom of design and fine grained control. The downside of this approach is of course a lot of exposed mathematics. I tried to tame some of it by using functions with hopefully self explaining names.

The widget uses an exponential scale (to the power of 0.68). The exponent is completely arbitrary and chosen for aesthetic reasons. Sensible values would range from 0.5 to 2.0). I chose this exponent to allow for a more fine grained display of lower values and more densely packed higher ones. If it is configured with 1, the scale will be linear. It has 20 segments (hence the rangeStep of 13.5).

Any other values should be fairly clear.

You can get to some of the configuration options by choosing “Show advanced”.

You may wonder why I did not include the battery, grid and inverter icons as well. While they all may look similar, under the hood they are completely different beasts. The battery widget has to deal with percentages of charge and all of them can have the power flowing in both directions. The battery also needs dynamic icons. So instead of incorporating all this functionality into one huge mess of code, I may be doing separate widgets for those. Don’t hold your breath though.

Additionally to the pv icon, you will need the consumption icon. Both of them have to be present in the /icons/classic directory.

I hope you like it. It also should be fairly easy to create the other parts of the fronius gauge, using this one as a template.

Changelog

Version 1.1

  • added: customization options for the exponential scale, icon and base color. Grouped some of them.
  • added: Comments.
  • changed: The widget can now be used for pv power as well as load power visualization.
  • changed: Numerical representation switches to kW when the value exceeds 1000 W.
  • changed: Cleaned up some formulas.

Version 1.0

  • initial release

Resources

uid: fronius_pv_gauge
tags:
  - energy
  - solar
  - fronius
props:
  parameters:
    - default: "8000"
      description: Maximum power in Watts.
      label: Maximum Power
      name: max_power
      required: false
      type: INTEGER
      min: 1
    - context: item
      description: Current power in Watts. Typically this would be the solar_yield,
        consumption, battery load etc.
      label: Power
      name: power
      required: false
      type: TEXT
    - default: "100"
      description: The size of the widget.
      label: Widget size
      name: size
      required: false
      type: INTEGER
      groupName: design
      advanced: true
    - default: "0.68"
      description: "Exponent for the scale (default: 0.68; 1=linear, reasonable values
        range from 0.5 to 2)"
      label: Exponent
      name: exponent
      required: false
      groupName: design
      advanced: true
    - default: pv
      description: Icon for the widget. The icon selection determines the base color
        for the widget as well.
      label: Icon
      name: icon
      required: false
      type: TEXT
      groupName: design
      advanced: true
      limitToOptions: true
      options:
        - label: PV
          value: pv
        - label: Load
          value: consumption
  parameterGroups:
    - name: design
      label: Design elements
      description: Group of parameters which determine the visual appearance of the widget.
timestamp: Aug 4, 2024, 1:53:32 PM
component: f7-block
config:
  comment: This component determines the size of the widget.
  style:
    width: =props.size + "px"
    height: =props.size + "px"
    padding: 0px
slots:
  default:
    - component: oh-context
      config:
        comment: "'arc_flag' determines which arc should be drawn (short or long).
          'scale_to_deg' makes sure the input value does not exceed to max_power
          and scales everything to 270°. 'switch_color' handles power less than
          1 and sets the color dependent on the icon selected, based on the
          dictionary in the function. 'to_cartesian' converts degree to
          cartesian coordinates to be used by arc."
        functions:
          arc_flag: "=(degree) => degree < 180 ? 0 : 1"
          scale_to_deg: =(value) => Math.pow(Math.min(Math.abs(value), props.max_power) *
            (270 / props.max_power), props.exponent) * (270 / Math.pow(270,
            props.exponent))
          switch_color: '=(value) => (Math.abs(value) >= 1) ? {"pv": "#f7c002",
            "consumption": "#70afcd"}[props.icon] : "grey"'
          to_cartesian: =(degree) => (50 + Math.sin(degree * Math.PI / 180) * 41) + " " +
            (50 - Math.cos(degree * Math.PI / 180) * 41)
      slots:
        default:
          - component: svg
            config:
              comment: The component scales freely to the size of its parent.
              width: 100%
              height: 100%
              viewBox: 0 0 100 100
            slots:
              default:
                - component: circle
                  config:
                    comment: Outer ring. The stroke color uses the base color with less opacity.
                    style:
                      stroke-width: 1px
                      stroke: =fn.switch_color(#props.power)
                      stroke-opacity: 0.4
                    r: 49.5px
                    cx: 50px
                    cy: 50px
                    fill: white
                - component: path
                  config:
                    comment: Background of the dial. The stroke color uses the base color with less
                      opacity.
                    style:
                      stroke-width: 14px
                      stroke: =fn.switch_color(#props.power)
                      stroke-opacity: 0.4
                    d: M50 9 A41 41 0 1 1 9 50
                    r: 41px
                    cx: 50px
                    cy: 50px
                    fill: rgba(0, 0, 0, 0)
                - component: circle
                  config:
                    comment: Inner ring. The stroke color uses the base color with less opacity.
                    style:
                      stroke-width: 1px
                      stroke: =fn.switch_color(#props.power)
                      stroke-opacity: 0.4
                    r: 32.5px
                    cx: 50px
                    cy: 50px
                    fill: rgba(0, 0, 0, 0)
                - component: path
                  config:
                    comment: Dial, using an exponential scale. If the value exceeds max_power, the
                      color dial turns red.
                    style:
                      stroke: '=(Math.abs(#props.power) < props.max_power) ?
                        fn.switch_color(#props.power) : "#f70202"'
                      stroke-width: 14px
                    d: =`M50 9 A41 41 0 ${fn.arc_flag(fn.scale_to_deg(Math.abs(#props.power)))} 1
                      ${fn.to_cartesian(fn.scale_to_deg(Math.abs(#props.power)))}`
                    fill: rgba(0, 0, 0, 0)
                - component: path
                  config:
                    comment: Path for numerical display of input value.
                    id: pv_text_path
                    d: M9,50 A41 41 0 0 1 50 9
                    fill: rgba(0, 0, 0, 0)
                - component: text
                  config:
                    style:
                      font-size: 10px
                      font-family: sans-serif
                      text-anchor: middle
                      fill: rgb(127, 127, 127)
                      letter-spacing: 1px
                      dominant-baseline: central
                  slots:
                    default:
                      - component: textPath
                        config:
                          xlink:href: "#pv_text_path"
                          startOffset: 50%
                          content: "=Math.abs(#props.power) > 999 ? `${(#props.power / 1000).toFixed(2)}
                            kW` : `${Math.round(#props.power) | 0} W`"
                - component: image
                  config:
                    comment: Icons pv.svg and consumption.svg have to be present in /icons/classic/.
                    width: 44px
                    height: 44px
                    xlink:href: =`/icon/${props.icon}?format=svg&anyFormat=true&iconset=classic`
                    x: 28px
                    y: 28px
                - component: oh-repeater
                  config:
                    comment: Draws the segments of the dial, using an exponential scale.
                    for: degree
                    sourceType: range
                    rangeStart: 0
                    rangeStop: 270
                    rangeStep: 13.5
                    fragment: true
                  slots:
                    default:
                      - component: line
                        config:
                          style:
                            stroke-width: 1px
                            stroke: white
                            transform: ="rotate(" + (Math.pow(loop.degree, props.exponent) * 270 /
                              Math.pow(270, props.exponent)) + "deg)"
                            transform-origin: center
                          x1: 50px
                          x2: 50px
                          y1: 2px
                          y2: 16px

pvconsumption

3 Likes

Standalone comments will not make it through the yaml → json → yaml transformation of saving and reloading, but you can add arbitrary keys to the config object of any component, so I just add a comment key when an object needs one:

- component: oh-block
  config:
    comment: This block must have blah blah blah
...rest of config....

Due to the use of the oh-context this widget will not work with earlier versions of OH4 or OH3, so I suggest that in addition to the OH 4.2 indication you already have you also append to the title the version range such as [4.2.0;5.0.0) which will automatically filter this out of the options when users look through the add-on store without the correct version.

2 Likes

Thank you for the suggestions. I will probably incorporate them in V1.1 of the widget together with configurable exponential scaling.

In order for OH to pick up these changes, you must add the latest code to the first post of the thread. Everything below that is ignored by OH when parsing the marketplace entries.

The original template presented to you when you first created the posting is required and should not be deviated from or else OH will not be able to parse your entry and present it to users who want to install it from the marketplace.

In particular, the template has a change log section where you can publish what changed between versions and the very last thing in the template is Resource which is the source of your widget or a link to the source of the widget. There should be nothing on the post after that point.

The tempalte again follows. The headers (## Screenshots, ## Changelog, etc) are required and should not be removed.

> **Please remove this block after reading these instructions**
> Your submission will be reviewed as soon as possible by the staff (moderators and/or admins). They will review the format of the post, but not necessarily the code of the submission itself. If it looks good, the _published_ tag will be added and it will be available in the openHAB UI though the community marketplace add-on service. You can self-assess the maturity level tags yourself and add appropriate tags from _mature_, _stable_, _beta_ or _alpha_.

_[🖍 Add a primary screenshot or a logo here. The first image of the post will be promoted seen in the add-on list in the UI.]_

_[🖍 Replace with your description.]_

## Screenshots

_[🖍 Upload other screenshots if necessary or remove the section.]_

## Changelog

_[🖍 Add a list of the changes you made for each version, as suggested below (remove example sections)]_
### Version 0.2
- fixed: ...
- added: ...
### Version 0.1
- initial release

## Resources
_[🖍 Depending on the content type, you can either add code fences with explicit `yaml` language (<code>\`\`\`yaml ... \`\`\`</code>), or a **direct link** to a .json or *.yaml file hosted by your preferred service: GitHub, GitHub Gist (gist.github.com), etc._

Thank you. I combined both posts into one and applied the template. Sorry about the different approach. I know some places frown upon changing the original post.

In this case we are kind of using a hack to make the marketplace by using the forum to host it. That means we need to set some restrictions on marketplace postings.

Elsewhere in the forum indeed, it can be better not to edit the first post but it’s not a strong rule and is largely context dependent. For example, it makes more sense to edit the first post of a tutorial than to force users to read through potentially hundreds of replies to find the latest verison.

Note, you might still have an issue because you have text after the code of your widget under Resources. All of that text can be and should be moved to just before the Changelog just in case.

You can test whether your widget appears in the Add-on store and is installable by navigating to Settings → Community Marketpalce and toggle on “Show Unpublished Entries”. This will let your post appear even though it doesn’t have the “published” tag yet. You can ensure that everything looks correctly and that you can install it without error.

Hello together

Great widget, thank you.
I have added a picture and extended the code a little. Now you can use the widget for ‘Photovoltaic’, ‘Grid consumption’ and ‘Own consumption’.

Have fun,
Michael

consumption
grid
pv

uid: fronius_pv_gauge
tags:
  - energy
  - solar
  - fronius
props:
  parameters:
    - default: "8000"
      description: Maximum power in Watts.
      label: Maximum Power
      name: max_power
      required: false
      type: INTEGER
      min: 1
    - context: item
      description: Current power in Watts.
      label: Power
      name: power
      required: false
      type: TEXT
    - default: "100"
      description: The size of the widget.
      label: Widget size
      name: size
      required: false
      type: INTEGER
      groupName: design
      advanced: true
    - default: "0.68"
      description: "Exponent for the scale (default: 0.68; 1=linear, reasonable values
        range from 0.5 to 2)"
      label: Exponent
      name: exponent
      required: false
      groupName: design
      advanced: true
    - default: pv
      description: Icon for the widget. The icon selection determines the base color
        for the widget as well.
      label: Icon
      name: icon
      required: false
      type: TEXT
      groupName: design
      advanced: true
      limitToOptions: true
      options:
        - label: PV
          value: pv
        - label: Consumption
          value: consumption
        - label: Grid
          value: grid
  parameterGroups:
    - name: design
      label: Design elements
      description: Group of parameters which determine the visual appearance of the widget.
timestamp: Aug 4, 2024, 1:53:32 PM
component: f7-block
config:
  comment: This component determines the size of the widget.
  style:
    width: =props.size + "px"
    height: =props.size + "px"
    padding: 0px
slots:
  default:
    - component: oh-context
      config:
        comment: "'arc_flag' determines which arc should be drawn (short or long).
          'scale_to_deg' makes sure the input value does not exceed to max_power
          and scales everything to 270°. 'switch_color' handles power less than
          1 and sets the color dependent on the icon selected, based on the
          dictionary in the function. 'to_cartesian' converts degree to
          cartesian coordinates to be used by arc."
        functions:
          arc_flag: "=(degree) => degree < 180 ? 0 : 1"
          scale_to_deg: =(value) => Math.pow(Math.min(Math.abs(value), props.max_power) *
            (270 / props.max_power), props.exponent) * (270 / Math.pow(270,
            props.exponent))
          switch_color: '=(value) => (Math.abs(value) >= 1) ? {"pv": "#f7c002",
            "consumption": "#70afcd","grid": "#ff0000"}[props.icon] : "grey"'
          to_cartesian: =(degree) => (50 + Math.sin(degree * Math.PI / 180) * 41) + " " +
            (50 - Math.cos(degree * Math.PI / 180) * 41)
      slots:
        default:
          - component: svg
            config:
              comment: The component scales freely to the size of its parent.
              width: 100%
              height: 100%
              viewBox: 0 0 100 100
            slots:
              default:
                - component: circle
                  config:
                    comment: Outer ring. The stroke color uses the base color with less opacity.
                    style:
                      stroke-width: 1px
                      stroke: =fn.switch_color(#props.power)
                      stroke-opacity: 0.4
                    r: 49.5px
                    cx: 50px
                    cy: 50px
                    fill: white
                - component: path
                  config:
                    comment: Background of the dial. The stroke color uses the base color with less
                      opacity.
                    cx: 50px
                    cy: 50px
                    d: M50 9 A41 41 0 1 1 9 50
                    fill: rgba(0, 0, 0, 0)
                    r: 41px
                    style:
                      stroke-width: 14px
                      stroke: =fn.switch_color(#props.power)
                      stroke-opacity: 0.4
                - component: circle
                  config:
                    comment: Inner ring. The stroke color uses the base color with less opacity.
                    cx: 50px
                    cy: 50px
                    fill: rgba(0, 0, 0, 0)
                    r: 32.5px
                    style:
                      stroke-width: 1px
                      stroke: =fn.switch_color(#props.power)
                      stroke-opacity: 0.4
                - component: path
                  config:
                    comment: Dial, using an exponential scale. If the value exceeds max_power, the
                      color dial turns red.
                    style:
                      stroke: '=(Math.abs(#props.power) < props.max_power) ?
                        fn.switch_color(#props.power) : "#f70202"'
                      stroke-width: 14px
                    d: =`M50 9 A41 41 0 ${fn.arc_flag(fn.scale_to_deg(Math.abs(#props.power)))} 1
                      ${fn.to_cartesian(fn.scale_to_deg(Math.abs(#props.power)))}`
                    fill: rgba(0, 0, 0, 0)
                - component: path
                  config:
                    comment: Path for numerical display of input value.
                    id: pv_text_path
                    d: M9,50 A41 41 0 0 1 50 9
                    fill: rgba(0, 0, 0, 0)
                - component: text
                  config:
                    style:
                      font-size: 10px
                      font-family: sans-serif
                      text-anchor: middle
                      fill: rgb(127, 127, 127)
                      letter-spacing: 1px
                      dominant-baseline: central
                  slots:
                    default:
                      - component: textPath
                        config:
                          xlink:href: "#pv_text_path"
                          startOffset: 50%
                          content: "=Math.abs(#props.power) > 999 ? `${(#props.power / 1000).toFixed(2)}
                            kW` : `${Math.round(#props.power) | 0} W`"
                - component: image
                  config:
                    comment: Icons pv.svg and consumption.svg have to be present in /icons/classic/.
                    width: 44px
                    height: 44px
                    xlink:href: =`/icon/${props.icon}?format=svg&anyFormat=true&iconset=classic`
                    x: 28px
                    y: 28px
                - component: oh-repeater
                  config:
                    comment: Draws the segments of the dial, using an exponential scale.
                    for: degree
                    sourceType: range
                    rangeStart: 0
                    rangeStop: 270
                    rangeStep: 13.5
                    fragment: true
                  slots:
                    default:
                      - component: line
                        config:
                          style:
                            stroke-width: 1px
                            stroke: white
                            transform: ="rotate(" + (Math.pow(loop.degree, props.exponent) * 270 /
                              Math.pow(270, props.exponent)) + "deg)"
                            transform-origin: center
                          x1: 50px
                          x2: 50px
                          y1: 2px
                          y2: 16px

1 Like

Great that you like the widget. I have made another - more comprehensive one - here.

Nice :grinning: