EQ3 MAX!

From Domoticz
Jump to navigation Jump to search

Background

EQ3 MAX! A system of inexpensive components including wireless battery operated TRV’s that easily retro-fit to your existing radiators without the need for a plumber, or to drain your system. Once MAX! is installed, it will control each radiators motorised valve individually, creating a zoned heating system with higher efficiency and in turn reduced bills.

All information here is condensed from the topic https://www.domoticz.com/forum/viewtopic.php?f=34&t=841

**** WARNING *****
The Lua version of this plugin is not actively maintained anymore. It may still work, but no guarantees. I strongly suggest to install the Python plugin version, for instructions see https://github.com/mvzut/maxcube-Domoticz-plugin

See also the forum topic https://www.domoticz.com/forum/viewtopic.php?f=34&t=25081

Component Overview

Cube LAN Gateway

The Cube is the gateway between your LAN and the RF comms of the MAX! system. An Ethernet port connects to your network and power is provided via a USB socket. Once attached you can configure the system with the MAX! software. In addition you can monitor the status of your system and control it from your PC or Smartphone / Tablet across the Internet via eQ-3’s server. Local control can be carried out without the need for an Internet connection.

Wireless Wall Thermostat

The wall thermostat can control up to eight MAX! radiator thermostats in any one room and has an internal sensor that measures the temperature in the room and transmits it to the radiator thermostats. The thermostat also reports back to the "Cube".

Radiator Thermostat

The Radiator Thermostat is responsible for regulating your radiators. All initial configuration settings are made with the MAX! Software and different settings can be made for each room. Communication between MAX! components is bi-directional. This ensures that the information sent reaches the recipient. There are three versions available, essentially a Basic, a Standard and a Plus. All units use 2 x AA cells and e-Q3 quote battery life as 2 years with 2 operations per day.

Other Components

Also available (but not yet necessarily Domoticz ready) are

  • Wireless Window Sensor that lets the system know when a door or window has been opened allowing the Radiator Thermostats to automatically reduce the temperature to save energy, and increase it again once the window or door has been closed.
  • The MAX! Eco switch which allows you to set back the temperature in all your rooms with a single button press when you leave home. When you return you can also set all rooms back to auto mode by pressing the button again.
  • There’s also a Plug Adaptor for controlling electric and water heaters but it appears to only be available with a European socket design. It’s unclear whether this could also be used for boiler control.

Hardware requirements

You will need

  • Domoticz - A Linux based Domoticz installation as these instructions do not cover a Windows install at this point.
  • A Cube LAN Gateway - You will only need one for whole installation. Once set up, additional rooms can be added as and when time and finance allows.
  • A Radiator Thermostat - Maximum 8 per room
  • (Optional) Window Sensor/Door Sensor
  • (Optional)Wireless Wall Thermostat - One per room
  • (Optional) Switch - A means of switching your boiler on and off. This is an individual case as everyone has a different heating system and different requirements. So I wouldn't dare to give advice as to which method to use. It is recommended that if you are unsure, seek advice from a Plumber/Electrician for safety.
    N.B. I personally have a "Fibaro FGS-222" that controls my heating and hot water as will be seen in the example.

Software Requirements

First you need to update Lua to 5.2. and install some extra lua files.

sudo apt-get install lua5.2
sudo apt-get install lua-socket
sudo apt-get install luarocks
sudo luarocks install luasec
sudo luarocks install luasocket
sudo luarocks install basexx

Then check if they have all installed ok

luarocks list

Which should show

Installed rocks:
----------------
basexx
  0.3.0-1 (installed) - /usr/local/lib/luarocks/rocks
luasec
  0.6-1 (installed) - /usr/local/lib/luarocks/rocks
luasocket
  3.0rc1-2 (installed) - /usr/local/lib/luarocks/rocks

If you get a problem such as openssl.a or openssl.so or openssl.so.* not found
This may be an issue withe where different systems install/store files
try

sudo apt-get install libssl-dev
sudo mkdir /usr/local/ssl
sudo cp /usr/lib/arm-linux-gnueabihf/libssl.* /usr/local/ssl
sudo luarocks install luasec OPENSSL_LIBDIR=/usr/local/ssl

