Skip to content

Instantly share code, notes, and snippets.

@lud
Last active November 16, 2022 10:21
Show Gist options
  • Save lud/6be0dfe6b52932e486d1aca0986dcb02 to your computer and use it in GitHub Desktop.
Save lud/6be0dfe6b52932e486d1aca0986dcb02 to your computer and use it in GitHub Desktop.
Heat the computer
#!/usr/bin/env elixir
Mix.install([:jason])
defmodule Heater do
import Kernel, except: [spawn_link: 1]
def spawn_link() do
Kernel.spawn_link(fn -> init() end)
end
defp init() do
send(self(), :heat)
loop_idle()
end
defp loop_heating(tasks) do
receive do
:heat -> loop_heating(tasks)
:stop -> end_tasks(tasks)
end
end
defp loop_idle() do
receive do
:heat -> loop_heating(start_tasks())
:stop -> :ok
end
end
defp start_tasks() do
_tasks = for _ <- 1..System.schedulers_online(), do: Task.async(&infinite_task/0)
end
defp end_tasks(tasks) do
for t <- tasks, do: Task.shutdown(t)
end
defp infinite_task() do
infinite_task([0])
end
defp infinite_task(hash) do
hash |> :erlang.md5() |> infinite_task()
end
end
defmodule Control do
def run(max) do
{pid, ref} = spawn_monitor(fn -> init(max) end)
receive do
{:DOWN, ^ref, :process, ^pid, 0} ->
_exit_code = 0
{:DOWN, ^ref, :process, ^pid, 1} ->
IO.puts([
"error when executing powermetrics. ",
IO.ANSI.bright(),
"did you run with sudo?",
IO.ANSI.reset()
])
_exit_code = 1
{:DOWN, ^ref, :process, ^pid, exit_code} when is_integer(exit_code) ->
exit_code
{:DOWN, ^ref, :process, ^pid, :normal} ->
_exit_code = 0
{:DOWN, ^ref, :process, ^pid, reason} ->
IO.puts([
"error when executing control process: ",
inspect(reason)
])
_exit_code = 1
end
end
defp init(max) do
heater = Heater.spawn_link()
state = %{min_temp: nil, max_temp: max}
loop(state, heater)
end
defp loop(%{max_temp: max} = state, heater) do
temp = get_temperature()
state = maybe_put_min(state, temp)
display_temp(state, temp)
if temp >= max do
IO.puts("\nReached desired temperature")
terminate(heater)
else
send(heater, :heat)
# sleeping to not spam sensors
Process.sleep(1000)
loop(state, heater)
end
end
defp maybe_put_min(%{min_temp: nil} = state, temp) do
%{state | min_temp: temp}
end
defp maybe_put_min(%{min_temp: min} = state, temp) when temp < min do
%{state | min_temp: temp}
end
defp maybe_put_min(state, _) do
state
end
defp display_temp(%{min_temp: min, max_temp: max}, cur) do
domain =
case max - min do
0 -> 1
d -> d
end
ratio = (cur - min) / domain
ratio = max(ratio, 0)
range = max(io_columns(), 20)
filled_size = trunc(range * ratio)
right_text = "#{trunc(ratio * 100)}% "
right_len = String.length(right_text)
left_text = "Current temperature: #{cur}°C" |> String.slice(0, range - right_len)
left_len = String.length(left_text)
spacing = range - right_len - left_len
message =
[left_text, List.duplicate(" ", spacing), right_text]
|> :erlang.iolist_to_binary()
{filled_text, unfilled_text} = String.split_at(message, filled_size)
IO.write([
"\r",
IO.ANSI.inverse(),
filled_text,
IO.ANSI.reset(),
unfilled_text
])
end
defp io_columns() do
case :io.columns() do
{:ok, v} -> v
_ -> 80
end
end
defp terminate(heater) do
ref = Process.monitor(heater)
IO.puts("Unlinking heater")
Process.unlink(heater)
send(heater, :stop)
receive do
{:DOWN, ^ref, :process, ^heater, :normal} -> exit(0)
{:DOWN, ^ref, :process, ^heater, reason} -> exit(reason)
end
end
defp get_temperature() do
case System.shell("sensors -j 2>/dev/null") do
{json, 0} -> json |> Jason.decode!() |> read_metrics()
{_, n} -> exit(n)
end
end
defp read_metrics(%{"acpitz-acpi-0" => temps}) do
temps =
for {"temp" <> n, vals} <- temps do
_temp = Map.fetch!(vals, "temp#{n}_input")
end
sum = Enum.reduce(temps, &Kernel.+/2)
avg = sum / length(temps)
avg
end
end
degrees =
case System.argv() do
[] ->
50
[arg | _] ->
case Integer.parse(arg) do
{deg, ""} ->
deg
_ ->
IO.puts("Could not parse '#{arg}'")
System.stop()
end
end
IO.puts("Target temperature: #{degrees}°C")
Control.run(degrees)
|> System.stop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment