Enable config_update for beta updates

Enable config_update for beta updates

defmodule Farmbot.CeleryScript.AST.Node.ConfigUpdate do
  @moduledoc false
  use Farmbot.CeleryScript.AST.Node
  use Farmbot.Logger
  allow_args [:package]

  def execute(%{package: :farmbot_os}, body, env) do
    env = mutate_env(env)
    do_reduce_os(body, env)
  end

  def execute(%{package: :arduino_firmware}, body, env) do
    env = mutate_env(env)
    do_reduce_fw(body, env)
  end

  defp do_reduce_os([%{args: %{label: key, value: value}} | rest], env) do
    case lookup_os_config(key, value) do
      {:ok, {:string, "settings", val}} when val in ["farmduino", "arduino"] ->
        Farmbot.System.ConfigStorage.update_config_value(:string, "settings", key, val)
        hack_file = Path.join(Application.get_env(:farmbot, :data_path), "firmware_flash")
        File.write!(hack_file, val)
        Farmbot.BotState.set_sync_status(:maintenance)
        Logger.warn 1, "Rebooting to flash firmware."
        Farmbot.System.reboot("Flash arduino fw.")
        # Stop enumeration here.
        {:ok, env}
      {:ok, {type, group, value}} ->
        Farmbot.System.ConfigStorage.update_config_value(type, group, key, value)
        Logger.success 3, "Updating: #{inspect key}: #{value}"
        do_reduce_os(rest, env)
      {:error, reason} ->
        {:error, reason, env}
    end
  end

  defp do_reduce_os([], env) do
    {:ok, env}
  end

  defp do_reduce_fw([%{args: %{label: key, value: value}} | rest], env) do
    case Farmbot.Firmware.update_param(:"#{key}", value) do
      :ok -> do_reduce_fw(rest, env)
      {:error, reason} -> {:error, reason, env}
    end
  end

  defp do_reduce_fw([], env), do: {:ok, env}

  defp lookup_os_config("os_auto_update",            val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("auto_sync",                 val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("timezone",                  val), do: {:ok, {:string, "settings", val}}

  defp lookup_os_config("disable_factory_reset",     val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}

  defp lookup_os_config("sequence_init_log",         val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("sequence_body_log",         val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("sequence_complete_log",     val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("arduino_debug_messages",    val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("firmware_input_log",        val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("firmware_output_log",       val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("beta_opt_in",               val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}

  defp lookup_os_config("network_not_found_timer",   val) when val > 0, do: {:ok, {:float,  "settings", to_float(val)}}
  defp lookup_os_config("network_not_found_timer",  _val), do: {:error, "network_not_found_timer must be greater than zero"}

  defp lookup_os_config("firmware_hardware", "farmduino"), do: {:ok, {:string, "settings", "farmduino"}}
  defp lookup_os_config("firmware_hardware",   "arduino"), do: {:ok, {:string, "settings", "arduino"}}
  defp lookup_os_config("firmware_hardware",     unknown), do: {:error, "unknown hardware: #{unknown}"}

  defp lookup_os_config(unknown_config,                _), do: {:error, "unknown config: #{unknown_config}"}

  defp format_bool_for_os(1), do: true
  defp format_bool_for_os(0), do: false
  defp format_bool_for_os(true), do: true
  defp format_bool_for_os(false), do: false

  defp to_float(int) when is_integer(int) do
    int / 1
  end

  defp to_float(float) when is_float(float) do
    float
  end
end

defmodule Farmbot.CeleryScript.AST.Node.ConfigUpdate do
  @moduledoc false
  use Farmbot.CeleryScript.AST.Node
  use Farmbot.Logger
  allow_args [:package]

  def execute(%{package: :farmbot_os}, body, env) do
    env = mutate_env(env)
    do_reduce_os(body, env)
  end

  def execute(%{package: :arduino_firmware}, body, env) do
    env = mutate_env(env)
    do_reduce_fw(body, env)
  end

  defp do_reduce_os([%{args: %{label: key, value: value}} | rest], env) do
    case lookup_os_config(key, value) do
      {:ok, {:string, "settings", val}} when val in ["farmduino", "arduino"] ->
        Farmbot.System.ConfigStorage.update_config_value(:string, "settings", key, val)
        hack_file = Path.join(Application.get_env(:farmbot, :data_path), "firmware_flash")
        File.write!(hack_file, val)
        Farmbot.BotState.set_sync_status(:maintenance)
        Logger.warn 1, "Rebooting to flash firmware."
        Farmbot.System.reboot("Flash arduino fw.")
        # Stop enumeration here.
        {:ok, env}
      {:ok, {type, group, value}} ->
        Farmbot.System.ConfigStorage.update_config_value(type, group, key, value)
        Logger.success 3, "Updating: #{inspect key}: #{value}"
        do_reduce_os(rest, env)
      {:error, reason} ->
        {:error, reason, env}
    end
  end

  defp do_reduce_os([], env) do
    {:ok, env}
  end

  defp do_reduce_fw([%{args: %{label: key, value: value}} | rest], env) do
    case Farmbot.Firmware.update_param(:"#{key}", value) do
      :ok -> do_reduce_fw(rest, env)
      {:error, reason} -> {:error, reason, env}
    end
  end

  defp do_reduce_fw([], env), do: {:ok, env}

  defp lookup_os_config("os_auto_update",            val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("auto_sync",                 val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("timezone",                  val), do: {:ok, {:string, "settings", val}}

  defp lookup_os_config("disable_factory_reset",     val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}

  defp lookup_os_config("sequence_init_log",         val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("sequence_body_log",         val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("sequence_complete_log",     val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("arduino_debug_messages",    val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("firmware_input_log",        val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}
  defp lookup_os_config("firmware_output_log",       val), do: {:ok, {:bool,   "settings", format_bool_for_os(val)}}

  defp lookup_os_config("network_not_found_timer",   val) when val > 0, do: {:ok, {:float,  "settings", to_float(val)}}
  defp lookup_os_config("network_not_found_timer",  _val), do: {:error, "network_not_found_timer must be greater than zero"}

  defp lookup_os_config("firmware_hardware", "farmduino"), do: {:ok, {:string, "settings", "farmduino"}}
  defp lookup_os_config("firmware_hardware",   "arduino"), do: {:ok, {:string, "settings", "arduino"}}
  defp lookup_os_config("firmware_hardware",     unknown), do: {:error, "unknown hardware: #{unknown}"}

  defp lookup_os_config(unknown_config,                _), do: {:error, "unknown config: #{unknown_config}"}

  defp format_bool_for_os(1), do: true
  defp format_bool_for_os(0), do: false
  defp format_bool_for_os(true), do: true
  defp format_bool_for_os(false), do: false

  defp to_float(int) when is_integer(int) do
    int / 1
  end

  defp to_float(float) when is_float(float) do
    float
  end
end


Fix timeouts

Fix timeouts

defmodule Farmbot.BotState do
  @moduledoc "JSON Serializable state tree that gets pushed over variour transports."

  use GenStage
  @version Mix.Project.config()[:version]
  @commit Mix.Project.config()[:commit]
  @target Mix.Project.config()[:target]
  @env Mix.env()

  alias Farmbot.CeleryScript.AST
  use Farmbot.Logger

  defstruct mcu_params: %{
      param_version: nil,
      param_test: nil,
      param_config_ok: nil,
      param_use_eeprom: nil,
      param_e_stop_on_mov_err: nil,
      param_mov_nr_retry: nil,
      movement_timeout_x: nil,
      movement_timeout_y: nil,
      movement_timeout_z: nil,
      movement_keep_active_x: nil,
      movement_keep_active_y: nil,
      movement_keep_active_z: nil,
      movement_home_at_boot_x: nil,
      movement_home_at_boot_y: nil,
      movement_home_at_boot_z: nil,
      movement_invert_endpoints_x: nil,
      movement_invert_endpoints_y: nil,
      movement_invert_endpoints_z: nil,
      movement_enable_endpoints_x: nil,
      movement_enable_endpoints_y: nil,
      movement_enable_endpoints_z: nil,
      movement_invert_motor_x: nil,
      movement_invert_motor_y: nil,
      movement_invert_motor_z: nil,
      movement_secondary_motor_x: nil,
      movement_secondary_motor_invert_x: nil,
      movement_steps_acc_dec_x: nil,
      movement_steps_acc_dec_y: nil,
      movement_steps_acc_dec_z: nil,
      movement_stop_at_home_x: nil,
      movement_stop_at_home_y: nil,
      movement_stop_at_home_z: nil,
      movement_home_up_x: nil,
      movement_home_up_y: nil,
      movement_home_up_z: nil,
      movement_step_per_mm_x: nil,
      movement_step_per_mm_y: nil,
      movement_step_per_mm_z: nil,
      movement_min_spd_x: nil,
      movement_min_spd_y: nil,
      movement_min_spd_z: nil,
      movement_home_spd_x: nil,
      movement_home_spd_y: nil,
      movement_home_spd_z: nil,
      movement_max_spd_x: nil,
      movement_max_spd_y: nil,
      movement_max_spd_z: nil,
      encoder_enabled_x: nil,
      encoder_enabled_y: nil,
      encoder_enabled_z: nil,
      encoder_type_x: nil,
      encoder_type_y: nil,
      encoder_type_z: nil,
      encoder_missed_steps_max_x: nil,
      encoder_missed_steps_max_y: nil,
      encoder_missed_steps_max_z: nil,
      encoder_scaling_x: nil,
      encoder_scaling_y: nil,
      encoder_scaling_z: nil,
      encoder_missed_steps_decay_x: nil,
      encoder_missed_steps_decay_y: nil,
      encoder_missed_steps_decay_z: nil,
      encoder_use_for_pos_x: nil,
      encoder_use_for_pos_y: nil,
      encoder_use_for_pos_z: nil,
      encoder_invert_x: nil,
      encoder_invert_y: nil,
      encoder_invert_z: nil,
      movement_axis_nr_steps_x: nil,
      movement_axis_nr_steps_y: nil,
      movement_axis_nr_steps_z: nil,
      movement_stop_at_max_x: nil,
      movement_stop_at_max_y: nil,
      movement_stop_at_max_z: nil,
      pin_guard_1_pin_nr: nil,
      pin_guard_1_time_out: nil,
      pin_guard_1_active_state: nil,
      pin_guard_2_pin_nr: nil,
      pin_guard_2_time_out: nil,
      pin_guard_2_active_state: nil,
      pin_guard_3_pin_nr: nil,
      pin_guard_3_time_out: nil,
      pin_guard_3_active_state: nil,
      pin_guard_4_pin_nr: nil,
      pin_guard_4_time_out: nil,
      pin_guard_4_active_state: nil,
      pin_guard_5_pin_nr: nil,
      pin_guard_5_time_out: nil,
      pin_guard_5_active_state: nil,
    },
            jobs: %{},
            location_data: %{
              position: %{x: nil, y: nil, z: nil},
              scaled_encoders: %{x: nil, y: nil, z: nil},
              raw_encoders: %{x: nil, y: nil, z: nil},
            },
            gpio_registry: %{},
            pins: %{},
            configuration: %{},
            informational_settings: %{
              controller_version: @version,
              firmware_version: "disconnected",
              commit: @commit,
              target: @target,
              env: @env,
              node_name: nil,
              busy: false,
              sync_status: :sync_now,
              sync_status_two: nil,
              locked: false
            },
            user_env: %{},
            process_info: %{
              farmwares: %{}
            }

    def download_progress_fun(name) do
    alias Farmbot.BotState.JobProgress
    fn(bytes, total) ->
      {do_send, prog} = cond do
        # if the total is complete spit out the bytes, and put a status of complete.
        total == :complete ->
          Logger.success 3, "#{name} complete."
          {true, %JobProgress.Bytes{bytes: bytes, status: :complete}}

        # if we don't know the total just spit out the bytes.
        total == nil ->
          # debug_log "#{name} - #{bytes} bytes."
          {rem(bytes, 10) == 0, %JobProgress.Bytes{bytes: bytes}}
        # if the number of bytes == the total bytes, percentage side is complete.
        (div(bytes, total)) == 1 ->
          Logger.success 3, "#{name} complete."
          {true, %JobProgress.Percent{percent: 100, status: :complete}}
        # anything else is a percent.
        true ->
          percent = ((bytes / total) * 100) |> round()
          # Logger.busy 3, "#{name} - #{bytes}/#{total} = #{percent}%"
          {rem(percent, 10) == 0, %JobProgress.Percent{percent: percent}}
      end
      if do_send do
        Farmbot.BotState.set_job_progress(name, prog)
      else
        :ok
      end
    end
  end

  def locked? do
    GenStage.call(__MODULE__, :locked?)
  end

  @doc "Set job progress."
  def set_job_progress(name, progress) do
    GenServer.call(__MODULE__, {:set_job_progress, name, progress})
  end

  @doc "Get a current pin value."
  def get_pin_value(num) do
    GenStage.call(__MODULE__, {:get_pin_value, num})
  end

  @doc "Get the bot's current position."
  def get_current_pos do
    GenStage.call(__MODULE__, :get_current_pos)
  end

  @doc "Get a arduino param."
  def get_param(param) do
    GenStage.call(__MODULE__, {:get_param, param})
  end

  @doc false
  def set_busy(bool) do
    GenStage.call(__MODULE__, {:set_busy, bool})
  end

  @valid_sync_status [:maintenance, :sync_error, :sync_now, :synced, :syncing, :unknown]
  @doc "Set the sync status above ticker to a message."
  def set_sync_status(cmd) when cmd in @valid_sync_status do
    GenStage.call(__MODULE__, {:set_sync_status, cmd})
  end

  @doc "Forces a state push over all transports."
  def force_state_push do
    GenStage.call(__MODULE__, :force_state_push)
  end

  @doc "Register a farmware in the bot's state."
  def register_farmware(%Farmbot.Farmware{} = fw) do
    GenStage.call(__MODULE__, {:register_farmware, fw})
  end

  @doc "Unregister a farmware form the bot's state."
  def unregister_farmware(%Farmbot.Farmware{} = fw) do
    GenStage.call(__MODULE__, {:unregister_farmware, fw})
  end

  @doc "Emit an AST."
  def emit(%AST{} = ast) do
    GenStage.call(__MODULE__, {:emit, ast})
  end

  @doc "Get user env."
  def get_user_env do
    GenStage.call(__MODULE__, :get_user_env)
  end

  @doc "Set user env."
  def set_user_env(key, val) do
    GenStage.call(__MODULE__, {:set_user_env, key, val})
  end

  @doc false
  def start_link() do
    GenStage.start_link(__MODULE__, [], [name: __MODULE__])
  end

  def init([]) do
    settings = Farmbot.System.ConfigStorage.get_config_as_map()["settings"]
    user_env = Poison.decode!(settings["user_env"])
    initial_state = struct(__MODULE__, configuration: Map.delete(settings, "user_env"), user_env: user_env)
    state = %{initial_state | informational_settings: %{initial_state.informational_settings | node_name: node()}}
    {
      :producer_consumer,
      state,
      subscribe_to: [Farmbot.Firmware, Farmbot.System.ConfigStorage.Dispatcher, Farmbot.System.GPIO],
      dispatcher: GenStage.BroadcastDispatcher
    }
  end

  def handle_events(events, _from, state) do
    state = do_handle(events, state)
    # Logger.success 3, "Finish handle bot state events"
    {:noreply, [state], state}
  end

  def handle_call(:locked?, _from, state) do
    {:reply, state.informational_settings.locked, [], state}
  end

  def handle_call({:get_pin_value, pin}, _from, state) do
    case state.pins[pin] do
      nil ->
        {:reply, {:error, :unknown_pin}, [], state}
      %{value: value} ->
        {:reply, {:ok, value}, [], state}
    end
  end

  def handle_call(:force_state_push, _from, state) do
    {:reply, state, [state], state}
  end

  def handle_call({:set_busy, bool}, _from, state) do
    new_info_settings = %{state.informational_settings | busy: bool}
    new_state = %{state | informational_settings: new_info_settings}
    {:reply, :ok, [new_state], new_state}
  end

  def handle_call({:emit, ast}, _from, state) do
    {:reply, :ok, [{:emit, ast}], state}
  end

  def handle_call({:set_sync_status, status}, _, state) do
    new_info_settings = %{state.informational_settings | sync_status: status}
    new_state = %{state | informational_settings: new_info_settings}
    {:reply, :ok, [new_state], new_state}
  end

  def handle_call({:set_user_env, key, val}, _, state) do
    new_user_env = Map.merge(state.user_env, %{to_string(key) => val})
    case Poison.encode(new_user_env) do
      {:ok, encoded} ->
        Farmbot.System.ConfigStorage.update_config_value(:string, "settings", "user_env", encoded)
        {:reply, :ok, [], state}
      _ -> {:reply, {:error, "user_env must be json encodeable!"}, [], state}
    end
  end

  def handle_call(:get_user_env, _from, state) do
    {:reply, state.user_env, [], state}
  end

  def handle_call(:get_current_pos, _from, state) do
    {:reply, state.location_data.position, [], state}
  end

  def handle_call({:get_param, param}, _from, state) do
    {:reply, state.mcu_params[param], [], state}
  end

  def handle_call({:set_job_progress, name, progress}, _from, state) do
    jobs = Map.put(state.jobs, name, progress)
    new_state = %{state | jobs: jobs}
    {:reply, :ok, [new_state], new_state}
  end

  def handle_call({:register_farmware, fw}, _, state) do
    ser_fw_meta = %{
      min_os_version_major: fw.min_os_version_major,
      description: fw.meta.description,
      language: fw.meta.language,
      version: to_string(fw.version),
      author: fw.meta.author,
      zip: fw.zip
    }
    ser_fw = %{
      args: fw.args,
      executable: fw.executable,
      meta: ser_fw_meta,
      name: fw.name,
      path: Farmbot.Farmware.Installer.install_path(fw),
      config: fw.config,
      url: fw.url
    }
    new_pi = Map.put(state.process_info.farmwares, fw.name, ser_fw)
    new_state = %{state | process_info: %{farmwares: new_pi}}
    {:reply, :ok, [new_state], new_state}
  end

  def handle_call({:unregister_farmware, fw}, _, state) do
    new_pi = Map.delete(state.process_info.farmwares, fw.name)
    new_state = %{state | process_info: %{farmwares: new_pi}}
    {:reply, :ok, [new_state], new_state}
  end

  defp do_handle([], state), do: state

  defp do_handle([{:config, "settings", "user_env", val} | rest], state) do
    new_env = Map.merge(state.user_env, Poison.decode!(val))
    new_state = %{state | user_env: new_env}
    do_handle(rest, new_state)
  end

  defp do_handle([{:config, "settings", "auto_sync" = key, true = val} | rest], state) do
    spawn Farmbot.Repo, :flip, []
    new_config = Map.put(state.configuration, key, val)
    new_state = %{state | configuration: new_config}
    do_handle(rest, new_state)
  end

  defp do_handle([{:config, "settings", key, val} | rest], state) do
    new_config = Map.put(state.configuration, key, val)
    new_state = %{state | configuration: new_config}
    do_handle(rest, new_state)
  end

  defp do_handle([{:config, _, _, _} | rest], state) do
    do_handle(rest, state)
  end

  # This one is special because it sends the _entire_ sub tree, not just
  # Parts of it.
  defp do_handle([{:gpio_registry, gpio_registry} | rest], state) do
    do_handle(rest, %{state | gpio_registry: gpio_registry})
  end

  defp do_handle([{key, diff} | rest], state) do
    state = %{state | key => Map.merge(Map.get(state, key), diff)}
    do_handle(rest, state)
  end
end

defmodule Farmbot.BotState do
  @moduledoc "JSON Serializable state tree that gets pushed over variour transports."

  use GenStage
  @version Mix.Project.config()[:version]
  @commit Mix.Project.config()[:commit]
  @target Mix.Project.config()[:target]
  @env Mix.env()

  alias Farmbot.CeleryScript.AST
  use Farmbot.Logger

  defstruct mcu_params: %{
      param_version: nil,
      param_test: nil,
      param_config_ok: nil,
      param_use_eeprom: nil,
      param_e_stop_on_mov_err: nil,
      param_mov_nr_retry: nil,
      movement_timeout_x: nil,
      movement_timeout_y: nil,
      movement_timeout_z: nil,
      movement_keep_active_x: nil,
      movement_keep_active_y: nil,
      movement_keep_active_z: nil,
      movement_home_at_boot_x: nil,
      movement_home_at_boot_y: nil,
      movement_home_at_boot_z: nil,
      movement_invert_endpoints_x: nil,
      movement_invert_endpoints_y: nil,
      movement_invert_endpoints_z: nil,
      movement_enable_endpoints_x: nil,
      movement_enable_endpoints_y: nil,
      movement_enable_endpoints_z: nil,
      movement_invert_motor_x: nil,
      movement_invert_motor_y: nil,
      movement_invert_motor_z: nil,
      movement_secondary_motor_x: nil,
      movement_secondary_motor_invert_x: nil,
      movement_steps_acc_dec_x: nil,
      movement_steps_acc_dec_y: nil,
      movement_steps_acc_dec_z: nil,
      movement_stop_at_home_x: nil,
      movement_stop_at_home_y: nil,
      movement_stop_at_home_z: nil,
      movement_home_up_x: nil,
      movement_home_up_y: nil,
      movement_home_up_z: nil,
      movement_step_per_mm_x: nil,
      movement_step_per_mm_y: nil,
      movement_step_per_mm_z: nil,
      movement_min_spd_x: nil,
      movement_min_spd_y: nil,
      movement_min_spd_z: nil,
      movement_home_spd_x: nil,
      movement_home_spd_y: nil,
      movement_home_spd_z: nil,
      movement_max_spd_x: nil,
      movement_max_spd_y: nil,
      movement_max_spd_z: nil,
      encoder_enabled_x: nil,
      encoder_enabled_y: nil,
      encoder_enabled_z: nil,
      encoder_type_x: nil,
      encoder_type_y: nil,
      encoder_type_z: nil,
      encoder_missed_steps_max_x: nil,
      encoder_missed_steps_max_y: nil,
      encoder_missed_steps_max_z: nil,
      encoder_scaling_x: nil,
      encoder_scaling_y: nil,
      encoder_scaling_z: nil,
      encoder_missed_steps_decay_x: nil,
      encoder_missed_steps_decay_y: nil,
      encoder_missed_steps_decay_z: nil,
      encoder_use_for_pos_x: nil,
      encoder_use_for_pos_y: nil,
      encoder_use_for_pos_z: nil,
      encoder_invert_x: nil,
      encoder_invert_y: nil,
      encoder_invert_z: nil,
      movement_axis_nr_steps_x: nil,
      movement_axis_nr_steps_y: nil,
      movement_axis_nr_steps_z: nil,
      movement_stop_at_max_x: nil,
      movement_stop_at_max_y: nil,
      movement_stop_at_max_z: nil,
      pin_guard_1_pin_nr: nil,
      pin_guard_1_time_out: nil,
      pin_guard_1_active_state: nil,
      pin_guard_2_pin_nr: nil,
      pin_guard_2_time_out: nil,
      pin_guard_2_active_state: nil,
      pin_guard_3_pin_nr: nil,
      pin_guard_3_time_out: nil,
      pin_guard_3_active_state: nil,
      pin_guard_4_pin_nr: nil,
      pin_guard_4_time_out: nil,
      pin_guard_4_active_state: nil,
      pin_guard_5_pin_nr: nil,
      pin_guard_5_time_out: nil,
      pin_guard_5_active_state: nil,
    },
            jobs: %{},
            location_data: %{
              position: %{x: nil, y: nil, z: nil},
              scaled_encoders: %{x: nil, y: nil, z: nil},
              raw_encoders: %{x: nil, y: nil, z: nil},
            },
            gpio_registry: %{},
            pins: %{},
            configuration: %{},
            informational_settings: %{
              controller_version: @version,
              firmware_version: "disconnected",
              commit: @commit,
              target: @target,
              env: @env,
              node_name: nil,
              busy: false,
              sync_status: :sync_now,
              sync_status_two: nil,
              locked: false
            },
            user_env: %{},
            process_info: %{
              farmwares: %{}
            }

    def download_progress_fun(name) do
    alias Farmbot.BotState.JobProgress
    fn(bytes, total) ->
      {do_send, prog} = cond do
        # if the total is complete spit out the bytes, and put a status of complete.
        total == :complete ->
          Logger.success 3, "#{name} complete."
          {true, %JobProgress.Bytes{bytes: bytes, status: :complete}}

        # if we don't know the total just spit out the bytes.
        total == nil ->
          # debug_log "#{name} - #{bytes} bytes."
          {rem(bytes, 10) == 0, %JobProgress.Bytes{bytes: bytes}}
        # if the number of bytes == the total bytes, percentage side is complete.
        (div(bytes, total)) == 1 ->
          Logger.success 3, "#{name} complete."
          {true, %JobProgress.Percent{percent: 100, status: :complete}}
        # anything else is a percent.
        true ->
          percent = ((bytes / total) * 100) |> round()
          # Logger.busy 3, "#{name} - #{bytes}/#{total} = #{percent}%"
          {rem(percent, 10) == 0, %JobProgress.Percent{percent: percent}}
      end
      if do_send do
        Farmbot.BotState.set_job_progress(name, prog)
      else
        :ok
      end
    end
  end

  def locked? do
    GenStage.call(__MODULE__, :locked?)
  end

  @doc "Set job progress."
  def set_job_progress(name, progress) do
    GenServer.call(__MODULE__, {:set_job_progress, name, progress})
  end

  @doc "Get a current pin value."
  def get_pin_value(num) do
    GenStage.call(__MODULE__, {:get_pin_value, num})
  end

  @doc "Get the bot's current position."
  def get_current_pos do
    GenStage.call(__MODULE__, :get_current_pos)
  end

  @doc "Get a arduino param."
  def get_param(param) do
    GenStage.call(__MODULE__, {:get_param, param})
  end

  @doc false
  def set_busy(bool) do
    GenStage.call(__MODULE__, {:set_busy, bool})
  end

  @valid_sync_status [:maintenance, :sync_error, :sync_now, :synced, :syncing, :unknown]
  @doc "Set the sync status above ticker to a message."
  def set_sync_status(cmd) when cmd in @valid_sync_status do
    GenStage.call(__MODULE__, {:set_sync_status, cmd})
  end

  @doc "Forces a state push over all transports."
  def force_state_push do
    GenStage.call(__MODULE__, :force_state_push)
  end

  @doc "Register a farmware in the bot's state."
  def register_farmware(%Farmbot.Farmware{} = fw) do
    GenStage.call(__MODULE__, {:register_farmware, fw})
  end

  @doc "Unregister a farmware form the bot's state."
  def unregister_farmware(%Farmbot.Farmware{} = fw) do
    GenStage.call(__MODULE__, {:unregister_farmware, fw})
  end

  @doc "Emit an AST."
  def emit(%AST{} = ast) do
    GenStage.call(__MODULE__, {:emit, ast})
  end

  @doc "Get user env."
  def get_user_env do
    GenStage.call(__MODULE__, :get_user_env)
  end

  @doc "Set user env."
  def set_user_env(key, val) do
    GenStage.call(__MODULE__, {:set_user_env, key, val})
  end

  @doc false
  def start_link() do
    GenStage.start_link(__MODULE__, [], [name: __MODULE__])
  end

  def init([]) do
    settings = Farmbot.System.ConfigStorage.get_config_as_map()["settings"]
    user_env = Poison.decode!(settings["user_env"])
    initial_state = struct(__MODULE__, configuration: Map.delete(settings, "user_env"), user_env: user_env)
    state = %{initial_state | informational_settings: %{initial_state.informational_settings | node_name: node()}}
    {
      :producer_consumer,
      state,
      subscribe_to: [Farmbot.Firmware, Farmbot.System.ConfigStorage.Dispatcher, Farmbot.System.GPIO],
      dispatcher: GenStage.BroadcastDispatcher
    }
  end

  def handle_events(events, _from, state) do
    state = do_handle(events, state)
    # Logger.success 3, "Finish handle bot state events"
    {:noreply, [state], state}
  end

  def handle_call(:locked?, _from, state) do
    {:reply, state.informational_settings.locked, [], state}
  end

  def handle_call({:get_pin_value, pin}, _from, state) do
    case state.pins[pin] do
      nil ->
        {:reply, {:error, :unknown_pin}, [], state}
      %{value: value} ->
        {:reply, {:ok, value}, [], state}
    end
  end

  def handle_call(:force_state_push, _from, state) do
    {:reply, state, [state], state}
  end

  def handle_call({:set_busy, bool}, _from, state) do
    new_info_settings = %{state.informational_settings | busy: bool}
    new_state = %{state | informational_settings: new_info_settings}
    {:reply, :ok, [new_state], new_state}
  end

  def handle_call({:emit, ast}, _from, state) do
    {:reply, :ok, [{:emit, ast}], state}
  end

  def handle_call({:set_sync_status, status}, _, state) do
    new_info_settings = %{state.informational_settings | sync_status: status}
    new_state = %{state | informational_settings: new_info_settings}
    {:reply, :ok, [new_state], new_state}
  end

  def handle_call({:set_user_env, key, val}, _, state) do
    new_user_env = Map.merge(state.user_env, %{to_string(key) => val})
    case Poison.encode(new_user_env) do
      {:ok, encoded} ->
        Farmbot.System.ConfigStorage.update_config_value(:string, "settings", "user_env", encoded)
        {:reply, :ok, [], state}
      _ -> {:reply, {:error, "user_env must be json encodeable!"}, [], state}
    end
  end

  def handle_call(:get_user_env, _from, state) do
    {:reply, state.user_env, [], state}
  end

  def handle_call(:get_current_pos, _from, state) do
    {:reply, state.location_data.position, [], state}
  end

  def handle_call({:get_param, param}, _from, state) do
    {:reply, state.mcu_params[param], [], state}
  end

  def handle_call({:set_job_progress, name, progress}, _from, state) do
    jobs = Map.put(state.jobs, name, progress)
    new_state = %{state | jobs: jobs}
    {:reply, :ok, [new_state], new_state}
  end

  def handle_call({:register_farmware, fw}, _, state) do
    ser_fw_meta = %{
      min_os_version_major: fw.min_os_version_major,
      description: fw.meta.description,
      language: fw.meta.language,
      version: to_string(fw.version),
      author: fw.meta.author,
      zip: fw.zip
    }
    ser_fw = %{
      args: fw.args,
      executable: fw.executable,
      meta: ser_fw_meta,
      name: fw.name,
      path: Farmbot.Farmware.Installer.install_path(fw),
      config: fw.config,
      url: fw.url
    }
    new_pi = Map.put(state.process_info.farmwares, fw.name, ser_fw)
    new_state = %{state | process_info: %{farmwares: new_pi}}
    {:reply, :ok, [new_state], new_state}
  end

  def handle_call({:unregister_farmware, fw}, _, state) do
    new_pi = Map.delete(state.process_info.farmwares, fw.name)
    new_state = %{state | process_info: %{farmwares: new_pi}}
    {:reply, :ok, [new_state], new_state}
  end

  defp do_handle([], state), do: state

  defp do_handle([{:config, "settings", "user_env", val} | rest], state) do
    new_env = Map.merge(state.user_env, Poison.decode!(val))
    new_state = %{state | user_env: new_env}
    do_handle(rest, new_state)
  end

  defp do_handle([{:config, "settings", "auto_sync" = key, true = val} | rest], state) do
    Farmbot.Repo.flip()
    new_config = Map.put(state.configuration, key, val)
    new_state = %{state | configuration: new_config}
    do_handle(rest, new_state)
  end

  defp do_handle([{:config, "settings", key, val} | rest], state) do
    new_config = Map.put(state.configuration, key, val)
    new_state = %{state | configuration: new_config}
    do_handle(rest, new_state)
  end

  defp do_handle([{:config, _, _, _} | rest], state) do
    do_handle(rest, state)
  end

  # This one is special because it sends the _entire_ sub tree, not just
  # Parts of it.
  defp do_handle([{:gpio_registry, gpio_registry} | rest], state) do
    do_handle(rest, %{state | gpio_registry: gpio_registry})
  end

  defp do_handle([{key, diff} | rest], state) do
    state = %{state | key => Map.merge(Map.get(state, key), diff)}
    do_handle(rest, state)
  end
end


Expose appearance age groups via API

Expose appearance age groups via API

defmodule JumubaseWeb.Schema.Objects do
  use Absinthe.Schema.Notation
  import Absinthe.Resolution.Helpers, only: [dataloader: 1, dataloader: 3]
  alias Jumubase.Foundation
  alias Jumubase.Showtime
  alias JumubaseWeb.{FoundationResolver, ShowtimeResolver}

  object :appearance do
    field :id, non_null(:id)

    field :participant_name, non_null(:string) do
      description "The full name of the appearance's participant."
      resolve &ShowtimeResolver.participant_name/3
    end

    field :instrument_name, non_null(:string) do
      description "The name of the participant's instrument in this appearance."
      resolve &ShowtimeResolver.instrument_name/3
    end

    field :is_accompanist, non_null(:boolean) do
      description "Whether the appearance's participant has an accompanist role."
      resolve &ShowtimeResolver.accompanist?/3
    end

    field :age_group, non_null(:string) do
      description "The appearance's age group (which may differ from the perfomance's)."
    end

    field :result, :result do
      description "The appearance's result, if publicly available."

      resolve dataloader(Showtime, :performance,
                args: %{scope: :result},
                callback: &ShowtimeResolver.result/3
              )
    end
  end

  object :contest do
    field :id, non_null(:id)

    field :name, non_null(:string) do
      description "The contest’s name containing the round, year and host."
      resolve &FoundationResolver.name/3
    end

    field :country_code, non_null(:string) do
      description "The country code of the contest's host."
      resolve &FoundationResolver.country_code/3
    end

    field :dates, non_null(list_of(non_null(:date))) do
      description "The dates on which the contest is happening."
      resolve &FoundationResolver.dates/3
    end

    field :stages, non_null(list_of(non_null(:stage))) do
      description "The stages used in this contest."
      resolve &FoundationResolver.stages/3
    end
  end

  object :contest_category do
    field :id, non_null(:id)

    field :name, non_null(:string) do
      description "The contest category's name."
      resolve &FoundationResolver.name/3
    end

    field :public_result_count, non_null(:integer) do
      description "The amount of performances with public results in this contest category."

      resolve dataloader(Foundation, :performances,
                callback: &FoundationResolver.public_result_count/3
              )
    end
  end

  object :host do
    field :id, non_null(:id)

    field :name, non_null(:string) do
      description "The name of the host."
    end

    field :country_code, non_null(:string) do
      description "The country code of the host."
    end
  end

  object :performance do
    field :id, non_null(:id)

    field :stage_date, non_null(:date) do
      description "The scheduled date of the performance."
      resolve &ShowtimeResolver.stage_date/3
    end

    field :stage_time, non_null(:time) do
      description "The scheduled wall time of the performance."
      resolve &ShowtimeResolver.stage_time/3
    end

    field :category_name, non_null(:string) do
      description "The name of the performance's category."
      resolve &ShowtimeResolver.category_name/3
    end

    field :age_group, non_null(:string) do
      description "The performance's age group."
    end

    field :predecessor_host, :host do
      description "The host of the performance's predecessor contest."
      resolve &ShowtimeResolver.predecessor_host/3
    end

    field :appearances, non_null(list_of(non_null(:appearance))) do
      description "The performance's appearances."
      resolve &ShowtimeResolver.appearances/3
    end

    field :pieces, non_null(list_of(non_null(:piece))) do
      description "The performance's pieces."
      resolve dataloader(Showtime)
    end
  end

  object :piece do
    field :id, non_null(:id)

    field :person_info, non_null(:string) do
      description """
      For classical pieces, this contains the name and biographical dates of the piece's composer.
      For popular pieces, the artist name is returned.
      """

      resolve &ShowtimeResolver.person_info/3
    end

    field :title, non_null(:string) do
      description "The title of the piece."
    end
  end

  object :result do
    field :points, non_null(:integer) do
      description "The points awarded to this appearance."
    end

    field :prize, :string do
      description "The prize corresponding to the appearance's points."
    end

    field :advances, non_null(:boolean) do
      description "Whether the participant will advance to the next round with this appearance."
    end
  end

  object :stage do
    field :id, non_null(:id)

    field :name, non_null(:string) do
      description "The public name of the stage."
    end
  end

  input_object :performance_filter do
    field :contest_category_id, :id do
      description "The ID of the performances' contest category."
    end

    field :results_public, :boolean do
      description "Whether the performances' results are public."
    end

    field :stage_date, :date do
      description "The date on which the performances are scheduled."
    end

    field :stage_id, :id do
      description "The ID of the stage on which the performances happen."
    end
  end
end

defmodule JumubaseWeb.Schema.Objects do
  use Absinthe.Schema.Notation
  import Absinthe.Resolution.Helpers, only: [dataloader: 1, dataloader: 3]
  alias Jumubase.Foundation
  alias Jumubase.Showtime
  alias JumubaseWeb.{FoundationResolver, ShowtimeResolver}

  object :appearance do
    field :id, non_null(:id)

    field :participant_name, non_null(:string) do
      description "The full name of the appearance's participant."
      resolve &ShowtimeResolver.participant_name/3
    end

    field :instrument_name, non_null(:string) do
      description "The name of the participant's instrument in this appearance."
      resolve &ShowtimeResolver.instrument_name/3
    end

    field :is_accompanist, non_null(:boolean) do
      description "Whether the appearance's participant has an accompanist role."
      resolve &ShowtimeResolver.accompanist?/3
    end

    field :result, :result do
      description "The appearance's result, if publicly available."

      resolve dataloader(Showtime, :performance,
                args: %{scope: :result},
                callback: &ShowtimeResolver.result/3
              )
    end
  end

  object :contest do
    field :id, non_null(:id)

    field :name, non_null(:string) do
      description "The contest’s name containing the round, year and host."
      resolve &FoundationResolver.name/3
    end

    field :country_code, non_null(:string) do
      description "The country code of the contest's host."
      resolve &FoundationResolver.country_code/3
    end

    field :dates, non_null(list_of(non_null(:date))) do
      description "The dates on which the contest is happening."
      resolve &FoundationResolver.dates/3
    end

    field :stages, non_null(list_of(non_null(:stage))) do
      description "The stages used in this contest."
      resolve &FoundationResolver.stages/3
    end
  end

  object :contest_category do
    field :id, non_null(:id)

    field :name, non_null(:string) do
      description "The contest category's name."
      resolve &FoundationResolver.name/3
    end

    field :public_result_count, non_null(:integer) do
      description "The amount of performances with public results in this contest category."

      resolve dataloader(Foundation, :performances,
                callback: &FoundationResolver.public_result_count/3
              )
    end
  end

  object :host do
    field :id, non_null(:id)

    field :name, non_null(:string) do
      description "The name of the host."
    end

    field :country_code, non_null(:string) do
      description "The country code of the host."
    end
  end

  object :performance do
    field :id, non_null(:id)

    field :stage_date, non_null(:date) do
      description "The scheduled date of the performance."
      resolve &ShowtimeResolver.stage_date/3
    end

    field :stage_time, non_null(:time) do
      description "The scheduled wall time of the performance."
      resolve &ShowtimeResolver.stage_time/3
    end

    field :category_name, non_null(:string) do
      description "The name of the performance's category."
      resolve &ShowtimeResolver.category_name/3
    end

    field :age_group, non_null(:string) do
      description "The performance's age group."
    end

    field :predecessor_host, :host do
      description "The host of the performance's predecessor contest."
      resolve &ShowtimeResolver.predecessor_host/3
    end

    field :appearances, non_null(list_of(non_null(:appearance))) do
      description "The performance's appearances."
      resolve &ShowtimeResolver.appearances/3
    end

    field :pieces, non_null(list_of(non_null(:piece))) do
      description "The performance's pieces."
      resolve dataloader(Showtime)
    end
  end

  object :piece do
    field :id, non_null(:id)

    field :person_info, non_null(:string) do
      description """
      For classical pieces, this contains the name and biographical dates of the piece's composer.
      For popular pieces, the artist name is returned.
      """

      resolve &ShowtimeResolver.person_info/3
    end

    field :title, non_null(:string) do
      description "The title of the piece."
    end
  end

  object :result do
    field :points, non_null(:integer) do
      description "The points awarded to this appearance."
    end

    field :prize, :string do
      description "The prize corresponding to the appearance's points."
    end

    field :advances, non_null(:boolean) do
      description "Whether the participant will advance to the next round with this appearance."
    end
  end

  object :stage do
    field :id, non_null(:id)

    field :name, non_null(:string) do
      description "The public name of the stage."
    end
  end

  input_object :performance_filter do
    field :contest_category_id, :id do
      description "The ID of the performances' contest category."
    end

    field :results_public, :boolean do
      description "Whether the performances' results are public."
    end

    field :stage_date, :date do
      description "The date on which the performances are scheduled."
    end

    field :stage_id, :id do
      description "The ID of the stage on which the performances happen."
    end
  end
end