--    ======================
--    Description
--    ======================
--
--    Logic calculates supply temperature based on outdoor temperature and preset heating curve.
--
--    ======================
--    Parameters
--    ======================
--
--    SubLogic supports following variables:
--
--    input.inside.demand.t.value             - demand inside temperature
--
--    input.outdoor.t.value
--    input.outdoor.t.err                     - input outdoor temperature - status and value
--
--    output.supply.demand.t.value            - generated demand supply temperature, depends on setting.supply.t.mode, it is counter.supply.demand.t.value
--                                              or setting.supply.t.value.manual
--
--
--
--    counter.alert.outdoor.t.err             - set to 1 if input.outdoor.t.err is different than 0
--
--    counter.supply.demand.t.value           - calculated demand supply temperature on the basis of heating curve
--
--    setting.supply.t.mode                   - work mode 0 - auto, 1 - manual
--
--    setting.supply.t.value.manual           - output supply temperature in manual mode
--
--    setting.supply.t.value.alert            - output supply temperature in the case of outdoor temperature error
--
--    setting.supply.t.value.min              - min supply temperature in auto mode
--
--    setting.supply.t.value.max              - max supply output temperature in auto mode
--
--    setting.hc.number                       - heating curve index
--
--    ======================
--    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
--    ======================
--
--    2024-06-24 ver 1.1.2
--
--    # zmiana nazwy parametru outside -> outdoor
--
--    2017-06-07 ver 0.0.1
--
--    # obsługa automatycznego build'a
--    # opis logiki
--
--    2017-03-28 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 - Hysteresis and DownCounter classes
-- require("Hysteresis");
-- require("DownCounter");

-- ---------------------------------------------------------------------------------------------------------
-- Heting curves haracteristics
-- ---------------------------------------------------------------------------------------------------------
HEATING_CURVES_Y_PTS =
{
--|HCNO  |-200|-150| 50|
  {25,   {300, 289, 237}},
  {26,   {317, 305, 245}},
  {27,   {333, 318, 254}},
  {28,   {350, 334, 262}},
  {29,   {367, 347, 271}},
  {30,   {383, 363, 279}},
  {31,   {400, 376, 288}},
  {32,   {417, 392, 296}},
  {33,   {433, 408, 304}},
  {34,   {450, 421, 313}},
  {35,   {467, 437, 321}},
  {36,   {483, 450, 330}},
  {37,   {500, 466, 338}},
  {38,   {517, 479, 347}},
  {39,   {533, 495, 355}},
  {40,   {550, 508, 364}},
  {41,   {567, 524, 372}},
  {42,   {583, 540, 380}},
  {43,   {600, 553, 389}},
  {44,   {617, 569, 397}},
  {45,   {633, 582, 406}},
  {46,   {650, 598, 414}},
  {47,   {667, 611, 423}},
  {48,   {683, 627, 431}},
  {49,   {700, 643, 439}},
  {50,   {717, 656, 448}},
  {51,   {733, 672, 456}},
  {52,   {750, 685, 465}},
  {53,   {767, 701, 473}},
  {54,   {783, 714, 482}},
  {55,   {800, 730, 490}}
};

HEATING_CURVES_X_PTS =
{
       -200, -150, 50
};


-- ---------------------------------------------------------------------------------------------------------
--
-- ---------------------------------------------------------------------------------------------------------

-- logic class
Logic = {};

Logic.__index = Logic;

-- ---------------------------------------------------------------------------------------------------------
--
-- ---------------------------------------------------------------------------------------------------------

function Logic.create()

  --our new object
  local logic         = {};
  setmetatable(logic, Logic);

  --our object fields initialization
  logic.inputOutdoorTValue      = nil;
  logic.inputOutdoorTErr        = nil;
  logic.inputInsideDemandTValue = nil;
  logic.hcNumber                = nil;
  logic.maxSupplyTemperature    = nil;
  logic.minSupplyTemperature    = nil;

  logic.numberOfHC              = 0;
  logic.numberOfXPts            = 0;

  for i, v in pairs(HEATING_CURVES_Y_PTS) do
    logic.numberOfHC = logic.numberOfHC + 1;
  end

  for i, v in pairs(HEATING_CURVES_X_PTS) do
    logic.numberOfXPts = logic.numberOfXPts + 1;
  end

  return logic;

end

-- ---------------------------------------------------------------------------------------------------------
--
-- ---------------------------------------------------------------------------------------------------------

