Developpement de la library pmoupnp

This commit is contained in:
2025-09-16 21:07:44 +02:00
parent a4001ae691
commit 921a29fa9f
40 changed files with 2024 additions and 1 deletions

2
.gitignore vendored
View File

@@ -12,7 +12,7 @@ xxx
**/.pmomusic_covers/** **/.pmomusic_covers/**
**/.DS_Strore/** **/.DS_Strore/**
**/.DS_Strore **/.DS_Strore
/target/** /target/
.pmomusic_covers .pmomusic_covers
C/src/soxr-0.1.3/Release/tests C/src/soxr-0.1.3/Release/tests
**/Release/ **/Release/

16
Cargo.lock generated
View File

@@ -290,6 +290,7 @@ dependencies = [
"thiserror", "thiserror",
"url", "url",
"uuid", "uuid",
"xmltree",
] ]
[[package]] [[package]]
@@ -583,6 +584,21 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "xml-rs"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
[[package]]
name = "xmltree"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6"
dependencies = [
"xml-rs",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.0" version = "0.8.0"

13
pmoupnp/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "pmoupnp"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = "0.4.42"
url = "2.5.7"
uuid = "1.18.1"
hex = "0.4.3"
base64 = "0.22.1"
thiserror = "2.0.16"
xmltree = "0.11.0"

14
pmoupnp/src/lib.rs Normal file
View File

@@ -0,0 +1,14 @@
mod object_trait;
pub mod variable_types;
pub mod state_variables;
pub mod value_ranges;
pub use crate::object_trait::UpnpObject;
#[derive(Clone)]
pub struct UpnpObjectType {
name: String,
object_type: String,
}

View File

@@ -0,0 +1,39 @@
use xmltree::{Element, EmitterConfig};
use crate::UpnpObjectType;
pub trait UpnpObject {
fn as_upnp_object_type(&self) -> &UpnpObjectType;
fn to_xml_element(&self) -> Element;
fn get_name(&self)-> &String {
return &self.as_upnp_object_type().name;
}
fn get_object_type(&self) -> &String {
&self.as_upnp_object_type().object_type
}
fn to_xml(&self) -> String {
let elem = self.to_xml_element();
// Configurer l'indentation
let config = EmitterConfig::new()
.perform_indent(true)
.indent_string(" "); // 2 espaces
// Sérialiser dans un buffer
let mut buf = Vec::new();
// écrire l'élément
elem.write_with_config(&mut buf, config)
.expect("Failed to write XML");
// Préfixer avec l'en-tête XML
let mut xml_string = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".to_string();
xml_string.push_str(&String::from_utf8(buf).expect("Invalid UTF-8"));
xml_string
}
}

View File

@@ -0,0 +1,28 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StateVariableError {
#[error("Conversion error: {0}")]
ConversionError(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Range error: {0}")]
RangeError(String),
#[error("Type error: {0}")]
TypeError(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Event condition error: {0}")]
EventConditionError(String),
#[error("Arithmetic error: {0}")]
ArithmeticError(String),
#[error("Unknown error: {0}")]
Unknown(String),
}

View File

@@ -0,0 +1,58 @@
use chrono::{DateTime, Utc};
use xmltree::Element;
use crate::{
state_variables::{StateVarInstance, StateVariable, UpnpVariable},
variable_types::StateValue,
UpnpObject,
UpnpObjectType
};
impl UpnpVariable for StateVarInstance {
fn get_definition(&self) -> &StateVariable {
return &self.definition;
}
}
impl UpnpObject for StateVarInstance {
fn as_upnp_object_type(&self) -> &UpnpObjectType {
return &self.object;
}
fn to_xml_element(&self) -> Element {
self.get_definition().to_xml_element()
}
}
impl StateVarInstance {
pub fn new(from: &StateVariable) -> Self {
Self {
object: UpnpObjectType {
name: from.object.name.clone(),
object_type: "StateVarInstance".to_string(),
},
definition: from.clone(),
value: from.get_default(),
old_value: from.get_default(),
last_modified: Utc::now(),
last_notification: Utc::now()
}
}
pub fn set_value(&mut self, new_value: StateValue) {
self.old_value = self.value.clone();
self.value = new_value;
self.last_modified = Utc::now(); // mise à jour automatique
}
/// Accès à la valeur
pub fn value(&self) -> &StateValue {
&self.value
}
/// Accès au timestamp
pub fn last_modified(&self) -> DateTime<Utc> {
self.last_modified
}
}

View File

@@ -0,0 +1,47 @@
mod errors;
mod variable_trait;
mod variable_methods;
mod instance_methods;
use std::{collections::HashMap, sync::{Arc, RwLock}};
use chrono::{DateTime, Utc};
pub use errors::{StateVariableError};
pub use crate::state_variables::variable_trait::UpnpVariable;
use crate::{value_ranges::ValueRange, variable_types::{StateValue, StateVarType}, UpnpObjectType};
/// Type pour les fonctions de condition d'événement
pub type StateConditionFunc = Arc<dyn Fn(&StateVarInstance) -> bool + Send + Sync>;
/// Type pour les fonctions de parsing de valeurs depuis des chaînes
pub type StringValueParser = Arc<dyn Fn(&str) -> Result<StateValue, StateVariableError> + Send + Sync>;
/// Type pour les fonctions de sérialisation de valeurs vers des chaînes
pub type ValueSerializer = Arc<dyn Fn(&StateValue) -> Result<String, StateVariableError> + Send + Sync>;
pub struct StateVariable {
object: UpnpObjectType,
value_type: StateVarType,
step: Option<StateValue>,
modifiable: bool,
event_conditions: Arc<RwLock<HashMap<String, StateConditionFunc>>>,
description: String,
default_value: Option<StateValue>,
value_range: Option<ValueRange>,
allowed_values: Arc<RwLock<Vec<StateValue>>>,
send_events: bool,
parse: Option<StringValueParser>,
marshal: Option<ValueSerializer>,
}
pub struct StateVarInstance {
object: UpnpObjectType,
definition: StateVariable,
value: StateValue,
old_value: StateValue,
last_modified: DateTime<Utc>,
last_notification: DateTime<Utc>,
}

View File

@@ -0,0 +1,353 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use xmltree::{Element, XMLNode};
use crate::{
UpnpObject, UpnpObjectType,
state_variables::{
StateConditionFunc, StateVariable, StringValueParser, ValueSerializer,
variable_trait::UpnpVariable,
},
value_ranges::ValueRange,
variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType},
};
impl UpnpObject for StateVariable {
fn as_upnp_object_type(&self) -> &UpnpObjectType {
return &self.object;
}
fn to_xml_element(&self) -> Element {
// Création de l'élément racine <stateVariable>
let mut root = Element::new("stateVariable");
root.attributes.insert(
"sendEvents".to_string(),
if self.send_events { "yes" } else { "no" }.to_string(),
);
// <name>
let mut name_elem = Element::new("name");
name_elem
.children
.push(XMLNode::Text(self.get_name().clone()));
// <dataType>
let mut datatype_elem = Element::new("dataType");
datatype_elem
.children
.push(XMLNode::Text(self.value_type.to_string())); // StateVarType doit impl Display
// <defaultValue> si défini
if let Some(default) = &self.default_value {
let mut def_elem = Element::new("defaultValue");
def_elem.children.push(XMLNode::Text(default.to_string()));
root.children.push(XMLNode::Element(def_elem));
}
// <allowedValueList> si défini
if let Ok(av) = self.allowed_values.read() {
if !av.is_empty() {
let mut list_elem = Element::new("allowedValueList");
for val in av.iter() {
let mut val_elem = Element::new("allowedValue");
val_elem.children.push(XMLNode::Text(val.to_string()));
list_elem.children.push(XMLNode::Element(val_elem));
}
root.children.push(XMLNode::Element(list_elem));
}
}
// <allowedValueRange> si défini
if let Some(range) = &self.value_range {
let mut range_elem = Element::new("allowedValueRange");
let mut min_elem = Element::new("minimum");
min_elem.children.push(XMLNode::Text(range.get_minimum().to_string()));
range_elem.children.push(XMLNode::Element(min_elem));
let mut max_elem = Element::new("maximum");
max_elem.children.push(XMLNode::Text(range.get_maximum().to_string()));
range_elem.children.push(XMLNode::Element(max_elem));
if let Some(step) = &self.step {
let mut step_elem = Element::new("step");
step_elem.children.push(XMLNode::Text(step.to_string()));
range_elem.children.push(XMLNode::Element(step_elem));
}
root.children.push(XMLNode::Element(range_elem));
}
// Ajouter les enfants communs
root.children.push(XMLNode::Element(name_elem));
root.children.push(XMLNode::Element(datatype_elem));
root
}
}
impl Clone for StateVariable {
fn clone(&self) -> Self {
// clone safe des structures protégées par RwLock en prenant un read lock
let event_conditions_clone = {
// si le lock est "poisoned" on panic - tu peux adapter la gestion si tu veux
let guard = self
.event_conditions
.read()
.expect("RwLock poisoned during clone");
// nécessite que Key: Clone, Value: Clone
Arc::new(RwLock::new(guard.clone()))
};
let allowed_values_clone = {
let guard = self
.allowed_values
.read()
.expect("RwLock poisoned during clone");
Arc::new(RwLock::new(guard.clone()))
};
Self {
object: self.object.clone(),
value_type: self.value_type.clone(),
step: self.step.clone(),
modifiable: self.modifiable,
event_conditions: event_conditions_clone,
description: self.description.clone(),
default_value: self.default_value.clone(),
value_range: self.value_range.clone(),
allowed_values: allowed_values_clone,
send_events: self.send_events,
// parse et marshal sont typiquement des Arc<dyn ...> — on clone l'Arc (shallow).
// Deep-cloner une closure ou un trait-objet n'est pas possible en général.
parse: self.parse.clone(),
marshal: self.marshal.clone(),
}
}
}
impl UpnpVarType for StateVariable {
fn as_state_var_type(&self) -> StateVarType {
self.value_type // utilise ton From<&StateValue> existant
}
}
impl UpnpVariable for StateVariable {
fn get_definition(&self) -> &StateVariable {
return self;
}
}
impl StateVariable {
pub fn new(vartype: StateVarType, name: String) -> StateVariable {
Self {
object: UpnpObjectType {
name,
object_type: "StateVariable".to_string(),
},
value_type: vartype.clone(),
step: None,
modifiable: true,
event_conditions: Arc::new(RwLock::new(HashMap::new())),
description: "".to_string(),
default_value: None,
value_range: None,
allowed_values: Arc::new(RwLock::new(Vec::new())),
send_events: false,
parse: None,
marshal: None,
}
}
pub fn set_step(&mut self, step: StateValue) -> Result<(), StateValueError> {
if self.as_state_var_type() != step.as_state_var_type() {
return Err(StateValueError::TypeError("Bad step type".to_string()));
}
self.step = Some(step);
Ok(())
}
pub fn set_range(&mut self, min: &StateValue, max: &StateValue) -> Result<(), StateValueError> {
if self.as_state_var_type() != min.as_state_var_type() {
return Err(StateValueError::TypeError("Bad range type".to_string()));
}
let range = ValueRange::new(min, max)?; // ? propage l'erreur si elle existe
self.value_range = Some(range);
Ok(())
}
pub fn update_minimum(&mut self, min: &StateValue) -> Result<(), StateValueError> {
if !self.has_range() {
return Err(StateValueError::RangeError(
"No range specified for this variable".to_string(),
));
}
if self
.value_range
.as_ref()
.expect("Range is not defined")
.as_state_var_type()
!= min.as_state_var_type()
{
return Err(StateValueError::TypeError(
"new minimum is not the same than state variable".to_string(),
));
}
self.value_range
.as_mut()
.expect("Range is not defined")
.set_minimum(min);
return Ok(());
}
pub fn update_maximum(&mut self, min: &StateValue) -> Result<(), StateValueError> {
if !self.has_range() {
return Err(StateValueError::RangeError(
"No range specified for this variable".to_string(),
));
}
if self
.value_range
.as_ref()
.expect("Range is not defined")
.as_state_var_type()
!= min.as_state_var_type()
{
return Err(StateValueError::TypeError(
"new minimum is not the same than state variable".to_string(),
));
}
self.value_range
.as_mut()
.expect("Range is not defined")
.set_maximum(min);
return Ok(());
}
pub fn get_range(&self) -> Option<&ValueRange> {
return self.value_range.as_ref();
}
pub fn set_modifiable(&mut self) {
self.modifiable = true;
}
pub fn set_not_modifiable(&mut self) {
self.modifiable = false;
}
pub fn add_event_condition(&self, name: String, func: StateConditionFunc) {
// on lock en écriture
let mut guard = self.event_conditions.write().unwrap();
guard.insert(name, func);
// le lock est automatiquement relâché ici (RAII)
}
pub fn remove_event_condition(&self, name: &str) {
let mut guard = self.event_conditions.write().unwrap();
guard.remove(name);
}
pub fn clear_event_conditions(&mut self) {
let mut guard = self.event_conditions.write().unwrap();
guard.clear()
}
pub fn set_description(&mut self, description: String) {
self.description = description;
}
pub fn set_default(&mut self, value: StateValue) -> Result<(), StateValueError> {
if self.as_state_var_type() != value.as_state_var_type() {
return Err(StateValueError::TypeError(
"value does not have the right type".to_string(),
));
}
self.default_value = Some(value.clone());
return Ok(());
}
pub fn unset_default(&mut self) {
self.default_value = None;
}
pub fn extend_allowed_values(&mut self, values: &[StateValue]) -> Result<(), StateValueError> {
let mut av = self
.allowed_values
.write()
.map_err(|_| StateValueError::TypeError("Lock poisoned".to_string()))?;
for v in values {
if self.as_state_var_type() == v.as_state_var_type() {
av.push(v.clone());
} else {
return Err(StateValueError::TypeError(
"new allowed value does not have the right type".to_string(),
));
}
}
Ok(())
}
pub fn push_allowed_value(&mut self, value: &StateValue) -> Result<(), StateValueError> {
let mut av = self
.allowed_values
.write()
.map_err(|_| StateValueError::TypeError("Lock poisoned".to_string()))?;
if self.as_state_var_type() == value.as_state_var_type() {
av.push(value.clone());
} else {
return Err(StateValueError::TypeError(
"new allowed value does not have the right type".to_string(),
));
}
return Ok(());
}
pub fn set_send_notification(&mut self) {
self.send_events = true;
}
pub fn unset_send_notification(&mut self) {
self.send_events = false;
}
pub fn set_value_parseur(&mut self, parser: StringValueParser) -> Result<(), StateValueError> {
if self.as_state_var_type() == StateVarType::String {
self.parse = Some(parser);
return Ok(());
}
return Err(StateValueError::TypeError(
"Only String variables can have a parser".to_string(),
));
}
pub fn unset_value_parser(&mut self) {
self.parse = None;
}
pub fn set_value_marshaler(
&mut self,
marshaler: ValueSerializer,
) -> Result<(), StateValueError> {
if self.as_state_var_type() == StateVarType::String {
self.marshal = Some(marshaler);
return Ok(());
}
return Err(StateValueError::TypeError(
"Only String variables can have a marshaler".to_string(),
));
}
pub fn unset_value_marshaler(&mut self) {
self.marshal = None;
}
}

View File

@@ -0,0 +1,73 @@
use crate::{
state_variables::StateVariable,
variable_types::{StateValue, UpnpVarType},
};
pub trait UpnpVariable {
fn get_definition(&self) -> &StateVariable;
fn has_step(&self) -> bool {
return self.get_definition().step.is_some();
}
fn get_step(&self) -> Option<StateValue> {
return self.get_definition().step.clone();
}
fn has_range(&self) -> bool {
return self.get_definition().value_range.is_some();
}
fn is_modifiable(&self) -> bool {
return self.get_definition().modifiable;
}
fn has_event_conditions(&self) -> bool {
return self.get_definition().event_conditions.read().unwrap().len() > 0;
}
fn has_event_condition(&self, name: &String) -> bool {
let guard = self.get_definition().event_conditions.read().unwrap();
return guard.contains_key(name);
}
fn has_description(&self) -> bool {
return !String::is_empty(&self.get_definition().description);
}
fn get_description(&self) -> String {
return self.get_definition().description.clone();
}
fn has_default(&self) -> bool {
return self.get_definition().default_value.is_some();
}
fn get_default(&self) -> StateValue {
self.get_definition()
.default_value
.clone()
.unwrap_or_else(|| self.get_definition().as_state_var_type().default_value())
}
fn has_allowed_values(&self) -> bool {
return self.get_definition().allowed_values.read().unwrap().len() > 0;
}
fn is_an_allowed_values(&self, value: &StateValue) -> bool {
let guard = self.get_definition().allowed_values.read().unwrap();
return guard.contains(value);
}
fn is_sending_notification(&self) -> bool {
self.get_definition().send_events
}
fn has_value_parser(&self) -> bool {
self.get_definition().parse.is_some()
}
fn has_value_marshaler(&self) -> bool {
self.get_definition().marshal.is_some()
}
}

View File

@@ -0,0 +1,69 @@
use std::cmp::Ordering;
use crate::{
value_ranges::ValueRange,
variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType},
};
impl UpnpVarType for ValueRange {
fn as_state_var_type(&self) -> StateVarType {
self.min.as_state_var_type() // utilise ton From<&StateValue> existant
}
}
impl ValueRange {
pub fn new(min: &StateValue, max: &StateValue) -> Result<Self, StateValueError> {
if min.as_state_var_type() != max.as_state_var_type() {
return Err(StateValueError::TypeError(
"min and max do not belong the same time".to_string(),
));
}
// Vérifier que min <= max
if let Some(cmp) = min.partial_cmp(max) {
if cmp == Ordering::Greater {
return Err(StateValueError::RangeError(
"Minimum cannot be greater than maximum".to_string(),
));
}
}
Ok(Self {
min: min.clone(),
max: max.clone(),
})
}
pub fn get_minimum(self: &ValueRange) -> StateValue {
return self.min.clone();
}
pub fn set_minimum(&mut self, value: &StateValue) {
self.min = value.clone()
}
pub fn get_maximum(self: &ValueRange) -> StateValue {
return self.max.clone();
}
pub fn set_maximum(&mut self, value: &StateValue) {
self.max = value.clone()
}
pub fn is_in_range(&self, value: &StateValue) -> bool {
if self.as_state_var_type() == value.as_state_var_type()
&& let Some(cmp) = self.min.partial_cmp(value)
{
if cmp == Ordering::Greater {
return false;
}
if let Some(cmp2) = self.max.partial_cmp(value) {
if cmp2 == Ordering::Less {
return false;
}
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,10 @@
mod methods;
use crate::variable_types::StateValue;
#[derive(Debug, Clone)]
pub struct ValueRange {
min: StateValue,
max: StateValue,
}

View File

@@ -0,0 +1,37 @@
use crate::variable_types::{StateValue, StateVarType, StateValueError};
use std::convert::TryFrom;
impl StateValue {
pub fn try_cast(&self, target: StateVarType) -> Result<StateValue, StateValueError> {
let source = StateVarType::from(self);
// Identité (même type)
if source == target {
return Ok(self.clone());
}
match (self, target) {
(val, StateVarType::String) => Ok(StateValue::String(val.to_string())),
(_,StateVarType::UI1) => Ok(StateValue::UI1(u8::try_from(self)?)),
(_,StateVarType::UI2) => Ok(StateValue::UI2(u16::try_from(self)?)),
(_,StateVarType::UI4) => Ok(StateValue::UI4(u32::try_from(self)?)),
(_,StateVarType::I1) => Ok(StateValue::I1(i8::try_from(self)?)),
(_,StateVarType::I2) => Ok(StateValue::I2(i16::try_from(self)?)),
(_,StateVarType::I4) => Ok(StateValue::I4(i32::try_from(self)?)),
(_,StateVarType::Int) => Ok(StateValue::Int(i32::try_from(self)?)),
(_,StateVarType::R8) => Ok(StateValue::R8(f64::try_from(self)?)),
(_,StateVarType::Number) => Ok(StateValue::Number(f64::try_from(self)?)),
(_,StateVarType::Fixed14_4) => Ok(StateValue::Fixed14_4(f64::try_from(self)?)),
(_,StateVarType::R4) => Ok(StateValue::R4(f32::try_from(self)?)),
// --- Pas encore implémenté pour les autres types ---
(val, target) => Err(StateValueError::TypeError(format!(
"Cannot cast {:?} to {:?}",
val, target
))),
}
}
}

View File

@@ -0,0 +1,43 @@
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
use url::Url;
use uuid::Uuid;
use crate::variable_types::{StateValue, StateVarType};
impl StateVarType {
pub fn default_value(&self) -> StateValue {
match self {
StateVarType::UI1 => StateValue::UI1(0),
StateVarType::UI2 => StateValue::UI2(0),
StateVarType::UI4 => StateValue::UI4(0),
StateVarType::I1 => StateValue::I1(0),
StateVarType::I2 => StateValue::I2(0),
StateVarType::I4 => StateValue::I4(0),
StateVarType::Int => StateValue::Int(0),
StateVarType::R4 => StateValue::R4(0.0),
StateVarType::R8 => StateValue::R8(0.0),
StateVarType::Number => StateValue::Number(0.0),
StateVarType::Fixed14_4 => StateValue::Fixed14_4(0.0),
StateVarType::Char => StateValue::Char('\0'),
StateVarType::String => StateValue::String(String::new()),
StateVarType::Boolean => StateValue::Boolean(false),
StateVarType::BinBase64 => StateValue::BinBase64(String::new()),
StateVarType::BinHex => StateValue::BinHex(String::new()),
StateVarType::Date => StateValue::Date(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()),
StateVarType::DateTime => StateValue::DateTime(DateTime::from_timestamp(0, 0).unwrap().naive_utc().into()),
StateVarType::DateTimeTZ => StateValue::DateTimeTZ(
DateTime::from_naive_utc_and_offset(
DateTime::from_timestamp(0, 0).unwrap().naive_utc(),
FixedOffset::east_opt(0).unwrap())
),
StateVarType::Time => StateValue::Time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
StateVarType::TimeTZ => StateValue::TimeTZ(
DateTime::from_naive_utc_and_offset(
DateTime::from_timestamp(0, 0).unwrap().naive_utc(),
FixedOffset::east_opt(0).unwrap())
),
StateVarType::UUID => StateValue::UUID(Uuid::nil()),
StateVarType::URI => StateValue::URI(Url::parse("http://localhost").unwrap()),
}
}
}

View File

@@ -0,0 +1,35 @@
use std::fmt;
use crate::variable_types::StateVarType;
impl fmt::Display for StateVarType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
StateVarType::UI1 => "ui1",
StateVarType::UI2 => "ui2",
StateVarType::UI4 => "ui4",
StateVarType::I1 => "i1",
StateVarType::I2 => "i2",
StateVarType::I4 => "i4",
StateVarType::Int => "int",
StateVarType::R4 => "r4",
StateVarType::R8 => "r8",
StateVarType::Number => "number",
StateVarType::Fixed14_4 => "fixed.14.4",
StateVarType::Char => "char",
StateVarType::String => "string",
StateVarType::Boolean => "boolean",
StateVarType::BinBase64 => "bin.base64",
StateVarType::BinHex => "bin.hex",
StateVarType::Date => "date",
StateVarType::DateTime => "dateTime",
StateVarType::DateTimeTZ => "dateTime.tz",
StateVarType::Time => "time",
StateVarType::TimeTZ => "time.tz",
StateVarType::UUID => "uuid",
StateVarType::URI => "uri",
};
write!(f, "{}", s)
}
}

View File

@@ -0,0 +1,45 @@
use std::fmt;
use base64::engine::general_purpose;
use base64::Engine;
use crate::variable_types::StateValue;
impl fmt::Display for StateValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
// Numériques
StateValue::UI1(v) => write!(f, "{}", v),
StateValue::UI2(v) => write!(f, "{}", v),
StateValue::UI4(v) => write!(f, "{}", v),
StateValue::I1(v) => write!(f, "{}", v),
StateValue::I2(v) => write!(f, "{}", v),
StateValue::I4(v) => write!(f, "{}", v),
StateValue::Int(v) => write!(f, "{}", v),
StateValue::R4(v) => write!(f, "{}", v),
StateValue::R8(v) => write!(f, "{}", v),
StateValue::Number(v) => write!(f, "{}", v),
StateValue::Fixed14_4(v) => write!(f, "{}", v),
// Types déjà Display
StateValue::Char(v) => write!(f, "{}", v),
StateValue::String(v) => write!(f, "{}", v),
StateValue::UUID(v) => write!(f, "{}", v),
StateValue::URI(v) => write!(f, "{}", v),
// Booléen : 1 ou 0
StateValue::Boolean(v) => write!(f, "{}", if *v { "1" } else { "0" }),
// Encodages binaires
StateValue::BinBase64(v) => write!(f, "{}", general_purpose::URL_SAFE.encode(v)),
StateValue::BinHex(v) => write!(f, "{}", hex::encode(v)),
// Dates et temps
StateValue::Date(v) => write!(f, "{}", v.format("%Y-%m-%d")),
StateValue::DateTime(v) => write!(f, "{}", v.format("%Y-%m-%dT%H:%M:%S")),
StateValue::DateTimeTZ(v) => write!(f, "{}", v.to_rfc3339()),
StateValue::Time(v) => write!(f, "{}", v.format("%H:%M:%S")),
StateValue::TimeTZ(v) => write!(f, "{}", v.format("%H:%M:%S%z")),
}
}
}

