Run Java script inside automation

Hello,
I currently use node-red for my automations. However, I am in the process of replacing this with pure Home Assistant automations.
However, I have a complex calculation in node-red in a Java Script function. This script calculates the fill level of my water tank.
How can I run the script in a native HA automation?

The script looks like this:

// zistern litering table (got from manufacturer GRAF)
// first column = fillheight in mm
// second column = liter
const literingTable = [
    [0, 0],
    [100, 135],
    [200, 320],
    [300, 550],
    [400, 825],
    [500, 1120],
    [600, 1450],
    [700, 1800],
    [800, 2160],
    [900, 2530],
    [1000, 2895],
    [1100, 3245],
    [1200, 3580],
    [1300, 3885],
    [1400, 4160],
    [1500, 4430],
    [1600, 4585],
    [1700, 4720],
    [1800, 4780],
];
//var msg;
var msg2;
var msg3;
var msg4;
// pressure Sensor parameters
const max_height = 5;   // 4m
const max_mA = 20;      // mA at max_height
const min_mA = 4;      // mA at zero height
// index to table for lowest level in zistern where water
// can still be pumped. Below pump cant reach the water
const percent_0_index = 2   // eg, 2 is 976l
//const lowest_level_Tindex = 2;

// Main starts here
var y;
var measured_mA = msg.payload; // get mA value from Sensor as input
var measured_mA_mean;
measured_mA_mean = context.get('measured_mA_mean');
if (context.get('Startup') == 341487) {
    context.set("Startup", 0)
    measured_mA_mean = measured_mA;
}
measured_mA_mean = (measured_mA_mean * 2 + measured_mA) / 3;
context.set('measured_mA_mean', measured_mA_mean);
if (measured_mA < min_mA) {
    // if its below allowed range, then exit
    node.warn("Error ! Input Value below 4mA")
    return;
}

// both possible: measured_mA_mean or measured_mA
spline(measured_mA_mean)

// calculates based on linear relationship. alternative below
var high_mm = (max_height * 1000 / (max_mA - min_mA) * (measured_mA_mean - min_mA));

//var x = Math.round(high_mm);
var x = high_mm;

//search for fitting table area, getting index into table as i
for (var i = 0; i < literingTable.length; i++) {
    if (literingTable[i][0] > x) break; // if "end entry" found, then exit
}
i = i - 1;  // correct index to "start entry"

// check if index was inside table and not the last entry
if (i < literingTable.length - 1) {
    // was inside table
    // get values from table
    var x1 = literingTable[i + 0][0];
    var y1 = literingTable[i + 0][1];
    var x2 = literingTable[i + 1][0];
    var y2 = literingTable[i + 1][1];
    // and calculate the value in between = lineare interpolation
    y = (y1 + ((x - x1) / (x2 - x1) * (y2 - y1)));
}
else {
    // was the last entry,
    // so return last entry as fixe value from table
    // no calcuation needed
    y = literingTable[i + 0][1]
}
// calculate percentage from table index 2 as 0% to last table index as 100%
var percent = 100 / (literingTable[literingTable.length - 1][1] - literingTable[percent_0_index][1]);
percent = Math.round(((percent * (y - literingTable[percent_0_index][1])) * 1) * 1e0) / 1e0;
if (percent < 0) {
    percent = -1;
}
y = Math.round(y);
msg.payload = [
    [   // first value
        {   // influxdb2 fields
            "menge": y
        },
        {   // influxdb2 tags
            "typ": "inhalt",
            "einheit": "liter",
            "quelle": "drucksensor",
            "ort": "zisterne"
        }
    ],
    [   // second value
        {   // influxdb2 fields
            "menge": percent
        },
        {   // influxdb2 tags
            "typ": "inhalt",
            "einheit": "prozent",
            "quelle": "drucksensor",
            "ort": "zisterne"
        }
    ]
];
return [msg];


