diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index fcc2c7847d..d095c8a0ca 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -21,31 +21,34 @@ matterManufacturer: vendorId: 0x130a productId: 0x0057 deviceProfileName: temperature-humidity-battery - - - id: "TUO/ContactDoorAndWindow" - deviceLabel: TUO Contact Sensor - vendorId: 0x141E - productId: 0x0002 - deviceProfileName: contact-battery #Heiman - id: "Heiman/Motion" deviceLabel: Heiman Motion Sensor vendorId: 0x120B productId: 0x1001 deviceProfileName: matter-motion-battery-illuminance + - id: "4619/4145" + deviceLabel: Heiman Door and Window D1-M + vendorId: 0x120B + productId: 0x1031 + deviceProfileName: contact-battery # Legrand - id: "Legrand/Netatmo/Smart-2-in-1-Sensor" deviceLabel: Netatmo Smart 2-in-1 Sensor vendorId: 0x1021 productId: 0x0001 deviceProfileName: motion-contact-battery -# HEIMAN - - id: "4619/4145" - deviceLabel: Heiman Door and Window D1-M - vendorId: 0x120B - productId: 0x1031 + #Tuo + - id: "5150/3" + deviceLabel: "TUO Temperature Sensor" + vendorId: 0x141E + productId: 0x0003 + deviceProfileName: "temperature-humidity-battery" + - id: "TUO/ContactDoorAndWindow" + deviceLabel: TUO Contact Sensor + vendorId: 0x141E + productId: 0x0002 deviceProfileName: contact-battery - matterGeneric: - id: "matter/contact/sensor" deviceLabel: Matter Contact Sensor diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index ec4f552045..c42af115d9 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -25,6 +25,16 @@ matterManufacturer: vendorId: 0x130A productId: 0x69 deviceProfileName: power-energy-powerConsumption + - id: "4874/106" + deviceLabel: Eve Energy Switzerland + vendorId: 0x130A + productId: 0x006A + deviceProfileName: power-energy-powerConsumption + - id: "4874/107" + deviceLabel: Eve Energy Outdoor + vendorId: 0x130A + productId: 0x006B + deviceProfileName: power-energy-powerConsumption #GE - id: "4921/177" diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 32800c214b..27823095a8 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -31,6 +31,11 @@ local COLOR_TEMPERATURE_MIRED_MAX = CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN local COLOR_TEMPERATURE_MIRED_MIN = CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX local SWITCH_INITIALIZED = "__switch_intialized" +-- COMPONENT_TO_ENDPOINT_MAP is here only to perserve the endpoint mapping for +-- devices that were joined to this driver as MCD devices before the transition +-- to join all matter-switch devices as parent-child. This value will only exist +-- in the device table for devices that joined prior to this transition, and it +-- will not be set for new devices. local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" local AGGREGATOR_DEVICE_TYPE_ID = 0x000E local detect_matter_thing diff --git a/drivers/SmartThings/philips-hue/src/disco.lua b/drivers/SmartThings/philips-hue/src/disco.lua index 72fe9aba67..99c9a87519 100644 --- a/drivers/SmartThings/philips-hue/src/disco.lua +++ b/drivers/SmartThings/philips-hue/src/disco.lua @@ -5,13 +5,16 @@ local mdns = require "st.mdns" local net_utils = require "st.net_utils" local st_utils = require "st.utils" -local Fields = require "hue.fields" +local Fields = require "fields" local HueApi = require "hue.api" local utils = require "utils" local SERVICE_TYPE = "_hue._tcp" local DOMAIN = "local" +-- This `api_keys` table is an in-memory fall-back table. It gets overwritten +-- with a reference to a driver datastore table before the Driver's `run` loop +-- can get spun up in `init.lua`. local HueDiscovery = { api_keys = {}, disco_api_instances = {}, @@ -94,8 +97,8 @@ function HueDiscovery.scan_bridge_and_update_devices(driver, bridge_id) end local known_bridge_device = known_identifier_to_device_map[bridge_id] - if known_bridge_device and known_bridge_device:get_field(Fields.API_KEY) then - HueDiscovery.api_keys[bridge_id] = known_bridge_device:get_field(Fields.API_KEY) + if known_bridge_device and known_bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) then + HueDiscovery.api_keys[bridge_id] = known_bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) end HueDiscovery.search_bridge_for_supported_devices(driver, bridge_id, HueDiscovery.disco_api_instances[bridge_id], @@ -116,8 +119,8 @@ discovered_bridge_callback = function(driver, bridge_ip, bridge_id, known_identi if driver.ignored_bridges[bridge_id] then return end local known_bridge_device = known_identifier_to_device_map[bridge_id] - if known_bridge_device and known_bridge_device:get_field(Fields.API_KEY) then - HueDiscovery.api_keys[bridge_id] = known_bridge_device:get_field(Fields.API_KEY) + if known_bridge_device and known_bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) then + HueDiscovery.api_keys[bridge_id] = known_bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) end if known_bridge_device ~= nil @@ -253,14 +256,17 @@ function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_id, api if do_delete then for _, device in ipairs(driver:get_devices()) do ---@cast device HueDevice + -- We're only interested in processing child/non-bridge devices here. + if utils.is_bridge(driver, device) then goto continue end local not_known_to_bridge = device_is_joined_to_bridge[device:get_field(Fields.HUE_DEVICE_ID) or ""] local parent_device_id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) or "" local parent_bridge_device = driver:get_device_info(parent_device_id) local is_child_of_bridge = parent_bridge_device and (parent_bridge_device:get_field(Fields.BRIDGE_ID) == bridge_id) - if is_child_of_bridge and not not_known_to_bridge then + if parent_bridge_device and is_child_of_bridge and not not_known_to_bridge then device.log.info(string.format("Device is no longer joined to Hue Bridge %q, deleting", parent_bridge_device.label)) driver:do_hue_light_delete(device) end + ::continue:: end end end @@ -283,7 +289,7 @@ end ---@param bridge_id string ---@param resource_id string ---@param device_info table ----@param known_identifier_to_device_map table +---@param known_identifier_to_device_map table process_discovered_light = function(driver, bridge_id, resource_id, device_info, known_identifier_to_device_map) local api_instance = HueDiscovery.disco_api_instances[bridge_id] if not api_instance then @@ -385,17 +391,17 @@ function HueDiscovery.do_mdns_scan(driver) for _, info in ipairs(mdns_responses.found) do if not net_utils.validate_ipv4_string(info.host_info.address) then -- we only care about the ipV4 types here. log.trace("Invalid IPv4 address: " .. info.host_info.address) - return + goto continue end if info.service_info.service_type ~= HueDiscovery.ServiceType then -- response for a different service type. Shouldn't happen. log.warn("Unexpected service type response: " .. info.service_info.service_type) - return + goto continue end if info.service_info.domain ~= HueDiscovery.Domain then -- response for a different domain. Shouldn't happen. log.warn("Unexpected domain response: " .. info.service_info.domain) - return + goto continue end -- Hue *typically* formats the BridgeID as the uppercase MAC address, minus separators. @@ -452,6 +458,7 @@ function HueDiscovery.do_mdns_scan(driver) driver:update_bridge_netinfo(bridge_id, bridge_info) end end + ::continue:: end end diff --git a/drivers/SmartThings/philips-hue/src/hue/fields.lua b/drivers/SmartThings/philips-hue/src/fields.lua similarity index 92% rename from drivers/SmartThings/philips-hue/src/hue/fields.lua rename to drivers/SmartThings/philips-hue/src/fields.lua index 9ea0ad10af..05e0b8c4f5 100644 --- a/drivers/SmartThings/philips-hue/src/hue/fields.lua +++ b/drivers/SmartThings/philips-hue/src/fields.lua @@ -1,4 +1,3 @@ -local APPLICATION_KEY_HEADER = require "hue.api".APPLICATION_KEY_HEADER --- Table of constants used to index in to device store fields --- @class Fields --- @field IPV4 string the ipV4 address of a Hue bridge @@ -14,7 +13,6 @@ local Fields = { _ADDED = "added", _INIT = "init", _REFRESH_AFTER_INIT = "force_refresh", - API_KEY = APPLICATION_KEY_HEADER, BRIDGE_API = "bridge_api", BRIDGE_ID = "bridgeid", BRIDGE_SW_VERSION = "swversion", diff --git a/drivers/SmartThings/philips-hue/src/handlers.lua b/drivers/SmartThings/philips-hue/src/handlers.lua index 7c89b134b2..a4e950ae9a 100644 --- a/drivers/SmartThings/philips-hue/src/handlers.lua +++ b/drivers/SmartThings/philips-hue/src/handlers.lua @@ -1,4 +1,4 @@ -local Fields = require "hue.fields" +local Fields = require "fields" local HueApi = require "hue.api" local HueColorUtils = require "hue.cie_utils" local log = require "log" @@ -7,6 +7,9 @@ local utils = require "utils" local cosock = require "cosock" local capabilities = require "st.capabilities" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: table, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local handlers = {} diff --git a/drivers/SmartThings/philips-hue/src/hue/api.lua b/drivers/SmartThings/philips-hue/src/hue/api.lua index 04a8bb70d4..4116715879 100644 --- a/drivers/SmartThings/philips-hue/src/hue/api.lua +++ b/drivers/SmartThings/philips-hue/src/hue/api.lua @@ -5,6 +5,9 @@ local json = require "st.json" local log = require "log" local RestClient = require "lunchbox.rest" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: table, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local APPLICATION_KEY_HEADER = "hue-application-key" @@ -35,9 +38,10 @@ end --- Phillips Hue REST API Module --- @class PhilipsHueApi ---- @field private client RestClient ---- @field private headers table ---- @field private _ctrl_tx table +--- @field public headers table +--- @field package client RestClient +--- @field package _ctrl_tx table +--- @field package _running boolean local PhilipsHueApi = {} PhilipsHueApi.__index = PhilipsHueApi @@ -138,13 +142,15 @@ function PhilipsHueApi.new_bridge_manager(base_url, api_key, socket_builder) local path, reply_tx = msg.path, msg.reply_tx if msg._type == ControlMessageTypes.Get then + local get_resp, get_err, partial = self.client:get(path, self.headers, retry_fn(5)) reply_tx:send( - table.pack(process_rest_response(self.client:get(path, self.headers, retry_fn(5), rest_err_callback))) + table.pack(process_rest_response(get_resp, get_err, partial, rest_err_callback)) ) elseif msg._type == ControlMessageTypes.Put then local payload = msg.payload + local put_resp, put_err, partial = self.client:put(path, payload, self.headers, retry_fn(5)) reply_tx:send( - table.pack(process_rest_response(self.client:put(path, payload, self.headers, retry_fn(5), rest_err_callback))) + table.pack(process_rest_response(put_resp, put_err, partial, rest_err_callback)) ) end else diff --git a/drivers/SmartThings/philips-hue/src/hue/types.lua b/drivers/SmartThings/philips-hue/src/hue/types.lua index f39870cf67..11907b535c 100644 --- a/drivers/SmartThings/philips-hue/src/hue/types.lua +++ b/drivers/SmartThings/philips-hue/src/hue/types.lua @@ -1,3 +1,5 @@ +--- @meta + --- Hue Bridge Info as returned by the unauthenticated API endpoint `/api/config` --- @class HueBridgeInfo --- @field public name string @@ -23,21 +25,30 @@ --- @field public update_bridge_netinfo fun(self: HueDriver, bridge_id: string, bridge_info: HueBridgeInfo) --- @field public emit_light_status_events fun(light_device: HueChildDevice, light: table) --- @field public get_device_by_dni fun(self: HueDriver, device_network_id: string, force_refresh?: boolean): HueDevice|nil ---- @field private _lights_pending_refresh table +--- @field public do_hue_light_delete fun(self: HueDriver, light_device: HueDevice) +--- @field public get_device_info fun(self: HueDriver, device_id: string, force_refresh: boolean?): HueDevice? +--- @field public check_hue_repr_for_capability_support fun(hue_repr: table, capability_id: string): boolean +--- @field public _lights_pending_refresh table --- @class HueDevice:st.Device --- @field public label string --- @field public id string --- @field public device_network_id string +--- @field public parent_device_id string +--- @field public parent_assigned_child_key string? +--- @field public manufacturer string +--- @field public model string +--- @field public vendor_provided_label string --- @field public data table|nil migration data for a migrated device --- @field public log table device-scoped logging module +--- @field public profile table --- @field public get_field fun(self: HueDevice, key: string):any --- @field public set_field fun(self: HueDevice, key: string, value: any, args?: table) --- @field public emit_event fun(self: HueDevice, event: any) +--- @field public supports_capability_by_id fun(self: HueDevice, capability_id: string, component: string?): boolean --- @class HueBridgeDevice:HueDevice --- @field public device_network_id string --- @class HueChildDevice:HueDevice ---- @field public parent_device_id string --- @field public parent_assigned_child_key string diff --git a/drivers/SmartThings/philips-hue/src/init.lua b/drivers/SmartThings/philips-hue/src/init.lua index 75d303ec43..9be833cd0e 100644 --- a/drivers/SmartThings/philips-hue/src/init.lua +++ b/drivers/SmartThings/philips-hue/src/init.lua @@ -24,10 +24,13 @@ local capabilities = require "st.capabilities" local Driver = require "st.driver" local json = require "st.json" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: table, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local Discovery = require "disco" local EventSource = require "lunchbox.sse.eventsource" -local Fields = require "hue.fields" +local Fields = require "fields" local handlers = require "handlers" local HueApi = require "hue.api" local HueColorUtils = require "hue.cie_utils" @@ -37,8 +40,6 @@ local utils = require "utils" local syncCapabilityId = "samsungim.hueSyncMode" local hueSyncMode = capabilities[syncCapabilityId] -local api_version = require("version").api - local StrayDeviceMessageTypes = { FoundBridge = "FOUND_BRIDGE", NewStrayLight = "NEW_STRAY_LIGHT", @@ -326,7 +327,7 @@ local function update_bridge_fields_from_info(driver, bridge_info, bridge_device bridge_device:set_field(Fields.BRIDGE_SW_VERSION, tonumber(bridge_info.swversion or "0", 10), { persist = true }) if Discovery.api_keys[device_bridge_id] then - bridge_device:set_field(Fields.API_KEY, Discovery.api_keys[device_bridge_id], { persist = true }) + bridge_device:set_field(HueApi.APPLICATION_KEY_HEADER, Discovery.api_keys[device_bridge_id], { persist = true }) driver.api_key_to_bridge_id[Discovery.api_keys[device_bridge_id]] = device_bridge_id end bridge_device:set_field(Fields.IPV4, bridge_ip, { persist = true }) @@ -592,7 +593,7 @@ light_added = function(driver, device, parent_device_id, resource_id) ) ) - local key = parent_bridge:get_field(Fields.API_KEY) + local key = parent_bridge:get_field(HueApi.APPLICATION_KEY_HEADER) local bridge_ip = parent_bridge:get_field(Fields.IPV4) local bridge_id = parent_bridge:get_field(Fields.BRIDGE_ID) if not (Discovery.api_keys[bridge_id or {}] or key) then @@ -612,7 +613,7 @@ light_added = function(driver, device, parent_device_id, resource_id) if not api_instance then api_instance = HueApi.new_bridge_manager( "https://" .. bridge_ip, - (parent_bridge:get_field(Fields.API_KEY) or Discovery.api_keys[bridge_id] or key), + (parent_bridge:get_field(HueApi.APPLICATION_KEY_HEADER) or Discovery.api_keys[bridge_id] or key), utils.labeled_socket_builder( (parent_bridge.label or bridge_id or parent_bridge.id or "unknown bridge") ) @@ -714,10 +715,13 @@ local function do_bridge_network_init(driver, bridge_device, bridge_url, api_key local bridge_api = bridge_device:get_field(Fields.BRIDGE_API) cosock.spawn(function() - -- auto-add/auto-delete is only supported with newer lua libs. We do a conditional - -- check in the delete routine, but we try to avoid doing it at all here with an - -- additional defensive check against the Lua Libs API version. - if api_version >= 9 then + -- We don't want to do a scan if we're already in a discovery loop, + -- because the event source connection will open if a bridge is discovered + -- and we'll effectively be scanning twice. + -- Two scans that find the same device close together can emit events close enough + -- together that the dedupe logic at the cloud layer will get bypassed and lead to + -- duplicate device records. + if not Discovery.discovery_active then Discovery.scan_bridge_and_update_devices(driver, bridge_device:get_field(Fields.BRIDGE_ID)) end local child_device_map = {} @@ -996,7 +1000,7 @@ init_bridge = function(driver, device) local bridge_manager = device:get_field(Fields.BRIDGE_API) or Discovery.disco_api_instances[device_bridge_id] local ip = device:get_field(Fields.IPV4) - local api_key = device:get_field(Fields.API_KEY) + local api_key = device:get_field(HueApi.APPLICATION_KEY_HEADER) local bridge_url = "https://" .. ip if not Discovery.api_keys[device_bridge_id] then @@ -1055,7 +1059,7 @@ init_bridge = function(driver, device) return end - log.info("Bridge info for %s received, initializing network configuration") + log.info(string.format("Bridge info for %s received, initializing network configuration", device_bridge_id)) driver.joined_bridges[device_bridge_id] = true do_bridge_network_init(driver, device, bridge_url, api_key) return @@ -1119,9 +1123,9 @@ local function device_init(driver, device) string.format("device_init for device %s, device_type: %s", (device.label or device.id or "unknown device"), device_type)) if device_type == "bridge" then - init_bridge(driver, device) + init_bridge(driver, device --[[@as HueBridgeDevice]]) elseif device_type == "light" then - init_light(driver, device) + init_light(driver, device --[[@as HueChildDevice]]) end end @@ -1132,15 +1136,15 @@ local function device_added(driver, device, _, _, parent_device_id) log.info( string.format("device_added for device %s", (device.label or device.id or "unknown device"))) if utils.is_dth_bridge(device) then - migrate_bridge(driver, device) + migrate_bridge(driver, device --[[@as HueBridgeDevice]]) elseif utils.is_dth_light(device) then - migrate_light(driver, device, parent_device_id) + migrate_light(driver, device --[[@as HueChildDevice]], parent_device_id) -- Don't do a refresh if it's a migration device:set_field(Fields._REFRESH_AFTER_INIT, false, { persist = true }) elseif utils.is_edge_bridge(device) then - bridge_added(driver, device) + bridge_added(driver, device --[[@as HueBridgeDevice]]) elseif utils.is_edge_light(device) then - light_added(driver, device, parent_device_id) + light_added(driver, device --[[@as HueChildDevice]], parent_device_id) else log.warn( st_utils.stringify_table(device, @@ -1241,7 +1245,7 @@ cosock.spawn(function() if not api_instance then api_instance = HueApi.new_bridge_manager( "https://" .. bridge_ip, - msg_device:get_field(Fields.API_KEY), + msg_device:get_field(HueApi.APPLICATION_KEY_HEADER), utils.labeled_socket_builder((msg_device.label or msg_device.device_network_id or msg_device.id or "unknown bridge")) ) Discovery.disco_api_instances[msg_device.device_network_id] = api_instance @@ -1367,7 +1371,7 @@ cosock.spawn(function() if not api_instance then api_instance = HueApi.new_bridge_manager( "https://" .. bridge_ip, - maybe_bridge:get_field(Fields.API_KEY), + maybe_bridge:get_field(HueApi.APPLICATION_KEY_HEADER), utils.labeled_socket_builder((maybe_bridge.label or maybe_bridge.device_network_id or maybe_bridge.id or "unknown bridge")) ) Discovery.disco_api_instances[maybe_bridge.device_network_id] = api_instance @@ -1421,6 +1425,8 @@ local function remove(driver, device) event_source:close() device:set_field(Fields.EVENT_SOURCE, nil) end + + Discovery.api_keys[device.device_network_id] = nil end end @@ -1527,7 +1533,7 @@ local hue = Driver("hue", if bridge_info.ip ~= bridge_device:get_field(Fields.IPV4) then update_bridge_fields_from_info(self, bridge_info, bridge_device) local maybe_api_client = bridge_device:get_field(Fields.BRIDGE_API) - local maybe_api_key = bridge_device:get_field(Fields.API_KEY) or Discovery.api_keys[bridge_id] + local maybe_api_key = bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) or Discovery.api_keys[bridge_id] local maybe_event_source = bridge_device:get_field(Fields.EVENT_SOURCE) local bridge_url = "https://" .. bridge_info.ip @@ -1561,6 +1567,27 @@ if hue.datastore["dni_to_device_id"] == nil then hue.datastore["dni_to_device_id"] = {} end + +if hue.datastore["api_keys"] == nil then + hue.datastore["api_keys"] = {} +end + +Discovery.api_keys = setmetatable({}, { + __newindex = function (self, k, v) + assert( + type(v) == "string", + string.format("Attempted to store value of type %s in application_key table which expects \"string\" types", + type(v) + ) + ) + hue.datastore.api_keys[k] = v + hue.datastore:save() + end, + __index = function(self, k) + return hue.datastore.api_keys[k] + end +}) + -- Kick off a scan right away to attempt to populate some information hue:call_with_delay(3, Discovery.do_mdns_scan, "Philips Hue mDNS Initial Scan") diff --git a/drivers/SmartThings/philips-hue/src/lunchbox/rest.lua b/drivers/SmartThings/philips-hue/src/lunchbox/rest.lua index c3752647ee..340b73e114 100644 --- a/drivers/SmartThings/philips-hue/src/lunchbox/rest.lua +++ b/drivers/SmartThings/philips-hue/src/lunchbox/rest.lua @@ -1,9 +1,16 @@ +---@class ChunkedResponse : Response +---@field package _received_body boolean +---@field package _parsed_headers boolean +---@field public new fun(status_code: number, socket: table?): ChunkedResponse +---@field public fill_body fun(self: ChunkedResponse): string? +---@field public append_body fun(self: ChunkedResponse, next_chunk_body: string): ChunkedResponse + local socket = require "cosock.socket" local utils = require "utils" local lb_utils = require "lunchbox.util" local Request = require "luncheon.request" -local Response = require "luncheon.response" +local Response = require "luncheon.response" --[[@as ChunkedResponse]] local api_version = require("version").api @@ -43,9 +50,15 @@ local function reconnect(client) return connect(client) end +---comment +---@param client RestClient +---@param request Request +---@return integer? bytes_sent +---@return string? err_msg +---@return integer idx local function send_request(client, request) if client.socket == nil then - return nil, "no socket available" + return nil, "no socket available", 0 end local payload = request:serialize() @@ -63,7 +76,7 @@ local function parse_chunked_response(original_response, sock) EXPECTING_BODY_CHUNK = "ExpectingBodyChunk", } - local full_response = Response.new(original_response.status, nil) + local full_response = Response.new(original_response.status, nil) --[[@as ChunkedResponse]] for header in original_response.headers:iter() do full_response.headers:append_chunk(header) end @@ -144,7 +157,7 @@ local function handle_response(sock) if initial_recv ~= nil then local headers = initial_recv:get_headers() - if headers:get_one("Transfer-Encoding") == "chunked" then + if headers and headers:get_one("Transfer-Encoding") == "chunked" then local response, err = parse_chunked_response(initial_recv, sock) if err ~= nil then return nil, err @@ -162,12 +175,12 @@ end local function execute_request(client, request, retry_fn) if not client._active then - return nil, "Called `execute request` on a terminated REST Client" + return nil, "Called `execute request` on a terminated REST Client", nil end if client.socket == nil then local success, err = connect(client) - if not success then return nil, err end + if not success then return nil, err, nil end end local should_retry = retry_fn @@ -179,7 +192,7 @@ local function execute_request(client, request, retry_fn) -- send output local _bytes_sent, send_err, _idx = nil, nil, 0 -- recv output - local response, recv_err, _partial = nil, nil, nil + local response, recv_err, partial = nil, nil, nil -- return values local ret, err = nil, nil @@ -206,7 +219,7 @@ local function execute_request(client, request, retry_fn) current_state = RestCallStates.COMPLETE end elseif current_state == RestCallStates.RECEIVE then - response, recv_err, _partial = handle_response(client.socket) + response, recv_err, partial = handle_response(client.socket) if not recv_err then ret = response @@ -242,7 +255,7 @@ local function execute_request(client, request, retry_fn) end until current_state == RestCallStates.COMPLETE - return ret, err + return ret, err, partial end ---@class RestClient @@ -257,7 +270,6 @@ function RestClient.one_shot_get(full_url, additional_headers, socket_builder) local client = RestClient.new(url_table.scheme .. "://" .. url_table.host, socket_builder) local ret, err = client:get(url_table.path, additional_headers) client:shutdown() - client = nil return ret, err end @@ -266,7 +278,6 @@ function RestClient.one_shot_post(full_url, body, additional_headers, socket_bui local client = RestClient.new(url_table.scheme .. "://" .. url_table.host, socket_builder) local ret, err = client:post(url_table.path, body, additional_headers) client:shutdown() - client = nil return ret, err end diff --git a/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua b/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua index 78fc95923b..67e4a33d20 100644 --- a/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua +++ b/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua @@ -21,15 +21,15 @@ local Response = require "luncheon.response" --- @field public onopen function in-line callback for on-open events --- @field public onmessage function in-line callback for on-message events --- @field public onerror function in-line callback for on-error events; error callbacks will fire ---- @field private _reconnect boolean flag that says whether or not the client should attempt to reconnect on close. ---- @field private _reconnect_time_millis number The amount of time to wait between reconnects, in millis. Can be sent by the server. ---- @field private _sock_builder function|nil optional. If this function exists, it will be called to create a new TCP socket on connection. ---- @field private _sock table the TCP socket for the connection ---- @field private _needs_more boolean flag to track whether or not we're still expecting mroe on this source before we dispatch ---- @field private _last_field string the last field the parsing path saw, in case it needs to append more to its value ---- @field private _extra_headers table a table of string:string key-value pairs that will be inserted in to the initial requests's headers. ---- @field private _parse_buffers table inner state, keeps track of the various event stream buffers in between dispatches. ---- @field private _listeners table event listeners attached using the add_event_listener API instead of the inline callbacks. +--- @field package _reconnect boolean flag that says whether or not the client should attempt to reconnect on close. +--- @field package _reconnect_time_millis number The amount of time to wait between reconnects, in millis. Can be sent by the server. +--- @field package _sock_builder function|nil optional. If this function exists, it will be called to create a new TCP socket on connection. +--- @field package _sock table? the TCP socket for the connection +--- @field package _needs_more boolean flag to track whether or not we're still expecting mroe on this source before we dispatch +--- @field package _last_field string the last field the parsing path saw, in case it needs to append more to its value +--- @field package _extra_headers table a table of string:string key-value pairs that will be inserted in to the initial requests's headers. +--- @field package _parse_buffers table inner state, keeps track of the various event stream buffers in between dispatches. +--- @field package _listeners table event listeners attached using the add_event_listener API instead of the inline callbacks. local EventSource = {} EventSource.__index = EventSource @@ -156,8 +156,8 @@ local valid_fields = util.read_only { -- h/t to github.com/FreeMasen for the suggestions on the efficient implementation of this local function find_line_endings(chunk) local r_idx, n_idx = string.find(chunk, "[\r\n]+") - if r_idx == n_idx then - -- 1 character + if r_idx == nil or r_idx == n_idx then + -- 1 character or no match return r_idx, n_idx end local slice = string.sub(chunk, r_idx, n_idx) @@ -289,8 +289,8 @@ local function connecting_action(source) local response response, err = Response.tcp_source(source._sock) - if err ~= nil then - return nil, err + if not response or err ~= nil then + return nil, err or "nil response from Response.tcp_source" end if response.status ~= 200 then @@ -301,7 +301,7 @@ local function connecting_action(source) if err ~= nil then return nil, err end - local content_type = string.lower((headers:get_one('content-type') or "none")) + local content_type = string.lower((headers and headers:get_one('content-type') or "none")) if not content_type:find("text/event-stream", 1, true) then local err_msg = "Expected content type of text/event-stream in response headers, received: " .. content_type return nil, err_msg diff --git a/drivers/SmartThings/philips-hue/src/utils.lua b/drivers/SmartThings/philips-hue/src/utils.lua index 14c27e02e4..546d70bdbd 100644 --- a/drivers/SmartThings/philips-hue/src/utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils.lua @@ -1,5 +1,6 @@ +local Fields = require "fields" local log = require "log" ----@module 'utils' +---@class hue.utils local utils = {} local MAC_ADDRESS_STR_LEN = 12 @@ -27,7 +28,23 @@ function utils.is_nan(number) return tostring(number) == tostring(0 / 0) end ---- Only checked during `added` callback + +--- Attempts an exhaustive check of all the ways a device +--- can indicate that it represents a Hue Bridge. +---@param driver HueDriver +---@param device HueDevice +---@return boolean is_bridge true if the device record represents a Hue Bridge +function utils.is_bridge(driver, device) + return (device:get_field(Fields.DEVICE_TYPE) == "bridge") + or (driver.datastore.bridge_netinfo[device.device_network_id] ~= nil) + or utils.is_edge_bridge(device) or utils.is_dth_light(device) + or (device.parent_assigned_child_key == nil) +end + +--- Only checked during `added` callback, or as a later +--- fallback check in the chain of booleans used in `is_bridge`. +--- +---@see hue.utils.is_bridge ---@param device HueDevice ---@return boolean function utils.is_edge_bridge(device) @@ -35,11 +52,14 @@ function utils.is_edge_bridge(device) not (device.data and device.data.username) end ---- Only checked during `added` callback +--- Only checked during `added` callback, or as a later +--- fallback check in the chain of booleans used in `is_bridge`. +--- +---@see hue.utils.is_bridge ---@param device HueDevice ---@return boolean function utils.is_edge_light(device) - return device.parent_assigned_child_key and #device.parent_assigned_child_key > MAC_ADDRESS_STR_LEN and + return device.parent_assigned_child_key ~= nil and #device.parent_assigned_child_key > MAC_ADDRESS_STR_LEN and not (device.data and device.data.username and device.data.bulbId) end @@ -147,8 +167,8 @@ function utils.labeled_socket_builder(label) ) sock, err = ssl.wrap(sock, { mode = "client", protocol = "any", verify = "none", options = "all" }) - if err ~= nil then - return nil, "SSL wrap error: " .. err + if not sock or err ~= nil then + return nil, (err and "SSL wrap error: " .. err) or "Unexpected nil socket returned from ssl.wrap" end log.info( string.format( diff --git a/drivers/SmartThings/zigbee-button/fingerprints.yml b/drivers/SmartThings/zigbee-button/fingerprints.yml index 0770ada8b2..c084f007ab 100644 --- a/drivers/SmartThings/zigbee-button/fingerprints.yml +++ b/drivers/SmartThings/zigbee-button/fingerprints.yml @@ -44,6 +44,11 @@ zigbeeManufacturer: manufacturer: KE model: TRADFRI open/close remote deviceProfileName: two-buttons-battery + - id: "02KE/TRADFRIopenclose" + deviceLabel: IKEA Remote Control + manufacturer: "\x02KE" + model: TRADFRI open/close remote + deviceProfileName: two-buttons-battery - id: "CentraLite/3450-L" deviceLabel: Iris Remote Control manufacturer: CentraLite diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua index 4bbff01b70..f01f29715f 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua @@ -29,6 +29,21 @@ local Groups = clusters.Groups local ENTRIES_READ = "ENTRIES_READ" +local IKEA_MFG = { + { mfr = "IKEA of Sweden" }, + { mfr = "KE" }, + { mfr = "\02KE" } +} + +local can_handle_ikea = function(opts, driver, device) + for _, fingerprint in ipairs(IKEA_MFG) do + if device:get_manufacturer() == fingerprint.mfr then + return true + end + end + return false +end + local do_configure = function(self, device) device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) device:send(device_management.build_bind_request(device, OnOff.ID, self.environment_info.hub_zigbee_eui)) @@ -129,9 +144,7 @@ local ikea_of_sweden = { require("zigbee-multi-button.ikea.TRADFRI_on_off_switch"), require("zigbee-multi-button.ikea.TRADFRI_open_close_remote") }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "IKEA of Sweden" or device:get_manufacturer() == "KE" - end + can_handle = can_handle_ikea } return ikea_of_sweden diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua index b0e210d7c4..f572fd79e4 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua @@ -29,6 +29,7 @@ local ZIGBEE_MULTI_BUTTON_FINGERPRINTS = { { mfr = "IKEA of Sweden", model = "TRADFRI open/close remote" }, { mfr = "IKEA of Sweden", model = "TRADFRI remote control" }, { mfr = "KE", model = "TRADFRI open/close remote" }, + { mfr = "\x02KE", model = "TRADFRI open/close remote" }, { mfr = "SOMFY", model = "Situo 1 Zigbee" }, { mfr = "SOMFY", model = "Situo 4 Zigbee" }, { mfr = "LDS", model = "ZBT-CCTSwitch-D0001" }, diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua index 711877fbaa..fabaa7da6f 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua @@ -39,7 +39,8 @@ local devices = { BUTTON_PUSH_2 = { MATCHING_MATRIX = { { mfr = "IKEA of Sweden", model = "TRADFRI open/close remote" }, - { mfr = "KE", model = "TRADFRI open/close remote" } + { mfr = "KE", model = "TRADFRI open/close remote" }, + { mfr = "\x02KE", model = "TRADFRI open/close remote" } }, SUPPORTED_BUTTON_VALUES = { "pushed" }, NUMBER_OF_BUTTONS = 2