View File

@@ -0,0 +1,28 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StateValueError {
#[error("Conversion error: {0}")]
ConversionError(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Range error: {0}")]
RangeError(String),
#[error("Type error: {0}")]
TypeError(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Event condition error: {0}")]
EventConditionError(String),
#[error("Arithmetic error: {0}")]
ArithmeticError(String),
#[error("Unknown error: {0}")]
Unknown(String),
}

View File

@@ -0,0 +1,36 @@
use std::str::FromStr;
use crate::variable_types::StateVarType;
impl FromStr for StateVarType {
type Err = String; // Type d'erreur personnalisé
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"ui1" => Ok(StateVarType::UI1),
"ui2" => Ok(StateVarType::UI2),
"ui4" => Ok(StateVarType::UI4),
"i1" => Ok(StateVarType::I1),
"i2" => Ok(StateVarType::I2),
"i4" => Ok(StateVarType::I4),
"int" => Ok(StateVarType::Int),
"r4" => Ok(StateVarType::R4),
"r8" => Ok(StateVarType::R8),
"number" => Ok(StateVarType::Number),
"fixed.14.4" => Ok(StateVarType::Fixed14_4),
"char" => Ok(StateVarType::Char),
"string" => Ok(StateVarType::String),
"boolean" => Ok(StateVarType::Boolean),
"bin.base64" => Ok(StateVarType::BinBase64),
"bin.hex" => Ok(StateVarType::BinHex),
"date" => Ok(StateVarType::Date),
"datetime" => Ok(StateVarType::DateTime),
"datetime.tz" => Ok(StateVarType::DateTimeTZ),
"time" => Ok(StateVarType::Time),
"time.tz" => Ok(StateVarType::TimeTZ),
"uuid" => Ok(StateVarType::UUID),
"uri" => Ok(StateVarType::URI),
_ => Err(format!("Type inconnu: {}", s)),
}
}
}

