@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.
@@ -0,0 +1,63 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('pebble-timeline-config', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: "" },
6
+ apiUrl: { value: "https://timeline-api.rebble.io", required: true }
7
+ },
8
+ credentials: {
9
+ timelineToken: { type: "password" }
10
+ },
11
+ label: function() {
12
+ return this.name || "Pebble Timeline Config";
13
+ }
14
+ });
15
+ </script>
16
+
17
+ <script type="text/html" data-template-name="pebble-timeline-config">
18
+ <div class="form-row">
19
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
20
+ <input type="text" id="node-config-input-name" placeholder="Name">
21
+ <div class="form-tips">Optional name to identify this configuration in the flow</div>
22
+ </div>
23
+ <div class="form-row">
24
+ <label for="node-config-input-apiUrl"><i class="fa fa-globe"></i> API URL</label>
25
+ <input type="text" id="node-config-input-apiUrl" placeholder="https://timeline-api.rebble.io">
26
+ <div class="form-tips">The URL of the Pebble Timeline API. The default URL (https://timeline-api.rebble.io) is
27
+ provided by the Rebble service which maintains Pebble functionality after official support ended.
28
+ </div>
29
+ </div>
30
+ <div class="form-row">
31
+ <label for="node-config-input-timelineToken"><i class="fa fa-key"></i> Timeline Token</label>
32
+ <input type="password" id="node-config-input-timelineToken">
33
+ <div class="form-tips">Your timeline token for authentication with the API. This token is used to authenticate
34
+ your app with the Pebble Timeline service.
35
+ </div>
36
+ </div>
37
+ </script>
38
+
39
+ <script type="text/html" data-help-name="pebble-timeline-config">
40
+ <p>Configuration for connecting to the Pebble Timeline API.</p>
41
+
42
+ <h3>Details</h3>
43
+ <dl class="message-properties">
44
+ <dt>Name <span class="property-type">string</span></dt>
45
+ <dd>Optional name for this configuration. Used to identify this configuration in your flows.</dd>
46
+
47
+ <dt>API URL <span class="property-type">string</span></dt>
48
+ <dd>The URL of the Pebble Timeline API. Defaults to https://timeline-api.rebble.io, which is the Rebble service
49
+ that maintains Pebble functionality.
50
+ </dd>
51
+
52
+ <dt>Timeline Token <span class="property-type">string</span></dt>
53
+ <dd>Your timeline token for authentication with the API. This is used to authenticate your app with the timeline
54
+ service. Each token has its own separate list of pins, and pins are automatically cleaned up after one month.
55
+ </dd>
56
+ </dl>
57
+
58
+ <h3>References</h3>
59
+ <ul>
60
+ <li><a href="https://developer.pebble.com/guides/pebble-timeline/">Pebble Timeline Developer Guide</a></li>
61
+ <li><a href="https://rebble.io">Rebble.io - The community maintaining Pebble services</a></li>
62
+ </ul>
63
+ </script>
@@ -0,0 +1,13 @@
1
+ module.exports = function(RED) {
2
+ function PebbleTimelineConfigNode(n) {
3
+ RED.nodes.createNode(this, n);
4
+ this.name = n.name;
5
+ this.apiUrl = n.apiUrl;
6
+ }
7
+
8
+ RED.nodes.registerType("pebble-timeline-config", PebbleTimelineConfigNode, {
9
+ credentials: {
10
+ timelineToken: { type: "password" }
11
+ }
12
+ });
13
+ };
@@ -0,0 +1,122 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('pebble-timeline-delete', {
3
+ category: 'Pebble',
4
+ color: '#1da1f2',
5
+ defaults: {
6
+ name: {value: ""},
7
+ config: {type: "pebble-timeline-config", required: true},
8
+
9
+ // Server override options
10
+ apiUrl: {value: "null"},
11
+ apiUrlType: {value: "jsonata"},
12
+
13
+ token: {value: "null"},
14
+ tokenType: {value: "jsonata"},
15
+
16
+ // Pin ID property
17
+ pinId: {value: "payload.id"},
18
+ pinIdType: {value: "msg"}
19
+ },
20
+ inputs: 1,
21
+ outputs: 1,
22
+ icon: "font-awesome/fa-trash-o",
23
+ label: function () {
24
+ return this.name || "Delete Timeline Pin";
25
+ },
26
+ oneditprepare: function () {
27
+ // Setup TypedInput for server override options
28
+ $("#node-input-apiUrl").typedInput({
29
+ types: ["msg", "flow", "global", "str", "jsonata"],
30
+ typeField: "#node-input-apiUrlType"
31
+ });
32
+
33
+ $("#node-input-token").typedInput({
34
+ types: ["msg", "flow", "global", "str", "jsonata"],
35
+ typeField: "#node-input-tokenType"
36
+ });
37
+
38
+ // Setup TypedInput for pinId
39
+ $("#node-input-pinId").typedInput({
40
+ types: ["msg", "flow", "global", "str", "jsonata"],
41
+ typeField: "#node-input-pinIdType"
42
+ });
43
+ }
44
+ });
45
+ </script>
46
+
47
+ <script type="text/html" data-template-name="pebble-timeline-delete">
48
+ <div class="form-row">
49
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
50
+ <input type="text" id="node-input-name" placeholder="Name">
51
+ </div>
52
+
53
+ <div class="form-row">
54
+ <label for="node-input-config"><i class="fa fa-cog"></i> Config</label>
55
+ <input type="text" id="node-input-config">
56
+ </div>
57
+
58
+ <div class="form-section">
59
+ <div class="form-section-title">Server Overrides (Optional)</div>
60
+
61
+ <div class="form-row">
62
+ <label for="node-input-apiUrl"><i class="fa fa-globe"></i> API URL</label>
63
+ <input type="text" id="node-input-apiUrl" style="width: 70%">
64
+ <input type="hidden" id="node-input-apiUrlType">
65
+ <div class="form-tips">Override the API URL configured in the config node. Defaults to
66
+ https://timeline-api.rebble.io
67
+ </div>
68
+ </div>
69
+
70
+ <div class="form-row">
71
+ <label for="node-input-token"><i class="fa fa-key"></i> Token</label>
72
+ <input type="text" id="node-input-token" style="width: 70%">
73
+ <input type="hidden" id="node-input-tokenType">
74
+ <div class="form-tips">Override the timeline token configured in the config node</div>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="form-row">
79
+ <label for="node-input-pinId"><i class="fa fa-id-card"></i> Pin ID</label>
80
+ <input type="text" id="node-input-pinId" style="width: 70%">
81
+ <input type="hidden" id="node-input-pinIdType">
82
+ <div class="form-tips">The ID of the pin to delete from the timeline. This must match exactly the ID that was
83
+ used when the pin was created.
84
+ </div>
85
+ </div>
86
+ </script>
87
+
88
+ <script type="text/html" data-help-name="pebble-timeline-delete">
89
+ <p>Deletes a pin from the Pebble Timeline.</p>
90
+
91
+ <h3>Inputs</h3>
92
+ <dl class="message-properties">
93
+ <dt>payload.id <span class="property-type">string</span></dt>
94
+ <dd>The ID of the pin to delete. This can be overridden by the node's configuration.</dd>
95
+ <dt>payload <span class="property-type">string</span></dt>
96
+ <dd>If payload is a string, it will be treated as the pin ID.</dd>
97
+ </dl>
98
+
99
+ <h3>Outputs</h3>
100
+ <dl class="message-properties">
101
+ <dt>payload <span class="property-type">object</span></dt>
102
+ <dd>The result of the API call, including success status and any error information.</dd>
103
+ <dt>payload.success <span class="property-type">boolean</span></dt>
104
+ <dd>Whether the delete operation was successful.</dd>
105
+ <dt>payload.pinId <span class="property-type">string</span></dt>
106
+ <dd>The ID of the pin that was deleted or attempted to be deleted.</dd>
107
+ </dl>
108
+
109
+ <h3>Details</h3>
110
+ <p>This node deletes a pin from the Pebble Timeline. You need to provide the ID of the pin to delete.</p>
111
+ <p>You can configure the pin ID directly in the node or provide it in the input message. If both are provided, the
112
+ node's configuration takes precedence.</p>
113
+ <p>The pin will be deleted from both the Pebble Timeline service and the local storage for the current timeline token.</p>
114
+ <p>When deleting pins, the system also automatically cleans up any pins older than one month from all tokens to prevent the storage file from growing too large.</p>
115
+ <p>Note that once a pin ID has been deleted, it cannot be reused for future pins.</p>
116
+
117
+ <h3>References</h3>
118
+ <ul>
119
+ <li><a href="https://developer.pebble.com/guides/pebble-timeline/pin-structure/">Pebble Timeline Pin
120
+ Structure</a></li>
121
+ </ul>
122
+ </script>
@@ -0,0 +1,190 @@
1
+ const axios = require('axios');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+
5
+ module.exports = function(RED) {
6
+ function PebbleTimelineDeleteNode(config) {
7
+ RED.nodes.createNode(this, config);
8
+ const node = this;
9
+
10
+ // Get the config node
11
+ const configNode = RED.nodes.getNode(config.config);
12
+ if (!configNode) {
13
+ node.error("No Pebble Timeline configuration found");
14
+ return;
15
+ }
16
+
17
+ // Make sure storage directory exists
18
+ const storageDir = path.join(RED.settings.userDir, 'pebble-timeline');
19
+ fs.ensureDirSync(storageDir);
20
+ const pinsFile = path.join(storageDir, 'timeline-pins.json');
21
+
22
+ // Load existing pins (organized by token)
23
+ let pinsData = {};
24
+ try {
25
+ if (fs.existsSync(pinsFile)) {
26
+ pinsData = JSON.parse(fs.readFileSync(pinsFile, 'utf8'));
27
+ }
28
+ } catch (error) {
29
+ node.warn(`Error loading pins file: ${error.message}`);
30
+ }
31
+
32
+ node.on('input', function(msg, send, done) {
33
+ // Backwards compatibility with Node-RED 0.x
34
+ send = send || function() { node.send.apply(node, arguments) };
35
+
36
+ // Get the pin ID to delete
37
+ let pinId;
38
+ // Process parameters in sequence
39
+ Promise.all([
40
+ new Promise(resolve => {
41
+ RED.util.evaluateNodeProperty(config.pinId, config.pinIdType, node, msg, (err, result) => {
42
+ if (err) {
43
+ node.error(`Error evaluating pin ID: ${err.message}`, msg);
44
+ if (done) done(err);
45
+ return;
46
+ }
47
+
48
+ pinId = result;
49
+
50
+ if (!pinId) {
51
+ node.error("Pin ID is required", msg);
52
+ if (done) done("Pin ID is required");
53
+ return;
54
+ }
55
+ resolve();
56
+ });
57
+ }),
58
+
59
+ // Check for server override options
60
+ new Promise(resolve => {
61
+ if (config.apiUrl) {
62
+ RED.util.evaluateNodeProperty(config.apiUrl, config.apiUrlType, node, msg, (err, result) => {
63
+ if (!err && result) {
64
+ node.apiUrlOverride = result;
65
+ }
66
+ resolve();
67
+ });
68
+ } else {
69
+ resolve();
70
+ }
71
+ }),
72
+
73
+ new Promise(resolve => {
74
+ if (config.token) {
75
+ RED.util.evaluateNodeProperty(config.token, config.tokenType, node, msg, (err, result) => {
76
+ if (!err && result) {
77
+ node.tokenOverride = result;
78
+ }
79
+ resolve();
80
+ });
81
+ } else {
82
+ resolve();
83
+ }
84
+ })
85
+ ]).then(() => {
86
+ // Use overrides if provided, otherwise use config node values
87
+ const apiUrl = `${node.apiUrlOverride || configNode.apiUrl}/v1/user/pins/${pinId}`;
88
+ const timelineToken = node.tokenOverride || configNode.credentials.timelineToken;
89
+
90
+ if (!timelineToken) {
91
+ node.error("Timeline token is required", msg);
92
+ if (done) done("Timeline token is required");
93
+ return;
94
+ }
95
+
96
+ axios.delete(apiUrl, {
97
+ headers: {
98
+ 'X-User-Token': timelineToken
99
+ }
100
+ })
101
+ .then(response => {
102
+ node.status({fill: "green", shape: "dot", text: "Pin deleted"});
103
+
104
+ // Remove the pin from our local storage
105
+ removePin(pinId);
106
+
107
+ // Prepare the output message
108
+ msg.payload = {
109
+ success: true,
110
+ pinId: pinId,
111
+ response: response.data
112
+ };
113
+
114
+ send(msg);
115
+ if (done) done();
116
+ })
117
+ .catch(error => {
118
+ node.status({fill: "red", shape: "dot", text: "Error: " + (error.response ? error.response.status : error.message)});
119
+
120
+ msg.payload = {
121
+ success: false,
122
+ pinId: pinId,
123
+ error: error.message,
124
+ response: error.response ? error.response.data : null
125
+ };
126
+
127
+ send(msg);
128
+ if (done) done(error);
129
+ });
130
+ }).catch(err => {
131
+ if (done) done(err);
132
+ });
133
+ });
134
+
135
+ // Helper to remove a pin from local storage
136
+ function removePin(pinId) {
137
+ const timelineToken = node.tokenOverride || configNode.credentials.timelineToken;
138
+
139
+ // Check if this token has any pins
140
+ if (!pinsData[timelineToken]) {
141
+ return; // No pins for this token
142
+ }
143
+
144
+ // Remove the pin with the specified ID from this token's pins
145
+ const initialCount = pinsData[timelineToken].length;
146
+ pinsData[timelineToken] = pinsData[timelineToken].filter(p => p.id !== pinId);
147
+
148
+ // Clean up old pins (older than 1 month) from all tokens
149
+ cleanupOldPins();
150
+
151
+ // Only write if we actually removed something
152
+ if (pinsData[timelineToken].length !== initialCount) {
153
+ try {
154
+ fs.writeFileSync(pinsFile, JSON.stringify(pinsData, null, 2));
155
+ } catch (error) {
156
+ node.warn(`Error saving pins to file: ${error.message}`);
157
+ }
158
+ }
159
+ }
160
+
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
+ node.on('close', function() {
183
+ // Clean up any resources
184
+ });
185
+ }
186
+
187
+ RED.nodes.registerType("pebble-timeline-delete", PebbleTimelineDeleteNode, {
188
+ credentials: {}
189
+ });
190
+ };
@@ -0,0 +1,151 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('pebble-timeline-list', {
3
+ category: 'Pebble',
4
+ color: '#1da1f2',
5
+ defaults: {
6
+ name: {value: ""},
7
+ config: {type: "pebble-timeline-config", required: true},
8
+
9
+ // Server override options
10
+ apiUrl: {value: "null"},
11
+ apiUrlType: {value: "jsonata"},
12
+
13
+ token: {value: "null"},
14
+ tokenType: {value: "jsonata"},
15
+
16
+ // Filter properties
17
+ startTime: {value: "payload.startTime"},
18
+ startTimeType: {value: "msg"},
19
+
20
+ endTime: {value: "payload.endTime"},
21
+ endTimeType: {value: "msg"}
22
+ },
23
+ inputs: 1,
24
+ outputs: 1,
25
+ icon: "font-awesome/fa-list",
26
+ label: function () {
27
+ return this.name || "List Timeline Pins";
28
+ },
29
+ oneditprepare: function () {
30
+ // Setup TypedInput for server override options
31
+ $("#node-input-apiUrl").typedInput({
32
+ types: ["msg", "flow", "global", "str", "jsonata"],
33
+ typeField: "#node-input-apiUrlType"
34
+ });
35
+
36
+ $("#node-input-token").typedInput({
37
+ types: ["msg", "flow", "global", "str", "jsonata"],
38
+ typeField: "#node-input-tokenType"
39
+ });
40
+
41
+ // Setup TypedInput for filter fields
42
+ $("#node-input-startTime").typedInput({
43
+ types: ["msg", "flow", "global", "str", "date", "jsonata"],
44
+ typeField: "#node-input-startTimeType"
45
+ });
46
+
47
+ $("#node-input-endTime").typedInput({
48
+ types: ["msg", "flow", "global", "str", "date", "jsonata"],
49
+ typeField: "#node-input-endTimeType"
50
+ });
51
+ }
52
+ });
53
+ </script>
54
+
55
+ <script type="text/html" data-template-name="pebble-timeline-list">
56
+ <div class="form-row">
57
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
58
+ <input type="text" id="node-input-name" placeholder="Name">
59
+ </div>
60
+
61
+ <div class="form-row">
62
+ <label for="node-input-config"><i class="fa fa-cog"></i> Config</label>
63
+ <input type="text" id="node-input-config">
64
+ </div>
65
+
66
+ <div class="form-section">
67
+ <div class="form-section-title">Server Overrides (Optional)</div>
68
+
69
+ <div class="form-row">
70
+ <label for="node-input-apiUrl"><i class="fa fa-globe"></i> API URL</label>
71
+ <input type="text" id="node-input-apiUrl" style="width: 70%">
72
+ <input type="hidden" id="node-input-apiUrlType">
73
+ <div class="form-tips">Override the API URL configured in the config node. Defaults to
74
+ https://timeline-api.rebble.io
75
+ </div>
76
+ </div>
77
+
78
+ <div class="form-row">
79
+ <label for="node-input-token"><i class="fa fa-key"></i> Token</label>
80
+ <input type="text" id="node-input-token" style="width: 70%">
81
+ <input type="hidden" id="node-input-tokenType">
82
+ <div class="form-tips">Override the timeline token configured in the config node</div>
83
+ </div>
84
+ </div>
85
+
86
+ <div class="form-section">
87
+ <div class="form-section-title">Filter Options</div>
88
+
89
+ <div class="form-row">
90
+ <label for="node-input-startTime"><i class="fa fa-calendar"></i> Start Time</label>
91
+ <input type="text" id="node-input-startTime" style="width: 70%">
92
+ <input type="hidden" id="node-input-startTimeType">
93
+ <div class="form-tips">Filter pins with a time greater than or equal to this value. Use ISO date-time format
94
+ (e.g., 2023-01-01T00:00:00Z).
95
+ </div>
96
+ </div>
97
+
98
+ <div class="form-row">
99
+ <label for="node-input-endTime"><i class="fa fa-calendar"></i> End Time</label>
100
+ <input type="text" id="node-input-endTime" style="width: 70%">
101
+ <input type="hidden" id="node-input-endTimeType">
102
+ <div class="form-tips">Filter pins with a time less than or equal to this value. Use ISO date-time format
103
+ (e.g., 2023-01-31T23:59:59Z).
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </script>
108
+
109
+ <script type="text/html" data-help-name="pebble-timeline-list">
110
+ <p>Lists pins that have been added to the Pebble Timeline.</p>
111
+
112
+ <h3>Inputs</h3>
113
+ <dl class="message-properties">
114
+ <dt class="optional">payload <span class="property-type">object</span></dt>
115
+ <dd>Optional filter criteria for the pins.</dd>
116
+ <dt class="optional">payload.startTime <span class="property-type">string | date</span></dt>
117
+ <dd>Filter pins with a time greater than or equal to this value. Use ISO date-time format.</dd>
118
+ <dt class="optional">payload.endTime <span class="property-type">string | date</span></dt>
119
+ <dd>Filter pins with a time less than or equal to this value. Use ISO date-time format.</dd>
120
+ </dl>
121
+
122
+ <h3>Outputs</h3>
123
+ <dl class="message-properties">
124
+ <dt>payload <span class="property-type">array</span></dt>
125
+ <dd>An array of pins that match the filter criteria.</dd>
126
+ <dt>count <span class="property-type">number</span></dt>
127
+ <dd>The number of pins that match the filter criteria.</dd>
128
+ </dl>
129
+
130
+ <h3>Details</h3>
131
+ <p>This node lists pins that have been successfully added to the Pebble Timeline. The pins are stored locally in the
132
+ Node-RED installation directory, organized by timeline token.</p>
133
+ <p>Only pins associated with the current timeline token will be listed. Each token maintains its own separate list of pins.</p>
134
+ <p>You can filter the pins by start and end times. The times should be in ISO date-time format (e.g.,
135
+ 2023-01-01T12:00:00Z).</p>
136
+ <p>By default, if no filters are specified, all pins for the current token will be returned.</p>
137
+ <p>Pins older than one month are automatically cleaned up when listing pins to prevent the storage file from growing too large.</p>
138
+
139
+ <h4>Example Use Cases</h4>
140
+ <ul>
141
+ <li>List all upcoming events for the day</li>
142
+ <li>Filter pins for a specific date range for reporting</li>
143
+ <li>Show pins for a specific event type based on time criteria</li>
144
+ </ul>
145
+
146
+ <h3>References</h3>
147
+ <ul>
148
+ <li><a href="https://developer.pebble.com/guides/pebble-timeline/pin-structure/">Pebble Timeline Pin
149
+ Structure</a></li>
150
+ </ul>
151
+ </script>