function spline(pressure) {
    // normalized needed for spline interpolation
    const normalized_pressure = (pressure - min_mA) / (max_mA - min_mA);

    // spline interpolation for x:
    var interval = -1; // Initialize with an invalid value
    for (var i = 0; i < literingTable.length - 1; i++) {
        if (normalized_pressure >= literingTable[i][0] && normalized_pressure <= literingTable[i + 1][0]) {
            interval = i;
            break; // if "end entry" found, then exit
        }
    }

    // Errorhandling
    if (interval === -1) {
        interval = normalized_pressure < literingTable[0][0] ? 0 : literingTable.length - 2;
    }

    // find value between two entries
    const x1 = literingTable[interval][0];
    const y1 = literingTable[interval][1];
    const x2 = literingTable[interval + 1][0];
    const y2 = literingTable[interval + 1][1];

    //non-linear interpolation
    const t = (normalized_pressure - x1) / (x2 - x1);
    const a = 2 * y1 - 2 * y2 + 1;
    const b = -3 * y1 + 3 * y2;
    const c = y1;

    // fill height in mm
    var y = a * Math.pow(t, 3) + b * Math.pow(t, 2) + c * t;

    // fill volume (1800 based on max_fillheight) in mm
    var x = (Math.PI * y ^ 2 * (3 * (1800 / 2) - y)) / 3;

    // return what you need.
    return y; // or x
}

Thank you very much for your help

You can’t, AFAIK. If you really need JavaScript you have to run it elsewhere, such as a separate NodeJS VM. Best you can do in sorta native Home Assistant would be to rewrite as Python and run using the pyscript integration (from HACS, not built-in). Or rewrite as pure Jinja, but that would probably be a much worse experience than Python…

What is the calculation to get the height in mm?

It seems as you have a lot of calculations that is done in Node red that should be done in the (ESP?) device.
The device would give you a stable value

I have a probe in the tank which gives me a mA value depending on the fill level. The script calculates the fill level of the tank based on the mA value and the parameters.

Unfortunately, I did not create the script myself.
But can I run Python directly? If so, how?

Is it an ESP device?
If so show us the config for that.

no. It is a knx actor, which only provides me with a mA value

As already mentioned, pyscript installable through HACS.

To me it seems as if you just need to create a median sensor of the mA value you get then use this in this template:

{% set measured_mA_mean = 5 %} # states('sensor.mean_value_ma')

{% set list = {
      0: 0,
    100: 135,
    200: 320,
    300: 550,
    400: 825,
    500: 1120,
    600: 1450,
    700: 1800,
    800: 2160,
    900: 2530,
    1000: 2895,
    1100: 3245,
    1200: 3580,
    1300: 3885,
    1400: 4160,
    1500: 4430,
    1600: 4585,
    1700: 4720,
    1800: 4780} %}
{{ list[((5 * 1000 / (20 - 4) * (measured_mA_mean - 4)) / 100) | int *100] }}

Thank you for your quick help. I would test the template code. But unfortunately I don’t get a value.

template:
  - sensor:
      - name: 'Füllstand_Zisterne_l'
        device_class: "water"
        unit_of_measurement: "L"
        state: >
          {% set measured_mA_mean = 5 %} # states('sensor.fullstand_zisterne_ma')
          {% set list = {
            0: 0,
            100: 135,
            200: 320,
            300: 550,
            400: 825,
            500: 1120,
            600: 1450,
            700: 1800,
            800: 2160,
            900: 2530,
            1000: 2895,
            1100: 3245,
            1200: 3580,
            1300: 3885,
            1400: 4160,
            1500: 4430,
            1600: 4585,
            1700: 4720,
            1800: 4780} %}
          {{ list[((5 * 1000 / (20 - 4) * (measured_mA_mean - 4)) / 100) | int *100] }}

