1#![cfg_attr(docsrs, feature(doc_cfg))]
52
53mod conn;
54mod device;
55
56use std::collections::hash_map::RandomState;
57use std::fmt;
58use std::hash::{BuildHasher, Hasher};
59use std::time::Duration;
60
61pub use ebus_homie_tokio;
62pub use rumqttc;
63
64pub use conn::Reresolve;
65pub use device::{DeviceTask, PairTask, RunningDevice, RunningPair, device, pair};
66
67use ebus_homie_tokio::ebus_homie::controller::{Config, ConfigError};
68use ebus_homie_tokio::{
69 Commands, ControllerDriver, ControllerHandle, ControllerOptions, DriverError, EventReceiver,
70 Inbox, attach_controller,
71};
72use rumqttc::{AsyncClient, EventLoop, MqttOptions};
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
76#[non_exhaustive]
77pub enum Error {
78 Driver(DriverError),
80}
81
82impl fmt::Display for Error {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Error::Driver(_) => f.write_str("driver stopped"),
86 }
87 }
88}
89
90impl std::error::Error for Error {
91 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
92 match self {
93 Error::Driver(e) => Some(e),
94 }
95 }
96}
97
98impl From<DriverError> for Error {
99 fn from(e: DriverError) -> Self {
100 Error::Driver(e)
101 }
102}
103
104#[derive(Debug)]
106#[non_exhaustive]
107pub struct RunningController {
108 pub handle: ControllerHandle,
110 pub events: EventReceiver,
112 pub task: ControllerTask,
114}
115
116pub struct ControllerTask {
119 driver: ControllerDriver,
120 inbox: Inbox,
121 commands: Commands,
122 client: AsyncClient,
123 eventloop: EventLoop,
124 reresolve: Option<Reresolve>,
125}
126
127impl fmt::Debug for ControllerTask {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 f.debug_struct("ControllerTask")
130 .field("driver", &self.driver)
131 .field("broker", &self.eventloop.mqtt_options.broker_address())
132 .finish_non_exhaustive()
133 }
134}
135
136pub fn controller(
140 mqtt: MqttOptions,
141 mut config: Config,
142 opts: ControllerOptions,
143) -> Result<RunningController, ConfigError> {
144 config.link_up_discards_queued = true;
145 let a = attach_controller(config, opts)?;
146 let (client, eventloop) = AsyncClient::new(mqtt, conn::REQUEST_CAPACITY);
147 Ok(RunningController {
148 handle: a.handle,
149 events: a.events,
150 task: ControllerTask {
151 driver: a.driver,
152 inbox: a.inbox,
153 commands: a.commands,
154 client,
155 eventloop,
156 reresolve: None,
157 },
158 })
159}
160
161pub fn controller_client_id() -> String {
164 let mut h = RandomState::new().build_hasher();
165 h.write_u128(
166 std::time::SystemTime::now()
167 .duration_since(std::time::UNIX_EPOCH)
168 .map_or(0, |d| d.as_nanos()),
169 );
170 h.write_u32(std::process::id());
171 format!("ebus-controller-{:016x}", h.finish())
172}
173
174impl ControllerTask {
175 pub fn reresolve(mut self, f: Reresolve) -> Self {
187 self.reresolve = Some(f);
188 self
189 }
190
191 pub async fn run(self) -> Result<(), Error> {
202 let ControllerTask {
203 driver,
204 inbox,
205 commands,
206 client,
207 eventloop,
208 reresolve,
209 } = self;
210 let conn = conn::Conn {
211 inbox,
212 commands,
213 client,
214 eventloop,
215 receipts: false,
216 flush_timeout: Duration::ZERO,
217 redirect: reresolve.map(|f| conn::Redirect::new(f, None)),
218 };
219 conn::run(driver.run(), conn).await.map_err(Error::Driver)
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 use conn::{BACKOFF_MIN, Executor, REQUEST_CAPACITY, Unsent, Wire, next_backoff};
228 use ebus_homie_tokio::ebus_mqtt::{self, Command};
229 use rumqttc::{Request, SubscribeFilter};
230 use tokio::sync::{Notify, watch};
231
232 fn try_send(client: &AsyncClient, cmd: Command) -> Result<(), Unsent> {
233 conn::try_send(client, cmd, None)
234 }
235
236 fn send_check<T: Send>(_: &T) {}
237
238 #[test]
239 fn task_future_is_send() {
240 let mqtt = MqttOptions::new(controller_client_id(), "localhost", 1883);
241 let run = controller(mqtt, Config::wildcard(), ControllerOptions::default())
242 .expect("valid config");
243 send_check(
244 &run.task
245 .reresolve(Box::new(|| Box::pin(async { None })))
246 .run(),
247 );
248 }
249
250 #[test]
251 fn driver_error_is_reported_once() {
252 use std::error::Error as _;
253 let inner = DriverError::CommandsClosed;
254 let e = Error::from(inner);
255 assert!(!e.to_string().contains(&inner.to_string()), "{e}");
256 assert_eq!(e.source().map(ToString::to_string), Some(inner.to_string()));
257 }
258
259 #[test]
260 fn client_ids_are_distinct_and_prefixed() {
261 let a = controller_client_id();
262 let b = controller_client_id();
263 assert!(a.starts_with("ebus-controller-"));
264 assert_eq!(a.len(), "ebus-controller-".len() + 16);
265 assert_ne!(a, b);
266 }
267
268 #[test]
269 fn backoff_doubles_to_thirty_seconds() {
270 let mut b = BACKOFF_MIN;
271 let mut seen = vec![b.as_secs()];
272 for _ in 0..7 {
273 b = next_backoff(b);
274 seen.push(b.as_secs());
275 }
276 assert_eq!(seen, [1, 2, 4, 8, 16, 30, 30, 30]);
277 }
278
279 #[tokio::test]
280 async fn commands_become_rumqttc_requests() {
281 let mqtt = MqttOptions::new("t", "localhost", 1883);
282 let (client, mut el) = AsyncClient::new(mqtt, 16);
283 let cmds = [
284 Command::subscribe(vec![
285 ("ebus/5/+/$state".into(), ebus_mqtt::QoS::ExactlyOnce),
286 ("ebus/5/d/+/+".into(), ebus_mqtt::QoS::AtLeastOnce),
287 ]),
288 Command::unsubscribe(vec!["a/b".into(), "c/d".into()]),
289 Command::publish(
290 "ebus/5/d/n/p/set",
291 b"true".to_vec(),
292 ebus_mqtt::QoS::AtMostOnce,
293 false,
294 ),
295 ];
296 for c in cmds {
297 try_send(&client, c).expect("queued");
298 }
299 el.clean();
301 let got: Vec<Request> = el.pending.drain(..).collect();
302 assert_eq!(got.len(), 4, "{got:?}");
303 let Request::Subscribe(s) = &got[0] else {
304 panic!("{got:?}");
305 };
306 assert_eq!(
307 s.filters,
308 vec![
309 SubscribeFilter::new("ebus/5/+/$state".into(), rumqttc::QoS::ExactlyOnce),
310 SubscribeFilter::new("ebus/5/d/+/+".into(), rumqttc::QoS::AtLeastOnce),
311 ]
312 );
313 assert!(matches!(&got[1], Request::Unsubscribe(u) if u.topics == ["a/b"]));
314 assert!(matches!(&got[2], Request::Unsubscribe(u) if u.topics == ["c/d"]));
315 let Request::Publish(p) = &got[3] else {
316 panic!("{got:?}");
317 };
318 assert_eq!(p.topic, "ebus/5/d/n/p/set");
319 assert_eq!(&p.payload[..], b"true");
320 assert_eq!(p.qos, rumqttc::QoS::AtMostOnce);
321 assert!(!p.retain);
322 }
323
324 #[tokio::test]
325 async fn shutdown_while_the_broker_is_down_ends_the_task() {
326 let port = std::net::TcpListener::bind("127.0.0.1:0")
327 .and_then(|l| l.local_addr())
328 .expect("free port")
329 .port();
330 let mqtt = MqttOptions::new(controller_client_id(), "127.0.0.1", port);
331 let run =
332 controller(mqtt, Config::wildcard(), ControllerOptions::default()).expect("valid");
333 let inbox = run.task.inbox.clone();
336 inbox.connected(false);
337 for i in 0..2 * REQUEST_CAPACITY {
338 let topic = format!("ebus/5/d{i}/$state");
339 assert!(inbox.message(topic, b"ready".to_vec().into(), Some(true)));
340 }
341 drop(inbox);
342 let task = tokio::spawn(run.task.run());
343 tokio::time::sleep(Duration::from_millis(200)).await;
344 run.handle.shutdown().await;
345 let ended = tokio::time::timeout(Duration::from_secs(10), task).await;
346 assert!(
347 matches!(ended, Ok(Ok(Ok(())))),
348 "the task did not end: {ended:?}"
349 );
350 }
351
352 async fn run_for(fut: &mut std::pin::Pin<&mut impl std::future::Future<Output = ()>>, ms: u64) {
354 tokio::select! {
355 () = fut.as_mut() => panic!("the executor stopped"),
356 () = tokio::time::sleep(Duration::from_millis(ms)) => {}
357 }
358 }
359
360 #[tokio::test]
361 async fn the_executor_holds_commands_while_down_and_skips_stale_subscribes() {
362 let mut cfg = Config::wildcard();
364 cfg.link_up_discards_queued = true;
365 let a = attach_controller(cfg, ControllerOptions::default()).expect("valid");
366 let inbox = a.inbox.clone();
367 tokio::spawn(a.driver.run());
368 let (client, mut el) = AsyncClient::new(MqttOptions::new("t", "localhost", 1883), 16);
369 let (end_tx, _end_rx) = watch::channel(conn::End::Running);
370 let (wire_tx, wire_rx) = watch::channel(Wire::Down);
371 let progress = Notify::new();
372 let exec = Executor {
373 client,
374 inbox: a.inbox,
375 track: None,
376 progress: &progress,
377 flush_timeout: Duration::ZERO,
378 }
379 .run(a.commands, end_tx, wire_rx);
380 tokio::pin!(exec);
381
382 inbox.connected(false);
384 run_for(&mut exec, 50).await;
385 el.clean();
386 assert!(el.pending.is_empty(), "{:?}", el.pending);
387
388 inbox.disconnected();
391 inbox.connected(false);
392 run_for(&mut exec, 50).await;
393 wire_tx.send_replace(Wire::Up);
394 run_for(&mut exec, 50).await;
395 el.clean();
396 let subs: Vec<&Request> = el
397 .pending
398 .iter()
399 .filter(|r| matches!(r, Request::Subscribe(_)))
400 .collect();
401 assert_eq!(subs.len(), 1, "{:?}", el.pending);
402 }
403
404 #[tokio::test]
405 async fn a_closed_client_returns_the_command() {
406 let mqtt = MqttOptions::new("t", "localhost", 1883);
407 let (client, el) = AsyncClient::new(mqtt, 16);
408 drop(el);
409 let cmd = Command::unsubscribe(vec!["a".into(), "b".into()]);
410 assert_eq!(try_send(&client, cmd.clone()), Err(Unsent::Full(cmd)));
411 let cmd = Command::subscribe(vec![("a".into(), ebus_mqtt::QoS::AtMostOnce)]);
412 assert_eq!(try_send(&client, cmd.clone()), Err(Unsent::Full(cmd)));
413 let cmd = Command::subscribe(vec![("a/#/b".into(), ebus_mqtt::QoS::AtMostOnce)]);
414 assert_eq!(try_send(&client, cmd.clone()), Err(Unsent::Invalid(cmd)));
415 }
416}
417
418#[cfg(doctest)]
419#[doc = include_str!("../README.md")]
420struct ReadmeDoctests;