node-red-contrib-tractive 0.1.0 → 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/README.md +95 -21
- package/index.js +116 -1
- package/nodes/tractive-config.cjs +25 -0
- package/nodes/tractive-config.html +25 -0
- package/nodes/tractive-tracker.cjs +74 -0
- package/nodes/tractive-tracker.html +80 -0
- package/package.json +28 -8
- package/src/account.js +60 -0
- package/src/commands.js +76 -0
- package/src/pet.js +32 -0
- package/src/tracker.js +93 -0
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,38 +1,112 @@
|
|
|
1
1
|
# node-red-contrib-tractive
|
|
2
2
|
|
|
3
|
-
A Node-RED palette for
|
|
3
|
+
A Node-RED palette for the [Tractive](https://tractive.com) GPS tracker API. Monitor your pet's battery level, location, and history — and control LED and buzzer — directly from your flows.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
Install via the Node-RED Palette Manager (search for `node-red-contrib-tractive`), or from the command line in your Node-RED data directory:
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
|
-
cd ~/.node-red
|
|
9
10
|
npm install node-red-contrib-tractive
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
Or install directly from the Node-RED palette manager by searching for `node-red-contrib-tractive`.
|
|
13
|
-
|
|
14
13
|
## Nodes
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
|
21
|
-
|
|
22
|
-
|
|
|
23
|
-
|
|
|
15
|
+
### tractive-config
|
|
16
|
+
|
|
17
|
+
A configuration node that stores your Tractive account credentials. One config node per account — shared across all `tractive-tracker` nodes in your flow.
|
|
18
|
+
|
|
19
|
+
| Field | Description |
|
|
20
|
+
|----------|-------------------------------------|
|
|
21
|
+
| Email | Your Tractive account email |
|
|
22
|
+
| Password | Your Tractive account password |
|
|
23
|
+
|
|
24
|
+
Authentication is performed automatically when the first operation runs.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
### tractive-tracker
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
Calls the Tractive API for a specific tracker. Configure the **Tracker ID** once (find it in the Tractive app under **Settings → Device → Device ID**) and select the operation.
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
2. Connect any other Tractive node to the config node.
|
|
29
|
-
3. Pass a `trackerID` in `msg.payload` or configure it directly on the node.
|
|
30
|
-
4. Results are returned in `msg.payload`.
|
|
32
|
+
#### Operations
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
| Operation | Description |
|
|
35
|
+
|-------------------------|----------------------------------------------------------|
|
|
36
|
+
| Get hardware | Battery level and charging state |
|
|
37
|
+
| Get location | Latest GPS fix with coordinates and address |
|
|
38
|
+
| Get history | Location history (requires `msg.from` and `msg.to`) |
|
|
39
|
+
| Live tracking ON / OFF | Enable or disable 2.5-second position updates |
|
|
40
|
+
| LED ON / OFF | Trigger the tracker's LED light |
|
|
41
|
+
| Buzzer ON / OFF | Trigger the tracker's buzzer |
|
|
42
|
+
| Get pets | List all pets on the account |
|
|
43
|
+
| Get all trackers | List all trackers on the account |
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
#### Runtime overrides
|
|
46
|
+
|
|
47
|
+
| Property | Description |
|
|
48
|
+
|-----------------|------------------------------------------------|
|
|
49
|
+
| `msg.trackerID` | Overrides the Tracker ID set in the node |
|
|
50
|
+
| `msg.operation` | Overrides the operation set in the node |
|
|
51
|
+
| `msg.from` | Start timestamp in ms (Get history only) |
|
|
52
|
+
| `msg.to` | End timestamp in ms (Get history only) |
|
|
53
|
+
|
|
54
|
+
#### Output
|
|
55
|
+
|
|
56
|
+
`msg.payload` contains the raw API response object.
|
|
57
|
+
|
|
58
|
+
#### Example — battery monitor
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
[inject every 5 min] → [tractive-tracker: getTrackerHardware] → [msg.payload.battery_level] → [display]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## JavaScript library
|
|
65
|
+
|
|
66
|
+
The package also works as a standalone ESM library:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import tractive from 'node-red-contrib-tractive';
|
|
70
|
+
|
|
71
|
+
await tractive.connect('email@example.com', 'password');
|
|
72
|
+
|
|
73
|
+
const hw = await tractive.getTrackerHardware('YOUR_TRACKER_ID');
|
|
74
|
+
console.log(hw.battery_level); // e.g. 85
|
|
75
|
+
|
|
76
|
+
const location = await tractive.getTrackerLocation('YOUR_TRACKER_ID');
|
|
77
|
+
console.log(location.latlong); // e.g. [-33.86, 151.20]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### All functions
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
// Account
|
|
84
|
+
getAccountInfo()
|
|
85
|
+
getAccountSubscriptions()
|
|
86
|
+
getAccountSubscription(subscriptionID)
|
|
87
|
+
getAccountShares()
|
|
88
|
+
|
|
89
|
+
// Pet
|
|
90
|
+
getPets()
|
|
91
|
+
getPet(petID)
|
|
92
|
+
|
|
93
|
+
// Tracker
|
|
94
|
+
getAllTrackers()
|
|
95
|
+
getTracker(trackerID)
|
|
96
|
+
getTrackerLocation(trackerID)
|
|
97
|
+
getTrackerHardware(trackerID) // includes battery_level, charging_state
|
|
98
|
+
getTrackerHistory(trackerID, from, to) // from/to are timestamps in ms
|
|
99
|
+
|
|
100
|
+
// Commands
|
|
101
|
+
liveOn(trackerID)
|
|
102
|
+
liveOff(trackerID)
|
|
103
|
+
LEDOn(trackerID)
|
|
104
|
+
LEDOff(trackerID)
|
|
105
|
+
buzzerOn(trackerID)
|
|
106
|
+
buzzerOff(trackerID)
|
|
107
|
+
```
|
|
35
108
|
|
|
36
|
-
##
|
|
109
|
+
## Notes
|
|
37
110
|
|
|
38
|
-
|
|
111
|
+
- Rate-limited requests (HTTP 4006) are automatically retried up to 3 times with a 2-second delay.
|
|
112
|
+
- This package is not affiliated with or maintained by Tractive. Tractive is a registered trademark of Tractive GmbH.
|
package/index.js
CHANGED
|
@@ -1 +1,116 @@
|
|
|
1
|
-
|
|
1
|
+
import * as tAccount from './src/account.js';
|
|
2
|
+
import * as tPet from './src/pet.js';
|
|
3
|
+
import * as tTracker from './src/tracker.js';
|
|
4
|
+
import * as tCommands from './src/commands.js';
|
|
5
|
+
|
|
6
|
+
const TractiveClient = "6536c228870a3c8857d452e8";
|
|
7
|
+
|
|
8
|
+
globalThis.accountDetails = {
|
|
9
|
+
email: "",
|
|
10
|
+
password: ""
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
globalThis.gloOpts = {
|
|
14
|
+
method: "GET",
|
|
15
|
+
hostname: "graph.tractive.com",
|
|
16
|
+
path: ``,
|
|
17
|
+
headers: {
|
|
18
|
+
"X-Tractive-Client": TractiveClient,
|
|
19
|
+
"Authorization": `Bearer ${accountDetails.token}`,
|
|
20
|
+
"content-type": "application/json"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
globalThis.isAuthenticated = function() {
|
|
25
|
+
if(accountDetails?.token) return true;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function authenticate() {
|
|
30
|
+
const options = {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
'X-Tractive-Client': TractiveClient,
|
|
34
|
+
'Content-Type': "application/json"
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const url = `https://graph.tractive.com/4/auth/token?grant_type=tractive&platform_email=${encodeURIComponent(accountDetails.email)}&platform_token=${encodeURIComponent(accountDetails.password)}`;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(url, options);
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
accountDetails.token = data.access_token;
|
|
44
|
+
accountDetails.uid = data.user_id;
|
|
45
|
+
globalThis.gloOpts = {
|
|
46
|
+
method: "GET",
|
|
47
|
+
hostname: "graph.tractive.com",
|
|
48
|
+
path: ``,
|
|
49
|
+
headers: {
|
|
50
|
+
"X-Tractive-Client": TractiveClient,
|
|
51
|
+
"Authorization": `Bearer ${accountDetails.token}`,
|
|
52
|
+
"content-type": "application/json"
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return true;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function connect(email, password) {
|
|
62
|
+
accountDetails.email = email;
|
|
63
|
+
accountDetails.password = password;
|
|
64
|
+
await authenticate();
|
|
65
|
+
return isAuthenticated() ? true : false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function getTrackerGeofences(trackerID) {
|
|
69
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
70
|
+
const url = `https://graph.tractive.com/4/tracker/${trackerID}/geofences`;
|
|
71
|
+
const res = await fetch(url, {
|
|
72
|
+
method: gloOpts.method,
|
|
73
|
+
headers: gloOpts.headers
|
|
74
|
+
});
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function getGeofence(fenceID) {
|
|
80
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
81
|
+
const url = `https://graph.tractive.com/4/geofence/${fenceID}`;
|
|
82
|
+
const res = await fetch(url, {
|
|
83
|
+
method: gloOpts.method,
|
|
84
|
+
headers: gloOpts.headers
|
|
85
|
+
});
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default {
|
|
91
|
+
connect,
|
|
92
|
+
isAuthenticated,
|
|
93
|
+
getTrackerGeofences,
|
|
94
|
+
getGeofence,
|
|
95
|
+
// Account
|
|
96
|
+
getAccountInfo: tAccount.getAccountInfo,
|
|
97
|
+
getAccountSubscriptions: tAccount.getAccountSubscriptions,
|
|
98
|
+
getAccountSubscription: tAccount.getAccountSubscription,
|
|
99
|
+
getAccountShares: tAccount.getAccountShares,
|
|
100
|
+
// Pet
|
|
101
|
+
getPets: tPet.getPets,
|
|
102
|
+
getPet: tPet.getPet,
|
|
103
|
+
// Tracker
|
|
104
|
+
getAllTrackers: tTracker.getAllTrackers,
|
|
105
|
+
getTracker: tTracker.getTracker,
|
|
106
|
+
getTrackerHistory: tTracker.getTrackerHistory,
|
|
107
|
+
getTrackerLocation: tTracker.getTrackerLocation,
|
|
108
|
+
getTrackerHardware: tTracker.getTrackerHardware,
|
|
109
|
+
// Commands
|
|
110
|
+
liveOn: tCommands.liveOn,
|
|
111
|
+
liveOff: tCommands.liveOff,
|
|
112
|
+
LEDOn: tCommands.LEDOn,
|
|
113
|
+
LEDOff: tCommands.LEDOff,
|
|
114
|
+
buzzerOn: tCommands.BuzzerOn,
|
|
115
|
+
buzzerOff: tCommands.BuzzerOff
|
|
116
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
// Import the ESM tractive module once at load time
|
|
3
|
+
let tractivePromise = import('../index.js').then(m => m.default);
|
|
4
|
+
|
|
5
|
+
function TractiveConfigNode(config) {
|
|
6
|
+
RED.nodes.createNode(this, config);
|
|
7
|
+
const node = this;
|
|
8
|
+
node.email = config.email;
|
|
9
|
+
|
|
10
|
+
node.getClient = async function () {
|
|
11
|
+
const tractive = await tractivePromise;
|
|
12
|
+
if (!tractive.isAuthenticated()) {
|
|
13
|
+
const ok = await tractive.connect(node.email, node.credentials.password);
|
|
14
|
+
if (!ok) throw new Error('Tractive authentication failed');
|
|
15
|
+
}
|
|
16
|
+
return tractive;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
RED.nodes.registerType('tractive-config', TractiveConfigNode, {
|
|
21
|
+
credentials: {
|
|
22
|
+
password: { type: 'password' }
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('tractive-config', {
|
|
3
|
+
category: 'config',
|
|
4
|
+
defaults: {
|
|
5
|
+
email: { value: '', required: true }
|
|
6
|
+
},
|
|
7
|
+
credentials: {
|
|
8
|
+
password: { type: 'password' }
|
|
9
|
+
},
|
|
10
|
+
label: function () {
|
|
11
|
+
return this.email || 'Tractive Account';
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<script type="text/html" data-template-name="tractive-config">
|
|
17
|
+
<div class="form-row">
|
|
18
|
+
<label for="node-config-input-email"><i class="fa fa-envelope"></i> Email</label>
|
|
19
|
+
<input type="email" id="node-config-input-email" placeholder="your@email.com" style="width:70%">
|
|
20
|
+
</div>
|
|
21
|
+
<div class="form-row">
|
|
22
|
+
<label for="node-config-input-password"><i class="fa fa-lock"></i> Password</label>
|
|
23
|
+
<input type="password" id="node-config-input-password" style="width:70%">
|
|
24
|
+
</div>
|
|
25
|
+
</script>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
function TractiveTrackerNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
node.configNode = RED.nodes.getNode(config.config);
|
|
6
|
+
node.operation = config.operation;
|
|
7
|
+
node.trackerID = config.trackerID;
|
|
8
|
+
|
|
9
|
+
node.on('input', async function (msg) {
|
|
10
|
+
if (!node.configNode) {
|
|
11
|
+
node.error('No Tractive config selected', msg);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const operation = msg.operation || node.operation;
|
|
16
|
+
const trackerID = msg.trackerID || node.trackerID;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
node.status({ fill: 'blue', shape: 'dot', text: operation });
|
|
20
|
+
const tractive = await node.configNode.getClient();
|
|
21
|
+
|
|
22
|
+
let result;
|
|
23
|
+
switch (operation) {
|
|
24
|
+
case 'getAllTrackers':
|
|
25
|
+
result = await tractive.getAllTrackers();
|
|
26
|
+
break;
|
|
27
|
+
case 'getTrackerLocation':
|
|
28
|
+
result = await tractive.getTrackerLocation(trackerID);
|
|
29
|
+
break;
|
|
30
|
+
case 'getTrackerHardware':
|
|
31
|
+
result = await tractive.getTrackerHardware(trackerID);
|
|
32
|
+
break;
|
|
33
|
+
case 'getTrackerHistory':
|
|
34
|
+
result = await tractive.getTrackerHistory(trackerID, msg.from, msg.to);
|
|
35
|
+
break;
|
|
36
|
+
case 'getPets':
|
|
37
|
+
result = await tractive.getPets();
|
|
38
|
+
break;
|
|
39
|
+
case 'liveOn':
|
|
40
|
+
result = await tractive.liveOn(trackerID);
|
|
41
|
+
break;
|
|
42
|
+
case 'liveOff':
|
|
43
|
+
result = await tractive.liveOff(trackerID);
|
|
44
|
+
break;
|
|
45
|
+
case 'LEDOn':
|
|
46
|
+
result = await tractive.LEDOn(trackerID);
|
|
47
|
+
break;
|
|
48
|
+
case 'LEDOff':
|
|
49
|
+
result = await tractive.LEDOff(trackerID);
|
|
50
|
+
break;
|
|
51
|
+
case 'buzzerOn':
|
|
52
|
+
result = await tractive.buzzerOn(trackerID);
|
|
53
|
+
break;
|
|
54
|
+
case 'buzzerOff':
|
|
55
|
+
result = await tractive.buzzerOff(trackerID);
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
node.error(`Unknown operation: ${operation}`, msg);
|
|
59
|
+
node.status({ fill: 'red', shape: 'ring', text: 'unknown operation' });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
node.status({ fill: 'green', shape: 'dot', text: 'ok' });
|
|
64
|
+
msg.payload = result;
|
|
65
|
+
node.send(msg);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
node.status({ fill: 'red', shape: 'ring', text: err.message });
|
|
68
|
+
node.error(err, msg);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
RED.nodes.registerType('tractive-tracker', TractiveTrackerNode);
|
|
74
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
const TRACKER_OPS = [
|
|
3
|
+
'getTrackerLocation', 'getTrackerHardware', 'getTrackerHistory',
|
|
4
|
+
'liveOn', 'liveOff', 'LEDOn', 'LEDOff', 'buzzerOn', 'buzzerOff'
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
RED.nodes.registerType('tractive-tracker', {
|
|
8
|
+
category: 'Tractive',
|
|
9
|
+
color: '#6dbfde',
|
|
10
|
+
defaults: {
|
|
11
|
+
name: { value: '' },
|
|
12
|
+
config: { value: '', type: 'tractive-config', required: true },
|
|
13
|
+
operation: { value: 'getTrackerHardware', required: true },
|
|
14
|
+
trackerID: { value: '', validate: function(v) {
|
|
15
|
+
const op = document.getElementById('node-input-operation');
|
|
16
|
+
if (op && TRACKER_OPS.includes(op.value)) return v && v.trim().length > 0;
|
|
17
|
+
return true;
|
|
18
|
+
}}
|
|
19
|
+
},
|
|
20
|
+
inputs: 1,
|
|
21
|
+
outputs: 1,
|
|
22
|
+
icon: 'font-awesome/fa-paw',
|
|
23
|
+
label: function () {
|
|
24
|
+
return this.name || this.operation || 'tractive';
|
|
25
|
+
},
|
|
26
|
+
oneditprepare: function () {
|
|
27
|
+
function toggleTrackerID() {
|
|
28
|
+
const op = document.getElementById('node-input-operation').value;
|
|
29
|
+
const row = document.getElementById('row-trackerID');
|
|
30
|
+
row.style.display = TRACKER_OPS.includes(op) ? '' : 'none';
|
|
31
|
+
}
|
|
32
|
+
document.getElementById('node-input-operation').addEventListener('change', toggleTrackerID);
|
|
33
|
+
toggleTrackerID();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<script type="text/html" data-template-name="tractive-tracker">
|
|
39
|
+
<div class="form-row">
|
|
40
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
41
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
42
|
+
</div>
|
|
43
|
+
<div class="form-row">
|
|
44
|
+
<label for="node-input-config"><i class="fa fa-user"></i> Account</label>
|
|
45
|
+
<input type="text" id="node-input-config">
|
|
46
|
+
</div>
|
|
47
|
+
<div class="form-row" id="row-trackerID">
|
|
48
|
+
<label for="node-input-trackerID"><i class="fa fa-map-marker"></i> Tracker ID</label>
|
|
49
|
+
<input type="text" id="node-input-trackerID" placeholder="Find in Tractive app → Settings → Device">
|
|
50
|
+
</div>
|
|
51
|
+
<div class="form-row">
|
|
52
|
+
<label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
|
53
|
+
<select id="node-input-operation" style="width:70%">
|
|
54
|
+
<optgroup label="Tracker">
|
|
55
|
+
<option value="getTrackerHardware">Get hardware (battery, charging)</option>
|
|
56
|
+
<option value="getTrackerLocation">Get location</option>
|
|
57
|
+
<option value="getTrackerHistory">Get history</option>
|
|
58
|
+
</optgroup>
|
|
59
|
+
<optgroup label="Commands">
|
|
60
|
+
<option value="liveOn">Live tracking ON</option>
|
|
61
|
+
<option value="liveOff">Live tracking OFF</option>
|
|
62
|
+
<option value="LEDOn">LED ON</option>
|
|
63
|
+
<option value="LEDOff">LED OFF</option>
|
|
64
|
+
<option value="buzzerOn">Buzzer ON</option>
|
|
65
|
+
<option value="buzzerOff">Buzzer OFF</option>
|
|
66
|
+
</optgroup>
|
|
67
|
+
<optgroup label="Account">
|
|
68
|
+
<option value="getPets">Get pets</option>
|
|
69
|
+
<option value="getAllTrackers">Get all trackers</option>
|
|
70
|
+
</optgroup>
|
|
71
|
+
</select>
|
|
72
|
+
</div>
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<script type="text/html" data-help-name="tractive-tracker">
|
|
76
|
+
<p>Calls the Tractive API for a configured tracker.</p>
|
|
77
|
+
<p><b>Tracker ID:</b> find it in the Tractive app under Settings → Device → Device ID.
|
|
78
|
+
Can be overridden at runtime with <code>msg.trackerID</code>.</p>
|
|
79
|
+
<p>For <b>Get history</b>, set <code>msg.from</code> and <code>msg.to</code> (Unix timestamps in ms).</p>
|
|
80
|
+
</script>
|
package/package.json
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-tractive",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Node-RED palette for Tractive GPS tracker
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Node-RED palette for the Tractive GPS tracker API — monitor battery, location, and control LED/buzzer.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --test"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/elarochejoubert/tractive.git"
|
|
13
|
+
},
|
|
6
14
|
"keywords": [
|
|
7
15
|
"node-red",
|
|
8
16
|
"tractive",
|
|
9
17
|
"GPS",
|
|
10
18
|
"tracker",
|
|
11
|
-
"pet"
|
|
19
|
+
"pet",
|
|
20
|
+
"tracking"
|
|
12
21
|
],
|
|
13
22
|
"node-red": {
|
|
14
|
-
"
|
|
23
|
+
"version": ">=3.0.0",
|
|
24
|
+
"nodes": {
|
|
25
|
+
"tractive-config": "nodes/tractive-config.cjs",
|
|
26
|
+
"tractive-tracker": "nodes/tractive-tracker.cjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"author": "Edouard Laroche-Joubert",
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/elarochejoubert/tractive/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/elarochejoubert/tractive#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
15
37
|
},
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"dependencies": {
|
|
19
|
-
"tractive": "^1.2.1"
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"undici": "^7.0.0"
|
|
20
40
|
}
|
|
21
41
|
}
|
package/src/account.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets Tractive account information.
|
|
3
|
+
* @returns {Object} Object
|
|
4
|
+
*/
|
|
5
|
+
export async function getAccountInfo() {
|
|
6
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
7
|
+
const url = `https://graph.tractive.com/4/user/${accountDetails.uid}`;
|
|
8
|
+
const res = await fetch(url, {
|
|
9
|
+
method: gloOpts.method,
|
|
10
|
+
headers: gloOpts.headers
|
|
11
|
+
});
|
|
12
|
+
const data = await res.json();
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get all account subscriptions
|
|
18
|
+
* @returns {Array} Array
|
|
19
|
+
*/
|
|
20
|
+
export async function getAccountSubscriptions() {
|
|
21
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
22
|
+
const url = `https://graph.tractive.com/4/user/${accountDetails.uid}/subscriptions`;
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
method: gloOpts.method,
|
|
25
|
+
headers: gloOpts.headers
|
|
26
|
+
});
|
|
27
|
+
const data = await res.json();
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get a subscription
|
|
33
|
+
* @param {String} subscriptionID
|
|
34
|
+
* @returns {Object} Object
|
|
35
|
+
*/
|
|
36
|
+
export async function getAccountSubscription(subscriptionID) {
|
|
37
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
38
|
+
const url = `https://graph.tractive.com/4/subscription/${subscriptionID}`;
|
|
39
|
+
const res = await fetch(url, {
|
|
40
|
+
method: gloOpts.method,
|
|
41
|
+
headers: gloOpts.headers
|
|
42
|
+
});
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get a list of accounts you share trackers with
|
|
49
|
+
* @returns {Array} Array
|
|
50
|
+
*/
|
|
51
|
+
export async function getAccountShares() {
|
|
52
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
53
|
+
const url = `https://graph.tractive.com/4/user/${accountDetails.uid}/shares`;
|
|
54
|
+
const res = await fetch(url, {
|
|
55
|
+
method: gloOpts.method,
|
|
56
|
+
headers: gloOpts.headers
|
|
57
|
+
});
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
return data;
|
|
60
|
+
}
|
package/src/commands.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const RATE_LIMIT_CODE = 4006;
|
|
2
|
+
const RATE_LIMIT_DELAY_MS = 2000;
|
|
3
|
+
const MAX_RETRIES = 3;
|
|
4
|
+
|
|
5
|
+
async function commandFetch(url, retries = MAX_RETRIES) {
|
|
6
|
+
const res = await fetch(url, {
|
|
7
|
+
method: gloOpts.method,
|
|
8
|
+
headers: gloOpts.headers
|
|
9
|
+
});
|
|
10
|
+
const data = await res.json();
|
|
11
|
+
if (data?.code === RATE_LIMIT_CODE && retries > 0) {
|
|
12
|
+
await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY_MS));
|
|
13
|
+
return commandFetch(url, retries - 1);
|
|
14
|
+
}
|
|
15
|
+
return data;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Toggle live tracking mode on for a tracker.
|
|
20
|
+
* @param {String} trackerID
|
|
21
|
+
* @returns {Object} Object
|
|
22
|
+
*/
|
|
23
|
+
export async function liveOn(trackerID) {
|
|
24
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
25
|
+
return commandFetch(`https://graph.tractive.com/4/tracker/${trackerID}/command/live_tracking/on`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Toggle live tracking mode off for a tracker.
|
|
30
|
+
* @param {String} trackerID
|
|
31
|
+
* @returns {Object} Object
|
|
32
|
+
*/
|
|
33
|
+
export async function liveOff(trackerID) {
|
|
34
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
35
|
+
return commandFetch(`https://graph.tractive.com/4/tracker/${trackerID}/command/live_tracking/off`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Turn the trackers LED light on for the specified tracker.
|
|
40
|
+
* @param {String} trackerID
|
|
41
|
+
* @returns {Object} Object
|
|
42
|
+
*/
|
|
43
|
+
export async function LEDOn(trackerID) {
|
|
44
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
45
|
+
return commandFetch(`https://graph.tractive.com/4/tracker/${trackerID}/command/led_control/on`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Turn the trackers LED light off for the specified tracker.
|
|
50
|
+
* @param {String} trackerID
|
|
51
|
+
* @returns {Object} Object
|
|
52
|
+
*/
|
|
53
|
+
export async function LEDOff(trackerID) {
|
|
54
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
55
|
+
return commandFetch(`https://graph.tractive.com/4/tracker/${trackerID}/command/led_control/off`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Turn the trackers buzzer sound on for the specified tracker.
|
|
60
|
+
* @param {String} trackerID
|
|
61
|
+
* @returns {Object} Object
|
|
62
|
+
*/
|
|
63
|
+
export async function BuzzerOn(trackerID) {
|
|
64
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
65
|
+
return commandFetch(`https://graph.tractive.com/4/tracker/${trackerID}/command/buzzer_control/on`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Turn the trackers buzzer sound off for the specified tracker.
|
|
70
|
+
* @param {String} trackerID
|
|
71
|
+
* @returns {Object} Object
|
|
72
|
+
*/
|
|
73
|
+
export async function BuzzerOff(trackerID) {
|
|
74
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
75
|
+
return commandFetch(`https://graph.tractive.com/4/tracker/${trackerID}/command/buzzer_control/off`);
|
|
76
|
+
}
|
package/src/pet.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get a pet and it's data, includes attached tracker, type of animal, and other pet details.
|
|
3
|
+
* @param {String} petID
|
|
4
|
+
* @returns {Object} Object
|
|
5
|
+
*/
|
|
6
|
+
export async function getPet(petID) {
|
|
7
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
8
|
+
const url = `https://graph.tractive.com/4/trackable_object/${petID}`;
|
|
9
|
+
const res = await fetch(url, {
|
|
10
|
+
method: gloOpts.method,
|
|
11
|
+
headers: gloOpts.headers
|
|
12
|
+
});
|
|
13
|
+
const parsedData = await res.json();
|
|
14
|
+
parsedData.details.profile_picture_link = `https://graph.tractive.com/4/media/resource/${parsedData.details.profile_picture_id}.jpg`;
|
|
15
|
+
parsedData.details.cover_picture_link = `https://graph.tractive.com/4/media/resource/${parsedData.details.cover_picture_id}.jpg`;
|
|
16
|
+
return parsedData;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get a list of all pets on the account
|
|
21
|
+
* @returns {Array} Array
|
|
22
|
+
*/
|
|
23
|
+
export async function getPets() {
|
|
24
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
25
|
+
const url = `https://graph.tractive.com/4/user/${accountDetails.uid}/trackable_objects`;
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
method: gloOpts.method,
|
|
28
|
+
headers: gloOpts.headers
|
|
29
|
+
});
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
return data;
|
|
32
|
+
}
|
package/src/tracker.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get an array of all trackers on the account
|
|
3
|
+
* @returns {Array}
|
|
4
|
+
*/
|
|
5
|
+
export async function getAllTrackers() {
|
|
6
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
7
|
+
const url = `https://graph.tractive.com/4/user/${accountDetails.uid}/trackers`;
|
|
8
|
+
const res = await fetch(url, {
|
|
9
|
+
method: gloOpts.method,
|
|
10
|
+
headers: gloOpts.headers
|
|
11
|
+
});
|
|
12
|
+
const trackers = await res.json();
|
|
13
|
+
return trackers;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the specified tracker
|
|
18
|
+
* @param {String} trackerID
|
|
19
|
+
* @returns {Object} Object
|
|
20
|
+
*/
|
|
21
|
+
export async function getTracker(trackerID) {
|
|
22
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
23
|
+
const url = `https://graph.tractive.com/4/tracker/${trackerID}`;
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method: gloOpts.method,
|
|
26
|
+
headers: gloOpts.headers
|
|
27
|
+
});
|
|
28
|
+
const tracker = await res.json();
|
|
29
|
+
return tracker;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the history of locations for a specified tracker
|
|
34
|
+
* @param {String} trackerID
|
|
35
|
+
* @param {Number} from
|
|
36
|
+
* @param {Number} to
|
|
37
|
+
* @returns {Array} Array
|
|
38
|
+
*/
|
|
39
|
+
export async function getTrackerHistory(trackerID, from, to) {
|
|
40
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
41
|
+
let calcFrom = typeof from == "object" ? (from.getTime() / 1000).toFixed(0) : from;
|
|
42
|
+
let calcTo = typeof to == "object" ? (to.getTime() / 1000).toFixed(0) : to;
|
|
43
|
+
const url = `https://graph.tractive.com/4/tracker/${encodeURIComponent(trackerID)}/positions?time_from=${encodeURIComponent(calcFrom)}&time_to=${encodeURIComponent(calcTo)}&format=json_segments`;
|
|
44
|
+
const res = await fetch(url, {
|
|
45
|
+
method: gloOpts.method,
|
|
46
|
+
headers: gloOpts.headers
|
|
47
|
+
});
|
|
48
|
+
const parsedData = await res.json();
|
|
49
|
+
return parsedData[0];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the latest position report for the tracker.
|
|
54
|
+
* @param {String} trackerID
|
|
55
|
+
* @returns {Object} Object
|
|
56
|
+
*/
|
|
57
|
+
export async function getTrackerLocation(trackerID) {
|
|
58
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
59
|
+
const url = `https://graph.tractive.com/4/device_pos_report/${trackerID}`;
|
|
60
|
+
const res = await fetch(url, {
|
|
61
|
+
method: gloOpts.method,
|
|
62
|
+
headers: gloOpts.headers
|
|
63
|
+
});
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
const addressUrl = `https://graph.tractive.com/4/platform/geo/address/location?latitude=${encodeURIComponent(data.latlong[0])}&longitude=${encodeURIComponent(data.latlong[1])}`;
|
|
66
|
+
try {
|
|
67
|
+
const addressRes = await fetch(addressUrl, {
|
|
68
|
+
method: gloOpts.method,
|
|
69
|
+
headers: gloOpts.headers
|
|
70
|
+
});
|
|
71
|
+
const address = await addressRes.json();
|
|
72
|
+
data.address = address;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// ignore address fetch error
|
|
75
|
+
}
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the latest hardware report for the tracker.
|
|
81
|
+
* @param {String} trackerID
|
|
82
|
+
* @returns {Object} Object
|
|
83
|
+
*/
|
|
84
|
+
export async function getTrackerHardware(trackerID) {
|
|
85
|
+
if(!isAuthenticated()) return console.log('Not authenticated.');
|
|
86
|
+
const url = `https://graph.tractive.com/4/device_hw_report/${trackerID}`;
|
|
87
|
+
const res = await fetch(url, {
|
|
88
|
+
method: gloOpts.method,
|
|
89
|
+
headers: gloOpts.headers
|
|
90
|
+
});
|
|
91
|
+
const data = await res.json();
|
|
92
|
+
return data;
|
|
93
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Edouard
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|