use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use crate::cli::OutputFormat;
use crate::format::{
FormatOptions, format_multi_reading_csv, format_multi_reading_json, format_multi_reading_text,
format_reading_csv, format_reading_json, format_reading_text, format_reading_text_with_name,
};
use crate::util::{require_device_interactive, write_output};
use anyhow::{Context, Result, bail};
use aranet_core::advertisement::parse_advertisement_with_name;
use aranet_core::scan::{ScanOptions, scan_with_options};
use aranet_types::CurrentReading;
use futures::future::join_all;
pub struct DeviceReading {
pub identifier: String,
pub reading: CurrentReading,
}
pub async fn cmd_read(
devices: Vec<String>,
timeout: Duration,
format: OutputFormat,
output: Option<&PathBuf>,
quiet: bool,
passive: bool,
opts: &FormatOptions,
) -> Result<()> {
if passive {
if devices.len() > 1 {
bail!(
"Passive mode only supports one device, but {} were specified. \
Use a single device address or omit --passive.",
devices.len()
);
}
let device = devices.first().cloned();
return cmd_read_passive(device, timeout, format, output, quiet, opts).await;
}
let devices = if devices.is_empty() {
vec![require_device_interactive(None).await?]
} else {
devices
};
if devices.len() == 1 {
return cmd_read_single(&devices[0], timeout, format, output, quiet, opts).await;
}
cmd_read_multi(devices, timeout, format, output, quiet, opts).await
}
async fn cmd_read_single(
identifier: &str,
timeout: Duration,
format: OutputFormat,
output: Option<&PathBuf>,
quiet: bool,
opts: &FormatOptions,
) -> Result<()> {
let show_progress = !quiet && matches!(format, OutputFormat::Text);
let device =
crate::util::connect_device_with_progress(identifier, timeout, show_progress).await?;
let device_id = device.address().to_string();
let device_name = device.name().map(|s| s.to_string());
let reading_result = device
.read_current()
.await
.context("Failed to read current values");
crate::util::disconnect_device(&device).await;
let reading = reading_result?;
crate::util::save_reading_to_store(&device_id, &reading);
let content = match format {
OutputFormat::Json => format_reading_json(&reading, opts)?,
OutputFormat::Text => format_reading_text_with_name(&reading, opts, device_name.as_deref()),
OutputFormat::Csv => format_reading_csv(&reading, opts),
};
write_output(output, &content)?;
Ok(())
}
async fn cmd_read_multi(
devices: Vec<String>,
timeout: Duration,
format: OutputFormat,
output: Option<&PathBuf>,
quiet: bool,
opts: &FormatOptions,
) -> Result<()> {
let total_devices = devices.len();
let show_progress = !quiet && matches!(format, OutputFormat::Text);
if show_progress {
eprintln!("Reading from {} devices...", total_devices);
}
let completed = Arc::new(AtomicUsize::new(0));
let futures = devices.iter().map(|id| {
let completed = Arc::clone(&completed);
let id = id.clone();
async move {
let result = read_device(id.clone(), timeout).await;
let done = completed.fetch_add(1, Ordering::SeqCst) + 1;
if show_progress {
match &result {
Ok(reading) => {
eprintln!(" [{}/{}] {} - OK", done, total_devices, reading.identifier);
}
Err((id, _)) => {
eprintln!(" [{}/{}] {} - FAILED", done, total_devices, id);
}
}
}
result
}
});
let results: Vec<Result<DeviceReading, (String, anyhow::Error)>> = join_all(futures).await;
let mut readings = Vec::new();
let mut errors = Vec::new();
for result in results {
match result {
Ok(reading) => readings.push(reading),
Err((id, err)) => errors.push((id, err)),
}
}
if !quiet && !errors.is_empty() {
eprintln!();
for (id, err) in &errors {
eprintln!("Error reading {}: {}", id, err);
}
}
if readings.is_empty() {
bail!("Failed to read from any device");
}
let content = match format {
OutputFormat::Json => format_multi_reading_json(&readings, opts)?,
OutputFormat::Text => format_multi_reading_text(&readings, opts),
OutputFormat::Csv => format_multi_reading_csv(&readings, opts),
};
write_output(output, &content)?;
Ok(())
}
async fn read_device(
identifier: String,
timeout: Duration,
) -> Result<DeviceReading, (String, anyhow::Error)> {
let device = crate::util::connect_device_with_progress(&identifier, timeout, false)
.await
.map_err(|e| (identifier.clone(), e))?;
let device_id = device.address().to_string();
let reading_result = device
.read_current()
.await
.context("Failed to read current values")
.map_err(|e| (identifier.clone(), e));
crate::util::disconnect_device(&device).await;
let reading = reading_result?;
crate::util::save_reading_to_store(&device_id, &reading);
Ok(DeviceReading {
identifier,
reading,
})
}
async fn cmd_read_passive(
device: Option<String>,
timeout: Duration,
format: OutputFormat,
output: Option<&PathBuf>,
quiet: bool,
opts: &FormatOptions,
) -> Result<()> {
if !quiet && matches!(format, OutputFormat::Text) {
eprintln!("Scanning for advertisements (passive mode)...");
}
let options = ScanOptions::default()
.duration(timeout)
.filter_aranet_only(true);
let devices = scan_with_options(options)
.await
.context("Failed to scan for devices")?;
let target = device.as_deref();
let found = devices.iter().find(|d| {
if let Some(target) = target {
d.name.as_deref() == Some(target) || d.address == target || d.identifier == target
} else {
d.manufacturer_data.is_some()
}
});
let discovered = match found {
Some(d) => d,
None => {
if let Some(target) = target {
bail!("Device '{}' not found in advertisements", target);
} else {
bail!(
"No Aranet devices found with advertisement data. \
Make sure Smart Home integration is enabled on the device."
);
}
}
};
let mfr_data = discovered.manufacturer_data.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"Device '{}' has no advertisement data. \
Enable Smart Home integration on the device to use passive mode.",
discovered.name.as_deref().unwrap_or(&discovered.identifier)
)
})?;
let device_name = discovered.name.as_deref();
let adv = parse_advertisement_with_name(mfr_data, device_name)
.context("Failed to parse advertisement data")?;
let mut builder = CurrentReading::builder()
.co2(adv.co2.unwrap_or(0))
.temperature(adv.temperature.unwrap_or(0.0))
.pressure(adv.pressure.unwrap_or(0.0))
.humidity(adv.humidity.unwrap_or(0))
.battery(adv.battery)
.status(adv.status)
.interval(adv.interval)
.age(adv.age);
if let Some(radon) = adv.radon {
builder = builder.radon(radon);
}
if let Some(rate) = adv.radiation_dose_rate {
builder = builder.radiation_rate(rate);
}
let reading = builder.build();
if !quiet && matches!(format, OutputFormat::Text) {
let name = discovered.name.as_deref().unwrap_or(&discovered.identifier);
eprintln!("Read from {} (passive)", name);
}
let content = match format {
OutputFormat::Json => format_reading_json(&reading, opts)?,
OutputFormat::Text => format_reading_text(&reading, opts),
OutputFormat::Csv => format_reading_csv(&reading, opts),
};
write_output(output, &content)?;
Ok(())
}