Example: PID thermostat with LM

PID function

There is a PID function already added by default in Logic Machine -> Scripts -> Tools menu.

Source code    
  1. PID = {
  2. -- default params
  3. defaults = {
  4. -- invert algorithm, used for cooling
  5. inverted = false,
  6. -- minimum output value
  7. min = 0,
  8. -- maximum output value
  9. max = 100,
  10. -- proportional gain
  11. kp = 1,
  12. -- integral gain
  13. ki = 1,
  14. -- derivative gain
  15. kd = 1,
  16. }
  17. }
  18.  
  19. -- PID init, returns new PID object
  20. function PID:init(params)
  21. local n = setmetatable({}, { __index = PID })
  22. local k, v
  23.  
  24. -- set user parameters
  25. n.params = params
  26.  
  27. -- copy parameters that are set by user
  28. for k, v in pairs(PID.defaults) do
  29. if n.params[ k ] == nil then
  30. n.params[ k ] = v
  31. end
  32. end
  33.  
  34. -- reverse gains in inverted mode
  35. if n.params.inverted then
  36. n.params.kp = -n.params.kp
  37. n.params.ki = -n.params.ki
  38. n.params.kd = -n.params.kd
  39. end
  40.  
  41. return n
  42. end
  43.  
  44. -- resets algorithm on init or a switch back from manual mode
  45. function PID:reset()
  46. -- previous value
  47. self.previous = grp.getvalue(self.params.current)
  48. -- reset iterm
  49. self.iterm = 0
  50. -- last running time
  51. self.lasttime = os.time()
  52.  
  53. -- clamp iterm
  54. self:clampiterm()
  55. end
  56.  
  57. -- clamps iterm value
  58. function PID:clampiterm()
  59. self.iterm = math.max(self.iterm, self.params.min)
  60. self.iterm = math.min(self.iterm, self.params.max)
  61. end
  62.  
  63. -- clamp and set new output value
  64. function PID:setoutput()
  65. local t, object, value
  66.  
  67. self.output = math.max(self.output, self.params.min)
  68. self.output = math.min(self.output, self.params.max)
  69.  
  70. value = math.floor(self.output)
  71. local t = type(self.params.output)
  72.  
  73. -- write to output if object is set
  74. if t == 'string' or t == 'table' then
  75. if t == 'string' then
  76. self.params.output = { self.params.output }
  77. end
  78.  
  79. for _, output in ipairs(self.params.output) do
  80. grp.write(output, value, dt.scale)
  81. end
  82. end
  83. end
  84.  
  85. -- algorithm step, returns nil when disabled or no action is required, output value otherwise
  86. function PID:run()
  87. local result
  88.  
  89. -- get manual mode status
  90. local manual = self.params.manual and grp.getvalue(self.params.manual) or false
  91.  
  92. -- in manual mode, do nothing
  93. if manual then
  94. self.running = false
  95. -- not in manual, check if reset is required after switching on
  96. elseif not self.running then
  97. self:reset()
  98. self.running = true
  99. end
  100.  
  101. -- compute new value if not in manual mode
  102. if self.running then
  103. -- get time between previous and current call
  104. local now = os.time()
  105. self.deltatime = now - self.lasttime
  106. self.lasttime = now
  107.  
  108. -- run if previous call was at least 1 second ago
  109. if self.deltatime > 0 then
  110. result = self:compute()
  111. end
  112. end
  113.  
  114. return result
  115. end
  116.  
  117. -- computes new output value
  118. function PID:compute()
  119. local current, setpoint, deltasc, deltain, output
  120.  
  121. -- get input values
  122. current = grp.getvalue(self.params.current)
  123. setpoint = grp.getvalue(self.params.setpoint)
  124.  
  125. -- delta between setpoint and current
  126. deltasc = setpoint - current
  127.  
  128. -- calculate new iterm
  129. self.iterm = self.iterm + self.params.ki * self.deltatime * deltasc
  130. self:clampiterm()
  131.  
  132. -- delta between current and previous value
  133. deltain = current - self.previous
  134.  
  135. -- calculate output value
  136. self.output = self.params.kp * deltasc + self.iterm
  137. self.output = self.output - self.params.kd / self.deltatime * deltain
  138.  
  139. -- write to output
  140. self:setoutput()
  141.  
  142. -- save previous value
  143. self.previous = current
  144.  
  145. return self.output
  146. end

Usage

Source code    
  1. p = PID:init(parameters)
  2. p:run()

Parameters

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

Adding Residential script

PID algorithm should be placed inside a Scripts -> Resident –> Add new script.

Script example:

Source code    
  1. -- init pid algorithm
  2. if not p then
  3. p = PID:init({
  4. current = '1/1/1',
  5. setpoint = '1/1/2',
  6. output = '1/1/3'
  7. })
  8. end
  9.  
  10. -- run algorithm
  11. p:run()

Script example with multiple output objects:

Source code    
  1. -- init pid algorithm
  2. if not p then
  3. p = PID:init({
  4. current = '1/1/1',
  5. setpoint = '1/1/2',
  6. output = { 'PWM 1', 'PWM 2', '1/1/5' }
  7. })
  8. end
  9.  
  10. -- run algorithm
  11. p:run()

Output value:
p:run() returns output value. If output parameter is not set, you can use the return value to control output objects manually in the script.

 

Maximum count of PID per device

LogicMachine normally can handle up to 30 PID loops. But it depends on resident script sleep time. We suggest to use a single script for all PID controllers instead of a separate script for each.