function Logic:readInputs()

  self.inputOutdoorTValue       = getLogicValue("input.outdoor.t.value");
  self.inputOutdoorTErr         = getLogicValue("input.outdoor.t.err");

end

-- ---------------------------------------------------------------------------------------------------------
--
-- ---------------------------------------------------------------------------------------------------------

function Logic:calculateHCSupplyTemperature()


  local calculatedTemperature = nil


  -- coordinates of last common point for all curves
  local x3Init = 200;
  local y3Init = 200;

  -- offset that should be added to the each measuring point because heating
  -- curves comes at the straight line with equation:
  -- demandTemperature = outputTemperature
  local xOffset = self.inputInsideDemandTValue - x3Init;
  local yOffset = xOffset;

  local curveIdx = nil;

  for i = 1, self.numberOfHC do

   if HEATING_CURVES_Y_PTS[i][1] == self.hcNumber then

     curveIdx = i;
     break;

   end

  end

  if curveIdx == nil then

    -- it should not happen
    error("out of range demand heating curve number: " .. self.hcNumber);

  elseif self.inputOutdoorTValue > self.inputInsideDemandTValue then

    --no heating curve can pass through this point. assume min temperature
    calculatedTemperature = self.minSupplyTemperature;

  elseif self.inputOutdoorTValue <= (HEATING_CURVES_X_PTS[1] + xOffset) then

    -- we are before first x chanracteristic point where curves are straight and parallel to x axis
    calculatedTemperature = HEATING_CURVES_Y_PTS[curveIdx][2][1] + yOffset;

  else

    --approximation
    local i = self.numberOfXPts
    local y1 = y3Init + yOffset;
    local x1 = x3Init + xOffset;
    local x0 = 0;
    local y0 = 0;

    -- heating curve consists of three straight sections. determining limits  of these sections
    while i >= 1 do

      x0 = HEATING_CURVES_X_PTS[i] + xOffset;
      y0 = HEATING_CURVES_Y_PTS[curveIdx][2][i] + yOffset;

      if x0 < self.inputOutdoorTValue then

        break;

      end

      x1 = x0;
      y1 = y0;

      i = i - 1;

    end

    calculatedTemperature = ((self.inputOutdoorTValue - x0) * (y1 - y0)) / (x1 - x0);
    calculatedTemperature = calculatedTemperature + y0;

  end

  return calculatedTemperature;

end

-- ---------------------------------------------------------------------------------------------------------
--
-- ---------------------------------------------------------------------------------------------------------

function Logic:handle()

  local alert                     = self.inputOutdoorTErr ~= 0;
  local calculatedTemperature     = nil;
  local outputSupplyTemperature   = nil;

  if alert then


    calculatedTemperature         = getLogicValue("setting.supply.t.value.emergency");
    outputSupplyTemperature       = calculatedTemperature;

  else

    self.minSupplyTemperature     = getLogicValue("setting.supply.t.value.min");
    self.maxSupplyTemperature     = getLogicValue("setting.supply.t.value.max");
    self.hcNumber                 = getLogicValue("setting.hc.number");
    self.inputInsideDemandTValue  = getLogicValue("input.inside.demand.t.value");
    --if less than 0 then 0
    self.inputInsideDemandTValue  = (self.inputInsideDemandTValue < 0) and 0 or self.inputInsideDemandTValue;

    calculatedTemperature         = self:calculateHCSupplyTemperature();
    calculatedTemperature         = math.min(self.maxSupplyTemperature, math.max(self.minSupplyTemperature, calculatedTemperature));

    local manualMode              = getLogicValue("setting.supply.t.mode") ~= 0;
    outputSupplyTemperature       = manualMode and getLogicValue("setting.supply.t.value.manual") or calculatedTemperature;

  end


  setLogicValue("counter.alert.outdoor.t.err", alert);
  setLogicValue("counter.supply.demand.t.value", calculatedTemperature);
  setLogicValue("output.supply.demand.t.value", outputSupplyTemperature);


end

-- ---------------------------------------------------------------------------------------------------------
--
-- ---------------------------------------------------------------------------------------------------------

function Logic:call()

  self:readInputs();
  self:handle();

end

-- ---------------------------------------------------------------------------------------------------------

-- main logic state object
g_logic = nil;

SUPPORTED_SUBLOGIC_TYPE = "HeatingCurve";
SUPPORTED_SUBLOGIC_VERSION = "1.1.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

  if g_logic == nil then

    g_logic = Logic.create();

  end


  g_logic:call()



end

-- ---------------------------------------------------------------------------------------------------------
