Tutorial on Transformations in OH5

Transformations are used for four purposes:

  1. convert data at channel level
  2. change item states with a profile
  3. change the way items are displayed
  4. convert data in rules

Here is what I could find in the Openhab documentation:
https://www.openhab.org/docs/configuration/transformations.html
https://www.openhab.org/docs/configuration/items.html#state-transformation
https://www.openhab.org/addons/automation/jsscripting/#js-transformation
https://www.openhab.org/docs/configuration/rules-dsl.html#transformations
https://www.openhab.org/docs/tutorial/things_advanced.html
https://www.openhab.org/docs/configuration/items.html#profiles
https://www.openhab.org/docs/concepts/units-of-measurement.html

Have a look here too. It is a good explanation from @rlkoshak.

This text aims to give some more information and examples. It is written from the perspective of incoming data and using the Main UI. Tested with OH 5.0.2

Basic concepts

Bindings are components that can communicate with physical devices or internet protocols. They can be compared to Windows device drivers. They have to be installed from the Add-on Store. If you want to get information from a Tapo P115 smart plug, then you have to install the Tapocontrol binding (in this case a bridge will also be installed).

Things can be seen as instances of a binding. If you want to control two Tapo smartplugs, then you have to define two things with for each one some configuration such as the ip address.

Channels represent the measurements or other data that is made available in the thing. The Tapo P115 can give you the actual measured power or the status of the output switch. Channel transformations (also known as binding transformations) can be used to convert the data that will be used by all the items linked to that channel.

Links. Here we decide what information we want to use. An Astro Sun Data thing can provide us with about 75 channels. We don’t want to use all this information. A link defines a connection between an available channel and an item. Inside the link we can use profile transformations to convert the information that we send to the item. The Astro sun data has a channel for the sun elevation with 16 decimals. At the link level I could round the value to 1 decimal. on the other hand I don’t want to do this with the electricity power as I will use this later to make some calculations and therefore need all the decimals.

Items are at the centre of Openhab. The collection of items is a real time database that collects data from different sources (links, rules, temporary variables,…). It is used to display the data in layout pages and charts. The item data is logged in the event log and can be persisted. Here we can use state transformations to change the way the data is displayed in layouts. I want to use all the decimals of my electricity consumption in calculations, but only display 1 decimal position in my layout page.

In OH4 the Units of Measurement (UoM) concept was added. It provides us with some automatic transformations. If you have a temperature sensor that outputs degrees Fahrenheit, the UoM concept can automatically convert this in the unit that is valid in your system locale (°C in a lot of countries).

To summarize:

Widget Expression tester

A good tool to see what is happening is the Widgets Expression Tester on the Developer Sidebar, which can be opened with Shift+Alt+D.

Here you see an example of a temperature sensor with no profile transformation and a state description pattern %.1f %unit%:

  • state is what comes out of the link. This is recorded in the persistence and the event log and can be used in calculations.
  • displayState is after the state description pattern %.1f %unit%. This can be used in layout pages. It is not recorded in the persistence.
  • numericState is what we want to use in rules.
  • type can be Quantity (with unit) or Decimal (without unit) for numeric values.

Using scripts

For some profiles, such as the javascript profile (SCRIPT ECMAScript…), you have to write a script.

The original way of doing it, is creating a .js file and saving it in the /etc/openhab/transform directory.

It could also be done from the Main UI with Settings / Transformations / click on the blue + icon at the bottom left side. Give the transformation a name and a label, then click on JS and Create. A script window is displayed with an example function.

Channel Transformations

Some bindings, such as MQTT, HTTP, Modbus, … allow for transformations at the channel level (before the links). These transformations are also called Binding transformations.

To go there go to Settings / Things / (your MQTT thing) / Channels tab / (your channel) / Configure channel. Then click on Show advanced and scroll down.

You can enter “Incoming Value Transformations”. For other bindings such as HTTP, it may be called “State transformation”.

Here you can find some extra information.

You could use these transformations if your MQTT values are received in JSON format. The JSONPATH transformation could then extract the value.

You can also chain transformations. You can add the transformations on separate lines.

Example : script transformation with parameters

I have an Epson ET8500 printer with 6 colors and a waste tank. It can be reached via html with http://192.168.1.20/PRESENTATION/HTML/TOP/PRTINFO.HTML