View File

@@ -0,0 +1,96 @@
mod type_methods;
mod value_methods;
mod type_trait;
mod value_trait;
mod fromstr;
mod cast;
mod display_type;
mod display_value;
mod default_value;
mod errors;
mod values_from_type;
mod values_from_u8;
mod values_from_u16;
mod values_from_u32;
mod values_from_i8;
mod values_from_i16;
mod values_from_i32;
mod values_from_i64;
mod values_from_f32;
mod values_from_f64;
mod values_from_uri;
mod values_from_uuid;
mod values_from_naivedate;
mod values_from_naivedatetime;
mod values_from_naivetime;
mod values_from_datetime;
mod values_from_vec_u8;
use std::fmt::{Debug};
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime};
use url::Url;
use uuid::Uuid;
pub use errors::StateValueError;
pub use type_trait::UpnpVarType;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StateVarType {
UI1, // Unsigned 8-bit integer
UI2, // Unsigned 16-bit integer
UI4, // Unsigned 32-bit integer
I1, // Signed 8-bit integer
I2, // Signed 16-bit integer
I4, // Signed 32-bit integer
Int, // Synonymous with i4
R4, // 32-bit floating point
R8, // 64-bit floating point
Number, // Synonymous with r8
Fixed14_4, // Fixed-point decimal
Char, // Single Unicode character
String, // Character string
Boolean, // Boolean value
BinBase64, // Base64-encoded binary
BinHex, // Hex-encoded binary
Date, // Date (YYYY-MM-DD)
DateTime, // DateTime without timezone
DateTimeTZ, // DateTime with timezone
Time, // Time without timezone
TimeTZ, // Time with timezone
UUID, // Universally unique identifier
URI, // Uniform Resource Identifier
}
#[derive(Debug, Clone)]
pub enum StateValue {
UI1(u8),
UI2(u16),
UI4(u32),
I1(i8),
I2(i16),
I4(i32),
Int(i32),
R4(f32),
R8(f64),
Number(f64),
Fixed14_4(f64),
Char(char),
String(String),
Boolean(bool),
BinBase64(String),
BinHex(String),
Date(NaiveDate),
DateTime(NaiveDateTime),
DateTimeTZ(DateTime<FixedOffset>),
Time(NaiveTime),
TimeTZ(DateTime<FixedOffset>),
UUID(Uuid),
URI(Url),
}

