From 541a7d092c3e58f47769d783e2ffd20c2f200daf Mon Sep 17 00:00:00 2001 From: Irek Kubicki Date: Thu, 26 Dec 2024 10:10:57 +0100 Subject: [PATCH] Salus local mode --- Config.lua | 60 ++++- SalusChildDevice.lua | 13 -- Salus.lua => SalusCloud.lua | 115 +++++----- SalusHumidity.lua | 20 ++ SalusProxy.lua | 106 +++++++++ SalusTemperature.lua | 19 ++ SalusUtils.lua | 31 +++ Salus_It600.fqa | 421 ++++++++++++++++++++++++++++++++++-- i18n.lua | 20 +- main.lua | 204 ++++++++++------- utils.lua | 21 +- 11 files changed, 833 insertions(+), 197 deletions(-) delete mode 100644 SalusChildDevice.lua rename Salus.lua => SalusCloud.lua (79%) create mode 100644 SalusHumidity.lua create mode 100644 SalusProxy.lua create mode 100644 SalusTemperature.lua create mode 100644 SalusUtils.lua diff --git a/Config.lua b/Config.lua index 67300df..8254ada 100644 --- a/Config.lua +++ b/Config.lua @@ -6,13 +6,19 @@ class 'Config' function Config:new(app) self.app = app + self.user = nil + self.password = nil + self.host = nil + self.port = nil + self.device_id = nil + self.interval = nil self:init() return self end -function Config:getUsername() - if self.username and self.username:len() > 3 then - return self.username +function Config:getUser() + if self.user and self.user:len() > 3 then + return self.user end return nil end @@ -21,6 +27,20 @@ function Config:getPassword() return self.password end +function Config:getHost() + if self.host and self.host:len() > 3 then + return self.host + end + return nil +end + +function Config:getPort() + if self.port and string.len(self.port) > 1 then + return self.port + end + return '80' +end + function Config:getDeviceID() return self.device_id end @@ -39,20 +59,24 @@ This way, adding other devices might be optional and leaves option for users, what they want to add into HC3 virtual devices. ]] function Config:init() - self.username = self.app:getVariable('Username') + self.user = self.app:getVariable('User') self.password = self.app:getVariable('Password') + self.host = self.app:getVariable('Host') + self.port = self.app:getVariable('Port') self.device_id = self.app:getVariable('DeviceID') self.interval = self.app:getVariable('Interval') local storedUsername = Globals:get('salus_username', '') local storedPassword = Globals:get('salus_password', '') + local storedHost = Globals:get('salus_host', '') + local storedPort = tonumber(Globals:get('salus_port', '')) -- handling username - if string.len(self.username) < 4 and string.len(storedUsername) > 3 then - self.app:setVariable("Username", storedUsername) - self.username = storedUsername - elseif (storedUsername == '' and self.username) then - Globals:set('salus_username', self.username) + if string.len(self.user) < 4 and string.len(storedUsername) > 3 then + self.app:setVariable("User", storedUsername) + self.user = storedUsername + elseif (storedUsername == '' and self.user) then + Globals:set('salus_username', self.user) end -- handling password if string.len(self.password) < 4 and string.len(storedPassword) > 3 then @@ -61,9 +85,23 @@ function Config:init() elseif (storedPassword == '' and self.password) then Globals:set('salus_password', self.password) end + -- handling host + if string.len(self.host) < 4 and string.len(storedHost) > 3 then + self.app:setVariable("Host", storedHost) + self.host = storedHost + elseif (storedHost == '' and self.host) then + Globals:set('salus_host', self.host) + end + -- handling port + if string.len(self.host) < 2 and string.len(storedPort) > 3 then + self.app:setVariable("Port", storedPort) + self.port = storedPort + elseif (storedPort == '' and self.port) then + Globals:set('salus_port', self.port) + end -- handling interval if not self.interval or self.interval == "" then - self.app:setVariable("Interval", 30) - self.interval = 30 + self.app:setVariable("Interval", 5) + self.interval = 5 end end \ No newline at end of file diff --git a/SalusChildDevice.lua b/SalusChildDevice.lua deleted file mode 100644 index e59dd52..0000000 --- a/SalusChildDevice.lua +++ /dev/null @@ -1,13 +0,0 @@ -class 'SalusChildDevice' (QuickAppChild) - -function SalusChildDevice:__init(device) - QuickAppChild.__init(self, device) -end - -function SalusChildDevice:setValue(value) - self:updateProperty("value", value) -end - -function SalusChildDevice:setState(value) - self:updateProperty("state", value > 0) -end \ No newline at end of file diff --git a/Salus.lua b/SalusCloud.lua similarity index 79% rename from Salus.lua rename to SalusCloud.lua index e471542..4b6a87c 100644 --- a/Salus.lua +++ b/SalusCloud.lua @@ -1,11 +1,11 @@ --[[ -Salus IT600 SDK +Salus IT600 Cloud SDK @author ikubicki ]] -class 'Salus' +class 'SalusCloud' -function Salus:new(config) - self.user = config:getUsername() +function SalusCloud:new(config) + self.user = config:getUser() self.pass = config:getPassword() self.device_id = config:getDeviceID() self.token = Globals:get('salus_token', '') @@ -16,7 +16,7 @@ function Salus:new(config) return self end -function Salus:getProperties(callback, failCallback) +function SalusCloud:getProperties(callback, failCallback) local properties = {} local batteryLevelCallback = function(response) properties["battery"] = response.value @@ -24,31 +24,31 @@ function Salus:getProperties(callback, failCallback) end local holdtypeCallback = function(response) properties["holdtype"] = response.value - Salus:batteryLevel(batteryLevelCallback, failCallback) + SalusCloud:batteryLevel(batteryLevelCallback, failCallback) end local runningCallback = function(response) properties["running"] = response.value - Salus:holdtype(holdtypeCallback, failCallback) + SalusCloud:holdtype(holdtypeCallback, failCallback) end local humidityCallback = function(response) properties["humidity"] = response.value - Salus:running(runningCallback, failCallback) + SalusCloud:running(runningCallback, failCallback) end local heatingSetpointCallback = function(response) properties["heatingSetpoint"] = response.value / 100 - Salus:humidity(humidityCallback, failCallback) + SalusCloud:humidity(humidityCallback, failCallback) end local temperatureCallback = function(response) properties["temperature"] = response.value / 100 - Salus:heatingSetpoint(heatingSetpointCallback, failCallback) + SalusCloud:heatingSetpoint(heatingSetpointCallback, failCallback) end local authCallback = function(response) - Salus:temperature(temperatureCallback, failCallback) + SalusCloud:temperature(temperatureCallback, failCallback) end - Salus:auth(authCallback, failCallback) + SalusCloud:auth(authCallback, failCallback) end -function Salus:searchDevices(callback) +function SalusCloud:searchDevices(callback) local buildGateway = function(data) return { id = data.dsn, @@ -82,12 +82,12 @@ function Salus:searchDevices(callback) callback(gateways) end local authCallback = function(response) - Salus:listDevices(listDevicesCallback) + SalusCloud:listDevices(listDevicesCallback) end - Salus:auth(authCallback) + SalusCloud:auth(authCallback) end -function Salus:batteryLevel(callback, failCallback, attempt) +function SalusCloud:batteryLevel(callback, failCallback, attempt) if attempt == nil then attempt = 1 end @@ -99,17 +99,17 @@ function Salus:batteryLevel(callback, failCallback, attempt) failCallback(json.encode(response)) end QuickApp:error('Unable to pull battery level') - Salus:setToken('') + SalusCloud:setToken('') -- QuickApp:debug(json.encode(response)) if attempt < 2 then attempt = attempt + 1 fibaro.setTimeout(3000, function() - QuickApp:debug('Salus:batteryLevel - Retry attempt #' .. attempt) + QuickApp:debug('SalusCloud:batteryLevel - Retry attempt #' .. attempt) local authCallback = function(response) self:batteryLevel(callback, failCallback, attempt) end - Salus:auth(authCallback, failCallback) + SalusCloud:auth(authCallback, failCallback) end) end end @@ -125,12 +125,12 @@ function Salus:batteryLevel(callback, failCallback, attempt) end local url = "/apiv1/dsns/" .. self.device_id .. "/properties/ep_9:sIT600TH:BatteryLevel.json" local headers = { - Authorization = "Bearer " .. Salus:getToken() + Authorization = "Bearer " .. SalusCloud:getToken() } self.http:get(url, success, fail, headers) end -function Salus:temperature(callback, failCallback, attempt) +function SalusCloud:temperature(callback, failCallback, attempt) if attempt == nil then attempt = 1 end @@ -139,17 +139,17 @@ function Salus:temperature(callback, failCallback, attempt) failCallback(json.encode(response)) end QuickApp:error('Unable to pull temperature') - Salus:setToken('') + SalusCloud:setToken('') --QuickApp:debug(json.encode(response)) if attempt < 2 then attempt = attempt + 1 fibaro.setTimeout(3000, function() - QuickApp:debug('Salus:temperature - Retry attempt #' .. attempt) + QuickApp:debug('SalusCloud:temperature - Retry attempt #' .. attempt) local authCallback = function(response) self:temperature(callback, failCallback, attempt) end - Salus:auth(authCallback, failCallback) + SalusCloud:auth(authCallback, failCallback) end) end end @@ -165,18 +165,18 @@ function Salus:temperature(callback, failCallback, attempt) end local url = "/apiv1/dsns/" .. self.device_id .. "/properties/ep_9:sIT600TH:LocalTemperature_x100.json" local headers = { - Authorization = "Bearer " .. Salus:getToken() + Authorization = "Bearer " .. SalusCloud:getToken() } self.http:get(url, success, fail, headers) end -function Salus:heatingSetpoint(callback, failCallback) +function SalusCloud:heatingSetpoint(callback, failCallback) local fail = function(response) if failCallback then failCallback(json.encode(response)) end QuickApp:error('Unable to pull heating setpoint') - Salus:setToken('') + SalusCloud:setToken('') end local success = function(response) if response.status > 299 then @@ -190,18 +190,18 @@ function Salus:heatingSetpoint(callback, failCallback) end local url = "/apiv1/dsns/" .. self.device_id .. "/properties/ep_9:sIT600TH:HeatingSetpoint_x100.json" local headers = { - Authorization = "Bearer " .. Salus:getToken() + Authorization = "Bearer " .. SalusCloud:getToken() } self.http:get(url, success, fail, headers) end -function Salus:setHeatingSetpoint(heatingSetpoint, callback, failCallback) +function SalusCloud:setHeatingSetpoint(heatingSetpoint, callback, failCallback) local fail = function(response) if failCallback then failCallback(json.encode(response)) end QuickApp:error('Unable to update heatingSetpoint') - Salus:setToken('') + SalusCloud:setToken('') end local success = function(response) if response.status > 299 then @@ -215,7 +215,7 @@ function Salus:setHeatingSetpoint(heatingSetpoint, callback, failCallback) end local url = "/apiv1/dsns/" .. self.device_id .. "/properties/ep_9:sIT600TH:SetHeatingSetpoint_x100/datapoints.json" local headers = { - Authorization = "Bearer " .. Salus:getToken(), + Authorization = "Bearer " .. SalusCloud:getToken(), ["Content-Type"] = "application/json", } local data = { @@ -226,13 +226,13 @@ function Salus:setHeatingSetpoint(heatingSetpoint, callback, failCallback) self.http:post(url, data, success, fail, headers) end -function Salus:humidity(callback, failCallback) +function SalusCloud:humidity(callback, failCallback) local fail = function(response) if failCallback then failCallback(json.encode(response)) end QuickApp:error('Unable to pull humidity') - Salus:setToken('') + SalusCloud:setToken('') end local success = function(response) if response.status > 299 then @@ -246,18 +246,18 @@ function Salus:humidity(callback, failCallback) end local url = "/apiv1/dsns/" .. self.device_id .. "/properties/ep_9:sIT600TH:SunnySetpoint_x100.json" local headers = { - Authorization = "Bearer " .. Salus:getToken() + Authorization = "Bearer " .. SalusCloud:getToken() } self.http:get(url, success, fail, headers) end -function Salus:running(callback, failCallback) +function SalusCloud:running(callback, failCallback) local fail = function(response) if failCallback then failCallback(json.encode(response)) end QuickApp:error('Unable to pull mode') - Salus:setToken('') + SalusCloud:setToken('') end local success = function(response) if response.status > 299 then @@ -271,18 +271,18 @@ function Salus:running(callback, failCallback) end local url = "/apiv1/dsns/" .. self.device_id .. "/properties/ep_9:sIT600TH:RunningState.json" local headers = { - Authorization = "Bearer " .. Salus:getToken() + Authorization = "Bearer " .. SalusCloud:getToken() } self.http:get(url, success, fail, headers) end -function Salus:holdtype(callback, failCallback) +function SalusCloud:holdtype(callback, failCallback) local fail = function(response) if failCallback then failCallback(json.encode(response)) end QuickApp:error('Unable to pull mode') - Salus:setToken('') + SalusCloud:setToken('') end local success = function(response) if response.status > 299 then @@ -296,18 +296,18 @@ function Salus:holdtype(callback, failCallback) end local url = "/apiv1/dsns/" .. self.device_id .. "/properties/ep_9:sIT600TH:HoldType.json" local headers = { - Authorization = "Bearer " .. Salus:getToken() + Authorization = "Bearer " .. SalusCloud:getToken() } self.http:get(url, success, fail, headers) end -function Salus:setHoldtype(holdtype, callback, failCallback) +function SalusCloud:setHoldtype(holdtype, callback, failCallback) local fail = function(response) if failCallback then failCallback(json.encode(response)) end QuickApp:error('Unable to update holdtype') - Salus:setToken('') + SalusCloud:setToken('') end local success = function(response) if response.status > 299 then @@ -321,7 +321,7 @@ function Salus:setHoldtype(holdtype, callback, failCallback) end local url = "/apiv1/dsns/" .. self.device_id .. "/properties/ep_9:sIT600TH:SetHoldType/datapoints.json" local headers = { - Authorization = "Bearer " .. Salus:getToken(), + Authorization = "Bearer " .. SalusCloud:getToken(), ["Content-Type"] = "application/json", } local data = { @@ -332,7 +332,7 @@ function Salus:setHoldtype(holdtype, callback, failCallback) self.http:post(url, data, success, fail, headers) end -function Salus:listDevices(callback, fail, attempt) +function SalusCloud:listDevices(callback, fail, attempt) if attempt == nil then attempt = 1 end @@ -342,16 +342,16 @@ function Salus:listDevices(callback, fail, attempt) failCallback(json.encode(response)) end QuickApp:error('Unable to pull devices') - Salus:setToken('') + SalusCloud:setToken('') if attempt < 2 then attempt = attempt + 1 fibaro.setTimeout(3000, function() - QuickApp:debug('Salus:listDevices - Retry attempt #' .. attempt) + QuickApp:debug('SalusCloud:listDevices - Retry attempt #' .. attempt) local authCallback = function(response) self:listDevices(callback, nil, attempt) end - Salus:auth(authCallback) + SalusCloud:auth(authCallback) end) end end @@ -368,12 +368,12 @@ function Salus:listDevices(callback, fail, attempt) end local url = "/apiv1/devices.json" local headers = { - Authorization = "Bearer " .. Salus:getToken() + Authorization = "Bearer " .. SalusCloud:getToken() } self.http:get(url, success, fail, headers) end -function Salus:auth(callback, failCallback) +function SalusCloud:auth(callback, failCallback) if string.len(self.token) > 1 then -- QuickApp:debug('Already authenticated') if callback ~= nil then @@ -386,7 +386,7 @@ function Salus:auth(callback, failCallback) failCallback(json.encode(response)) end QuickApp:error('Unable to authenticate') - Salus:setToken('') + SalusCloud:setToken('') end local success = function(response) -- QuickApp:debug(json.encode(response)) @@ -395,7 +395,7 @@ function Salus:auth(callback, failCallback) return end local data = json.decode(response.data) - Salus:setToken(data.access_token) + SalusCloud:setToken(data.access_token) if callback ~= nil then callback(data) end @@ -413,14 +413,14 @@ function Salus:auth(callback, failCallback) self.http:post(url, data, success, fail, headers) end -function Salus:setToken(token) +function SalusCloud:setToken(token) self.token = token self.token_time = os.time(os.date("!*t")) Globals:set('salus_token', token) Globals:set('salus_token_time', self.token_time) end -function Salus:getToken() +function SalusCloud:getToken() if not self:checkTokenTime() then self:setToken('') return '' @@ -433,17 +433,10 @@ function Salus:getToken() return '' end -function Salus:checkTokenTime() +function SalusCloud:checkTokenTime() if self.token_time < 1 then self.token_time = tonumber(Globals:get('salus_token_time', 0)) end return self.token_time > 0 and os.time(os.date("!*t")) - self.token_time < 43200 end -function Salus:translateBatteryLevel(batteryLevel) - if batteryLevel > 4 then return 100 end; - if batteryLevel == 4 then return 75 end; - if batteryLevel == 3 then return 50 end; - if batteryLevel == 2 then return 25 end; - if batteryLevel > 2 then return 0 end; -end \ No newline at end of file diff --git a/SalusHumidity.lua b/SalusHumidity.lua new file mode 100644 index 0000000..b7ee2d2 --- /dev/null +++ b/SalusHumidity.lua @@ -0,0 +1,20 @@ +--[[ +Salus humidity sendor child device class +@author ikubicki +]] + +class 'SalusHumidity' (QuickAppChild) + +function SalusHumidity:__init(device) + QuickAppChild.__init(self, device) +end + +function SalusHumidity:setName(name) + api.put('/devices/' .. self.id, { + name = name, + }) +end + +function SalusHumidity:setValue(value) + self:updateProperty("value", value) +end diff --git a/SalusProxy.lua b/SalusProxy.lua new file mode 100644 index 0000000..80cca66 --- /dev/null +++ b/SalusProxy.lua @@ -0,0 +1,106 @@ +--[[ +Salus IT600 Proxy SDK +@author ikubicki +]] +class 'SalusProxy' + +function SalusProxy:new(config) + self.config = config + self.user = config:getUser() + self.pass = config:getPassword() + self.device_id = config:getDeviceID() + self.token = Utils:base64(config:getUser() .. ':' .. config:getPassword()) + self.http = HTTPClient:new({ + baseUrl = 'http://' .. config:getHost() .. ':' .. config:getPort() .. '/api/v1' + }) + return self +end + +function SalusProxy:getProperties(ok, nok) + local h = function(r) + if (r.status > 200) then + return nok(r) + end + local data = json.decode(r.data) + local running = 0 + if data.isRunning then running = 1 end + return ok({ + id = data.id, + name = data.name, + model = data.model, + heatingSetpoint = data.temperature, + temperature = data.currentTemperature, + humidity = data.humidity, + running = running, + holdtype = data.mode, + battery = data.battery, + }) + end + local headers = { + Authorization = 'Basic ' .. self.token, + ['Content-Type'] = 'application/json', + } + if not nok then nok = function() end end + self.http:get('/devices/' .. self.config:getDeviceID(), h, nok, headers) +end + +function SalusProxy:searchDevices(ok, nok) + local h = function(r) + if (r.status > 200) then + return nok(r) + end + return ok({{ + name = 'proxy', + devices = json.decode(r.data) + }}) + end + local headers = { + Authorization = 'Basic ' .. self.token, + ['Content-Type'] = 'application/json', + } + if not nok then nok = function() end end + self.http:get('/devices', h, nok, headers) +end + +function SalusProxy:setHeatingSetpoint(setpoint, ok, nok) +local h = function(r) + if (r.status > 200) then + return nok(r) + end + return ok({{ + name = 'proxy', + devices = json.decode(r.data) + }}) + end + local headers = { + Authorization = 'Basic ' .. self.token, + ['Content-Type'] = 'application/json', + } + local d = { + temperature = setpoint, + } + if not nok then nok = function() end end + self.http:post('/devices/' .. self.config:getDeviceID() .. '/temperature', d, h, nok, headers) +end + + +function SalusProxy:setHoldtype(holdtype, ok, nok) + local h = function(r) + if (r.status > 200) then + return nok(r) + end + return ok({{ + name = 'proxy', + devices = json.decode(r.data) + }}) + end + local headers = { + Authorization = 'Basic ' .. self.token, + ['Content-Type'] = 'application/json', + } + local d = { + mode = holdtype, + } + if not nok then nok = function() end end + self.http:post('/devices/' .. self.config:getDeviceID() .. '/mode', d, h, nok, headers) +end \ No newline at end of file diff --git a/SalusTemperature.lua b/SalusTemperature.lua new file mode 100644 index 0000000..fd506aa --- /dev/null +++ b/SalusTemperature.lua @@ -0,0 +1,19 @@ +--[[ +Salus temperature sendor child device class +@author ikubicki +]] +class 'SalusTemperature' (QuickAppChild) + +function SalusTemperature:__init(device) + QuickAppChild.__init(self, device) +end + +function SalusTemperature:setName(name) + api.put('/devices/' .. self.id, { + name = name, + }) +end + +function SalusTemperature:setValue(value) + self:updateProperty("value", value) +end diff --git a/SalusUtils.lua b/SalusUtils.lua new file mode 100644 index 0000000..aff8492 --- /dev/null +++ b/SalusUtils.lua @@ -0,0 +1,31 @@ +--[[ +Salus utilities +@author ikubicki +]] +class 'SalusUtils' + +function SalusUtils:translateHoldType(holdtype) + if holdtype == 2 then + return 'Heat' + elseif holdtype == 7 then + return 'Off' + end + return 'Auto' -- 0 or 1 +end + +function SalusUtils:translateBattery(battery) + if battery > 4 then return 100 end; + if battery == 4 then return 75 end; + if battery == 3 then return 50 end; + if battery == 2 then return 25 end; + if battery > 2 then return 0 end; +end + +function SalusUtils:translateMode(mode) + if mode == 'Off' then + return 7 + elseif mode == 'Heat' then + return 2 + end + return 0 +end \ No newline at end of file diff --git a/Salus_It600.fqa b/Salus_It600.fqa index 72bd6f8..c1f7492 100644 --- a/Salus_It600.fqa +++ b/Salus_It600.fqa @@ -1,7 +1,7 @@ { "name": "Salus IT600", "type": "com.fibaro.hvacSystemHeat", - "apiVersion": "1.2", + "apiVersion": "1.3", "initialProperties": { "viewLayout": { "$jason": { @@ -10,14 +10,14 @@ "style": { "height": "0" }, - "title": "salus130" + "title": "salus_it600" }, "sections": { "items": [ { "components": [ { - "name": "label1", + "name": "title", "style": { "weight": "1.2" }, @@ -37,6 +37,32 @@ }, "type": "vertical" }, + { + "components": [ + { + "name": "deviceSelect", + "options": [], + "selectionType": "single", + "style": { + "weight": "1.2" + }, + "text": "Wybierz urządzenie", + "type": "select", + "values": [], + "visible": true + }, + { + "style": { + "weight": "0.5" + }, + "type": "space" + } + ], + "style": { + "weight": "1.2" + }, + "type": "vertical" + }, { "components": [ { @@ -77,6 +103,52 @@ }, "type": "vertical" }, + { + "components": [ + { + "name": "button2_2__b", + "style": { + "weight": "1.2" + }, + "text": "Odśwież dane", + "type": "button", + "visible": false + }, + { + "style": { + "weight": "0.5" + }, + "type": "space" + } + ], + "style": { + "weight": "1.2" + }, + "type": "vertical" + }, + { + "components": [ + { + "name": "label1", + "style": { + "weight": "1.2" + }, + "text": "", + "type": "label", + "visible": true + }, + { + "style": { + "weight": "0.5" + }, + "type": "space" + } + ], + "style": { + "weight": "1.2" + }, + "type": "vertical" + }, { "components": [ { @@ -104,25 +176,311 @@ } }, "head": { - "title": "salus130" + "title": "salus_it600" } } }, + "uiView": [ + { + "components": [ + { + "name": "title", + "style": { + "weight": "1.0" + }, + "text": "Salus IT600", + "type": "label", + "visible": true + } + ], + "style": { + "weight": "1.0" + }, + "type": "horizontal" + }, + { + "components": [ + { + "eventBinding": { + "onToggled": [ + { + "params": { + "actionName": "UIAction", + "args": [ + "onToggled", + "deviceSelect", + "$event.value" + ] + }, + "type": "deviceAction" + } + ] + }, + "name": "deviceSelect", + "options": [], + "selectionType": "single", + "style": { + "weight": "1.0" + }, + "text": "Wybierz urządzenie", + "type": "select", + "values": [], + "visible": true + } + ], + "style": { + "weight": "1.0" + }, + "type": "horizontal" + }, + { + "components": [ + { + "eventBinding": { + "onLongPressDown": [ + { + "params": { + "actionName": "UIAction", + "args": [ + "onLongPressDown", + "button2_1" + ] + }, + "type": "deviceAction" + } + ], + "onLongPressReleased": [ + { + "params": { + "actionName": "UIAction", + "args": [ + "onLongPressReleased", + "button2_1" + ] + }, + "type": "deviceAction" + } + ], + "onReleased": [ + { + "params": { + "actionName": "UIAction", + "args": [ + "onReleased", + "button2_1" + ] + }, + "type": "deviceAction" + } + ] + }, + "name": "button2_1", + "style": { + "weight": "0.50" + }, + "text": "Szukaj urządzeń", + "type": "button", + "visible": true + }, + { + "eventBinding": { + "onLongPressDown": [ + { + "params": { + "actionName": "UIAction", + "args": [ + "onLongPressDown", + "button2_2" + ] + }, + "type": "deviceAction" + } + ], + "onLongPressReleased": [ + { + "params": { + "actionName": "UIAction", + "args": [ + "onLongPressReleased", + "button2_2" + ] + }, + "type": "deviceAction" + } + ], + "onReleased": [ + { + "params": { + "actionName": "UIAction", + "args": [ + "onReleased", + "button2_2" + ] + }, + "type": "deviceAction" + } + ] + }, + "name": "button2_2", + "style": { + "weight": "0.50" + }, + "text": "Odśwież dane", + "type": "button", + "visible": true + } + ], + "style": { + "weight": "1.0" + }, + "type": "horizontal" + }, + { + "components": [ + { + "eventBinding": { + "onLongPressDown": [ + { + "params": { + "actionName": "UIAction", + "args": [ + "onLongPressDown", + "button2_2__b" + ] + }, + "type": "deviceAction" + } + ], + "onLongPressReleased": [ + { + "params": { + "actionName": "UIAction", + "args": [ + "onLongPressReleased", + "button2_2__b" + ] + }, + "type": "deviceAction" + } + ], + "onReleased": [ + { + "params": { + "actionName": "UIAction", + "args": [ + "onReleased", + "button2_2__b" + ] + }, + "type": "deviceAction" + } + ] + }, + "name": "button2_2__b", + "style": { + "weight": "1.0" + }, + "text": "Odśwież dane", + "type": "button", + "visible": false + } + ], + "style": { + "weight": "1.0" + }, + "type": "horizontal" + }, + { + "components": [ + { + "name": "label1", + "style": { + "weight": "1.0" + }, + "text": "", + "type": "label", + "visible": true + } + ], + "style": { + "weight": "1.0" + }, + "type": "horizontal" + }, + { + "components": [ + { + "name": "label2", + "style": { + "weight": "1.0" + }, + "text": "", + "type": "label", + "visible": true + } + ], + "style": { + "weight": "1.0" + }, + "type": "horizontal" + } + ], + "useUiView": true, "uiCallbacks": [ + { + "callback": "selectDeviceEvent", + "eventType": "onToggled", + "name": "deviceSelect" + }, { "callback": "searchEvent", "eventType": "onReleased", "name": "button2_1" }, + { + "callback": "", + "eventType": "onLongPressDown", + "name": "button2_1" + }, + { + "callback": "", + "eventType": "onLongPressReleased", + "name": "button2_1" + }, { "callback": "refreshEvent", "eventType": "onReleased", "name": "button2_2" + }, + { + "callback": "", + "eventType": "onLongPressDown", + "name": "button2_2" + }, + { + "callback": "", + "eventType": "onLongPressReleased", + "name": "button2_2" + }, + { + "callback": "refreshEvent", + "eventType": "onReleased", + "name": "button2_2__b" + }, + { + "callback": "", + "eventType": "onLongPressDown", + "name": "button2_2__b" + }, + { + "callback": "", + "eventType": "onLongPressReleased", + "name": "button2_2__b" } ], "quickAppVariables": [ { - "name": "Username", + "name": "User", "type": "string", "value": "" }, @@ -131,6 +489,16 @@ "type": "password", "value": "" }, + { + "name": "Host", + "type": "string", + "value": "" + }, + { + "name": "Port", + "type": "string", + "value": "80" + }, { "name": "DeviceID", "type": "string", @@ -139,24 +507,25 @@ { "name": "Interval", "type": "string", - "value": "30" + "value": 5 } ], - "typeTemplateInitialized": true + "typeTemplateInitialized": true, + "userDescription": "" }, "initialInterfaces": [], "files": [ { "name": "main", "isMain": true, - "isOpen": false, - "content": "--[[\nSalus IT600 thermostats integration\n@author ikubicki\n@version 1.3.0\n]]\n\nfunction QuickApp:onInit()\n self.config = Config:new(self)\n self.failover = false\n self.salus = Salus:new(self.config)\n self.i18n = i18n:new(api.get(\"/settings/info\").defaultLanguage)\n self:trace('')\n self:trace(string.format(self.i18n:get('name'), self.name))\n self:updateProperty('manufacturer', 'Salus')\n self:updateProperty('model', 'IT600')\n self.childrenIds = {}\n self.interfaces = api.get(\"/devices/\" .. self.id).interfaces\n\n self:updateProperty(\"supportedThermostatModes\", {\"Off\", \"Heat\", \"Auto\"})\n self:updateProperty(\"heatingThermostatSetpointCapabilitiesMax\", 35)\n self:updateProperty(\"heatingThermostatSetpointCapabilitiesMin\", 10)\n\n self:updateView(\"label1\", \"text\", string.format(self.i18n:get('name'), self.name))\n self:updateView(\"button2_1\", \"text\", self.i18n:get('search-devices'))\n self:updateView(\"button2_2\", \"text\", self.i18n:get('refresh'))\n\n self:initChildDevices({\n [\"com.fibaro.temperatureSensor\"] = SalusChildDevice,\n [\"com.fibaro.humiditySensor\"] = SalusChildDevice,\n [\"com.fibaro.binarySwitch\"] = SalusChildDevice,\n })\n for id, device in pairs(self.childDevices) do\n self.childrenIds[device.type] = id\n end\n\n if string.len(self.config:getDeviceID()) > 10 then\n if self.childrenIds[\"com.fibaro.temperatureSensor\"] == nil then\n local child = self:createChildDevice({\n name = self.name .. ' Temperature',\n type = \"com.fibaro.temperatureSensor\",\n }, SalusChildDevice)\n end\n if self.childrenIds[\"com.fibaro.humiditySensor\"] == nil then\n local child = self:createChildDevice({\n name = self.name .. ' Humidity',\n type = \"com.fibaro.humiditySensor\",\n }, SalusChildDevice)\n end\n if self.childrenIds[\"com.fibaro.binarySwitch\"] == nil then\n local child = self:createChildDevice({\n name = self.name .. ' Heating',\n type = \"com.fibaro.binarySwitch\",\n deviceRole = 'Valve',\n isLight = false,\n }, SalusChildDevice)\n end\n self:run()\n else \n self:updateView(\"label1\", \"text\", self.i18n:get('not-configured'))\n end\nend\n\nfunction QuickApp:setThermostatMode(mode)\n self:updateProperty(\"thermostatMode\", mode)\n local holdtype = 0\n if mode == 'Off' then\n holdtype = 7\n elseif mode == 'Heat' then\n holdtype = 2\n end\n local setHoldtypeCallback = function(response)\n self:pullDataFromCloud()\n end\n self.salus:setHoldtype(holdtype, setHoldtypeCallback)\nend\n\nfunction QuickApp:setHeatingThermostatSetpoint(value) \n self:updateProperty(\"heatingThermostatSetpoint\", value)\n local setHeatingSetpointCallback = function(response)\n self:pullDataFromCloud()\n end\n self.salus:setHeatingSetpoint(value, setHeatingSetpointCallback)\nend\n\nfunction QuickApp:refreshEvent(event)\n self:updateView(\"label\", \"text\", self.i18n:get('refreshing'))\n self:pullDataFromCloud()\nend\n\nfunction QuickApp:run()\n self:pullDataFromCloud()\n local interval = self.config:getInterval()\n if self.failover then\n interval = 300000\n end\n if (interval > 0) then\n fibaro.setTimeout(interval, function() self:run() end)\n end\nend\n\nfunction QuickApp:pullDataFromCloud()\n local getFailCallback = function(error)\n self:updateView(\"button2_2\", \"text\", self.i18n:get('refresh'))\n self:updateView(\"label1\", \"text\", \"API Error: \" .. error)\n self:updateView(\"label2\", \"text\", \"\")\n self.failover = true\n end\n local getPropertiesCallback = function(properties)\n local label2Text = \"\"\n self.failover = false\n -- QuickApp:debug(json.encode(properties))\n self:updateView(\"button2_2\", \"text\", self.i18n:get('refresh'))\n if self.childrenIds[\"com.fibaro.temperatureSensor\"] ~= nil then\n self.childDevices[self.childrenIds[\"com.fibaro.temperatureSensor\"]]:setValue(properties.temperature)\n label2Text = properties.temperature .. \"C / \" .. properties.heatingSetpoint .. \"C\" \n end\n if self.childrenIds[\"com.fibaro.humiditySensor\"] ~= nil then\n self.childDevices[self.childrenIds[\"com.fibaro.humiditySensor\"]]:setValue(properties.humidity)\n label2Text = label2Text .. \" / \" .. properties.humidity .. \"%\"\n end\n if self.childrenIds[\"com.fibaro.binarySwitch\"] ~= nil then\n local isRunningValue = 0\n if properties.running and properties.running > 0 then\n isRunningValue = 1\n end\n self.childDevices[self.childrenIds[\"com.fibaro.binarySwitch\"]]:setValue(isRunningValue > 0)\n if isRunningValue > 0 then\n label2Text = self.i18n:get('heating') .. \" / \" .. label2Text\n else\n label2Text = self.i18n:get('off') .. \" / \" .. label2Text\n end\n end\n local mode = 'Auto' -- 0 or 1\n if properties.holdtype == 2 then\n mode = 'Heat'\n elseif properties.holdtype == 7 then\n mode = 'Off'\n end\n self:updateProperty(\"thermostatMode\", mode)\n self:updateProperty(\"heatingThermostatSetpoint\", properties.heatingSetpoint)\n self:updateView(\"label1\", \"text\", string.format(self.i18n:get('last-update'), os.date('%Y-%m-%d %H:%M:%S')))\n self:updateView(\"label2\", \"text\", label2Text)\n \n if properties.battery ~= nil then\n self:updateProperty(\"batteryLevel\", Salus:translateBatteryLevel(properties.battery))\n if not utils:contains(self.interfaces, \"battery\") then\n api.put(\"/devices/\" .. self.id, { interfaces = {\n \"quickApp\", \"battery\", \"heatingThermostatSetpoint\", \"thermostatMode\"\n }})\n end\n else\n\n if not utils:contains(self.interfaces, \"power\") then\n api.put(\"/devices/\" .. self.id, { interfaces = {\n \"quickApp\", \"power\", \"heatingThermostatSetpoint\", \"thermostatMode\"\n }})\n end\n end\n end\n self:updateView(\"button2_2\", \"text\", self.i18n:get('refreshing'))\n self.salus:getProperties(getPropertiesCallback, getFailCallback)\nend\n\nfunction QuickApp:searchEvent(param)\n self:debug(self.i18n:get('searching-devices'))\n self:updateView(\"button2_1\", \"text\", self.i18n:get('searching-devices'))\n local searchDevicesCallback = function(gateways)\n -- QuickApp:debug(json.encode(gateways))\n self:updateView(\"button2_1\", \"text\", self.i18n:get('search-devices'))\n -- printing results\n for _, gateway in pairs(gateways) do\n QuickApp:trace(string.format(self.i18n:get('search-row-gateway'), gateway.name, gateway.id))\n QuickApp:trace(string.format(self.i18n:get('search-row-gateway-devices'), #gateway.devices))\n for __, device in ipairs(gateway.devices) do\n QuickApp:trace(string.format(self.i18n:get('search-row-device'), device.name, device.id, device.model))\n end\n end\n self:updateView(\"label2\", \"text\", string.format(self.i18n:get('check-logs'), 'QUICKAPP' .. self.id))\n end\n self.salus:searchDevices(searchDevicesCallback)\nend\n" + "isOpen": true, + "content": "--[[\nSalus IT600 thermostats integration\n@author ikubicki\n@version 2.0.0\n]]\n\nlocal REFRESH_BUTTON = \"button2_2\"\nlocal SEARCH_BUTTON = \"button2_1\"\nlocal STATUS_LABEL = \"label1\"\nlocal STATS_LABEL = \"label2\"\nlocal TITLE_LABEL = \"title\"\n\nfunction QuickApp:onInit()\n self.config = Config:new(self)\n self.failover = false\n -- self.sdk = SalusCloud:new(self.config)\n self.sdk = SalusProxy:new(self.config)\n self.i18n = i18n:new(api.get(\"/settings/info\").defaultLanguage)\n self:trace('')\n self:trace(string.format(self.i18n:get('name'), self.name))\n self:updateProperty('manufacturer', 'Salus')\n self:updateProperty('model', 'IT600')\n self.childrenIds = {}\n if not Utils:contains(self.interfaces, \"thermostatOperatingState\") then\n self:addInterfaces({\"thermostatOperatingState\"})\n end\n\n self:searchEvent()\n if self.config:getDeviceID() then\n self:updateView(\"deviceSelect\", \"selectedItem\", self.config:getDeviceID())\n end\n\n self:updateProperty(\"supportedThermostatModes\", {\"Off\", \"Heat\", \"Auto\"})\n self:updateProperty(\"heatingThermostatSetpointCapabilitiesMax\", 35)\n self:updateProperty(\"heatingThermostatSetpointCapabilitiesMin\", 15)\n self:updateProperty(\"autoThermostatSetpointCapabilitiesMax\", 35)\n self:updateProperty(\"autoThermostatSetpointCapabilitiesMin\", 15)\n\n self:updateView(TITLE_LABEL, \"text\", string.format(self.i18n:get('name'), self.name))\n self:updateView(SEARCH_BUTTON, \"text\", self.i18n:get('search-devices'))\n self:updateView(REFRESH_BUTTON, \"text\", self.i18n:get('refresh'))\n\n self:initChildDevices({\n [\"com.fibaro.temperatureSensor\"] = SalusTemperature,\n [\"com.fibaro.humiditySensor\"] = SalusHumidity,\n })\n for id, device in pairs(self.childDevices) do\n self.childrenIds[device.type] = id\n end\n\n if string.len(self.config:getDeviceID()) > 10 then\n if self.childrenIds[\"com.fibaro.temperatureSensor\"] == nil then\n local child = self:createChildDevice({\n name = self.name .. ' Temperature',\n type = \"com.fibaro.temperatureSensor\",\n }, SalusTemperature)\n end\n if self.childrenIds[\"com.fibaro.humiditySensor\"] == nil then\n local child = self:createChildDevice({\n name = self.name .. ' Humidity',\n type = \"com.fibaro.humiditySensor\",\n }, SalusHumidity)\n end\n self:run()\n else \n self:updateView(STATS_LABEL, \"text\", self.i18n:get('not-configured'))\n end\nend\n\nfunction QuickApp:setThermostatMode(mode)\n self:updateProperty(\"thermostatMode\", mode)\n local ok = function(r)\n self:updateView(STATUS_LABEL, \"text\", \"Setpoint updated\")\n self:pullDataFromCloud()\n end\n local nok = function(r)\n self:updateView(STATUS_LABEL, \"text\", r.status .. \": Unable to update thermostat mode\")\n end\n self.sdk:setHoldtype(SalusUtils:translateMode(mode), ok, nok)\nend\n\nfunction QuickApp:setHeatingThermostatSetpoint(value) \n self:updateProperty(\"heatingThermostatSetpoint\", value)\n local ok = function(response)\n self:updateView(STATUS_LABEL, \"text\", \"Thermostat mode updated\")\n self:pullDataFromCloud()\n end\n local nok = function(r)\n self:updateView(STATUS_LABEL, \"text\", r.status .. \": Unable to update thermostat setpoint\")\n end\n self.sdk:setHeatingSetpoint(value, ok, nok)\nend\n\nfunction QuickApp:refreshEvent(event)\n self:updateView(STATUS_LABEL, \"text\", self.i18n:get('refreshing'))\n self:pullDataFromCloud()\nend\n\nfunction QuickApp:run()\n self:pullDataFromCloud()\n local interval = self.config:getInterval()\n if self.failover then\n interval = 3600000\n end\n if (interval > 0) then\n fibaro.setTimeout(interval, function() self:run() end)\n end\nend\n\nfunction QuickApp:pullDataFromCloud()\n local nok = function(error)\n self:updateView(REFRESH_BUTTON, \"text\", self.i18n:get('refresh'))\n self:updateView(STATUS_LABEL, \"text\", error.status .. \": Unable to pull device data\")\n self:updateView(STATS_LABEL, \"text\", \"\")\n self.failover = true\n end\n local ok = function(properties)\n local label2Text = \"\"\n self.failover = false\n self:updateView(TITLE_LABEL, \"text\", \"Salus \" .. properties.model .. ' - ' .. properties.name)\n self:updateView(REFRESH_BUTTON, \"text\", self.i18n:get('refresh'))\n if self.childrenIds[\"com.fibaro.temperatureSensor\"] ~= nil then\n self.childDevices[self.childrenIds[\"com.fibaro.temperatureSensor\"]]:setValue(properties.temperature)\n label2Text = properties.temperature .. \"C / \" .. properties.heatingSetpoint .. \"C\" \n end\n if self.childrenIds[\"com.fibaro.humiditySensor\"] ~= nil then\n self.childDevices[self.childrenIds[\"com.fibaro.humiditySensor\"]]:setValue(properties.humidity)\n label2Text = label2Text .. \" / \" .. properties.humidity .. \"%\"\n end\n local operatingState = 'Idle'\n local isRunningValue = 0\n if properties.running and properties.running > 0 then\n operatingState = 'Heating'\n isRunningValue = 1\n end\n self:updateProperty(\"thermostatOperatingState\", operatingState)\n if isRunningValue > 0 then\n label2Text = self.i18n:get('heating') .. \" / \" .. label2Text\n else\n label2Text = self.i18n:get('off') .. \" / \" .. label2Text\n end\n\n self:updateProperty(\"thermostatMode\", SalusUtils:translateHoldType(properties.holdtype))\n self:updateProperty(\"heatingThermostatSetpoint\", properties.heatingSetpoint)\n self:updateView(STATUS_LABEL, \"text\", string.format(self.i18n:get('last-update'), os.date('%Y-%m-%d %H:%M:%S')))\n self:updateView(STATS_LABEL, \"text\", label2Text)\n if properties.battery ~= nil then\n self:updateProperty(\"batteryLevel\", SalusUtils:translateBattery(properties.battery))\n if not Utils:contains(self.interfaces, \"battery\") then\n self:addInterfaces({\"battery\"})\n end\n else\n if not Utils:contains(self.interfaces, \"power\") then\n self:addInterfaces({\"power\"})\n end\n end\n end\n self:updateView(REFRESH_BUTTON, \"text\", self.i18n:get('refreshing'))\n self.sdk:getProperties(ok, nok)\nend\n\nfunction QuickApp:searchEvent(param)\n self:updateView(STATUS_LABEL, \"text\", self.i18n:get('searching-devices'))\n self:updateView(SEARCH_BUTTON, \"text\", self.i18n:get('searching-devices'))\n local searchDevicesCallback = function(gateways)\n -- QuickApp:debug(json.encode(gateways))\n self:updateView(SEARCH_BUTTON, \"text\", self.i18n:get('search-devices'))\n local options = {}\n for _, gateway in pairs(gateways) do\n -- do nothing with gateway anymore\n for __, device in pairs(gateway.devices) do\n table.insert(options, {\n type = 'option',\n text = device.model .. ' - ' .. device.name,\n value = device.id,\n })\n end\n end\n self:updateView(\"deviceSelect\", \"options\", options)\n self:updateView(STATUS_LABEL, \"text\", string.format(self.i18n:get('check-select')))\n end\n local nok = function(r)\n self:updateView(SEARCH_BUTTON, \"text\", self.i18n:get('search-devices'))\n self:updateView(STATUS_LABEL, \"text\", r.status .. \": Unable to pull devices\")\n end\n self.sdk:searchDevices(searchDevicesCallback, nok)\nend\n\nfunction QuickApp:selectDeviceEvent(args)\n self:setVariable('DeviceID', args.values[1])\n self:updateView(STATS_LABEL, \"text\", \"\")\n self:updateView(STATUS_LABEL, \"text\", self.i18n:get('device-selected'))\n self.config:setDeviceID(args.values[1])\n local QA = self\n local ok = function(p)\n if self.childrenIds[\"com.fibaro.temperatureSensor\"] ~= nil then\n self.childDevices[self.childrenIds[\"com.fibaro.temperatureSensor\"]]:setName(\n string.format(self.i18n:get('temperature-suffix'), p.name)\n )\n end\n if self.childrenIds[\"com.fibaro.humiditySensor\"] ~= nil then\n self.childDevices[self.childrenIds[\"com.fibaro.humiditySensor\"]]:setName(\n string.format(self.i18n:get('humidity-suffix'), p.name)\n )\n end\n self:setName(p.name)\n\n end\n local nok = function(r)\n self:updateView(STATUS_LABEL, \"text\", r.status .. \": Unable to pull selected device information\")\n end\n\n self.sdk:getProperties(ok, nok)\n\nend\n\nfunction QuickApp:setName(name)\n api.put('/devices/' .. self.id, {\n name = name,\n })\nend\n" }, { "name": "Config", "isMain": false, "isOpen": false, - "content": "--[[\nConfiguration handler\n@author ikubicki\n]]\nclass 'Config'\n\nfunction Config:new(app)\n self.app = app\n self:init()\n return self\nend\n\nfunction Config:getUsername()\n if self.username and self.username:len() > 3 then\n return self.username\n end\n return nil\nend\n\nfunction Config:getPassword()\n return self.password\nend\n\nfunction Config:getDeviceID()\n return self.device_id\nend\n\nfunction Config:setDeviceID(device_id)\n self.device_id = device_id\nend\n\nfunction Config:getInterval()\n return tonumber(self.interval) * 1000\nend\n\n--[[\nThis function takes variables and sets as global variables if those are not set already.\nThis way, adding other devices might be optional and leaves option for users, \nwhat they want to add into HC3 virtual devices.\n]]\nfunction Config:init()\n self.username = self.app:getVariable('Username')\n self.password = self.app:getVariable('Password')\n self.device_id = self.app:getVariable('DeviceID')\n self.interval = self.app:getVariable('Interval')\n\n local storedUsername = Globals:get('salus_username', '')\n local storedPassword = Globals:get('salus_password', '')\n\n -- handling username\n if string.len(self.username) < 4 and string.len(storedUsername) > 3 then\n self.app:setVariable(\"Username\", storedUsername)\n self.username = storedUsername\n elseif (storedUsername == '' and self.username) then\n Globals:set('salus_username', self.username)\n end\n -- handling password\n if string.len(self.password) < 4 and string.len(storedPassword) > 3 then\n self.app:setVariable(\"Password\", storedPassword)\n self.password = storedPassword\n elseif (storedPassword == '' and self.password) then\n Globals:set('salus_password', self.password)\n end\n -- handling interval\n if not self.interval or self.interval == \"\" then\n self.app:setVariable(\"Interval\", 30)\n self.interval = 30\n end\nend" + "content": "--[[\nConfiguration handler\n@author ikubicki\n]]\nclass 'Config'\n\nfunction Config:new(app)\n self.app = app\n self.user = nil\n self.password = nil\n self.host = nil\n self.port = nil\n self.device_id = nil\n self.interval = nil\n self:init()\n return self\nend\n\nfunction Config:getUser()\n if self.user and self.user:len() > 3 then\n return self.user\n end\n return nil\nend\n\nfunction Config:getPassword()\n return self.password\nend\n\nfunction Config:getHost()\n if self.host and self.host:len() > 3 then\n return self.host\n end\n return nil\nend\n\nfunction Config:getPort()\n if self.port and string.len(self.port) > 1 then\n return self.port\n end\n return '80'\nend\n\nfunction Config:getDeviceID()\n return self.device_id\nend\n\nfunction Config:setDeviceID(device_id)\n self.device_id = device_id\nend\n\nfunction Config:getInterval()\n return tonumber(self.interval) * 1000\nend\n\n--[[\nThis function takes variables and sets as global variables if those are not set already.\nThis way, adding other devices might be optional and leaves option for users, \nwhat they want to add into HC3 virtual devices.\n]]\nfunction Config:init()\n self.user = self.app:getVariable('User')\n self.password = self.app:getVariable('Password')\n self.host = self.app:getVariable('Host')\n self.port = self.app:getVariable('Port')\n self.device_id = self.app:getVariable('DeviceID')\n self.interval = self.app:getVariable('Interval')\n\n local storedUsername = Globals:get('salus_username', '')\n local storedPassword = Globals:get('salus_password', '')\n local storedHost = Globals:get('salus_host', '')\n local storedPort = tonumber(Globals:get('salus_port', ''))\n\n -- handling username\n if string.len(self.user) < 4 and string.len(storedUsername) > 3 then\n self.app:setVariable(\"User\", storedUsername)\n self.user = storedUsername\n elseif (storedUsername == '' and self.user) then\n Globals:set('salus_username', self.user)\n end\n -- handling password\n if string.len(self.password) < 4 and string.len(storedPassword) > 3 then\n self.app:setVariable(\"Password\", storedPassword)\n self.password = storedPassword\n elseif (storedPassword == '' and self.password) then\n Globals:set('salus_password', self.password)\n end\n -- handling host\n if string.len(self.host) < 4 and string.len(storedHost) > 3 then\n self.app:setVariable(\"Host\", storedHost)\n self.host = storedHost\n elseif (storedHost == '' and self.host) then\n Globals:set('salus_host', self.host)\n end\n -- handling port\n if string.len(self.host) < 2 and string.len(storedPort) > 3 then\n self.app:setVariable(\"Port\", storedPort)\n self.port = storedPort\n elseif (storedPort == '' and self.port) then\n Globals:set('salus_port', self.port)\n end\n -- handling interval\n if not self.interval or self.interval == \"\" then\n self.app:setVariable(\"Interval\", 5)\n self.interval = 5\n end\nend" }, { "name": "HTTPClient", @@ -174,25 +543,43 @@ "name": "i18n", "isMain": false, "isOpen": false, - "content": "--[[\nInternationalization tool\n@author ikubicki\n]]\nclass 'i18n'\n\nfunction i18n:new(langCode)\n if phrases[langCode] == nil then\n langCode = 'en'\n end\n self.phrases = phrases[langCode]\n return self\nend\n\nfunction i18n:get(key)\n if self.phrases[key] then\n return self.phrases[key]\n end\n return key\nend\n\nphrases = {\n pl = {\n ['name'] = 'Salus IT600 - %s',\n ['search-devices'] = 'Szukaj urządzeń',\n ['searching-devices'] = 'Szukam...',\n ['refresh'] = 'Odśwież dane',\n ['refreshing'] = 'Odświeżam...',\n ['device-updated'] = 'Zaktualizowano dane urządzenia',\n ['last-update'] = 'Ostatnia aktualizacja: %s',\n ['not-configured'] = 'Urządzenie nie skonfigurowane',\n ['check-logs'] = 'Zakończono wyszukiwanie. Sprawdź logi tego urządzenia: %s',\n ['search-row-gateway'] = '__ BRAMKA %s (# %s)',\n ['search-row-gateway-devices'] = '__ Wykryto %d urządzeń',\n ['search-row-device'] = '____ URZĄDZENIE %s (DeviceID: %s, Model: %s)',\n ['heating'] = 'Grzanie',\n ['off'] = 'Wyłączony',\n },\n en = {\n ['name'] = 'Salus IT600 - %s',\n ['search-devices'] = 'Search devices',\n ['searching-devices'] = 'Searching...',\n ['refresh'] = 'Update data',\n ['refreshing'] = 'Updating...',\n ['device-updated'] = 'Device updates',\n ['last-update'] = 'Last update: %s',\n ['not-configured'] = 'Device not configured',\n ['check-logs'] = 'Check device logs (%s) for search results',\n ['search-row-gateway'] = '__ GATEWAY %s (# %s)',\n ['search-row-gateway-devices'] = '__ %d devices found',\n ['search-row-device'] = '____ DEVICE %s (DeviceID: %s, Model: %s)',\n ['heating'] = 'Heating',\n ['off'] = 'Off',\n },\n}" + "content": "--[[\nInternationalization tool\n@author ikubicki\n]]\nclass 'i18n'\n\nfunction i18n:new(langCode)\n if phrases[langCode] == nil then\n langCode = 'en'\n end\n self.phrases = phrases[langCode]\n return self\nend\n\nfunction i18n:get(key)\n if self.phrases[key] then\n return self.phrases[key]\n end\n return key\nend\n\nphrases = {\n pl = {\n ['name'] = 'Salus - %s',\n ['search-devices'] = 'Szukaj urządzeń',\n ['searching-devices'] = 'Szukam...',\n ['refresh'] = 'Odśwież dane',\n ['refreshing'] = 'Odświeżam...',\n ['device-updated'] = 'Zaktualizowano dane urządzenia',\n ['last-update'] = 'Ostatnia aktualizacja: %s',\n ['not-configured'] = 'Urządzenie nie skonfigurowane',\n ['check-select'] = 'Zakończono wyszukiwanie. Zaktualizowano pole wyboru powyżej. Może być wymagane zamknięcie i ponowne otwarcie panelu tego urządzenia.',\n ['device-selected'] = 'Wybrano urządzenie.',\n ['heating'] = 'Grzanie',\n ['off'] = 'Wyłączony',\n ['temperature-suffix'] = '%s - Temperatura',\n ['humidity-suffix'] = '%s - Wilgotność',\n },\n en = {\n ['name'] = 'Salus - %s',\n ['search-devices'] = 'Search devices',\n ['searching-devices'] = 'Searching...',\n ['refresh'] = 'Update data',\n ['refreshing'] = 'Updating...',\n ['device-updated'] = 'Device updates',\n ['last-update'] = 'Last update: %s',\n ['not-configured'] = 'Device not configured',\n ['check-select'] = 'Search complete. Select field above have been updated. Reopening a popup of this device might be necessary.',\n ['device-selected'] = 'Device selected.',\n ['heating'] = 'Heating',\n ['off'] = 'Off',\n ['temperature-suffix'] = '%s - Temperature',\n ['humidity-suffix'] = '%s - Temperature',\n },\n}" + }, + { + "name": "SalusCloud", + "isMain": false, + "isOpen": false, + "content": "--[[\nSalus IT600 Cloud SDK\n@author ikubicki\n]]\nclass 'SalusCloud'\n\nfunction SalusCloud:new(config)\n self.user = config:getUser()\n self.pass = config:getPassword()\n self.device_id = config:getDeviceID()\n self.token = Globals:get('salus_token', '')\n self.token_time = tonumber(Globals:get('salus_token_time', 0))\n self.http = HTTPClient:new({\n baseUrl = 'https://eu.salusconnect.io'\n })\n return self\nend\n\nfunction SalusCloud:getProperties(callback, failCallback)\n local properties = {}\n local batteryLevelCallback = function(response)\n properties[\"battery\"] = response.value\n callback(properties)\n end\n local holdtypeCallback = function(response)\n properties[\"holdtype\"] = response.value\n SalusCloud:batteryLevel(batteryLevelCallback, failCallback)\n end\n local runningCallback = function(response)\n properties[\"running\"] = response.value\n SalusCloud:holdtype(holdtypeCallback, failCallback)\n end\n local humidityCallback = function(response)\n properties[\"humidity\"] = response.value\n SalusCloud:running(runningCallback, failCallback)\n end\n local heatingSetpointCallback = function(response)\n properties[\"heatingSetpoint\"] = response.value / 100\n SalusCloud:humidity(humidityCallback, failCallback)\n end\n local temperatureCallback = function(response)\n properties[\"temperature\"] = response.value / 100\n SalusCloud:heatingSetpoint(heatingSetpointCallback, failCallback)\n end\n local authCallback = function(response)\n SalusCloud:temperature(temperatureCallback, failCallback)\n end\n SalusCloud:auth(authCallback, failCallback)\nend\n\nfunction SalusCloud:searchDevices(callback)\n local buildGateway = function(data) \n return {\n id = data.dsn,\n name = data.product_name,\n ip = data.lan_ip,\n devices = {}\n }\n end\n local buildDevice = function(data)\n return {\n id = data.dsn,\n name = data.product_name,\n model = data.oem_model,\n }\n end\n local listDevicesCallback = function(response)\n QuickApp:debug('OK');\n local gateways = {}\n -- gateways\n for _, d in ipairs(response) do\n if d.device.device_type == 'Gateway' then\n gateways[d.device.dsn] = buildGateway(d.device)\n end\n end\n -- devices\n for _, d in ipairs(response) do\n if d.device.dsn ~= d.device.product_name and d.device.device_type == 'Node' and gateways[d.device.gateway_dsn] ~= nil then\n table.insert(gateways[d.device.gateway_dsn].devices, buildDevice(d.device))\n end\n end\n callback(gateways)\n end\n local authCallback = function(response)\n SalusCloud:listDevices(listDevicesCallback)\n end\n SalusCloud:auth(authCallback)\nend\n\nfunction SalusCloud:batteryLevel(callback, failCallback, attempt)\n if attempt == nil then\n attempt = 1\n end\n local fail = function(response)\n if response.status == 404 then\n return callback({})\n end\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull battery level')\n SalusCloud:setToken('')\n -- QuickApp:debug(json.encode(response))\n \n if attempt < 2 then\n attempt = attempt + 1\n fibaro.setTimeout(3000, function()\n QuickApp:debug('SalusCloud:batteryLevel - Retry attempt #' .. attempt)\n local authCallback = function(response)\n self:batteryLevel(callback, failCallback, attempt)\n end\n SalusCloud:auth(authCallback, failCallback)\n end)\n end\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:BatteryLevel.json\"\n local headers = {\n Authorization = \"Bearer \" .. SalusCloud:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction SalusCloud:temperature(callback, failCallback, attempt)\n if attempt == nil then\n attempt = 1\n end\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull temperature')\n SalusCloud:setToken('')\n --QuickApp:debug(json.encode(response))\n \n if attempt < 2 then\n attempt = attempt + 1\n fibaro.setTimeout(3000, function()\n QuickApp:debug('SalusCloud:temperature - Retry attempt #' .. attempt)\n local authCallback = function(response)\n self:temperature(callback, failCallback, attempt)\n end\n SalusCloud:auth(authCallback, failCallback)\n end)\n end\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:LocalTemperature_x100.json\"\n local headers = {\n Authorization = \"Bearer \" .. SalusCloud:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction SalusCloud:heatingSetpoint(callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull heating setpoint')\n SalusCloud:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:HeatingSetpoint_x100.json\"\n local headers = {\n Authorization = \"Bearer \" .. SalusCloud:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction SalusCloud:setHeatingSetpoint(heatingSetpoint, callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to update heatingSetpoint')\n SalusCloud:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:SetHeatingSetpoint_x100/datapoints.json\"\n local headers = {\n Authorization = \"Bearer \" .. SalusCloud:getToken(),\n [\"Content-Type\"] = \"application/json\",\n }\n local data = {\n datapoint = {\n value = heatingSetpoint * 100\n }\n }\n self.http:post(url, data, success, fail, headers)\nend\n\nfunction SalusCloud:humidity(callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull humidity')\n SalusCloud:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:SunnySetpoint_x100.json\"\n local headers = {\n Authorization = \"Bearer \" .. SalusCloud:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction SalusCloud:running(callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull mode')\n SalusCloud:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:RunningState.json\"\n local headers = {\n Authorization = \"Bearer \" .. SalusCloud:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction SalusCloud:holdtype(callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull mode')\n SalusCloud:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:HoldType.json\"\n local headers = {\n Authorization = \"Bearer \" .. SalusCloud:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction SalusCloud:setHoldtype(holdtype, callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to update holdtype')\n SalusCloud:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:SetHoldType/datapoints.json\"\n local headers = {\n Authorization = \"Bearer \" .. SalusCloud:getToken(),\n [\"Content-Type\"] = \"application/json\",\n }\n local data = {\n datapoint = {\n value = holdtype\n }\n }\n self.http:post(url, data, success, fail, headers)\nend\n\nfunction SalusCloud:listDevices(callback, fail, attempt)\n if attempt == nil then\n attempt = 1\n end\n if fail == nil then\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull devices')\n SalusCloud:setToken('')\n \n if attempt < 2 then\n attempt = attempt + 1\n fibaro.setTimeout(3000, function()\n QuickApp:debug('SalusCloud:listDevices - Retry attempt #' .. attempt)\n local authCallback = function(response)\n self:listDevices(callback, nil, attempt)\n end\n SalusCloud:auth(authCallback)\n end)\n end\n end\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data)\n end\n end\n local url = \"/apiv1/devices.json\"\n local headers = {\n Authorization = \"Bearer \" .. SalusCloud:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction SalusCloud:auth(callback, failCallback)\n if string.len(self.token) > 1 then\n -- QuickApp:debug('Already authenticated')\n if callback ~= nil then\n callback({})\n end\n return\n end\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to authenticate')\n SalusCloud:setToken('')\n end\n local success = function(response)\n -- QuickApp:debug(json.encode(response))\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n SalusCloud:setToken(data.access_token)\n if callback ~= nil then\n callback(data)\n end\n end\n local url = \"/users/sign_in.json\"\n local headers = {\n [\"Content-Type\"] = \"application/json\"\n }\n local data = {\n user = {\n email = self.user,\n password = self.pass,\n }\n }\n self.http:post(url, data, success, fail, headers)\nend\n\nfunction SalusCloud:setToken(token)\n self.token = token\n self.token_time = os.time(os.date(\"!*t\"))\n Globals:set('salus_token', token)\n Globals:set('salus_token_time', self.token_time)\nend\n\nfunction SalusCloud:getToken()\n if not self:checkTokenTime() then\n self:setToken('')\n return ''\n end\n if string.len(self.token) > 10 then\n return self.token\n elseif string.len(Globals:get('salus_token', '')) > 10 then\n return Globals:get('salus_token', '')\n end\n return ''\nend\n\nfunction SalusCloud:checkTokenTime()\n if self.token_time < 1 then\n self.token_time = tonumber(Globals:get('salus_token_time', 0))\n end\n return self.token_time > 0 and os.time(os.date(\"!*t\")) - self.token_time < 43200\nend\n\n" + }, + { + "name": "Utils", + "isMain": false, + "isOpen": false, + "content": "--[[\nLUA utilities\n@author ikubicki\n]]\nclass 'Utils'\n\nfunction Utils:new()\n return self\nend\n\nfunction Utils:contains(a, n)\n for k, v in pairs(a) do\n if v == n then\n return k\n end\n end\n return false\nend\n\n-- borrowed from https://github.com/jangabrielsson/TQAE\nfunction Utils:base64(data)\n __assert_type(data,\"string\")\n local bC='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'\n return ((data:gsub('.', function(x) \n local r,b='',x:byte() for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end\n return r;\n end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)\n if (#x < 6) then return '' end\n local c=0\n for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end\n return bC:sub(c+1,c+1)\n end)..({ '', '==', '=' })[#data%3+1])\nend" + }, + { + "name": "SalusProxy", + "isMain": false, + "isOpen": false, + "content": "--[[\nSalus IT600 Proxy SDK\n@author ikubicki\n]]\nclass 'SalusProxy'\n\nfunction SalusProxy:new(config)\n self.config = config\n self.user = config:getUser()\n self.pass = config:getPassword()\n self.device_id = config:getDeviceID()\n self.token = Utils:base64(config:getUser() .. ':' .. config:getPassword())\n self.http = HTTPClient:new({\n baseUrl = 'http://' .. config:getHost() .. ':' .. config:getPort() .. '/api/v1'\n })\n return self\nend\n\nfunction SalusProxy:getProperties(ok, nok)\n local h = function(r)\n if (r.status > 200) then\n return nok(r)\n end\n local data = json.decode(r.data)\n local running = 0\n if data.isRunning then running = 1 end\n return ok({\n id = data.id,\n name = data.name,\n model = data.model,\n heatingSetpoint = data.temperature,\n temperature = data.currentTemperature,\n humidity = data.humidity,\n running = running,\n holdtype = data.mode,\n battery = data.battery,\n })\n end\n local headers = {\n Authorization = 'Basic ' .. self.token,\n ['Content-Type'] = 'application/json',\n }\n if not nok then nok = function() end end\n self.http:get('/devices/' .. self.config:getDeviceID(), h, nok, headers)\nend\n\nfunction SalusProxy:searchDevices(ok, nok)\n local h = function(r)\n if (r.status > 200) then\n return nok(r)\n end\n return ok({{\n name = 'proxy',\n devices = json.decode(r.data)\n }})\n end\n local headers = {\n Authorization = 'Basic ' .. self.token,\n ['Content-Type'] = 'application/json',\n }\n if not nok then nok = function() end end\n self.http:get('/devices', h, nok, headers)\nend\n\nfunction SalusProxy:setHeatingSetpoint(setpoint, ok, nok)\nlocal h = function(r)\n if (r.status > 200) then\n return nok(r)\n end\n return ok({{\n name = 'proxy',\n devices = json.decode(r.data)\n }})\n end\n local headers = {\n Authorization = 'Basic ' .. self.token,\n ['Content-Type'] = 'application/json',\n }\n local d = {\n temperature = setpoint,\n }\n if not nok then nok = function() end end\n self.http:post('/devices/' .. self.config:getDeviceID() .. '/temperature', d, h, nok, headers)\nend\n\n\nfunction SalusProxy:setHoldtype(holdtype, ok, nok)\n local h = function(r)\n if (r.status > 200) then\n return nok(r)\n end\n return ok({{\n name = 'proxy',\n devices = json.decode(r.data)\n }})\n end\n local headers = {\n Authorization = 'Basic ' .. self.token,\n ['Content-Type'] = 'application/json',\n }\n local d = {\n mode = holdtype,\n }\n if not nok then nok = function() end end\n self.http:post('/devices/' .. self.config:getDeviceID() .. '/mode', d, h, nok, headers)\nend" }, { - "name": "Salus", + "name": "SalusTemperature", "isMain": false, "isOpen": false, - "content": "--[[\nSalus IT600 SDK\n@author ikubicki\n]]\nclass 'Salus'\n\nfunction Salus:new(config)\n self.user = config:getUsername()\n self.pass = config:getPassword()\n self.device_id = config:getDeviceID()\n self.token = Globals:get('salus_token', '')\n self.token_time = tonumber(Globals:get('salus_token_time', 0))\n self.http = HTTPClient:new({\n baseUrl = 'https://eu.salusconnect.io'\n })\n return self\nend\n\nfunction Salus:getProperties(callback, failCallback)\n local properties = {}\n local batteryLevelCallback = function(response)\n properties[\"battery\"] = response.value\n callback(properties)\n end\n local holdtypeCallback = function(response)\n properties[\"holdtype\"] = response.value\n Salus:batteryLevel(batteryLevelCallback, failCallback)\n end\n local runningCallback = function(response)\n properties[\"running\"] = response.value\n Salus:holdtype(holdtypeCallback, failCallback)\n end\n local humidityCallback = function(response)\n properties[\"humidity\"] = response.value\n Salus:running(runningCallback, failCallback)\n end\n local heatingSetpointCallback = function(response)\n properties[\"heatingSetpoint\"] = response.value / 100\n Salus:humidity(humidityCallback, failCallback)\n end\n local temperatureCallback = function(response)\n properties[\"temperature\"] = response.value / 100\n Salus:heatingSetpoint(heatingSetpointCallback, failCallback)\n end\n local authCallback = function(response)\n Salus:temperature(temperatureCallback, failCallback)\n end\n Salus:auth(authCallback, failCallback)\nend\n\nfunction Salus:searchDevices(callback)\n local buildGateway = function(data) \n return {\n id = data.dsn,\n name = data.product_name,\n ip = data.lan_ip,\n devices = {}\n }\n end\n local buildDevice = function(data)\n return {\n id = data.dsn,\n name = data.product_name,\n model = data.oem_model,\n }\n end\n local listDevicesCallback = function(response)\n QuickApp:debug('OK');\n local gateways = {}\n -- gateways\n for _, d in ipairs(response) do\n if d.device.device_type == 'Gateway' then\n gateways[d.device.dsn] = buildGateway(d.device)\n end\n end\n -- devices\n for _, d in ipairs(response) do\n if d.device.dsn ~= d.device.product_name and d.device.device_type == 'Node' and gateways[d.device.gateway_dsn] ~= nil then\n table.insert(gateways[d.device.gateway_dsn].devices, buildDevice(d.device))\n end\n end\n callback(gateways)\n end\n local authCallback = function(response)\n Salus:listDevices(listDevicesCallback)\n end\n Salus:auth(authCallback)\nend\n\nfunction Salus:batteryLevel(callback, failCallback, attempt)\n if attempt == nil then\n attempt = 1\n end\n local fail = function(response)\n if response.status == 404 then\n return callback({})\n end\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull battery level')\n Salus:setToken('')\n -- QuickApp:debug(json.encode(response))\n \n if attempt < 2 then\n attempt = attempt + 1\n fibaro.setTimeout(3000, function()\n QuickApp:debug('Salus:batteryLevel - Retry attempt #' .. attempt)\n local authCallback = function(response)\n self:batteryLevel(callback, failCallback, attempt)\n end\n Salus:auth(authCallback, failCallback)\n end)\n end\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:BatteryLevel.json\"\n local headers = {\n Authorization = \"Bearer \" .. Salus:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction Salus:temperature(callback, failCallback, attempt)\n if attempt == nil then\n attempt = 1\n end\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull temperature')\n Salus:setToken('')\n --QuickApp:debug(json.encode(response))\n \n if attempt < 2 then\n attempt = attempt + 1\n fibaro.setTimeout(3000, function()\n QuickApp:debug('Salus:temperature - Retry attempt #' .. attempt)\n local authCallback = function(response)\n self:temperature(callback, failCallback, attempt)\n end\n Salus:auth(authCallback, failCallback)\n end)\n end\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:LocalTemperature_x100.json\"\n local headers = {\n Authorization = \"Bearer \" .. Salus:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction Salus:heatingSetpoint(callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull heating setpoint')\n Salus:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:HeatingSetpoint_x100.json\"\n local headers = {\n Authorization = \"Bearer \" .. Salus:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction Salus:setHeatingSetpoint(heatingSetpoint, callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to update heatingSetpoint')\n Salus:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:SetHeatingSetpoint_x100/datapoints.json\"\n local headers = {\n Authorization = \"Bearer \" .. Salus:getToken(),\n [\"Content-Type\"] = \"application/json\",\n }\n local data = {\n datapoint = {\n value = heatingSetpoint * 100\n }\n }\n self.http:post(url, data, success, fail, headers)\nend\n\nfunction Salus:humidity(callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull humidity')\n Salus:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:SunnySetpoint_x100.json\"\n local headers = {\n Authorization = \"Bearer \" .. Salus:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction Salus:running(callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull mode')\n Salus:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:RunningState.json\"\n local headers = {\n Authorization = \"Bearer \" .. Salus:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction Salus:holdtype(callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull mode')\n Salus:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:HoldType.json\"\n local headers = {\n Authorization = \"Bearer \" .. Salus:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction Salus:setHoldtype(holdtype, callback, failCallback)\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to update holdtype')\n Salus:setToken('')\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data.property)\n end\n end\n local url = \"/apiv1/dsns/\" .. self.device_id .. \"/properties/ep_9:sIT600TH:SetHoldType/datapoints.json\"\n local headers = {\n Authorization = \"Bearer \" .. Salus:getToken(),\n [\"Content-Type\"] = \"application/json\",\n }\n local data = {\n datapoint = {\n value = holdtype\n }\n }\n self.http:post(url, data, success, fail, headers)\nend\n\nfunction Salus:listDevices(callback, fail, attempt)\n if attempt == nil then\n attempt = 1\n end\n if fail == nil then\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to pull devices')\n Salus:setToken('')\n \n if attempt < 2 then\n attempt = attempt + 1\n fibaro.setTimeout(3000, function()\n QuickApp:debug('Salus:listDevices - Retry attempt #' .. attempt)\n local authCallback = function(response)\n self:listDevices(callback, nil, attempt)\n end\n Salus:auth(authCallback)\n end)\n end\n end\n end\n local success = function(response)\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n if callback ~= nil then\n callback(data)\n end\n end\n local url = \"/apiv1/devices.json\"\n local headers = {\n Authorization = \"Bearer \" .. Salus:getToken()\n }\n self.http:get(url, success, fail, headers)\nend\n\nfunction Salus:auth(callback, failCallback)\n if string.len(self.token) > 1 then\n -- QuickApp:debug('Already authenticated')\n if callback ~= nil then\n callback({})\n end\n return\n end\n local fail = function(response)\n if failCallback then\n failCallback(json.encode(response))\n end\n QuickApp:error('Unable to authenticate')\n Salus:setToken('')\n end\n local success = function(response)\n -- QuickApp:debug(json.encode(response))\n if response.status > 299 then\n fail(response)\n return\n end\n local data = json.decode(response.data)\n Salus:setToken(data.access_token)\n if callback ~= nil then\n callback(data)\n end\n end\n local url = \"/users/sign_in.json\"\n local headers = {\n [\"Content-Type\"] = \"application/json\"\n }\n local data = {\n user = {\n email = self.user,\n password = self.pass,\n }\n }\n self.http:post(url, data, success, fail, headers)\nend\n\nfunction Salus:setToken(token)\n self.token = token\n self.token_time = os.time(os.date(\"!*t\"))\n Globals:set('salus_token', token)\n Globals:set('salus_token_time', self.token_time)\nend\n\nfunction Salus:getToken()\n if not self:checkTokenTime() then\n self:setToken('')\n return ''\n end\n if string.len(self.token) > 10 then\n return self.token\n elseif string.len(Globals:get('salus_token', '')) > 10 then\n return Globals:get('salus_token', '')\n end\n return ''\nend\n\nfunction Salus:checkTokenTime()\n if self.token_time < 1 then\n self.token_time = tonumber(Globals:get('salus_token_time', 0))\n end\n return self.token_time > 0 and os.time(os.date(\"!*t\")) - self.token_time < 43200\nend\n\nfunction Salus:translateBatteryLevel(batteryLevel)\n if batteryLevel > 4 then return 100 end;\n if batteryLevel == 4 then return 75 end;\n if batteryLevel == 3 then return 50 end;\n if batteryLevel == 2 then return 25 end;\n if batteryLevel > 2 then return 0 end;\nend" + "content": "--[[\nSalus temperature sendor child device class\n@author ikubicki\n]]\nclass 'SalusTemperature' (QuickAppChild)\n\nfunction SalusTemperature:__init(device)\n QuickAppChild.__init(self, device)\nend\n\nfunction SalusTemperature:setName(name)\n api.put('/devices/' .. self.id, {\n name = name,\n })\nend\n\nfunction SalusTemperature:setValue(value)\n self:updateProperty(\"value\", value)\nend\n" }, { - "name": "SalusChildDevice", + "name": "SalusHumidity", "isMain": false, "isOpen": false, - "content": "class 'SalusChildDevice' (QuickAppChild)\n\nfunction SalusChildDevice:__init(device)\n QuickAppChild.__init(self, device)\nend\n\nfunction SalusChildDevice:setValue(value)\n self:updateProperty(\"value\", value)\nend\n\nfunction SalusChildDevice:setState(value)\n self:updateProperty(\"state\", value > 0)\nend" + "content": "--[[\nSalus humidity sendor child device class\n@author ikubicki\n]]\n\nclass 'SalusHumidity' (QuickAppChild)\n\nfunction SalusHumidity:__init(device)\n QuickAppChild.__init(self, device)\nend\n\nfunction SalusHumidity:setName(name)\n api.put('/devices/' .. self.id, {\n name = name,\n })\nend\n\nfunction SalusHumidity:setValue(value)\n self:updateProperty(\"value\", value)\nend\n" }, { - "name": "utils", + "name": "SalusUtils", "isMain": false, "isOpen": false, - "content": "--[[\nLUA utilities\n@author ikubicki\n]]\nclass 'utils'\n\nfunction utils:new()\n return self\nend\n\nfunction utils:contains(a, n)\n for k, v in pairs(a) do\n if v == n then\n return k\n end\n end\n return false\nend" + "content": "--[[\nSalus utilities\n@author ikubicki\n]]\nclass 'SalusUtils'\n\nfunction SalusUtils:translateHoldType(holdtype)\n if holdtype == 2 then\n return 'Heat'\n elseif holdtype == 7 then\n return 'Off'\n end\n return 'Auto' -- 0 or 1\nend\n\nfunction SalusUtils:translateBattery(battery)\n if battery > 4 then return 100 end;\n if battery == 4 then return 75 end;\n if battery == 3 then return 50 end;\n if battery == 2 then return 25 end;\n if battery > 2 then return 0 end;\nend\n\nfunction SalusUtils:translateMode(mode)\n if mode == 'Off' then\n return 7\n elseif mode == 'Heat' then\n return 2\n end\n return 0\nend" } ] } \ No newline at end of file diff --git a/i18n.lua b/i18n.lua index c127013..c7ae146 100644 --- a/i18n.lua +++ b/i18n.lua @@ -21,7 +21,7 @@ end phrases = { pl = { - ['name'] = 'Salus IT600 - %s', + ['name'] = 'Salus - %s', ['search-devices'] = 'Szukaj urządzeń', ['searching-devices'] = 'Szukam...', ['refresh'] = 'Odśwież dane', @@ -29,15 +29,15 @@ phrases = { ['device-updated'] = 'Zaktualizowano dane urządzenia', ['last-update'] = 'Ostatnia aktualizacja: %s', ['not-configured'] = 'Urządzenie nie skonfigurowane', - ['check-logs'] = 'Zakończono wyszukiwanie. Sprawdź logi tego urządzenia: %s', - ['search-row-gateway'] = '__ BRAMKA %s (# %s)', - ['search-row-gateway-devices'] = '__ Wykryto %d urządzeń', - ['search-row-device'] = '____ URZĄDZENIE %s (DeviceID: %s, Model: %s)', + ['check-select'] = 'Zakończono wyszukiwanie. Zaktualizowano pole wyboru powyżej. Może być wymagane zamknięcie i ponowne otwarcie panelu tego urządzenia.', + ['device-selected'] = 'Wybrano urządzenie.', ['heating'] = 'Grzanie', ['off'] = 'Wyłączony', + ['temperature-suffix'] = '%s - Temperatura', + ['humidity-suffix'] = '%s - Wilgotność', }, en = { - ['name'] = 'Salus IT600 - %s', + ['name'] = 'Salus - %s', ['search-devices'] = 'Search devices', ['searching-devices'] = 'Searching...', ['refresh'] = 'Update data', @@ -45,11 +45,11 @@ phrases = { ['device-updated'] = 'Device updates', ['last-update'] = 'Last update: %s', ['not-configured'] = 'Device not configured', - ['check-logs'] = 'Check device logs (%s) for search results', - ['search-row-gateway'] = '__ GATEWAY %s (# %s)', - ['search-row-gateway-devices'] = '__ %d devices found', - ['search-row-device'] = '____ DEVICE %s (DeviceID: %s, Model: %s)', + ['check-select'] = 'Search complete. Select field above have been updated. Reopening a popup of this device might be necessary.', + ['device-selected'] = 'Device selected.', ['heating'] = 'Heating', ['off'] = 'Off', + ['temperature-suffix'] = '%s - Temperature', + ['humidity-suffix'] = '%s - Temperature', }, } \ No newline at end of file diff --git a/main.lua b/main.lua index 576132a..8235195 100644 --- a/main.lua +++ b/main.lua @@ -1,33 +1,48 @@ --[[ Salus IT600 thermostats integration @author ikubicki -@versio 1.3.0 +@version 2.0.0 ]] +local REFRESH_BUTTON = "button2_2" +local SEARCH_BUTTON = "button2_1" +local STATUS_LABEL = "label1" +local STATS_LABEL = "label2" +local TITLE_LABEL = "title" + function QuickApp:onInit() self.config = Config:new(self) self.failover = false - self.salus = Salus:new(self.config) + -- self.sdk = SalusCloud:new(self.config) + self.sdk = SalusProxy:new(self.config) self.i18n = i18n:new(api.get("/settings/info").defaultLanguage) self:trace('') self:trace(string.format(self.i18n:get('name'), self.name)) self:updateProperty('manufacturer', 'Salus') self:updateProperty('model', 'IT600') self.childrenIds = {} - self.interfaces = api.get("/devices/" .. self.id).interfaces + if not Utils:contains(self.interfaces, "thermostatOperatingState") then + self:addInterfaces({"thermostatOperatingState"}) + end + + self:searchEvent() + if self.config:getDeviceID() then + self:updateView("deviceSelect", "selectedItem", self.config:getDeviceID()) + end self:updateProperty("supportedThermostatModes", {"Off", "Heat", "Auto"}) self:updateProperty("heatingThermostatSetpointCapabilitiesMax", 35) - self:updateProperty("heatingThermostatSetpointCapabilitiesMin", 10) + self:updateProperty("heatingThermostatSetpointCapabilitiesMin", 15) + self:updateProperty("autoThermostatSetpointCapabilitiesMax", 35) + self:updateProperty("autoThermostatSetpointCapabilitiesMin", 15) - self:updateView("label1", "text", string.format(self.i18n:get('name'), self.name)) - self:updateView("button2_1", "text", self.i18n:get('search-devices')) - self:updateView("button2_2", "text", self.i18n:get('refresh')) + self:updateView(TITLE_LABEL, "text", string.format(self.i18n:get('name'), self.name)) + self:updateView(SEARCH_BUTTON, "text", self.i18n:get('search-devices')) + self:updateView(REFRESH_BUTTON, "text", self.i18n:get('refresh')) self:initChildDevices({ - ["com.fibaro.temperatureSensor"] = SalusChildDevice, - ["com.fibaro.humiditySensor"] = SalusChildDevice, - ["com.fibaro.binarySwitch"] = SalusChildDevice, + ["com.fibaro.temperatureSensor"] = SalusTemperature, + ["com.fibaro.humiditySensor"] = SalusHumidity, }) for id, device in pairs(self.childDevices) do self.childrenIds[device.type] = id @@ -38,52 +53,46 @@ function QuickApp:onInit() local child = self:createChildDevice({ name = self.name .. ' Temperature', type = "com.fibaro.temperatureSensor", - }, SalusChildDevice) + }, SalusTemperature) end if self.childrenIds["com.fibaro.humiditySensor"] == nil then local child = self:createChildDevice({ name = self.name .. ' Humidity', type = "com.fibaro.humiditySensor", - }, SalusChildDevice) - end - if self.childrenIds["com.fibaro.binarySwitch"] == nil then - local child = self:createChildDevice({ - name = self.name .. ' Heating', - type = "com.fibaro.binarySwitch", - deviceRole = 'Valve', - isLight = false, - }, SalusChildDevice) + }, SalusHumidity) end self:run() else - self:updateView("label1", "text", self.i18n:get('not-configured')) + self:updateView(STATS_LABEL, "text", self.i18n:get('not-configured')) end end function QuickApp:setThermostatMode(mode) self:updateProperty("thermostatMode", mode) - local holdtype = 0 - if mode == 'Off' then - holdtype = 7 - elseif mode == 'Heat' then - holdtype = 2 - end - local setHoldtypeCallback = function(response) + local ok = function(r) + self:updateView(STATUS_LABEL, "text", "Setpoint updated") self:pullDataFromCloud() end - self.salus:setHoldtype(holdtype, setHoldtypeCallback) + local nok = function(r) + self:updateView(STATUS_LABEL, "text", r.status .. ": Unable to update thermostat mode") + end + self.sdk:setHoldtype(SalusUtils:translateMode(mode), ok, nok) end function QuickApp:setHeatingThermostatSetpoint(value) self:updateProperty("heatingThermostatSetpoint", value) - local setHeatingSetpointCallback = function(response) + local ok = function(response) + self:updateView(STATUS_LABEL, "text", "Thermostat mode updated") self:pullDataFromCloud() end - self.salus:setHeatingSetpoint(value, setHeatingSetpointCallback) + local nok = function(r) + self:updateView(STATUS_LABEL, "text", r.status .. ": Unable to update thermostat setpoint") + end + self.sdk:setHeatingSetpoint(value, ok, nok) end function QuickApp:refreshEvent(event) - self:updateView("label", "text", self.i18n:get('refreshing')) + self:updateView(STATUS_LABEL, "text", self.i18n:get('refreshing')) self:pullDataFromCloud() end @@ -91,7 +100,7 @@ function QuickApp:run() self:pullDataFromCloud() local interval = self.config:getInterval() if self.failover then - interval = 300000 + interval = 3600000 end if (interval > 0) then fibaro.setTimeout(interval, function() self:run() end) @@ -99,17 +108,17 @@ function QuickApp:run() end function QuickApp:pullDataFromCloud() - local getFailCallback = function(error) - self:updateView("button2_2", "text", self.i18n:get('refresh')) - self:updateView("label1", "text", "API Error: " .. error) - self:updateView("label2", "text", "") + local nok = function(error) + self:updateView(REFRESH_BUTTON, "text", self.i18n:get('refresh')) + self:updateView(STATUS_LABEL, "text", error.status .. ": Unable to pull device data") + self:updateView(STATS_LABEL, "text", "") self.failover = true end - local getPropertiesCallback = function(properties) + local ok = function(properties) local label2Text = "" self.failover = false - -- QuickApp:debug(json.encode(properties)) - self:updateView("button2_2", "text", self.i18n:get('refresh')) + self:updateView(TITLE_LABEL, "text", "Salus " .. properties.model .. ' - ' .. properties.name) + self:updateView(REFRESH_BUTTON, "text", self.i18n:get('refresh')) if self.childrenIds["com.fibaro.temperatureSensor"] ~= nil then self.childDevices[self.childrenIds["com.fibaro.temperatureSensor"]]:setValue(properties.temperature) label2Text = properties.temperature .. "C / " .. properties.heatingSetpoint .. "C" @@ -118,64 +127,95 @@ function QuickApp:pullDataFromCloud() self.childDevices[self.childrenIds["com.fibaro.humiditySensor"]]:setValue(properties.humidity) label2Text = label2Text .. " / " .. properties.humidity .. "%" end - if self.childrenIds["com.fibaro.binarySwitch"] ~= nil then - local isRunningValue = 0 - if properties.running and properties.running > 0 then - isRunningValue = 1 - end - self.childDevices[self.childrenIds["com.fibaro.binarySwitch"]]:setValue(isRunningValue > 0) - if isRunningValue > 0 then - label2Text = self.i18n:get('heating') .. " / " .. label2Text - else - label2Text = self.i18n:get('off') .. " / " .. label2Text - end + local operatingState = 'Idle' + local isRunningValue = 0 + if properties.running and properties.running > 0 then + operatingState = 'Heating' + isRunningValue = 1 end - local mode = 'Auto' -- 0 or 1 - if properties.holdtype == 2 then - mode = 'Heat' - elseif properties.holdtype == 7 then - mode = 'Off' + self:updateProperty("thermostatOperatingState", operatingState) + if isRunningValue > 0 then + label2Text = self.i18n:get('heating') .. " / " .. label2Text + else + label2Text = self.i18n:get('off') .. " / " .. label2Text end - self:updateProperty("thermostatMode", mode) + + self:updateProperty("thermostatMode", SalusUtils:translateHoldType(properties.holdtype)) self:updateProperty("heatingThermostatSetpoint", properties.heatingSetpoint) - self:updateView("label1", "text", string.format(self.i18n:get('last-update'), os.date('%Y-%m-%d %H:%M:%S'))) - self:updateView("label2", "text", label2Text) - + self:updateView(STATUS_LABEL, "text", string.format(self.i18n:get('last-update'), os.date('%Y-%m-%d %H:%M:%S'))) + self:updateView(STATS_LABEL, "text", label2Text) if properties.battery ~= nil then - self:updateProperty("batteryLevel", Salus:translateBatteryLevel(properties.battery)) - if not utils:contains(self.interfaces, "battery") then - api.put("/devices/" .. self.id, { interfaces = { - "quickApp", "battery", "heatingThermostatSetpoint", "thermostatMode" - }}) + self:updateProperty("batteryLevel", SalusUtils:translateBattery(properties.battery)) + if not Utils:contains(self.interfaces, "battery") then + self:addInterfaces({"battery"}) end else - - if not utils:contains(self.interfaces, "power") then - api.put("/devices/" .. self.id, { interfaces = { - "quickApp", "power", "heatingThermostatSetpoint", "thermostatMode" - }}) + if not Utils:contains(self.interfaces, "power") then + self:addInterfaces({"power"}) end end end - self:updateView("button2_2", "text", self.i18n:get('refreshing')) - self.salus:getProperties(getPropertiesCallback, getFailCallback) + self:updateView(REFRESH_BUTTON, "text", self.i18n:get('refreshing')) + self.sdk:getProperties(ok, nok) end function QuickApp:searchEvent(param) - self:debug(self.i18n:get('searching-devices')) - self:updateView("button2_1", "text", self.i18n:get('searching-devices')) + self:updateView(STATUS_LABEL, "text", self.i18n:get('searching-devices')) + self:updateView(SEARCH_BUTTON, "text", self.i18n:get('searching-devices')) local searchDevicesCallback = function(gateways) -- QuickApp:debug(json.encode(gateways)) - self:updateView("button2_1", "text", self.i18n:get('search-devices')) - -- printing results + self:updateView(SEARCH_BUTTON, "text", self.i18n:get('search-devices')) + local options = {} for _, gateway in pairs(gateways) do - QuickApp:trace(string.format(self.i18n:get('search-row-gateway'), gateway.name, gateway.id)) - QuickApp:trace(string.format(self.i18n:get('search-row-gateway-devices'), #gateway.devices)) - for __, device in ipairs(gateway.devices) do - QuickApp:trace(string.format(self.i18n:get('search-row-device'), device.name, device.id, device.model)) + -- do nothing with gateway anymore + for __, device in pairs(gateway.devices) do + table.insert(options, { + type = 'option', + text = device.model .. ' - ' .. device.name, + value = device.id, + }) end end - self:updateView("label2", "text", string.format(self.i18n:get('check-logs'), 'QUICKAPP' .. self.id)) + self:updateView("deviceSelect", "options", options) + self:updateView(STATUS_LABEL, "text", string.format(self.i18n:get('check-select'))) + end + local nok = function(r) + self:updateView(SEARCH_BUTTON, "text", self.i18n:get('search-devices')) + self:updateView(STATUS_LABEL, "text", r.status .. ": Unable to pull devices") end - self.salus:searchDevices(searchDevicesCallback) + self.sdk:searchDevices(searchDevicesCallback, nok) +end + +function QuickApp:selectDeviceEvent(args) + self:setVariable('DeviceID', args.values[1]) + self:updateView(STATS_LABEL, "text", "") + self:updateView(STATUS_LABEL, "text", self.i18n:get('device-selected')) + self.config:setDeviceID(args.values[1]) + local QA = self + local ok = function(p) + if self.childrenIds["com.fibaro.temperatureSensor"] ~= nil then + self.childDevices[self.childrenIds["com.fibaro.temperatureSensor"]]:setName( + string.format(self.i18n:get('temperature-suffix'), p.name) + ) + end + if self.childrenIds["com.fibaro.humiditySensor"] ~= nil then + self.childDevices[self.childrenIds["com.fibaro.humiditySensor"]]:setName( + string.format(self.i18n:get('humidity-suffix'), p.name) + ) + end + self:setName(p.name) + + end + local nok = function(r) + self:updateView(STATUS_LABEL, "text", r.status .. ": Unable to pull selected device information") + end + + self.sdk:getProperties(ok, nok) + +end + +function QuickApp:setName(name) + api.put('/devices/' .. self.id, { + name = name, + }) end diff --git a/utils.lua b/utils.lua index c2582b2..ec2c455 100644 --- a/utils.lua +++ b/utils.lua @@ -2,17 +2,32 @@ LUA utilities @author ikubicki ]] -class 'utils' +class 'Utils' -function utils:new() +function Utils:new() return self end -function utils:contains(a, n) +function Utils:contains(a, n) for k, v in pairs(a) do if v == n then return k end end return false +end + +-- borrowed from https://github.com/jangabrielsson/TQAE +function Utils:base64(data) + __assert_type(data,"string") + local bC='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + return ((data:gsub('.', function(x) + local r,b='',x:byte() for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end + return r; + end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x) + if (#x < 6) then return '' end + local c=0 + for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end + return bC:sub(c+1,c+1) + end)..({ '', '==', '=' })[#data%3+1]) end \ No newline at end of file