pamoja_dashboard/command.rs
1//! The control contract: the authenticated actions a client can ask a node to take.
2//!
3//! Reading the dashboard needs no command; changing the node does. A [`Command`] arrives
4//! over the authenticated `POST /command` path (see the serving layer) and is dispatched
5//! to the [`StateSource`](crate::StateSource), which is the only thing that can move an
6//! actuator or change the fleet. The wire form is a serde-tagged object, so the page
7//! sends `{"type":"actuate", ...}`.
8
9use serde::Deserialize;
10
11use crate::state::{Group, Sensor};
12
13/// A control action a client asks the node to take.
14///
15/// The provisioning variants carry the group or sensor the client built, so the device
16/// records the structure the operator described; the device owns and shares it across
17/// every client.
18#[derive(Debug, Clone, Deserialize)]
19#[serde(tag = "type", rename_all = "camelCase")]
20pub enum Command {
21 /// Set a discrete actuator to one of its actions, such as opening a valve.
22 Actuate {
23 /// The actuator's `"groupId/sensorId"` path.
24 target: String,
25 /// The action to apply, one of the reading's advertised actions (`"open"`).
26 action: String,
27 },
28 /// Add a group to an organization.
29 AddGroup {
30 /// The organization's id.
31 org: String,
32 /// The group to add.
33 group: Group,
34 },
35 /// Remove a group by id.
36 RemoveGroup {
37 /// The group's id.
38 id: String,
39 },
40 /// Add a sensor to a group.
41 AddSensor {
42 /// The group's id.
43 group: String,
44 /// The sensor to add.
45 sensor: Sensor,
46 /// An optional gateway-defined hardware binding the device uses to find the sensor,
47 /// such as `"i2c:0x76"`, `"gpio:4"`, or `"lora:ab12"`. The dashboard only carries it
48 /// through; binding a real driver is the gateway's job when it drains the command.
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 binding: Option<String>,
51 },
52 /// Remove a sensor by its `"groupId/sensorId"` path.
53 RemoveSensor {
54 /// The sensor's `"groupId/sensorId"` path.
55 target: String,
56 },
57}
58
59/// Why a command could not be carried out. The [`code`](CommandError::code) is a stable,
60/// language-neutral string the page localizes.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum CommandError {
63 /// The source does not handle this kind of command.
64 Unsupported,
65 /// No actuator or target matches the command.
66 UnknownTarget,
67 /// The target exists but does not accept the requested action.
68 InvalidAction,
69 /// The sensor being added is not one this device supports, so it was not added. A real
70 /// device only accepts the sensor types it can actually bind a driver to.
71 UnknownSensor,
72}
73
74impl CommandError {
75 /// Returns the stable error code for this failure.
76 ///
77 /// # Returns
78 ///
79 /// A dotted, language-neutral code such as `"command.unknown_target"`.
80 pub fn code(self) -> &'static str {
81 match self {
82 CommandError::Unsupported => "command.unsupported",
83 CommandError::UnknownTarget => "command.unknown_target",
84 CommandError::InvalidAction => "command.invalid_action",
85 CommandError::UnknownSensor => "command.unknown_sensor",
86 }
87 }
88}