dzVents script Toon for domoticz  [Solved]

Easy to use, 100% Lua-based event scripting framework.

Moderator: leecollings

Post Reply
nf999
Posts: 15
Joined: Friday 06 March 2015 16:51
Target OS: Raspberry Pi / ODroid
Domoticz version: stable
Location: Nederland
Contact:

dzVents script Toon for domoticz  [Solved]

Post by nf999 »

Note: This is for a regular paid Toon & Eneco subscription, won't work for a hacked Toon.

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
}
Last edited by nf999 on Thursday 27 June 2019 11:00, edited 1 time in total.
passion75
Posts: 2
Joined: Monday 09 October 2017 14:01
Target OS: Windows
Domoticz version:
Contact:

Re: dzVents script Toon for domoticz

Post by passion75 »

I am a newbie in scripting....i’m having a Windows install.. do I need some extra things to install...

Is there a Manual of it


Regards
User avatar
waaren
Posts: 6028
Joined: Tuesday 03 January 2017 14:18
Target OS: Linux
Domoticz version: Beta
Location: Netherlands
Contact:

Re: dzVents script Toon for domoticz

Post by waaren »

passion75 wrote: Wednesday 26 June 2019 21:31 I am a newbie in scripting....i’m having a Windows install.. do I need some extra things to install...
Is there a Manual of it
No need to install anything extra if you use dzVents, Lua or Blockly. For Python or PHP some additional software needs to be installed.

dzVents is documented in this wiki

When not yet familiar with dzVents please start with reading Get started before implementing. Special attention please for
"In Domoticz go to Setup > Settings > Other and in the section EventSystem make sure the checkbox 'dzVents disabled' is not checked. Also make sure that in the Security section in the settings you allow 127.0.0.1 to not need a password. dzVents uses that port to send certain commands to Domoticz. Finally make sure you have set your current location in Setup > Settings > System > Location, otherwise there is no way to determine nighttime/daytime state."
Debian buster, bullseye on RPI-4, Intel NUC.
dz Beta, Z-Wave, RFLink, RFXtrx433e, P1, Youless, Hue, Yeelight, Xiaomi, MQTT
==>> dzVents wiki
nf999
Posts: 15
Joined: Friday 06 March 2015 16:51
Target OS: Raspberry Pi / ODroid
Domoticz version: stable
Location: Nederland
Contact:

Re: dzVents script Toon for domoticz

Post by nf999 »

yep, what waaren says. I prefer to not have to install all kinds of extra stuff ;-) So this script should be working out of the box with domoticz with 1 exception that curl is required, since not all options for http calls are covered by dzVents.openUrl

fyi I use curl to avoid redirects (dzVents.openURL follows redirects automatically) and to PUT a call (PUT method does not work in domoticz) Especially the redirect avoidance is critical to be able to login.
LoudRex
Posts: 1
Joined: Wednesday 20 January 2021 4:00
Target OS: Linux
Domoticz version:
Contact:

Error dzVents with Toon code

Post by LoudRex »

Hello all,

I am having trouble using domoticz and connecting and retreiving data using a non-Rooted Toon with Eneco-subscription.
I've imported this code and replaced it with most of my login settings (now anonamized).

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="****"
local        client_secret="****"

-- enter Eneco credentials below (if not Eneco, you need to replace eneco below)
local        username="***"
local        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
}
Everything is working fine (except for the counters) but in the Domoticz log, everything looks ok.
The only error in the log I get from this script is:

Code: Select all

2021-01-20 04:03:01.053 Error: dzVents: Error: (3.0.2) An error occurred when calling event handler Toon via dzVents
2021-01-20 04:03:01.054 Error: dzVents: Error: (3.0.2) ...g/scripts/dzVents/generated_scripts/Toon via dzVents.lua:94: attempt to concatenate a nil value (local 'code')
As I am not a great scripter yet, I do not know how to solve this error line and let the counters do their job.
Because the gasUsage and powerUsage counters are still after a few minutes on 0 watt and 0 cubic metre...

I hope I posted my question right in this category.
Many thanks for helping me out.

Kind Regards,
LoudRex

PS. I have these two on my devices tab, don't know if they are correctly set-up...
Image
pdejong1977
Posts: 2
Joined: Tuesday 06 April 2021 20:45
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: dzVents script Toon for domoticz

Post by pdejong1977 »

I have the same problem as LoudRex. Until November 2020 the script worked fine but since then I get the same error and no Toon values in my Domoticz. Does anyone have an idea for the solution?
janz1961
Posts: 12
Joined: Sunday 18 March 2018 20:58
Target OS: Windows
Domoticz version:
Contact:

Re: dzVents script Toon for domoticz

Post by janz1961 »

I haven't done much debugging yet, but just to keep the thread alive: I'm also getting al lot of:

Code: Select all

2022-01-03 19:33:00.627 Error: dzVents: Error: (3.1.7) An error occurred when calling event handler TOON
2022-01-03 19:33:00.627 Error: dzVents: Error: (3.1.7) ...x86)\Domoticz\scripts\dzVents\generated_scripts/TOON.lua:94: attempt to concatenate a nil value (local 'code')
What I'm curious about is how it works with the callback URL. This seems to indicate that Domoticz needs to be accessible from the Internet. Now in my case it is, but it requires authentication. And if I look in my firewall logs, I see

Code: Select all

2022:01:03-19:18:40 sophos httpd[7106]: [url_hardening:error] [pid 7106:tid 4071074672] [client 192.241.211.78:45032] Hostname in HTTP request (My IP Address) does not match the server name (<my FQDN>)
Obviously, it is the FQDN that I specified for the TOON API, not the IP
pdejong1977
Posts: 2
Joined: Tuesday 06 April 2021 20:45
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: dzVents script Toon for domoticz

Post by pdejong1977 »

Does someone have an idea for a solution for this?
Post Reply

Who is online

Users browsing this forum: Bing [Bot] and 1 guest