exports.id = 'cmd_manager';
exports.title = 'CMD Manager';
exports.group = 'Worksys';
exports.color = '#5D9CEC';
exports.version = '0.0.3';
exports.output = ['red', 'blue', 'yellow', 'blue', 'white', 'green'];
exports.input = 3;
exports.icon = 'cloud-upload';

exports.html = `
<div class="padding">
  <div class="row">
    <div class="col-md-12">
      <div>RPC - run RPC calls</div><br>
    </div>
  </div>
  <div data-jc="textbox" data-jc-path="username" class="m" data-jc-config="required:true">@(User)</div>
  <div class="row">
    <div class="col-md-6 m">
      <div data-jc="textbox" data-jc-path="userpassword" data-jc-config="required:true">@(Password)</div>
    </div>
    <div class="col-md-6 m">
      <div data-jc="textbox" data-jc-path="edge" data-jc-config="required:true">@(My edge)</div>
    </div>
  </div>
</div>
`;
exports.readme = `Manager for CMD calls`;


exports.install = function(instance) {

  const SerialPort = require('serialport');
  const { exec } = require('child_process');
  const { crc16 } = require('easy-crc');
  const { runSyncExec, writeData } = require('./helper/serialport_helper');
  const { bytesToInt, longToByteArray, addZeroBefore, isEmptyObject, emptyJsObject, writeToFile, addToArrayIfUnique } = require('./helper/utils');
  const bitwise = require('bitwise');

  var SunCalc = require('./helper/suncalc');
  const DataToTbHandler = require('./helper/DataToTbHandler');
  const errorHandler = require('./helper/ErrorToServiceHandler');
  const { sendNotification } = require('./helper/notification_reporter');
  const process = require('process');
  const { errLogger, logger, monitor } = require('./helper/logger');

  //for accelerometer purposes 
  const { naklony } = require("../databases/accelerometer_db");

  const dbNodes = TABLE("nodes");
  const dbRelays = TABLE("relays");

  let GLOBALS;
  let SETTINGS;
  let rsPort;
  let tbHandler;

  // runTasks intervals
  const SHORT_INTERVAL = 30;
  const LONG_INTERVAL = 300;

  //send data to following instances:
  const SEND_TO = {
    debug: 0,
    tb: 1,
    http_response: 2,
    dido_controller: 3,
    infoSender: 4,
    dailyReport: 5
  }

  const PRIORITY_TYPES = {
    terminal: 0,
    fw_detection: 1,//reserved only for FW detection - SETTINGS.masterNodeIsResponding
    high_priority: 2,//reserverd only for: read dimming / brightness (after set dimming from platform)
    relay_profile: 3,
    node_broadcast: 4,
    node_profile: 5,
    node_cmd: 6
  }

  const TIME_AFTER_WE_UPDATE_LAST_NODE_COMMUNICATION = 600000; // 10 minutes

  //list of command calls to process. Processing in runTasks function
  let tasks = [];

  let interval = null;//timeout for procesing tasks
  let customTasksInterval = null; // interval for reportEdgeDateTimeAndNumberOfLuminaires();
  let setCorrectTime = null; // interval for setting a correct edgeTime 
  let sendNodeReadout = null; // interval for sending agregate data from node 

  let refFlowdataObj = {};

  //load from settings
  let latitude = 48.70826502;//48.682255758;
  let longitude = 17.28455203;//17.278910807;

  const gmtOffset = 0;

  //ak nie je nastaveny 
  //https://www.tecmint.com/set-time-timezone-and-synchronize-time-using-timedatectl-command/
  //https://stackoverflow.com/questions/16086962/how-to-get-a-time-zone-from-a-location-using-latitude-and-longitude-coordinates

  //priorities for registers
  let priorities = [];

  let minutes = 1;
  priorities["1"] = minutes; // dimming
  priorities["76"] = minutes; // power

  minutes = 5;
  priorities["75"] = minutes; // current
  priorities["79"] = minutes; // energy
  priorities["87"] = minutes; // aktualny cas
  //priorities["84"] = minutes; //accelerometer

  minutes = 10;
  priorities["74"] = minutes; // voltage
  priorities["77"] = minutes; // power factor
  priorities["78"] = minutes; // frequency

  minutes = 60;
  priorities["0"] = minutes; // statecode
  priorities["6"] = minutes; // dusk
  priorities["7"] = minutes; // dawn
  priorities["8"] = minutes; // profile

  minutes = 60 * 24;
  priorities["89"] = minutes; // verzia fw
  priorities["80"] = minutes; // lifetime

  //prikazy kt sa budu spustat na dany node - see config.js in terminal-oms.app. (1 - dimming)
  let listOfCommands = [0, 1, 6, 7, 8, 74, 75, 76, 77, 78, 79, 80, 87, 89];

  let rotary_switch_state;
  let lux_sensor;
  let state_of_breaker = {};//key is line, value is On/Off
  let disconnectedReport = {};//key is tbname, value true/false

  let relaysData;
  let nodesData;

  // holds dusk, dawn (and more) values
  let sunCalcResult;

  //helper container for counting resolved group of commands (commands related to set profile)
  let cmdCounter = {};//key is node, value is counter

  //TODO: is this set working?? According to previous checks, sets do not work
  //if sending of profile to node fails, we send notification and push node into set, so we do not send notification twice
  const nodeProfileSendFail = new Set();

  //we expect to get current temperature in Senica from senica-prod01
  let temperatureInSenica = null;
  let accelerometerInterval = null;

  //NOTE: daily reports senica:
  //rvo_is_on status, we monitor all nodes - if true, power and dimming should be not 0, if false, 24/7 nodes should be 0, other rvo should not respond 
  let rvo_is_on;
  let previous_rvo_is_on_value;
  let handleDailyReport; //setInterval for dailyReportHandler function
  let dailyReport = {}; //data needed for daily report
  let reportToSend = {}; //final report which we send to cloud
  let breakerCounter = null; // status of breakers after flow starts is not in report
  let rvoPeriod; // interval of checking dusk dawn and if rvo_is_on
  let initialReportStatus = false;

  const DAILY_REPORT_HANDLER_TIME = 3_600_000;
  const SET_SUNCALC_RESULT_TIME = 3_600_000;
  const SET_RVO_PERIOD_TIME = 300_000;
  const ADD_NODE_TO_REPORT_TIME = 3_600_000;

  //END OF VARIABLE SETTINGS
  //--------------------------------


  function main() {
    GLOBALS = FLOW.GLOBALS;
    SETTINGS = FLOW.GLOBALS.settings;
    relaysData = GLOBALS.relaysData;
    nodesData = GLOBALS.nodesData;
    latitude = GLOBALS.settings.latitude;
    longitude = GLOBALS.settings.longitude;

    tbHandler = new DataToTbHandler(SEND_TO.tb);
    tbHandler.setSender(exports.title);

    let now = new Date();
    console.log("Cmd-mngr installed", now.toLocaleString("sk-SK"));

    sunCalcResult = calculateDuskDawn();

    handleRsPort();

    customTasksInterval = setInterval(reportEdgeDateTimeAndNumberOfLuminaires, 120_000);
    setTimeout(reportEdgeDateTimeAndNumberOfLuminaires, 4000);

    //dailyReport related
    emptyReportToSend();
    breakerCounter = Object.keys(relaysData).length - 1; // we get number of lines (breakers) except of line 0
    rvoPeriod = setInterval(setRvoPeriod, SET_RVO_PERIOD_TIME);
    setTimeout(setRvoPeriod, 3000);
    setInterval(setSunCalcResult, SET_SUNCALC_RESULT_TIME);
    handleDailyReport = setInterval(dailyReportHandler, DAILY_REPORT_HANDLER_TIME);


    setCorrectTime = setInterval(setCorrectPlcTimeOnceADay, 60000 * 60);  // 1 hour
    setCorrectPlcTimeOnceADay();

    sendNodeReadout = setInterval(sendNodesData, 150_000);
    accelerometerInterval = setInterval(accelerometerData, 60000 * 30); //30 min
  }


  function cmdCounterResolve(address) {
    if (cmdCounter.hasOwnProperty(address)) {
      cmdCounter[address] = cmdCounter[address] - 1;

      let result = cmdCounter[address];
      if (result == 0) delete cmdCounter[address];
      return result;
    }
    return -1;
  }


  function getParams(priority) {
    let params = {};

    //core rpc values
    params.address = 0;//if(recipient === 0) address = 0;
    params.byte1 = 0;//msb, podla dokumentacie data3
    params.byte2 = 0;//podla dokumentacie data2
    params.byte3 = 0;//podla dokumentacie data1
    params.byte4 = 0;//lsb, podla dokumentacie data0
    params.recipient = 0;//0: Master, 1: Slave, 2: Broadcast
    params.register = -1;//register number
    params.rw = 0;//0: read, 1: write

    //other values
    //params.type = "cmd"; "relay" "cmd-terminal" "set_node_profile" "process_profiles"
    //params.tbname = tbname;
    params.priority = PRIORITY_TYPES.node_cmd; //default priority - if more tasks with the same timestamp, we sort them based on priority
    params.timestamp = 0; //execution time - if timestamp < Date.now(), the task is processed
    if (priority != undefined) {
      params.timestamp = priority;
      params.priority = priority;
    }

    params.addMinutesToTimestamp = 0;//repeat task if value is > 0
    // if node regular readout does not respond, we repeat request
    params.repeatCounter = 0;
    //params.timePointName = "luxOff" // "luxOn", "dusk", "dawn", "profileTimepoint"
    //params.info = "";
    //params.debug = true; // will console.log params in writeData response

    return params;
  }


  //nastav profil nodu
  function processNodeProfile(node) {
    if (rotary_switch_state != "Automatic") {
      logger.debug("unable to process profile for node", node, "rotary_switch_state != Automatic");
      return;
    }

    let nodeObj = nodesData[node];
    let line = nodeObj.line;

    if (relaysData[line].contactor == 0) {
      logger.debug("line line is off", line, node);
      return;
    }

    if (nodeObj.processed == 1) {
      //logger.debug("node was already processed", node);
      return;
    }

    let nodeProfile = nodeObj.profile;
    logger.debug("processNodeProfile: start - set profile for ", node, nodeProfile);
    if (nodeProfile) {

      try {
        nodeProfile = JSON.parse(nodeProfile);
      } catch (error) {
        logger.debug("Cmd-mngr: Error parsing node profile", error);
      }

    }

    logger.debug("processNodeProfile", node, line, nodeObj, nodeProfile);

    let timestamp = PRIORITY_TYPES.node_cmd;

    removeTask({ type: "set_node_profile", address: node });

    if (nodeProfile === "") {
      //vypneme profil nodu, posleme cmd
      //Pokiaľ je hodnota rovná 1 – Profil sa zapne, ostatné bity sa nezmenia.
      //Pokiaľ sa hodnota rovná 2 – profil sa vypne, ostatné bity sa nezmenia

      logger.debug("turn off profile");

      let params = getParams(PRIORITY_TYPES.node_cmd);
      params.type = "set_node_profile";
      params.address = node;
      params.byte4 = 96;
      params.recipient = 1;
      params.register = 8;
      params.rw = 1;//write
      params.timestamp = timestamp;
      params.info = 'turn off/reset node profile';

      cmdCounter[node] = 1;

      tasks.push(params);
    }
    else {
      let tasksProfile = [];

      //vypneme profil -  Zapísať hodnotu 32 do registra Time Schedule Settings – reset profilu
      let params = getParams(PRIORITY_TYPES.node_cmd);
      params.type = "set_node_profile";
      params.address = node;
      params.byte4 = 96;
      params.recipient = 1;
      params.register = 8;
      params.rw = 1;//write
      params.timestamp = timestamp;
      params.info = 'turn off node profile';

      tasksProfile.push(params);

      timestamp++;

      logger.debug("processNodeProfile: TS1 Time point a TS1 Time Point Levels ", node);

      //TS1 Time point a TS1 Time Point Levels
      let register = 9;
      for (let i = 0; i < nodeProfile.intervals.length; i++) {
        let obj = nodeProfile.intervals[i];
        //let timePoint = obj.time_point;
        let dim_value = obj.value;


        //Reg 9 až Reg 40

        /*
        Samotný profil sa zapisuje do max. 16 párov – časový bod a úroveň.
        Prázdny profil je vtedy keď časový bod obsahuje hodnotu 0xFFFFFFFF (táto hodnota sa zapíše do registrov keď sa aktivuje reset profilu do registru 8).
        Páry sa prechádzajú časovo zoradené takže teoreticky je jedno v akom poradí sa zapisujú ale je lepšie ich zapisovať v chronologickom poradí od 13:00.
        Časový bod má formát:
        Byte 3: hodiny  Byte 2: minúty  Byte 1: sekundy   Byte 0 – rezervované
        Register úrovne má rovnaký formát ako dimming register (Reg 1). 
        */

        let start_time = obj.start_time;
        let t = start_time.split(":");
        //if(timePoint != undefined) t = timePoint.split(":");
        //else t = [0,0];

        logger.debug("processNodeProfile: TS1 Time point ", (i + 1), node);

        params = getParams(PRIORITY_TYPES.node_cmd);
        params.type = "set_node_profile";
        params.address = node;
        params.byte1 = parseInt(t[0]);//hh
        params.byte2 = parseInt(t[1]);//mm
        params.recipient = 1;
        params.register = register;
        params.rw = 1;//write
        params.timestamp = timestamp;
        params.addMinutesToTimestamp = 0;
        params.info = 'TS1 Time point ' + (i + 1);

        tasksProfile.push(params);

        register++;
        timestamp++;

        params = getParams(PRIORITY_TYPES.node_cmd);
        params.type = "set_node_profile";
        params.address = node;
        params.byte4 = parseInt(dim_value) + 128;//
        params.recipient = 1;
        params.register = register;
        params.rw = 1;//write
        params.timestamp = timestamp;
        params.addMinutesToTimestamp = 0;
        params.info = 'TS1 Time point Levels ' + (i + 1);

        tasksProfile.push(params);

        register++;
        timestamp++;
      }

      //Threshold lux level for DUSK/DAWN
      {

        logger.debug("processNodeProfile: Threshold lux level for DUSK/DAWN", node);

        let params = getParams();
        params.type = "set_node_profile";
        params.address = node;
        params.register = 96;
        params.recipient = 1;
        params.rw = 1;//write
        params.timestamp = timestamp;
        params.info = "Threshold lux level for DUSK/DAWN";

        if (nodeProfile.dusk_lux_sensor) {
          let v = nodeProfile.dusk_lux_sensor_value;
          let ba = longToByteArray(v);

          params.byte1 = ba[1];//msb
          params.byte2 = ba[0];
        }

        if (nodeProfile.dawn_lux_sensor) {
          let v = nodeProfile.dawn_lux_sensor_value;
          let ba = longToByteArray(v);

          params.byte3 = ba[1];//msb
          params.byte4 = ba[0];
        }

        tasksProfile.push(params);
        timestamp++;

      }

      //DUSK/DAWN max. adjust period
      {

        logger.debug("processNodeProfile: DUSK/DAWN max. adjust period", node);

        let params = getParams();
        params.type = "set_node_profile";
        params.address = node;
        params.register = 97;
        params.recipient = 1;
        params.rw = 1;//write
        params.timestamp = timestamp;
        params.info = "DUSK/DAWN max. adjust period";

        if (nodeProfile.astro_clock) {
          let v = nodeProfile.dusk_lux_sensor_time_window;
          let ba = longToByteArray(v);

          params.byte1 = ba[1];//msb
          params.byte2 = ba[0];
        }

        if (nodeProfile.astro_clock) {
          let v = nodeProfile.dawn_lux_sensor_time_window;
          let ba = longToByteArray(v);

          params.byte3 = ba[1];//msb
          params.byte4 = ba[0];
        }

        tasksProfile.push(params);
        timestamp++;

      }

      //Static offset
      {

        //Statický offset pre časy úsvitu a súmraku. Byte 1 je pre DUSK, Byte 0 je pre DAWN. Formát:
        //Bity 0 – 6: hodnota v minútach
        //Bit 7:  znamienko (1 – mínus)

        logger.debug("processNodeProfile: Static offset", node);

        let params = getParams(PRIORITY_TYPES.node_cmd);
        params.type = "set_node_profile";
        params.address = node;
        params.register = 98;
        params.recipient = 1;
        params.rw = 1;//write
        params.timestamp = timestamp;
        params.info = "Static offset";

        if (nodeProfile.astro_clock) {
          let dusk_astro_clock_offset = parseInt(nodeProfile.dusk_astro_clock_offset);
          let dawn_astro_clock_offset = parseInt(nodeProfile.dawn_astro_clock_offset);

          if (dusk_astro_clock_offset < 0) {
            params.byte3 = (dusk_astro_clock_offset * -1) + 128;
          }
          else {
            params.byte3 = dusk_astro_clock_offset;
          }

          if (dawn_astro_clock_offset < 0) {
            params.byte4 = (dawn_astro_clock_offset * -1) + 128;
          }
          else {
            params.byte4 = dawn_astro_clock_offset;
          }
        }

        tasksProfile.push(params);
        timestamp++;
      }

      logger.debug("Time schedule settings - turn on", node);

      params = getParams(PRIORITY_TYPES.node_cmd);
      params.type = "set_node_profile";
      params.address = node;
      params.register = 8;
      params.recipient = 1;
      params.rw = 1;//write

      //Time schedule settings
      let bits = [];

      //Byte 0 (LSB):
      //Bit 0 (LSB) – zapnutie/vypnutie profilov ako takých (1 – zapnuté). 
      bits.push(1);
      //Bit 1 – 3     - zatiaľ nepoužité (zapisovať 0)
      bits.push(0);
      bits.push(0);
      bits.push(0);
      if (nodeProfile.astro_clock == true) {
        //Bit 4 – ak je nastavený profil sa riadi podľa astrohodín, a je 0 tak profil je jednoduchý
        bits.push(1);
      }
      else bits.push(0);

      //Bit 5 – zápis 1 spôsobí reset nastavení profilu (nastavenie prázdneho profilu)
      bits.push(0);

      //Bity 6-7    - zatiaľ nepoužité
      bits.push(0);
      bits.push(0);

      params.byte4 = bitwise.byte.write(bits.reverse());

      //Byte 2 – nastavenie pre lux senzor:
      bits = [];

      //Bit 0 (LSB) – riadenie súmraku podľa lux senzoru (1 – zapnuté). Súmrak sa môže posúvať v rámci  času v registri 97 podľa intenzity osvetlenia
      if (nodeProfile.dusk_lux_sensor == true)//sumrak
      {
        bits.push(1);
      }
      else bits.push(0);

      //Bit 1 - riadenie úsvitu podľa lux senzoru (1 – zapnuté). Úsvit sa môže posúvať v rámci  času v registri 97 podľa intenzity osvetlenia
      if (nodeProfile.dawn_lux_sensor == true)//usvit
      {
        bits.push(1);
      }
      else bits.push(0);

      //Bit 2 – zdroj pre hodnotu luxov – 0 – RVO posiela hodnoty zo svojho luxmetra, 1 – node má pripojený svoj vlastný lux meter.
      bits.push(0);//zatial neimplementovane

      //Bit 3 – 7  - nepoužité
      bits.push(0);
      bits.push(0);
      bits.push(0);
      bits.push(0);
      bits.push(0);

      params.byte2 = bitwise.byte.write(bits.reverse());
      params.timestamp = timestamp;
      params.info = "Time schedule settings - turn on";

      tasksProfile.push(params);

      //zaver
      cmdCounter[node] = tasksProfile.length;

      //tasks.push(tasksProfile);
      tasks = tasks.concat(tasksProfile);

    }

    logger.debug("finished set profile for ", node);

    console.log("proces profile finished *********************")
  }


  function cleanUpRefFlowdataObj() {
    let now = new Date();
    let timestamp = now.getTime();

    //clear old refFlowdata references
    let keys = Object.keys(refFlowdataObj);
    for (let i = 0; i < keys.length; i++) {
      let timestampKey = keys[i];

      if ((timestamp - timestampKey) > 60 * 1000) {
        console.log("cleanUpRefFlowdataObj delete", timestampKey);
        delete refFlowdataObj[timestampKey];
      }
    }
  }


  function removeTask(obj) {
    let keys = Object.keys(obj);
    tasks = tasks.filter((task) => {

      let counter = 0;
      for (let i = 0; i < keys.length; i++) {
        let key = keys[i];
        if (task.hasOwnProperty(key) && obj.hasOwnProperty(key)) {
          if (task[key] == obj[key]) counter++;
        }
      }

      if (counter == keys.length) return false;
      return true;
    });
  }


  process.on('uncaughtException', function(err) {
    //TODO send to service

    errLogger.error('uncaughtException:', err.message)
    errLogger.error(err.stack);

    errorHandler.sendMessageToService(err.message + "\n" + err.stack, 0, "js_error");
    //process.exit(1);
  })

  //te();//force error


  function processAllNodeProfilesOnLine(line) {
    for (let k in nodesData) {
      if (line == nodesData[k].line) {
        let node = nodesData[k].node;
        let processed = nodesData[k].processed;

        if (!processed) processNodeProfile(node);
        //else logger.debug( `Node ${node} profile for line ${nodesData[k].line} was already processed`);
      }
    }
  }


  function loadRelaysData(line) {
    for (const [key, value] of Object.entries(relaysData)) {
      if (key == "0") continue;
      if (line != undefined) {
        //ak sa jedna o update profilu linie - pozor dido_controller posiela command pre loadRelaysData
        if (line != value.line) continue;
      }

      if (value.contactor == 1) processAllNodeProfilesOnLine(value.line);
    }
  }


  function reportOnlineNodeStatus(line) {
    //Po zapnutí línie broadcastovo aktualizovať predtým čas a o 3 sek neskor - status, brightness

    logger.debug("Cmd-mngr: ----->reportOnlineNodeStatus for line", line);

    const d = new Date();

    // broadcast actual time
    let params = getParams();
    params.address = 0xffffffff;//Broadcast
    params.byte1 = d.getHours();
    params.byte2 = d.getMinutes();
    params.recipient = 2;//2 broadcast, address = 0
    params.register = 87;//Actual time
    params.rw = 1;//write
    params.type = "node-onetime-write";
    params.timestamp = d.getTime() + 30000;
    params.info = "run broadcast: Actual time";
    //params.debug = true;

    tasks.push(params);

    let sec = 3;
    setTimeout(function() {
      //Po zapnutí línie - spraviť hromadný refresh stavu práve zapnutých svietidiel

      let time = Date.now();

      for (let k in nodesData) {

        //potrebujem nody k danej linii
        if (line == nodesData[k].line || line == undefined) {

          let tbname = nodesData[k].tbname;
          let node = nodesData[k].node;
          let status = "NOK";

          // if status of node was "OK" before switching it off, we set the node's time_of_last_communication on time, it was switched on again and send OK status to tb.
          if (nodesData[k].node_status_before_offline === true || nodesData[k].status === true) {
            status = "OK";
            nodesData[k].time_of_last_communication = time;
          }

          nodesData[k].readout.status = status;

          updateNodeStatus(k, status === "OK" ? true : false);
          if (nodesData[k].hasOwnProperty("node_status_before_offline")) delete nodesData[k].node_status_before_offline;
          sendTelemetry({ status: status }, tbname, time);

          //vyreportovanie dimming, current, input power pre liniu pre vsetky nody
          //Prud
          {
            let params = getParams();

            params.type = "node-onetime-read";
            params.tbname = tbname;
            params.address = node;
            params.register = 75;//prud
            params.recipient = 1;//slave
            params.rw = 0;//read
            params.timestamp = time + 4000;
            params.info = 'read current';
            //params.debug = true;
            tasks.push(params);
          }

          //vykon
          {
            let params = getParams();

            params.type = "node-onetime-read";
            params.tbname = tbname;
            params.address = node;
            params.register = 76;//výkon
            params.recipient = 1;//slave
            params.rw = 0;//read
            params.timestamp = time + 4100;
            params.info = 'read power';
            //params.debug = true;

            tasks.push(params);
          }
          //dimming
          {
            let params = getParams();

            params.type = "node-onetime-read";
            params.tbname = tbname;
            params.address = node;
            params.register = 1;//dimming
            params.recipient = 1;//slave
            params.rw = 0;//read
            params.timestamp = time + 4200;
            params.info = 'read dimming';
            //params.debug = true;

            tasks.push(params);
          }

        }
      }

    }, sec * 1000);
  }


  function reportOfflineNodeStatus(line) {

    logger.info("Cmd-mngr: ------>reportOffLineNodeStatus for line ", line);

    values = {};
    values["dimming"] = 0;//brightness
    values["power"] = 0;//výkon
    values["current"] = 0;//prúd
    values["status"] = "OFFLINE";

    const date = Date.now();

    Object.keys(nodesData).forEach(node => {

      //potrebujem nody k danej linii
      if (line == nodesData[node].line || line == undefined) {

        let tbname = nodesData[node].tbname;
        let nodeStatus = nodesData[node].status;

        //in case we have reported offline node status, we return (continue with next node)
        if (nodeStatus === "OFFLINE") return;

        nodesData[node].node_status_before_offline = nodeStatus;
        nodesData[node].status = "OFFLINE";
        nodesData[node].readout = {};

        sendTelemetry({ ...values }, tbname, date);
      }
    });

  }


  function turnLine(onOrOff, line, info) {
    let obj = {
      line: line,
      command: onOrOff,
      info: info
    };

    //to assure, handleDailyReportFunction do not run before rvo_is_on variable changes. So after every line switch we reset the timer
    clearInterval(handleDailyReport);
    handleDailyReport = setInterval(dailyReportHandler, DAILY_REPORT_HANDLER_TIME);

    //logger.debug("linia", line, obj);
    instance.send(SEND_TO.dido_controller, obj);
  }


  function detectIfResponseIsValid(bytes) {
    //ak sa odpoved zacina 0 - je to v poriadku, inak je NOK
    let type = "RESPONSE";
    if (bytes.length == 1) type = "BROADCAST"; // odpoved z rsPortu na broadcast command: ["broadcast"]
    else if (bytes[4] == 0) type = "RESPONSE";
    else if (bytes[4] == 1) type = "ERROR";
    else if (bytes[4] == 2) type = "EVENT";
    else type = "UNKNOWN";

    let message = "OK";
    let error = "";
    if (type == "BROADCAST") return { message, type, error };

    let crc = crc16('ARC', bytes.slice(0, 9));
    let c1 = (crc >> 8) & 0xFF;
    let c2 = crc & 0xFF;

    if (c1 != bytes[9]) {
      //CRC_ERROR
      message = "NOK";
      error = "CRC_ERROR c1";
      instance.send(SEND_TO.debug, "CRC_ERROR c1");
    }

    if (c2 != bytes[10]) {
      //CRC_ERROR
      message = "NOK";
      error = "CRC_ERROR c2";
      instance.send(SEND_TO.debug, "CRC_ERROR c2");
    }

    //crc error
    if (type != "RESPONSE") {
      instance.send(SEND_TO.debug, bytes);
      instance.send(SEND_TO.debug, "RESPONSE " + type + " - " + bytes[4]);

      //logger.debug(SEND_TO.debug, "RESPONSE " + type + " - " + bytes[4], bytes);

      error = "type is: " + type;

      message = "NOK";
    }

    return { message, type, error };
  }


  //BUILD TASKS//
  function buildTasks(params) {

    //return;
    console.log("buidTAaasks start ****************", params);
    monitor.info("buildTasks - params", params);

    let processLine;  //defined line
    let init = false;
    let processLineProfiles = true;
    let processBroadcast = true;
    let processNodes = true;

    if (params == undefined) {
      init = true;
      tasks = [];
      logger.debug("-->buildTasks clear tasks");
    }
    else {
      processLineProfiles = params.processLineProfiles;
      processBroadcast = false;
      processNodes = false;

      processLine = params.line;
    }

    let now = new Date();

    //process line profiles
    if (processLineProfiles) {

      let keys = Object.keys(relaysData);

      for (let i = 0; i < keys.length; i++) {

        let line = parseInt(keys[i]);
        let profilestr = relaysData[line].profile;

        if (processLine != undefined) {
          if (processLine != line) continue;
        }

        try {

          /**
           * we process line profiles: timepoints, astro clock, lux_sensor, offsets ...
           */
          if (profilestr === "") throw ("Profile is not defined");
          let profile = JSON.parse(profilestr);
          if (Object.keys(profile).length === 0) throw ("Profile is empty");

          monitor.info("buildTasks: profile for line", line);
          monitor.info("profile:", profile);

          let time_points = profile.intervals;

          // add name to regular profile timepoint and delete unused end_time key:
          time_points.forEach(point => {
            point.name = "profileTimepoint"
            delete point.end_time;
          });

          //monitor.info("buildTasks: time_points", time_points);


          /**
           * if astro_clock is true, we create timepoints, that switch on/off relays accordingly. 
           * we need to manage, astro clock timepoints has the greatest priority - normal timepoints will not switch off/on lines before dusk or dawn
           * if dawn/dusk_lux_sensor is true, it has higher priority than astro_clock switching
          */
          if (profile.astro_clock == true) {

            // if astro clock true, we remove all regular profile points
            time_points = [];

            //TODO: when astro clock == true, is offset calculated ???
            //let duskAstroClockOffset = profile.dusk_astro_clock_offset;
            //let dawnAstroClockOffset = profile.dawn_astro_clock_offset;
            //console.log("line astro clock offsets dusk a dawn: ", duskAstroClockOffset, dawnAstroClockOffset);
            // maybe add dusk dawn offset to calculateDuskDawn ?? what dusk dawn will be if dawn_lux_sensor == true??
            //let sunCalcResult = calculateDuskDawn(new Date(), line, duskAstroClockOffset, dawnAstroClockOffset);

            let sunCalcResult = calculateDuskDawn(new Date(), line);

            // adding dusk dawn to timpoints
            if (profile.dawn_lux_sensor == false) time_points.push({ "start_time": sunCalcResult["dawn"], "value": 0, "name": "dawn" });
            if (profile.dusk_lux_sensor == false) time_points.push({ "start_time": sunCalcResult["dusk"], "value": 1, "name": "dusk" });

            //if dusk/dawn is true, lines will switch on/off according to lux_sensor value. In case it fails, we create lux_timepoints, to make sure lines will switch on/off (aby nam to nezostalo svietit)
            //force to turn off after timestamp: dawn + dawn_lux_sensor_time_window
            if (profile.dawn_lux_sensor == true) {
              let [ahours, aminutes] = sunCalcResult["dawn"].split(':');
              let ad = new Date();
              ad.setHours(parseInt(ahours), parseInt(aminutes) + profile.dawn_lux_sensor_time_window, 0);

              let strDate = ad.getHours() + ":" + ad.getMinutes();
              time_points.push({ "value": 0, "start_time": strDate, "name": "luxOff" });
            }

            if (profile.dusk_lux_sensor == true) {
              let [ahours, aminutes] = sunCalcResult["dusk"].split(':');
              let ad = new Date();
              ad.setHours(parseInt(ahours), parseInt(aminutes) + profile.dusk_lux_sensor_time_window, 0);

              let strDate = ad.getHours() + ":" + ad.getMinutes();
              time_points.push({ "value": 1, "start_time": strDate, "name": "luxOn" });
              //time_points.push({"value": 1, "start_time": "15:19", "name": "luxOn"}); //testing
            }
          }

          //sort time_points
          time_points.sort(function(a, b) {

            let [ahours, aminutes] = a.start_time.split(':');
            let [bhours, bminutes] = b.start_time.split(':');

            let ad = new Date();
            ad.setHours(parseInt(ahours), parseInt(aminutes), 0);

            let bd = new Date();
            bd.setHours(parseInt(bhours), parseInt(bminutes), 0);

            return ad.getTime() - bd.getTime();
          });

          console.log("line timepoints ........", time_points);

          let currentValue = 0;
          if (time_points.length > 0) currentValue = time_points[time_points.length - 1].value;

          monitor.info("-->comming events turn on/off lines:");
          for (let t = 0; t < time_points.length; t++) {

            let start_time = new Date();
            let [hours, minutes] = time_points[t].start_time.split(':');
            start_time.setHours(parseInt(hours), parseInt(minutes), 0);

            //task is in the past
            if (now.getTime() > start_time.getTime()) {
              currentValue = time_points[t].value;

              //timepoint is in past, we add 24 hours
              start_time.setDate(start_time.getDate() + 1);
            }

            let params = getParams();
            params.type = "relay";
            params.line = parseInt(line);
            params.value = time_points[t].value;
            params.tbname = relaysData[line].tbname;
            params.timestamp = start_time.getTime();

            // it timepoints are not calculated (dawn, dusk, lux_timepoint), but static points in line profile, we just repeat the task every day
            if (time_points[t].name == "profileTimepoint") params.addMinutesToTimestamp = 24 * 60;

            //astro timepoints will be recalculated dynamically:
            params.timePointName = time_points[t].name;

            // if astro timepoint, we save time window:
            if (['luxOn', 'luxOff', 'dusk', 'dawn'].includes(params.timePointName)) {
              params.dawn_lux_sensor_time_window = profile.dawn_lux_sensor_time_window;
              params.dusk_lux_sensor_time_window = profile.dusk_lux_sensor_time_window;
            }

            if (params.value == 0) params.info = `${params.timePointName}: turn off line: ` + line;
            else if (params.value == 1) params.info = `${params.timePointName}: turn on line: ` + line;

            params.debug = true;

            //turn on/off line
            tasks.push(params);
            monitor.info("TimePoint params: ", params.info, start_time);
          }

          monitor.info("-->time_points final", line, time_points);

          //ensure to turn on/off according to calculated currentValue
          let params = getParams();
          params.type = "relay";
          params.line = parseInt(line);
          params.tbname = relaysData[line].tbname;
          params.value = currentValue;
          params.timestamp = i;
          params.debug = true;

          //logger.debug(now.toLocaleString("sk-SK"));
          monitor.info("-->currentValue for relay", line, currentValue);

          //turn on/off line
          if (params.value == 0) params.info = "turn off line on startup: " + line;
          else if (params.value == 1) params.info = "turn on line on startup: " + line;

          tasks.push(params);

        } catch (error) {
          if (profilestr !== "") {
            //errLogger.error(profilestr, error);
            console.log(`Cmd_mngr: Unable to process line profile ${line}. Error: `, error);
            errorHandler.sendMessageToService(profilestr + "-" + error, 0, "js_error");
          } else {
            turnLine("off", line, "No line profile. Switching it off on startup");
          }
        }

      }
      //logger.debug("tasks:");
      //logger.debug(tasks);
    }


    //NOTE: PROCESS DEFAULT BROADCASTS - Time of dusk, Time of dawn, Actual Time
    if (processBroadcast) {

      let d = new Date();
      let time = d.getTime();
      //let sunCalcResult = calculateDuskDawn();

      {
        let params = getParams();

        params.address = 0xffffffff;//broadcast
        params.byte1 = sunCalcResult["dusk_hours"];
        params.byte2 = sunCalcResult["dusk_minutes"];
        params.recipient = 2;//2 broadcast, 
        params.register = 6;//Time of dusk
        params.rw = 1;//write
        params.type = "node-regular-write";
        params.timestamp = time + 60000;
        params.addMinutesToTimestamp = 60 * 3; //kazde 3 hodiny zisti novy dusk
        params.info = "Broadcast-duskTime";

        tasks.push(params);
      }

      {
        let params = getParams();

        params.address = 0xffffffff;//broadcast
        params.byte1 = sunCalcResult["dawn_hours"];
        params.byte2 = sunCalcResult["dawn_minutes"];
        params.recipient = 2; //2 broadcast
        params.register = 7;//Time of dawn
        params.rw = 1;//write
        params.type = "node-regular-write";
        params.timestamp = time + 60001;
        params.addMinutesToTimestamp = 60 * 3; //kazde 3 hodiny zisti novy dawn
        params.info = "Broadcast-dawnTime";

        tasks.push(params);
      }

      {
        let params = getParams();

        params.address = 0xffffffff;//broadcast
        params.byte1 = d.getHours();
        params.byte2 = d.getMinutes();
        params.recipient = 2; //2 broadcast
        params.register = 87;//Actual time
        params.rw = 1;//write
        params.type = "node-regular-write";
        params.timestamp = time + 60002;
        params.addMinutesToTimestamp = 5;
        params.info = "run broadcast: Actual time";

        tasks.push(params);
      }

    }

    //process nodes & tasks - read node's data
    if (processNodes) {

      let time = Date.now();

      for (let k in nodesData) {
        let address = parseInt(k);
        let tbname = nodesData[k].tbname;
        let register = 0;

        for (let i = 0; i < listOfCommands.length; i++) {

          register = listOfCommands[i];
          let addMinutesToTimestamp = priorities[register];

          let params = getParams();

          params.address = address;
          params.recipient = 1;
          params.register = register;
          params.type = register == 1 ? "node-dimming-read" : "node-regular-read";
          params.tbname = tbname;
          params.timestamp = time + 5000 + i * 500 + addMinutesToTimestamp * 1000; //to make slight time difference
          params.addMinutesToTimestamp = addMinutesToTimestamp;
          params.info = "Node regular read command";

          tasks.push(params);
        }

      }
    }


    //niektore ulohy sa vygeneruju iba 1x pri starte!!!
    if (!init) return;


    //Master node FW version - modifies SETTINGS.masterNodeIsResponding
    {
      let params = getParams();
      params.type = "cmd-master";
      params.register = 4;
      params.address = 0;
      params.timestamp = 0;
      params.addMinutesToTimestamp = 5;
      params.tbname = SETTINGS.rvoTbName;
      params.info = "Master node FW verzia";
      //params.debug = true;

      tasks.push(params);
    }

    //kazdu hodinu skontrolovat nastavenie profilov
    {
      let params = getParams();
      params.type = "process_profiles";
      params.timestamp = Date.now() + 60001;
      params.addMinutesToTimestamp = 60;//60 = every hour
      params.info = "detekcia nespracovaných profilov linie a nodov";
      //params.debug = true;

      tasks.push(params);
    }

    monitor.info("tasks created:", tasks.length);
  }


  /**
   * We process line profile, where "astro_clock": true
   * example profile:
   * 
        "dawn_lux_sensor": true,
        "dusk_lux_sensor": true,
        "dawn_lux_sensor_value": 5,
        "dusk_lux_sensor_value": 5,
        "dawn_astro_clock_offset": 0,
        "dusk_astro_clock_offset": 10,
        "dawn_lux_sensor_time_window": 30,
        "dusk_lux_sensor_time_window": 30,
        "dawn_astro_clock_time_window": 60,
        "dusk_astro_clock_time_window": 60

   * if dawn: if currentTimestamp is in timewindow "dawnTime + and - dawn_lux_sensor_time_window" and lux value >= lux_sensor_value, we switch off the line.
   * if dusk: we do oposite
   * 
   * dawn: usvit - lux je nad hranicou - vypnem
   * dusk: sumrak - lux je pod hranicou - zapnem 
   */
  function turnOnOffLinesAccordingToLuxSensor(lux_sensor_value) {

    let now = new Date();
    let currentTimestamp = now.getTime();
    let keys = Object.keys(relaysData);

    for (let i = 0; i < keys.length; i++) {

      let line = keys[i]; //line is turned off by default
      let profilestr = relaysData[line].profile;
      const contactor = relaysData[line].contactor;

      try {

        let profile = JSON.parse(profilestr);
        if (Object.keys(profile).length === 0) throw ("turnOnOffLinesAccordingToLuxSensor - profile is not defined");

        if (profile.astro_clock == true) {
          let sunCalcResult = calculateDuskDawn(now, line);

          //usvit
          if (profile.dawn_lux_sensor == true) {
            let lux_sensor_time_window1 = sunCalcResult.dawn_time - (parseInt(profile.dawn_lux_sensor_time_window) * 1000 * 60); // LUX_SENSOR_TIME_WINDOW x 1000 x 60 --> dostaneme odpocet/pripocitanie minut
            let lux_sensor_time_window2 = sunCalcResult.dawn_time + (parseInt(profile.dawn_lux_sensor_time_window) * 1000 * 60);

            if (currentTimestamp >= lux_sensor_time_window1 && currentTimestamp <= lux_sensor_time_window2) {
              if (lux_sensor_value > profile.dawn_lux_sensor_value) {
                if (contactor) turnLine("off", line, "Profile: dawn - turnOff line according to lux sensor");
              }
            }
          }

          //sumrak
          if (profile.dusk_lux_sensor == true) {
            let lux_sensor_time_window1 = sunCalcResult.dusk_time - (parseInt(profile.dusk_lux_sensor_time_window) * 1000 * 60);
            let lux_sensor_time_window2 = sunCalcResult.dusk_time + (parseInt(profile.dusk_lux_sensor_time_window) * 1000 * 60);

            if (currentTimestamp >= lux_sensor_time_window1 && currentTimestamp <= lux_sensor_time_window2) {
              if (lux_sensor_value < profile.dusk_lux_sensor_value) {
                if (!contactor) turnLine("on", line, "Profile: dusk - turnOn line according to lux sensor");
              }
            }
          }

        }

      } catch (error) {
        if (profilestr !== "") monitor.info('Error parsing profile in turnOnOffLinesAccordingToLuxSensor', error);
      }

    }

  }

  /**
   * function updates status and time_of_last_communication of node in the dbNodes
   * it only updates if conditions are met
   * it only updates time_of_last_communication of node, if the last written time was more than 10 minutes ago (600000 miliseconds)
   * if newStatus of node is always receiving false, and it is already for more than SETTINGS.node_status_nok_time value, we update status to "NOK" in tb
   * function returns true, if status of node needs to be updated in TB (newStatus attribute is false in this case).
  */
  function updateNodeStatus(node, newStatus) {
    //MASTER
    if (node == 0) return;

    let nodeObj = nodesData[node];
    if (nodeObj == undefined) return;

    let nodeCurrentStatus = nodeObj.status;
    const now = Date.now();

    let data = null;

    if (nodeCurrentStatus === "OFFLINE") {
      data = { status: newStatus };
      nodeDbStatusModify(node, data);
      return;
    }
    else if (newStatus == true && nodeCurrentStatus == true && nodeObj.time_of_last_communication > now - TIME_AFTER_WE_UPDATE_LAST_NODE_COMMUNICATION) return;
    else if (newStatus == true && nodeCurrentStatus == true && nodeObj.time_of_last_communication < now - TIME_AFTER_WE_UPDATE_LAST_NODE_COMMUNICATION) {
      data = { time_of_last_communication: now };
      nodeDbStatusModify(node, data);
      return;
    }
    else if (newStatus == false && nodeCurrentStatus == false) return true;
    else if (newStatus == false && nodeCurrentStatus == true) {
      if (nodeObj.time_of_last_communication + SETTINGS.node_status_nok_time > now) return;
      else {
        data = { status: newStatus };
        nodeDbStatusModify(node, data);
        return true;
      }
    }
    else if (newStatus == true && nodeCurrentStatus == false) {
      data = { status: newStatus, time_of_last_communication: now };
      nodeDbStatusModify(node, data);
      return;
    }

  }


  function nodeDbStatusModify(node, data) {
    dbNodes.modify(data).where("node", node).make(function(builder) {
      builder.callback(function(err, response) {
        if (!err) {
          nodesData[node] = { ...nodesData[node], ...data };
        }
      });
    });
  }


  function processDailyReport() {

    const now = Date.now();

    for (const [key, value] of Object.entries(dailyReport)) {

      if (["name", "time", "dusk_and_dawn"].includes(key)) continue;

      let poleNumber = nodesData[key]["pole_number"];
      let line = dailyReport[key].line;
      let nodeType = nodesData[key]["node_type"];
      let fullNodeName = key // key == nodeNumber

      if (poleNumber && nodeType) {
        fullNodeName = SETTINGS.rvo_number + "/" + poleNumber + "_" + line + "L_" + key + "_" + nodeType;
      } else {
        fullNodeName = key;
      }

      if (rvo_is_on) {

        if (value.initialTs) {
          if (value.initialTs + ADD_NODE_TO_REPORT_TIME < now) {
            addToArrayIfUnique(reportToSend["night_no_data"], fullNodeName);
            console.log('report nedostava ziadne data uz hodinu', fullNodeName);
            value.initialTs = now;
          }
        }
        if (value.dimmingIsZeroTime) {
          if (value.dimmingIsZeroTime + ADD_NODE_TO_REPORT_TIME < now) {
            addToArrayIfUnique(reportToSend["night_dimming=0"], fullNodeName);
            console.log("report node dimming je 0 ale ma svietit", fullNodeName);
            value.dimmingIsZeroTime = now;
          }
        }
        if (value.powerIsZeroTime) {
          if (value.powerIsZeroTime + ADD_NODE_TO_REPORT_TIME < now) {
            addToArrayIfUnique(reportToSend["night_power=0"], fullNodeName);
            console.log("report node power je 0 ale ma svietit", fullNodeName);
            value.powerIsZeroTime = now;
          }
        }

      } else {

        let contactorStatus = relaysData[line].contactor;
        if (contactorStatus === 1) {

          if (value.initialTs) {
            if (value.initialTs + ADD_NODE_TO_REPORT_TIME < now) {
              addToArrayIfUnique(reportToSend["day_24/7_no_data"], fullNodeName);
              console.log('node je na 24/7 ale nedostava ziadne data uz hodinu', fullNodeName);
              value.initialTs = now;
            }
          }
          if (value.dimmingIsOnButShouldBeOffTime) {
            if (value.dimmingIsOnButShouldBeOffTime + ADD_NODE_TO_REPORT_TIME < now) {
              addToArrayIfUnique(reportToSend["day_24/7_dimming>0"], fullNodeName);
              console.log("report dimming je > 0 ale nema svietit", fullNodeName);
              value.dimmingIsOnButShouldBeOffTime = now;
            }
          }
          if (value.powerIsOnButShouldBeOffTime) {
            if (value.powerIsOnButShouldBeOffTime + ADD_NODE_TO_REPORT_TIME < now) {
              addToArrayIfUnique(reportToSend["day_24/7_power>0"], fullNodeName);
              console.log("report power je > 0 ale nema svietit", fullNodeName);
              value.powerIsOnButShouldBeOffTime = now;
            }
          }

        }

      }

    }

    let report = {};
    report["name"] = SETTINGS.rvo_name;
    report["time"] = new Date();
    report["report"] = reportToSend;

    writeToFile(F.path.root("report_data.log"), report, true);
    instance.send(SEND_TO.dailyReport, report);
  }


  function dailyReportHandler() {

    if (!SETTINGS.daily_report) return;

    // after dawn we empty reportToSend and start to get data for a new day
    const date = new Date();
    const hour = date.getHours();
    const minute = date.getMinutes();

    console.log("now: ", hour, minute, "suncalc: ", sunCalcResult.dawn_hours, sunCalcResult.dawn_minutes);

    if (hour === sunCalcResult.dawn_hours) {

      //NOTE: ak je este pred usvitom, spracujeme report a posleme na cloud, ak uz je po usvite, resetujeme report plnime data noveho dna
      if (minute >= sunCalcResult.dawn_minutes - 2) {
        emptyReportToSend();
        emptyDailyReport();
        console.log("Podmienka 1 v dailyReport handler");
      } else {
        processDailyReport();
        emptyReportToSend();
        emptyDailyReport();
        console.log("Podmienka 2 v dailyReport handler");
      }

      initialReportStatus = true;

      // ak sa funkcia spusti o hodinu alebo 2 neskor, ako je usvit (moze sa to stat, kedze kazde zapnutie/vypnutie linii odznova spusti casovac.
    } else if (hour === sunCalcResult.dawn_hours + 1 || hour === sunCalcResult.dawn_hours + 2) {
      if (!initialReportStatus) {
        emptyReportToSend();
        emptyDailyReport();
        initialReportStatus = true;
        console.log("Podmienka 3 v dailyReport handler");
      } else {
        processDailyReport();
        initialReportStatus = true;
        console.log("Podmienka 4 v dailyReport handler");
      }
    } else {
      processDailyReport();
      initialReportStatus = false;
      console.log("Podmienka 5 v dailyReport handler");
    }

    console.log("initialReportStatus: ", initialReportStatus);

  }

  function emptyReportToSend() {
    emptyJsObject(reportToSend);
    reportToSend["contactor"] = { off: [], on: [] };
    reportToSend["night_no_data"] = [];
    reportToSend["night_dimming=0"] = [];
    reportToSend["night_power=0"] = [];
    reportToSend["day_24/7_no_data"] = [];
    reportToSend["day_24/7_dimming>0"] = [];
    reportToSend["day_24/7_power>0"] = [];
    console.log(`resetuje sa reportToSend`);
  }


  //NOTE: ked je initialTs stale rovnaky a zistime ze linie su vypnute (relaysData.line.contactor) - zistim, ci je node na tej linii. Vieme, ze ak je linia vypnuta je to v poriadku lebo nie je na 24/7 linii
  //ak je na 24/7 linii, reportujeme, ze neprijima data
  function emptyDailyReport() {
    const ts = Date.now();
    emptyJsObject(dailyReport);

    //NOTE: do dailyReport dame vsetky nody a pravidelne kontrolujeme ci maju data
    //ked nastane sumrak/usvit a svetla maju zacat/prestat svietit, dailyReport spravime nanovo:
    for (const [key, value] of Object.entries(nodesData)) {
      dailyReport[value.node] = { line: value.line, initialTs: ts };
    }

    console.log("dailyReport: ", dailyReport);
  }


  async function runTasks() {

    clearInterval(interval);

    let currentTimestamp = Date.now();

    //sort tasks based on timestamp
    tasks.sort(function(a, b) {
      if (a.timestamp <= currentTimestamp && b.timestamp <= currentTimestamp) {
        return a.priority - b.priority;
      }
      return a.timestamp - b.timestamp;
    });

    if (tasks.length == 0) {
      instance.send(SEND_TO.debug, "no tasks created");
      interval = setInterval(runTasks, LONG_INTERVAL);
      return;
    }

    if (!rsPort.isOpen) {
      instance.send(SEND_TO.debug, "!rsPort.isOpen");
      //await rsPort.open();
      //console.log("Cmd-mngr: !rsPort.isOpen");
    }

    let currentTask = tasks[0];

    if (currentTask.debug) {
      //logger.debug("--->task to process", currentTask);
    }

    if (currentTask.timestamp <= currentTimestamp) {
      let params = { ...tasks[0] };

      //allow terminal commands
      if (SETTINGS.maintenance_mode && params.type !== "cmd-terminal") {
        interval = setInterval(runTasks, LONG_INTERVAL);
        return;
      }

      let type = params.type;
      let tbname = params.tbname;
      let node = params.address;
      let register = params.register;
      let line = null;
      let itIsNodeCommand;

      if (nodesData[node] !== undefined) {
        line = nodesData[node].line;
        itIsNodeCommand = true;
      }

      if (params.line !== undefined) line = params.line;

      if (params.addMinutesToTimestamp > 0 || params.timePointName) {
        tasks[0].timestamp = currentTimestamp + tasks[0].addMinutesToTimestamp * 60000;
      } else {
        tasks.shift();
      }

      //kontrola nespracovanych profilov nodov
      //TODO: co ked niektore nody neprijimaju profily? kazdu hodinu zahltuju "tasks" array
      if (type == "process_profiles") {
        //na vsetky zapnutych liniach sa spracuju nespracovane profily nodov
        loadRelaysData();
        interval = setInterval(runTasks, SHORT_INTERVAL);
        return;
      }

      //relay
      if (type == "relay") {

        const timePointName = params.timePointName;
        const value = params.value;

        let date = new Date();
        date.setDate(date.getDate() + 1);//next day

        let sunCalcResult;
        if (timePointName) sunCalcResult = calculateDuskDawn(date, params.line);


        if (timePointName == "dawn") {
          tasks[0].timestamp = sunCalcResult.dawn_time;
        }
        else if (timePointName == "dusk") {
          tasks[0].timestamp = sunCalcResult.dusk_time;
        }
        else if (timePointName == "luxOn") {
          tasks[0].timestamp = sunCalcResult.dusk_time + params.dusk_lux_sensor_time_window * 60000;
        }
        else if (timePointName == "luxOff") {
          tasks[0].timestamp = sunCalcResult.dawn_time + params.dawn_lux_sensor_time_window * 60000;
        }
        else if (timePointName == "profileTimepoint") {
          tasks[0].timestamp = currentTimestamp + tasks[0].addMinutesToTimestamp * 60000;
        }

        let info = "aplikovany bod profilu";
        let onOrOff = "";
        value == 1 ? onOrOff = "on" : onOrOff = "off";

        turnLine(onOrOff, params.line, info);

        interval = setInterval(runTasks, LONG_INTERVAL);
        return;
      }

      if (!SETTINGS.masterNodeIsResponding) {
        //ak neodpoveda, nebudeme vykonavat ziadne commands, okrem cmd-terminal cmd-master
        errorHandler.sendMessageToService("Master node is not responding");

        let stop = true;

        if (type === "cmd-terminal" || type === "cmd-master") stop = false;
        if (stop) {
          interval = setInterval(runTasks, LONG_INTERVAL);
          return;
        }
      }

      let contactorStatus = 1;
      if (relaysData[line] != undefined) contactorStatus = relaysData[line].contactor;

      if (line === 0 || contactorStatus === 0 || FLOW.deviceStatus.state_of_breaker[line] === "Off") {
        interval = setInterval(runTasks, LONG_INTERVAL);
        return;
      }

      // TODO: -> status offline for rvo if rotary_switch_state is OFF, this is source of errors
      // check if rotary_switch_state == "Off"
      // state_of_braker: disconnected = true?

      if (!rsPort.isOpen) {
        interval = setInterval(runTasks, LONG_INTERVAL);
        return;
      }

      //RE-CALCULATE VALUES
      //set actual time for broadcast
      if (register == 87 && params.recipient === 2) {
        var d = new Date();
        params.byte1 = d.getHours();//h
        params.byte2 = d.getMinutes();//m
      }

      //SET DUSK/DAWN FOR BROADCAST
      //Time of dusk
      if (register == 6 && params.recipient === 2) {

        if (type != "cmd-terminal") {
          params.byte1 = sunCalcResult["dusk_hours"];//h
          params.byte2 = sunCalcResult["dusk_minutes"];//m
        }
      }

      //Time of dawn
      if (register == 7 && params.recipient === 2) {
        if (type != "cmd-terminal") {
          params.byte1 = sunCalcResult["dawn_hours"];//h
          params.byte2 = sunCalcResult["dawn_minutes"];//m
        }
      }
      //-----------------------

      instance.send(SEND_TO.debug, "address: " + node + " register:" + register + "type: " + type);

      var startTime, endTime;
      startTime = new Date();

      let saveToTb = true;
      if (!tbname) saveToTb = false;

      let resp = com_generic(node, params.recipient, params.rw, register, params.name, params.byte1, params.byte2, params.byte3, params.byte4);
      let readBytes = 11;
      let timeout = 4000;


      // await keyword is important, otherwise incorrect data is returned!
      await writeData(rsPort, resp, readBytes, timeout).then(function(data) {

        //sometimes happens, that status of node changes to OK, NOK even if line was turned off and should be status OFFLINE. To prevent this, we return if line contactor is 0:
        if (itIsNodeCommand && line && relaysData[line].contactor !== 1) return;

        endTime = new Date();
        var timeDiff = endTime - startTime;

        //data je array z 11 bytov: 1-4 adresa, 5 status ak je status 0 - ok, nasleduju 4 byty data a 2 byty CRC
        let dataBytes = data.slice(5, 9);
        let result = detectIfResponseIsValid(data);

        //ak sa odpoved zacina 0 - je to v poriadku, inak je NOK
        let message = result.message; // OK, NOK
        let message_type = result.type;

        if (params.hasOwnProperty("debug")) {
          if (params.debug) {
            console.log("detected response:", result);
            logger.debug("Cmd-mngr: writeData done " + message_type + " duration: " + timeDiff + " type: " + params.debug, params, result);
          }
        }

        let values = {};

        //CMD FINISHED
        if (message == "OK") {

          updateNodeStatus(node, true);

          //write
          if (type == "set_node_profile") {
            let result = cmdCounterResolve(node);
            if (result == 0) {
              dbNodes.modify({ processed: true }).where("node", node).make(function(builder) {
                builder.callback(function(err, response) {

                  sendNotification("Cmd-mngr: process cmd", SETTINGS.rvoTbName, "dimming_profile_was_successfully_received_by_node", { node: node }, "", SEND_TO.tb, instance);

                  logger.debug("--> profil úspešne odoslaný na node č. " + node);
                  nodesData[node].processed = true;
                  nodeProfileSendFail.delete(node);
                });
              });
            }
          }

          //parse read response
          if (params.rw == 0) {
            values = processResponse(register, dataBytes); //read
          }

          if (itIsNodeCommand) {
            values.comm_status = "OK";
            values.status = "OK";
            nodesData[node].readout = { ...nodesData[node].readout, ...values };


            if (SETTINGS.daily_report) {
              //TODO: co ak nedostavame odpovede z nodu ? Musime vyreportovat!!
              //v dailyReport teda musi byt kompletny zoznam nodov a neustale kontrolovat, ci maju data, ak nie report node - neodpoveda
              if (register === 1) {

                let now = Date.now();

                if (rvo_is_on === true) {

                  if ("dimmingIsOnButShouldBeOffTime" in dailyReport[node]) delete dailyReport[node].dimmingIsOnButShouldBeOffTime;

                  if (values.dimming > 0) {
                    if ("dimmingIsZeroTime" in dailyReport[node]) delete dailyReport[node].dimmingIsZeroTime;
                    dailyReport[node] = { ...dailyReport[node], initialTs: now };
                  } else {
                    if (!("dimmingIsZeroTime" in dailyReport[node])) dailyReport[node] = { ...dailyReport[node], dimmingIsZeroTime: now, initialTs: now };
                    else dailyReport[node] = { ...dailyReport[node], initialTs: now };
                  }

                } else {

                  if ("dimmingIsZeroTime" in dailyReport[node]) delete dailyReport[node].dimmingIsZeroTime;

                  if (values.dimming > 0) {
                    if (!("dimmingIsOnButShouldBeOffTime" in dailyReport[node])) dailyReport[node] = { ...dailyReport[node], dimmingIsOnButShouldBeOffTime: now, initialTs: now };
                    else dailyReport[node] = { ...dailyReport[node], initialTs: now };
                  } else {
                    if ("dimmingIsOnButShouldBeOffTime" in dailyReport[node]) delete dailyReport[node].dimmingIsOnButShouldBeOffTime;
                    dailyReport[node] = { ...dailyReport[node], initialTs: now };
                  }

                }
              }

              if (register === 76) {

                let now = Date.now();

                if (rvo_is_on === true) {

                  if ("powerIsOnButShouldBeOffTime" in dailyReport[node]) delete dailyReport[node].powerIsOnButShouldBeOffTime;

                  if (values.power > 1) {
                    if ("powerIsZeroTime" in dailyReport[node]) delete dailyReport[node].powerIsZeroTime;
                    dailyReport[node] = { ...dailyReport[node], initialTs: now };
                  } else {
                    if (!("powerIsZeroTime" in dailyReport[node])) dailyReport[node] = { ...dailyReport[node], powerIsZeroTime: now, initialTs: now };
                    else dailyReport[node] = { ...dailyReport[node], initialTs: now };
                  }

                } else {

                  if ("powerIsZeroTime" in dailyReport[node]) delete dailyReport[node].powerIsZeroTime;

                  if (values.power > 1) {
                    if (!("powerIsOnButShouldBeOffTime" in dailyReport[node])) dailyReport[node] = { ...dailyReport[node], powerIsOnButShouldBeOffTime: now, initialTs: now };
                    else dailyReport[node] = { ...dailyReport[node], initialTs: now };
                  } else {
                    if ("powerIsOnButShouldBeOffTime" in dailyReport[node]) delete dailyReport[node].powerIsOnButShouldBeOffTime;
                    dailyReport[node] = { ...dailyReport[node], initialTs: now };
                  }

                }
              }
            }

          }

          //master node
          if (node == 0) {
            sendNotification("Cmd-mngr: process cmd", SETTINGS.rvoTbName, "master_node_is_responding_again", {}, "", SEND_TO.tb, instance, "rvo_status");
            SETTINGS.masterNodeIsResponding = true;
            if (register == 4) values["edge_fw_version"] = SETTINGS.edge_fw_version;
          }

          if (params.debug) {
            //logger.debug("saveToTb", saveToTb, tbname, values);
          }

          if (saveToTb && type != "node-regular-read") {
            sendTelemetry(values, tbname);
          }
          else {
            if (type == "cmd-terminal") {
              terminalCommandResponse(params, "SUCCESS", data);
            }
          }

        }
        else {
          terminalCommandResponse(params, "ERROR", data);
          handleNokResponseOnRsPort("handleNOK else block", params, itIsNodeCommand, saveToTb);

          if (params.hasOwnProperty("debug")) {
            if (params.debug) {
              //logger.debug("writeData err: ", error, result, params);
              logger.debug("writeData err: ", tbname, node, register, values);
            }
          }

          //logger.debug(error, result, params);
        }
      }).catch(function(reason) {

        //console.log("writeData catch exception", reason);
        instance.send(SEND_TO.debug, reason);

        terminalCommandResponse(params, "FAILURE", null, reason);
        handleNokResponseOnRsPort("handleNOK catch block", params, itIsNodeCommand, saveToTb);

        if (params.hasOwnProperty("debug")) {
          if (params.debug) {
            logger.debug("-->WRITE FAILED: " + reason, params.debug, params);
          }
        }

      });

    }
    else {
      if (currentTask.debug) {
        // currentTask.timestamp <= currentTimestamp && logger.debug("currentTask is not processed - task is in the future", currentTask);
      }

      interval = setInterval(runTasks, LONG_INTERVAL);
      return;
    }

    //console.log("----->runTasks - setInterval", new Date());
    interval = setInterval(runTasks, SHORT_INTERVAL);
  }


  // if node does not respond to request, we repeat request 3 times:
  function repeatCommand(params) {
    params.repeatCounter++;
    if (params.repeatCounter < 4) {
      params.timestamp = 0;
      params.addMinutesToTimestamp = 0;
      tasks.push(params);
    }
  }

  function handleNokResponseOnRsPort(message, params, itIsNodeCommand, saveToTb) {

    let node = params.address;
    let register = params.register;
    let type = params.type;
    let tbName = params.tbname;
    if (!tbName) return;

    let values = {};

    let updateStatus = updateNodeStatus(node, false);

    if (itIsNodeCommand) {
      values.comm_status = "NOK";
      nodesData[node].readout.comm_status = "NOK";
      repeatCommand(params);
    }

    if (updateStatus) {
      values.status = "NOK";
      nodesData[node].readout.status = "NOK";
    }

    if (type === "node-regular-read") return;

    //master node
    if (node == 0) {
      sendNotification("Cmd-mngr: process cmd", SETTINGS.rvoTbName, "master_node_is_not_responding", {}, "", SEND_TO.tb, instance, "rvo_status");
      logger.debug("master_node_is_not_responding", params);
      SETTINGS.masterNodeIsResponding = false;

      if (register == 4) values["master_node_version"] = "NOK";
    }

    if (type == "set_node_profile") {
      delete cmdCounter[node];
      logger.debug("profil nebol úspešne odoslaný na node č.  ", params);

      if (!nodeProfileSendFail.has(node)) {
        sendNotification("Cmd-mngr: process cmd", tbName, "configuration_of_dimming_profile_to_node_failed", { node: node }, "", SEND_TO.tb, instance);
        nodeProfileSendFail.add(node);
      }
    }

    // console.log("------",node, register, type, itIsNodeCommand, updateStatus, saveToTb, values);
    if (saveToTb) {
      sendTelemetry(values, tbName);
    }

  }


  function sendNodesData() {
    Object.keys(nodesData).forEach(node => {
      if (nodesData[node]["status"] !== "OFFLINE") {
        sendTelemetry(nodesData[node].readout, nodesData[node].tbname);
        nodesData[node].readout = {};
      }
    })
  }


  /**
   * function handles requests from terminal
   * responseType can be "SUCCESS", "ERROR" or "FAILURE", depending on rsPort data.
   * FAILURE means, that we got into catch block of writeData function.
  */
  function terminalCommandResponse(params, responseType, data = null, reason = "") { //success, error, failure

    if (params.refFlowdataKey === undefined) {
      //console.log("params.refFlowdataKey is undefined", params); 
      return;
    }

    let message = null;
    let type = null;

    switch (responseType) {
      case "SUCCESS":
        message = "cmd-terminal SUCCESS";
        type = "SUCCESS";
        break;
      case "ERROR":
        message = "cmd-terminal FAILED";
        type = "ERROR";
        break;
      case "FAILURE":
        message = "ERROR WRITE FAILED: " + reason;
        type = "ERROR";
        break;
      default:
        type = undefined;
    }

    logger.debug(message);

    //make http response
    let responseObj = {}
    responseObj["type"] = type;

    if (responseType == "FAILURE") responseObj["message"] = "ERROR WRITE FAILED: " + reason;
    else responseObj["bytes"] = data;

    let refFlowdata = refFlowdataObj[params.refFlowdataKey]; //holds reference to httprequest flowdata
    if (refFlowdata) {
      refFlowdata.data = responseObj;
      instance.send(SEND_TO.http_response, refFlowdata);
    }
  }


  /**
   * function handles tasks, that are not needed to run through masterNode. To make them run smooth without waiting for other tasks to be completed, we moved them in separate function
  */
  function reportEdgeDateTimeAndNumberOfLuminaires() {

    //Number of ok and nok nodes on platform does not equals to total number of nodes. 
    //possible error is, that nodesData object is changing all the time. To make a proper calculation of ok,nok luminaires, we make a copy of it:
    let nodesData_clone = JSON.parse(JSON.stringify(nodesData));

    const ts = Date.now();
    const keys = Object.keys(nodesData_clone);

    const number_of_luminaires = keys.length;
    let number_of_ok_luminaires = 0;
    let number_of_nok_luminaires = 0;

    for (let i = 0; i < keys.length; i++) {
      let key = keys[i];
      let nodeObj = nodesData_clone[key];
      if (nodeObj.tbname == undefined) continue;

      if (nodeObj.status === "OFFLINE") {
        nodeObj.node_status_before_offline === true ? number_of_ok_luminaires++ : number_of_nok_luminaires++;
      }
      else if (nodeObj.status == true) number_of_ok_luminaires++;
      else number_of_nok_luminaires++;

    }

    const values = {
      "number_of_luminaires": number_of_luminaires,
      "number_of_ok_luminaires": number_of_ok_luminaires,
      "number_of_nok_luminaires": number_of_nok_luminaires,
      "edge_date_time": ts - ts % 60000 //round to full minute
    };

    sendTelemetry(values, SETTINGS.rvoTbName, ts);
  }


  function setSunCalcResult() {
    //if next day, we get new dusk and dawn. To make sure, in local timezone 2:00 means 00:00 in utc (it means next day)
    if (new Date().getHours() === 2) {
      sunCalcResult = calculateDuskDawn();
      console.log("Novy suncalc: ", sunCalcResult);
    }
  }


  function setRvoPeriod() {

    if (!SETTINGS.daily_report) return;

    const ts = Date.now();

    previous_rvo_is_on_value = rvo_is_on;

    if (ts < sunCalcResult.dawn_time - 1_800_000) { // nodes should be on (00:00 -> 06:00 ( - pol hodina, lebo rvo vypina skor)
      rvo_is_on = true;
    } else if (sunCalcResult.dawn_time - 1_800_000 < ts && ts < sunCalcResult.dusk_time + 1_800_000) { // nodes should be off 06:00 -> 21:00
      rvo_is_on = false;
    } else if (sunCalcResult.dusk_time + 1_800_000 < ts) { // nodes should be on 21:00 -> 00:00
      rvo_is_on = true;
    }

    if (previous_rvo_is_on_value !== rvo_is_on) {
      emptyDailyReport();
      console.log("rvo_is: ", rvo_is_on, previous_rvo_is_on_value);
    }
  }


  function handleRsPort() {

    if (rsPort) {
      rsPort.removeAllListeners();
      rsPort = null;
    }

    //! rsPort LM = "/dev/ttymxc4", rsPort UNIPI = "/dev/ttyUSB0"
    // const rsPort = new SerialPort("/dev/ttymxc4", { autoOpen: false });  //LM
    // const rsPort = new SerialPort("/dev/ttyUSB0", { autoOpen: false });  // UNIPI

    if (SETTINGS.serial_port == "" || SETTINGS.serial_port == undefined || SETTINGS.serial_port.length === 1) SETTINGS.serial_port = "ttymxc4";
    rsPort = new SerialPort(`/dev/${SETTINGS.serial_port}`, { autoOpen: false });
    //(node:16372) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 13 data listeners added to [SerialPort]. Use emitter.setMaxListeners() to increase limit
    //rsPort.setMaxListeners(0);

    rsPort.on('open', async function() {

      logger.debug("Cmd-mngr: rsPort opened success");

      await runSyncExec(`stty -F /dev/${SETTINGS.serial_port} 115200 min 1 time 5 ignbrk -brkint -icrnl -imaxbel -opost -isig -icanon -iexten -echo -echoe -echok -echoctl -echoke`).then(function(status) {
        instance.send(SEND_TO.debug, "RPC runSyncExec - Promise Resolved:" + status);

        logger.debug(0, "RPC runSyncExec - Promise Resolved:" + status);

      }).catch(function(reason) {
        instance.send(SEND_TO.debug, "Cmd-mngr: RPC runSyncExec - promise rejected:" + reason);
      });
    });

    rsPort.on('error', function(err) {
      errorHandler.sendMessageToService([exports.title, "unable to open port", SETTINGS.serial_port, err.message], 0);
      monitor.info("Cmd-mngr: Error on rsPort", err.message);
    });

    rsPort.on("close", () => {
      monitor.info("Cmd-mngr: rsPort closed, reconnecting...");
      setTimeout(handleRsPort, 1000);
    });

    rsPort.open();
  }


  instance.on("close", () => {
    clearInterval(interval);
    clearInterval(customTasksInterval);
    clearInterval(setCorrectTime);
    clearInterval(sendNodeReadout);
    clearInterval(accelerometerInterval);
    rsPort.close();
  });

  instance.on("0", _ => {
    main();
  })


  instance.on("2", _ => {
    console.log("dailyReport, reportToSend, rvo_is_on", dailyReport, reportToSend, rvo_is_on, previous_rvo_is_on_value);
  })

  instance.on("1", async function(flowdata) {

    //instance.send(SEND_TO.debug, "on Data");
    //instance.send(SEND_TO.debug, flowdata);

    //logger.debug(flowdata.data);

    //just testing functions
    if (flowdata.data == "open") {
      if (!rsPort.isOpen) rsPort.open();
      return;
    }
    else if (flowdata.data == "close") {
      rsPort.close();
      return;
    }
    else if (flowdata.data == "clean") {
      tasks = [];
      return;
    }
    else if (flowdata.data == "buildtasks") {
      //build & run
      return;
    }
    else if (flowdata.data == "run") {
      //durations = [];

      if (tasks.length == 0) {

        buildTasks();

        if (rsPort.isOpen) {
          interval = setInterval(runTasks, 100);
        }
        else {
          instance.send(SEND_TO.debug, "port is not opened!!!");
        }
      }
    }
    else {
      //terminal data - object
      //logger.debug("flowdata", flowdata.data);

      if (typeof flowdata.data === 'object') {
        //logger.debug("dido", flowdata.data);
        if (flowdata.data.hasOwnProperty("sender")) {
          //data from dido_controller
          if (flowdata.data.sender == "dido_controller") {

            if (flowdata.data.hasOwnProperty("cmd")) {
              let cmd = flowdata.data.cmd;

              if (cmd == "buildTasks") {
                clearInterval(interval);

                logger.debug("-->Cmd-mngr: BUILD TASKS");
                buildTasks();

                //logger.debug("tasks:");
                //logger.debug(tasks);

                logger.debug("-->Cmd-mngr: RUN TASKS");
                interval = setInterval(runTasks, 5000);
              }
              else if (cmd == "reload_relays") {
                loadRelaysData(flowdata.data.line);

                if (flowdata.data.dataChanged) {
                  if (!flowdata.data.value) {
                    reportOfflineNodeStatus(flowdata.data.line);
                  }
                  else {
                    reportOnlineNodeStatus(flowdata.data.line);
                  }
                }

              }
              else if (cmd == "rotary_switch_state") {
                let value = flowdata.data.value;

                //state was changed
                if (rotary_switch_state != value) {
                  if (value == "Off") {
                    //vyreportovat vsetky svietdla
                    reportOfflineNodeStatus();
                  }

                  rotary_switch_state = value;
                }
              }
              else if (cmd == "lux_sensor") {
                lux_sensor = parseInt(flowdata.data.value);

                // POSSIBLE SOURCE OF PROBLEMS, IF USER SETS LUX TRESHOLD LEVEL GREATER THAN 100 - WE SHOULD BE CHECKING "DUSK/DAWN_LUX_SENSOR_VALUE" IN PROFILE MAYBE ??
                if (lux_sensor < 100) {

                  // we send lux_sensor value to all nodes:
                  let params = getParams(PRIORITY_TYPES.node_broadcast);

                  params.recipient = 2;//2 broadcast, address = 0
                  params.address = 0xffffffff;//Broadcast

                  let ba = longToByteArray(lux_sensor);

                  params.byte3 = ba[1];//msb
                  params.byte4 = ba[0];
                  params.timestamp = PRIORITY_TYPES.node_broadcast;
                  params.info = "run broadcast: Actual Lux level from cabinet";
                  params.register = 95;//Actual Lux level from cabinet
                  params.rw = 1;//write

                  tasks.push(params);

                  //process profiles
                  turnOnOffLinesAccordingToLuxSensor(lux_sensor);
                }
              }
              else if (cmd == "state_of_breaker") {
                //istic linie

                breakerCounter--;

                let value = flowdata.data.value;
                let line = parseInt(flowdata.data.line);

                let dataChanged = false;
                if (state_of_breaker[line] != value) dataChanged = true;

                state_of_breaker[line] = value;

                let status = "OK";
                if (value == "Off") status = "NOK";

                if (dataChanged) {

                  if (relaysData.hasOwnProperty(line)) {
                    let tbname = relaysData[line].tbname;

                    if (value == "Off") {
                      sendNotification("Cmd-mngr: onData", tbname, "circuit_breaker_was_turned_off_line", { line: line }, "", SEND_TO.tb, instance, "circuit_breaker");
                      if (breakerCounter < 0) reportToSend["contactor"]["off"].push({ [line]: Date.now(), maintenance_mode: SETTINGS.maintenance_mode });
                    }
                    else {
                      sendNotification("Cmd-mngr: onData", tbname, "circuit_breaker_was_turned_on_line", { line: line }, "", SEND_TO.tb, instance, "circuit_breaker");
                      if (breakerCounter < 0) reportToSend["contactor"]["on"].push({ [line]: Date.now(), maintenance_mode: SETTINGS.maintenance_mode });
                    }

                    //report status liniu
                    sendTelemetry({ status: status }, tbname)

                    //current value
                    if (value == "Off") reportOfflineNodeStatus(line); //vyreportovat vsetky svietidla na linii 
                  }

                }
              }
              else {
                logger.debug("undefined cmd", cmd);
              }
            }
          }

          return;
        }

        //data from worksys
        if (flowdata.data.hasOwnProperty("topic")) {

          let data = getNested(flowdata.data, "content", "data");

          //if we get temperature in senica from senica-prod01
          let temperature = getNested(flowdata.data, "content", "senica_temperature");

          if (temperature !== undefined) {
            temperatureInSenica = temperature;
            return;
          }

          if (data === undefined) {
            console.log("Invalid rpc command came from platform");
            return;
          }

          let command = data.params.command;
          let method = data.method;
          let profile = data.params.payload;
          if (profile == undefined) profile = "";
          let entity = data.params.entities[0];
          let entity_type = entity.entity_type;
          let tbname = entity.tb_name;

          instance.send(SEND_TO.debug, flowdata.data);
          logger.debug("--->worksys", flowdata.data, data.params, entity, entity_type, command, method);
          logger.debug("----------------------------");

          if (entity_type == "street_luminaire" || entity_type === "street_luminaire_v4_1" || entity_type === "street_luminaire_v4_1cez" || entity_type === "street_luminaire_v4") {
            if (method == "set_command") {

              //let command = data.params.command;
              let value = data.params.payload.value;

              if (command == "dimming") {

                let nodeWasFound = false;
                let keys = Object.keys(nodesData);

                //logger.debug("-----", keys);

                for (let i = 0; i < keys.length; i++) {
                  let node = keys[i];
                  //logger.debug( node, nodesData[node], tbname);

                  if (tbname == nodesData[node].tbname) {
                    let params = getParams(PRIORITY_TYPES.high_priority);

                    value = parseInt(value);
                    if (value > 0) value = value + 128;

                    params.type = "node-onetime-write";
                    params.tbname = tbname;
                    params.address = node;
                    params.register = 1;
                    params.recipient = 1;
                    params.byte4 = value;
                    params.rw = 1;
                    params.timestamp = PRIORITY_TYPES.high_priority;
                    params.info = 'set dimming from platform';
                    //params.debug = true;

                    //debug(params);
                    logger.debug("dimming", params);

                    tasks.push(params);

                    setTimeout(function() {

                      //spustime o 4 sekundy neskor, s prioritou PRIORITY_TYPES.high_priority
                      //a pridame aj vyreportovanie dimmingu
                      {
                        let params = getParams(PRIORITY_TYPES.high_priority);

                        params.type = "node-onetime-read";
                        params.tbname = tbname;
                        params.address = node;
                        params.register = 1;
                        params.recipient = 1;
                        params.rw = 0;
                        params.timestamp = PRIORITY_TYPES.high_priority;
                        params.info = 'read dimming (after set dimming from platform)';
                        //params.debug = true;

                        tasks.push(params);
                      }

                      //pridame aj vyreportovanie - vykon
                      {
                        let params = getParams(PRIORITY_TYPES.high_priority);

                        params.type = "node-onetime-read";
                        params.tbname = tbname;
                        params.address = node;
                        params.register = 76;
                        params.recipient = 1;
                        params.rw = 0;
                        params.timestamp = PRIORITY_TYPES.high_priority;
                        params.info = 'read Input Power (after set dimming from platform)';
                        //params.debug = true;

                        tasks.push(params);
                      }

                      //pridame aj vyreportovanie - prud svietidla
                      {
                        let params = getParams(PRIORITY_TYPES.high_priority);

                        params.type = "node-onetime-read";
                        params.tbname = tbname;
                        params.address = node;
                        params.register = 75;
                        params.recipient = 1;
                        params.rw = 0;
                        params.timestamp = PRIORITY_TYPES.high_priority;
                        params.info = 'read Input Current (after set dimming from platform)';
                        //params.debug = true;

                        tasks.push(params);
                      }

                      //pridame aj vyreportovanie - power faktor - ucinnik
                      {
                        let params = getParams(PRIORITY_TYPES.high_priority);

                        params.type = "node-onetime-read";
                        params.tbname = tbname;
                        params.address = node;
                        params.register = 77;
                        params.recipient = 1;
                        params.rw = 0;
                        params.timestamp = PRIORITY_TYPES.high_priority;
                        params.info = 'read power factor (after set dimming from platform)';
                        //params.debug = true;

                        tasks.push(params);
                      }

                    }, 4000);

                    nodeWasFound = true;
                    break;
                  }
                }

                if (!nodeWasFound) {
                  logger.debug("set dimming from platform", "unable to find tbname", tbname);
                }
              }
              else {
                instance.send(SEND_TO.debug, "undefined command " + command);
                logger.debug("undefined command", command);
              }

              return;
            }
            else if (method == "set_profile") {
              //nastav profil nodu
              logger.debug("-->set_profile for node", data.params);
              logger.debug("------profile data", profile);
              //instance.send(SEND_TO.debug, "set_profile" + command);

              let keys = Object.keys(nodesData);
              for (let i = 0; i < keys.length; i++) {
                let node = keys[i];
                if (tbname == nodesData[node].tbname) {

                  if (profile != "") profile = JSON.stringify(profile);
                  dbNodes.modify({ processed: false, profile: profile }).where("node", node).make(function(builder) {

                    builder.callback(function(err, response) {

                      logger.debug("worksys - update node profile done", profile);
                      if (profile === "") logger.debug("worksys - update node profile done - profile is empty");

                      //profil úspešne prijatý pre node č. xx
                      sendNotification("Cmd-mngr", tbname, "dimming_profile_was_processed_for_node", { node: node }, profile, SEND_TO.tb, instance);

                      nodesData[node].processed = false;
                      nodesData[node].profile = profile;

                      processNodeProfile(node);
                    });
                  });
                }
              }
            }
            else {

              instance.send(SEND_TO.debug, "unknown method " + method);
              logger.debug("unknown method", method);

              return;
            }
          }

          //nastav profil linie z platformy
          else if (entity_type == "edb_line" || entity_type == "edb" || entity_type == "edb_line_ver4" || entity_type == "edb_ver4_se") {
            //profil linie
            //relays.table line:number|tbname:string|contactor:number|profile:string
            //najdeme line relaysData

            if (method == "set_profile") {

              logger.debug("-->set_profile for line", data.params);
              logger.debug("profile data:", profile);

              let keys = Object.keys(relaysData);
              for (let i = 0; i < keys.length; i++) {
                let line = keys[i];
                if (tbname == relaysData[line].tbname) {
                  //zmazeme tasky
                  removeTask({ type: "relay", line: line });

                  if (profile != "") profile = JSON.stringify(profile);
                  dbRelays.modify({ profile: profile }).where("line", line).make(function(builder) {

                    builder.callback(function(err, response) {

                      //update profile
                      logger.debug("worksys - update relay profile done:", profile);
                      instance.send(SEND_TO.debug, "worksys - update relay profile done");

                      relaysData[line].profile = profile;

                      loadRelaysData(line)
                      logger.debug("loadRelaysData DONE for line", line);

                      buildTasks({ processLineProfiles: true, line: line });

                      sendNotification("Cmd-mngr: set profile from worksys", tbname, "switching_profile_was_processed_for_line", { line: line }, profile, SEND_TO.tb, instance);
                    });
                  });
                  break;
                }
              }
            }
            else if (method == "set_command") {
              let value = data.params.payload.value;

              if (command === "switch") {

                // if we receive rpc from platform, to switch maintenance mode, we set SETTINGS.maintenance_mode flow variable to value;
                if (entity_type === "edb" || entity_type === "edb_ver4_se") SETTINGS.maintenance_mode = value;

                const relayObject = getObjectByTbValue(relaysData, tbname);
                let line = 0;
                if (isObject(relayObject)) line = relayObject.line;

                // v relaysData je contactor bud 0 alebo 1, ale z platformy prichadza true, false;
                if (value == false) {
                  turnLine("off", line, "command received from platform");
                  if (line != 0) reportToSend["contactor"]["off"].push({ [line]: Date.now(), maintenance_mode: SETTINGS.maintenance_mode });
                }
                else {
                  turnLine("on", line, "command received from platform");
                  if (line != 0) reportToSend["contactor"]["on"].push({ [line]: Date.now(), maintenance_mode: SETTINGS.maintenance_mode });
                }
              }
            }
            else {
              instance.send(SEND_TO.debug, "undefined method " + method);
              logger.debug("undefined method", method);
            }

            return;
          }
          else {
            instance.send(SEND_TO.debug, "UNKNOW entity_type " + entity_type);
            logger.debug("UNKNOW entity_type", entity_type);
          }
          return;
        }

        //terminal
        if (!rsPort.isOpen) await rsPort.open();

        let params = flowdata.data.body;
        if (params == undefined) {
          //logger.debug("Cmd-mngr: flowdata.data.body is undefined");
          return;
        }

        params.priority = PRIORITY_TYPES.terminal;
        params.type = "cmd-terminal";
        params.tbname = "";
        params.timestamp = PRIORITY_TYPES.terminal;
        params.addMinutesToTimestamp = 0;// do not repeat task!!!
        params.debug = true;

        let timestamp = Date.now();
        params.refFlowdataKey = timestamp;
        //params.refFlowdata = flowdata;
        //refFlowdata = flowdata;

        //console.log("flowdata", flowdata);

        cleanUpRefFlowdataObj();

        refFlowdataObj[timestamp] = flowdata;

        //fix
        //params.address = params.adress;
        logger.debug("received from terminal", params);
        logger.debug("date/time:", new Date());
        logger.debug("tasks length:", tasks.length);

        //tasks = [];

        //add to tasks
        tasks.push(params);

      }
    }
  })


  //function gets value of a nested property in an object and returns undefined if it does not exists:
  function getNested(obj, ...args) {
    return args.reduce((obj, level) => obj && obj[level], obj)
  }


  /**
   * setCorrectTime function runs once per hour
   * If it is 3 o'clock, it sets actual time, which is got from services
   * https://service-prod01.worksys.io/gettime
   * If also detects Read Only Filesystem once a day
   */
  function setCorrectPlcTimeOnceADay() {

    const currentTime = new Date();
    if (currentTime.getHours() != 3) return;

    RESTBuilder.make(function(builder) {

      if (!builder) return;

      builder.method('GET');
      builder.url('http://192.168.252.2:8004/gettime?projects_id=1');

      builder.callback(function(err, response, output) {

        if (err) {
          console.log(err);
          return;
        }

        const res = output.response;

        try {

          const obj = JSON.parse(res);
          let d = new Date(obj.date);

          const now = new Date();

          let diffInMinutes = now.getTimezoneOffset();
          console.log("---->TimezoneOffset", diffInMinutes);

          if (d instanceof Date) {

            // monitor.info("----------->setCorrectPlcTimeOnceADay() current js date:", d, d.getHours());

            let year = d.getFullYear();
            let month = addZeroBefore(d.getMonth() + 1);
            let day = addZeroBefore(d.getDate());

            let hours = addZeroBefore(d.getHours());
            let minutes = addZeroBefore(d.getMinutes());
            let seconds = addZeroBefore(d.getSeconds());

            let dateStr = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;

            exec(`sudo timedatectl set-time "${dateStr}"`, (err, stdout, stderr) => {
              if (err || stderr) {
                console.error(err);
                console.log(stderr);
                console.log(dateStr);

                monitor.info("failed timedatectl set-time", err, stderr);
              }
              else {
                monitor.info("setCorrectPlcTimeOnceADay() --> Nastaveny cas na: ", dateStr);
              }

            });
          }

        } catch (error) {
          logger.debug("setCorrectPlcTimeOnceADay - function error", error, res);
          monitor.info("setCorrectPlcTimeOnceADay - function error", error, res);
        }

        // we detect readOnlyFileSystem once an hour as well
        detectReadOnlyFilesystem();

      });
    });

  }


  function detectReadOnlyFilesystem() {
    exec(`sudo egrep " ro,|,ro " /proc/mounts`, (err, stdout, stderr) => {
      if (err || stderr) {
        console.error(err);
        console.log(stderr);

      } else {
        //console.log("Read-only", stdout);

        let lines = stdout + "";
        lines = lines.split("\n");

        let readOnlyDetected = "";
        for (let i = 0; i < lines.length; i++) {
          if (lines[i].startsWith("/dev/mmcblk0p2")) {
            readOnlyDetected = lines[i];
          }
        }

        if (readOnlyDetected !== "") {
          errorHandler.sendMessageToService("Detected: Read-only file system: " + readOnlyDetected);
          monitor.info("Read only filesystem detected");
        }

      }
    });
  }








  ///helper functions
  function sendTelemetry(values, tbname, date = Date.now()) {
    const dataToTb = {
      [tbname]: [
        {
          "ts": date,
          "values": values
        }
      ]
    }

    tbHandler.sendToTb(dataToTb, instance);
  }


  function calculateDuskDawn(date, line, duskOffset = 0, dawnOffset = 0) {

    if (date === undefined) date = new Date();

    let profilestr = "";
    if (relaysData[line] != undefined) profilestr = relaysData[line].profile;

    let result = {};

    var times = SunCalc.getTimes(date, latitude, longitude);
    let dawn = new Date(times.sunrise);//usvit
    let dusk = new Date(times.sunset);//sumrak


    //http://suncalc.net/#/48.5598,18.169,11/2021.04.07/11:06
    //https://mapa.zoznam.sk/zisti-gps-suradnice-m6

    let dusk_astro_clock_offset = duskOffset;//minutes
    let dawn_astro_clock_offset = dawnOffset;//minutes

    try {

      let profile = JSON.parse(profilestr);
      if (Object.keys(profile).length === 0) throw ("profile is not defined");

      //Jednoduchý režim
      if (profile.astro_clock == false && profile.dusk_lux_sensor == false && profile.dawn_lux_sensor == false) {

      }

      //Režim astrohodín
      if (profile.astro_clock == true) {
        //if(profile.dusk_lux_sensor == false)
        {
          if (profile.hasOwnProperty("dusk_astro_clock_offset")) dusk_astro_clock_offset = parseInt(profile.dusk_astro_clock_offset);
        }

        //if(profile.dawn_lux_sensor == false)
        {
          if (profile.hasOwnProperty("dawn_astro_clock_offset")) dawn_astro_clock_offset = parseInt(profile.dawn_astro_clock_offset);
        }

      }

    } catch (error) {
      if (profilestr != "") {
        logger.debug(profilestr);
        logger.debug(error);
      }
    }

    result.dusk_no_offset = addZeroBefore(dusk.getHours()) + ":" + addZeroBefore(dusk.getMinutes());
    result.dawn_no_offset = addZeroBefore(dawn.getHours()) + ":" + addZeroBefore(dawn.getMinutes());

    dusk = new Date(dusk.getTime() + gmtOffset + dusk_astro_clock_offset * 60000);
    dawn = new Date(dawn.getTime() + gmtOffset + dawn_astro_clock_offset * 60000);

    result.dusk = addZeroBefore(dusk.getHours()) + ":" + addZeroBefore(dusk.getMinutes());
    result.dusk_hours = dusk.getHours();
    result.dusk_minutes = dusk.getMinutes();

    result.dawn = addZeroBefore(dawn.getHours()) + ":" + addZeroBefore(dawn.getMinutes());
    result.dawn_hours = dawn.getHours();
    result.dawn_minutes = dawn.getMinutes();

    result.dusk_time = dusk.getTime();
    result.dawn_time = dawn.getTime();

    result.dusk_astro_clock_offset = dusk_astro_clock_offset;
    result.dawn_astro_clock_offset = dawn_astro_clock_offset;

    return result;
  }


  function processResponse(register, bytes) {

    let values = {};

    let byte3 = bytes[0];
    let byte2 = bytes[1];
    let byte1 = bytes[2];
    let byte0 = bytes[3];

    //status
    if (register == 0) {
      let statecode = bytesToInt(bytes);
      values = { "statecode": statecode };
      return values;
    }

    //Dimming, CCT
    else if (register == 1) {
      let brightness = 0;
      let dimming = byte0;
      if (dimming > 128) {
        //dimming = -128;
        brightness = dimming - 128;
      }

      //cct
      //Ak Byte3 == 1: CCT = (Byte2*256)+Byte1
      let cct;
      if (byte3 == 1) cct = byte2 * 256 + byte1;
      else cct = bytesToInt(bytes.slice(0, 3));

      //cct podla auditu

      values["dimming"] = brightness;
      return values;
    }

    //
    else if (register == 4) {
      values["master_node_version"] = bytes[1] + "." + bytes[2];
      //logger.debug("FW Version", register, bytes);
    }

    //Napätie
    else if (register == 74) {
      let voltage = (bytesToInt(bytes) * 0.1).toFixed(1);
      values["voltage"] = Number(voltage);
    }

    //Prúd
    else if (register == 75) {
      let current = bytesToInt(bytes);
      values["current"] = current;
    }

    //výkon
    else if (register == 76) {
      let power = (bytesToInt(bytes) * 0.1).toFixed(2);
      values["power"] = Number(power);
    }

    //účinník
    else if (register == 77) {
      let power_factor = Math.cos(bytesToInt(bytes) * 0.1 * (Math.PI / 180)).toFixed(2);
      values["power_factor"] = Number(power_factor);
    }

    //frekvencia
    else if (register == 78) {
      let frequency = (bytesToInt(bytes) * 0.1).toFixed(2);
      values["frequency"] = Number(frequency);
    }

    //energia
    else if (register == 79) {
      let energy = bytesToInt(bytes);
      values["energy"] = energy / 1000; //energia v kWh -> delit 1000
    }

    //doba života
    else if (register == 80) {
      let lifetime = (bytesToInt(bytes) / 60).toFixed(2);
      values["lifetime"] = Number(lifetime);
    }

    //nastavenie profilu
    else if (register == 8) {
      let time_schedule_settings = bytesToInt(bytes);
      values["time_schedule_settings"] = time_schedule_settings;
    }

    //naklon - nateraz sa z nodu nevycitava! kvoli problemom s accelerometrom a vracanymi hodnotami, posielame temp a x y z vo funkcii accelerometerData()
    else if (register == 84) {
      values["temperature"] = byte3 >= 128 ? (byte3 - 128) * (-1) : byte3;
      values["inclination_x"] = byte2 >= 128 ? (byte2 - 128) * (-1) : byte2;
      values["inclination_y"] = byte1 >= 128 ? (byte1 - 128) * (-1) : byte1;
      values["inclination_z"] = byte0 >= 128 ? (byte0 - 128) * (-1) : byte0;
    }

    //FW verzia nodu
    else if (register == 89) {
      //formát: "Byte3: Byte2.Byte1 (Byte0)"
      values["fw_version"] = byte3 + ":" + byte2 + "." + byte1 + "(" + byte0 + ")";
    }

    else if (register == 87 || register == 6 || register == 7) {
      var d = new Date();
      d.setHours(byte3, byte2, 0, 0);
      let timestamp = d.getTime();

      //aktuálny čas
      if (register == 87) values["actual_time"] = timestamp;
      //čas súmraku
      else if (register == 6) values["dusk_time"] = timestamp;
      //čas úsvitu
      else if (register == 7) values["dawn_time"] = timestamp;
    }

    return values;
  }


  //byte1 MSB = data3, byte2 = data2, byte3 = data1, byte4 = data0 LSB
  function com_generic(adresa, rec, rw, register, name, byte1, byte2, byte3, byte4) {
    let resp = [];

    let cmd = register;

    if (typeof adresa === 'string') adresa = parseInt(adresa);
    if (typeof byte1 === 'string') byte1 = parseInt(byte1);
    if (typeof byte2 === 'string') byte2 = parseInt(byte2);
    if (typeof byte3 === 'string') byte3 = parseInt(byte3);
    if (typeof byte4 === 'string') byte4 = parseInt(byte4);

    if (rw === 0) {
      cmd = cmd + 0x8000;
    }

    //master
    if (rec === 0) adresa = 0;

    if (rec === 2) {
      adresa = 0xffffffff;//Broadcast
    }

    //recipient
    if (rec === 3) {
      resp.push(0xFF);
      resp.push(0xFF);
      resp.push(0xFF);
      resp.push(0xFF);
      resp.push(adresa & 0xFF);//band
    }
    else {
      resp.push((adresa >> 24) & 0xFF);//rshift
      resp.push((adresa >> 16) & 0xFF);
      resp.push((adresa >> 8) & 0xFF);
      resp.push(adresa & 0xFF);

      if (rec === 2) {
        resp.push(0xFF);
      }
      else resp.push(0);
    }

    resp.push((cmd >> 8) & 0xFF);//rshift
    resp.push(cmd & 0xFF);//band
    resp.push(byte1 & 0xFF);//band
    resp.push(byte2 & 0xFF);//band
    resp.push(byte3 & 0xFF);//band
    resp.push(byte4 & 0xFF);//band

    //let data = '12345';
    let crc = crc16('ARC', resp);
    let c1 = (crc >> 8) & 0xFF;
    let c2 = crc & 0xFF;

    resp.push(c1);
    resp.push(c2);

    //logger.debug("checksum", crc);
    //logger.debug("resp", resp);

    return resp;

  }


  function getObjectByTbValue(object, tbname) {
    return object[Object.keys(object).find(key => object[key].tbname === tbname)];
  }


  function isObject(item) {
    return (typeof item === "object" && !Array.isArray(item) && item !== null);
  }


  // we fake data, that should be received from accelerometer, as they are a bit unreliable. (temperature, x,y,z)
  function accelerometerData() {

    if (temperatureInSenica === null) return;

    //clone nodesData and relaysData objects
    let nodesData_clone = JSON.parse(JSON.stringify(nodesData));
    let relaysData_clone = JSON.parse(JSON.stringify(relaysData));

    for (const key in relaysData_clone) {

      const lineData = relaysData_clone[key];
      const lineNumber = lineData.line;
      const contactor = lineData.contactor;

      if (lineNumber === 0) continue;

      if (contactor === 1) {

        let date = Date.now();

        Object.keys(nodesData_clone).forEach((node, index) => {

          setTimeout(function() {

            if (nodesData_clone[node].line === lineNumber) {

              // NOTE: if status of luminaire is NOK or OFFLINE, we do not send data;
              let status = nodesData_clone[node].status;
              if (status === "OFFLINE" || !status) return;

              let x = null;
              if (naklony.hasOwnProperty(node)) x = naklony[node].naklon;
              if (x === null) x = 0;

              sendTelemetry({ temperature: Math.round(temperatureInSenica + 10 + Math.floor(Math.random() * 3)), inclination_x: x, inclination_y: 0, inclination_z: 0 }, nodesData_clone[node].tbname, date);
            }

          }, (index + 1) * 500);
        })

      }
    }
  }


} // end of instance.export

