Daikin Onecta
Про плагін
Плагін Daikin Onecta дозволяє вам контролювати будь-який пристрій Daikin, перелічений тут: Daikin Onecta compatible units
примітка
Вам потрібно зареєструвати обліковий запис розробника Daikin, щоб мати можливість використовувати цей плагін
Інструкція зі встановлення
Створення облікового запису
- Перейдіть на Daikin Developer Portal і створіть обліковий запис або увійдіть у систему.
- Натисніть на своє ім'я користувача/адресу електронної пошти у верхньому правому куті сторінки, а потім перейдіть на сторінку My Apps.
- Натисніть кнопку + New App
- Заповніть поля відповідними даними:
- Application name - назва вашого додатку, наприклад "Butler Integration"
- Auth Strategy - залиште "Onecta OIDC"
- Redirect URI - введіть
https://c-home.ua/code-confirmation.html
- Description - ви можете написати що завгодно, або просто залишити поле порожнім
- Натисніть кнопку Create
- Скопіюйте та збережіть Client ID та Client Secret десь для подальших кроків
- Натисніть кнопку Proceed
Отримання коду авторизації
- Створіть 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- Відкрийте згенероване посилання та увійдіть у свій обліковий запис Daikin
- Коли з'явиться сторінка згоди, натисніть кнопку I Agree
- Вас буде перенаправлено на сторінку з кодом авторизації, скопіюйте його та збережіть десь для наступного кроку
Встановлення плагіна
Дотримуйтесь загальних інструкцій зі встановлення, поки не вставите код плагіна в текстову область. Не натискайте кнопку Save поки що.
Вам потрібно замінити наступні константи в коді плагіна:
- D_CLIENT_ID - на Client ID
- D_CLIENT_SECRET - на Client Secret
- D_AUTH_CODE - на код авторизації
щоб це виглядало так, але з вашими значеннями:
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"
Продовжуйте згідно з загальними інструкціями зі встановлення
Вихідний код
Вихідний код плагіна
-- 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 = "Ee7E4wUvvABa8bPHLtnyVxnN"
local D_CLIENT_SECRET = "HTE_2H7Rj5oy8rODdYWZbGBBtYuGWZNye6RP_F9MbY71g5Q0rmgFfKCYwPUU4SAXes5gREEOKPg0qDU6AiAzNQ"
local D_AUTH_CODE =
"st2.s.AtLtr33o1w.CJNFLSkJLTZSYeDSne-RGV_uMKDAIUe8q4LQ6mlKD9yNaIfX2oPUMEJ0ItsHnTkBL3W60qaKrfzSd2hhmkpg7-r5BG86_SclRGvX-2eyv2-o-rIFI2QxD7YF5gF79Jdb.penPgGw3FKwm6BcpP8UIHk03uDW3zkQnyzugDt6bS8bytYPmC2iuL6HDHbXoLTEM720Zj6HW-6Tpw6fk3NHTQQ.sc3"
--[[============================================]]
--[[ ]]
--[[ 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