Files
nr-flow-validator/policy/flow.rego
T
david 517f3dbdae Add multi-plug toggle, debounce via unique topic subscriptions
Button (Door Button) now toggles Squiggle, Sideboard Lamp, and Bamboo Lights
via three parallel mqtt-out publishes.

Key fix: avoid subscribing to a wildcard '#' topic alongside a specific topic
in the same Node-RED instance. The mqtt-broker re-adds its handler as an
EventEmitter listener on each subscribe call (file:
  /usr/src/node-red/node_modules/@node-red/nodes/core/network/10-mqtt.js
  _clientOn('message', subscription.handler) is unconditional), so after
each deploy/restart the same message arrives N times to internal listeners.
Each mqtt-in node now uses a unique topic (zigbee2mqtt/<device>).

Removed zigbee-in # wildcard and plug-state ui-text widgets (state now
displayed via per-plug ui-text widgets).

Added function node support to flow2json.py for msg filtering logic.
Added ui-text format/width/height defaults support.
Added dashboard 2.0 config node defaults matching reference flow:
  ui-base, ui-theme, ui-page, ui-group with full property set.
2026-06-30 22:12:22 -07:00

178 lines
4.7 KiB
Rego

package nodered
import future.keywords.if
import future.keywords.contains
import future.keywords.in
# helpers
config_node(typ) if typ in {"mqtt-broker", "ui-base", "ui-page", "ui-group"}
welcome_isolated(typ) if typ in {"debug", "mqtt-broker", "ui-base", "ui-page", "ui-group", "ui-text", "ui-chart", "ui-gauge", "ui-button", "ui-switch", "ui-slider", "inject", "comment", "mqtt in", "catch", "status", "link in"}
broker_node(id) if {
some node in input
node.type == "mqtt-broker"
node.id == id
}
node_exists(id) if {
some node in input
node.id == id
}
has_inbound_wire(target_id) if {
some node in input
some wire in node.wires
target_id in wire
}
has_outbound_wire(source_id) if {
some node in input
node.id == source_id
node.wires
count(node.wires) > 0
}
# ---- DENY RULES ----
deny_missing_type contains msg if {
some node in input
not node.type
msg := sprintf("node %s: missing type field", [node.id])
}
deny_missing_field contains msg if {
some node in input
node.type == "mqtt-broker"
required := {"type", "broker", "port"}
some f in required
not node[f]
msg := sprintf("node %s (mqtt-broker): missing %s", [node.id, f])
}
deny_missing_field contains msg if {
some node in input
node.type == "mqtt in"
required := {"type", "topic", "broker", "z"}
some f in required
not node[f]
msg := sprintf("node %s (mqtt in): missing %s", [node.id, f])
}
deny_missing_field contains msg if {
some node in input
node.type == "mqtt out"
required := {"type", "broker", "z"}
some f in required
not node[f]
msg := sprintf("node %s (mqtt out): missing %s", [node.id, f])
}
deny_missing_field contains msg if {
some node in input
node.type == "debug"
required := {"type", "z"}
some f in required
not node[f]
msg := sprintf("node %s (debug): missing %s", [node.id, f])
}
deny_missing_field contains msg if {
some node in input
node.type in {"ui-text", "ui-button", "ui-chart", "ui-gauge", "ui-switch", "ui-slider", "ui-dropdown"}
required := {"type", "group", "z"}
some f in required
not node[f]
msg := sprintf("node %s (%s): missing %s", [node.id, node.type, f])
}
deny_missing_field contains msg if {
some node in input
node.type == "ui-group"
required := {"type", "page", "name"}
some f in required
not node[f]
msg := sprintf("node %s (ui-group): missing %s", [node.id, f])
}
deny_missing_field contains msg if {
some node in input
node.type == "ui-page"
required := {"type", "name", "ui"}
some f in required
not node[f]
msg := sprintf("node %s (ui-page): missing %s", [node.id, f])
}
deny_missing_field contains msg if {
some node in input
node.type == "ui-base"
required := {"type", "name"}
some f in required
not node[f]
msg := sprintf("node %s (ui-base): missing %s", [node.id, f])
}
deny_broker_ref contains msg if {
some node in input
node.type in {"mqtt in", "mqtt out"}
not broker_node(node.broker)
msg := sprintf("node %s (%s): references unknown broker %s", [node.id, node.type, node.broker])
}
ui_page(id) if {
some node in input
node.type == "ui-page"
node.id == id
}
ui_group(id) if {
some node in input
node.type == "ui-group"
node.id == id
}
deny_group_ref contains msg if {
some node in input
node.type in {"ui-text", "ui-button", "ui-chart", "ui-gauge", "ui-switch", "ui-slider", "ui-dropdown"}
not ui_group(node.group)
msg := sprintf("node %s (%s): references unknown group %s", [node.id, node.type, node.group])
}
deny_page_ref contains msg if {
some node in input
node.type == "ui-group"
not ui_page(node.page)
msg := sprintf("node %s (ui-group): references unknown page %s", [node.id, node.page])
}
deny_wire_ref contains msg if {
some node in input
some wire in node.wires
some target in wire
not node_exists(target)
msg := sprintf("node %s (%s): wire to unknown node %s", [node.id, node.type, target])
}
deny_missing_flow contains msg if {
some node in input
not config_node(node.type)
not node.z
msg := sprintf("node %s (%s): missing flow assignment (z)", [node.id, node.type])
}
deny_isolated_node contains msg if {
some node in input
not welcome_isolated(node.type)
not has_inbound_wire(node.id)
not has_outbound_wire(node.id)
msg := sprintf("node %s (%s): has no wires", [node.id, node.type])
}
deny_broker_port contains msg if {
some node in input
node.type == "mqtt-broker"
not is_valid_port(to_number(node.port))
msg := sprintf("node %s (mqtt-broker): invalid port %s", [node.id, node.port])
}
is_valid_port(p) if { p >= 1; p <= 65535 }