How to create OH3 widget that works with JSON (adapting old HabPanel widget)

I use a HabPanel widget to adjust a specific String Item that holds a JSON of a “Profile”. For example, one item might look like this:

Profilestore_json_1:
{"label":"Adaptive Light Brightness","type":"percentage","intervals": {"0":12,"1":13,"2":13,"3":13,"4":11,"5":15,"6":33,"7":64,"8":81,"9":91,"10":99,"11":100,"12":99,"13":97,"14":97,"15":94,"16":92,"17":89,"18":85,"19":74,"20":57,"21":40,"22":30,"23":20}}

and this renders to a widget that looks like this:

Also, there’s a nifty little popup settings panel too that appears when you press the button:

With this widget, I can adjust the sliders to change the hourly target value, and in the settings panel, I can change the name and type of the profile (as stored in the JSON) along with resetting the whole thing to default values (hardcoded ones).

In the background, there’s also a second item that receives commands, so when I adjust a slider it sends a command to a designated “input” item and then a rule adjusts the “store” item. In the log, this looks like so:

Item 'Profilestore_json_1_input' received command {"interval":"6","value":"41"}
Item 'Profilestore_json_1' received command {"label":"Adaptive Light Brightness","type":"percentage","intervals": {"0":12,"1":13,"2":13,"3":13,"4":11,"5":15,"6":41,"7":64,"8":81,"9":91,"10":99,"11":100,"12":99,"13":97,"14":97,"15":94,"16":92,"17":89,"18":85,"19":74,"20":57,"21":40,"22":30,"23":20}}

Ultimately, I think it would be possible to do this without the separate “input” item, but handling it with a rule was easier for me.

Here’s the code for the HabPanel Widget:

<style>
/*CHROME*/
  input[type=range]::-webkit-slider-runnable-track {
    width: 100%;
    height: 100%;
    background: transparent;
    border: none;
    border-radius: 3px;
}

input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none;
    border: none;
    height: 16px;
    width: 16px;
    border-radius: 50%;
    background: dodgerblue;
    margin-top: -4px;
}

input[type=range]:focus {
    outline: none;
}

input[type=range]:focus::-webkit-slider-runnable-track {
    background: #ccc;
}
  /*FIREFOX*/
   input[type=range][orient=vertical]
  {
    writing-mode: bt-lr; /* IE */
    -webkit-appearance: slider-vertical; /* WebKit */
    width: 100%;
    height: 100%;
    padding: 0 5px;
  	background-color: transparent;
  }
  
  input[type=range]{
    /* fix for FF unable to apply focus style bug  */
    border: 1px solid transparent; 

    /*required for proper track sizing in FF*/
    width: 300px;
}

input[type=range]::-moz-range-track {
    width: 100%;
    height: 100%;
    background: white;
    border: none;
    border-radius: 3px;
}

input[type=range]::-moz-range-thumb {
    border: none;
    height: 16px;
    width: 16px;
    border-radius: 50%;
    background: dodgerblue;
}

/*hide the outline behind the border*/
input[type=range]:-moz-focusring{
    outline: 1px solid white;
    outline-offset: -1px;
}

input[type=range]:focus::-moz-range-track {
    background: #ccc;
}
  
  
  .grapharea {
  	background-color: #acacac;
  	color: black;
  	padding: 20px;
  }
  table {
    table-layout: fixed;
  	width: 100%;
  }
  td {
  	width: 4%;
  }
  .topbar {
      position: relative;
  		margin: auto;
      width: 90%;
  		height: 10%;
  }
  .grapharea {
  	position: relative;
  	margin: auto;
  	width: 90%
  }
  .rotate {
    transform: rotate(90deg);
    -webkit-transform: rotate(90deg);
    -moz-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    -o-transform: rotate(90deg);
    filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1);
  }
</style>

