mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-27 12:53:47 -05:00
read-only signals support (#28)
This is mostly untested and useless by itself, but it's a starting point. In particular: * there's no way to set up signals or add/remove/update events yet except by manual changes to the database. * if you associate a signal with a camera then remove the camera, hitting /api/ will error out.
This commit is contained in:
135
src/json.rs
135
src/json.rs
@@ -31,7 +31,7 @@
|
||||
use db::auth::SessionHash;
|
||||
use failure::{Error, format_err};
|
||||
use serde::Serialize;
|
||||
use serde::ser::{SerializeMap, SerializeSeq, Serializer};
|
||||
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Not;
|
||||
use uuid::Uuid;
|
||||
@@ -46,7 +46,14 @@ pub struct TopLevel<'a> {
|
||||
#[serde(serialize_with = "TopLevel::serialize_cameras")]
|
||||
pub cameras: (&'a db::LockedDatabase, bool),
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session: Option<Session>,
|
||||
|
||||
#[serde(serialize_with = "TopLevel::serialize_signals")]
|
||||
pub signals: (&'a db::LockedDatabase, bool),
|
||||
|
||||
#[serde(serialize_with = "TopLevel::serialize_signal_types")]
|
||||
pub signal_types: &'a db::LockedDatabase,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -94,6 +101,44 @@ pub struct Stream<'a> {
|
||||
pub days: Option<&'a BTreeMap<db::StreamDayKey, db::StreamDayValue>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all="camelCase")]
|
||||
pub struct Signal<'a> {
|
||||
#[serde(serialize_with = "Signal::serialize_cameras")]
|
||||
pub cameras: (&'a db::Signal, &'a db::LockedDatabase),
|
||||
pub source: Uuid,
|
||||
pub type_: Uuid,
|
||||
pub short_name: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
#[serde(rename_all="camelCase")]
|
||||
pub struct Signals {
|
||||
pub times_90k: Vec<i64>,
|
||||
pub signal_ids: Vec<u32>,
|
||||
pub states: Vec<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all="camelCase")]
|
||||
pub struct SignalType<'a> {
|
||||
pub uuid: Uuid,
|
||||
|
||||
#[serde(serialize_with = "SignalType::serialize_states")]
|
||||
pub states: &'a db::signal::Type,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all="camelCase")]
|
||||
pub struct SignalTypeState<'a> {
|
||||
value: u16,
|
||||
name: &'a str,
|
||||
|
||||
#[serde(skip_serializing_if = "Not::not")]
|
||||
motion: bool,
|
||||
color: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Camera<'a> {
|
||||
pub fn wrap(c: &'a db::Camera, db: &'a db::LockedDatabase, include_days: bool) -> Result<Self, Error> {
|
||||
Ok(Camera {
|
||||
@@ -158,6 +203,66 @@ impl<'a> Stream<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Signal<'a> {
|
||||
pub fn wrap(s: &'a db::Signal, db: &'a db::LockedDatabase, _include_days: bool) -> Self {
|
||||
Signal {
|
||||
cameras: (s, db),
|
||||
source: s.source,
|
||||
type_: s.type_,
|
||||
short_name: &s.short_name,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_cameras<S>(cameras: &(&db::Signal, &db::LockedDatabase),
|
||||
serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
let (s, db) = cameras;
|
||||
let mut map = serializer.serialize_map(Some(s.cameras.len()))?;
|
||||
for sc in &s.cameras {
|
||||
let c = db.cameras_by_id()
|
||||
.get(&sc.camera_id)
|
||||
.ok_or_else(|| S::Error::custom(format!("signal has missing camera id {}",
|
||||
sc.camera_id)))?;
|
||||
map.serialize_key(&c.uuid)?;
|
||||
map.serialize_value(match sc.type_ {
|
||||
db::signal::SignalCameraType::Direct => "direct",
|
||||
db::signal::SignalCameraType::Indirect => "indirect",
|
||||
})?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SignalType<'a> {
|
||||
pub fn wrap(uuid: Uuid, type_: &'a db::signal::Type) -> Self {
|
||||
SignalType {
|
||||
uuid,
|
||||
states: type_,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_states<S>(type_: &db::signal::Type,
|
||||
serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
let mut seq = serializer.serialize_seq(Some(type_.states.len()))?;
|
||||
for s in &type_.states {
|
||||
seq.serialize_element(&SignalTypeState::wrap(s))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SignalTypeState<'a> {
|
||||
pub fn wrap(s: &'a db::signal::TypeState) -> Self {
|
||||
SignalTypeState {
|
||||
value: s.value,
|
||||
name: &s.name,
|
||||
motion: s.motion,
|
||||
color: &s.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all="camelCase")]
|
||||
struct StreamDayValue {
|
||||
@@ -175,7 +280,33 @@ impl<'a> TopLevel<'a> {
|
||||
let cs = db.cameras_by_id();
|
||||
let mut seq = serializer.serialize_seq(Some(cs.len()))?;
|
||||
for (_, c) in cs {
|
||||
seq.serialize_element(&Camera::wrap(c, db, include_days).unwrap())?; // TODO: no unwrap.
|
||||
seq.serialize_element(
|
||||
&Camera::wrap(c, db, include_days).map_err(|e| S::Error::custom(e))?)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
/// Serializes signals as a list (rather than a map), optionally including the `days` field.
|
||||
fn serialize_signals<S>(signals: &(&db::LockedDatabase, bool),
|
||||
serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
let (db, include_days) = *signals;
|
||||
let ss = db.signals_by_id();
|
||||
let mut seq = serializer.serialize_seq(Some(ss.len()))?;
|
||||
for (_, s) in ss {
|
||||
seq.serialize_element(&Signal::wrap(s, db, include_days))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
/// Serializes signals as a list (rather than a map), optionally including the `days` field.
|
||||
fn serialize_signal_types<S>(db: &db::LockedDatabase,
|
||||
serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
let ss = db.signal_types_by_uuid();
|
||||
let mut seq = serializer.serialize_seq(Some(ss.len()))?;
|
||||
for (u, t) in ss {
|
||||
seq.serialize_element(&SignalType::wrap(*u, t))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
89
src/web.rs
89
src/web.rs
@@ -74,6 +74,7 @@ enum Path {
|
||||
Request, // "/api/request"
|
||||
InitSegment([u8; 20], bool), // "/api/init/<sha1>.mp4{.txt}"
|
||||
Camera(Uuid), // "/api/cameras/<uuid>/"
|
||||
Signals, // "/api/signals"
|
||||
StreamRecordings(Uuid, db::StreamType), // "/api/cameras/<uuid>/<type>/recordings"
|
||||
StreamViewMp4(Uuid, db::StreamType, bool), // "/api/cameras/<uuid>/<type>/view.mp4{.txt}"
|
||||
StreamViewMp4Segment(Uuid, db::StreamType, bool), // "/api/cameras/<uuid>/<type>/view.m4s{.txt}"
|
||||
@@ -94,9 +95,10 @@ impl Path {
|
||||
return Path::TopLevel;
|
||||
}
|
||||
match path {
|
||||
"/request" => return Path::Request,
|
||||
"/login" => return Path::Login,
|
||||
"/logout" => return Path::Logout,
|
||||
"/request" => return Path::Request,
|
||||
"/signals" => return Path::Signals,
|
||||
_ => {},
|
||||
};
|
||||
if path.starts_with("/init/") {
|
||||
@@ -251,6 +253,16 @@ struct ServiceInner {
|
||||
|
||||
type ResponseResult = Result<Response<Body>, Response<Body>>;
|
||||
|
||||
fn serve_json<T: serde::ser::Serialize>(req: &Request<hyper::Body>, out: &T) -> ResponseResult {
|
||||
let (mut resp, writer) = http_serve::streaming_body(&req).build();
|
||||
resp.headers_mut().insert(header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/json"));
|
||||
if let Some(mut w) = writer {
|
||||
serde_json::to_writer(&mut w, out).map_err(internal_server_err)?;
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
impl ServiceInner {
|
||||
fn top_level(&self, req: &Request<::hyper::Body>, session: Option<json::Session>)
|
||||
-> ResponseResult {
|
||||
@@ -265,34 +277,21 @@ impl ServiceInner {
|
||||
}
|
||||
}
|
||||
|
||||
let (mut resp, writer) = http_serve::streaming_body(&req).build();
|
||||
resp.headers_mut().insert(header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/json"));
|
||||
if let Some(mut w) = writer {
|
||||
let db = self.db.lock();
|
||||
serde_json::to_writer(&mut w, &json::TopLevel {
|
||||
time_zone_name: &self.time_zone_name,
|
||||
cameras: (&db, days),
|
||||
session,
|
||||
}).map_err(internal_server_err)?;
|
||||
}
|
||||
Ok(resp)
|
||||
let db = self.db.lock();
|
||||
serve_json(req, &json::TopLevel {
|
||||
time_zone_name: &self.time_zone_name,
|
||||
cameras: (&db, days),
|
||||
session,
|
||||
signals: (&db, days),
|
||||
signal_types: &db,
|
||||
})
|
||||
}
|
||||
|
||||
fn camera(&self, req: &Request<::hyper::Body>, uuid: Uuid) -> ResponseResult {
|
||||
let (mut resp, writer) = http_serve::streaming_body(&req).build();
|
||||
resp.headers_mut().insert(header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/json"));
|
||||
if let Some(mut w) = writer {
|
||||
let db = self.db.lock();
|
||||
let camera = db.get_camera(uuid)
|
||||
.ok_or_else(|| not_found(format!("no such camera {}", uuid)))?;
|
||||
serde_json::to_writer(
|
||||
&mut w,
|
||||
&json::Camera::wrap(camera, &db, true).map_err(internal_server_err)?
|
||||
).map_err(internal_server_err)?
|
||||
};
|
||||
Ok(resp)
|
||||
let db = self.db.lock();
|
||||
let camera = db.get_camera(uuid)
|
||||
.ok_or_else(|| not_found(format!("no such camera {}", uuid)))?;
|
||||
serve_json(req, &json::Camera::wrap(camera, &db, true).map_err(internal_server_err)?)
|
||||
}
|
||||
|
||||
fn stream_recordings(&self, req: &Request<::hyper::Body>, uuid: Uuid, type_: db::StreamType)
|
||||
@@ -351,13 +350,7 @@ impl ServiceInner {
|
||||
Ok(())
|
||||
}).map_err(internal_server_err)?;
|
||||
}
|
||||
let (mut resp, writer) = http_serve::streaming_body(&req).build();
|
||||
resp.headers_mut().insert(header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/json"));
|
||||
if let Some(mut w) = writer {
|
||||
serde_json::to_writer(&mut w, &out).map_err(internal_server_err)?
|
||||
};
|
||||
Ok(resp)
|
||||
serve_json(req, &out)
|
||||
}
|
||||
|
||||
fn init_segment(&self, sha1: [u8; 20], debug: bool, req: &Request<::hyper::Body>)
|
||||
@@ -636,6 +629,34 @@ impl ServiceInner {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn signals(&self, req: &Request<hyper::Body>) -> ResponseResult {
|
||||
let mut time = recording::Time(i64::min_value()) .. recording::Time(i64::max_value());
|
||||
if let Some(q) = req.uri().query() {
|
||||
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
||||
let (key, value) = (key.borrow(), value.borrow());
|
||||
match key {
|
||||
"startTime90k" => {
|
||||
time.start = recording::Time::parse(value)
|
||||
.map_err(|_| bad_req("unparseable startTime90k"))?
|
||||
},
|
||||
"endTime90k" => {
|
||||
time.end = recording::Time::parse(value)
|
||||
.map_err(|_| bad_req("unparseable endTime90k"))?
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut signals = json::Signals::default();
|
||||
self.db.lock().list_changes_by_time(time, &mut |c: &db::signal::ListStateChangesRow| {
|
||||
signals.times_90k.push(c.when.0);
|
||||
signals.signal_ids.push(c.signal);
|
||||
signals.states.push(c.state);
|
||||
}).map_err(internal_server_err)?;
|
||||
serve_json(req, &signals)
|
||||
}
|
||||
|
||||
fn authenticated(&self, req: &Request<hyper::Body>) -> Result<Option<json::Session>, Error> {
|
||||
if let Some(sid) = extract_sid(req) {
|
||||
let authreq = self.authreq(req);
|
||||
@@ -950,6 +971,7 @@ impl ::hyper::service::Service for Service {
|
||||
let s = self.clone();
|
||||
move |(req, b)| { s.0.logout(&req, b) }
|
||||
})),
|
||||
Path::Signals => wrap_r(true, self.0.signals(&req)),
|
||||
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
|
||||
}
|
||||
}
|
||||
@@ -1098,6 +1120,7 @@ mod tests {
|
||||
Path::NotFound);
|
||||
assert_eq!(Path::decode("/api/login"), Path::Login);
|
||||
assert_eq!(Path::decode("/api/logout"), Path::Logout);
|
||||
assert_eq!(Path::decode("/api/signals"), Path::Signals);
|
||||
assert_eq!(Path::decode("/api/junk"), Path::NotFound);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user