use std::future::Future;
use std::time::Instant;
use super::report::{Category, CheckResult};
use crate::{OnvifError, OnvifSession};
async fn one<F>(id: &'static str, category: Category, fut: F) -> CheckResult
where
F: Future<Output = Result<String, OnvifError>>,
{
let start = Instant::now();
let r = fut.await;
let elapsed = start.elapsed();
match r {
Ok(detail) => CheckResult::pass(id, category, detail).with_elapsed(elapsed),
Err(e) => CheckResult::fail(id, category, e.to_string()).with_elapsed(elapsed),
}
}
pub(super) async fn device_info(s: &OnvifSession) -> Vec<CheckResult> {
vec![
one("get_device_info", Category::Connectivity, async {
let i = s.get_device_info().await?;
Ok(format!(
"{} {} fw {}",
i.manufacturer, i.model, i.firmware_version
))
})
.await,
]
}
pub(super) async fn time(s: &OnvifSession) -> Vec<CheckResult> {
let start = Instant::now();
let r = s.get_system_date_and_time().await;
let elapsed = start.elapsed();
let res = match r {
Ok(dt) => {
let skew = dt.utc_offset_secs();
if skew.abs() > 5 {
CheckResult::warn(
"system_date_time",
Category::Time,
format!("clock skew {skew}s vs local — may break WS-Security auth"),
format!("skew {skew}s"),
)
} else {
CheckResult::pass("system_date_time", Category::Time, format!("skew {skew}s"))
}
}
Err(e) => CheckResult::fail("system_date_time", Category::Time, e.to_string()),
};
vec![res.with_elapsed(elapsed)]
}
pub(super) async fn services(s: &OnvifSession) -> Vec<CheckResult> {
vec![
one("get_services", Category::Services, async {
let svcs = s.get_services().await?;
Ok(format!("{} service(s)", svcs.len()))
})
.await,
]
}
pub(super) async fn media(s: &OnvifSession) -> Vec<CheckResult> {
let mut out = Vec::new();
let start = Instant::now();
let profiles = s.get_profiles().await;
let elapsed = start.elapsed();
let first_token = match &profiles {
Ok(p) if !p.is_empty() => {
out.push(
CheckResult::pass(
"get_profiles",
Category::Media,
format!("{} profile(s)", p.len()),
)
.with_elapsed(elapsed),
);
Some(p[0].token.clone())
}
Ok(_) => {
out.push(
CheckResult::warn(
"get_profiles",
Category::Media,
"no media profiles",
"0 profiles",
)
.with_elapsed(elapsed),
);
None
}
Err(e) => {
out.push(
CheckResult::fail("get_profiles", Category::Media, e.to_string())
.with_elapsed(elapsed),
);
None
}
};
if let Some(token) = first_token {
let start = Instant::now();
match s.get_stream_uri(&token).await {
Ok(u) if u.uri.starts_with("rtsp://") => out.push(
CheckResult::pass("get_stream_uri", Category::Media, u.uri)
.with_elapsed(start.elapsed()),
),
Ok(u) => out.push(
CheckResult::warn("get_stream_uri", Category::Media, "non-rtsp scheme", u.uri)
.with_elapsed(start.elapsed()),
),
Err(e) => out.push(
CheckResult::fail("get_stream_uri", Category::Media, e.to_string())
.with_elapsed(start.elapsed()),
),
}
let start = Instant::now();
match s.get_snapshot_uri(&token).await {
Ok(u) if u.uri.starts_with("http") => out.push(
CheckResult::pass("get_snapshot_uri", Category::Media, u.uri)
.with_elapsed(start.elapsed()),
),
Ok(u) => out.push(
CheckResult::warn(
"get_snapshot_uri",
Category::Media,
"non-http scheme",
u.uri,
)
.with_elapsed(start.elapsed()),
),
Err(e) => out.push(
CheckResult::fail("get_snapshot_uri", Category::Media, e.to_string())
.with_elapsed(start.elapsed()),
),
}
}
out.push(
one("get_video_encoder_configurations", Category::Media, async {
let cfgs = s.get_video_encoder_configurations().await?;
Ok(format!("{} encoder config(s)", cfgs.len()))
})
.await,
);
out
}
pub(super) async fn imaging(s: &OnvifSession) -> Vec<CheckResult> {
if s.capabilities().imaging.url.is_none() {
return vec![CheckResult::skip(
"get_imaging_settings",
Category::Imaging,
"Imaging service not advertised",
)];
}
let start = Instant::now();
let token = match s.get_video_sources().await {
Ok(v) if !v.is_empty() => v[0].token.clone(),
Ok(_) => {
return vec![
CheckResult::warn(
"get_imaging_settings",
Category::Imaging,
"no video sources",
"",
)
.with_elapsed(start.elapsed()),
];
}
Err(e) => {
return vec![
CheckResult::fail("get_video_sources", Category::Imaging, e.to_string())
.with_elapsed(start.elapsed()),
];
}
};
vec![
one("get_imaging_settings", Category::Imaging, async {
s.get_imaging_settings(&token).await?;
s.get_imaging_options(&token).await?;
Ok("settings + options".to_string())
})
.await,
]
}
pub(super) async fn ptz(s: &OnvifSession) -> Vec<CheckResult> {
if s.capabilities().ptz.url.is_none() {
return vec![CheckResult::skip(
"ptz_get_nodes",
Category::Ptz,
"PTZ service not advertised",
)];
}
vec![
one("ptz_get_nodes", Category::Ptz, async {
let nodes = s.ptz_get_nodes().await?;
Ok(format!("{} node(s)", nodes.len()))
})
.await,
]
}
pub(super) async fn events(s: &OnvifSession) -> Vec<CheckResult> {
if s.capabilities().events.url.is_none() {
return vec![CheckResult::skip(
"get_event_properties",
Category::Events,
"Events service not advertised",
)];
}
let mut out = vec![
one("get_event_properties", Category::Events, async {
s.get_event_properties().await?;
Ok("ok".to_string())
})
.await,
];
let start = Instant::now();
match s.create_pull_point_subscription(None, Some("PT1M")).await {
Ok(sub) => {
let _ = s.pull_messages(&sub.reference_url, "PT1S", 10).await;
let _ = s.unsubscribe(&sub.reference_url).await;
out.push(
CheckResult::pass(
"pull_point_subscription",
Category::Events,
"subscribe / pull / unsubscribe ok",
)
.with_elapsed(start.elapsed()),
);
}
Err(e) => out.push(
CheckResult::fail("pull_point_subscription", Category::Events, e.to_string())
.with_elapsed(start.elapsed()),
),
}
out
}
pub(super) async fn network(s: &OnvifSession) -> Vec<CheckResult> {
vec![
one("get_network_interfaces", Category::Network, async {
let n = s.get_network_interfaces().await?;
Ok(format!("{} interface(s)", n.len()))
})
.await,
one("get_ntp", Category::Network, async {
s.get_ntp().await?;
Ok("ok".to_string())
})
.await,
one("get_dns", Category::Network, async {
s.get_dns().await?;
Ok("ok".to_string())
})
.await,
]
}
pub(super) async fn users(s: &OnvifSession) -> Vec<CheckResult> {
vec![
one("get_users", Category::Users, async {
let u = s.get_users().await?;
Ok(format!("{} user(s)", u.len()))
})
.await,
]
}
pub(super) async fn write_roundtrip(s: &OnvifSession) -> Vec<CheckResult> {
let start = Instant::now();
let cfg = match s.get_video_encoder_configurations().await {
Ok(mut v) if !v.is_empty() => v.remove(0),
Ok(_) => {
return vec![
CheckResult::skip(
"set_video_encoder_roundtrip",
Category::Write,
"no encoder config to round-trip",
)
.with_elapsed(start.elapsed()),
];
}
Err(e) => {
return vec![
CheckResult::fail(
"set_video_encoder_roundtrip",
Category::Write,
format!("read failed: {e}"),
)
.with_elapsed(start.elapsed()),
];
}
};
let res = match s.set_video_encoder_configuration(&cfg).await {
Ok(()) => CheckResult::pass(
"set_video_encoder_roundtrip",
Category::Write,
"Set accepted (unchanged values)",
),
Err(e) => CheckResult::fail(
"set_video_encoder_roundtrip",
Category::Write,
e.to_string(),
),
};
vec![res.with_elapsed(start.elapsed())]
}