View File

@@ -0,0 +1,109 @@
use crate::variable_types::{type_trait::UpnpVarType, StateVarType};
impl UpnpVarType for StateVarType {
fn as_state_var_type(&self) -> StateVarType {
*self
}
fn bit_size(&self) -> Option<usize> {
match self {
StateVarType::UI1 | StateVarType::I1 => Some(8),
StateVarType::UI2 | StateVarType::I2 => Some(16),
StateVarType::UI4 | StateVarType::I4 | StateVarType::Int | StateVarType::R4 => Some(32),
StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => Some(64),
_ => None,
}
}
fn is_numeric(&self) -> bool {
matches!(
self,
StateVarType::UI1
| StateVarType::UI2
| StateVarType::UI4
| StateVarType::I1
| StateVarType::I2
| StateVarType::I4
| StateVarType::Int
| StateVarType::R4
| StateVarType::R8
| StateVarType::Number
| StateVarType::Fixed14_4
)
}
fn is_integer(&self) -> bool {
matches!(
self,
StateVarType::UI1
| StateVarType::UI2
| StateVarType::UI4
| StateVarType::I1
| StateVarType::I2
| StateVarType::I4
| StateVarType::Int
)
}
fn is_signed_int(&self) -> bool {
matches!(
self,
StateVarType::I1 | StateVarType::I2 | StateVarType::I4 | StateVarType::Int
)
}
fn is_unsigned_int(&self) -> bool {
matches!(
self,
StateVarType::UI1 | StateVarType::UI2 | StateVarType::UI4
)
}
fn is_float(&self) -> bool {
matches!(
self,
StateVarType::R4
| StateVarType::R8
| StateVarType::Number
| StateVarType::Fixed14_4
)
}
fn is_bool(&self) -> bool {
matches!(self, StateVarType::Boolean)
}
fn is_string(&self) -> bool {
matches!(self, StateVarType::String
| StateVarType::Char
| StateVarType::BinHex
| StateVarType::BinBase64)
}
fn is_time(&self) -> bool {
matches!(
self,
StateVarType::Date
| StateVarType::DateTime
| StateVarType::DateTimeTZ
| StateVarType::Time
| StateVarType::TimeTZ
)
}
fn is_uuid(&self) -> bool {
matches!(self, StateVarType::UUID)
}
fn is_uri(&self) -> bool {
matches!(self, StateVarType::URI)
}
fn is_binary(&self) -> bool {
matches!(self, StateVarType::BinBase64 | StateVarType::BinHex)
}
fn is_comparable(&self) -> bool {
!self.is_binary()
}
}

