farmbot_os/farmbot_core/lib/farmbot_core/asset.ex

731 lines
18 KiB
Elixir

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