TLDR; I recently got my hands on some Elgato gear (holy cow, are those expensive at recommended retail prices!). Time to use Hammerspoon to automatically enable them, once a camera turns on. The code applies also to other lights, if you have a way to switch those lights with a command line tool.
Just to quickly repeat: Hammerspoon is an awesome automation framework for OSX. It’s one of the first things I install when setting up a new MacBook. You can read an older post about my setup from 2016 or a little bit newer post about properly supporting DnD mode
- you may want to check with your setup if that is still needed nowadays.
So, with those two lights on my desktop, it’s time to automate away enabled/disabling those lights. Two steps are required:
- Figure out a way to get notified about cameras turning on
- Find a tool to enable/disable the Elgato lights.
Let’s start with the second part by installing keylightctl
go install github.com/endocrimes/keylightctl@latest
Now you can run
~/go/bin/keylightctl --describe all
Now one can use the keylightctl
to switch the lights on or off or change
the brightness. For now, all I need is switching the lights on and off.
Let’s bind a key to toggle the lights:
function toggleLights()
hs.execute("~/go/bin/keylightctl switch -timeout 1s -all toggle")
end
hs.hotkey.bind({}, 'F15', toggleLights)
My external keyboard has an F15
key so filling that with some live sounded
good. Won’t work without external keyboard, but it is likely that I will not
have my lights on either in that case.
Ok, so toggling works, time to find some code, that checks for all cameras, and if any camera is in use, then we can turn on the light:
function checkLights()
local anyCameraInUse = false
for k, camera in pairs(hs.camera.allCameras()) do
print(string.format('Checking camera %s, is in use: %s', camera:name(), camera:isInUse()))
if camera:isInUse() then
anyCameraInUse = true
end
end
print(string.format('Any camera in use: %s', anyCameraInUse))
if (anyCameraInUse) then
-- lights on
hs.execute("~/go/bin/keylightctl switch -timeout 2s -all on")
else
-- lights off
hs.execute("~/go/bin/keylightctl switch -timeout 2s -all off")
end
end
Adding this function to your Hammerspoon configuration allows you to run
checkLights()
in the console and see if this works. I usually test by
opening zoom and its video preferences, which enables a camera.
Ok, so this is the code to turn on or off the lights. I picked a timeout
of two seconds, to make sure that both lights trigger, but it seems to work
with one second as well.
Lastly, when should be check for the lights?
- When Hammerspoon is loaded or reloaded
- When a new camera is added or removed
- When a camera is activated or deactivated
The hs.camera module helps for all the cases.
This adds a property watcher to all currently plugged in cameras, that trigger when a camera becomes active.
for k, camera in pairs(hs.camera.allCameras()) do
-- stop old watcher
if camera:isPropertyWatcherRunning() then
camera:stopPropertyWatcher()
end
camera:setPropertyWatcherCallback(function(camera, property, scope, element)
print("camera watcher call back triggered for " .. camera:name())
checkLights()
end)
camera:startPropertyWatcher()
print("started camera watcher for " .. camera:name())
end
The following code covers the case of camera state changes like plugging in or removing.
hs.camera.setWatcherCallback(function(camera, state)
print('Camera change callback triggered ' .. state)
checkLights()
end)
hs.camera.startWatcher()
And lastly this needs to be triggered once Hammerspoon starts.
-- check lights on startup to make sure they are set properly
checkLights()
Now this looks good and covers every case? Or doesn’t it? Let’s see, the first snippet only executes on Hammerspoon startup - so it does not register a property watcher, if a camera is plugged in after that. That needs to be done as well, when a new camera is added. Having a dedicated function to start a property watcher for a camera on startup as well as when plugging it in sounds like a good idea. Let’s go with the full snippet here
function stopConfigureAndStartPropertyWatcher(camera)
if camera:isPropertyWatcherRunning() then
camera:stopPropertyWatcher()
end
camera:setPropertyWatcherCallback(function(camera, property, scope, element)
checkLights()
end)
camera:startPropertyWatcher()
end
function checkLights()
local anyCameraInUse = false
for k, camera in pairs(hs.camera.allCameras()) do
if camera:isInUse() then
anyCameraInUse = true
end
end
if (anyCameraInUse) then
hs.execute("~/go/bin/keylightctl switch -timeout 2s -all on")
else
hs.execute("~/go/bin/keylightctl switch -timeout 2s -all off")
end
end
for k, camera in pairs(hs.camera.allCameras()) do
stopConfigureAndStartPropertyWatcher(camera)
end
hs.camera.setWatcherCallback(function(camera, state)
if state == 'Added' then
stopConfigureAndStartPropertyWatcher(camera)
end
checkLights()
end)
hs.camera.startWatcher()
-- check lights on startup to make sure they are on
checkLights()
Now stopConfigureAndStartPropertyWatcher()
is run on startup and whenever
a new camera is added. You can take the above snippet and put it in your
Hammerspoon configuration.
That’s it for today. Still happy I found Hammerspoon a couple of years back. The API keeps evolving and new features are being still added.
Finally, time for some professional animated image, showing how lights flip on, when enabled the camera in Zoom settings.
One last note: The Elgato light seems not to have any security features built-in - there is no authentication, so everyone on the network can fiddle with them. Apparently this is not built for corporate networks, but just for streamers in their own LAN it seems.