From 303541929caa0d87be54894458a76e7411ab299b Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Sun, 19 Oct 2025 04:23:03 +0200 Subject: [PATCH] chore: Restructured config to not rely on mqtt client being available In preparation of changes to the mqtt client the config is rewritten to use a device creation function for devices that need the mqtt client. This also fixes a but where hallway_top_light was not actually added to the device manager. --- config.lua | 1213 ++++++++++++++++++++++++++-------------------------- 1 file changed, 612 insertions(+), 601 deletions(-) diff --git a/config.lua b/config.lua index 2920bf6..24b9566 100644 --- a/config.lua +++ b/config.lua @@ -21,296 +21,8 @@ local function mqtt_automation(topic) return "automation/" .. topic end -local mqtt_client = require("automation:mqtt").new(device_manager, { - host = ((host == "zeus" or host == "hephaestus") and "olympus.lan.huizinga.dev") or "mosquitto", - port = 8883, - client_name = "automation-" .. host, - username = "mqtt", - password = secrets.mqtt_password, - tls = host == "zeus" or host == "hephaestus", -}) - -local devs = {} -function devs:add(device) - table.insert(self, device) -end - -local ntfy_topic = secrets.ntfy_topic -if ntfy_topic == nil then - error("Ntfy topic is not specified") -end -local ntfy = devices.Ntfy.new({ - topic = ntfy_topic, -}) -devs:add(ntfy) - ---- @type {[string]: number} -local low_battery = {} ---- @param device DeviceInterface ---- @param battery number -local function check_battery(device, battery) - local id = device:get_id() - if battery < 15 then - print("Device '" .. id .. "' has low battery: " .. tostring(battery)) - low_battery[id] = battery - else - low_battery[id] = nil - end -end -local function notify_low_battery() - -- Don't send notifications if there are now devices with low battery - if next(low_battery) == nil then - print("No devices with low battery") - return - end - - local lines = {} - for name, battery in pairs(low_battery) do - table.insert(lines, name .. ": " .. tostring(battery) .. "%") - end - local message = table.concat(lines, "\n") - - ntfy:send_notification({ - title = "Low battery", - message = message, - tags = { "battery" }, - priority = "default", - }) -end - ---- @class OnPresence ---- @field [integer] fun(presence: boolean) -local on_presence = {} ---- @param f fun(presence: boolean) -function on_presence:add(f) - self[#self + 1] = f -end - -local presence_system = devices.Presence.new({ - topic = mqtt_automation("presence/+/#"), - client = mqtt_client, - callback = function(_, presence) - for _, f in ipairs(on_presence) do - if type(f) == "function" then - f(presence) - end - end - end, -}) -devs:add(presence_system) -on_presence:add(function(presence) - ntfy:send_notification({ - title = "Presence", - message = presence and "Home" or "Away", - tags = { "house" }, - priority = "low", - actions = { - { - action = "broadcast", - extras = { - cmd = "presence", - state = presence and "0" or "1", - }, - label = presence and "Set away" or "Set home", - clear = true, - }, - }, - }) -end) -on_presence:add(function(presence) - mqtt_client:send_message(mqtt_automation("debug") .. "/presence", { - state = presence, - updated = utils.get_epoch(), - }) -end) - ---- @class WindowSensor ---- @field [integer] OpenCloseInterface -local window_sensors = {} ---- @param sensor OpenCloseInterface -function window_sensors:add(sensor) - self[#self + 1] = sensor -end -on_presence:add(function(presence) - if not presence then - local open = {} - for _, sensor in ipairs(window_sensors) do - if sensor:open_percent() > 0 then - local id = sensor:get_id() - print("Open window detected: " .. id) - table.insert(open, id) - end - end - - if #open > 0 then - local message = table.concat(open, "\n") - - ntfy:send_notification({ - title = "Windows are open", - message = message, - tags = { "window" }, - priority = "high", - }) - end - end -end) - ---- @param device OnOffInterface -local function turn_off_when_away(device) - on_presence:add(function(presence) - if not presence then - device:set_on(false) - end - end) -end - ---- @class OnLight ---- @field [integer] fun(light: boolean) -local on_light = {} ---- @param f fun(light: boolean) -function on_light:add(f) - self[#self + 1] = f -end -devs:add(devices.LightSensor.new({ - identifier = "living_light_sensor", - topic = mqtt_z2m("living/light"), - client = mqtt_client, - min = 22000, - max = 23500, - callback = function(_, light) - for _, f in ipairs(on_light) do - if type(f) == "function" then - f(light) - end - end - end, -})) -on_light:add(function(light) - mqtt_client:send_message(mqtt_automation("debug") .. "/darkness", { - state = not light, - updated = utils.get_epoch(), - }) -end) - -local hue_ip = "10.0.0.102" -local hue_token = secrets.hue_token -if hue_token == nil then - error("Hue token is not specified") -end - -local hue_bridge = devices.HueBridge.new({ - identifier = "hue_bridge", - ip = hue_ip, - login = hue_token, - flags = { - presence = 41, - darkness = 43, - }, -}) -devs:add(hue_bridge) -on_light:add(function(light) - hue_bridge:set_flag("darkness", not light) -end) -on_presence:add(function(presence) - hue_bridge:set_flag("presence", presence) -end) - -local kitchen_lights = devices.HueGroup.new({ - identifier = "kitchen_lights", - ip = hue_ip, - login = hue_token, - group_id = 7, - scene_id = "7MJLG27RzeRAEVJ", -}) -devs:add(kitchen_lights) -local living_lights = devices.HueGroup.new({ - identifier = "living_lights", - ip = hue_ip, - login = hue_token, - group_id = 1, - scene_id = "SNZw7jUhQ3cXSjkj", -}) -devs:add(living_lights) -local living_lights_relax = devices.HueGroup.new({ - identifier = "living_lights", - ip = hue_ip, - login = hue_token, - group_id = 1, - scene_id = "eRJ3fvGHCcb6yNw", -}) -devs:add(living_lights_relax) - -devs:add(devices.HueSwitch.new({ - name = "Switch", - room = "Living", - client = mqtt_client, - topic = mqtt_z2m("living/switch"), - left_callback = function() - kitchen_lights:set_on(not kitchen_lights:on()) - end, - right_callback = function() - living_lights:set_on(not living_lights:on()) - end, - right_hold_callback = function() - living_lights_relax:set_on(true) - end, - battery_callback = check_battery, -})) - -devs:add(devices.WakeOnLAN.new({ - name = "Zeus", - room = "Living Room", - topic = mqtt_automation("appliance/living_room/zeus"), - client = mqtt_client, - mac_address = "30:9c:23:60:9c:13", - broadcast_ip = "10.0.3.255", -})) - -local living_mixer = devices.OutletOnOff.new({ - name = "Mixer", - room = "Living Room", - topic = mqtt_z2m("living/mixer"), - client = mqtt_client, -}) -turn_off_when_away(living_mixer) -devs:add(living_mixer) -local living_speakers = devices.OutletOnOff.new({ - name = "Speakers", - room = "Living Room", - topic = mqtt_z2m("living/speakers"), - client = mqtt_client, -}) -turn_off_when_away(living_speakers) -devs:add(living_speakers) - -devs:add(devices.IkeaRemote.new({ - name = "Remote", - room = "Living Room", - client = mqtt_client, - topic = mqtt_z2m("living/remote"), - single_button = true, - callback = function(_, on) - if on then - if living_mixer:on() then - living_mixer:set_on(false) - living_speakers:set_on(false) - else - living_mixer:set_on(true) - living_speakers:set_on(true) - end - else - if not living_mixer:on() then - living_mixer:set_on(true) - else - living_speakers:set_on(not living_speakers:on()) - end - end - end, - battery_callback = check_battery, -})) - --- @return fun(self: OnOffInterface, state: {state: boolean, power: number}) -local function kettle_timeout() +local function auto_off() local timeout = utils.Timeout.new() return function(self, state) @@ -324,43 +36,6 @@ local function kettle_timeout() end end ---- @type OutletPower -local kettle = devices.OutletPower.new({ - outlet_type = "Kettle", - name = "Kettle", - room = "Kitchen", - topic = mqtt_z2m("kitchen/kettle"), - client = mqtt_client, - callback = kettle_timeout(), -}) -turn_off_when_away(kettle) -devs:add(kettle) - ---- @param on boolean -local function set_kettle(_, on) - kettle:set_on(on) -end - -devs:add(devices.IkeaRemote.new({ - name = "Remote", - room = "Bedroom", - client = mqtt_client, - topic = mqtt_z2m("bedroom/remote"), - single_button = true, - callback = set_kettle, - battery_callback = check_battery, -})) - -devs:add(devices.IkeaRemote.new({ - name = "Remote", - room = "Kitchen", - client = mqtt_client, - topic = mqtt_z2m("kitchen/remote"), - single_button = true, - callback = set_kettle, - battery_callback = check_battery, -})) - --- @param duration number --- @return fun(self: OnOffInterface, state: {state: boolean}) local function off_timeout(duration) @@ -377,108 +52,6 @@ local function off_timeout(duration) end end -local bathroom_light = devices.LightOnOff.new({ - name = "Light", - room = "Bathroom", - topic = mqtt_z2m("bathroom/light"), - client = mqtt_client, - callback = off_timeout(debug and 60 or 45 * 60), -}) -devs:add(bathroom_light) - -devs:add(devices.Washer.new({ - identifier = "bathroom_washer", - topic = mqtt_z2m("bathroom/washer"), - client = mqtt_client, - threshold = 1, - done_callback = function() - ntfy:send_notification({ - title = "Laundy is done", - message = "Don't forget to hang it!", - tags = { "womans_clothes" }, - priority = "high", - }) - end, -})) - -devs:add(devices.OutletOnOff.new({ - name = "Charger", - room = "Workbench", - topic = mqtt_z2m("workbench/charger"), - client = mqtt_client, - callback = off_timeout(debug and 5 or 20 * 3600), -})) - -local workbench_outlet = devices.OutletOnOff.new({ - name = "Outlet", - room = "Workbench", - topic = mqtt_z2m("workbench/outlet"), - client = mqtt_client, -}) -turn_off_when_away(workbench_outlet) -devs:add(workbench_outlet) - -local workbench_light = devices.LightColorTemperature.new({ - name = "Light", - room = "Workbench", - topic = mqtt_z2m("workbench/light"), - client = mqtt_client, -}) -turn_off_when_away(workbench_light) -devs:add(workbench_light) - -local delay_color_temp = utils.Timeout.new() -devs:add(devices.IkeaRemote.new({ - name = "Remote", - room = "Workbench", - client = mqtt_client, - topic = mqtt_z2m("workbench/remote"), - callback = function(_, on) - delay_color_temp:cancel() - if on then - workbench_light:set_brightness(82) - -- NOTE: This light does NOT support changing both the brightness and color - -- temperature at the same time, so we first change the brightness and once - -- that is complete we change the color temperature, as that is less likely - -- to have to actually change. - delay_color_temp:start(0.5, function() - workbench_light:set_color_temperature(3333) - end) - else - workbench_light:set_on(false) - end - end, - battery_callback = check_battery, -})) - -local hallway_top_light = devices.HueGroup.new({ - identifier = "hallway_top_light", - ip = hue_ip, - login = hue_token, - group_id = 83, - scene_id = "QeufkFDICEHWeKJ7", -}) -devs:add(devices.HueSwitch.new({ - name = "SwitchBottom", - room = "Hallway", - client = mqtt_client, - topic = mqtt_z2m("hallway/switchbottom"), - left_callback = function() - hallway_top_light:set_on(not hallway_top_light:on()) - end, - battery_callback = check_battery, -})) -devs:add(devices.HueSwitch.new({ - name = "SwitchTop", - room = "Hallway", - client = mqtt_client, - topic = mqtt_z2m("hallway/switchtop"), - left_callback = function() - hallway_top_light:set_on(not hallway_top_light:on()) - end, - battery_callback = check_battery, -})) - local hallway_light_automation = { timeout = utils.Timeout.new(), forced = false, @@ -543,16 +116,175 @@ function hallway_light_automation:light_callback() end end -local hallway_storage = devices.LightBrightness.new({ - name = "Storage", - room = "Hallway", - topic = mqtt_z2m("hallway/storage"), - client = mqtt_client, - callback = hallway_light_automation:light_callback(), -}) -turn_off_when_away(hallway_storage) -devs:add(hallway_storage) +--- @class OnPresence +--- @field [integer] fun(presence: boolean) +local on_presence = {} +--- @param f fun(presence: boolean) +function on_presence:add(f) + self[#self + 1] = f +end +--- @param device OnOffInterface +local function turn_off_when_away(device) + on_presence:add(function(presence) + if not presence then + device:set_on(false) + end + end) +end + +--- @class WindowSensor +--- @field [integer] OpenCloseInterface +local window_sensors = {} +--- @param sensor OpenCloseInterface +function window_sensors:add(sensor) + self[#self + 1] = sensor +end + +--- @class OnLight +--- @field [integer] fun(light: boolean) +local on_light = {} +--- @param f fun(light: boolean) +function on_light:add(f) + self[#self + 1] = f +end + +--- @type {[string]: number} +local low_battery = {} +--- @param device DeviceInterface +--- @param battery number +local function check_battery(device, battery) + local id = device:get_id() + if battery < 15 then + print("Device '" .. id .. "' has low battery: " .. tostring(battery)) + low_battery[id] = battery + else + low_battery[id] = nil + end +end + +local ntfy_topic = secrets.ntfy_topic +if ntfy_topic == nil then + error("Ntfy topic is not specified") +end +local ntfy = devices.Ntfy.new({ + topic = ntfy_topic, +}) + +on_presence:add(function(presence) + ntfy:send_notification({ + title = "Presence", + message = presence and "Home" or "Away", + tags = { "house" }, + priority = "low", + actions = { + { + action = "broadcast", + extras = { + cmd = "presence", + state = presence and "0" or "1", + }, + label = presence and "Set away" or "Set home", + clear = true, + }, + }, + }) +end) + +on_presence:add(function(presence) + if not presence then + local open = {} + for _, sensor in ipairs(window_sensors) do + if sensor:open_percent() > 0 then + local id = sensor:get_id() + print("Open window detected: " .. id) + table.insert(open, id) + end + end + + if #open > 0 then + local message = table.concat(open, "\n") + + ntfy:send_notification({ + title = "Windows are open", + message = message, + tags = { "window" }, + priority = "high", + }) + end + end +end) + +local function notify_low_battery() + -- Don't send notifications if there are now devices with low battery + if next(low_battery) == nil then + print("No devices with low battery") + return + end + + local lines = {} + for name, battery in pairs(low_battery) do + table.insert(lines, name .. ": " .. tostring(battery) .. "%") + end + local message = table.concat(lines, "\n") + + ntfy:send_notification({ + title = "Low battery", + message = message, + tags = { "battery" }, + priority = "default", + }) +end + +local hue_ip = "10.0.0.102" +local hue_token = secrets.hue_token +if hue_token == nil then + error("Hue token is not specified") +end +local hue_bridge = devices.HueBridge.new({ + identifier = "hue_bridge", + ip = hue_ip, + login = hue_token, + flags = { + presence = 41, + darkness = 43, + }, +}) +on_light:add(function(light) + hue_bridge:set_flag("darkness", not light) +end) +on_presence:add(function(presence) + hue_bridge:set_flag("presence", presence) +end) + +local kitchen_lights = devices.HueGroup.new({ + identifier = "kitchen_lights", + ip = hue_ip, + login = hue_token, + group_id = 7, + scene_id = "7MJLG27RzeRAEVJ", +}) +local living_lights = devices.HueGroup.new({ + identifier = "living_lights", + ip = hue_ip, + login = hue_token, + group_id = 1, + scene_id = "SNZw7jUhQ3cXSjkj", +}) +local living_lights_relax = devices.HueGroup.new({ + identifier = "living_lights", + ip = hue_ip, + login = hue_token, + group_id = 1, + scene_id = "eRJ3fvGHCcb6yNw", +}) +local hallway_top_light = devices.HueGroup.new({ + identifier = "hallway_top_light", + ip = hue_ip, + login = hue_token, + group_id = 83, + scene_id = "QeufkFDICEHWeKJ7", +}) local hallway_bottom_lights = devices.HueGroup.new({ identifier = "hallway_bottom_lights", ip = hue_ip, @@ -560,94 +292,6 @@ local hallway_bottom_lights = devices.HueGroup.new({ group_id = 81, scene_id = "3qWKxGVadXFFG4o", }) -devs:add(hallway_bottom_lights) - -hallway_light_automation.group = { - set_on = function(on) - if on then - hallway_storage:set_brightness(80) - else - hallway_storage:set_on(false) - end - hallway_bottom_lights:set_on(on) - end, -} - ----@param duration number ----@return fun(_, open: boolean) -local function presence(duration) - local timeout = utils.Timeout.new() - - return function(_, open) - if open then - timeout:cancel() - - if not presence_system:overall_presence() then - mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), { - state = true, - updated = utils.get_epoch(), - }) - end - else - timeout:start(duration, function() - mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), nil) - end) - end - end -end - -devs:add(devices.IkeaRemote.new({ - name = "Remote", - room = "Hallway", - client = mqtt_client, - topic = mqtt_z2m("hallway/remote"), - callback = hallway_light_automation:switch_callback(), - battery_callback = check_battery, -})) -local hallway_frontdoor = devices.ContactSensor.new({ - name = "Frontdoor", - room = "Hallway", - sensor_type = "Door", - topic = mqtt_z2m("hallway/frontdoor"), - client = mqtt_client, - callback = { - presence(debug and 10 or 15 * 60), - hallway_light_automation:door_callback(), - }, - battery_callback = check_battery, -}) -devs:add(hallway_frontdoor) -window_sensors:add(hallway_frontdoor) -hallway_light_automation.door = hallway_frontdoor - -local hallway_trash = devices.ContactSensor.new({ - name = "Trash", - room = "Hallway", - sensor_type = "Drawer", - topic = mqtt_z2m("hallway/trash"), - client = mqtt_client, - callback = hallway_light_automation:trash_callback(), - battery_callback = check_battery, -}) -devs:add(hallway_trash) -hallway_light_automation.trash = hallway_trash - -local guest_light = devices.LightOnOff.new({ - name = "Light", - room = "Guest Room", - topic = mqtt_z2m("guest/light"), - client = mqtt_client, -}) -turn_off_when_away(guest_light) -devs:add(guest_light) - -local bedroom_air_filter = devices.AirFilter.new({ - name = "Air Filter", - room = "Bedroom", - url = "http://10.0.0.103", -}) -devs:add(bedroom_air_filter) - local bedroom_lights = devices.HueGroup.new({ identifier = "bedroom_lights", ip = hue_ip, @@ -655,7 +299,6 @@ local bedroom_lights = devices.HueGroup.new({ group_id = 3, scene_id = "PvRs-lGD4VRytL9", }) -devs:add(bedroom_lights) local bedroom_lights_relax = devices.HueGroup.new({ identifier = "bedroom_lights", ip = hue_ip, @@ -663,87 +306,443 @@ local bedroom_lights_relax = devices.HueGroup.new({ group_id = 3, scene_id = "60tfTyR168v2csz", }) -devs:add(bedroom_lights_relax) -devs:add(devices.HueSwitch.new({ - name = "Switch", +local bedroom_air_filter = devices.AirFilter.new({ + name = "Air Filter", room = "Bedroom", - client = mqtt_client, - topic = mqtt_z2m("bedroom/switch"), - left_callback = function() - bedroom_lights:set_on(not bedroom_lights:on()) - end, - left_hold_callback = function() - bedroom_lights_relax:set_on(true) - end, - battery_callback = check_battery, -})) + url = "http://10.0.0.103", +}) -local balcony = devices.ContactSensor.new({ - name = "Balcony", - room = "Living Room", - sensor_type = "Door", - topic = mqtt_z2m("living/balcony"), - client = mqtt_client, - battery_callback = check_battery, -}) -devs:add(balcony) -window_sensors:add(balcony) -local living_window = devices.ContactSensor.new({ - name = "Window", - room = "Living Room", - topic = mqtt_z2m("living/window"), - client = mqtt_client, - battery_callback = check_battery, -}) -devs:add(living_window) -window_sensors:add(living_window) -local bedroom_window = devices.ContactSensor.new({ - name = "Window", - room = "Bedroom", - topic = mqtt_z2m("bedroom/window"), - client = mqtt_client, - battery_callback = check_battery, -}) -devs:add(bedroom_window) -window_sensors:add(bedroom_window) -local guest_window = devices.ContactSensor.new({ - name = "Window", - room = "Guest Room", - topic = mqtt_z2m("guest/window"), - client = mqtt_client, - battery_callback = check_battery, -}) -devs:add(guest_window) -window_sensors:add(guest_window) +local function create_devs(mqtt_client) + on_presence:add(function(presence) + mqtt_client:send_message(mqtt_automation("debug") .. "/presence", { + state = presence, + updated = utils.get_epoch(), + }) + end) -local storage_light = devices.LightBrightness.new({ - name = "Light", - room = "Storage", - topic = mqtt_z2m("storage/light"), - client = mqtt_client, -}) -turn_off_when_away(storage_light) -devs:add(storage_light) + on_light:add(function(light) + mqtt_client:send_message(mqtt_automation("debug") .. "/darkness", { + state = not light, + updated = utils.get_epoch(), + }) + end) -devs:add(devices.ContactSensor.new({ - name = "Door", - room = "Storage", - sensor_type = "Door", - topic = mqtt_z2m("storage/door"), - client = mqtt_client, - callback = function(_, open) - if open then - storage_light:set_brightness(100) - else - storage_light:set_on(false) + local devs = {} + function devs:add(device) + table.insert(self, device) + end + + local presence_system = devices.Presence.new({ + topic = mqtt_automation("presence/+/#"), + client = mqtt_client, + callback = function(_, presence) + for _, f in ipairs(on_presence) do + if type(f) == "function" then + f(presence) + end + end + end, + }) + devs:add(presence_system) + + devs:add(devices.LightSensor.new({ + identifier = "living_light_sensor", + topic = mqtt_z2m("living/light"), + client = mqtt_client, + min = 22000, + max = 23500, + callback = function(_, light) + for _, f in ipairs(on_light) do + if type(f) == "function" then + f(light) + end + end + end, + })) + + devs:add(devices.HueSwitch.new({ + name = "Switch", + room = "Living", + client = mqtt_client, + topic = mqtt_z2m("living/switch"), + left_callback = function() + kitchen_lights:set_on(not kitchen_lights:on()) + end, + right_callback = function() + living_lights:set_on(not living_lights:on()) + end, + right_hold_callback = function() + living_lights_relax:set_on(true) + end, + battery_callback = check_battery, + })) + + devs:add(devices.WakeOnLAN.new({ + name = "Zeus", + room = "Living Room", + topic = mqtt_automation("appliance/living_room/zeus"), + client = mqtt_client, + mac_address = "30:9c:23:60:9c:13", + broadcast_ip = "10.0.3.255", + })) + + local living_mixer = devices.OutletOnOff.new({ + name = "Mixer", + room = "Living Room", + topic = mqtt_z2m("living/mixer"), + client = mqtt_client, + }) + turn_off_when_away(living_mixer) + devs:add(living_mixer) + local living_speakers = devices.OutletOnOff.new({ + name = "Speakers", + room = "Living Room", + topic = mqtt_z2m("living/speakers"), + client = mqtt_client, + }) + turn_off_when_away(living_speakers) + devs:add(living_speakers) + + devs:add(devices.IkeaRemote.new({ + name = "Remote", + room = "Living Room", + client = mqtt_client, + topic = mqtt_z2m("living/remote"), + single_button = true, + callback = function(_, on) + if on then + if living_mixer:on() then + living_mixer:set_on(false) + living_speakers:set_on(false) + else + living_mixer:set_on(true) + living_speakers:set_on(true) + end + else + if not living_mixer:on() then + living_mixer:set_on(true) + else + living_speakers:set_on(not living_speakers:on()) + end + end + end, + battery_callback = check_battery, + })) + + --- @type OutletPower + local kettle = devices.OutletPower.new({ + outlet_type = "Kettle", + name = "Kettle", + room = "Kitchen", + topic = mqtt_z2m("kitchen/kettle"), + client = mqtt_client, + callback = auto_off(), + }) + turn_off_when_away(kettle) + devs:add(kettle) + + --- @param on boolean + local function set_kettle(_, on) + kettle:set_on(on) + end + + devs:add(devices.IkeaRemote.new({ + name = "Remote", + room = "Bedroom", + client = mqtt_client, + topic = mqtt_z2m("bedroom/remote"), + single_button = true, + callback = set_kettle, + battery_callback = check_battery, + })) + + devs:add(devices.IkeaRemote.new({ + name = "Remote", + room = "Kitchen", + client = mqtt_client, + topic = mqtt_z2m("kitchen/remote"), + single_button = true, + callback = set_kettle, + battery_callback = check_battery, + })) + + local bathroom_light = devices.LightOnOff.new({ + name = "Light", + room = "Bathroom", + topic = mqtt_z2m("bathroom/light"), + client = mqtt_client, + callback = off_timeout(debug and 60 or 45 * 60), + }) + devs:add(bathroom_light) + + devs:add(devices.Washer.new({ + identifier = "bathroom_washer", + topic = mqtt_z2m("bathroom/washer"), + client = mqtt_client, + threshold = 1, + done_callback = function() + ntfy:send_notification({ + title = "Laundy is done", + message = "Don't forget to hang it!", + tags = { "womans_clothes" }, + priority = "high", + }) + end, + })) + + devs:add(devices.OutletOnOff.new({ + name = "Charger", + room = "Workbench", + topic = mqtt_z2m("workbench/charger"), + client = mqtt_client, + callback = off_timeout(debug and 5 or 20 * 3600), + })) + + local workbench_outlet = devices.OutletOnOff.new({ + name = "Outlet", + room = "Workbench", + topic = mqtt_z2m("workbench/outlet"), + client = mqtt_client, + }) + turn_off_when_away(workbench_outlet) + devs:add(workbench_outlet) + + local workbench_light = devices.LightColorTemperature.new({ + name = "Light", + room = "Workbench", + topic = mqtt_z2m("workbench/light"), + client = mqtt_client, + }) + turn_off_when_away(workbench_light) + devs:add(workbench_light) + + local delay_color_temp = utils.Timeout.new() + devs:add(devices.IkeaRemote.new({ + name = "Remote", + room = "Workbench", + client = mqtt_client, + topic = mqtt_z2m("workbench/remote"), + callback = function(_, on) + delay_color_temp:cancel() + if on then + workbench_light:set_brightness(82) + -- NOTE: This light does NOT support changing both the brightness and color + -- temperature at the same time, so we first change the brightness and once + -- that is complete we change the color temperature, as that is less likely + -- to have to actually change. + delay_color_temp:start(0.5, function() + workbench_light:set_color_temperature(3333) + end) + else + workbench_light:set_on(false) + end + end, + battery_callback = check_battery, + })) + + devs:add(devices.HueSwitch.new({ + name = "SwitchBottom", + room = "Hallway", + client = mqtt_client, + topic = mqtt_z2m("hallway/switchbottom"), + left_callback = function() + hallway_top_light:set_on(not hallway_top_light:on()) + end, + battery_callback = check_battery, + })) + devs:add(devices.HueSwitch.new({ + name = "SwitchTop", + room = "Hallway", + client = mqtt_client, + topic = mqtt_z2m("hallway/switchtop"), + left_callback = function() + hallway_top_light:set_on(not hallway_top_light:on()) + end, + battery_callback = check_battery, + })) + + local hallway_storage = devices.LightBrightness.new({ + name = "Storage", + room = "Hallway", + topic = mqtt_z2m("hallway/storage"), + client = mqtt_client, + callback = hallway_light_automation:light_callback(), + }) + turn_off_when_away(hallway_storage) + devs:add(hallway_storage) + + -- TODO: Rework + hallway_light_automation.group = { + set_on = function(on) + if on then + hallway_storage:set_brightness(80) + else + hallway_storage:set_on(false) + end + hallway_bottom_lights:set_on(on) + end, + } + + devs:add(devices.IkeaRemote.new({ + name = "Remote", + room = "Hallway", + client = mqtt_client, + topic = mqtt_z2m("hallway/remote"), + callback = hallway_light_automation:switch_callback(), + battery_callback = check_battery, + })) + + ---@param duration number + ---@return fun(_, open: boolean) + local function presence(duration) + local timeout = utils.Timeout.new() + + return function(_, open) + if open then + timeout:cancel() + + if not presence_system:overall_presence() then + mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), { + state = true, + updated = utils.get_epoch(), + }) + end + else + timeout:start(duration, function() + mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), nil) + end) + end end - end, - battery_callback = check_battery, -})) + end + local hallway_frontdoor = devices.ContactSensor.new({ + name = "Frontdoor", + room = "Hallway", + sensor_type = "Door", + topic = mqtt_z2m("hallway/frontdoor"), + client = mqtt_client, + callback = { + presence(debug and 10 or 15 * 60), + hallway_light_automation:door_callback(), + }, + battery_callback = check_battery, + }) + devs:add(hallway_frontdoor) + window_sensors:add(hallway_frontdoor) + hallway_light_automation.door = hallway_frontdoor --- HACK: If the devices config contains a function it will call it so we have to remove it -devs.add = nil + local hallway_trash = devices.ContactSensor.new({ + name = "Trash", + room = "Hallway", + sensor_type = "Drawer", + topic = mqtt_z2m("hallway/trash"), + client = mqtt_client, + callback = hallway_light_automation:trash_callback(), + battery_callback = check_battery, + }) + devs:add(hallway_trash) + hallway_light_automation.trash = hallway_trash + + local guest_light = devices.LightOnOff.new({ + name = "Light", + room = "Guest Room", + topic = mqtt_z2m("guest/light"), + client = mqtt_client, + }) + turn_off_when_away(guest_light) + devs:add(guest_light) + + devs:add(devices.HueSwitch.new({ + name = "Switch", + room = "Bedroom", + client = mqtt_client, + topic = mqtt_z2m("bedroom/switch"), + left_callback = function() + bedroom_lights:set_on(not bedroom_lights:on()) + end, + left_hold_callback = function() + bedroom_lights_relax:set_on(true) + end, + battery_callback = check_battery, + })) + + local balcony = devices.ContactSensor.new({ + name = "Balcony", + room = "Living Room", + sensor_type = "Door", + topic = mqtt_z2m("living/balcony"), + client = mqtt_client, + battery_callback = check_battery, + }) + devs:add(balcony) + window_sensors:add(balcony) + local living_window = devices.ContactSensor.new({ + name = "Window", + room = "Living Room", + topic = mqtt_z2m("living/window"), + client = mqtt_client, + battery_callback = check_battery, + }) + devs:add(living_window) + window_sensors:add(living_window) + local bedroom_window = devices.ContactSensor.new({ + name = "Window", + room = "Bedroom", + topic = mqtt_z2m("bedroom/window"), + client = mqtt_client, + battery_callback = check_battery, + }) + devs:add(bedroom_window) + window_sensors:add(bedroom_window) + local guest_window = devices.ContactSensor.new({ + name = "Window", + room = "Guest Room", + topic = mqtt_z2m("guest/window"), + client = mqtt_client, + battery_callback = check_battery, + }) + devs:add(guest_window) + window_sensors:add(guest_window) + + local storage_light = devices.LightBrightness.new({ + name = "Light", + room = "Storage", + topic = mqtt_z2m("storage/light"), + client = mqtt_client, + }) + turn_off_when_away(storage_light) + devs:add(storage_light) + + devs:add(devices.ContactSensor.new({ + name = "Door", + room = "Storage", + sensor_type = "Door", + topic = mqtt_z2m("storage/door"), + client = mqtt_client, + callback = function(_, open) + if open then + storage_light:set_brightness(100) + else + storage_light:set_on(false) + end + end, + battery_callback = check_battery, + })) + + devs.add = nil + + return devs +end + +-- TODO: Pass the mqtt config to the output config, instead of constructing the client here +local mqtt_client = require("automation:mqtt").new(device_manager, { + host = ((host == "zeus" or host == "hephaestus") and "olympus.lan.huizinga.dev") or "mosquitto", + port = 8883, + client_name = "automation-" .. host, + username = "mqtt", + password = secrets.mqtt_password, + tls = host == "zeus" or host == "hephaestus", +}) ---@type Config return { @@ -751,7 +750,19 @@ return { openid_url = "https://login.huizinga.dev/api/oidc", }, mqtt = mqtt_client, - devices = devs, + devices = { + create_devs, + ntfy, + hue_bridge, + kitchen_lights, + living_lights, + living_lights_relax, + hallway_top_light, + hallway_bottom_lights, + bedroom_lights, + bedroom_lights_relax, + bedroom_air_filter, + }, schedule = { ["0 0 19 * * *"] = function() bedroom_air_filter:set_on(true)