--  ======================
--  Description
--  ======================
--
--  Logika sterująca systemem solarnym, ładowanie zawsze gdy setting.solar.enabled, niezależnie od warunków temperaturowych.
--
--  Trzy zbiorniki tank.0, tank.1, tank.2 ładowane przez solar. Zbiorniki tank.1 i tank.2 ładowane do limitu temperatury i tylko gdy warunek różnicy
--  temperatury między temperaturą zasilania solar a zbiornikiem jest spełniony. Jeżeli zbiorniki tank.1 i tank.2 są już zapełnione lub nie ma warunków na
--  ładowanie tych zbiorników to ładowany jest tank.0
--
--  Obsługa zrzutu nadmiaru ciepła do gruntu ze zbiornika tank.0. Zrzut jest realizowany w zależności od sezonu grzewczego:
--
--  w lecie:
--
--  może być realizowany tylko wtedy gdy temperatura w tank.0 jest powyżej minimalnej
--  kiedy zbiornik tank.0 jest ładowany przez solary to gdy tank.1 i tank.2 nie są załadowane to zrzut jest blokowany. Jeżeli zbiorniki są załadowane to
--  zrzut jest możliwy.
--
--  ======================
--  Parameters
--  ======================
--
--  SubLogic supports following variables:
--
--  input.solar.output.t.value
--  input.solar.output.t.err                - temperatura zasilania solar
--
--  input.tank.0.t.value
--  input.tank.0.t.err                      - temperatura zbiornika 0
--
--  input.tank.1.t.value
--  input.tank.1.t.err                      - temperatura zbiornika 1
--
--  input.tank.2.t.value
--  input.tank.2.t.err                      - temperatura zbiornika 2
--
--  input.season                            - 0 - winter, 1 - summer. [0..1]
--
--  input.heatpump.state                    - stan pompy ciepła. Zimą rozładowywanie tank.0. do gruntu jest możliwe tylko jeżeli pompa ciepła pracuje. [0..1]
--
--  output.solar.tank.0                     - pompa solarna ładująca zbiornik 0 [0..1].
--
--  output.solar.tank.1                     - pompa solarna ładująca zbiornik 1 [0..1].
--
--  output.solar.tank.2                     - pompa solarna ładująca zbiornik 2 [0..1].
--
--  output.ground.pump                      - pompa zrzucająca ciepło ze zbiornika 0 do gruntu [0..1]. Zrzut do gruntu jest możliwy tylko w przypadku gdy:
--                                              - temperatura w tank.0 jest wyższe niż zadana
--                                              - tank.0 nie jest ładowany przez solary lub jest ładowany przez solar i zbiorniki tank.1 i tank.2 są załadowane maksymalnie
--                                              - zimą: jeżeli pompa ciepła pracuje
--
--  setting.solar.enabled                   - czy ładowanie solarami jest aktywne. Kiedy 1 to zawsze któraś pompa ładująca solarna pracuje.
--                                            Kiedy 0 to żadna pompa ładująca nie pracuje. [0..1]
--
--  setting.tank.1.max.value
--  setting.tank.1.max.hyst                 - temperatura maksymalna do jakiej jest ładowany zbiornik 1. Wartość i histereza
--
--  setting.tank.2.max.value
--  setting.tank.2.max.hyst                 - temperatura maksymalna do jakiej jest ładowany zbiornik 2. Wartość i histereza
--
--  setting.tank.0.min.value
--  setting.tank.0.min.hyst                 - temperatura minimalna do której schładzany jest zbiornik 0 przez pompę gruntową. Wartość i histereza
--
--  setting.solar.pump.dt.on
--  setting.solar.pump.dt.off               - delta przełączenia pompy kolektora
--
--  setting.alert.t.err.solar.tank.0.state  - jaką ma przyjąć wartość pompa ładująca zbiornik 0 w razie uszkodzenia któregoś czujnika. [0..1]
--
--  setting.alert.t.err.solar.tank.1.state  - jaką ma przyjąć wartość pompa ładująca zbiornik 1 w razie uszkodzenia któregoś czujnika. [0..1]
--
--  setting.alert.t.err.solar.tank.2.state  - jaką ma przyjąć wartość pompa ładująca zbiornik 2 w razie uszkodzenia któregoś czujnika. [0..1]
--
--  counter.tank.1.max.state                - czy zbiornik tank.1 został maksymalnie załadowany. [0..1]
--
--  counter.tank.2.max.state                - czy zbiornik tank.1 został maksymalnie załadowany. [0..1]
--
--  counter.tank.0.min.state                - czy zbiornik tank.0 jest gotowy aby go rozładowywać do gruntu. [0..1]
--
--  counter.tank.1.dt.state                 - czy warunek ładowania zbiornika tank.1 przez solar jest spełniony
--
--  counter.tank.2.dt.state                 - czy warunek ładowania zbiornika tank.2 przez solar jest spełniony
--
--  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.solar.tank.X.state. [0..1]
--
--  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
--  ======================
--
--    2018-07-07 ver 0.0.2
--
--    # zabezpieczenie delty
--
--  2017-06-07 ver 0.0.1.825
--
--  # obsługa automatycznego build'a
--
--  2017-05-23 ver 0.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");

