PID thermostat

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 1
  • output – (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 cooling
  • min – (number, defaults to 0) minimum output value
  • max – (number, defaults to 100) maximum output value
  • kp – (number, defaults to 1) proportional gain
  • ki – (number, defaults to 1) integral gain
  • kd – (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()