use std::time::Duration;
use serde::{Deserialize, Deserializer};
use crate::types::parse_uptime;
fn deserialize_string_or_number<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct StringOrNumber;
impl Visitor<'_> for StringOrNumber {
type Value = u8;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a number or a string containing a number")
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
u8::try_from(value).map_err(|_| E::custom(format!("u8 out of range: {value}")))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
u8::try_from(value).map_err(|_| E::custom(format!("u8 out of range: {value}")))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.parse::<u8>().map_err(de::Error::custom)
}
}
deserializer.deserialize_any(StringOrNumber)
}
fn deserialize_string_or_number_opt<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: Deserializer<'de>,
{
deserialize_string_or_number(deserializer).or(Ok(0))
}
fn deserialize_string_or_number_i8<'de, D>(deserializer: D) -> Result<i8, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct StringOrNumberI8;
impl Visitor<'_> for StringOrNumberI8 {
type Value = i8;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a number or a string containing a number")
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
i8::try_from(value).map_err(|_| E::custom(format!("i8 out of range: {value}")))
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
i8::try_from(value).map_err(|_| E::custom(format!("i8 out of range: {value}")))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.parse::<i8>().map_err(de::Error::custom)
}
}
deserializer.deserialize_any(StringOrNumberI8)
}
fn deserialize_number_or_string_as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct NumberOrString;
impl Visitor<'_> for NumberOrString {
type Value = String;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a number or a string")
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value.to_string())
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value.to_string())
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value.to_string())
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value.to_string())
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value)
}
}
deserializer.deserialize_any(NumberOrString)
}
fn deserialize_string_or_number_i8_opt<'de, D>(deserializer: D) -> Result<i8, D::Error>
where
D: Deserializer<'de>,
{
deserialize_string_or_number_i8(deserializer).or(Ok(0))
}
fn deserialize_string_or_number_u16<'de, D>(deserializer: D) -> Result<u16, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct StringOrNumberU16;
impl Visitor<'_> for StringOrNumberU16 {
type Value = u16;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a number or a string containing a number")
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
u16::try_from(value).map_err(|_| E::custom(format!("u16 out of range: {value}")))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
u16::try_from(value).map_err(|_| E::custom(format!("u16 out of range: {value}")))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.parse::<u16>().map_err(de::Error::custom)
}
}
deserializer.deserialize_any(StringOrNumberU16)
}
fn deserialize_string_or_number_u16_opt<'de, D>(deserializer: D) -> Result<u16, D::Error>
where
D: Deserializer<'de>,
{
deserialize_string_or_number_u16(deserializer).or(Ok(0))
}
fn deserialize_string_or_number_u32<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct StringOrNumberU32;
impl Visitor<'_> for StringOrNumberU32 {
type Value = u32;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a number or a string containing a number")
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
u32::try_from(value).map_err(|_| E::custom(format!("u32 out of range: {value}")))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
u32::try_from(value).map_err(|_| E::custom(format!("u32 out of range: {value}")))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.parse::<u32>().map_err(de::Error::custom)
}
}
deserializer.deserialize_any(StringOrNumberU32)
}
fn deserialize_string_or_number_u32_opt<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: Deserializer<'de>,
{
deserialize_string_or_number_u32(deserializer).or(Ok(0))
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct StatusResponse {
#[serde(rename = "Status")]
pub status: Option<StatusDeviceParameters>,
#[serde(rename = "StatusPRM")]
pub status_prm: Option<StatusParameters>,
#[serde(rename = "StatusFWR")]
pub firmware: Option<StatusFirmware>,
#[serde(rename = "StatusLOG")]
pub logging: Option<StatusLogging>,
#[serde(rename = "StatusMEM")]
pub memory: Option<StatusMemory>,
#[serde(rename = "StatusNET")]
pub network: Option<StatusNetwork>,
#[serde(rename = "StatusMQT")]
pub mqtt: Option<StatusMqtt>,
#[serde(rename = "StatusTIM")]
pub time: Option<StatusTime>,
#[serde(rename = "StatusSNS")]
pub sensors: Option<serde_json::Value>,
#[serde(rename = "StatusPTH")]
pub power_thresholds: Option<serde_json::Value>,
#[serde(rename = "StatusSTS")]
pub sensor_status: Option<serde_json::Value>,
}
impl StatusResponse {
#[must_use]
pub fn module_id(&self) -> Option<u8> {
self.status.as_ref().map(|s| s.module)
}
#[must_use]
pub fn device_name(&self) -> Option<&str> {
self.status.as_ref().map(|s| s.device_name.as_str())
}
#[must_use]
pub fn firmware_version(&self) -> Option<&str> {
self.firmware.as_ref().map(|f| f.version.as_str())
}
#[must_use]
pub fn ip_address(&self) -> Option<&str> {
self.network.as_ref().map(|n| n.ip_address.as_str())
}
#[must_use]
pub fn hostname(&self) -> Option<&str> {
self.network.as_ref().map(|n| n.hostname.as_str())
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct StatusDeviceParameters {
#[serde(default, deserialize_with = "deserialize_string_or_number_opt")]
pub module: u8,
#[serde(default)]
pub device_name: String,
#[serde(default)]
pub friendly_name: Vec<String>,
#[serde(default)]
pub topic: String,
#[serde(default)]
pub button_topic: String,
#[serde(default, deserialize_with = "deserialize_string_or_number_opt")]
pub power: u8,
#[serde(default, deserialize_with = "deserialize_string_or_number_opt")]
pub power_retain: u8,
#[serde(
default,
rename = "LedState",
deserialize_with = "deserialize_string_or_number_opt"
)]
pub led_state: u8,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct StatusParameters {
#[serde(default, deserialize_with = "deserialize_string_or_number_u32_opt")]
pub baudrate: u32,
#[serde(default)]
pub serial_config: String,
#[serde(default)]
pub group_topic: String,
#[serde(default, rename = "OtaUrl")]
pub ota_url: String,
#[serde(default)]
pub restart_reason: String,
#[serde(default, rename = "Uptime")]
pub uptime_string: String,
#[serde(default, deserialize_with = "deserialize_string_or_number_u32_opt")]
pub boot_count: u32,
}
impl StatusParameters {
#[must_use]
pub fn uptime(&self) -> Option<Duration> {
if self.uptime_string.is_empty() {
return None;
}
parse_uptime(&self.uptime_string).ok()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct StatusFirmware {
#[serde(default)]
pub version: String,
#[serde(default)]
pub build_date_time: String,
#[serde(default, deserialize_with = "deserialize_string_or_number_opt")]
pub boot: u8,
#[serde(default)]
pub core: String,
#[serde(default, rename = "SDK")]
pub sdk: String,
#[serde(
default,
rename = "CpuFrequency",
deserialize_with = "deserialize_string_or_number_u16_opt"
)]
pub cpu_frequency: u16,
#[serde(default)]
pub hardware: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct StatusLogging {
#[serde(default, deserialize_with = "deserialize_string_or_number_opt")]
pub serial_log: u8,
#[serde(default, deserialize_with = "deserialize_string_or_number_opt")]
pub web_log: u8,
#[serde(default, deserialize_with = "deserialize_string_or_number_opt")]
pub mqtt_log: u8,
#[serde(default, deserialize_with = "deserialize_string_or_number_opt")]
pub sys_log: u8,
#[serde(default)]
pub log_host: String,
#[serde(default, deserialize_with = "deserialize_string_or_number_u16_opt")]
pub log_port: u16,
#[serde(
default,
rename = "TelePeriod",
deserialize_with = "deserialize_string_or_number_u16_opt"
)]
pub tele_period: u16,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct StatusMemory {
#[serde(default, deserialize_with = "deserialize_string_or_number_u32_opt")]
pub program_size: u32,
#[serde(default, deserialize_with = "deserialize_string_or_number_u32_opt")]
pub free: u32,
#[serde(default, deserialize_with = "deserialize_string_or_number_u32_opt")]
pub heap: u32,
#[serde(
default,
rename = "ProgramFlashSize",
deserialize_with = "deserialize_string_or_number_u32_opt"
)]
pub program_flash_size: u32,
#[serde(default, deserialize_with = "deserialize_string_or_number_u32_opt")]
pub flash_size: u32,
#[serde(default, rename = "FlashChipId")]
pub flash_chip_id: String,
#[serde(default, deserialize_with = "deserialize_string_or_number_opt")]
pub flash_mode: u8,
#[serde(default)]
pub features: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct StatusNetwork {
#[serde(default)]
pub hostname: String,
#[serde(default, rename = "IPAddress")]
pub ip_address: String,
#[serde(default)]
pub gateway: String,
#[serde(default, rename = "Subnetmask")]
pub subnet_mask: String,
#[serde(default, rename = "DNSServer1")]
pub dns_server: String,
#[serde(default)]
pub mac: String,
#[serde(default, rename = "SSId")]
pub ssid: String,
#[serde(default, rename = "BSSId")]
pub bssid: String,
#[serde(default, deserialize_with = "deserialize_string_or_number_opt")]
pub channel: u8,
#[serde(default)]
pub mode: String,
#[serde(
default,
rename = "RSSI",
deserialize_with = "deserialize_string_or_number_i8_opt"
)]
pub rssi: i8,
#[serde(default, deserialize_with = "deserialize_string_or_number_i8_opt")]
pub signal: i8,
#[serde(default, deserialize_with = "deserialize_string_or_number_u32_opt")]
pub link_count: u32,
#[serde(default)]
pub downtime: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct StatusMqtt {
#[serde(default, rename = "MqttHost")]
pub host: String,
#[serde(
default,
rename = "MqttPort",
deserialize_with = "deserialize_string_or_number_u16_opt"
)]
pub port: u16,
#[serde(default, rename = "MqttClientMask")]
pub client_mask: String,
#[serde(default, rename = "MqttClient")]
pub client: String,
#[serde(default, rename = "MqttUser")]
pub user: String,
#[serde(
default,
rename = "MqttCount",
deserialize_with = "deserialize_string_or_number_u32_opt"
)]
pub count: u32,
#[serde(
default,
rename = "MAX_PACKET_SIZE",
deserialize_with = "deserialize_string_or_number_u32_opt"
)]
pub max_packet_size: u32,
#[serde(
default,
rename = "KEEPALIVE",
deserialize_with = "deserialize_string_or_number_u16_opt"
)]
pub keepalive: u16,
#[serde(
default,
rename = "SOCKET_TIMEOUT",
deserialize_with = "deserialize_string_or_number_opt"
)]
pub socket_timeout: u8,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct StatusTime {
#[serde(default, rename = "UTC")]
pub utc: String,
#[serde(default)]
pub local: String,
#[serde(default, rename = "StartDST")]
pub start_dst: String,
#[serde(default, rename = "EndDST")]
pub end_dst: String,
#[serde(default, deserialize_with = "deserialize_number_or_string_as_string")]
pub timezone: String,
#[serde(default)]
pub sunrise: String,
#[serde(default)]
pub sunset: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_status_response() {
let json = r#"{
"Status": {
"Module": 18,
"DeviceName": "Tasmota",
"FriendlyName": ["Light"],
"Topic": "tasmota",
"ButtonTopic": "0",
"Power": 1,
"PowerRetain": 0,
"LedState": 0
}
}"#;
let response: StatusResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.module_id(), Some(18));
assert_eq!(response.device_name(), Some("Tasmota"));
}
#[test]
fn parse_firmware_info() {
let json = r#"{
"StatusFWR": {
"Version": "13.1.0",
"BuildDateTime": "2024-01-01T00:00:00",
"Boot": 7,
"Core": "3.0.2",
"SDK": "2.2.2",
"CpuFrequency": 80,
"Hardware": "ESP8266"
}
}"#;
let response: StatusResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.firmware_version(), Some("13.1.0"));
}
#[test]
fn parse_network_info() {
let json = r#"{
"StatusNET": {
"Hostname": "tasmota-device",
"IPAddress": "192.168.1.100",
"Gateway": "192.168.1.1",
"Subnetmask": "255.255.255.0",
"DNSServer1": "192.168.1.1",
"Mac": "AA:BB:CC:DD:EE:FF",
"SSId": "MyNetwork",
"Channel": 6,
"RSSI": -50,
"Signal": 100
}
}"#;
let response: StatusResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.ip_address(), Some("192.168.1.100"));
assert_eq!(response.hostname(), Some("tasmota-device"));
}
#[test]
fn parse_mqtt_info() {
let json = r#"{
"StatusMQT": {
"MqttHost": "192.168.1.50",
"MqttPort": 1883,
"MqttClient": "tasmota_123456",
"MqttUser": "mqtt_user",
"MqttCount": 1,
"MAX_PACKET_SIZE": 1200,
"KEEPALIVE": 30
}
}"#;
let response: StatusResponse = serde_json::from_str(json).unwrap();
let mqtt = response.mqtt.unwrap();
assert_eq!(mqtt.host, "192.168.1.50");
assert_eq!(mqtt.port, 1883);
}
}