--[[                    Ogólny opis i zasady użycia / General description and rules of use
PL
Rozwiązanie sterowania dla zaworów elektromagnetycznych lub/i pieca zasilającego grzejniki.
Patrz filmy DIY: https://www.portal.ztechnikazapanbrat.pl/filmpagescollection/650

Algorytm na podstawie nastawionych temperatur w pokojach i odczytu parametrów z termostatów steruje zezwoleniem na pracę pieca lub otwarciem zaworów doprowadzających gorący czynnik CO.
Algorytm zarządza pracą pieca/zaworów porównując temperaturę nastawioną na termostacie z temperaturą  mierzoną w pokoju.
Algorytm nie ingeruje w proces sterowania termostatu, ale uwzględnia każdą nastawę temperatury termostatu (harmonogram, sterowanie ręczne, tryb wakacji).
Algorytm pozwoli na pracę pieca/otworzy zawór CO, jeżeli choć jeden grzejnik będzie potrzebować grzać.
Zapotrzebowanie na grzanie zostanie wycofane, gdy wszystkie grzejniki (wszystkie pokoje) osiągną zadaną temperaturę.
Opcjonalnie istnieje możliwość wstrzymania pozwolenia na grzanie w całym domu dla wszystkich grzejników (np. w okresie lata) przez ustawienie wartości "true" w zmiennej globalnej o nazwie "heatingMasterDevStat".
Zalecane jest użycie Quick App - głównego panela sterowania ogrzewaniem, który kontroluje zezwoleniem na ogrzewanie np. podczas ciepłych dni przez zmienną globalną o nazwie "heatingMasterDevStat".

Poniżej znajduje się legenda parametrów, jakie należy skonfigurować.

UWAGA!
Algorytm ma za zadanie wygenerować pozwolenie na włączenie lub wyłączenie obiegu ciepłego ogrzewania CO. Algorytm NIE steruje pracą pieca, za którą odpowiada wewnętrzny regulator pieca.


EN
New control solution for solenoid valves or a heating furnace/oven that powers the radiators/heaters.
Look at DIY videos at: https://www.portal.ztechnikazapanbrat.pl/filmpagescollection/650

The algorithm, based on the set temperatures in the rooms and reading the parameters from thermostats, controls the permission to operate the oven/valves.
Algorithm manages the operation of the stove/valves by comparing the temperature set on the thermostat with the temperature measured in the room.
The algorithm does not interfere with the thermostat control process but takes into account each thermostat temperature setting (schedule, manual control, holiday mode).

The algorithm will allow the oven to work/will open the valve if at least one heater (thermostat) needs to heat.
The heating demand will be withdrawn when all radiators (all rooms) have reached the set temperature.
Optionally, it is possible to suspend the heating permit for the whole house for all heaters (e.g. during the summer period) by setting the value "true" in the global variable named "heatingMasterDevStat"
It is recommended to use Quick App - the central heating control panel, which controls the heating permission. The Quick App can stop approval for heating a house on warm days by managing a global variable named "heatingMasterDevStat".

Below is the legend of the parameters to be configured.

WARNING!
The algorithm is designed to generate permission to enable or disable the central heating circuit. Algorym does NOT control the oven operation.

Designed by Lukasz Gawryjolek 2020
https://www.portal.ztechnikazapanbrat.pl/ 
Pierwsza platforma DIY na rynku dla Inteligentnego Domu
email: lukasz@ztechnikazapanbrat.pl

-------------------------------------
Release on DIY portal at: 2020.11.01
-------------------------------------
2021.11.05 - add communication with Master Quick App showing the detailed status of valves and heaters, some important fixes
2021.11.01 - fix for calling unexisting method 'updateHeatingStatusForMainControlDevice'
             add labels to describe the status of scene algorithms
2021.06.05 - add optional functionality to suspend the heating permit for the whole house for all heaters (e.g. during the summer period) by setting the value "true" in the global variable named "heatingMasterDevStat"
2021.04.04 - change state of valve/oven only then the current state is different (limit logs in systems)
2021.02.22 - add immediate request to stop heating for heater when FIBARO Heat Controller ver 4.7 or higher set Par3Bit2(Window open detected) to 1
2021.02.20 - fix for wrong calaculation of running devices if any device is in error state 
           - printing logs on terminal can generate errors when the device does not exist, 
           - add log on terminal after all calculation
2020.12.22 - change the way to verify if FIBARO Heat Controller set "Provide heat in order to maintain set temp" bit using parent device
2020.11.09 - add the declarations(Conditions/Triggers) that guarantee that the scene runs after the gateway startup
2020.11.03 - add immediate request to start heating for heater when FIBARO Heat Controller ver 4.7 or higher set Par3Bit3(ProvideHeat bit) to 1
2020.11.02 - log message "Heater cannot control the request to start/stop heating" in every cycle



]]