You cannot write a YAML comment inside a Jinja template. If you need to add a comment inside a template, the notation is to put it between {# and #}.

ah, I overlooked that. It took 5 as the temporary value. The correct mA value comes from the sensor fullstand_cistern_ma

I have now adjusted this as follows, but now there is this error message

TemplateError('TypeError: unsupported operand type(s) for -: 'str' and 'int'') while processing template 'Template<template=({% set measured_mA_mean = states('sensor.fullstand_zisterne_ma') %} {% set list = { 0: 0, 100: 135, 200: 320, 300: 550, 400: 825, 500: 1120, 600: 1450, 700: 1800, 800: 2160, 900: 2530, 1000: 2895, 1100: 3245, 1200: 3580, 1300: 3885, 1400: 4160, 1500: 4430, 1600: 4585, 1700: 4720, 1800: 4780} %} {{ list[((5 * 1000 / (20 - 4) * (measured_mA_mean - 4)) / 100) | int *100] }}) renders=4>' for attribute '_attr_native_value' in entity 'sensor.fullstand_zisterne_l'

the code

template:
  - sensor:
      - name: 'Füllstand_Zisterne_l'
        device_class: "water"
        unit_of_measurement: "L"
        state: >
          {% set measured_mA_mean = states('sensor.fullstand_zisterne_ma') %} 
          {% set list = {
            0: 0,
            100: 135,
            200: 320,
            300: 550,
            400: 825,
            500: 1120,
            600: 1450,
            700: 1800,
            800: 2160,
            900: 2530,
            1000: 2895,
            1100: 3245,
            1200: 3580,
            1300: 3885,
            1400: 4160,
            1500: 4430,
            1600: 4585,
            1700: 4720,
            1800: 4780} %}
          {{ list[((5 * 1000 / (20 - 4) * (measured_mA_mean - 4)) / 100) | int *100] }}

The idea was that you used the 5 to test it in developer tools then replaced it with the sensor.

{% set measured_mA_mean = states('sensor.fullstand_zisterne_ma') %} 

EDIT; sorry didn´t see the message above.

My bad:

{% set measured_mA_mean = states('sensor.fullstand_zisterne_ma') | float %} 

have adjusted it, unfortunately it doesn’t quite want to yet

ValueError: Sensor sensor.fullstand_zisterne_l has device class 'water', state class 'None' unit 'L' and suggested precision 'None' thus indicating it has a numeric value; however, it has the non-numeric value: '' (<class 'str'>)
template:
  - sensor:
      - name: 'Füllstand_Zisterne_l'
        device_class: "water"
        unit_of_measurement: "L"
        state: >
          {% set measured_mA_mean = states('sensor.fullstand_zisterne_ma') | float %} 
          {% set list = {
            0: 0,
            100: 135,
            200: 320,
            300: 550,
            400: 825,
            500: 1120,
            600: 1450,
            700: 1800,
            800: 2160,
            900: 2530,
            1000: 2895,
            1100: 3245,
            1200: 3580,
            1300: 3885,
            1400: 4160,
            1500: 4430,
            1600: 4585,
            1700: 4720,
            1800: 4780} %}
          {{ list[((5 * 1000 / (20 - 4) * (measured_mA_mean - 4)) / 100) | int *100] }}

{{ list[((5 * 1000 / (20 - 4) * (measured_mA_mean - 4)) / 100) | int *100] | int }}

I guess?

Can you test it in developer tools instead?

The keys in a dict are always strings, you are attempting to access an item by number. You need to cast the result of your expression to a string.

{{ list[(((5 * 1000 / (20 - 4) * (measured_mA_mean - 4)) / 100) | int *100) | string] }}

(Note that I have not yet checked any other parts of the code for validity, such as whether the calculation actually generates a valid number/key…)

no…

ValueError: Sensor sensor.fullstand_zisterne_l has device class 'water', state class 'None' unit 'L' and suggested precision 'None' thus indicating it has a numeric value; however, it has the non-numeric value: '' (<class 'str'>)
template:
  - sensor:
      - name: 'Füllstand_Zisterne_l'
        device_class: "water"
        unit_of_measurement: "L"
        state: >
          {% set measured_mA_mean = states('sensor.fullstand_zisterne_ma') | float %} 
          {% set list = {
            0: 0,
            100: 135,
            200: 320,
            300: 550,
            400: 825,
            500: 1120,
            600: 1450,
            700: 1800,
            800: 2160,
            900: 2530,
            1000: 2895,
            1100: 3245,
            1200: 3580,
            1300: 3885,
            1400: 4160,
            1500: 4430,
            1600: 4585,
            1700: 4720,
            1800: 4780} %}
          {{ list[(((5 * 1000 / (20 - 4) * (measured_mA_mean - 4)) / 100) | int *100) | string] }}

Try it first in developer tools so that it works.
Then my next guess is the device class and/or unit of measurement.

If it is the string that is missing then it shouldn’t be there.
The string cast should be after the closing bracket.
But it makes no sense to make it a string. It should be numeric so that it can be graphed

No. The result returned as the sensor state should (probably) not be a string, but I wouldn’t know as I haven’t worked with them much. It is the key that used to look up a value in the dict which must be a string. The value is fetched from a dict, not from a list.

Oh, that’s much easier in the dev tools :wink:
Thank you.

It looks like there is nothing in the variable or a string is expected, but there is a float in the variable.
The value of the variable is currently 10.6

dict object has no element 2000
Ergebnistyp: string

Dieses Template abonniert die folgenden Ereignisse zur Zustandsänderung:

Entität: sensor.fullstand_zisterne_ma