--    ======================
--    Description
--    ======================
--
--    Logika sterująca systemem solarnym połaciowym, jeden zbiornik.
--
--    Osobna temperatura w kolektorze i na zasilaniu. Załączenie pompy na podstawie temperatury w kolektorze,
--    wyłączenie na podstawie temperatury zasilania z uwzględnieniem zwłoki
--
--    Alarm krytyczny wyłączający pompę priorytetowo.
--
--    ======================
--    Parameters
--    ======================
--
--    SubLogic supports following variables:
--
--    input.solar.inside.t.value
--    input.solar.inside.t.err                - temperatura solar (wewnątrz, pod połacią)
--
--    input.solar.output.t.value
--    input.solar.output.t.err                - temperatura solar (wyjścia)
--
--    input.tank.t.value
--    input.tank.t.err                        - temperatura w zbiorniku
--
--    input.cooldown.tank                     - stan informujący logikę o konieczności wychłodzenia zbiornika. Jeżeli ta wartość jest == 1 to logika stara się wychłodzić
--                                              zbiornik [0..1]. Procedura wychładzania ma wyższy priorytet niż ładowania. 
--
--    output.pump                             - pompa solarna [0..1].
--
--    setting.pump.dt.on
--    setting.pump.dt.off                     - delta włączenia/wyłączenia pompy kolektora
--
--    setting.alert.t.err.pump.state          - jaką ma przyjąć wartość pompa solarna w razie uszkodzenia któregoś czujnika
--
--    setting.alert.t.solar.max.pump.state    - jaką ma przyjąć wartość pompa solarna w razie przekroczenia wysokiej temperatury na solarze
--
--    setting.alert.t.solar.max.value 
--    setting.alert.t.solar.max.hyst          - przy jakiej temperaturze solarnej uaktywnia się counter.alert.t.solar.max, wartość i histereza
--
--    setting.alert.t.tank.max.value 
--    setting.alert.t.tank.max.hyst           - przy jakiej temperaturze w zbiorniku uaktywnia się counter.alert.t.tank.max, wartość i histereza
--
--    setting.pump.off.delay                  - wartość: czas między możliwością wyłączenia pompy (pompa nie może zostać wyłączona wcześniej niż ten odcinek czasu)
--
--    counter.pump.off.downcounter            - licznik: czas między możliwością wyłączenia pompy (pompa nie może zostać wyłączona wcześniej niż ten odcinek czasu)
--
--    counter.alert.t.tank.max                - wartość 1 oznacza przekroczenie maksymalnej temperatury czujnika w zbiorniku - odpowiedni błąd na czujniku lub 
--                                              przekroczenie wartości setting.alert.t.tank.max.value. Aktywny alert wyłącza pompę solarną. Ten alert ma najwyższy priorytet.
--
--    counter.alert.t.err                     - wartość 1 oznacza uszkodzenie jakiegokolwiek czujnika. Powyżej i poniżej maksymalnej nie są traktowane jako błędy. Aktywny
--                                              alert ustawia pompę solarną zgodnie z nastawą setting.alert.t.err.pump.state. Ten alert ma drugi w kolejności priorytet.
--
--    counter.alert.t.solar.max               - wartość 1 oznacza przekroczenie maksymalnej temperatury czujnika na kolektorze - odpowiedni błąd na czujniku lub 
--                                              przekroczenie wartości setting.alert.t.solar.max.value. Aktywny alert ustawia pompę solarną zgodnie 
--                                              z nastawą setting.alert.t.solar.max.pump.state. ten alert ma trzeci w kolejności priorytet.
--
--    counter.cooldown.tank.ready             - czy jest możliwe wychładzanie zbiornika. Temperatura w zbiorniku jest niższa niż w kolektorze z uwzględnieniem setting.pump.dt 
--
--    counter.pump.dt.on.state                - flaga: czy pompa solarna ma zostać załączona ze względu na delte: temperatura solar (wewnątrz, pod połacią) a temperatura w zbiorniku 
--                                              (1 - pompa ma pracować; 0 - pompa ma być wyłączona)
--
--    counter.pump.dt.off.state                - flaga: czy pompa solarna ma zostać wyłączona ze względu na delte: temperatura solar (wyjścia) a temperatura w zbiorniku 
--                                              (1 - pompa ma pracować; 0 - pompa ma być wyłączona)
--
--    Logic uses lua language to implement own behaviour
--
--    ======================
--    mandatory variables
--    ======================
--
--    Logic expects following mandatory variables:
--
--    reload.trigger                          - causes reloading lua script
--
--    memcnt                                  - current amount of memory used by lua in bytes
--
--    Logic expects following kv settings:
--
--    LuaScriptPath                           - path to the lua script - must be absolute
--
--    ======================
--    ChangeLog
--    ======================
--
--    2019-11-02 ver 1.0.0
--
--    # First release
--
--
-- user can use some functions provided by ibmanager.

