Python library for controlling KEF wireless speakers: LS50 Wireless II, LS60 Wireless, LSX II, LSX II LT, and XIO Soundbar
π οΈ For the Home Assistant integration, please see hass-kef-connector
This library works with the KEF LS50 Wireless II, LSX II and LS60 only. If you are searching for a library for the first generation LS50W or LSX, you can use aiokef.
Pykefcontrol has 2 main components: KefConnector and KefAsyncConnector. The first one can be used in all classic scripts and python programs, whereas the second one (KefAsyncConnector) can be used in asynchronous programs.
All KEF W2 platform speakers with network connectivity (WiFi/Ethernet) are supported:
| Model | Type | Physical Inputs | Features | Tested |
|---|---|---|---|---|
| LS50 Wireless II | Bookshelf | WiFi, BT, Optical, Coaxial, Analogue, HDMI | DSP, EQ, Sub out, HDMI eARC, MAT | |
| LS60 Wireless | Floorstanding | WiFi, BT, Optical, Coaxial, Analogue, HDMI | DSP, EQ, Sub out, MAT | |
| LSX II | Compact bookshelf | WiFi, BT, Optical, USB, Analogue, HDMI | DSP, EQ, Sub out | β Tested |
| LSX II LT | Compact bookshelf | WiFi, BT, Optical, USB, HDMI | DSP, EQ, Sub out | β Tested |
| XIO Soundbar | Soundbar (5.1.2) | WiFi, BT, Optical, HDMI eARC | DSP, EQ, Dolby Atmos, DTS:X, Sound profiles, Dialogue mode | β Tested |
Incompatible Models:
- Coda W, Muo - Bluetooth-only, no network API
- LS50 Wireless (Gen 1), LSX (Gen 1) - Use aiokef instead
β API Discovery 100% COMPLETE - All 209 KEF API endpoints discovered! π
Using full JADX decompilation of KEF Connect v1.26.1 APK, we have discovered ALL 209 API endpoints from the definitive source code (ApiPath.java). This represents 89 new endpoints beyond the previous 120 documented.
API Discovery Breakdown (209 total endpoints):
- π§ 124 Settings paths (
settings:/) - Speaker configuration - βοΈ 37 KEF Operations (
kef:) - System operations βΆοΈ 5 Player Control (player:) - Playback control (NEW)- π 3 Power Management (
powermanager:) - Standby/reboot (NEW) - β° 10 Alerts & Timers (
alerts:/) - Alarms and timers (NEW) - π± 4 Bluetooth (
bluetooth:) - BT device management (NEW) - π 3 Firmware Updates (
firmwareupdate:) - Update checking (NEW) - π‘ 5 Google Cast (
googlecast:) - Cast configuration (NEW) - π 7 Network (
network:/networkwizard:) - WiFi management - π 2 Grouping (
grouping:) - Multi-room grouping (NEW) - π 3 Notifications (
notifications:/) - UI notifications (NEW) - ποΈ 6 Other - XIO-specific and legacy endpoints (NEW)
Currently Implemented (v0.9 - 188 methods):
- β 46 core methods - Power, volume, source control, playback, queuing
- β 36 DSP/EQ methods - Complete DSP control
- β 10 subwoofer methods - Enable, gain, preset, low-pass, polarity, stereo
- β 14 XIO methods - Sound profiles, calibration, BLE firmware
- β 57 system methods - Volume management, network diagnostics, system behavior, LED/remote control, device info, privacy
- β 25 NEW methods - Bluetooth (4), Alerts/Timers (13), Grouping (2), Notifications (3), Google Cast (3)
Remaining (~5 endpoints - mostly redundant):
βΆοΈ Player Control (5 methods) - Already covered via polling/playback methods- π Power Management (3 methods) - Already covered via power_on/shutdown
- ποΈ Additional XIO features (6 methods)
See apk_analysis.md for complete API documentation with all 209 endpoints cataloged.
To install pykefcontrol, you can use pip:
pip install pykefcontrolYou can make sure you have the latest version by typing:
>>> print(pykefcontrol.__version__)Currently, the latest version is version 0.8
To use the pykefcontrol library, you need to know the IP address of your speakers. To do so, you can have a look at your router web page, or check in the KEF Connect app by doing the following:
- Launch the KEF Connect app
- Tap the gear icon on the bottom right
- Then your speaker name (It should be right below your profile information)
- Finally, the little circled "i" next to your speaker name in the My Speakers section
- You should find your IP address in the "IP address" section under the form
www.xxx.yyy.zzz, wherewww,xxx,yyyandzzzare integers between0and255.
Once pykefcontrol is installed and you have your KEF Speaker IP address, you can use pykefcontrol in the following way:
First, import the class and create a KefConnector object:
from pykefcontrol.kef_connector import KefConnector
my_speaker = KefConnector("www.xxx.yyy.zzz")www.xxx.yyy.zzz with your speaker IP address. You should give your IP address as a string. It's to say that you should leave the quotation marks " around the IP address
Once the my_speaker object is created, you can use it in the following ways:
Power, Shutdown and Status
# Power on
my_speaker.power_on()
# Shutdown
my_speaker.shutdown()
# Get speaker status : it returns a string ('powerOn' or 'standby')
my_speaker.status # it is not a method so it does not requires parenthesis
# (output example) >>> 'powerOn'Speaker Info
# Get the speaker MAC hardress
my_speaker.mac_address
# (output example) >>> 'AB:CD:EF:12:13:45'
# Get the speaker friendly name if configured
my_speaker.speaker_name
# (output example) >>> 'My Kef LS50W2'
# Get the speaker model
my_speaker.speaker_model
# (output example) >>> 'LS50WII'
# Get the firmware version
my_speaker.firmware_version
# (output example) >>> 'V27100'Source Control
# Get currently selected source : it returns a string ('wifi', 'bluetooth', 'tv', 'optical', 'coaxial', 'analog')
# If the speaker is not powered on, it will return 'standby'
my_speaker.source # it is not a method so it does not requires parenthesis
# (output example) >>> 'wifi'
# Set the input source
# If the speaker is shutdown, setting a source will power it on
my_speaker.source = 'wifi' # 'wifi is an example, you can use any other supported string 'tv', 'analog', etc..Control playback
# Toggle play/pause
my_speaker.toggle_play_pause()
# Next track
my_speaker.next_track()
# Previous track
my_speaker.previous_track()Control volume
# Muste speaker
my_speaker.mute()
# Unmute speaker
my_speaker.unmute()
# Get volume : it reruns an integer between 0 and 100
my_speaker.volume # it is not a method so it does not require parenthesis
# (output example) >>> 23
# Set volume
my_speaker.volume = 42 # 42 for example but it can by any integer between 0 and 100.
# Set volume (alternative way)
my_speaker.set_volume(42) # 42 for example but it can by any integer between 0 and 100.Playback info
# Check if the speaker is playing : it returns a boolean (either True or False)
my_speaker.is_playing # it is not a method so it does not require parenthesis
# (output example) >>> True
# Get current media information : it retuns a dictionnary
# (works on songs/podcast/radio. It may works on other media but I have not tested it yet)
my_speaker.get_song_information()
# (output example) >>> {'title':'Money','artist':'Pink Floyd','album':'The Dark Side of the Moon','cover_url':'http://cover.url'}
# Get media length in miliseconds : it returns an integer representing the song length in ms
my_speaker.song_length # it is not a method so it does not require parenthesis
# (output example) >>> 300251
# Get song progress : it returns the current playhead position in the current track in ms
my_speaker.song_status # it is not a method so it does not require parenthesis
# (output example) >>> 136900DSP / EQ Profile Control
Pykefcontrol provides comprehensive control over the speaker's DSP (Digital Signal Processing) and EQ (Equalizer) settings. These settings allow you to customize the audio output to match your room acoustics and personal preferences.
# Get complete EQ profile
profile = my_speaker.get_eq_profile()
print(profile['kefEqProfileV2']['profileName']) # e.g., "Expert" or "Kantoor"
print(profile['kefEqProfileV2']['isExpertMode']) # True/False
# Profile Name Management
name = my_speaker.get_profile_name() # Get current profile name
profile_id = my_speaker.get_profile_id() # Get unique profile ID (UUID)
my_speaker.rename_profile("Living Room") # Rename current profile
# Note: KEF speakers do not support deleting profiles via API
# The profile ID remains the same when renaming
# Desk Mode - compensates for speaker placement on a desk
# Note: Only available on bookshelf speakers (LSX II, LSX II LT, LS50 Wireless II)
# Not available on LS60 (floorstanding) or XIO (soundbar)
# Returns (enabled: bool, db_value: float)
enabled, db = my_speaker.get_desk_mode()
print(f"Desk mode: {enabled}, attenuation: {db} dB")
# Enable desk mode with -3dB attenuation (range: -10.0 to 0.0 dB)
my_speaker.set_desk_mode(enabled=True, db_value=-3.0)
# Disable desk mode
my_speaker.set_desk_mode(enabled=False)
# Wall Mode - compensates for speaker placement near walls
# Note: Only available on bookshelf speakers (LSX II, LSX II LT, LS50 Wireless II)
# Not available on LS60 (floorstanding) or XIO (soundbar - use wall_mounted instead)
enabled, db = my_speaker.get_wall_mode()
my_speaker.set_wall_mode(enabled=True, db_value=-2.0) # -10.0 to 0.0 dB
# Bass Extension - controls low-frequency extension
mode = my_speaker.get_bass_extension() # Returns: "standard", "less", or "extra"
my_speaker.set_bass_extension("extra") # Options: "standard", "less", "extra"
# Treble Amount - adjust high-frequency balance
treble_db = my_speaker.get_treble_amount() # Returns float in dB
my_speaker.set_treble_amount(1.5) # Range: -3.0 to +3.0 dB
# Balance - adjust left/right balance
balance = my_speaker.get_balance() # Returns float
my_speaker.set_balance(0.0) # Range: -6.0 (left) to +6.0 (right), 0=center
# Phase Correction - enable/disable phase correction
phase = my_speaker.get_phase_correction() # Returns bool
my_speaker.set_phase_correction(True)
# Advanced: Set complete EQ profile
profile = my_speaker.get_eq_profile()
profile['kefEqProfileV2']['deskMode'] = True
profile['kefEqProfileV2']['deskModeSetting'] = -3.0
profile['kefEqProfileV2']['trebleAmount'] = 1.5
my_speaker.set_eq_profile(profile)
# Advanced: Update single DSP setting
my_speaker.update_dsp_setting('trebleAmount', 1.5)
my_speaker.update_dsp_setting('phaseCorrection', True)Subwoofer Control (for speakers with subwoofer output)
# Check if subwoofer is enabled
enabled = my_speaker.get_subwoofer_enabled() # Returns bool
# Enable/disable subwoofer output
my_speaker.set_subwoofer_enabled(True)
# Subwoofer gain control
gain = my_speaker.get_subwoofer_gain() # Returns float in dB
my_speaker.set_subwoofer_gain(5.0) # Range: -10.0 to +10.0 dB
# Subwoofer preset (auto-configuration for KEF subwoofers)
preset = my_speaker.get_subwoofer_preset() # Returns str
my_speaker.set_subwoofer_preset('kube8b') # Options: "custom", "kube8b", "kc62",
# "kf92", "kube10b", "kube12b", "kube15", "t2"
# Subwoofer low-pass filter (crossover frequency)
freq = my_speaker.get_subwoofer_lowpass() # Returns float in Hz
my_speaker.set_subwoofer_lowpass(80.0) # Range: 40.0 to 250.0 Hz
# Subwoofer polarity
polarity = my_speaker.get_subwoofer_polarity() # Returns "normal" or "inverted"
my_speaker.set_subwoofer_polarity('normal')
# Subwoofer stereo mode
# Note: API field exists but has no audible effect on current firmware
stereo = my_speaker.get_subwoofer_stereo() # Returns bool
my_speaker.set_subwoofer_stereo(False) # False=mono, True=stereo (no effect)
# KW1 Wireless Subwoofer Adapter
# The KW1 is KEF's wireless subwoofer adapter for all W2 platform speakers
kw1_enabled = my_speaker.get_kw1_enabled() # Returns bool
my_speaker.set_kw1_enabled(True) # Enable KW1 wireless adapter
# Note: KW2 (built-in wireless module in XIO) cannot be controlled via HTTP API.
# KW2 uses Bluetooth Low Energy pairing which is handled by the KEF Connect app.
# High-pass filter for main speakers (use with subwoofer)
# Returns (enabled: bool, freq_hz: float)
enabled, freq = my_speaker.get_high_pass_filter()
my_speaker.set_high_pass_filter(enabled=True, freq_hz=80.0) # Range: 50.0 to 120.0 Hz
# Audio polarity for main speakers
polarity = my_speaker.get_audio_polarity() # Returns "normal" or "inverted"
my_speaker.set_audio_polarity('normal')For the async version (KefAsyncConnector), all DSP methods work identically but require await:
# Async examples - Basic DSP
profile = await my_speaker.get_eq_profile()
enabled, db = await my_speaker.get_desk_mode()
await my_speaker.set_desk_mode(enabled=True, db_value=-3.0)
await my_speaker.set_bass_extension("extra")
await my_speaker.set_treble_amount(1.5)
# Async examples - Subwoofer control
gain = await my_speaker.get_subwoofer_gain()
await my_speaker.set_subwoofer_gain(5.0)
await my_speaker.set_subwoofer_preset('kube8b')
await my_speaker.set_subwoofer_lowpass(80.0)
enabled, freq = await my_speaker.get_high_pass_filter()
await my_speaker.set_high_pass_filter(enabled=True, freq_hz=80.0)
# Async examples - KW1 wireless adapter
kw1_enabled = await my_speaker.get_kw1_enabled()
await my_speaker.set_kw1_enabled(True)Save, load, and manage custom EQ profiles. Profiles are stored as JSON files and can be shared across speakers or backed up.
Profile Storage (Standalone/CLI Usage):
- Profiles are saved to
~/.kef_profiles/(or custom directory viaprofile_dirparameter) - Each profile includes all DSP/EQ settings, metadata, and timestamps
- Profiles can be exported/imported as JSON files for sharing or backup
- Not used by Home Assistant integration (HA uses
.storage/instead)
# Save current speaker settings as a named profile
my_speaker.save_eq_profile("Movie Night", "Extra bass for movies")
# List all saved profiles
profiles = my_speaker.list_eq_profiles()
for profile in profiles:
print(f"{profile['name']}: {profile['description']}")
print(f" Created: {profile['created_at']}")
print(f" Modified: {profile['modified_at']}")
# Load a saved profile (applies to speaker immediately)
my_speaker.load_eq_profile("Movie Night")
# Check if a profile exists
if my_speaker.profile_exists("Music Mode"):
my_speaker.load_eq_profile("Music Mode")
# Rename a profile
my_speaker.rename_eq_profile("Old Name", "New Name")
# Delete a profile
my_speaker.delete_eq_profile("Unused Profile")
# Get profile count
count = my_speaker.get_profile_count()
print(f"You have {count} saved profiles")
# Export profile for backup or sharing
my_speaker.export_eq_profile("Movie Night", "/backup/movie_profile.json")
# Import profile from file
profile_name = my_speaker.import_eq_profile("/backup/movie_profile.json", "Imported Profile")
my_speaker.load_eq_profile(profile_name)
# Example workflow: Create custom profiles for different use cases
# 1. Adjust settings for movies
my_speaker.set_bass_extension("extra")
my_speaker.set_treble_amount(0.5)
my_speaker.set_subwoofer_gain(3.0)
my_speaker.save_eq_profile("Movies", "Extra bass for cinema experience")
# 2. Adjust settings for music
my_speaker.set_bass_extension("standard")
my_speaker.set_treble_amount(1.0)
my_speaker.set_subwoofer_gain(0.0)
my_speaker.save_eq_profile("Music", "Balanced for music listening")
# 3. Switch between profiles easily
my_speaker.load_eq_profile("Movies") # Watch a movie
# ... later ...
my_speaker.load_eq_profile("Music") # Listen to musicAsync version - Only save_eq_profile() and load_eq_profile() are async (they interact with the speaker). All other profile management methods are synchronous (they only access local files):
# Async methods (interact with speaker)
await my_speaker.save_eq_profile("Movie Night", "Extra bass")
await my_speaker.load_eq_profile("Movie Night")
# Sync methods (local file operations - no await needed)
profiles = my_speaker.list_eq_profiles()
my_speaker.delete_eq_profile("Old Profile")
my_speaker.rename_eq_profile("Old", "New")
my_speaker.export_eq_profile("Profile", "/backup/profile.json")
profile_name = my_speaker.import_eq_profile("/backup/profile.json")Custom Profile Directory:
# Specify custom profile storage location
my_speaker = KefConnector('192.168.1.100', profile_dir='/custom/path/profiles')
# Or use default: ~/.kef_profiles/ or /config/.kef_profiles/ (Home Assistant)
my_speaker = KefConnector('192.168.1.100')Control per-input default volumes and volume behavior settings. Each physical input (WiFi, Bluetooth, Optical, etc.) can have its own default volume level, or you can use a global volume for all inputs.
Set different default volumes for each input source:
import pykefcontrol as pkf
speaker = pkf.KefConnector('192.168.1.100')
# Get default volume for a specific input
wifi_volume = speaker.get_default_volume('wifi') # Returns 0-100
bluetooth_volume = speaker.get_default_volume('bluetooth')
# Set default volume for specific inputs
speaker.set_default_volume('wifi', 50) # Set WiFi to 50%
speaker.set_default_volume('bluetooth', 40) # Set Bluetooth to 40%
speaker.set_default_volume('optical', 60) # Set Optical to 60%
# Get all default volumes at once
all_volumes = speaker.get_all_default_volumes()
# Returns: {'global': 30, 'wifi': 50, 'bluetooth': 40, 'optical': 60, ...}
# Print all volumes
for source, volume in sorted(all_volumes.items()):
print(f"{source:12s}: {volume}%")Available input sources by model:
- LSX II: wifi, bluetooth, optical, usb, analogue, tv (6 inputs)
- LSX II LT: wifi, bluetooth, optical, usb, tv (5 inputs)
- LS50 Wireless II: wifi, bluetooth, optical, coaxial, analogue, tv (6 inputs)
- LS60 Wireless: wifi, bluetooth, optical, coaxial, analogue, tv (6 inputs)
- XIO Soundbar: wifi, bluetooth, optical, tv (4 inputs)
Configure global volume limits and behavior:
# Get current volume settings
settings = speaker.get_volume_settings()
# Returns: {'max_volume': 100, 'step': 1, 'limit': 100, 'display': 'linear'}
# Set maximum volume limit (safety feature for children/hearing protection)
speaker.set_volume_settings(max_volume=80) # Limit to 80%
# Set volume step size (how much volume changes per button press)
speaker.set_volume_settings(step=2) # Change by 2% per step
# Set volume limiter
speaker.set_volume_settings(limit=75) # Soft limit at 75%
# Combine multiple settings
speaker.set_volume_settings(max_volume=85, step=2, limit=80)The "Reset Volume" feature (called "Startup Volume" in some contexts) controls what volume the speaker uses when waking from standby. This matches the KEF Connect app's "Reset volume" setting.
# Check if reset volume is enabled
is_enabled = speaker.get_startup_volume_enabled()
# Returns: True = enabled, False = disabled (resumes at last volume)
# Enable reset volume feature
speaker.set_startup_volume_enabled(True)
# Disable reset volume (speaker resumes at last volume level)
speaker.set_startup_volume_enabled(False)When reset volume is enabled, choose between "All Sources" (global) or "Individual Sources" (per-input) mode:
# Check current mode
is_all_sources = speaker.get_standby_volume_behavior()
# Returns: True = All Sources, False = Individual Sources
# Set to "All Sources" mode (same reset volume for all inputs)
speaker.set_standby_volume_behavior(True)
# Set to "Individual Sources" mode (different reset volume per input)
speaker.set_standby_volume_behavior(False)How it works:
- All Sources (True): All inputs use the same reset volume (set via
defaultVolumeGlobal) - Individual Sources (False): Each input has its own reset volume (WiFi, Bluetooth, etc.)
All volume management methods support async:
import asyncio
import pykefcontrol as pkf
async def manage_volumes():
speaker = pkf.KefAsyncConnector('192.168.1.100')
# Get all volumes
volumes = await speaker.get_all_default_volumes()
# Set specific input volumes
await speaker.set_default_volume('wifi', 45)
await speaker.set_default_volume('bluetooth', 35)
# Configure volume settings
await speaker.set_volume_settings(max_volume=80, step=2)
# Enable reset volume with Individual Sources mode
await speaker.set_standby_volume_behavior(False) # Individual Sources
await speaker.set_startup_volume_enabled(True) # Enable reset volume
asyncio.run(manage_volumes())Monitor network connectivity, stability, and perform speed tests. Useful for troubleshooting streaming issues or verifying network performance.
Test if the speaker can reach the internet:
import pykefcontrol as pkf
speaker = pkf.KefConnector('192.168.1.100')
# Ping internet
ping_ms = speaker.ping_internet()
if ping_ms > 0:
print(f"Internet connected: {ping_ms}ms ping")
else:
print("No internet connection")
# Check network stability
stability = speaker.get_network_stability()
print(f"Network stability: {stability}") # Returns 'idle', 'stable', or 'unstable'Run a complete speed test to measure download speeds and packet loss:
import time
# Start speed test
speaker.start_speed_test()
print("Speed test started...")
# Monitor progress
while True:
status = speaker.get_speed_test_status()
print(f"Status: {status}")
if status == 'complete':
break
elif status == 'idle':
print("Speed test failed to start")
break
time.sleep(2)
# Get results when complete
results = speaker.get_speed_test_results()
print(f"Average download: {results['avg_download']} Mbps")
print(f"Current download: {results['current_download']} Mbps")
print(f"Packet loss: {results['packet_loss']}%")
# Stop test if needed
# speaker.stop_speed_test()The get_speed_test_results() method returns a dictionary with:
avg_download: Average download speed in Mbpscurrent_download: Current download speed in Mbpspacket_loss: Packet loss percentage
Note: Speed test results are only meaningful when status is 'running' or 'complete'. When idle, all values return 0.
All network diagnostic methods support async:
import asyncio
import pykefcontrol as pkf
async def check_network():
speaker = pkf.KefAsyncConnector('192.168.1.100')
# Quick connectivity check
ping = await speaker.ping_internet()
stability = await speaker.get_network_stability()
print(f"Ping: {ping}ms, Stability: {stability}")
# Run speed test
await speaker.start_speed_test()
# Wait for completion
while True:
status = await speaker.get_speed_test_status()
if status == 'complete':
break
await asyncio.sleep(2)
# Get results
results = await speaker.get_speed_test_results()
print(f"Speed: {results['avg_download']} Mbps")
asyncio.run(check_network())Configure speaker power management, startup behavior, and inter-speaker connection settings.
Control when the speaker automatically enters standby mode:
import pykefcontrol as pkf
speaker = pkf.KefConnector('192.168.1.100')
# Get current standby mode
mode = speaker.get_standby_mode()
print(f"Current mode: {mode}") # Returns 'standby_20mins'
# Set standby mode
speaker.set_standby_mode('standby_20mins') # ECO mode (20 minutes)
speaker.set_standby_mode('standby_30mins') # 30 minutes
speaker.set_standby_mode('standby_60mins') # 60 minutes
speaker.set_standby_mode('standby_none') # Never auto-standbyStandby Modes:
standby_20mins- ECO mode (shown as "ECO" in KEF Connect app)standby_30mins- 30 minutes auto-standbystandby_60mins- 60 minutes auto-standbystandby_none- Never auto-standby (manual standby only)
Configure which input wakes the speaker and HDMI auto-switching:
# Set wake source (which input can wake speaker from standby)
speaker.set_wake_source('wakeup_default') # All inputs can wake
speaker.set_wake_source('tv') # Only TV/HDMI wakes
speaker.set_wake_source('optical') # Only optical wakes
# Enable auto-switch to HDMI when signal detected
speaker.set_auto_switch_hdmi(True) # Auto-switch enabled
speaker.set_auto_switch_hdmi(False) # Manual input selection
# Check current settings
wake = speaker.get_wake_source()
auto_hdmi = speaker.get_auto_switch_hdmi()
print(f"Wake source: {wake}, Auto-HDMI: {auto_hdmi}")Control startup tone and USB charging:
# Disable startup beep
speaker.set_startup_tone(False)
# Enable USB port charging
speaker.set_usb_charging(True)
# Check current settings
tone = speaker.get_startup_tone()
usb = speaker.get_usb_charging()Configure wired vs wireless connection between left/right speakers:
# Set cable mode (for stereo pairs)
speaker.set_cable_mode('wired') # Use cable connection
speaker.set_cable_mode('wireless') # Use wireless connection
# Set master channel designation
speaker.set_master_channel('left') # This is the left speaker
speaker.set_master_channel('right') # This is the right speaker
# Get current settings
cable = speaker.get_cable_mode()
channel = speaker.get_master_channel()Check if the speaker is powered on or in standby:
status = speaker.get_speaker_status()
if status == 'powerOn':
print("Speaker is powered on")
elif status == 'standby':
print("Speaker is in standby mode")import pykefcontrol as pkf
speaker = pkf.KefConnector('192.168.1.100')
# Configure for home theater use
speaker.set_standby_mode('standby_60mins') # Long timeout
speaker.set_wake_source('tv') # Wake on TV signal
speaker.set_auto_switch_hdmi(True) # Auto-switch to HDMI
speaker.set_startup_tone(False) # Silent startup
# Configure stereo pair
speaker.set_cable_mode('wired') # Use cable for better quality
speaker.set_master_channel('left') # Designate as left speakerAll system behavior methods support async:
import asyncio
import pykefcontrol as pkf
async def configure_speaker():
speaker = pkf.KefAsyncConnector('192.168.1.100')
# Get all settings
mode = await speaker.get_standby_mode()
wake = await speaker.get_wake_source()
status = await speaker.get_speaker_status()
print(f"Standby: {mode}, Wake: {wake}, Status: {status}")
# Configure settings
await speaker.set_standby_mode('standby_30mins')
await speaker.set_startup_tone(False)
asyncio.run(configure_speaker())The KEF XIO soundbar includes exclusive features for home theater optimization. These features work on XIO soundbars only (model firmware V13xxx+).
Sound profiles optimize audio for different content types with specialized DSP processing:
# Get current sound profile
profile = my_speaker.get_sound_profile()
print(f"Current profile: {profile}") # Returns: "default", "music", "movie", "night", "dialogue", or "direct"
# Switch to movie mode for films
my_speaker.set_sound_profile("movie")
# Available profiles:
# - "default": Balanced sound for general content
# - "music": Optimized for music playback
# - "movie": Cinema-like experience with enhanced dynamics
# - "night": Reduced dynamic range for late-night viewing
# - "dialogue": Enhanced speech clarity
# - "direct": Minimal DSP processing, pure sound
# Example: Automatically switch profiles based on input source
my_speaker.set_source("opt") # Switch to TV input
my_speaker.set_sound_profile("movie") # Use movie profile for TV
my_speaker.set_source("wifi") # Switch to music streaming
my_speaker.set_sound_profile("music") # Use music profile for streamingAsync version:
profile = await my_speaker.get_sound_profile()
await my_speaker.set_sound_profile("movie")
β οΈ Note: ThedialogueModeboolean field exists in the KEF API and can be read/written, but does not appear to have any audible effect on current firmware (tested on XIO V13120). This is separate from the "dialogue" sound profile which works correctly. ThedialogueModetoggle may be a placeholder for a future feature. For dialogue enhancement, useset_sound_profile("dialogue")instead.
The dialogueMode toggle was intended to enhance speech clarity independently of the sound profile, but currently has no effect:
# Get dialogue enhancement state (reads correctly but toggle has no effect)
enabled = my_speaker.get_dialogue_mode()
print(f"Dialogue enhancement: {'On' if enabled else 'Off'}")
# These API calls succeed but have no audible effect
my_speaker.set_dialogue_mode(True)
my_speaker.set_dialogue_mode(False)
# For actual dialogue enhancement, use the dialogue sound profile instead:
my_speaker.set_sound_profile("dialogue") # This works!Async version:
enabled = await my_speaker.get_dialogue_mode()
await my_speaker.set_dialogue_mode(True) # No audible effect
await my_speaker.set_sound_profile("dialogue") # Use this insteadXIO soundbars include a gravity sensor that detects wall mounting. You can read the current state or override it manually:
# Check if soundbar is wall-mounted (via g-sensor)
is_wall_mounted = my_speaker.get_wall_mounted()
print(f"Wall mounted: {is_wall_mounted}")
# Manually override wall mount setting (if g-sensor is incorrect)
my_speaker.set_wall_mounted(True) # Force wall-mounted mode
my_speaker.set_wall_mounted(False) # Force shelf/TV-stand mode
# Note: Wall mounting affects speaker positioning (drivers used for sound)
# The g-sensor usually detects this automatically, but manual override is availableAsync version:
is_wall_mounted = await my_speaker.get_wall_mounted()
await my_speaker.set_wall_mounted(True)XIO soundbars include acoustic room calibration that measures your room and adjusts audio output for optimal sound. The calibration process uses a microphone to analyze room acoustics and applies dB adjustments automatically.
Reading Calibration Data:
import pykefcontrol as pkf
xio = pkf.KefConnector('192.168.1.100') # XIO Soundbar
# Check calibration status
status = xio.get_calibration_status()
if status['isCalibrated']:
print(f"Calibrated on: {status['year']}-{status['month']:02d}-{status['day']:02d}")
print(f"Network stability during calibration: {status['stability']}")
else:
print("Room calibration not performed")
# Get calibration dB adjustment
result = xio.get_calibration_result()
print(f"Calibration applied: {result} dB adjustment")
# Check calibration progress (during calibration)
step = xio.get_calibration_step()
print(f"Calibration step: {step}")
# Possible values:
# - 'step_1_start': Calibration starting
# - 'step_2_processing': Calibration in progress
# - 'step_3_complete': Calibration completeAsync version:
status = await xio.get_calibration_status()
result = await xio.get_calibration_result()
step = await xio.get_calibration_step()Note: These methods are read-only and return the current calibration state. To perform a new calibration, use the KEF Connect app which guides you through the microphone-based calibration process.
XIO soundbars have a built-in KW2 wireless subwoofer module with its own Bluetooth Low Energy (BLE) firmware that can be updated independently from the main speaker firmware.
Checking BLE Firmware:
import pykefcontrol as pkf
xio = pkf.KefConnector('192.168.1.100') # XIO Soundbar
# Get current BLE firmware version
version = xio.get_ble_firmware_version()
print(f"BLE firmware version: {version}")
# Get current update status
status = xio.get_ble_firmware_status()
print(f"Update status: {status}")
# Possible values: 'startUp', 'downloading', 'installing', 'complete'
# Check for available updates
update = xio.check_ble_firmware_update()
if update:
print(f"BLE firmware update available: {update}")
else:
print("No BLE firmware update available")Installing BLE Firmware Updates:
# Install BLE firmware update immediately
xio.install_ble_firmware_now()
print("BLE firmware update started")
# Or schedule update for later
xio.install_ble_firmware_later()
print("BLE firmware update scheduled")Async version:
version = await xio.get_ble_firmware_version()
status = await xio.get_ble_firmware_status()
update = await xio.check_ble_firmware_update()
await xio.install_ble_firmware_now()
await xio.install_ble_firmware_later()Note: BLE firmware updates are for the KW2 wireless subwoofer module only and are separate from the main speaker firmware updates. This feature only works on XIO soundbars with the built-in wireless subwoofer.
Control LED indicators and startup behavior to minimize distractions. In the KEF Connect app, these appear under "Do Not Disturb" settings.
Important Note: The API endpoints work on all KEF W2 platform speakers, but the physical effects vary by model:
- LSX II / LSX II LT / LS50 W2 / LS60: Only standby LED and startup tone are exposed in KEF Connect app
- XIO Soundbar: Full control panel LED controls (4 settings: control panel LED, control panel in standby, startup tone, control panel lock)
Control whether the LED indicator is visible when the speaker is in standby mode:
import pykefcontrol as pkf
speaker = pkf.KefConnector('192.168.1.100')
# Enable standby LED (default)
speaker.set_standby_led(True)
# Disable standby LED (for dark rooms)
speaker.set_standby_led(False)
# Check current setting
enabled = speaker.get_standby_led()
print(f"Standby LED: {'On' if enabled else 'Off'}")Async version:
enabled = await speaker.get_standby_led()
await speaker.set_standby_led(False)Control the audible beep when powering on (also in System Behavior Settings):
# Disable startup beep for silent power-on
speaker.set_startup_tone(False)
# Enable startup beep
speaker.set_startup_tone(True)
# Check current setting
enabled = speaker.get_startup_tone()The XIO soundbar has exclusive control panel LED settings (4 controls shown in KEF Connect app under "Do Not Disturb"). The set_top_panel_* methods only work on XIO models.
Note: The
get/set_front_led()methods exist for all models but have no visible effect on any currently tested speakers (LSX II, LSX II LT, XIO). The API field exists in firmware but appears to have no hardware implementation. These methods are kept for completeness in case future models support this feature.
# Control panel LED during operation
speaker.set_front_led(True) # LED on during operation (default)
speaker.set_front_led(False) # LED off during operation
# Control panel LED in standby
speaker.set_top_panel_standby_led(True) # LED on in standby
speaker.set_top_panel_standby_led(False) # LED off in standby
# Enable/disable top panel entirely (control panel lock)
speaker.set_top_panel_enabled(True) # Panel active (default)
speaker.set_top_panel_enabled(False) # Panel locked/disabled
# Check current settings
front_led = speaker.get_front_led()
panel_enabled = speaker.get_top_panel_enabled()
standby_led = speaker.get_top_panel_standby_led()
print(f"Front LED: {front_led}, Panel enabled: {panel_enabled}, Standby LED: {standby_led}")XIO Async version:
# XIO-specific async methods
await speaker.set_front_led(False)
await speaker.set_top_panel_standby_led(False)
await speaker.set_top_panel_enabled(False)import pykefcontrol as pkf
# Configure LSX II for bedroom use (minimal LEDs)
lsx_speaker = pkf.KefConnector('192.168.1.100') # LSX II
lsx_speaker.set_standby_led(False) # No standby indicator
lsx_speaker.set_startup_tone(False) # Silent power-on
# Configure XIO for home theater (all LEDs off)
xio_speaker = pkf.KefConnector('192.168.1.101') # XIO Soundbar
xio_speaker.set_standby_led(False) # No standby LED
xio_speaker.set_startup_tone(False) # Silent power-on
xio_speaker.set_front_led(False) # Control panel off during operation
xio_speaker.set_top_panel_standby_led(False) # Control panel off in standby
xio_speaker.set_top_panel_enabled(False) # Lock control panel (optional)Configure physical IR remote control behavior, including button assignments and volume locking.
Enable/disable IR remote control and set the IR code set to avoid conflicts with other devices:
import pykefcontrol as pkf
speaker = pkf.KefConnector('192.168.1.100')
# Enable/disable IR remote
speaker.set_remote_ir_enabled(True) # Enable IR remote (default)
speaker.set_remote_ir_enabled(False) # Disable IR remote
# Check current setting
enabled = speaker.get_remote_ir_enabled()
print(f"IR remote: {'Enabled' if enabled else 'Disabled'}")
# Set IR code set (to avoid conflicts with other IR devices)
speaker.set_ir_code_set('ir_code_set_a') # Default code set
speaker.set_ir_code_set('ir_code_set_b') # Alternative if conflicts occur
speaker.set_ir_code_set('ir_code_set_c') # Second alternative
# Check current code set
code = speaker.get_ir_code_set()
print(f"IR code set: {code}")Async version:
enabled = await speaker.get_remote_ir_enabled()
await speaker.set_remote_ir_enabled(True)
code = await speaker.get_ir_code_set()
await speaker.set_ir_code_set('ir_code_set_a')On XIO soundbars, assign sound profiles to the two EQ buttons on the remote:
# Get current EQ button assignments (XIO only)
preset1 = speaker.get_eq_button(1)
preset2 = speaker.get_eq_button(2)
print(f"EQ Button 1: {preset1}, Button 2: {preset2}")
# Assign sound profiles to EQ buttons
speaker.set_eq_button(1, 'dialogue') # Button 1 = dialogue mode
speaker.set_eq_button(2, 'night') # Button 2 = night mode
speaker.set_eq_button(1, 'music') # Button 1 = music mode
speaker.set_eq_button(2, 'movie') # Button 2 = movie mode
# Available presets: 'dialogue', 'night', 'music', 'movie', 'default', 'direct'Note: EQ button assignment only works on XIO soundbars. LSX/LS50/LS60 models will return an error.
Async version:
preset1 = await speaker.get_eq_button(1)
await speaker.set_eq_button(1, 'dialogue')Configure the action assigned to the favourite button on the remote:
# Get current favourite button action
action = speaker.get_favourite_button_action()
print(f"Favourite button: {action}")
# Set favourite button action
speaker.set_favourite_button_action('nextSource') # Cycle through inputsAsync version:
action = await speaker.get_favourite_button_action()
await speaker.set_favourite_button_action('nextSource')Lock the speaker volume at a fixed level, preventing volume changes via remote or app:
# Enable fixed volume mode (lock at specific level)
speaker.set_fixed_volume_mode(50) # Lock volume at 50%
speaker.set_fixed_volume_mode(75) # Lock volume at 75%
# Disable fixed volume mode (allow volume changes)
speaker.set_fixed_volume_mode(None)
# Check current setting
volume = speaker.get_fixed_volume_mode()
if volume is not None:
print(f"Volume locked at: {volume}%")
else:
print("Fixed volume mode disabled")Use case: Commercial installations, public spaces, or preventing accidental volume changes.
Async version:
volume = await speaker.get_fixed_volume_mode()
await speaker.set_fixed_volume_mode(50)
await speaker.set_fixed_volume_mode(None)import pykefcontrol as pkf
speaker = pkf.KefConnector('192.168.1.100')
# Enable IR remote with alternative code set (avoid conflicts)
speaker.set_remote_ir_enabled(True)
speaker.set_ir_code_set('ir_code_set_b')
# For XIO: Configure EQ buttons
if speaker.speaker_model == 'XIO':
speaker.set_eq_button(1, 'dialogue')
speaker.set_eq_button(2, 'night')
# Configure favourite button
speaker.set_favourite_button_action('nextSource')
# Lock volume for commercial use
speaker.set_fixed_volume_mode(60) # Lock at 60%Check for and manage firmware updates for your KEF speakers:
# Sync version
import pykefcontrol as pkf
my_speaker = pkf.KefConnector("192.168.1.100")
# Check current firmware version
print(f"Current firmware: {my_speaker.firmware_version}")
# Trigger check for available updates (speaker needs internet connection)
update_info = my_speaker.check_for_firmware_update()
if update_info:
print(f"Update available: {update_info}")
# Install the update
result = my_speaker.install_firmware_update()
print("Firmware update started!")
# Monitor progress (speaker will restart during update)
status = my_speaker.get_firmware_update_status()
if status:
print(f"Update status: {status}")# Async version
import asyncio
import pykefcontrol as pkf
async def check_updates():
my_speaker = pkf.KefAsyncConnector("192.168.1.100")
# Check current firmware version
firmware = await my_speaker.get_firmware_version()
print(f"Current firmware: {firmware}")
# Trigger check for available updates
update_info = await my_speaker.check_for_firmware_update()
if update_info:
print(f"Update available: {update_info}")
# Install the update
result = await my_speaker.install_firmware_update()
print("Firmware update started!")
# Monitor progress (speaker will restart during update)
status = await my_speaker.get_firmware_update_status()
if status:
print(f"Update status: {status}")
asyncio.run(check_updates())Fetch official firmware release notes from KEF's website to show users the latest available firmware and release history:
import pykefcontrol as pkf
# Get release notes for all KEF models
releases = pkf.get_kef_firmware_releases()
# Or get releases for specific model
releases = pkf.get_kef_firmware_releases(model_filter="LSX II")
# Can also use API model name format
releases = pkf.get_kef_firmware_releases(model_filter="LSXII")
# Show latest firmware
if "LSX II" in releases and releases["LSX II"]:
latest = releases["LSX II"][0]
print(f"Latest version: {latest['version']}")
print(f"Release date: {latest['date']}")
print("Changes:")
for note in latest['notes']:
print(f" β’ {note}")Home Assistant Integration: Use this to create sensors showing latest available firmware versions and release notes without requiring internet connection on the speaker itself.
The KEF XIO soundbar has additional features not available on other KEF speakers:
Sound Profiles - Six preset audio modes optimized for different content:
# Get current sound profile
profile = my_speaker.get_sound_profile() # Returns str
# Set sound profile
my_speaker.set_sound_profile('movie')
# Available profiles:
# - 'default' - Default sound profile
# - 'music' - Optimized for music playback
# - 'movie' - Enhanced for movie audio
# - 'night' - Reduced dynamics for late-night listening
# - 'dialogue' - Voice clarity enhancement
# - 'direct' - No processing, direct audio pathWall Mount Configuration:
# Check if soundbar is configured as wall-mounted
mounted = my_speaker.get_wall_mounted() # Returns bool
# Update wall mount configuration
my_speaker.set_wall_mounted(True)Async Version:
profile = await my_speaker.get_sound_profile()
await my_speaker.set_sound_profile('movie')
mounted = await my_speaker.get_wall_mounted()
await my_speaker.set_wall_mounted(True)Note: These methods work on XIO soundbars only. Using them on other KEF models will work via update_dsp_setting() but may not have any effect depending on the model.
Get comprehensive device information for your KEF speakers:
import pykefcontrol as pkf
speaker = pkf.KefConnector('192.168.1.100')
# Get all device info at once
info = speaker.get_device_info()
print(f"Model: {info['model_name']}")
print(f"Serial: {info['serial_number']}")
print(f"KEF ID: {info['kef_id']}")
print(f"Hardware: {info['hardware_version']}")
print(f"MAC: {info['mac_address']}")
# Or get individual values
model = speaker.get_model_name() # Returns: 'SP4041', 'SP4077', 'SP4083', etc.
serial = speaker.get_serial_number()
kef_id = speaker.get_kef_id() # UUID for KEF cloud services
hw_ver = speaker.get_hardware_version()
mac = speaker.get_mac_address() # Format: 'XX:XX:XX:XX:XX:XX'Model codes:
SP4041= LSX IISP4077= LSX II LTSP4083= XIO SoundbarSP4045= LS50 Wireless IISP4065= LS60 Wireless
Async version:
info = await speaker.get_device_info()
model = await speaker.get_model_name()
serial = await speaker.get_serial_number()Control analytics, streaming quality, and language preferences:
# Check KEF analytics state (speaker usage data to KEF)
analytics_enabled = speaker.get_analytics_enabled() # Returns bool
# Enable/disable KEF analytics
speaker.set_analytics_enabled(True) # Allow KEF to collect data
speaker.set_analytics_enabled(False) # Disable data collection
# Check app analytics state (KEF Connect app usage data)
app_analytics = speaker.get_app_analytics_enabled() # Returns bool
# Enable/disable app analytics
speaker.set_app_analytics_enabled(True)
speaker.set_app_analytics_enabled(False)Configure streaming service bitrate limits:
# Get current quality setting
quality = speaker.get_streaming_quality() # Returns: 'unlimited', '320', '256', '192', or '128'
# Set streaming quality (kbps)
speaker.set_streaming_quality('unlimited') # No limit (best quality)
speaker.set_streaming_quality('320') # 320 kbps (high quality)
speaker.set_streaming_quality('256') # 256 kbps (good quality)
speaker.set_streaming_quality('192') # 192 kbps (medium quality)
speaker.set_streaming_quality('128') # 128 kbps (data saving)Note: Lower bitrates reduce bandwidth usage but decrease audio quality. Only affects streaming services (Spotify, Tidal, etc.), not local sources.
# Get current language
lang = speaker.get_ui_language() # Returns: 'en_GB', 'nl_NL', etc.
# Set UI language (ISO language codes)
speaker.set_ui_language('en_GB') # English (UK)
speaker.set_ui_language('en_US') # English (US)
speaker.set_ui_language('nl_NL') # Dutch
speaker.set_ui_language('de_DE') # German
speaker.set_ui_language('fr_FR') # French
speaker.set_ui_language('es_ES') # Spanish
speaker.set_ui_language('it_IT') # Italian
speaker.set_ui_language('ja_JP') # Japanese
speaker.set_ui_language('zh_CN') # Chinese (Simplified)
speaker.set_ui_language('zh_TW') # Chinese (Traditional)Async version:
analytics = await speaker.get_analytics_enabled()
await speaker.set_analytics_enabled(False)
quality = await speaker.get_streaming_quality()
await speaker.set_streaming_quality('320')
lang = await speaker.get_ui_language()
await speaker.set_ui_language('en_GB')Configure the speaker's geographic region (affects available streaming services and regional settings):
# Get current location code
location = speaker.get_speaker_location() # Returns integer country code
# Set speaker location
speaker.set_speaker_location(44) # Set to UK (example code)Async version:
location = await speaker.get_speaker_location()
await speaker.set_speaker_location(44)# Restore DSP settings to factory defaults
# Resets: EQ, bass extension, wall/desk mode, phase, etc.
# Does NOT affect: Network settings, user profiles, streaming accounts
speaker.restore_dsp_defaults()
# Get complete DSP state information
dsp_info = speaker.get_dsp_info()
print(f"DSP configuration: {dsp_info}")Async version:
await speaker.restore_dsp_defaults()
dsp_info = await speaker.get_dsp_info()Monitor firmware update progress for all components:
# Get firmware upgrade progress during an update
progress = speaker.get_firmware_upgrade_progress()
print(f"Main firmware: {progress.get('main', 0)}%")
print(f"DSP firmware: {progress.get('dsp', 0)}%")
print(f"BLE firmware: {progress.get('ble', 0)}%") # XIO onlyAsync version:
progress = await speaker.get_firmware_upgrade_progress()Performing a factory reset will erase ALL settings and return the speaker to factory defaults. This includes:
- Network configuration (WiFi credentials)
- User preferences and profiles
- Streaming service accounts
- Paired Bluetooth devices
- All custom settings
The speaker will require complete setup again through the KEF Connect app.
# Perform factory reset (NO CONFIRMATION PROMPT!)
speaker.factory_reset()Async version:
await speaker.factory_reset()Recommendation: Only use in troubleshooting or before selling/transferring the speaker.
Scan and manage WiFi networks:
import time
# Trigger a new WiFi scan
speaker.activate_wifi_scan()
# Wait for scan to complete
time.sleep(3)
# Get available networks
networks = speaker.scan_wifi_networks()
for network in networks:
print(f"SSID: {network['ssid']}")
print(f" Security: {network['security']}")
print(f" Signal: {network['signalStrength']}")
print(f" Frequency: {network['frequency']}")
print()Network information includes:
ssid- Network namesecurity- Security type (WPA2, WPA3, Open, etc.)signalStrength- Signal strength indicatorfrequency- Band (2.4GHz or 5GHz)
Async version:
import asyncio
# Trigger scan
await speaker.activate_wifi_scan()
# Wait for scan
await asyncio.sleep(3)
# Get results
networks = await speaker.scan_wifi_networks()
for network in networks:
print(f"{network['ssid']}: {network['signalStrength']}")Use case: Network diagnostics, finding optimal WiFi channel, checking signal strength before placement.
Information Polling
Pykefcontrol offers a polling functionality. Instead of manually fetching all parameters to see what has changed, you can use the method poll_speaker. This method returns the updated properties since the last time the changes were polled. If multiple changes are made to the same property, only the last change will be kept. It is technically possible to track all the changes to a property since the last poll, although it is not implemented. Please submit an issue if you need such a feature.
poll_speaker will return a dictionary whose keys are the names of the properties which have been updated.
poll_speaker arguments:
| Argument | Required | Default Value | Comment |
|---|---|---|---|
timeout |
Optional | 10 |
timeout is in seconds. If no change has been made since the last poll when you call poll_speaker, the method will wait for changes during timeout seconds for new changes. If there is a change before the end of the timeout, poll_speaker will return them immediately and stop monitoring changes. If no changes are made, the method will return an empty dictionary. timeout+ 0.5 seconds. The speaker will wait for timeout seconds before returning an empty dictionary if no changes are made. Therefore it is necessary to add a small margin in the python function to account for processing/networking time. Please submit an issue if you feel that this parameter needs tweaking. |
song_status |
Optional | False |
Deprecated, please use poll_song_status instead |
poll_song_status |
Optional | False |
If poll_song_status if set to True, it will poll the song status (how many miliseconds of the current song have been played so far). If a song is playing and poll_song_status is set to True, poll_speaker will return almost imediately since song_status is updated at each second. This is forcing you to poll agressively to get other events. By default it is set to False in order to track other events more efficiently. |
my_speaker.poll_speaker(timeout=3) # example of a 3 seconds timeout
# (output example) >>> {} # it will return an empty dict if no changes were made
# no suppose you start playing a song
my_speaker.poll_speaker(poll_song_satus=True) # timeout is 10 seconds by default
# (output example) >>> {'song_info': {'title': 'Am I Wrong',
# 'artist': 'Etienne de CrΓ©cy',
# 'album': 'Am I Wrong',
# 'cover_url': 'https://some-url/some-image.jpg'},
# 'song_length': 238000,
# 'status': 'playing',
# 'song_status': 26085}
#
# -> in this case it returns a dictionary with the keys "song_info" ,
# "song_length", "status" and "song_status" containing information about the new song
# now suppose you change the volume to 32
my_speaker.poll_speaker(poll_song_satus=True)
# (output example) >>> {'song_status': 175085, 'volume': 32}
#
# -> in this case it returns both "song_status" (because the song kept playing),
# and "volume" because you updated the volume
# if you do not want to poll the song status
my_speaker.poll_speaker()
# (output example) >>> {'volume': 32}All the possible keys of the dictionary are:
source, song_status, volume, song_info, song_length, status, speaker_status, device_name, mute and other.
other contains some of the speaker-specific information that might have changed, but are not properties of either KefConnector or KefAsyncConnector.
This function is used internally by pykefcontrol and returns a JSON output with a lot of information. You might want to use them to get extra information such as the artwork/album cover URL, which does not have a dedicated function yet in pykefcontrol.
# Get currently playing media information
my_speaker._get_player_data()
# (output example) >>> {'trackRoles': {'icon': 'http://www.xxx.yyy.zzz:80/file/stream//tmp/temp_data_airPlayAlbum_xxxxxxxxx', 'title': 'I Want To Break Free', 'mediaData': {'resources': [{'duration': 263131}], 'metaData': {'album': 'Greatest Hits', 'artist': 'Queen'}}}, 'playId': {'systemMemberId': 'kef_one-xxxxxxxx', 'timestamp': 676181357}, 'mediaRoles': {'audioType': 'audioBroadcast', 'title': 'AirPlay', 'doNotTrack': True, 'type': 'audio', 'mediaData': {'resources': [{'mimeType': 'audio/unknown', 'uri': 'airplay://'}], 'metaData': {'serviceID': 'airplay', 'live': True, 'playLogicPath': 'airplay:playlogic'}}}, 'state': 'playing', 'status': {'duration': 263131, 'playSpeed': 1}, 'controls': {'pause': True, 'next_': True, 'previous': True}}Pykefcontrol offers an asynchronous connector with the same feature set as the synchronous connector. However, there are a few changes in the property setters. You can no longer use my_speaker.volume = 28 to set a property. You have to use the setter like so: await my_speaker.set_volume(28).
The actions you make with KefAsyncConnector should be embedded in an async function. Here is a quick example:
import asyncio
from pykefcontrol.kef_connector import KefAsyncConnector
# Define an async function
async def main():
my_speaker = KefAsyncConnector("192.168.yyy.zz")
# Get speaker name
print(await my_speaker.speaker_name)
# Get volume
print(await my_speaker.volume)
# Turn on speaker
await my_speaker.power_on()
# Toggle play/pause
await my_speaker.toggle_play_pause()
# Set volume
# Please read through to see precision about setters
await my_speaker.set_volume(28)
# Set source
await my_speaker.set_source("bluetooth")
# Set status
await my_speaker.set_status("powerOn")
# To avoid warning about an unclosed session
# once your program is over, run:
await my_speaker._session.close()
# This is not mandatory. But if you do not close
# the session, you will have a warning.
# Get loop
loop = asyncio.get_event_loop()
# Run main function in async context
loop.run_until_complete(main())KefAsyncConnector has the same property and methods as its synchronous counterpart KefConnector. You can access the same properties and methods in an asynchronous context by using await my_speaker.property or await my_speaker.method(). For the list of available properties and methods, read Available features.
However, to have an asynchronous property setter, the way to set properties has changed. You should use the specific setter. For a property, the setter is called set_property. As you can see in the example script above, to set the volume, use set_volume. Here is the list of properties with such setters:
- volume: use
set_volume - state: use
set_state - source: use
set_source
The testing.py script provides comprehensive testing for all pykefcontrol features. It supports network discovery, interactive mode, and non-interactive testing.
Automatically discover KEF speakers on your network:
# Auto-detect network and scan
python3 testing.py --discover
# Specify network range
python3 testing.py --discover --network 192.168.16.0/24This will scan your network and display a table of all discovered KEF speakers with their IP addresses, names, models, firmware versions, and MAC addresses.
Features:
- Fast parallel scanning (50 concurrent threads)
- Auto-detects network from local IP if not specified
- Works with all KEF speaker models (LSX II, LSX II LT, LS50 Wireless II, LS60, XIO soundbar)
- Displays results in a formatted table
Run the full interactive test suite:
python3 testing.pyThe script will guide you through testing all features with prompts and confirmations.
For automated testing or quick verification, use command-line arguments:
Quick connection test:
python3 testing.py --host 192.168.16.22 --test info --model LSXIITest specific features:
# Test DSP/EQ features (desk mode, wall mode, bass, treble, balance, etc.)
python3 testing.py --host 192.168.16.22 --test dsp --model LSXII
# Test subwoofer controls
python3 testing.py --host 192.168.16.22 --test subwoofer --model LSXII
# Test firmware update features
python3 testing.py --host 192.168.16.22 --test firmware --model LSXII
# Test XIO soundbar-specific features
python3 testing.py --host 192.168.16.26 --test xio --model XIO
# Test Bluetooth control (4 methods)
python3 testing.py --host 192.168.16.22 --test bluetooth --model LSXII
# Test Grouping/Multiroom (2 methods)
python3 testing.py --host 192.168.16.22 --test grouping --model LSXII
# Test Notifications (3 methods)
python3 testing.py --host 192.168.16.22 --test notifications --model LSXII
# Test Alerts & Timers (13 methods)
python3 testing.py --host 192.168.16.22 --test alerts --model LSXII
# Test Google Cast (3 methods)
python3 testing.py --host 192.168.16.22 --test googlecast --model LSXII
# Test ALL new API methods (25 methods total)
python3 testing.py --host 192.168.16.22 --test new --model LSXIIRun all tests non-interactively:
python3 testing.py --host 192.168.16.22 --test all --model LSXIIModel identifiers:
LSXII= LSX IILSXIILT= LSX II LTLS50WirelessII= LS50 Wireless IILS60Wireless= LS60 WirelessXIO= XIO Soundbar
Available test suites:
info- Speaker information onlydsp- DSP/EQ controls (11 methods)subwoofer- Subwoofer controls (6 methods)preset-analysis- Comprehensive subwoofer preset analysisxio- XIO soundbar features (2 methods)firmware- Firmware update features (3 methods)bluetooth- Bluetooth device management (4 methods) NEWgrouping- Multiroom speaker grouping (2 methods) NEWnotifications- UI notifications (3 methods) NEWalerts- Alarms and timers (13 methods) NEWgooglecast- Google Cast configuration (3 methods) NEWnew- All new API methods (25 methods total) NEWall- Complete test suite (188 methods)
Goal: Achieve feature parity with KEF Connect app for official Home Assistant integration
- Save/load/manage EQ profiles
- JSON storage with metadata
- Import/export functionality
- Share profiles between speakers
- Sound profile control (default/music/movie/night/dialogue/direct)
- Dialogue enhancement mode
- Room calibration control
- Wall mount detection
- BLE firmware updates (KW2 subwoofer)
See SPEAKER_FEATURES_ANALYSIS.md for complete feature analysis.
-
version 0.7.1
- Fix issue with async version of
get_speaker_modelandget_firmware_version.
- Fix issue with async version of
-
version 0.7
- Now compatible with LSX II and LS60 !
- Add
speaker_modelandfirmware_versionproperties. β οΈ song_statusargument ofpoll_speakeris now deprecated. Please usepoll_song_status
-
Version 0.6.2
- modify
poll_speakerto prevent falling ifsong_statusis not properly defined by the speaker - regenerate the queue_id if
song_statuswas changed before the queue timeout.
- modify
-
Version 0.6.1
- Add parameter
song_statusto the methodpoll_speaker.
- Add parameter
-
Version 0.6
- Add method
poll_speakerthat returns the last changes made to properties since the last poll.
- Add method
-
Version 0.5
- Add option to pass existing
session=aiohttp.ClientSession()toKefAsyncConnector. - Add method
close_sessionandresurect_session
- Add option to pass existing
-
Version 0.4
- Add
KefAsyncConnector. A class with the same functionality asKefConnectorbut with async properties and methods.
- Add
-
Version 0.3
β οΈ Breaking change :get_song_information()now returns a dictionary, no longer a tuple- add property
mac_addressthat returns the MAC address of the speaker as a string - add property
speaker_namethat returns the friendly speaker name as defined in the KEF Connect app onboarding process
-
Version 0.2
- correct a bug in
power_onandshutdown
- correct a bug in
-
Version 0.1
- first version
The XIO soundbar has been tested and confirmed to support all features. Additional XIO-specific findings:
Confirmed Features:
- Sound Profiles: Six modes confirmed (default, music, movie, night, dialogue, direct) - all stored in lowercase in API
- Wall Mounted: Boolean field indicating soundbar mounting configuration
- Stability: Field exists (always
0), purpose unknown - may be related to wall mounting or vibration control
API Details:
- XIO uses same
kef:eqProfile/v2endpoint as other speakers - All 25 standard fields present
- 3 additional fields:
soundProfile,wallMounted,stability
Known Limitations:
- Some EQ settings may only be visible when device is actively playing audio
isKW2toggle mentioned in app not found in API responses (may be app-only UI element)
Home Assistant Integration:
Recommended HA entities for firmware updates:
- sensor.kef_speaker_firmware - Current firmware version with model/name attributes
- button.kef_speaker_check_update - Trigger update check with
check_for_firmware_update() - binary_sensor.kef_speaker_update_available - Update availability from check response
- button.kef_speaker_install_update - Install with
install_firmware_update()(show warning) - sensor.kef_speaker_update_status - Progress tracking via
get_firmware_update_status()
Recommended HA entities for XIO soundbar:
- select.xio_sound_profile - Six profiles via
get_sound_profile()/set_sound_profile() - binary_sensor.xio_wall_mounted - Config via
get_wall_mounted()/set_wall_mounted()
XIO Development - Real-time Monitoring:
import pykefcontrol as pkf
import time
speaker = pkf.KefConnector('192.168.16.26')
previous = None
print("Monitoring XIO - change settings in KEF app...")
while True:
current = speaker.get_eq_profile()['kefEqProfileV2']
if previous and current != previous:
for key in current:
if current[key] != previous.get(key):
print(f"{key}: {previous[key]} -> {current[key]}")
previous = current.copy()
time.sleep(1)See test suite in testing.py for complete feature coverage (22 automated tests).
The Home Assistant integration for KEF speakers uses HA's native Storage API instead of the JSON file-based ProfileManager included in this library. This provides better integration with HA's backup system and follows HA best practices.
For Home Assistant Integration Developers:
Use homeassistant.helpers.storage.Store for profile storage:
from homeassistant.helpers.storage import Store
from datetime import datetime
STORAGE_VERSION = 1
STORAGE_KEY = "kef_connector.profiles"
class KefProfileStorage:
"""Manage KEF EQ profiles using HA Storage API."""
def __init__(self, hass, speaker_mac):
self.store = Store(hass, STORAGE_VERSION, f"{STORAGE_KEY}.{speaker_mac}")
async def async_save_profile(self, name, profile_data, description=""):
"""Save profile to HA storage (.storage/kef_connector.profiles.{mac})."""
profiles = await self.store.async_load() or {}
profiles[name] = {
"name": name,
"description": description,
"created_at": datetime.now().isoformat() if name not in profiles
else profiles[name].get("created_at"),
"modified_at": datetime.now().isoformat(),
"profile_data": profile_data
}
await self.store.async_save(profiles)
async def async_load_profile(self, name):
"""Load profile from HA storage."""
profiles = await self.store.async_load() or {}
if name not in profiles:
raise ValueError(f"Profile '{name}' not found")
return profiles[name]["profile_data"]
async def async_list_profiles(self):
"""List all saved profiles."""
profiles = await self.store.async_load() or {}
return list(profiles.keys())Integration with media_player entity:
@property
def sound_mode(self):
"""Return current profile name from speaker."""
return self.coordinator.data.get("profile_name", "Expert")
@property
def sound_mode_list(self):
"""Return list of saved profiles."""
return self._cached_profile_list
async def async_select_sound_mode(self, sound_mode):
"""Load and apply saved profile."""
profile_data = await self._profile_storage.async_load_profile(sound_mode)
await self.coordinator.speaker.set_eq_profile(profile_data)
await self.coordinator.async_request_refresh()Key points:
- β
Profiles stored per-speaker using MAC address:
.storage/kef_connector.profiles.XX_XX_XX_XX_XX_XX - β Integrated with HA backups automatically
- β Fully async operation
- β
Use
sound_modeentity feature for profile selection - β Add custom services for save/delete/rename operations
Complete KEF speaker API documentation from APK analysis is available in apk_analysis.md.
This includes:
- 57 newly discovered API methods (doubles library capability to 103 methods)
- Complete endpoint catalog (121 endpoints tested)
- Model-specific feature comparison (LSX II, LSX II LT, XIO)
- Implementation roadmap with code examples
To test API compatibility on your speakers, run:
python apk_analysis.py --host YOUR_SPEAKER_IP --verboseThe analysis achieved 98% feature parity with the KEF Connect app.
Several features are not available via HTTP API on XIO soundbar firmware V13120 (paths not implemented):
Alarms & Timers:
- β
add_timer(),remove_timer(),add_alarm(),remove_alarm(),enable_alarm(),disable_alarm() - β
list_alerts(),get/set_snooze_time(),play/stop_default_alert_sound()
Multiroom Grouping:
- β
get_group_members(),save_persistent_group()- XIO doesn't support multiroom
Bluetooth:
- β
set_bluetooth_discoverable() - β
get_bluetooth_state(),disconnect_bluetooth(),clear_bluetooth_devices()
Google Cast:
- β
get_cast_usage_report(),set_cast_usage_report(),get_cast_tos_accepted()
AirPlay Playback (FIRMWARE BUG):
- β
next_track(),previous_track()fail with "Control is not supported" - β
toggle_play_pause()may stop playback unexpectedly - β LSX II works perfectly with AirPlay - this is XIO-specific
β οΈ KEF Connect app also affected (next/prev buttons greyed out during AirPlay)- Workaround: Control playback from source device (iPhone, Mac, etc.)
TV Mode:
- When XIO source is set to TV, media controls (play/pause/next/prev) are unavailable
Workaround: Use KEF Connect mobile app for unavailable features, or switch to WiFi/Bluetooth sources for full control.
These are firmware limitations, not library bugs. See CLAUDE.md for technical details.
For detailed technical documentation, architecture notes, and AI assistant context, see CLAUDE.md.
KEF speaker model information sourced from: