--    ======================
--    Description
--    ======================
--
--    Logic provides Thermostat function. Thermostat operates on two outputs: output.radiator and output.floor based on two temperatures: air and floor.
--    Depends of setting.mode there is different way of outputs control.
--    Season dependences based on setting.season.enabled
--
--    ======================
--    Parameters
--    ======================
--
--    SubLogic ThermostatAF_2o supports following variables:
--
--    input.inside.t.value
--    input.inside.t.err                      - input inside air temperature
--
--    input.floor.t.value
--    input.floor.t.err                       - input inside air temperature
--
--    input.season                            - 0 - winter, 1 - summer.
--
--    output.radiator                         - radiator output value [0..1].
--
--    output.floor                            - radiator output value [0..1].
--
--    output.supply.t.demand                  - calculated demand supply temperature. If counter.floor.t.demand.state = 1 this value is equal setting.floor.t.demand + setting.supply.t.add
--
--    setting.inside.t.demand                 - inside demand temperature expressed in tenthes of Celsius degrees.
--
--    setting.floor.t.demand                  - floor demand temperature expressed in tenthes of Celsius degrees.
--
--    setting.inside.t.hyst                   - thermostat inside temperature hysteresis
--
--    setting.floor.t.hyst                    - thermostat floor temperature hysteresis
--
--    setting.output.on.err                   - if thermostat is in auto mode and input.inside.t.err or input.floor.t.err points to error, then to the output.radiator and output.floor
--                                              is copied value of this variable.
--
--    setting.mode                            - work mode:
--                                              0 - manual. always off (output.radiator = 0 and output.floor = 0)
--                                              1 - manual. always on  (output.radiator = 1 and output.floor = 1)
--                                              2 - auto A (Air) heating mode. Thermostat operates on inside air temperature and only on output.radiator. output.floor is always 0
--                                              3 - auto F (Floor) heating mode. Thermostat operates on floor temperature and only on output.floor. output.radiator is always 0
--                                              4 - auto AF (Air-Floor) heating mode. Thermostat operates on output.radiator and output.floor based on inside air temperature and 
--                                                  in the same time operates on output.floor based on floor temperature. For example:
--                                                  a) if inside temperature is too low both outputs are on.
--                                                  b) if inside temperature is high and floor temperature is too low output.radiator=0 and output.floor=1
--                                                  c) if both temperatures are fine then both outputs are off
--
--    setting.season.enabled                  - enable season in thermostat. 
--                                              0 - season is disabled. Thermostat operates on both outputs always. No matter of season.
--                                              1 - season is enabled. Thermostat operates on both outputs in winter only. In summer both outputs are set to 0
--
--    setting.supply.t.add                    - additional value for demand supply temperature.
--
--    counter.inside.t.demand.state           - Thermostat status based on inside temperatura
--
--    counter.floor.t.demand.state            - Thermostat status based on floor temperatura
--
--    counter.alert.t.state                   - set to 1 if input.floor.t.err or input.inside.t.err is different than 0
--
--    Logic uses lua language to implement own behavior
--
--    ======================
--    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
--    ======================
--
--    2017-10-28 ver 0.1.3.0
--
--    # poprawne zachowanie podczas błędu czujnika
--
--    2017-06-07 ver 0.1.2.825
--
--    # obsługa automatycznego build'a
--
--    2017-03-21 ver 0.1.1
--
--    + output.supply.t.demand variable
--    + setting.supply.t.add variable
--
--    2017-03-21 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_outputRadiator = nil;
g_outputFloor = nil;
g_counterInsideTDemandState = nil;
g_counterFloorTDemandState = nil;

SUPPORTED_SUBLOGIC_TYPE = "ThermostatAF_2o";
SUPPORTED_SUBLOGIC_VERSION = "0.1.3";
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 inputInsideTValue = getLogicValue("input.inside.t.value");
  local inputInsideTErr = getLogicValue("input.inside.t.err");
  local inputFloorTValue = getLogicValue("input.floor.t.value");
  local inputFloorTErr = getLogicValue("input.floor.t.err");
  local inputSeason = getLogicValue("input.season");
  local settingInsideTDemand = getLogicValue("setting.inside.t.demand");
  local settingFloorTDemand = getLogicValue("setting.floor.t.demand");
  local settingInsideTHyst = getLogicValue("setting.inside.t.hyst");
  local settingFloorTHyst = getLogicValue("setting.floor.t.hyst");
  local settingOutputOnErr = getLogicValue("setting.output.on.err");
  local settingMode = getLogicValue("setting.mode");
  local settingSeasonEnabled = getLogicValue("setting.season.enabled");
  local settingSupplyTAdd = getLogicValue("setting.supply.t.add");

  local outputSupplyTDemand;

