Skip to content

Instantly share code, notes, and snippets.

@cr0t
Created February 2, 2026 12:24
Show Gist options
  • Select an option

  • Save cr0t/c8c2b7d027291d590b457beaf9e114f1 to your computer and use it in GitHub Desktop.

Select an option

Save cr0t/c8c2b7d027291d590b457beaf9e114f1 to your computer and use it in GitHub Desktop.
Automatic Elgato lights controller over HomeAssistant
#!/usr/bin/env -S ERL_FLAGS=+B elixir
Mix.install([{:httpoison, "~> 2.3"}])
defmodule Lights do
import Config
require Logger
@selfname Path.basename(__ENV__.file)
@config Path.expand("~/.local/lights.conf")
def main(_argv) do
configure_logger()
load_settings()
Logger.info("#{@selfname} starts watching...")
Lights.Application.start(nil, [])
end
defp load_settings() do
settings = Config.Reader.read!(@config)
for {app, app_config} <- settings, {key, value} <- app_config do
Application.put_env(app, key, value)
end
end
defp configure_logger() do
formatter = Logger.default_formatter(format: "$date $time [$level] $message\n")
:logger.update_handler_config(:default, :formatter, formatter)
end
end
defmodule Lights.Application do
use Application
def start(_type, _args) do
children = [{Lights.Watchdog, []}]
Supervisor.start_link(children, strategy: :one_for_one, name: Lights.Supervisor)
end
end
defmodule Lights.Watchdog do
use GenServer
@scruffy Path.expand("~/.dotfiles/littles/scruffy.sh")
@log_cmd ~s[#{@scruffy} sudo log stream --color none --style compact --predicate 'subsystem == "com.apple.UVCExtension" AND composedMessage CONTAINS "Post PowerLog"']
def start_link(_opts),
do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def init(nil) do
Process.flag(:trap_exit, true)
port = Port.open({:spawn, @log_cmd}, [:binary, :exit_status])
{:ok, port}
end
def handle_info({_port, {:data, data}}, port) do
cond do
String.contains?(data, "\"VDCAssistant_Power_State\" = On") ->
Lights.Controller.turn_all(:on)
String.contains?(data, "\"VDCAssistant_Power_State\" = Off") ->
Lights.Controller.turn_all(:off)
true ->
nil
end
{:noreply, port}
end
end
defmodule Lights.Controller do
require Logger
def turn_all(action) when action in [:on, :off] do
list_lamps()
|> Task.async_stream(fn %{"entity_id" => lamp_id} -> turn_lamp(lamp_id, action) end)
|> Stream.run()
end
defp list_lamps() do
url = "#{base_url()}/api/states"
headers = auth_headers()
case HTTPoison.get(url, headers) do
{:ok, %{status_code: 200, body: body}} ->
:json.decode(body) |> Enum.filter(&is_elgato_lamp?/1)
_ ->
[]
end
end
defp base_url(), do: Application.get_env(:lights, :ha_base)
defp auth_headers(),
do: [{"Authorization", "Bearer #{Application.get_env(:lights, :ha_token)}"}]
defp is_elgato_lamp?(%{"entity_id" => entity_id}),
do: String.starts_with?(entity_id, "light.elgato_")
defp turn_lamp(lamp_id, action) when action in [:on, :off] do
url = "#{base_url()}/api/services/light/turn_#{action}"
body = :json.encode(%{entity_id: lamp_id})
headers = auth_headers()
Logger.info("turning #{lamp_id} #{action}")
case HTTPoison.post(url, body, headers) do
{:ok, %{status_code: 200}} -> :ok
_ -> :error
end
end
end
Lights.main(System.argv())
Process.sleep(:infinity)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>lights</string>
<key>ProgramArguments</key>
<array>
<string>/Users/sergey.kuznetsov/.local/bin/lights.exs</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/Users/sergey.kuznetsov/.asdf/shims:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/sergey.kuznetsov/</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/sergey.kuznetsov/Library/Logs/lights.log</string>
<key>StandardErrorPath</key>
<string>/Users/sergey.kuznetsov/Library/Logs/lights_error.log</string>
</dict>
</plist>
#!/usr/bin/env bash
# Scruffy Scruffington: "Scruffy's gonna clean up them processes. Yup."
#
# This script shall be used as a wrapper to execute long-running processes via Port to avoid
# leaving orphaned zombie processes in case of BEAM abnormal termination. Read more:
#
# https://hexdocs.pm/elixir/Port.html#module-zombie-operating-system-processes
# Start the program in the background
exec "$@" &
pid1=$!
# Silence warnings from here on
exec >/dev/null 2>&1
# Read from stdin in the background and
# kill running program when stdin closes
exec 0<&0 $(
while read; do :; done
kill -KILL $pid1
) &
pid2=$!
# Clean up
wait $pid1
ret=$?
kill -KILL $pid2
exit $ret
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment