The idea is mine, the heavy loading was done by DeepSeek and the finetuning by Claude.
Code: Select all
-- Presence Detection Script for Domoticz
-- This script detects devices on the network using ARP and categorizes them into:
-- 1. Detect: Devices to monitor (e.g., user devices).
-- 2. Ignore: Devices to exclude from monitoring.
-- 3. Unknown: New or unrecognized devices.
-- Devices are read from and written to CSV files for easy management.
-- In DEBUG mode, all actions are logged, and the text device shows detected and unknown devices.
-- In ERROR mode, file writing and reading actions occur only at maintenance time (03:00)
-- The text sensor is updated every timer event like 5 minutes showing to be detected and unknown devices.
-- Output is limited to 16 lines to avoid Domoticz display issues.
-- Designed by humans, scripted by AI.
-- Constants for ease of maintenance
local DETECT_FILE = '/home/pi/csvData/presence_detection_detect.csv'
local IGNORE_FILE = '/home/pi/csvData/presence_detection_ignore.csv'
local UNKNOWN_FILE = '/home/pi/csvData/presence_detection_unknown.csv'
local MAINTENANCE_TIME = '03:00' -- Time for maintenance tasks in LOG_ERROR mode
local SHOW_UNKNOWN_DEVICES = true -- Set to false to exclude unknown devices from the text sensor display
local INITIATE = false -- set up environment including csv files with headers
local HEADER = "device,IP-address,MAC-address,user-name,friendly-device-name" -- HEADER for the csv files
return {
on = {
timer = {
'every 1 minutes' -- Run the script every 1 minute
}
},
logging = {
level = domoticz.LOG_DEBUG, -- Change to LOG_ERROR for maintenance mode
marker = 'PD' --'---- Presence Detection Script -----'
},
execute = function(domoticz)
domoticz.log('Presence detection check started', domoticz.LOG_DEBUG)
-- Text device where the results will be written
local textDevice = domoticz.devices('Presence detected') -- Replace with your text device name
if INITIATE then
-- Function to initialize a file with the header
local function initializeFile(filePath, header)
local file = io.open(filePath, "r")
if file then
-- File already exists, do nothing
file:close()
print("File already exists: " .. filePath)
else
local file = io.open(filePath, "w")
if file then
file:write(header .. "\n")
file:close()
print("Initialized file: " .. filePath)
else
print("Error: Could not open file for writing: " .. filePath)
end
end
end
-- Initialize each file
initializeFile(DETECT_FILE, HEADER)
initializeFile(IGNORE_FILE, HEADER)
initializeFile(UNKNOWN_FILE, HEADER)
end
-- Function to read a CSV file
local function readCSVFile(filePath)
local devices = {}
local file = io.open(filePath, 'r')
if not file then
domoticz.log('Could not open CSV file: ' .. filePath, domoticz.LOG_ERROR)
return devices
end
-- Skip the header line
file:read()
-- Read the file line by line
for line in file:lines() do
line = line:gsub('\r', '') -- Remove carriage returns
local device, ip, mac, user, friendlyName = line:match('([^;]+);([^;]+);([^;]+);([^;]*);([^;]*)')
if device and ip and mac then
table.insert(devices, {
device = device,
ip = ip,
mac = mac:lower(), -- Ensure MAC is in lowercase
user = user,
friendlyName = friendlyName
})
end
end
file:close()
return devices
end
-- Function to write a CSV file
local function writeCSVFile(filePath, devices, header)
local file = io.open(filePath, 'w')
if not file then
domoticz.log('Could not open CSV file for writing: ' .. filePath, domoticz.LOG_ERROR)
return
end
-- Write the header
file:write(header .. '\n')
-- Write the devices
for _, device in ipairs(devices) do
file:write(string.format('%s;%s;%s;%s;%s\n', device.device, device.ip, device.mac, device.user or '', device.friendlyName or ''))
end
file:close()
domoticz.log('CSV file updated successfully: ' .. filePath, domoticz.LOG_DEBUG)
end
-- Persistent variables to retain data between loops
local detectDevices = detectDevices or readCSVFile(DETECT_FILE)
local ignoreDevices = ignoreDevices or readCSVFile(IGNORE_FILE)
local unknownDevices = unknownDevices or readCSVFile(UNKNOWN_FILE)
-- Reload CSV files during maintenance time in ERROR mode
if domoticz.settings.logLevel == domoticz.LOG_ERROR and os.date('%H:%M') == MAINTENANCE_TIME then
detectDevices = readCSVFile(DETECT_FILE)
ignoreDevices = readCSVFile(IGNORE_FILE)
unknownDevices = readCSVFile(UNKNOWN_FILE)
domoticz.log('CSV files reloaded at maintenance time', domoticz.LOG_DEBUG)
end
if domoticz.settings.logLevel == domoticz.LOG_DEBUG then
-- Log the contents of the files for debugging
domoticz.log('Detect devices:', domoticz.LOG_DEBUG)
for _, device in ipairs(detectDevices) do
domoticz.log(string.format('Device: %s, IP: %s, MAC: %s, User: %s, Friendly Name: %s', device.device, device.ip, device.mac, device.user or 'N/A', device.friendlyName or 'N/A'), domoticz.LOG_DEBUG)
end
domoticz.log('Ignore devices:', domoticz.LOG_DEBUG)
for _, device in ipairs(ignoreDevices) do
domoticz.log(string.format('Device: %s, IP: %s, MAC: %s, User: %s, Friendly Name: %s', device.device, device.ip, device.mac, device.user or 'N/A', device.friendlyName or 'N/A'), domoticz.LOG_DEBUG)
end
domoticz.log('Unknown devices:', domoticz.LOG_DEBUG)
for _, device in ipairs(unknownDevices) do
domoticz.log(string.format('Device: %s, IP: %s, MAC: %s, User: %s, Friendly Name: %s', device.device, device.ip, device.mac, device.user or 'N/A', device.friendlyName or 'N/A'), domoticz.LOG_DEBUG)
end
end
-- Empty the ARP-cache every full hour
if os.date('%M') == "00" then
os.execute('sudo ip -s -s neigh flush all')
end
-- Execute `arp -a` to get the current ARP table
local handle = io.popen('arp -a')
local arpOutput = handle:read('*a')
handle:close()
-- Log the ARP table for debugging
domoticz.log('ARP table result:', domoticz.LOG_DEBUG)
domoticz.log(arpOutput, domoticz.LOG_DEBUG)
-- Parse the ARP table and categorize devices
local detectedDevices = {}
local newUnknownDevices = {}
for line in arpOutput:gmatch('[^\r\n]+') do
domoticz.log('Processing ARP line: ' .. line, domoticz.LOG_DEBUG)
-- Try multiple matching patterns to handle different ARP output formats
local hostname, ip, mac
-- Standard pattern: hostname (ip) at mac
hostname, ip, mac = line:match('([^%s]+) %(([^)]+)%) at (%S+)')
-- If standard pattern fails, try pattern for ? (ip) at mac
if not mac then
ip, mac = line:match('%? %(([^)]+)%) at (%S+)')
hostname = "Unknown"
end
-- Ensure we have an IP and MAC (may be "<incomplete>")
if ip and mac then
-- Skip the [ether] part if it exists
mac = mac:gsub('%s+%[ether%]', '')
-- Skip "on eth0" or similar interface identifiers
mac = mac:gsub('%s+on%s+%S+', '')
-- If mac is "<incomplete>", generate a placeholder but still track the device
local isIncomplete = mac:match('<incomplete>')
if isIncomplete then
-- Use IP as a placeholder for incomplete MACs
mac = "incomplete_" .. ip:gsub('%D', '')
else
mac = mac:lower() -- Ensure MAC is in lowercase
end
local found = false
-- Check if the device is in the detect list
for _, device in ipairs(detectDevices) do
if device.mac == mac then
table.insert(detectedDevices, device)
found = true
break
end
end
-- Check if the device is in the ignore list
if not found then
for _, device in ipairs(ignoreDevices) do
if device.mac == mac then
found = true
break
end
end
end
-- If the device is not in detect or ignore, add it to the unknown list
if not found then
local deviceName = hostname or "Unknown"
-- Check if it's a question mark hostname
if deviceName == "?" then deviceName = "Unknown" end
table.insert(newUnknownDevices, {
device = deviceName,
ip = ip,
mac = mac,
user = 'Unknown',
friendlyName = isIncomplete and 'Incomplete MAC' or 'Unknown'
})
domoticz.log(string.format('New unknown device found: %s, %s, %s', deviceName, ip, mac), domoticz.LOG_DEBUG)
end
else
domoticz.log('Could not parse line: ' .. line, domoticz.LOG_DEBUG)
end
end
-- Debug logging for new unknown devices
domoticz.log('New unknown devices count: ' .. #newUnknownDevices, domoticz.LOG_DEBUG)
for _, device in ipairs(newUnknownDevices) do
domoticz.log(string.format('Unknown Device: %s, IP: %s, MAC: %s', device.device, device.ip, device.mac), domoticz.LOG_DEBUG)
end
-- Prepare text device output
local textOutput = {}
if domoticz.settings.logLevel == domoticz.LOG_DEBUG then
-- In DEBUG mode, show detected and unknown devices with full details
table.insert(textOutput, 'Detected devices:')
for _, device in ipairs(detectedDevices) do
table.insert(textOutput, string.format('%s, %s, %s, %s, %s', device.device, device.ip, device.mac, device.user or 'N/A', device.friendlyName or 'N/A'))
end
if #newUnknownDevices > 0 then
table.insert(textOutput, 'Unknown devices:')
for _, device in ipairs(newUnknownDevices) do
table.insert(textOutput, string.format('%s, %s, %s', device.device, device.ip, device.mac))
end
else
table.insert(textOutput, 'No unknown devices')
end
else
-- In ERROR mode, show detected devices with user and friendly name, and unknown devices with basic details
table.insert(textOutput, 'Detected devices:')
for _, device in ipairs(detectedDevices) do
table.insert(textOutput, string.format('%s, %s', device.user or 'N/A', device.friendlyName or 'N/A'))
end
if SHOW_UNKNOWN_DEVICES and #newUnknownDevices > 0 then
table.insert(textOutput, 'Unknown devices:')
for _, device in ipairs(newUnknownDevices) do
table.insert(textOutput, string.format('%s, %s, %s', device.device, device.ip, device.mac))
end
end
end
-- Limit output to 16 lines
local maxLines = 16
local limitedOutput = {}
for i = 1, math.min(#textOutput, maxLines) do
table.insert(limitedOutput, textOutput[i])
end
if #textOutput > maxLines then
table.insert(limitedOutput, '... (more devices detected)')
end
-- Update the text device
textDevice.updateText(table.concat(limitedOutput, '\n'))
-- Write the updated unknown devices to the unknown file (only at maintenance time in LOG_ERROR mode)
if domoticz.settings.logLevel == domoticz.LOG_DEBUG or os.date('%H:%M') == MAINTENANCE_TIME then
writeCSVFile(UNKNOWN_FILE, newUnknownDevices, 'device;IP-address;MAC-address;user-name;friendly-device-name')
end
end
}
All you have to do is download the script and load it in your Domoticz.
Then add a new Text device with the name 'Presence detected' and off you go.
The files are created by the script by setting local INITIATE = true (don't forget to change it back to false after the files are created)
Set the time trigger back to 5 minutes when everything is OK and change LOG_DEBUG to LOG_ERROR (level = domoticz.LOG_ERROR)
To select the devices that are displayed install WinSCP on Windows.
Connect to your RPI and open the file presence_detection_unknown.csv
Select the devices that you want to be detected copy those lines to presence_detection_detect.csv and delete these lines from presence_detection_unknown.csv
The devices that can be ignored like your RPI, your computer etc. can be put in the file presence_detection_ignore.csv. Don't forget to delete these from presence_detection_unknown.csv
Once you get visitors in your house you'll find those devices in presence_detection_unknown.csv and in the text sensor 'Presence detected'. You can choose if you want to add those devices to your detect file or your ignore file.
The header of the files show how you can create nice output.
I did it like this:
Code: Select all
device,IP-address,MAC-address,user-name,friendly-device-name
a54.home;192.168.2.4;16:64:81:93:72:82;myName;Samsung A54
The text before that <a54.home;192.168.2.4;16:64:87:95:69:80;> is created by arp and the script.