defmodule FarmbotCore.Asset do @moduledoc """ Top level module, with some helpers. Persists application resources to disk. Submodules of this module usually (but not always) correspond to a resource in the REST API. See official REST API docs for details. """ alias FarmbotCore.Asset.{ CriteriaRetriever, Device, DeviceCert, FarmEvent, FarmwareEnv, FarmwareInstallation, FbosConfig, FirmwareConfig, FirstPartyFarmware, Peripheral, PinBinding, Point, PointGroup, PublicKey, Regimen, RegimenInstance, Repo, Sensor, SensorReading, Sequence, Tool, } alias FarmbotCore.AssetSupervisor import Ecto.Query require Logger ## Begin Device def device() do Repo.one(Device) || %Device{} end def device(field) do Map.fetch!(device(), field) end def update_device!(params) do device() |> Device.changeset(params) |> Repo.insert_or_update!() end def delete_device!(id) do if device = Repo.get_by(Device, id: id) do Repo.delete!(device) end :ok end ## End Device ## Begin FarmEvent def new_farm_event!(params) do %FarmEvent{} |> FarmEvent.changeset(params) |> Repo.insert!() end @doc "Returns a FarmEvent by its API id." def get_farm_event(id) do Repo.get_by(FarmEvent, id: id) end def update_farm_event!(farm_event, params) do farm_event = farm_event |> FarmEvent.changeset(params) |> Repo.update!() if farm_event.executable_type == "Regimen" do regimen_instance = get_regimen_instance(farm_event) if regimen_instance do regimen_instance |> Repo.preload([:farm_event, :regimen]) |> RegimenInstance.changeset(%{updated_at: DateTime.utc_now()}) |> Repo.update!() end end farm_event end def delete_farm_event!(farm_event) do ri = get_regimen_instance(farm_event) ri && Repo.delete!(ri) Repo.delete!(farm_event) end def add_execution_to_farm_event!(%FarmEvent{} = farm_event, params \\ %{}) do %FarmEvent.Execution{} |> FarmEvent.Execution.changeset(params) |> Ecto.Changeset.put_assoc(:farm_event, farm_event) |> Repo.insert!() end def get_farm_event_execution(%FarmEvent{} = farm_event, scheduled_at) do Repo.one( from(e in FarmEvent.Execution, where: e.farm_event_local_id == ^farm_event.local_id and e.scheduled_at == ^scheduled_at ) ) end ## End FarmEvent ## Begin FbosConfig @doc "Gets the local config" def fbos_config() do Repo.one(FbosConfig) || %FbosConfig{} end @doc "Gets a field on the local config." def fbos_config(field) do Map.fetch!(fbos_config(), field) end @doc """ This function updates Farmbot OS's local database. It will **NOT** send any HTTP requests to the API. To do this, `FarmbotCore.Asset.Private.mark_dirty!/2` is almost certainly what you want. """ def update_fbos_config!(fbos_config \\ nil, params) do new_data = FbosConfig.changeset(fbos_config || fbos_config(), params) |> Repo.insert_or_update!() AssetSupervisor.cast_child(new_data, {:new_data, new_data}) new_data end def delete_fbos_config!(id) do if fbos_config = Repo.get_by(FbosConfig, id: id) do Repo.delete!(fbos_config) end :ok end ## End FbosConfig ## Begin FirmwareConfig def firmware_config() do Repo.one(FirmwareConfig) || %FirmwareConfig{} end def firmware_config(field) do Map.fetch!(firmware_config(), field) end def update_firmware_config!(firmware_config \\ nil, params) do new_data = FirmwareConfig.changeset(firmware_config || firmware_config(), params) |> Repo.insert_or_update!() AssetSupervisor.cast_child(new_data, {:new_data, new_data}) new_data end def delete_firmware_config!(id) do if firmware_config = Repo.get_by(FirmwareConfig, id: id) do Repo.delete!(firmware_config) end :ok end ## End FirmwareConfig ## Begin RegimenInstance @doc "returns every regimen instance" def list_regimen_instances() do RegimenInstance |> Repo.all() |> Repo.preload([:regimen, :farm_event]) end def get_regimen_instance(%FarmEvent{} = farm_event) do regimen = Repo.one(from(r in Regimen, where: r.id == ^farm_event.executable_id)) regimen && Repo.one( from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id and ri.farm_event_id == ^farm_event.local_id ) ) end def new_regimen_instance!(%FarmEvent{} = farm_event, params \\ %{}) do regimen = Repo.one!(from(r in Regimen, where: r.id == ^farm_event.executable_id)) RegimenInstance.changeset(%RegimenInstance{}, params) |> Ecto.Changeset.put_assoc(:regimen, regimen) |> Ecto.Changeset.put_assoc(:farm_event, farm_event) |> Repo.insert!() end def delete_regimen_instance!(%RegimenInstance{} = ri) do Repo.delete!(ri) end def add_execution_to_regimen_instance!(%RegimenInstance{} = ri, params \\ %{}) do %RegimenInstance.Execution{} |> RegimenInstance.Execution.changeset(params) |> Ecto.Changeset.put_assoc(:regimen_instance, ri) |> Repo.insert!() end def get_regimen_instance_execution(%RegimenInstance{} = ri, scheduled_at) do Repo.one( from(e in RegimenInstance.Execution, where: e.regimen_instance_local_id == ^ri.local_id and e.scheduled_at == ^scheduled_at ) ) end ## End RegimenInstance ## Begin PinBinding @doc "Lists all available pin bindings" def list_pin_bindings do Repo.all(PinBinding) end ## End PinBinding ## Begin Point def get_point(params) do Repo.get_by(Point, params) end def update_point(point, params) do # TODO: RC 8 MAY 2020 - We need to hard refresh the point. # The CSVM appears to be caching resources. This leads # to problems when a user runs a sequence that has two # MARK AS steps. # NOTE: Updating the `meta` attribute is a _replace_ action # by default, not a merge action. # MORE NOTES: Mixed keys (symbol vs. string) will crash this FN. # Let's just stringify everything... new_meta = params[:meta] || params["meta"] || %{} old_meta = point.meta || %{} updated_meta = Map.merge(old_meta, new_meta) clean_params = params |> Map.merge(%{meta: updated_meta}) |> Enum.map(fn {k, v} -> {"#{k}", v} end) |> Map.new() Repo.get_by(Point, id: point.id) |> Point.changeset(clean_params) |> Repo.update() end @doc "Returns all points matching Point.pointer_type" def get_all_points_by_type(type, order_by \\ "random") do from(p in Point, where: p.pointer_type == ^type and is_nil(p.discarded_at)) |> Repo.all() |> sort_points(order_by) end def sort_points(points, order_by) do points |> Enum.group_by(&group_points_by(&1, order_by)) |> Enum.sort(&group_sort(&1, &2, order_by)) |> Enum.map(fn {_group_index, group} -> Enum.sort(group, &sort_points(&1, &2, order_by)) end) |> List.flatten() end def group_points_by(%{x: x}, algo) when algo in ~w(xy_ascending xy_descending), do: x def group_points_by(%{y: y}, algo) when algo in ~w(yx_ascending yx_descending), do: y def group_points_by(%{x: x, y: y}, "random"), do: Enum.random([x, y]) def group_sort({lgroup, _}, {rgroup, _}, "xy_ascending"), do: lgroup <= rgroup def group_sort({lgroup, _}, {rgroup, _}, "yx_ascending"), do: lgroup <= rgroup def group_sort({lgroup, _}, {rgroup, _}, "xy_descending"), do: lgroup >= rgroup def group_sort({lgroup, _}, {rgroup, _}, "yx_descending"), do: lgroup >= rgroup def group_sort(_, _, "random"), do: Enum.random([true, false]) def sort_points(%{y: ly}, %{y: ry}, "xy_ascending"), do: ly <= ry def sort_points(%{y: ly}, %{y: ry}, "xy_descending"), do: ly >= ry def sort_points(%{x: lx}, %{x: rx}, "yx_ascending"), do: lx <= rx def sort_points(%{x: lx}, %{x: rx}, "yx_descending"), do: lx >= rx def sort_points(_, _, "random"), do: Enum.random([true, false]) ## End Point ## Begin PointGroup def get_point_group(params) do case Repo.get_by(PointGroup, params) do nil -> nil %{sort_type: nil} = group -> group %{point_ids: unsorted, sort_type: sort_by} = point_group -> sorted = Repo.all(from(p in Point, where: p.id in ^unsorted)) |> sort_points(sort_by) |> Enum.map(&Map.fetch!(&1, :id)) %{point_group | point_ids: sorted} end end def find_points_via_group(id) do case Repo.get_by(PointGroup, id: id) do %{id: _id, sort_type: sort_by} = point_group -> # I don't like this because it makes the code # harder to understand. # We are essentially patching the value of # point_group.point_ids with additional IDs. # Keep this in mind when debugging sequences # that deal with point groups- the point_ids # value is not a reflection of what is in # the DB / API. sorted = CriteriaRetriever.run(point_group) |> sort_points(sort_by || "xy_ascending") |> Enum.map(fn point -> point.id end) %{ point_group | point_ids: sorted } other -> # Swallow all other errors a = inspect(id) b = inspect(other) Logger.debug("Unexpected point group #{a} #{b}") nil end end def new_point_group!(params) do %PointGroup{} |> PointGroup.changeset(params) |> Repo.insert!() end def update_point_group!(point_group, params) do updated = point_group |> PointGroup.changeset(params) |> Repo.update!() regimen_instances = list_regimen_instances() farm_events = Repo.all(FarmEvent) # check for any matching asset using this point group. # This is pretty recursive and probably isn't super great # for performance, but SQL can't check this stuff unfortunately. for asset <- farm_events ++ regimen_instances do # TODO(Connor) this might be worth creating a behaviour for if uses_point_group?(asset, point_group) do Logger.debug("#{inspect(asset)} uses PointGroup: #{inspect(point_group)}. Reindexing it.") FarmbotCore.AssetSupervisor.update_child(asset) end end updated end def delete_point_group!(%PointGroup{} = point_group) do Repo.delete!(point_group) end def uses_point_group?(%FarmEvent{body: body}, %PointGroup{id: point_group_id}) do any_body_node_uses_point_group?(body, point_group_id) end def uses_point_group?(%Regimen{body: body, regimen_items: regimen_items}, %PointGroup{ id: point_group_id }) do any_body_node_uses_point_group?(body, point_group_id) || Enum.find(regimen_items, fn %{sequence_id: sequence_id} -> any_body_node_uses_point_group?(get_sequence(sequence_id).body, point_group_id) end) end def uses_point_group?(%RegimenInstance{farm_event: farm_event, regimen: regimen}, point_group) do uses_point_group?(farm_event, point_group) || uses_point_group?(regimen, point_group) end def any_body_node_uses_point_group?(body, point_group_id) do Enum.find(body, fn %{ kind: "execute", body: execute_body } -> any_body_node_uses_point_group?(execute_body, point_group_id) %{ args: %{ "data_value" => %{ "args" => %{"resource_id" => ^point_group_id}, "kind" => "point_group" }, "label" => "parent" }, kind: "parameter_application" } -> true %{ args: %{ "data_value" => %{ "args" => %{"point_group_id" => ^point_group_id}, "kind" => "point_group" }, "label" => "parent" }, kind: "parameter_application" } -> true _ -> false end) end ## End PointGroup ## Begin PublicKey def get_public_key(id) do Repo.get_by(PublicKey, id: id) end def new_public_key!(params) do %PublicKey{} |> PublicKey.changeset(params) |> Repo.insert!() end def update_public_key!(public_key, params) do public_key |> PublicKey.changeset(params) |> Repo.update!() end def delete_public_key!(public_key) do Repo.delete!(public_key) end def new_public_key_from_home!() do public_key_path = Path.join([System.get_env("HOME"), ".ssh", "id_rsa.pub"]) public_key = File.read!(public_key_path) %PublicKey{} |> PublicKey.changeset(%{public_key: public_key}) |> Repo.insert() end def new_public_key_from_string!(public_key) do %PublicKey{} |> PublicKey.changeset(%{public_key: public_key}) |> Repo.insert() end ## End PublicKey ## Begin Regimen @doc "Get a regimen by it's API id" def get_regimen(id) do Repo.get_by(Regimen, id: id) end @doc "Enter a new regimen into the DB" def new_regimen!(params) do %Regimen{} |> Regimen.changeset(params) |> Repo.insert!() end def delete_regimen!(regimen) do regimen_instances = Repo.all(from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id)) for ri <- regimen_instances do IO.puts("deleting regimen instance: #{inspect(ri)}") delete_regimen_instance!(ri) end Repo.delete!(regimen) end @doc "Update an existing regimen" def update_regimen!(regimen, params) do regimen_instances = Repo.all(from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id)) |> Repo.preload([:farm_event, :regimen]) for ri <- regimen_instances do ri |> RegimenInstance.changeset(%{updated_at: DateTime.utc_now()}) |> Repo.update!() end regimen |> Regimen.changeset(params) |> Repo.update!() end ## End Regimen ## Begin Sequence @doc "Get a sequence by it's API id" def get_sequence(id) do Repo.get_by(Sequence, id: id) end def update_sequence!(%Sequence{} = sequence, params \\ %{}) do sequence_id = sequence.id farm_events = Repo.all( from(f in FarmEvent, where: f.executable_type == "Sequence" and f.executable_id == ^sequence_id ) ) regimen_instances = RegimenInstance |> Repo.all() |> Repo.preload([:regimen, :farm_event]) |> Enum.filter(fn %{regimen: %{regimen_items: items}} -> Enum.find(items, fn %{sequence_id: ^sequence_id} -> true %{sequence_id: _} -> true end) %{regimen: nil} -> false end) for asset <- farm_events ++ regimen_instances do FarmbotCore.AssetSupervisor.update_child(asset) end Sequence.changeset(sequence, params) |> Repo.update!() end def new_sequence!(params \\ %{}) do Sequence.changeset(%Sequence{}, params) |> Repo.insert!() end ## End Sequence ## Begin FarmwareInstallation @doc "Get a FarmwareManifest by it's name." def get_farmware_manifest(package) do first_party_farmwares = Repo.all(from(fwi in FirstPartyFarmware, select: fwi.manifest)) regular_farmwares = Repo.all(from(fwi in FarmwareInstallation, select: fwi.manifest)) Enum.find( first_party_farmwares ++ regular_farmwares, fn %{package: pkg} -> pkg == package _ -> false end ) end def get_farmware_installation(package) do first_party_farmwares = Repo.all(from(fwi in FirstPartyFarmware)) regular_farmwares = Repo.all(from(fwi in FarmwareInstallation)) Enum.find( first_party_farmwares ++ regular_farmwares, fn %{manifest: %{package: pkg}} -> pkg == package _ -> false end ) end def upsert_farmware_manifest_by_id(id, params) do fwi = Repo.get_by(FarmwareInstallation, id: id) || %FarmwareInstallation{} FarmwareInstallation.changeset(fwi, params) |> Repo.insert_or_update() end def upsert_first_party_farmware_manifest_by_id(id, params) do fwi = Repo.get_by(FirstPartyFarmware, id: id) || %FirstPartyFarmware{} FirstPartyFarmware.changeset(fwi, params) |> Repo.insert_or_update() end ## End FarmwareInstallation ## Begin FarmwareEnv def list_farmware_env() do Repo.all(FarmwareEnv) end def upsert_farmware_env_by_id(id, params) do fwe = Repo.get_by(FarmwareEnv, id: id) || %FarmwareEnv{} FarmwareEnv.changeset(fwe, params) |> Repo.insert_or_update() end def new_farmware_env(params) do key = params["key"] || params[:key] fwe = with key when is_binary(key) <- key, [fwe | _] <- Repo.all(from(fwe in FarmwareEnv, where: fwe.key == ^key)) do fwe else _ -> %FarmwareEnv{} end FarmwareEnv.changeset(fwe, params) |> Repo.insert_or_update() end ## End FarmwareEnv ## Begin Peripheral def get_peripheral(args) do Repo.get_by(Peripheral, args) end def get_peripheral_by_pin(pin) do Repo.get_by(Peripheral, pin: pin) end ## End Peripheral ## Begin Sensor def get_sensor(id) do Repo.get_by(Sensor, id: id) end def get_sensor_by_pin(pin) do Repo.get_by(Sensor, pin: pin) end def new_sensor!(params) do Sensor.changeset(%Sensor{}, params) |> Repo.insert!() end def update_sensor!(sensor, params) do sensor |> Sensor.changeset(params) |> Repo.update!() end ## End Sensor ## Begin SensorReading def get_sensor_reading(id) do Repo.get_by(SensorReading, id: id) end def new_sensor_reading!(params) do SensorReading.changeset(%SensorReading{}, params) |> Repo.insert!() end def update_sensor_reading!(sensor_reading, params) do sensor_reading |> SensorReading.changeset(params) |> Repo.update!() end ## End SensorReading ## Begin DeviceCert def new_device_cert(params) do DeviceCert.changeset(%DeviceCert{}, params) |> Repo.insert() end def get_device_cert(args) do Repo.get_by(DeviceCert, args) end def update_device_cert(cert, params) do cert |> DeviceCert.changeset(params) |> Repo.update() end ## End DeviceCert ## Begin Tool def get_tool(args) do Repo.get_by(Tool, args) end ## End Tool end