Transformation from liquid depth to volume in an inverted cone shaped tank

I recently installed an EPTTECH Smart TLC 2326-WF-S sensor to track the water level in my rainwater tank. It uses the Smarthome/J binding for Tuya appliances and works fine so far. One of the channels shows a percentage value, the other (after careful calibration) the liquid depth in cm.

The tank has an inverted cone shape (see picture above) and I wanted to convert the depth reading into volume, taking into account the specific shape of the tank. The transformation I wrote takes the measurement in dm (decimeter) so there is no need to do any additional conversion to get liters (apologies to the US, Liberia and Myanmar, you will have to do your own conversions; you can use feet and will get the correct amount of cubic feet out of it, but you will still have to convert to gallons; do not forget to replace the division by 10 by the conversion rate for cm to feet).

The values in the transformation call have to be replaced with the actual dimensions of your tank. Please refer to the diagram below to get the right measurements. depth is the sensors output in cm (it gets divided by 10 to get dm).

Tank-diagram

The transformation is used for the liquid_depth channel of the sensor as “Thing to Item transformation” as a SCRIPT ECMAScript (ECMAScript 262 Edition 11) profile.

The math used is taken from here, there are formulas for different shapes as well.

Screenshots

Changelog

Version 1.1

  • changed the constants to parameters (as per this suggestion)

Version 1.0

  • initial release

Resources

(function(depth, top_d, bot_d, height) {
  const z = height * (bot_d / (top_d - bot_d));
  const r = (top_d * (depth / 10 + z)) / (2 * (height + z));
  return Math.round((Math.PI * depth / 10) * (Math.pow(r, 2) + r * bot_d / 2 + Math.pow(bot_d / 2, 2)) / 3, 1);
})(input, top_d, bot_d, height)
1 Like

Be sure to come back and test that this installs once support for the transformation marketplace is added to MainUI.

Also, you could make it generic by passing in the dimensions instead of hard coding them in the transformation. That would make it much more flexible for those who install it from the marketplace later.

Calling it with arguments would look like: config:js:depth_to_litres?bot_d=5.12&top_d=7.22&height=8.74

and the transformation itself would become:

(function(depth, bot_d, top_d, height) {
  const z = height * (bot_d / (top_d - bot_d));
  const r = (top_d * (depth / 10 + z)) / ( 2 * (height + z));
  return Math.round((Math.PI * depth / 10) * (Math.pow(r, 2) + r * bot_d / 2 + Math.pow(bot_d / 2, 2)) / 3, 1);
})(input, bot_d, top_d, height)
2 Likes

Thank you for the information. It was quite hard for me to piece together how transformations work anyway. The documentation is pretty sparse and cryptic. There are not that much examples around either. It does not mention parameters can be passed along, when they are called.

I will test and update the version to 1.1 as soon as I have the time.

It does but it’s easy to miss.

  1. Passing parameters is also possible by using a URL like syntax: JS(<scriptname>.js?arg=value). Parameters are injected into the script and can be referenced like variables.

Additional parameters can be injected into the script by adding them to the script identifier in URL style (scale.js?correctionFactor=1.1&divider=10 would also inject correctionFactor and divider ). These additional parameters are also injected into the script context as string variables.

Hello Rich!

Thank you for the pointers. I have to admit I read this but did not actually understand “injection” in this context (passed to the script may have been easier to understand). Also without your example I would have had no idea how to access those parameters in the transformation and the syntax used to invoke them from the Profile settings (the URL style is clear enough, but the naming of the script not so much).

Thanks for your help!

PS: It may have something to do with me not disliking js but loathing it and therefore never actually could be bothered to take a dive into the syntax. :slight_smile:

You can write a script transform in any language supported by OH except Blockly.

I know people have seeing opinions about things but unless you are going really out there (e.g Lisp, Haskell, Prolog) the overall syntax between comply used languages is pretty minimal.

It’s probably worth mentioning that the whole “function” bit isn’t strictly required. It’s not really doing much here and personally I didn’t use it. It’s kind of best practice to do so but that has more to do with how JS works in browsers.

The following works work just as well.

var z = height * (bot_d / (top_d - bot_d));
var r = (top_d * (input / 10 + z)) / ( 2 * (height + z));
Math.round((Math.PI * input / 10) * (Math.pow(r, 2) + r * bot_d / 2 + Math.pow(bot_d / 2, 2)) / 3, 1);

What ever the last line evaluates to is what gets returned. input and all the passes in variables just exist.

I’m Rules DSL the treatment would look like the following (note Rules DSL can’t convert from steering to number automatically like JS):

val ht = new BigDecimal(height).floatValue
val bd = new BigDecimal(bot_d).floatValue
val td = new BigDecimal(top_d).floatValue
val depth = new BigDecimal(input).floatValue
val z = ht * (bd/(td-bd))
val r = (td*(depth /10 + z)) / (2 * (h + z))
Math.round(((Math.round(Math.pi * depth / 10) * Math.pow(r, 2) + r * bd / 2 + Math.pow(bd/2, 2)) / 3, 1).floatValue)

Note I’m guessing at the arguments for the Math functions, Java is pretty close to JS in this regard.