zeroclaw-labs/zeroclaw · warning

state must be 'on' or 'off'

Error message

state must be 'on' or 'off'

What it means

The SmartRoom peripheral's execute maps a device state command to gpio_write; only the exact lowercase strings "on" and "off" are accepted, mapping to values 1 and 0. Any other state string — "ON", "1", "true", "toggle", or values with surrounding whitespace — bails before any transport request is sent.

Source

Thrown at crates/zeroclaw-hardware/src/peripherals/smartroom.rs:84

    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let device = args
            .get("device")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::Error::msg("missing device"))?;

        let state = args
            .get("state")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::Error::msg("missing state"))?;

        let pin = output_pin(device)
            .ok_or_else(|| anyhow::Error::msg(format!("unknown output device: {}", device)))?;

        let value = match state {
            "on" => 1,
            "off" => 0,
            _ => anyhow::bail!("state must be 'on' or 'off'"),
        };

        let result = self
            .transport
            .request("gpio_write", json!({ "pin": pin, "value": value }))
            .await?;

        Ok(result)
    }
}

/// Tool: read a smart-room input device (currently only motion_sensor).
pub struct ReadDeviceTool {
    pub transport: Arc<SerialTransport>,
}

#[async_trait]
impl Tool for ReadDeviceTool {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Pass exactly "on" or "off" — lowercase, no whitespace
  2. Normalize before calling: trim and lowercase the state string, mapping synonyms like 1/true to "on"
  3. Validate upstream against an enum and reject unknown values with your own error before reaching the peripheral

Example fix

// before
let state = raw_input; // "ON", "1", " on"...
room.execute(device, state).await?;

// after
let state = match raw_input.trim().to_lowercase().as_str() {
    "1" | "true" => "on",
    "0" | "false" => "off",
    s => s,
};
if state != "on" && state != "off" {
    anyhow::bail!("state must be 'on' or 'off'");
}
room.execute(device, state).await?;
Defensive patterns

Strategy: type-guard

Validate before calling

let state = state.trim().to_lowercase();
if !is_valid_state(&state) {
    anyhow::bail!("state must be 'on' or 'off', got {state:?}");
}
room.execute(device, &state).await?;

Type guard

fn is_valid_state(state: &str) -> bool {
    matches!(state.trim().to_lowercase().as_str(), "on" | "off")
}

Try / catch

match room.execute(device, &state).await {
    Err(e) if format!("{e}").contains("state must be 'on' or 'off'") => {
        // pure input rejection: normalize and retry, never a device fault
    }
    rest => rest,
}

Prevention

When it happens

Trigger: Calling the smartroom execute with state = "ON" (case differs), "1" or "true" (synonyms), or " on" (whitespace) — the match arms compare the raw string with no normalization.

Common situations: Agent/LLM tool callers passing user-typed values verbatim; configs copied from Home Assistant-style integrations that accept on/off/true/false; capitalized or localized input.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/7ab373baec853676. Report an issue: GitHub.