--    ======================
--    Description
--    ======================
--
--    SubLogic MixZone drives heating and cooling zone using mixing valve. Logic includes internal temperature.
--    If supply temperature is too low (only when cooling), or too high (always) then pump is disabled and raised is applicable alert.
--    3-way mixing valve driving covers 3 point control:
--
--    - Heating: 0 - takes heating medium from supply, 1 - takes cooling medium from output, 2 - idle
--      if internal temperature input.inside.t.value is greater by setting.inside.dt.alert than input.inside.demand.t.value then pump will be disabled.
--
--    - Cooling: 0 - takes cooling medium from supply, 1 - takes heating medium from output, 2 - idle
--      if internal temperature input.inside.t.value is less by setting.inside.dt.alert than input.inside.demand.t.value then pump will be disabled.
--
--    Logic does not heat in winter and does not cool in summer. In these cases, pump is disabled and valve takes medium from return (output.valve = 0)
--
--    ======================
--    Parameters
--    ======================
--
--    SubLogic supports following variables:
--
--    input.supply.demand.t.value             - demand supply temperature [300]
--
--    input.inside.demand.t.value             - demand inside temperature [210]
--
--    input.supply.t.value
--    input.supply.t.err                      - input supply temperature - status and value [0]
--
--    input.inside.t.value
--    input.inside.t.err                      - input internal temperature - status and value [0]
--
--    input.season                            - current season 0 - winter, 1 - summer [0]
--
--    output.valve                            - output valve state 0 - takes from return, 1 - takes from supply, 2 - no changes
--
--    output.pump                             - pump state - 0 - off, 1 - on
--
--    counter.alert.supply.t.min              - too low supply temperature. If is set then pump is disabled and valve takes medium from return (output.valve = 0).
--                                              Can be active only if counter.current.cooling == 1. [0..1]
--
--    counter.alert.supply.t.max              - too high supply temperature. If is set then pump is disabled and valve takes medium from return (output.valve = 0). [0..1]
--
--    counter.alert.supply.t.err              - supply temperature error. If is set then pump is disabled. [0..1]
--
--    counter.alert.inside.t.min              - too low inside temperature. If is set then pump is disabled. Can be active only if counter.current.cooling == 1. [0..1]
--
--    counter.alert.inside.t.max              - too high inside temperature. If is set then pump is disabled. Can be active only if counter.current.cooling == 1. [0..1]
--
--    counter.alert.inside.t.err              - inside temperature error. If is set then copies setting.supply.demand.t.alert.inside to counter.supply.demand.t [0..1]
--
--    counter.supply.demand.t.value           - current demand supply temperature
--
--    counter.current.heating                 - if 1 then logic currently heats (pump is enabled and valve is driven on the basis of heating process)
--
--    counter.current.cooling                 - if 1 then logic currently cools. (pump is enable d and valve is driven ot the basis of cooling process)
--
--    counter.alert.inside.t.value            - internal temperature base threshold, that activates counter.alert.inside.t.min or counter.alert.inside.t.max alerts.
--                                              Heating, threshold = input.inside.demand.t.value + setting.inside.dt.alert
--                                              Cooling, threshold = input.inside.demand.t.value - setting.inside.dt.alert
--
--    counter.valve.downtime                  - time to end of work/idle valve stage expressed in 0.1 sec
--
--    setting.supply.demand.t.alert.inside    - demand supply temperature when counter.alert.inside.t.err alert is raised [300]
--
--    setting.heating.enabled
--    setting.cooling.enabled                 - flags tells if currently logic heats or cools. [1][0]
--                                              - setting.heating.enabled = 0, setting.cooling.enabled = 0 - not allowed. pump is disabled and valve takes medium from return (output.valve = 0).
--                                              - setting.heating.enabled = 0, setting.cooling.enabled = 1 - if input.season = 1 (summer) then counter.current.cooling = 1 - cooling is active.
--                                                if input.season = 1 (winter) then counter.current.cooling = 0 - pump is disabled and valve takes medium from return (output.valve = 0)
--                                              - setting.heating.enabled = 1, setting.cooling.enabled = 0 - if input.season = 0 (winter) then counter.current.heating = 1 - heating is active.
--                                                if input.season = 0 (summer) then counter.current.heating = 0 - pump is disabled and valve takes medium from return (output.valve = 0)
--                                              - setting.heating.enabled = 1, setting.cooling.enabled = 1 - Logic makes decision if currently is heating or cooling on the basis of input.season.
--                                                if input.season = 0 (winter) and rest control conditions are fulfilled, then heating is enabled. if input.season = 1 (summer) and rest control
--                                                conditions are fulfilled, then cooling is enabled.
--
--
--    setting.supply.t.value.min              - min. supply temperature when cooling is active [170]
--    setting.supply.t.value.max              - max. supply temperature [550]
--    setting.supply.t.hyst                   - supply temperature hysteresis [40]
--    setting.inside.dt.alert                 - delta internal temperature - used to determine counter.alert.inside.t.value. [20]
--    setting.inside.t.hyst                   - internal temperature hysteresis [10]
--    setting.inside.t.enabled                - enable/disable internal temperature handling [1]
--    setting.valve.on                        - valve active time [20] - 0.1sec
--    setting.valve.off                       - valve idle time [1200] - 0.1sec
--
--    ======================
--    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-09-25 ver 0.0.4.0
--
--    # bugfix
--
--    2017-09-20 ver 0.0.3.0
--
--    # jeżeli błąd czujnika zasilania to wymagana = 0
--
--    2017-06-07 ver 0.0.2.825
--
--    # obsługa automatycznego build'a
--    # opis logiki
--
--    2017-03-28 ver 0.0.1
--
--    # fixed logic according to clues.
--
--    2017-03-17 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");
require("DeadZone");

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