--[[
      -------------- DESCRIPTION OF THE CONFIGURATION (heaters and valves/oven) -----------------------

UWAGA: Używane są obie nazwy „zawór” i „piec/kociał”. Niektóre instalacje grzewcze sterowane są za pomocą pieca, inne za pomocą zaworów elektromagnetycznych dostarczających gorące medium CO. Algorytm steruje gorącą wodą CO poprzez otwieranie/zamykanie zaworów dla np. piętra i parteru lub włączaniem/wyłączaniem pieca CO.      

REMARK: Both names "valve" and "oven" are used. Some heat installations are control thanks to the oven and other thanks to electromagnetic valves.  The algorithm controls the hot water in the heaters thanks to opening/closing the valves or turn on/off the oven.

----------------------
GRZEJNIK   /   HEATER
----------------------
>> Każdy grzejnik - FIBARO Heater Controller musi być zadeklarowany jako samodzielny JSON o nazwie grzejnik1, grzejnik2 itd  / Each heater with FIBARO Heater Controller must be declared as standalone JSON named heater1, heater2, etc.

----------------------
     Zmienne GRZEJNIKA / Fields of HEATER:
        name - nazwa (miejsce) grzejnika np. salon, kuchnia / the name (place) of heater eg. living room, kitchen
        termostatID - Adres FIBARO Kontrolera - Termostatu / FIBARO address of the Heater Controller
        tempID - Adres FIBARO czujnika temperatury (może być wewnętrznym czujnikiem Heater Controller lub czujnikiem temperatury Motion Sensora lub Door/Window Sensor) / FIBARO address of the temperature sensor (can be Heater Controller internal sensor or temperature sensor of Motion Sensor or Door/Window Sensor)
        lowNeutralZone - strefa neutralna poniżej wartości zadanej (jednostki są takie same jak jednostki temperatury) / the neutral zone below the setpoint value  (units are the same as temperature units)
        hiNeutralZone -  strefa neutralna powyżej wartości zadanej (jednostki są takie same jak jednostki temperatury) / the neutral zone over the setpoint value (units are the same as temperature units)
        lowLevelDelayMinut - czas w minutach, jaki musi upłynąć dla temperatury niższej niż lowNeutralZone, po którym można otworzyć/włączyć zawór/piec  /  the amount of time in minutes that must be passed for the temperature lower then lowNeutralZone after which the valve/oven can be opened/turn on
        hiLevelDelayMinut - czas w minutach, jaki musi upłynąć dla temperatury wyższej niż hiNeutralZone, po którym można zamknąć/wyłączyć zawór/piec  /  the amount of time in minutes that must be passed for the temperature higher then hiNeutralZone after which the valve/oven will be closed/turn off
        canControlValve - opcjonalny parametr mówiący, czy dany grzejnik może otworzyć/zamknąć zawór (włączać/wyłączać piec) kiedy jest to wymagane, domyślna wartość to 1 (jeśli nie użyjesz tego parametru to grzejnik może sterować zaworem/piecem). Ustawienie tego parametru na 0 wyłącza grzejnik z oblicznania zapotrzebownie na CO.  / optional parameter telling if the given heater is allowed to open/close the valve (turn on/turn off the oven) when it is required, the default value is 1 (if you do not use this parameter the heater can control the valve/oven). Setting this parameter to 0 disable the heater from a heating demand calculation process.

 1)     -- Heat Controller FIBARO
        e.g. local heater1 = { name = "Biuro_L", termostatID = 828, tempID = 829, lowNeutralZone = 1.0, lowLevelDelayMinut =  10, hiNeutralZone = 0.0,  hiLevelDelayMinut = 5}


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

        parametersConfiguration = {thermostat... - opcjonalna lista poleceń używanych do odczytu parametrów z różnych regulatorów ciepła. Użyj tej opcji, jeśli masz grzejnik innej firmy, który używa różnych poleceń, aby uzyskać parametry, takie jak temperatura zadana, tryb, pełne otwarcie lub zamknięcie, pomiar temperatury z grzejnika  / an optional list of commands used to read parameters from various heat regulators. Use this option if you have a third-party termostat that uses different commands to get parameters such as set temperature, mode, full open or close, temperature measurement.
        heatingThermostatSetpoint = XX - definiuje stała wartość dla nastawy temperatury  /  defines a fixed value for the temperature setpoint 

 2)     -- EUROtronic Spirit
        e.g. local heater7 = { name = "Biuro_D", termostatID = 873, tempID = 874, lowNeutralZone = 0.5, lowLevelDelayMinut =  10, hiNeutralZone = 0.5,  hiLevelDelayMinut = 5, canControlValve = 0, parametersConfiguration = {thermostat = {thermostatModeParameterForFullHeatValue = 'FullPower'}}}

        

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

        parametersConfiguration = {fixedSetpoint - użyj tej opcji gdy nie masz termostatu ale chcesz utrzymać zadaną temperaturę w pomieszczeniu poprzez załącznie/wyłączanie grzania np elektrozaworu podłogowego ogrzewania   / use this option when you do not have a thermostat, but you want to maintain the desired temperature in the room by turning on / off the heating, e.g. a floor heating solenoid valve.

 3)     -- no thermostat  
        e.g. local heater15 = { name = "no Thermostat", tempID = 816, lowNeutralZone = 2.0, lowLevelDelayMinut =  1, hiNeutralZone = 1.0,  hiLevelDelayMinut = 1,  parametersConfiguration = {fixedSetpoint = {heatingThermostatSetpoint = 22,  measureTemperatureParameterName = 'value'}}}
        
        tempID = xxx  - określa adres czujnika temperatury w systemie który będzie mierzył aktualną temperaturę pomieszczenia   /  determines the address of the temperature sensor in the system that will measure the current room temperature
        measureTemperatureParameterName = 'value' - nazwa parmatru który w urządzeniu zawiera wartość zmierzonej temperatury pomieszczenia  /  name of the parameter which in the device contains the value of the measured room temperature
        
      
>> Parametry tylko do odczytu:  /  Read-only parameters:
    Zmienne grzejnika:  /  Fields of the heater:
        - heaterOven - bit zwraca informację czy dany grzejnik chce grzać (1) czy nie musi grzać (0) - czyli czy ciepła woda w grzejniku jest wymagana czy nie / the bit returns information if the given heater wants to heat (1) or do not need to heat (0)  - meaning if the hot water in the heater is required or not
        - prevTime - czas w sekundach (w oparciu o czas Epoki) używany do sterowania zaworem/piecem, gdy mierzona temperatura jest poza strefą neutralną  /  the time in seconds (based on Epoch time) used to control valve/oven when the measured temperature is out of the neutral zone
        - currentMode, prevMode - stan grzałki na bieżącym/poprzednim kroku (Hi, Lo, Neutral, ForcedMaxHeater, ForcedOffHeater, Err)  /  the status of the heater at current/previous step (Hi, Lo, Neutral, ForcedMaxHeater, ForcedOffHeater, Err)


-----------------------------
ZAWÓR/PIEC.  /  VALVE/OVEN
-----------------------------
>> Każdy zawór/piec może być kontrolowany przez różne urządzenia FIBARO, takie jak Single lub Double Switch, Smart Implant, Wall Plug, które muszą być zadeklarowane przez JSON o nazwie valve1, valve2 itp.  /  Each valve/oven can be controlled by FIBARO units like Single or Double Switch, Smart Implant, Wall Plug must be declared as standalone JSON named valve1, valve2, etc.

     Zmienne dla zaworu/pieca  /  Fields of the VALVE/OVEN:

        heaters - zbiór zdefiniowanych grzejników, które są podłączone do określonego zaworu/pieca  /  a collection of defined heaters that are connected to the certain valve/oven 
        name - nazwa zaworu/pieca np. zawór pierwszego piętra  /  the name of the valve/oven ex. first-floor valve
        valveID - Adres FIBARO Single/Double Switch, który steruje zaworem/piecem CO.  /   FIBARO address of the Single/Double Switch that controls this valve/oven

>> Parametry tylko do odczytu:  /  Read-only parameters:
     Zmienne dla zaworu/pieca  /  Fields of the valve/oven:
         - calculatedState - bit informacji czy dany zawór/piec musi być otwarty (1) czy zamknięty (0) brak pola oznacza, że zawór/piec powinien zachować wcześniej obliczony stan  /  the bit of information if certain valve must be open (1) or closed (0) if the field is not present it means that the valve should keep previously calculated state
        


-----------------------------        
FUNKCJE  /  FUNCTIONS
-----------------------------

    function initalSetup() - funkcja jest wywoływana na początku każdego cyklu algorytmu, tutaj można zaimplementować wstępne obliczenia  /  function is called at the begining of each algorithm cycle, here you can implement some initial calculations/wyłączyć zawór/piekarnik przekazany do funkcji przez argument /  function is called only when the algorithm calculates to turn on the valve/oven (passed as the argument of this function) 

    function turnOff(valve) - funkcja jest wywoływana tylko wtedy, gdy algorytm chce wyłączyć zawór/piekarnik przekazany do funkcji przez argument /  function is called only when the algorithm calculates to turn off the valve/oven (passed as the argument of this function) 

    function finalAdjustments() - funkcja jest wywoływana na końcu każdego cyklu algorytmu, tutaj można wykonać pewne obliczenia końcowe (sterowanie pompą itp.)  /  function is called at the end of each algorithm cycle, here you can implement some final calculations (control pump etc.)

 ]]




 --------------------------------------------------------------------------------------------------------------
 --                              WYMAGANE USTAWIENIA   
 --                    REQUIRED SETTINGS - YOU MUST FILL THIS DATA  
 --------------------------------------------------------------------------------------------------------------


