Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Aqara] Presence Sensor FP2 #1546

Merged
merged 10 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions drivers/Aqara/aqara-presence-sensor/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: 'aqara-presence-sensor'
tpmanley marked this conversation as resolved.
Show resolved Hide resolved
packageKey: 'aqara-presence-sensor'
permissions:
lan: {}
discovery: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: aqara-fp2-fallDetection
components:
- id: main
capabilities:
- id: activitySensor
version: 1
- id: presenceSensor
version: 1
- id: illuminanceMeasurement
version: 1
- id: refresh
version: 1
categories:
- name: PresenceSensor
- id: mode
capabilities:
- id: stse.deviceMode
version: 1
categories:
- name: PresenceSensor
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: aqara-fp2-sleepMonitoring
components:
- id: main
capabilities:
- id: illuminanceMeasurement
version: 1
- id: refresh
version: 1
categories:
- name: PresenceSensor
- id: mode
capabilities:
- id: stse.deviceMode
version: 1
categories:
- name: PresenceSensor
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: aqara-fp2-zoneDetection
components:
- id: main
capabilities:
- id: presenceSensor
version: 1
- id: movementSensor
version: 1
- id: multipleZonePresence
version: 1
- id: illuminanceMeasurement
version: 1
- id: refresh
version: 1
categories:
- name: PresenceSensor
- id: mode
capabilities:
- id: stse.deviceMode
version: 1
categories:
- name: PresenceSensor
2 changes: 2 additions & 0 deletions drivers/Aqara/aqara-presence-sensor/search-parameters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mdns:
- service: "_Aqara-FP2._tcp"
92 changes: 92 additions & 0 deletions drivers/Aqara/aqara-presence-sensor/src/discovery.lua
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice discovery module. Very easy to review and understand what is happening.

Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
local log = require "log"
local discovery = {}
local fields = require "fields"
local discovery_mdns = require "discovery_mdns"
local socket = require "cosock.socket"

-- mapping from device DNI to info needed at discovery/init time
local device_discovery_cache = {}

local function set_device_field(driver, device)
local device_cache_value = device_discovery_cache[device.device_network_id]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should check for nil here


-- persistent fields
device:set_field(fields.DEVICE_IPV4, device_cache_value.ip, { persist = true })
device:set_field(fields.DEVICE_INFO, device_cache_value.device_info, { persist = true })
device:set_field(fields.CREDENTIAL, device_cache_value.credential, { persist = true })
end

local function update_device_discovery_cache(driver, dni, ip, credential)
local device_info = driver.discovery_helper.get_device_info(driver, dni, ip)
device_discovery_cache[dni] = {
ip = ip,
device_info = device_info,
credential = credential,
}
end

local function try_add_device(driver, device_dni, device_ip)
log.trace(string.format("try_add_device : dni= %s, ip= %s", device_dni, device_ip))

local credential = driver.discovery_helper.get_credential(driver, device_dni, device_ip)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: error messages are typically included after the value, this will make line 34 more helpful


if not credential then
log.error(string.format("failed to get credential. dni= %s, ip= %s", device_dni, device_ip))
return
end

update_device_discovery_cache(driver, device_dni, device_ip, credential)
local create_device_msg = driver.discovery_helper.get_device_create_msg(driver, device_dni, device_ip)
driver:try_create_device(create_device_msg)
end

function discovery.device_added(driver, device)
set_device_field(driver, device)
device_discovery_cache[device.device_network_id] = nil
driver.lifecycle_handlers.init(driver, device)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe you should be calling lifecycle handlers directly, this will be called by the framework

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this is needed in order to make sure init is called after added. Init requires the device fields to have been populated.

end

function discovery.find_ip_table(driver)
local ip_table = discovery_mdns.find_ip_table_by_mdns(driver)
return ip_table
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needless assignment

Suggested change
local ip_table = discovery_mdns.find_ip_table_by_mdns(driver)
return ip_table
return discovery_mdns.find_ip_table_by_mdns(driver)

end

local function discovery_device(driver)
local unknown_discovered_devices = {}
local known_discovered_devices = {}
local known_devices = {}

for _, device in pairs(driver:get_devices()) do
known_devices[device.device_network_id] = device
end

local ip_table = discovery.find_ip_table(driver)

for dni, ip in pairs(ip_table) do
if not known_devices or not known_devices[dni] then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why wold known_devices be nil here?

unknown_discovered_devices[dni] = ip
else
known_discovered_devices[dni] = ip
end
end

for dni, ip in pairs(known_discovered_devices) do
log.trace(string.format("known dni= %s, ip= %s", dni, ip))
end

for dni, ip in pairs(unknown_discovered_devices) do
log.trace(string.format("unknown dni= %s, ip= %s", dni, ip))
if not device_discovery_cache[dni] then
try_add_device(driver, dni, ip)
end
end
end

function discovery.do_network_discovery(driver, _, should_continue)
while should_continue() do
discovery_device(driver)
socket.sleep(0.2)
end
end

