dirkjanfaber / node-red-contrib-eskomsepush

Node-RED node for the EskomsePush API
MIT License
7 stars 2 forks source link

[BUG]"TypeError: dates.stages[(EskomSePushInfo.calc.stage - 1)] is not iterable" #10

Closed nicopret1 closed 1 year ago

nicopret1 commented 1 year ago

Describe the bug

The node currently returns the error: "TypeError: dates.stages[(EskomSePushInfo.calc.stage - 1)] is not iterable". My curl query returns the correct data which confirms my credentials and account are working correctly.

To Reproduce Steps to reproduce the behavior:

  1. Using node 'eskomsepush'
  2. Click on 'Deploy node with License key=token, Area id=westerncape-9-paarl and Status select= National(Eskom)'
  3. See error: "TypeError: dates.stages[(EskomSePushInfo.calc.stage - 1)] is not iterable"

Expected behavior The correct data should be returned. Please note that loadshedding is currently suspended until 16:00 on Sunday, 17 September. The flow worked correctly yesterday while there was loadshedding.

Screenshots If applicable, add screenshots to help explain your problem. See below: Flow: Screenshot 2023-09-17 at 09 55 04

Node config with error info: Screenshot 2023-09-17 at 09 55 50

Flow If applicable, add a flow to help explain your problem. Below the content of my flow in json: [{"id":"fddc3c3b17e3caa7","type":"tab","label":"Victron","disabled":false,"info":"","env":[]},{"id":"c7e0a8b8221196b2","type":"junction","z":"fddc3c3b17e3caa7","x":780,"y":240,"wires":[["1304ce8952d56b0e","42af2b5e525397d7"]]},{"id":"d083105e39f8ba68","type":"debug","z":"fddc3c3b17e3caa7","name":"Load shedding","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":560,"y":60,"wires":[]},{"id":"1d3e18f144e4911a","type":"debug","z":"fddc3c3b17e3caa7","name":"Schedule and events","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":580,"y":460,"wires":[]},{"id":"42af2b5e525397d7","type":"mqtt out","z":"fddc3c3b17e3caa7","name":"Set minSOC via MQTT","topic":"victron-home/W/c0847d9b49b3/settings/0/Settings/CGwacs/BatteryLife/MinimumSocLimit","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"1f709b6e6000e30b","x":940,"y":240,"wires":[]},{"id":"d29469701d379fef","type":"eskomsepush","z":"fddc3c3b17e3caa7","name":"","licensekey":"my-license-key","area":"westerncape-9-paarl","statusselect":"eskom","test":false,"verbose":false,"x":210,"y":260,"wires":[["d083105e39f8ba68","685df7425884177e"],["1d3e18f144e4911a"]]},{"id":"1304ce8952d56b0e","type":"debug","z":"fddc3c3b17e3caa7","name":"Scheduled charging","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":940,"y":200,"wires":[]},{"id":"685df7425884177e","type":"switch","z":"fddc3c3b17e3caa7","name":"Stage","property":"stage","propertyType":"msg","rules":[{"t":"eq","v":"1","vt":"str"},{"t":"eq","v":"2","vt":"str"},{"t":"eq","v":"3","vt":"str"},{"t":"eq","v":"4","vt":"str"},{"t":"eq","v":"5","vt":"str"},{"t":"eq","v":"6","vt":"str"},{"t":"eq","v":"7","vt":"str"},{"t":"eq","v":"8","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":9,"x":430,"y":260,"wires":[["d52bbef6b535646e"],["d52bbef6b535646e"],["9a6d3dab537b880f"],["f1c3df4e680f41a6"],["02a4b5afeb44828f"],["a4c5cec636c88ea4"],["d9ce6b3c5e1fe35e"],["a5f3427bd40f7aa1"],["c80133f055cc326a"]],"inputLabels":["EskomSePush API"],"outputLabels":["stage 1","stage 2","stage 3","stage 4","stage 5","stage 6","stage 7","stage 8","otherwise"]},{"id":"9a6d3dab537b880f","type":"change","z":"fddc3c3b17e3caa7","name":"60%","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"value\": 60.0}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":160,"wires":[["c7e0a8b8221196b2"]]},{"id":"02a4b5afeb44828f","type":"change","z":"fddc3c3b17e3caa7","name":"70%","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"value\": 70.0}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":240,"wires":[["c7e0a8b8221196b2"]]},{"id":"a5f3427bd40f7aa1","type":"change","z":"fddc3c3b17e3caa7","name":"100%","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"value\": 100.0}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":360,"wires":[["c7e0a8b8221196b2"]]},{"id":"a4c5cec636c88ea4","type":"change","z":"fddc3c3b17e3caa7","name":"80%","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"value\": 80.0}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":280,"wires":[["c7e0a8b8221196b2"]]},{"id":"f1c3df4e680f41a6","type":"change","z":"fddc3c3b17e3caa7","name":"65%","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"value\": 65.0}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":200,"wires":[["c7e0a8b8221196b2"]]},{"id":"d52bbef6b535646e","type":"change","z":"fddc3c3b17e3caa7","name":"50%","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"value\": 50.0}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":120,"wires":[["c7e0a8b8221196b2"]]},{"id":"c80133f055cc326a","type":"change","z":"fddc3c3b17e3caa7","name":"40%","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"value\": 40.0}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":400,"wires":[["c7e0a8b8221196b2"]]},{"id":"d9ce6b3c5e1fe35e","type":"change","z":"fddc3c3b17e3caa7","name":"90%","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"value\": 90.0}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":320,"wires":[[]]},{"id":"1f709b6e6000e30b","type":"mqtt-broker","name":"","broker":"192.168.0.152","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""}]

Software (please complete the following information):

Additional context Add any other context about the problem here. It was working correctly yesterday, until I depleted my free quota. After midnight the quota was restored, but it then set the wrong stage. It set Stage 8 while there was in fact no active loadshedding. Test mode was enabled and I though the problem might be related to that. I then disabled test mode, but the result was the same. Below content of the json object displayed in debug window: {"payload":{"value":100},"stage":"8","statusselect":"eskom","api":{"count":18,"limit":50},"calc":{"sleeptime":"63","stage":"8","active":true,"next":{"type":"schedule","start":1694930400000,"end":1694946600000,"stage":"8","duration":16200,"islong":true,"isHigherStage":false},"type":"schedule","start":1694930400000,"end":1694946600000,"secondstostatechange":11697,"duration":16200,"islong":true},"_msgid":"9c739d61bc181f6f"}

I then decided to delete the node and setup a new node (in case there was an issue with the existing node), but now get the error: "TypeError: dates.stages[(EskomSePushInfo.calc.stage - 1)] is not iterable" despite the correct credentials and settings and the curl confirming my account is working correctly. See below detail from curl: curl --location --request GET 'https://developer.sepush.co.za/business/2.0/area?id=westerncape-9-paarl' \ --header 'token: my-token' {"events":[{"end":"2023-09-18T02:30:00+02:00","note":"Stage 3","start":"2023-09-18T00:00:00+02:00"},{"end":"2023-09-18T10:30:00+02:00","note":"Stage 4","start":"2023-09-18T08:00:00+02:00"},{"end":"2023-09-18T18:30:00+02:00","note":"Stage 4","start":"2023-09-18T16:00:00+02:00"},{"end":"2023-09-19T02:30:00+02:00","note":"Stage 4","start":"2023-09-19T00:00:00+02:00"},{"end":"2023-09-19T10:30:00+02:00","note":"Stage 4","start":"2023-09-19T08:00:00+02:00"},{"end":"2023-09-19T18:30:00+02:00","note":"Stage 4","start":"2023-09-19T16:00:00+02:00"}],"info":{"name":"Paarl (9)","region":"Western Cape"},"schedule":{"days":[{"date":"2023-09-17","name":"Sunday","stages":[["08:00-10:30"],["00:00-02:30","08:00-10:30"],["00:00-02:30","08:00-10:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-12:30","16:00-18:30"],["00:00-04:30","08:00-12:30","16:00-18:30"],["00:00-04:30","08:00-12:30","16:00-18:30"],["00:00-04:30","08:00-12:30","16:00-20:30"]]},{"date":"2023-09-18","name":"Monday","stages":[["16:00-18:30"],["08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-20:30"],["00:00-02:30","08:00-12:30","16:00-20:30"],["00:00-04:30","08:00-12:30","16:00-20:30"],["00:00-04:30","08:00-12:30","16:00-20:30"]]},{"date":"2023-09-19","name":"Tuesday","stages":[[],["16:00-18:30"],["08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-20:30"],["00:00-02:30","08:00-12:30","16:00-20:30"],["00:00-04:30","08:00-12:30","16:00-20:30"]]},{"date":"2023-09-20","name":"Wednesday","stages":[["00:00-02:30"],["00:00-02:30"],["00:00-02:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-04:30","08:00-10:30","16:00-18:30"],["00:00-04:30","08:00-10:30","16:00-18:30"],["00:00-04:30","08:00-10:30","16:00-20:30"],["00:00-04:30","08:00-12:30","16:00-20:30"]]},{"date":"2023-09-21","name":"Thursday","stages":[["06:00-08:30"],["06:00-08:30"],["06:00-08:30","22:00-00:30"],["06:00-08:30","14:00-16:30","22:00-00:30"],["06:00-10:30","14:00-16:30","22:00-00:30"],["06:00-10:30","14:00-16:30","22:00-00:30"],["06:00-10:30","14:00-16:30","22:00-00:30"],["06:00-10:30","14:00-18:30","22:00-00:30"]]},{"date":"2023-09-22","name":"Friday","stages":[["14:00-16:30"],["06:00-08:30","14:00-16:30"],["06:00-08:30","14:00-16:30"],["06:00-08:30","14:00-16:30","22:00-00:30"],["06:00-08:30","14:00-18:30","22:00-00:30"],["06:00-10:30","14:00-18:30","22:00-00:30"],["00:00-02:30","06:00-10:30","14:00-18:30","22:00-00:30"],["00:00-02:30","06:00-10:30","14:00-18:30","22:00-00:30"]]},{"date":"2023-09-23","name":"Saturday","stages":[["22:00-00:30"],["14:00-16:30","22:00-00:30"],["06:00-08:30","14:00-16:30","22:00-00:30"],["06:00-08:30","14:00-16:30","22:00-00:30"],["06:00-08:30","14:00-16:30","22:00-00:30"],["06:00-08:30","14:00-18:30","22:00-00:30"],["06:00-10:30","14:00-18:30","22:00-00:30"],["00:00-02:30","06:00-10:30","14:00-18:30","22:00-00:30"]]}],"source":"https://sepush.co.za/"}}

With verbose logging enabled I see the following in the debug window: msg : Object content: {"api":{"lastUpdate":"2023-09-17T07:25:58.329Z","info":{"allowance":{"count":34,"limit":50,"type":"daily"}}},"status":{"lastUpdate":"2023-09-17T07:25:59.129Z","info":{"status":{"capetown":{"name":"Cape Town","next_stages":[{"stage":"3","stage_start_timestamp":"2023-09-17T16:00:00+02:00"}],"stage":"0","stage_updated":"2023-09-16T22:00:00.342998+02:00"},"eskom":{"name":"Eskom","next_stages":[{"stage":"3","stage_start_timestamp":"2023-09-17T16:00:00+02:00"},{"stage":"4","stage_start_timestamp":"2023-09-18T05:00:00+02:00"}],"stage":"0","stage_updated":"2023-09-16T22:00:00.342998+02:00"}}}},"area":{"lastUpdate":"2023-09-17T07:26:00.787Z","info":{"events":[{"end":"2023-09-18T02:30:00+02:00","note":"Stage 3","start":"2023-09-18T00:00:00+02:00"},{"end":"2023-09-18T10:30:00+02:00","note":"Stage 4","start":"2023-09-18T08:00:00+02:00"},{"end":"2023-09-18T18:30:00+02:00","note":"Stage 4","start":"2023-09-18T16:00:00+02:00"},{"end":"2023-09-19T02:30:00+02:00","note":"Stage 4","start":"2023-09-19T00:00:00+02:00"},{"end":"2023-09-19T10:30:00+02:00","note":"Stage 4","start":"2023-09-19T08:00:00+02:00"},{"end":"2023-09-19T18:30:00+02:00","note":"Stage 4","start":"2023-09-19T16:00:00+02:00"}],"info":{"name":"Paarl (9)","region":"Western Cape"},"schedule":{"days":[{"date":"2023-09-17","name":"Sunday","stages":[["08:00-10:30"],["00:00-02:30","08:00-10:30"],["00:00-02:30","08:00-10:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-12:30","16:00-18:30"],["00:00-04:30","08:00-12:30","16:00-18:30"],["00:00-04:30","08:00-12:30","16:00-18:30"],["00:00-04:30","08:00-12:30","16:00-20:30"]]},{"date":"2023-09-18","name":"Monday","stages":[["16:00-18:30"],["08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-20:30"],["00:00-02:30","08:00-12:30","16:00-20:30"],["00:00-04:30","08:00-12:30","16:00-20:30"],["00:00-04:30","08:00-12:30","16:00-20:30"]]},{"date":"2023-09-19","name":"Tuesday","stages":[[],["16:00-18:30"],["08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-20:30"],["00:00-02:30","08:00-12:30","16:00-20:30"],["00:00-04:30","08:00-12:30","16:00-20:30"]]},{"date":"2023-09-20","name":"Wednesday","stages":[["00:00-02:30"],["00:00-02:30"],["00:00-02:30","16:00-18:30"],["00:00-02:30","08:00-10:30","16:00-18:30"],["00:00-04:30","08:00-10:30","16:00-18:30"],["00:00-04:30","08:00-10:30","16:00-18:30"],["00:00-04:30","08:00-10:30","16:00-20:30"],["00:00-04:30","08:00-12:30","16:00-20:30"]]},{"date":"2023-09-21","name":"Thursday","stages":[["06:00-08:30"],["06:00-08:30"],["06:00-08:30","22:00-00:30"],["06:00-08:30","14:00-16:30","22:00-00:30"],["06:00-10:30","14:00-16:30","22:00-00:30"],["06:00-10:30","14:00-16:30","22:00-00:30"],["06:00-10:30","14:00-16:30","22:00-00:30"],["06:00-10:30","14:00-18:30","22:00-00:30"]]},{"date":"2023-09-22","name":"Friday","stages":[["14:00-16:30"],["06:00-08:30","14:00-16:30"],["06:00-08:30","14:00-16:30"],["06:00-08:30","14:00-16:30","22:00-00:30"],["06:00-08:30","14:00-18:30","22:00-00:30"],["06:00-10:30","14:00-18:30","22:00-00:30"],["00:00-02:30","06:00-10:30","14:00-18:30","22:00-00:30"],["00:00-02:30","06:00-10:30","14:00-18:30","22:00-00:30"]]},{"date":"2023-09-23","name":"Saturday","stages":[["22:00-00:30"],["14:00-16:30","22:00-00:30"],["06:00-08:30","14:00-16:30","22:00-00:30"],["06:00-08:30","14:00-16:30","22:00-00:30"],["06:00-08:30","14:00-16:30","22:00-00:30"],["06:00-08:30","14:00-18:30","22:00-00:30"],["06:00-10:30","14:00-18:30","22:00-00:30"],["00:00-02:30","06:00-10:30","14:00-18:30","22:00-00:30"]]}],"source":"https://sepush.co.za/"}}},"calc":{"sleeptime":"124","stage":"0","active":false,"next":{"type":"event","start":1694988000000,"end":1694997000000,"stage":"3"}}}

Followed by msg: error TypeError: dates.stages[(EskomSePushInfo.calc.stage - 1)] is not iterable

nicopret1 commented 1 year ago

I modified eskomsepush.js as follows at line 186: // Scheduled downtime has the thing that the time is in locatime // So not just like events, where they are in UTC with an offset let BreakLoop = false; for (const dates of EskomSePushInfo.area.info.schedule.days) { let stageIndex = EskomSePushInfo.calc.stage - 1; if (stageIndex >= 0 && stageIndex < dates.stages.length) { if(Array.isArray(dates.stages[stageIndex])) { for (const schedule of dates.stages[stageIndex]) { const ScheduleStart = Date.parse(dates.date + ' ' + schedule.split('-')[0]); let ScheduleEnd = Date.parse(dates.date + ' ' + schedule.split('-')[1]); if (ScheduleEnd < ScheduleStart) { ScheduleEnd += (24 * 60 * 60 * 1000); } if (now < ScheduleEnd) { BreakLoop = true; // This schedule is either active or will be next if (now >= ScheduleStart) { EskomSePushInfo.calc.active = true; EskomSePushInfo.calc.type = 'schedule'; EskomSePushInfo.calc.start = ScheduleStart; EskomSePushInfo.calc.end = ScheduleEnd; } else { EskomSePushInfo.calc.next = { type: 'schedule', start: ScheduleStart, end: ScheduleEnd, stage: EskomSePushInfo.calc.stage }; } } if (BreakLoop) { break; } } } else { console.warn('Not an array:', dates.stages[stageIndex]); // Warning if not an array } } else { console.warn('Invalid stage index:', stageIndex); // Warning if stage index is out of bounds } if (BreakLoop) { break; } }

The error is indicating that dates.stages[EskomSePushInfo.calc.stage-1] is not an iterable object (for stage 0 loadshedding) at the time this code runs and therefore:

  1. I declared stageIndex before the inner loop to hold the value of EskomSePushInfo.calc.stage - 1.
  2. I added a conditional check to ensure that stageIndex is within a valid range before entering the inner loop.
  3. I added another conditional check to confirm that dates.stages[stageIndex] is an array before starting the inner loop.

It now runs correctly. Below the full updated code:

module.exports = function (RED) {
  'use strict'

  const axios = require('axios')
  const EskomSePushInfo = {
    api: {
      lastUpdate: null,
      info: {}
    },
    status: {
      lastUpdate: null,
      info: {}
    },
    area: {
      lastUpdate: null,
      info: {}
    },
    calc: {}
  }

  function getMinutesToAPIReset () {
    const now = new Date()
    const targetTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 2, 0, 0);
    if (now > targetTime) {
      targetTime.setDate(targetTime.getDate() + 1);
    }
    const timeDiff = targetTime - now;
    const minutesLeft = Math.floor(timeDiff / (1000 * 60))

    return minutesLeft
  }

  function checkAllowance (node) {
    const options = {}
    const headers = { token: node.config.licensekey }

    if (node.config.verbose === true) {
      node.warn('Running function checkAllowance')
    }
    axios.get('https://developer.sepush.co.za/business/2.0/api_allowance',
      { params: options, headers }).then(function (response) {
      EskomSePushInfo.api.info = response.data
      if (EskomSePushInfo.api.lastUpdate === null) {
        EskomSePushInfo.api.lastUpdate = new Date()
        updateSheddingStatus(node)
      }
      EskomSePushInfo.api.lastUpdate = new Date()
    })
      .catch(error => {
        node.warn({ error: error.message })
      })
  }

  function checkStage (node) {
    const options = {}
    const headers = { token: node.config.licensekey }

    if (node.config.verbose === true) {
      let warnstring = 'Running function checkStage'
      if (EskomSePushInfo.status.lastUpdate === null) {
        warnstring += ' - initial run'
      } else {
        warnstring += ' after ' + ((new Date() - EskomSePushInfo.status.lastUpdate) / 60000).toFixed(0) + ' minutes'
      }
      node.warn(warnstring)
    }
    axios.get('https://developer.sepush.co.za/business/2.0/status',
      { params: options, headers }).then(function (response) {
      EskomSePushInfo.status.info = response.data
      EskomSePushInfo.status.lastUpdate = new Date()
      // Call updateSheddingStatus again now we have new data
      updateSheddingStatus(node)
    })
      .catch(error => {
        node.warn({ error: error.message })
      })
  }

  function checkArea (node) {
    const options = { id: node.config.area }
    const headers = { token: node.config.licensekey }
    const url = 'https://developer.sepush.co.za/business/2.0/area'

    if (node.config.verbose === true) {
      let warnstring = 'Running function checkArea'
      if (EskomSePushInfo.area.lastUpdate === null) {
        warnstring += ' - initial run'
      } else {
        warnstring += ' after ' + ((new Date() - EskomSePushInfo.area.lastUpdate) / 60000).toFixed(0) + ' minutes'
      }
      node.warn(warnstring)
    }
    if (node.config.test) {
      options.test = 'current'
    }
    axios.get(url,
      { params: options, headers }).then(function (response) {
      EskomSePushInfo.area.info = response.data
      EskomSePushInfo.area.lastUpdate = new Date()
      // Call updateSheddingStatus again now we have new data
      updateSheddingStatus(node)
    })
      .catch(error => {
        node.warn({ error: error.message })
      })
  }

  function updateSheddingStatus (node, msg) {
    const now = new Date()

    // Check allowance every ten minutes
    if ((msg && msg.payload === 'allowance' ) || EskomSePushInfo.api.lastUpdate === null || (now.getTime() - EskomSePushInfo.api.lastUpdate.getTime()) > 600000) {
      checkAllowance(node)
    }

    // If we don't have API info, we just return
    if (Object.entries(EskomSePushInfo.api.info).length === 0) {
      node.warn('No API info (yet), refusing to continue')
      return
    }

    // The same is true if we have no API calls left
    if (EskomSePushInfo.api.info.allowance.count >= EskomSePushInfo.api.info.allowance.limit) {
      node.warn('No API calls left, not checking status/schedule')
      return
    }

    // Fetching actual information takes 2 calls, so calculate how long until the next API count
    // reset and divide the calls over the day. Wait at least 10 minutes between calls
    if ((EskomSePushInfo.api.info.allowance.limit - EskomSePushInfo.api.info.allowance.count) > 0) {
      EskomSePushInfo.calc.sleeptime = (getMinutesToAPIReset() / ((EskomSePushInfo.api.info.allowance.limit - EskomSePushInfo.api.info.allowance.count) / 2)).toFixed(0)
      if (EskomSePushInfo.calc.sleeptime < 10) { EskomSePushInfo.calc.sleeptime = 10 }
    } else {
      EskomSePushInfo.calc.sleeptime = 30
    }

    if (( msg && msg.payload === 'stage' ) || EskomSePushInfo.status.lastUpdate === null || (now.getTime() - EskomSePushInfo.status.lastUpdate) > (EskomSePushInfo.calc.sleeptime * 60000)) {
      checkStage(node)
    }

    if (( msg && msg.payload === 'area' ) || EskomSePushInfo.area.lastUpdate === null || (now.getTime() - EskomSePushInfo.area.lastUpdate) > (EskomSePushInfo.calc.sleeptime * 60000)) {
      checkArea(node)
    }

    // Now we have all info to continue. Just making sure that all update values are non null.
    if (EskomSePushInfo.api.lastUpdate === null ||
        EskomSePushInfo.status.lastUpdate === null ||
        EskomSePushInfo.area.lastUpdate === null) {
      (node.config.verbose === true) && node.warn('Not enough info to continue.')
      return
    }

    // Determine the current stage
    EskomSePushInfo.calc.stage = EskomSePushInfo.status.info.status[node.config.statusselect].stage

    if (node.config.verbose === true) {
      node.warn('API call status: ' + EskomSePushInfo.api.info.allowance.count + '/' + EskomSePushInfo.api.info.allowance.limit)
      node.warn(EskomSePushInfo)
    }

    // Default to false, overrule of loadshedding is active
    EskomSePushInfo.calc.active = false

    // Are there any events going on?
    if (Object.entries(EskomSePushInfo.area.info.events).length > 0) {
      const EventStart = Date.parse(EskomSePushInfo.area.info.events[0].start)
      const EventEnd = Date.parse(EskomSePushInfo.area.info.events[0].end)
      if (now >= EventStart && now < EventEnd) {
        EskomSePushInfo.calc.type = 'event'
        EskomSePushInfo.calc.active = true
        if (EskomSePushInfo.area.info.events[0].note.match(/Stage (\d+)/i)) {
          EskomSePushInfo.calc.stage = EskomSePushInfo.area.info.events[0].note.match(/Stage (\d+)/i)[1]
        }
        EskomSePushInfo.calc.start = EventStart
        EskomSePushInfo.calc.end = EventEnd
      } else {
        EskomSePushInfo.calc.next = {
          type: 'event',
          start: EventStart,
          end: EventEnd,
          stage: EskomSePushInfo.area.info.events[0].note.match(/Stage (\d+)/i)[1]
        }
      }
    }

    // Scheduled downtime has the thing that the time is in locatime
    // So not just like events, where they are in UTC with an offset
    let BreakLoop = false;
    for (const dates of EskomSePushInfo.area.info.schedule.days) {
      let stageIndex = EskomSePushInfo.calc.stage - 1;
      if (stageIndex >= 0 && stageIndex < dates.stages.length) {
        if(Array.isArray(dates.stages[stageIndex])) {
          for (const schedule of dates.stages[stageIndex]) {
            const ScheduleStart = Date.parse(dates.date + ' ' + schedule.split('-')[0]);
            let ScheduleEnd = Date.parse(dates.date + ' ' + schedule.split('-')[1]);
            if (ScheduleEnd < ScheduleStart) {
              ScheduleEnd += (24 * 60 * 60 * 1000);
            }
            if (now < ScheduleEnd) {
              BreakLoop = true;
              // This schedule is either active or will be next
              if (now >= ScheduleStart) {
                EskomSePushInfo.calc.active = true;
                EskomSePushInfo.calc.type = 'schedule';
                EskomSePushInfo.calc.start = ScheduleStart;
                EskomSePushInfo.calc.end = ScheduleEnd;
              } else {
                EskomSePushInfo.calc.next = {
                  type: 'schedule',
                  start: ScheduleStart,
                  end: ScheduleEnd,
                  stage: EskomSePushInfo.calc.stage
                };
              }
            }
            if (BreakLoop) { break; }
          }
        } else {
          console.warn('Not an array:', dates.stages[stageIndex]); // Warning if not an array
        }
      } else {
        console.warn('Invalid stage index:', stageIndex); // Warning if stage index is out of bounds
      }
      if (BreakLoop) { break; }
    }

    if (EskomSePushInfo.calc.next) {
      EskomSePushInfo.calc.next.duration = (EskomSePushInfo.calc.next.end - EskomSePushInfo.calc.next.start) / 1000
      EskomSePushInfo.calc.next.islong = EskomSePushInfo.calc.next.duration >= (4 * 3600)
      EskomSePushInfo.calc.secondstostatechange = parseInt((EskomSePushInfo.calc.next.start - now) / 1000)
      EskomSePushInfo.calc.next.isHigherStage = EskomSePushInfo.calc.next.stage > EskomSePushInfo.calc.next.stage
    }

    if (EskomSePushInfo.calc.active) {
      EskomSePushInfo.calc.duration = (EskomSePushInfo.calc.end - EskomSePushInfo.calc.start) / 1000
      EskomSePushInfo.calc.islong = EskomSePushInfo.calc.duration >= (4 * 3600)
      EskomSePushInfo.calc.secondstostatechange = parseInt((EskomSePushInfo.calc.end - now) / 1000)
    }

    if (node.config.verbose === true) {
      node.warn(EskomSePushInfo.calc)
    }

    // Send output
    node.send([{
      payload: EskomSePushInfo.calc.active,
      stage: EskomSePushInfo.calc.stage,
      statusselect: node.config.statusselect,
      api: {
        count: EskomSePushInfo.api.info.allowance.count,
        limit: EskomSePushInfo.api.info.allowance.limit
      },
      calc: EskomSePushInfo.calc
    }, {
      stage: EskomSePushInfo.status,
      schedule: EskomSePushInfo.area
    }])

    // And update the status
    let fill = 'green'
    let shape = 'ring'
    let statusText = 'Stage ' + EskomSePushInfo.calc.stage + ': '

    if (EskomSePushInfo.calc.active) {
      fill = 'yellow'
      if (EskomSePushInfo.calc.type === 'event') {
        shape = 'dot'
      }
      if ( EskomSePushInfo.calc.start ) {
        statusText += new Date(EskomSePushInfo.calc.start).toLocaleTimeString([], {timeStyle: 'short'})
        statusText += ' - ' + new Date(EskomSePushInfo.calc.end).toLocaleTimeString([], {timeStyle: 'short'})
      }
    } else {
      if (new Date(EskomSePushInfo.calc.next.start).getUTCDay() !==  now.getUTCDate) {
        statusText += new Date(EskomSePushInfo.calc.next.start).toLocaleString([], {weekday: 'short'}) + ' '
      }
      statusText += new Date(EskomSePushInfo.calc.next.start).toLocaleTimeString([], {timeStyle: 'short'})
      statusText += ' - ' + new Date(EskomSePushInfo.calc.next.end).toLocaleTimeString([], {timeStyle: 'short'})
    }

    statusText += ' (API: ' + EskomSePushInfo.api.info.allowance.count + '/' + EskomSePushInfo.api.info.allowance.limit + ')'
    node.status({
      fill, shape, text: statusText
    })
  }

  function EskomSePush (config) {
    RED.nodes.createNode(this, config)

    const node = this
    node.config = config

    updateSheddingStatus(node)
    const intervalId = setInterval(function () {
      updateSheddingStatus(node)
    }, 60000)

    node.on('input', function(msg) {
      updateSheddingStatus(node, msg)
    })

    node.on('close', function () {
      clearInterval(intervalId)
    })
  }

  RED.nodes.registerType('eskomsepush', EskomSePush)

  RED.httpNode.get('/eskomsepush/search', (req, res) => {
    if (!req.query || !req.query.token || !req.query.search) {
      res.setHeader('Content-Type', 'application/json')
      return res.send('invalid')
    }
    const headers = {
      token: req.query.token
    }
    const options = {
      text: req.query.search
    }

    res.setHeader('Content-Type', 'application/json')
    axios.get('https://developer.sepush.co.za/business/2.0/areas_search',
      { params: options, headers }).then(function (response) {
      return res.send(response.data)
    })
      .catch(error => {
        return res.send({ error: error.message })
      })
  })
  RED.httpNode.get('/eskomsepush/api', (req, res) => {
    if (!req.query.token) {
      res.setHeader('Content-Type', 'application/json')
      return res.send('invalid')
    }
    const headers = {
      token: req.query.token
    }
    const options = {}

    res.setHeader('Content-Type', 'application/json')
    axios.get('https://developer.sepush.co.za/business/2.0/api_allowance',
      { params: options, headers }).then(function (response) {
      return res.send(response.data)
    })
      .catch(error => {
        return res.send({ error: error.message })
      })
  })
}
dirkjanfaber commented 1 year ago

Thanks for de detailed report and update (I've edited it a bit for readability; three backticks helps with multiline pre-formated text in markdown). I'll review the code and create a new release.

nicopret1 commented 1 year ago

Thank you, and thanks for the three backtics hint. I also identified an issue with the sleeptime calculation which caused the free API quota to be depleted. JavaScript is performing a string comparison instead of a numerical one.

To fix it, I used parseFloat() to convert it to a number, like so:

EskomSePushInfo.calc.sleeptime = parseFloat((getMinutesToAPIReset() / ((EskomSePushInfo.api.info.allowance.limit - api_allowance_buffer - EskomSePushInfo.api.info.allowance.count) / 2)).toFixed(0)); if (EskomSePushInfo.calc.sleeptime < 60) { EskomSePushInfo.calc.sleeptime = 60 if (node.config.verbose === true) {node.warn("Calculated sleeptime was less than 60. Set it to 60: " + EskomSePushInfo.calc.sleeptime) } }

Since I also use the API for another function in Home Assistant I introduced "api_allowance_buffer" to enable me to allocate quota for other function. Below the code for the updated function updateSheddingStatus:

  function updateSheddingStatus (node, msg) {
    const now = new Date()
    const api_allowance_buffer = 15

    // Check allowance every ten minutes
    if ((msg && msg.payload === 'allowance' ) || EskomSePushInfo.api.lastUpdate === null || (now.getTime() - EskomSePushInfo.api.lastUpdate.getTime()) > 600000) {
      checkAllowance(node)
    }

    // If we don't have API info, we just return
    if (Object.entries(EskomSePushInfo.api.info).length === 0) {
      node.warn('No API info (yet), refusing to continue')
      return
    }

    // The same is true if we have no API calls left
    if (EskomSePushInfo.api.info.allowance.count >= EskomSePushInfo.api.info.allowance.limit) {
      node.warn('No API calls left, not checking status/schedule')
      return
    }

    // Fetching actual information takes 2 calls, so calculate how long until the next API count
    // reset and divide the calls over the day. Wait at least 60 minutes between calls
    // reduce limit by api_allowance_buffer to cater for units consumed by other API calls
    if (node.config.verbose === true) { node.warn("Minutes to API Reset: " + getMinutesToAPIReset()) }

    if ((EskomSePushInfo.api.info.allowance.limit - EskomSePushInfo.api.info.allowance.count) > 0) {
      EskomSePushInfo.calc.sleeptime = (getMinutesToAPIReset() / ((EskomSePushInfo.api.info.allowance.limit - api_allowance_buffer - EskomSePushInfo.api.info.allowance.count) / 2)).toFixed(0)
      if (node.config.verbose === true) {
        node.warn("API allowance limit: " + EskomSePushInfo.api.info.allowance.limit)
        node.warn("API allowance count: " + EskomSePushInfo.api.info.allowance.count)
        node.warn("Calculated sleeptime: " + EskomSePushInfo.calc.sleeptime)
      }
      EskomSePushInfo.calc.sleeptime = parseFloat((getMinutesToAPIReset() / ((EskomSePushInfo.api.info.allowance.limit - api_allowance_buffer - EskomSePushInfo.api.info.allowance.count) / 2)).toFixed(0));
      if (EskomSePushInfo.calc.sleeptime < 60) { 
          EskomSePushInfo.calc.sleeptime = 60 
          if (node.config.verbose === true) {node.warn("Calculated sleeptime was less than 60. Set it to 60: " + EskomSePushInfo.calc.sleeptime) }
      }
    } else {
      EskomSePushInfo.calc.sleeptime = 120
      if (node.config.verbose === true) { node.warn("Set sleeptime to 120 since allowance count = 0: " + EskomSePushInfo.calc.sleeptime) }
    }

    if (( msg && msg.payload === 'stage' ) || EskomSePushInfo.status.lastUpdate === null || (now.getTime() - EskomSePushInfo.status.lastUpdate) > (EskomSePushInfo.calc.sleeptime * 60000)) {
      checkStage(node)
    }

    if (( msg && msg.payload === 'area' ) || EskomSePushInfo.area.lastUpdate === null || (now.getTime() - EskomSePushInfo.area.lastUpdate) > (EskomSePushInfo.calc.sleeptime * 60000)) {
      checkArea(node)
    }

    // Now we have all info to continue. Just making sure that all update values are non null.
    if (EskomSePushInfo.api.lastUpdate === null ||
        EskomSePushInfo.status.lastUpdate === null ||
        EskomSePushInfo.area.lastUpdate === null) {
      (node.config.verbose === true) && node.warn('Not enough info to continue.')
      return
    }
dirkjanfaber commented 1 year ago

Thanks for all of the improvements. I've added the first to changes to the code. I don't like the api_allowance_buffer being fixed, so I'll adjust that part of the code a bit to be adjustable in the editPanel.

nicopret1 commented 1 year ago

Thank you, I agree api_allowance_buffer should rather be adjustable in the editPanel. The code I posted also did not cater for the case where sleeptime will be negative due to .limit - .count < api_allowance_buffer

nicopret1 commented 1 year ago

Herewith the part that I updated to catch "negative" sleeptime due to api_allowance_buffer:


 // Fetching actual information takes 2 calls, so calculate how long until the next API count
    // reset and divide the calls over the day. Wait at least 60 minutes between calls
    // reduce limit by api_allowance_buffer to cater for units consumed by other API calls
    if (node.config.verbose === true) {
        node.warn("Minutes to API Reset: " + getMinutesToAPIReset())
    }

    let allowanceRemaining = EskomSePushInfo.api.info.allowance.limit - api_allowance_buffer - EskomSePushInfo.api.info.allowance.count

    if (allowanceRemaining > 0) {
        EskomSePushInfo.calc.sleeptime = Math.round(getMinutesToAPIReset() / Math.ceil(allowanceRemaining / 2))
        if (node.config.verbose === true) {
            node.warn("API allowance limit: " + EskomSePushInfo.api.info.allowance.limit)
            node.warn("API allowance count: " + EskomSePushInfo.api.info.allowance.count)
            node.warn("Calculated sleeptime: " + EskomSePushInfo.calc.sleeptime)
        }
        if (EskomSePushInfo.calc.sleeptime < 60) {
            EskomSePushInfo.calc.sleeptime = 60
            if (node.config.verbose === true) {
                node.warn("Calculated sleeptime was less than 60. Set it to 60: " + EskomSePushInfo.calc.sleeptime)
            }
        }
    } else {
        EskomSePushInfo.calc.sleeptime = 120
        if (node.config.verbose === true) {
            node.warn("Set sleeptime to 120 since allowance count is low: " + EskomSePushInfo.calc.sleeptime)
        }
    }`
dirkjanfaber commented 1 year ago

Ok, added that as well as the ability to fill out the limit in the edit panel. Released a new version.

Not too sure about the 60 minutes minimum. So if people start complaining, I might lower that number again (or make it configurable too).

Closing this issue for now. Thanks for your help and ideas. If you think of new things, please file a new issue.

nicopret1 commented 1 year ago

Thank you for your quick response and your excellent work. The Node works very well. I agree that " 60 minutes minimum" can be reset to "10 minutes minimum", especially now that sleeptime is calculated correctly.