amélioration de la webapp

This commit is contained in:
2025-10-19 13:42:29 +02:00
parent bdfba87de4
commit 82fe72d659
179 changed files with 2426 additions and 1685 deletions

View File

@@ -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.
///

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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()),
}
}
}

View File

@@ -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()),
}
}
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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))
}
}
}

View File

@@ -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));
}

View File

@@ -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)
)
)

View File

@@ -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.

View File

@@ -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
///

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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));
}

View File

@@ -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() {

View File

@@ -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,
};

View File

@@ -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),
}

View File

@@ -194,4 +194,4 @@ impl<T: UpnpTypedObject> UpnpObjectSet<T> {
let guard = self.objects.read().unwrap();
guard.values().cloned().collect()
}
}
}

View File

@@ -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 {}

View File

@@ -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 {
}
}
}
}
}

View File

@@ -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")
}
}

View File

@@ -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(),

View File

@@ -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;
}
}

View File

@@ -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\""));

View File

@@ -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

View File

@@ -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));

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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};

View File

@@ -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 {

View File

@@ -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>;

View File

@@ -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()),
}
}
}

View File

@@ -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;
}

View File

@@ -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());

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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 {}: {}",

View File

@@ -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)
})
},
}
}
}
}

View File

@@ -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))),

View File

@@ -17,4 +17,4 @@ impl TryFrom<String> for StateValue {
fn try_from(s: String) -> Result<Self, StateValueError> {
Ok(StateValue::String(s))
}
}
}