-- states
  if g_outputRadiator == nil or firstCall then
    g_outputRadiator = State.create(0, 0, 1, true);
  end
  g_outputRadiator:call();
  
  if g_outputFloor == nil or firstCall then
    g_outputFloor = State.create(0, 0, 1, true);
  end
  g_outputFloor:call();
  
  if g_counterInsideTDemandState == nil or firstCall then
    g_counterInsideTDemandState = State.create(0, 0, 1, false);
  end
  g_counterInsideTDemandState:call();
  
  if g_counterFloorTDemandState == nil or firstCall then
    g_counterFloorTDemandState = State.create(0, 0, 1, false);
  end
  g_counterFloorTDemandState:call();
  
-- temperature error alert
  local counterAlertTState = 0;
  if (((settingMode == 2) or (settingMode == 4)) and (inputInsideTErr ~= 0)) then
    counterAlertTState = 1;
  end
  
  if (((settingMode == 3) or (settingMode == 4)) and (inputFloorTErr ~= 0)) then
    counterAlertTState = 1;
  end
  
  if (counterAlertTState ~= 0) then
    g_outputRadiator:setValue(settingOutputOnErr);
    g_outputFloor:setValue(settingOutputOnErr);
  end

-- demand temperatures states
  g_counterInsideTDemandState:set1IfLowerThan(inputInsideTValue, settingInsideTDemand, settingInsideTHyst);
  g_counterFloorTDemandState:set1IfLowerThan(inputFloorTValue, settingFloorTDemand, settingFloorTHyst);

-- output operations
  if settingMode == 0 then
    g_outputRadiator:setValue(0);
    g_outputFloor:setValue(0);
    outputSupplyTDemand = 0;
  elseif settingMode == 1 then
    g_outputRadiator:setValue(1);
    g_outputFloor:setValue(1);
    outputSupplyTDemand = settingFloorTDemand + settingSupplyTAdd;
  elseif settingMode == 2 then
    if ((settingSeasonEnabled == 0) or (inputSeason == 0)) then
      g_outputRadiator:setValue(g_counterInsideTDemandState:getValue());
      g_outputFloor:setValue(0);
      outputSupplyTDemand = 0;
    else
      g_outputRadiator:setValue(0);
      g_outputFloor:setValue(0);
      outputSupplyTDemand = 0;
    end
  elseif settingMode == 3 then
    if ((settingSeasonEnabled == 0) or (inputSeason == 0)) then
      g_outputFloor:setValue(g_counterFloorTDemandState:getValue());
      g_outputRadiator:setValue(0);
      outputSupplyTDemand = settingFloorTDemand + settingSupplyTAdd;
    else
      g_outputRadiator:setValue(0);
      g_outputFloor:setValue(0);
      outputSupplyTDemand = 0;
    end
  elseif settingMode == 4 then
    if ((settingSeasonEnabled == 0) or (inputSeason == 0)) then
      g_outputRadiator:setValue(g_counterInsideTDemandState:getValue());
      if ((g_counterInsideTDemandState:getValue() == 1) or (g_counterFloorTDemandState:getValue() == 1)) then
        g_outputFloor:setValue(1);
      else
        g_outputFloor:setValue(0);
      end

      if (g_counterFloorTDemandState:getValue() == 1) then
        outputSupplyTDemand = settingFloorTDemand + settingSupplyTAdd;
      else
        outputSupplyTDemand = 0;
      end
    else
      g_outputRadiator:setValue(0);
      g_outputFloor:setValue(0);
      outputSupplyTDemand = 0;
    end
  end
  
  if (counterAlertTState == 1) then
    outputSupplyTDemand = 0;
  end

-- send logic variables to ibmanager
  setLogicValue("counter.inside.t.demand.state", g_counterInsideTDemandState:getValue());
  setLogicValue("counter.floor.t.demand.state", g_counterFloorTDemandState:getValue());
  setLogicValue("counter.alert.t.state", counterAlertTState);
  setLogicValue("output.radiator", g_outputRadiator:getValue());
  setLogicValue("output.floor", g_outputFloor:getValue());
  setLogicValue("output.supply.t.demand", outputSupplyTDemand);

end
