Adding Widgets via AddOn

I create a widget like the oh-button. This button only triggers the event, if the button is pressed for a certain time. I like to create a addon to contribute the widget. Is this possible? Is there an example to add widgets via osgi bundle?

AFAIK no, you can only contribute widgets via marketplace.

Thanks! Basically there is no need to implement a whole osgi bundle.

Ok, I find out this won’t work for my widget. The Marketplace seems only consist of custom widgets which are composite of basic components. My widget needs a small javascript to work. As far as I understand, it’s only possible to add controls by changing the org.openhab.ui bundle.

Where does your widget look for the javascript ?

You can add instructions to the widget doc where to download the script and where to put it.

Maybe, I do it the wrong way. I implement a component similar to the oh-button.vue in org.openhab.ui called oh-long-press-button.vue. This one has an additional script part, which is necessary to get the time when the button is pressed (plays a small animation - filling up the button). If the button is released it checks if the was pressed a specific time (from property) and send the message to the server via perform action.
Thanks for help.

Sorry, but I have no knowledge of developing vue components.
Have just done marketplace widgets.

1 Like

There are two aspects to that:

  1. Custom widget registry
  2. Custom script to be run in browser

For first point - I believe this is coming into OH 3.3 via https://github.com/openhab/openhab-core/issues/2513 and related changes (it caused some friction in few places).
For second point its something you need to confirm with mainui: Enable widgets to use Javascript · Issue #626 · openhab/openhab-webui · GitHub. As far I can see, your custom vue element will rather not fly with UI.

Given that UI is compiled into an optimized javascript there is no “injection point” other than permitted ones. And the only permitted javascript which can be evaluated by browser are basic expressions you can do in component with =something ? 'a' : 'b' syntax.

You could try to inject your widget javascript code separately into browser through preloading of your javascript/vue element but I honestly have no idea how it could fly as you would end up with multiple vue contexts on one page. :slight_smile:

EDIT: maybe something like this would help UI: https://single-spa.js.org/docs/ecosystem-vue/

1 Like

This isn’t 100% true. Basic js can be built into widgets directly. The f7 components (and probably by extension their oh derivatives) all accept the appropriate basic event definitions. For example, I have a widget where I want dynamic text scaling (which requires knowing the width of the parent element) and use:

    - component: f7-block
      config:
        onclick: ="document.getElementById('base-block-"  + props.item + "').style.setProperty('--nest-width',document.getElementById('base-therm-" + props.item + "').offsetHeight + 'px');"

This successfully sets the css variable when the element is clicked, and, as you can see is even preprocessed by the expression parser.

Here’s a more complete example that combines the js with the tag property in the f7-row component turning it into a canvas for js drawing:

uid: demo:js
tags: []
props:
  parameters: []
  parameterGroups: []
component: f7-card
config:
  title: OnClick JS example
slots:
  default:
    - component: f7-row
      config:
        onclick: >
          var c = document.getElementById("canvas1");
          var ctx = c.getContext("2d");
          ctx.fillStyle = "red";
          ctx.fillRect(20, 20, 75, 50);
          ctx.globalAlpha = 0.2;
          ctx.fillStyle = "blue";
          ctx.fillRect(50, 50, 75, 50);
          ctx.fillStyle = "green";
          ctx.fillRect(80, 80, 75, 50);
        tag: button
        type: button
      slots:
        default:
          - component: Label
            config:
              text: Click me to draw.
    - component: f7-row
      config:
        id: canvas1
        tag: canvas

Using the above pattern it is even possible to: 1) run some js on widget load because a f7-row can have a script tag set and script elements accept onload events, and 2) run js from an outside library file in the \static folder because you can set the src property for that script element.

I don’t know if any of this helps with the original question about how to deploy the OPs widget.

1 Like

Are you sure about use of anything more than props and vars in widget code? From my tests it seems that any reference to document, navigator and so on fail to give any output:

You can’t just put any js into most of the widget properties. As you said above those are limited to what js-like syntax is available through the expression parser.

To my knowledge, the actual js only works in properties that the f7 components pass directly on to the elements, like the event properties.

1 Like

That’s really great to know as I come over these constraints far too many times. This will definitely help OP, and other visitors to this topic, to deliver more advanced functionality! :slight_smile:

Thanks, I will try this suggestion.

As a developer I think this is quite an ugly solution. I was able to implement this widget/component in org.openahb.ui in a couple of minutes. Now my code will be scattered in couple of YAML text properties. This makes it much harder to support and develop a good solution.
On the other hand: To support a fork of og.openhab.ui is much more worst ;-).

Have you discussed with @ysc submitting it as a PR to the main repo?

Yep. About as ugly as it gets.

1 Like

Declarative approach is fine for basic and simple things. Its getting worse when you try to build something more complex. Nobody really complains at grafana dashboards cause we usually see nice, rounded corners of polished UI. We don’t look at how it is being stored and processed, if someone is brave enough to write a plugin to it, the UI is strictly collaborating with backend. Here, with OH ui, we don’t have such luxury as backend API is untouchable.
With mainui we see an attempt to cut make a mixed way to avoid (till some degree) HTML and CSS and JS. On other hand, when someone needs any of above, there is no clear guidance on how to achieve that. I see a lot of great examples of widgets on forums. Kudos to their authors and sacrifice to learn all this stuff.

I have couple of managed widgets working since a year or so. I been generating them in two ways - most complex which query available items to render pages/widgets - with custom java code, more repetitive parts I do since few months with handlebars templates which generate YAML which is then parsed and mapped to internal representation of ui components (widgets and pages).