<!-- POPUP SCRIPT -->
<script type="text/ng-template" id="editProfileName2.html">
    <div class="container" style="padding: 30px; border: 1px solid #456;">
      <a ng-click="$close()" class="pull-right btn btn-danger">X</a>
      <h3 style="color: white">Adjust profile settings</h3>
      <br></br>
      <h4 style="color: white">Change Profile Name</h4>
      	<div class="form-inline">
          <div class="form-group">
            <input type="text" class="form-control" no-snap-drag="true"
                   ng-model="newlabel" ng-value="$eval(getItem(config.p_item).state).label"></input>
            <button type="button" class="btn btn-primary"
                    ng-click="sendCmd(getItem(config.p_item).name.concat('_input'), '{&quot;rename&quot;:&quot;'.concat(newlabel).concat('&quot;}'))">Update</button>
          </div>
      </div>
      <h4 style="color: white">Change Type </h4>
			<p style="color: white">Currently set to: <font style="color: DodgerBlue">{{$eval(getItem(config.p_item).state).type}}</font></p>
      <div class="btn-group" role="group" aria-label="Basic example">
        <button type="button" class="btn btn-secondary" ng-click="sendCmd(getItem(config.p_item).name.concat('_input'), '{&quot;retype&quot;:&quot;percentage&quot;}')">Percentage</button>
        <button type="button" class="btn btn-secondary" ng-click="sendCmd(getItem(config.p_item).name.concat('_input'), '{&quot;retype&quot;:&quot;temperature&quot;}')">Temperature</button>
        <button type="button" class="btn btn-secondary" ng-click="sendCmd(getItem(config.p_item).name.concat('_input'), '{&quot;retype&quot;:&quot;boolean&quot;}')">Boolean</button>
      </div>
      <br></br>
      <div>
      	<button type="button" class="btn btn-alert"
           ng-click="sendCmd(getItem(config.p_item).name.concat('_input'), '{&quot;reset&quot;:&quot;&quot;}' )">Reset Profile !</button>
      </div>
    </div>
</script>

<!-- END POPUP SCRIPT -->

<div class="topbar">
	<h4 style="float: left">
  	Profile: {{$eval(getItem(config.p_item).state).label}}
  </h4>
  <button style="float: right" class="btn btn-default" ng-click="openModal('editProfileName2.html', true, 'lg')">
    Settings
  </button>
</div>
<br style="clear: both"></br>
<div class="grapharea"> 
	<table>
    <tr>
      <td>hr:</td>
    	<td ng-repeat="c in ['00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23']">
      	<p>{{c}}</p>
      </td>
    </tr>
    
    <!--TEMPERATURE-->
  	<tr ng-if="$eval(getItem(config.p_item).state).type == 'temperature'">
      <td class="rotate">
      	Temp.
      </td>
    	<td class="" ng-repeat="c in [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]">
        <div ng-init="intervalvalue = $eval(getItem(config.p_item).state).intervals[$index]">
          <input class="slider" orient="vertical"  type="range" ng-model="intervalvalue" name="string" min="8" max="26" step="0.5" ng-mouseup="sendCmd(getItem(config.p_item).name.concat('_input'), '{&quot;interval&quot;:&quot;'.concat($index).concat('&quot;,&quot;value&quot;:&quot;'.concat(intervalvalue).concat('&quot;}')))"></input>
          <p class="rotate"> {{intervalvalue}}</p>        
        </div>
      </td>
    </tr>
    
    <!--PERCENTAGE-->
  	<tr ng-if="$eval(getItem(config.p_item).state).type == 'percentage'">
      <td class="rotate">
      	%
      </td>
    	<td class="" ng-repeat="c in [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]">
        <div ng-init="intervalvalue = $eval(getItem(config.p_item).state).intervals[$index]">
          <input class="slider" orient="vertical"  type="range" ng-model="intervalvalue" name="string" min="0" max="100" step="1" ng-mouseup="sendCmd(getItem(config.p_item).name.concat('_input'), '{&quot;interval&quot;:&quot;'.concat($index).concat('&quot;,&quot;value&quot;:&quot;'.concat(intervalvalue).concat('&quot;}')))"></input>
        	<p class="rotate"> {{intervalvalue}}</p>
        </div>
      </td>
    </tr>
    
    <!--BOOLEAN-->
  	<tr ng-if="$eval(getItem(config.p_item).state).type == 'boolean'">
      <td class="rotate">
      	I/O
      </td>
    	<td class="" ng-repeat="c in [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]">
        <div ng-init="intervalvalue = $eval(getItem(config.p_item).state).intervals[$index]">
          <input class="slider" orient="vertical"  type="range" ng-model="intervalvalue" name="string" min="0" max="1" step="1" ng-mouseup="sendCmd(getItem(config.p_item).name.concat('_input'), '{&quot;interval&quot;:&quot;'.concat($index).concat('&quot;,&quot;value&quot;:&quot;'.concat(intervalvalue).concat('&quot;}')))"></input>
        	<p class="rotate"> {{intervalvalue}}</p>   
        </div>
      </td>
    </tr>
    
    <tr>
      <td>
      	<p></p>
      </td>
      
      <td ng-if="$eval(getItem(config.p_item).state).type == 'temperature'" ng-repeat="c in [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]">
      	<p class="rotate" > °C</p>
      </td>
      
      <td ng-if="$eval(getItem(config.p_item).state).type == 'percentage'" ng-repeat="c in [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]">
      	<p class="rotate" > %</p>
      </td>
      
    </tr>
  </table>
