homie_controller/
types.rs

1use crate::values::{ColorFormat, EnumValue, Value, ValueError};
2use std::collections::HashMap;
3use std::fmt::{self, Debug, Display, Formatter};
4use std::ops::RangeInclusive;
5use std::str::FromStr;
6use std::time::Duration;
7use thiserror::Error;
8
9/// The state of a Homie device according to the Homie
10/// [device lifecycle](https://homieiot.github.io/specification/#device-lifecycle).
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum State {
13    /// The state of the device is not yet known to the controller because device discovery is still
14    /// underway.
15    Unknown,
16    /// The device is connected to the MQTT broker but is not yet ready to operate.
17    Init,
18    /// The device is connected and operational.
19    Ready,
20    /// The device has cleanly disconnected from the MQTT broker.
21    Disconnected,
22    /// The device is currently sleeping.
23    Sleeping,
24    /// The device was uncleanly disconnected from the MQTT broker. This could happen due to a
25    /// network issue, power failure or some other unexpected failure.
26    Lost,
27    /// The device is connected to the MQTT broker but something is wrong and it may require human
28    /// intervention.
29    Alert,
30}
31
32impl State {
33    fn as_str(&self) -> &'static str {
34        match self {
35            Self::Unknown => "unknown",
36            Self::Init => "init",
37            Self::Ready => "ready",
38            Self::Disconnected => "disconnected",
39            Self::Sleeping => "sleeping",
40            Self::Lost => "lost",
41            Self::Alert => "alert",
42        }
43    }
44}
45
46/// An error which can be returned when parsing a `State` from a string, if the string does not
47/// match a valid Homie
48/// [device lifecycle](https://homieiot.github.io/specification/#device-lifecycle) state.
49#[derive(Clone, Debug, Error, Eq, PartialEq)]
50#[error("Invalid state '{0}'")]
51pub struct ParseStateError(String);
52
53impl FromStr for State {
54    type Err = ParseStateError;
55
56    fn from_str(s: &str) -> Result<Self, Self::Err> {
57        match s {
58            "init" => Ok(Self::Init),
59            "ready" => Ok(Self::Ready),
60            "disconnected" => Ok(Self::Disconnected),
61            "sleeping" => Ok(Self::Sleeping),
62            "lost" => Ok(Self::Lost),
63            "alert" => Ok(Self::Alert),
64            _ => Err(ParseStateError(s.to_owned())),
65        }
66    }
67}
68
69impl Display for State {
70    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
71        f.write_str(self.as_str())
72    }
73}
74
75/// The data type of a Homie property.
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77pub enum Datatype {
78    /// A [64-bit signed integer](https://homieiot.github.io/specification/#integer).
79    Integer,
80    /// A [64-bit floating-point number](https://homieiot.github.io/specification/#float).
81    Float,
82    /// A [boolean value](https://homieiot.github.io/specification/#boolean).
83    Boolean,
84    /// A [UTF-8 encoded string](https://homieiot.github.io/specification/#string).
85    String,
86    /// An [enum value](https://homieiot.github.io/specification/#enum) from a set of possible
87    /// values specified by the property format.
88    Enum,
89    /// An [RGB](enum.ColorFormat.html#variant.Rgb) or [HSV](enum.ColorFormat.html#variant.Hsv)
90    /// [color](https://homieiot.github.io/specification/#color), depending on the property
91    /// [format](struct.Property.html#method.color_format).
92    Color,
93}
94
95impl Datatype {
96    fn as_str(&self) -> &'static str {
97        match self {
98            Self::Integer => "integer",
99            Self::Float => "float",
100            Self::Boolean => "boolean",
101            Self::String => "string",
102            Self::Enum => "enum",
103            Self::Color => "color",
104        }
105    }
106}
107
108impl Display for Datatype {
109    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
110        f.write_str(self.as_str())
111    }
112}
113
114/// An error which can be returned when parsing a `Datatype` from a string, if the string does not
115/// match a valid Homie `$datatype` attribute.
116#[derive(Clone, Debug, Error, Eq, PartialEq)]
117#[error("Invalid datatype '{0}'")]
118pub struct ParseDatatypeError(String);
119
120impl FromStr for Datatype {
121    type Err = ParseDatatypeError;
122
123    fn from_str(s: &str) -> Result<Self, Self::Err> {
124        match s {
125            "integer" => Ok(Self::Integer),
126            "float" => Ok(Self::Float),
127            "boolean" => Ok(Self::Boolean),
128            "string" => Ok(Self::String),
129            "enum" => Ok(Self::Enum),
130            "color" => Ok(Self::Color),
131            _ => Err(ParseDatatypeError(s.to_owned())),
132        }
133    }
134}
135
136/// A [property](https://homieiot.github.io/specification/#properties) of a Homie node.
137///
138/// The `id`, `name` and `datatype` are required, but might not be available immediately when the
139/// property is first discovered. The other attributes are optional.
140#[derive(Clone, Debug, Eq, PartialEq)]
141pub struct Property {
142    /// The subtopic ID of the property. This is unique per node, and should follow the Homie
143    /// [ID format](https://homieiot.github.io/specification/#topic-ids).
144    pub id: String,
145
146    /// The human-readable name of the property. This is a required attribute, but might not be
147    /// available as soon as the property is first discovered.
148    pub name: Option<String>,
149
150    /// The data type of the property. This is a required attribute, but might not be available as
151    /// soon as the property is first discovered.
152    pub datatype: Option<Datatype>,
153
154    /// Whether the property can be set by the Homie controller. This should be true for properties
155    /// like the brightness or power state of a light, and false for things like the temperature
156    /// reading of a sensor. It is false by default.
157    pub settable: bool,
158
159    /// Whether the property value is retained by the MQTT broker. This is true by default.
160    pub retained: bool,
161
162    /// The unit of the property, if any. This may be one of the
163    /// [recommended units](https://homieiot.github.io/specification/#property-attributes), or any
164    /// other custom unit.
165    pub unit: Option<String>,
166
167    /// The format of the property, if any. This should be specified if the datatype is `Enum` or
168    /// `Color`, and may be specified if the datatype is `Integer` or `Float`.
169    ///
170    /// This field holds the raw string received from the device. Use
171    /// [color_format](#method.color_format), [enum_values](#method.enum_values) or
172    /// [range](#method.range) to parse it according to the datatype of the property.
173    pub format: Option<String>,
174
175    /// The current value of the property, if known. This may change frequently.
176    ///
177    /// This field holds the raw string received from the device. Use [value](#method.value) to
178    /// parse it according to the datatype of the property.
179    pub value: Option<String>,
180}
181
182impl Property {
183    /// Create a new property with the given ID.
184    ///
185    /// # Arguments
186    /// * `id`: The subtopic ID for the property. This must be unique per device, and follow the
187    ///   Homie [ID format](https://homieiot.github.io/specification/#topic-ids).
188    pub(crate) fn new(id: &str) -> Property {
189        Property {
190            id: id.to_owned(),
191            name: None,
192            datatype: None,
193            settable: false,
194            retained: true,
195            unit: None,
196            format: None,
197            value: None,
198        }
199    }
200
201    /// Returns whether all the required
202    /// [attributes](https://homieiot.github.io/specification/#property-attributes) of the property
203    /// are filled in.
204    pub fn has_required_attributes(&self) -> bool {
205        self.name.is_some() && self.datatype.is_some()
206    }
207
208    /// The value of the property, parsed as the appropriate Homie `Value` type. This will return
209    /// `WrongDatatype` if you try to parse it as a type which doesn't match the datatype declared
210    /// by the property.
211    pub fn value<T: Value>(&self) -> Result<T, ValueError> {
212        T::valid_for(self.datatype, &self.format)?;
213
214        match self.value {
215            None => Err(ValueError::Unknown),
216            Some(ref value) => value.parse().map_err(|_| ValueError::ParseFailed {
217                value: value.to_owned(),
218                datatype: T::datatype(),
219            }),
220        }
221    }
222
223    /// If the datatype of the property is `Color`, returns the color format.
224    pub fn color_format(&self) -> Result<ColorFormat, ValueError> {
225        // If the datatype is known and it isn't color, that's an error. If it's not known, maybe
226        // parsing the format will succeed, so try anyway.
227        if let Some(actual) = self.datatype {
228            if actual != Datatype::Color {
229                return Err(ValueError::WrongDatatype {
230                    expected: Datatype::Color,
231                    actual,
232                });
233            }
234        }
235
236        match self.format {
237            None => Err(ValueError::Unknown),
238            Some(ref format) => format.parse(),
239        }
240    }
241
242    /// If the datatype of the property is `Enum`, gets the possible values of the enum.
243    pub fn enum_values(&self) -> Result<Vec<&str>, ValueError> {
244        EnumValue::valid_for(self.datatype, &self.format)?;
245
246        match self.format {
247            None => Err(ValueError::Unknown),
248            Some(ref format) => {
249                if format.is_empty() {
250                    Err(ValueError::WrongFormat {
251                        format: "".to_owned(),
252                    })
253                } else {
254                    Ok(format.split(',').collect())
255                }
256            }
257        }
258    }
259
260    /// If the dataype of the property is `Integer` or `Float`, gets the allowed range of values (if
261    /// any is declared by the device).
262    pub fn range<T: Value + Copy>(&self) -> Result<RangeInclusive<T>, ValueError> {
263        T::valid_for(self.datatype, &self.format)?;
264
265        match self.format {
266            None => Err(ValueError::Unknown),
267            Some(ref format) => {
268                if let [Ok(start), Ok(end)] = format
269                    .splitn(2, ':')
270                    .map(|part| part.parse())
271                    .collect::<Vec<_>>()
272                    .as_slice()
273                {
274                    Ok(RangeInclusive::new(*start, *end))
275                } else {
276                    Err(ValueError::WrongFormat {
277                        format: format.to_owned(),
278                    })
279                }
280            }
281        }
282    }
283}
284
285/// A [node](https://homieiot.github.io/specification/#nodes) of a Homie device.
286///
287/// All attributes are required, but might not be available immediately when the node is first
288/// discovered.
289#[derive(Clone, Debug, Eq, PartialEq)]
290pub struct Node {
291    /// The subtopic ID of the node. This is unique per device, and should follow the Homie
292    /// [ID format](https://homieiot.github.io/specification/#topic-ids).
293    pub id: String,
294
295    /// The human-readable name of the node. This is a required attribute, but might not be
296    /// available as soon as the node is first discovered.
297    pub name: Option<String>,
298
299    /// The type of the node. This is an arbitrary string. It is a required attribute, but might not
300    /// be available as soon as the node is first discovered.
301    pub node_type: Option<String>,
302
303    /// The properties of the node, keyed by their IDs. There should be at least one.
304    pub properties: HashMap<String, Property>,
305}
306
307impl Node {
308    /// Create a new node with the given ID.
309    ///
310    /// # Arguments
311    /// * `id`: The subtopic ID for the node. This must be unique per device, and follow the Homie
312    ///   [ID format](https://homieiot.github.io/specification/#topic-ids).
313    pub(crate) fn new(id: &str) -> Node {
314        Node {
315            id: id.to_owned(),
316            name: None,
317            node_type: None,
318            properties: HashMap::new(),
319        }
320    }
321
322    /// Add the given property to the node's set of properties.
323    pub(crate) fn add_property(&mut self, property: Property) {
324        self.properties.insert(property.id.clone(), property);
325    }
326
327    /// Returns whether all the required
328    /// [attributes](https://homieiot.github.io/specification/#node-attributes) of the node and its
329    /// properties are filled in.
330    pub fn has_required_attributes(&self) -> bool {
331        self.name.is_some()
332            && self.node_type.is_some()
333            && !self.properties.is_empty()
334            && self
335                .properties
336                .values()
337                .all(|property| property.has_required_attributes())
338    }
339}
340
341/// A Homie [extension](https://homieiot.github.io/extensions/) supported by a device.
342#[derive(Clone, Debug, Eq, PartialEq)]
343pub struct Extension {
344    /// The identifier of the extension. This should be a reverse domain name followed by some
345    /// suffix.
346    pub id: String,
347    /// The version of the extension.
348    pub version: String,
349    /// The versions of the Homie spec which the extension supports.
350    pub homie_versions: Vec<String>,
351}
352
353/// An error which can be returned when parsing an `Extension` from a string.
354#[derive(Clone, Debug, Error, Eq, PartialEq)]
355#[error("Invalid extension '{0}'")]
356pub struct ParseExtensionError(String);
357
358impl FromStr for Extension {
359    type Err = ParseExtensionError;
360
361    fn from_str(s: &str) -> Result<Self, Self::Err> {
362        let parts: Vec<_> = s.split(':').collect();
363        if let [id, version, homie_versions] = parts.as_slice() {
364            if let Some(homie_versions) = homie_versions.strip_prefix('[') {
365                if let Some(homie_versions) = homie_versions.strip_suffix(']') {
366                    return Ok(Extension {
367                        id: (*id).to_owned(),
368                        version: (*version).to_owned(),
369                        homie_versions: homie_versions.split(';').map(|p| p.to_owned()).collect(),
370                    });
371                }
372            }
373        }
374        Err(ParseExtensionError(s.to_owned()))
375    }
376}
377
378/// A Homie [device](https://homieiot.github.io/specification/#devices) which has been discovered.
379///
380/// The `id`, `homie_version`, `name` and `state` are required, but might not be available
381/// immediately when the device is first discovered. The `implementation` is optional.
382#[derive(Clone, Debug, PartialEq)]
383pub struct Device {
384    /// The subtopic ID of the device. This is unique per Homie base topic, and should follow the
385    /// Homie [ID format](https://homieiot.github.io/specification/#topic-ids).
386    pub id: String,
387
388    /// The version of the Homie convention which the device implements.
389    pub homie_version: String,
390
391    /// The human-readable name of the device. This is a required attribute, but might not be
392    /// available as soon as the device is first discovered.
393    pub name: Option<String>,
394
395    /// The current state of the device according to the Homie
396    /// [device lifecycle](https://homieiot.github.io/specification/#device-lifecycle).
397    pub state: State,
398
399    /// An identifier for the Homie implementation which the device uses.
400    pub implementation: Option<String>,
401
402    /// The nodes of the device, keyed by their IDs.
403    pub nodes: HashMap<String, Node>,
404
405    /// The Homie extensions implemented by the device.
406    pub extensions: Vec<Extension>,
407
408    /// The IP address of the device on the local network.
409    pub local_ip: Option<String>,
410
411    /// The MAC address of the device's network interface.
412    pub mac: Option<String>,
413
414    /// The name of the firmware running on the device.
415    pub firmware_name: Option<String>,
416
417    /// The version of the firware running on the device.
418    pub firmware_version: Option<String>,
419
420    /// The interval at which the device refreshes its stats.
421    pub stats_interval: Option<Duration>,
422
423    /// The amount of time since the device booted.
424    pub stats_uptime: Option<Duration>,
425
426    /// The device's signal strength in %.
427    pub stats_signal: Option<i64>,
428
429    /// The device's CPU temperature in °C.
430    pub stats_cputemp: Option<f64>,
431
432    /// The device's CPU load in %, averaged across all CPUs over the last `stats_interval`.
433    pub stats_cpuload: Option<i64>,
434
435    /// The device's battery level in %.
436    pub stats_battery: Option<i64>,
437
438    /// The device's free heap space in bytes.
439    pub stats_freeheap: Option<u64>,
440
441    /// The device's power supply voltage in volts.
442    pub stats_supply: Option<f64>,
443}
444
445impl Device {
446    /// Create a new device with the given ID.
447    ///
448    /// # Arguments
449    /// * `id`: The subtopic ID for the device. This must be unique per Homie base topic, and follow
450    ///   the Homie [ID format](https://homieiot.github.io/specification/#topic-ids).
451    /// * `homie_version`: The version of the Homie convention which the device implements.
452    pub(crate) fn new(id: &str, homie_version: &str) -> Device {
453        Device {
454            id: id.to_owned(),
455            homie_version: homie_version.to_owned(),
456            name: None,
457            state: State::Unknown,
458            implementation: None,
459            nodes: HashMap::new(),
460            extensions: Vec::default(),
461            local_ip: None,
462            mac: None,
463            firmware_name: None,
464            firmware_version: None,
465            stats_interval: None,
466            stats_uptime: None,
467            stats_signal: None,
468            stats_cputemp: None,
469            stats_cpuload: None,
470            stats_battery: None,
471            stats_freeheap: None,
472            stats_supply: None,
473        }
474    }
475
476    /// Add the given node to the devices's set of nodes.
477    pub(crate) fn add_node(&mut self, node: Node) {
478        self.nodes.insert(node.id.clone(), node);
479    }
480
481    /// Returns whether all the required
482    /// [attributes](https://homieiot.github.io/specification/#device-attributes) of the device and
483    /// all its nodes and properties are filled in.
484    pub fn has_required_attributes(&self) -> bool {
485        self.name.is_some()
486            && self.state != State::Unknown
487            && self
488                .nodes
489                .values()
490                .all(|node| node.has_required_attributes())
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use crate::values::{ColorHsv, ColorRgb, EnumValue};
498
499    #[test]
500    fn extension_parse_succeeds() {
501        let legacy_stats: Extension = "org.homie.legacy-stats:0.1.1:[4.x]".parse().unwrap();
502        assert_eq!(legacy_stats.id, "org.homie.legacy-stats");
503        assert_eq!(legacy_stats.version, "0.1.1");
504        assert_eq!(legacy_stats.homie_versions, &["4.x"]);
505
506        let meta: Extension = "eu.epnw.meta:1.1.0:[3.0.1;4.x]".parse().unwrap();
507        assert_eq!(meta.id, "eu.epnw.meta");
508        assert_eq!(meta.version, "1.1.0");
509        assert_eq!(meta.homie_versions, &["3.0.1", "4.x"]);
510
511        let minimal: Extension = "a:0:[]".parse().unwrap();
512        assert_eq!(minimal.id, "a");
513        assert_eq!(minimal.version, "0");
514        assert_eq!(minimal.homie_versions, &[""]);
515    }
516
517    #[test]
518    fn extension_parse_fails() {
519        assert_eq!(
520            "".parse::<Extension>(),
521            Err(ParseExtensionError("".to_owned()))
522        );
523        assert_eq!(
524            "test.blah:1.2.3".parse::<Extension>(),
525            Err(ParseExtensionError("test.blah:1.2.3".to_owned()))
526        );
527        assert_eq!(
528            "test.blah:1.2.3:4.x".parse::<Extension>(),
529            Err(ParseExtensionError("test.blah:1.2.3:4.x".to_owned()))
530        );
531    }
532
533    #[test]
534    fn property_integer_parse() {
535        let mut property = Property::new("property_id");
536
537        // With no known value, parsing fails.
538        assert_eq!(property.value::<i64>(), Err(ValueError::Unknown));
539
540        // With an invalid value, parsing also fails.
541        property.value = Some("-".to_owned());
542        assert_eq!(
543            property.value::<i64>(),
544            Err(ValueError::ParseFailed {
545                value: "-".to_owned(),
546                datatype: Datatype::Integer,
547            })
548        );
549
550        // With a valid value but unknown datatype, parsing succeeds.
551        property.value = Some("42".to_owned());
552        assert_eq!(property.value(), Ok(42));
553
554        // With the correct datatype, parsing still succeeds.
555        property.datatype = Some(Datatype::Integer);
556        assert_eq!(property.value(), Ok(42));
557
558        // Negative values can be parsed.
559        property.value = Some("-66".to_owned());
560        assert_eq!(property.value(), Ok(-66));
561
562        // With the wrong datatype, parsing fails.
563        property.datatype = Some(Datatype::Float);
564        assert_eq!(
565            property.value::<i64>(),
566            Err(ValueError::WrongDatatype {
567                actual: Datatype::Float,
568                expected: Datatype::Integer,
569            })
570        );
571    }
572
573    #[test]
574    fn property_float_parse() {
575        let mut property = Property::new("property_id");
576
577        // With no known value, parsing fails.
578        assert_eq!(property.value::<f64>(), Err(ValueError::Unknown));
579
580        // With an invalid value, parsing also fails.
581        property.value = Some("-".to_owned());
582        assert_eq!(
583            property.value::<f64>(),
584            Err(ValueError::ParseFailed {
585                value: "-".to_owned(),
586                datatype: Datatype::Float,
587            })
588        );
589
590        // With a valid value but unknown datatype, parsing succeeds.
591        property.value = Some("42.36".to_owned());
592        assert_eq!(property.value(), Ok(42.36));
593
594        // With the correct datatype, parsing still succeeds.
595        property.datatype = Some(Datatype::Float);
596        assert_eq!(property.value(), Ok(42.36));
597
598        // With the wrong datatype, parsing fails.
599        property.datatype = Some(Datatype::Integer);
600        assert_eq!(
601            property.value::<f64>(),
602            Err(ValueError::WrongDatatype {
603                actual: Datatype::Integer,
604                expected: Datatype::Float,
605            })
606        );
607    }
608
609    #[test]
610    fn property_color_parse() {
611        let mut property = Property::new("property_id");
612
613        // With no known value, parsing fails.
614        assert_eq!(property.value::<ColorRgb>(), Err(ValueError::Unknown));
615        assert_eq!(property.value::<ColorHsv>(), Err(ValueError::Unknown));
616
617        // With an invalid value, parsing also fails.
618        property.value = Some("".to_owned());
619        assert_eq!(
620            property.value::<ColorRgb>(),
621            Err(ValueError::ParseFailed {
622                value: "".to_owned(),
623                datatype: Datatype::Color,
624            })
625        );
626
627        // With a valid value but unknown datatype, parsing succeeds as either kind of colour.
628        property.value = Some("12,34,56".to_owned());
629        assert_eq!(
630            property.value(),
631            Ok(ColorRgb {
632                r: 12,
633                g: 34,
634                b: 56
635            })
636        );
637        assert_eq!(
638            property.value(),
639            Ok(ColorHsv {
640                h: 12,
641                s: 34,
642                v: 56
643            })
644        );
645
646        // With the correct datatype and no format, parsing succeeds as either kind of colour.
647        property.datatype = Some(Datatype::Color);
648        assert_eq!(
649            property.value(),
650            Ok(ColorRgb {
651                r: 12,
652                g: 34,
653                b: 56
654            })
655        );
656        assert_eq!(
657            property.value(),
658            Ok(ColorHsv {
659                h: 12,
660                s: 34,
661                v: 56
662            })
663        );
664
665        // With a format set, parsing succeeds only as the correct kind of colour.
666        property.format = Some("rgb".to_owned());
667        assert_eq!(
668            property.value(),
669            Ok(ColorRgb {
670                r: 12,
671                g: 34,
672                b: 56
673            })
674        );
675        assert_eq!(
676            property.value::<ColorHsv>(),
677            Err(ValueError::WrongFormat {
678                format: "rgb".to_owned()
679            })
680        );
681
682        // With the wrong datatype, parsing fails.
683        property.datatype = Some(Datatype::Integer);
684        assert_eq!(
685            property.value::<ColorRgb>(),
686            Err(ValueError::WrongDatatype {
687                actual: Datatype::Integer,
688                expected: Datatype::Color,
689            })
690        );
691        assert_eq!(
692            property.value::<ColorHsv>(),
693            Err(ValueError::WrongDatatype {
694                actual: Datatype::Integer,
695                expected: Datatype::Color,
696            })
697        );
698    }
699
700    #[test]
701    fn property_enum_parse() {
702        let mut property = Property::new("property_id");
703
704        // With no known value, parsing fails.
705        assert_eq!(property.value::<EnumValue>(), Err(ValueError::Unknown));
706
707        // With an invalid value, parsing also fails.
708        property.value = Some("".to_owned());
709        assert_eq!(
710            property.value::<EnumValue>(),
711            Err(ValueError::ParseFailed {
712                value: "".to_owned(),
713                datatype: Datatype::Enum,
714            })
715        );
716
717        // With a valid value but unknown datatype, parsing succeeds.
718        property.value = Some("anything".to_owned());
719        assert_eq!(property.value(), Ok(EnumValue::new("anything")));
720
721        // With the correct datatype, parsing still succeeds.
722        property.datatype = Some(Datatype::Enum);
723        assert_eq!(property.value(), Ok(EnumValue::new("anything")));
724
725        // With the wrong datatype, parsing fails.
726        property.datatype = Some(Datatype::String);
727        assert_eq!(
728            property.value::<EnumValue>(),
729            Err(ValueError::WrongDatatype {
730                actual: Datatype::String,
731                expected: Datatype::Enum,
732            })
733        );
734    }
735
736    #[test]
737    fn property_color_format() {
738        let mut property = Property::new("property_id");
739
740        // With no known format or datatype, format parsing fails.
741        assert_eq!(property.color_format(), Err(ValueError::Unknown));
742
743        // Parsing an invalid format fails.
744        property.format = Some("".to_owned());
745        assert_eq!(
746            property.color_format(),
747            Err(ValueError::WrongFormat {
748                format: "".to_owned()
749            })
750        );
751
752        // Parsing valid formats works even if datatype is unnkown.
753        property.format = Some("rgb".to_owned());
754        assert_eq!(property.color_format(), Ok(ColorFormat::Rgb));
755        property.format = Some("hsv".to_owned());
756        assert_eq!(property.color_format(), Ok(ColorFormat::Hsv));
757
758        // With the wrong datatype, parsing fails.
759        property.datatype = Some(Datatype::Integer);
760        assert_eq!(
761            property.color_format(),
762            Err(ValueError::WrongDatatype {
763                actual: Datatype::Integer,
764                expected: Datatype::Color
765            })
766        );
767
768        // With the correct datatype, parsing works.
769        property.datatype = Some(Datatype::Color);
770        assert_eq!(property.color_format(), Ok(ColorFormat::Hsv));
771    }
772
773    #[test]
774    fn property_enum_format() {
775        let mut property = Property::new("property_id");
776
777        // With no known format or datatype, format parsing fails.
778        assert_eq!(property.enum_values(), Err(ValueError::Unknown));
779
780        // An empty format string is invalid.
781        property.format = Some("".to_owned());
782        assert_eq!(
783            property.enum_values(),
784            Err(ValueError::WrongFormat {
785                format: "".to_owned()
786            })
787        );
788
789        // A single value is valid.
790        property.format = Some("one".to_owned());
791        assert_eq!(property.enum_values(), Ok(vec!["one"]));
792
793        // Several values are parsed correctly.
794        property.format = Some("one,two,three".to_owned());
795        assert_eq!(property.enum_values(), Ok(vec!["one", "two", "three"]));
796
797        // With the correct datatype, parsing works.
798        property.datatype = Some(Datatype::Enum);
799        assert_eq!(property.enum_values(), Ok(vec!["one", "two", "three"]));
800
801        // With the wrong datatype, parsing fails.
802        property.datatype = Some(Datatype::Color);
803        assert_eq!(
804            property.enum_values(),
805            Err(ValueError::WrongDatatype {
806                actual: Datatype::Color,
807                expected: Datatype::Enum
808            })
809        );
810    }
811
812    #[test]
813    fn property_numeric_format() {
814        let mut property = Property::new("property_id");
815
816        // With no known format or datatype, format parsing fails.
817        assert_eq!(property.range::<i64>(), Err(ValueError::Unknown));
818        assert_eq!(property.range::<f64>(), Err(ValueError::Unknown));
819
820        // An empty format string is invalid.
821        property.format = Some("".to_owned());
822        assert_eq!(
823            property.range::<i64>(),
824            Err(ValueError::WrongFormat {
825                format: "".to_owned()
826            })
827        );
828        assert_eq!(
829            property.range::<f64>(),
830            Err(ValueError::WrongFormat {
831                format: "".to_owned()
832            })
833        );
834
835        // A valid range is parsed correctly.
836        property.format = Some("1:10".to_owned());
837        assert_eq!(property.range(), Ok(1..=10));
838        assert_eq!(property.range(), Ok(1.0..=10.0));
839
840        // A range with a decimal point must be a float.
841        property.format = Some("3.6:4.2".to_owned());
842        assert_eq!(property.range(), Ok(3.6..=4.2));
843        assert_eq!(
844            property.range::<i64>(),
845            Err(ValueError::WrongFormat {
846                format: "3.6:4.2".to_owned()
847            })
848        );
849
850        // With the correct datatype, parsing works.
851        property.datatype = Some(Datatype::Integer);
852        property.format = Some("1:10".to_owned());
853        assert_eq!(property.range(), Ok(1..=10));
854
855        // For the wrong datatype, parsing fails.
856        assert_eq!(
857            property.range::<f64>(),
858            Err(ValueError::WrongDatatype {
859                actual: Datatype::Integer,
860                expected: Datatype::Float
861            })
862        );
863    }
864
865    #[test]
866    fn property_has_required_attributes() {
867        let mut property = Property::new("property_id");
868        assert!(!property.has_required_attributes());
869
870        property.name = Some("Property name".to_owned());
871        assert!(!property.has_required_attributes());
872
873        property.datatype = Some(Datatype::Integer);
874        assert!(property.has_required_attributes());
875    }
876
877    /// Construct a minimal `Property` with all the required attributes.
878    fn property_with_required_attributes() -> Property {
879        let mut property = Property::new("property_id");
880        property.name = Some("Property name".to_owned());
881        property.datatype = Some(Datatype::Integer);
882        property
883    }
884
885    #[test]
886    fn node_has_required_attributes() {
887        let mut node = Node::new("node_id");
888        assert!(!node.has_required_attributes());
889
890        node.name = Some("Node name".to_owned());
891        assert!(!node.has_required_attributes());
892
893        node.node_type = Some("Node type".to_owned());
894        assert!(!node.has_required_attributes());
895
896        node.add_property(property_with_required_attributes());
897        assert!(node.has_required_attributes());
898
899        node.add_property(Property::new("property_without_required_attributes"));
900        assert!(!node.has_required_attributes());
901    }
902
903    /// Construct a minimal `Node` with all the required attributes.
904    fn node_with_required_attributes() -> Node {
905        let mut node = Node::new("node_id");
906        node.name = Some("Node name".to_owned());
907        node.node_type = Some("Node type".to_owned());
908        node.add_property(property_with_required_attributes());
909        node
910    }
911
912    #[test]
913    fn device_has_required_attributes() {
914        let mut device = Device::new("device_id", "123");
915        assert!(!device.has_required_attributes());
916
917        device.name = Some("Device name".to_owned());
918        assert!(!device.has_required_attributes());
919
920        device.state = State::Init;
921        assert!(device.has_required_attributes());
922
923        device.add_node(node_with_required_attributes());
924        assert!(device.has_required_attributes());
925
926        device.add_node(Node::new("node_without_required_attributes"));
927        assert!(!device.has_required_attributes());
928    }
929}