Tue June 11th 2019
I was not happy with the python plugin for Toon toonapilib4domoticz. Could not get it to work properly, and was using unnecessary resources etc ..
I was also missing some of the functionality of Home Assistant but dropped back to Domoticz since this was going to take too much work to convert to .. and again, it needed a lot of resources to get everything setup (more than my rpi2 which is plenty plenty for Domoticz)
So I started out to see if I could get the Toon api integrated in Domoticz via. Et Voila, it is working! Currently in testing fase.
Implemented features:
status
call, so light on calls and resources for local and ToonNot Implemented (I don’t have those devices):
-- Toon for domoticz via dzVents
-- https://de-neef.net/notes/#toon-for-domoticz-via-dzvents
-- add domoticz forum url later
-- 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 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, 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_INFO -- 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 *:4', 'at *:9', 'at *:14', 'at *:19', 'at *:24', 'at *:29', 'at *:34',
'at *:39', 'at *:44', 'at *:49', 'at *:54', 'at *:59' },
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
}