use crate::values::{ColorFormat, EnumValue, Value, ValueError};
use std::collections::HashMap;
use std::fmt::{self, Debug, Display, Formatter};
use std::ops::RangeInclusive;
use std::str::FromStr;
use std::time::Duration;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum State {
Unknown,
Init,
Ready,
Disconnected,
Sleeping,
Lost,
Alert,
}
impl State {
fn as_str(&self) -> &'static str {
match self {
Self::Unknown => "unknown",
Self::Init => "init",
Self::Ready => "ready",
Self::Disconnected => "disconnected",
Self::Sleeping => "sleeping",
Self::Lost => "lost",
Self::Alert => "alert",
}
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[error("Invalid state '{0}'")]
pub struct ParseStateError(String);
impl FromStr for State {
type Err = ParseStateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"init" => Ok(Self::Init),
"ready" => Ok(Self::Ready),
"disconnected" => Ok(Self::Disconnected),
"sleeping" => Ok(Self::Sleeping),
"lost" => Ok(Self::Lost),
"alert" => Ok(Self::Alert),
_ => Err(ParseStateError(s.to_owned())),
}
}
}
impl Display for State {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Datatype {
Integer,
Float,
Boolean,
String,
Enum,
Color,
}
impl Datatype {
fn as_str(&self) -> &'static str {
match self {
Self::Integer => "integer",
Self::Float => "float",
Self::Boolean => "boolean",
Self::String => "string",
Self::Enum => "enum",
Self::Color => "color",
}
}
}
impl Display for Datatype {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[error("Invalid datatype '{0}'")]
pub struct ParseDatatypeError(String);
impl FromStr for Datatype {
type Err = ParseDatatypeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"integer" => Ok(Self::Integer),
"float" => Ok(Self::Float),
"boolean" => Ok(Self::Boolean),
"string" => Ok(Self::String),
"enum" => Ok(Self::Enum),
"color" => Ok(Self::Color),
_ => Err(ParseDatatypeError(s.to_owned())),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Property {
pub id: String,
pub name: Option<String>,
pub datatype: Option<Datatype>,
pub settable: bool,
pub retained: bool,
pub unit: Option<String>,
pub format: Option<String>,
pub value: Option<String>,
}
impl Property {
pub(crate) fn new(id: &str) -> Property {
Property {
id: id.to_owned(),
name: None,
datatype: None,
settable: false,
retained: true,
unit: None,
format: None,
value: None,
}
}
pub fn has_required_attributes(&self) -> bool {
self.name.is_some() && self.datatype.is_some()
}
pub fn value<T: Value>(&self) -> Result<T, ValueError> {
T::valid_for(self.datatype, &self.format)?;
match self.value {
None => Err(ValueError::Unknown),
Some(ref value) => value.parse().map_err(|_| ValueError::ParseFailed {
value: value.to_owned(),
datatype: T::datatype(),
}),
}
}
pub fn color_format(&self) -> Result<ColorFormat, ValueError> {
if let Some(actual) = self.datatype {
if actual != Datatype::Color {
return Err(ValueError::WrongDatatype {
expected: Datatype::Color,
actual,
});
}
}
match self.format {
None => Err(ValueError::Unknown),
Some(ref format) => format.parse(),
}
}
pub fn enum_values(&self) -> Result<Vec<&str>, ValueError> {
EnumValue::valid_for(self.datatype, &self.format)?;
match self.format {
None => Err(ValueError::Unknown),
Some(ref format) => {
if format.is_empty() {
Err(ValueError::WrongFormat {
format: "".to_owned(),
})
} else {
Ok(format.split(',').collect())
}
}
}
}
pub fn range<T: Value + Copy>(&self) -> Result<RangeInclusive<T>, ValueError> {
T::valid_for(self.datatype, &self.format)?;
match self.format {
None => Err(ValueError::Unknown),
Some(ref format) => {
if let [Ok(start), Ok(end)] = format
.splitn(2, ':')
.map(|part| part.parse())
.collect::<Vec<_>>()
.as_slice()
{
Ok(RangeInclusive::new(*start, *end))
} else {
Err(ValueError::WrongFormat {
format: format.to_owned(),
})
}
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Node {
pub id: String,
pub name: Option<String>,
pub node_type: Option<String>,
pub properties: HashMap<String, Property>,
}
impl Node {
pub(crate) fn new(id: &str) -> Node {
Node {
id: id.to_owned(),
name: None,
node_type: None,
properties: HashMap::new(),
}
}
pub(crate) fn add_property(&mut self, property: Property) {
self.properties.insert(property.id.clone(), property);
}
pub fn has_required_attributes(&self) -> bool {
self.name.is_some()
&& self.node_type.is_some()
&& !self.properties.is_empty()
&& self
.properties
.values()
.all(|property| property.has_required_attributes())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Extension {
pub id: String,
pub version: String,
pub homie_versions: Vec<String>,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[error("Invalid extension '{0}'")]
pub struct ParseExtensionError(String);
impl FromStr for Extension {
type Err = ParseExtensionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<_> = s.split(':').collect();
if let [id, version, homie_versions] = parts.as_slice() {
if let Some(homie_versions) = homie_versions.strip_prefix('[') {
if let Some(homie_versions) = homie_versions.strip_suffix(']') {
return Ok(Extension {
id: (*id).to_owned(),
version: (*version).to_owned(),
homie_versions: homie_versions.split(';').map(|p| p.to_owned()).collect(),
});
}
}
}
Err(ParseExtensionError(s.to_owned()))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Device {
pub id: String,
pub homie_version: String,
pub name: Option<String>,
pub state: State,
pub implementation: Option<String>,
pub nodes: HashMap<String, Node>,
pub extensions: Vec<Extension>,
pub local_ip: Option<String>,
pub mac: Option<String>,
pub firmware_name: Option<String>,
pub firmware_version: Option<String>,
pub stats_interval: Option<Duration>,
pub stats_uptime: Option<Duration>,
pub stats_signal: Option<i64>,
pub stats_cputemp: Option<f64>,
pub stats_cpuload: Option<i64>,
pub stats_battery: Option<i64>,
pub stats_freeheap: Option<u64>,
pub stats_supply: Option<f64>,
}
impl Device {
pub(crate) fn new(id: &str, homie_version: &str) -> Device {
Device {
id: id.to_owned(),
homie_version: homie_version.to_owned(),
name: None,
state: State::Unknown,
implementation: None,
nodes: HashMap::new(),
extensions: Vec::default(),
local_ip: None,
mac: None,
firmware_name: None,
firmware_version: None,
stats_interval: None,
stats_uptime: None,
stats_signal: None,
stats_cputemp: None,
stats_cpuload: None,
stats_battery: None,
stats_freeheap: None,
stats_supply: None,
}
}
pub(crate) fn add_node(&mut self, node: Node) {
self.nodes.insert(node.id.clone(), node);
}
pub fn has_required_attributes(&self) -> bool {
self.name.is_some()
&& self.state != State::Unknown
&& self
.nodes
.values()
.all(|node| node.has_required_attributes())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::values::{ColorHsv, ColorRgb, EnumValue};
#[test]
fn extension_parse_succeeds() {
let legacy_stats: Extension = "org.homie.legacy-stats:0.1.1:[4.x]".parse().unwrap();
assert_eq!(legacy_stats.id, "org.homie.legacy-stats");
assert_eq!(legacy_stats.version, "0.1.1");
assert_eq!(legacy_stats.homie_versions, &["4.x"]);
let meta: Extension = "eu.epnw.meta:1.1.0:[3.0.1;4.x]".parse().unwrap();
assert_eq!(meta.id, "eu.epnw.meta");
assert_eq!(meta.version, "1.1.0");
assert_eq!(meta.homie_versions, &["3.0.1", "4.x"]);
let minimal: Extension = "a:0:[]".parse().unwrap();
assert_eq!(minimal.id, "a");
assert_eq!(minimal.version, "0");
assert_eq!(minimal.homie_versions, &[""]);
}
#[test]
fn extension_parse_fails() {
assert_eq!(
"".parse::<Extension>(),
Err(ParseExtensionError("".to_owned()))
);
assert_eq!(
"test.blah:1.2.3".parse::<Extension>(),
Err(ParseExtensionError("test.blah:1.2.3".to_owned()))
);
assert_eq!(
"test.blah:1.2.3:4.x".parse::<Extension>(),
Err(ParseExtensionError("test.blah:1.2.3:4.x".to_owned()))
);
}
#[test]
fn property_integer_parse() {
let mut property = Property::new("property_id");
assert_eq!(property.value::<i64>(), Err(ValueError::Unknown));
property.value = Some("-".to_owned());
assert_eq!(
property.value::<i64>(),
Err(ValueError::ParseFailed {
value: "-".to_owned(),
datatype: Datatype::Integer,
})
);
property.value = Some("42".to_owned());
assert_eq!(property.value(), Ok(42));
property.datatype = Some(Datatype::Integer);
assert_eq!(property.value(), Ok(42));
property.value = Some("-66".to_owned());
assert_eq!(property.value(), Ok(-66));
property.datatype = Some(Datatype::Float);
assert_eq!(
property.value::<i64>(),
Err(ValueError::WrongDatatype {
actual: Datatype::Float,
expected: Datatype::Integer,
})
);
}
#[test]
fn property_float_parse() {
let mut property = Property::new("property_id");
assert_eq!(property.value::<f64>(), Err(ValueError::Unknown));
property.value = Some("-".to_owned());
assert_eq!(
property.value::<f64>(),
Err(ValueError::ParseFailed {
value: "-".to_owned(),
datatype: Datatype::Float,
})
);
property.value = Some("42.36".to_owned());
assert_eq!(property.value(), Ok(42.36));
property.datatype = Some(Datatype::Float);
assert_eq!(property.value(), Ok(42.36));
property.datatype = Some(Datatype::Integer);
assert_eq!(
property.value::<f64>(),
Err(ValueError::WrongDatatype {
actual: Datatype::Integer,
expected: Datatype::Float,
})
);
}
#[test]
fn property_color_parse() {
let mut property = Property::new("property_id");
assert_eq!(property.value::<ColorRgb>(), Err(ValueError::Unknown));
assert_eq!(property.value::<ColorHsv>(), Err(ValueError::Unknown));
property.value = Some("".to_owned());
assert_eq!(
property.value::<ColorRgb>(),
Err(ValueError::ParseFailed {
value: "".to_owned(),
datatype: Datatype::Color,
})
);
property.value = Some("12,34,56".to_owned());
assert_eq!(
property.value(),
Ok(ColorRgb {
r: 12,
g: 34,
b: 56
})
);
assert_eq!(
property.value(),
Ok(ColorHsv {
h: 12,
s: 34,
v: 56
})
);
property.datatype = Some(Datatype::Color);
assert_eq!(
property.value(),
Ok(ColorRgb {
r: 12,
g: 34,
b: 56
})
);
assert_eq!(
property.value(),
Ok(ColorHsv {
h: 12,
s: 34,
v: 56
})
);
property.format = Some("rgb".to_owned());
assert_eq!(
property.value(),
Ok(ColorRgb {
r: 12,
g: 34,
b: 56
})
);
assert_eq!(
property.value::<ColorHsv>(),
Err(ValueError::WrongFormat {
format: "rgb".to_owned()
})
);
property.datatype = Some(Datatype::Integer);
assert_eq!(
property.value::<ColorRgb>(),
Err(ValueError::WrongDatatype {
actual: Datatype::Integer,
expected: Datatype::Color,
})
);
assert_eq!(
property.value::<ColorHsv>(),
Err(ValueError::WrongDatatype {
actual: Datatype::Integer,
expected: Datatype::Color,
})
);
}
#[test]
fn property_enum_parse() {
let mut property = Property::new("property_id");
assert_eq!(property.value::<EnumValue>(), Err(ValueError::Unknown));
property.value = Some("".to_owned());
assert_eq!(
property.value::<EnumValue>(),
Err(ValueError::ParseFailed {
value: "".to_owned(),
datatype: Datatype::Enum,
})
);
property.value = Some("anything".to_owned());
assert_eq!(property.value(), Ok(EnumValue::new("anything")));
property.datatype = Some(Datatype::Enum);
assert_eq!(property.value(), Ok(EnumValue::new("anything")));
property.datatype = Some(Datatype::String);
assert_eq!(
property.value::<EnumValue>(),
Err(ValueError::WrongDatatype {
actual: Datatype::String,
expected: Datatype::Enum,
})
);
}
#[test]
fn property_color_format() {
let mut property = Property::new("property_id");
assert_eq!(property.color_format(), Err(ValueError::Unknown));
property.format = Some("".to_owned());
assert_eq!(
property.color_format(),
Err(ValueError::WrongFormat {
format: "".to_owned()
})
);
property.format = Some("rgb".to_owned());
assert_eq!(property.color_format(), Ok(ColorFormat::Rgb));
property.format = Some("hsv".to_owned());
assert_eq!(property.color_format(), Ok(ColorFormat::Hsv));
property.datatype = Some(Datatype::Integer);
assert_eq!(
property.color_format(),
Err(ValueError::WrongDatatype {
actual: Datatype::Integer,
expected: Datatype::Color
})
);
property.datatype = Some(Datatype::Color);
assert_eq!(property.color_format(), Ok(ColorFormat::Hsv));
}
#[test]
fn property_enum_format() {
let mut property = Property::new("property_id");
assert_eq!(property.enum_values(), Err(ValueError::Unknown));
property.format = Some("".to_owned());
assert_eq!(
property.enum_values(),
Err(ValueError::WrongFormat {
format: "".to_owned()
})
);
property.format = Some("one".to_owned());
assert_eq!(property.enum_values(), Ok(vec!["one"]));
property.format = Some("one,two,three".to_owned());
assert_eq!(property.enum_values(), Ok(vec!["one", "two", "three"]));
property.datatype = Some(Datatype::Enum);
assert_eq!(property.enum_values(), Ok(vec!["one", "two", "three"]));
property.datatype = Some(Datatype::Color);
assert_eq!(
property.enum_values(),
Err(ValueError::WrongDatatype {
actual: Datatype::Color,
expected: Datatype::Enum
})
);
}
#[test]
fn property_numeric_format() {
let mut property = Property::new("property_id");
assert_eq!(property.range::<i64>(), Err(ValueError::Unknown));
assert_eq!(property.range::<f64>(), Err(ValueError::Unknown));
property.format = Some("".to_owned());
assert_eq!(
property.range::<i64>(),
Err(ValueError::WrongFormat {
format: "".to_owned()
})
);
assert_eq!(
property.range::<f64>(),
Err(ValueError::WrongFormat {
format: "".to_owned()
})
);
property.format = Some("1:10".to_owned());
assert_eq!(property.range(), Ok(1..=10));
assert_eq!(property.range(), Ok(1.0..=10.0));
property.format = Some("3.6:4.2".to_owned());
assert_eq!(property.range(), Ok(3.6..=4.2));
assert_eq!(
property.range::<i64>(),
Err(ValueError::WrongFormat {
format: "3.6:4.2".to_owned()
})
);
property.datatype = Some(Datatype::Integer);
property.format = Some("1:10".to_owned());
assert_eq!(property.range(), Ok(1..=10));
assert_eq!(
property.range::<f64>(),
Err(ValueError::WrongDatatype {
actual: Datatype::Integer,
expected: Datatype::Float
})
);
}
#[test]
fn property_has_required_attributes() {
let mut property = Property::new("property_id");
assert!(!property.has_required_attributes());
property.name = Some("Property name".to_owned());
assert!(!property.has_required_attributes());
property.datatype = Some(Datatype::Integer);
assert!(property.has_required_attributes());
}
fn property_with_required_attributes() -> Property {
let mut property = Property::new("property_id");
property.name = Some("Property name".to_owned());
property.datatype = Some(Datatype::Integer);
property
}
#[test]
fn node_has_required_attributes() {
let mut node = Node::new("node_id");
assert!(!node.has_required_attributes());
node.name = Some("Node name".to_owned());
assert!(!node.has_required_attributes());
node.node_type = Some("Node type".to_owned());
assert!(!node.has_required_attributes());
node.add_property(property_with_required_attributes());
assert!(node.has_required_attributes());
node.add_property(Property::new("property_without_required_attributes"));
assert!(!node.has_required_attributes());
}
fn node_with_required_attributes() -> Node {
let mut node = Node::new("node_id");
node.name = Some("Node name".to_owned());
node.node_type = Some("Node type".to_owned());
node.add_property(property_with_required_attributes());
node
}
#[test]
fn device_has_required_attributes() {
let mut device = Device::new("device_id", "123");
assert!(!device.has_required_attributes());
device.name = Some("Device name".to_owned());
assert!(!device.has_required_attributes());
device.state = State::Init;
assert!(device.has_required_attributes());
device.add_node(node_with_required_attributes());
assert!(device.has_required_attributes());
device.add_node(Node::new("node_without_required_attributes"));
assert!(!device.has_required_attributes());
}
}