--[[
    !!!  Skopiuj poniższą (Conditions/Triggers) do lewego okna sceny !!!  /  Copy this on DECLARATIONS (Conditions/Triggers) to the left part of Scene !!!

{    
    type = "se-start",    
    property = "start",    
    operator = "==",    
    value = true,    
    isTrigger = true
}


]]

local aliasForThisScene = "Scene145" -- użyj do filtrowania alarmów, użyj tutaj swojego numeru sceny  /  use for alarm filtering, use your scene number here
local masterDashboardQuickAppID = 931 -- opcjonalny adres urządzenia Quick App (głównego panelu dla sceny)  /  optional address of the Quick App (a master panel overview for the scene)

----------------------
-- GRZEJNIK   /   HEATER
----------------------
 -- definicja grzejników (produkcji FIBARO, Danfoss, EUROtronic itp.)  /  definition of the heaters (manufactured by FIBARO, Danfoss, EUROtronic etc.)
local heater1 = { name = "Biuro_L", termostatID = 828, tempID = 829, lowNeutralZone = 1.0, lowLevelDelayMinut =  10, hiNeutralZone = 0.0,  hiLevelDelayMinut = 5}
local heater2 = { name = "Pokoj_A", termostatID = 1004, tempID = 1005, lowNeutralZone = 0.2, lowLevelDelayMinut =  1, hiNeutralZone = 0.5,  hiLevelDelayMinut = 1}
local heater3 = { name = "Pokoj_O", termostatID = 999, tempID = 1000, lowNeutralZone = 0.2, lowLevelDelayMinut =  3, hiNeutralZone = 1.0,  hiLevelDelayMinut = 10}
local heater4 = { name = "Rodzice", termostatID = 1009, tempID = 1010, lowNeutralZone = 0.5, lowLevelDelayMinut =  10, hiNeutralZone = 0.5,  hiLevelDelayMinut = 5}
local heater5 = { name = "Lazienka", termostatID = 994, tempID = 995, lowNeutralZone = 0.5, lowLevelDelayMinut =  0, hiNeutralZone = 0.5,  hiLevelDelayMinut = 15}
local heater6 = { name = "Salon", termostatID = 1014, tempID = 1015, lowNeutralZone = 2.0, lowLevelDelayMinut =  5, hiNeutralZone = 0.5,  hiLevelDelayMinut = 10}