return discovery
143 changes: 143 additions & 0 deletions drivers/Aqara/aqara-presence-sensor/src/discovery_mdns.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
local log = require "log"
local mdns = require "st.mdns"
local net_utils = require "st.net_utils"

local discovery_mdns = {}

local function byte_array_to_plain_text(byte_array)
return string.char(table.unpack(byte_array))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why this would be needed? I thought all the responses from the mdns API would be strings

end

local function get_text_by_srvname(srvname, discovery_responses)
for _, answer_item in pairs(discovery_responses.answers or {}) do
if answer_item.kind.TxtRecord ~= nil and answer_item.name == srvname then
return answer_item.kind.TxtRecord.text
end
end
end

local function get_srvname_by_hostname(hostname, discovery_responses)
for _, answer_item in pairs(discovery_responses.answers or {}) do
if answer_item.kind.SrvRecord ~= nil and answer_item.kind.SrvRecord.target == hostname then
return answer_item.name
end
end
end

local function get_hostname_by_ip(ip, discovery_responses)
for _, answer_item in pairs(discovery_responses.answers or {}) do
if answer_item.kind.ARecord ~= nil and answer_item.kind.ARecord.ipv4 == ip then
return answer_item.name
end
end
end


local function find_text_in_answers_by_ip(ip, discovery_responses)
local hostname = get_hostname_by_ip(ip, discovery_responses)
local srvname = get_srvname_by_hostname(hostname, discovery_responses)
local text = get_text_by_srvname(srvname, discovery_responses)

return text
end

function discovery_mdns.find_text_list_in_mdns_response(driver, ip, discovery_responses)
local text_list = {}

for _, found_item in pairs(discovery_responses.found or {}) do
if found_item.host_info.address == ip then
for _, raw_text_array in pairs(found_item.txt.text or {}) do
local text_item = byte_array_to_plain_text(raw_text_array)
table.insert(text_list, text_item)
end
end
end

local answer_text = find_text_in_answers_by_ip(ip, discovery_responses)
for _, text_item in pairs(answer_text or {}) do
table.insert(text_list, text_item)
end
return text_list
end

local function filter_response_by_service_name(service_type, domain, discovery_responses)
local filtered_responses = {
answers = {},
found = {}
}

for _, answer in pairs(discovery_responses.answers or {}) do
table.insert(filtered_responses.answers, answer)
end

for _, additional in pairs(discovery_responses.additional or {}) do
table.insert(filtered_responses.answers, additional)
end

for _, found in pairs(discovery_responses.found or {}) do
if found.service_info.service_type == service_type then
table.insert(filtered_responses.found, found)
end
end

return filtered_responses
end

local function insert_dni_ip_from_answers(driver, filtered_responses, target_table)
for _, answer in pairs(filtered_responses.answers) do
local dni, ip
log.info("answer_name, arecod = " .. tostring(answer.name) .. ", " .. tostring(answer.kind.ARecord))

if answer.kind.ARecord ~= nil then
ip = answer.kind.ARecord.ipv4
end

if ip ~= nil then
dni = driver.discovery_helper.get_dni(driver, ip, filtered_responses)

if dni ~= nil then
target_table[dni] = ip
end
end
end
end

local function insert_dni_ip_from_found(driver, filtered_responses, target_table)
for _, found in pairs(filtered_responses.found) do
local dni, ip
log.info("found_name = " .. tostring(found.service_info.service_type))
if found.host_info.address ~= nil and net_utils.validate_ipv4_string(found.host_info.address) then
log.info("ip = " .. tostring(found.host_info.address))
ip = found.host_info.address
end

if ip ~= nil then
dni = driver.discovery_helper.get_dni(driver, ip, filtered_responses)

if dni ~= nil then
target_table[dni] = ip
end
end
end
end

local function get_dni_ip_table_from_mdns_responses(driver, service_type, domain, discovery_responses)
local dni_ip_table = {}

local filtered_responses = filter_response_by_service_name(service_type, domain, discovery_responses)

insert_dni_ip_from_answers(driver, filtered_responses, dni_ip_table)
insert_dni_ip_from_found(driver, filtered_responses, dni_ip_table)

return dni_ip_table
end

function discovery_mdns.find_ip_table_by_mdns(driver)
log.info("discovery_mdns.find_device_ips")
local service_type, domain = driver.discovery_helper.get_service_type_and_domain()
local discovery_responses = mdns.discover(service_type, domain) or { found = {} }
local dni_ip_table = get_dni_ip_table_from_mdns_responses(driver, service_type, domain, discovery_responses)
return dni_ip_table
end

return discovery_mdns
16 changes: 16 additions & 0 deletions drivers/Aqara/aqara-presence-sensor/src/fields.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--- Table of constants used to index in to device store fields
--- @module "fields"
--- @class table
--- @field IPV4 string the ipV4 address of the device

local fields = {
DEVICE_IPV4 = "device_ipv4",
DEVICE_INFO = "device_info",
CONN_INFO = "conn_info",
EVENT_SOURCE = "eventsource",
MONITORING_TIMER = "monitoring_timer",
CREDENTIAL = "credential",
_INIT = "init"
}

return fields
Loading
Loading