@skylord123/node-red-pebble-timeline 1.0.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/LICENSE +21 -0
- package/README.md +73 -0
- package/examples/README.md +7 -0
- package/examples/example.json +598 -0
- package/examples/example.png +0 -0
- package/package.json +25 -0
- package/pebble-timeline-add.html +926 -0
- package/pebble-timeline-add.js +744 -0
- package/pebble-timeline-config.html +63 -0
- package/pebble-timeline-config.js +13 -0
- package/pebble-timeline-delete.html +122 -0
- package/pebble-timeline-delete.js +190 -0
- package/pebble-timeline-list.html +151 -0
- package/pebble-timeline-list.js +195 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
module.exports = function(RED) {
|
|
5
|
+
function PebbleTimelineListNode(config) {
|
|
6
|
+
RED.nodes.createNode(this, config);
|
|
7
|
+
const node = this;
|
|
8
|
+
|
|
9
|
+
// Get the config node
|
|
10
|
+
const configNode = RED.nodes.getNode(config.config);
|
|
11
|
+
if (!configNode) {
|
|
12
|
+
node.error("No Pebble Timeline configuration found");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Make sure storage directory exists
|
|
17
|
+
const storageDir = path.join(RED.settings.userDir, 'pebble-timeline');
|
|
18
|
+
fs.ensureDirSync(storageDir);
|
|
19
|
+
const pinsFile = path.join(storageDir, 'timeline-pins.json');
|
|
20
|
+
|
|
21
|
+
node.on('input', function(msg, send, done) {
|
|
22
|
+
// Backwards compatibility with Node-RED 0.x
|
|
23
|
+
send = send || function() { node.send.apply(node, arguments) };
|
|
24
|
+
|
|
25
|
+
let startTime = null;
|
|
26
|
+
let endTime = null;
|
|
27
|
+
let apiUrlOverride = null;
|
|
28
|
+
let tokenOverride = null;
|
|
29
|
+
|
|
30
|
+
// Process filter parameters in sequence
|
|
31
|
+
Promise.resolve()
|
|
32
|
+
.then(() => {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
if (config.apiUrl) {
|
|
35
|
+
RED.util.evaluateNodeProperty(config.apiUrl, config.apiUrlType, node, msg, (err, result) => {
|
|
36
|
+
if (!err && result) {
|
|
37
|
+
apiUrlOverride = result;
|
|
38
|
+
}
|
|
39
|
+
resolve();
|
|
40
|
+
});
|
|
41
|
+
} else {
|
|
42
|
+
resolve();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
})
|
|
46
|
+
.then(() => {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
if (config.token) {
|
|
49
|
+
RED.util.evaluateNodeProperty(config.token, config.tokenType, node, msg, (err, result) => {
|
|
50
|
+
if (!err && result) {
|
|
51
|
+
tokenOverride = result;
|
|
52
|
+
}
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
resolve();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
})
|
|
60
|
+
.then(() => {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
if (config.startTime) {
|
|
63
|
+
RED.util.evaluateNodeProperty(config.startTime, config.startTimeType, node, msg, (err, result) => {
|
|
64
|
+
if (!err && result) {
|
|
65
|
+
startTime = new Date(result);
|
|
66
|
+
if (isNaN(startTime.getTime())) {
|
|
67
|
+
node.warn("Invalid start time format");
|
|
68
|
+
startTime = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
})
|
|
78
|
+
.then(() => {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
if (config.endTime) {
|
|
81
|
+
RED.util.evaluateNodeProperty(config.endTime, config.endTimeType, node, msg, (err, result) => {
|
|
82
|
+
if (!err && result) {
|
|
83
|
+
endTime = new Date(result);
|
|
84
|
+
if (isNaN(endTime.getTime())) {
|
|
85
|
+
node.warn("Invalid end time format");
|
|
86
|
+
endTime = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
resolve();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
})
|
|
96
|
+
.then(() => {
|
|
97
|
+
// Load the pins
|
|
98
|
+
let pinsData = {};
|
|
99
|
+
try {
|
|
100
|
+
if (fs.existsSync(pinsFile)) {
|
|
101
|
+
pinsData = JSON.parse(fs.readFileSync(pinsFile, 'utf8'));
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
node.warn(`Error loading pins file: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get the timeline token to use
|
|
108
|
+
const timelineToken = tokenOverride || configNode.credentials.timelineToken;
|
|
109
|
+
|
|
110
|
+
// Get pins for this token only
|
|
111
|
+
let pins = [];
|
|
112
|
+
if (pinsData[timelineToken]) {
|
|
113
|
+
pins = pinsData[timelineToken];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Apply filters
|
|
117
|
+
const filteredPins = pins.filter(pin => {
|
|
118
|
+
let include = true;
|
|
119
|
+
|
|
120
|
+
if (startTime !== null) {
|
|
121
|
+
const pinTime = new Date(pin.time);
|
|
122
|
+
if (pinTime < startTime) {
|
|
123
|
+
include = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (endTime !== null) {
|
|
128
|
+
const pinTime = new Date(pin.time);
|
|
129
|
+
if (pinTime > endTime) {
|
|
130
|
+
include = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return include;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Clean up old pins (older than 1 month) from all tokens
|
|
138
|
+
cleanupOldPins(pinsData);
|
|
139
|
+
|
|
140
|
+
// Create output message
|
|
141
|
+
msg.payload = filteredPins;
|
|
142
|
+
msg.count = filteredPins.length;
|
|
143
|
+
|
|
144
|
+
node.status({fill: "green", shape: "dot", text: `${filteredPins.length} pins found`});
|
|
145
|
+
|
|
146
|
+
send(msg);
|
|
147
|
+
if (done) done();
|
|
148
|
+
})
|
|
149
|
+
.catch(error => {
|
|
150
|
+
node.error(`Error listing pins: ${error.message}`, msg);
|
|
151
|
+
if (done) done(error);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
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
|
+
node.on('close', function() {
|
|
188
|
+
// Clean up any resources
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
RED.nodes.registerType("pebble-timeline-list", PebbleTimelineListNode, {
|
|
193
|
+
credentials: {}
|
|
194
|
+
});
|
|
195
|
+
};
|