-- ibmanager provides following functions to use:
--
-- function returns value of required ibmanager variable
-- @param fullName - string - variable name - name of variable of which value must be returned, for example "rs.0.id.255.input.t.0.value"
-- @return         - string or integer - variable value or "nil" if variable not exist or is not readable
--
-- getValue(fullName)


-- function set value of given ibmanager variable
-- @param fullName - string - variable name - name of variable of which value we want to set, for example "rs.0.id.255.input.t.0.value"
-- @param value    - string, int or boolean - value to set
-- @return         - nothing
--
-- setValue(fullName, value)


-- function returns value of required ibmanager variable
-- @param key      - placed in xml logic configuration file:
--                   * as attribute: Name, in Var, RemoteVar and ImportVar elements in the case of stand alone variables
--                   * as concatenation of two attributes: ListName.Postfix in VarListItem, RemoteVarListItem and ImportVarListItem elements
--                     in the case of variables that are placed in lists
-- @return         - string or integer - variable value or "nil" if variable not exist or is not readable
--
-- getLogicValue(key)


-- function set value of given ibmanager variable
-- @param key      - placed in xml logic configuration file:
--                   * as attribute: Name, in Var, RemoteVar and ImportVar elements in the case of stand alone variables
--                   * as concatenation of two attributes: ListName.Postfix in VarListItem, RemoteVarListItem and ImportVarListItem elements
--                     in the case of variables that are placed in lists
-- @param value    - string, int or boolean - value to set
-- @return         - nothing
--
-- setLogicValue(key, value)

-- function returns two sections of kvsettings from xml configuration file
-- returned value is two element table, each of these elements is table too.
-- indices of returned table are strings and equal "instance" and "global"
-- values of returned tables are tables and contain KVsettings for applicable section.
-- nested tables have form key = value, where key is index in nested table and value is value.
-- example: {"instance" = {"ikey1" = "ivalue1", "ikey2" = "gvalue2"}, "global" = {"gkey1" = "gvalue1", "gkey2" = "gvalue2", "gkey3" = "gvalue3"}}
-- @return          - two dimensional array - kvsettings for global and instance sections
-- getKvSettings()


-- function schedules alert to send.
-- rules are defined in separated alert configuration files and are described in ibmanager instruction manual
-- @param id - alert identifier - must be defined in current logic configuration file in section: <Alert Id="any_identifier" ...
-- scheduleAlert(id)

-- function cancels alert sending, if was previously scheduled. if not then only wakes up alerts handling thread, so if there is no need to call this function, then do not call it.
-- rules are defined in separated alert configuration files and are described in ibmanager instruction manual
-- @param id - alert identifier - must be defined in current logic configuration file in section: <Alert Id="any_identifier" ...
-- cancelAlert(name)

