| name | implement-service-sdk |
| description | Expose service properties through the DOM-like sonos-sdk API. Implements Fetchable trait, type aliases, and Speaker struct fields. Use after implementing the service in sonos-state. |
Implement Service SDK Layer
Overview
This skill exposes service properties through the DOM-like sonos-sdk API. It handles:
- Fetchable Trait - Implement for properties that can be fetched via API calls
- Type Aliases - Create convenient handle type aliases
- Speaker Fields - Add property handles to the Speaker struct
Prerequisites
- Service implemented in sonos-api (operations)
- Service implemented in sonos-stream (event types)
- Service implemented in sonos-state (properties)
- Understanding of which properties can be fetched (have dedicated API operations)
Quick Start
python .claude/skills/implement-service-sdk/scripts/analyze_handles.py --list
python .claude/skills/implement-service-sdk/scripts/analyze_handles.py --coverage
python .claude/skills/implement-service-sdk/scripts/test_sdk_property.py <speaker_ip> volume --get
Workflow
Step 1: Determine Fetchability
Not all properties can be fetched directly. Check if your property has a dedicated UPnP operation:
| Property Can Be Fetched | If It Has |
|---|
| Yes | Dedicated Get/GetXxx operation in sonos-api |
| No | Only available via events (LastChange) |
Fetchable properties (examples):
- Volume - has
GetVolumeOperation
- PlaybackState - has
GetTransportInfoOperation
- Position - has
GetPositionInfoOperation
Non-fetchable properties (event-only):
- Mute, Bass, Treble, Loudness - from RenderingControl events
- CurrentTrack - from AVTransport events
- GroupMembership - from ZoneGroupTopology events
Step 2: Implement Fetchable Trait (if applicable)
Add to sonos-sdk/src/property/handles.rs:
use sonos_api::services::new_service::{
self, GetNewPropertyOperation, GetNewPropertyResponse,
};
use sonos_state::NewProperty;
impl Fetchable for NewProperty {
type Operation = GetNewPropertyOperation;
fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
new_service::get_new_property_operation()
.build()
.map_err(|e| build_error("GetNewProperty", e))
}
fn from_response(response: GetNewPropertyResponse) -> Self {
NewProperty::new(response.value_field)
}
}
Step 3: Add Type Alias
Add to the type aliases section in sonos-sdk/src/property/handles.rs:
pub type NewPropertyHandle = PropertyHandle<NewProperty>;
Step 4: Add Imports
Update imports in sonos-sdk/src/property/handles.rs:
use sonos_api::services::{
new_service::{self, GetNewPropertyOperation, GetNewPropertyResponse},
};
use sonos_state::{
NewProperty,
};
Step 5: Add to Speaker Struct
Update sonos-sdk/src/speaker.rs:
use crate::property::{
NewPropertyHandle,
};
pub struct Speaker {
pub new_property: NewPropertyHandle,
}
Step 6: Initialize in Speaker::new()
Update the Speaker::new() constructor:
impl Speaker {
pub fn new() -> Self {
let context = SpeakerContext::new(id.clone(), ip, state_manager, api_client);
Self {
new_property: PropertyHandle::new(Arc::clone(&context)),
}
}
}
Step 7: Re-export (if needed)
Update sonos-sdk/src/lib.rs to export new types:
pub use property::{
NewPropertyHandle,
};
Step 8: Add Tests
Add tests in sonos-sdk/src/property/handles.rs:
#[cfg(test)]
mod tests {
#[test]
fn test_new_property_handle_creation() {
let state_manager = create_test_state_manager();
let context = create_test_context(state_manager);
let handle: NewPropertyHandle = PropertyHandle::new(context);
assert_eq!(handle.speaker_id().as_str(), "RINCON_TEST123");
}
#[test]
fn test_new_property_get_cached() {
let state_manager = create_test_state_manager();
let speaker_id = SpeakerId::new("RINCON_TEST123");
state_manager.set_property(&speaker_id, NewProperty::new(42));
let context = create_test_context(Arc::clone(&state_manager));
let handle: NewPropertyHandle = PropertyHandle::new(context);
assert_eq!(handle.get(), Some(NewProperty::new(42)));
}
}
Step 9: Verify
cargo test -p sonos-sdk
python .claude/skills/implement-service-sdk/scripts/analyze_handles.py --coverage
Files Modified
| File | Changes |
|---|
sonos-sdk/src/property/handles.rs | Add Fetchable impl (if applicable), type alias, imports |
sonos-sdk/src/speaker.rs | Add field to Speaker struct, initialize in new() |
sonos-sdk/src/lib.rs | Re-export new types (optional) |
Fetchable Implementation Guidelines
Build Operation Pattern
fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
service_module::operation_function()
.build()
.map_err(|e| build_error("OperationName", e))
}
From Response Pattern
fn from_response(response: OperationResponse) -> Self {
PropertyType::new(response.relevant_field)
}
With Parsing
fn from_response(response: GetPositionInfoResponse) -> Self {
let position_ms = Position::parse_time_to_ms(&response.rel_time).unwrap_or(0);
let duration_ms = Position::parse_time_to_ms(&response.track_duration).unwrap_or(0);
Position::new(position_ms, duration_ms)
}
With Enum Mapping
fn from_response(response: GetTransportInfoResponse) -> Self {
match response.current_transport_state.as_str() {
"PLAYING" => PlaybackState::Playing,
"PAUSED" | "PAUSED_PLAYBACK" => PlaybackState::Paused,
"STOPPED" => PlaybackState::Stopped,
_ => PlaybackState::Transitioning,
}
}
Common Issues
Missing Fetchable Implementation
If fetch() is not available, the property is event-only. Document this in the handles.rs comments.
Type Mismatch in from_response
Ensure the response field type matches what the property constructor expects. Use parsing if needed.
Speaker Field Not Working
Verify:
- Type alias is defined
- Import is added to speaker.rs
- Field is initialized with
PropertyHandle::new(Arc::clone(&context))
References