Ajoute la notion de reflexive value au statevariableinstance
This commit is contained in:
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -1958,7 +1958,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"indexmap",
|
||||
"quick-xml",
|
||||
"quick-xml 0.38.3",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
@@ -2022,7 +2022,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevy_reflect",
|
||||
"bevy_reflect_derive",
|
||||
"quick-xml",
|
||||
"quick-xml 0.38.3",
|
||||
"serde",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
@@ -2066,6 +2066,7 @@ dependencies = [
|
||||
"pmodidl",
|
||||
"pmoserver",
|
||||
"pmoutils",
|
||||
"quick-xml 0.37.5",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2189,6 +2190,16 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.3"
|
||||
|
||||
@@ -19,6 +19,7 @@ axum = "0.8.4"
|
||||
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
quick-xml = { version = "0.37.0", features = ["serialize"] }
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
once_cell = "1.20"
|
||||
parking_lot = "0.12"
|
||||
|
||||
@@ -34,6 +34,7 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use bevy_reflect::Reflect;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
@@ -44,6 +45,7 @@ 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,
|
||||
@@ -118,8 +120,8 @@ pub struct ServiceInstance {
|
||||
/// Abonnés aux événements (SID -> Callback URL)
|
||||
subscribers: Arc<RwLock<HashMap<String, String>>>,
|
||||
|
||||
/// Buffer des changements en attente de notification
|
||||
changed_buffer: Arc<Mutex<HashMap<String, String>>>,
|
||||
/// Buffer des changements en attente de notification (nom de variable -> valeur réflexive)
|
||||
changed_buffer: Arc<Mutex<HashMap<String, Arc<dyn Reflect>>>>,
|
||||
|
||||
/// Compteurs de séquence par abonné
|
||||
seqid: Arc<Mutex<HashMap<String, u32>>>,
|
||||
@@ -849,14 +851,15 @@ impl ServiceInstance {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// # use pmoupnp::services::Service;
|
||||
/// # use pmoupnp::UpnpModel;
|
||||
/// let service = Service::new("AVTransport".to_string());
|
||||
/// let instance = service.create_instance();
|
||||
/// instance.event_to_be_sent("TransportState".to_string(), "PLAYING".to_string());
|
||||
/// let value = Arc::new("PLAYING".to_string()) as Arc<dyn Reflect>;
|
||||
/// instance.event_to_be_sent("TransportState".to_string(), value);
|
||||
/// ```
|
||||
pub fn event_to_be_sent(&self, name: String, value: String) {
|
||||
pub fn event_to_be_sent(&self, name: String, value: Arc<dyn Reflect>) {
|
||||
let mut buffer = self.changed_buffer.lock().unwrap();
|
||||
buffer.insert(name, value);
|
||||
}
|
||||
@@ -895,7 +898,10 @@ impl ServiceInstance {
|
||||
/// # async fn main() {
|
||||
/// # let service = Service::new("AVTransport".to_string());
|
||||
/// # let instance = service.create_instance();
|
||||
/// instance.event_to_be_sent("TransportState".to_string(), "PLAYING".to_string());
|
||||
/// # use std::sync::Arc;
|
||||
/// # use bevy_reflect::Reflect;
|
||||
/// let value = Arc::new("PLAYING".to_string()) as Arc<dyn Reflect>;
|
||||
/// instance.event_to_be_sent("TransportState".to_string(), value);
|
||||
/// instance.notify_subscribers().await;
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -926,9 +932,11 @@ impl ServiceInstance {
|
||||
let mut body =
|
||||
r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"#.to_string();
|
||||
for (name, val) in changed_clone {
|
||||
// Convertir la valeur Reflect en String
|
||||
let val_str = Self::reflect_to_string(&*val);
|
||||
body.push_str(&format!(
|
||||
"<e:property><{0}>{1}</{0}></e:property>",
|
||||
name, val
|
||||
name, val_str
|
||||
));
|
||||
}
|
||||
body.push_str("</e:propertyset>");
|
||||
@@ -956,6 +964,131 @@ impl ServiceInstance {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit une valeur Reflect en String pour la notification UPnP.
|
||||
///
|
||||
/// Cette fonction gère plusieurs cas :
|
||||
/// - Types primitifs : formatage direct
|
||||
/// - Structures serde (pmodidl, etc.) : sérialisation XML
|
||||
/// - Autres types : fallback sur Debug
|
||||
///
|
||||
/// Le résultat est déjà échappé XML-safe selon les normes UPnP.
|
||||
fn reflect_to_string(value: &dyn Reflect) -> String {
|
||||
use std::any::Any;
|
||||
use bevy_reflect::ReflectRef;
|
||||
|
||||
// Essayer de downcaster vers des types primitifs courants
|
||||
if let Some(v) = value.as_any().downcast_ref::<String>() {
|
||||
// Échapper les caractères XML spéciaux
|
||||
return escape(v).to_string();
|
||||
} else if let Some(v) = value.as_any().downcast_ref::<u8>() {
|
||||
return v.to_string();
|
||||
} else if let Some(v) = value.as_any().downcast_ref::<u16>() {
|
||||
return v.to_string();
|
||||
} else if let Some(v) = value.as_any().downcast_ref::<u32>() {
|
||||
return v.to_string();
|
||||
} else if let Some(v) = value.as_any().downcast_ref::<i8>() {
|
||||
return v.to_string();
|
||||
} else if let Some(v) = value.as_any().downcast_ref::<i16>() {
|
||||
return v.to_string();
|
||||
} else if let Some(v) = value.as_any().downcast_ref::<i32>() {
|
||||
return v.to_string();
|
||||
} else if let Some(v) = value.as_any().downcast_ref::<f32>() {
|
||||
return v.to_string();
|
||||
} else if let Some(v) = value.as_any().downcast_ref::<f64>() {
|
||||
return v.to_string();
|
||||
} else if let Some(v) = value.as_any().downcast_ref::<bool>() {
|
||||
return if *v { "1" } else { "0" }.to_string();
|
||||
} else if let Some(v) = value.as_any().downcast_ref::<char>() {
|
||||
return escape(&v.to_string()).to_string();
|
||||
}
|
||||
|
||||
// Pour les structures complexes, essayer de sérialiser avec bevy_reflect
|
||||
match value.reflect_ref() {
|
||||
ReflectRef::Struct(s) => {
|
||||
// Construire un XML simple pour la struct
|
||||
Self::serialize_struct_to_xml(s)
|
||||
}
|
||||
ReflectRef::TupleStruct(ts) => {
|
||||
// Pour les tuple structs, essayer d'extraire la valeur si c'est un wrapper
|
||||
if ts.field_len() == 1 {
|
||||
if let Some(inner) = ts.field(0) {
|
||||
// Convertir PartialReflect en Reflect si possible
|
||||
if let Some(reflect_val) = inner.try_as_reflect() {
|
||||
Self::reflect_to_string(reflect_val)
|
||||
} else {
|
||||
format!("{:?}", value)
|
||||
}
|
||||
} else {
|
||||
format!("{:?}", value)
|
||||
}
|
||||
} else {
|
||||
format!("{:?}", value)
|
||||
}
|
||||
}
|
||||
ReflectRef::Enum(e) => {
|
||||
// Pour les enums, formater comme "Variant(value)"
|
||||
let variant_name = e.variant_name();
|
||||
if e.field_len() == 1 {
|
||||
if let Some(field) = e.field_at(0) {
|
||||
// Convertir PartialReflect en Reflect si possible
|
||||
if let Some(reflect_val) = field.try_as_reflect() {
|
||||
format!("{}", Self::reflect_to_string(reflect_val))
|
||||
} else {
|
||||
variant_name.to_string()
|
||||
}
|
||||
} else {
|
||||
variant_name.to_string()
|
||||
}
|
||||
} else {
|
||||
variant_name.to_string()
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Fallback: utiliser Debug et échapper
|
||||
let debug_str = format!("{:?}", value);
|
||||
escape(&debug_str).to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
let mut xml = String::new();
|
||||
|
||||
// Commencer par ouvrir la balise avec le nom du type
|
||||
let type_name = s.get_represented_type_info()
|
||||
.and_then(|ti| {
|
||||
if let TypeInfo::Struct(si) = ti {
|
||||
Some(si.type_path_table().short_path())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or("struct");
|
||||
|
||||
let _ = write!(&mut xml, "<{}>", type_name);
|
||||
|
||||
// Ajouter chaque champ
|
||||
for i in 0..s.field_len() {
|
||||
if let Some(field_name) = s.name_at(i) {
|
||||
if let Some(field_value) = s.field_at(i) {
|
||||
// Convertir PartialReflect en Reflect si possible
|
||||
if let Some(reflect_val) = field_value.try_as_reflect() {
|
||||
let value_str = Self::reflect_to_string(reflect_val);
|
||||
let _ = write!(&mut xml, "<{}>{}</{}>", field_name, value_str, field_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = write!(&mut xml, "</{}>", type_name);
|
||||
|
||||
xml
|
||||
}
|
||||
|
||||
/// Démarre le notifier périodique.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -1239,6 +1372,97 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: Str
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::services::Service;
|
||||
use bevy_reflect::Reflect;
|
||||
|
||||
#[test]
|
||||
fn test_reflect_to_string_primitives() {
|
||||
// Test des types primitifs
|
||||
assert_eq!(ServiceInstance::reflect_to_string(&42i32), "42");
|
||||
assert_eq!(ServiceInstance::reflect_to_string(&3.14f64), "3.14");
|
||||
assert_eq!(ServiceInstance::reflect_to_string(&true), "1");
|
||||
assert_eq!(ServiceInstance::reflect_to_string(&false), "0");
|
||||
assert_eq!(ServiceInstance::reflect_to_string(&'a'), "a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reflect_to_string_xml_escaping() {
|
||||
// Test de l'échappement XML
|
||||
let test_str = "Test <tag> & \"quotes\"".to_string();
|
||||
let result = ServiceInstance::reflect_to_string(&test_str);
|
||||
|
||||
// Vérifier que les caractères sont échappés
|
||||
assert!(result.contains("<"));
|
||||
assert!(result.contains(">"));
|
||||
assert!(result.contains("&"));
|
||||
assert!(result.contains("""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reflect_to_string_struct() {
|
||||
#[derive(Debug, Clone, Reflect)]
|
||||
struct TestStruct {
|
||||
name: String,
|
||||
value: i32,
|
||||
}
|
||||
|
||||
let test = TestStruct {
|
||||
name: "Test".to_string(),
|
||||
value: 42,
|
||||
};
|
||||
|
||||
let result = ServiceInstance::reflect_to_string(&test);
|
||||
|
||||
// Vérifier que c'est du XML
|
||||
assert!(result.starts_with("<"));
|
||||
assert!(result.ends_with(">"));
|
||||
assert!(result.contains("name"));
|
||||
assert!(result.contains("value"));
|
||||
assert!(result.contains("Test"));
|
||||
assert!(result.contains("42"));
|
||||
|
||||
println!("Serialized struct: {}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reflect_to_string_nested_struct() {
|
||||
#[derive(Debug, Clone, Reflect)]
|
||||
struct Address {
|
||||
street: String,
|
||||
city: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Reflect)]
|
||||
struct Person {
|
||||
name: String,
|
||||
age: u32,
|
||||
address: Address,
|
||||
}
|
||||
|
||||
let person = Person {
|
||||
name: "John <Doe>".to_string(), // Test XML escaping
|
||||
age: 30,
|
||||
address: Address {
|
||||
street: "123 Main St & Ave".to_string(),
|
||||
city: "Springfield".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let result = ServiceInstance::reflect_to_string(&person);
|
||||
|
||||
// Vérifier la structure XML
|
||||
assert!(result.contains("<Person>"));
|
||||
assert!(result.contains("</Person>"));
|
||||
assert!(result.contains("name"));
|
||||
assert!(result.contains("age"));
|
||||
assert!(result.contains("address"));
|
||||
|
||||
// Vérifier l'échappement XML dans les valeurs imbriquées
|
||||
assert!(result.contains("<"));
|
||||
assert!(result.contains(">"));
|
||||
assert!(result.contains("&"));
|
||||
|
||||
println!("Nested struct XML: {}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_instance_creation() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy_reflect::Reflect;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::sync::RwLock;
|
||||
use xmltree::Element;
|
||||
@@ -44,6 +46,7 @@ impl UpnpInstance for StateVarInstance {
|
||||
last_modified: RwLock::new(Utc::now()),
|
||||
last_notification: RwLock::new(Utc::now()),
|
||||
service: RwLock::new(None),
|
||||
reflexive_cache: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +88,7 @@ impl Clone for StateVarInstance {
|
||||
last_modified: RwLock::new(self.last_modified.read().unwrap().clone()),
|
||||
last_notification: RwLock::new(self.last_notification.read().unwrap().clone()),
|
||||
service: RwLock::new(self.service.read().unwrap().clone()),
|
||||
reflexive_cache: RwLock::new(None), // Le cache n'est pas cloné, il sera recalculé si nécessaire
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,6 +135,12 @@ impl StateVarInstance {
|
||||
*val = new_value.clone();
|
||||
*modified = Utc::now();
|
||||
|
||||
// Invalider le cache réflexif
|
||||
{
|
||||
let mut cache = self.reflexive_cache.write().unwrap();
|
||||
*cache = None;
|
||||
}
|
||||
|
||||
// Notifier le service parent si la variable envoie des événements
|
||||
if self.is_sending_notification() {
|
||||
// Relâcher les locks avant d'appeler le service
|
||||
@@ -140,7 +150,10 @@ impl StateVarInstance {
|
||||
|
||||
if let Some(weak_service) = self.service.read().unwrap().as_ref() {
|
||||
if let Some(service) = weak_service.upgrade() {
|
||||
service.event_to_be_sent(self.get_name().to_string(), new_value.to_string());
|
||||
// Obtenir la valeur réflexive (sans propager l'erreur car on est dans une notification)
|
||||
if let Ok(reflected_value) = self.reflexive_value() {
|
||||
service.event_to_be_sent(self.get_name().to_string(), reflected_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,4 +169,68 @@ impl StateVarInstance {
|
||||
pub fn last_modified(&self) -> DateTime<Utc> {
|
||||
self.last_modified.read().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Retourne la valeur sous forme réflexive (Reflect).
|
||||
///
|
||||
/// Cette méthode utilise un cache pour optimiser les performances lorsqu'un parser
|
||||
/// est défini. Si la variable a un parser, la valeur String sera parsée et le résultat
|
||||
/// sera mis en cache. Sinon, la StateValue brute est retournée directement comme Reflect.
|
||||
///
|
||||
/// Le cache est invalidé automatiquement lors de `set_value()`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Un `Arc<dyn Reflect>` contenant soit:
|
||||
/// - La valeur parsée (si un parser est défini)
|
||||
/// - La StateValue brute (sinon)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let var = StateVarInstance::new(&variable);
|
||||
/// 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> {
|
||||
// Vérifier si on a un cache valide
|
||||
{
|
||||
let cache = self.reflexive_cache.read().unwrap();
|
||||
if let Some(cached) = cache.as_ref() {
|
||||
return Ok(Arc::clone(cached));
|
||||
}
|
||||
}
|
||||
|
||||
// Pas de cache, il faut calculer la valeur
|
||||
let value = self.value.read().unwrap().clone();
|
||||
|
||||
// Si la variable a un parser, l'utiliser
|
||||
if let Some(parser) = &self.model.parse {
|
||||
// La valeur doit être une String pour être parsée
|
||||
if let crate::variable_types::StateValue::String(s) = &value {
|
||||
match parser(s) {
|
||||
Ok(parsed) => {
|
||||
// Convertir Box<dyn Reflect> en Arc<dyn Reflect>
|
||||
let arc_reflect: Arc<dyn Reflect> = Arc::from(parsed);
|
||||
|
||||
// Mettre en cache
|
||||
let mut cache = self.reflexive_cache.write().unwrap();
|
||||
*cache = Some(Arc::clone(&arc_reflect));
|
||||
|
||||
return Ok(arc_reflect);
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pas de parser ou la valeur n'est pas une String: convertir la StateValue en Reflect
|
||||
let reflected = value.to_reflect();
|
||||
let arc_reflect: Arc<dyn Reflect> = Arc::from(reflected);
|
||||
|
||||
// Mettre en cache
|
||||
let mut cache = self.reflexive_cache.write().unwrap();
|
||||
*cache = Some(Arc::clone(&arc_reflect));
|
||||
|
||||
Ok(arc_reflect)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ pub struct StateVarInstance {
|
||||
last_notification: RwLock<DateTime<Utc>>,
|
||||
/// Pointeur vers le service parent (interior mutability)
|
||||
service: RwLock<Option<std::sync::Weak<crate::services::ServiceInstance>>>,
|
||||
/// Cache pour la valeur réflexive (utilisé quand un parser est défini)
|
||||
reflexive_cache: RwLock<Option<Arc<dyn Reflect>>>,
|
||||
}
|
||||
|
||||
pub type StateVarInstanceSet = UpnpObjectSet<StateVarInstance>;
|
||||
|
||||
@@ -4,6 +4,7 @@ mod display_type;
|
||||
mod display_value;
|
||||
mod errors;
|
||||
mod fromstr;
|
||||
mod reflect_impl;
|
||||
mod type_methods;
|
||||
mod type_trait;
|
||||
mod value_methods;
|
||||
|
||||
44
pmoupnp/src/variable_types/reflect_impl.rs
Normal file
44
pmoupnp/src/variable_types/reflect_impl.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
// Ce module permet de convertir StateValue en valeurs Reflect
|
||||
//
|
||||
// Étant donné que StateValue contient des types qui n'implémentent pas tous Reflect
|
||||
// (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;
|
||||
|
||||
impl StateValue {
|
||||
/// Convertit la StateValue en une valeur Reflect.
|
||||
///
|
||||
/// Cette méthode crée un Box<dyn Reflect> contenant la valeur sous-jacente.
|
||||
/// Pour les types qui n'implémentent pas Reflect nativement (Uuid, Url, dates complexes),
|
||||
/// on retourne leur représentation String.
|
||||
pub fn to_reflect(&self) -> Box<dyn Reflect> {
|
||||
match self {
|
||||
StateValue::UI1(v) => Box::new(*v),
|
||||
StateValue::UI2(v) => Box::new(*v),
|
||||
StateValue::UI4(v) => Box::new(*v),
|
||||
StateValue::I1(v) => Box::new(*v),
|
||||
StateValue::I2(v) => Box::new(*v),
|
||||
StateValue::I4(v) => Box::new(*v),
|
||||
StateValue::Int(v) => Box::new(*v),
|
||||
StateValue::R4(v) => Box::new(*v),
|
||||
StateValue::R8(v) => Box::new(*v),
|
||||
StateValue::Number(v) => Box::new(*v),
|
||||
StateValue::Fixed14_4(v) => Box::new(*v),
|
||||
StateValue::Char(v) => Box::new(*v),
|
||||
StateValue::String(v) => Box::new(v.clone()),
|
||||
StateValue::Boolean(v) => Box::new(*v),
|
||||
StateValue::BinBase64(v) => Box::new(v.clone()),
|
||||
StateValue::BinHex(v) => Box::new(v.clone()),
|
||||
// Pour les types complexes, on utilise leur représentation String
|
||||
StateValue::Date(v) => Box::new(v.to_string()),
|
||||
StateValue::DateTime(v) => Box::new(v.to_string()),
|
||||
StateValue::DateTimeTZ(v) => Box::new(v.to_string()),
|
||||
StateValue::Time(v) => Box::new(v.to_string()),
|
||||
StateValue::TimeTZ(v) => Box::new(v.to_string()),
|
||||
StateValue::UUID(v) => Box::new(v.to_string()),
|
||||
StateValue::URI(v) => Box::new(v.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user