</div>

Now, my question:

How can I recreate this widget as an OH3 F7-Card Widget?

Admittedly, I’m not that good at programming, and even worse at web development, but the whole YAML thing doesn’t make nearly as much sense to me as writing the HTML for HabPanel widgets. I don’t know where to start, and since it’s much more niche, I haven’t found any resources outside of the OH community that could help teach me how to use it and the F7 stuff.

Because of the json based system you have constructed, this is not at all a simple widget design, and almost certainly not the place to start to learn to build custom widgets, but I suspect it is doable. There is access to a JSON object in the widget definitions so you can easily parse the input json string; replicating your settings popup is fairly trivial; oh-repeater widgets make rendering serial widgets such as your sliders very easy. These are all pieces that you can find info on here in the forums and learn to use.

Most of the widget system, however, is built around items and working with non-item-based values tends to complicate matters. One of the most difficult aspects would be setting initial values to the various slider widgets from your json. I don’t know your reasons for choosing this setup to begin with but, honestly, my guess would be that it would be quicker to reconfigure your “Profile” system to work with items and then build a simple widget than it will be to work through the development of a widget that uses this system.

1 Like

Hmm, thanks for the input, I’ll check out the JSON parsing.

The main reason I chose not to have each slider be an individual item is that I use this system many times. If I did, and had 24 sliders per profile, I would potentially end up with over a thousand items just in sliders because the profiles control everything from lighting, to heating, to coffee machines. Maybe you’re right though… the copy-pasting might be quicker than the full widget.

I’m in a similar situation so interested on whether you made any progress on this?

I’m migrating from OH2, I have a a large number of items for each light in the house with setpoints and modes for a complex automated presence/lighting rule. These items include the “mode” of the light (colour, white setpoint or “following the sun” where the colour temperature moves to match the sun through the day), brightness/color/temp data for the sepoints for each mode etc, and there are sets of these for each Time of Day also (ie, lights in the bedroom may be white during the day, but a colour at night). This creates an explosion of items that do nothing except store a setting for a light that are rarely touched (but it is important they can be tweaked easily by others).

I’ve been experimenting with storing all this data in item metadata on each light item. Seems easy. In a javascript rule I can read and write that metadata and so should be able to port my rule. What I’m not sure about is how to create a UI to allow all these setpoints to be tweaked by me or family. Currently I’m using a sitemap heirarchy for this, but was hoping to move to a fancier UI in OH3.

