From b0658efbc25b8db99816ab099ca6882b5bba7349 Mon Sep 17 00:00:00 2001 From: connor rigby Date: Fri, 10 Nov 2017 12:10:02 -0800 Subject: [PATCH] bring back farmevents and regimens --- config/host/dev.exs | 1 + lib/farmbot/bootstrap/supervisor.ex | 14 +- .../bot_state/transport/gen_mqtt/client.ex | 20 +- lib/farmbot/farm_event/execution/execution.ex | 17 ++ lib/farmbot/farm_event/execution/regimen.ex | 10 + lib/farmbot/farm_event/execution/sequence.ex | 15 ++ lib/farmbot/farm_event/manager.ex | 223 ++++++++++++++++++ lib/farmbot/farm_event/supervisor.ex | 16 ++ lib/farmbot/firmware/firmware.ex | 2 +- .../firmware/uart_handler/uart_handler.ex | 2 +- lib/farmbot/regimen/manager.ex | 164 +++++++++++++ lib/farmbot/regimen/supervisor.ex | 25 ++ lib/farmbot/repo/device.ex | 1 + lib/farmbot/repo/farm_event.ex | 8 +- lib/farmbot/repo/regimen.ex | 5 +- lib/farmbot/repo/repo.ex | 17 +- .../20170919150432_add_farm_events_table.exs | 5 +- .../20170920182805_add_regimens_table.exs | 1 + .../20171025231225_add_devices_table.exs | 1 + 19 files changed, 522 insertions(+), 25 deletions(-) create mode 100644 lib/farmbot/farm_event/execution/execution.ex create mode 100644 lib/farmbot/farm_event/execution/regimen.ex create mode 100644 lib/farmbot/farm_event/execution/sequence.ex create mode 100644 lib/farmbot/farm_event/manager.ex create mode 100644 lib/farmbot/farm_event/supervisor.ex create mode 100644 lib/farmbot/regimen/manager.ex create mode 100644 lib/farmbot/regimen/supervisor.ex diff --git a/config/host/dev.exs b/config/host/dev.exs index 3fd36379..cc570d64 100644 --- a/config/host/dev.exs +++ b/config/host/dev.exs @@ -30,6 +30,7 @@ config :farmbot, ecto_repos: repos for repo <- [Farmbot.Repo.A, Farmbot.Repo.B] do config :farmbot, repo, adapter: Sqlite.Ecto2, + loggers: [], database: "tmp/#{repo}_dev.sqlite3" end diff --git a/lib/farmbot/bootstrap/supervisor.ex b/lib/farmbot/bootstrap/supervisor.ex index b78dbd11..27c4382e 100644 --- a/lib/farmbot/bootstrap/supervisor.ex +++ b/lib/farmbot/bootstrap/supervisor.ex @@ -121,13 +121,15 @@ defmodule Farmbot.Bootstrap.Supervisor do ConfigStorage.update_config_value(:string, "authorization", "last_shutdown_reason", nil) children = [ - worker(Farmbot.Bootstrap.AuthTask, []), - supervisor(Farmbot.Firmware.Supervisor, []), - supervisor(Farmbot.BotState.Supervisor, []), + worker(Farmbot.Bootstrap.AuthTask, []), + supervisor(Farmbot.Firmware.Supervisor, []), + supervisor(Farmbot.BotState.Supervisor, []), supervisor(Farmbot.BotState.Transport.Supervisor, []), - supervisor(Farmbot.HTTP.Supervisor, []), - supervisor(Farmbot.Repo.Supervisor, []), - supervisor(Farmbot.Farmware.Supervisor, []) + supervisor(Farmbot.HTTP.Supervisor, []), + supervisor(Farmbot.Repo.Supervisor, []), + supervisor(Farmbot.Farmware.Supervisor, []), + supervisor(Farmbot.Regimen.Supervisor, []), + supervisor(Farmbot.FarmEvent.Supervisor, []) ] opts = [strategy: :one_for_one] diff --git a/lib/farmbot/bot_state/transport/gen_mqtt/client.ex b/lib/farmbot/bot_state/transport/gen_mqtt/client.ex index 936689df..a3663cfa 100644 --- a/lib/farmbot/bot_state/transport/gen_mqtt/client.ex +++ b/lib/farmbot/bot_state/transport/gen_mqtt/client.ex @@ -88,17 +88,19 @@ defmodule Farmbot.BotState.Transport.GenMQTT.Client do end def on_publish(["bot", _, "sync", kind, id], msg, state) do - mod = Module.concat(["Farmbot", "Repo", kind]) - if Code.ensure_loaded?(mod) do - body = struct(mod) - sync_cmd = msg |> Poison.decode!(as: struct(Farmbot.Repo.SyncCmd, kind: mod, body: body, id: id)) - Farmbot.Repo.register_sync_cmd(sync_cmd) + spawn fn() -> + mod = Module.concat(["Farmbot", "Repo", kind]) + if Code.ensure_loaded?(mod) do + body = struct(mod) + sync_cmd = msg |> Poison.decode!(as: struct(Farmbot.Repo.SyncCmd, kind: mod, body: body, id: id)) + Farmbot.Repo.register_sync_cmd(sync_cmd) - if Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "auto_sync") do - Farmbot.Repo.flip() + if Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "auto_sync") do + Farmbot.Repo.flip() + end + else + Logger.warn 2, "Unknown syncable: #{mod}: #{inspect Poison.decode!(msg)}" end - else - Logger.warn 2, "Unknown syncable: #{mod}: #{inspect Poison.decode!(msg)}" end {:ok, state} end diff --git a/lib/farmbot/farm_event/execution/execution.ex b/lib/farmbot/farm_event/execution/execution.ex new file mode 100644 index 00000000..2de7aef4 --- /dev/null +++ b/lib/farmbot/farm_event/execution/execution.ex @@ -0,0 +1,17 @@ +defprotocol Farmbot.FarmEvent.Execution do + @moduledoc """ + Protocol to be implemented by any struct that can be executed by + Farmbot.FarmEvent.Manager. + """ + + @typedoc "Data to be executed." + @type data :: map + + @doc """ + Execute an item. + * `data` - A `Farmbot.Database.Syncable` struct that is implemented. + * `now` - `DateTime` of execution. + """ + @spec execute_event(data, DateTime.t) :: any + def execute_event(data, now) +end diff --git a/lib/farmbot/farm_event/execution/regimen.ex b/lib/farmbot/farm_event/execution/regimen.ex new file mode 100644 index 00000000..047fc9d6 --- /dev/null +++ b/lib/farmbot/farm_event/execution/regimen.ex @@ -0,0 +1,10 @@ +defimpl Farmbot.FarmEvent.Execution, for: Farmbot.Repo.Regimen do + + def execute_event(regimen, now) do + case Process.whereis(:"regimen-#{regimen.id}") do + nil -> {:ok, _pid} = Farmbot.Regimen.Supervisor.add_child(regimen, now) + pid -> {:ok, pid} + end + end + +end diff --git a/lib/farmbot/farm_event/execution/sequence.ex b/lib/farmbot/farm_event/execution/sequence.ex new file mode 100644 index 00000000..ccc71957 --- /dev/null +++ b/lib/farmbot/farm_event/execution/sequence.ex @@ -0,0 +1,15 @@ +defimpl Farmbot.FarmEvent.Execution, for: Farmbot.Repo.Sequence do + + def execute_event(sequence, _now) do + with {:ok, ast} <- Farmbot.CeleryScript.AST.decode(sequence) do + case Farmbot.CeleryScript.execute(ast) do + {:ok, _} -> :ok + {:error, reason, _} -> + {:error, reason} + end + else + {:error, reason} -> {:error, reason} + end + end + +end diff --git a/lib/farmbot/farm_event/manager.ex b/lib/farmbot/farm_event/manager.ex new file mode 100644 index 00000000..234a7dcd --- /dev/null +++ b/lib/farmbot/farm_event/manager.ex @@ -0,0 +1,223 @@ +defmodule Farmbot.FarmEvent.Manager do + @moduledoc """ + Manages execution of FarmEvents. + + ## Rules for FarmEvent execution. + * Regimen + * ignore `end_time`. + * ignore calendar. + * if start_time is more than 60 seconds passed due, assume it already started, and don't start it again. + * Sequence + * if `start_time` is late, check the calendar. + * for each item in the calendar, check if it's event is more than 60 seconds in the past. if not, execute it. + * if there is only one event in the calendar, ignore the `end_time` + """ + use GenServer + use Farmbot.Logger + alias Farmbot.FarmEvent.Execution + alias Farmbot.Repo.FarmEvent + + @checkup_time 20_000 + + ## GenServer + + defmodule State do + @moduledoc false + defstruct [timer: nil, last_time_index: %{}] + end + + @doc false + def start_link do + GenServer.start_link(__MODULE__, [], [name: __MODULE__]) + end + + def init([]) do + send self(), :checkup + {:ok, struct(State)} + end + + def handle_info(:checkup, state) do + now = get_now() + + all_events = Farmbot.Repo.current_repo().all(Farmbot.Repo.FarmEvent) + + # do checkup is the bulk of the work. + {late_events, new} = do_checkup(all_events, now, state) + + #TODO(Connor) Conditionally start events based on some state info. + unless Enum.empty?(late_events) do + Logger.info 3, "Time for event to run at: #{now.hour}:#{now.minute}" + start_events(late_events, now) + end + + # Start a new timer. + timer = Process.send_after self(), :checkup, @checkup_time + {:noreply, %{new | timer: timer}} + end + + defp do_checkup(list, time, late_events \\ [], state) + + defp do_checkup([], _now, late_events, state), do: {late_events, state} + + defp do_checkup([farm_event | rest], now, late_events, state) do + # new_late will be a executable event (Regimen or Sequence.) + {new_late_event, last_time} = check_event(farm_event, now, state.last_time_index[farm_event.id]) + + # update state. + new_state = %{state | last_time_index: Map.put(state.last_time_index, farm_event.id, last_time)} + case new_late_event do + # if `new_late_event` is nil, don't accumulate it. + nil -> do_checkup(rest, now, late_events, new_state) + # if there is a new event, accumulate it. + event -> do_checkup(rest, now, [event | late_events], new_state) + end + end + + defp check_event(%FarmEvent{} = f, now, last_time) do + # Get the executable out of the database this may fail. + # mod_list = ["Farmbot", "Repo", f.executable_type] + mod = Module.safe_concat([f.executable_type]) + + event = lookup(mod, f.executable_id) + + # build a local start time and end time + start_time = Timex.parse! f.start_time, "{ISO:Extended}" + end_time = Timex.parse! f.end_time, "{ISO:Extended}" + # start_time = f.start_time + # end_time = f.end_time + # get local bool of if the event is started and finished. + started? = Timex.after? now, start_time + finished? = Timex.after? now, end_time + + case f.executable_type do + "Elixir.Farmbot.Repo.Regimen" -> maybe_start_regimen(started?, start_time, last_time, event, now) + "Elixir.Farmbot.Repo.Sequence" -> maybe_start_sequence(started?, finished?, f, last_time, event, now) + end + end + + defp maybe_start_regimen(started?, start_time, last_time, event, now) + defp maybe_start_regimen(_started? = true, start_time, last_time, event, now) do + case is_too_old?(now, start_time) do + true -> + Logger.debug 3, "regimen #{event.name} (#{event.id}) is too old to start." + {nil, last_time} + false -> + Logger.debug 3, "regimen #{event.name} (#{event.id}) not to old; starting." + {event, now} + end + end + + defp maybe_start_regimen(_started? = false, start_time, last_time, event, _) do + Logger.debug 3, "regimen #{event.name} (#{event.id}) is not started yet. (#{inspect start_time}) (#{inspect Timex.now()})" + {nil, last_time} + end + + defp lookup(module, sr_id) do + case Farmbot.Repo.current_repo().get(module, sr_id) do + nil -> raise "Could not find #{module} by id: #{sr_id}" + item -> item + end + end + + # signals the start of a sequence based on the described logic. + defp maybe_start_sequence(started?, finished?, farm_event, last_time, event, now) + + # We only want to check if the sequence is started, and not finished. + defp maybe_start_sequence(_started? = true, _finished? = false, farm_event, last_time, event, now) do + {run?, next_time} = should_run_sequence?(farm_event.calendar, last_time, now) + case run? do + true -> {event, next_time} + false -> {nil, last_time} + end + end + + # if `farm_event.time_unit` is "never" we can't use the `end_time`. + # if we have no `last_time`, time to execute. + defp maybe_start_sequence(true, _, %{time_unit: "never"} = f, _last_time = nil, event, now) do + Logger.debug 3, "Ignoring end_time." + case should_run_sequence?(f.calendar, nil, now) do + {true, next} -> {event, next} + {false, _} -> {nil, nil } + end + end + + # if started is false, the event isn't ready to be executed. + defp maybe_start_sequence(_started? = false, _fin, _farm_event, last_time, event, _now) do + Logger.debug 3, "sequence #{event.name} (#{event.id}) is not started yet." + {nil, last_time} + end + + # if the event is finished (but not a "never" time_unit), we don't execute. + defp maybe_start_sequence(_started?, _finished? = true, _farm_event, last_time, event, _now) do + Logger.success 3, "sequence #{event.name} (#{event.id}) is finished." + {nil, last_time} + end + + # Checks if we shoudl run a sequence or not. returns {event | nil, time | nil} + defp should_run_sequence?(calendar, last_time, now) + + # if there is no last time, check if time is passed now within 60 seconds. + defp should_run_sequence?([first_time | _], nil, now) do; + + Logger.debug 3, "Checking sequence event that hasn't run before #{first_time}" + # convert the first_time to a DateTime + dt = Timex.parse! first_time, "{ISO:Extended}" + # if now is after the time, we are in fact late + if Timex.after?(now, dt) do + {true, now} + else + # make sure to return nil as the last time because it stil hasnt executed yet. + Logger.debug 3, "Sequence Event not ready yet." + {false, nil} + end + end + + defp should_run_sequence?(calendar, last_time, now) do + # get rid of all the items that happened before last_time + filtered_calendar = Enum.filter(calendar, fn(iso_time) -> + dt = Timex.parse! iso_time, "{ISO:Extended}" + # we only want this time if it happened after the last_time + Timex.after?(dt, last_time) + end) + + # if after filtering, there are events that need to be run + # check if they are older than a minute ago, + case filtered_calendar do + [iso_time | _] -> + dt = Timex.parse! iso_time, "{ISO:Extended}" + if Timex.after?(now, dt) do + {true, dt} + # too_old? = is_too_old?(now, dt) + # if too_old?, do: {false, last_time}, else: {true, dt} + else + Logger.debug 3, "Sequence Event not ready yet." + {false, dt} + end + [] -> + Logger.debug 3, "No items in calendar." + {false, last_time} + end + end + + # Enumeration is complete. + defp start_events([], _now), do: :ok + + # Enumerate the events to be started. + defp start_events([event | rest], now) do + # Spawn to be non blocking here. Maybe link to this process? + spawn fn() -> Execution.execute_event(event, now) end + # Continue enumeration. + start_events(rest, now) + end + + # is then more than 1 minute in the past? + defp is_too_old?(now, then) do + time_str_fun = fn(dt) -> "#{dt.hour}:#{dt.minute}:#{dt.second}" end + seconds = DateTime.to_unix(now, :second) - DateTime.to_unix(then, :second) + c = seconds > 60 # not in MS here + Logger.debug 3, "is checking #{time_str_fun.(now)} - #{time_str_fun.(then)} = #{seconds} seconds ago. is_too_old? => #{c}" + c + end + + defp get_now(), do: Timex.now() +end diff --git a/lib/farmbot/farm_event/supervisor.ex b/lib/farmbot/farm_event/supervisor.ex new file mode 100644 index 00000000..9f0218c4 --- /dev/null +++ b/lib/farmbot/farm_event/supervisor.ex @@ -0,0 +1,16 @@ +defmodule Farmbot.FarmEvent.Supervisor do + @moduledoc false + use Supervisor + + def start_link do + Supervisor.start_link(__MODULE__, [], [name: __MODULE__]) + end + + def init([]) do + children = [ + worker(Farmbot.FarmEvent.Manager, []) + ] + + supervise(children, strategy: :one_for_one) + end +end diff --git a/lib/farmbot/firmware/firmware.ex b/lib/farmbot/firmware/firmware.ex index e8419528..e456bbc1 100644 --- a/lib/farmbot/firmware/firmware.ex +++ b/lib/farmbot/firmware/firmware.ex @@ -140,7 +140,7 @@ defmodule Farmbot.Firmware do end defp do_begin_cmd(%Current{fun: fun, args: args, from: _from} = current, state, dispatch) do - Logger.debug 3, "Firmware command: #{fun}#{inspect(args)}" + # Logger.debug 3, "Firmware command: #{fun}#{inspect(args)}" if fun == :emergency_unlock, do: Farmbot.BotState.set_sync_status(:sync_now) case apply(state.handler_mod, fun, [state.handler | args]) do diff --git a/lib/farmbot/firmware/uart_handler/uart_handler.ex b/lib/farmbot/firmware/uart_handler/uart_handler.ex index 243ba3fa..8455e127 100644 --- a/lib/farmbot/firmware/uart_handler/uart_handler.ex +++ b/lib/farmbot/firmware/uart_handler/uart_handler.ex @@ -158,7 +158,7 @@ defmodule Farmbot.Firmware.UartHandler do end defp do_write(bin, state, dispatch \\ []) do - Logger.debug 3, "writing: #{bin}" + # Logger.debug 3, "writing: #{bin}" case UART.write(state.nerves, bin) do :ok -> {:reply, :ok, dispatch, %{state | current_cmd: bin}} err -> {:reply, err, [], %{state | current_cmd: nil}} diff --git a/lib/farmbot/regimen/manager.ex b/lib/farmbot/regimen/manager.ex new file mode 100644 index 00000000..dda08441 --- /dev/null +++ b/lib/farmbot/regimen/manager.ex @@ -0,0 +1,164 @@ +defmodule Farmbot.Regimen.Manager do + @moduledoc "Manages a Regimen" + + use Farmbot.Logger + use GenServer + alias Farmbot.Repo.Regimen + + defmodule Error do + @moduledoc false + defexception [:epoch, :regimen, :message] + end + + defmodule Item do + @moduledoc false + @type t :: %__MODULE__{ + name: binary, + time_offset: integer, + sequence: Farmbot.CeleryScript.Ast.t + } + + defstruct [:time_offset, :sequence, :name] + + def parse(%{time_offset: offset, sequence_id: sequence_id}) + do + sequence = fetch_sequence(sequence_id) + %__MODULE__{ + name: sequence.name, + time_offset: offset, + sequence: Farmbot.CeleryScript.AST.decode(sequence) |> elem(1)} + end + + def fetch_sequence(id) do + case Farmbot.Repo.current_repo().get(Farmbot.Repo.Sequence, id) do + nil -> raise "Could not find sequence by id: #{inspect id}" + obj -> obj + end + end + end + + @doc false + def start_link(regimen, time) do + GenServer.start_link(__MODULE__, [regimen, time], name: :"regimen-#{regimen.id}") + 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 = build_epoch(time) || raise Error, + message: "Could not determine EPOCH because no timezone was supplied.", + epoch: :error, regimen: regimen + + 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 + Logger.warn 2, "[#{regimen.name}] has no items on regimen." + :ignore + 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 + Logger.success 2, "[#{regimen.name}] is complete!" + # spawn fn() -> + # RegSup.remove_child(regimen) + # end + {:stop, :normal, state} + # {:noreply, :finished} + end + + defp filter_items(regimen) do + regimen.regimen_items + |> Enum.map(&Item.parse(&1)) + |> Enum.sort(&(&1.time_offset <= &2.time_offset)) + end + + defp do_item(item, regimen, state) do + if item do + Logger.busy 2, "[#{regimen.name}] is going to execute: #{item.name}" + Farmbot.CeleryScript.execute(item.sequence) + 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 + next_dt = Timex.shift(state.epoch, milliseconds: nx_itm.time_offset) + timezone = Farmbot.System.ConfigStorage.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 + Logger.info 3, "[#{regimen.name}] #{[nx_itm.name]} has been scheduled " <> + "to happen more than one minute ago: #{offset_from_now} Skipping it." + Process.send_after(pid, :skip, 1000) + else + {msg, real_offset} = ensure_not_negative(offset_from_now) + Process.send_after(pid, msg, real_offset) + end + + timestr = "#{next_dt.month}/#{next_dt.day}/#{next_dt.year} " <> + "at: #{next_dt.hour}:#{next_dt.minute} (#{offset_from_now} milliseconds)" + + Logger.info 3, "[#{regimen.name}] next item will execute on #{timestr}" + + %{state | timer: timer, + regimen: regimen, + next_execution: next_dt} + end + + defp ensure_not_negative(offset) when offset < -60_000, do: {:skip, 1000} + 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 + + # returns midnight of today + @spec build_epoch(DateTime.t) :: DateTime.t + def build_epoch(time) do + tz = Farmbot.System.ConfigStorage.get_config_value(:string, "settings", "timezone") + n = Timex.Timezone.convert(time, tz) + Timex.shift(n, hours: -n.hour, seconds: -n.second, minutes: -n.minute) + end +end diff --git a/lib/farmbot/regimen/supervisor.ex b/lib/farmbot/regimen/supervisor.ex new file mode 100644 index 00000000..b22659c7 --- /dev/null +++ b/lib/farmbot/regimen/supervisor.ex @@ -0,0 +1,25 @@ +defmodule Farmbot.Regimen.Supervisor do + @moduledoc false + use Supervisor + + @doc false + def start_link do + Supervisor.start_link(__MODULE__, [], [name: __MODULE__]) + end + + def init([]) do + children = [] + opts = [strategy: :one_for_one] + supervise(children, opts) + end + + def add_child(regimen, time) do + spec = worker(Farmbot.Regimen.Manager, [regimen, time], [restart: :transient, id: regimen.id]) + Supervisor.start_child(__MODULE__, spec) + end + + def remove_child(regimen) do + Supervisor.terminate_child(__MODULE__, regimen.id) + Supervisor.delete_child(__MODULE__, regimen.id) + end +end diff --git a/lib/farmbot/repo/device.ex b/lib/farmbot/repo/device.ex index 2cc990b7..0675c896 100644 --- a/lib/farmbot/repo/device.ex +++ b/lib/farmbot/repo/device.ex @@ -7,6 +7,7 @@ defmodule Farmbot.Repo.Device do schema "devices" do field(:name, :string) + field(:timezone, :string) end use Farmbot.Repo.Syncable diff --git a/lib/farmbot/repo/farm_event.ex b/lib/farmbot/repo/farm_event.ex index 26004ae8..161b3911 100644 --- a/lib/farmbot/repo/farm_event.ex +++ b/lib/farmbot/repo/farm_event.ex @@ -7,16 +7,19 @@ defmodule Farmbot.Repo.FarmEvent do * A Sequence will execute. """ + alias Farmbot.Repo.JSONType + use Ecto.Schema import Ecto.Changeset schema "farm_events" do - field(:start_time, :utc_datetime) - field(:end_time, :utc_datetime) + field(:start_time, :string) + field(:end_time, :string) field(:repeat, :integer) field(:time_unit, :string) field(:executable_type, Farmbot.Repo.ModuleType.FarmEvent) field(:executable_id, :integer) + field(:calendar, JSONType) end use Farmbot.Repo.Syncable @@ -33,7 +36,6 @@ defmodule Farmbot.Repo.FarmEvent do def changeset(farm_event, params \\ %{}) do farm_event - |> ensure_time([:start_time, :end_time]) |> cast(params, @required_fields) |> validate_required(@required_fields) |> unique_constraint(:id) diff --git a/lib/farmbot/repo/regimen.ex b/lib/farmbot/repo/regimen.ex index 4007d623..41cb2eed 100644 --- a/lib/farmbot/repo/regimen.ex +++ b/lib/farmbot/repo/regimen.ex @@ -3,15 +3,18 @@ defmodule Farmbot.Repo.Regimen do A Regimen is a schedule to run sequences on. """ + alias Farmbot.Repo.JSONType + use Ecto.Schema import Ecto.Changeset schema "regimens" do field(:name, :string) + field(:regimen_items, JSONType) end use Farmbot.Repo.Syncable - @required_fields [:id, :name] + @required_fields [:id, :name, :regimen_items] def changeset(farm_event, params \\ %{}) do farm_event diff --git a/lib/farmbot/repo/repo.ex b/lib/farmbot/repo/repo.ex index 735eec35..1fc70129 100644 --- a/lib/farmbot/repo/repo.ex +++ b/lib/farmbot/repo/repo.ex @@ -33,7 +33,7 @@ defmodule Farmbot.Repo do @doc "Register a diff to be stored until a flip." def register_sync_cmd(sync_cmd) do - GenServer.call(__MODULE__, {:register_sync_cmd, sync_cmd}) + GenServer.call(__MODULE__, {:register_sync_cmd, sync_cmd}, :infinity) end @doc false @@ -52,9 +52,21 @@ defmodule Farmbot.Repo do "A" -> [repo_a, repo_b] "B" -> [repo_b, repo_a] end + # Copy configs + [current, _] = repos + copy_configs(current) {:ok, %{repos: repos, sync_cmds: []}} end + defp copy_configs(repo) do + case repo.one(Farmbot.Repo.Device) do + nil -> :ok + %{timezone: tz} -> + Farmbot.System.ConfigStorage.update_config_value(:string, "settings", "timezone", tz) + :ok + end + end + def terminate(_,_) do Farmbot.BotState.set_sync_status(:sync_error) end @@ -80,7 +92,7 @@ defmodule Farmbot.Repo do Farmbot.System.ConfigStorage.update_config_value(:string, "settings", "current_repo", "A") end Farmbot.BotState.set_sync_status(:synced) - + copy_configs(repo_b) {:reply, repo_b, %{state | repos: [repo_b, repo_a]}} end @@ -183,6 +195,7 @@ defmodule Farmbot.Repo do end defp sync_resource(repo, resource, slug) do + Logger.debug 3, "syncing: #{resource} (#{slug})" as = if resource in @singular_resources do struct(resource) else diff --git a/priv/repo/migrations/20170919150432_add_farm_events_table.exs b/priv/repo/migrations/20170919150432_add_farm_events_table.exs index fe973401..4f83f7c0 100644 --- a/priv/repo/migrations/20170919150432_add_farm_events_table.exs +++ b/priv/repo/migrations/20170919150432_add_farm_events_table.exs @@ -4,12 +4,13 @@ defmodule Farmbot.Repo.Migrations.AddFarmEventsTable do def change do create table("farm_events", primary_key: false) do add(:id, :integer) - add(:start_time, :utc_datetime) - add(:end_time, :utc_datetime) + add(:start_time, :string) + add(:end_time, :string) add(:repeat, :integer) add(:time_unit, :string) add(:executable_type, :string) add(:executable_id, :integer) + add(:calendar, :string) end create(unique_index("farm_events", [:id])) diff --git a/priv/repo/migrations/20170920182805_add_regimens_table.exs b/priv/repo/migrations/20170920182805_add_regimens_table.exs index 4a80c64c..877e1b64 100644 --- a/priv/repo/migrations/20170920182805_add_regimens_table.exs +++ b/priv/repo/migrations/20170920182805_add_regimens_table.exs @@ -5,6 +5,7 @@ defmodule Farmbot.Repo.Migrations.AddRegimensTable do create table("regimens", primary_key: false) do add(:id, :integer) add(:name, :string) + add(:regimen_items, :string) end create(unique_index("regimens", [:id])) diff --git a/priv/repo/migrations/20171025231225_add_devices_table.exs b/priv/repo/migrations/20171025231225_add_devices_table.exs index 7ebec5b9..154512de 100644 --- a/priv/repo/migrations/20171025231225_add_devices_table.exs +++ b/priv/repo/migrations/20171025231225_add_devices_table.exs @@ -5,6 +5,7 @@ defmodule Farmbot.Repo.A.Migrations.AddDevicesTable do create table("devices", primary_key: false) do add(:id, :integer) add(:name, :string) + add(:timezone, :string) end create(unique_index("devices", [:id]))