What is the best design practice to transform raw values to physical values?

  • Platform information:
    • Hardware: OpenHAB hosted on Synology, sensors connected to several custom made PCBs based on Arduino and ADC IC.
      Sensors are:
  • pressure sensors (4-20 mA)
  • temperature sensors (4-20 mA)
  • flow sensor (4-20 mA)
    • openHAB version: 2.4
  • Goal:
    What have I done so far: I am able to receive sensor values on openHAB with a mqtt binding/things, etc. Currently, the mqtt topics sends physical (real) values as strings. Only values over a given threshold change are sent and rounding is performed before sending (only one decimal digit of precision). It allows to have persistence on an InfluxDB based on value change (or updates) and a 5 minute cron. It works very well :slight_smile: But openHAB does not do any computation.

But, as there is no hardware FP in ATMega2560 and I would like to compute integrations over time (for the flow sensor to have water consumption), it would be best (to my opinion) to have a better segregation of roles:

  • ATMega2560 MCU sends current values with a 16-bits integer, Flow sensor can be acquired for example at 10 Hz, summed and send at 1 Hz (or less).
  • OpenHAB performs scaling to convert electrical value as int to float, roundings (too much precision) and persist if the change is over a given threshold.

My question is: what is the best ‘design practice’ to do this? I have searched a long time on the forum, but between rules, transformation service (only for strings?) or something else, I just could not figure the ‘best way’.

Based on the forum, I would tend to think to be similar to those threads:

If this is the right way to do it, it would mean:

  • for every sensor to define an item that stores the integer value
  • generate a floating value that scales/transform the raw value to a physical one
  • use a trigger based on a change event.
    But, does it allows to compare current values with previous ones? If possible, I would like to apply the rules only if the sensor has changed over a given threshold (I would like to avoid generating useless samples because of noise / precisions)

yeah… creating items for each sensor value seems a great idea! You can probably use a transform service or map to get your values formatted correctly. Then yes, use a trigger based on these items changing. You mentioned using influxDB so yes, you can pull previous states to compare to
good on you!

Is the value just the number? No white spaces or units or anything else like that? If so then you can use a Number Item and OH will convert the String to a numerical value (BigDecimal is the specific representation) and you can do calculations with it. If if does have extra stuff around it, then that extra stuff could be removed using the REGEX transformation. With the MQTT 2.5 M1 version of the binding or later, you can even chain transformations (e.g. use a JSONPATH transform and then REGEX on the result of that).

That will get you the value as something you can calculate. In general, anyone who as taken a numerical analysis class will tell you to never round the results of your calculations. This will increase how quickly the rounding error grows and it doesn’t take too long for the error to become significant. Instead, round the value only when showing the value to a human. In OH, that means using [%.2f] in the label on your sitemap and String::format for logging. That “too much precision” is because not every floating point value can be represented in typical floating point representations. For example, it’s impossible for a float or double to store the value 9.2. It can get super close but it will never exactly be 9.2.

For the InfluxDB part there are two approaches. If the result is under the threshold update an Item and configure that Item with an everyUpdate strategy in the influxdb.persist file.

Thank you very much to the both of you for taking the time to answer :slight_smile:

Sure, it is custom made so I can send the int value as a string.
Just tested it and it works - from the sensor:

    uint16_t rawV = adc1.readRawVoltage(WP1_channel);
    char buffer[5];
    mqttClient.publish("smarthub/analog/1", itoa(rawV, buffer, 10) , true);

And value is well displayed on openHAB - I am a bit surprised by the ‘String’ representation, I misunderstood your reply and it ended up as:

I have searched within documentation (not on the forum yet), it looks like float or int values can not be used directly with openHAB - is this to avoid any endianism issue?

Sure, I will do it this way - i ‘mixed’ things because I wanted to avoid writing useless values in InfluxDB - the analog acquisition is surprisingly very precise, I only need 1 decimal digit for relative pressure.

I have not done the rule yet, but I get the idea :slight_smile:

I can’t say what the reasoning is. But given that OH is a great big bridge between hundreds of technologies and APIs, normalizing on Strings in the MQTT binding, given that OH (Java really) has great built in parsing capabilities it is not surprising.

