I created a dzVents script for Toon. Just sharing here. It's the basic setup pulling the status info (p1 info, gasusage and thermostatinfo) from the toon api, and adding functionality to switch on/off autoprogram or scene. Should be enough for most Toon users.
-- api calls based on toonapilib & Toon api documentation
-- Toon api credentials required via 'https://developer.toon.eu'
-- only using Toon's 1st agreement, fine if you have 1 Toon
-- only thermostat, p1 meter and gass supported
-- no hue, smokedetectors or others (I don't have those devices

Create the required devices as in the dzVents code below, add this script in the setup -> more options -> events section, add Toon api credentials and Eneco login and it should work.
Currently testing, let me know if you have strange results
I'm not connected to either Toon or Eneco, but please spread the timing: in the timer section, to avoid all calls end up at the same time at the Toon api so we can keep using this functionality. Note that in order to avoid gaps you need to pick minutes *:1 to *:4 so nu *:0 or *:5. It will work but will give gaps in the power usage chart.
Code: Select all
-- Toon for domoticz via dzVents
-- https://www.domoticz.com/forum/viewtopic.php?f=59&t=28498
-- 2019-06-10 intial setup, authentication and get status
-- 2019-06-11 added update for programState and scenes
-- 2019-06-15 added extra error handling and logging
-- api calls based on toonapilib & Toon api documentation
-- Toon api credentials required via 'https://developer.toon.eu'
-- only using Toons 1st agreement, fine if you have 1 Toon
-- only thermostat, p1 meter and gass supported
-- no hue, smokedetectors or others
-- create Toon api account at https://developer.toon.eu
local client_id="enter your api.toon.eu client_id"
local client_secret="enter your api.toon.eu client_secret"
-- enter Eneco credentials below (if not Eneco, you need to replace eneco below)
local username="enter your Eneco username"
local password="enter your Eneco password"
-- create devices in domoticz
-- create new hardware dummy device TOON and add virtual devices from there
-- at start the powerUsage will spike; select the spike click shift-leftmouse
-- to delete the spike ;-)
local toonThermostat = "Toon thermostat" -- type=Thermostat - SetPoint
local toonTemperature = "Toon temperature" -- type=Temp - LacrosseTX3
local toonScenes = "Toon scenes" -- type=Light/switch - selector switch
local toonProgram = "Toon program" -- type=Light/switch - selector switch
local toonModulation = "Toon modulation level" -- type=percentage
local toonBurnerInfo = "Toon burner info" -- type=Light/switch - selector switch
local toonBoilerError = "Toon boiler state" -- type=text
local toonNextProgram = "Toon next program" -- type=text
local toonGasUsage = "Toon gas" -- type=gas
local toonSmartMeter = "Toon smart meter" -- type=p1 smart meter
local scenesText = { 'Comfort', 'Home', 'Sleep', 'Away', 'Holiday' }
local burnerInfo = { 'Burner off', 'Burner on', 'Hot Water', 'Preheat' }
return {
logging = {
-- level = domoticz.LOG_DEBUG -- comment when not in dev
},
on = {
-- comment timer and create testButton for development
-- timer manually specified to avoid 5min cutoffs (0) in charts
-- and add spreading, use *:1 to *:4 as starting point
timer = { 'at *:3', 'at *:8', 'at *:13', 'at *:18', 'at *:23', 'at *:28', 'at *:33',
'at *:38', 'at *:43', 'at *:48', 'at *:53', 'at *:58' },
devices = { toonProgram, toonScenes }, -- toonThermostat removed, no updateSetPoint().silent()
httpResponses = { 'toon_getToken', 'toon_getAgreements', 'toon_getStatus', 'toon_setThermostat' },
},
data = {
'access_token',
'access_token_timestamp',
'access_token_expires_in',
'refresh_token',
'agreementId',
'displayCommonName',
'action', 'actionData'
},
execute = function(dz, item)
-- execute command to get site
function os.capture(cmd)
local s = ""
local f = assert(io.popen(cmd, 'r'))
s = assert(f:read('*a'))
f:close()
return s
end
function get_access_token()
dz.log('in get_access_token', dz.LOG_DEBUG)
-- get code, using curl because of the redirect
local command = "curl --max-time 5 -i -X POST " ..
"-H 'Host: api.toon.eu' " ..
"-H 'Content-Type: application/x-www-form-urlencoded' " ..
"-d 'username=" .. username .. "&password=" .. password ..
"&tenant_id=eneco&response_type=code&client_id=" .. client_id ..
"' 'https://api.toon.eu/authorize/legacy'"
local tmp = os.capture(command)
local code=string.match(tmp, '?code=(.*)&scope')
-- get tokens
dz.openURL({
url = 'https://api.toon.eu/token',
method = 'POST',
callback = 'toon_getToken',
headers = { ['Content-Type']='application/x-www-form-urlencoded' },
postData = "client_id=" .. client_id ..
"&client_secret=" .. client_secret ..
"&code=" .. code .. "&grant_type=authorization_code"
})
end
function refresh_token()
dz.log('in refresh_token',dz.LOG_DEBUG)
dz.openURL({
url = 'https://api.toon.eu/token',
method = 'POST',
callback = 'toon_getToken',
headers = { ['Content-Type']='application/x-www-form-urlencoded' },
postData = "client_id=" .. client_id ..
"&client_secret=" .. client_secret ..
"&refresh_token=" .. dz.data.refresh_token .. "&grant_type=refresh_token"
})
end
function get_agreements()
dz.log('in get_agreements', dz.LOG_DEBUG)
dz.openURL({
url='https://api.toon.eu/toon/v3/agreements',
method='GET',
callback='toon_getAgreements',
headers = {
Authorization = 'Bearer ' .. dz.data.access_token,
['Content-Type'] = 'application/json',
['Cache-Control'] = 'no-cache'
}
})
end
function get_status()
dz.log('in get_status', dz.LOG_DEBUG)
dz.openURL({
url='https://api.toon.eu/toon/v3/' .. dz.data.agreementId .. '/status',
method='GET',
callback='toon_getStatus',
headers = {
Authorization = 'Bearer ' .. dz.data.access_token,
['Content-Type'] = 'application/json',
['Cache-Control'] = 'no-cache',
['X-Agreement-ID'] = dz.data.agreementId,
['X-Common-Name'] = dz.data.displayCommonName
}
})
end
function set_thermostat(pd)
dz.log('in set_thermostat: ' .. dz.utils.toJSON(pd), dz.LOG_DEBUG)
-- adjust json
if (pd.programState) then
pd['activeState'] = dz.utils.round(dz.devices(toonScenes).level/10)
pd['currentSetpoint'] = 0 -- not relevant
elseif (pd.activeState) then
pd['programState'] = 2
pd['currentSetpoint'] = 0 -- not relevant
dz.devices(toonProgram).switchSelector(20).silent()
elseif (pd.currentSetpoint) then
pd['programState'] = 2
pd['activeState'] = -1
else
-- something strange
dz.log('invalid input for set_thermostat', dz.LOG_ERROR)
return
end
dz.log('set_thermostat json: ' .. dz.utils.toJSON(pd), dz.LOG_DEBUG)
-- removed, no working put in domoticz??
-- dz.openURL({
-- url = 'https://api.toon.eu/toon/v3/' .. dz.data.agreementId .. '/thermostat',
-- method = 'POST',
-- callback = 'toon_setThermostat',
-- postData = pd,
-- headers = {
-- Authorization = 'Bearer ' .. dz.data.access_token,
-- ['Content-Type'] = 'application/json',
-- ['Cache-Control'] = 'no-cache',
-- ['X-Agreement-ID'] = dz.data.agreementId,
-- ['X-Common-Name'] = dz.data.displayCommonName
-- }
-- })
local command = "curl --max-time 5 -i -s -X PUT " ..
"-H 'Host: api.toon.eu' " ..
"-H 'Authorization: Bearer " .. dz.data.access_token .. "' " ..
"-H 'Content-Type: application/json' " ..
"-H 'Cache-Control: no-cache' " ..
"-H 'X-Agreement-ID: " .. dz.data.agreementId .. "' " ..
"-H 'X-Common-Name: " .. dz.data.displayCommonName .. "' " ..
"-d '" .. dz.utils.toJSON(pd) .. "' " ..
"'https://api.toon.eu/toon/v3/" .. dz.data.agreementId .. "/thermostat'"
dz.log('command: ' .. command, dz.LOG_DEBUG)
local tmp = os.capture(command)
dz.log(tmp, dz.LOG_DEBUG)
local status=string.match(tmp, "HTTP/1.1 (.-)\r\n")
if (status ~= "200 ") then -- don't forget the space
dz.log('set_thermostat failed: [' .. status .. ']', dz.LOG_ERROR)
else
dz.log('set_thermostat success: [' .. status .. ']', dz.LOG_INFO)
end
end
function next_action()
dz.log('in next_action', dz.LOG_DEBUG)
if (dz.data.action == "get_status") then
get_status()
elseif (dz.data.action == "set_thermostat") then
set_thermostat(dz.data.actionData)
else
dz.log('next_action missing: ' .. dz.data.action, dz.LOG_ERROR)
end
end
function get_thermostatInfo(ti)
-- ti == item.json.thermostatInfo
local t1, t2
-- toon temp
t1 = ti.currentDisplayTemp/100
t2 = dz.devices(toonTemperature).temperature
if ( t1 ~= t2) then
dz.devices(toonTemperature).updateTemperature(t1)
end
-- toon setpoint
t1 = ti.currentSetpoint/100
-- t2 = dz.devices(toonThermostat).setPoint
-- if (t1 ~= t2) then
dz.log('updating toonThermostat setPoint', dz.LOG_DEBUG)
dz.devices(toonThermostat).updateSetPoint(t1).silent()
-- end
-- toon programstate
t1 = ti.programState*10
t2 = dz.devices(toonProgram).level
if (t1 ~= t2) then
dz.devices(toonProgram).switchSelector(t1).silent()
end
-- toon scene
t1 = ti.activeState*10
t2 = dz.devices(toonScenes).level
if (t1 ~= t2) then
dz.devices(toonScenes).switchSelector(t1).silent()
end
-- toon modulation level
t1 = ti.currentModulationLevel
t2 = dz.devices(toonModulation).percentage
if (t1 ~= t2) then
dz.devices(toonModulation).updatePercentage(t1)
end
-- toon burnerinfo
t1 = ti.burnerInfo*10
t2 = dz.devices(toonBurnerInfo).level
if (t1 ~= t2) then
dz.devices(toonBurnerInfo).switchSelector(t1)
end
-- boiler errors
t1 = "no errors"
t2 = dz.devices(toonBoilerError).text
if (ti.hasBoilerFault ~= 0) then
t1 = 'error: ' .. tostring(item.json.thermostatInfo.errorFound)
end
if (t1 ~= t2) then
dz.devices(toonBoilerError).updateText(t1)
dz.notify('Boiler error', t1, dz.PRIORITY_HIGH, dz.SOUND_FALLING)
end
-- nextProgram
t1 = "Toon autoprogram not active"
t2 = dz.devices(toonNextProgram).text
if (ti.nextProgram ~= -1) then
local d = os.date("%H:%M", item.json.thermostatInfo.nextTime)
t1 = 'at ' .. d .. ' set to ' .. scenesText[tonumber(item.json.thermostatInfo.nextState+1)]
.. ' ' .. item.json.thermostatInfo.nextSetpoint/100 .. '°C'
end
if (t1 ~= t2) then
dz.devices(toonNextProgram).updateText(t1)
end
end
-- control script
local Time = require('Time')
local now = Time()
if (item.isTimer or item.isDevice) then
-- set action from Device
if (item.isTimer) then
dz.log('Timer event was triggered by ' .. item.trigger, dz.LOG_INFO)
dz.data.action="get_status"
dz.data.actionData=nil
elseif (item.isDevice) then
dz.log('Device event was triggered by ' .. item.name, dz.LOG_INFO)
if(item.name == toonProgram) then
dz.log('update from toonProgram device', dz.LOG_INFO)
dz.data.action = "set_thermostat"
dz.data.actionData = { programState = math.floor(item.level/10) }
elseif (item.name == toonScenes) then
dz.log('update from toonScenes device', dz.LOG_INFO)
dz.data.action = "set_thermostat"
dz.data.actionData = { activeState = math.floor(item.level/10) }
elseif (item.name == toonThermostat) then
dz.log('update from toonThermostat device', dz.LOG_INFO)
dz.data.action = "set_thermostat"
dz.data.actionData = { currentSetpoint = item.setPoint*100 }
end
else
dz.log('update unknown', dz.LOG_ERROR)
return
end
-- debug info
if (dz.data.access_token ~= nil) then
dz.log('access_token: ' .. dz.data.access_token, dz.LOG_DEBUG)
end
if (dz.data.access_token_timestamp ~= nil) then
dz.log('access_token_timestamp: ' .. tostring(dz.data.access_token_timestamp.dDate), dz.LOG_DEBUG)
dz.log('deltaT: ' .. tostring(now.compare(dz.data.access_token_timestamp).secs), dz.LOG_DEBUG)
dz.log('access_token_expires_in: ' .. dz.data.access_token_expires_in, dz.LOG_DEBUG)
dz.log('result: ' .. tostring(now.compare(dz.data.access_token_timestamp).secs>tonumber(dz.data.access_token_expires_in)), dz.LOG_DEBUG)
end
if (dz.data.refresh_token ~= nil) then
dz.log('refresh_token: ' .. dz.data.refresh_token, dz.LOG_DEBUG)
end
-- poll toon data
-- compare timestamps
if (dz.data.access_token == nil or
dz.data.access_token_timestamp == nil or
now.compare(dz.data.access_token_timestamp).secs>tonumber(dz.data.access_token_expires_in)) then
dz.log('need to get new access_token', dz.LOG_INFO)
if (dz.data.refresh_token ~= nil) then
dz.log('getting new access_token via refresh_token', dz.LOG_DEBUG)
refresh_token()
else
dz.log('getting new accesst_token via get_access_token', dz.LOG_DEBUG)
get_access_token()
end
elseif (dz.data.agreementId == nil or dz.data.displayCommonName == nil) then
dz.log('need to get agreements', dz.LOG_INFO)
get_agreements()
else
dz.log('all available, next_action', dz.LOG_DEBUG)
next_action()
end
elseif (item.isHTTPResponse) then
-- debug info
dz.log('HTTPResponse event was triggered by ' .. item.trigger, dz.LOG_INFO)
-- if no proper response report and stop
if (not item.ok or item.statusCode ~= 200) then
dz.log('response NOK, bailing out; ' .. tostring(item.statusCode)
.. ": " .. item.statusText, dz.LOG_ERROR)
dz.log('responseOK: ' .. tostring(item.ok), dz.LOG_DEBUG)
dz.log('statusCode: ' .. item.statusCode, dz.LOG_DEBUG)
dz.log('statusText: ' .. item.statusText, dz.LOG_DEBUG)
if (item.isJSON) then
dz.log('json data:', dz.LOG_DEBUG)
dz.log(dz.utils.toJSON(item.json), dz.LOG_DEBUG)
elseif (item.data) then
dz.log('non-json data:', dz.LOG_DEBUG)
dz.log(item.data, dz.LOG_DEBUG)
end
return
end
-- handle async http response
if (item.trigger == 'toon_setThermostat') then
-- nothing here
elseif (item.trigger == 'toon_getToken'
and item.json.access_token) then
dz.data.access_token = item.json.access_token
dz.data.access_token_expires_in = item.json.expires_in
dz.data.access_token_timestamp = now
dz.data.refresh_token = item.json.refresh_token
dz.log('access_token: ' .. dz.data.access_token, dz.LOG_DEBUG)
dz.log('refresh_token: ' .. dz.data.refresh_token, dz.LOG_DEBUG)
-- skip getting agreementId if already available
if (dz.data.agreementId == nil or dz.data.displayCommonName == nil) then
dz.log('need to get agreements', dz.LOG_INFO)
get_agreements()
else
next_action()
end
elseif (item.trigger == "toon_getAgreements" and
item.json[1].agreementId) then
dz.data.agreementId = item.json[1].agreementId
dz.data.displayCommonName = item.json[1].displayCommonName
dz.log('agreementId:' .. dz.data.agreementId, dz.LOG_DEBUG)
dz.log('displayCommonName: ' .. dz.data.displayCommonName, dz.LOG_DEBUG)
next_action()
elseif (item.trigger == "toon_getStatus") then
dz.log('callback get_status', dz.LOG_DEBUG)
-- thermostatInfo
if (item.json.thermostatInfo) then
get_thermostatInfo(item.json.thermostatInfo)
else
dz.log('thermostatInfo missing in response', dz.LOG_ERROR)
dz.log(dz.utils.toJSON(item.json), dz.LOG_INFO)
end
-- gasUsage
if (item.json.gasUsage) then
dz.devices(toonGasUsage).updateGas(item.json.gasUsage.dayUsage)
else
dz.log('gasUsage missing in response', dz.LOG_ERROR)
dz.log(dz.utils.toJSON(item.json), dz.LOG_INFO)
end
-- powerUsage
if (item.json.powerUsage) then
dz.log('p1: ' .. item.json.powerUsage.meterReadingLow .. ', ' ..
item.json.powerUsage.meterReading .. ', ' ..
item.json.powerUsage.meterReadingLowProdu .. ', ' ..
item.json.powerUsage.meterReadingProdu .. ', ' ..
item.json.powerUsage.value .. ', ' ..
item.json.powerUsage.valueProduced, dz.LOG_INFO)
dz.devices(toonSmartMeter).updateP1(
item.json.powerUsage.meterReadingLow,
item.json.powerUsage.meterReading, item.json.powerUsage.meterReadingLowProdu,
item.json.powerUsage.meterReadingProdu, item.json.powerUsage.value,
item.json.powerUsage.valueProduced)
else
dz.log('powerUsage missing in response', dz.LOG_ERROR)
dz.log(dz.utils.toJSON(item.json), dz.LOG_INFO)
end
else
dz.log('unknwon trigger HTTPResponse: ' .. item.trigger, dz.LOG_ERROR)
end
end
end
}