-- logic class
Logic = {};

Logic.__index = Logic;

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

function Logic.create()

  --our new object
  local logic         = {};  
  setmetatable(logic, Logic);
  
  --our object fields initialization
  logic.insideHyst              = Hysteresis.create(0, false, false, 0, false);
  logic.supplyMinHyst           = Hysteresis.create(0, false, false, 0, false);
  logic.supplyMaxHyst           = Hysteresis.create(0, false, false, 0, false);
  logic.deadZone                = DeadZone.create(0, 1, 0, 0, 2);
  logic.valveWorkDownCounter    = DownCounter.create();
  logic.valveCycleDownCounter   = DownCounter.create();
  logic.handleInsideTemp        = nil;
  logic.currSupplyDemandTemp    = nil;
  logic.currInsideDemandTemp    = nil;
  logic.isCooling               = nil;
  logic.isHeating               = nil;
  logic.supplyTemperature       = nil;
  logic.insideTemperature       = nil;
  logic.insideAlertTemperature  = nil;
  logic.pumpOut                 = nil;
  logic.valveOut                = nil;
  logic.valveDowntime           = nil;
  logic.isSummer                = nil;
  logic.alerts                  = {sTMin = nil, sTMax = nil, sTErr = nil, iTMin = nil, iTMax = nil, iTErr = nil};
  
  
  return logic;
  
end

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

function Logic:calculateDemandTempeartures()
  
  local dt                    = getLogicValue("setting.inside.dt.alert");
  self.currInsideDemandTemp   = getLogicValue("input.inside.demand.t.value");
  self.currInsideDemandTemp   = self.isHeating and (self.currInsideDemandTemp + dt) or (self.currInsideDemandTemp - dt);
  
  if (self.alerts.sTErr == false) then
    self.currSupplyDemandTemp   = self.alerts.iTErr and getLogicValue("setting.supply.demand.t.alert.inside") or getLogicValue("input.supply.demand.t.value");
  else
    self.currSupplyDemandTemp   = 0;
  end
  
end

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

function Logic:readInputs()
  
  self.supplyTemperature      = getLogicValue("input.supply.t.value");
  self.insideTemperature      = getLogicValue("input.inside.t.value");
  self.insideAlertTemperature = getLogicValue("input.inside.demand.t.value");
  self.isSummer               = getLogicValue("input.season") ~= 0;
  
  self.handleInsideTemp       = getLogicValue("setting.inside.t.enabled") ~= 0;
  self.alerts.sTErr           = getLogicValue("input.supply.t.err") ~= 0;
  self.alerts.iTErr           = self.handleInsideTemp and (getLogicValue("input.inside.t.err") ~= 0);

end

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

function Logic:setOutputs()
  
  setLogicValue("counter.alert.inside.t.value", self.insideAlertTemperature);
  setLogicValue("counter.supply.demand.t.value", self.currSupplyDemandTemp);
  setLogicValue("counter.current.heating", self.isHeating);
  setLogicValue("counter.current.cooling", self.isCooling);
  setLogicValue("counter.valve.downtime", self.valveDowntime);
  
  setLogicValue("output.valve", self.valveOut);
  setLogicValue("output.pump", self.pumpOut);
  
  setLogicValue("counter.alert.supply.t.min", self.alerts.sTMin);
  setLogicValue("counter.alert.supply.t.max", self.alerts.sTMax);
  setLogicValue("counter.alert.supply.t.err", self.alerts.sTErr);
  setLogicValue("counter.alert.inside.t.min", self.alerts.iTMin);
  setLogicValue("counter.alert.inside.t.max", self.alerts.iTMax);
  setLogicValue("counter.alert.inside.t.err", self.alerts.iTErr);


end

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

function Logic:determineCoolingOrHeating()
  
  self.isHeating     = getLogicValue("setting.heating.enabled") ~= 0;
  self.isCooling     = getLogicValue("setting.cooling.enabled") ~= 0;
  
  if (self.isHeating and not self.isCooling) or (self.isHeating  and self.isCooling  and not self.isSummer) then
  
    -- is heating
    self.isCooling     = false;
    
  elseif (not self.isHeating and self.isCooling) or (self.isHeating and self.isCooling and self.isSummer) then
  
    -- is cooling
    self.isHeating     = false;
    
  else
  
    --not allowed, neither heating nor cooling
    self.isCooling     = false;
    self.isHeating     = false;
    
  end
  
  
