use std::time::Duration;
use crate::DiscoveredDevice;
use crate::settings::DeviceSettings;
use aranet_types::{CurrentReading, DeviceType, HistoryRecord};
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub message: String,
pub retryable: bool,
pub suggestion: Option<String>,
}
impl ErrorContext {
pub fn permanent(message: impl Into<String>) -> Self {
Self {
message: message.into(),
retryable: false,
suggestion: None,
}
}
pub fn transient(message: impl Into<String>, suggestion: impl Into<String>) -> Self {
Self {
message: message.into(),
retryable: true,
suggestion: Some(suggestion.into()),
}
}
pub fn from_error(error: &crate::Error) -> Self {
use crate::error::ConnectionFailureReason;
match error {
crate::Error::Timeout { operation, .. } => Self::transient(
error.to_string(),
format!(
"The {} operation timed out. The device may be out of range or busy. Try moving closer.",
operation
),
),
crate::Error::ConnectionFailed { reason, .. } => match reason {
ConnectionFailureReason::OutOfRange => Self::transient(
error.to_string(),
"Device is out of Bluetooth range. Move closer and try again.",
),
ConnectionFailureReason::Timeout => Self::transient(
error.to_string(),
"Connection timed out. The device may be busy or out of range.",
),
ConnectionFailureReason::BleError(_) => Self::transient(
error.to_string(),
"Bluetooth error occurred. Try toggling Bluetooth off and on.",
),
ConnectionFailureReason::AdapterUnavailable => Self {
message: error.to_string(),
retryable: false,
suggestion: Some(
"Bluetooth adapter is unavailable. Enable Bluetooth and try again."
.to_string(),
),
},
ConnectionFailureReason::Rejected => Self {
message: error.to_string(),
retryable: false,
suggestion: Some(
"Connection was rejected by the device. Try re-pairing.".to_string(),
),
},
ConnectionFailureReason::AlreadyConnected => Self {
message: error.to_string(),
retryable: false,
suggestion: Some("Device is already connected.".to_string()),
},
ConnectionFailureReason::PairingFailed => Self {
message: error.to_string(),
retryable: false,
suggestion: Some(
"Pairing failed. Try removing the device and re-pairing.".to_string(),
),
},
ConnectionFailureReason::Other(_) => Self::transient(
error.to_string(),
"Connection failed. Try again or restart the device.",
),
},
crate::Error::NotConnected => Self::transient(
error.to_string(),
"Device disconnected unexpectedly. Reconnecting...",
),
crate::Error::Bluetooth(_) => Self::transient(
error.to_string(),
"Bluetooth error. Try moving closer to the device or restarting Bluetooth.",
),
crate::Error::DeviceNotFound(_) => Self::permanent(error.to_string()),
crate::Error::CharacteristicNotFound { .. } => Self {
message: error.to_string(),
retryable: false,
suggestion: Some(
"This device may have incompatible firmware. Check for updates.".to_string(),
),
},
crate::Error::InvalidData(_)
| crate::Error::InvalidHistoryData { .. }
| crate::Error::InvalidReadingFormat { .. } => Self::permanent(error.to_string()),
crate::Error::Cancelled => Self::permanent("Operation was cancelled.".to_string()),
crate::Error::WriteFailed { .. } => {
Self::transient(error.to_string(), "Failed to write to device. Try again.")
}
crate::Error::Io(_) => {
Self::transient(error.to_string(), "I/O error occurred. Try again.")
}
crate::Error::InvalidConfig(_) | crate::Error::Unsupported(_) => {
Self::permanent(error.to_string())
}
}
}
}
#[derive(Debug, Clone)]
pub enum Command {
LoadCachedData,
Scan {
duration: Duration,
},
Connect {
device_id: String,
},
Disconnect {
device_id: String,
},
RefreshReading {
device_id: String,
},
RefreshAll,
SyncHistory {
device_id: String,
},
SetInterval {
device_id: String,
interval_secs: u16,
},
SetBluetoothRange {
device_id: String,
extended: bool,
},
SetSmartHome {
device_id: String,
enabled: bool,
},
RefreshServiceStatus,
StartServiceCollector,
StopServiceCollector,
SetAlias {
device_id: String,
alias: Option<String>,
},
ForgetDevice {
device_id: String,
},
CancelOperation,
StartBackgroundPolling {
device_id: String,
interval_secs: u64,
},
StopBackgroundPolling {
device_id: String,
},
Shutdown,
InstallSystemService {
user_level: bool,
},
UninstallSystemService {
user_level: bool,
},
StartSystemService {
user_level: bool,
},
StopSystemService {
user_level: bool,
},
CheckSystemServiceStatus {
user_level: bool,
},
FetchServiceConfig,
AddServiceDevice {
address: String,
alias: Option<String>,
poll_interval: u64,
},
UpdateServiceDevice {
address: String,
alias: Option<String>,
poll_interval: u64,
},
RemoveServiceDevice {
address: String,
},
}
#[derive(Debug, Clone)]
pub struct CachedDevice {
pub id: String,
pub name: Option<String>,
pub device_type: Option<DeviceType>,
pub reading: Option<CurrentReading>,
pub last_sync: Option<time::OffsetDateTime>,
}
#[derive(Debug, Clone)]
pub enum SensorEvent {
CachedDataLoaded {
devices: Vec<CachedDevice>,
},
ScanStarted,
ScanComplete {
devices: Vec<DiscoveredDevice>,
},
ScanError {
error: String,
},
DeviceConnecting {
device_id: String,
},
DeviceConnected {
device_id: String,
name: Option<String>,
device_type: Option<DeviceType>,
rssi: Option<i16>,
},
DeviceDisconnected {
device_id: String,
},
ConnectionError {
device_id: String,
error: String,
context: Option<ErrorContext>,
},
ReadingUpdated {
device_id: String,
reading: CurrentReading,
},
ReadingError {
device_id: String,
error: String,
context: Option<ErrorContext>,
},
HistoryLoaded {
device_id: String,
records: Vec<HistoryRecord>,
},
HistorySyncStarted {
device_id: String,
total_records: Option<u16>,
},
HistorySyncProgress {
device_id: String,
downloaded: usize,
total: usize,
},
HistorySynced {
device_id: String,
count: usize,
},
HistorySyncError {
device_id: String,
error: String,
context: Option<ErrorContext>,
},
IntervalChanged {
device_id: String,
interval_secs: u16,
},
IntervalError {
device_id: String,
error: String,
context: Option<ErrorContext>,
},
SettingsLoaded {
device_id: String,
settings: DeviceSettings,
},
BluetoothRangeChanged {
device_id: String,
extended: bool,
},
BluetoothRangeError {
device_id: String,
error: String,
context: Option<ErrorContext>,
},
SmartHomeChanged {
device_id: String,
enabled: bool,
},
SmartHomeError {
device_id: String,
error: String,
context: Option<ErrorContext>,
},
ServiceStatusRefreshed {
reachable: bool,
collector_running: bool,
uptime_seconds: Option<u64>,
devices: Vec<ServiceDeviceStats>,
},
ServiceStatusError {
error: String,
},
ServiceCollectorStarted,
ServiceCollectorStopped,
ServiceCollectorError {
error: String,
},
AliasChanged {
device_id: String,
alias: Option<String>,
},
AliasError {
device_id: String,
error: String,
},
DeviceForgotten {
device_id: String,
},
ForgetDeviceError {
device_id: String,
error: String,
},
OperationCancelled {
operation: String,
},
BackgroundPollingStarted {
device_id: String,
interval_secs: u64,
},
BackgroundPollingStopped {
device_id: String,
},
SignalStrengthUpdate {
device_id: String,
rssi: i16,
quality: SignalQuality,
},
SystemServiceStatus {
installed: bool,
running: bool,
},
SystemServiceInstalled,
SystemServiceUninstalled,
SystemServiceStarted,
SystemServiceStopped,
SystemServiceError {
operation: String,
error: String,
},
ServiceConfigFetched {
devices: Vec<ServiceMonitoredDevice>,
},
ServiceConfigError {
error: String,
},
ServiceDeviceAdded {
device: ServiceMonitoredDevice,
},
ServiceDeviceUpdated {
device: ServiceMonitoredDevice,
},
ServiceDeviceRemoved {
address: String,
},
ServiceDeviceError {
operation: String,
error: String,
},
}
#[derive(Debug, Clone)]
pub struct ServiceMonitoredDevice {
pub address: String,
pub alias: Option<String>,
pub poll_interval: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignalQuality {
Excellent,
Good,
Fair,
Weak,
}
impl SignalQuality {
pub fn from_rssi(rssi: i16) -> Self {
match rssi {
r if r > -50 => SignalQuality::Excellent,
r if r > -70 => SignalQuality::Good,
r if r > -80 => SignalQuality::Fair,
_ => SignalQuality::Weak,
}
}
pub fn description(&self) -> &'static str {
match self {
SignalQuality::Excellent => "Excellent",
SignalQuality::Good => "Good",
SignalQuality::Fair => "Fair",
SignalQuality::Weak => "Weak - move closer",
}
}
}
#[derive(Debug, Clone)]
pub struct ServiceDeviceStats {
pub device_id: String,
pub alias: Option<String>,
pub poll_interval: u64,
pub polling: bool,
pub success_count: u64,
pub failure_count: u64,
pub last_poll_at: Option<time::OffsetDateTime>,
pub last_error: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_command_debug() {
let cmd = Command::Scan {
duration: Duration::from_secs(5),
};
let debug = format!("{:?}", cmd);
assert!(debug.contains("Scan"));
assert!(debug.contains("5"));
}
#[test]
fn test_command_clone() {
let cmd = Command::Connect {
device_id: "test-device".to_string(),
};
let cloned = cmd.clone();
match cloned {
Command::Connect { device_id } => assert_eq!(device_id, "test-device"),
other => panic!("Expected Connect variant, got {:?}", other),
}
}
#[test]
fn test_sensor_event_debug() {
let event = SensorEvent::ScanStarted;
let debug = format!("{:?}", event);
assert!(debug.contains("ScanStarted"));
}
#[test]
fn test_cached_device_default_values() {
let device = CachedDevice {
id: "test".to_string(),
name: None,
device_type: None,
reading: None,
last_sync: None,
};
assert_eq!(device.id, "test");
assert!(device.name.is_none());
}
#[test]
fn test_signal_quality_from_rssi() {
assert_eq!(SignalQuality::from_rssi(-40), SignalQuality::Excellent);
assert_eq!(SignalQuality::from_rssi(-50), SignalQuality::Good);
assert_eq!(SignalQuality::from_rssi(-60), SignalQuality::Good);
assert_eq!(SignalQuality::from_rssi(-70), SignalQuality::Fair);
assert_eq!(SignalQuality::from_rssi(-75), SignalQuality::Fair);
assert_eq!(SignalQuality::from_rssi(-80), SignalQuality::Weak);
assert_eq!(SignalQuality::from_rssi(-90), SignalQuality::Weak);
}
#[test]
fn test_signal_quality_description() {
assert_eq!(SignalQuality::Excellent.description(), "Excellent");
assert_eq!(SignalQuality::Good.description(), "Good");
assert_eq!(SignalQuality::Fair.description(), "Fair");
assert_eq!(SignalQuality::Weak.description(), "Weak - move closer");
}
#[test]
fn test_error_context_permanent() {
let ctx = ErrorContext::permanent("Device not found");
assert!(!ctx.retryable);
assert!(ctx.suggestion.is_none());
assert_eq!(ctx.message, "Device not found");
}
#[test]
fn test_error_context_transient() {
let ctx = ErrorContext::transient("Connection timeout", "Move closer and retry");
assert!(ctx.retryable);
assert_eq!(ctx.suggestion, Some("Move closer and retry".to_string()));
}
}