When I say “complex” I mean widgets which contents is conditional and which do not require me to pass million options. Instead they rely on tag queries within loading, as making 10 calls to itemsWithTags to render single label on widget on each browser visit is far from being optimal, saving configuration to bare minimum. As semantic model is well… just a bunch of tags, but in UI you can’t query it as an graph, server side lookups are just more flexible. Parametrization of widget config is actually limited to visible reporting/aggregation dimensions as they directly impact also page links which should be generated.

Below you can see two expressions which are working with OH ui. :slight_smile: First one I even managed to maintain for few weeks across few installations. Later I got into troubles and had to make a generator.

"( vars.period !== undefined && vars.period === '_Current' ? 'page:installations_photovoltaic_realtime' : ( !(vars.period !== undefined) || vars.period === '_DAY' ? 'page:installations_photovoltaic_hour' : ( vars.period !== undefined && vars.period === '_YEAR' ? 'page:installations_photovoltaic_month' : ( vars.period !== undefined && vars.period === '_MONTH' ? 'page:installations_photovoltaic_day' : '#' ) ) ) )"

Note you see specific item names below, but these are result of tag lookup. See later part with template helpers.

  ( items['Installations_Electricity_ElectricMeter_2_Power'] !== undefined ? ( items['Installations_Electricity_ElectricMeter_2_Power'].displayState !== undefined && items['Installations_Electricity_ElectricMeter_2_Power'].displayState.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Power'].displayState.trim() !== '-' ? items['Installations_Electricity_ElectricMeter_2_Power'].displayState :
  ( items['Installations_Electricity_ElectricMeter_2_Power'].state !== undefined && items['Installations_Electricity_ElectricMeter_2_Power'].state.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Power'].state.trim() !== '-' ?
  ( items['Installations_Electricity_ElectricMeter_2_Power'].state === 'NULL' ? ' ' :
  ( items['Installations_Electricity_ElectricMeter_2_Power'].state === '❔' ? ' ' : items['Installations_Electricity_ElectricMeter_2_Power'].state ) ) : '' ) ) : '' ) : ( vars.period !== undefined && vars.period === '_YEAR' ?
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'] !== undefined ? ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'].displayState !== undefined && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'].displayState.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'].displayState.trim() !== '-' ? items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'].displayState :
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'].state !== undefined && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'].state.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'].state.trim() !== '-' ?
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'].state === 'NULL' ? ' ' :
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'].state === '❔' ? ' ' : items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Yearly'].state ) ) : '' ) ) : '' ) : ( vars.period !== undefined && vars.period === '_MONTH' ?
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'] !== undefined ? ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'].displayState !== undefined && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'].displayState.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'].displayState.trim() !== '-' ? items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'].displayState :
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'].state !== undefined && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'].state.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'].state.trim() !== '-' ?
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'].state === 'NULL' ? ' ' :
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'].state === '❔' ? ' ' : items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Monthly'].state ) ) : '' ) ) : '' ) : ( !(vars.period !== undefined) || vars.period === '_DAY' ?
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'] !== undefined ? ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'].displayState !== undefined && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'].displayState.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'].displayState.trim() !== '-' ? items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'].displayState :
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'].state !== undefined && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'].state.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'].state.trim() !== '-' ?
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'].state === 'NULL' ? ' ' :
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'].state === '❔' ? ' ' : items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Daily'].state ) ) : '' ) ) : '' ) : ( vars.period !== undefined && vars.period === '_HOUR' ?
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'] !== undefined ? ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'].displayState !== undefined && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'].displayState.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'].displayState.trim() !== '-' ? items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'].displayState :
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'].state !== undefined && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'].state.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'].state.trim() !== '-' ?
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'].state === 'NULL' ? ' ' :
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'].state === '❔' ? ' ' : items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_Hourly'].state ) ) : '' ) ) : '' ) : ( vars.period !== undefined && vars.period === '_QUARTER_HOUR' ?
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'] !== undefined ? ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'].displayState !== undefined && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'].displayState.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'].displayState.trim() !== '-' ? items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'].displayState :
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'].state !== undefined && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'].state.trim().length > 0 && items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'].state.trim() !== '-' ?
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'].state === 'NULL' ? ' ' :
  ( items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'].state === '❔' ? ' ' : items['Installations_Electricity_ElectricMeter_2_Total_Energy_Counter_Utilization_QuarterHourly'].state ) ) : '' ) ) : '' ) : ' ' ) ) ) ) ) );

For templates, its gets much closer in shape to what we usually see in UI cause it is just a yaml with bunch of fragments and helpers to ie. translate item labels. I guess that at some point of time I could port custom widget provider and its expression generator to get it working in below way.

basic: true
--
uid: installations_grid_day
{{>fragment_chart_header chartType="month" chartLabel=(translate scope="dictionary" key="chart_grid_title_month" fallback="Grid utilization (M)")}}
slots:
  grid:
    - component: oh-chart-grid
      config:
        containLabel: true
  xAxis:
    {{>fragment_category_axis categoryType="month"}}
  yAxis:
    {{>fragment_energy_axis}}

  series:
    {{>fragment_utilization_axis period="DAY" dimension1="date" element=(equipment single=true types="VirtualElectricMeter, ElectricMeter" tags="Electricity, Import")}}
    {{>fragment_utilization_axis period="DAY" dimension1="date" element=(equipment single=true types="VirtualElectricMeter, ElectricMeter" tags="Electricity, Export")}}
{{>fragment_chart_footer}}

I believe that developing widgets and components as standalone “things” would help a lot. Anyhow, its possible to bend many things in OH, if you are stubborn enough. :wink:

2 Likes