end

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

function Logic:updateControlObjects()

  local dt                    = getLogicValue("setting.inside.dt.alert");
  local width                 = getLogicValue("setting.inside.t.hyst");
  local supplyMin             = getLogicValue("setting.supply.t.value.min");
  local supplyMax             = getLogicValue("setting.supply.t.value.max");
  -- conversion to milliseconds from 0.1sec:
  local valveWorkPeriod       = getLogicValue("setting.valve.on") * 100;
  local valvePausePeriod      = getLogicValue("setting.valve.off") * 100;

  if (self.isHeating) then
  
    self.insideAlertTemperature  = self.insideAlertTemperature + dt;
    
  elseif (self.isCooling) then
  
    self.insideAlertTemperature  = self.insideAlertTemperature - dt;
    
  end

  -- inside temperature hysteresis. if inside temperature should be ignored, then this output is always enabled. boolean output
  self.insideHyst:updateParameters(self.insideAlertTemperature, self.isHeating or (not self.handleInsideTemp), self.isCooling or (not self.handleInsideTemp), width);

  width = getLogicValue("setting.supply.t.hyst");

  -- valve drive dead zone from supply. 0 - take from return, 1 - take from supply, 2 - no changes
  -- heating - take heat from supply, take cold from return
  -- cooling - take heat from return, take cold from supply
  self.deadZone:updateParameters(self.currSupplyDemandTemp, self.isHeating and 1 or 0, self.isCooling and 1 or 0, width, 2);
  
  -- min supply hysteresis updating - if not cooling then skipped
  self.supplyMinHyst:updateParameters(supplyMin, not self.isCooling, true, width);
  
  -- max supply hysteresis updating
  self.supplyMaxHyst:updateParameters(supplyMax, true, false, width);
  
  -- update work time down counter
  self.valveWorkDownCounter:updateParams(valveWorkPeriod);
  
  -- update pause plus work time down counter
  self.valveCycleDownCounter:updateParams(valvePausePeriod + valveWorkPeriod);
  
  
end

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

function Logic:handle()
  
  local insideOut = self.insideHyst:getOutputValue(self.insideTemperature);
  local supplyAboveMin = self.supplyMinHyst:getOutputValue(self.supplyTemperature);
  local supplyBelowMax = self.supplyMaxHyst:getOutputValue(self.supplyTemperature);
  
  self.alerts.sTMin = not supplyAboveMin;
  self.alerts.sTMax = not supplyBelowMax;
  
  self.alerts.iTMin = self.isCooling and (not insideOut);
  self.alerts.iTMax = self.isHeating and (not insideOut);
  
  self.pumpOut = (insideOut and supplyAboveMin and supplyBelowMax and (not self.alerts.sTErr)) and 1 or 0;
  
  local supplyOut = self.deadZone:getOutputValue(self.supplyTemperature);
  
  self.valveOut = (self.pumpOut ~= 0) and supplyOut or 2;
  
  if self.valveWorkDownCounter:elapsed() then
  
    self.valveDowntime = self.valveCycleDownCounter:timeTo0();
    
    if (self.valveCycleDownCounter:elapsed()) then
      
      --elapsed both work time and work plus pause time. leave valveOut as is and reset down counters
      self.valveWorkDownCounter:reset();
      self.valveCycleDownCounter:reset();
      
    else
    
      --elapsed work valve time but cycle not finished. disable valve
      self.valveOut = 2;
    
    end
  
  
  else
  
    self.valveDowntime = self.valveWorkDownCounter:timeTo0();
  
  end
  
 
  -- counter.alert.supply.t.min or counter.alert.supply.t.max closes valve
  self.valveOut = (self.alerts.sTMin or self.alerts.sTMax) and 0 or self.valveOut;

  -- if selected neither heating nor cooling then closing valve and disable pump
  self.valveOut = ((not self.isCooling) and (not self.isHeating)) and 0 or self.valveOut;
  self.pumpOut = ((not self.isCooling) and (not self.isHeating)) and 0 or self.pumpOut;

  -- in summer we do not heating and in winter we do not cooling. disable pump and closing valve
  self.valveOut = ((not self.isSummer and self.isCooling) or (self.isSummer and self.isHeating)) and 0 or self.valveOut;
  self.pumpOut = ((not self.isSummer and self.isCooling) or (self.isSummer and self.isHeating)) and 0 or self.pumpOut;
 
  -- can be negative
  self.valveDowntime = (self.valveDowntime < 0) and 0 or self.valveDowntime;
  -- from milliseconds to 0.1 sec. 0 if not working
  self.valveDowntime = (self.pumpOut ~= 0) and (self.valveDowntime / 100) or 0;
  
end

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

function Logic:call()
  
  self:readInputs();
  self:determineCoolingOrHeating();
  self:calculateDemandTempeartures();
  self:updateControlObjects();
  self:handle();
  self:setOutputs();
  
end

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

-- main logic state object
g_logic = nil;

SUPPORTED_SUBLOGIC_TYPE = "MixZone";
SUPPORTED_SUBLOGIC_VERSION = "0.0.4";
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

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