View File

@@ -0,0 +1,57 @@
use crate::variable_types::StateVarType;
pub trait UpnpVarType {
fn as_state_var_type(&self) -> StateVarType;
fn bit_size(&self) -> Option<usize> {
self.as_state_var_type().bit_size()
}
fn is_numeric(&self) -> bool {
self.as_state_var_type().is_numeric()
}
fn is_integer(&self) -> bool {
self.as_state_var_type().is_integer()
}
fn is_signed_int(&self) -> bool {
self.as_state_var_type().is_signed_int()
}
fn is_unsigned_int(&self) -> bool {
self.as_state_var_type().is_unsigned_int()
}
fn is_float(&self) -> bool {
self.as_state_var_type().is_float()
}
fn is_bool(&self) -> bool {
self.as_state_var_type().is_bool()
}
fn is_string(&self) -> bool {
self.as_state_var_type().is_string()
}
fn is_time(&self) -> bool {
self.as_state_var_type().is_time()
}
fn is_uuid(&self) -> bool {
self.as_state_var_type().is_uuid()
}
fn is_uri(&self) -> bool {
self.as_state_var_type().is_uri()
}
fn is_binary(&self) -> bool {
self.as_state_var_type().is_binary()
}
fn is_comparable(&self) -> bool {
self.as_state_var_type().is_comparable()
}
}

View File

@@ -0,0 +1,89 @@
use std::cmp::Ordering;
use crate::variable_types::{StateValue, StateVarType, type_trait::UpnpVarType};
impl UpnpVarType for StateValue {
fn as_state_var_type(&self) -> StateVarType {
StateVarType::from(self) // utilise ton From<&StateValue> existant
}
}
impl PartialEq for StateValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(a, b) if a.is_integer() && b.is_integer() => {
if let (Ok(ia), Ok(ib)) = (i64::try_from(a), i64::try_from(b)) {
return ia == ib;
};
return false;
}
(a, b) if a.is_float() && b.is_float() => {
if let (Ok(a), Ok(b)) = (f64::try_from(self), f64::try_from(other)) {
return a == b; // NaN respecte la sémantique IEEE (NaN != NaN)
}
return false;
}
(a, b) if a.is_string() && b.is_string() => {
let (a, b) = (self.to_string(), other.to_string());
return a == b; // NaN respecte la sémantique IEEE (NaN != NaN)
}
(StateValue::Date(a), StateValue::Date(b)) => {
return a == b;
}
(StateValue::Time(a), StateValue::Time(b)) => {
return a == b;
}
(StateValue::DateTime(a), StateValue::DateTime(b)) => {
return a == b;
}
(StateValue::DateTimeTZ(a), StateValue::DateTimeTZ(b)) => {
return a == b;
}
(StateValue::TimeTZ(a), StateValue::TimeTZ(b)) => {
return a == b;
}
(_, _) => return false,
}
}
}
impl PartialOrd for StateValue {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(a, b) if a.is_integer() && b.is_integer() => {
if let (Ok(ia), Ok(ib)) = (i64::try_from(a), i64::try_from(b)) {
return Some(ia.cmp(&ib));
};
return None;
}
(a, b) if a.is_float() && b.is_float() => {
if let (Ok(a), Ok(b)) = (f64::try_from(self), f64::try_from(other)) {
return a.partial_cmp(&b); // NaN respecte la sémantique IEEE (NaN != NaN)
}
return None;
}
(a, b) if a.is_string() && b.is_string() => {
let (a, b) = (self.to_string(), other.to_string());
return Some(a.cmp(&b)); // NaN respecte la sémantique IEEE (NaN != NaN)
}
(StateValue::Date(a), StateValue::Date(b)) => {
return Some(a.cmp(&b));
}
(StateValue::Time(a), StateValue::Time(b)) => {
return Some(a.cmp(&b));
}
(StateValue::DateTime(a), StateValue::DateTime(b)) => {
return Some(a.cmp(&b));
}
(StateValue::DateTimeTZ(a), StateValue::DateTimeTZ(b)) => {
return Some(a.cmp(&b));
}
(StateValue::TimeTZ(a), StateValue::TimeTZ(b)) => {
return Some(a.cmp(&b));
}
(_, _) => return None,
}
}
}