-- function returns table, containing Variables that belongs to required list.
-- @param listName - Name attribute of VarList, RemoteVarList or ImportVarList elemetnst in configurationfile
-- @return array of key-value pairs. Key - variable postfix, Value - Variable value
-- getVarList(listName)

-- function returns monotonic system clock value, that elapsed since specific epoch
-- returned time is expressed in milliseconds.
-- getClock()

-- function logs message to file, if defined in configuration file log level is less than passed to this function
-- @param logLevel - one of:
--        LogLevel.TraceLo
--        LogLevel.Trace
--        LogLevel.TraceHi
--        LogLevel.DebugLo
--        LogLevel.Debug
--        LogLevel.DebugHi
--        LogLevel.Info
--        LogLevel.Notice
--        LogLevel.Warning
--        LogLevel.Error
--        LogLevel.Critical
-- @param logMessage - string to log
-- log(logLevel, logMessage)


-- ibmanager provides following global variables:

-- logic type, (in this case it will always be "Lua") - the same as in logic configuration file in section: <Logic Type="Lua" ...
-- LOGIC_TYPE

-- logic version, the same as in logic configuration file in section: <Logic ... Version="x.y.z" ...
-- LOGIC_VERSION

-- logic sub-type, the same as in logic configuration file in section: <Logic ... SubType="Hysteresis" ...
-- LOGIC_SUBTYPE

-- logic sub-version, the same as in logic configuration file in section: <Logic ... SubVersion="x.y.z" ...
-- LOGIC_SUBVERSION

-- logic instance name - the same as in logic configuration file, in section: <Instance Name="0">
-- LOGIC_INSTANCE_NAME

-- add script directory to package path
package.path = package.path .. ";./logic/scripts/utils/?.lua";

-- use script - without .lua extension - delta class
require("State");
require("DownCounter");

g_outputPump = nil;
g_counterAlertTSolarMax = nil;
g_counterAlertTTankMax = nil;
g_counterCooldownTankReady = nil;
g_counterPumpDtOnState = nil;
g_counterPumpDtOffState = nil;

g_settingPumpDtOn = nil;
g_settingPumpDtOff = nil;

g_counterPumpOffDowncounter = nil;

SUPPORTED_SUBLOGIC_TYPE = "Solar04";
SUPPORTED_SUBLOGIC_VERSION = "1.0.0";
g_versionChecked = false;

-- entry point to the logic
-- @param firstCall - tells if logic is called first time
-- @return        - nothing
function onLogicCall(firstCall)

  if not g_versionChecked then
    -- checking sublogic type and sublogic version
    if LOGIC_SUBTYPE ~= SUPPORTED_SUBLOGIC_TYPE then
      error("Wrong logic sub-type. expected " .. SUPPORTED_SUBLOGIC_TYPE .. " but used " .. LOGIC_SUBTYPE);
    end
    local versionWithoutBuild = string.match(LOGIC_SUBVERSION, "[0-9]+%.[0-9]+%.[0-9]+");
    if versionWithoutBuild ~= SUPPORTED_SUBLOGIC_VERSION then
      error("Wrong logic sub-version. expected " .. SUPPORTED_SUBLOGIC_VERSION .. " but used " .. LOGIC_SUBVERSION);
    end
    g_versionChecked = true;
  end

  local inputSolarInsideTValue = getLogicValue("input.solar.inside.t.value");
  local inputSolarInsideTErr = getLogicValue("input.solar.inside.t.err");
  local inputSolarOutputTValue = getLogicValue("input.solar.output.t.value");
  local inputSolarOutputTErr = getLogicValue("input.solar.output.t.err");
  local inputTankTValue = getLogicValue("input.tank.t.value");
  local inputTankTErr = getLogicValue("input.tank.t.err");
  local inputCooldownTank = getLogicValue("input.cooldown.tank");
  local settingPumpDtOn = getLogicValue("setting.pump.dt.on");
  local settingPumpDtOff = getLogicValue("setting.pump.dt.off");
  local settingAlertTErrPumpState = getLogicValue("setting.alert.t.err.pump.state");
  local settingAlertTSolarMaxPumpState = getLogicValue("setting.alert.t.solar.max.pump.state");
  local settingAlertTSolarMaxValue = getLogicValue("setting.alert.t.solar.max.value");
  local settingAlertTSolarMaxHyst = getLogicValue("setting.alert.t.solar.max.hyst");
  local settingAlertTTankMaxValue = getLogicValue("setting.alert.t.tank.max.value");
  local settingAlertTTankMaxHyst = getLogicValue("setting.alert.t.tank.max.hyst");
  local settingPumpOffDelay = getLogicValue("setting.pump.off.delay");

  local counterAlertTErr = 0;
  local counterPumpOffDowncounter = 0;

  -- timers

  if g_counterPumpOffDowncounter == nil or firstCall then
    g_counterPumpOffDowncounter = DownCounter.create();
  end
