CLI, library and apps (planned) for controlling KEFs W2 platform based speakers over the network.
Grab a version for your OS from the releases page.
Install with Homebrew:
brew install hilli/tap/kefw2Install with Homebrew:
brew install hilli/tap/kefw2Install with Scoop
In a Windows Powershell window:
# Install Scoop if it's not already on your system
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
scoop install git
# Add repo for kefw2
scoop bucket add hilli https://github.com/hilli/scoop-bucket.git
# Install kefw2
scoop install hilli/go-kef-w2Note that speaker discovery might not work in Windows.
Setup the speakers
# Auto discovery
kefw2 config speaker discover --save
# Manually add a speaker
kefw2 config speaker add 10.0.0.149If you only have one set of speakers, then that will be the default, otherwise configure that with
kefw2 config speaker default <name or IP>If you want to control a speaker that is not the default use the -s global flag. Example:
kefw2 -s 10.0.0.93 statusGet status of the default speaker
kefw2 statusGet detailed info
kefw2 infoGet volume
kefw2 volume
# or
kefw2 volSet volume, 35%
kefw2 vol 35Skip to next track if in wifi mode
kefw2 nextSelect source
kefw2 source wifi
# or just display current source
kefw2 sourcePlay and pause in wifi mode
kefw2 play
kefw2 pauseTurn the speakers off
kefw2 offBackup the current EQ Profile
kefw2 config eq_profile > my_profile.jsonSet the max volume limit
kefw2 config maxvol 65All with tab completion available of the options, where applicable.
Play internet radio stations via KEF's Airable integration:
# Browse favorite stations
kefw2 radio favorites
# Play a station (with tab completion)
kefw2 radio play "BBC Radio 1"
# Browse by category
kefw2 radio popular
kefw2 radio local
kefw2 radio trending
kefw2 radio hq
kefw2 radio new
# Search for stations
kefw2 radio search "jazz"
# Interactive browser
kefw2 radio browseBrowse and play podcasts:
# Browse favorite podcasts
kefw2 podcast favorites
# Play an episode (with tab completion - use "Show/Episode" format)
kefw2 podcast play "The Daily/Latest Episode"
# Browse by category
kefw2 podcast popular
kefw2 podcast trending
kefw2 podcast history
# Search for podcasts
kefw2 podcast search "technology"
# Interactive browser
kefw2 podcast browsePlay music from local network media servers:
# Configure default UPnP server
kefw2 config upnp server default "Plex Media Server: homesrv"
# Browse a server interactively
kefw2 upnp browse
# Browse a specific path (with tab completion)
kefw2 upnp browse "Music/Albums"
# Play media from server
kefw2 upnp play "Music/Jazz/Album/Track.flac"Search your entire music library instantly with a local index:
# Search for tracks by title, artist, or album
kefw2 upnp search "beatles"
kefw2 upnp search "abbey road"
kefw2 upnp search "come together beatles"
# Add search result to queue instead of playing
kefw2 upnp search -q "bohemian"The search feature builds a local index of your UPnP music library for instant results. The index is cached and automatically refreshes after 24 hours.
For large libraries (like Plex), you can configure which folder to index to avoid scanning duplicate views (By Genre, By Album, etc. contain the same tracks):
# Set the container path to index (with tab completion)
kefw2 config upnp index container "Music/Hilli's Music/By Folder"
# Show current index status
kefw2 upnp index
# Rebuild the index (uses configured container automatically)
kefw2 upnp index --rebuild
# Override container for a one-time rebuild
kefw2 upnp index --rebuild --container "Music/My Library/By Album"
# Clear the container setting (index entire server)
kefw2 config upnp index container ""The container path uses / as separator and supports tab completion at each level.
Manage the playback queue:
# Show current queue
kefw2 queue list
# Add items to queue
kefw2 queue add "Radio Station Name"
# Clear the queue
kefw2 queue clear
# Save current queue as a preset
kefw2 queue save "My Playlist"
# Load a saved queue
kefw2 queue load "My Playlist"
# Set playback mode
kefw2 queue mode shuffle
kefw2 queue mode repeatAdd and manage favorites:
# Add current playing item to favorites
kefw2 radio favorites add
kefw2 podcast favorites add
# Add a specific item to favorites
kefw2 radio favorites add "BBC Radio 1"
kefw2 podcast favorites add "The Daily"
# Remove from favorites
kefw2 radio favorites remove "BBC Radio 1"Configure caching for faster tab completion:
# Show all cache settings
kefw2 config cache
# Show specific setting
kefw2 config cache ttl-radio
# Enable/disable caching
kefw2 config cache enable
kefw2 config cache disable
# Set TTL per service (in seconds)
kefw2 config cache ttl-radio 600 # 10 minutes
kefw2 config cache ttl-podcast 1800 # 30 minutes
kefw2 config cache ttl-upnp 120 # 2 minutes
kefw2 config cache ttl-default 300 # 5 minutes (for future services)
# Clear cache
kefw2 cache clear
# View cache status
kefw2 cache statusTrack the event from the KEFs
kefw2 event
# Or as JSON
kefw2 events --json- Set volume
- Mute/unmute
- Select source
- Get status
- Turn on/off
- Track next/previous
- Discover speakers automatically
- Display cover art in ASCII (wifi media)
- Backup speaker settings/eq profiles to file
- Play Internet Radio
- Play Podcasts
- Play from UPnP/DLNA media servers
- Queue management
- UPnP library search with local indexing
- Restore speaker settings/eq profiles from file
- Play titles from built-in music streaming services (Amazon Music, Deezer, Qobuz, Spotify, Tidal)
go get github.com/hilli/go-kef-w2/kefw2package main
import (
"context"
"fmt"
"log"
"time"
"github.com/hilli/go-kef-w2/kefw2"
)
func main() {
// Create a speaker connection
speaker, err := kefw2.NewSpeaker("10.0.0.93")
if err != nil {
log.Fatal(err)
}
// Print speaker info
fmt.Printf("Name: %s\n", speaker.Name)
fmt.Printf("Model: %s\n", speaker.Model)
fmt.Printf("Firmware: %s\n", speaker.FirmwareVersion)
fmt.Printf("MAC: %s\n", speaker.MacAddress)
ctx := context.Background()
// Control volume
volume, _ := speaker.GetVolume(ctx)
fmt.Printf("Volume: %d\n", volume)
speaker.SetVolume(ctx, 30)
// Change source
speaker.SetSource(ctx, kefw2.SourceWiFi)
}// With custom timeout
speaker, err := kefw2.NewSpeaker("10.0.0.93",
kefw2.WithTimeout(5*time.Second),
)
// With custom HTTP client
customClient := &http.Client{Timeout: 10 * time.Second}
speaker, err := kefw2.NewSpeaker("10.0.0.93",
kefw2.WithHTTPClient(customClient),
)All operations have context support for cancellation and timeout control:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
volume, err := speaker.GetVolume(ctx)
if err != nil {
log.Fatal(err)
}ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
speakers, err := kefw2.DiscoverSpeakers(ctx, 5*time.Second)
if err != nil {
log.Fatal(err)
}
for _, s := range speakers {
fmt.Printf("Found: %s at %s\n", s.Name, s.IPAddress)
}Subscribe to real-time speaker events:
client, err := speaker.NewEventClient()
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go client.Start(ctx)
for event := range client.Events() {
switch e := event.(type) {
case *kefw2.VolumeEvent:
fmt.Printf("Volume changed: %d\n", e.Volume)
case *kefw2.SourceEvent:
fmt.Printf("Source changed: %s\n", e.Source)
case *kefw2.PlayerDataEvent:
fmt.Printf("Now playing: %s - %s\n", e.Artist, e.Title)
}
}ctx := context.Background()
// Create Airable client for streaming services
client := kefw2.NewAirableClient(speaker)
// Get radio favorites
favorites, err := client.GetRadioFavorites(ctx)
for _, station := range favorites {
fmt.Printf("Station: %s\n", station.Title)
}
// Play a radio station
err = client.PlayRadioStation(ctx, stationPath)
// Get podcast episodes
episodes, err := client.GetPodcastEpisodes(ctx, podcastPath)
// Browse UPnP servers
servers, err := client.GetMediaServers(ctx)MIT License
