@janart19/node-red-fusebox
Version:
A comprehensive collection of custom nodes for interfacing with Fusebox automation controllers - data streams, energy management, and utilities
389 lines (326 loc) • 18.6 kB
HTML
<script type="text/javascript">
RED.nodes.registerType("fusebox-inverter-control", {
category: "fusebox energy",
color: "#E7E7AE",
// Define the default values for the node configuration
defaults: {
name: { value: "" },
outputTopic: { value: "", required: true },
topicCurrentGridPower: { value: "", required: true },
topicExternalSetpoint: { value: "", required: true },
topicMaxGridImport: { value: "", required: false },
topicMaxGridExport: { value: "", required: false },
topicMaxInverterImport: { value: "", required: false },
topicMaxInverterExport: { value: "", required: false },
topicDebug: { value: "", required: false }
},
// Define the inputs and outputs of the node
inputs: 1,
outputs: 1,
icon: "font-awesome/fa-flash",
label: function () {
return this.name || "inverter control";
},
paletteLabel: function () {
return "inverter control";
},
// Update form fields
oneditprepare: function () {
const node = this;
let _controllers = {};
// Populate form with node values
$("#node-input-name").val(node.name);
$("#node-input-outputTopic").val(node.outputTopic);
$("#node-input-topicCurrentGridPower").val(node.topicCurrentGridPower);
$("#node-input-topicExternalSetpoint").val(node.topicExternalSetpoint);
$("#node-input-topicMaxGridImport").val(node.topicMaxGridImport);
$("#node-input-topicMaxGridExport").val(node.topicMaxGridExport);
$("#node-input-topicMaxInverterImport").val(node.topicMaxInverterImport);
$("#node-input-topicMaxInverterExport").val(node.topicMaxInverterExport);
$("#node-input-topicDebug").val(node.topicDebug);
// Define event listeners for form fields
$(
"#node-input-topicCurrentGridPower, #node-input-topicExternalSetpoint, #node-input-topicMaxGridImport, #node-input-topicMaxGridExport, #node-input-topicMaxInverterImport, #node-input-topicMaxInverterExport, #node-input-topicDebug"
).on("input", function () {
updateTipText();
});
// Initialize topic dropdown and tip text
queryControllerConfig();
updateTipText();
// Query topics defined in read and write nodes
function queryControllerConfig() {
$.getJSON(`fusebox/controller-config`, function (data) {
_controllers = data;
initializeAutocomplete($("#node-input-outputTopic"));
initializeAutocomplete($("#node-input-topicCurrentGridPower"));
initializeAutocomplete($("#node-input-topicExternalSetpoint"));
initializeAutocomplete($("#node-input-topicMaxGridImport"));
initializeAutocomplete($("#node-input-topicMaxGridExport"));
initializeAutocomplete($("#node-input-topicMaxInverterImport"));
initializeAutocomplete($("#node-input-topicMaxInverterExport"));
initializeAutocomplete($("#node-input-topicDebug"));
}).fail(function () {
console.error("Failed to get topics!");
_controllers = {};
});
}
// Initialize autocomplete for topic inputs in the editable list
function initializeAutocomplete(element, onSelect = null) {
element
.autocomplete({
minLength: 0,
source: function (request, response) {
const term = request.term.toLowerCase();
const selection = getTopics();
const matches = selection?.filter((obj) => {
return obj.label.toLowerCase().indexOf(term) > -1;
});
response(matches);
},
focus: function (event, ui) {
// Don't change the input value on hover/focus
event.preventDefault();
},
select: function (event, ui) {
event.preventDefault();
element.val(ui.item.topic);
// Call the onSelect callback if provided
if (onSelect) onSelect(element, ui.item);
}
})
.on("focus", function () {
element.autocomplete("search", element.val() || "");
})
.autocomplete("instance")._renderItem = function (ul, item) {
const term = this.term.trim();
const label = item.label;
let highlightedLabel = label;
if (term) {
const regex = new RegExp("(" + term.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") + ")", "ig");
highlightedLabel = label.replace(regex, '<strong style="color:#e65; font-weight:bold">$1</strong>');
}
return $("<li></li>").data("ui-autocomplete-item", item).append(`<div>${highlightedLabel}</div>`).appendTo(ul);
};
}
// Return a list of topics from the controller configuration
// Structure: [ { key: "ABC", member: 1, topic: test/1, label: ... } ]
function getTopics() {
const result = [];
const controllers = _controllers?.controllers || [];
for (const controller of controllers) {
result.push(...controller.formattedTopics);
}
return result;
}
// Update the tip text based on current settings
function updateTipText() {
const requiredTopics = [$("#node-input-topicCurrentGridPower").val(), $("#node-input-topicExternalSetpoint").val()].filter((t) => t && t.trim());
const optionalTopics = [
$("#node-input-topicMaxGridImport").val(),
$("#node-input-topicMaxGridExport").val(),
$("#node-input-topicMaxInverterImport").val(),
$("#node-input-topicMaxInverterExport").val(),
$("#node-input-topicDebug").val()
].filter((t) => t && t.trim());
const tipText1 = `This inverter control requires 2 required topics and supports 5 optional topics.`;
const tipText2 = `Required topics configured: ${requiredTopics.length}/2 | Optional topics: ${optionalTopics.length}/5`;
const tipText3 = `Power values are bipolar: positive for import, negative for export.`;
const tipText4 = `Input formats supported: single topic messages, object messages, and nested payload objects.`;
const tipText5 = `Incoming message formats:
1. { topic: "pid/actual", payload: 0.5 }
2. { "pid/actual": 0.5, "pid/setpoint": 1.0, ... }
3. { payload: { "pid/actual": 0.5, "pid/setpoint": 1.0, ... } }`;
$("#node-input-tip-text-1").text(tipText1);
$("#node-input-tip-text-2").text(tipText2);
$("#node-input-tip-text-3").text(tipText3);
$("#node-input-tip-text-4").text(tipText4);
$("#node-input-tip-text-5").text(tipText5);
}
},
// Save the values from the UI to the node configuration
oneditsave: function () {
const node = this;
node.name = $("#node-input-name").val();
node.outputTopic = $("#node-input-outputTopic").val();
node.topicCurrentGridPower = $("#node-input-topicCurrentGridPower").val();
node.topicExternalSetpoint = $("#node-input-topicExternalSetpoint").val();
node.topicMaxGridImport = $("#node-input-topicMaxGridImport").val();
node.topicMaxGridExport = $("#node-input-topicMaxGridExport").val();
node.topicMaxInverterImport = $("#node-input-topicMaxInverterImport").val();
node.topicMaxInverterExport = $("#node-input-topicMaxInverterExport").val();
node.topicDebug = $("#node-input-topicDebug").val();
}
});
</script>
<!-- Define style for the form fields -->
<style type="text/css">
.inverter-control-div .form-row {
margin-bottom: 10px;
}
.inverter-control-div .form-row label {
width: 33% ;
vertical-align: middle;
}
.inverter-control-div .form-row div,
.inverter-control-div .form-row input,
.inverter-control-div .form-row textarea {
max-width: 66% ;
}
.inverter-control-div .form-tips {
max-width: 100% ;
text-align: center;
}
.inverter-control-div .help-text {
font-size: 0.8em;
color: #666;
margin-top: 1px;
margin-bottom: 6px;
}
.inverter-control-div .form-divider {
border-top: 1px solid #ccc;
margin: 5px 0;
}
/* Autocomplete widget styling */
.inverter-control-div .ui-autocomplete {
max-height: 250px;
overflow-y: auto;
overflow-x: hidden;
z-index: 2000;
background: #fff;
border: 1px solid #ccc;
border-radius: 3px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.inverter-control-div .ui-menu-item {
font-size: 12px;
padding: 5px;
cursor: pointer;
position: relative;
}
.inverter-control-div .ui-menu-item:hover {
background-color: #f5f5f5;
}
</style>
<!-- Form fields are defined in the template below -->
<script type="text/html" data-template-name="fusebox-inverter-control">
<div class="inverter-control-div">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name" />
</div>
<div class="form-row">
<label for="node-input-outputTopic"><i class="fa fa-comment"></i> Output topic</label>
<input type="text" id="node-input-outputTopic" placeholder="" />
<div class="help-text">Topic name for the calculated inverter setpoint output message</div>
</div>
<div class="form-divider"></div>
<div class="form-row">
<label for="node-input-topicCurrentGridPower"><i class="fa fa-bolt"></i> Grid power topic</label>
<input type="text" id="node-input-topicCurrentGridPower" placeholder="" />
<div class="help-text">Topic for current grid power (required)</div>
</div>
<div class="form-row">
<label for="node-input-topicExternalSetpoint"><i class="fa fa-bullseye"></i> External setpoint topic</label>
<input type="text" id="node-input-topicExternalSetpoint" placeholder="" />
<div class="help-text">Topic for external inverter setpoint (required)</div>
</div>
<div class="form-row">
<label for="node-input-topicMaxGridImport"><i class="fa fa-arrow-up"></i> Max grid import topic</label>
<input type="text" id="node-input-topicMaxGridImport" placeholder="" />
<div class="help-text">Topic for maximum grid import power (default: 1500)</div>
</div>
<div class="form-row">
<label for="node-input-topicMaxGridExport"><i class="fa fa-arrow-down"></i> Max grid export topic</label>
<input type="text" id="node-input-topicMaxGridExport" placeholder="" />
<div class="help-text">Topic for maximum grid export power (default: -1500)</div>
</div>
<div class="form-row">
<label for="node-input-topicMaxInverterImport"><i class="fa fa-arrow-up"></i> Max inverter import topic</label>
<input type="text" id="node-input-topicMaxInverterImport" placeholder="" />
<div class="help-text">Topic for maximum inverter import power (default: 1000)</div>
</div>
<div class="form-row">
<label for="node-input-topicMaxInverterExport"><i class="fa fa-arrow-down"></i> Max inverter export topic</label>
<input type="text" id="node-input-topicMaxInverterExport" placeholder="" />
<div class="help-text">Topic for maximum inverter export power (default: -1000)</div>
</div>
<div class="form-row">
<label for="node-input-topicDebug"><i class="fa fa-bug"></i> Debug topic</label>
<input type="text" id="node-input-topicDebug" placeholder="" />
<div class="help-text">Topic to enable debug logging (optional)</div>
</div>
<div class="form-tips" id="node-input-tip-text-1"></div>
<div class="form-tips" id="node-input-tip-text-2"></div>
<div class="form-tips" id="node-input-tip-text-3"></div>
<div class="form-tips" id="node-input-tip-text-4"></div>
<div class="form-tips" id="node-input-tip-text-5" style="white-space: pre-line;"></div>
</div>
</script>
<!-- Define node description -->
<script type="text/html" data-help-name="fusebox-inverter-control">
<p>Calculate a new setpoint for the inverter, taking into account the limitations for the grid and the inverter.</p>
<p>This node implements inverter control logic with grid congestion management and anti-windup protection.</p>
<h3>Parameters</h3>
<dl class="message-properties">
<dt class="optional">name <span class="property-type">string</span></dt>
<dd>User-friendly name for the node</dd>
<dt>outputTopic <span class="property-type">string</span></dt>
<dd>The topic name for the output message containing the calculated inverter setpoint</dd>
<dt>topicCurrentGridPower <span class="property-type">string</span></dt>
<dd>Topic name for the current grid power measurement (required)</dd>
<dt>topicExternalSetpoint <span class="property-type">string</span></dt>
<dd>Topic name for the external inverter setpoint (required)</dd>
<dt>topicMaxGridImport <span class="property-type">string</span></dt>
<dd>Topic name for the maximum grid import power limit (optional, default: 1500)</dd>
<dt>topicMaxGridExport <span class="property-type">string</span></dt>
<dd>Topic name for the maximum grid export power limit (optional, default: -1500)</dd>
<dt>topicMaxInverterImport <span class="property-type">string</span></dt>
<dd>Topic name for the maximum inverter import power limit (optional, default: 1000)</dd>
<dt>topicMaxInverterExport <span class="property-type">string</span></dt>
<dd>Topic name for the maximum inverter export power limit (optional, default: -1000)</dd>
<dt>topicDebug <span class="property-type">string</span></dt>
<dd>Topic name to enable debug logging (optional)</dd>
</dl>
<h3>Inputs</h3>
<p>This node accepts input messages in 3 different formats (using your configured topic names):</p>
<dl class="message-properties">
<dt>Single topic format</dt>
<dd><code>{ topic: "ic/grid/actual", payload: 150.5 }</code></dd>
<dt>Object format</dt>
<dd><code>{ "ic/grid/actual": 150.5, "ic/inverter/setpoint": 100.0, ... }</code></dd>
<dt>Nested payload format</dt>
<dd><code>{ payload: { "ic/grid/actual": 150.5, "ic/inverter/setpoint": 100.0, ... } }</code></dd>
</dl>
<h3>Output</h3>
<p>A successful output message includes the following properties:</p>
<dl class="message-properties">
<dt>payload <span class="property-type">number</span></dt>
<dd>The calculated inverter setpoint value</dd>
<dt>topic <span class="property-type">string</span></dt>
<dd>The output topic as configured in the node (if specified)</dd>
<dt>metadata <span class="property-type">object</span></dt>
<dd>
Internal variables and calculations for debugging, including:
<ul>
<li><code>current_grid_power</code> - current grid power measurement</li>
<li><code>external_setpoint</code> - external setpoint value</li>
<li><code>max_grid_import, max_grid_export</code> - grid power limits</li>
<li><code>max_inverter_import, max_inverter_export</code> - inverter power limits</li>
<li><code>setpoint</code> - calculated setpoint</li>
<li><code>on_inverter_limit</code> - limitation status (0, 1, or -1)</li>
</ul>
</dd>
</dl>
<h3>Additional details</h3>
<p>The inverter control maintains internal state between executions, storing previous setpoint values and limitation status.</p>
<p>Power values are bipolar: positive values represent import, negative values represent export.</p>
<p>Export limits must be negative or zero, import limits must be positive or zero.</p>
<p>The algorithm prevents grid power limit overshoots by adjusting the inverter setpoint dynamically.</p>
<p>Setpoint changes are limited by available grid headroom to prevent oscillations.</p>
<p><strong>Topic Configuration:</strong> Each parameter has its own dedicated topic field, allowing complete flexibility in your topic naming convention.</p>
<p>Example output message:</p>
<code style="white-space: pre-wrap;">
{ "payload": 85.5, "topic": "ic/inverter/out", "metadata": { "current_grid_power": 150.0, "external_setpoint": 100.0, "max_grid_import": 1500, "max_grid_export": -1500,
"max_inverter_import": 1000, "max_inverter_export": -1000, "setpoint": 85.5, "on_inverter_limit": 0 } }
</code>
</script>