print("-------------------") --[[ This script is a engine heater script which turns the engine heater on and off depending on the out door temperature There is currently a weekly schedule to program one leave time per day Prerequisites: User variable 'EngHeat_Mode' (string) must exist User variable 'EngHeat_LeaveTime' (string) must exist ]]-- -- Set leave times. leaveTime = {'Monday 07:25', 'Tuesday 07:25', 'Wednesday 07:25', 'Thursday 07:25', 'Friday 07:25' } -- Enginge heater maximum runtime in seconds MAXRUNTIME = 180 * 60 -- Name of the engine heater switch device HEATERSWITCH = 'SWITCH_Fd_EngineHeater' -- Name of the outdoor temperate sensor device OUTDOORTEMP = 'Utetemp' --Name of engine heater watts measurement device --Set to '' if no watt measure device exist HEATERWATTS = 'SWITCH_Fd_EngineHeater_Watts' -- Minimun time for watt measurements in seconds HEATERWATTSPERIOD = 10*60 -- FUNCTIONS START -- --integer converts a float into an integer function integer(x) return x<0 and math.ceil(x) or math.floor(x) end -- calculateRuntime calucates heater run time in seconds depending on outdoor temp -- It has a different formula depending on if the outdoor temp is above or below zero function calculateRuntime(temp) local ntemp = 11 - temp -- If temperature is above +10 set runtime to 0 if (temp > 10) then return 0 elseif (ntemp > 0) then return integer((0.0044*ntemp^3+0.3377*ntemp^2-1.1128*ntemp+21.496)) * 60 else return integer((0.0036*ntemp^3-0.4179*ntemp^2+18.205*ntemp-99.417)) * 60 end end -- secsToClock return a string representing seconds as HH:MM:SS function secsToClock(seconds) if ((seconds == 0) or (seconds == nil) ) then return "00:00:00" else days = integer(seconds / (60 * 60 * 24)) hours = integer((seconds - days * 60 * 60 * 24) / (60 * 60)) mins = integer((seconds - days * 60 * 60 * 24 - hours * 60 * 60) / 60) secs = seconds - days * 60 * 60 * 24 - hours * 60 * 60 - mins * 60; if (days > 99) then return string.format("%03d:%02d:%02d:%02d (DDD:HH:MM:SS)", days, hours, mins, secs) elseif (days > 0) then return string.format("%02d:%02d:%02d:%02d (DD:HH:MM:SS)", days, hours, mins, secs) else return string.format("%02d:%02d:%02d (HH:MM:SS)", hours, mins, secs) end end end -- getCurrentRunTime returns current runtime in seconds as an integer or nil if not available function getCurrentRunTime() local lastupdated = getHeaterLastUpdate() if (lastupdated ~= nil) then return integer(os.difftime (os.time(), deviceToTime(lastupdated))) else return nil end end -- getHeaterWatts returns current heater watts in watts or nil if not available function getHeaterWatts() if (HEATERWATTS == '') then return nil end if (otherdevices_lastupdate[HEATERWATTS] == nil) then return nil end -- if lastupdatediff is greater than HEATERWATTSPERIOD it probably means that the device has not yet reported any watts local timediff = integer(os.difftime (os.time(), deviceToTime(otherdevices_lastupdate[HEATERWATTS]))) if (timediff > HEATERWATTSPERIOD) then return nil end if (otherdevices_svalues[HEATERWATTS] ~= nil) then return integer(tonumber(otherdevices_svalues[HEATERWATTS])) else return nil end end -- isHeaterOn returns true or false if heater is consuming power (on) or not consuming power (off). Returns nil if status is not available function isHeaterRunning() local runtime = getCurrentRunTime() if (runtime == nil) then return nil end -- if runtime is less than HEATERWATTSPERIOD beacause the device has not yet reported any watts, return nil. if (runtime < HEATERWATTSPERIOD) then return nil end watts = getHeaterWatts() if (watts == nil) then return nil end -- return true if energy consumtion is greater than 5 watts return (watts > 5 and true or false) end function getHeaterLastUpdate() if (otherdevices_lastupdate[HEATERSWITCH] ~= nil) then return otherdevices_lastupdate[HEATERSWITCH] else return nil end end -- deviceToTime returns a device time string in a format which can be used by the os.difftime function function deviceToTime(time) return os.time { year = string.sub(time, 1, 4), month = string.sub(time, 6, 7), day = string.sub(time, 9, 10), hour = string.sub(time, 12, 13), min = string.sub(time, 15, 16), sec = string.sub(time, 18, 19) } end -- getOutdoorTemp gets the out door temp -- TODO: If local temperature device is unreachable, fetch temp from weather service instead function getOutdoorTemp() if ((otherdevices_lastupdate[OUTDOORTEMP] ~= nil) and (os.difftime(os.time(), deviceToTime(otherdevices_lastupdate[OUTDOORTEMP])) < 10*60)) then if (otherdevices_svalues[OUTDOORTEMP] ~= nil) then return tonumber(otherdevices_svalues[OUTDOORTEMP]) else print("Error!! Could not get outdoor temp from otherdevices_svalues['"..OUTDOORTEMP.."'], key field does not exist") end else print("Warning. No updates for local outdpor temperature sensor has been registered for 10 minutes.") if (otherdevices_svalues[OUTDOORTEMP] ~= nil) then return tonumber(otherdevices_svalues[OUTDOORTEMP]) else print("Error!! Could not get outdoor temp from otherdevices_svalues['"..OUTDOORTEMP.."'], key field does not exist") end end -- Todo, get temp from Internet weather service return 0 end -- END OF FUNCTIONS -- -- Calculate timestamp for this day mid night temp = os.date("*t", os.time()) todayMidnight = os.time{year=temp['year'], month=temp['month'], day=temp['day'], hour=0, min=0, sec=0 } -- print("todayMidnight = "..os.date("%Y-%m-%d %H:%M:%S", todayMidnight)) -- Get Current weekday as a number. [0-6 = Sunday-Saturday] currentWeekday = tonumber(os.date("%w",todayMidnight)) -- Calculate timestamp for sunday midnight sundayMidnight = todayMidnight - (60 * 60 * 24 * currentWeekday) -- print("sundayMidnight = "..os.date("%Y-%m-%d %H:%M:%S", sundayMidnight)) now = os.time() -- Convert leave time to timestamps local weekDays = { ['Sunday'] = 0, ['Monday'] = 1, ['Tuesday'] = 2, ['Wednesday'] = 3, ['Thursday'] = 4, ['Friday'] = 5, ['Saturday'] = 6 } for key,value in pairs(leaveTime) do leaveWeekday = string.match(value, "^(%a+) ") if (weekDays[leaveWeekday] == nil) then print("Warning, scheduled weekday "..leaveWeekday.." in "..value.." is not a valid weekday") end leaveHours = string.match(value, "[0-9][0-9]:+[0-9][0-9]") local tmp = os.date("*t", sundayMidnight + weekDays[leaveWeekday] * 60 * 60 * 24) leaveTime[key] = tonumber(os.time{year=tmp['year'], month=tmp['month'], day=tmp['day'], hour = string.sub(leaveHours, 1, 2), min = string.sub(leaveHours, 4, 5), sec=0 }) -- If leave time has passed current time add one week if (leaveTime[key] < now) then leaveTime[key] = leaveTime[key] + 60 * 60 * 24 * 7 end end -- sort in ascending order table.sort(leaveTime) -- Print scheduled leave times in correct order for key, dayLeaveTime in ipairs(leaveTime) do print("Next scheduled leave time is "..os.date("%Y-%m-%d %H:%M:%S", dayLeaveTime)) end -- Get engine heater mode if (uservariables['EngHeat_Mode'] ~= nil) then heaterMode = uservariables['EngHeat_Mode'] print("Engine heating mode is "..heaterMode) else heaterMode = "unknown" print("Warning! Cannot read user variable 'EngHeat_Mode") end -- Get engine heater switch status if (otherdevices[HEATERSWITCH] ~= nil) then engineHeaterStatus = otherdevices[HEATERSWITCH] else engineHeaterStatus = "unknown" print("Warning! Cannot read state for otherdevices['"..HEATERSWITCH.."']") end print ("Engine heater status is "..engineHeaterStatus) -- Get outdoor temperature currTemp = getOutdoorTemp() print ("Current outdoor temperature is "..currTemp.." Celsius") commandArray = {} if ((heaterMode == "Off") and (engineHeaterStatus == "Off") ) then -- Calculate runtime in seconds calculatedRuntime = calculateRuntime(currTemp) -- Handle if calculated runtime exceed maximum runtime if (calculatedRuntime > MAXRUNTIME) then print ("Calculated runtime using outdoor temp "..currTemp.." is "..secsToClock(calculatedRuntime)..") which exceeds maximum runtime "..secsToClock(MAXRUNTIME)..". Setting calculated runtime to "..secsToClock(MAXRUNTIME)) calculatedRuntime = MAXRUNTIME elseif (calculatedRuntime > 0) then print ("Calculated runtime using outdoor temp "..currTemp.." is "..secsToClock(calculatedRuntime)) elseif (calculatedRuntime == 0) then print ("Calculated runtime using outdoor temp "..currTemp.." is zero. Outdoor temp too high") else print ("ERROR!! Calculated runtime using outdoor temp "..currTemp.." is out of boundary: "..calculatedRuntime..". Setting it to zero") calculatedRuntime = 0 end -- Check if heater is running even if device is set to off in Domoticz (due to communication error?) runStatus = isHeaterRunning() if (runStatus == true) then print ("Engine heater current watts: "..getHeaterWatts()) print ("Error! Engine heater is running despite heater switch is set to off. Stopping engine heater") commandArray[HEATERSWITCH] = "Off" commandArray['Variable:EngHeat_Mode'] = "Off" commandArray['Variable:EngHeat_LeaveTime'] = "" return commandArray end -- Check if any scheduled start condition is met for key, dayLeaveTime in ipairs(leaveTime) do dayStartTime = dayLeaveTime - calculatedRuntime if ((now > dayStartTime) and (now < dayLeaveTime)) then -- Start heater only if calculated runtime is greater than zero if (calculatedRuntime > 0) then print("Next leave time is "..os.date("%Y-%m-%d %H:%M:%S", dayLeaveTime)) print("Engine heater start time is "..os.date("%Y-%m-%d %H:%M:%S", dayStartTime)..". Time to leave is "..secsToClock(dayLeaveTime-now)) commandArray[HEATERSWITCH] = "On" commandArray['Variable:EngHeat_Mode'] = "Auto" commandArray['Variable:EngHeat_LeaveTime'] = os.date("%Y-%m-%d %H:%M:%S", dayLeaveTime) print("Starting Engine heater") elseif (calculatedRuntime == 0) then print("Today leave time is "..os.date("%Y-%m-%d %H:%M:%S", dayLeaveTime)) print("Engine heater start time is "..os.date("%Y-%m-%d %H:%M:%S", dayStartTime)) print("Engine heater not started since calculatedRuntime is zero") else Print("ERROR! Calculated runtime is out of boundary: "..calculatedRuntime) end break end end elseif (engineHeaterStatus == "On") then -- Get the last time engine heater was set to on lastupdated = getHeaterLastUpdate() if (lastupdated == nil) then print ("Could not detect when engine heater was automatically turned on. For security reasons engine heater is now turned off") print ("Automatic mode is turned off, you have to manually turn engine heater on") commandArray[HEATERSWITCH] = "Off" commandArray['Variable:EngHeat_Mode'] = "Stopped" commandArray['Variable:EngHeat_LeaveTime'] = "" return commandArray end -- get runtime in seconds currentRuntime = getCurrentRunTime() if (currentRuntime == nil) then print ("Could not detect current runtime. For security reasons engine heater is now turned off") print ("Automatic mode is turned off, you have to manually turn engine heater on") commandArray[HEATERSWITCH] = "Off" commandArray['Variable:EngHeat_Mode'] = "Stopped" commandArray['Variable:EngHeat_LeaveTime'] = "" return commandArray end runStatus = isHeaterRunning() if (runStatus == false) then print ("Engine heater plug is not connected, cancelling heating") commandArray[HEATERSWITCH] = "Off" commandArray['Variable:EngHeat_Mode'] = (heaterMode == "Auto" and "Cancel" or "Off") commandArray['Variable:EngHeat_LeaveTime'] = "" return commandArray elseif (runStatus == true) then print ("Engine heater current watts: "..getHeaterWatts()) end -- Automatic mode if (heaterMode == "Auto") then print ("Engine heater was automatically set to on at "..lastupdated) -- Get leave time if (uservariables['EngHeat_LeaveTime'] == "") then print("Cannot read user variable 'EngHeat_LeaveTime'. Engine heater will run until max runtime is reached") else -- Assuming a date pattern like: yyyy-mm-dd hh:mm:ss dayLeaveTime = deviceToTime(uservariables['EngHeat_LeaveTime']) -- Has leave time passed? if (now > dayLeaveTime) then print("Today leave time is "..os.date("%Y-%m-%d %H:%M:%S", dayLeaveTime)) commandArray[HEATERSWITCH] = "Off" commandArray['Variable:EngHeat_Mode'] = "Off" commandArray['Variable:EngHeat_LeaveTime'] = "" print("Leave time has passed. Stopping Engine heater") return commandArray else print("Leave time is "..os.date("%H:%M:%S", dayLeaveTime)..". Time to leave is "..secsToClock(dayLeaveTime-now)) end end -- Manual mode elseif (heaterMode == "Manual") then print ("Engine heater was set to manually run at "..lastupdated) end print("Engine heater current runtime is "..secsToClock(currentRuntime)) -- Check if max runtime is exceeded if (currentRuntime > MAXRUNTIME) then print ("Engine heater maximum runtime exceeded. Cancelling engine heater") commandArray[HEATERSWITCH] = "Off" commandArray['Variable:EngHeat_Mode'] = (heaterMode == "Auto" and "Cancel" or "Off") commandArray['Variable:EngHeat_LeaveTime'] = "" return commandArray end -- If heaterMode is set to cancel and heater is manually shut off, heaterMode must be set to cancel until leave time has passed, otherwise it will start heating again elseif (heaterMode == "Cancel") then if (uservariables['EngHeat_LeaveTime'] == "") then print("Engine heater logic ran into unknown state. Resetting") commandArray[HEATERSWITCH] = "Off" commandArray['Variable:EngHeat_Mode'] = "Off" commandArray['Variable:EngHeat_LeaveTime'] = "" return commandArray end runStatus = isHeaterRunning() if (runStatus == true) then print ("Engine heater current watts: "..getHeaterWatts()) print ("Error! Engine heater is running despite heater switch is set to off. Stopping engine heater") commandArray[HEATERSWITCH] = "Off" commandArray['Variable:EngHeat_Mode'] = "Off" commandArray['Variable:EngHeat_LeaveTime'] = "" return commandArray end -- Assuming a date pattern like: yyyy-mm-dd hh:mm:ss dayLeaveTime = deviceToTime(uservariables['EngHeat_LeaveTime']) -- If leave time has passed if (now > dayLeaveTime) then -- Get the last time when engine heater was changed lastupdated = getHeaterLastUpdate() if (lastupdated == nil) then print("Engine heater logic ran into unkown state. Resetting") commandArray[HEATERSWITCH] = "Off" commandArray['Variable:EngHeat_Mode'] = "Off" commandArray['Variable:EngHeat_LeaveTime'] = "" return commandArray end print("Heating was manually canceled "..lastupdated) print("Today leave time was "..os.date("%Y-%m-%d %H:%M:%S", dayLeaveTime)) print("Leave time has passed. Changine engine heater mode from Cancel to Off") commandArray['Variable:EngHeat_Mode'] = "Off" commandArray['Variable:EngHeat_LeaveTime'] = "" return commandArray end elseif (heaterMode == "Stopped") then print("Engine heater mode is set to stopped. Check for errors in log. Heater must be manually started.") end return commandArray