farmbot_os/farmbot_core/lib/regimen/manager.ex

199 lines
5.5 KiB
Elixir

defmodule Farmbot.Regimen.Manager do
@moduledoc "Manages a Regimen"
require Farmbot.Logger
use GenServer
alias Farmbot.Core.CeleryScript
alias Farmbot.Asset
alias Asset.Regimen
import Farmbot.Regimen.NameProvider
import Farmbot.Config,
only: [
get_config_value: 3
]
defmodule Error do
@moduledoc false
defexception [:epoch, :regimen, :message]
end
defmodule Item do
@moduledoc false
@type t :: %__MODULE__{
time_offset: integer,
sequence_id: integer,
ref: reference
}
defstruct [:time_offset, :sequence, :sequence_id, :name, :ref]
def parse(%{time_offset: offset, sequence_id: sequence_id}) do
%Item{
time_offset: offset,
sequence_id: sequence_id,
ref: make_ref()
}
end
end
def filter_items(regimen) do
regimen.regimen_items
|> Enum.map(&Item.parse(&1))
|> Enum.sort(&(&1.time_offset <= &2.time_offset))
end
@doc false
def start_link(regimen, time) do
regimen.farm_event_id || raise "Starting a regimen requires a farm_event id"
GenServer.start_link(__MODULE__, [regimen, time], name: via(regimen))
end
def init([regimen, time]) do
# parse and sort the regimen items
items = filter_items(regimen)
first_item = List.first(items)
regimen = %{regimen | regimen_items: items}
epoch =
Farmbot.TimeUtils.build_epoch(time)
initial_state = %{
next_execution: nil,
regimen: regimen,
epoch: epoch,
timer: nil
}
if first_item do
state = build_next_state(regimen, first_item, self(), initial_state)
{:ok, state}
else
Farmbot.Logger.warn(2, "[#{regimen.name} #{regimen.farm_event_id}] has no items on regimen.")
{:ok, initial_state}
end
end
def handle_call({:reindex, regimen, time}, _from, state) do
Farmbot.Logger.debug(3, "Reindexing regimen by id: #{regimen.id}")
regimen.farm_event_id || raise "Can't reindex without farm_event_id"
# parse and sort the regimen items
items = filter_items(regimen)
first_item = List.first(items)
regimen = %{regimen | regimen_items: items}
epoch = if time, do: Farmbot.TimeUtils.build_epoch(time), else: state.epoch
initial_state = %{
regimen: regimen,
epoch: epoch,
# Leave these so they get cleaned up
next_execution: state.next_execution,
timer: state.timer
}
if first_item do
state = build_next_state(regimen, first_item, self(), initial_state)
{:reply, :ok, state}
else
Farmbot.Logger.warn(2, "[#{regimen.name} #{regimen.farm_event_id}] has no items on regimen.")
{:reply, :ok, initial_state}
end
end
def handle_info(:execute, state) do
{item, regimen} = pop_item(state.regimen)
if item do
do_item(item, regimen, state)
else
complete(regimen, state)
end
end
def handle_info(:skip, state) do
{item, regimen} = pop_item(state.regimen)
if item do
do_item(nil, regimen, state)
else
complete(regimen, state)
end
end
defp complete(regimen, state) do
Farmbot.Logger.success(
2,
"[#{regimen.name} #{regimen.farm_event_id}] has executed all current items!"
)
items = filter_items(state.regimen)
regimen = %{state.regimen | regimen_items: items}
{:noreply, %{state | regimen: regimen}}
end
defp do_item(item, regimen, state) do
if item do
sequence = Farmbot.Asset.get_sequence_by_id!(item.sequence_id)
CeleryScript.sequence(sequence, fn(results) ->
case results do
:ok ->
Farmbot.Logger.success(1, "[#{sequence.name}] executed by [#{regimen.name}] complete.")
{:error, _} ->
Farmbot.Logger.error(1, "[#{sequence.name}] executed by [#{regimen.name}] failed.")
end
end)
end
next_item = List.first(regimen.regimen_items)
if next_item do
new_state = build_next_state(regimen, next_item, self(), state)
{:noreply, new_state}
else
complete(regimen, state)
end
end
def build_next_state(%Regimen{} = regimen, %Item{} = nx_itm, pid, state) do
if state.timer do
Process.cancel_timer(state.timer)
end
next_dt = Timex.shift(state.epoch, milliseconds: nx_itm.time_offset)
timezone = get_config_value(:string, "settings", "timezone")
now = Timex.now(timezone)
offset_from_now = Timex.diff(next_dt, now, :milliseconds)
timer =
if offset_from_now < 0 and offset_from_now < -60_000 do
Process.send_after(pid, :skip, 1)
else
{msg, real_offset} = ensure_not_negative(offset_from_now)
Process.send_after(pid, msg, real_offset)
end
if offset_from_now > 0 do
timestr = Farmbot.TimeUtils.format_time(next_dt)
from_now = Timex.from_now(next_dt, Farmbot.Asset.device().timezone)
msg =
"[#{regimen.name}] scheduled by FarmEvent (#{regimen.farm_event_id}) " <>
"will execute next item #{from_now} (#{timestr})"
Farmbot.Logger.info(3, msg)
end
%{state | timer: timer, regimen: regimen, next_execution: next_dt}
end
defp ensure_not_negative(offset) when offset < -60_000, do: {:skip, 1}
defp ensure_not_negative(offset) when offset < 0, do: {:execute, 1000}
defp ensure_not_negative(offset), do: {:execute, offset}
@spec pop_item(Regimen.t()) :: {Item.t() | nil, Regimen.t()}
# when there is more than one item pop the top one
defp pop_item(%Regimen{regimen_items: [do_this_one | items]} = r) do
{do_this_one, %Regimen{r | regimen_items: items}}
end
end