The color information is in the html text:

Ink_K.PNG' height='48'     for black
Ink_K.PNG' height='49'     for photo black (second instance of Ink_K!)
Ink_C.PNG' height='47'     for cyan
Ink_Y.PNG' height='47'     for yellow
Ink_M.PNG' height='45'     for magenta
Ink_Waste.PNG' height='45' for grey
Ink_Waste.PNG' height='38' for waste tank (second instance of Ink_Waste!)

As you see, they have made some errors in the used names.

The number after height is the fill status from 0 to 50. We will double this to 0-100.

The way to get the colors is using the HTTP binding. There, 7 channels have to be created. For each channel a js transform file has to be created such as epsonBlack.js:

(function(html)
{
    return html.split("Ink_K.PNG' height='")[1].split("'")[0]* 2;  
})(input)

And for the second instance of the same color name (epsonPhotoBlack.js):


(function(html)
{
    return html.split("Ink_K.PNG' height='")[2].split("'")[0] * 2;  
})(input)

These files should come in /etc/openhab/transform.

In the Channel configuration, the State Transformation should point to the correct file:

JS:epsonBlack.js
JS:epsonPhotoBlack.js
...

With parameters, you only have to use one transformation script (epsonColor.js):

(function(html) {
  var logger = Java.type("org.slf4j.LoggerFactory").getLogger("epsonColor");
  logger.warn("color="+color+" "+"instance="+instance);
  return html.split("Ink_"+color+".PNG' height='")[instance].split("'")[0] * 2;
})(input)

Lines 2 and 3 are for debugging as console.info() does not work here.

In the Channel configuration / State Transformation you can add the parameters as URL parameters (first a ‘?’, then a key=value pair, parameters are separated by ‘&’):

JS:epsonColor.js?color=K&instance=1 (for black)
JS:epsonColor.js?color=K&instance=2 (for photo black)
JS:epsonColor.js?color=C&instance=1 (for cyan)
...

The first parameter is given as color=K or color=Y, …

The second parameter is instance=1 or instance=2.

You can also decide not to use files, but enter the script in Main UI: Settings / Transformations / click on the blue + icon at the bottom left side. You can call the script this way:

JS:config:js:epsonColor?color=K&instance=1

Profile transformations

Profiles are used at the link level to alter the data before it reaches the item. Profiles are a bit hidden in Main UI.