-- EUROtronic Spirit
local heater7 = { name = "Biuro_D", termostatID = 873, tempID = 874, lowNeutralZone = 0.5, lowLevelDelayMinut =  10, hiNeutralZone = 0.5,  hiLevelDelayMinut = 5, canControlValve = 1, parametersConfiguration = {thermostat = {thermostatModeParameterForFullHeatValue = 'FullPower'}}}



-- konfiguracja parametrów dla grzejników bez termostatów np dla podlogówki /  e.g. advanced parameters' configuration for heaters without thermostats like underfloor heating
local heater15 = { name = "no Thermostat", tempID = 816, lowNeutralZone = 2.0, lowLevelDelayMinut =  1, hiNeutralZone = 1.0,  hiLevelDelayMinut = 1,  parametersConfiguration = {fixedSetpoint = {heatingThermostatSetpoint = 22,  measureTemperatureParameterName = 'value'}}}


-----------------------------
-- ZAWÓR/PIEC.  /  VALVE/OVEN
-----------------------------
-- definicja zaworów/pieca, które dostarczają ciepło do grzejników (zazwyczaj są to urządzenia typu on/off, takie jak Single/Double Switch, Relay Switch or Smart Module)  /  definition of the valves/ovens that deliver the heat to the  heaters (usually they are on/off devices such as Single/Double Switch, Relay Switch or Smart Module)

local valve1 = {name = "Kociol_CO", heaters = {heater1, heater2, heater3, heater4, heater5, heater6, heater7}, valveID = 847}
local valve2 = {name = "Kociol_2", heaters = {heater15, heater5}, valveID = 870}
local valve3 = {}