More likely is that it simply cannot represent the actual value so you end up with a floating point number that is really really close to the “correct” value. Getting really really close means lots and lots of decimal places. Like I said, it is impossible to represent the value 9.2 in the standard way that floating point numbers are represented in memory. Instead it ends up being 9.1999999999999993.

A last reply to share the result of the results used.

My need is to acquire analog values with 4-20 mA sensors, convert them to physical values and persist them if they have change more than a treshold:

  • no conversion are performed in the acquisition PCB: all electric values are sent straight in mqtt (as encoded by the ADS1115 IC in 16 bits integer). I want to avoid float usage on a PCB based on a ATMega2560 (no floating point unit) for performance (it is intended to perform several other tasks like DMX, etc.)
  • mapping to sensors and conversion to physical values are performed in openHAB
  • only values that has changed enough are persisted

.items

Group gRawAnalogSensors
Number Analog_WT1 "Water Temperature #1 Raw Value"  <line> (gRawAnalogSensors) {channel="mqtt:topic:9e31783b:Analog4"}
Number Analog_WP1 "Water Pressure #1 Raw Value"     <line> (gRawAnalogSensors) {channel="mqtt:topic:9e31783b:Analog1"}
Number Analog_WP2 "Water Pressure #2 Raw Value"     <line> (gRawAnalogSensors) {channel="mqtt:topic:9e31783b:Analog2"}
Number Analog_WP3 "Water Pressure #3 Raw Value"     <line> (gRawAnalogSensors) {channel="mqtt:topic:9e31783b:Analog3"}
Number Analog5    "4-20 mA #5 Raw Value"            <line>                     {channel="mqtt:topic:9e31783b:Analog5"}
Number Analog6    "4-20 mA #6 Raw Value"            <line>                     {channel="mqtt:topic:9e31783b:Analog6"}

Group gAnalogSensors
Number Irrigation_WT1 "Water Temperature [%.2f °C]" <temperature> (gAnalogSensors)
Number Irrigation_WP1 "Water Pressure #1 [%.2f b]"  <pressure>    (gAnalogSensors)
Number Irrigation_WP2 "Water Pressure #2 [%.2f b]"  <pressure>    (gAnalogSensors)
Number Irrigation_WP3 "Water Pressure #3 [%.2f b]"  <pressure>    (gAnalogSensors)

Group gAnalogPersistedTresholds
Number Irrigation_WT1_treshold "Water Temperature Treshold [%.2f °C]" <temperature> (gAnalogPersistedTresholds)
Number Irrigation_WP1_treshold "Water Pressure #1 Treshold [%.2f b]"  <pressure>    (gAnalogPersistedTresholds)
Number Irrigation_WP2_treshold "Water Pressure #2 Treshold [%.2f b]"  <pressure>    (gAnalogPersistedTresholds)
Number Irrigation_WP3_treshold "Water Pressure #3 Treshold [%.2f b]"  <pressure>    (gAnalogPersistedTresholds)

Group gAnalogPersistedItems
Number Irrigation_WT1_saved "Water Temperature [%.2f °C]" <temperature> (gAnalogPersistedItems)
Number Irrigation_WP1_saved "Water Pressure #1 [%.2f b]"  <pressure>    (gAnalogPersistedItems)
Number Irrigation_WP2_saved "Water Pressure #2 [%.2f b]"  <pressure>    (gAnalogPersistedItems)
Number Irrigation_WP3_saved "Water Pressure #3 [%.2f b]"  <pressure>    (gAnalogPersistedItems)

.rules

/**
 * Converts a raw value acquired by the ADS1115 IC as a 16 bits integer to
   an analog current value in a 4-20 mA current loop {@Link https://en.wikipedia.org/wiki/Current_loop}.

   @param  rawValue the Item providing the 16 bits integer to convert
   @return the current value in mA (that should be in the 4-20 mA range)
 */