View File

@@ -0,0 +1,2 @@
pub trait UpnpValue {
}

View File

@@ -0,0 +1,33 @@
use std::convert::TryFrom;
use chrono::{DateTime,FixedOffset};
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<&StateValue> for DateTime<FixedOffset> {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
StateValue::DateTimeTZ(v) => Ok(v.clone()),
StateValue::TimeTZ(v) => Ok(v.clone()),
StateValue::String(v) => DateTime::parse_from_rfc3339(v)
.map_err(|e| StateValueError::ParseError(format!("Cannot parse DateTimeTZ from string '{}': {}", v, e))),
_ => Err(StateValueError::TypeError("Cannot cast to DateTime<FixedOffset>".into())),
}
}
}
impl TryFrom<StateValue> for DateTime<FixedOffset> {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
DateTime::<FixedOffset>::try_from(&value)
}
}
impl From<DateTime<FixedOffset>> for StateValue {
fn from(value: DateTime<FixedOffset>) -> Self {
StateValue::DateTimeTZ(value)
}
}

View File

@@ -0,0 +1,82 @@
use std::convert::TryFrom;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<&StateValue> for f32 {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
const MAX_EXACT: i32 = 1 << f32::MANTISSA_DIGITS;
match value {
// --- Signed integers ---
StateValue::I1(v) => Ok(*v as f32),
StateValue::I2(v) => Ok(*v as f32),
StateValue::I4(v)
if *v > -MAX_EXACT &&
*v < MAX_EXACT => Ok(*v as f32),
StateValue::Int(v)
if *v >= -MAX_EXACT &&
*v <= MAX_EXACT as i32 => Ok(*v as f32),
// --- Unsigned integers ---
StateValue::UI1(v) => Ok(*v as f32),
StateValue::UI2(v) => Ok(*v as f32),
StateValue::UI4(v) if *v <= MAX_EXACT as u32 => Ok(*v as f32),
StateValue::UI4(_) => Err(StateValueError::TypeError(
"Cannot cast UI4 to f32: out of range".into(),
)),
// --- Floats ---
StateValue::R4(v) => Ok(*v), // déjà un f32
StateValue::R8(v)
if ! v.is_finite() ||
(*v <= f32::MAX as f64 &&
*v >= f32::MIN as f64) => Ok(*v as f32),
StateValue::R8(_) => Err(StateValueError::TypeError(
"Cannot cast R8 to f32: out of range".into(),
)),
StateValue::Number(v)
if ! v.is_finite() ||
(*v <= f32::MAX as f64 &&
*v >= f32::MIN as f64) => Ok(*v as f32),
StateValue::Number(_) => Err(StateValueError::TypeError(
"Cannot cast Number to f32: out of range".into(),
)),
StateValue::Fixed14_4(v)
if ! v.is_finite() ||
(*v <= f32::MAX as f64 &&
*v >= f32::MIN as f64) => Ok(*v as f32),
StateValue::Fixed14_4(_) => Err(StateValueError::TypeError(
"Cannot cast Fixed14_4 to f32: out of range".into(),
)),
StateValue::Boolean(v) => Ok((*v as i32) as Self),
StateValue::String(s) => s.parse::<f32>().map_err(|_| {
StateValueError::TypeError(format!("Cannot parse '{}' as f32", s))
}),
// --- Par défaut : erreur ---
_ => Err(StateValueError::TypeError(
"Cannot cast to f32".into(),
)),
}
}
}
impl TryFrom<StateValue> for f32 {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
f32::try_from(&value)
}
}
impl From<f32> for StateValue {
fn from(value: f32) -> Self {
StateValue::R4(value)
}
}

View File

@@ -0,0 +1,53 @@
use std::convert::TryFrom;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<&StateValue> for f64 {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
// --- Signed integers ---
StateValue::I1(v) => Ok(*v as f64),
StateValue::I2(v) => Ok(*v as f64),
StateValue::I4(v) => Ok(*v as f64),
StateValue::Int(v) => Ok(*v as f64),
// --- Unsigned integers ---
StateValue::UI1(v) => Ok(*v as f64),
StateValue::UI2(v) => Ok(*v as f64),
StateValue::UI4(v) => Ok(*v as f64),
// --- Floats ---
StateValue::R4(v) => Ok(*v as f64),
StateValue::R8(v) => Ok(*v),
StateValue::Number(v) => Ok(*v),
StateValue::Fixed14_4(v) => Ok(*v),
StateValue::Boolean(v) => Ok((*v as i32) as Self),
StateValue::String(s) => s.parse::<f64>().map_err(|_| {
StateValueError::TypeError(format!("Cannot parse '{}' as f64", s))
}),
// --- Par défaut : erreur ---
_ => Err(StateValueError::TypeError(
"Cannot cast to f64".into(),
)),
}
}
}
impl TryFrom<StateValue> for f64 {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
f64::try_from(&value)
}
}
impl From<f64> for StateValue {
fn from(value: f64) -> Self {
StateValue::R8(value)
}
}

View File

@@ -0,0 +1,46 @@
use std::convert::TryFrom;
use crate::variable_types::{StateValue, StateValueError};
// Implémentations TryFrom<StateValue> pour types numériques
impl TryFrom<&StateValue> for i16 {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
StateValue::I1(v) => Ok(*v as i16),
StateValue::I2(v) => Ok(*v),
StateValue::I4(v) if *v <= i16::MAX as i32 && *v >= i16::MIN as i32 => Ok(*v as i16),
StateValue::Int(v) if *v <= i16::MAX as i32 && *v >= i16::MIN as i32 => Ok(*v as i16),
StateValue::UI1(v) => Ok(*v as i16),
StateValue::UI2(v) if *v <= i16::MAX as u16 => Ok(*v as i16),
StateValue::UI4(v) if *v <= i16::MAX as u32 => Ok(*v as i16),
StateValue::Boolean(v) => Ok(*v as Self),
StateValue::String(s) => s.parse::<i16>().map_err(|_| {
StateValueError::TypeError(format!("Cannot parse '{}' as i16", s))
}),
_ => Err(StateValueError::TypeError("Cannot cast to i32".into())),
}
}
}
impl TryFrom<StateValue> for i16 {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
i16::try_from(&value)
}
}
impl From<i16> for StateValue {
fn from(value: i16) -> Self {
StateValue::I2(value)
}
}

View File

@@ -0,0 +1,46 @@
use std::convert::TryFrom;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<&StateValue> for i32 {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
// signés
StateValue::I1(v) => Ok(*v as i32),
StateValue::I2(v) => Ok(*v as i32),
StateValue::I4(v) => Ok(*v),
StateValue::Int(v) => Ok(*v),
// non signés
StateValue::UI1(v) => Ok(*v as i32),
StateValue::UI2(v) => Ok(*v as i32),
StateValue::UI4(v) if *v <= i32::MAX as u32 =>Ok(*v as i32),
StateValue::Boolean(v) => Ok(*v as Self),
StateValue::String(s) => s.parse::<i32>().map_err(|_| {
StateValueError::TypeError(format!("Cannot parse '{}' as i32", s))
}),
_ => Err(StateValueError::TypeError("Cannot cast to i32".into())),
}
}
}
impl TryFrom<StateValue> for i32 {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
i32::try_from(&value)
}
}
impl From<i32> for StateValue {
fn from(value: i32) -> Self {
StateValue::I4(value)
}
}

View File

@@ -0,0 +1,45 @@
use std::convert::TryFrom;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<&StateValue> for i64 {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
// signés
StateValue::I1(v) => Ok(*v as i64),
StateValue::I2(v) => Ok(*v as i64),
StateValue::I4(v) => Ok(*v as i64),
StateValue::Int(v) => Ok(*v as i64),
// non signés
StateValue::UI1(v) => Ok(*v as i64),
StateValue::UI2(v) => Ok(*v as i64),
StateValue::UI4(v) => Ok(*v as i64), // toujours dans l'intervalle d'un i64
// booléen
StateValue::Boolean(v) => Ok(*v as i64),
// chaîne → i64
StateValue::String(s) => s.parse::<i64>().map_err(|_| {
StateValueError::TypeError(format!("Cannot parse '{}' as i64", s))
}),
_ => Err(StateValueError::TypeError("Cannot cast to i64".into())),
}
}
}
impl TryFrom<StateValue> for i64 {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
i64::try_from(&value)
}
}
impl From<i64> for StateValue {
fn from(value: i64) -> Self {
StateValue::Int(value as i32) // ⚠️ choix à discuter : Int est i32, pas i64
}
}

View File

@@ -0,0 +1,46 @@
use std::convert::TryFrom;
use crate::variable_types::{StateValue, StateValueError};
// Implémentations TryFrom<StateValue> pour types numériques
impl TryFrom<&StateValue> for i8 {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
StateValue::I1(v) if *v >= 0 => Ok(*v as i8),
StateValue::I2(v) if *v <= i8::MAX as i16 && *v >= i8::MIN as i16 => Ok(*v as i8),
StateValue::I4(v) if *v <= i8::MAX as i32 && *v >= i8::MIN as i32=> Ok(*v as i8),
StateValue::Int(v) if *v <= i8::MAX as i32 && *v >= i8::MIN as i32=> Ok(*v as i8),
StateValue::UI1(v) if *v <= i8::MAX as u8 => Ok(*v as i8),
StateValue::UI2(v) if *v <= i8::MAX as u16 => Ok(*v as i8),
StateValue::UI4(v) if *v <= i8::MAX as u32 => Ok(*v as i8),
StateValue::Boolean(v) => Ok(*v as Self),
StateValue::String(s) => s.parse::<i8>().map_err(|_| {
StateValueError::TypeError(format!("Cannot parse '{}' as i8", s))
}),
_ => Err(StateValueError::TypeError("Cannot cast to i8".into())),
}
}
}
impl TryFrom<StateValue> for i8 {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
i8::try_from(&value)
}
}
impl From<i8> for StateValue {
fn from(value: i8) -> Self {
StateValue::I1(value)
}
}

View File

@@ -0,0 +1,32 @@
use std::convert::TryFrom;
use chrono::NaiveDate;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<&StateValue> for NaiveDate {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
StateValue::Date(v) => Ok(v.clone()),
StateValue::String(v) => NaiveDate::parse_from_str(v, "%Y-%m-%d")
.map_err(|e| StateValueError::ParseError(format!("Cannot parse Date from string '{}': {}", v, e))),
_ => Err(StateValueError::TypeError("Cannot cast to NaiveDate".into())),
}
}
}
impl TryFrom<StateValue> for NaiveDate {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
NaiveDate::try_from(&value)
}
}
impl From<NaiveDate> for StateValue {
fn from(value: NaiveDate) -> Self {
StateValue::Date(value)
}
}

View File

@@ -0,0 +1,31 @@
use std::convert::TryFrom;
use chrono::NaiveDateTime;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<&StateValue> for NaiveDateTime {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
StateValue::DateTime(v) => Ok(v.clone()),
StateValue::String(v) => NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S")
.map_err(|e| StateValueError::ParseError(format!("Cannot parse DateTime from string '{}': {}", v, e))),
_ => Err(StateValueError::TypeError("Cannot cast to NaiveDateTime".into())),
}
}
}
impl TryFrom<StateValue> for NaiveDateTime {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
NaiveDateTime::try_from(&value)
}
}
impl From<NaiveDateTime> for StateValue {
fn from(value: NaiveDateTime) -> Self {
StateValue::DateTime(value)
}
}

View File

@@ -0,0 +1,31 @@
use std::convert::TryFrom;
use chrono::NaiveTime;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<&StateValue> for NaiveTime {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
StateValue::Time(v) => Ok(v.clone()),
StateValue::String(v) => NaiveTime::parse_from_str(&v, "%H:%M:%S")
.map_err(|e| StateValueError::ParseError(format!("Cannot parse Time from string '{}': {}", v, e))),
_ => Err(StateValueError::TypeError("Cannot cast to NaiveTime".into())),
}
}
}
impl TryFrom<StateValue> for NaiveTime {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
NaiveTime::try_from(&value)
}
}
impl From<NaiveTime> for StateValue {
fn from(value: NaiveTime) -> Self {
StateValue::Time(value)
}
}

View File

@@ -0,0 +1,37 @@
use crate::variable_types::{StateValue, StateVarType};
impl From<&StateValue> for StateVarType {
fn from(value: &StateValue) -> Self {
match value {
StateValue::UI1(_) => StateVarType::UI1,
StateValue::UI2(_) => StateVarType::UI2,
StateValue::UI4(_) => StateVarType::UI4,
StateValue::I1(_) => StateVarType::I1,
StateValue::I2(_) => StateVarType::I2,
StateValue::I4(_) => StateVarType::I4,
StateValue::Int(_) => StateVarType::Int,
StateValue::R4(_) => StateVarType::R4,
StateValue::R8(_) => StateVarType::R8,
StateValue::Number(_) => StateVarType::Number,
StateValue::Fixed14_4(_) => StateVarType::Fixed14_4,
StateValue::Char(_) => StateVarType::Char,
StateValue::String(_) => StateVarType::String,
StateValue::BinBase64(_) => StateVarType::BinBase64,
StateValue::BinHex(_) => StateVarType::BinHex,
StateValue::URI(_) => StateVarType::URI,
StateValue::UUID(_) => StateVarType::UUID,
StateValue::Date(_) => StateVarType::Date,
StateValue::DateTime(_) => StateVarType::DateTime,
StateValue::DateTimeTZ(_) => StateVarType::DateTimeTZ,
StateValue::Time(_) => StateVarType::Time,
StateValue::TimeTZ(_) => StateVarType::TimeTZ,
StateValue::Boolean(_) => StateVarType::Boolean,
}
}
}
impl From<StateValue> for StateVarType {
fn from(value: StateValue) -> Self {
StateVarType::from(&value)
}
}

View File

@@ -0,0 +1,47 @@
use std::convert::TryFrom;
use crate::variable_types::{StateValue, StateValueError};
// Implémentations TryFrom<StateValue> pour types numériques
impl TryFrom<&StateValue> for u16 {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
StateValue::UI1(v) => Ok(*v as Self),
StateValue::UI2(v) => Ok(*v),
StateValue::UI4(v) if *v <= i16::MAX as u32 => Ok(*v as Self),
StateValue::I1(v) if *v >= 0 => Ok(*v as Self),
StateValue::I2(v) if *v >= 0 => Ok(*v as Self),
StateValue::I4(v) if *v >= 0 && *v <= u16::MAX as i32 => Ok(*v as Self),
StateValue::Int(v) if *v >= 0 && *v <= u16::MAX as i32 => Ok(*v as Self),
StateValue::Boolean(v) => Ok(*v as Self),
StateValue::String(s) => s.parse::<u16>().map_err(|_| {
StateValueError::TypeError(format!("Cannot parse '{}' as u16", s))
}),
_ => Err(StateValueError::TypeError("Cannot cast to u16".into())),
}
}
}
impl TryFrom<StateValue> for u16 {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
u16::try_from(&value)
}
}
impl From<u16> for StateValue {
fn from(value: u16) -> Self {
StateValue::UI2(value)
}
}

View File

@@ -0,0 +1,45 @@
use std::convert::TryFrom;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<&StateValue> for u32 {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
StateValue::UI1(v) => Ok(*v as Self),
StateValue::UI2(v) => Ok(*v as Self),
StateValue::UI4(v) => Ok(*v),
StateValue::I1(v) if *v >= 0 => Ok(*v as Self),
StateValue::I2(v) if *v >= 0 => Ok(*v as Self),
StateValue::I4(v) if *v >= 0 => Ok(*v as Self),
StateValue::Int(v) if *v >= 0 => Ok(*v as Self),
StateValue::Boolean(v) => Ok(*v as Self),
StateValue::String(s) => s.parse::<u32>().map_err(|_| {
StateValueError::TypeError(format!("Cannot parse '{}' as u32", s))
}),
_ => Err(StateValueError::TypeError("Cannot cast to u32".into())),
}
}
}
impl TryFrom<StateValue> for u32 {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
u32::try_from(&value)
}
}
impl From<u32> for StateValue {
fn from(value: u32) -> Self {
StateValue::UI4(value)
}
}

View File

@@ -0,0 +1,45 @@
use std::convert::TryFrom;
use crate::variable_types::{StateValue, StateValueError};
// Implémentations TryFrom<StateValue> pour types numériques
impl TryFrom<&StateValue> for u8 {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
StateValue::UI1(v) => Ok(*v),
StateValue::UI2(v) if *v <= u8::MAX as u16 => Ok(*v as u8),
StateValue::UI4(v) if *v <= i8::MAX as u32 => Ok(*v as u8),
StateValue::I1(v) if *v >= 0 => Ok(*v as u8),
StateValue::I2(v) if *v >= 0 && *v <= u8::MAX as i16 => Ok(*v as u8),
StateValue::I4(v) if *v >= 0 && *v <= u8::MAX as i32 => Ok(*v as u8),
StateValue::Int(v) if *v >= 0 && *v <= u8::MAX as i32 => Ok(*v as u8),
StateValue::Boolean(v) => Ok(*v as Self),
StateValue::String(s) => s.parse::<u8>().map_err(|_| {
StateValueError::TypeError(format!("Cannot parse '{}' as u8", s))
}),
_ => Err(StateValueError::TypeError("Cannot cast to u8".into())),
}
}
}
impl TryFrom<StateValue> for u8 {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
u8::try_from(&value)
}
}
impl From<u8> for StateValue {
fn from(value: u8) -> Self {
StateValue::UI1(value)
}
}

View File

@@ -0,0 +1,31 @@
use std::convert::TryFrom;
use url::Url;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<StateValue> for Url {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
match value {
// Si déjà un URI encodé comme StateValue::URI
StateValue::URI(v) => Ok(v),
// Si c'est une String, on tente un parse
StateValue::String(v) => {
Url::parse(&v)
.map_err(|_| StateValueError::TypeError("Invalid URI string".into()))
}
// Autres types : erreur
_ => Err(StateValueError::TypeError("Cannot cast to Url".into())),
}
}
}
impl From<Url> for StateValue {
fn from(value: Url) -> Self {
StateValue::URI(value)
}
}

View File

@@ -0,0 +1,31 @@
use std::convert::TryFrom;
use uuid::Uuid;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<StateValue> for Uuid {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
match value {
// Si déjà un URI encodé comme StateValue::URI
StateValue::UUID(v) => Ok(v),
// Si c'est une String, on tente un parse
StateValue::String(v) => {
Uuid::parse_str(&v)
.map_err(|_| StateValueError::TypeError("Invalid UUID string".into()))
}
// Autres types : erreur
_ => Err(StateValueError::TypeError("Cannot cast to Uuid".into())),
}
}
}
impl From<Uuid> for StateValue {
fn from(value: Uuid) -> Self {
StateValue::UUID(value)
}
}

View File

@@ -0,0 +1,45 @@
use std::convert::TryFrom;
use crate::variable_types::{StateValue, StateValueError};
use base64::{engine::general_purpose::STANDARD, Engine as _};
impl TryFrom<&StateValue> for Vec<u8> {
type Error = StateValueError;
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
match value {
// Déjà un vecteur binaire
StateValue::BinBase64(v) => STANDARD.decode(v).map_err(
|e| StateValueError::ParseError(format!("Base64 decode error: {}", e))),
StateValue::BinHex(v) => hex::decode(v).map_err(
|e| StateValueError::ParseError(format!("BinHex decode error: {}", e))),
// Conversion depuis une chaîne encodée
StateValue::String(s) => {
// Essayer Base64
if let Ok(bytes) = STANDARD.decode(s) {
return Ok(bytes);
}
// Essayer Hex
if let Ok(bytes) = hex::decode(s) {
return Ok(bytes);
}
Err(StateValueError::ParseError(format!(
"Cannot parse string '{}' as binary",
s
)))
}
_ => Err(StateValueError::TypeError(
"Cannot cast to binary Vec<u8>".into(),
)),
}
}
}
impl TryFrom<StateValue> for Vec<u8> {
type Error = StateValueError;
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
Vec::<u8>::try_from(&value)
}
}