use super::{xml_escape, xml_str, xml_u32};
use crate::error::OnvifError;
use crate::soap::{SoapError, XmlNode};
#[derive(Debug, Clone, Default)]
pub struct PtzSpaceRange {
pub uri: String,
pub x_range: (f32, f32),
pub y_range: Option<(f32, f32)>,
}
fn parse_space_range(node: &XmlNode) -> PtzSpaceRange {
let uri = xml_str(node, "URI").unwrap_or_default();
let x_range = node
.child("XRange")
.map(|r| {
let min = r
.child("Min")
.and_then(|n| n.text().parse().ok())
.unwrap_or(-1.0);
let max = r
.child("Max")
.and_then(|n| n.text().parse().ok())
.unwrap_or(1.0);
(min, max)
})
.unwrap_or((-1.0, 1.0));
let y_range = node.child("YRange").map(|r| {
let min = r
.child("Min")
.and_then(|n| n.text().parse().ok())
.unwrap_or(-1.0);
let max = r
.child("Max")
.and_then(|n| n.text().parse().ok())
.unwrap_or(1.0);
(min, max)
});
PtzSpaceRange {
uri,
x_range,
y_range,
}
}
#[derive(Debug, Clone)]
pub struct PtzConfiguration {
pub token: String,
pub name: String,
pub use_count: u32,
pub node_token: String,
pub default_ptz_timeout: Option<String>,
pub pan_tilt_limits: Option<PtzSpaceRange>,
pub zoom_limits: Option<PtzSpaceRange>,
}
impl PtzConfiguration {
pub(crate) fn from_xml(node: &XmlNode) -> Result<Self, OnvifError> {
let token = node
.attr("token")
.filter(|t| !t.is_empty())
.ok_or_else(|| SoapError::missing("PTZConfiguration/@token"))?
.to_string();
Ok(Self {
token,
name: xml_str(node, "Name").unwrap_or_default(),
use_count: xml_u32(node, "UseCount").unwrap_or(0),
node_token: xml_str(node, "NodeToken").unwrap_or_default(),
default_ptz_timeout: xml_str(node, "DefaultPTZTimeout"),
pan_tilt_limits: node
.path(&["PanTiltLimits", "Range"])
.map(parse_space_range),
zoom_limits: node.path(&["ZoomLimits", "Range"]).map(parse_space_range),
})
}
pub(crate) fn vec_from_xml(resp: &XmlNode) -> Result<Vec<Self>, OnvifError> {
resp.children_named("PTZConfiguration")
.map(Self::from_xml)
.collect()
}
pub(crate) fn to_xml_body(&self) -> String {
let timeout_el = self
.default_ptz_timeout
.as_deref()
.map(|t| {
format!(
"<tt:DefaultPTZTimeout>{}</tt:DefaultPTZTimeout>",
xml_escape(t)
)
})
.unwrap_or_default();
format!(
"<tptz:PTZConfiguration token=\"{token}\">\
<tt:Name>{name}</tt:Name>\
<tt:UseCount>{use_count}</tt:UseCount>\
<tt:NodeToken>{node_token}</tt:NodeToken>\
{timeout_el}\
</tptz:PTZConfiguration>",
token = xml_escape(&self.token),
name = xml_escape(&self.name),
use_count = self.use_count,
node_token = xml_escape(&self.node_token),
)
}
}
#[derive(Debug, Clone, Default)]
pub struct PtzConfigurationOptions {
pub ptz_timeout_min: Option<String>,
pub ptz_timeout_max: Option<String>,
}
impl PtzConfigurationOptions {
pub(crate) fn from_xml(resp: &XmlNode) -> Result<Self, OnvifError> {
let opts = resp
.child("PTZConfigurationOptions")
.ok_or_else(|| SoapError::missing("PTZConfigurationOptions"))?;
Ok(Self {
ptz_timeout_min: opts
.path(&["PTZTimeout", "Min"])
.map(|n| n.text().to_string()),
ptz_timeout_max: opts
.path(&["PTZTimeout", "Max"])
.map(|n| n.text().to_string()),
})
}
}
#[derive(Debug, Clone)]
pub struct PtzNode {
pub token: String,
pub name: String,
pub fixed_home_position: bool,
pub home_supported: bool,
pub max_presets: u32,
pub aux_commands: Vec<String>,
}
impl PtzNode {
pub(crate) fn vec_from_xml(resp: &XmlNode) -> Result<Vec<Self>, OnvifError> {
resp.children_named("PTZNode")
.map(|n| {
let token = n
.attr("token")
.filter(|t| !t.is_empty())
.ok_or_else(|| SoapError::missing("PTZNode/@token"))?
.to_string();
Ok(Self {
token,
name: xml_str(n, "Name").unwrap_or_default(),
fixed_home_position: n.attr("FixedHomePosition") == Some("true"),
home_supported: n
.child("HomeSupported")
.is_some_and(|h| h.text() == "true" || h.text() == "1"),
max_presets: xml_u32(n, "MaximumNumberOfPresets").unwrap_or(0),
aux_commands: n
.children_named("AuxiliaryCommands")
.map(|c| c.text().to_string())
.collect(),
})
})
.collect()
}
}