g_outputSolarTank0 = nil;
g_outputSolarTank1 = nil;
g_outputSolarTank2 = nil;
g_outputGroundPump = nil;
g_counterTank1MaxState = nil;
g_counterTank2MaxState = nil;
g_counterTank0MinState = nil;
g_counterTank1DtState = nil;
g_counterTank2DtState = nil;

g_settingSolarPumpDtOn = nil;
g_settingSolarPumpDtOff = nil;

SUPPORTED_SUBLOGIC_TYPE = "Solar02";
SUPPORTED_SUBLOGIC_VERSION = "0.0.2";
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 inputSolarOutputTValue = getLogicValue("input.solar.output.t.value");
  local inputSolarOutputTErr = getLogicValue("input.solar.output.t.err");
  local inputTank0TValue = getLogicValue("input.tank.0.t.value");
  local inputTank0TErr = getLogicValue("input.tank.0.t.err");
  local inputTank1TValue = getLogicValue("input.tank.1.t.value");
  local inputTank1TErr = getLogicValue("input.tank.1.t.err");
  local inputTank2TValue = getLogicValue("input.tank.2.t.value");
  local inputTank2TErr = getLogicValue("input.tank.2.t.err");
  local inputSeason = getLogicValue("input.season");
  local inputHeatpumpState = getLogicValue("input.heatpump.state");
  local settingSolarEnabled = getLogicValue("setting.solar.enabled");
  local settingTank1MaxValue = getLogicValue("setting.tank.1.max.value");
  local settingTank1MaxHyst = getLogicValue("setting.tank.1.max.hyst");
  local settingTank2MaxValue = getLogicValue("setting.tank.2.max.value");
  local settingTank2MaxHyst = getLogicValue("setting.tank.2.max.hyst");
  local settingTank0MinValue = getLogicValue("setting.tank.0.min.value");
  local settingTank0MinHyst = getLogicValue("setting.tank.0.min.hyst");
  local settingSolarPumpDtOn = getLogicValue("setting.solar.pump.dt.on");
  local settingSolarPumpDtOff = getLogicValue("setting.solar.pump.dt.off");
  local settingAlertTErrSolarTank0State = getLogicValue("setting.alert.t.err.solar.tank.0.state");
  local settingAlertTErrSolarTank1State = getLogicValue("setting.alert.t.err.solar.tank.1.state");
  local settingAlertTErrSolarTank2State = getLogicValue("setting.alert.t.err.solar.tank.2.state");

  local counterAlertTErr = 0;

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

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

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

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

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

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

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

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

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

  --SolarPumpDt
  if g_settingSolarPumpDtOn == nil or firstCall then
    g_settingSolarPumpDtOn = settingSolarPumpDtOn;
  end

  if g_settingSolarPumpDtOff == nil or firstCall then
    g_settingSolarPumpDtOff = settingSolarPumpDtOff;
  end 

  if (g_settingSolarPumpDtOn ~= settingSolarPumpDtOn) then
    if (settingSolarPumpDtOn <= settingSolarPumpDtOff) then
      settingSolarPumpDtOn = settingSolarPumpDtOff + 1;
      setLogicValue("setting.solar.pump.dt.on", settingSolarPumpDtOn);
    end
  end

  if (g_settingSolarPumpDtOff ~= settingSolarPumpDtOff) then
    if (settingSolarPumpDtOff >= settingSolarPumpDtOn) then
      settingSolarPumpDtOff = settingSolarPumpDtOn - 1;
      setLogicValue("setting.solar.pump.dt.off", settingSolarPumpDtOff);
    end
  end

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

  g_settingSolarPumpDtOn = settingSolarPumpDtOn;
  g_settingSolarPumpDtOff = settingSolarPumpDtOff;

  -- temperature error alert
  if ((inputSolarOutputTErr ~= 0) and (inputSolarOutputTErr ~= 3)) or
     ((inputTank0TErr ~= 0) and (inputTank0TErr ~= 3)) or
     ((inputTank1TErr ~= 0) and (inputTank1TErr ~= 3)) or
     ((inputTank2TErr ~= 0) and (inputTank2TErr ~= 3)) then
    counterAlertTErr = 1;

    g_outputSolarTank0:setValue(settingAlertTErrSolarTank0State);
    g_outputSolarTank1:setValue(settingAlertTErrSolarTank1State);
    g_outputSolarTank2:setValue(settingAlertTErrSolarTank2State);
  end

  -- temperature based states
  g_counterTank1MaxState:set1IfHigherThan(inputTank1TValue, settingTank1MaxValue, settingTank1MaxHyst);
  g_counterTank2MaxState:set1IfHigherThan(inputTank2TValue, settingTank2MaxValue, settingTank2MaxHyst);
  g_counterTank0MinState:set1IfHigherThan(inputTank0TValue, settingTank0MinValue, settingTank0MinHyst);
  g_counterTank1DtState:setIfDelta(inputSolarOutputTValue, inputTank1TValue, settingSolarPumpDtOn, settingSolarPumpDtOff);
  g_counterTank2DtState:setIfDelta(inputSolarOutputTValue, inputTank2TValue, settingSolarPumpDtOn, settingSolarPumpDtOff);
  
  -- solar nieaktywny - wyłącz pompy
  if settingSolarEnabled == 0 then
    g_outputSolarTank0:setValue(0);
    g_outputSolarTank1:setValue(0);
    g_outputSolarTank2:setValue(0);
  end
  
  -- wybór który zbiornik ładować 
  if (g_counterTank2MaxState:getValue() == 0) and (g_counterTank2DtState:getValue() == 1) then
    g_outputSolarTank0:setValue(0);
    g_outputSolarTank1:setValue(0);
    g_outputSolarTank2:setValue(1);   
  elseif (g_counterTank1MaxState:getValue() == 0) and (g_counterTank1DtState:getValue() == 1) then
    g_outputSolarTank0:setValue(0);
    g_outputSolarTank1:setValue(1);
    g_outputSolarTank2:setValue(0);
  else
    g_outputSolarTank0:setValue(1);
    g_outputSolarTank1:setValue(0);
    g_outputSolarTank2:setValue(0);
  end
  
  -- zrzut do gruntu
  if (inputSeason == 1) then
    -- lato
    if (g_outputSolarTank0:getValue() == 0) then
      g_outputGroundPump:setValue(g_counterTank0MinState:getValue());
    else
      if (g_counterTank2MaxState:getValue() == 1) and (g_counterTank1MaxState:getValue() == 1) then
        g_outputGroundPump:setValue(g_counterTank0MinState:getValue());
      else
        g_outputGroundPump:setValue(0);
      end
    end
  else
    -- zima
    if (inputHeatpumpState == 0) then
      g_outputGroundPump:setValue(0);
    else
      g_outputGroundPump:setValue(g_counterTank0MinState:getValue());
    end
  end

  -- send logic variables to ibmanager
  setLogicValue("output.solar.tank.0", g_outputSolarTank0:getValue());
  setLogicValue("output.solar.tank.1", g_outputSolarTank1:getValue());
  setLogicValue("output.solar.tank.2", g_outputSolarTank2:getValue());
  setLogicValue("output.ground.pump", g_outputGroundPump:getValue());
  setLogicValue("counter.tank.1.max.state", g_counterTank1MaxState:getValue());
  setLogicValue("counter.tank.2.max.state", g_counterTank2MaxState:getValue());
  setLogicValue("counter.tank.0.min.state", g_counterTank0MinState:getValue());
  setLogicValue("counter.tank.1.dt.state", g_counterTank1DtState:getValue());
  setLogicValue("counter.tank.2.dt.state", g_counterTank2DtState:getValue());
  setLogicValue("counter.alert.t.err", counterAlertTErr);

end