--  g_counterPumpOffDowncounter:updateParams(settingPumpOffDelay * 1000);

--  if g_counterPumpOffDowncounter:elapsed() then
--    g_counterPumpOffDowncounter:reset();
--  end

  counterPumpOffDowncounter = g_counterPumpOffDowncounter:timeTo0() / 1000;
  counterPumpOffDowncounter = (counterPumpOffDowncounter < 0) and 0 or counterPumpOffDowncounter;

-- states
  if g_outputPump == nil or firstCall then
    g_outputPump = State.create(0, 0, 1, true);
  end
  g_outputPump:call();

  if g_counterAlertTSolarMax == nil or firstCall then
    g_counterAlertTSolarMax = State.create(0, 0, 1, false);
  end
  g_counterAlertTSolarMax:call();

  if g_counterAlertTTankMax == nil or firstCall then
    g_counterAlertTTankMax = State.create(0, 0, 1, false);
  end
  g_counterAlertTTankMax:call();

  if g_counterCooldownTankReady == nil or firstCall then
    g_counterCooldownTankReady = State.create(0, 0, 1, false);
  end
  g_counterCooldownTankReady:call();

  if g_counterPumpDtOnState == nil or firstCall then
    g_counterPumpDtOnState = State.create(0, 0, 1, false);
  end
  g_counterPumpDtOnState:call();

  if g_counterPumpDtOffState == nil or firstCall then
    g_counterPumpDtOffState = State.create(0, 0, 1, false);
  end
  g_counterPumpDtOffState:call();

  --PumpDt
  if g_settingPumpDtOn == nil or firstCall then
    g_settingPumpDtOn = settingPumpDtOn;
  end

  if g_settingPumpDtOff == nil or firstCall then
    g_settingPumpDtOff = settingPumpDtOff;
  end 

  if (g_settingPumpDtOn ~= settingPumpDtOn) then
    if (settingPumpDtOn <= settingPumpDtOff) then
      settingPumpDtOn = settingPumpDtOff + 1;
      setLogicValue("setting.pump.dt.on", settingPumpDtOn);
    end
  end

  if (g_settingPumpDtOff ~= settingPumpDtOff) then
    if (settingPumpDtOff >= settingPumpDtOn) then
      settingPumpDtOff = settingPumpDtOn - 1;
      setLogicValue("setting.pump.dt.off", settingPumpDtOff);
    end
  end

  -- w przypadku pozostałości ze storage
  if (settingPumpDtOn <= settingPumpDtOff) then
    settingPumpDtOn = settingPumpDtOff + 1;
    setLogicValue("setting.pump.dt.on", settingPumpDtOn);
  end

  g_settingPumpDtOn = settingPumpDtOn;
  g_settingPumpDtOff = settingPumpDtOff;

-- alert zbyt wysokiej temperatury w zbiorniku
  if (inputTankTErr == 3) then
    g_counterAlertTTankMax:setValue(1);
  else
    local highestTankTemp = inputTankTValue;

    g_counterAlertTTankMax:set1IfHigherThan(highestTankTemp, settingAlertTTankMaxValue, settingAlertTTankMaxHyst);
  end

