node-red-contrib-event-calc 0.1.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  defaults: {
11
11
  name: { value: "" },
12
12
  cache: { value: "", type: "event-cache", required: true },
13
- pattern: { value: "*" },
13
+ topic: { value: "" },
14
14
  outputFormat: { value: "value" },
15
15
  outputOnStart: { value: false }
16
16
  },
@@ -18,7 +18,7 @@
18
18
  outputs: 1,
19
19
  icon: "font-awesome/fa-filter",
20
20
  label: function() {
21
- return this.name || this.pattern || "event topic";
21
+ return this.name || this.topic || "event topic";
22
22
  },
23
23
  paletteLabel: "event topic",
24
24
  labelStyle: function() { return (this.name ? "node_label_italic" : "") + " event-calc-white-text"; },
@@ -30,18 +30,18 @@
30
30
  const cacheId = $("#node-input-cache").val();
31
31
  if (cacheId) {
32
32
  $.getJSON("event-cache/" + cacheId + "/topics", function(topics) {
33
- $("#node-input-pattern").autocomplete("option", "source", topics || []);
33
+ $("#node-input-topic").autocomplete("option", "source", topics || []);
34
34
  });
35
35
  }
36
36
  }
37
37
 
38
- // Setup autocomplete on pattern input
39
- $("#node-input-pattern").autocomplete({
38
+ // Setup autocomplete on topic input
39
+ $("#node-input-topic").autocomplete({
40
40
  source: [],
41
41
  minLength: 0,
42
42
  delay: 0
43
43
  }).on("focus", function() {
44
- if (!$(this).val() || $(this).val() === "*") {
44
+ if (!$(this).val()) {
45
45
  $(this).autocomplete("search", "");
46
46
  }
47
47
  });
@@ -65,19 +65,14 @@
65
65
  <input type="text" id="node-input-cache">
66
66
  </div>
67
67
  <div class="form-row">
68
- <label for="node-input-pattern"><i class="fa fa-bookmark"></i> Topic Pattern</label>
69
- <input type="text" id="node-input-pattern" placeholder="*">
70
- <div class="form-tips">
71
- Wildcards: <code>?</code> = exactly one character, <code>*</code> = one or more characters.<br>
72
- Examples: <code>sensor?</code> matches sensor1, <code>sensors/*</code> matches sensors/temp
73
- </div>
68
+ <label for="node-input-topic"><i class="fa fa-bookmark"></i> Topic</label>
69
+ <input type="text" id="node-input-topic" placeholder="sensors/room1/temp">
74
70
  </div>
75
71
  <div class="form-row">
76
72
  <label for="node-input-outputFormat"><i class="fa fa-sign-out"></i> Output Format</label>
77
73
  <select id="node-input-outputFormat" style="width:70%;">
78
74
  <option value="value">Value only (msg.payload = value)</option>
79
75
  <option value="full">Full entry (msg.payload = {value, ts, metadata})</option>
80
- <option value="all">All matching (msg.payload = {topic: value, ...})</option>
81
76
  </select>
82
77
  </div>
83
78
  <div class="form-row">
@@ -88,32 +83,31 @@
88
83
  </script>
89
84
 
90
85
  <script type="text/html" data-help-name="event-topic">
91
- <p>Subscribes to a topic pattern and outputs when matching topics update in the cache.</p>
86
+ <p>Subscribes to a topic and outputs when that topic updates in the cache.</p>
92
87
 
93
88
  <h3>Properties</h3>
94
89
  <dl class="message-properties">
95
90
  <dt>Cache</dt>
96
91
  <dd>The event-cache config node to use</dd>
97
- <dt>Topic Pattern</dt>
98
- <dd>Pattern with wildcards. <code>?</code> = exactly one character, <code>*</code> = one or more characters.</dd>
92
+ <dt>Topic</dt>
93
+ <dd>The exact topic to subscribe to</dd>
99
94
  <dt>Output Format</dt>
100
95
  <dd>
101
96
  <ul>
102
97
  <li><b>Value only</b>: <code>msg.payload</code> contains just the value</li>
103
98
  <li><b>Full entry</b>: <code>msg.payload</code> contains <code>{value, ts, metadata}</code></li>
104
- <li><b>All matching</b>: <code>msg.payload</code> contains all cached values matching the pattern</li>
105
99
  </ul>
106
100
  </dd>
107
101
  <dt>Output on deploy</dt>
108
- <dd>If checked, outputs all currently cached values matching the pattern when the flow starts</dd>
102
+ <dd>If checked, outputs the current cached value when the flow starts</dd>
109
103
  </dl>
110
104
 
111
105
  <h3>Inputs</h3>
112
106
  <dl class="message-properties">
113
- <dt class="optional">pattern <span class="property-type">string</span></dt>
114
- <dd>Dynamically change the subscription pattern</dd>
115
- <dt class="optional">payload/topic = "refresh"</dt>
116
- <dd>Output all currently cached values matching the pattern</dd>
107
+ <dt class="optional">topic + payload="subscribe"</dt>
108
+ <dd>Dynamically change the subscription to a new topic</dd>
109
+ <dt class="optional">payload = "refresh"</dt>
110
+ <dd>Output the current cached value</dd>
117
111
  </dl>
118
112
 
119
113
  <h3>Outputs</h3>
@@ -125,12 +119,4 @@
125
119
  <dt>timestamp <span class="property-type">number</span></dt>
126
120
  <dd>Unix timestamp when the value was cached</dd>
127
121
  </dl>
128
-
129
- <h3>Wildcard Examples</h3>
130
- <ul>
131
- <li><code>sensor?</code> - matches sensor1, sensorA (exactly one char)</li>
132
- <li><code>sensors/*</code> - matches sensors/temp, sensors/room1 (one or more chars after /)</li>
133
- <li><code>*/temp</code> - matches room1/temp, sensors/temp</li>
134
- <li><code>*</code> - matches any topic with one or more characters</li>
135
- </ul>
136
122
  </script>
@@ -1,12 +1,12 @@
1
1
  /**
2
- * event-topic - Subscription node for topic patterns
2
+ * event-topic - Subscription node for exact topics
3
3
  *
4
4
  * Features:
5
- * - Subscribes to cache using MQTT-style topic patterns
6
- * - Outputs when matching topics update
7
- * - Multiple output formats: value only, full entry, or all matching
8
- * - Optional output of existing values on start
9
- * - Dynamic pattern change via input message
5
+ * - Subscribes to cache for a specific topic
6
+ * - Outputs when the topic updates
7
+ * - Multiple output formats: value only or full entry
8
+ * - Optional output of existing value on start
9
+ * - Dynamic topic change via input message
10
10
  */
11
11
  module.exports = function(RED) {
12
12
  function EventTopicNode(config) {
@@ -14,7 +14,8 @@ module.exports = function(RED) {
14
14
  const node = this;
15
15
 
16
16
  node.cacheConfig = RED.nodes.getNode(config.cache);
17
- node.pattern = config.pattern || '#';
17
+ // Support both old 'pattern' and new 'topic' config
18
+ node.topic = config.topic || config.pattern || '';
18
19
  node.outputFormat = config.outputFormat || 'value';
19
20
  node.outputOnStart = config.outputOnStart || false;
20
21
 
@@ -25,6 +26,11 @@ module.exports = function(RED) {
25
26
  return;
26
27
  }
27
28
 
29
+ if (!node.topic) {
30
+ node.status({ fill: "yellow", shape: "ring", text: "no topic" });
31
+ return;
32
+ }
33
+
28
34
  /**
29
35
  * Build output message based on configured format
30
36
  */
@@ -45,21 +51,6 @@ module.exports = function(RED) {
45
51
  metadata: entry.metadata
46
52
  }
47
53
  };
48
- case 'all':
49
- const all = node.cacheConfig.getMatching(node.pattern);
50
- const values = {};
51
- for (const [t, e] of all) {
52
- values[t] = e.value;
53
- }
54
- return {
55
- topic: topic,
56
- payload: values,
57
- trigger: {
58
- topic: topic,
59
- value: entry.value
60
- },
61
- timestamp: entry.ts
62
- };
63
54
  default:
64
55
  return {
65
56
  topic: topic,
@@ -69,10 +60,10 @@ module.exports = function(RED) {
69
60
  }
70
61
 
71
62
  /**
72
- * Subscribe to the current pattern
63
+ * Subscribe to the current topic
73
64
  */
74
65
  function subscribe() {
75
- subscriptionId = node.cacheConfig.subscribe(node.pattern, (topic, entry) => {
66
+ subscriptionId = node.cacheConfig.subscribe(node.topic, (topic, entry) => {
76
67
  const msg = buildOutputMessage(topic, entry);
77
68
  node.send(msg);
78
69
 
@@ -84,43 +75,45 @@ module.exports = function(RED) {
84
75
 
85
76
  // Initial subscription
86
77
  subscribe();
87
- node.status({ fill: "green", shape: "dot", text: node.pattern });
78
+ const displayTopic = node.topic.length > 20 ? node.topic.substring(0, 17) + '...' : node.topic;
79
+ node.status({ fill: "green", shape: "dot", text: displayTopic });
88
80
 
89
- // Output existing values on start if configured
81
+ // Output existing value on start if configured
90
82
  if (node.outputOnStart) {
91
83
  setImmediate(() => {
92
- const matching = node.cacheConfig.getMatching(node.pattern);
93
- for (const [topic, entry] of matching) {
94
- const msg = buildOutputMessage(topic, entry);
84
+ const entry = node.cacheConfig.getValue(node.topic);
85
+ if (entry) {
86
+ const msg = buildOutputMessage(node.topic, entry);
95
87
  node.send(msg);
96
88
  }
97
89
  });
98
90
  }
99
91
 
100
- // Handle input messages for dynamic pattern change
92
+ // Handle input messages for dynamic topic change
101
93
  node.on('input', function(msg, send, done) {
102
94
  // For Node-RED 0.x compatibility
103
95
  send = send || function() { node.send.apply(node, arguments); };
104
96
  done = done || function(err) { if (err) node.error(err, msg); };
105
97
 
106
- if (msg.pattern && typeof msg.pattern === 'string') {
107
- // Unsubscribe from old pattern
98
+ if (msg.topic && typeof msg.topic === 'string' && msg.payload === 'subscribe') {
99
+ // Unsubscribe from old topic
108
100
  if (subscriptionId && node.cacheConfig) {
109
101
  node.cacheConfig.unsubscribe(subscriptionId);
110
102
  }
111
103
 
112
- // Update pattern and resubscribe
113
- node.pattern = msg.pattern;
104
+ // Update topic and resubscribe
105
+ node.topic = msg.topic;
114
106
  subscribe();
115
107
 
116
- node.status({ fill: "blue", shape: "dot", text: node.pattern });
108
+ const dt = node.topic.length > 20 ? node.topic.substring(0, 17) + '...' : node.topic;
109
+ node.status({ fill: "blue", shape: "dot", text: dt });
117
110
  }
118
111
 
119
- // Allow manual trigger to output all current values
120
- if (msg.topic === 'refresh' || msg.payload === 'refresh') {
121
- const matching = node.cacheConfig.getMatching(node.pattern);
122
- for (const [topic, entry] of matching) {
123
- const outMsg = buildOutputMessage(topic, entry);
112
+ // Allow manual trigger to output current value
113
+ if (msg.payload === 'refresh') {
114
+ const entry = node.cacheConfig.getValue(node.topic);
115
+ if (entry) {
116
+ const outMsg = buildOutputMessage(node.topic, entry);
124
117
  send(outMsg);
125
118
  }
126
119
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-red-contrib-event-calc",
3
- "version": "0.1.2",
4
- "description": "Node-RED nodes for event caching and calculations with topic wildcard patterns",
3
+ "version": "2.0.1",
4
+ "description": "Node-RED nodes for event caching and calculations",
5
5
  "author": {
6
6
  "name": "Holger Amort"
7
7
  },
@@ -9,19 +9,18 @@
9
9
  "node-red",
10
10
  "events",
11
11
  "cache",
12
- "wildcards",
13
12
  "reactive",
14
13
  "calculation",
15
14
  "streaming"
16
15
  ],
17
- "license": "MIT",
18
- "homepage": "https://github.com/ErnstHolger/node-red#readme",
16
+ "license": "SEE LICENSE IN LICENSE",
17
+ "homepage": "https://github.com/ErnstHolger/node-red-contrib-event-calc#readme",
19
18
  "bugs": {
20
- "url": "https://github.com/ErnstHolger/node-red/issues"
19
+ "url": "https://github.com/ErnstHolger/node-red-contrib-event-calc/issues"
21
20
  },
22
21
  "repository": {
23
22
  "type": "git",
24
- "url": "git+https://github.com/ErnstHolger/node-red.git"
23
+ "url": "git+https://github.com/ErnstHolger/node-red-contrib-event-calc.git"
25
24
  },
26
25
  "node-red": {
27
26
  "version": ">=2.0.0",
@@ -29,14 +28,29 @@
29
28
  "event-cache": "nodes/event-cache.js",
30
29
  "event-in": "nodes/event-in.js",
31
30
  "event-topic": "nodes/event-topic.js",
32
- "event-calc": "nodes/event-calc.js"
31
+ "event-calc": "nodes/event-calc.js",
32
+ "event-json": "nodes/event-json.js",
33
+ "event-simulator": "nodes/event-simulator.js",
34
+ "event-chart": "nodes/event-chart.js"
33
35
  }
34
36
  },
35
37
  "engines": {
36
38
  "node": ">=18.0.0"
37
39
  },
38
40
  "scripts": {
39
- "publish:dry": "npm publish --dry-run"
41
+ "deploy": "npm link",
42
+ "deploy:patch": "npm version patch --no-git-tag-version && npm link",
43
+ "deploy:minor": "npm version minor --no-git-tag-version && npm link",
44
+ "deploy:major": "npm version major --no-git-tag-version && npm link",
45
+ "publish:dry": "npm publish --dry-run",
46
+ "publish:patch": "npm version patch && npm publish && start https://flows.nodered.org/add/node",
47
+ "publish:minor": "npm version minor && npm publish && start https://flows.nodered.org/add/node",
48
+ "publish:major": "npm version major && npm publish && start https://flows.nodered.org/add/node",
49
+ "test": "playwright test",
50
+ "test:ui": "playwright test --ui",
51
+ "test:headed": "playwright test --headed"
40
52
  },
41
- "dependencies": {}
53
+ "devDependencies": {
54
+ "@playwright/test": "^1.57.0"
55
+ }
42
56
  }
@@ -0,0 +1,22 @@
1
+ // @ts-check
2
+ const { defineConfig } = require('@playwright/test');
3
+
4
+ module.exports = defineConfig({
5
+ testDir: './tests',
6
+ fullyParallel: false,
7
+ forbidOnly: !!process.env.CI,
8
+ retries: process.env.CI ? 2 : 0,
9
+ workers: 1,
10
+ reporter: 'html',
11
+ use: {
12
+ baseURL: 'http://localhost:1880',
13
+ trace: 'on-first-retry',
14
+ screenshot: 'only-on-failure',
15
+ },
16
+ projects: [
17
+ {
18
+ name: 'chromium',
19
+ use: { browserName: 'chromium' },
20
+ },
21
+ ],
22
+ });
@@ -0,0 +1,141 @@
1
+ // @ts-check
2
+ const { test, expect } = require('@playwright/test');
3
+
4
+ test.describe('Event Calc - External Trigger Feature', () => {
5
+ test.beforeEach(async ({ page }) => {
6
+ // Navigate to Node-RED editor
7
+ await page.goto('/');
8
+ // Wait for Node-RED to load
9
+ await page.waitForSelector('#red-ui-palette', { timeout: 30000 });
10
+ });
11
+
12
+ test('should show External Trigger checkbox in event-calc node configuration', async ({ page }) => {
13
+ // Search for the event-calc node in the palette
14
+ const paletteSearch = page.locator('#red-ui-palette-search input');
15
+ await paletteSearch.fill('event calc');
16
+
17
+ // Wait for search results
18
+ await page.waitForTimeout(500);
19
+
20
+ // Find the event-calc node in the palette
21
+ const eventCalcNode = page.locator('.red-ui-palette-node[data-palette-type="event-calc"]');
22
+ await expect(eventCalcNode).toBeVisible();
23
+
24
+ // Drag the node to the workspace
25
+ const workspace = page.locator('#red-ui-workspace-chart');
26
+ await eventCalcNode.dragTo(workspace);
27
+
28
+ // Double-click on the newly created node to open the editor
29
+ // First find the node in the workspace
30
+ const nodeInWorkspace = page.locator('.red-ui-flow-node-group').last();
31
+ await nodeInWorkspace.dblclick();
32
+
33
+ // Wait for the edit dialog to open
34
+ await page.waitForSelector('.red-ui-editor', { timeout: 5000 });
35
+
36
+ // Verify the External Trigger checkbox exists
37
+ const externalTriggerCheckbox = page.locator('#node-input-externalTrigger');
38
+ await expect(externalTriggerCheckbox).toBeVisible();
39
+
40
+ // Verify it's unchecked by default
41
+ await expect(externalTriggerCheckbox).not.toBeChecked();
42
+
43
+ // Check the checkbox
44
+ await externalTriggerCheckbox.check();
45
+ await expect(externalTriggerCheckbox).toBeChecked();
46
+
47
+ // Verify the label is correct
48
+ const label = page.locator('label[for="node-input-externalTrigger"]');
49
+ await expect(label).toContainText('External Trigger');
50
+ await expect(label).toContainText('calculate on any input message');
51
+ });
52
+
53
+ test('should toggle External Trigger checkbox', async ({ page }) => {
54
+ // Search for the event-calc node
55
+ const paletteSearch = page.locator('#red-ui-palette-search input');
56
+ await paletteSearch.fill('event calc');
57
+ await page.waitForTimeout(500);
58
+
59
+ // Find and drag the node
60
+ const eventCalcNode = page.locator('.red-ui-palette-node[data-palette-type="event-calc"]');
61
+ const workspace = page.locator('#red-ui-workspace-chart');
62
+ await eventCalcNode.dragTo(workspace);
63
+
64
+ // Open the node editor
65
+ const nodeInWorkspace = page.locator('.red-ui-flow-node-group').last();
66
+ await nodeInWorkspace.dblclick();
67
+ await page.waitForSelector('.red-ui-editor', { timeout: 5000 });
68
+
69
+ const externalTriggerCheckbox = page.locator('#node-input-externalTrigger');
70
+
71
+ // Toggle on
72
+ await externalTriggerCheckbox.check();
73
+ await expect(externalTriggerCheckbox).toBeChecked();
74
+
75
+ // Toggle off
76
+ await externalTriggerCheckbox.uncheck();
77
+ await expect(externalTriggerCheckbox).not.toBeChecked();
78
+
79
+ // Toggle on again
80
+ await externalTriggerCheckbox.check();
81
+ await expect(externalTriggerCheckbox).toBeChecked();
82
+ });
83
+
84
+ test('should persist External Trigger setting after save', async ({ page }) => {
85
+ // Search for event-cache node first (required config)
86
+ const paletteSearch = page.locator('#red-ui-palette-search input');
87
+ await paletteSearch.fill('event cache');
88
+ await page.waitForTimeout(500);
89
+
90
+ // Add event-cache config node if needed
91
+ const eventCacheNode = page.locator('.red-ui-palette-node[data-palette-type="event-cache"]');
92
+ if (await eventCacheNode.isVisible()) {
93
+ const workspace = page.locator('#red-ui-workspace-chart');
94
+ await eventCacheNode.dragTo(workspace);
95
+
96
+ // Configure the cache node
97
+ const cacheNodeInWorkspace = page.locator('.red-ui-flow-node-group').last();
98
+ await cacheNodeInWorkspace.dblclick();
99
+ await page.waitForSelector('.red-ui-editor', { timeout: 5000 });
100
+
101
+ // Set a name for the cache
102
+ const nameInput = page.locator('#node-input-name');
103
+ await nameInput.fill('Test Cache');
104
+
105
+ // Save the cache node
106
+ const doneButton = page.locator('.red-ui-tray-footer button').filter({ hasText: 'Done' });
107
+ await doneButton.click();
108
+ await page.waitForTimeout(500);
109
+ }
110
+
111
+ // Now add the event-calc node
112
+ await paletteSearch.clear();
113
+ await paletteSearch.fill('event calc');
114
+ await page.waitForTimeout(500);
115
+
116
+ const eventCalcNode = page.locator('.red-ui-palette-node[data-palette-type="event-calc"]');
117
+ const workspace = page.locator('#red-ui-workspace-chart');
118
+ await eventCalcNode.dragTo(workspace);
119
+
120
+ // Open the node editor
121
+ const calcNodeInWorkspace = page.locator('.red-ui-flow-node-group').last();
122
+ await calcNodeInWorkspace.dblclick();
123
+ await page.waitForSelector('.red-ui-editor', { timeout: 5000 });
124
+
125
+ // Check the External Trigger checkbox
126
+ const externalTriggerCheckbox = page.locator('#node-input-externalTrigger');
127
+ await externalTriggerCheckbox.check();
128
+
129
+ // Save the node configuration
130
+ const doneButton = page.locator('.red-ui-tray-footer button').filter({ hasText: 'Done' });
131
+ await doneButton.click();
132
+ await page.waitForTimeout(500);
133
+
134
+ // Reopen the node editor
135
+ await calcNodeInWorkspace.dblclick();
136
+ await page.waitForSelector('.red-ui-editor', { timeout: 5000 });
137
+
138
+ // Verify the checkbox is still checked
139
+ await expect(externalTriggerCheckbox).toBeChecked();
140
+ });
141
+ });