diff --git a/L_ZWay2.lua b/L_ZWay2.lua index d04d490..1320d57 100644 --- a/L_ZWay2.lua +++ b/L_ZWay2.lua @@ -2,14 +2,14 @@ module (..., package.seeall) ABOUT = { NAME = "L_ZWay2", - VERSION = "2020.03.29", + VERSION = "2024.02.22", DESCRIPTION = "Z-Way interface for openLuup", AUTHOR = "@akbooer", - COPYRIGHT = "(c) 2013-2020 AKBooer", + COPYRIGHT = "(c) 2013-2024 AKBooer", DOCUMENTATION = "https://community.getvera.com/t/openluup-zway-plugin-for-zwave-me-hardware/193746", DEBUG = false, LICENSE = [[ - Copyright 2013-2020 AK Booer + Copyright 2013-2024 AK Booer Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -48,11 +48,28 @@ ABOUT = { -- 2020.03.16 continued refactoring ... including asynchronous HTTP requests -- 2020.03.23 improve thermostat recognition (thanks @ronluna) -- 2020.03.29 fix handling of missing class #67 in thermostats (thanks @ronluna) +-- 2020.03.30 @rafale77, pull request#24, fix Window Covering Service +-- 2020.03.31 fix x_or_y() functions to work with 1 or 0 as well as '1' or '0' (thanks @DesT) +-- 2020.04.05 @rafale77, pull request #27, scene controller LED handling (Leviton 4-button) +-- 2020.05.11 don't disable devices in Room101 (because they weren't enabled when restored. Thanks @kfxo) +-- see: https://smarthome.community/topic/111/all-z-way-devices-disabled-attribute-is-set-to-1 +-- 2020.05.12 add command class 1 updater for ZWave.me battery switch, thanks @ArcherS +-- see: https://smarthome.community/topic/113/changing-device-type-for-wall-controller +-- 2020.07.12 add CC 91, Central Scene for Remotec ZRC90 (and others?) +-- see: https://smarthome.community/topic/171/remotec-zrc90 +-- 2020.11.24 add category_num, if not set already, when checking devices + +-- 2021.01.19 flag authorisation failure during synchronous or asynchronous requests (thanks @PerH) +-- 2021.02.09 Add GetConfig action to poll device and report back configuration settings +-- 2021.07.24 @rafale77, pull request #31, add support for Fibaro TRV heater device as a thermostat + +-- 2024.02.22 put devices data into global environment to allow external access (room/device nremae for @DesT) local json = require "openLuup.json" local chdev = require "openLuup.chdev" -- NOT the same as the luup.chdev module! (special create fct) local async = require "openLuup.http_async" +local loader = require "openLuup.loader" local http = require "socket.http" local ltn12 = require "ltn12" @@ -74,11 +91,12 @@ end -- Z-WayVDev() API -- +_G["DEVS"] = {"empty"} -- 2024.02.22 global device table access + local function ZWayAPI (ip, sid) local cookie = "ZWAYSession=" .. (sid or '') --- local ok, err = async.request (url, VeraBridge_async_callback) local function build_request (url, body, response_body) return { @@ -135,8 +153,8 @@ local function ZWayAPI (ip, sid) local function devices () local url = "http://%s:8083/ZAutomation/api/v1/devices" - local _, d = HTTP_request_json (url: format (ip)) - return d and d.data and d.data.devices or empty + local status, d = HTTP_request_json (url: format (ip)) + return d and d.data and d.data.devices or empty, status end -- send a command @@ -198,10 +216,16 @@ local function ZWayAPI (ip, sid) local status, response = request "/ZWaveAPI/Run/zway.controller" return status, json.decode (response) end, + command = zwcommand, + send = zwsend, + }, -- Virtual Device API - vDevAPI = {}, + vDevAPI = { + command = command, + status = status, + }, -- JavaScript API JSAPI = {}, @@ -283,10 +307,10 @@ local KnownSID = { -- list of all implemented serviceIds "urn:micasaverde-com:serviceId:SceneController1", "urn:micasaverde-com:serviceId:SceneControllerLED1", "urn:micasaverde-com:serviceId:SecuritySensor1", - "urn:micasaverde-com:serviceId:WindowCovering1", "urn:micasaverde-com:serviceId:ZWaveNetwork1", "urn:upnp-org:serviceId:Dimming1", + "urn:upnp-org:serviceId:WindowCovering1", "urn:upnp-org:serviceId:FanSpeed1", "urn:upnp-org:serviceId:HVAC_FanOperatingMode1", "urn:upnp-org:serviceId:HVAC_UserOperatingMode1", @@ -386,28 +410,43 @@ end -- given "on" or "off" or "1" or "0" -- return "1" or "0" or "on" or "off" local function on_or_off (x) - local y = {["on"] = "1", ["off"] = "0", ["1"] = "on", ["0"] = "off", [true] = "on"} + local y = { + ["on"] = "1", ["off"] = "0", + ["1"] = "on", ["0"] = "off", + [1] = "on", [0] = "off", + [true] = "on"} local z = tonumber (x) local on = z and z > 0 return y[on or x] or x end local function open_or_close (x) - local y = {["open"] = "0", ["close"] = "1", ["0"] = "open", ["1"] = "close"} + local y = { + ["open"] = "0", ["close"] = "1", + ["0"] = "open", ["1"] = "close", + [0] = "open", [1] = "close"} return y[x] or x end local function rev_open_or_close (x) - local y = {["open"] = "1", ["close"] = "0", ["1"] = "open", ["0"] = "close"} + local y = { + ["open"] = "1", ["close"] = "0", + ["1"] = "open", ["0"] = "close", + [1] = "open", [0] = "close"} return y[x] or x end -- make either "1" or "true" or true work the same way local function is_true (flag) - local y = {["true"] = true, ["1"] = true, [true] = true} + local y = {["true"] = true, ["1"] = true, [1] = true, [true] = true} return y [flag] end +-- 2021.01.19 flag authorisation failure +local function authentication_failure() + luup.set_failure (2) + setVar ("DisplayLine1", 'Login required', SID.AltUI) +end ---------------------------------------------------- -- @@ -499,6 +538,22 @@ SRV.HaDevice = { local id, inst = altid: match (NIaltid) Z.zwcommand(id, inst, cc, cmd) end, + + GetConfig = { + run = function (d,args) + local cc = 112 + local par = args.parameter + local data = "Get(%s)" + local data2 = "data[%s].val.value" + local altid = luup.devices[d].id + local id, inst = altid: match (NIaltid) + data = data: format(par) + data2 = data2: format(par) + Z.zwcommand(id, inst, cc, data) + ret, conf = Z.zwcommand(id, inst, cc, data2) + end, + extra_returns = {Config = function () return conf end} + }, SendConfig = function (d,args) local cc = 112 @@ -509,6 +564,7 @@ SRV.HaDevice = { local id, inst = altid: match (NIaltid) Z.zwcommand(id, inst, cc, data) end, + } @@ -584,65 +640,85 @@ SRV.GenericSensor = { } SRV.HumiditySensor = { } SRV.LightSensor = { } + +-- returns the first n bits of the binary representation of x +local function num2bits (x, n) + local b = {} + for i = 1,n do b[i] = x % 2; x = (x - b[i]) / 2 end + return b +end + +-- assemble number from binary array representation +local function bits2num (b) + local d = 0 + for i = #b,1,-1 do d = d + d + b[i] end + return d +end + +-- see: https://community.getvera.com/t/leviton-scene-controller-questions/172155 +-- also: http://wiki.micasaverde.com/index.php/Leviton_LED_Debugging SRV.SceneControllerLED = { +-- newValue = 0,1,2 or 3 where 0=off, 1=green, 2=red, 3=orange (red and green) +-- Indicator = 1-4, or 5 to set all to same colour +-- LightSettings bits are arranged as: MSB RRRRGGGG LSB +-- 43214321 corresponding button number +-- 2020.04.05 thanks to @rafale77 for explaining how this works! SetLight = function (d, args) + local curled = luup.variable_get(SID.SceneControllerLED, "LightSettings", d) or 0 + + local curbit = num2bits (curled, 8) -- extract 8 LSBs + local colbit = num2bits (args.newValue, 2) -- extract 2 LSBs + local indicator = tonumber(args.Indicator) + + for _, lamp in ipairs (indicator==5 and {1,2,3,4} or {indicator}) do + curbit[lamp] = colbit[1] -- green LED + curbit[lamp + 4] = colbit[2] -- red LED + end + + local led = bits2num(curbit) + luup.variable_set(SID.SceneControllerLED, "LightSettings", led, d) + local altid = luup.devices[d].id local id = altid: match (NIaltid) local cc = 145 --command class - local color = tonumber(args.newValue) - local indicator = args.Indicator local data = "[%s,0,29,13,1,255,%s,0,0,10]" - local ItoL = {["1"] = 1, ["2"] = 2, ["3"] = 4, ["4"] = 8, ["5"] = 15} - local led = ItoL[indicator] - if led then - local bit_lshift_led = led * 16 --bit.lshift(led,4) - if color == 2 then led = bit_lshift_led - elseif color == 3 then led = led + bit_lshift_led - elseif color == 0 then led = 0 - end - data = data: format(cc,led) - Z.zwsend(id,data) - end + data = data: format(cc,led) + Z.zwsend(id,data) end, } + SRV.WindowCovering = { --------------- -- 2020.03.25 rafale77 Additions -- Up = function (d) - --- local off = level == '0' - local class = "-38" - luup.variable_set (SID.SwitchPower, "Target", '1', d) luup.variable_set (SID.Dimming, "LoadLevelTarget", "100", d) local altid = luup.devices[d].id - altid = altid: match (NIaltid) and altid..class or altid + altid = altid: match (NIaltid) and altid.."-38" or altid Z.command (altid, "up") end, Down = function (d) - local class = "-38" luup.variable_set (SID.SwitchPower, "Target", '0', d) luup.variable_set (SID.Dimming, "LoadLevelTarget", '0', d) local altid = luup.devices[d].id - altid = altid: match (NIaltid) and altid..class or altid + altid = altid: match (NIaltid) and altid.."-38" or altid Z.command (altid, "down") end, Stop = function (d) - local class = "-38" luup.variable_set (SID.SwitchPower, "Target", '1', d) local val = luup.variable_get (SID.Dimming, "LoadLevelStatus", d) luup.variable_set (SID.Dimming, "LoadLevelTarget", val, d) local altid = luup.devices[d].id - altid = altid: match (NIaltid) and altid..class or altid + altid = altid: match (NIaltid) and altid.."-38" or altid Z.command (altid, "stop") end, @@ -806,8 +882,8 @@ end -- local CC = { -- command class object - - -- catch-all + + -- scene controller, also used for CC ["1"] ["0"] = { updater = function (d, inst, meta) local dev = luup.devices[d] @@ -817,7 +893,7 @@ local CC = { -- command class object local click = inst.updateTime if click ~= meta.click then -- force variable updates local scene = meta.scale - local time = os.time() -- "◷" == json.decode [["\u25F7"]] + local time = os.time() luup.variable_set (SID.SceneController, "sl_SceneActivated", scene, d) luup.variable_set (SID.SceneController, "LastSceneTime",time, d) @@ -826,6 +902,7 @@ local CC = { -- command class object end else + -- catch-all -- local message = "no update for device %d [%s] %s %s" -- log (message: format (d, inst.id, inst.deviceType or '?', (inst.metrics or {}).icon or '')) --... @@ -834,6 +911,13 @@ local CC = { -- command class object files = { nil, SID.HaDevice }, -- device, service, json files }, + + -- dumb switch, like ZWave.me battery switch + ["1"] = { + updater = function () end, -- dummy stub to be assigned at end of CC table + + files = { nil, SID.HaDevice }, + }, -- binary switch ["37"] = { @@ -1058,7 +1142,23 @@ local CC = { -- command class object files = {nil, SID.HVAC_FanOperatingMode}, }, + + --central scene + ["91"] = { + updater = function (d, inst, meta) + local click = inst.updateTime + if click ~= meta.click then -- force variable updates + local button = inst.metrics.level + local time = os.time() + luup.variable_set (SID.SceneController, "sl_CentralScene", button, d) + luup.variable_set (SID.SceneController, "LastSceneTime",time, d) + meta.click = click + end + end, + files = { DEV.controller, SID.SceneController }, + }, + -- door lock ["98"] = { updater = function (d, inst) @@ -1137,6 +1237,8 @@ local CC = { -- command class object } + +CC ["1"].updater = CC ["0"].updater -- dumb switch / controller CC ["113"].updater = CC ["48"].updater -- alarm CC ["156"].updater = CC ["48"].updater -- tamper switch (deprecated) @@ -1251,14 +1353,14 @@ local function move_to_room_101 (devices) local dev = luup.devices[n] _log (table.concat {"Room 101: [", n, "] ", dev.description}) dev: rename (nil, 101) -- move to Room 101 - dev: attr_set ("disabled", 1) -- and make sure it can't run +-- dev: attr_set ("disabled", 1) -- and make sure it can't run end end -- index list of vDevs by command class, saving altids of occurrences local dont_count = { ["0"] = true, -- generic - ["1"] = true, -- ??? + ["1"] = true, -- button of some sort ??? ["50"] = true, -- power ["51"] = true, -- switch colour ["128"] = true, -- batteries @@ -1324,6 +1426,14 @@ local function configureDevice (id, name, ldv, child) add_updater (button) -- should work for Minimote, at least end + elseif classes.n == 0 and classes["1"] and #classes["1"] > 0 then -- just buttons? + upnp_file = DEV.controller + name = classes["1"][1].metrics.title +-- print ("CC1", id, name) + for _,button in ipairs (classes["1"]) do + add_updater (button) -- should work for ZWave.me battery switch + end + elseif classes.n <= 1 then -- a singleton device local vDev = ldv[1] -- there may be a better choice selected below for _,v in ipairs (ldv) do @@ -1361,6 +1471,10 @@ local function configureDevice (id, name, ldv, child) local temp = classes["49"] add_updater(temp[1]) + elseif classes["64"] and classes["67"] then -- a fibaro heater with temperature in another instance + upnp_file, json_file, name = add_updater (classes["64"][1]) + upnp_file, json_file, name = add_updater (classes["67"][1]) + elseif ((classes["37"] and #classes["37"] == 1) -- ... just one switch or (classes["38"] and #classes["38"] == 1) ) then -- ... OR just one dimmer -- @rafale77, pull request #17 was for DesT’s GE combo device @@ -1391,6 +1505,10 @@ local function configureDevice (id, name, ldv, child) name = v.metrics.title -- updaters are set at the end of this if-then-elseif statement + elseif classes["91"] and #classes["91"] == 1 then -- central scene + local v = classes["91"][1] + upnp_file, json_file, name = add_updater (v) + elseif classes["102"] and #classes["102"] == 1 then -- door lock (barrier) local v = classes["102"][1] upnp_file, json_file, name = add_updater (v) @@ -1501,7 +1619,15 @@ local function createChildren (bridgeDevNo, vDevs, room, OFFSET) dev = createZwaveDevice (parent, id, name, altid, upnp_file, json_file, room) end dev.handle_children = true -- ensure that any child devices are handled - dev.attributes.host = "Z-Way" -- flag as Z-Way hosted device [+luup.variable_set()] + local attr = dev.attributes + -- 2020.11.24 add category num, if not set already + local dt = attr.device_type + local cn = attr.category_num or 0 + if cn == 0 then + attr.category_num = loader.cat_by_dev[dt] or 0 + end + -- + attr.host = "Z-Way" -- flag as Z-Way hosted device [+luup.variable_set()] if CLONEROOMS then dev: rename (nil, room) end -- force to given room name if dev.room_num == 101 then dev: rename (nil, room) end -- ensure it's not in Room 101!! list[#list+1] = id -- add to new list @@ -1582,6 +1708,7 @@ local D = {} -- latest device structure ---- this needs to be as fast as possible, since all vDevs are cycled through every update local function updateChildren (vDevs) + DEVS = vDevs -- 2024.02.22 allow global access for scripting local sid = SID.ZWay local failed = {} for _,inst in pairs (vDevs) do @@ -1594,6 +1721,7 @@ local function updateChildren (vDevs) local id = vtype .. altid if getVar (id, sid, zDevNo) then -- fast update of existing variable values (this really does make a difference) + -- NB. this means that you CAN'T trigger, watch, or log these variables local vars = zDev.services[sid].variables vars[id].value = inst.metrics.level vars[id .. "_LastUpdate"].value = inst.updateTime @@ -1650,9 +1778,13 @@ do -- original synchronous polling function _G.ZWay_delay_callback () - local vDevs = Z.devices() - if vDevs then updateChildren (vDevs) end - luup.call_delay ("ZWay_delay_callback", POLLRATE) + local vDevs, status = Z.devices() + if status == 401 then + authentication_failure() + else + if vDevs then updateChildren (vDevs) end + luup.call_delay ("ZWay_delay_callback", POLLRATE) + end end -- asynchronous polling @@ -1685,6 +1817,8 @@ do updateChildren (vDevs) end -- delay = POLL_MINIMUM end -- yes, ask for another one soon... -- init = '' -- ... without initialising data version + elseif status == 401 then + authentication_failure() else luup.log (log: format (status or '?', #(response or ''))) end @@ -1843,8 +1977,7 @@ function init (lul_device) luup.set_failure (0) -- all's well with the world else - luup.set_failure (2, devNo) -- authorisation failure - setVar ("DisplayLine1", 'Login required', SID.AltUI) + authentication_failure() status, comment = false, "Failed to authenticate" end