use alloc::{string::String, sync::Arc, vec::Vec};
use core::fmt;
use ebus_mqtt::{Link, QoS, filter};
use rpds::RedBlackTreeMapSync;
pub use crate::{Id, PropertyAddr};
pub use ebus_mqtt::Handled;
use crate::EBUS_DOMAIN;
use crate::datatype::Problem;
use crate::description::{DescriptionError, DeviceDescription, Ignored};
use crate::id::is_valid_id;
use crate::state::DeviceState;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Domain {
Named(String),
Any,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Discovery {
Wildcard,
Device(Id),
Tree {
root: Id,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Pacing {
pub batch_devices: usize,
pub batch_values: usize,
}
impl Default for Pacing {
fn default() -> Self {
Pacing {
batch_devices: 8,
batch_values: 500,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Config {
pub domain: Domain,
pub discovery: Discovery,
pub subscribe_qos: QoS,
pub pacing: Option<Pacing>,
pub stuck_timeout_ms: Option<u64>,
pub max_resubscribe_attempts: u8,
pub absent_after_ms: u64,
pub sync_timeout_ms: u64,
pub trust_session: bool,
pub link_up_discards_queued: bool,
pub max_early_values: usize,
pub max_alerts: usize,
pub max_devices: Option<usize>,
pub host_filters: Vec<String>,
}
impl Config {
pub fn wildcard() -> Self {
Config {
domain: Domain::Named(String::from(EBUS_DOMAIN)),
discovery: Discovery::Wildcard,
subscribe_qos: QoS::ExactlyOnce,
pacing: Some(Pacing::default()),
stuck_timeout_ms: Some(10_000),
max_resubscribe_attempts: 3,
absent_after_ms: 30_000,
sync_timeout_ms: 10_000,
trust_session: false,
link_up_discards_queued: false,
max_early_values: 1024,
max_alerts: 256,
max_devices: None,
host_filters: Vec::new(),
}
}
pub fn tree(root: &str) -> Result<Self, ConfigError> {
let root = Id::new(root).ok_or(ConfigError::InvalidDeviceId)?;
Ok(Config {
discovery: Discovery::Tree { root },
..Config::wildcard()
})
}
pub fn device(id: &str) -> Result<Self, ConfigError> {
let id = Id::new(id).ok_or(ConfigError::InvalidDeviceId)?;
Ok(Config {
discovery: Discovery::Device(id),
..Config::wildcard()
})
}
pub(super) fn validate(&self) -> Result<(), ConfigError> {
match (&self.domain, &self.discovery) {
(Domain::Named(d), _) if !is_valid_id(d) => return Err(ConfigError::InvalidDomain),
(Domain::Any, Discovery::Device(_) | Discovery::Tree { .. }) => {
return Err(ConfigError::InvalidDomain);
}
_ => {}
}
if self
.pacing
.is_some_and(|p| p.batch_devices == 0 || p.batch_values == 0)
{
return Err(ConfigError::ZeroBatch);
}
if !self.host_filters.iter().all(|f| filter::is_valid_filter(f)) {
return Err(ConfigError::InvalidHostFilter);
}
Ok(())
}
}
impl Default for Config {
fn default() -> Self {
Config::wildcard()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ConfigError {
InvalidDomain,
InvalidDeviceId,
ZeroBatch,
InvalidHostFilter,
}
impl ConfigError {
pub const fn as_str(self) -> &'static str {
match self {
ConfigError::InvalidDomain => "invalidDomain",
ConfigError::InvalidDeviceId => "invalidDeviceId",
ConfigError::ZeroBatch => "zeroBatch",
ConfigError::InvalidHostFilter => "invalidHostFilter",
}
}
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl core::error::Error for ConfigError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Origin {
Retained,
Live,
Unknown,
}
impl Origin {
pub(super) fn of(retain: Option<bool>) -> Self {
match retain {
Some(true) => Origin::Retained,
Some(false) => Origin::Live,
None => Origin::Unknown,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Value {
pub raw: Arc<str>,
pub check: Result<(), Problem>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Missing {
State,
Description,
Properties,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Event {
Link(Link),
#[non_exhaustive]
DeviceDiscovered {
device: Id,
state: DeviceState,
},
#[non_exhaustive]
StateChanged {
device: Id,
old: Option<DeviceState>,
new: DeviceState,
effective: DeviceState,
},
#[non_exhaustive]
EffectiveStateChanged {
device: Id,
old: DeviceState,
new: DeviceState,
},
#[non_exhaustive]
DescriptionChanged {
device: Id,
description: Arc<DeviceDescription>,
ignored: Vec<Ignored>,
while_ready: bool,
},
#[non_exhaustive]
DescriptionRejected {
device: Id,
error: DescriptionError,
},
#[non_exhaustive]
PropertyChanged {
addr: PropertyAddr,
value: Value,
origin: Origin,
},
#[non_exhaustive]
PropertyEvent {
addr: PropertyAddr,
value: Value,
origin: Origin,
},
#[non_exhaustive]
PropertyCleared {
addr: PropertyAddr,
},
#[non_exhaustive]
TargetChanged {
addr: PropertyAddr,
target: Option<Value>,
},
#[non_exhaustive]
Alert {
device: Id,
alert: Id,
message: Option<String>,
},
#[non_exhaustive]
DeviceSynced {
device: Id,
missing: u32,
},
#[non_exhaustive]
DeviceStuck {
device: Id,
missing: Missing,
attempt: u8,
gave_up: bool,
},
#[non_exhaustive]
DeviceAbsent {
device: Id,
},
#[non_exhaustive]
DeviceRemoved {
device: Id,
},
#[non_exhaustive]
TreeComplete {
root: Id,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct SetOptions {
pub allow_not_ready: bool,
pub unchecked: bool,
}
impl SetOptions {
#[must_use]
pub const fn allow_not_ready(mut self, allow: bool) -> Self {
self.allow_not_ready = allow;
self
}
#[must_use]
pub const fn unchecked(mut self, unchecked: bool) -> Self {
self.unchecked = unchecked;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SetError {
NotConnected,
UnknownDevice,
UnknownProperty,
NotSettable,
DeviceNotReady(DeviceState),
Invalid(Problem),
InvalidTopic,
}
impl SetError {
pub const fn as_str(self) -> &'static str {
match self {
SetError::NotConnected => "notConnected",
SetError::UnknownDevice => "unknownDevice",
SetError::UnknownProperty => "unknownProperty",
SetError::NotSettable => "notSettable",
SetError::DeviceNotReady(_) => "deviceNotReady",
SetError::Invalid(_) => "invalid",
SetError::InvalidTopic => "invalidTopic",
}
}
}
impl fmt::Display for SetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SetError::DeviceNotReady(s) => write!(f, "deviceNotReady({s})"),
SetError::Invalid(p) => write!(f, "invalid({p})"),
other => f.write_str(other.as_str()),
}
}
}
impl core::error::Error for SetError {}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PropertyView {
pub value: Option<Value>,
pub last_valid: Option<Arc<str>>,
pub target: Option<Value>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct Slot {
pub(super) view: PropertyView,
pub(super) node: Id,
pub(super) prop: Id,
pub(super) node_idx: usize,
pub(super) prop_idx: usize,
pub(super) retained: bool,
}
pub(super) type Props = RedBlackTreeMapSync<Id, RedBlackTreeMapSync<Id, Slot>>;
#[derive(Clone, Debug)]
pub struct DeviceView {
pub id: Id,
pub domain: Arc<str>,
pub state: Option<DeviceState>,
pub effective: Option<DeviceState>,
pub description: Option<Arc<DeviceDescription>>,
pub confirmed: bool,
pub synced: bool,
pub(super) props: Props,
pub(super) alerts: RedBlackTreeMapSync<Id, String>,
}
impl DeviceView {
pub(super) fn new(id: Id, domain: Arc<str>) -> Self {
DeviceView {
id,
domain,
state: None,
effective: None,
description: None,
confirmed: true,
synced: false,
props: Props::new_sync(),
alerts: RedBlackTreeMapSync::new_sync(),
}
}
pub fn shares_values(&self, other: &DeviceView) -> bool {
self.props.ptr_eq(&other.props) && self.alerts.ptr_eq(&other.alerts)
}
pub fn property(&self, node: &str, property: &str) -> Option<&PropertyView> {
self.props.get(node)?.get(property).map(|s| &s.view)
}
pub fn properties(&self) -> impl Iterator<Item = (&Id, &Id, &PropertyView)> + '_ {
self.props
.values()
.flat_map(|m| m.values())
.map(|s| (&s.node, &s.prop, &s.view))
}
pub fn alert(&self, id: &str) -> Option<&str> {
self.alerts.get(id).map(String::as_str)
}
pub fn alerts(&self) -> impl Iterator<Item = (&Id, &str)> + '_ {
self.alerts.iter().map(|(k, v)| (k, v.as_str()))
}
}