@skylord123/node-red-pebble-timeline 1.0.0 → 1.1.0
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.
- package/.claude/settings.local.json +11 -0
- package/node-red-sync-flow.json +124 -0
- package/package.json +4 -2
- package/pebble-timeline-add.html +1 -0
- package/pebble-timeline-add.js +31 -5
- package/pebble-timeline-delete.html +1 -0
- package/pebble-timeline-delete.js +42 -37
- package/pebble-timeline-list.html +11 -5
- package/pebble-timeline-list.js +12 -36
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "sync-flow-tab",
|
|
4
|
+
"type": "tab",
|
|
5
|
+
"label": "Rebble Timeline Sync",
|
|
6
|
+
"disabled": false,
|
|
7
|
+
"info": "Flow to fetch timeline pins from Rebble sync endpoint"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "inject-token",
|
|
11
|
+
"type": "inject",
|
|
12
|
+
"z": "sync-flow-tab",
|
|
13
|
+
"name": "Set Access Token",
|
|
14
|
+
"props": [
|
|
15
|
+
{
|
|
16
|
+
"p": "access_token",
|
|
17
|
+
"v": "YOUR_ACCESS_TOKEN_HERE",
|
|
18
|
+
"vt": "str"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"repeat": "",
|
|
22
|
+
"crontab": "",
|
|
23
|
+
"once": false,
|
|
24
|
+
"onceDelay": 0.1,
|
|
25
|
+
"topic": "",
|
|
26
|
+
"x": 140,
|
|
27
|
+
"y": 100,
|
|
28
|
+
"wires": [
|
|
29
|
+
["setup-request"]
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "setup-request",
|
|
34
|
+
"type": "function",
|
|
35
|
+
"z": "sync-flow-tab",
|
|
36
|
+
"name": "Setup Request",
|
|
37
|
+
"func": "msg.url = 'https://timeline-sync.rebble.io/v1/sync';\nmsg.headers = {\n 'Authorization': 'Bearer ' + msg.access_token\n};\n\n// Optional: pass timeline/glance cursor for incremental sync\nif (msg.timeline) {\n msg.url += '?timeline=' + msg.timeline;\n if (msg.glance) {\n msg.url += '&glance=' + msg.glance;\n }\n} else if (msg.glance) {\n msg.url += '?glance=' + msg.glance;\n}\n\nreturn msg;",
|
|
38
|
+
"outputs": 1,
|
|
39
|
+
"timeout": "",
|
|
40
|
+
"noerr": 0,
|
|
41
|
+
"initialize": "",
|
|
42
|
+
"finalize": "",
|
|
43
|
+
"libs": [],
|
|
44
|
+
"x": 340,
|
|
45
|
+
"y": 100,
|
|
46
|
+
"wires": [
|
|
47
|
+
["http-request"]
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "http-request",
|
|
52
|
+
"type": "http request",
|
|
53
|
+
"z": "sync-flow-tab",
|
|
54
|
+
"name": "GET /v1/sync",
|
|
55
|
+
"method": "GET",
|
|
56
|
+
"ret": "obj",
|
|
57
|
+
"paytoqs": "ignore",
|
|
58
|
+
"url": "",
|
|
59
|
+
"tls": "",
|
|
60
|
+
"persist": false,
|
|
61
|
+
"proxy": "",
|
|
62
|
+
"insecureHTTPParser": false,
|
|
63
|
+
"authType": "",
|
|
64
|
+
"senderr": false,
|
|
65
|
+
"headers": [],
|
|
66
|
+
"x": 530,
|
|
67
|
+
"y": 100,
|
|
68
|
+
"wires": [
|
|
69
|
+
["debug-output", "parse-response"]
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "debug-output",
|
|
74
|
+
"type": "debug",
|
|
75
|
+
"z": "sync-flow-tab",
|
|
76
|
+
"name": "Sync Response",
|
|
77
|
+
"active": true,
|
|
78
|
+
"tosidebar": true,
|
|
79
|
+
"console": false,
|
|
80
|
+
"tostatus": false,
|
|
81
|
+
"complete": "payload",
|
|
82
|
+
"targetType": "msg",
|
|
83
|
+
"statusVal": "",
|
|
84
|
+
"statusType": "auto",
|
|
85
|
+
"x": 740,
|
|
86
|
+
"y": 60,
|
|
87
|
+
"wires": []
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "parse-response",
|
|
91
|
+
"type": "function",
|
|
92
|
+
"z": "sync-flow-tab",
|
|
93
|
+
"name": "Parse Updates",
|
|
94
|
+
"func": "// Extract updates array and syncURL for next request\nlet response = msg.payload;\n\nif (response.updates && response.updates.length > 0) {\n node.status({fill: 'green', shape: 'dot', text: response.updates.length + ' updates'});\n} else {\n node.status({fill: 'yellow', shape: 'ring', text: 'No updates'});\n}\n\n// Store syncURL for subsequent requests\nmsg.syncURL = response.syncURL;\nmsg.updates = response.updates || [];\n\nreturn msg;",
|
|
95
|
+
"outputs": 1,
|
|
96
|
+
"timeout": "",
|
|
97
|
+
"noerr": 0,
|
|
98
|
+
"initialize": "",
|
|
99
|
+
"finalize": "",
|
|
100
|
+
"libs": [],
|
|
101
|
+
"x": 740,
|
|
102
|
+
"y": 140,
|
|
103
|
+
"wires": [
|
|
104
|
+
["debug-updates"]
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"id": "debug-updates",
|
|
109
|
+
"type": "debug",
|
|
110
|
+
"z": "sync-flow-tab",
|
|
111
|
+
"name": "Parsed Updates",
|
|
112
|
+
"active": true,
|
|
113
|
+
"tosidebar": true,
|
|
114
|
+
"console": false,
|
|
115
|
+
"tostatus": false,
|
|
116
|
+
"complete": "true",
|
|
117
|
+
"targetType": "full",
|
|
118
|
+
"statusVal": "",
|
|
119
|
+
"statusType": "auto",
|
|
120
|
+
"x": 940,
|
|
121
|
+
"y": 140,
|
|
122
|
+
"wires": []
|
|
123
|
+
}
|
|
124
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skylord123/node-red-pebble-timeline",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Node-RED nodes for interacting with the Pebble Timeline API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red",
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"axios": "
|
|
22
|
+
"axios": "1.12.0",
|
|
23
23
|
"fs-extra": "^11.1.0"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
|
package/pebble-timeline-add.html
CHANGED
package/pebble-timeline-add.js
CHANGED
|
@@ -288,7 +288,7 @@ module.exports = function(RED) {
|
|
|
288
288
|
node.status({fill: "green", shape: "dot", text: "OK"});
|
|
289
289
|
|
|
290
290
|
// Store the pin in our local storage
|
|
291
|
-
storePin(pin);
|
|
291
|
+
storePin(pin, timelineToken);
|
|
292
292
|
|
|
293
293
|
// Prepare the output message
|
|
294
294
|
msg.payload = {
|
|
@@ -336,8 +336,15 @@ module.exports = function(RED) {
|
|
|
336
336
|
});
|
|
337
337
|
|
|
338
338
|
// Helper to store a pin in local storage
|
|
339
|
-
function storePin(pin) {
|
|
340
|
-
|
|
339
|
+
function storePin(pin, timelineToken) {
|
|
340
|
+
// Ensure we have a valid token
|
|
341
|
+
if (!timelineToken) {
|
|
342
|
+
node.warn("Cannot store pin: No valid timeline token provided");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Convert token to string to ensure it can be used as an object key
|
|
347
|
+
timelineToken = String(timelineToken);
|
|
341
348
|
|
|
342
349
|
// Initialize the token's pins array if it doesn't exist
|
|
343
350
|
if (!pinsData[timelineToken]) {
|
|
@@ -368,21 +375,40 @@ module.exports = function(RED) {
|
|
|
368
375
|
function cleanupOldPins() {
|
|
369
376
|
const oneMonthAgo = new Date();
|
|
370
377
|
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
|
378
|
+
let changed = false;
|
|
371
379
|
|
|
372
380
|
// Iterate through all tokens
|
|
373
381
|
Object.keys(pinsData).forEach(token => {
|
|
382
|
+
// Make sure the token's data is an array
|
|
383
|
+
if (!Array.isArray(pinsData[token])) {
|
|
384
|
+
pinsData[token] = [];
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
374
388
|
// Filter out pins older than 1 month
|
|
375
389
|
const initialCount = pinsData[token].length;
|
|
376
390
|
pinsData[token] = pinsData[token].filter(pin => {
|
|
377
|
-
|
|
378
|
-
|
|
391
|
+
// Make sure pin has _stored property
|
|
392
|
+
if (!pin || !pin._stored) return false;
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const storedDate = new Date(pin._stored);
|
|
396
|
+
return storedDate >= oneMonthAgo;
|
|
397
|
+
} catch (e) {
|
|
398
|
+
// If date parsing fails, remove the pin
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
379
401
|
});
|
|
380
402
|
|
|
381
403
|
// Log if pins were removed
|
|
382
404
|
if (pinsData[token].length < initialCount) {
|
|
383
405
|
node.debug(`Removed ${initialCount - pinsData[token].length} old pins for token ${token.substring(0, 8)}...`);
|
|
406
|
+
changed = true;
|
|
384
407
|
}
|
|
385
408
|
});
|
|
409
|
+
|
|
410
|
+
// No need to save here as the calling function will save the file
|
|
411
|
+
return changed;
|
|
386
412
|
}
|
|
387
413
|
|
|
388
414
|
// Apply node configuration to the pin
|
|
@@ -102,7 +102,7 @@ module.exports = function(RED) {
|
|
|
102
102
|
node.status({fill: "green", shape: "dot", text: "Pin deleted"});
|
|
103
103
|
|
|
104
104
|
// Remove the pin from our local storage
|
|
105
|
-
removePin(pinId);
|
|
105
|
+
removePin(pinId, timelineToken);
|
|
106
106
|
|
|
107
107
|
// Prepare the output message
|
|
108
108
|
msg.payload = {
|
|
@@ -115,17 +115,37 @@ module.exports = function(RED) {
|
|
|
115
115
|
if (done) done();
|
|
116
116
|
})
|
|
117
117
|
.catch(error => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
// Handle 404 - pin already deleted
|
|
119
|
+
if (error.response && error.response.status === 404) {
|
|
120
|
+
node.warn(`Pin ${pinId} not found on server (404) - assuming already deleted`);
|
|
121
|
+
node.status({fill: "yellow", shape: "dot", text: "Pin already deleted"});
|
|
122
|
+
|
|
123
|
+
// Remove from local storage anyway
|
|
124
|
+
removePin(pinId, timelineToken);
|
|
125
|
+
|
|
126
|
+
msg.payload = {
|
|
127
|
+
success: true,
|
|
128
|
+
pinId: pinId,
|
|
129
|
+
alreadyDeleted: true,
|
|
130
|
+
message: "Pin not found on server, removed from local storage"
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
send(msg);
|
|
134
|
+
if (done) done();
|
|
135
|
+
} else {
|
|
136
|
+
// Other errors
|
|
137
|
+
node.status({fill: "red", shape: "dot", text: "Error: " + (error.response ? error.response.status : error.message)});
|
|
138
|
+
|
|
139
|
+
msg.payload = {
|
|
140
|
+
success: false,
|
|
141
|
+
pinId: pinId,
|
|
142
|
+
error: error.message,
|
|
143
|
+
response: error.response ? error.response.data : null
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
send(msg);
|
|
147
|
+
if (done) done(error);
|
|
148
|
+
}
|
|
129
149
|
});
|
|
130
150
|
}).catch(err => {
|
|
131
151
|
if (done) done(err);
|
|
@@ -133,8 +153,15 @@ module.exports = function(RED) {
|
|
|
133
153
|
});
|
|
134
154
|
|
|
135
155
|
// Helper to remove a pin from local storage
|
|
136
|
-
function removePin(pinId) {
|
|
137
|
-
|
|
156
|
+
function removePin(pinId, timelineToken) {
|
|
157
|
+
// Ensure we have a valid token
|
|
158
|
+
if (!timelineToken) {
|
|
159
|
+
node.warn("Cannot remove pin: No valid timeline token provided");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Convert token to string to ensure it can be used as an object key
|
|
164
|
+
timelineToken = String(timelineToken);
|
|
138
165
|
|
|
139
166
|
// Check if this token has any pins
|
|
140
167
|
if (!pinsData[timelineToken]) {
|
|
@@ -145,8 +172,7 @@ module.exports = function(RED) {
|
|
|
145
172
|
const initialCount = pinsData[timelineToken].length;
|
|
146
173
|
pinsData[timelineToken] = pinsData[timelineToken].filter(p => p.id !== pinId);
|
|
147
174
|
|
|
148
|
-
//
|
|
149
|
-
cleanupOldPins();
|
|
175
|
+
// Note: Cleanup of old pins is handled in the add node
|
|
150
176
|
|
|
151
177
|
// Only write if we actually removed something
|
|
152
178
|
if (pinsData[timelineToken].length !== initialCount) {
|
|
@@ -158,27 +184,6 @@ module.exports = function(RED) {
|
|
|
158
184
|
}
|
|
159
185
|
}
|
|
160
186
|
|
|
161
|
-
// Helper to clean up pins older than 1 month
|
|
162
|
-
function cleanupOldPins() {
|
|
163
|
-
const oneMonthAgo = new Date();
|
|
164
|
-
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
|
165
|
-
|
|
166
|
-
// Iterate through all tokens
|
|
167
|
-
Object.keys(pinsData).forEach(token => {
|
|
168
|
-
// Filter out pins older than 1 month
|
|
169
|
-
const initialCount = pinsData[token].length;
|
|
170
|
-
pinsData[token] = pinsData[token].filter(pin => {
|
|
171
|
-
const storedDate = new Date(pin._stored);
|
|
172
|
-
return storedDate >= oneMonthAgo;
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Log if pins were removed
|
|
176
|
-
if (pinsData[token].length < initialCount) {
|
|
177
|
-
node.debug(`Removed ${initialCount - pinsData[token].length} old pins for token ${token.substring(0, 8)}...`);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
187
|
node.on('close', function() {
|
|
183
188
|
// Clean up any resources
|
|
184
189
|
});
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
label: function () {
|
|
27
27
|
return this.name || "List Timeline Pins";
|
|
28
28
|
},
|
|
29
|
+
paletteLabel: "List Timeline Pins",
|
|
29
30
|
oneditprepare: function () {
|
|
30
31
|
// Setup TypedInput for server override options
|
|
31
32
|
$("#node-input-apiUrl").typedInput({
|
|
@@ -107,7 +108,7 @@
|
|
|
107
108
|
</script>
|
|
108
109
|
|
|
109
110
|
<script type="text/html" data-help-name="pebble-timeline-list">
|
|
110
|
-
<p>Lists pins that
|
|
111
|
+
<p>Lists pins from the local storage file that tracks pins added via Node-RED.</p>
|
|
111
112
|
|
|
112
113
|
<h3>Inputs</h3>
|
|
113
114
|
<dl class="message-properties">
|
|
@@ -128,9 +129,13 @@
|
|
|
128
129
|
</dl>
|
|
129
130
|
|
|
130
131
|
<h3>Details</h3>
|
|
131
|
-
<p>This node
|
|
132
|
-
|
|
133
|
-
<p>
|
|
132
|
+
<p><strong>Important:</strong> This node does <strong>not</strong> fetch pins from the remote Pebble Timeline server.
|
|
133
|
+
Instead, it reads from a local file (<code>timeline-pins.json</code>) stored in the Node-RED user directory.</p>
|
|
134
|
+
<p>The local file is automatically updated whenever you use the <strong>pebble-timeline-add</strong> or
|
|
135
|
+
<strong>pebble-timeline-delete</strong> nodes. This provides a local record of pins that have been
|
|
136
|
+
added or removed via Node-RED.</p>
|
|
137
|
+
<p>Pins are organized by timeline token in the storage file. Only pins associated with the current timeline
|
|
138
|
+
token will be listed. Each token maintains its own separate list of pins.</p>
|
|
134
139
|
<p>You can filter the pins by start and end times. The times should be in ISO date-time format (e.g.,
|
|
135
140
|
2023-01-01T12:00:00Z).</p>
|
|
136
141
|
<p>By default, if no filters are specified, all pins for the current token will be returned.</p>
|
|
@@ -138,9 +143,10 @@
|
|
|
138
143
|
|
|
139
144
|
<h4>Example Use Cases</h4>
|
|
140
145
|
<ul>
|
|
141
|
-
<li>List all upcoming events for the day</li>
|
|
146
|
+
<li>List all upcoming events for the day that were added via Node-RED</li>
|
|
142
147
|
<li>Filter pins for a specific date range for reporting</li>
|
|
143
148
|
<li>Show pins for a specific event type based on time criteria</li>
|
|
149
|
+
<li>Track what pins have been sent to the Pebble Timeline</li>
|
|
144
150
|
</ul>
|
|
145
151
|
|
|
146
152
|
<h3>References</h3>
|
package/pebble-timeline-list.js
CHANGED
|
@@ -105,11 +105,20 @@ module.exports = function(RED) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// Get the timeline token to use
|
|
108
|
-
|
|
108
|
+
let timelineToken = tokenOverride || configNode.credentials.timelineToken;
|
|
109
|
+
|
|
110
|
+
// Ensure we have a valid token
|
|
111
|
+
if (!timelineToken) {
|
|
112
|
+
node.warn("No valid timeline token provided");
|
|
113
|
+
timelineToken = "default"; // Use a default key to avoid errors
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Convert token to string to ensure it can be used as an object key
|
|
117
|
+
timelineToken = String(timelineToken);
|
|
109
118
|
|
|
110
119
|
// Get pins for this token only
|
|
111
120
|
let pins = [];
|
|
112
|
-
if (pinsData[timelineToken]) {
|
|
121
|
+
if (pinsData[timelineToken] && Array.isArray(pinsData[timelineToken])) {
|
|
113
122
|
pins = pinsData[timelineToken];
|
|
114
123
|
}
|
|
115
124
|
|
|
@@ -134,8 +143,7 @@ module.exports = function(RED) {
|
|
|
134
143
|
return include;
|
|
135
144
|
});
|
|
136
145
|
|
|
137
|
-
//
|
|
138
|
-
cleanupOldPins(pinsData);
|
|
146
|
+
// Note: Cleanup of old pins is handled in the add node
|
|
139
147
|
|
|
140
148
|
// Create output message
|
|
141
149
|
msg.payload = filteredPins;
|
|
@@ -152,38 +160,6 @@ module.exports = function(RED) {
|
|
|
152
160
|
});
|
|
153
161
|
});
|
|
154
162
|
|
|
155
|
-
// Helper to clean up pins older than 1 month
|
|
156
|
-
function cleanupOldPins(pinsData) {
|
|
157
|
-
const oneMonthAgo = new Date();
|
|
158
|
-
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
|
159
|
-
let changed = false;
|
|
160
|
-
|
|
161
|
-
// Iterate through all tokens
|
|
162
|
-
Object.keys(pinsData).forEach(token => {
|
|
163
|
-
// Filter out pins older than 1 month
|
|
164
|
-
const initialCount = pinsData[token].length;
|
|
165
|
-
pinsData[token] = pinsData[token].filter(pin => {
|
|
166
|
-
const storedDate = new Date(pin._stored);
|
|
167
|
-
return storedDate >= oneMonthAgo;
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// Log if pins were removed
|
|
171
|
-
if (pinsData[token].length < initialCount) {
|
|
172
|
-
node.debug(`Removed ${initialCount - pinsData[token].length} old pins for token ${token.substring(0, 8)}...`);
|
|
173
|
-
changed = true;
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Save the updated pins data if any pins were removed
|
|
178
|
-
if (changed) {
|
|
179
|
-
try {
|
|
180
|
-
fs.writeFileSync(pinsFile, JSON.stringify(pinsData, null, 2));
|
|
181
|
-
} catch (error) {
|
|
182
|
-
node.warn(`Error saving pins to file: ${error.message}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
163
|
node.on('close', function() {
|
|
188
164
|
// Clean up any resources
|
|
189
165
|
});
|