Skip to main content

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}