edifier_es300
Control an Edifier ES300 speaker over Wi-Fi, without the Edifier Home app.
Ships an asyncio library and a click-based command-line interface.
- Library (
edifier_es300): stdlib-only, fully async. - CLI (
python -m edifier_es300): thin wrapper over the library; needsclick.
Requirements
- Python 3.13+
click(CLI only — the library has no third-party dependencies)
uv sync # or: pip install click
Library usage
Everything is async. The ES300 connection is an async context manager — reuse a
single connection for a burst of commands (the device drops an idle socket after a
few seconds).
import asyncio
from edifier_es300 import ES300, Source, EqPreset, LightEffect, LightColor
async def main():
async with ES300("192.168.1.123", 8080) as device:
print(await device.status()) # parsed Status (see below)
await device.volume(20) # 0..30
await device.play() # resume
await device.pause() # pause
await device.play_pause_toggle() # toggle
await device.next_track()
await device.previous_track()
await device.input_source(Source.AIRPLAY)
await device.light_switch(True)
await device.brightness(60) # 0..100
await device.light_effect(LightEffect.BREATHING)
await device.light_color(LightColor.YELLOW)
await device.eq_preset(EqPreset.VOCAL)
await device.eq_custom((10, 5, 0, 0, 0, -5)) # 6 gains, tenths of a dB (-30..30)
await device.timer_shutdown(30) # sleep timer in minutes (0 = off)
await device.shutdown() # power off (no remote power-on; physical button to wake)
asyncio.run(main())
Internally the context manager holds the socket open and runs a background task that
reads every inbound frame. Each command carries a generated id; the device echoes
it in its reply, and the background task hands the reply to the awaiting call — so
await device.volume(20) returns only once that command's ack comes back.
Discovery
ES300.discover() broadcasts on the LAN and returns a list of ready-to-use
ES300 objects (host, port, and name filled in):
speakers = await ES300.discover(seconds=3.0)
for speaker in speakers:
print(speaker) # "EDIFIER ES300 192.168.1.123:8080"
async with speakers[0] as device:
await device.volume(15)
Return values & errors
- Command methods (
volume,play,input_source,eq_custom, …) return the device's raw ack frame (aFrameDatadict). They raiseCommandFailedif the device acks with a non-successmessage, orEndOfStreamif the socket drops before the ack arrives. status()returns aStatus | None(Noneonly if the device stays silent).
Live callbacks
Register callbacks to react to unsolicited frames the device pushes while a connection is held open. Both are usable as decorators and hold the callback by weak reference (drop your reference and it stops firing):
async with ES300("192.168.1.123") as device:
@device.status_callback
async def on_status(status: Status):
print("state changed:", status.volume)
@device.heartbeat_callback
async def on_heartbeat(frame):
print("heartbeat")
await asyncio.sleep(60) # callbacks fire as frames arrive
Status
str(status) renders a human-readable dump (this is what the CLI status prints):
playing: - / - (status <PlayerStatus.PLAYING: 1>)
volume : 6 / 30
source : <Source.USB: 2>
effect : <LightEffect.STATIC: 1>
color : <LightColor.YELLOW: {'r': 255, 'g': 170, 'b': 60}>
eq : <EqPreset.CLASSIC: 0> gains=[20, 10, 0, -5, 5, 10]
battery: 40% (<BatteryStatus.DISCONNECTED: 2>)
timer : off
Fields: volume, max_volume, song, lyric, player_status, input_source,
light_effect, sound_index, eq_selected_index, eq_gains, battery, and
raw (the full status frame for anything not surfaced).
Enums
| Enum | Values | Notes |
|---|---|---|
Source |
BLUETOOTH=0, AUX=1, USB=2, AIRPLAY=3 |
input source |
EqPreset |
CLASSIC=0, MONITOR=1, GAME=2, VOCAL=3, CUSTOMIZED=4 |
CUSTOMIZED is the editable slot |
LightEffect |
STATIC=1, BREATHING=2, WATERFLOW=3 |
ambient LED effect |
LightColor |
YELLOW, WHITE |
value is the RGB dict; hardware only does these two |
BatteryStatus |
CONNECTED=1, DISCONNECTED=2 |
external power state (read-only) |
Source, EqPreset, and LightEffect are IntEnums, so methods also accept a
plain int. Setting eq_custom gains automatically selects EqPreset.CUSTOMIZED.
CLI usage
python -m edifier_es300 [--host IP] [--port N] COMMAND [ARGS]
--host— speaker IP. Omit it to auto-discover the first speaker on the LAN.--port— control-channel TCP port (default8080).
| Command | Args | Description |
|---|---|---|
discover |
— | list speakers on the LAN (name ip:port) |
status |
— | dump volume / source / light / EQ / battery |
volume |
LEVEL (0..30) |
set volume |
play / pause |
— | resume / pause playback |
play-pause |
— | toggle play/pause |
next-track / previous-track |
— | skip track |
shutdown |
— | power the speaker off (no remote power-on) |
timer-shutdown |
MINUTES (0..1440) |
sleep timer (0 = off; app presets 5/15/30/60/180) |
light |
on | off |
LED strip on/off |
light-brightness |
LEVEL (0..100) |
LED brightness |
light-effect |
static | breathing | waterflow |
LED effect |
light-color |
yellow | white |
LED color |
source |
bluetooth | aux | usb | airplay |
input source |
eq-preset |
classic | monitor | game | vocal | customized |
EQ preset |
eq |
G1..G6 |
exactly 6 custom gains, tenths of a dB (-30..30 = -3.0..+3.0 dB) |
Examples
python -m edifier_es300 discover
python -m edifier_es300 status # auto-discover, then dump state
python -m edifier_es300 --host 192.168.1.123 volume 22
python -m edifier_es300 source airplay
python -m edifier_es300 light-effect breathing
python -m edifier_es300 light-color yellow
python -m edifier_es300 eq-preset vocal
python -m edifier_es300 eq -- 10 5 0 0 0 -5 # use -- so negatives aren't read as options
Note: negative EQ gains look like CLI options, so prefix the gain list with
--.
Metadata
Release files for edifier-es300 1.0.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| edifier_es300-1.0.3.tar.gz | 16.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| edifier_es300-1.0.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.4 kB
Release files / edifier_es300-1.0.3.tar.gz
| Download URL | edifier_es300-1.0.3.tar.gz |
|---|---|
| Size | 16.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
879738f8fc4e29037c975e932bf23723896690dd4f5c873884dc06ec03a1d331
|
|
BLAKE2b-256 checksum How to use checksums |
57a8bb3db492e4265c9d51fbb04e34496c393ed00666acb028c79fab53b68b2e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / edifier_es300-1.0.3-py3-none-any.whl
| Download URL | edifier_es300-1.0.3-py3-none-any.whl |
|---|---|
| Size | 14.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6bb8d24092e9c8b6856698431744d5c1709d68ea4e78576df4110cf28f7b3aad
|
|
BLAKE2b-256 checksum How to use checksums |
17b7d505bda825fc3e6c5f3f90e49873b7e50c05af5e888c4c1606d00df51c58
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|