then check

luarocks list

N.B. If you had lua5.1 installed you may need to copy some files across to lua5.2 as they appear to install differently.

sudo cp /usr/local/share/lua/5.1/*.lua /usr/local/share/lua/5.2

Hopefully, your system now has everything in place now to run a simple script to detect your EQ3 MAX! devices.

Installing EQ3 MAX! Devices

Once you have powered up your cube and connected it to your network via the ethernet lead, you will be able to find its local IP address and network name.
Next visit https://max.eq-3.de/login.jsp and download the Windows/Mac OS software. Unfortunately no Linux option is available. This software will enable you to access your cube, create an EQ3 account and add devices.
Each device needs to be "taught in" as per the manual. During this process you can name each device and create names for each device.
Device names are critical to how Domoticz will "see" your devices. Later, you will create dummy devices that must have exactly the same name (including capitals and spacing etc.) as used in the EQ3 App.
If you are not a coder or just want an easy life, the following naming structure will work with the scripts without much head scratching.

  • Sitting Room
    • Sitting Room-Stat
    • Sitting Room-Rad
    • Sitting Room-Sens
  • Kitchen
    • Kitchen-Stat
    • Kitchen-Rad
    • Kitchen-Sens
  • Another Room
    • Another Room-Stat
    • Another Room-Rad
    • Another Room-Sens

The reasoning for this is that the second script for heating control looks for "-Stat" to identify a Wall Thermostat and "-Rad" to identify a Radiator Thermostat. The first script uses the "-Sens" suffix to identify contact sensors. If you have more than one radiator or contact sensor per room, you can name them e.g. "Kitchen left-Rad" and "Kitchen right-Rad" or "Sitting Room window-Sens" and "Sitting Room door-Sens".
N.B. Make sure all rooms are set to "Manual" before exiting the App.
Now if we have at least one room set up we can move on to testing.

Initial Testing

Create a lua file called maxtest.lua and copy and paste this script into it. Then edit the IP details to match your Cube's details.

--maxtest.lua

package.loadlib("core.so", "*")
local Socket = require "socket"
local Basexx = require "basexx"

-- Enter your cube name or ip address and port here
--MaxIP='192.168.1.5'
MaxIP='LEQ1281041'
MaxPort=62910

local Rooms   = {}
local Devices = {}

function maxCmd_H(data)
--   print('H='..data)
end

function maxCmd_M(data)
   i = 0
   j = 0
   
    while true do    -- find 'next' comma
      i = string.find(data, ",", i+1)
      if not i then break end
     j = i
    end
   
   s   = data:sub(j+1)
   dec = Basexx.from_base64(s)
   num_rooms = string.byte(dec,3)
   pos=4
   
   for i=1, num_rooms do
      room_num = string.byte(dec, pos)
      name_len = string.byte(dec, pos+1)
      pos  = pos+2
      name = dec:sub(pos, pos+name_len)
      pos  = pos+name_len
      adr  = Basexx.to_hex(dec:sub(pos, pos+2))
      Rooms[adr] = name
      pos = pos+3
   end
   
   print("Rooms\n-----")
   for adr, name in pairs(Rooms) do
      print(name, adr)
   end
   
   num_devs = string.byte(dec, pos)
   for i=1, num_devs do
   
      dtype = string.byte(dec, pos+1)
      adr   = Basexx.to_hex(dec:sub(pos+2, pos+4))
      snum  = dec:sub(pos+5, pos+14)
      name_len = string.byte(dec, pos+15)
      pos  = pos+16
      name = dec:sub(pos, pos+name_len)
      pos  = pos+name_len
      room_num = string.byte(dec, pos)
      Devices[adr] = name
   end
   
   print("\nDevices\n-------")
      for adr, name in pairs(Devices) do
      print(name, adr)
   end

end

function maxCmd_C(data)
--   print('C='..data)
end

function maxCmd_L(data)
print("\nDevice status\n-------------")
   pos = 1
   dec = Basexx.from_base64(data)
   L_hex = Basexx.to_hex(dec)
   L_len = string.len(L_hex)
   
   while (pos < L_len) do

      s = L_hex:sub(pos,(pos+1))
      data_len  = tonumber(s,16) + 1
      hex  = L_hex:sub(pos,pos+(data_len*2))
      adr  = hex:sub(3,8)
      name = Devices[adr]
      if not name then name=adr end
      valve_info = tonumber(hex:sub(13,14),16)
      batt = bit32.extract(valve_info,7,1)
      bst  = bit32.extract(valve_info,3,1)
      mode = bit32.extract(valve_info,0,2)
      
      if (batt==0) then sbat="OK" else sbat="Low" end
      if (mode==0) then smode="Auto" elseif (mode==1) then smode="Manual"
      elseif (mode==2) then smode="Holiday" elseif (mode==3) then smode="Boost" end
      
      if (data_len == 13) then   -- WallMountedThermostat (dev_type 3)
         valve_pos = -1
         s = hex:sub(17,18)
         setpoint = tonumber(s,16) / 2
         s = hex:sub(25,26)
         temp = tonumber(s,16) / 10
         dtype = "Thermostat"
      elseif (data_len == 12) then -- HeatingThermostat (dev_type 1 or 2)
         s = hex:sub(15,16)
         valve_pos = tonumber(s,16)
         s = hex:sub(17,18)
         setpoint = tonumber(s,16) / 2
         s = hex:sub(21,22)
         temp = tonumber(s,16) / 10
         dtype = "Valve    "
      end
print(dtype, name, "Setpoint="..setpoint, "Temp="..temp, "Valve pos="..valve_pos, "Battery="..sbat, "Mode="..smode)
      pos = pos + (data_len*2)
   end
end

local tcp = Socket.connect(MaxIP, MaxPort)

if not tcp then
   print("Socket connect failed for "..MaxIP..':'..MaxPort)
   return
end

tcp:settimeout(2)

while true do
    s, status, partial = tcp:receive()
   
    if (status) then
      print("TCP receive - "..status)
      break
   end
      
    local line = (s or partial)
   local cmd  = line:sub(1,1)
   local data = line:sub(3)
   
   if (cmd == 'H') then
      maxCmd_H(data)
   elseif (cmd == 'M') then
      maxCmd_M(data)
   elseif (cmd == 'C') then
      maxCmd_C(data)
   elseif (cmd == 'L') then
      maxCmd_L(data)
      break
   end
end

tcp:close()

Run it with

lua maxtest.lua

Make sure to close the Max app, the cube only accepts 1 connection.
If everything is ok, this test script will gather information from each of your devices that looks something like this...

Thermostat SittingRoom-Stat	Setpoint=20.5	Temp=20.4 Valve pos=-1	Battery=OK	Mode=Manual
Valve      SittingRoom-Rad	Setpoint=20.5	Temp=0	  Valve pos=80	Battery=OK	Mode=Manual

If not, go back over your previous steps and look for an error, failing that ask in the forum https://www.domoticz.com/forum/viewtopic.php?f=34&t=841 and one of us can hopefully help you.

Dummy Devices

It is a good idea to create a new "Dummy" in the hardware tab and give it a name such as "EQ3", "MAX!" or "Heating" as it keeps things organised and keeps the OCD at bay.
Now for each room create...

  • A "Dummy Temperature Sensor" with exactly the same name as your room (as defined in MAX!), i.e. "Sitting Room" (Only necessary when you have wall thermostats)
  • A "Dummy Percentage" with exactly the same name as your radiator valve (as defined in MAX!), i.e. "Sitting Room-Rad"
  • A "Dummy Thermostat Setpoint" with exactly the same name as your thermostat (as defined in MAX!), i.e. "Sitting Room-Stat" (If you don't have wall thermostats, name the Domoticz thermostats after your radiator valves but then with the "-Stat" suffix instead of "-Rad")

You see that there is no dummy device for the Window/Door contact sensors. this is because the're not(yet) integrated into domoticz.

Scripts

There are two scripts that I use. The first (which is necessary) to "Read" the cube and change setpoints and a second (optional/personal choice) to use the information gathered by the first to actually control my heating.

Max Script

This is the script that extracts all the juicy information from the cube and drops it into those dummy devices we made earlier.

Create a file called script_time_max.lua and paste this script into it. Remember to edit the "MaxIP" and "MaxPort" to match your Cube, and change the value of "DomoticzPort" if you don't use the default 8080. If you don't have wall mounted thermostats in every room, change the value of "useWMT" into false. This will use your valves instead of your thermostats to synchronize the thermostat setpoints.

--./script_time_max.lua
----------------------------------------------------------------------------------------------------------
-- Script parameters
----------------------------------------------------------------------------------------------------------
package.loadlib("core.so", "*")
local Socket = require "socket"
local Basexx = require "basexx"

local MaxIP='192.168.178.46'
local MaxPort = 62910
local useWMT = true --Set to true if there is a wall mounted thermostat in every room
local interval = 5 --Polling interval in minutes, possible range 1-59. Cube doesn't seem to like too frequent communication, 5 minutes is a safe value
local DomoticzPort = 8080

local Rooms   = {}
local Devices = {}
local Room_nums = {}

function age(timestring)
  t = {}
  t.year = string.sub(timestring,1,4)
  t.month = string.sub(timestring,6,7)
  t.day = string.sub(timestring,9,10)
  t.hour = string.sub(timestring,12,13)
  t.min = string.sub(timestring,15,16)
  t.sec = string.sub(timestring,18,19)
  return os.difftime(os.time(),os.time(t))
end

function maxCmd_H(data)
--   print('H='..data)
end

function maxCmd_M(data)
   i = 0
   j = 0
   
    while true do    -- find next comma
      i = string.find(data, ",", i+1)
      if not i then break end
     j = i
    end
   
   s   = data:sub(j+1)
   dec = Basexx.from_base64(s)
   num_rooms = string.byte(dec,3)
   pos=4
   
   for i=1, num_rooms do
      room_num = string.byte(dec, pos)
      name_len = string.byte(dec, pos+1)
      pos  = pos+2
      name = dec:sub(pos, pos+name_len-1)
      pos  = pos+name_len
      adr  = Basexx.to_hex(dec:sub(pos, pos+2))
      Rooms[room_num] = name
      pos = pos+3
   end
      
   num_devs = string.byte(dec, pos)
   for i=1, num_devs do
      dtype = string.byte(dec, pos+1)
      adr   = Basexx.to_hex(dec:sub(pos+2, pos+4))
      snum  = dec:sub(pos+5, pos+14)
      name_len = string.byte(dec, pos+15)
      pos  = pos+16
      name = dec:sub(pos, pos+name_len-1)
      pos  = pos+name_len
      room_num = string.byte(dec, pos)
      Room_nums[adr] = room_num
      Devices[adr] = name
   end

end

function maxCmd_C(data)
--   print('C='..data)
end

function maxCmd_L(data)
   pos = 1
   dec = Basexx.from_base64(data)
   L_hex = Basexx.to_hex(dec)
   L_len = string.len(L_hex)
   
   while (pos < L_len) do

      s = L_hex:sub(pos,(pos+1))
      data_len  = tonumber(s,16) + 1
      hex  = L_hex:sub(pos,pos+(data_len*2))
      adr  = hex:sub(3,8)
      room_num = string.format("%02X", Room_nums[adr])
      room = Rooms[Room_nums[adr]]
      name = Devices[adr]
      if not name then name=adr end
      valve_info = tonumber(hex:sub(13,14),16)
      batt = bit32.extract(valve_info,7,1)
      bst  = bit32.extract(valve_info,3,1)
      mode = bit32.extract(valve_info,0,2)
      
      if (batt==0) then sbat="OK" else sbat="Low" end
      if (mode==0) then smode="Auto" elseif (mode==1) then smode="Manual"
      elseif (mode==2) then smode="Holiday" elseif (mode==3) then smode="Boost" end
      
      if (data_len == 13) then   -- WallMountedThermostat (dev_type 3)
        valve_pos = -1
        s = hex:sub(17,18)
        setpoint = tonumber(s,16) / 2
        s = hex:sub(23,26)
        temp = tonumber(s,16) / 10
        dtype = "Thermostat"
      elseif (data_len == 12) then -- HeatingThermostat (dev_type 1 or 2)
        s = hex:sub(15,16)
        valve_pos = tonumber(s,16)
        s = hex:sub(17,18)
        setpoint = tonumber(s,16) / 2
        if (mode ~= 2) then
          s = hex:sub(19,22)
          temp = tonumber(s,16) / 10
        else
          temp = 0
        end
        dtype = "Valve"
      end
      
      --Following two lines correct temperatures over 25.5 degrees, since e.g. 26 degrees is reported as 0.5 degrees
      --This is due to the fact that temperatures seem to be stored as two Hex characters only (= max 255 in decimal)
      --Pending better solution
      if temp < 5 then temp = temp + 25.5 end
      if setpoint > 50 then setpoint = setpoint - 64 end

      -- Update virtual devices in Domoticz and update MAX! setpoints if necessary

      --print(dtype.."  "..name.."  Setpoint="..setpoint.."  Temp="..temp.."  Valve pos="..valve_pos)
      if dtype == "Valve" and name:sub(-5,-1) ~= "-Sens" then
        table.insert(commandArray, { ['UpdateDevice'] = otherdevices_idx[name]..'|0|'..valve_pos})   
        if not useWMT then --Use valve to update temperature and synchronize setpoints
          name = name:sub(1,-5) .. "-Stat"  -- set thermostat name to valve name with "-Stat" instead of "-Rad" suffix
          setpoint_Domoticz = tonumber(otherdevices_svalues[name])
          if setpoint_Domoticz ~= setpoint then
            if age(otherdevices_lastupdate[name]) > interval * 60 then --Domoticz thermostat value must be updated
              table.insert(commandArray, { ['OpenURL'] = 'http://127.0.0.1:'..DomoticzPort..'/json.htm?type=command&param=udevice&idx='..otherdevices_idx[name]..'&nvalue=0&svalue='..setpoint})
              print('Domoticz setpoint ' .. name .. ' updated')
            else --Max! setpoint must be updated
              MaxCmdSend(adr, room_num, "manual", setpoint_Domoticz)
              print('MAX! setpoint ' .. name .. ' updated')
            end
          end
        end
      elseif dtype == "Thermostat" then
        table.insert(commandArray, { ['UpdateDevice'] = otherdevices_idx[room]..'|0|'..temp})
        setpoint_Domoticz = tonumber(otherdevices_svalues[name])
        if setpoint_Domoticz ~= setpoint then
          if age(otherdevices_lastupdate[name]) > interval * 60 then --Domoticz thermostat value must be updated
            table.insert(commandArray, { ['OpenURL'] = 'http://127.0.0.1:'..DomoticzPort..'/json.htm?type=command&param=udevice&idx='..otherdevices_idx[name]..'&nvalue=0&svalue='..setpoint})
            print('Domoticz setpoint ' .. name .. ' updated')
          else --Max! setpoint must be updated
            MaxCmdSend(adr, room_num, "manual", setpoint_Domoticz)
            print('MAX! setpoint ' .. name .. ' updated')
          end
        end   
      end

      pos = pos + (data_len*2)
   end
end


function MaxCmdSend(id, room, mode, setpoint)

   bits  = setpoint * 2
   smode = string.upper(mode)
   if smode == 'MANUAL' then
      bits = 64 + bits
   elseif smode == 'BOOST' then
      bits = 192 + bits
   elseif smode == 'VACATION' then
      bits = 128 + bits
   end

   hex = "000440000000"..id..room..string.format("%x",bits)
   sendStr = Basexx.to_base64(Basexx.from_hex(hex))
   i, status = tcp:send("s:"..sendStr.."\r\n")
   
   if not i then
      print("MAX TCP send failed - "..status)
      return
   end
end


commandArray = {}

local m = os.date('%M')
if (m % interval == 0) and (m ~= 0) then

  tcp = Socket.connect(MaxIP, MaxPort)
  if not tcp then
    print("Socket connect failed for "..MaxIP..':'..MaxPort)
    return
  end
  tcp:settimeout(2)

  while true do
    s, status, partial = tcp:receive()
    if (status) then
      print("TCP receive - "..status)
     break
    end
      
    local line = (s or partial)
    local cmd  = line:sub(1,1)
    local data = line:sub(3)
   
    if (cmd == 'H') then
      status = maxCmd_H(data)
      if status == 'Error' then break end
    elseif (cmd == 'M') then
      maxCmd_M(data)
    elseif (cmd == 'C') then
      maxCmd_C(data)
    elseif (cmd == 'L') then
      maxCmd_L(data)
      break
    end
  end

  tcp:close()

end

return commandArray

Place the script in your domoticz/scripts/lua folder (pasting into the internal editor should work just as well) and soon the Dummy Devices should start to show data.

If you will see error in Domoticz log you should do this:

mv usrlocalsharelua5p2.tar.gz /usr/local/share/lua/5.2/
mv usrlocalliblua5p2.tar.gz /usr/local/lib/lua/5.2/
cd /usr/local/share/lua/5.2/
sudo tar -xvf usrlocalsharelua5p2.tar.gz
sudo rm  usrlocalsharelua5p2.tar.gz
cd /usr/local/lib/lua/5.2/
sudo tar -xvf usrlocalliblua5p2.tar.gz
rm usrlocalliblua5p2.tar.gz

Files: usrlocalsharelua5p2.tar.gz usrlocalliblua5p2.tar.gz

Valves Script

This script uses the information from the new devices to control a boiler and is an example of how I use the system.
I have

  • A dummy switch called "Heating"
  • A dummy switch called "Holiday"
  • A Fibaro Double Relay called "CH_Switch" and "HW_Switch"
  • A "Selector Switch" with Off|Hot Water|Heating|Holiday

The "Selector Switch" triggers one of four scenes

  • Off = Heating off + HW_Switch off + Holiday Off
  • Hot Water = Heating off + HW_Switch on + Holiday Off
  • Heating = Heating on + HW_Switch off + Holiday Off
  • Holiday = Heating off + HW_Switch off + Holiday On

From within the switch settings I can set timers for when I want heating or hot water.
Don't forget that the "Thermostat Setpoint" also has in-built timers so you can raise and lower temperatures for different times of the day.
This script looks not only at temperatures but how much the valves are open and calling for heat. It has saved me fuel over the last winter by this method.
Ok, so here's script_time-Valves.lua to make use of it you will need to edit the devices to suit your needs.
There are some "Preset Values" at the beginning of the script that will need to reflect your needs.
You will also need to change the "Switch" variables to match your switches.

-- script_time_Heating Valves.lua
-- Version 2.0 07/11/16
-- Script to read the % open of radiator valves
-- All radiator valves are labelled "<room name>-Rad"
-- search is made for "-Rad" to indicate a radiator valve
-- If found it will be interrogated for % open value
 
-- Thermostat are named <room name>-Stat so a search is made for "-Stat" to indicate thermostats
-- If found it will be interrogated for temperature value
 
-- If demand is greater than BoilerOnPercent value then fire up boiler
-- If demand is less than BoilerOnPercent minus HysterysisOffPercent then switch off boiler
 
-- Preset Values
BoilerOnPercent = 30               -- percentage valve open at which the boiler will be turned on
HysterysisOffPercent = 20             -- percentage below BoilerOnPercent to switch off the boiler
MinValves = 2                      -- Number of Valves that need to be open before boiler is turned on
ValvePercentOveride = 75            -- Percentage value of valve open required to override MinValves value (one room is very cold)
HolidayMinTemp = 10                -- Minimum room temperature before boiler is turned on during holiday period
HolidayHysterysisTemp = 2             -- Value to increase house temperature by while in holiday mode if boiler is turned on due to low temperatures
MissingDevicesTime = 86400            -- Value in seconds to allow before reporting a device has not been updated
email = "[email protected]"             -- email address for warnings
 
-- Script Variables
PercentMax = 0
TempMin = 10
ValveCount = 0
MissingDeviceCount = 0
SendAnEmail = false
HeatingSwitch = "Heating"
BoilerSwitch = "CH_Switch"
HolidaySwitch = "Holiday"
 
-- Set printing to log options (true / false)
printData = true
printDebug = false
 
 
-- Get current date & time
t1 = os.time()
local currentDate = os.date("*t");  -- sets up currentDate.[table]
-- (currentDate.year [full], .month [1-12], .day [1-31], .hour [0-23], .min [0-59], .sec [0-59], .wday [0-6 {Sun-Sat}])
sCurrentTime = currentDate.year .. "-" .. currentDate.month .. "-" .. currentDate.day .. " " .. currentDate.hour .. ":" .. currentDate.min .. ":" .. currentDate.sec
 
function TimeElapsed(s) -- expects date & time in the form of 2010-01-23 12:34:56
   year = string.sub(s, 1, 4)
   month = string.sub(s, 6, 7)
   day = string.sub(s, 9, 10)
   hour = string.sub(s, 12, 13)
   minutes = string.sub(s, 15, 16)
   seconds = string.sub(s, 18, 19)
   t1 = os.time()
   t2 = os.time{year=year, month=month, day=day, hour=hour, min=minutes, sec=seconds}
   difference = os.difftime (t1, t2)
   return difference -- in seconds
end
 
 
 
commandArray = {}
 
       -- print blank line in log
       if printData == true then
         print (" ")
         print (" *** Heating Script Output ***")
         print (" ")
       end
 
       -- Get Data from Radiator Valves
      for i, v in pairs(otherdevices) do -- Get all devices in the database
         v = i:sub(-4,-1) -- Grab the last four characters of the device name
 
            if (v == '-Rad') then -- are the last four characters "-Rad"? If so we have a Radiator Valve
 
                RoomName = i:sub(1,-5) -- Get the rest of the name, which will be the room name
                sSetTempValue = otherdevices_svalues[RoomName..'-Stat']
                sValvePercentOpen = otherdevices_svalues[i]
                sLastUpdateTime = otherdevices_lastupdate[i]
                sElapsedTime = TimeElapsed(otherdevices_lastupdate[i])
                message = RoomName .. " valve is open " .. sValvePercentOpen .. " percent " .. " Setpoint temperature is " .. sSetTempValue .. "C"  -- for debug
                message2 = RoomName .. " last seen " .. sLastUpdateTime  ..  " Elapsed time " .. sElapsedTime
                if printData == true then
                  print (message)
                  print (message2)
                end
 
                -- check for missing devices
                if sElapsedTime > MissingDevicesTime then
                SendAnEmail = false
                MissingDeviceCount = MissingDeviceCount + 1
                end
 
                -- get the % value of the most open Radiator Valve
                if tonumber(sValvePercentOpen) > PercentMax then
                  PercentMax = tonumber(sValvePercentOpen)
                end
 
                -- Count the number of valves that are open more than BoilerOnPercent
                if tonumber(sValvePercentOpen) >= BoilerOnPercent then
                  ValveCount = ValveCount + 1
                end   
 
 
            end
      end
 
       if printData == true then
         print (" ")
       end     
 
      -- Get Data from Thermostats
      for i, v in pairs(otherdevices) do -- Get all devices in the database
         v = i:sub(-5,-1) -- Grab the last five characters of the device name
 
            if (v == '-Stat') then -- are the last five characters "-Stat "? If so we have an EQ-3 Thermostat
 
                RoomName = i:sub(1,-6) -- Get the rest of the name, which will be the room name
                  sTemp = otherdevices_svalues[i] -- get the temperature   
                sLastUpdateTime = otherdevices_lastupdate[i]
                sElapsedTime = TimeElapsed(otherdevices_lastupdate[i])               
                message = RoomName.." temperature is " .. sTemp .. " Centigrade "  -- for debug
                message2 = RoomName .. " last seen " .. sLastUpdateTime  ..  " Elapsed time " .. sElapsedTime
                if printData == true then
                  print(message)
                  print(message2)
                end
 
                -- get the lowest temperature of the thermostats
                if tonumber(sTemp) < TempMin then
                  TempMin = tonumber(sTemp)
                end
 
                -- check for missing devices
                if sElapsedTime > MissingDevicesTime then
                SendAnEmail = true                      -- change this to false if you do not require emails to be sent
                MissingDeviceCount = MissingDeviceCount + 1
                end
 
            end   
 
      end
 
        if printData == true then
         print (" ")
         print ("Number of valves open more than " .. BoilerOnPercent .. "% is " .. ValveCount .." valves")
         print("Highest valve open value is " .. PercentMax .." percent ")
         print("Lowest thermostat reading is " .. TempMin .." Centigrade ")
         print (" ")
        end
 
    if printData == true then
      if (otherdevices[BoilerSwitch] == 'On')then
         print ("Current state - Boiler is ON ")
      else
         print ("Current state - Boiler is OFF ")
      end   
 
   end         
 
   -- Check the elapsed time and email if overdue
      if (SendAnEmail == true) then
         print (" ")
         print("Heating Script: missing device email sent to " .. (email) );
         notifyString = os.date("Domoticz Alert # The current time is %X on %A Lost contact with " .. MissingDeviceCount .. " Heating script devices, check if Max!Buddy is running.  #") .. (email)
         commandArray['SendEmail'] = notifyString
       end
 
 
 
 
 
      -- Perform logic
 
      if printDebug == true then
      -- view the settings to understand logic performance
         print ("PercentMax (" .. PercentMax .. "%) " .. "Boiler On value (" .. BoilerOnPercent .. "%) " .. "Boiler Off value (" .. (BoilerOnPercent - HysterysisOffPercent) .. ")% ")
         print ("Number of valves open more than " .. BoilerOnPercent .. "% is " .. ValveCount .." valves. Minimum valves setting " .. MinValves )
         print ("Maximum open value " .. PercentMax .. "%" .. " Override value " .. ValvePercentOveride .."%")
 
      print (" ")
       end   
      if (otherdevices[HolidaySwitch] == 'Off')then -- Not on holiday
 
   if (otherdevices[HeatingSwitch] == 'On')then -- It's time to heat the house
 
            if (otherdevices[BoilerSwitch] == 'Off') then --If a minimum of 'MinValves' valves are on by more that pre-set value BoilerOnPercent
 
                     if printDebug == true then
                     print ("Test passed - Boiler is OFF ")
                     end   
 
               if (PercentMax > BoilerOnPercent) then
 
                     if printDebug == true then
                     print ("Test passed - Radiators are open beyond the threshold ")
                     end               
 
                  if (ValveCount >= MinValves) or (BoilerOnPercent >= ValvePercentOveride) then
 
                     if printDebug == true then
                     print ("Test passed - Either multiple valves are open or override count is reached ")
                     end   
 
                  commandArray[BoilerSwitch]='On' -- turn on boiler
                        if printData == true then
                        print ("Command sent - Turn ON Boiler ")
                        end
      end     
               end
            end
         end
 
         if (PercentMax < (BoilerOnPercent - HysterysisOffPercent) or (ValveCount < MinValves)) and (otherdevices[BoilerSwitch] == 'On')  then -- If the number of valves open more than BoilerOnPercent minus HysterysisOffPercent
            commandArray[BoilerSwitch]='Off' -- turn off boiler
                  if printData == true then
                     print ("Command sent - Turn OFF Boiler ")
                  end   
         end
 
      else -- on holiday
 
         if (TempMin <= HolidayMinTemp) and (otherdevices[BoilerSwitch] == 'Off') then  -- house is very cold
            commandArray[BoilerSwitch]='On' -- turn on boiler
         end
 
         if (TempMin >= (HolidayMinTemp + HolidayHysterysisTemp)) and (otherdevices[BoilerSwitch] == 'On') then  -- house is warm enough
            commandArray[BoilerSwitch]='Off' -- turn on boiler
         end
 
      end
    if printData == true then
      print (" ")
    end
 
 
return commandArray

CREDITS

None of this would exist without the hard work of a group of people pulling together with a common cause and they are, in no particular order and not exclusively

  • The Domoticz Devs
  • Westcott
  • l0gic
  • mvzut
  • All the guys that tested
  • Everyone who asked an intelligent question
  • Anyone I forgot!