use crate::helpers::{resp_empty, soap};
use crate::state::SharedState;
use crate::xml_parse::{extract_all_tags, extract_tag};
const NS: &str = r#"xmlns:tds="http://www.onvif.org/ver10/device/wsdl""#;
pub fn resp_system_date_and_time(state: &SharedState) -> String {
let s = state.read();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let secs = now % 60;
let mins = (now / 60) % 60;
let hours = (now / 3600) % 24;
let dst = if s.daylight_savings { "true" } else { "false" };
soap(
NS,
&format!(
r#"<tds:GetSystemDateAndTimeResponse>
<tds:SystemDateAndTime>
<tt:DateTimeType>NTP</tt:DateTimeType>
<tt:DaylightSavings>{dst}</tt:DaylightSavings>
<tt:TimeZone><tt:TZ>{tz}</tt:TZ></tt:TimeZone>
<tt:UTCDateTime>
<tt:Time><tt:Hour>{hours}</tt:Hour><tt:Minute>{mins}</tt:Minute><tt:Second>{secs}</tt:Second></tt:Time>
<tt:Date><tt:Year>2026</tt:Year><tt:Month>4</tt:Month><tt:Day>15</tt:Day></tt:Date>
</tt:UTCDateTime>
</tds:SystemDateAndTime>
</tds:GetSystemDateAndTimeResponse>"#,
tz = s.timezone,
),
)
}
pub fn resp_device_info(state: &SharedState) -> String {
let s = state.read();
soap(
NS,
&format!(
r#"<tds:GetDeviceInformationResponse>
<tds:Manufacturer>{}</tds:Manufacturer>
<tds:Model>{}</tds:Model>
<tds:FirmwareVersion>{}</tds:FirmwareVersion>
<tds:SerialNumber>{}</tds:SerialNumber>
<tds:HardwareId>{}</tds:HardwareId>
</tds:GetDeviceInformationResponse>"#,
s.info.manufacturer,
s.info.model,
s.info.firmware_version,
s.info.serial_number,
s.info.hardware_id,
),
)
}
pub fn resp_hostname(state: &SharedState) -> String {
let s = state.read();
let dhcp = if s.hostname_from_dhcp {
"true"
} else {
"false"
};
soap(
NS,
&format!(
r#"<tds:GetHostnameResponse>
<tds:HostnameInformation>
<tt:FromDHCP>{dhcp}</tt:FromDHCP>
<tt:Name>{name}</tt:Name>
</tds:HostnameInformation>
</tds:GetHostnameResponse>"#,
name = s.hostname,
),
)
}
pub fn resp_ntp(state: &SharedState) -> String {
let s = state.read();
let dhcp = if s.ntp.from_dhcp { "true" } else { "false" };
let servers: String = s
.ntp.servers
.iter()
.map(|srv| {
format!(
r#"<tt:NTPManual><tt:Type>DNS</tt:Type><tt:DNSname>{srv}</tt:DNSname></tt:NTPManual>"#
)
})
.collect();
soap(
NS,
&format!(
r#"<tds:GetNTPResponse>
<tds:NTPInformation>
<tt:FromDHCP>{dhcp}</tt:FromDHCP>
{servers}
</tds:NTPInformation>
</tds:GetNTPResponse>"#
),
)
}
pub fn resp_scopes(state: &SharedState) -> String {
let s = state.read();
let items: String = s
.scopes
.iter()
.map(|scope| {
format!(
r#"<tds:Scopes><tt:ScopeAttribute>Fixed</tt:ScopeAttribute><tt:ScopeItem>{scope}</tt:ScopeItem></tds:Scopes>"#
)
})
.collect();
soap(
NS,
&format!("<tds:GetScopesResponse>{items}</tds:GetScopesResponse>"),
)
}
pub fn resp_users(state: &SharedState) -> String {
let s = state.read();
let items: String = s
.users
.iter()
.map(|u| {
format!(
r#"<tds:User><tt:Username>{}</tt:Username><tt:UserLevel>{}</tt:UserLevel></tds:User>"#,
u.username, u.level,
)
})
.collect();
soap(
NS,
&format!("<tds:GetUsersResponse>{items}</tds:GetUsersResponse>"),
)
}
pub fn resp_dns(state: &SharedState) -> String {
let s = state.read();
let dhcp = if s.dns.from_dhcp { "true" } else { "false" };
let servers: String = s
.dns.servers
.iter()
.map(|srv| {
format!(
r#"<tt:DNSManual><tt:Type>IPv4</tt:Type><tt:IPv4Address>{srv}</tt:IPv4Address></tt:DNSManual>"#
)
})
.collect();
soap(
NS,
&format!(
r#"<tds:GetDNSResponse>
<tds:DNSInformation>
<tt:FromDHCP>{dhcp}</tt:FromDHCP>
{servers}
</tds:DNSInformation>
</tds:GetDNSResponse>"#
),
)
}
pub fn resp_network_default_gateway(state: &SharedState) -> String {
let s = state.read();
let addrs: String = s
.gateway_ipv4
.iter()
.map(|a| format!("<tt:IPv4Address>{a}</tt:IPv4Address>"))
.collect();
soap(
NS,
&format!(
r#"<tds:GetNetworkDefaultGatewayResponse>
<tds:NetworkGateway>{addrs}</tds:NetworkGateway>
</tds:GetNetworkDefaultGatewayResponse>"#
),
)
}
pub fn resp_discovery_mode(state: &SharedState) -> String {
let s = state.read();
soap(
NS,
&format!(
r#"<tds:GetDiscoveryModeResponse>
<tds:DiscoveryMode>{}</tds:DiscoveryMode>
</tds:GetDiscoveryModeResponse>"#,
s.discovery_mode,
),
)
}
pub fn handle_set_hostname(state: &SharedState, body: &str) -> String {
if let Some(name) = extract_tag(body, "Name") {
state.modify(|s| {
s.hostname = name;
eprintln!(" [STATE] hostname updated");
});
}
resp_empty("tds", "SetHostnameResponse")
}
pub fn handle_set_ntp(state: &SharedState, body: &str) -> String {
let servers = extract_all_tags(body, "DNSname");
let from_dhcp = extract_tag(body, "FromDHCP")
.map(|v| v == "true")
.unwrap_or(false);
state.modify(|s| {
s.ntp.servers = servers;
s.ntp.from_dhcp = from_dhcp;
eprintln!(
" [STATE] NTP updated: dhcp={} servers={:?}",
s.ntp.from_dhcp, s.ntp.servers
);
});
resp_empty("tds", "SetNTPResponse")
}
pub fn handle_set_dns(state: &SharedState, body: &str) -> String {
let servers = extract_all_tags(body, "IPv4Address");
let from_dhcp = extract_tag(body, "FromDHCP")
.map(|v| v == "true")
.unwrap_or(false);
state.modify(|s| {
s.dns.servers = servers;
s.dns.from_dhcp = from_dhcp;
eprintln!(
" [STATE] DNS updated: dhcp={} servers={:?}",
s.dns.from_dhcp, s.dns.servers
);
});
resp_empty("tds", "SetDNSResponse")
}
pub fn handle_set_scopes(state: &SharedState, body: &str) -> String {
let scopes = extract_all_tags(body, "Scopes");
if !scopes.is_empty() {
state.modify(|s| {
s.scopes = scopes;
eprintln!(" [STATE] scopes updated: {:?}", s.scopes);
});
}
resp_empty("tds", "SetScopesResponse")
}
pub fn handle_set_system_date_and_time(state: &SharedState, body: &str) -> String {
let tz = extract_tag(body, "TZ");
let dst = extract_tag(body, "DaylightSavings");
if tz.is_some() || dst.is_some() {
state.modify(|s| {
if let Some(tz) = tz {
s.timezone = tz;
eprintln!(" [STATE] timezone updated");
}
if let Some(dst) = dst {
s.daylight_savings = dst == "true";
}
});
}
resp_empty("tds", "SetSystemDateAndTimeResponse")
}
pub fn handle_create_users(state: &SharedState, body: &str) -> String {
let inner = extract_tag(body, "CreateUsers").unwrap_or_default();
let usernames = extract_all_tags(&inner, "Username");
let passwords = extract_all_tags(&inner, "Password");
let levels = extract_all_tags(&inner, "UserLevel");
state.modify(|s| {
for (i, username) in usernames.into_iter().enumerate() {
let level = levels.get(i).cloned().unwrap_or_else(|| "User".to_string());
let password = passwords.get(i).cloned().unwrap_or_default();
eprintln!(" [STATE] user created: {username} ({level})");
s.users.push(crate::state::MockUser {
username,
level,
password,
});
}
});
resp_empty("tds", "CreateUsersResponse")
}
pub fn handle_delete_users(state: &SharedState, body: &str) -> String {
let inner = extract_tag(body, "DeleteUsers").unwrap_or_default();
let usernames = extract_all_tags(&inner, "Username");
state.modify(|s| {
for name in &usernames {
s.users.retain(|u| u.username != *name);
eprintln!(" [STATE] user deleted: {name}");
}
});
resp_empty("tds", "DeleteUsersResponse")
}
pub fn handle_set_user(state: &SharedState, body: &str) -> String {
let inner = extract_tag(body, "SetUser").unwrap_or_default();
let username = extract_tag(&inner, "Username");
let level = extract_tag(&inner, "UserLevel");
let password = extract_tag(&inner, "Password");
if let Some(username) = username {
state.modify(|s| {
if let Some(user) = s.users.iter_mut().find(|u| u.username == username) {
if let Some(l) = &level {
user.level = l.clone();
}
if let Some(p) = &password {
user.password = p.clone();
}
eprintln!(" [STATE] user updated: {username}");
}
});
}
resp_empty("tds", "SetUserResponse")
}
pub fn resp_capabilities(base: &str) -> String {
soap(
NS,
&format!(
r#"<tds:GetCapabilitiesResponse>
<tds:Capabilities>
<tt:Device><tt:XAddr>{base}/onvif/device</tt:XAddr></tt:Device>
<tt:Media>
<tt:XAddr>{base}/onvif/media</tt:XAddr>
<tt:StreamingCapabilities>
<tt:RTPMulticast>false</tt:RTPMulticast>
<tt:RTP_TCP>true</tt:RTP_TCP>
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
</tt:StreamingCapabilities>
</tt:Media>
<tt:PTZ><tt:XAddr>{base}/onvif/ptz</tt:XAddr></tt:PTZ>
<tt:Imaging><tt:XAddr>{base}/onvif/imaging</tt:XAddr></tt:Imaging>
<tt:Events>
<tt:XAddr>{base}/onvif/events</tt:XAddr>
<tt:WSPullPointSupport>true</tt:WSPullPointSupport>
</tt:Events>
<tt:Extension>
<tt:Recording><tt:XAddr>{base}/onvif/recording</tt:XAddr></tt:Recording>
<tt:Search><tt:XAddr>{base}/onvif/search</tt:XAddr></tt:Search>
<tt:Replay><tt:XAddr>{base}/onvif/replay</tt:XAddr></tt:Replay>
<tt:Media2><tt:XAddr>{base}/onvif/media2</tt:XAddr></tt:Media2>
</tt:Extension>
</tds:Capabilities>
</tds:GetCapabilitiesResponse>"#
),
)
}
pub fn resp_services(base: &str) -> String {
soap(
NS,
&format!(
r#"<tds:GetServicesResponse>
<tds:Service><tds:Namespace>http://www.onvif.org/ver10/device/wsdl</tds:Namespace><tds:XAddr>{base}/onvif/device</tds:XAddr><tds:Version><tt:Major>2</tt:Major><tt:Minor>6</tt:Minor></tds:Version></tds:Service>
<tds:Service><tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace><tds:XAddr>{base}/onvif/media</tds:XAddr><tds:Version><tt:Major>2</tt:Major><tt:Minor>6</tt:Minor></tds:Version></tds:Service>
<tds:Service><tds:Namespace>http://www.onvif.org/ver20/media/wsdl</tds:Namespace><tds:XAddr>{base}/onvif/media2</tds:XAddr><tds:Version><tt:Major>2</tt:Major><tt:Minor>0</tt:Minor></tds:Version></tds:Service>
<tds:Service><tds:Namespace>http://www.onvif.org/ver20/ptz/wsdl</tds:Namespace><tds:XAddr>{base}/onvif/ptz</tds:XAddr><tds:Version><tt:Major>2</tt:Major><tt:Minor>0</tt:Minor></tds:Version></tds:Service>
<tds:Service><tds:Namespace>http://www.onvif.org/ver20/imaging/wsdl</tds:Namespace><tds:XAddr>{base}/onvif/imaging</tds:XAddr><tds:Version><tt:Major>2</tt:Major><tt:Minor>0</tt:Minor></tds:Version></tds:Service>
<tds:Service><tds:Namespace>http://www.onvif.org/ver10/recording/wsdl</tds:Namespace><tds:XAddr>{base}/onvif/recording</tds:XAddr><tds:Version><tt:Major>2</tt:Major><tt:Minor>0</tt:Minor></tds:Version></tds:Service>
<tds:Service><tds:Namespace>http://www.onvif.org/ver10/search/wsdl</tds:Namespace><tds:XAddr>{base}/onvif/search</tds:XAddr><tds:Version><tt:Major>2</tt:Major><tt:Minor>0</tt:Minor></tds:Version></tds:Service>
<tds:Service><tds:Namespace>http://www.onvif.org/ver10/replay/wsdl</tds:Namespace><tds:XAddr>{base}/onvif/replay</tds:XAddr><tds:Version><tt:Major>2</tt:Major><tt:Minor>0</tt:Minor></tds:Version></tds:Service>
</tds:GetServicesResponse>"#
),
)
}
pub fn resp_network_interfaces(state: &SharedState) -> String {
let s = state.read();
let i = &s.interface;
let enabled = if i.enabled { "true" } else { "false" };
let dhcp = if i.ipv4_from_dhcp { "true" } else { "false" };
soap(
NS,
&format!(
r#"<tds:GetNetworkInterfacesResponse>
<tds:NetworkInterfaces token="{token}">
<tt:Enabled>{enabled}</tt:Enabled>
<tt:Info>
<tt:Name>{name}</tt:Name>
<tt:HwAddress>{mac}</tt:HwAddress>
<tt:MTU>{mtu}</tt:MTU>
</tt:Info>
<tt:IPv4>
<tt:Enabled>true</tt:Enabled>
<tt:Config>
<tt:FromDHCP>{dhcp}</tt:FromDHCP>
<tt:Manual>
<tt:Address>{address}</tt:Address>
<tt:PrefixLength>{prefix}</tt:PrefixLength>
</tt:Manual>
</tt:Config>
</tt:IPv4>
</tds:NetworkInterfaces>
</tds:GetNetworkInterfacesResponse>"#,
token = i.token,
name = i.name,
mac = i.mac,
mtu = i.mtu,
address = i.ipv4_address,
prefix = i.ipv4_prefix_length,
),
)
}
pub fn handle_set_network_interfaces(state: &SharedState, body: &str) -> String {
let token = extract_tag(body, "InterfaceToken").unwrap_or_default();
let enabled = extract_tag(body, "Enabled").map(|v| v == "true");
let dhcp = extract_tag(body, "FromDHCP").map(|v| v == "true");
let address = extract_tag(body, "Address");
let prefix: Option<u32> = extract_tag(body, "PrefixLength").and_then(|p| p.parse().ok());
state.modify(|s| {
if !token.is_empty() && token != s.interface.token {
return;
}
if let Some(e) = enabled {
s.interface.enabled = e;
}
if let Some(d) = dhcp {
s.interface.ipv4_from_dhcp = d;
}
if let Some(a) = address {
s.interface.ipv4_address = a;
}
if let Some(p) = prefix {
s.interface.ipv4_prefix_length = p;
}
eprintln!(
" [STATE] interface updated: dhcp={} addr={} /{}",
s.interface.ipv4_from_dhcp, s.interface.ipv4_address, s.interface.ipv4_prefix_length,
);
});
soap(
NS,
r#"<tds:SetNetworkInterfacesResponse>
<tds:RebootNeeded>false</tds:RebootNeeded>
</tds:SetNetworkInterfacesResponse>"#,
)
}
pub fn resp_network_protocols(state: &SharedState) -> String {
let s = state.read();
let items: String = s
.protocols
.iter()
.map(|p| {
let enabled = if p.enabled { "true" } else { "false" };
let ports: String = p
.ports
.iter()
.map(|n| format!("<tt:Port>{n}</tt:Port>"))
.collect();
format!(
r#"<tds:NetworkProtocols><tt:Name>{name}</tt:Name><tt:Enabled>{enabled}</tt:Enabled>{ports}</tds:NetworkProtocols>"#,
name = p.name,
)
})
.collect();
soap(
NS,
&format!("<tds:GetNetworkProtocolsResponse>{items}</tds:GetNetworkProtocolsResponse>"),
)
}
pub fn handle_set_network_protocols(state: &SharedState, body: &str) -> String {
let names = extract_all_tags(body, "Name");
let enableds = extract_all_tags(body, "Enabled");
let ports = extract_all_tags(body, "Port");
if names.is_empty() {
return resp_empty("tds", "SetNetworkProtocolsResponse");
}
state.modify(|s| {
for (i, name) in names.iter().enumerate() {
let enabled = enableds.get(i).map(|v| v == "true").unwrap_or(true);
let port: Option<u32> = ports.get(i).and_then(|p| p.parse().ok());
if let Some(p) = s
.protocols
.iter_mut()
.find(|p| p.name.eq_ignore_ascii_case(name))
{
p.enabled = enabled;
if let Some(port) = port {
p.ports = vec![port];
}
} else {
s.protocols.push(crate::state::NetworkProtocolState {
name: name.clone(),
enabled,
ports: port.map(|p| vec![p]).unwrap_or_default(),
});
}
eprintln!(" [STATE] protocol {name}: enabled={enabled} port={port:?}");
}
});
resp_empty("tds", "SetNetworkProtocolsResponse")
}
pub fn handle_set_network_default_gateway(state: &SharedState, body: &str) -> String {
let addrs = extract_all_tags(body, "IPv4Address");
state.modify(|s| {
s.gateway_ipv4 = addrs;
eprintln!(" [STATE] gateway updated: {:?}", s.gateway_ipv4);
});
resp_empty("tds", "SetNetworkDefaultGatewayResponse")
}
pub fn resp_system_log() -> String {
soap(
NS,
r#"<tds:GetSystemLogResponse>
<tds:SystemLog>
<tt:String>2026-04-15 12:00:00 mock system started</tt:String>
</tds:SystemLog>
</tds:GetSystemLogResponse>"#,
)
}
pub fn resp_relay_outputs() -> String {
soap(
NS,
r#"<tds:GetRelayOutputsResponse>
<tds:RelayOutputs token="RelayOutput_1">
<tt:Properties>
<tt:Mode>Bistable</tt:Mode>
<tt:DelayTime>PT0S</tt:DelayTime>
<tt:IdleState>open</tt:IdleState>
</tt:Properties>
</tds:RelayOutputs>
</tds:GetRelayOutputsResponse>"#,
)
}
pub fn resp_send_auxiliary_command() -> String {
soap(
NS,
r#"<tds:SendAuxiliaryCommandResponse>
<tds:AuxiliaryCommandResponse>OK</tds:AuxiliaryCommandResponse>
</tds:SendAuxiliaryCommandResponse>"#,
)
}
pub fn resp_storage_configurations() -> String {
soap(
&format!("{NS} xmlns:tt=\"http://www.onvif.org/ver10/schema\""),
r#"<tds:GetStorageConfigurationsResponse>
<tds:StorageConfigurations token="SD_01">
<tt:Data type="LocalStorage"><tt:LocalPath>/mnt/sd</tt:LocalPath></tt:Data>
</tds:StorageConfigurations>
</tds:GetStorageConfigurationsResponse>"#,
)
}
pub fn resp_system_uris(base: &str) -> String {
soap(
&format!("{NS} xmlns:tt=\"http://www.onvif.org/ver10/schema\""),
&format!(
r#"<tds:GetSystemUrisResponse>
<tds:SystemLogUris>
<tt:SystemLogUri><tt:Uri>{base}/syslog</tt:Uri><tt:LogType>System</tt:LogType></tt:SystemLogUri>
</tds:SystemLogUris>
<tds:SupportInfoUri>{base}/support</tds:SupportInfoUri>
</tds:GetSystemUrisResponse>"#
),
)
}
pub fn resp_system_reboot() -> String {
soap(
NS,
r#"<tds:SystemRebootResponse>
<tds:Message>Rebooting in 30 seconds</tds:Message>
</tds:SystemRebootResponse>"#,
)
}