homie_controller/
values.rs

1use crate::types::Datatype;
2use std::fmt::{self, Debug, Display, Formatter};
3use std::num::ParseIntError;
4use std::str::FromStr;
5use thiserror::Error;
6
7/// An error encountered while parsing the value or format of a property.
8#[derive(Clone, Debug, Error, Eq, PartialEq)]
9pub enum ValueError {
10    /// The value of the property or attribute is not yet known, or not set by the device.
11    #[error("Value not yet known.")]
12    Unknown,
13    /// The method call expected the property to have a particular datatype, but the datatype sent
14    /// by the device was something different.
15    #[error("Expected value of type {expected} but was {actual}.")]
16    WrongDatatype {
17        /// The datatype expected by the method call.
18        expected: Datatype,
19        /// The actual datatype of the property, as sent by the device.
20        actual: Datatype,
21    },
22    /// The format of the property couldn't be parsed or didn't match what was expected by the
23    /// method call.
24    #[error("Invalid or unexpected format {format}.")]
25    WrongFormat {
26        /// The format string of the property.
27        format: String,
28    },
29    /// The value of the property couldn't be parsed as the expected type.
30    #[error("Parsing {value} as datatype {datatype} failed.")]
31    ParseFailed {
32        /// The string value of the property.
33        value: String,
34        /// The datatype as which the value was attempted to be parsed.
35        datatype: Datatype,
36    },
37}
38
39/// The value of a Homie property. This has implementations corresponding to the possible property datatypes.
40pub trait Value: ToString + FromStr {
41    /// The Homie datatype corresponding to this type.
42    fn datatype() -> Datatype;
43
44    /// Check whether this value type is valid for the given property datatype and format string.
45    ///
46    /// Returns `Ok(())` if so, or `Err(WrongFormat(...))` or `Err(WrongDatatype(...))` if not.
47    ///
48    /// The default implementation checks the datatype, and delegates to `valid_for_format` to check
49    /// the format.
50    fn valid_for(datatype: Option<Datatype>, format: &Option<String>) -> Result<(), ValueError> {
51        // If the datatype is known and it doesn't match what is being asked for, that's an error.
52        // If it's not known, maybe parsing will succeed.
53        if let Some(actual) = datatype {
54            let expected = Self::datatype();
55            if actual != expected {
56                return Err(ValueError::WrongDatatype { expected, actual });
57            }
58        }
59
60        if let Some(format) = format {
61            Self::valid_for_format(format)
62        } else {
63            Ok(())
64        }
65    }
66
67    /// Check whether this value type is valid for the given property format string.
68    ///
69    /// Returns `Ok(())` if so, or `Err(WrongFormat(...))` if not.
70    fn valid_for_format(_format: &str) -> Result<(), ValueError> {
71        Ok(())
72    }
73}
74
75impl Value for i64 {
76    fn datatype() -> Datatype {
77        Datatype::Integer
78    }
79}
80
81impl Value for f64 {
82    fn datatype() -> Datatype {
83        Datatype::Float
84    }
85}
86
87impl Value for bool {
88    fn datatype() -> Datatype {
89        Datatype::Boolean
90    }
91}
92
93// TODO: What about &str?
94impl Value for String {
95    fn datatype() -> Datatype {
96        Datatype::String
97    }
98}
99
100/// The format of a [colour](https://homieiot.github.io/specification/#color) property, either RGB
101/// or HSV.
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub enum ColorFormat {
104    /// The colour is in red-green-blue format.
105    Rgb,
106    /// The colour is in hue-saturation-value format.
107    Hsv,
108}
109
110impl ColorFormat {
111    fn as_str(&self) -> &'static str {
112        match self {
113            Self::Rgb => "rgb",
114            Self::Hsv => "hsv",
115        }
116    }
117}
118
119impl FromStr for ColorFormat {
120    type Err = ValueError;
121
122    fn from_str(s: &str) -> Result<Self, Self::Err> {
123        match s {
124            "rgb" => Ok(Self::Rgb),
125            "hsv" => Ok(Self::Hsv),
126            _ => Err(ValueError::WrongFormat {
127                format: s.to_owned(),
128            }),
129        }
130    }
131}
132
133impl Display for ColorFormat {
134    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
135        f.write_str(self.as_str())
136    }
137}
138
139pub trait Color: Value {
140    fn format() -> ColorFormat;
141}
142
143impl<T: Color> Value for T {
144    fn datatype() -> Datatype {
145        Datatype::Color
146    }
147
148    fn valid_for_format(format: &str) -> Result<(), ValueError> {
149        if format == Self::format().as_str() {
150            Ok(())
151        } else {
152            Err(ValueError::WrongFormat {
153                format: format.to_owned(),
154            })
155        }
156    }
157}
158
159/// An error while attempting to parse a `Color` from a string.
160#[derive(Clone, Debug, Error, Eq, PartialEq)]
161#[error("Failed to parse color.")]
162pub struct ParseColorError();
163
164impl From<ParseIntError> for ParseColorError {
165    fn from(_: ParseIntError) -> Self {
166        ParseColorError()
167    }
168}
169
170/// A [colour](https://homieiot.github.io/specification/#color) in red-green-blue format.
171#[derive(Clone, Debug, Eq, PartialEq)]
172pub struct ColorRgb {
173    /// The red channel of the colour, between 0 and 255.
174    pub r: u8,
175    /// The green channel of the colour, between 0 and 255.
176    pub g: u8,
177    /// The blue channel of the colour, between 0 and 255.
178    pub b: u8,
179}
180
181impl ColorRgb {
182    /// Construct a new RGB colour.
183    pub fn new(r: u8, g: u8, b: u8) -> Self {
184        ColorRgb { r, g, b }
185    }
186}
187
188impl Display for ColorRgb {
189    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
190        write!(f, "{},{},{}", self.r, self.g, self.b)
191    }
192}
193
194impl FromStr for ColorRgb {
195    type Err = ParseColorError;
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        let parts: Vec<_> = s.split(',').collect();
199        if let [r, g, b] = parts.as_slice() {
200            Ok(ColorRgb {
201                r: r.parse()?,
202                g: g.parse()?,
203                b: b.parse()?,
204            })
205        } else {
206            Err(ParseColorError())
207        }
208    }
209}
210
211impl Color for ColorRgb {
212    fn format() -> ColorFormat {
213        ColorFormat::Rgb
214    }
215}
216
217/// A [colour](https://homieiot.github.io/specification/#color) in hue-saturation-value format.
218#[derive(Clone, Debug, Eq, PartialEq)]
219pub struct ColorHsv {
220    /// The hue of the colour, between 0 and 360.
221    pub h: u16,
222    /// The saturation of the colour, between 0 and 100.
223    pub s: u8,
224    /// The value of the colour, between 0 and 100.
225    pub v: u8,
226}
227
228impl ColorHsv {
229    /// Construct a new HSV colour, or panic if the values given are out of range.
230    pub fn new(h: u16, s: u8, v: u8) -> Self {
231        assert!(h <= 360);
232        assert!(s <= 100);
233        assert!(v <= 100);
234        ColorHsv { h, s, v }
235    }
236}
237
238impl Display for ColorHsv {
239    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
240        write!(f, "{},{},{}", self.h, self.s, self.v)
241    }
242}
243
244impl FromStr for ColorHsv {
245    type Err = ParseColorError;
246
247    fn from_str(s: &str) -> Result<Self, Self::Err> {
248        let parts: Vec<_> = s.split(',').collect();
249        if let [h, s, v] = parts.as_slice() {
250            let h = h.parse()?;
251            let s = s.parse()?;
252            let v = v.parse()?;
253            if h <= 360 && s <= 100 && v <= 100 {
254                return Ok(ColorHsv { h, s, v });
255            }
256        }
257        Err(ParseColorError())
258    }
259}
260
261impl Color for ColorHsv {
262    fn format() -> ColorFormat {
263        ColorFormat::Hsv
264    }
265}
266
267/// The value of a Homie [enum](https://homieiot.github.io/specification/#enum) property.
268///
269/// This must be a non-empty string.
270#[derive(Clone, Debug, Eq, PartialEq, Hash)]
271pub struct EnumValue(String);
272
273impl EnumValue {
274    pub fn new(s: &str) -> Self {
275        assert!(!s.is_empty());
276        EnumValue(s.to_owned())
277    }
278}
279
280/// An error while attempting to parse an `EnumValue` from a string, because the string is empty.
281#[derive(Clone, Debug, Error, Eq, PartialEq)]
282#[error("Empty string is not a valid enum value.")]
283pub struct ParseEnumError();
284
285impl FromStr for EnumValue {
286    type Err = ParseEnumError;
287
288    fn from_str(s: &str) -> Result<Self, Self::Err> {
289        if s.is_empty() {
290            Err(ParseEnumError())
291        } else {
292            Ok(EnumValue::new(s))
293        }
294    }
295}
296
297impl Display for EnumValue {
298    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
299        f.write_str(&self.0)
300    }
301}
302
303impl Value for EnumValue {
304    fn datatype() -> Datatype {
305        Datatype::Enum
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn color_rgb_to_from_string() {
315        let color = ColorRgb::new(111, 222, 42);
316        assert_eq!(color.to_string().parse(), Ok(color));
317    }
318
319    #[test]
320    fn color_hsv_to_from_string() {
321        let color = ColorHsv::new(231, 88, 77);
322        assert_eq!(color.to_string().parse(), Ok(color));
323    }
324
325    #[test]
326    fn color_rgb_parse_invalid() {
327        assert_eq!("".parse::<ColorRgb>(), Err(ParseColorError()));
328        assert_eq!("1,2".parse::<ColorRgb>(), Err(ParseColorError()));
329        assert_eq!("1,2,3,4".parse::<ColorRgb>(), Err(ParseColorError()));
330        assert_eq!("1,2,256".parse::<ColorRgb>(), Err(ParseColorError()));
331        assert_eq!("1,256,3".parse::<ColorRgb>(), Err(ParseColorError()));
332        assert_eq!("256,2,3".parse::<ColorRgb>(), Err(ParseColorError()));
333        assert_eq!("1,-2,3".parse::<ColorRgb>(), Err(ParseColorError()));
334    }
335
336    #[test]
337    fn color_hsv_parse_invalid() {
338        assert_eq!("".parse::<ColorHsv>(), Err(ParseColorError()));
339        assert_eq!("1,2".parse::<ColorHsv>(), Err(ParseColorError()));
340        assert_eq!("1,2,3,4".parse::<ColorHsv>(), Err(ParseColorError()));
341        assert_eq!("1,2,101".parse::<ColorHsv>(), Err(ParseColorError()));
342        assert_eq!("1,101,3".parse::<ColorHsv>(), Err(ParseColorError()));
343        assert_eq!("361,2,3".parse::<ColorHsv>(), Err(ParseColorError()));
344        assert_eq!("1,-2,3".parse::<ColorHsv>(), Err(ParseColorError()));
345    }
346}