UNPKG

@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
<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% !important; vertical-align: middle; } .inverter-control-div .form-row div, .inverter-control-div .form-row input, .inverter-control-div .form-row textarea { max-width: 66% !important; } .inverter-control-div .form-tips { max-width: 100% !important; 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>