You can access them form two sides:

  1. Settings / Things / (choose a thing) / Channels / (click on the channel / (click on the item link)
  2. Settings / Items / (choose an item) / (click on the channel link)

Here you have the impression that the temperature is already rounded with one decimal, but this is wrong. The event log shows the real raw value with 4 decimals in this case:

Item ‘freezerTemperature’ changed from -19.5556 °C to -19.6667 °C

The widget expression tester shows the same “state”. I find this a bit confusing. What is displayed here is the “displayState”.

Example 1. If you are not interested in persisting 4 decimals, you can use the Round profile (Basic profiles add-on) to alter it to 1 decimal. The item will be persisted with one decimal and the event log will show the value with one decimal.

For some profiles, such as the javascript profile (SCRIPT ECMAScript…), you have to write a script.

Example 2:

My system locale shows a the freezer temperature in °C, but I also want to persist it in °F.

Create a script in the Main UI: Settings / Transformations / click on the blue + icon at the left bottom.

//Javascript transform function to convert degrees celsius to fahrenheit

(function(data) {
  // The value sent by OH is a string so we parse into an float
  var celsius = parseFloat(data);
  var fahrenheit = (celsius * 1.8) + 32;
  // we want to record the value with one decimal
  return fahrenheit.toFixed(1);
})(input)

Open the thing that contains the temperature channel.
In the channel make a link to a new item freezerTemperatureF.
The Type is Number
The Dimension is Temperature (°C)
The Unit must be set to °F.
The State Description Pattern is not important here.

In the Profile section choose the SCRIPT ECMAScript profile. When scrolling down you see the Profile Configuration.
Click on “Things to item Transformation”. You will see a list of available scripts.
Choose the right one: C2F (config:js:C2F)
At the bottom, click on the blue Link button.

As I now have two links to the freezer temperature, I see this in the event log:

Item 'freezerTemperature' changed from -18.7778 °C to -18.5556 °C
Item 'freezerTemperatureF' changed from -1.8 °F to -1.4 °F

This is perhaps not a good example as you can also achieve the same result with UoM (see later)

State Transformations

Here we only change the displayState. You will see the change in layout pages, but not in the event log (which records the states, not displayStates).

Example 1. If we have an item with a number of decimals and want to display it with less decimals, then we can do this in the item definition by clicking on State Description:

Here we see a pattern %.1f %unit% which means that the item will be displayed as floating point with one decimal, a space and the unit.

The syntax of possible patterns can be found here.

Example 2. Another example is translation of open/closed contact values to your own language.

Here is an example for me in Dutch:

You can also use scripts.

Example 3. If you want to display a string “The freezer temperature is -20,1 °C” instead of just “-20,1 °C”, you can use this script via Settings / Transformations / blue + icon, give script a name, click JS and Create:

// freezerTemperature

(function(data) {
  var output = "The freezer temperature is " + data;
  return output;
})(input)

Then in the item definition you go to State Description. When you click on the empty Pattern field, a list of available transformations appears. Choose the right one (freezerTemperature) and save.

This is the result:

If you want to reduce the number of decimals, you have to adapt the script:

(function(data) {
  var temperature = parseFloat(data);
  var output = "The freezer temperature is " + temperature.toFixed(1) + " °C";
  return output;
})(input)

With simple scripts that can be put into one line, you can use the inline format in the Pattern field:

JS(|"The freezer temperature is: " + parseFloat(input).toFixed(1) + " °C" ):%s

Transformations in rules

You can also use transformations inside rules, where you assign the result of the transformation to a variable.

More information here for DSL rules.

Example:

From my solar panel inverter a JSON string is received in the form:

{ "ppv": 344, "active_power": 6, ... }

in an item called ZP_JSON_Out.
To split this string in its different components, a JSONPATH transform is used in a rule:

rule "ZP JSON transform" 
when 
    Item ZP_JSON_Out changed 
then
    val Pv = transform("JSONPATH","$.ppv",ZP_JSON_Out.state.toString)
    val ActivePower = transform("JSONPATH","$.active_power",ZP_JSON_Out.state.toString)
...

Units of Measurement

In OH 4 a new concept was introduced to link numeric data to units.

When defining a new item you have to define the type (string, number, contact, …).

If you define the item as Number, you are encouraged to add a Dimension. This can be Temperature, Volume, Power, Energy, …

After choosing a Dimension, a unit is proposed, but you can choose another one if you want. For Energy, this could be kWh, kcal, J, …

A State Description Pattern is also proposed: %.0f %unit%

The values coming from the binding also contain a unit. These become Quantitytype states. If the unit from the binding is different from the unit that was defined by you in the item definition, then a conversion will take place.

So if the binding sends an energy state of 0.5 kWh and the item wants Wh, the item will get 500 Wh. This will be persisted and will appear in the event log.

A conversion between metric and imperial units will also be automatic, depending on your system locale. °C and °F will automatically be converted depending on where you live.

There is a catch. The persistence does not store the unit, it only stores the value. In our energy example, the number 500 is stored. If we later change the unit to kWh in the item definition, then the previously persisted data will not be accurate anymore. It will now show 500 kWh.

Special cases

Numbers without dimension

If the binding has a channel without dimension (e.g. a count), then in the item definition you should not add a dimension. It is a Decimal Type number (numbers with dimension are Quantity Type numbers).

Dimensionless numbers

If the binding gives you a ratio (percent, decibel, ppm or simply a ratio), then in the item definition, you should add Dimensionless as dimension. Then choose the unit (%, dB, ppm, ONE).

10 Likes

Thanks for posting!

While this is a good example to demonstrate how to write a script transformation, it’s not really needed. It would be sufficient to simply link the same Channel to the °F Item and setting that Item’s unit to °F. The value will be converted automatically without any separate transformation script.

You actually explain this later on in the tutorial, but I wanted to point out that a transformation isn’t need to change the units of an Item.

UoM has actually been around since OH 2.5. But there was a lot of work done in OH 4 to make the way they work a little more apparent and controllable by end users.

2 Likes

With UoM, it‘s indeed harder to find good examples for transformations.

Added some special cases at the end of the text after reading this text from @rlkoshak.

Edit: rearanged the chapters, added an example for transformations at Channel level.

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.