val toCurrentValue = [ NumberItem rawValue |
    // for 4-20 mA acquisition, the ADS1115 IC is configured with a gain of 0x400
    // it provides a range of 2.048 V {@Link https://github.com/circuitar/Nanoshield_ADC}
    (rawValue.state as Number) * 20.48 / 32767
]

/**
 * Converts a standart 4-20 mA current value into a physical value
   depending of the sensor range.

   @param  current the 4-20 mA current value in mA to convert
   @param  min     the beginning of the sensor range (same unit as the sensor physical unit)
   @param  max     the ending of the sensor range (same unit as the sensor physical unit)
   @return the physical value with the sensor unit
 */
val toPhysicalValue = [ Number current, Number min, Number max |
    // For example, the formula is for a -20°C to 100 °C temperature sensor:
    // physical_value (°C) = -20°C + (current(mA) - 4mA) * (100°C - (-20°C)) / 16mA
    (min + (current - 4) * (max - min) / 16)
]

rule "System Is Starting"
when
    System started
then
    // Analog sensors thresholds depends of sensor used
    Irrigation_WT1_treshold.postUpdate(0.5)
    Irrigation_WP1_treshold.postUpdate(0.1)
    Irrigation_WP2_treshold.postUpdate(0.1)
    Irrigation_WP3_treshold.postUpdate(0.1)
end

rule "An Irrigation Analog Sensor Raw Value Changed"
when
    Member of gRawAnalogSensors changed
then
    val analog = triggeringItem
    // get the current value for the analog sensor
    val currentValue = toCurrentValue.apply(analog)
    // get the item used for the physical value that match analog sensor
    // convention name is that it should end with the same suffix: Analog_WPX -> Irrigation_???
    val analogSensorItem = 
        gAnalogSensors.members.findFirst[ s | s.name == "Irrigation"+analog.name.substring( analog.name.length() - 4)]
    // and finally convert it in the 0 to 10 bar range
    // TODO: use items for the min and max values when other sensors will be used
    analogSensorItem.postUpdate(toPhysicalValue.apply(currentValue, 0, 10))
end

rule "An Analog Sensor Value Changed"
when
    Member of gAnalogSensors changed
then
    val sensor = triggeringItem
    // Get the item used for the persistence
    val persistedItem = gAnalogPersistedItems.members.findFirst[ p | p.name == sensor.name+"_saved"]
    // Get the treshold used for the persistence
    val tresholdItem = gAnalogPersistedTresholds.members.findFirst[ t | t.name == sensor.name+"_treshold"]
    // in case of a NULL state, we refresh the saved Item with the current value
    if (persistedItem.state == NULL) {
        persistedItem.postUpdate(sensor.state)
    } else {
        // if we are not within the allowed range, we persist the new value
        val newValue = sensor.state as Number
        val oldValue = persistedItem.state as Number
        val treshold = tresholdItem.state as Number
        if ((newValue < (oldValue - treshold)) || (newValue > (oldValue + treshold))) {
                persistedItem.postUpdate(sensor.state)
        }
    }
end

.persist

Strategies {
    everyMinute : "0 * * * * ?"
    every5Minutes : "0 0/5 * * * ?"
    everyHour : "0 0 * * * ?"
    everyDay : "0 0 0 * * ?"
}

Items {
    gAnalogPersistedItems* : strategy = everyUpdate, every5Minutes
}

I will elaborate a bit on that - no, it is not such obvious :wink:
The ADS1115 IC I used has several modes that can be configured and that change ADC results: sampling rate or single-shot/continuous mode. Continuous mode is based on delta-sigma that provides excellent precision.
All tests are performed in my desktop with a basic relative pressure sensor - since I can not use any watering pipe in this room ;), I only measure ambiant pressure with a relative sensor. Hence a 0 bar value represented by a 4 mA result.
In continuous mode, with 8 samples per second, all measeared values are within 0.000764 mA range! (comparison was done in the 16 bits integer result, so no roundings).
In single-shot mode, at 8 samples per second, with a gain of 2, I have ‘only’ 0.002144 mA variation. That is why I said the analog acquisition is surprisingly very precise. I do not need such precision :slight_smile: (precision should degrade a bit in a 4-20 mA range but without any calibrator at home, I can not measure it)