PID thermostat with LogicMachine #
PID user library #
Go to Scripting > User libraries and create a library named pid.
Copy/paste the following code into the library.
PID = {
-- default params
defaults = {
-- invert algorithm, used for cooling
inverted = false,
-- minimum output value
min = 0,
-- maximum output value
max = 100,
-- proportional gain
kp = 1,
-- integral gain
ki = 1,
-- derivative gain
kd = 1,
}
}
-- PID init, returns new PID object
function PID:init(params)
local n = setmetatable({}, { __index = PID })
local k, v
-- set user parameters
n.params = params
-- copy parameters that are set by user
for k, v in pairs(PID.defaults) do
if n.params[ k ] == nil then
n.params[ k ] = v
end
end
-- reverse gains in inverted mode
if n.params.inverted then
n.params.kp = -n.params.kp
n.params.ki = -n.params.ki
n.params.kd = -n.params.kd
end
return n
end
-- resets algorithm on init or a switch back from manual mode
function PID:reset()
-- previous value
self.previous = grp.getvalue(self.params.current)
-- reset iterm
self.iterm = 0
-- last running time
self.lasttime = os.time()
-- clamp iterm
self:clampiterm()
end
-- clamps iterm value
function PID:clampiterm()
self.iterm = math.max(self.iterm, self.params.min)
self.iterm = math.min(self.iterm, self.params.max)
end
-- clamp and set new output value
function PID:setoutput()
local t, object, value
self.output = math.max(self.output, self.params.min)
self.output = math.min(self.output, self.params.max)
value = math.floor(self.output)
local t = type(self.params.output)
-- write to output if object is set
if t == 'string' or t == 'table' then
if t == 'string' then
self.params.output = { self.params.output }
end
for _, output in ipairs(self.params.output) do
grp.write(output, value, dt.scale)
end
end
end
-- algorithm step, returns nil when disabled or no action is required,
-- output value otherwise
function PID:run()
local result
-- get manual mode status
local manual = false
if self.params.manual then
manual = grp.getvalue(self.params.manual)
end
-- in manual mode, do nothing
if manual then
self.running = false
-- not in manual, check if reset is required after switching on
elseif not self.running then
self:reset()
self.running = true
end
-- compute new value if not in manual mode
if self.running then
-- get time between previous and current call
local now = os.time()
self.deltatime = now - self.lasttime
self.lasttime = now
-- run if previous call was at least 1 second ago
if self.deltatime > 0 then
result = self:compute()
end
end
return result
end
-- computes new output value
function PID:compute()
local current, setpoint, deltasc, deltain, output
-- get input values
current = grp.getvalue(self.params.current)
setpoint = grp.getvalue(self.params.setpoint)
-- delta between setpoint and current
deltasc = setpoint - current
-- calculate new iterm
self.iterm = self.iterm + self.params.ki * self.deltatime * deltasc
self:clampiterm()
-- delta between current and previous value
deltain = current - self.previous
-- calculate output value
self.output = self.params.kp * deltasc + self.iterm
self.output = self.output - self.params.kd / self.deltatime * deltain
-- write to output
self:setoutput()
-- save previous value
self.previous = current
return self.output
end
Usage #
p = PID:init(parameters)
p:run()
Parameters (Lua table) #
Mandatory:
current- (object address or name) current temperature value (2 byte float or any numeric value)setpoint– (object address or name) temperature set point value (2 byte float or any numeric value)
Optional:
manual– (object address or name) PID algorithm is stopped when this object value is 1output– (object address or name, can be a table with multiple objects) output object (1 byte scaled)inverted– (boolean, defaults to false) invert algorithm, can be used for coolingmin– (number, defaults to 0) minimum output valuemax– (number, defaults to 100) maximum output valuekp– (number, defaults to 1) proportional gainki– (number, defaults to 1) integral gainkd– (number, defaults to 1) derivative gain
Output value #
p:run() returns the calculated output value. If the output parameter
is not set, you can use the return value to control
output objects manually in the script.
Resident script #
Recommended sleep time is at least 10 seconds. This affects how often the PID algorithm runs.
Script example #
-- init pid algorithm
if not p then
require('user.pid')
p = PID:init({
current = '1/1/1',
setpoint = '1/1/2',
output = '1/1/3'
})
end
-- run algorithm
p:run()
Script example with multiple output objects #
-- init pid algorithm
if not p then
require('user.pid')
p = PID:init({
current = '1/1/1',
setpoint = '1/1/2',
output = { 'PWM 1', 'PWM 2', '1/1/5' }
})
end
-- run algorithm
p:run()
Multiple PIDs in a single script #
It is recommended to use a single script when multiple PIDs are needed.
if not p1 then
require('user.pid')
p1 = PID:init({
current = '1/1/1',
setpoint = '1/1/2',
output = '1/1/3'
})
p2 = PID:init({
current = '2/1/1',
setpoint = '2/1/2',
output = '2/1/3'
})
end
p1:run()
p2:run()