Skip to content

Bug Report: Battery priority rotation string concatenation bug (floating-point accumulation) #133

@SuperTus

Description

@SuperTus

Bug Report: Battery priority rotation string concatenation bug (floating-point accumulation)

Description

A bug in the battery priority rotation logic causes the input_number.house_battery_control_prioritize_battery entity to accumulate trailing decimals (e.g., mutating from 2.0 to 2.01, 2.011, 2.0111 over time) every 30 minutes during automatic rotation.

This happens because Home Assistant returns input_number states as floats (or strings representing floats, like "2.0"), causing JavaScript's += operator to perform string concatenation ("2.0" + 1 = "2.01") instead of mathematical addition. Furthermore, casting the payload as a string template in the Home Assistant Call Service node enforces this behavior upon writing back to the API.


Technical Analysis & Affected Nodes

1. Node: Change priority (Function Node)

Root Cause: The node reads the current battery priority but does not enforce strict integer parsing.

  • Original Code:
    let M = RED.util.getMessageProperty(msg,"prioritize_battery") || 1;
    M += 1;
  • Mechanism: When M is "2.0" (string), M += 1 evaluates to "2.01". The boundary check if(M > msg.battery_count) ("2.01" > 3.0) evaluates to false in loose comparison, allowing the corrupt value to pass through.

2. Node: Update prioritized battery (Home Assistant Call Service Node)

Root Cause: The Data field is configured as a JSON string template with quotes.

  • Original Configuration: {"value": "{{payload}}"}

  • Mechanism: This implicitly casts any numeric payload into a strict string before sending it to the Home Assistant API. While Home Assistant accepts this because the float value falls within boundaries, it permanently stores the incorrect decimal representation.

3. Node: Update battery order (Function Node)

Secondary Impact: Loose equality checking (M == 1) creates unexpected code execution paths if M contains an abnormal string format. Strict type safety (===) should be enforced here after parsing inputs.

Proposed Fix / Remediation

Fix for Change priority (Function Node)

Enforce base-10 integer parsing on both the input entity state and the battery count before executing any arithmetic operations:

Javascript

// Current prioritized battery is the Mth battery
// Get value and enforce to integer
let rawM = RED.util.getMessageProperty(msg,"prioritize_battery") || 1;
let M = parseInt(rawM, 10) || 1;

// Also enforce integer on battery count to prevent float comparison issues
let batteryCount = parseInt(msg.battery_count, 10) || 3;

// Prioritize the next battery (Now it is a strict mathematical addition)
M += 1;

// If the next battery does not exist, prioritize the first again
if (M > batteryCount) M = 1;

// Pass along as payload
msg.payload = M;

// Show which battery has priority
node.status({fill:"green",shape:"dot",text:Prioritize battery #${M}});

// Done
return msg;

Fix for Update prioritized battery (Call Service Node)

  1. Switch the Data field format dropdown from JSON to J: expression (JSONata).
  2. Update the expression to explicitly cast the payload to a number, removing string wrappers:

JSON

{ "value": $number(payload) }

Fix for Update battery order (Function Node)

Ensure that the prioritize_battery parameter is parsed as an integer at the top of the function to keep the array slicing functions robust:

JavaScript changes:

let rawM = RED.util.getMessageProperty(msg,"prioritize_battery") || 1;
const M = parseInt(rawM, 10) || 1;

// ... intermediate logic ...
if (M === 1) return msg;

JavaScript (complete):

// Cycles battery priority as follows
// M=1 [1,2,3,4] | M=2 [2,3,4,1] | M=3 [3,4,1,2] | etc.
// With M the battery to prioritize

// msg.batteries (original order)
let currentArray = msg.batteries;

// ---- HERE IS FIX 1 ----
// Retrieve the value, convert it to a string, and strip all decimals to a hard whole number (Integer).
// If the value is unreadable or empty, it falls back to 1.
let rawM = RED.util.getMessageProperty(msg,"prioritize_battery") || 1;
const M = parseInt(rawM, 10) || 1;
// -------------------------

// Flag wether the order may be reversed when discharging (e.g. Self-consumption utilizes this)
const reverseDischargePriority = RED.util.getMessageProperty(msg,"battery_priority_cycle_interval") !== "Auto balance";
RED.util.setMessageProperty(msg, "advanced_settings.reverse_discharge_priority", reverseDischargePriority,true);

// 1st battery has prio? Skip and continue without change
// ---- HERE IS FIX 2 ----
// Strict type safety (===) should be enforced here after parsing inputs.
if (M===1) return msg;

// N equals the number of items to take from the front to the back of the array, effectivly rotating battery priority
const N = M - 1; // base-0 version of M
node.status({fill:"blue",shape:"ring",text:N = ${N}});

// 1. Check if the array is valid and long enough
if (!Array.isArray(currentArray) || currentArray.length < N) {
// Send an error or the original array back if the array is invalid/too short
node.error(Array is not valid or shorter than ${N} items., msg);
return null;
}

// 2. Get the first N items (these will go to the back)
// slice(0, N) retrieves elements from the start (index 0) up to index N (exclusive).
let firstN = currentArray.slice(0, N);

// 3. Get the remaining items (these will stay at the front)
// slice(N) retrieves elements from index N to the end of the array.
let remaining = currentArray.slice(N);

// 4. Concatenate them: remaining items (front) + first N items (back)
let newArray = remaining.concat(firstN);

// 5. Place the new array back into msg.batteries
msg.batteries = newArray;

return msg;

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions