amélioration de la webapp
This commit is contained in:
@@ -97,7 +97,8 @@ pub type ActionData = HashMap<String, Box<dyn Reflect>>;
|
||||
/// - Le handler retourne les données modifiées (ActionData unifié pour entrée/sortie)
|
||||
/// - Rarement utilisé directement (la macro `action_handler!` s'en charge)
|
||||
/// - Nécessaire pour la compatibilité avec les trait objects
|
||||
pub type ActionFuture = Pin<Box<dyn Future<Output = Result<ActionData, crate::actions::ActionError>> + Send>>;
|
||||
pub type ActionFuture =
|
||||
Pin<Box<dyn Future<Output = Result<ActionData, crate::actions::ActionError>> + Send>>;
|
||||
|
||||
/// Handler d'action UPnP asynchrone.
|
||||
///
|
||||
|
||||
@@ -6,20 +6,9 @@ use std::{
|
||||
use bevy_reflect::Reflect;
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::{
|
||||
UpnpInstance,
|
||||
UpnpObject,
|
||||
UpnpObjectType,
|
||||
UpnpTyped,
|
||||
UpnpTypedInstance,
|
||||
};
|
||||
use crate::actions::{
|
||||
Action,
|
||||
ActionData,
|
||||
ActionInstance,
|
||||
ArgInstanceSet,
|
||||
};
|
||||
use crate::actions::{Action, ActionData, ActionInstance, ArgInstanceSet};
|
||||
use crate::variable_types::StateValue;
|
||||
use crate::{UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance};
|
||||
|
||||
impl UpnpObject for ActionInstance {
|
||||
fn to_xml_element(&self) -> Element {
|
||||
@@ -27,7 +16,9 @@ impl UpnpObject for ActionInstance {
|
||||
|
||||
// <name>
|
||||
let mut name_elem = Element::new("name");
|
||||
name_elem.children.push(XMLNode::Text(self.get_name().clone()));
|
||||
name_elem
|
||||
.children
|
||||
.push(XMLNode::Text(self.get_name().clone()));
|
||||
elem.children.push(XMLNode::Element(name_elem));
|
||||
|
||||
// Utiliser le set d'instances d'arguments
|
||||
@@ -35,7 +26,7 @@ impl UpnpObject for ActionInstance {
|
||||
elem.children.push(XMLNode::Element(args_container));
|
||||
|
||||
elem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UpnpTyped for ActionInstance {
|
||||
@@ -45,35 +36,31 @@ impl UpnpTyped for ActionInstance {
|
||||
}
|
||||
|
||||
impl UpnpInstance for ActionInstance {
|
||||
|
||||
type Model = Action;
|
||||
|
||||
fn new(action: &Action) -> Self {
|
||||
// Créer les instances d'arguments
|
||||
let mut arguments = ArgInstanceSet::new();
|
||||
|
||||
|
||||
for arg_model in action.arguments().all() {
|
||||
let arg_instance = Arc::new(crate::actions::ArgumentInstance::new(&*arg_model));
|
||||
if let Err(e) = arguments.insert(arg_instance) {
|
||||
tracing::error!("Failed to insert argument instance: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Self {
|
||||
object: UpnpObjectType {
|
||||
name: action.get_name().clone(),
|
||||
object_type: "ActionInstance".to_string(),
|
||||
},
|
||||
model: action.clone(),
|
||||
arguments, // ⬅️ Set d'instances, pas le modèle !
|
||||
arguments, // ⬅️ Set d'instances, pas le modèle !
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
impl UpnpTypedInstance for ActionInstance {
|
||||
|
||||
fn get_model(&self) -> &Self::Model {
|
||||
&self.model
|
||||
}
|
||||
@@ -131,7 +118,7 @@ impl ActionInstance {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn arguments_set(&self) -> &ArgInstanceSet {
|
||||
&self.arguments // ⬅️ Retourne les INSTANCES, pas les modèles !
|
||||
&self.arguments // ⬅️ Retourne les INSTANCES, pas les modèles !
|
||||
}
|
||||
|
||||
/// Construit un [`ActionData`] initial à partir des variables d'état liées.
|
||||
@@ -186,10 +173,9 @@ impl ActionInstance {
|
||||
if arg_inst.get_model().is_in() && updated_keys.contains(arg_inst.get_name()) {
|
||||
if let Some(var_inst) = arg_inst.get_variable_instance() {
|
||||
if let Some(reflect_value) = action_data.get(arg_inst.get_name()) {
|
||||
let cloned = reflect_value
|
||||
.as_ref()
|
||||
.reflect_clone()
|
||||
.map_err(|e| crate::actions::ActionError::ArgumentError(e.to_string()))?;
|
||||
let cloned = reflect_value.as_ref().reflect_clone().map_err(|e| {
|
||||
crate::actions::ActionError::ArgumentError(e.to_string())
|
||||
})?;
|
||||
var_inst
|
||||
.set_reflect_value(cloned)
|
||||
.await
|
||||
@@ -269,22 +255,22 @@ impl ActionInstance {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::actions::Action;
|
||||
use crate::UpnpInstance;
|
||||
use crate::actions::Action;
|
||||
|
||||
#[test]
|
||||
fn test_action_instance_creation() {
|
||||
let action = Action::new("Play".to_string());
|
||||
let instance = ActionInstance::new(&action);
|
||||
|
||||
|
||||
assert_eq!(instance.get_name(), "Play");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_action_instance_has_argument_instances() {
|
||||
let action = Action::new("Play".to_string());
|
||||
let instance = ActionInstance::new(&action);
|
||||
|
||||
|
||||
// Vérifier que arguments_set() retourne bien des instances
|
||||
assert!(instance.arguments_set().all().iter().all(|arg| {
|
||||
// Chaque argument doit être une ArgumentInstance
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::{
|
||||
UpnpObject,
|
||||
actions::{ActionInstanceSet},
|
||||
};
|
||||
use crate::{UpnpObject, actions::ActionInstanceSet};
|
||||
|
||||
use xmltree::{Element,XMLNode};
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
impl UpnpObject for ActionInstanceSet {
|
||||
// Méthode pour convertir en XML (à implémenter avec une librairie XML)
|
||||
@@ -18,4 +15,3 @@ impl UpnpObject for ActionInstanceSet {
|
||||
elem
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,8 @@ use std::sync::Arc;
|
||||
use tracing::{info, trace};
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::{
|
||||
action_handler,
|
||||
UpnpModel,
|
||||
UpnpObject,
|
||||
UpnpObjectSetError,
|
||||
UpnpObjectType,
|
||||
UpnpTyped,
|
||||
};
|
||||
use crate::actions::{
|
||||
Action,
|
||||
ActionHandler,
|
||||
ActionInstance,
|
||||
Argument,
|
||||
ArgumentSet,
|
||||
};
|
||||
use crate::actions::{Action, ActionHandler, ActionInstance, Argument, ArgumentSet};
|
||||
use crate::{UpnpModel, UpnpObject, UpnpObjectSetError, UpnpObjectType, UpnpTyped, action_handler};
|
||||
|
||||
impl UpnpObject for Action {
|
||||
fn to_xml_element(&self) -> Element {
|
||||
@@ -108,7 +95,7 @@ impl Action {
|
||||
},
|
||||
arguments: ArgumentSet::new(),
|
||||
handle: Self::default_handler(),
|
||||
stateful: true, // Par défaut, les actions sont stateful
|
||||
stateful: true, // Par défaut, les actions sont stateful
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,13 +175,12 @@ impl Action {
|
||||
self.stateful = stateful;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn set_stateless(&mut self, stateless: bool) -> &mut Self {
|
||||
self.stateful = !stateless;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Retourne `true` si l'action est stateful.
|
||||
///
|
||||
/// # Returns
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::actions::{ActionInstanceSet, ActionSet};
|
||||
use crate::{UpnpModel, UpnpObject};
|
||||
|
||||
impl UpnpObject for ActionSet {
|
||||
fn to_xml_element(&self) -> Element {
|
||||
fn to_xml_element(&self) -> Element {
|
||||
let mut elem = Element::new("actionList");
|
||||
|
||||
for action in self.all() {
|
||||
@@ -14,10 +14,8 @@ impl UpnpObject for ActionSet {
|
||||
|
||||
elem
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
impl UpnpModel for ActionSet {
|
||||
type Instance = ActionInstanceSet;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@ use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::{actions::{ArgInstanceSet, ArgumentSet}, UpnpObject};
|
||||
use crate::{
|
||||
UpnpObject,
|
||||
actions::{ArgInstanceSet, ArgumentSet},
|
||||
};
|
||||
|
||||
use crate::UpnpInstance;
|
||||
|
||||
impl UpnpObject for ArgInstanceSet {
|
||||
fn to_xml_element(&self) -> Element {
|
||||
let mut elem = Element::new("serviceStateTable");
|
||||
|
||||
|
||||
for state_var in self.all() {
|
||||
let state_var_elem = state_var.to_xml_element(); // retourne un <stateVariable> complet
|
||||
elem.children.push(XMLNode::Element(state_var_elem));
|
||||
@@ -24,8 +27,8 @@ impl UpnpInstance for ArgInstanceSet {
|
||||
type Model = ArgumentSet;
|
||||
|
||||
fn new(_: &ArgumentSet) -> Self {
|
||||
Self { objects: RwLock::new(HashMap::new()) }
|
||||
Self {
|
||||
objects: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
use std::{collections::HashMap, sync::{Arc, RwLock}};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use xmltree::Element;
|
||||
|
||||
use crate::{actions::{ActionInstanceSet, ActionSet, Argument, ArgumentInstance}, state_variables::StateVarInstance, UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance};
|
||||
|
||||
use crate::{
|
||||
UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance,
|
||||
actions::{ActionInstanceSet, ActionSet, Argument, ArgumentInstance},
|
||||
state_variables::StateVarInstance,
|
||||
};
|
||||
|
||||
impl UpnpObject for ArgumentInstance {
|
||||
fn to_xml_element(&self) -> Element {
|
||||
@@ -46,7 +52,6 @@ impl UpnpTypedInstance for ArgumentInstance {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Implémentation de [`UpnpInstance`] pour [`ArgumentInstance`].
|
||||
///
|
||||
/// Cette implémentation fournit le constructeur standard qui crée une instance
|
||||
@@ -80,14 +85,14 @@ impl UpnpTypedInstance for ArgumentInstance {
|
||||
/// use pmoupnp::UpnpInstance;
|
||||
///
|
||||
/// let arg_model = Argument::new_in("InstanceID".to_string(), instance_id_var);
|
||||
///
|
||||
///
|
||||
/// // Création de l'instance - Phase 1
|
||||
/// let arg_instance = ArgumentInstance::new(&arg_model);
|
||||
///
|
||||
///
|
||||
/// // À ce stade, l'instance existe mais n'est pas encore liée
|
||||
/// assert_eq!(arg_instance.get_name(), "InstanceID");
|
||||
/// assert!(arg_instance.get_variable_instance().is_none());
|
||||
///
|
||||
///
|
||||
/// // La liaison se fera plus tard via bind_variable()
|
||||
/// ```
|
||||
impl UpnpInstance for ArgumentInstance {
|
||||
@@ -119,14 +124,14 @@ impl UpnpInstance for ArgumentInstance {
|
||||
///
|
||||
/// ```ignore
|
||||
/// use pmoupnp::UpnpInstance;
|
||||
///
|
||||
///
|
||||
/// // Création depuis un modèle
|
||||
/// let instance = ArgumentInstance::new(&arg_model);
|
||||
///
|
||||
///
|
||||
/// // L'instance hérite des propriétés du modèle
|
||||
/// assert_eq!(instance.get_name(), arg_model.get_name());
|
||||
/// assert_eq!(instance.is_in(), arg_model.is_in());
|
||||
///
|
||||
///
|
||||
/// // Mais n'a pas encore de valeur runtime
|
||||
/// assert!(instance.get_variable_instance().is_none());
|
||||
/// ```
|
||||
@@ -176,13 +181,13 @@ impl ArgumentInstance {
|
||||
///
|
||||
/// ```ignore
|
||||
/// use std::sync::Arc;
|
||||
///
|
||||
///
|
||||
/// let arg_instance = ArgumentInstance::new(&arg_model);
|
||||
/// let var_instance = Arc::new(StateVarInstance::new(&state_var));
|
||||
///
|
||||
///
|
||||
/// // Établir la liaison
|
||||
/// arg_instance.bind_variable(var_instance.clone());
|
||||
///
|
||||
///
|
||||
/// // Vérifier que la liaison est établie
|
||||
/// assert!(arg_instance.get_variable_instance().is_some());
|
||||
/// ```
|
||||
@@ -244,13 +249,12 @@ impl ArgumentInstance {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl UpnpInstance for ActionInstanceSet {
|
||||
type Model = ActionSet;
|
||||
|
||||
|
||||
fn new(_: &ActionSet) -> Self {
|
||||
Self {
|
||||
objects: RwLock::new(HashMap::new())
|
||||
Self {
|
||||
objects: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::actions::ArgInstanceSet;
|
||||
use crate::UpnpModel;
|
||||
use crate::{
|
||||
UpnpObject,
|
||||
actions::{ArgumentSet},
|
||||
};
|
||||
use crate::actions::ArgInstanceSet;
|
||||
use crate::{UpnpObject, actions::ArgumentSet};
|
||||
use xmltree::Element;
|
||||
|
||||
impl UpnpObject for ArgumentSet {
|
||||
|
||||
@@ -3,7 +3,9 @@ use std::sync::Arc;
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::{
|
||||
actions::{Argument, ArgumentInstance}, state_variables::StateVariable, UpnpModel, UpnpObject, UpnpObjectType, UpnpTyped
|
||||
UpnpModel, UpnpObject, UpnpObjectType, UpnpTyped,
|
||||
actions::{Argument, ArgumentInstance},
|
||||
state_variables::StateVariable,
|
||||
};
|
||||
|
||||
impl UpnpTyped for Argument {
|
||||
@@ -46,8 +48,6 @@ impl UpnpModel for Argument {
|
||||
type Instance = ArgumentInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Argument {
|
||||
fn new(name: String, state_variable: Arc<StateVariable>) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -4,10 +4,10 @@ use thiserror::Error;
|
||||
pub enum ActionError {
|
||||
#[error("Action error: {0}")]
|
||||
GeneralError(String),
|
||||
|
||||
|
||||
#[error("Argument error: {0}")]
|
||||
ArgumentError(String),
|
||||
|
||||
|
||||
#[error("Set operation error: {0}")]
|
||||
SetError(String),
|
||||
}
|
||||
@@ -22,10 +22,10 @@ impl From<std::io::Error> for ActionError {
|
||||
pub enum ArgumentError {
|
||||
#[error("Argument error: {0}")]
|
||||
GeneralError(String),
|
||||
|
||||
|
||||
#[error("Argument error: {0}")]
|
||||
ArgumentError(String),
|
||||
|
||||
|
||||
#[error("Set operation error: {0}")]
|
||||
SetError(String),
|
||||
}
|
||||
@@ -34,4 +34,4 @@ impl From<std::io::Error> for ArgumentError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
ArgumentError::GeneralError(format!("IO error: {}", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
//! });
|
||||
//! ```
|
||||
|
||||
use bevy_reflect::Reflect;
|
||||
use crate::actions::{ActionData, ActionError};
|
||||
use bevy_reflect::Reflect;
|
||||
|
||||
/// Extrait une valeur typée depuis ActionData.
|
||||
///
|
||||
@@ -73,17 +73,13 @@ use crate::actions::{ActionData, ActionError};
|
||||
/// let volume: u32 = get_value(&data, "Volume").unwrap();
|
||||
/// assert_eq!(volume, 50);
|
||||
/// ```
|
||||
pub fn get_value<T: Reflect + Clone>(
|
||||
data: &ActionData,
|
||||
key: &str
|
||||
) -> Result<T, ActionError> {
|
||||
pub fn get_value<T: Reflect + Clone>(data: &ActionData, key: &str) -> Result<T, ActionError> {
|
||||
data.get(key)
|
||||
.and_then(|boxed| boxed.as_any().downcast_ref::<T>())
|
||||
.cloned()
|
||||
.ok_or_else(|| ActionError::ArgumentError(format!(
|
||||
"Argument '{}' not found or type mismatch",
|
||||
key
|
||||
)))
|
||||
.ok_or_else(|| {
|
||||
ActionError::ArgumentError(format!("Argument '{}' not found or type mismatch", key))
|
||||
})
|
||||
}
|
||||
|
||||
/// Insère une valeur dans ActionData.
|
||||
@@ -115,11 +111,7 @@ pub fn get_value<T: Reflect + Clone>(
|
||||
/// let volume: u32 = get_value(&data, "Volume").unwrap();
|
||||
/// assert_eq!(volume, 75);
|
||||
/// ```
|
||||
pub fn set_value<T: Reflect + 'static>(
|
||||
data: &mut ActionData,
|
||||
key: impl Into<String>,
|
||||
value: T
|
||||
) {
|
||||
pub fn set_value<T: Reflect + 'static>(data: &mut ActionData, key: impl Into<String>, value: T) {
|
||||
data.insert(key.into(), Box::new(value));
|
||||
}
|
||||
|
||||
|
||||
@@ -189,22 +189,22 @@ macro_rules! define_action {
|
||||
std::sync::Arc::new(ac)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Helper interne pour créer un argument d'entrée
|
||||
(@arg in $name:literal, $var:expr) => {
|
||||
std::sync::Arc::new(
|
||||
$crate::actions::Argument::new_in(
|
||||
$name.to_string(),
|
||||
$name.to_string(),
|
||||
std::sync::Arc::clone(&$var)
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
// Helper interne pour créer un argument de sortie
|
||||
(@arg out $name:literal, $var:expr) => {
|
||||
std::sync::Arc::new(
|
||||
$crate::actions::Argument::new_out(
|
||||
$name.to_string(),
|
||||
$name.to_string(),
|
||||
std::sync::Arc::clone(&$var)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
mod errors;
|
||||
|
||||
mod action_handler;
|
||||
mod action_instance;
|
||||
mod action_instance_set;
|
||||
mod action_methods;
|
||||
mod action_handler;
|
||||
mod action_set_methods;
|
||||
mod arg_inst_set_methods;
|
||||
mod arg_instance_methods;
|
||||
@@ -19,8 +19,8 @@ use crate::{
|
||||
};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
pub use errors::ActionError;
|
||||
pub use action_handler::{ActionData, ActionFuture, ActionHandler};
|
||||
pub use errors::ActionError;
|
||||
pub use handler_helpers::{get_value, reflect_to_string, set_value};
|
||||
|
||||
/// Action UPnP.
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
//! Les caches supportent les collections, permettant à chaque source
|
||||
//! d'avoir sa propre collection dans le cache partagé.
|
||||
|
||||
use std::sync::Arc;
|
||||
use once_cell::sync::Lazy;
|
||||
use pmocache::FileCache;
|
||||
use std::sync::RwLock;
|
||||
use pmocovers::Cache as CoverCache;
|
||||
use pmoaudiocache::Cache as AudioCache;
|
||||
use pmocache::FileCache;
|
||||
use pmocovers::Cache as CoverCache;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
/// Registre global des caches
|
||||
///
|
||||
@@ -80,16 +80,16 @@ impl CacheRegistry {
|
||||
///
|
||||
/// URL complète (ex: "http://localhost:8080/covers/images/abc123/300")
|
||||
pub fn build_cover_url(&self, pk: &str, size: Option<usize>) -> anyhow::Result<String> {
|
||||
let base_url = self.base_url
|
||||
let base_url = self
|
||||
.base_url
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Base URL not set in CacheRegistry"))?;
|
||||
let cache = get_cover_cache()
|
||||
.ok_or_else(|| anyhow::anyhow!("No registred cover cache"))?;
|
||||
let param= match size {
|
||||
let cache = get_cover_cache().ok_or_else(|| anyhow::anyhow!("No registred cover cache"))?;
|
||||
let param = match size {
|
||||
Some(size_) => Some(size_.to_string()),
|
||||
None => None
|
||||
None => None,
|
||||
};
|
||||
let route = cache.route_for(pk, param.as_deref());
|
||||
let route = cache.route_for(pk, param.as_deref());
|
||||
Ok(format!("{}{}", base_url, route))
|
||||
}
|
||||
|
||||
@@ -104,12 +104,12 @@ impl CacheRegistry {
|
||||
///
|
||||
/// URL complète (ex: "http://localhost:8080/audio/tracks/abc123/orig")
|
||||
pub fn build_audio_url(&self, pk: &str, param: Option<&str>) -> anyhow::Result<String> {
|
||||
let base_url = self.base_url
|
||||
let base_url = self
|
||||
.base_url
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Base URL not set in CacheRegistry"))?;
|
||||
let cache = get_audio_cache()
|
||||
.ok_or_else(|| anyhow::anyhow!("No registred audio cache"))?;
|
||||
let route = cache.route_for(pk, param);
|
||||
let cache = get_audio_cache().ok_or_else(|| anyhow::anyhow!("No registred audio cache"))?;
|
||||
let route = cache.route_for(pk, param);
|
||||
Ok(format!("{}{}", base_url, route))
|
||||
}
|
||||
}
|
||||
@@ -124,9 +124,8 @@ impl Default for CacheRegistry {
|
||||
///
|
||||
/// Utilise Lazy pour une initialisation paresseuse et RwLock pour le partage entre threads.
|
||||
/// Permet aux handlers et aux sources d'accéder aux caches depuis n'importe où.
|
||||
pub(crate) static CACHE_REGISTRY: Lazy<RwLock<CacheRegistry>> = Lazy::new(|| {
|
||||
RwLock::new(CacheRegistry::new())
|
||||
});
|
||||
pub(crate) static CACHE_REGISTRY: Lazy<RwLock<CacheRegistry>> =
|
||||
Lazy::new(|| RwLock::new(CacheRegistry::new()));
|
||||
|
||||
/// Accès global au cache de couvertures
|
||||
///
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
//! Définition du modèle Device UPnP.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use crate::{
|
||||
UpnpTyped, UpnpObjectType,
|
||||
services::Service,
|
||||
};
|
||||
use crate::{UpnpObjectType, UpnpTyped, services::Service};
|
||||
|
||||
use super::errors::DeviceError;
|
||||
|
||||
@@ -129,7 +126,10 @@ impl Device {
|
||||
///
|
||||
/// Format: `urn:schemas-upnp-org:device:{type}:{version}`
|
||||
pub fn device_type(&self) -> String {
|
||||
format!("urn:schemas-upnp-org:device:{}:{}", self.device_type, self.version)
|
||||
format!(
|
||||
"urn:schemas-upnp-org:device:{}:{}",
|
||||
self.device_type, self.version
|
||||
)
|
||||
}
|
||||
|
||||
pub fn device_category(&self) -> &String {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
//! Implémentation de DeviceInstance.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use tracing::info;
|
||||
use xmltree::{Element, XMLNode, EmitterConfig};
|
||||
use xmltree::{Element, EmitterConfig, XMLNode};
|
||||
|
||||
use crate::{
|
||||
UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance,
|
||||
devices::{Device, errors::DeviceError},
|
||||
services::ServiceInstance,
|
||||
UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType,
|
||||
};
|
||||
|
||||
/// Instance d'un device UPnP.
|
||||
@@ -79,7 +79,9 @@ impl UpnpInstance for DeviceInstance {
|
||||
// Obtenir ou créer un UDN persistant via la configuration
|
||||
let device_name = model.get_name();
|
||||
let device_type = model.device_category();
|
||||
let udn = if let Ok(config_udn) = pmoconfig::get_config().get_device_udn(&device_type, device_name) {
|
||||
let udn = if let Ok(config_udn) =
|
||||
pmoconfig::get_config().get_device_udn(&device_type, device_name)
|
||||
{
|
||||
config_udn
|
||||
} else {
|
||||
// Fallback : générer un UDN
|
||||
@@ -88,6 +90,7 @@ impl UpnpInstance for DeviceInstance {
|
||||
};
|
||||
|
||||
// Obtenir l'IP locale et le port depuis la configuration
|
||||
// TODO: c'est amusant cet instanciation sauvage de base_url
|
||||
let local_ip = pmoutils::guess_local_ip();
|
||||
let port = pmoconfig::get_config().get_http_port();
|
||||
let server_base_url = format!("http://{}:{}", local_ip, port);
|
||||
@@ -118,22 +121,30 @@ impl UpnpObject for DeviceInstance {
|
||||
|
||||
// deviceType
|
||||
let mut device_type = Element::new("deviceType");
|
||||
device_type.children.push(XMLNode::Text(self.model.device_type()));
|
||||
device_type
|
||||
.children
|
||||
.push(XMLNode::Text(self.model.device_type()));
|
||||
elem.children.push(XMLNode::Element(device_type));
|
||||
|
||||
// friendlyName
|
||||
let mut friendly_name = Element::new("friendlyName");
|
||||
friendly_name.children.push(XMLNode::Text(self.model.friendly_name().to_string()));
|
||||
friendly_name
|
||||
.children
|
||||
.push(XMLNode::Text(self.model.friendly_name().to_string()));
|
||||
elem.children.push(XMLNode::Element(friendly_name));
|
||||
|
||||
// manufacturer
|
||||
let mut manufacturer = Element::new("manufacturer");
|
||||
manufacturer.children.push(XMLNode::Text(self.model.manufacturer().to_string()));
|
||||
manufacturer
|
||||
.children
|
||||
.push(XMLNode::Text(self.model.manufacturer().to_string()));
|
||||
elem.children.push(XMLNode::Element(manufacturer));
|
||||
|
||||
// modelName
|
||||
let mut model_name = Element::new("modelName");
|
||||
model_name.children.push(XMLNode::Text(self.model.model_name().to_string()));
|
||||
model_name
|
||||
.children
|
||||
.push(XMLNode::Text(self.model.model_name().to_string()));
|
||||
elem.children.push(XMLNode::Element(model_name));
|
||||
|
||||
// UDN
|
||||
@@ -146,7 +157,9 @@ impl UpnpObject for DeviceInstance {
|
||||
if !services.is_empty() {
|
||||
let mut service_list = Element::new("serviceList");
|
||||
for service in services.values() {
|
||||
service_list.children.push(XMLNode::Element(service.to_xml_element()));
|
||||
service_list
|
||||
.children
|
||||
.push(XMLNode::Element(service.to_xml_element()));
|
||||
}
|
||||
elem.children.push(XMLNode::Element(service_list));
|
||||
}
|
||||
@@ -156,7 +169,9 @@ impl UpnpObject for DeviceInstance {
|
||||
if !devices.is_empty() {
|
||||
let mut device_list = Element::new("deviceList");
|
||||
for device in devices.values() {
|
||||
device_list.children.push(XMLNode::Element(device.to_xml_element()));
|
||||
device_list
|
||||
.children
|
||||
.push(XMLNode::Element(device.to_xml_element()));
|
||||
}
|
||||
elem.children.push(XMLNode::Element(device_list));
|
||||
}
|
||||
@@ -164,7 +179,9 @@ impl UpnpObject for DeviceInstance {
|
||||
// presentationURL
|
||||
if let Some(url) = self.model.presentation_url() {
|
||||
let mut presentation_url = Element::new("presentationURL");
|
||||
presentation_url.children.push(XMLNode::Text(url.to_string()));
|
||||
presentation_url
|
||||
.children
|
||||
.push(XMLNode::Text(url.to_string()));
|
||||
elem.children.push(XMLNode::Element(presentation_url));
|
||||
}
|
||||
|
||||
@@ -251,7 +268,10 @@ impl DeviceInstance {
|
||||
}
|
||||
|
||||
/// Enregistre toutes les URLs du device et de ses services dans le serveur.
|
||||
pub fn register_urls<'a>(&'a self, server: &'a mut pmoserver::Server) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DeviceError>> + 'a>> {
|
||||
pub fn register_urls<'a>(
|
||||
&'a self,
|
||||
server: &'a mut pmoserver::Server,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DeviceError>> + 'a>> {
|
||||
Box::pin(async move {
|
||||
info!(
|
||||
"✅ Device description for {} available at: {}{}",
|
||||
@@ -262,14 +282,18 @@ impl DeviceInstance {
|
||||
|
||||
// Handler pour la description du device
|
||||
let instance_desc = self.clone();
|
||||
server.add_handler(&self.description_route(), move || {
|
||||
let instance = instance_desc.clone();
|
||||
async move { instance.description_handler().await }
|
||||
}).await;
|
||||
server
|
||||
.add_handler(&self.description_route(), move || {
|
||||
let instance = instance_desc.clone();
|
||||
async move { instance.description_handler().await }
|
||||
})
|
||||
.await;
|
||||
|
||||
// Enregistrer les services
|
||||
for service in self.services() {
|
||||
service.register_urls(server).await
|
||||
service
|
||||
.register_urls(server)
|
||||
.await
|
||||
.map_err(|e| DeviceError::UrlRegistrationError(e.to_string()))?;
|
||||
}
|
||||
|
||||
@@ -330,9 +354,13 @@ impl DeviceInstance {
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
[(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"text/xml; charset=\"utf-8\"",
|
||||
)],
|
||||
xml,
|
||||
).into_response()
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// Crée un SsdpDevice configuré pour ce device UPnP.
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::sync::Arc;
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::{
|
||||
UpnpInstance, UpnpModel, UpnpObject,
|
||||
devices::{Device, DeviceInstance},
|
||||
UpnpObject, UpnpModel, UpnpInstance,
|
||||
};
|
||||
|
||||
impl UpnpObject for Device {
|
||||
@@ -19,37 +19,49 @@ impl UpnpObject for Device {
|
||||
|
||||
// friendlyName
|
||||
let mut friendly_name = Element::new("friendlyName");
|
||||
friendly_name.children.push(XMLNode::Text(self.friendly_name().to_string()));
|
||||
friendly_name
|
||||
.children
|
||||
.push(XMLNode::Text(self.friendly_name().to_string()));
|
||||
elem.children.push(XMLNode::Element(friendly_name));
|
||||
|
||||
// manufacturer
|
||||
let mut manufacturer = Element::new("manufacturer");
|
||||
manufacturer.children.push(XMLNode::Text(self.manufacturer().to_string()));
|
||||
manufacturer
|
||||
.children
|
||||
.push(XMLNode::Text(self.manufacturer().to_string()));
|
||||
elem.children.push(XMLNode::Element(manufacturer));
|
||||
|
||||
// manufacturerURL (optionnel)
|
||||
if let Some(url) = self.manufacturer_url() {
|
||||
let mut manufacturer_url = Element::new("manufacturerURL");
|
||||
manufacturer_url.children.push(XMLNode::Text(url.to_string()));
|
||||
manufacturer_url
|
||||
.children
|
||||
.push(XMLNode::Text(url.to_string()));
|
||||
elem.children.push(XMLNode::Element(manufacturer_url));
|
||||
}
|
||||
|
||||
// modelDescription (optionnel)
|
||||
if let Some(desc) = self.model_description() {
|
||||
let mut model_description = Element::new("modelDescription");
|
||||
model_description.children.push(XMLNode::Text(desc.to_string()));
|
||||
model_description
|
||||
.children
|
||||
.push(XMLNode::Text(desc.to_string()));
|
||||
elem.children.push(XMLNode::Element(model_description));
|
||||
}
|
||||
|
||||
// modelName
|
||||
let mut model_name = Element::new("modelName");
|
||||
model_name.children.push(XMLNode::Text(self.model_name().to_string()));
|
||||
model_name
|
||||
.children
|
||||
.push(XMLNode::Text(self.model_name().to_string()));
|
||||
elem.children.push(XMLNode::Element(model_name));
|
||||
|
||||
// modelNumber (optionnel)
|
||||
if let Some(number) = self.model_number() {
|
||||
let mut model_number = Element::new("modelNumber");
|
||||
model_number.children.push(XMLNode::Text(number.to_string()));
|
||||
model_number
|
||||
.children
|
||||
.push(XMLNode::Text(number.to_string()));
|
||||
elem.children.push(XMLNode::Element(model_number));
|
||||
}
|
||||
|
||||
@@ -63,7 +75,9 @@ impl UpnpObject for Device {
|
||||
// serialNumber (optionnel)
|
||||
if let Some(serial) = self.serial_number() {
|
||||
let mut serial_number = Element::new("serialNumber");
|
||||
serial_number.children.push(XMLNode::Text(serial.to_string()));
|
||||
serial_number
|
||||
.children
|
||||
.push(XMLNode::Text(serial.to_string()));
|
||||
elem.children.push(XMLNode::Element(serial_number));
|
||||
}
|
||||
|
||||
@@ -80,7 +94,9 @@ impl UpnpObject for Device {
|
||||
let mut icon = Element::new("icon");
|
||||
|
||||
let mut mimetype = Element::new("mimetype");
|
||||
mimetype.children.push(XMLNode::Text("image/png".to_string()));
|
||||
mimetype
|
||||
.children
|
||||
.push(XMLNode::Text("image/png".to_string()));
|
||||
icon.children.push(XMLNode::Element(mimetype));
|
||||
|
||||
let mut width = Element::new("width");
|
||||
@@ -106,7 +122,9 @@ impl UpnpObject for Device {
|
||||
// presentationURL (optionnel)
|
||||
if let Some(url) = self.presentation_url() {
|
||||
let mut presentation_url = Element::new("presentationURL");
|
||||
presentation_url.children.push(XMLNode::Text(url.to_string()));
|
||||
presentation_url
|
||||
.children
|
||||
.push(XMLNode::Text(url.to_string()));
|
||||
elem.children.push(XMLNode::Element(presentation_url));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
//! les `DeviceInstance` actifs, permettant l'introspection et la modification
|
||||
//! de l'état du serveur UPnP.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use crate::{
|
||||
devices::DeviceInstance,
|
||||
UpnpObjectSet, UpnpTyped, UpnpTypedInstance, devices::DeviceInstance,
|
||||
state_variables::UpnpVariable,
|
||||
UpnpTyped, UpnpObjectSet, UpnpTypedInstance,
|
||||
};
|
||||
|
||||
/// Ensemble de DeviceInstance.
|
||||
@@ -118,7 +117,8 @@ impl DeviceRegistry {
|
||||
}
|
||||
|
||||
// Insérer dans le DeviceInstanceSet (par nom)
|
||||
self.devices.insert(device)
|
||||
self.devices
|
||||
.insert(device)
|
||||
.map_err(|e| format!("Failed to register device in registry: {:?}", e))?;
|
||||
|
||||
// Mettre à jour l'index UDN
|
||||
@@ -225,7 +225,12 @@ impl DeviceRegistry {
|
||||
/// # Returns
|
||||
///
|
||||
/// `Some(String)` contenant la valeur de la variable, `None` si non trouvée.
|
||||
pub fn get_variable(&self, udn: &str, service_name: &str, variable_name: &str) -> Option<String> {
|
||||
pub fn get_variable(
|
||||
&self,
|
||||
udn: &str,
|
||||
service_name: &str,
|
||||
variable_name: &str,
|
||||
) -> Option<String> {
|
||||
let device = self.get_device(udn)?;
|
||||
let service = device.get_service(service_name)?;
|
||||
let variable = service.get_variable(variable_name)?;
|
||||
@@ -244,14 +249,23 @@ impl DeviceRegistry {
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` si la modification réussit, `Err(String)` en cas d'erreur.
|
||||
pub async fn set_variable(&self, udn: &str, service_name: &str, variable_name: &str, value: &str) -> Result<(), String> {
|
||||
let device = self.get_device(udn)
|
||||
pub async fn set_variable(
|
||||
&self,
|
||||
udn: &str,
|
||||
service_name: &str,
|
||||
variable_name: &str,
|
||||
value: &str,
|
||||
) -> Result<(), String> {
|
||||
let device = self
|
||||
.get_device(udn)
|
||||
.ok_or_else(|| format!("Device {} not found", udn))?;
|
||||
|
||||
let service = device.get_service(service_name)
|
||||
let service = device
|
||||
.get_service(service_name)
|
||||
.ok_or_else(|| format!("Service {} not found", service_name))?;
|
||||
|
||||
let variable = service.get_variable(variable_name)
|
||||
let variable = service
|
||||
.get_variable(variable_name)
|
||||
.ok_or_else(|| format!("Variable {} not found", variable_name))?;
|
||||
|
||||
// Parser et valider la valeur selon le type de la variable
|
||||
@@ -260,7 +274,9 @@ impl DeviceRegistry {
|
||||
let state_value = StateValue::from_string(value, &var_model.as_state_var_type())
|
||||
.map_err(|e| format!("Invalid value for variable {}: {:?}", variable_name, e))?;
|
||||
|
||||
variable.set_value(state_value).await
|
||||
variable
|
||||
.set_value(state_value)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to set value: {:?}", e))?;
|
||||
|
||||
Ok(())
|
||||
@@ -276,7 +292,11 @@ impl DeviceRegistry {
|
||||
/// # Returns
|
||||
///
|
||||
/// `Some(HashMap<String, String>)` avec les variables (nom -> valeur), `None` si non trouvé.
|
||||
pub fn get_service_variables(&self, udn: &str, service_name: &str) -> Option<HashMap<String, String>> {
|
||||
pub fn get_service_variables(
|
||||
&self,
|
||||
udn: &str,
|
||||
service_name: &str,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
let device = self.get_device(udn)?;
|
||||
let service = device.get_service(service_name)?;
|
||||
|
||||
@@ -327,11 +347,13 @@ impl DeviceInfo {
|
||||
manufacturer: model.manufacturer().to_string(),
|
||||
model_name: model.model_name().to_string(),
|
||||
base_url: instance.base_url().to_string(),
|
||||
services: instance.services()
|
||||
services: instance
|
||||
.services()
|
||||
.iter()
|
||||
.map(|s| ServiceInfo::from_instance(s))
|
||||
.collect(),
|
||||
devices: instance.devices()
|
||||
devices: instance
|
||||
.devices()
|
||||
.iter()
|
||||
.map(|d| DeviceInfo::from_instance(d))
|
||||
.collect(),
|
||||
@@ -361,12 +383,14 @@ impl ServiceInfo {
|
||||
name: instance.get_name().to_string(),
|
||||
service_type: instance.service_type(),
|
||||
service_id: instance.service_id(),
|
||||
actions: instance.actions()
|
||||
actions: instance
|
||||
.actions()
|
||||
.all()
|
||||
.iter()
|
||||
.map(|a| ActionInfo::from_instance(a))
|
||||
.collect(),
|
||||
variables: instance.statevariables()
|
||||
variables: instance
|
||||
.statevariables()
|
||||
.all()
|
||||
.iter()
|
||||
.map(|v| VariableInfo::from_instance(v))
|
||||
@@ -463,10 +487,7 @@ impl VariableInfo {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
devices::Device,
|
||||
UpnpModel,
|
||||
};
|
||||
use crate::{UpnpModel, devices::Device};
|
||||
|
||||
#[test]
|
||||
fn test_registry_creation() {
|
||||
|
||||
@@ -39,4 +39,7 @@ pub mod errors;
|
||||
|
||||
pub use device::Device;
|
||||
pub use device_instance::DeviceInstance;
|
||||
pub use device_registry::{DeviceRegistry, DeviceInstanceSet, DeviceInfo, ServiceInfo, ActionInfo, ArgumentInfo, VariableInfo};
|
||||
pub use device_registry::{
|
||||
ActionInfo, ArgumentInfo, DeviceInfo, DeviceInstanceSet, DeviceRegistry, ServiceInfo,
|
||||
VariableInfo,
|
||||
};
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
mod object_trait;
|
||||
mod object_set;
|
||||
mod object_trait;
|
||||
|
||||
pub mod cache_registry;
|
||||
pub mod upnp_server;
|
||||
pub mod upnp_api;
|
||||
pub mod actions;
|
||||
pub mod cache_registry;
|
||||
pub mod devices;
|
||||
pub mod services;
|
||||
pub mod soap;
|
||||
pub mod ssdp;
|
||||
pub mod state_variables;
|
||||
pub mod upnp_api;
|
||||
pub mod upnp_server;
|
||||
pub mod value_ranges;
|
||||
pub mod variable_types;
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::sync::RwLock;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
pub use crate::cache_registry::{get_audio_cache, get_cover_cache};
|
||||
pub use crate::object_trait::*;
|
||||
pub use crate::upnp_server::UpnpServerExt;
|
||||
pub use crate::cache_registry::{get_cover_cache, get_audio_cache};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpnpObjectType {
|
||||
@@ -35,4 +35,3 @@ pub struct UpnpObjectSet<T: UpnpTypedObject> {
|
||||
pub enum UpnpObjectSetError {
|
||||
AlreadyExists(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -194,4 +194,4 @@ impl<T: UpnpTypedObject> UpnpObjectSet<T> {
|
||||
let guard = self.objects.read().unwrap();
|
||||
guard.values().cloned().collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ pub trait UpnpObject: Clone + Debug {
|
||||
pub trait UpnpModel: UpnpObject {
|
||||
/// Le type d'instance créée par ce modèle.
|
||||
type Instance: UpnpInstance<Model = Self>;
|
||||
|
||||
|
||||
/// Crée une nouvelle instance à partir de ce modèle.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -320,7 +320,7 @@ pub trait UpnpModel: UpnpObject {
|
||||
pub trait UpnpInstance: UpnpObject {
|
||||
/// Le type du modèle dont cette instance est dérivée.
|
||||
type Model: UpnpModel<Instance = Self>;
|
||||
|
||||
|
||||
/// Crée une nouvelle instance à partir d'un modèle.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -331,7 +331,7 @@ pub trait UpnpInstance: UpnpObject {
|
||||
///
|
||||
/// Une nouvelle instance initialisée depuis le modèle.
|
||||
fn new(model: &Self::Model) -> Self;
|
||||
|
||||
|
||||
/// Indique que cet objet est une instance.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -421,9 +421,9 @@ pub trait UpnpTypedObject: UpnpObject + UpnpTyped {}
|
||||
/// Ce trait ajoute la méthode [`get_model`](Self::get_model) pour accéder
|
||||
/// au modèle de l'instance. Les collections d'instances ([`UpnInstanceSet`])
|
||||
/// n'ont pas cette méthode car elles contiennent plusieurs instances.
|
||||
pub trait UpnpTypedInstance: UpnpTypedObject + UpnpInstance
|
||||
pub trait UpnpTypedInstance: UpnpTypedObject + UpnpInstance
|
||||
where
|
||||
Self::Model: UpnpModel<Instance = Self>
|
||||
Self::Model: UpnpModel<Instance = Self>,
|
||||
{
|
||||
/// Retourne une référence vers le modèle dont cette instance est dérivée.
|
||||
///
|
||||
@@ -438,11 +438,11 @@ where
|
||||
///
|
||||
/// ```ignore
|
||||
/// let instance = model.create_instance();
|
||||
///
|
||||
///
|
||||
/// // Accéder aux propriétés du modèle depuis l'instance
|
||||
/// let model_ref = instance.get_model();
|
||||
/// println!("Instance du modèle: {}", model_ref.get_name());
|
||||
///
|
||||
///
|
||||
/// // Vérifier les contraintes définies dans le modèle
|
||||
/// if let Some(range) = model_ref.get_range() {
|
||||
/// println!("Plage autorisée: {:?}", range);
|
||||
@@ -465,7 +465,6 @@ where
|
||||
fn get_model(&self) -> &Self::Model;
|
||||
}
|
||||
|
||||
|
||||
/// Trait marqueur pour les collections UPnP.
|
||||
///
|
||||
/// Représente un ensemble (set) d'objets UPnP.
|
||||
@@ -607,7 +606,6 @@ pub trait UpnpSet: UpnpObject + UpnpDeepClone {
|
||||
/// ```
|
||||
pub trait UpnpModelSet: UpnpSet + UpnpModel {}
|
||||
|
||||
|
||||
/// Trait marqueur pour les collections d'instances UPnP.
|
||||
///
|
||||
/// Combine [`UpnpSet`] et [`UpnpInstance`] pour représenter une collection
|
||||
@@ -626,7 +624,6 @@ pub trait UpnpModelSet: UpnpSet + UpnpModel {}
|
||||
/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires.
|
||||
pub trait UpnInstanceSet: UpnpSet + UpnpInstance {}
|
||||
|
||||
|
||||
/// Implémentation automatique de [`UpnInstanceSet`] pour tous les types éligibles.
|
||||
///
|
||||
/// Cette *blanket implementation* fournit automatiquement le trait [`UpnInstanceSet`]
|
||||
@@ -667,10 +664,7 @@ pub trait UpnInstanceSet: UpnpSet + UpnpInstance {}
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
impl<T> UpnInstanceSet for T
|
||||
where
|
||||
T: UpnpSet + UpnpInstance
|
||||
{}
|
||||
impl<T> UpnInstanceSet for T where T: UpnpSet + UpnpInstance {}
|
||||
|
||||
/// Implémentation automatique de [`UpnpTypedObject`] pour tous les types éligibles.
|
||||
///
|
||||
@@ -697,11 +691,7 @@ where
|
||||
/// println!("{}", obj.get_name());
|
||||
/// }
|
||||
/// ```
|
||||
impl<T> UpnpTypedObject for T
|
||||
where
|
||||
T: UpnpObject + UpnpTyped
|
||||
{}
|
||||
|
||||
impl<T> UpnpTypedObject for T where T: UpnpObject + UpnpTyped {}
|
||||
|
||||
/// Implémentation automatique de [`UpnpModelSet`] pour tous les types éligibles.
|
||||
///
|
||||
@@ -756,8 +746,4 @@ where
|
||||
///
|
||||
/// - [`UpnpModelSet`] : Collection de **modèles** (peut créer des instances)
|
||||
/// - [`UpnInstanceSet`] : Collection d'**instances** (créée depuis un modèle)
|
||||
impl<T> UpnpModelSet for T
|
||||
where
|
||||
T: UpnpSet + UpnpModel
|
||||
{}
|
||||
|
||||
impl<T> UpnpModelSet for T where T: UpnpSet + UpnpModel {}
|
||||
|
||||
@@ -11,35 +11,35 @@ pub enum ServiceError {
|
||||
/// Erreur générale du service.
|
||||
#[error("Service error: {0}")]
|
||||
GeneralError(String),
|
||||
|
||||
|
||||
/// Erreur de validation (paramètres invalides).
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
|
||||
|
||||
/// Erreur lors d'une opération sur un ensemble (Set).
|
||||
#[error("Set operation error: {0}")]
|
||||
SetError(String),
|
||||
|
||||
|
||||
/// Erreur liée à une action.
|
||||
#[error("Action error: {0}")]
|
||||
ActionError(String),
|
||||
|
||||
|
||||
/// Erreur liée à une variable d'état.
|
||||
#[error("State variable error: {0}")]
|
||||
StateVariableError(String),
|
||||
|
||||
|
||||
/// Erreur de configuration.
|
||||
#[error("Configuration error: {0}")]
|
||||
ConfigError(String),
|
||||
|
||||
|
||||
/// Erreur réseau ou HTTP.
|
||||
#[error("Network error: {0}")]
|
||||
NetworkError(String),
|
||||
|
||||
|
||||
/// Erreur de sérialisation XML.
|
||||
#[error("XML serialization error: {0}")]
|
||||
XmlError(String),
|
||||
|
||||
|
||||
/// Erreur lors du traitement SOAP.
|
||||
#[error("SOAP error: {0}")]
|
||||
SoapError(String),
|
||||
@@ -59,4 +59,4 @@ impl From<crate::UpnpObjectSetError> for ServiceError {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ pub use errors::ServiceError;
|
||||
pub use service_instance::ServiceInstance;
|
||||
use xmltree::{Element, EmitterConfig, XMLNode};
|
||||
|
||||
use crate::{actions::ActionSet, state_variables::StateVariableSet, UpnpObject, UpnpObjectType};
|
||||
use crate::{UpnpObject, UpnpObjectType, actions::ActionSet, state_variables::StateVariableSet};
|
||||
|
||||
/// Service UPnP (modèle).
|
||||
///
|
||||
@@ -470,18 +470,18 @@ impl Service {
|
||||
);
|
||||
|
||||
let mut specversion = Element::new("specVersion");
|
||||
let mut major= Element::new("major");
|
||||
major.children
|
||||
.push(XMLNode::Text("1".to_string()));
|
||||
let mut major = Element::new("major");
|
||||
major.children.push(XMLNode::Text("1".to_string()));
|
||||
specversion.children.push(XMLNode::Element(major));
|
||||
let mut minor = Element::new("minor");
|
||||
minor.children
|
||||
.push(XMLNode::Text("0".to_string()));
|
||||
minor.children.push(XMLNode::Text("0".to_string()));
|
||||
specversion.children.push(XMLNode::Element(minor));
|
||||
scpd.children.push(XMLNode::Element(specversion));
|
||||
|
||||
scpd.children.push(XMLNode::Element(self.actions.to_xml_element()));
|
||||
scpd.children.push(XMLNode::Element(self.state_table.to_xml_element()));
|
||||
scpd.children
|
||||
.push(XMLNode::Element(self.actions.to_xml_element()));
|
||||
scpd.children
|
||||
.push(XMLNode::Element(self.state_table.to_xml_element()));
|
||||
|
||||
scpd
|
||||
}
|
||||
@@ -519,7 +519,6 @@ impl Service {
|
||||
.expect("Failed to write XML");
|
||||
|
||||
String::from_utf8(buf).expect("Invalid UTF-8")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use bevy_reflect::Reflect;
|
||||
use quick_xml::escape::escape;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
@@ -43,7 +44,6 @@ use std::{
|
||||
use tokio::time;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use xmltree::{Element, EmitterConfig, XMLNode};
|
||||
use quick_xml::escape::escape;
|
||||
|
||||
use crate::{
|
||||
UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance,
|
||||
@@ -671,7 +671,11 @@ impl ServiceInstance {
|
||||
|
||||
let xml = String::from_utf8_lossy(&xml_output).to_string();
|
||||
|
||||
debug!("✅ SCPD generated for {} ({} bytes)", self.get_name(), xml.len());
|
||||
debug!(
|
||||
"✅ SCPD generated for {} ({} bytes)",
|
||||
self.get_name(),
|
||||
xml.len()
|
||||
);
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
@@ -1050,13 +1054,14 @@ impl ServiceInstance {
|
||||
|
||||
/// Sérialise une structure Reflect en XML simple.
|
||||
fn serialize_struct_to_xml(s: &dyn bevy_reflect::Struct) -> String {
|
||||
use std::fmt::Write;
|
||||
use bevy_reflect::TypeInfo;
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut xml = String::new();
|
||||
|
||||
// Commencer par ouvrir la balise avec le nom du type
|
||||
let type_name = s.get_represented_type_info()
|
||||
let type_name = s
|
||||
.get_represented_type_info()
|
||||
.and_then(|ti| {
|
||||
if let TypeInfo::Struct(si) = ti {
|
||||
Some(si.type_path_table().short_path())
|
||||
@@ -1237,9 +1242,9 @@ async fn event_sub_handler(
|
||||
/// - Échec de l'exécution de l'action
|
||||
async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: String) -> Response {
|
||||
use crate::{
|
||||
soap::{parse_soap_action, build_soap_response, build_soap_fault, error_codes},
|
||||
variable_types::{StateValue, UpnpVarType},
|
||||
UpnpTypedInstance,
|
||||
soap::{build_soap_fault, build_soap_response, error_codes, parse_soap_action},
|
||||
variable_types::{StateValue, UpnpVarType},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use tracing::debug;
|
||||
@@ -1259,9 +1264,13 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: Str
|
||||
).unwrap_or_else(|_| String::from("<?xml version=\"1.0\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope>"));
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
[(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"text/xml; charset=\"utf-8\"",
|
||||
)],
|
||||
fault_xml,
|
||||
).into_response();
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1281,9 +1290,13 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: Str
|
||||
).unwrap_or_else(|_| String::from("<?xml version=\"1.0\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope>"));
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
[(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"text/xml; charset=\"utf-8\"",
|
||||
)],
|
||||
fault_xml,
|
||||
).into_response();
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1311,9 +1324,13 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: Str
|
||||
).unwrap_or_else(|_| String::from("<?xml version=\"1.0\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope>"));
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
[(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"text/xml; charset=\"utf-8\"",
|
||||
)],
|
||||
fault_xml,
|
||||
).into_response();
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1334,7 +1351,8 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: Str
|
||||
let arg_model = arg_inst.as_ref().get_model();
|
||||
if arg_model.is_out() {
|
||||
if let Some(reflect_value) = output_data.get(arg_inst.get_name()) {
|
||||
let soap_string = ServiceInstance::reflect_to_string(reflect_value.as_ref());
|
||||
let soap_string =
|
||||
ServiceInstance::reflect_to_string(reflect_value.as_ref());
|
||||
soap_values.insert(arg_inst.get_name().to_string(), soap_string);
|
||||
}
|
||||
}
|
||||
@@ -1356,9 +1374,13 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: Str
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
[(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"text/xml; charset=\"utf-8\"",
|
||||
)],
|
||||
response_xml,
|
||||
).into_response()
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
error!("❌ Action execution failed: {:?}", e);
|
||||
@@ -1370,9 +1392,13 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: Str
|
||||
).unwrap_or_else(|_| String::from("<?xml version=\"1.0\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope>"));
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
[(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
"text/xml; charset=\"utf-8\"",
|
||||
)],
|
||||
fault_xml,
|
||||
).into_response()
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1448,7 +1474,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let person = Person {
|
||||
name: "John <Doe>".to_string(), // Test XML escaping
|
||||
name: "John <Doe>".to_string(), // Test XML escaping
|
||||
age: 30,
|
||||
address: Address {
|
||||
street: "123 Main St & Ave".to_string(),
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::{
|
||||
UpnpModel, UpnpObject, UpnpObjectType, UpnpTyped,
|
||||
services::{Service, ServiceInstance},
|
||||
UpnpObject, UpnpModel, UpnpTyped, UpnpObjectType,
|
||||
};
|
||||
|
||||
impl std::fmt::Display for Service {
|
||||
@@ -36,7 +36,9 @@ impl UpnpObject for Service {
|
||||
|
||||
// serviceType
|
||||
let mut service_type = Element::new("serviceType");
|
||||
service_type.children.push(XMLNode::Text(self.service_type()));
|
||||
service_type
|
||||
.children
|
||||
.push(XMLNode::Text(self.service_type()));
|
||||
elem.children.push(XMLNode::Element(service_type));
|
||||
|
||||
// serviceId
|
||||
@@ -51,7 +53,9 @@ impl UpnpObject for Service {
|
||||
|
||||
// controlURL
|
||||
let mut controlURL = Element::new("controlURL");
|
||||
controlURL.children.push(XMLNode::Text(self.control_route()));
|
||||
controlURL
|
||||
.children
|
||||
.push(XMLNode::Text(self.control_route()));
|
||||
elem.children.push(XMLNode::Element(controlURL));
|
||||
|
||||
// eventSubURL
|
||||
@@ -65,4 +69,4 @@ impl UpnpObject for Service {
|
||||
|
||||
impl UpnpModel for Service {
|
||||
type Instance = ServiceInstance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,12 +88,8 @@ mod tests {
|
||||
fn test_build_empty_response() {
|
||||
let values = HashMap::new();
|
||||
|
||||
let xml = build_soap_response(
|
||||
"urn:schemas-upnp-org:service:AVTransport:1",
|
||||
"Stop",
|
||||
values,
|
||||
)
|
||||
.unwrap();
|
||||
let xml = build_soap_response("urn:schemas-upnp-org:service:AVTransport:1", "Stop", values)
|
||||
.unwrap();
|
||||
|
||||
assert!(xml.contains("StopResponse"));
|
||||
assert!(xml.contains("xmlns:u=\"urn:schemas-upnp-org:service:AVTransport:1\""));
|
||||
|
||||
@@ -29,10 +29,7 @@ pub struct SoapBody {
|
||||
impl SoapEnvelope {
|
||||
/// Crée une nouvelle enveloppe SOAP
|
||||
pub fn new(body: SoapBody) -> Self {
|
||||
Self {
|
||||
header: None,
|
||||
body,
|
||||
}
|
||||
Self { header: None, body }
|
||||
}
|
||||
|
||||
/// Crée une nouvelle enveloppe avec header
|
||||
|
||||
@@ -102,17 +102,13 @@ pub fn build_soap_fault(
|
||||
error_code_elem
|
||||
.children
|
||||
.push(XMLNode::Text(code.to_string()));
|
||||
upnp_error
|
||||
.children
|
||||
.push(XMLNode::Element(error_code_elem));
|
||||
upnp_error.children.push(XMLNode::Element(error_code_elem));
|
||||
|
||||
let mut error_desc_elem = Element::new("errorDescription");
|
||||
error_desc_elem
|
||||
.children
|
||||
.push(XMLNode::Text(desc.to_string()));
|
||||
upnp_error
|
||||
.children
|
||||
.push(XMLNode::Element(error_desc_elem));
|
||||
upnp_error.children.push(XMLNode::Element(error_desc_elem));
|
||||
|
||||
detail.children.push(XMLNode::Element(upnp_error));
|
||||
fault.children.push(XMLNode::Element(detail));
|
||||
|
||||
@@ -48,15 +48,15 @@
|
||||
//! ).unwrap();
|
||||
//! ```
|
||||
|
||||
mod envelope;
|
||||
mod parser;
|
||||
mod builder;
|
||||
mod envelope;
|
||||
mod fault;
|
||||
mod parser;
|
||||
|
||||
pub use envelope::{SoapEnvelope, SoapHeader, SoapBody};
|
||||
pub use parser::{parse_soap_action, SoapAction};
|
||||
pub use builder::build_soap_response;
|
||||
pub use envelope::{SoapBody, SoapEnvelope, SoapHeader};
|
||||
pub use fault::{SoapFault, build_soap_fault};
|
||||
pub use parser::{SoapAction, parse_soap_action};
|
||||
|
||||
/// Codes d'erreur SOAP UPnP standards
|
||||
pub mod error_codes {
|
||||
|
||||
@@ -55,17 +55,16 @@ pub fn parse_soap_envelope(xml: &[u8]) -> Result<SoapEnvelope, SoapParseError> {
|
||||
.get_child("Header")
|
||||
.or_else(|| root.children.iter().find_map(|n| n.as_element()))
|
||||
.filter(|e| e.name.ends_with("Header"))
|
||||
.map(|e| SoapHeader {
|
||||
content: e.clone(),
|
||||
});
|
||||
.map(|e| SoapHeader { content: e.clone() });
|
||||
|
||||
// Extraire Body (obligatoire)
|
||||
let body_elem = root
|
||||
.get_child("Body")
|
||||
.or_else(|| root.children.iter().find_map(|n| {
|
||||
n.as_element()
|
||||
.filter(|e| e.name.ends_with("Body"))
|
||||
}))
|
||||
.or_else(|| {
|
||||
root.children
|
||||
.iter()
|
||||
.find_map(|n| n.as_element().filter(|e| e.name.ends_with("Body")))
|
||||
})
|
||||
.ok_or(SoapParseError::MissingBody)?;
|
||||
|
||||
let body = SoapBody {
|
||||
|
||||
@@ -22,12 +22,7 @@ pub struct SsdpDevice {
|
||||
|
||||
impl SsdpDevice {
|
||||
/// Crée un nouveau device SSDP
|
||||
pub fn new(
|
||||
uuid: String,
|
||||
device_type: String,
|
||||
location: String,
|
||||
server: String,
|
||||
) -> Self {
|
||||
pub fn new(uuid: String, device_type: String, location: String, server: String) -> Self {
|
||||
// Construction automatique des NTs standards
|
||||
let notification_types = vec![
|
||||
format!("uuid:{}", uuid),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Serveur SSDP
|
||||
|
||||
use super::{SsdpDevice, SSDP_MULTICAST_ADDR, SSDP_PORT, MAX_AGE};
|
||||
use super::{MAX_AGE, SSDP_MULTICAST_ADDR, SSDP_PORT, SsdpDevice};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
@@ -7,10 +7,10 @@ use std::sync::RwLock;
|
||||
use xmltree::Element;
|
||||
|
||||
use crate::{
|
||||
object_trait::{UpnpInstance, UpnpObject},
|
||||
state_variables::{StateVarInstance, StateVariable, UpnpVariable},
|
||||
variable_types::{StateValue, StateValueError, UpnpVarType},
|
||||
UpnpObjectType, UpnpTyped, UpnpTypedInstance
|
||||
UpnpObjectType, UpnpTyped, UpnpTypedInstance,
|
||||
object_trait::{UpnpInstance, UpnpObject},
|
||||
state_variables::{StateVarInstance, StateVariable, UpnpVariable},
|
||||
variable_types::{StateValue, StateValueError, UpnpVarType},
|
||||
};
|
||||
|
||||
impl UpnpVariable for StateVarInstance {
|
||||
@@ -49,7 +49,6 @@ impl UpnpInstance for StateVarInstance {
|
||||
reflexive_cache: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl UpnpTyped for StateVarInstance {
|
||||
@@ -59,7 +58,6 @@ impl UpnpTyped for StateVarInstance {
|
||||
}
|
||||
|
||||
impl UpnpTypedInstance for StateVarInstance {
|
||||
|
||||
fn get_model(&self) -> &Self::Model {
|
||||
&self.model
|
||||
}
|
||||
@@ -122,7 +120,7 @@ impl StateVarInstance {
|
||||
// Validation du type
|
||||
if self.as_state_var_type() != new_value.as_state_var_type() {
|
||||
return Err(StateValueError::TypeError(
|
||||
"Value type mismatch".to_string()
|
||||
"Value type mismatch".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -191,7 +189,9 @@ impl StateVarInstance {
|
||||
/// let reflected = var.reflexive_value();
|
||||
/// // reflected peut maintenant être inspecté avec l'API Reflect
|
||||
/// ```
|
||||
pub fn reflexive_value(&self) -> Result<Arc<dyn Reflect>, crate::state_variables::StateVariableError> {
|
||||
pub fn reflexive_value(
|
||||
&self,
|
||||
) -> Result<Arc<dyn Reflect>, crate::state_variables::StateVariableError> {
|
||||
// Vérifier si on a un cache valide
|
||||
{
|
||||
let cache = self.reflexive_cache.read().unwrap();
|
||||
@@ -256,7 +256,9 @@ impl StateVarInstance {
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to parse value '{}' for variable '{}': {:?}, using raw string",
|
||||
s, self.get_name(), e
|
||||
s,
|
||||
self.get_name(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -285,7 +287,10 @@ impl StateVarInstance {
|
||||
/// - La conversion Reflect → StateValue échoue
|
||||
/// - Le marshalling échoue
|
||||
/// - La mise à jour de la valeur échoue
|
||||
pub async fn set_reflect_value(&self, reflect_value: Box<dyn Reflect>) -> Result<(), StateValueError> {
|
||||
pub async fn set_reflect_value(
|
||||
&self,
|
||||
reflect_value: Box<dyn Reflect>,
|
||||
) -> Result<(), StateValueError> {
|
||||
use crate::variable_types::StateVarType;
|
||||
|
||||
// Convertir Reflect → StateValue
|
||||
@@ -297,19 +302,18 @@ impl StateVarInstance {
|
||||
Ok(temp_value) => {
|
||||
// Utiliser le marshal pour obtenir la String marshallée
|
||||
match marshal(&temp_value) {
|
||||
Ok(marshalled_string) => {
|
||||
StateValue::String(marshalled_string)
|
||||
},
|
||||
Ok(marshalled_string) => StateValue::String(marshalled_string),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to marshal value for variable '{}': {:?}, using standard conversion",
|
||||
self.get_name(), e
|
||||
self.get_name(),
|
||||
e
|
||||
);
|
||||
// Fallback
|
||||
temp_value
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
mod errors;
|
||||
mod instance_methods;
|
||||
mod macros;
|
||||
mod variable_methods;
|
||||
mod var_set_methods;
|
||||
mod var_inst_set_methods;
|
||||
mod var_set_methods;
|
||||
mod variable_methods;
|
||||
mod variable_trait;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
pub use crate::state_variables::variable_trait::UpnpVariable;
|
||||
use bevy_reflect::Reflect;
|
||||
@@ -18,9 +15,9 @@ pub use errors::StateVariableError;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
value_ranges::ValueRange,
|
||||
variable_types::{StateValue, StateVarType},
|
||||
UpnpObjectSet, UpnpObjectType,
|
||||
value_ranges::ValueRange,
|
||||
variable_types::{StateValue, StateVarType},
|
||||
};
|
||||
|
||||
/// Type pour les fonctions de condition d'événement
|
||||
@@ -65,4 +62,3 @@ pub struct StateVarInstance {
|
||||
}
|
||||
|
||||
pub type StateVarInstanceSet = UpnpObjectSet<StateVarInstance>;
|
||||
|
||||
|
||||
@@ -3,14 +3,17 @@ use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::{state_variables::{StateVarInstanceSet, StateVariableSet}, UpnpObject};
|
||||
use crate::{
|
||||
UpnpObject,
|
||||
state_variables::{StateVarInstanceSet, StateVariableSet},
|
||||
};
|
||||
|
||||
use crate::UpnpInstance;
|
||||
|
||||
impl UpnpObject for StateVarInstanceSet {
|
||||
fn to_xml_element(&self) -> Element {
|
||||
let mut elem = Element::new("serviceStateTable");
|
||||
|
||||
|
||||
for state_var in self.all() {
|
||||
let state_var_elem = state_var.to_xml_element(); // retourne un <stateVariable> complet
|
||||
elem.children.push(XMLNode::Element(state_var_elem));
|
||||
@@ -24,10 +27,8 @@ impl UpnpInstance for StateVarInstanceSet {
|
||||
type Model = StateVariableSet;
|
||||
|
||||
fn new(_: &StateVariableSet) -> Self {
|
||||
Self { objects: RwLock::new(HashMap::new()) }
|
||||
Self {
|
||||
objects: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::{object_trait::UpnpModel, state_variables::{StateVarInstanceSet, StateVariableSet}, UpnpObject};
|
||||
|
||||
use crate::{
|
||||
UpnpObject,
|
||||
object_trait::UpnpModel,
|
||||
state_variables::{StateVarInstanceSet, StateVariableSet},
|
||||
};
|
||||
|
||||
impl UpnpObject for StateVariableSet {
|
||||
|
||||
fn to_xml_element(&self) -> Element {
|
||||
let mut elem = Element::new("serviceStateTable");
|
||||
|
||||
|
||||
for state_var in self.all() {
|
||||
let state_var_elem = state_var.to_xml_element(); // retourne un <stateVariable> complet
|
||||
elem.children.push(XMLNode::Element(state_var_elem));
|
||||
@@ -15,11 +17,8 @@ impl UpnpObject for StateVariableSet {
|
||||
|
||||
elem
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl UpnpModel for StateVariableSet {
|
||||
type Instance = StateVarInstanceSet;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{collections::HashMap, fmt, sync::Arc};
|
||||
|
||||
use std::sync::RwLock;
|
||||
use xmltree::{Element, XMLNode};
|
||||
@@ -112,17 +108,13 @@ impl Clone for StateVariable {
|
||||
// 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().unwrap();
|
||||
let guard = self.event_conditions.read().unwrap();
|
||||
// nécessite que Key: Clone, Value: Clone
|
||||
Arc::new(RwLock::new(guard.clone()))
|
||||
};
|
||||
|
||||
let allowed_values_clone = {
|
||||
let guard = self
|
||||
.allowed_values
|
||||
.read().unwrap();
|
||||
let guard = self.allowed_values.read().unwrap();
|
||||
Arc::new(RwLock::new(guard.clone()))
|
||||
};
|
||||
|
||||
@@ -154,20 +146,14 @@ impl fmt::Debug for StateVariable {
|
||||
.field("modifiable", &self.modifiable)
|
||||
.field(
|
||||
"event_conditions",
|
||||
&format_args!(
|
||||
"len={}",
|
||||
self.event_conditions.read().unwrap().len()
|
||||
),
|
||||
&format_args!("len={}", self.event_conditions.read().unwrap().len()),
|
||||
)
|
||||
.field("description", &self.description)
|
||||
.field("default_value", &self.default_value)
|
||||
.field("value_range", &self.value_range)
|
||||
.field(
|
||||
"allowed_values",
|
||||
&format_args!(
|
||||
"len={}",
|
||||
self.allowed_values.read().unwrap().len()
|
||||
),
|
||||
&format_args!("len={}", self.allowed_values.read().unwrap().len()),
|
||||
)
|
||||
.field("send_events", &self.send_events)
|
||||
.field(
|
||||
@@ -332,9 +318,7 @@ impl StateVariable {
|
||||
}
|
||||
|
||||
pub fn extend_allowed_values(&mut self, values: &[StateValue]) -> Result<(), StateValueError> {
|
||||
let mut av = self
|
||||
.allowed_values
|
||||
.write().unwrap();
|
||||
let mut av = self.allowed_values.write().unwrap();
|
||||
|
||||
for v in values {
|
||||
if self.as_state_var_type() == v.as_state_var_type() {
|
||||
@@ -350,9 +334,7 @@ impl StateVariable {
|
||||
}
|
||||
|
||||
pub fn push_allowed_value(&mut self, value: &StateValue) -> Result<(), StateValueError> {
|
||||
let mut av = self
|
||||
.allowed_values
|
||||
.write().unwrap();
|
||||
let mut av = self.allowed_values.write().unwrap();
|
||||
|
||||
if self.as_state_var_type() == value.as_state_var_type() {
|
||||
av.push(value.clone());
|
||||
|
||||
@@ -216,9 +216,7 @@ pub trait UpnpVariable {
|
||||
///
|
||||
/// Retourne `false` si le lock est empoisonné (poisoned).
|
||||
fn has_allowed_values(&self) -> bool {
|
||||
let guard = self.get_definition()
|
||||
.allowed_values
|
||||
.read().unwrap();
|
||||
let guard = self.get_definition().allowed_values.read().unwrap();
|
||||
|
||||
!guard.is_empty()
|
||||
}
|
||||
@@ -250,9 +248,7 @@ pub trait UpnpVariable {
|
||||
/// retourne `false`. Utilisez [`has_allowed_values`](Self::has_allowed_values)
|
||||
/// pour distinguer "pas de liste" de "valeur non autorisée".
|
||||
fn is_an_allowed_value(&self, value: &StateValue) -> bool {
|
||||
let guard = self.get_definition()
|
||||
.allowed_values
|
||||
.read().unwrap();
|
||||
let guard = self.get_definition().allowed_values.read().unwrap();
|
||||
|
||||
guard.contains(value)
|
||||
}
|
||||
|
||||
@@ -9,17 +9,13 @@
|
||||
//! - `GET /api/upnp/devices/:udn` - Détails d'un device
|
||||
//! - `GET /api/upnp/devices/:udn/services/:service/variables` - Variables d'un service
|
||||
|
||||
use crate::{UpnpTyped, UpnpTypedInstance, state_variables::UpnpVariable, upnp_server};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Json},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use crate::{
|
||||
state_variables::UpnpVariable,
|
||||
upnp_server,
|
||||
UpnpTyped, UpnpTypedInstance,
|
||||
};
|
||||
use pmoserver::Server;
|
||||
use serde_json::json;
|
||||
@@ -65,7 +61,8 @@ async fn get_device(Path(udn): Path<String>) -> impl IntoResponse {
|
||||
.iter()
|
||||
.map(|s| {
|
||||
// Collecter les actions
|
||||
let actions: Vec<_> = s.actions()
|
||||
let actions: Vec<_> = s
|
||||
.actions()
|
||||
.all()
|
||||
.iter()
|
||||
.map(|a| {
|
||||
@@ -91,12 +88,13 @@ async fn get_device(Path(udn): Path<String>) -> impl IntoResponse {
|
||||
json!({
|
||||
"name": arg.get_name(),
|
||||
"related_state_variable": model.state_variable().get_name()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
json!({
|
||||
"name": a.get_name(),
|
||||
"stateless": !a.is_stateful(),
|
||||
"in_arguments": in_args,
|
||||
"out_arguments": out_args
|
||||
})
|
||||
@@ -143,7 +141,9 @@ async fn get_device(Path(udn): Path<String>) -> impl IntoResponse {
|
||||
/// Handler : Variables d'un service.
|
||||
///
|
||||
/// GET /api/upnp/devices/:udn/services/:service/variables
|
||||
async fn get_service_variables(Path((udn, service_name)): Path<(String, String)>) -> impl IntoResponse {
|
||||
async fn get_service_variables(
|
||||
Path((udn, service_name)): Path<(String, String)>,
|
||||
) -> impl IntoResponse {
|
||||
match upnp_server::get_device_by_udn(&udn) {
|
||||
Some(device) => match device.get_service(&service_name) {
|
||||
Some(service) => {
|
||||
@@ -168,7 +168,7 @@ async fn get_service_variables(Path((udn, service_name)): Path<(String, String)>
|
||||
let (min, max) = if let Some(range) = model.get_range() {
|
||||
(
|
||||
Some(range.get_minimum().to_string()),
|
||||
Some(range.get_maximum().to_string())
|
||||
Some(range.get_maximum().to_string()),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
|
||||
@@ -18,40 +18,37 @@
|
||||
//! + DeviceRegistry (thread_local storage)
|
||||
//! ```
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use pmoserver::Server;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use crate::devices::errors::DeviceError;
|
||||
use crate::devices::{Device, DeviceInstance, DeviceRegistry};
|
||||
use crate::UpnpModel;
|
||||
use crate::cache_registry::CACHE_REGISTRY;
|
||||
use crate::devices::errors::DeviceError;
|
||||
use crate::devices::{Device, DeviceInstance, DeviceRegistry};
|
||||
use crate::ssdp::SsdpServer;
|
||||
use crate::upnp_api::UpnpApiExt;
|
||||
|
||||
use pmocovers::Cache as CoverCache;
|
||||
use pmoaudiocache::Cache as AudioCache;
|
||||
use pmoutils::{find_process_using_port, TransportProtocol};
|
||||
use pmocovers::Cache as CoverCache;
|
||||
use pmoutils::{TransportProtocol, find_process_using_port};
|
||||
|
||||
/// Registre de devices global et thread-safe.
|
||||
///
|
||||
/// Utilise Lazy pour une initialisation paresseuse et RwLock pour le partage entre threads.
|
||||
/// Ceci permet aux API handlers (qui s'exécutent dans des threads différents) d'accéder
|
||||
/// au même registre de devices.
|
||||
static DEVICE_REGISTRY: Lazy<RwLock<DeviceRegistry>> = Lazy::new(|| {
|
||||
RwLock::new(DeviceRegistry::new())
|
||||
});
|
||||
static DEVICE_REGISTRY: Lazy<RwLock<DeviceRegistry>> =
|
||||
Lazy::new(|| RwLock::new(DeviceRegistry::new()));
|
||||
|
||||
/// Serveur SSDP global et thread-safe.
|
||||
///
|
||||
/// Utilise Lazy pour une initialisation paresseuse et RwLock pour le partage entre threads.
|
||||
/// Permet l'annonce automatique des devices UPnP sur le réseau.
|
||||
static SSDP_SERVER: Lazy<RwLock<Option<SsdpServer>>> = Lazy::new(|| {
|
||||
RwLock::new(None)
|
||||
});
|
||||
static SSDP_SERVER: Lazy<RwLock<Option<SsdpServer>>> = Lazy::new(|| RwLock::new(None));
|
||||
|
||||
/// Trait pour étendre un serveur avec des fonctionnalités UPnP.
|
||||
///
|
||||
@@ -97,7 +94,10 @@ pub trait UpnpServerExt {
|
||||
/// # Returns
|
||||
///
|
||||
/// L'instance du device créée et enregistrée.
|
||||
async fn register_device(&mut self, device: Arc<Device>) -> Result<Arc<DeviceInstance>, DeviceError>;
|
||||
async fn register_device(
|
||||
&mut self,
|
||||
device: Arc<Device>,
|
||||
) -> Result<Arc<DeviceInstance>, DeviceError>;
|
||||
|
||||
/// Retourne le nombre de devices enregistrés.
|
||||
fn device_count(&self) -> usize;
|
||||
@@ -123,8 +123,11 @@ pub trait UpnpServerExt {
|
||||
/// # Returns
|
||||
///
|
||||
/// Instance partagée du cache
|
||||
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize)
|
||||
-> Result<Arc<CoverCache>, anyhow::Error>;
|
||||
async fn init_cover_cache(
|
||||
&mut self,
|
||||
cache_dir: &str,
|
||||
limit: usize,
|
||||
) -> Result<Arc<CoverCache>, anyhow::Error>;
|
||||
|
||||
/// Initialiser le cache audio centralisé
|
||||
///
|
||||
@@ -139,8 +142,11 @@ pub trait UpnpServerExt {
|
||||
/// # Returns
|
||||
///
|
||||
/// Instance partagée du cache
|
||||
async fn init_audio_cache(&mut self, cache_dir: &str, limit: usize)
|
||||
-> Result<Arc<AudioCache>, anyhow::Error>;
|
||||
async fn init_audio_cache(
|
||||
&mut self,
|
||||
cache_dir: &str,
|
||||
limit: usize,
|
||||
) -> Result<Arc<AudioCache>, anyhow::Error>;
|
||||
|
||||
/// Initialiser les caches depuis la configuration
|
||||
///
|
||||
@@ -150,8 +156,7 @@ pub trait UpnpServerExt {
|
||||
/// # Returns
|
||||
///
|
||||
/// Tuple (cache de couvertures, cache audio)
|
||||
async fn init_caches(&mut self)
|
||||
-> Result<(Arc<CoverCache>, Arc<AudioCache>), anyhow::Error>;
|
||||
async fn init_caches(&mut self) -> Result<(Arc<CoverCache>, Arc<AudioCache>), anyhow::Error>;
|
||||
|
||||
/// Récupérer le cache de couvertures
|
||||
fn cover_cache(&self) -> Option<Arc<CoverCache>>;
|
||||
@@ -219,17 +224,32 @@ pub trait UpnpServerExt {
|
||||
|
||||
// Implémentation du trait UpnpServer pour pmoserver::Server
|
||||
impl UpnpServerExt for Server {
|
||||
async fn register_device(&mut self, device: Arc<Device>) -> Result<Arc<DeviceInstance>, DeviceError> {
|
||||
async fn register_device(
|
||||
&mut self,
|
||||
device: Arc<Device>,
|
||||
) -> Result<Arc<DeviceInstance>, DeviceError> {
|
||||
use tracing::info;
|
||||
|
||||
// Créer l'instance (retourne déjà un Arc<DeviceInstance>)
|
||||
let di = device.create_instance();
|
||||
let mut di = device.create_instance();
|
||||
|
||||
// Normaliser la base URL HTTP avant tout enregistrement.
|
||||
let server_base_url = self.base_url();
|
||||
if let Some(instance) = Arc::get_mut(&mut di) {
|
||||
instance.set_server_base_url(server_base_url);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Unable to set base URL on device {} before registration; keeping existing value",
|
||||
di.udn()
|
||||
);
|
||||
}
|
||||
|
||||
// Enregistrer les URLs dans le serveur web
|
||||
di.register_urls(self).await?;
|
||||
|
||||
// Ajouter au registre pour l'introspection
|
||||
DEVICE_REGISTRY.write()
|
||||
DEVICE_REGISTRY
|
||||
.write()
|
||||
.unwrap()
|
||||
.register(di.clone())
|
||||
.map_err(|e| DeviceError::UrlRegistrationError(e))?;
|
||||
@@ -261,10 +281,13 @@ impl UpnpServerExt for Server {
|
||||
|
||||
// ========= Cache Management Implementation =========
|
||||
|
||||
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize)
|
||||
-> Result<Arc<CoverCache>, anyhow::Error> {
|
||||
async fn init_cover_cache(
|
||||
&mut self,
|
||||
cache_dir: &str,
|
||||
limit: usize,
|
||||
) -> Result<Arc<CoverCache>, anyhow::Error> {
|
||||
use pmocache::pmoserver_ext::{create_api_router, create_file_router_with_generator};
|
||||
use pmocovers::new_cache;
|
||||
use pmocache::pmoserver_ext::{create_file_router_with_generator, create_api_router};
|
||||
|
||||
let base_url = self.info().base_url.clone();
|
||||
let cache = Arc::new(new_cache(cache_dir, limit)?);
|
||||
@@ -279,7 +302,13 @@ impl UpnpServerExt for Server {
|
||||
match pmocovers::webp::generate_variant(&cache, &pk, size).await {
|
||||
Ok(data) => return Some(data),
|
||||
Err(e) => {
|
||||
tracing::warn!("Cannot generate variant {}x{} for {}: {}", size, size, pk, e);
|
||||
tracing::warn!(
|
||||
"Cannot generate variant {}x{} for {}: {}",
|
||||
size,
|
||||
size,
|
||||
pk,
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -288,11 +317,8 @@ impl UpnpServerExt for Server {
|
||||
})
|
||||
});
|
||||
|
||||
let file_router = create_file_router_with_generator(
|
||||
cache.clone(),
|
||||
"image/webp",
|
||||
Some(variant_generator)
|
||||
);
|
||||
let file_router =
|
||||
create_file_router_with_generator(cache.clone(), "image/webp", Some(variant_generator));
|
||||
self.add_router("/", file_router).await;
|
||||
|
||||
// API REST générique (pmocache)
|
||||
@@ -310,10 +336,13 @@ impl UpnpServerExt for Server {
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
async fn init_audio_cache(&mut self, cache_dir: &str, limit: usize)
|
||||
-> Result<Arc<AudioCache>, anyhow::Error> {
|
||||
async fn init_audio_cache(
|
||||
&mut self,
|
||||
cache_dir: &str,
|
||||
limit: usize,
|
||||
) -> Result<Arc<AudioCache>, anyhow::Error> {
|
||||
use pmoaudiocache::new_cache;
|
||||
use pmocache::pmoserver_ext::{create_file_router, create_api_router};
|
||||
use pmocache::pmoserver_ext::{create_api_router, create_file_router};
|
||||
|
||||
let base_url = self.info().base_url.clone();
|
||||
let cache = Arc::new(new_cache(cache_dir, limit)?);
|
||||
@@ -337,19 +366,22 @@ impl UpnpServerExt for Server {
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
async fn init_caches(&mut self)
|
||||
-> Result<(Arc<CoverCache>, Arc<AudioCache>), anyhow::Error> {
|
||||
async fn init_caches(&mut self) -> Result<(Arc<CoverCache>, Arc<AudioCache>), anyhow::Error> {
|
||||
let config = pmoconfig::get_config();
|
||||
|
||||
let cover_cache = self.init_cover_cache(
|
||||
&config.get_cover_cache_dir()?,
|
||||
config.get_cover_cache_size()?
|
||||
).await?;
|
||||
let cover_cache = self
|
||||
.init_cover_cache(
|
||||
&config.get_cover_cache_dir()?,
|
||||
config.get_cover_cache_size()?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let audio_cache = self.init_audio_cache(
|
||||
&config.get_audio_cache_dir()?,
|
||||
config.get_audio_cache_size()?
|
||||
).await?;
|
||||
let audio_cache = self
|
||||
.init_audio_cache(
|
||||
&config.get_audio_cache_dir()?,
|
||||
config.get_audio_cache_size()?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((cover_cache, audio_cache))
|
||||
}
|
||||
@@ -425,9 +457,7 @@ impl UpnpServerExt for Server {
|
||||
let kind = e.kind();
|
||||
if kind == std::io::ErrorKind::AddrInUse {
|
||||
let port = crate::ssdp::SSDP_PORT;
|
||||
if let Some(process) =
|
||||
find_process_using_port(port, TransportProtocol::Udp)
|
||||
{
|
||||
if let Some(process) = find_process_using_port(port, TransportProtocol::Udp) {
|
||||
error!(
|
||||
"❌ SSDP initialization failed: port {} is already in use by \
|
||||
PID {} ({}) owned by {}: {}",
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// (comme Uuid, Url, et certains types chrono), nous fournissons des méthodes de conversion
|
||||
// vers des types primitifs qui supportent Reflect.
|
||||
|
||||
use bevy_reflect::Reflect;
|
||||
use crate::variable_types::{StateValue, StateValueError, StateVarType};
|
||||
use bevy_reflect::Reflect;
|
||||
|
||||
impl StateValue {
|
||||
/// Convertit la StateValue en une valeur Reflect.
|
||||
@@ -47,79 +47,88 @@ impl StateValue {
|
||||
/// Méthode statique utilisée pour reconstruire StateValue depuis Reflect
|
||||
pub fn from_reflect(
|
||||
value: &dyn Reflect,
|
||||
expected_type: StateVarType
|
||||
expected_type: StateVarType,
|
||||
) -> Result<StateValue, StateValueError> {
|
||||
match expected_type {
|
||||
StateVarType::UI1 => {
|
||||
value.as_any().downcast_ref::<u8>()
|
||||
.map(|v| StateValue::UI1(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected u8".into()))
|
||||
},
|
||||
StateVarType::UI2 => {
|
||||
value.as_any().downcast_ref::<u16>()
|
||||
.map(|v| StateValue::UI2(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected u16".into()))
|
||||
},
|
||||
StateVarType::UI4 => {
|
||||
value.as_any().downcast_ref::<u32>()
|
||||
.map(|v| StateValue::UI4(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected u32".into()))
|
||||
},
|
||||
StateVarType::I1 => {
|
||||
value.as_any().downcast_ref::<i8>()
|
||||
.map(|v| StateValue::I1(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected i8".into()))
|
||||
},
|
||||
StateVarType::I2 => {
|
||||
value.as_any().downcast_ref::<i16>()
|
||||
.map(|v| StateValue::I2(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected i16".into()))
|
||||
},
|
||||
StateVarType::I4 | StateVarType::Int => {
|
||||
value.as_any().downcast_ref::<i32>()
|
||||
.map(|v| StateValue::I4(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected i32".into()))
|
||||
},
|
||||
StateVarType::R4 => {
|
||||
value.as_any().downcast_ref::<f32>()
|
||||
.map(|v| StateValue::R4(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected f32".into()))
|
||||
},
|
||||
StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => {
|
||||
value.as_any().downcast_ref::<f64>()
|
||||
.map(|v| StateValue::R8(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected f64".into()))
|
||||
},
|
||||
StateVarType::String | StateVarType::BinBase64 | StateVarType::BinHex => {
|
||||
value.as_any().downcast_ref::<String>()
|
||||
.map(|v| match expected_type {
|
||||
StateVarType::String => StateValue::String(v.clone()),
|
||||
StateVarType::BinBase64 => StateValue::BinBase64(v.clone()),
|
||||
StateVarType::BinHex => StateValue::BinHex(v.clone()),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected String".into()))
|
||||
},
|
||||
StateVarType::Boolean => {
|
||||
value.as_any().downcast_ref::<bool>()
|
||||
.map(|v| StateValue::Boolean(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected bool".into()))
|
||||
},
|
||||
StateVarType::Char => {
|
||||
value.as_any().downcast_ref::<char>()
|
||||
.map(|v| StateValue::Char(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected char".into()))
|
||||
},
|
||||
StateVarType::UI1 => value
|
||||
.as_any()
|
||||
.downcast_ref::<u8>()
|
||||
.map(|v| StateValue::UI1(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected u8".into())),
|
||||
StateVarType::UI2 => value
|
||||
.as_any()
|
||||
.downcast_ref::<u16>()
|
||||
.map(|v| StateValue::UI2(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected u16".into())),
|
||||
StateVarType::UI4 => value
|
||||
.as_any()
|
||||
.downcast_ref::<u32>()
|
||||
.map(|v| StateValue::UI4(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected u32".into())),
|
||||
StateVarType::I1 => value
|
||||
.as_any()
|
||||
.downcast_ref::<i8>()
|
||||
.map(|v| StateValue::I1(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected i8".into())),
|
||||
StateVarType::I2 => value
|
||||
.as_any()
|
||||
.downcast_ref::<i16>()
|
||||
.map(|v| StateValue::I2(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected i16".into())),
|
||||
StateVarType::I4 | StateVarType::Int => value
|
||||
.as_any()
|
||||
.downcast_ref::<i32>()
|
||||
.map(|v| StateValue::I4(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected i32".into())),
|
||||
StateVarType::R4 => value
|
||||
.as_any()
|
||||
.downcast_ref::<f32>()
|
||||
.map(|v| StateValue::R4(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected f32".into())),
|
||||
StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => value
|
||||
.as_any()
|
||||
.downcast_ref::<f64>()
|
||||
.map(|v| StateValue::R8(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected f64".into())),
|
||||
StateVarType::String | StateVarType::BinBase64 | StateVarType::BinHex => value
|
||||
.as_any()
|
||||
.downcast_ref::<String>()
|
||||
.map(|v| match expected_type {
|
||||
StateVarType::String => StateValue::String(v.clone()),
|
||||
StateVarType::BinBase64 => StateValue::BinBase64(v.clone()),
|
||||
StateVarType::BinHex => StateValue::BinHex(v.clone()),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected String".into())),
|
||||
StateVarType::Boolean => value
|
||||
.as_any()
|
||||
.downcast_ref::<bool>()
|
||||
.map(|v| StateValue::Boolean(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected bool".into())),
|
||||
StateVarType::Char => value
|
||||
.as_any()
|
||||
.downcast_ref::<char>()
|
||||
.map(|v| StateValue::Char(*v))
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected char".into())),
|
||||
// Pour les types complexes, on essaie de reconstruire depuis String
|
||||
StateVarType::Date | StateVarType::DateTime | StateVarType::DateTimeTZ |
|
||||
StateVarType::Time | StateVarType::TimeTZ | StateVarType::UUID | StateVarType::URI => {
|
||||
value.as_any().downcast_ref::<String>()
|
||||
.ok_or_else(|| StateValueError::TypeError("Expected String representation".into()))
|
||||
StateVarType::Date
|
||||
| StateVarType::DateTime
|
||||
| StateVarType::DateTimeTZ
|
||||
| StateVarType::Time
|
||||
| StateVarType::TimeTZ
|
||||
| StateVarType::UUID
|
||||
| StateVarType::URI => {
|
||||
value
|
||||
.as_any()
|
||||
.downcast_ref::<String>()
|
||||
.ok_or_else(|| {
|
||||
StateValueError::TypeError("Expected String representation".into())
|
||||
})
|
||||
.and_then(|s| {
|
||||
// Utiliser les méthodes from_string existantes
|
||||
StateValue::from_string(s, &expected_type)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,41 +116,53 @@ impl StateValue {
|
||||
use uuid::Uuid;
|
||||
|
||||
match var_type {
|
||||
StateVarType::UI1 => s.parse::<u8>()
|
||||
StateVarType::UI1 => s
|
||||
.parse::<u8>()
|
||||
.map(StateValue::UI1)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse UI1: {}", e))),
|
||||
StateVarType::UI2 => s.parse::<u16>()
|
||||
StateVarType::UI2 => s
|
||||
.parse::<u16>()
|
||||
.map(StateValue::UI2)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse UI2: {}", e))),
|
||||
StateVarType::UI4 => s.parse::<u32>()
|
||||
StateVarType::UI4 => s
|
||||
.parse::<u32>()
|
||||
.map(StateValue::UI4)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse UI4: {}", e))),
|
||||
StateVarType::I1 => s.parse::<i8>()
|
||||
StateVarType::I1 => s
|
||||
.parse::<i8>()
|
||||
.map(StateValue::I1)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse I1: {}", e))),
|
||||
StateVarType::I2 => s.parse::<i16>()
|
||||
StateVarType::I2 => s
|
||||
.parse::<i16>()
|
||||
.map(StateValue::I2)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse I2: {}", e))),
|
||||
StateVarType::I4 | StateVarType::Int => s.parse::<i32>()
|
||||
StateVarType::I4 | StateVarType::Int => s
|
||||
.parse::<i32>()
|
||||
.map(StateValue::I4)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse I4/Int: {}", e))),
|
||||
StateVarType::R4 => s.parse::<f32>()
|
||||
StateVarType::R4 => s
|
||||
.parse::<f32>()
|
||||
.map(StateValue::R4)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse R4: {}", e))),
|
||||
StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => s.parse::<f64>()
|
||||
.map(StateValue::R8)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse R8/Number: {}", e))),
|
||||
StateVarType::Char => s.chars().next()
|
||||
StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => {
|
||||
s.parse::<f64>().map(StateValue::R8).map_err(|e| {
|
||||
StateValueError::ParseError(format!("Failed to parse R8/Number: {}", e))
|
||||
})
|
||||
}
|
||||
StateVarType::Char => s
|
||||
.chars()
|
||||
.next()
|
||||
.ok_or_else(|| StateValueError::ParseError("Empty string for Char".to_string()))
|
||||
.map(StateValue::Char),
|
||||
StateVarType::String => Ok(StateValue::String(s.to_string())),
|
||||
StateVarType::Boolean => {
|
||||
match s.to_lowercase().as_str() {
|
||||
"true" | "1" | "yes" => Ok(StateValue::Boolean(true)),
|
||||
"false" | "0" | "no" => Ok(StateValue::Boolean(false)),
|
||||
_ => Err(StateValueError::ParseError(format!("Invalid boolean value: {}", s))),
|
||||
}
|
||||
}
|
||||
StateVarType::Boolean => match s.to_lowercase().as_str() {
|
||||
"true" | "1" | "yes" => Ok(StateValue::Boolean(true)),
|
||||
"false" | "0" | "no" => Ok(StateValue::Boolean(false)),
|
||||
_ => Err(StateValueError::ParseError(format!(
|
||||
"Invalid boolean value: {}",
|
||||
s
|
||||
))),
|
||||
},
|
||||
StateVarType::BinBase64 => Ok(StateValue::BinBase64(s.to_string())),
|
||||
StateVarType::BinHex => Ok(StateValue::BinHex(s.to_string())),
|
||||
StateVarType::Date => NaiveDate::parse_from_str(s, "%Y-%m-%d")
|
||||
@@ -159,16 +171,24 @@ impl StateValue {
|
||||
StateVarType::DateTime => chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")
|
||||
.or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S"))
|
||||
.map(StateValue::DateTime)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse DateTime: {}", e))),
|
||||
.map_err(|e| {
|
||||
StateValueError::ParseError(format!("Failed to parse DateTime: {}", e))
|
||||
}),
|
||||
StateVarType::DateTimeTZ => chrono::DateTime::parse_from_rfc3339(s)
|
||||
.map(|dt| StateValue::DateTimeTZ(dt.into()))
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse DateTimeTZ: {}", e))),
|
||||
.map_err(|e| {
|
||||
StateValueError::ParseError(format!("Failed to parse DateTimeTZ: {}", e))
|
||||
}),
|
||||
StateVarType::Time => chrono::NaiveTime::parse_from_str(s, "%H:%M:%S")
|
||||
.map(StateValue::Time)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse Time: {}", e))),
|
||||
StateVarType::TimeTZ => chrono::DateTime::parse_from_rfc3339(&format!("1970-01-01T{}", s))
|
||||
.map(|dt| StateValue::TimeTZ(dt.into()))
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse TimeTZ: {}", e))),
|
||||
StateVarType::TimeTZ => {
|
||||
chrono::DateTime::parse_from_rfc3339(&format!("1970-01-01T{}", s))
|
||||
.map(|dt| StateValue::TimeTZ(dt.into()))
|
||||
.map_err(|e| {
|
||||
StateValueError::ParseError(format!("Failed to parse TimeTZ: {}", e))
|
||||
})
|
||||
}
|
||||
StateVarType::UUID => Uuid::parse_str(s)
|
||||
.map(StateValue::UUID)
|
||||
.map_err(|e| StateValueError::ParseError(format!("Failed to parse UUID: {}", e))),
|
||||
|
||||
@@ -17,4 +17,4 @@ impl TryFrom<String> for StateValue {
|
||||
fn try_from(s: String) -> Result<Self, StateValueError> {
|
||||
Ok(StateValue::String(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user