diff --git a/echonet-lite-core/src/object/mod.rs b/echonet-lite-core/src/object/mod.rs index 518367f..3d20b62 100644 --- a/echonet-lite-core/src/object/mod.rs +++ b/echonet-lite-core/src/object/mod.rs @@ -1,7 +1,7 @@ -use crate::{ElPacket, Properties}; -use core::fmt; +use crate::{deserialize, ElPacket, Properties}; +use core::fmt::{self, Formatter}; pub use property_maps::*; -use serde::{Deserialize, Serialize}; +use serde::{de::Visitor, ser::SerializeTuple, Deserialize, Serialize}; mod property_maps; @@ -143,7 +143,7 @@ impl fmt::Display for ClassPacket { } } -mod code { +pub mod code { pub const HOME_AIR_CONDITIONER: [u8; 2] = [0x01, 0x30]; pub const INSTANTANEOUS_WATER_HEATER: [u8; 2] = [0x02, 0x72]; pub const HOUSEHOLD_SOLAR_POWER: [u8; 2] = [0x02, 0x79]; @@ -156,13 +156,19 @@ mod code { pub const GENERAL_LIGHTING: [u8; 2] = [0x02, 0x90]; pub const MONO_FUNCTION_LIGHTING: [u8; 2] = [0x02, 0x91]; pub const LIGHTING_SYSTEM: [u8; 2] = [0x02, 0xA3]; - pub const CONTROLLER: [u8; 2] = [0x05, 0xFE]; + pub const CONTROLLER: [u8; 2] = [0x05, 0xFF]; pub const PROFILE: [u8; 2] = [0x0E, 0xF0]; } #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)] struct ClassCode([u8; 2]); +impl From<[u8; 2]> for ClassCode { + fn from(value: [u8; 2]) -> Self { + Self(value) + } +} + impl fmt::Display for ClassCode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:02X} {:02X}", self.0[0], self.0[1]) @@ -182,10 +188,10 @@ impl fmt::Display for UnimplementedPacket { writeln!(f, "Unimplemented Class: {}", self.0)?; for prop in self.1.iter() { if let Some(name) = SUPER_CLASS.get(&prop.epc) { - writeln!(f, "[{name}]\t {prop}")?; + writeln!(f, "{prop}\t\t[{name}]")?; continue; } - writeln!(f, "[unknown]\t {prop}")?; + writeln!(f, "{prop}\t\t[unknown]")?; } Ok(()) } @@ -220,14 +226,14 @@ macro_rules! convert_packet { )?; for prop in self.0.iter() { if let Some(name) = SUPER_CLASS.get(&prop.epc) { - writeln!(f, "[{}]\t {}", name, prop)?; + writeln!(f, "{prop}\t\t[{name}]")?; continue; } if let Some(name) = $class.get(&prop.epc) { - writeln!(f, "[{}]\t {}", name, prop)?; + writeln!(f, "{prop}\t\t[{name}]")?; continue; } - writeln!(f, "[unknown]\t {}", prop)?; + writeln!(f, "{prop}\t\t[unknown]")?; } Ok(()) } @@ -349,27 +355,6 @@ impl fmt::Display for Controller { } } -enum Class { - Controller(Controller), -} - -impl From for Class { - fn from(code: ClassCode) -> Self { - match code.0 { - code::CONTROLLER => Class::Controller(Controller), - _ => { - todo!() - } - } - } -} - -impl From for Class { - fn from(obj: EchonetObject) -> Self { - Self::from(obj.class) - } -} - /// An ECHONET object. /// /// ECHONET objects are described using the formats [X1.X2] and [X3]. @@ -378,6 +363,7 @@ impl From for Class { /// - X3: Instance code #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)] pub struct EchonetObject { + // TODO: use `ElClass` instead. class: ClassCode, instance: u8, } @@ -391,9 +377,247 @@ impl From<[u8; 3]> for EchonetObject { } } +impl From<(ElClass, u8)> for EchonetObject { + fn from(value: (ElClass, u8)) -> Self { + let (class, instance) = value; + use code::*; + match class { + ElClass::HomeAC => Self { + class: HOME_AIR_CONDITIONER.into(), + instance, + }, + ElClass::Hp => Self { + class: HP.into(), + instance, + }, + ElClass::InstantaneousWaterHeater => Self { + class: INSTANTANEOUS_WATER_HEATER.into(), + instance, + }, + ElClass::Pv => Self { + class: HOUSEHOLD_SOLAR_POWER.into(), + instance, + }, + ElClass::FuelCell => Self { + class: FUEL_CELL.into(), + instance, + }, + ElClass::Battery => Self { + class: STORAGE_BATTERY.into(), + instance, + }, + ElClass::Evps => Self { + class: EVPS.into(), + instance, + }, + ElClass::Metering => Self { + class: POWER_DISTRIBUTION_BOARD_METERING.into(), + instance, + }, + ElClass::SmartMeter => Self { + class: SMART_METER.into(), + instance, + }, + ElClass::MultiInputPCS => Self { + class: [0x02, 0xA5].into(), + instance, + }, + ElClass::GeneralLighting => Self { + class: GENERAL_LIGHTING.into(), + instance, + }, + ElClass::MonoFunctionLighting => Self { + class: MONO_FUNCTION_LIGHTING.into(), + instance, + }, + ElClass::LightingSystem => Self { + class: LIGHTING_SYSTEM.into(), + instance, + }, + ElClass::Controller => Self { + class: CONTROLLER.into(), + instance, + }, + ElClass::Profile => Self { + class: PROFILE.into(), + instance, + }, + ElClass::Unknown(code) => Self { + class: code.into(), + instance, + }, + } + } +} + +impl TryFrom<&[u8]> for EchonetObject { + type Error = crate::error::Error; + + fn try_from(value: &[u8]) -> Result { + let (_, eobj) = deserialize(value)?; + Ok(eobj) + } +} + impl fmt::Display for EchonetObject { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "[{} {:02X}]", self.class, self.instance) + use code::*; + let class = match self.class.0 { + HOME_AIR_CONDITIONER => "Home AC", + HP => "Heat pump", + INSTANTANEOUS_WATER_HEATER => "Instantaneous water heater", + HOUSEHOLD_SOLAR_POWER => "Household solar power", + FUEL_CELL => "Fuel cell", + STORAGE_BATTERY => "Storage battery", + EVPS => "V2H", + POWER_DISTRIBUTION_BOARD_METERING => "Power distribution Metering", + SMART_METER => "Smart meter", + GENERAL_LIGHTING => "General lighting", + MONO_FUNCTION_LIGHTING => "Mono function lighting", + LIGHTING_SYSTEM => "Lighting system", + CONTROLLER => "Controller", + PROFILE => "Profile", + _ => "Unknown", + }; + write!(f, "{} [{} {:02X}]", class, self.class, self.instance) + } +} + +/// echonet-lite class representation. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum ElClass { + HomeAC, + Hp, + InstantaneousWaterHeater, + Pv, + FuelCell, + Battery, + Evps, + Metering, + SmartMeter, + MultiInputPCS, + GeneralLighting, + MonoFunctionLighting, + LightingSystem, + Controller, + Profile, + Unknown([u8; 2]), +} + +impl Serialize for ElClass { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let raw = Into::<[u8; 2]>::into(*self); + let mut seq = serializer.serialize_tuple(2)?; + seq.serialize_element(&raw[0])?; + seq.serialize_element(&raw[1])?; + seq.end() + } +} + +struct ElClassVisitor; +impl<'de> Visitor<'de> for ElClassVisitor { + type Value = (u8, u8); + + fn expecting(&self, formatter: &mut Formatter) -> Result<(), fmt::Error> { + formatter.write_str("never failed") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let group: u8 = seq.next_element()?.unwrap(); + let class: u8 = seq.next_element()?.unwrap(); + Ok((group, class)) + } +} + +impl<'de> Deserialize<'de> for ElClass { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let (group, class) = deserializer.deserialize_tuple(2, ElClassVisitor)?; + Ok(ElClass::from(&[group, class])) + } +} + +impl From<&[u8; 2]> for ElClass { + fn from(value: &[u8; 2]) -> Self { + use code::*; + use ElClass::*; + match *value { + HOME_AIR_CONDITIONER => HomeAC, + HP => Hp, + INSTANTANEOUS_WATER_HEATER => InstantaneousWaterHeater, + HOUSEHOLD_SOLAR_POWER => Pv, + FUEL_CELL => FuelCell, + STORAGE_BATTERY => Battery, + EVPS => Evps, + POWER_DISTRIBUTION_BOARD_METERING => Metering, + SMART_METER => SmartMeter, + [0x02, 0xA5] => MultiInputPCS, + GENERAL_LIGHTING => GeneralLighting, + MONO_FUNCTION_LIGHTING => MonoFunctionLighting, + LIGHTING_SYSTEM => LightingSystem, + CONTROLLER => Controller, + PROFILE => Profile, + _ => Unknown(*value), + } + } +} + +#[allow(clippy::from_over_into)] +impl Into<[u8; 2]> for ElClass { + fn into(self) -> [u8; 2] { + use code::*; + use ElClass::*; + match self { + HomeAC => HOME_AIR_CONDITIONER, + Hp => HP, + InstantaneousWaterHeater => INSTANTANEOUS_WATER_HEATER, + Pv => HOUSEHOLD_SOLAR_POWER, + FuelCell => FUEL_CELL, + Battery => STORAGE_BATTERY, + Evps => EVPS, + Metering => POWER_DISTRIBUTION_BOARD_METERING, + SmartMeter => SMART_METER, + MultiInputPCS => [0x02, 0xA5], + GeneralLighting => GENERAL_LIGHTING, + MonoFunctionLighting => MONO_FUNCTION_LIGHTING, + LightingSystem => LIGHTING_SYSTEM, + Controller => CONTROLLER, + Profile => PROFILE, + Unknown(raw) => raw, + } + } +} + +impl From for ElClass { + fn from(value: EchonetObject) -> Self { + use code::*; + use ElClass::*; + match value.class.0 { + HOME_AIR_CONDITIONER => HomeAC, + HP => Hp, + INSTANTANEOUS_WATER_HEATER => InstantaneousWaterHeater, + HOUSEHOLD_SOLAR_POWER => Pv, + FUEL_CELL => FuelCell, + STORAGE_BATTERY => Battery, + EVPS => Evps, + POWER_DISTRIBUTION_BOARD_METERING => Metering, + SMART_METER => SmartMeter, + [0x02, 0xA5] => MultiInputPCS, + GENERAL_LIGHTING => GeneralLighting, + MONO_FUNCTION_LIGHTING => MonoFunctionLighting, + LIGHTING_SYSTEM => LightingSystem, + CONTROLLER => Controller, + PROFILE => Profile, + _ => Unknown(value.class.0), + } } } @@ -402,9 +626,41 @@ mod test { use super::*; #[test] - fn conversion() { - let obj: EchonetObject = [0x05, 0xFE, 0x01].into(); - let _class: Class = obj.class.into(); - let _class: Class = obj.into(); + fn to_elclass() { + let eobj = EchonetObject::from([0x01, 0x30, 0x01]); + let class = ElClass::from(eobj); + assert_eq!(class, ElClass::HomeAC); + + let raw = [0x01u8, 0x30u8]; + let class = ElClass::from(&raw); + assert_eq!(class, ElClass::HomeAC); + } + + #[test] + fn to_echonet_object() { + let class = ElClass::HomeAC; + let eobj: EchonetObject = (class, 1u8).into(); + assert_eq!( + eobj, + EchonetObject { + class: ClassCode([0x01, 0x30]), + instance: 1 + } + ); + } + + #[test] + fn serialize_el_class() { + let class = ElClass::HomeAC; + let bytes = crate::ser::serialize(&class).unwrap(); + assert_eq!(bytes, vec![0x01, 0x30]); + } + + #[test] + fn deserialize_el_class() { + let input = [0x01, 0x30]; + let (bytes_read, class): (usize, ElClass) = deserialize(&input).unwrap(); + assert_eq!(bytes_read, 2); + assert_eq!(class, ElClass::HomeAC); } }