refactor: move temperature backfill to a release-safe module

Mix tasks aren't usable from the prod release image — `/app/bin/towerops`
ships without `mix`, and `Mix.shell()` raises at runtime. Move the work
to `Towerops.Maintenance.FixMikrotikTemperatureScaling` (plain module
with `run/0`, IO.puts + Logger.info) and keep the Mix.Task as a thin
dev-only delegator.

Run in prod via:

    /app/bin/towerops eval 'Towerops.Maintenance.FixMikrotikTemperatureScaling.run()'
This commit is contained in:
Graham McIntire 2026-05-10 12:50:35 -05:00
parent d051968ae6
commit f8c3b00ba0
2 changed files with 61 additions and 42 deletions

View file

@ -2,57 +2,25 @@ defmodule Mix.Tasks.Towerops.FixMikrotikTemperatureScaling do
@shortdoc "Renormalise stored temperature sensor readings using SensorScale"
@moduledoc """
One-shot backfill that re-runs `Towerops.Snmp.SensorScale.normalize/2`
over every stored temperature reading. Catches historical mis-scaled
rows from MikroTik `mtxrGaugeValue` (some models report deci-degrees)
and any other sensor whose YAML profile didn't include the right
divisor at discovery time.
Run from inside a release pod:
/app/bin/towerops eval 'Mix.Tasks.Towerops.FixMikrotikTemperatureScaling.run([])'
Or via mix during dev:
Dev-only convenience wrapper. Delegates to
`Towerops.Maintenance.FixMikrotikTemperatureScaling.run/0` so you can
run the backfill from a local checkout via:
mix towerops.fix_mikrotik_temperature_scaling
In prod the release has no `mix` binary invoke the plain module
directly:
/app/bin/towerops eval 'Towerops.Maintenance.FixMikrotikTemperatureScaling.run()'
"""
use Mix.Task
import Ecto.Query
alias Towerops.Repo
alias Towerops.Snmp.Sensor
alias Towerops.Snmp.SensorScale
alias Towerops.Maintenance.FixMikrotikTemperatureScaling
@impl Mix.Task
def run(_argv) do
{:ok, _} = Application.ensure_all_started(:towerops)
sensors =
Repo.all(
from(s in Sensor, where: s.sensor_type == "temperature" and not is_nil(s.last_value) and s.last_value > 150.0)
)
{fixed, skipped} =
Enum.reduce(sensors, {0, 0}, fn sensor, {fixed, skipped} ->
normalized = SensorScale.normalize(sensor.sensor_type, sensor.last_value)
if normalized == sensor.last_value do
{fixed, skipped + 1}
else
{:ok, _} =
sensor
|> Sensor.changeset(%{last_value: Float.round(normalized / 1.0, 2)})
|> Repo.update()
{fixed + 1, skipped}
end
end)
Mix.shell().info(
"Temperature backfill complete: rescaled #{fixed} rows, " <>
"left #{skipped} unchanged."
)
FixMikrotikTemperatureScaling.run()
end
end

View file

@ -0,0 +1,51 @@
defmodule Towerops.Maintenance.FixMikrotikTemperatureScaling do
@moduledoc """
One-shot backfill that re-runs `Towerops.Snmp.SensorScale.normalize/2`
over every stored temperature reading. Catches historical mis-scaled
rows from MikroTik `mtxrGaugeValue` (some models report deci-degrees)
and any other sensor whose YAML profile didn't include the right
divisor at discovery time.
Plain module rather than a `Mix.Task` so it can run from the prod
Elixir release (where `mix` itself isn't installed):
/app/bin/towerops eval 'Towerops.Maintenance.FixMikrotikTemperatureScaling.run()'
"""
import Ecto.Query
alias Towerops.Repo
alias Towerops.Snmp.Sensor
alias Towerops.Snmp.SensorScale
require Logger
@spec run() :: :ok
def run do
sensors =
Repo.all(
from(s in Sensor, where: s.sensor_type == "temperature" and not is_nil(s.last_value) and s.last_value > 150.0)
)
{fixed, skipped} =
Enum.reduce(sensors, {0, 0}, fn sensor, {fixed, skipped} ->
normalized = SensorScale.normalize(sensor.sensor_type, sensor.last_value)
if normalized == sensor.last_value do
{fixed, skipped + 1}
else
{:ok, _} =
sensor
|> Sensor.changeset(%{last_value: Float.round(normalized / 1.0, 2)})
|> Repo.update()
{fixed + 1, skipped}
end
end)
msg = "Temperature backfill complete: rescaled #{fixed} rows, left #{skipped} unchanged."
IO.puts(msg)
Logger.info(msg)
:ok
end
end