-- definicja wszystkich zaworów/piecy w jednym zestawie (zbierze) (jeżeli dodajesz więcej zaworów powyżej, po prostu dodaj je tutaj)  /  definition of all valves/ovens in one set (if you add more valves above, just add them here)
local valves = {valve1, valve2}


-----------------------------        
-- FUNKCJE  /  FUNCTIONS
-----------------------------
function turnOn(valve)
    if fibaro.getValue(valve.valveID, "value") ~= true then
        fibaro.call(valve.valveID, "turnOn") 
    end
end

function turnOff(valve)
    if fibaro.getValue(valve.valveID, "value") ~= false then
        fibaro.call(valve.valveID, "turnOff") 
    end
end


function initalSetup()
    
end

function finalAdjustments()

end


------------------------------------------------------------------------------------------------------
-- Kod sceny -- NIE ZMIENIAJ GO !   /  Code of the scene - DO NOT CHANGE IT !
------------------------------------------------------------------------------------------------------
--[[
    Designed by Lukasz Gawryjolek 2020
https://www.portal.ztechnikazapanbrat.pl/ 
Pierwsza platforma DIY na rynku dla Inteligentnego Domu
email: lukasz@ztechnikazapanbrat.pl
]]

local heatingIsAllowedFromMasterDeviceController = true  --by default heating is allowed
local errorDesc = "" --collect all errors for cetrain valve/oven
local statusItem = "" --collect all desciptions for cetrain valve/oven
local tempDesc = "" --collect all temperatures and setpoints


-- function checks the main request for heating coming from external master controller 
function masterHeatingDemand()
    heatingIsAllowedFromMasterDeviceController = true --by default heating is allowed

    local mainDeviceStatus = fibaro.getGlobalVariable("heatingMasterDevStat")
    if mainDeviceStatus ~= nil then
        if mainDeviceStatus == "false" then
            --stop heating procedure
            logs("Master Device sends a demand to STOP HEATING!")
            heatingIsAllowedFromMasterDeviceController = false
        end
    end
end


-- the function prints the logs (debugging) code on the screen
function logs(arg)
   local printOnScreen = true  -- false/true values control if the debug logs should be printed on screen or not
   if printOnScreen then
        print(arg)
   end
end

---Function validate if the above configuration is valid based on JSON and Lua notation 
--If you get an error while running the scene it means that there is an error in the configuration above 
function validateConfiguration(valves)
    logs("=================================  Validation ====================================")
    local l = json.encode(valves)
    local p = json.decode(l)
    logs("ok")
end


--Main function to control all heaters and valves/oven. This function must be called regularly (eg. every 5 minutes but not faster)
function execute()

    initalSetup()
    masterHeatingDemand()

    local statusDesc = {} --use for Master Device Quick App communicaton
    local statusError = {}--use for Master Device Quick App communicaton
    tempDesc = ""--use for Master Device Quick App communicaton

    for k1, valve in pairs(valves) do
        if valve == nil or next(valve) == nil then break end

        local totalHeatersAllowedToControlValve = 0
        local countHeatersWantToCloseValve = 0
        local valveMustBeOpen = false
        statusItem = valve.name
        errorDesc = ""
        logs("")
        logs("--------- Calculation for valve/oven named: " .. valve.name.. " ---------")

        for k2, heater in pairs(valve.heaters) do
            if heater == nil  or next(heater) == nil then break; end

            --reset former calculations
            heater.heaterOven = nil

            logs("                  ")
            logs("Heater: " .. heater.name)
            --logs(json.encode(heater))

            --do heater job
            heaterDemandCalculation(heater)
            if heater.currentMode == "Err" then
                fibaro.error(aliasForThisScene, "Check this device, currentMode - Error")
                
            else

                --check if the heater is allowed to open/close the Valve (turn on/off the oven)
                local canHeaterOpenValve = heater.canControlValve
                if canHeaterOpenValve ~= nil and canHeaterOpenValve == 0 then
                    logs("Heater cannot control the request to start/stop heating")
                    templog("*")
                    ; -- do noting when the heater is not allowed to control the valve/oven
                else
                    totalHeatersAllowedToControlValve = totalHeatersAllowedToControlValve + 1

                    if heater.heaterOven ~= nil then
                        if heater.heaterOven == 1 then
                            valveMustBeOpen = true
                        else
                            countHeatersWantToCloseValve = countHeatersWantToCloseValve + 1
                        end
                    end
                end
            end --error
           
        end --for heater internal loop
        logs("")
        
        statuslog("Stopped Heaters:" ..  countHeatersWantToCloseValve .."/" .. totalHeatersAllowedToControlValve)

        --open the valve (turn on the oven) if at least one heater requests it
        if valveMustBeOpen then
            turnOn(valve)
            statuslog(" -> NEEDS HEAT") 
            logs(">>>>>>>>> + STARTING OVEN/VALVE named: " .. valve.name)      
            valve["calculatedState"] = 1
            if valve.turnOnOvenTime == nil then
                valve.turnOnOvenTime = os.time()
            end
        end

        --close the valve (turn off the oven) if all heaters request it
        if totalHeatersAllowedToControlValve == countHeatersWantToCloseValve then
            logs("Request to Turn OFF oven/valve named: " .. valve.name)
            valve["calculatedState"] = 0
        end

        --request to stop oven after certain time to protect against a fast change (on, off)
        if valve.calculatedState == 0 then
            if valve.turnOnOvenTime == nil or os.time() > valve.turnOnOvenTime + 4 * 60 then
                 turnOff(valve)
                 statuslog(" -> NO HEAT REQUIRED") 
                 valve.turnOnOvenTime = nil
                 valve.calculatedState = -1 --next loop must calculate the valve state from the begining 
                 logs(">>>>>>>>> ---- TURN OFF OVEN/VALVE named: " .. valve.name)
            else
                logs("Request to Turn OFF oven/valve named: " .. valve.name .. "  but counting down .... ")
                statuslog(" -> NO HEAT REQUIRED, delay") 
            end
        end

        table.insert(statusDesc, statusItem)
        table.insert(statusError, errorDesc)
        
    end -- valves loop

    finalAdjustments()

    logs("--------- End of calculation --------- ")

    if masterDashboardQuickAppID ~= 0 then
        logs("Information sent to the Master device:")
        logs(tempDesc)
        logs(json.encode(statusDesc))
        logs(json.encode(statusError))
        
        --send colledted information to the Master Quick App
        fibaro.call(masterDashboardQuickAppID, "heaterStatus", tempDesc)
        fibaro.call(masterDashboardQuickAppID, "valves", json.encode(statusDesc))
        fibaro.call(masterDashboardQuickAppID, "errors", json.encode(statusError))
    end
end

-- Function returns a mode (Neutral, Hi, Lo, ForcedMaxHeater, ForcedOffHeater) depends on current temperature, setpoint and hi, lo zones
function getCurrMode(heater)

    --general external controller stops heating
    if heatingIsAllowedFromMasterDeviceController == false then
        logs("Heater is stopped by Master Device!")
        templog("|" .. heater.name .. ":Q")
        return "ForcedOffHeater" --heater is closed
    end


    -- default configuration for FIBARO Heat Controller
    local setpointTemperatureParameterName = 'heatingThermostatSetpoint'
    local thermostatModeParameterName = 'thermostatMode'
    local thermostatModeParameterForFullHeatValue = 'ManufacturerSpecific'
    local thermostatModeParameterForStopHeatValue = 'Off'
    local measureTemperatureParameterName = 'value'

    local isThermostat = false

    if heater.parametersConfiguration ~= nil and heater.parametersConfiguration.thermostat ~= nil then
        local configuration = heater.parametersConfiguration.thermostat
        isThermostat = true
        if configuration.setpointTemperatureParameterName ~= nil then
            setpointTemperatureParameterName = configuration.setpointTemperatureParameterName
        end
        if configuration.thermostatModeParameterName ~= nil then
            thermostatModeParameterName = configuration.thermostatModeParameterName
        end
        if configuration.thermostatModeParameterForFullHeatValue ~= nil then
            thermostatModeParameterForFullHeatValue = configuration.thermostatModeParameterForFullHeatValue
        end
        if configuration.thermostatModeParameterForStopHeatValue ~= nil then
            thermostatModeParameterForStopHeatValue = configuration.thermostatModeParameterForStopHeatValue
        end
        if configuration.measureTemperatureParameterName ~= nil then
            measureTemperatureParameterName = configuration.measureTemperatureParameterName
        end
    end

    --use default parameter names
    if heater.parametersConfiguration == nil then
        isThermostat = true
    end

    --reading parmeters and values from devices
    local temp
    local setTemp
    local thermostatMode

    if isThermostat then

        temp = tonumber(fibaro.getValue(tonumber(heater.tempID), measureTemperatureParameterName))
        setTemp = tonumber(fibaro.getValue(tonumber(heater.termostatID), setpointTemperatureParameterName))
        thermostatMode = fibaro.getValue(tonumber(heater.termostatID), thermostatModeParameterName)

        if thermostatMode == nil then
            fibaro.error(aliasForThisScene, "Error. Can not get the mode of the thermostat !")
            errorLog(heater.name .. " - Error. Can not get the mode of the thermostat !")
            return "Err"
        end

        if temp == nil then
            fibaro.error(aliasForThisScene, "Error. Can not get current value of the temperature !")
            errorLog(heater.name .. " - Error. Can not get current value of the temperature !")
            return "Err"
        end

        if setTemp == nil then
            fibaro.error(aliasForThisScene,"Error. Can not get the temperature setpoint of thermostat !")
            errorLog(heater.name .. " - Error. Can not get the temperature setpoint of thermostat !")
            return "Err"
        end
        
        logs("T=" .. temp .. " , SP=" .. setTemp)
        templog("|" .. heater.name .. "=" .. temp .. "(" .. setTemp .. ")")


    -- fixedSetpoint = {heatingThermostatSetpoint = 22} -> configuration for no thermostat devices
    elseif heater.parametersConfiguration ~= nil and heater.parametersConfiguration.fixedSetpoint ~= nil then
        setTemp = tonumber(heater.parametersConfiguration.fixedSetpoint.heatingThermostatSetpoint)
        local measureTemperatureParameterName = heater.parametersConfiguration.fixedSetpoint.measureTemperatureParameterName
        temp = tonumber(fibaro.getValue(tonumber(heater.tempID), measureTemperatureParameterName))
        thermostatMode = "Heat"
        logs("T=" .. temp .. " , SPfix=" .. setTemp)
        templog("|" .. heater.name .. "=" .. temp .. "(" .. setTemp .. ")")
    end

-- Thermostat like Spirit: thermostatMode == "FullPower" 
    if thermostatMode == thermostatModeParameterForFullHeatValue then
        statuslog(heater.name .. ": fully opened")
        templog("O")
        return "ForcedMaxHeater" --heater set to max temperature
    elseif thermostatMode == thermostatModeParameterForStopHeatValue then
        statuslog(heater.name .. ": fully closed")
        templog("C")
        return "ForcedOffHeater" --heater is closed
    elseif isHeaterControllerDetectedWindowOpen(heater) == true then
        logs("Heater Controller detected opened window !!!")
        statuslog(heater.name .. ": Opened window")
        templog("W")
        return "ForcedOffHeater" -- do not need overn because Heater Controller automatically reduce thermostat,
    elseif temp > setTemp + heater.hiNeutralZone then
        templog("H")
        return "Hi" 
    
    elseif temp < setTemp - heater.lowNeutralZone then
        templog("L")
        return "Lo"
    end    

    templog("N")
    return "Neutral"
end

-- Function sets the command to open the valve (turn on the oven) for the given heater. Function sets a bit 'heaterOven= (1, 0)' in heater's internal data 
function updateValve(heater, currentMode)
     --check if heater is allowed to open/close the Valve
    local canHeaterOpenValve = heater.canControlValve
    if canHeaterOpenValve ~= nil and canHeaterOpenValve == 0 then
        ; -- do noting when the heater is not allowed to control the valve
    else 
        local ovenFlag = 0
        if currentMode == "Lo" or currentMode == "ForcedMaxHeater" then
            ovenFlag = 1
        
        end
        
        if currentMode == "Hi" or currentMode == "ForcedOffHeater" then
            ovenFlag = 0
  
        end
        heater["heaterOven"] = ovenFlag

        if ovenFlag == 1 then
            logs("Heater wants to start heating")
        else
            logs("Heater wants to stop heating")
            templog("S")
        end
    end
end

--Updates the stored time for a given heater. This time is used to calculate if defined delay expired
function updateStartDelayTimer(heater)
    heater["prevTime"] = os.time()
end

--Returns the defined delay time for a certain zone
function getDelayTime(heater, currentMode)
    if currentMode == "Hi" then
        return tonumber(heater.hiLevelDelayMinut)
    end
    
    if currentMode == "Lo" then
        return tonumber(heater.lowLevelDelayMinut)
    end
end

--Returns a bool value to confirm if the delay time for given zone already passed
function delayTimeElapsed(heater, currentMode)
    local prevTime = heater.prevTime
    if prevTime == nil then
        heater["prevTime"] = os.time()
    end
    if currentMode == "Hi" then
        return os.time() - heater.prevTime > heater.hiLevelDelayMinut * 60
    end

    if currentMode == "Lo" then
        return os.time() - heater.prevTime > heater.lowLevelDelayMinut * 60
    end
end

--Saved the calculated current mode of the heater
function updatePrevMode(heater, currentMode)
    heater["prevMode"] = currentMode
end

