Author Avatar Image
Alexander Reelsen

Backend developer, productivity fan, likes the JVM, full text search, distributed databases & systems

Using Hammerspoon to enable lighting for meetings
Feb 1, 2023
4 minutes read

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.


Back to posts