node-red-contrib-eskomsepush 0.0.18 → 0.1.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.
- package/.claude/settings.local.json +8 -0
- package/README.md +5 -3
- package/jest.config.js +9 -0
- package/package.json +14 -7
- package/src/__tests__/api.test.ts +103 -0
- package/src/__tests__/loadshedding.test.ts +262 -0
- package/src/lib/api.ts +45 -0
- package/src/lib/loadshedding.ts +131 -0
- package/src/lib/types.ts +109 -0
- package/src/nodes/eskomsepush.html +9 -8
- package/src/nodes/eskomsepush.ts +203 -0
- package/tsconfig.json +17 -0
- package/tsconfig.test.json +8 -0
- package/src/nodes/eskomsepush.js +0 -371
package/src/nodes/eskomsepush.js
DELETED
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
module.exports = function (RED) {
|
|
2
|
-
'use strict'
|
|
3
|
-
|
|
4
|
-
const axios = require('axios')
|
|
5
|
-
const EskomSePushInfo = {
|
|
6
|
-
api: {
|
|
7
|
-
lastUpdate: null,
|
|
8
|
-
info: {}
|
|
9
|
-
},
|
|
10
|
-
status: {
|
|
11
|
-
lastUpdate: null,
|
|
12
|
-
info: {}
|
|
13
|
-
},
|
|
14
|
-
area: {
|
|
15
|
-
lastUpdate: null,
|
|
16
|
-
info: {}
|
|
17
|
-
},
|
|
18
|
-
calc: {}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function getMinutesToAPIReset () {
|
|
22
|
-
const now = new Date()
|
|
23
|
-
const targetTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 2, 0, 0)
|
|
24
|
-
if (now > targetTime) {
|
|
25
|
-
targetTime.setDate(targetTime.getDate() + 1)
|
|
26
|
-
}
|
|
27
|
-
const timeDiff = targetTime - now
|
|
28
|
-
const minutesLeft = Math.floor(timeDiff / (1000 * 60))
|
|
29
|
-
|
|
30
|
-
return minutesLeft
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function checkAllowance (node) {
|
|
34
|
-
const options = {}
|
|
35
|
-
const headers = { token: node.config.licensekey }
|
|
36
|
-
|
|
37
|
-
if (node.config.verbose === true) {
|
|
38
|
-
node.warn('Running function checkAllowance')
|
|
39
|
-
}
|
|
40
|
-
axios.get('https://developer.sepush.co.za/business/2.0/api_allowance',
|
|
41
|
-
{ params: options, headers }).then(function (response) {
|
|
42
|
-
EskomSePushInfo.api.info = response.data
|
|
43
|
-
if (EskomSePushInfo.api.lastUpdate === null) {
|
|
44
|
-
EskomSePushInfo.api.lastUpdate = new Date()
|
|
45
|
-
updateSheddingStatus(node)
|
|
46
|
-
}
|
|
47
|
-
EskomSePushInfo.api.lastUpdate = new Date()
|
|
48
|
-
})
|
|
49
|
-
.catch(error => {
|
|
50
|
-
node.warn({ error: error.message })
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function checkStage (node) {
|
|
55
|
-
const options = {}
|
|
56
|
-
const headers = { token: node.config.licensekey }
|
|
57
|
-
|
|
58
|
-
if (node.config.verbose === true) {
|
|
59
|
-
let warnstring = 'Running function checkStage'
|
|
60
|
-
if (EskomSePushInfo.status.lastUpdate === null) {
|
|
61
|
-
warnstring += ' - initial run'
|
|
62
|
-
} else {
|
|
63
|
-
warnstring += ' after ' + ((new Date() - EskomSePushInfo.status.lastUpdate) / 60000).toFixed(0) + ' minutes'
|
|
64
|
-
}
|
|
65
|
-
node.warn(warnstring)
|
|
66
|
-
}
|
|
67
|
-
axios.get('https://developer.sepush.co.za/business/2.0/status',
|
|
68
|
-
{ params: options, headers }).then(function (response) {
|
|
69
|
-
EskomSePushInfo.status.info = response.data
|
|
70
|
-
EskomSePushInfo.status.lastUpdate = new Date()
|
|
71
|
-
// Call updateSheddingStatus again now we have new data
|
|
72
|
-
updateSheddingStatus(node)
|
|
73
|
-
})
|
|
74
|
-
.catch(error => {
|
|
75
|
-
node.warn({ error: error.message })
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function checkArea (node) {
|
|
80
|
-
const options = { id: node.config.area }
|
|
81
|
-
const headers = { token: node.config.licensekey }
|
|
82
|
-
const url = 'https://developer.sepush.co.za/business/2.0/area'
|
|
83
|
-
|
|
84
|
-
if (node.config.verbose === true) {
|
|
85
|
-
let warnstring = 'Running function checkArea'
|
|
86
|
-
if (EskomSePushInfo.area.lastUpdate === null) {
|
|
87
|
-
warnstring += ' - initial run'
|
|
88
|
-
} else {
|
|
89
|
-
warnstring += ' after ' + ((new Date() - EskomSePushInfo.area.lastUpdate) / 60000).toFixed(0) + ' minutes'
|
|
90
|
-
}
|
|
91
|
-
node.warn(warnstring)
|
|
92
|
-
}
|
|
93
|
-
if (node.config.test) {
|
|
94
|
-
options.test = 'current'
|
|
95
|
-
}
|
|
96
|
-
axios.get(url,
|
|
97
|
-
{ params: options, headers }).then(function (response) {
|
|
98
|
-
EskomSePushInfo.area.info = response.data
|
|
99
|
-
EskomSePushInfo.area.lastUpdate = new Date()
|
|
100
|
-
// Call updateSheddingStatus again now we have new data
|
|
101
|
-
updateSheddingStatus(node)
|
|
102
|
-
})
|
|
103
|
-
.catch(error => {
|
|
104
|
-
node.warn({ error: error.message })
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function updateSheddingStatus (node, msg) {
|
|
109
|
-
const now = new Date()
|
|
110
|
-
|
|
111
|
-
// Check allowance every ten minutes
|
|
112
|
-
if ((msg && msg.payload === 'allowance') || EskomSePushInfo.api.lastUpdate === null || (now.getTime() - EskomSePushInfo.api.lastUpdate.getTime()) > 600000) {
|
|
113
|
-
checkAllowance(node)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// If we don't have API info, we just return
|
|
117
|
-
if (Object.entries(EskomSePushInfo.api.info).length === 0) {
|
|
118
|
-
node.warn('No API info (yet), refusing to continue')
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// The same is true if we have no API calls left
|
|
123
|
-
if (EskomSePushInfo.api.info.allowance.count >= EskomSePushInfo.api.info.allowance.limit) {
|
|
124
|
-
node.warn('No API calls left, not checking status/schedule')
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Fetching actual information takes 2 calls, so calculate how long until the next API count
|
|
129
|
-
// reset and divide the calls over the day. Wait at least 60 minutes between calls
|
|
130
|
-
// reduce limit by api_allowance_buffer to cater for units consumed by other API calls
|
|
131
|
-
if (node.config.verbose === true) {
|
|
132
|
-
node.warn('Minutes to API Reset: ' + getMinutesToAPIReset())
|
|
133
|
-
}
|
|
134
|
-
const allowanceRemaining = EskomSePushInfo.api.info.allowance.limit - node.config.api_allowance_buffer - EskomSePushInfo.api.info.allowance.count
|
|
135
|
-
|
|
136
|
-
if (allowanceRemaining > 0) {
|
|
137
|
-
EskomSePushInfo.calc.sleeptime = Math.round(getMinutesToAPIReset() / Math.ceil(allowanceRemaining / 2))
|
|
138
|
-
if (node.config.verbose === true) {
|
|
139
|
-
node.warn('API allowance limit: ' + EskomSePushInfo.api.info.allowance.limit)
|
|
140
|
-
node.warn('API allowance count: ' + EskomSePushInfo.api.info.allowance.count)
|
|
141
|
-
node.warn('Calculated sleeptime: ' + EskomSePushInfo.calc.sleeptime)
|
|
142
|
-
}
|
|
143
|
-
if (EskomSePushInfo.calc.sleeptime < 10) {
|
|
144
|
-
EskomSePushInfo.calc.sleeptime = 10
|
|
145
|
-
if (node.config.verbose === true) {
|
|
146
|
-
node.warn('Calculated sleeptime was less than 10. Set it to 10: ' + EskomSePushInfo.calc.sleeptime)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
EskomSePushInfo.calc.sleeptime = 60
|
|
151
|
-
if (node.config.verbose === true) {
|
|
152
|
-
node.warn('Set sleeptime to 60 since allowance count is low: ' + EskomSePushInfo.calc.sleeptime)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if ((msg && msg.payload === 'stage') || EskomSePushInfo.status.lastUpdate === null || (now.getTime() - EskomSePushInfo.status.lastUpdate) > (EskomSePushInfo.calc.sleeptime * 60000)) {
|
|
157
|
-
checkStage(node)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if ((msg && msg.payload === 'area') || EskomSePushInfo.area.lastUpdate === null || (now.getTime() - EskomSePushInfo.area.lastUpdate) > (EskomSePushInfo.calc.sleeptime * 60000)) {
|
|
161
|
-
checkArea(node)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Now we have all info to continue. Just making sure that all update values are non null.
|
|
165
|
-
if (EskomSePushInfo.api.lastUpdate === null ||
|
|
166
|
-
EskomSePushInfo.status.lastUpdate === null ||
|
|
167
|
-
EskomSePushInfo.area.lastUpdate === null) {
|
|
168
|
-
(node.config.verbose === true) && node.warn('Not enough info to continue.')
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Determine the current stage
|
|
173
|
-
EskomSePushInfo.calc.stage = EskomSePushInfo.status.info.status[node.config.statusselect].stage
|
|
174
|
-
|
|
175
|
-
if (node.config.verbose === true) {
|
|
176
|
-
node.warn('API call status: ' + EskomSePushInfo.api.info.allowance.count + '/' + EskomSePushInfo.api.info.allowance.limit)
|
|
177
|
-
node.warn(EskomSePushInfo)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Default to false, overrule of loadshedding is active
|
|
181
|
-
EskomSePushInfo.calc.active = false
|
|
182
|
-
|
|
183
|
-
// Are there any events going on?
|
|
184
|
-
if (Object.entries(EskomSePushInfo.area.info.events).length > 0) {
|
|
185
|
-
const EventStart = Date.parse(EskomSePushInfo.area.info.events[0].start)
|
|
186
|
-
const EventEnd = Date.parse(EskomSePushInfo.area.info.events[0].end)
|
|
187
|
-
if (now >= EventStart && now < EventEnd) {
|
|
188
|
-
EskomSePushInfo.calc.type = 'event'
|
|
189
|
-
EskomSePushInfo.calc.active = true
|
|
190
|
-
if (EskomSePushInfo.area.info.events[0].note.match(/Stage (\d+)/i)) {
|
|
191
|
-
EskomSePushInfo.calc.stage = EskomSePushInfo.area.info.events[0].note.match(/Stage (\d+)/i)[1]
|
|
192
|
-
}
|
|
193
|
-
EskomSePushInfo.calc.start = EventStart
|
|
194
|
-
EskomSePushInfo.calc.end = EventEnd
|
|
195
|
-
} else {
|
|
196
|
-
EskomSePushInfo.calc.next = {
|
|
197
|
-
type: 'event',
|
|
198
|
-
start: EventStart,
|
|
199
|
-
end: EventEnd,
|
|
200
|
-
stage: EskomSePushInfo.area.info.events[0].note.match(/Stage (\d+)/i)[1]
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Scheduled downtime has the thing that the time is in locatime
|
|
206
|
-
// So not just like events, where they are in UTC with an offset
|
|
207
|
-
let BreakLoop = false
|
|
208
|
-
for (const dates of EskomSePushInfo.area.info.schedule.days) {
|
|
209
|
-
const stageIndex = EskomSePushInfo.calc.stage - 1
|
|
210
|
-
if (stageIndex >= 0 && stageIndex < dates.stages.length) {
|
|
211
|
-
if (Array.isArray(dates.stages[stageIndex])) {
|
|
212
|
-
for (const schedule of dates.stages[stageIndex]) {
|
|
213
|
-
const ScheduleStart = Date.parse(dates.date + ' ' + schedule.split('-')[0])
|
|
214
|
-
let ScheduleEnd = Date.parse(dates.date + ' ' + schedule.split('-')[1])
|
|
215
|
-
if (ScheduleEnd < ScheduleStart) {
|
|
216
|
-
ScheduleEnd += (24 * 60 * 60 * 1000)
|
|
217
|
-
}
|
|
218
|
-
if (now < ScheduleEnd) {
|
|
219
|
-
BreakLoop = true
|
|
220
|
-
// This schedule is either active or will be next
|
|
221
|
-
if (now >= ScheduleStart) {
|
|
222
|
-
EskomSePushInfo.calc.active = true
|
|
223
|
-
EskomSePushInfo.calc.type = 'schedule'
|
|
224
|
-
EskomSePushInfo.calc.start = ScheduleStart
|
|
225
|
-
EskomSePushInfo.calc.end = ScheduleEnd
|
|
226
|
-
} else {
|
|
227
|
-
EskomSePushInfo.calc.next = {
|
|
228
|
-
type: 'schedule',
|
|
229
|
-
start: ScheduleStart,
|
|
230
|
-
end: ScheduleEnd,
|
|
231
|
-
stage: EskomSePushInfo.calc.stage
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (BreakLoop) { break }
|
|
236
|
-
}
|
|
237
|
-
} else {
|
|
238
|
-
console.warn('Not an array:', dates.stages[stageIndex]) // Warning if not an array
|
|
239
|
-
}
|
|
240
|
-
} else {
|
|
241
|
-
console.warn(`Invalid stage index: ${stageIndex}. No loadshedding going on?`) // Warning if stage index is out of bounds
|
|
242
|
-
BreakLoop = true
|
|
243
|
-
}
|
|
244
|
-
if (BreakLoop) { break }
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (EskomSePushInfo.calc.next) {
|
|
248
|
-
EskomSePushInfo.calc.next.duration = (EskomSePushInfo.calc.next.end - EskomSePushInfo.calc.next.start) / 1000
|
|
249
|
-
EskomSePushInfo.calc.next.islong = EskomSePushInfo.calc.next.duration >= (4 * 3600)
|
|
250
|
-
EskomSePushInfo.calc.secondstostatechange = parseInt((EskomSePushInfo.calc.next.start - now) / 1000)
|
|
251
|
-
EskomSePushInfo.calc.next.isHigherStage = EskomSePushInfo.calc.next.stage > EskomSePushInfo.calc.stage
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (EskomSePushInfo.calc.active) {
|
|
255
|
-
EskomSePushInfo.calc.duration = (EskomSePushInfo.calc.end - EskomSePushInfo.calc.start) / 1000
|
|
256
|
-
EskomSePushInfo.calc.islong = EskomSePushInfo.calc.duration >= (4 * 3600)
|
|
257
|
-
EskomSePushInfo.calc.secondstostatechange = parseInt((EskomSePushInfo.calc.end - now) / 1000)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (node.config.verbose === true) {
|
|
261
|
-
node.warn(EskomSePushInfo.calc)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Send output
|
|
265
|
-
node.send([{
|
|
266
|
-
payload: EskomSePushInfo.calc.active,
|
|
267
|
-
stage: EskomSePushInfo.calc.stage,
|
|
268
|
-
statusselect: node.config.statusselect,
|
|
269
|
-
api: {
|
|
270
|
-
count: EskomSePushInfo.api.info.allowance.count,
|
|
271
|
-
limit: EskomSePushInfo.api.info.allowance.limit
|
|
272
|
-
},
|
|
273
|
-
calc: EskomSePushInfo.calc
|
|
274
|
-
}, {
|
|
275
|
-
stage: EskomSePushInfo.status,
|
|
276
|
-
schedule: EskomSePushInfo.area
|
|
277
|
-
}])
|
|
278
|
-
|
|
279
|
-
// And update the status
|
|
280
|
-
let fill = 'green'
|
|
281
|
-
let shape = 'ring'
|
|
282
|
-
let statusText = 'Stage ' + EskomSePushInfo.calc.stage + ': '
|
|
283
|
-
|
|
284
|
-
if (EskomSePushInfo.calc.active) {
|
|
285
|
-
fill = 'yellow'
|
|
286
|
-
if (EskomSePushInfo.calc.type === 'event') {
|
|
287
|
-
shape = 'dot'
|
|
288
|
-
}
|
|
289
|
-
if (EskomSePushInfo.calc.start) {
|
|
290
|
-
statusText += new Date(EskomSePushInfo.calc.start).toLocaleTimeString([], { timeStyle: 'short' })
|
|
291
|
-
statusText += ' - ' + new Date(EskomSePushInfo.calc.end).toLocaleTimeString([], { timeStyle: 'short' })
|
|
292
|
-
}
|
|
293
|
-
} else {
|
|
294
|
-
if (EskomSePushInfo.calc.next) {
|
|
295
|
-
if (new Date(EskomSePushInfo.calc.next.start).getUTCDay() !== now.getUTCDate()) {
|
|
296
|
-
statusText += new Date(EskomSePushInfo.calc.next.start).toLocaleString([], { weekday: 'short' }) + ' '
|
|
297
|
-
}
|
|
298
|
-
statusText += new Date(EskomSePushInfo.calc.next.start).toLocaleTimeString([], { timeStyle: 'short' })
|
|
299
|
-
statusText += ' - ' + new Date(EskomSePushInfo.calc.next.end).toLocaleTimeString([], { timeStyle: 'short' })
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
statusText += ' (API: ' + EskomSePushInfo.api.info.allowance.count + '/' + EskomSePushInfo.api.info.allowance.limit + ')'
|
|
304
|
-
node.status({
|
|
305
|
-
fill, shape, text: statusText
|
|
306
|
-
})
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function EskomSePush (config) {
|
|
310
|
-
RED.nodes.createNode(this, config)
|
|
311
|
-
|
|
312
|
-
const node = this
|
|
313
|
-
node.config = config
|
|
314
|
-
|
|
315
|
-
updateSheddingStatus(node)
|
|
316
|
-
const intervalId = setInterval(function () {
|
|
317
|
-
updateSheddingStatus(node)
|
|
318
|
-
}, 60000)
|
|
319
|
-
|
|
320
|
-
node.on('input', function (msg) {
|
|
321
|
-
updateSheddingStatus(node, msg)
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
node.on('close', function () {
|
|
325
|
-
clearInterval(intervalId)
|
|
326
|
-
})
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
RED.nodes.registerType('eskomsepush', EskomSePush)
|
|
330
|
-
|
|
331
|
-
RED.httpNode.get('/eskomsepush/search', (req, res) => {
|
|
332
|
-
if (!req.query || !req.query.token || !req.query.search) {
|
|
333
|
-
res.setHeader('Content-Type', 'application/json')
|
|
334
|
-
return res.send('invalid')
|
|
335
|
-
}
|
|
336
|
-
const headers = {
|
|
337
|
-
token: req.query.token
|
|
338
|
-
}
|
|
339
|
-
const options = {
|
|
340
|
-
text: req.query.search
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
res.setHeader('Content-Type', 'application/json')
|
|
344
|
-
axios.get('https://developer.sepush.co.za/business/2.0/areas_search',
|
|
345
|
-
{ params: options, headers }).then(function (response) {
|
|
346
|
-
return res.send(response.data)
|
|
347
|
-
})
|
|
348
|
-
.catch(error => {
|
|
349
|
-
return res.send({ error: error.message })
|
|
350
|
-
})
|
|
351
|
-
})
|
|
352
|
-
RED.httpNode.get('/eskomsepush/api', (req, res) => {
|
|
353
|
-
if (!req.query.token) {
|
|
354
|
-
res.setHeader('Content-Type', 'application/json')
|
|
355
|
-
return res.send('invalid')
|
|
356
|
-
}
|
|
357
|
-
const headers = {
|
|
358
|
-
token: req.query.token
|
|
359
|
-
}
|
|
360
|
-
const options = {}
|
|
361
|
-
|
|
362
|
-
res.setHeader('Content-Type', 'application/json')
|
|
363
|
-
axios.get('https://developer.sepush.co.za/business/2.0/api_allowance',
|
|
364
|
-
{ params: options, headers }).then(function (response) {
|
|
365
|
-
return res.send(response.data)
|
|
366
|
-
})
|
|
367
|
-
.catch(error => {
|
|
368
|
-
return res.send({ error: error.message })
|
|
369
|
-
})
|
|
370
|
-
})
|
|
371
|
-
}
|