Daikin Onecta
About
Daikin Onecta plugin allow you to control any Daikin device listed here: Daikin Onecta compatible units
note
You need to register Daikin developer account to be able to use this account
Installation guide
Creating account
- Go to Daikin Developer Portal and create an account/login there.
- Click on your username/email address at the top-right corner of the page and then go to My Apps page.
- Click on + New App button
- Fill fields with appropriate data:
- Application name - your app name, e.g. "Butler Integration"
- Auth Strategy - leave "Onecta OIDC"
- Redirect URI - enter
https://c-home.ua/code-confirmation.html
- Description - you can write anything there, or just leave it blank
- Press Create button
- Copy and save Client ID and Client Secret somewhere for later steps
- Press Proceed button
Getting authorization code
- Constuct an authorization URL
URL: https://idp.onecta.daikineurope.com/v1/oidc/authorize?response_type=code&client_id={client_id}&redirect_uri=https%3A%2F%2Fc-home.ua%2Fcode-confirmation.html&scope=openid%20onecta:basic.integration
Open Link- Open generated link and login to your daikin account there
- When consent page appears, click I Agree button
- You will be redirected to the page displaying the authorization code, copy it and save somewhere for the next step
Plugin installation
Follow generic installation instructions until you paste, plugin code in text area. Do not press Save button yet.
You need to replace the following constants in the plugin code:
- D_CLIENT_ID - with Client ID
- D_CLIENT_SECRET - with Client Secret
- D_AUTH_CODE - with Authorization code
so it looks like this but with your values:
local D_CLIENT_ID = "AbCdefgHiJkLMNopQrStuvwX"
local D_CLIENT_SECRET = "ABC_DEFghijklmnopqrstuvwxyz0123456789abcdefGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklm"
local D_AUTH_CODE =
"st2.s.ABCDEFGHIJ.KLMNOPQRSTUVWXYZ0123456789LoremipsumdolorsitametconsecteturadipiscingelitNullaacsapie-nsitam_etleofe-ugiat-m-attissitametquisera.tDonecvitaegravidaleoEtiameuhendreritrisusvelportaipsumDonecfringillapre-tiumesteuelem.sc3"
Continue on with generic installation instructions
Source code
Plugin source code
-- TODO: Add outdoor temperature sensor
local https = require('https')
local http = require('http')
local json = require('json')
local timer = require('timer')
local querystring = require('querystring')
local D_CLIENT_ID = ""
local D_CLIENT_SECRET = ""
local D_AUTH_CODE =
""
--[[============================================]]
--[[ ]]
--[[ NO NEED TO CHANGE ANYTHING BELOW THIS LINE ]]
--[[ ]]
--[[============================================]]
local D_ACCESS_TOKEN =
""
local D_ID_TOKEN =
""
local D_REFRESH_TOKEN =
""
local REQUEST_DEBUG = false
-- https://developer.cloud.daikineurope.com/spec/b0dffcaa-7b51-428a-bdff-a7c8a64195c0/70b10aca-1b4c-470b-907d-56879784ea9c#/info/get_info
local ONECTA_BASE_URL = "https://api.onecta.daikineurope.com/v1"
local ONECTA_INFO_URL = ONECTA_BASE_URL .. "/info"
local ONECTA_DEVICES_URL = ONECTA_BASE_URL .. "/gateway-devices"
local ONECTA_REDIRECT_URL = "https://c-home.ua/code-confirmation.html"
--[[ Log print wrapper ]]
local original_print = print
---
--- @brief Define the new print function with the prefix
--- @param ... any Arguments to print
function print(...)
-- Create a table to hold the new arguments
local new_args = { "[Daikin]" }
-- Add the original arguments to the new arguments table
for i = 1, select("#", ...) do
new_args[#new_args + 1] = select(i, ...)
end
-- Call the original print function with the new arguments
original_print(table.unpack(new_args))
end
local plugin = {}
plugin.name = "Daikin"
plugin.version = { 0, 0, 1 }
plugin.provided_interfaces = {
DaikinDevice = {
actions = {
},
parameters = {
identity = {
type = "string",
value = "123445678-90AB-CDEF-1234-567890ABCDEF"
},
version = {
type = "string",
value = "1_0_0"
},
manufacturer = {
type = "string",
value = "Daikin",
},
model = {
type = "string",
value = "Altherma"
},
name = {
type = "string",
value = "Some Name"
},
serial = {
type = "string",
value = "1234567890"
},
mac = {
type = "string",
value = "12:34:56:78:90:AF"
}
}
}
}
plugin.overridden_interfaces = {
ThermostatMode = {
actions = {
setMode = {}
}
},
ThermostatSetpoint = {
actions = {
setSetpoint = {}
}
},
}
plugin.devices = {
HVAC = {
type = "DevThermostat",
role = "ClimateThermostat",
icon = "ch-lightbulb",
interfaces = { "DaikinDevice", "ThermostatMode", "ThermostatOperatingState", "ThermostatSetpoint", "SensorMultilevel" },
parameters = {
identity = {
type = "string",
value = "123445678-90AB-CDEF-1234-567890ABCDEF",
role = "edit",
locale = {
en = "Daikin device ID",
ua = "Ідентифікатор пристрою Daikin",
ru = "Идентификатор устройства Daikin"
}
},
version = {
type = "string",
value = "1_0_0",
role = "show",
locale = {
en = "Device version",
ua = "Версія пристрою",
ru = "Версия устройства"
}
},
manufacturer = {
type = "string",
value = "Daikin",
role = "show",
locale = {
en = "Device manufacturer",
ua = "Виробник пристрою",
ru = "Производитель устройства"
}
},
model = {
type = "string",
value = "Altherma",
role = "show",
locale = {
en = "Device model",
ua = "Модель пристрою",
ru = "Модель устройства"
}
},
name = {
type = "string",
value = "Some Name",
role = "show",
locale = {
en = "Device name",
ua = "Ім'я пристрою",
ru = "Имя устройства"
}
},
serial = {
type = "string",
value = "1234567890",
role = "show",
locale = {
en = "Device serial",
ua = "Серійний номер",
ru = "Серийный номер"
}
},
mac = {
type = "string",
value = "12:34:56:78:90:AF",
role = "show",
locale = {
en = "Device MAC",
ua = "MAC адреса",
ru = "MAC адрес"
}
},
--[[ Thermostat Mode interface parameters ]] --
availableModes = {
type = "json",
value = { "Off", "Auto", "Dry", "Heat", "Fan", "Cool" }
},
currentMode = {
type = "string",
value = "Off"
},
autoModeIsActive = {
type = "boolean",
value = false
},
--[[ Thermostat Setpoint interface parameter ]] --
setpointCapabilities = {
type = "json",
value = {
Heat = {
minimum = 10,
maximum = 31,
precision = 0.5,
scale = 0,
size = 4, -- N/A
},
Cool = {
minimum = 18,
maximum = 33,
precision = 0.5,
scale = 0,
size = 4, -- N/A
},
Auto = {
minimum = 18,
maximum = 30,
precision = 0.5,
scale = 0,
size = 4, -- N/A
}
}
},
currentSetpoints = {
type = "json",
value = {
Cool = 25,
Heat = 25,
Auto = 25
}
},
step = {
type = "double",
value = 0.5,
},
--[[ Thermostat Operation State interface parameters ]] --
currentOperationState = {
type = "string",
value = "Idle"
},
--[[ SensorMultilevel interface parameters ]] --
Value = {
type = "double",
value = 0
},
sensorType = {
type = "string",
value = "AirTemperature"
},
valueUnit = {
type = "string",
value = "°C"
},
valueScale = {
type = "string",
value = "Celcius",
}
}
}
}
plugin.access_data = {}
-- [[ internal fields ]] --
local discovery_cache = {}
local registered_devices = {}
local poll_timer = nil
local rate_limits = {
minute = { limit = 20, remaining = 20, reset = 0 },
day = { limit = 200, remaining = 200, reset = 0 }
}
local error_state = false
local request_queue = {}
local characteristic_updates = {}
-- [[ Util Functions ]] --
-- TODO: Add fanonly and dry modes to all conversion functions
--- @brief Convert Daikin mode to plugin mode
--- @param mode string Daikin mode
--- @return string Plugin mode
local function convert_daikin_mode_to_mode(mode)
if mode == "heating" then
return "Heat"
elseif mode == "cooling" then
return "Cool"
elseif mode == "auto" then
return "Auto"
end
end
--- @brief Convert Daikin mode to operating state
--- @param mode string Daikin mode
--- @return string Plugin operating state
local function convert_daikin_mode_to_state(mode)
if mode == "heating" then
return "Heating"
elseif mode == "cooling" then
return "Cooling"
elseif mode == "auto" then
return "Auto"
elseif mode == "off" then
return "Idle"
end
end
--- @brief Convert mode to daikin mode
--- @param mode string Mode
--- @return string Daikin mode
local function convert_mode_to_daikin_mode(mode)
if mode == "Heat" then
return "heating"
elseif mode == "Cool" then
return "cooling"
elseif mode == "Auto" then
return "auto"
end
end
--- @brief Convert operating state to daikin mode
--- @param state string Operating state
--- @return string Daikin mode
local function convert_state_to_daikin_mode(state)
if state == "Heating" then
return "heating"
elseif state == "Cooling" then
return "cooling"
elseif state == "Auto" then
return "auto"
elseif state == "Idle" then
return "off"
end
end
-- [[ API wrapper functions ]] --
---Sets the authentication tokens for the Daikin Cloud API
---@param access string Access token for API authentication
---@param refresh string Refresh token for getting new access tokens
---@param id string ID token containing user information
---@param save_to_db boolean|nil Should tokens be saved to DB
function plugin:daikin_cloud_set_tokens(access, refresh, id, save_to_db)
self.access_data.access_token = access
self.access_data.refresh_token = refresh
self.access_data.id_token = id
if save_to_db then
self:kv_db_update_records({
access_token = access,
refresh_token = refresh,
id_token = id
})
end
end
---Makes an HTTP request to the Daikin Cloud API with automatic token refresh and rate limiting
---@param method string HTTP method (GET, POST, PATCH etc)
---@param url string Full URL for the request
---@param body table|string|nil Request body (will be JSON encoded if table)
---@param content_type string|nil Content-Type header value
---@param with_auth boolean|nil Whether to include Authorization header
---@param callback function|nil Callback function(error, response, status_code)
---@param retry_count number|nil Number of retry attempts made
function plugin:daikin_cloud_make_request(method, url, body, content_type, with_auth, callback, retry_count)
retry_count = retry_count or 0
local options = http.parseUrl(url)
options.method = method
local payload = nil
if body then
payload = type(body) == 'string' and body or json.encode(body)
options.headers = {
['Content-Type'] = content_type or 'application/json',
['Content-Length'] = #payload
}
else
options.headers = {
['Content-Length'] = 0
}
end
if with_auth then
options.headers['Authorization'] = 'Bearer ' .. self.access_data.access_token
end
if REQUEST_DEBUG then
print("Request: " .. method .. " " .. url)
print("Headers:")
for key, value in pairs(options.headers) do
print(key, value)
end
if payload then
print("Body: " .. payload)
end
end
local req = https.request(options, function(res)
local data = ''
res:on('data', function(chunk)
data = data .. chunk
end)
res:on('end', function()
if REQUEST_DEBUG then
print("Status: ", res.statusCode)
print("Response:")
print(data)
end
local function update_rate_limits()
rate_limits.minute.limit = tonumber(res.headers['x-ratelimit-limit-minute']) or rate_limits.minute.limit
rate_limits.minute.remaining = tonumber(res.headers['x-ratelimit-remaining-minute']) or
rate_limits.minute.remaining
rate_limits.day.limit = tonumber(res.headers['x-ratelimit-limit-day']) or rate_limits.day.limit
rate_limits.day.remaining = tonumber(res.headers['x-ratelimit-remaining-day']) or
rate_limits.day.remaining
rate_limits.minute.reset = tonumber(res.headers['ratelimit-reset']) or rate_limits.minute.reset
end
if res.statusCode == 401 and retry_count < 1 then
self:daikin_cloud_get_tokens('refresh_token', self.access_data.refresh_token, function(err)
if err then
if callback then
callback(err)
else
print('Error: ' .. err)
end
return
end
self:daikin_cloud_make_request(method, url, body, content_type, with_auth, callback,
retry_count + 1)
end)
return
end
if res.statusCode == 429 then
local retry_after = tonumber(res.headers['retry-after']) or 60
print('Rate limit exceeded, retrying after ' .. retry_after .. ' seconds')
timer.setTimeout(retry_after * 1000, function()
self:daikin_cloud_make_request(method, url, body, content_type, with_auth, callback, retry_count)
end)
return
end
update_rate_limits()
local response = data ~= '' and json.decode(data) or nil
if callback then
callback(nil, response, res.statusCode)
end
end)
end)
if payload then
req:write(payload)
end
req:done()
end
---Gets new access and refresh tokens from the Daikin Cloud API
---@param grant_type string Either 'authorization_code' or 'refresh_token'
---@param token string Authorization code or refresh token
---@param callback function|nil Callback function(error, response)
function plugin:daikin_cloud_get_tokens(grant_type, token, callback)
local params = {
grant_type = grant_type,
client_id = self.access_data.client_id,
client_secret = self.access_data.client_secret
}
if grant_type == 'authorization_code' then
params.code = token
params.redirect_uri = ONECTA_REDIRECT_URL
else
params.refresh_token = token
end
local params_str = querystring.stringify(params);
self:daikin_cloud_make_request(
'POST',
'https://idp.onecta.daikineurope.com/v1/oidc/token?' .. params_str,
nil,
nil,
false,
function(err, res)
if err then
if callback then
callback(err)
end
return
end
for key, value in pairs(res) do
print(key, value)
end
self:daikin_cloud_set_tokens(res.access_token, res.refresh_token, res.id_token, true)
if callback then
callback(nil, res)
end
end
)
end
---Gets list of all available Daikin devices
---@param callback function|nil Callback function(error, devices)
function plugin:daikin_cloud_get_devices(callback)
self:daikin_cloud_make_request(
'GET',
'https://api.onecta.daikineurope.com/v1/gateway-devices',
nil,
nil,
true,
callback
)
end
---Gets detailed information about a specific Daikin device
---@param device_id string Device identifier
---@param callback function|nil Callback function(error, device_info)
function plugin:daikin_cloud_get_device(device_id, callback)
self:daikin_cloud_make_request(
'GET',
string.format('https://api.onecta.daikineurope.com/v1/gateway-devices/%s', device_id),
nil,
nil,
true,
callback
)
end
---Updates a characteristic value for a specific device
---@param device_id string Device identifier
---@param embedded_id string Management point identifier
---@param name string Characteristic name
---@param path string|nil Characteristic path
---@param value any New value to set
---@param callback function|nil Callback function(error, response)
function plugin:daikin_cloud_set_characteristic(device_id, embedded_id, name, path, value, callback)
local url = string.format(
'https://api.onecta.daikineurope.com/v1/gateway-devices/%s/management-points/%s/characteristics/%s',
device_id,
embedded_id,
name
)
local body = {
value = value
}
if path then
body.path = path
end
self:daikin_cloud_make_request(
'PATCH',
url,
body,
nil,
true,
callback
)
end
---Gets the value of a specific characteristic from device data
---@param device table Device data structure
---@param embedded_id string Management point identifier
---@param name string Characteristic name
---@param field string|nil Field to get from characteristic (default: 'value')
---@return any|nil Value of the characteristic or nil if not found
function plugin:daikin_cloud_get_characteristic_value(device, embedded_id, name, field)
if not device or not device.managementPoints then
return nil
end
for _, point in ipairs(device.managementPoints) do
if point.embeddedId == embedded_id then
if point[name] and point[name].value ~= nil then
if field then
return point[name][field]
else
return point[name].value
end
end
break
end
end
return nil
end
--- @brief Update leaf value in table
--- @param tbl table Table to update
--- @param path string Path to the leaf
--- @param value any New value
local function update_leaf(tbl, path, value)
-- Split the path into segments
local segments = {}
for segment in string.gmatch(path, "([^/]+)") do
table.insert(segments, segment)
end
-- Traverse the table following the path
local current = tbl
for i = 1, #segments - 1 do
local segment = segments[i]
if current[segment] == nil or type(current[segment]) ~= "table" then
return false -- Path segment does not exist or is not a table
end
current = current[segment]
end
-- Update the leaf value
local leaf = segments[#segments]
if current[leaf] == nil then
return false -- Leaf does not exist
end
current[leaf] = value
return true
end
--- @brief Update characteristic value in device data cache
--- @param device table Device data
--- @param embedded_id string Management point identifier
--- @param name string Characteristic name
--- @param path string|nil path to the field to update in characteristic
--- @param value any New value
function plugin:daikin_cloud_update_characteristic_value(device, embedded_id, name, path, value)
if not device or not device.managementPoints then
return
end
for _, point in ipairs(device.managementPoints) do
if point.embeddedId == embedded_id then
if not point[name] then
point[name] = {}
end
if path then
if not update_leaf(point[name].value, path, value) then
print("Failed to update characteristic value")
end
else
point[name].value = value
end
break
end
end
end
--[[ Custom db table functions, mainly for persistant KV storage ]] --
--- @brief Check if table exists
---
--- @param tableName string Table name
--- @return boolean true if table exists, false otherwise
function plugin:db_has_table(tableName)
print("Checking table " .. tableName)
local query = string.format("SELECT name FROM sqlite_master WHERE type='table' AND name='%s';", tableName)
local result = self.db:rowexec(query);
if result then
return true
end
return false
end
--- @brief Create `tbl_custom_kv` table
function plugin:kv_db_create_tables()
local query = [[
CREATE TABLE IF NOT EXISTS tbl_custom_kv (
key TEXT PRIMARY KEY,
value TEXT
);
]]
self.db:exec(query)
end
--- @brief Initialize `tbl_custom_kv table
--- @param initialValues table initial values
function plugin:kv_db_initialize(initialValues)
for key, value in pairs(initialValues) do
self:kv_db_update_record(key, value)
end
end
--- @brief Load data from kv store
function plugin:kv_db_load()
local query = "SELECT key,value FROM tbl_custom_kv;"
local result = {}
local stmt = self.db:prepare(query)
local row = {};
while stmt:step(row) do
result[row[1]] = row[2]
end
return result
end
--- @brief Update one record in `tbl_custom_kv` table
--- @param key string Record name
--- @param value string Record value
function plugin:kv_db_update_record(key, value)
local query = string.format("INSERT OR REPLACE INTO tbl_custom_kv (key, value) VALUES ('%s', '%s');", key, value)
self.db:exec(query)
end
--- @brief Batch update access data records
--- @param updates table Table containing updates (k = key, v = value),
--- where both key and value ar strings
function plugin:kv_db_update_records(updates)
local query = "BEGIN TRANSACTION;"
for key, value in pairs(updates) do
query = query ..
string.format("INSERT OR REPLACE INTO tbl_custom_kv (key, value) VALUES ('%s', '%s');", key, value)
end
query = query .. "COMMIT;"
self.db:exec(query)
end
-- [[ Plugin lifecycle functions ]] --
--- Plugin initialization
--- @param reason string The reason for the initialization of the plugin
function plugin:init(reason)
print("Init:", reason)
local const_auth_code = D_AUTH_CODE or ''
if not self:db_has_table('tbl_custom_kv') then
print("Creating needed table(s)")
self:kv_db_create_tables()
print("Tables created")
self:kv_db_initialize({
authorization_code = const_auth_code,
client_id = D_CLIENT_ID or '',
client_secret = D_CLIENT_SECRET or '',
access_token = D_ACCESS_TOKEN or '',
refresh_token = D_REFRESH_TOKEN or '',
id_token = D_ID_TOKEN or ''
})
print("Data initialized")
end
self.access_data = self:kv_db_load()
-- Check cliend id and secret
if (D_CLIENT_ID and D_CLIENT_ID ~= '' and self.access_data.client_id ~= D_CLIENT_ID)
or (D_CLIENT_SECRET and D_CLIENT_SECRET ~= '' and self.access_data.client_secret ~= D_CLIENT_SECRET) then
self:kv_db_update_records({
client_id = D_CLIENT_ID,
client_secret = D_CLIENT_SECRET
})
self.access_data.client_id = D_CLIENT_ID
self.access_data.client_secret = D_CLIENT_SECRET
self:daikin_cloud_set_tokens('', '', '', true)
end
if self.access_data.client_id == '' or self.access_data.client_secret == '' then
print("Error: Client ID or Client Secret is not set")
self:notify("[DAIKIN] Error: Client ID or Client Secret is not set")
error_state = true
return
end
local get_new_tokens = false
if self.access_data.authorization_code ~= '' then
if const_auth_code ~= '' and const_auth_code ~= self.access_data.authorization_code then
self:kv_db_update_record('authorization_code', D_AUTH_CODE)
get_new_tokens = true
elseif self.access_data.refresh_token == '' then
get_new_tokens = true
end
else
if const_auth_code ~= '' then
self:kv_db_update_record('authorization_code', D_AUTH_CODE)
get_new_tokens = true
end
end
if get_new_tokens then
self:daikin_cloud_get_tokens('authorization_code', self.access_data.authorization_code, function(err, res, status)
if err then
print('Error: ' .. err)
self:notify("[DAIKIN] Error: " .. err)
error_state = true
elseif status ~= 200 then
print('Error: ' .. status)
self:notify("[DAIKIN] Error: " .. status .. " " .. json.stringify(res))
error_state = true
end
end)
end
if not error_state then
timer.setTimeout(3000, function()
self:poll()
end)
end
end
--- Plugin registred at the hub
--- @param uuid string The uuid of the device
function plugin:register(uuid)
print("Plugin registered")
end
--- Plugin unregistered (hub's going down)
--- @param topic string MQTT topic
--- @param payload table MQTT table
function plugin:unregister(topic, payload)
print("Plugin unregistered")
end
--- Plugin need to re-register
function plugin:reconnect()
print("Plugin reconnect")
end
--- Discover devices and return discovery results
--- @param topic string MQTT topic
--- @param payload table MQTT table
function plugin:discover_devices(topic, payload)
print("Discovering devices...")
local discovery_results = {}
self:daikin_cloud_get_devices(function(err, res, status)
if err then
print('Error: ' .. err)
elseif res then
if status ~= 200 then
print('Error: ' .. status)
return
end
for _, value in ipairs(res) do
local identity = value.id
local device_model = value.deviceModel
local gateway_model = ""
local device_name = ""
local device_is_hvac = false
for _, point in ipairs(value.managementPoints) do
local pointType = point.managementPointType
local pointCategory = point.managementPointCategory
if pointType == "climateControl" and pointCategory == "primary" then
device_is_hvac = true
end
if pointType == "gateway" and point.modelInfo then
gateway_model = point.modelInfo.value
end
if pointCategory == "primary" and point.name then
device_name = point.name.value
end
end
if device_is_hvac then
table.insert(discovery_results, {
identity = identity,
info = string.format("%s (%s via %s)", device_name, device_model, gateway_model),
type = "HVAC",
icon = "ch-thermostat",
params = {
model = device_model,
manufacturer = "Daikin"
}
})
end
end
else
print("Huh?")
end
self:reply(topic, payload.message_id, 200, true, {
devices = discovery_results
})
discovery_cache = res
end)
end
--- @brief Create params table for new device, from whatever initial data
--- @param device table Device table (from plugin.devices)
--- @param identity integer Device identity (Google's device ID in this case)
local function get_params_name_value(device, identity)
local params = {}
for k, v in pairs(device.parameters) do
params[k] = v.value
end
if identity then
params.identity = identity
end
return params
end
--- @brief Merge two params tables
--- @param base_params table Base params table
--- @param overlay_params table Overlay params table
local function merge_params(base_params, overlay_params)
for k, v in pairs(overlay_params) do
base_params[k] = v
end
return base_params
end
--- Function that is called when device creation is requested
--- @param topic string MQTT topic
--- @param payload table MQTT table
function plugin:device_create(topic, payload)
print("Create device")
local message_id = payload.message_id
local name = payload.body.params.name
local dev_type = payload.body.params.type or ""
local identity = payload.body.params.identity
local room_id = payload.body.params.room_id or 0
local params = {
}
print(name, dev_type, identity, room_id)
local new_device = plugin.devices and plugin.devices[dev_type]
if not new_device then
self:reply(topic, message_id, 400, true, {
error = "Unknown device type"
})
return
end
local device_data = nil
if discovery_cache then
for _, device in pairs(discovery_cache) do
if device.id == identity then
device_data = device
break
end
end
end
if not device_data then
self:reply(topic, message_id, 400, true, {
error = "Unknown device"
})
return
end
local on_off_mode = self:daikin_cloud_get_characteristic_value(device_data, "climateControl", "onOffMode")
local operation_mode = self:daikin_cloud_get_characteristic_value(device_data, "climateControl", "operationMode")
local available_modes = self:daikin_cloud_get_characteristic_value(device_data, "climateControl", "operationMode",
"values")
local temperature_control = self:daikin_cloud_get_characteristic_value(device_data, "climateControl",
"temperatureControl")
local sensory_data = self:daikin_cloud_get_characteristic_value(device_data, "climateControl", "sensoryData")
if on_off_mode == nil or operation_mode == nil or available_modes == nil or temperature_control == nil or
sensory_data == nil then
self:reply(topic, message_id, 400, true, {
error = "Device data incomplete"
})
return
end
-- Thermostat Mode interface parameters
params.availableModes = { "Off" }
for _, mode in ipairs(available_modes) do
local new_mode = convert_daikin_mode_to_mode(mode)
print("Adding mode", mode, new_mode)
table.insert(params.availableModes, new_mode)
end
params.currentMode = convert_daikin_mode_to_mode(operation_mode)
if on_off_mode == "off" then
params.currentMode = "Off"
end
params.autoModeIsActive = false
-- Thermostat setpoint interface parameters
params.setpointCapabilities = {}
params.currentSetpoints = {}
for setpoint_mode, setpoint_operation_mode in pairs(temperature_control.operationModes) do
local mode = convert_daikin_mode_to_mode(setpoint_mode)
local step = 1
for _, setpoint_settings in pairs(setpoint_operation_mode.setpoints) do
step = setpoint_settings.stepValue
params.setpointCapabilities[mode] = {
minimum = setpoint_settings.minValue,
maximum = setpoint_settings.maxValue,
precision = setpoint_settings.stepValue,
scale = 0,
size = 4
}
params.currentSetpoints[mode] = setpoint_settings.value
break
end
params.step = step
end
-- Thermostat Operating State interface parameters
params.currentState = convert_daikin_mode_to_state(operation_mode)
-- Sensor Multilevel interface parameters
if sensory_data.roomTemperature then
params.Value = sensory_data.roomTemperature.value
params.sensorType = "Temperature"
params.valueUnit = "°C"
params.valueScale = "Celcius"
end
params = merge_params(get_params_name_value(new_device, identity), params)
self:pub {
topic = "internal/plugins_hub/" .. self.uuid .. "/devices/create",
payload = json.stringify {
version = "2",
method = "POST",
message_id = self.mqtt:pending_message_add(new_device),
token = self.token,
body = {
plugin_type = dev_type,
name = name,
type = new_device.type,
role = new_device.role,
alive = true,
hidden = false,
room_id = room_id,
interfaces = new_device.interfaces,
icon = new_device.icon,
params = params
}
},
qos = 2
}
self:reply(topic, message_id, 200, true)
end
--- @brief Update device paramaeters in batches
--- @param device_id integer Device ID
--- @param params table Parameters to update (k = param name, v = new param value)
function plugin:custom_update_params(device_id, params)
self.mqtt:publish {
topic = "internal/plugins_hub/" .. self.uuid .. "/devices/parameters_set",
payload = json.stringify {
message_id = self.mqtt:pending_message_add {
uuid = self.uuid
},
token = self.token,
version = "2",
method = "POST",
body = {
device_id = device_id,
params = params
}
},
qos = 2,
retain = false
}
end
--- Device created callback
--- @param topic string MQTT topic
--- @param payload table MQTT table
function plugin:device_created(topic, payload)
print("Device created")
if not payload.body.success then
print("Device creation failed:", payload.status)
return
end
local device_id = payload.body.device_id
self:custom_update_params(device_id, payload.body.params)
self:db_add_device(device_id, payload.body.plugin_type)
self:db_add_default_params(device_id, payload.body.interfaces, payload.body.params)
local identity = payload.body.params.identity
if discovery_cache then
for _, device in pairs(discovery_cache) do
if device.id == identity then
registered_devices[device_id] = device
break
end
end
end
print(json.encode(registered_devices))
end
--- Device registered callback
--- @param device_id string device Id
--- @param name string device name
--- @param params table device parameters
function plugin:device_registered(device_id, name, params)
print("Registered device " .. name .. "(" .. device_id .. ")")
self:notify("Registered device " .. name .. "(" .. device_id .. ")")
local identity = params.identity
registered_devices[device_id] = {
id = identity
}
end
--- Device removed, do cleanup here
--- @param topic string MQTT topic
--- @param payload table MQTT table
function plugin:device_removed(topic, payload)
print("Device removed")
if payload.body.success ~= true then
print("Device removal failed")
return
end
local device_id = payload.body.device_id
local identity = self:db_get_param(device_id, "identity")
if registered_devices[device_id] then
registered_devices[device_id] = nil
end
-- Removing device from plugin db
self:db_delete_params(device_id)
self:db_remove_device(device_id)
end
--- @brief Device action handler
--- @param topic string MQTT topic
--- @param payload table MQTT payload
function plugin:action(topic, payload)
print("Device action called")
local device_id_str = self:get_topic_segment(topic, 5)
local device_id = tonumber(device_id_str)
if device_id == nil then
print("Device " .. device_id_str .. " not found")
return
end
local identity = self:db_get_param(device_id, "identity")
local interface = payload.body.interface
local method = payload.body.method
print("Device identity " .. identity)
print(string.format("Device %d, interface %s, method %s", device_id, interface, method))
if not payload.body.params then
print("No params in body")
return
end
local device_data = registered_devices[device_id]
if interface == "ThermostatMode" and method == "setMode" then
local current_onOffMode = self:daikin_cloud_get_characteristic_value(device_data, "climateControl", "onOffMode")
local mode = payload.body.params[1] or payload.body.params["mode"] or nil
if mode and mode ~= "Off" then
-- TODO: Save current state to not send redundant requests
if current_onOffMode ~= "on" then
self:daikin_cloud_set_characteristic(
identity,
"climateControl",
"onOffMode",
nil,
"on",
function(err, _, status)
if err then
print("error: " .. err)
else
if status == 204 then
print("Turned device on successfully")
self:daikin_cloud_update_characteristic_value(device_data, "climateControl", "onOffMode", nil, "on") end
end
end)
end
local new_daikin_mode = convert_mode_to_daikin_mode(mode)
self:daikin_cloud_set_characteristic(
identity,
"climateControl",
"operationMode",
nil,
new_daikin_mode,
function(err, _, status)
if err then
print("Error: " .. err)
else
if status == 204 then
print("Set mode to " .. mode .. " successfully")
self:daikin_cloud_update_characteristic_value(device_data, "climateControl", "operationMode", nil, new_daikin_mode)
-- TODO: Save new current mode
end
end
end)
elseif mode and mode == "Off" then
self:daikin_cloud_set_characteristic(
identity,
"climateControl",
"onOffMode",
nil,
"off",
function(err, _, status)
if err then
print("Error: " .. err)
else
if status == 204 then
print("Turned device off successfully")
self:daikin_cloud_update_characteristic_value(device_data, "climateControl", "onOffMode", nil, "off")
end
end
end)
end
elseif interface == "ThermostatSetpoint" and method == "setSetpoint" then
local setpoint_str = payload.body.params[1] or payload.body.params["setpoint"] or nil
local mode = payload.body.params[2] or payload.body.params["setpointType"] or nil
local setpoint = setpoint_str and tonumber(setpoint_str) or nil
if not setpoint_str or not setpoint or not mode then
print("No setpoint or mode provided")
return
end
local path = "/operationModes/" .. convert_mode_to_daikin_mode(mode) .. "/setpoints/roomTemperature"
self:daikin_cloud_set_characteristic(
identity,
"climateControl",
"temperatureControl",
path,
setpoint,
function(err, res, status)
if err then
print("Error: " .. err)
else
if status == 204 then
print("Set setpoint to " .. setpoint .. " successfully")
self:daikin_cloud_update_characteristic_value(device_data, "climateControl", "onOffMode", path .. "/value", "off")
end
end
end)
end
original_print(json.encode(registered_devices))
end
--- @brief Polling callback
function plugin:poll()
print("Polling devices")
if registered_devices == nil or next(registered_devices) == nil then
print("No registered devices")
poll_timer = timer.setTimeout(6000, function()
self:poll()
end)
return
end
self:daikin_cloud_get_devices(function(err, res, status)
if (err) then
print("Error: " .. err)
elseif status ~= 200 then
print("Error: " .. status)
print("Response: " .. json.encode(res))
-- TODO: Handle error
else
for device_id, device in pairs(registered_devices) do
print("Device ID: " .. device_id)
local device_data = nil
for _, value in pairs(res) do
if value.id == device.id then
device_data = value
break
end
end
if device_data == nil then
print("Device " .. device.id .. " not found")
goto continue
end
local params = {}
local on_off_mode = self:daikin_cloud_get_characteristic_value(device_data, "climateControl", "onOffMode")
local operation_mode = self:daikin_cloud_get_characteristic_value(device_data, "climateControl",
"operationMode")
local available_modes = self:daikin_cloud_get_characteristic_value(device_data, "climateControl",
"operationMode",
"values")
local temperature_control = self:daikin_cloud_get_characteristic_value(device_data, "climateControl",
"temperatureControl")
local sensory_data = self:daikin_cloud_get_characteristic_value(device_data, "climateControl",
"sensoryData")
if on_off_mode == nil or operation_mode == nil or available_modes == nil or temperature_control == nil or
sensory_data == nil then
print("Device data incomplete")
goto continue
end
-- Thermostat Mode interface parameters
params.availableModes = { "Off" }
for _, mode in ipairs(available_modes) do
local new_mode = convert_daikin_mode_to_mode(mode)
print("Adding mode", mode, new_mode)
table.insert(params.availableModes, new_mode)
end
params.currentMode = convert_daikin_mode_to_mode(operation_mode)
if on_off_mode == "off" then
params.currentMode = "Off"
end
params.autoModeIsActive = false
-- Thermostat setpoint interface parameters
params.setpointCapabilities = {}
params.currentSetpoints = {}
for setpoint_mode, setpoint_operation_mode in pairs(temperature_control.operationModes) do
local mode = convert_daikin_mode_to_mode(setpoint_mode)
local step = 1
for _, setpoint_settings in pairs(setpoint_operation_mode.setpoints) do
step = setpoint_settings.stepValue
params.setpointCapabilities[mode] = {
minimum = setpoint_settings.minValue,
maximum = setpoint_settings.maxValue,
precision = setpoint_settings.stepValue,
scale = 0,
size = 4
}
params.currentSetpoints[mode] = setpoint_settings.value
break
end
params.step = step
end
-- Thermostat Operating State interface parameters
if params.currentMode == "Off" then
params.currentState = "Idle"
else
params.currentState = convert_daikin_mode_to_state(operation_mode)
end
-- Sensor Multilevel interface parameters
if sensory_data.roomTemperature then
params.Value = sensory_data.roomTemperature.value
params.sensorType = "Temperature"
params.valueUnit = "°C"
params.valueScale = "Celcius"
end
self:custom_update_params(device_id, params)
registered_devices[device_id] = device_data
::continue::
end
end
if poll_timer then
poll_timer:again()
else
poll_timer = timer.setTimeout(10 * 60 * 1000, function()
self:poll()
end)
end
end)
end
return plugin