use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use sonos_api::SonosClient;
use sonos_discovery::{self, Device};
use sonos_event_manager::SonosEventManager;
use sonos_state::{GroupId, SpeakerId, StateManager};
use crate::{Group, SdkError, Speaker};
pub struct SonosSystem {
state_manager: Arc<StateManager>,
_event_manager: Arc<SonosEventManager>,
_api_client: SonosClient,
speakers: RwLock<HashMap<String, Speaker>>,
}
impl SonosSystem {
pub fn new() -> Result<Self, SdkError> {
let devices = sonos_discovery::get();
Self::from_discovered_devices(devices)
}
pub fn from_discovered_devices(devices: Vec<Device>) -> Result<Self, SdkError> {
let event_manager =
Arc::new(SonosEventManager::new().map_err(|e| SdkError::EventManager(e.to_string()))?);
let state_manager = Arc::new(
StateManager::builder()
.with_event_manager(Arc::clone(&event_manager))
.build()
.map_err(SdkError::StateError)?,
);
state_manager
.add_devices(devices.clone())
.map_err(SdkError::StateError)?;
let api_client = SonosClient::new();
let mut speakers = HashMap::new();
for device in devices {
let speaker_id = SpeakerId::new(&device.id);
let ip = device
.ip_address
.parse()
.map_err(|_| SdkError::InvalidIpAddress)?;
let speaker = Speaker::new(
speaker_id,
device.name.clone(),
ip,
device.model_name.clone(),
Arc::clone(&state_manager),
api_client.clone(),
);
speakers.insert(device.name, speaker);
}
Ok(Self {
state_manager,
_event_manager: event_manager,
_api_client: api_client,
speakers: RwLock::new(speakers),
})
}
pub fn get_speaker_by_name(&self, name: &str) -> Option<Speaker> {
self.speakers.read().ok()?.get(name).cloned()
}
pub fn speakers(&self) -> Vec<Speaker> {
self.speakers
.read()
.map(|s| s.values().cloned().collect())
.unwrap_or_default()
}
pub fn get_speaker_by_id(&self, speaker_id: &SpeakerId) -> Option<Speaker> {
let speakers = self.speakers.read().ok()?;
speakers.values().find(|s| s.id == *speaker_id).cloned()
}
pub fn speaker_names(&self) -> Vec<String> {
self.speakers
.read()
.map(|s| s.keys().cloned().collect())
.unwrap_or_default()
}
pub fn state_manager(&self) -> &Arc<StateManager> {
&self.state_manager
}
pub fn iter(&self) -> sonos_state::ChangeIterator {
self.state_manager.iter()
}
pub fn groups(&self) -> Vec<Group> {
self.state_manager
.groups()
.into_iter()
.filter_map(|info| {
Group::from_info(
info,
Arc::clone(&self.state_manager),
self._api_client.clone(),
)
})
.collect()
}
pub fn get_group_by_id(&self, group_id: &GroupId) -> Option<Group> {
let info = self.state_manager.get_group(group_id)?;
Group::from_info(
info,
Arc::clone(&self.state_manager),
self._api_client.clone(),
)
}
pub fn get_group_for_speaker(&self, speaker_id: &SpeakerId) -> Option<Group> {
let info = self.state_manager.get_group_for_speaker(speaker_id)?;
Group::from_info(
info,
Arc::clone(&self.state_manager),
self._api_client.clone(),
)
}
pub fn create_group(
&self,
coordinator: &Speaker,
members: &[&Speaker],
) -> Result<crate::group::GroupChangeResult, SdkError> {
let coord_group = self
.get_group_for_speaker(&coordinator.id)
.ok_or_else(|| SdkError::SpeakerNotFound(coordinator.id.as_str().to_string()))?;
let mut succeeded = Vec::new();
let mut failed = Vec::new();
for member in members {
match coord_group.add_speaker(member) {
Ok(()) => succeeded.push(member.id.clone()),
Err(e) => failed.push((member.id.clone(), e)),
}
}
Ok(crate::group::GroupChangeResult { succeeded, failed })
}
}
#[cfg(test)]
mod tests {
use super::*;
use sonos_state::{GroupInfo, Topology};
fn create_test_system(devices: Vec<Device>) -> Result<SonosSystem, SdkError> {
SonosSystem::from_discovered_devices(devices)
}
#[test]
fn test_groups_returns_all_groups() {
let devices = vec![
Device {
id: "RINCON_111".to_string(),
name: "Living Room".to_string(),
room_name: "Living Room".to_string(),
ip_address: "192.168.1.100".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
},
Device {
id: "RINCON_222".to_string(),
name: "Kitchen".to_string(),
room_name: "Kitchen".to_string(),
ip_address: "192.168.1.101".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
},
];
let system = create_test_system(devices).unwrap();
let speaker1 = SpeakerId::new("RINCON_111");
let speaker2 = SpeakerId::new("RINCON_222");
let group1 = GroupInfo::new(
GroupId::new("RINCON_111:1"),
speaker1.clone(),
vec![speaker1.clone()],
);
let group2 = GroupInfo::new(
GroupId::new("RINCON_222:1"),
speaker2.clone(),
vec![speaker2.clone()],
);
let topology = Topology::new(system.state_manager.speaker_infos(), vec![group1, group2]);
system.state_manager.initialize(topology);
let groups = system.groups();
assert_eq!(groups.len(), 2);
let group_ids: Vec<_> = groups.iter().map(|g| g.id.as_str().to_string()).collect();
assert!(group_ids.contains(&"RINCON_111:1".to_string()));
assert!(group_ids.contains(&"RINCON_222:1".to_string()));
}
#[test]
fn test_groups_returns_empty_when_no_groups() {
let devices = vec![Device {
id: "RINCON_111".to_string(),
name: "Living Room".to_string(),
room_name: "Living Room".to_string(),
ip_address: "192.168.1.100".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
}];
let system = create_test_system(devices).unwrap();
let groups = system.groups();
assert!(groups.is_empty());
}
#[test]
fn test_get_group_by_id_returns_correct_group() {
let devices = vec![Device {
id: "RINCON_111".to_string(),
name: "Living Room".to_string(),
room_name: "Living Room".to_string(),
ip_address: "192.168.1.100".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
}];
let system = create_test_system(devices).unwrap();
let speaker = SpeakerId::new("RINCON_111");
let group_id = GroupId::new("RINCON_111:1");
let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
let topology = Topology::new(system.state_manager.speaker_infos(), vec![group]);
system.state_manager.initialize(topology);
let found = system.get_group_by_id(&group_id);
assert!(found.is_some());
let found = found.unwrap();
assert_eq!(found.id.as_str(), "RINCON_111:1");
assert_eq!(found.coordinator_id.as_str(), "RINCON_111");
assert_eq!(found.member_ids.len(), 1);
}
#[test]
fn test_get_group_by_id_returns_none_for_unknown() {
let devices = vec![Device {
id: "RINCON_111".to_string(),
name: "Living Room".to_string(),
room_name: "Living Room".to_string(),
ip_address: "192.168.1.100".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
}];
let system = create_test_system(devices).unwrap();
let unknown_id = GroupId::new("RINCON_UNKNOWN:1");
let found = system.get_group_by_id(&unknown_id);
assert!(found.is_none());
}
#[test]
fn test_get_group_for_speaker_returns_correct_group() {
let devices = vec![
Device {
id: "RINCON_111".to_string(),
name: "Living Room".to_string(),
room_name: "Living Room".to_string(),
ip_address: "192.168.1.100".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
},
Device {
id: "RINCON_222".to_string(),
name: "Kitchen".to_string(),
room_name: "Kitchen".to_string(),
ip_address: "192.168.1.101".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
},
];
let system = create_test_system(devices).unwrap();
let speaker1 = SpeakerId::new("RINCON_111");
let speaker2 = SpeakerId::new("RINCON_222");
let group = GroupInfo::new(
GroupId::new("RINCON_111:1"),
speaker1.clone(),
vec![speaker1.clone(), speaker2.clone()],
);
let topology = Topology::new(system.state_manager.speaker_infos(), vec![group]);
system.state_manager.initialize(topology);
let found1 = system.get_group_for_speaker(&speaker1);
assert!(found1.is_some());
let found1 = found1.unwrap();
assert_eq!(found1.id.as_str(), "RINCON_111:1");
assert_eq!(found1.member_ids.len(), 2);
let found2 = system.get_group_for_speaker(&speaker2);
assert!(found2.is_some());
let found2 = found2.unwrap();
assert_eq!(found2.id.as_str(), "RINCON_111:1");
assert_eq!(found2.member_ids.len(), 2);
}
#[test]
fn test_get_group_for_speaker_returns_none_for_unknown() {
let devices = vec![Device {
id: "RINCON_111".to_string(),
name: "Living Room".to_string(),
room_name: "Living Room".to_string(),
ip_address: "192.168.1.100".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
}];
let system = create_test_system(devices).unwrap();
let unknown_speaker = SpeakerId::new("RINCON_UNKNOWN");
let found = system.get_group_for_speaker(&unknown_speaker);
assert!(found.is_none());
}
#[test]
fn test_group_methods_consistency() {
let devices = vec![Device {
id: "RINCON_111".to_string(),
name: "Living Room".to_string(),
room_name: "Living Room".to_string(),
ip_address: "192.168.1.100".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
}];
let system = create_test_system(devices).unwrap();
let speaker = SpeakerId::new("RINCON_111");
let group_id = GroupId::new("RINCON_111:1");
let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
let topology = Topology::new(system.state_manager.speaker_infos(), vec![group]);
system.state_manager.initialize(topology);
let groups = system.groups();
assert_eq!(groups.len(), 1);
let by_id = system.get_group_by_id(&group_id);
assert!(by_id.is_some());
let by_speaker = system.get_group_for_speaker(&speaker);
assert!(by_speaker.is_some());
assert_eq!(groups[0].id.as_str(), by_id.as_ref().unwrap().id.as_str());
assert_eq!(
groups[0].id.as_str(),
by_speaker.as_ref().unwrap().id.as_str()
);
assert_eq!(
groups[0].coordinator_id.as_str(),
by_id.as_ref().unwrap().coordinator_id.as_str()
);
assert_eq!(
groups[0].coordinator_id.as_str(),
by_speaker.as_ref().unwrap().coordinator_id.as_str()
);
}
#[test]
fn test_create_group_method_exists() {
fn assert_change_result(_r: Result<crate::group::GroupChangeResult, SdkError>) {}
let devices = vec![
Device {
id: "RINCON_111".to_string(),
name: "Living Room".to_string(),
room_name: "Living Room".to_string(),
ip_address: "192.168.1.100".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
},
Device {
id: "RINCON_222".to_string(),
name: "Kitchen".to_string(),
room_name: "Kitchen".to_string(),
ip_address: "192.168.1.101".to_string(),
port: 1400,
model_name: "Sonos One".to_string(),
},
];
let system = create_test_system(devices).unwrap();
let speaker1 = SpeakerId::new("RINCON_111");
let speaker2 = SpeakerId::new("RINCON_222");
let group = GroupInfo::new(
GroupId::new("RINCON_111:1"),
speaker1.clone(),
vec![speaker1.clone()],
);
let topology = Topology::new(system.state_manager.speaker_infos(), vec![group]);
system.state_manager.initialize(topology);
let coordinator = system.get_speaker_by_id(&speaker1).unwrap();
let member = system.get_speaker_by_id(&speaker2).unwrap();
assert_change_result(system.create_group(&coordinator, &[&member]));
}
}