--For FIBARO Heat Controller from v.4.7 check if Parameter3 Bit 3 is True = ProvideHeat request
function get3Bit3Parmeter_ProvideHeat(heater)

    if heater.termostatID == nil then
        return false --heater is not type Heat Controller by FIBARO
    end

    if fibaro.getType(heater.termostatID) ~= "com.fibaro.FGT001" then
        return false
    end
    
    local parentIDJson = api.get('/devices/' .. heater.termostatID .. "") 
    local parentID = parentIDJson.parentId

    local par = fibaro.getValue(parentID, 'parameters')
    local valueInt = 0
    for k, v in pairs(par) do 
        if v.id == 3 then
            valueInt = v.value --decimal
        end
    end
    --print(parentID, json.encode(par))
    
    local heatBit = getBitNoFromValue(3, valueInt)
    if heatBit == 1 then
        return true
    else
        return false
    end 
end

--For FIBARO Heat Controller from v.4.7 can detect if window is opened. Detects when Parameter3 Bit 2 is True
function isHeaterControllerDetectedWindowOpen(heater) 
    
    if heater.termostatID == nil then
        return false --heater is not type Heat Controller by FIBARO
    end

    if fibaro.getType(heater.termostatID) ~= "com.fibaro.FGT001" then
        return false
    end
    
    local parentIDJson = api.get('/devices/' .. heater.termostatID .. "") 
    local parentID = parentIDJson.parentId

    local par = fibaro.getValue(parentID, 'parameters')
    local valueInt = 0
    for k, v in pairs(par) do 
        if v.id == 3 then
            valueInt = v.value --decimal
        end
    end
    --print(parentID, json.encode(par))
    
    local heatBit = getBitNoFromValue(2, valueInt)
    if heatBit == 1 then
        return true
    else
        return false
    end 

end


--convert decimal to binary
function getBitNoFromValue(bitNo, valueInt)
-- bitNo counts from 1 ... n
    local bin = {}
    local i = 1
    while valueInt > 0 do
        local modulo = valueInt % 2
        bin[i] = modulo
        i = i + 1
        valueInt = math.floor(valueInt/2)
    end
    local bit = bin[bitNo] 
    if bit == nil then
        return 0
    elseif bit == 0 then
        return 0
    else
        return bit --bit == 1
    end
end

-- Calculates a new state of the heater 
function heaterDemandCalculation(heater)
    local prevMode = heater.prevMode
    if prevMode == nil then prevMode = "Neutral" end
    
    local currentMode = getCurrMode(heater)
    heater["currentMode"] = currentMode

    logs("Heater mode: " .. prevMode .. " -> " .. currentMode)
    if currentMode == "Err" then
        logs("Heater can not process, error.")
        fibaro.error(aliasForThisScene, "Error. Heater named: " .. heater.name .. " can not process, error.")
        return
    end

    local heaterFIBARORequestHeatingBit3 = get3Bit3Parmeter_ProvideHeat(heater)
    if heaterFIBARORequestHeatingBit3 then
logs("@0 - FIBARO heater ask to provide heat in order to maintain set temperature (Par3, bit3 = 1)")
        heater["heaterOven"] = 1
        templog("H")
    elseif currentMode == "ForcedMaxHeater" or currentMode == "ForcedOffHeater" then
logs("@1 - Forced to low or high temperature")
        updateStartDelayTimer(heater)
        updateValve(heater, currentMode)

    elseif currentMode == "Neutral" then
logs("@2 - In the neutral zone")
        updateStartDelayTimer(heater)

    elseif getDelayTime(heater, currentMode) == 0 then
logs("@3 - No delay")
        updateStartDelayTimer(heater)
        updateValve(heater, currentMode)

    elseif prevMode ~= currentMode then
logs("@4 - Changed mode")
        templog("?")
        updateStartDelayTimer(heater)

    elseif delayTimeElapsed(heater, currentMode) == true then
logs("@5 - Delay time passed")
        updateValve(heater, currentMode)
    else
logs("@6 - Counting down...")
        templog("D")
    end

    updatePrevMode(heater, currentMode)

    --logs("New calculated state of heater: ", json.encode(heater))
end

 
function templog(...)
 local arg = {...}
    local text = ""
    
    for i,v in ipairs(arg) do
       text = text .. tostring(v) .. ""
    end
    tempDesc = tempDesc .. text
end


function statuslog(...)
 local arg = {...}
    local text = ""
    
    for i,v in ipairs(arg) do
       text = text .. tostring(v) .. " "
    end
    statusItem = statusItem .. " | " .. text
end

function errorLog(...)
    local arg = {...}
    local text = ""
    
    for i,v in ipairs(arg) do
       text = text .. tostring(v) .. " "
    end
    errorDesc = errorDesc .. " | " .. text
end


function timeFunc()
    execute()
 
    fibaro.setTimeout(3 * 60 * 1000, function() timeFunc() end)
end

--======================= Main Part ===============================
validateConfiguration()
timeFunc()
