Skip to main content

Wake On Lan Plugin

About

This simple plugin provides ability to wake up ethernet devices remotely using Wake On Lan packet

note

Device must be on subnet and most likely must be configured properly to allow it to be waken up this way, consult your OS/hardware documentation

Wake Up Lan plugin does not need any special instalation or configuration steps, just follow the generic instructions

Source code

Plugin source code
local json = require("json")
local io = require("io")
local uv = require("uv")
local dgram = require("dgram")
local timer = require("timer")
local plugin = {}

local function print_variable(var, ident, depth)
if depth > 10 then
print(ident .. "...")
return
end
if type(var) == "table" then
print(ident .. "{")
for k, v in pairs(var) do
print(ident .. " " .. tostring(k) .. " = ")
print_variable(v, ident .. " ", depth + 1)
end
print(ident .. "}")
else
print(ident .. tostring(var))
end
end

-- Plugin definition
plugin.name = "Wake on LAN"
plugin.version = { 0, 0, 1 }

plugin.provided_interfaces = {
WakeOnLAN = {
actions = {
wakeUp = {
description = "Send WOL packet to computer",
arguments = {},
handler = function(self, args)
print("Wake up called")
return true
end
}
},
parameters = {
identity = {
type = "string",
value = "<Computer MAC address>"
},
version = {
type = "integer",
value = "1"
},
manufacturer = {
type = "string",
value = "ConnectHome"
},
network_interface = {
type = "string",
value = "eth0"
}
}
}
}

plugin.overridden_interfaces = {
SwitchBinary = {
actions = {
setStatus = {}
}
}
}

plugin.devices = {
Computer = {
type = "DevSwitch",
role = "Control",
icon = "ch-socket",
interfaces = { "WakeOnLAN", "SwitchBinary" },
parameters = {
identity = {
type = "string",
value = "11:22:33:44:55:66",
role = "edit",
locale = {
en = "MAC address",
ru = "MAC адрес",
ua = "MAC адреса"
}
},
version = {
type = "integer",
value = 1
},
manufacturer = {
type = "string",
value = "ConnectHome",
role = "show",
locale = {
en = "Manufacturer",
ru = "Производитель",
ua = "Виробник"
}
},
network_interface = {
type = "string",
value = "eth0",
role = "edit",
locale = {
en = "Network interface",
ru = "Сетевой интерфейс",
ua = "Мережевий інтерфейс"
}
},
Status = {
type = "boolean",
value = false,
},
pulsable = {
type = "boolean",
value = false,
}
}
}
}

-- Plugin initialization
function plugin:init(reason)
print("Wake on LAN plugin init")
end

-- Plugin registered
function plugin:register(uuid)
print("Wake on LAN plugin registered")
end

-- Plugin unregistered
function plugin:unregister(topic, payload)
print("Wake on LAN plugin unregistered")
end

local function gethostname(ip)
local address = {
ip = ip,
port = nil,
family = nil
}
local hostname = uv.getnameinfo(address, nil)
return hostname
end

-- Discover devices
function plugin:discover_devices(topic, payload)
print("Discover devices")

local discovery_result = {}

-- /sbin/ip -json neighbour
local f = io.popen("/sbin/ip -json neighbour")
local output = f:read("*a")
f:close()

local json_data = json.decode(output)

local discovered_devices = {} -- Table to keep track of discovered devices

for _, v in ipairs(json_data) do
if v["state"][1] ~= "FAILED" then
local mac = v["lladdr"]
local ip = v["dst"]
local hostname = gethostname(ip)
local state = v["state"][1]
print("Discovered device: " .. mac .. " " .. ip .. " " .. hostname .. " " .. state)

-- Check if device with the same MAC address already exists
if not discovered_devices[mac] then
local device = {
identity = mac,
info = ip,
type = "Computer",
icon = "ch-socket",
params = {
network_interface = v["dev"],
}
}
table.insert(discovery_result, device)
discovered_devices[mac] = true -- Mark device as discovered
end
end
end


self:reply(topic, payload.message_id, 200, true, {
devices = discovery_result
})
end

-- helper function
local function get_params_name_value(device, identity)
local params = {}
for k, v in pairs(device.parameters) do
params[k] = v.value
end
if identity then
params.identity = identity
end
return params
end

function plugin:device_create(topic, payload)
print("Device create")
local message_id = payload.message_id
local name = payload.body.params.name
local dev_type = payload.body.params.type or ""
local identity = payload.body.params.identity
local room_id = payload.body.params.room_id or 0
local params = payload.body.params.params or {}
if not message_id or not name or not identity or not dev_type or not room_id then
self:reply(topic, message_id, 400, true, {
error = "Missing required parameters"
})
return
end

local new_device = plugin.devices and plugin.devices[dev_type]
if not new_device then
self:reply(topic, message_id, 400, true, {
error = "Unknown device type"
})
return
end

