jbl-ma-control
A pure-Python client library for the JBL MA-series AV receivers (MA510, MA710, MA7100HP, MA9100HP) over their IP control protocol.
It implements every command and option in Harman's IP control interface and commands for JBL MA510/MA710/MA7100HP/MA9100HP (document v1.7): power, input selection, volume, mute, surround decoding, equalization, room EQ, Dolby/DTS options, streaming status, IR passthrough, reboot, factory reset and more.
- No third-party runtime dependencies.
- Fully type-hinted (ships
py.typed). - Thorough error handling with a typed exception hierarchy.
- Framing layer is transport-independent and unit-tested; the client is tested end-to-end against an in-process mock AVR.
Installation
pip install jbl-ma-control
Quick start
from jbl_ma import JBLClient, InputSource, SurroundMode
# The AVR always listens on TCP port 50000.
with JBLClient("192.168.1.50") as avr:
model = avr.initialize() # identify ourselves; returns the Model
print("Connected to", model.name)
avr.power_on()
avr.set_input_source(InputSource.HDMI1)
avr.set_volume(35) # 0..99
avr.set_surround_mode(SurroundMode.ALL_STEREO)
avr.set_bass(4) # dB, -12..12
avr.unmute()
Async
For asyncio applications (including Home Assistant) use AsyncJBLClient, which
exposes the same method names as coroutines:
import asyncio
from jbl_ma import AsyncJBLClient, InputSource
async def main():
async with AsyncJBLClient("192.168.1.50") as avr:
await avr.initialize()
await avr.power_on()
await avr.set_input_source(InputSource.HDMI1)
await avr.set_volume(35)
asyncio.run(main())
Concurrent coroutines sharing one AsyncJBLClient are safe — transactions are
serialized internally so request/response pairs never interleave.
Push updates (no polling)
The AVR sends unsolicited frames whenever its state changes from the front
panel or IR remote. AsyncJBLClient runs a background reader while connected,
so an on_event callback receives those changes as they happen — you don't
have to poll:
def on_change(frame):
print("state changed:", hex(frame.command), frame.data)
async with AsyncJBLClient("192.168.1.50", on_event=on_change) as avr:
await avr.initialize()
await asyncio.sleep(3600) # on_change fires whenever the AVR changes
on_event may be a plain function or a coroutine function. Note the protocol
has no request/response correlation id, so a state-change frame that arrives in
the exact window while a command's response is awaited may be consumed as that
response; this is a protocol limitation, not a library one.
Manual connection management
The client is a context manager; you can also manage the connection manually:
avr = JBLClient("192.168.1.50", connect=False)
avr.connect()
try:
print(avr.get_volume())
finally:
avr.close()
Protocol notes
Every transmission uses the frame format from the PDF:
Controller -> AVR : <0x23><CmdID><DataLen><Data...><0x0D>
AVR -> Controller : <0x02 0x23><CmdID><RspCode><DataLen><Data...><0x0D>
-
After connecting you should call
initialize()so the AVR registers this controller. It returns the connectedModel. -
Call
heartbeat()periodically to keep the AVR out of its auto-standby timer;initialize()works as a heartbeat too. -
reboot()is acknowledged and then the unit restarts, dropping the connection. On the MA510 (verified on hardware) it comes back up in standby rather than powered on — callpower_on()afterwards if you need it on, and allow time for it to reboot before reconnecting. -
The AVR also emits unsolicited frames when its state changes from the front panel or IR remote. Pass an
on_eventcallback to receive them:def on_change(frame): print("state change:", frame.command, frame.data) avr = JBLClient("192.168.1.50", on_event=on_change)
API overview
| Area | Methods |
|---|---|
| System | initialize(), heartbeat(), reboot(), factory_reset(), get_software_version() |
| Power | get_power(), set_power(), power_on(), standby() |
| Display | get_display_dim(), set_display_dim() |
| Source | get_input_source(), set_input_source() |
| Volume | get_volume(), set_volume(), get_mute(), set_mute(), mute(), unmute() |
| Sound | get_surround_mode(), set_surround_mode(), get_dolby_audio_mode(), set_dolby_audio_mode(), get_dialog_enhanced(), set_dialog_enhanced(), get_drc(), set_drc() |
| EQ | get_treble(), set_treble(), get_bass(), set_bass(), get_room_eq(), set_room_eq() |
| Party mode | get_party_mode(), set_party_mode(), get_party_volume(), set_party_volume() |
| Streaming | get_streaming_state() |
| IR remote | send_ir() |
All enums (InputSource, SurroundMode, DisplayDim, RoomEQ,
DolbyAudioMode, PowerState, StreamingServer, PlayState,
SoftwareModule, Model, IRCommand) and value ranges are exported from the
top-level jbl_ma package.
Model availability: some options only exist on certain models (for example HDMI 5/6, Phono, party mode, DTS Neural:X and Dirac Live). The library exposes every documented value; sending one an attached model does not support results in the AVR replying with
ParameterNotRecognizedErrororCommandInvalidError.
Error handling
All errors derive from jbl_ma.JBLError:
from jbl_ma import JBLClient, JBLCommandError, CommandInvalidError, RoomEQ
with JBLClient("192.168.1.50") as avr:
try:
avr.set_room_eq(RoomEQ.DIRAC_LIVE)
except CommandInvalidError:
print("No room-correction filter has been uploaded yet.")
except JBLCommandError as exc:
print("AVR rejected the command, code", hex(exc.code))
| Exception | Meaning |
|---|---|
JBLConnectionError |
Socket refused / reset / closed |
JBLTimeoutError |
No complete response within the timeout |
JBLProtocolError |
A malformed frame was sent or received |
CommandNotRecognizedError |
Response code 0xC1 |
ParameterNotRecognizedError |
Response code 0xC2 |
CommandInvalidError |
Response code 0xC3 (command invalid at this time) |
InvalidDataLengthError |
Response code 0xC4 |
Low-level framing
The jbl_ma.protocol module can encode and decode frames without any I/O,
which is useful for testing or building your own transport:
from jbl_ma import encode_command, decode_responses, Command
frame = encode_command(Command.MASTER_VOLUME, b"\x28") # set volume to 40
responses, leftover = decode_responses(b"\x02\x23\x06\x00\x01\x28\x0d")
Development
pip install -e ".[dev]"
pytest
License
MIT
Metadata
Release files for jbl-ma-control 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jbl_ma_control-1.2.0.tar.gz | 24.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jbl_ma_control-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 46.2 kB
Release files / jbl_ma_control-1.2.0.tar.gz
| Download URL | jbl_ma_control-1.2.0.tar.gz |
|---|---|
| Size | 24.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
53f8821e1e459b2b3514f025804562dca3104feffb703982a5b1929ce4175c41
|
|
BLAKE2b-256 checksum How to use checksums |
c65b74ae5d0c0406a29c1974b5f54b8db06e8689db00ebe54cfaa2dcd50d22dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / jbl_ma_control-1.2.0-py3-none-any.whl
| Download URL | jbl_ma_control-1.2.0-py3-none-any.whl |
|---|---|
| Size | 22.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8e6a257a9a28b0a7996548e46e2dc83d771bf08a5341842202782b60310b57eb
|
|
BLAKE2b-256 checksum How to use checksums |
367282407a6f6b518fac64e9cd8d14312cfdfbf8899e60163282f00b7cb333c5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|