mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-24 19:46:17 -05:00
more flexible signals
Now there's room to add arbitrary configuration to signals and types. Several things are no longer fixed columns/tables but instead within the configuration types.
This commit is contained in:
@@ -3,12 +3,44 @@
|
||||
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
||||
|
||||
//! JSON types for use in the database schema. See references from `schema.sql`.
|
||||
//!
|
||||
//! In general, every table in the database with reasonably low expected row
|
||||
//! count should have a JSON config column. This allows the schema to be
|
||||
//! modified without a major migration.
|
||||
//!
|
||||
//! JSON should be avoided for very high-row-count tables (eg `reocrding`) for
|
||||
//! storage efficiency, in favor of separate columns or a binary type.
|
||||
//! (Currently protobuf is used within `user_session`. A future schema version
|
||||
//! might switch to a more JSON-like binary format to minimize impedance
|
||||
//! mismatch.)
|
||||
//!
|
||||
//! JSON types should be designed for extensibility with forward and backward
|
||||
//! compatibility:
|
||||
//!
|
||||
//! * Every struct has a flattened `unknown` so that if an unknown attribute is
|
||||
//! written with a newer version of the binary, then the config is saved
|
||||
//! (read and re-written) with an older version, the value will be
|
||||
//! preserved.
|
||||
//! * If a field is only for use by the UI and there's no need for the server
|
||||
//! to constrain it, leave it in `unknown`.
|
||||
//! * Fields shouldn't use closed enumerations or other restrictive types,
|
||||
//! so that parsing the config with a non-understood value will not fail. If
|
||||
//! the behavior of unknown values is not obvious, it should be clarified
|
||||
//! via a comment.
|
||||
//! * Fields should generally parse without values, via `#[serde(default)]`,
|
||||
//! so that they can be removed in a future version if they no longer make
|
||||
//! sense. It also makes sense to avoid serializing them when empty.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rusqlite::types::{FromSqlError, ValueRef};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Serializes and deserializes JSON as a SQLite3 `text` column, compatible with the
|
||||
/// [JSON1 extension](https://www.sqlite.org/json1.html).
|
||||
macro_rules! sql {
|
||||
($l:ident) => {
|
||||
impl rusqlite::types::FromSql for $l {
|
||||
@@ -18,6 +50,7 @@ macro_rules! sql {
|
||||
Ok(serde_json::from_slice(t)
|
||||
.map_err(|e| FromSqlError::Other(Box::new(e)))?)
|
||||
}
|
||||
ValueRef::Null => Ok($l::default()),
|
||||
_ => Err(FromSqlError::InvalidType),
|
||||
}
|
||||
}
|
||||
@@ -33,24 +66,98 @@ macro_rules! sql {
|
||||
};
|
||||
}
|
||||
|
||||
/// Global configuration, used in the `config` column of the `meta` table.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GlobalConfig {
|
||||
/// The maximum number of entries in the `signal_state` table (or `None` for unlimited).
|
||||
///
|
||||
/// If an update causes this to be exceeded, older times will be garbage
|
||||
/// collected to stay within the limit.
|
||||
pub max_signal_changes: Option<u32>,
|
||||
|
||||
/// Information about signal types.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub signal_types: BTreeMap<Uuid, SignalTypeConfig>,
|
||||
|
||||
/// Information about signals.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub signals: BTreeMap<u32, SignalConfig>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub unknown: Map<String, Value>,
|
||||
}
|
||||
sql!(GlobalConfig);
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignalTypeConfig {
|
||||
/// Information about possible enumeration values of this signal type.
|
||||
///
|
||||
/// 0 always means `unknown`. Other values may be specified here to set
|
||||
/// their configuration. It's more efficient in terms of encoded length
|
||||
/// and RAM at runtime for common values (eg, `still`, `normal`, or
|
||||
/// `disarmed`) to be numerically lower than rarer values (eg `motion`,
|
||||
/// `violated`, or `armed`) and for the value space to be dense (eg, to use
|
||||
/// values 1, 2, 3 rather than 1, 10, 20).
|
||||
///
|
||||
/// Currently values must be in the range `[0, 16)`.
|
||||
///
|
||||
/// Nothing enforces that only values specified here may be set for a signal
|
||||
/// of this type.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub values: BTreeMap<u8, SignalTypeValueConfig>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub unknown: Map<String, Value>,
|
||||
}
|
||||
sql!(SignalTypeConfig);
|
||||
|
||||
/// Information about a signal type value; used in `SignalTypeConfig::values`.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignalTypeValueConfig {
|
||||
pub name: String,
|
||||
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub motion: bool,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub color: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub unknown: Map<String, Value>,
|
||||
}
|
||||
|
||||
impl SignalTypeValueConfig {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.unknown.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Camera configuration, used in the `config` column of the `camera` table.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CameraConfig {
|
||||
/// A short description of the camera.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub description: String,
|
||||
|
||||
/// The base URL for accessing ONVIF; `device_service` will be joined on
|
||||
/// automatically to form the device management service URL.
|
||||
/// Eg with `onvif_base=http://192.168.1.110:85`, the full
|
||||
/// URL of the devie management service will be
|
||||
/// URL of the device management service will be
|
||||
/// `http://192.168.1.110:85/device_service`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub onvif_base_url: Option<Url>,
|
||||
|
||||
/// The username to use when accessing the camera.
|
||||
/// If empty, no username or password will be supplied.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub username: String,
|
||||
|
||||
/// The password to use when accessing the camera.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub password: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
@@ -68,6 +175,7 @@ impl CameraConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream configuration, used in the `config` column of the `stream` table.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StreamConfig {
|
||||
@@ -75,7 +183,7 @@ pub struct StreamConfig {
|
||||
///
|
||||
/// Null means entirely disabled. At present, so does any value other than
|
||||
/// `record`.
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub mode: String,
|
||||
|
||||
/// The `rtsp://` URL to use for this stream, excluding username and
|
||||
@@ -86,7 +194,7 @@ pub struct StreamConfig {
|
||||
/// protocol](https://github.com/thirtythreeforty/neolink).
|
||||
///
|
||||
/// (Credentials are taken from [`CameraConfig`]'s respective fields.)
|
||||
// TODO: should this really be Option?
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<Url>,
|
||||
|
||||
/// The number of bytes of video to retain, excluding the
|
||||
@@ -126,3 +234,35 @@ impl StreamConfig {
|
||||
&& self.unknown.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Signal configuration, used in the `config` column of the `signal` table.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignalConfig {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub short_name: String,
|
||||
|
||||
/// Map of associated cameras to the type of association.
|
||||
///
|
||||
/// `direct` is as if the event source is the camera's own motion detection.
|
||||
/// Here are a couple ways this could be used:
|
||||
///
|
||||
/// * when viewing the camera, hotkeys to go to the start of the next or
|
||||
/// previous event should respect this event.
|
||||
/// * a list of events might include the recordings associated with the
|
||||
/// camera in the same timespan.
|
||||
///
|
||||
/// `indirect` might mean a screen associated with the camera should given
|
||||
/// some indication of this event, but there should be no assumption that
|
||||
/// the camera will have a direct view of the event. For example, all
|
||||
/// cameras might be indirectly associated with a doorknob press. Cameras
|
||||
/// at the back of the house shouldn't be expected to have a direct view of
|
||||
/// this event, but motion events shortly afterward might warrant extra
|
||||
/// scrutiny.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub camera_associations: BTreeMap<i32, String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub unknown: Map<String, Value>,
|
||||
}
|
||||
sql!(SignalConfig);
|
||||
|
||||
Reference in New Issue
Block a user