dahua-camera-server 0.3.1

axum HTTP, SSE and WebSocket API for the dahua-camera stack
Documentation
//! Alarm lifecycle over HTTP: what is active, what happened, acknowledge.
//!
//! # ⚠ Safety
//!
//! Alarms served here come from camera IVS/SMD analytics. They are an
//! **operator aid** and are **not safety-rated**: no SIL level, no
//! EN 50128 / EN 50129 assessment. They degrade in fog, rain, backlight and at
//! range.
//!
//! **An empty active-alarm list means nothing.** It is equally consistent with
//! "nothing is happening", "the camera is blind", "the event link is down" and
//! "the rule was never configured". Never drive an interlock, a permissive, or
//! a machine-motion condition from this endpoint. See `SAFETY.md`.

use crate::state::{now_unix_ms, AppState};
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::Json;
use dahua_camera_alarms::Alarm;
use serde::Serialize;

/// Body of `GET /api/alarms`.
#[derive(Debug, Serialize)]
pub struct AlarmsView {
    /// Conditions currently latched, oldest first.
    pub active: Vec<Alarm>,
    /// Conditions that have cleared, oldest first.
    pub history: Vec<Alarm>,
    /// Restated on every response so a client cannot show this data without
    /// the caveat having been available to it.
    pub safety_notice: &'static str,
}

/// The notice carried on every alarm response.
pub const SAFETY_NOTICE: &str = "Camera IVS/SMD detection is an operator aid. It is NOT \
     safety-rated (no SIL, no EN 50128/50129 assessment) and degrades in fog, rain, backlight \
     and at range. It MUST NOT be used as an interlock, a permissive, or a condition for \
     machine motion. Absence of an alarm means nothing.";

/// `GET /api/alarms` — active and historical alarms.
pub async fn list_alarms(State(state): State<AppState>) -> Json<AlarmsView> {
    let now = now_unix_ms();
    let mut registry = state.alarms.lock().expect("alarm registry mutex");

    Json(AlarmsView {
        active: registry.active(now),
        history: registry.history().to_vec(),
        safety_notice: SAFETY_NOTICE,
    })
}

/// `POST /api/alarms/{id}/ack` — acknowledge one alarm.
///
/// Acknowledging records that an operator has *seen* the alarm. It does not
/// clear an ongoing condition, because those are different facts.
pub async fn acknowledge(
    State(state): State<AppState>,
    Path(id): Path<u64>,
) -> Result<StatusCode, StatusCode> {
    let mut registry = state.alarms.lock().expect("alarm registry mutex");

    if registry.acknowledge(id) {
        Ok(StatusCode::NO_CONTENT)
    } else {
        Err(StatusCode::NOT_FOUND)
    }
}

/// `POST /api/alarms/ack` — acknowledge everything.
pub async fn acknowledge_all(State(state): State<AppState>) -> Json<usize> {
    let mut registry = state.alarms.lock().expect("alarm registry mutex");
    Json(registry.acknowledge_all())
}