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", ...}` for a discrete actuator and `{"type":"set", ...}` for
8//! a numeric one.
9
10use serde::Deserialize;
11
12use crate::state::{Group, Sensor};
13
14/// A control action a client asks the node to take.
15///
16/// The provisioning variants carry the group or sensor the client built, so the device
17/// records the structure the operator described; the device owns and shares it across
18/// every client.
19// A command is decoded once per request and queued a handful at a time, so the sensor
20// the add variant carries stays inline rather than boxed for the wire type's sake.
21#[allow(clippy::large_enum_variant)]
22#[derive(Debug, Clone, Deserialize)]
23#[serde(tag = "type", rename_all = "camelCase")]
24pub enum Command {
25    /// Set a discrete actuator to one of its actions, such as opening a valve.
26    Actuate {
27        /// The actuator's `"groupId/sensorId"` path.
28        target: String,
29        /// The action to apply, one of the reading's advertised actions (`"open"`).
30        action: String,
31    },
32    /// Set a numeric actuator to a value inside its advertised range, such as a pump
33    /// speed or a setpoint.
34    Set {
35        /// The actuator's `"groupId/sensorId"` path.
36        target: String,
37        /// The value to apply, in the reading's unit and inside its
38        /// [`range`](crate::Reading::range).
39        value: f32,
40    },
41    /// Add a group to an organization.
42    AddGroup {
43        /// The organization's id.
44        org: String,
45        /// The group to add.
46        group: Group,
47    },
48    /// Remove a group by id.
49    RemoveGroup {
50        /// The group's id.
51        id: String,
52    },
53    /// Add a sensor to a group.
54    AddSensor {
55        /// The group's id.
56        group: String,
57        /// The sensor to add.
58        sensor: Sensor,
59        /// An optional gateway-defined hardware binding the device uses to find the sensor,
60        /// such as `"i2c:0x76"`, `"gpio:4"`, or `"lora:ab12"`. The dashboard only carries it
61        /// through; binding a real driver is the gateway's job when it drains the command.
62        #[serde(default, skip_serializing_if = "Option::is_none")]
63        binding: Option<String>,
64    },
65    /// Remove a sensor by its `"groupId/sensorId"` path.
66    RemoveSensor {
67        /// The sensor's `"groupId/sensorId"` path.
68        target: String,
69    },
70}
71
72/// Why a command could not be carried out. The [`code`](CommandError::code) is a stable,
73/// language-neutral string the page localizes.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum CommandError {
76    /// The source does not handle this kind of command.
77    Unsupported,
78    /// No actuator or target matches the command.
79    UnknownTarget,
80    /// The target exists but does not accept the requested action.
81    InvalidAction,
82    /// The value lies outside the range the target advertises.
83    OutOfRange,
84    /// The sensor being added is not one this device supports, so it was not added. A real
85    /// device only accepts the sensor types it can actually bind a driver to.
86    UnknownSensor,
87}
88
89impl CommandError {
90    /// Returns the stable error code for this failure.
91    ///
92    /// # Returns
93    ///
94    /// A dotted, language-neutral code such as `"command.unknown_target"`.
95    pub fn code(self) -> &'static str {
96        match self {
97            CommandError::Unsupported => "command.unsupported",
98            CommandError::UnknownTarget => "command.unknown_target",
99            CommandError::InvalidAction => "command.invalid_action",
100            CommandError::OutOfRange => "command.out_of_range",
101            CommandError::UnknownSensor => "command.unknown_sensor",
102        }
103    }
104}