Initial commit: CODESYS tree, integration docs, Node-RED scripts.
PLC application sources (NVL, rooms, boiler), HA/Node-RED integration guide, and NVL patch utilities. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
167
scripts/nodered_fast_toggle_patch.py
Normal file
167
scripts/nodered_fast_toggle_patch.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch Node-RED flows for fast HA toggle reliability."""
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
path = "/srv/docker-volumes/config/nodered/data/flows.json"
|
||||
backup = path + ".bak-fasttoggle-" + datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
shutil.copy2(path, backup)
|
||||
print("backup:", backup)
|
||||
|
||||
with open(path) as f:
|
||||
flows = json.load(f)
|
||||
|
||||
HA_TO_NVL = r"""const lib = global.get('roomConfigLib');
|
||||
const config = global.get('roomConfig') || {};
|
||||
const parsed = msg.plcLight || (lib ? lib.parseHaStateMessage(msg, config) : null);
|
||||
if (!parsed) return null;
|
||||
|
||||
const ROOM_NAME = parsed.cmdRoom;
|
||||
const lightNum = parsed.lightNum;
|
||||
const isOn = parsed.isOn;
|
||||
|
||||
// User toggle: update mirror cache so NVL→HA feedback does not re-write HA or set echo suppress.
|
||||
const flowKey = 'nvlToHa_' + parsed.entityId.replace(/\./g, '_');
|
||||
flow.set(flowKey, isOn);
|
||||
const echo = flow.get('haEchoSuppress') || {};
|
||||
delete echo[parsed.entityId];
|
||||
flow.set('haEchoSuppress', echo);
|
||||
|
||||
if (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });
|
||||
const state = flow.get('nvlInState');
|
||||
if (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {};
|
||||
const r = state.rooms[ROOM_NAME];
|
||||
const onKey = 'ha_l' + lightNum + '_on';
|
||||
const offKey = 'ha_l' + lightNum + '_off';
|
||||
r[onKey] = false;
|
||||
r[offKey] = false;
|
||||
if (isOn) r[onKey] = true; else r[offKey] = true;
|
||||
flow.set('nvlInState', state);
|
||||
|
||||
const seq = (flow.get('nvlClearSeq') || 0) + 1;
|
||||
flow.set('nvlClearSeq', seq);
|
||||
|
||||
node.warn('[HA to NVL] ' + parsed.entityId + ' → ' + ROOM_NAME + ' ' + (isOn ? onKey : offKey));
|
||||
|
||||
msg.payload = { buildAndSend: true };
|
||||
msg.zigbeeClear = {
|
||||
seq,
|
||||
list: [
|
||||
{ room: ROOM_NAME, key: onKey },
|
||||
{ room: ROOM_NAME, key: offKey },
|
||||
],
|
||||
};
|
||||
return msg;"""
|
||||
|
||||
CLEAR_NVL = r"""if (msg.zigbeeClear) {
|
||||
const spec = Array.isArray(msg.zigbeeClear)
|
||||
? { seq: msg.zigbeeClearSeq, list: msg.zigbeeClear }
|
||||
: msg.zigbeeClear;
|
||||
const list = spec.list || (spec.room ? [spec] : []);
|
||||
const seq = spec.seq != null ? spec.seq : msg.zigbeeClearSeq;
|
||||
if (seq != null && seq !== flow.get('nvlClearSeq')) {
|
||||
return null;
|
||||
}
|
||||
const state = flow.get('nvlInState') || { rooms: {}, boiler: {} };
|
||||
for (const item of list) {
|
||||
if (item.room === 'boiler') {
|
||||
if (!state.boiler) state.boiler = {};
|
||||
if (item.key) state.boiler[item.key] = false;
|
||||
continue;
|
||||
}
|
||||
const r = state.rooms[item.room];
|
||||
if (r && item.key) r[item.key] = false;
|
||||
}
|
||||
flow.set('nvlInState', state);
|
||||
}
|
||||
msg.payload = { buildAndSend: true };
|
||||
return msg;"""
|
||||
|
||||
NVL_TO_HA = r"""const config = global.get('roomConfig');
|
||||
const LIGHT_ENTITY_MAP = (config && config.lightEntityMap && config.lightEntityMap.length)
|
||||
? config.lightEntityMap
|
||||
: [];
|
||||
|
||||
const ECHO_MS = 400;
|
||||
const payload = msg.payload || {};
|
||||
const calls = [];
|
||||
const echo = flow.get('haEchoSuppress') || {};
|
||||
const until = Date.now() + ECHO_MS;
|
||||
|
||||
for (const entry of LIGHT_ENTITY_MAP) {
|
||||
const room = payload[entry.room] || {};
|
||||
const li = entry.light;
|
||||
const isOn = !!(room['l_' + li + '_status'] || room['l_' + li] || room['l' + li]);
|
||||
const flowKey = 'nvlToHa_' + entry.entityId.replace(/\./g, '_');
|
||||
const last = flow.get(flowKey);
|
||||
if (last === isOn) continue;
|
||||
flow.set(flowKey, isOn);
|
||||
echo[entry.entityId] = until;
|
||||
|
||||
const domain = entry.entityId.split('.')[0] || 'input_boolean';
|
||||
const service = isOn ? 'turn_on' : 'turn_off';
|
||||
calls.push({
|
||||
payload: {
|
||||
action: domain + '.' + service,
|
||||
target: { entity_id: [entry.entityId] },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
flow.set('haEchoSuppress', echo);
|
||||
if (!calls.length) return null;
|
||||
return calls;"""
|
||||
|
||||
patches = {
|
||||
"4045239f675b77d2": HA_TO_NVL,
|
||||
"44822e048dbaee31": CLEAR_NVL,
|
||||
"d5cdc38171230744": NVL_TO_HA,
|
||||
}
|
||||
|
||||
for n in flows:
|
||||
nid = n.get("id")
|
||||
if nid in patches:
|
||||
n["func"] = patches[nid]
|
||||
print("patched", nid, n.get("name"))
|
||||
|
||||
if nid == "wh_pulse_fn" and "nvlClearSeq" not in n.get("func", ""):
|
||||
n["func"] = n["func"].replace(
|
||||
"return {\n payload: { buildAndSend: true },\n zigbeeClear: { room: 'boiler', key: key },\n};",
|
||||
"""const seq = (flow.get('nvlClearSeq') || 0) + 1;
|
||||
flow.set('nvlClearSeq', seq);
|
||||
return {
|
||||
payload: { buildAndSend: true },
|
||||
zigbeeClear: { seq, list: [{ room: 'boiler', key }] },
|
||||
};""",
|
||||
)
|
||||
print("patched wh_pulse_fn")
|
||||
|
||||
if nid == "plc_tt_pulse" and "nvlClearSeq" not in n.get("func", ""):
|
||||
n["func"] = n["func"].replace(
|
||||
"return {\n payload: { buildAndSend: true },\n zigbeeClear: { room: room, key: key },\n};",
|
||||
"""const seq = (flow.get('nvlClearSeq') || 0) + 1;
|
||||
flow.set('nvlClearSeq', seq);
|
||||
return {
|
||||
payload: { buildAndSend: true },
|
||||
zigbeeClear: { seq, list: [{ room: room, key: key }] },
|
||||
};""",
|
||||
)
|
||||
print("patched plc_tt_pulse")
|
||||
|
||||
if nid == "718ba68ae647874f" and "nvlClearSeq" not in n.get("func", ""):
|
||||
n["func"] = n["func"].replace(
|
||||
" outPlc.push({\n payload: { buildAndSend: true },\n zigbeeClear: clearList,\n });",
|
||||
""" const seq = (flow.get('nvlClearSeq') || 0) + 1;
|
||||
flow.set('nvlClearSeq', seq);
|
||||
outPlc.push({
|
||||
payload: { buildAndSend: true },
|
||||
zigbeeClear: { seq, list: clearList },
|
||||
});""",
|
||||
)
|
||||
print("patched zigbee dispatch")
|
||||
|
||||
with open(path, "w") as f:
|
||||
json.dump(flows, f)
|
||||
|
||||
print("done")
|
||||
Reference in New Issue
Block a user