-- stan pompy solarnej podczas alertu zbyt wysokiej temperatury zbiornika
  if g_counterAlertTTankMax:getValue() == 1 then
    g_outputPump:setValue(0);
  end

-- alert uszkodzonego czujnika
  if ((inputSolarOutputTErr ~= 0) and (inputSolarOutputTErr ~= 3)) or
     ((inputSolarInsideTErr ~= 0) and (inputSolarInsideTErr ~= 3)) or
     ((inputTankTErr ~= 0) and (inputTankTErr ~= 3)) then
    counterAlertTErr = 1;
  end

-- stan pompy solarnej podczas alertu uszkodzonego czujnika
  if counterAlertTErr == 1 then
    g_outputPump:setValue(settingAlertTErrPumpState);
  end

-- alert zbyt wysokiej temperatury na kolektorze
  if inputSolarOutputTErr == 3 then
    g_counterAlertTSolarMax:setValue(1);
  else
    g_counterAlertTSolarMax:set1IfHigherThan(inputSolarOutputTValue, settingAlertTSolarMaxValue, settingAlertTSolarMaxHyst);
  end

-- stan pompy solarnej podczas alertu zbyt wysokiej temperatury solar
  if g_counterAlertTSolarMax:getValue() == 1 then
    g_outputPump:setValue(settingAlertTSolarMaxPumpState);
  end

-- obsługa wychładzania
  g_counterCooldownTankReady:setIfDelta(inputTankTValue, inputSolarInsideTValue, settingPumpDtOn, settingPumpDtOff);

  if (inputCooldownTank == 1) and (g_counterCooldownTankReady:getValue() == 1) then
    g_outputPump:setValue(1);
  end

  -- Obsługa pompy kolektora
  
  g_counterPumpDtOnState:setIfDelta(inputSolarInsideTValue, inputTankTValue, settingPumpDtOn, settingPumpDtOff);
  g_counterPumpDtOffState:setIfDelta(inputSolarOutputTValue, inputTankTValue, settingPumpDtOn, settingPumpDtOff);
  
  if (counterPumpOffDowncounter ~= 0) then
    g_outputPump:setValue(1);
  else
    
    if (g_counterPumpDtOnState:getValue() == 1) and (g_counterPumpDtOffState:getValue() == 0) then
      g_outputPump:setValue(1);
    end
    
    if (g_outputPump:getValue() == 0) then
      g_outputPump:setValue(g_counterPumpDtOnState:getValue());
    else
      g_outputPump:setValue(g_counterPumpDtOffState:getValue());
    end
  end
   
-- jeżeli żaden warunek wcześniej nie operował na pompie solarnej to ją wyłącz
--  g_outputPump:setValue(0);
  
  if ((g_outputPump:getValue() == 1) and (g_outputPump:isChanged() == true)) then
    g_counterPumpOffDowncounter:updateParams(settingPumpOffDelay * 1000);
    g_counterPumpOffDowncounter:reset();
  end

-- send logic variables to ibmanager
  setLogicValue("output.pump", g_outputPump:getValue());
  setLogicValue("counter.pump.off.downcounter", counterPumpOffDowncounter);
  setLogicValue("counter.alert.t.err", counterAlertTErr);
  setLogicValue("counter.alert.t.solar.max", g_counterAlertTSolarMax:getValue());
  setLogicValue("counter.alert.t.tank.max", g_counterAlertTTankMax:getValue());
  setLogicValue("counter.cooldown.tank.ready", g_counterCooldownTankReady:getValue());
  setLogicValue("counter.pump.dt.on.state", g_counterPumpDtOnState:getValue());
  setLogicValue("counter.pump.dt.off.state", g_counterPumpDtOffState:getValue());

end