self:pub {
topic = "internal/plugins_hub/" .. self.uuid .. "/devices/create",
payload = json.stringify({
version = "2",
method = "POST",
message_id = self.mqtt:pending_message_add(new_device),
token = self.token,
body = {
plugin_type = dev_type,
name = name,
type = new_device.type,
role = new_device.role,
alive = true,
hidden = false,
room_id = room_id,
interfaces = new_device.interfaces,
icon = new_device.icon,
params = get_params_name_value(new_device, identity)
}
}),
qos = 2
}

self:reply(topic, message_id, 200, true)
end

-- Device was created
function plugin:device_created(topic, payload)
print("Device created")

if not payload.body.success then
print("Device created error:", payload.status)
return
end

self:db_add_device(payload.body.device_id, payload.body.plugin_type)
self:db_add_default_params(payload.body.device_id, payload.body.interfaces, payload.body.params)
end

-- callback
function plugin:device_registered(device_id, name, params)
print("device_registered")
self:notify("Registered device " .. device_id)
end

-- callback
function plugin:device_removed(topic, payload)
print("Device removed")
p(payload)
if payload.body.success ~= true then
print("plugin device_removed error:", payload.status)
return
end
local device_id = payload.body.device_id
-- removing items from db
self:db_delete_params(device_id)
self:db_remove_device(device_id)
end

-- Prepare and send WOL packet
local function send_wol_packet(mac, broadcast_address)
local mac_bytes = {}
local magic_packet_str = ""

-- Convert MAC address to byte array
local mac0, mac1, mac2, mac3, mac4, mac5 = string.match(mac, "(%x%x):(%x%x):(%x%x):(%x%x):(%x%x):(%x%x)")
mac_bytes = {
tonumber(mac0, 16),
tonumber(mac1, 16),
tonumber(mac2, 16),
tonumber(mac3, 16),
tonumber(mac4, 16),
tonumber(mac5, 16)
}

-- Add 6 bytes with 0xFF
for i = 1, 6 do
magic_packet_str = magic_packet_str .. string.char(0xFF)
end

-- Add 16 repetitions of MAC address
for i = 1, 16 do
for j = 1, 6 do
magic_packet_str = magic_packet_str .. string.char(mac_bytes[j])
end
end

-- Send packet
local socket = dgram:createSocket("udp4")
socket:bind(0, broadcast_address)
socket:setBroadcast(true)

-- Repeat packet sending 5 times
for i = 1, 5 do
socket:send(magic_packet_str, 9, broadcast_address, function(err)
if err then
print("Error sending WOL packet: " .. err)
end
if i == 5 then
socket:close()
end
end)
end
end

-- Convert IP address from string to binary form
local function inet_pton(ip_str)
local ip_bytes = {}
local ip_addr = 0

local ip0, ip1, ip2, ip3 = string.match(ip_str, "(%d+)%.(%d+)%.(%d+)%.(%d+)")

if ip0 and ip1 and ip2 and ip3 then
ip_bytes = {
tonumber(ip0),
tonumber(ip1),
tonumber(ip2),
tonumber(ip3)
}
else
return 0
end

for i = 1, 4 do
ip_addr = bit.bor(ip_addr, bit.lshift(ip_bytes[i], (4 - i) * 8))
end

return ip_addr
end

-- Convert IP address from binary form to string
local function inet_ntoa(ip_addr)
local ip_bytes = {}
local ip_str = ""

for i = 1, 4 do
ip_bytes[i] = bit.band(bit.rshift(ip_addr, (4 - i) * 8), 0xFF)
end

ip_str = ip_bytes[1] .. "." .. ip_bytes[2] .. "." .. ip_bytes[3] .. "." .. ip_bytes[4]

return ip_str
end

-- Get broadcast address for specified network interface
local function get_broadcast_address(interface)
local addresses = uv.interface_addresses()
local interface_address = addresses[interface]
if not interface_address then
return nil
end


for _, address in ipairs(interface_address) do
if address.family == "inet" then
local ip = inet_pton(address.ip)
local netmask = inet_pton(address.netmask)
local broadcast = inet_ntoa(bit.bor(ip, bit.bnot(netmask)));

print(" IP: " .. address.ip)
print(" Netmask: " .. address.netmask)
print("Broadcast: " .. broadcast)

return broadcast
end
end

return nil
end

function plugin:action(topic, payload)
print("Action")
local device_id = tonumber(self:get_topic_segment(topic, 5))

if payload.body.method == "setStatus" and payload.body.interface == "SwitchBinary" and payload.body.params then
local status = payload.body.params[1] or false
print("Set status: " .. tostring(status) .. " for device " .. tostring(device_id))

if status == "true" then
local mac_address = self:db_get_param(device_id, "identity")
local network_interface = self:db_get_param(device_id, "network_interface")
local broadcast_address = get_broadcast_address(network_interface)

if not broadcast_address then
print("Broadcast address not found")
return
end

-- Send WOL packet
send_wol_packet(mac_address, broadcast_address)

timer.setTimeout(107, function()
self:update_param_boolean(device_id, "Status", false)
end)
end
end
end

return plugin