It seemed item metadata was perfect for this, but perhaps I am mistaken? Should I just stick with my thousands of items? One of the problems with the items was performance issues, especially for things like habpanel, which doesn’t like so many items, and it just seems less elegant that using metadata.

I’m in the learning stages with OH3 and haven’t started porting over yet - just learning, so I could go either way, but interested in others opinions or the experts recommendation on which way to go as its going to be a lot of work to port over my DSL lighting rules and so I only want to tackle it once (I know I can just replicate my OH2 setup in OH3, but want to use the opportunity to modernise everything and move to JS).

It is possible to build widgets that take advantage of data stored in an item’s metadata, but only in a limited way. An oh-repeater item can query the specific metadata namespace that you request while retrieving items by group or tag. But there is no built in mechanism to write metadata from a widget, that requires an intermediary item to hold the value or values that you want and a rule to parse that item and write the metadata.

In my opinion the solution is to just make all the items. OH is designed around item tracking and manipulation and all the features and bells and whistles take advantage of that. Yes, there is certainly an up-front time investment if you have a system that’s going to take hundreds of items, but in the long run you will save time if you stick with the native system. There’s many hours more R&D if you’re trying to find a way to bend OH outside of it’s intended use. There’s more upkeep as the likelihood is high that upgrades interfere with your complex edge case. If you find any of the posts around here about some of the more complex systems you’ll see a common pattern: in a system with only 150-200 actual IoT devices or data endpoints will have 700-800 items because creating extra items only costs the few seconds it takes for each item and otherwise makes everything easier (the performance issues you mention are unusual, though I don’t have much experience with HabPanel).

Thanks JustinG both for your advice and taking the time, I think I will re-assess my desire to use metadata for this and stick with items. The upside is less work as it’s a more straightforward port of what I have in OH2.x.

FYI, the Habpanel issue I was referring to is here [SOLVED] HABPanel sluggish and is basically that, especially on lower powered devices, habpanel takes a while to load the items as it loads ALL of them, not just those on the page, it can also get overwhelmed by the number of events. A forum member posted an addon to strip items and events that are not used in the panel to improve performance, which I’m using, but its still not great on my old wall panels (ipad air 1’s). OH3 may have improved on this, although I see people are still finding a need for it based on that thread. I’ll see how I go. I have a few thousand items from memory (about half due to the lighting) and quite busy events with lots of chatty devices.

Early days yet in exploring how OH3 will work out for me - I’m finding the learning curve surprising steep considering I’ve been using Openhab since V1, perhaps as I’ve been file based the whole time. I want to try the “new” way with the UI this time though, but for some reason I’m finding the UI quite unintuitive despite the doco. Maybe my brain is just not wired for it. I suspect once I get the hang of it I’ll love it.

Re: interested on whether you made any progress on this?

Short answer, not notably.

Long answer, I’ve decided not to upgrade to OH3 for a while yet, and when I do, I will probably just continue to use HabPanel instead of the MainUI (as sad as that is, because mainUI looks really cool). Right now, the learning curve for OH3 is just too steep for me–like you mentioned–and it’s still missing too many quality-of-life features and generally doesn’t feel as “mature”. Also, the documentation just isn’t quite there yet and I’m just not a fan of setting up my home via GUI, something OH3 heavily favors. I have hundreds of devices each with anywhere from 5 to 50 items associated with them (many of which are string items that hold JSON!) and configuring this without the ability to simply copy/paste lines in .things and .items files would see me die of old age before I got everything set up properly.

I mean, if I were to separate out my “profiles” into 24 sliders, then each light in my house (of which there are many) would start with at least 48 sliders (one set for brightness, one for temperature) and probably more because there are other toggles to control behavior. This means that if I don’t use JSON or some sort of data compression, my item counts would balloon into the tens of thousands easily, considering that each room would have north of 250 items in lighting alone.

1 Like