Summary

The NAD M15 is a BluOS-enabled streaming component controlled via HTTP GET requests on TCP port 11000. This spec covers the BluOS Custom Integration (CI) API subset: status queries, volume, playback, queue, presets, browsing, grouping, reboot, doorbell, input selection, and Bluetooth mode.

Transport

protocols:
  - http
  - udp
addressing:
  port: 11000
  base_url: "http://<player_ip>:11000"
auth:
  type: none  # inferred: no auth procedure in source

Traits

- queryable  # inferred from /Status, /SyncStatus, /Volume queries
- levelable  # inferred from volume control commands
- routable   # inferred from input selection / direct input commands

Actions

# Each endpoint listed in the source as a distinct URL is one action.
# Parameterized endpoints (e.g. /Volume?level=N) use the {param} template.

- id: status_query
  label: Playback Status
  kind: query
  command: "GET /Status"
  params:
    - name: timeout
      type: integer
      description: Long-poll timeout in seconds (recommended 100, min 10, max ~60s)
    - name: etag
      type: string
      description: ETag from previous /Status response

- id: sync_status_query
  label: Player and Group Sync Status
  kind: query
  command: "GET /SyncStatus"
  params:
    - name: timeout
      type: integer
      description: Long-poll timeout in seconds (recommended 180)
    - name: etag
      type: string
      description: ETag from previous /SyncStatus response

- id: volume_set
  label: Set Volume (level 0-100)
  kind: action
  command: "GET /Volume?level={level}&tell_slaves={tell_slaves}"
  params:
    - name: level
      type: integer
      description: Absolute volume level 0..100
    - name: tell_slaves
      type: integer
      description: 0 = this player only, 1 = apply to all grouped players

- id: volume_set_db_absolute
  label: Set Volume (absolute dB)
  kind: action
  command: "GET /Volume?abs_db={db}&tell_slaves={tell_slaves}"
  params:
    - name: db
      type: number
      description: Volume in dB
    - name: tell_slaves
      type: integer
      description: 0 = this player only, 1 = apply to all grouped players

- id: volume_up
  label: Volume Up
  kind: action
  command: "GET /Volume?db={db_value}"
  params:
    - name: db_value
      type: number
      description: Volume step in dB (typical 2)

- id: volume_down
  label: Volume Down
  kind: action
  command: "GET /Volume?db={db_value}"
  params:
    - name: db_value
      type: number
      description: Negative volume step in dB (typical -2)

- id: volume_mute
  label: Mute
  kind: action
  command: "GET /Volume?mute=1&tell_slaves={tell_slaves}"
  params:
    - name: tell_slaves
      type: integer
      description: 0 = this player only, 1 = apply to all grouped players

- id: volume_unmute
  label: Unmute
  kind: action
  command: "GET /Volume?mute=0&tell_slaves={tell_slaves}"
  params:
    - name: tell_slaves
      type: integer
      description: 0 = this player only, 1 = apply to all grouped players

- id: play
  label: Play
  kind: action
  command: "GET /Play"
  params: []

- id: play_seek
  label: Play with Seek
  kind: action
  command: "GET /Play?seek={seconds}&id={trackid}"
  params:
    - name: seconds
      type: integer
      description: Position to seek to in the current track
    - name: trackid
      type: integer
      description: Track id in the queue

- id: play_url
  label: Play Stream URL
  kind: action
  command: "GET /Play?url={encodedStreamURL}"
  params:
    - name: encodedStreamURL
      type: string
      description: URL-encoded stream URL

- id: pause
  label: Pause
  kind: action
  command: "GET /Pause"
  params: []

- id: pause_toggle
  label: Pause Toggle
  kind: action
  command: "GET /Pause?toggle=1"
  params: []

- id: stop
  label: Stop
  kind: action
  command: "GET /Stop"
  params: []

- id: skip
  label: Skip Next
  kind: action
  command: "GET /Skip"
  params: []

- id: back
  label: Skip Back
  kind: action
  command: "GET /Back"
  params: []

- id: shuffle
  label: Shuffle Queue
  kind: action
  command: "GET /Shuffle?state={state}"
  params:
    - name: state
      type: integer
      description: 0 = disable, 1 = enable

- id: repeat
  label: Repeat Mode
  kind: action
  command: "GET /Repeat?state={state}"
  params:
    - name: state
      type: integer
      description: 0 = repeat queue, 1 = repeat track, 2 = repeat off

- id: action_skip
  label: Streaming Radio Skip
  kind: action
  command: "GET /Action?service={service}&skip={id}"
  params:
    - name: service
      type: string
      description: Service name (e.g. Slacker)
    - name: id
      type: string
      description: Track id

- id: action_love
  label: Streaming Radio Love
  kind: action
  command: "GET /Action?service={service}&love={id}"
  params:
    - name: service
      type: string
      description: Service name
    - name: id
      type: string
      description: Track id

- id: action_ban
  label: Streaming Radio Ban
  kind: action
  command: "GET /Action?service={service}&ban={id}"
  params:
    - name: service
      type: string
      description: Service name
    - name: id
      type: string
      description: Track id

- id: playlist
  label: List Play Queue
  kind: query
  command: "GET /Playlist"
  params:
    - name: length
      type: integer
      description: If 1, return only top-level queue status
    - name: start
      type: integer
      description: First entry index (0-based) for paginated listing
    - name: end
      type: integer
      description: Last entry index for paginated listing

- id: delete_track
  label: Delete Track from Queue
  kind: action
  command: "GET /Delete?id={position}"
  params:
    - name: position
      type: integer
      description: Track position in the queue

- id: move_track
  label: Move Track in Queue
  kind: action
  command: "GET /Move?new={destination}&old={origin}"
  params:
    - name: destination
      type: integer
      description: New track position
    - name: origin
      type: integer
      description: Old track position

- id: clear_queue
  label: Clear Queue
  kind: action
  command: "GET /Clear"
  params: []

- id: save_queue
  label: Save Queue as Playlist
  kind: action
  command: "GET /Save?name={playlist_name}"
  params:
    - name: playlist_name
      type: string
      description: Name for the saved playlist (URL-encode spaces as +)

- id: presets_list
  label: List Presets
  kind: query
  command: "GET /Presets"
  params: []

- id: preset_load
  label: Load Preset
  kind: action
  command: "GET /Preset?id={presetId}"
  params:
    - name: presetId
      type: string
      description: Preset id, +1 for next, -1 for previous

- id: browse
  label: Browse Content
  kind: query
  command: "GET /Browse?key={key}&withContextMenuItems={withContextMenuItems}"
  params:
    - name: key
      type: string
      description: Browse key from a prior response (URL-encoded)
    - name: withContextMenuItems
      type: integer
      description: Set to 1 to include inline context menu

- id: browse_search
  label: Search Content
  kind: query
  command: "GET /Browse?key={key}&q={searchText}"
  params:
    - name: key
      type: string
      description: searchKey from a prior response (URL-encoded)
    - name: searchText
      type: string
      description: Search query

- id: add_slave
  label: Group One Secondary Player
  kind: action
  command: "GET /AddSlave?slave={secondaryPlayerIP}&port={secondaryPlayerPort}&group={GroupName}"
  params:
    - name: secondaryPlayerIP
      type: string
      description: IP of secondary player
    - name: secondaryPlayerPort
      type: integer
      description: Port of secondary player (default 11000)
    - name: GroupName
      type: string
      description: Optional group name

- id: add_slaves
  label: Group Multiple Secondary Players
  kind: action
  command: "GET /AddSlave?slaves={secondaryPlayerIPs}&ports={secondaryPlayerPorts}"
  params:
    - name: secondaryPlayerIPs
      type: string
      description: Comma-separated secondary player IPs
    - name: secondaryPlayerPorts
      type: string
      description: Comma-separated secondary player ports

- id: remove_slave
  label: Remove One Player from Group
  kind: action
  command: "GET /RemoveSlave?slave={secondaryPlayerIP}&port={secondaryPlayerPort}"
  params:
    - name: secondaryPlayerIP
      type: string
      description: IP of secondary player
    - name: secondaryPlayerPort
      type: integer
      description: Port of secondary player

- id: remove_slaves
  label: Remove Multiple Players from Group
  kind: action
  command: "GET /RemoveSlave?slaves={secondaryPlayerIPs}&ports={secondaryPlayerPorts}"
  params:
    - name: secondaryPlayerIPs
      type: string
      description: Comma-separated secondary player IPs
    - name: secondaryPlayerPorts
      type: string
      description: Comma-separated secondary player ports

- id: reboot
  label: Soft Reboot Player
  kind: action
  command: "POST /reboot"
  params:
    - name: yes
      type: string
      description: Any value (e.g. 1)

- id: doorbell
  label: Doorbell Chime
  kind: action
  command: "GET /Doorbell?play=1"
  params: []

- id: play_input_capture
  label: Active Input Selection (Capture URL)
  kind: action
  command: "GET /Play?url={URL_value}"
  params:
    - name: URL_value
      type: string
      description: URL attribute from /RadioBrowse?service=Capture response (URL-encoded)

- id: play_input_index
  label: External Input Selection (index, firmware v3.8.0 to v4.2.0)
  kind: action
  command: "GET /Play?inputIndex={IndexId}"
  params:
    - name: IndexId
      type: integer
      description: Index of input (1-based, Bluetooth excluded) from /Settings?id=capture

- id: play_input_type_index
  label: External Input Selection (type-index, firmware v4.2.0+)
  kind: action
  command: "GET /Play?inputTypeIndex={typeIndex}"
  params:
    - name: typeIndex
      type: string
      description: Format "type-index" e.g. spdif-2. Types: spdif, analog, coax, bluetooth, arc, earc, phono, computer, aesebu, balanced, microphone. Index starts at 1.

- id: bluetooth_mode
  label: Set Bluetooth Mode
  kind: action
  command: "GET /audiomodes?bluetoothAutoplay={value}"
  params:
    - name: value
      type: integer
      description: 0 = Manual, 1 = Automatic, 2 = Guest, 3 = Disabled

Feedbacks

- id: playback_state
  type: enum
  values: [play, pause, stop, stream, connecting]
- id: volume_level
  type: integer
  description: Volume level 0..100, -1 means fixed volume
- id: volume_db
  type: number
  description: Volume level in dB
- id: mute
  type: enum
  values: [0, 1]
  description: 1 = muted, 0 = unmuted
- id: mute_volume
  type: integer
  description: Pre-mute volume level 0..100 (present when muted)
- id: mute_db
  type: number
  description: Pre-mute volume in dB (present when muted)
- id: shuffle_state
  type: enum
  values: [0, 1]
- id: repeat_state
  type: enum
  values: [0, 1, 2]
  description: 0 = repeat queue, 1 = repeat track, 2 = repeat off
- id: current_track
  type: object
  description: Album, artist, name, title1-3, song position, totlen, secs, quality, service
- id: sync_stat
  type: string
  description: Sync status id matching /Status syncStat
- id: etag
  type: string
  description: Opaque change-detection token for long polling
- id: group
  type: string
  description: Group name (when player is primary)
- id: group_volume
  type: integer
  description: Group volume 0..100 (primary only)
- id: player_id
  type: string
  description: "IP:port identifier from /SyncStatus"
- id: mac
  type: string
  description: Player network interface MAC
- id: schema_version
  type: integer
- id: battery
  type: object
  description: Optional battery info (level, charging, icon)

Variables

# UNRESOLVED: source documents queryable state (volume, mute, mode) but no settable
# parameters independent of the discrete actions above. Section retained per template.

Events

# UNRESOLVED: source describes long-polling change notifications via etag/syncStat but
# no unsolicited push events are documented.

Macros

# UNRESOLVED: source does not document user-defined multi-step sequences.

Safety

confirmation_required_for: []
interlocks: []
# UNRESOLVED: source contains no safety warnings, interlocks, or power-on sequencing.

Notes

BluOS uses HTTP GET on port 11000 by default (CI580 uses 11000/11010/11020/11030 for its four streamer nodes). Port discovery recommended via mDNS (musc.tcp, musp.tcp) or LSDP (UDP broadcast on port 11430, ASCII magic "LSDP", protocol version 1).

Polling rate: cap at one request per 30 seconds without long-polling; do not issue two consecutive long-poll requests for the same resource within 1 second.

Long-polling: /Status and /SyncStatus accept timeout and etag parameters; result returns early on etag change or after timeout.

The operator-supplied "Known protocol: RS-232C" does not match this source. The source exclusively documents the BluOS HTTP CI API and LSDP UDP discovery protocol. No serial commands, baud rate, or pinout appear anywhere in the document. If RS-232 control is required for the M15, a separate source document must be supplied.

The available volume range (typically -80..0 dB) is configured via BluOS Controller app (Settings -> Player -> Audio); the API clamps requested levels to that range.

Input selection has two request forms: /Play?inputIndex=N for firmware between v3.8.0 and v4.2.0 (Bluetooth excluded), and /Play?inputTypeIndex=type-index for firmware v4.2.0 or newer. Types: spdif, analog, coax, bluetooth, arc, earc, phono, computer, aesebu, balanced, microphone.

/AddSlave and /RemoveSlave may be used with either slave+port (single) or slaves+ports (comma-separated, multiple). Player grouping: BluOS supports default grouping (covered) and fixed grouping (out of scope per source).

LSDP discovery: UDP broadcast, port 11430, magic "LSDP", version 1. Startup burst: 7 packets at absolute times [0,1,2,3,5,7,10s] + 0-250ms random. Steady-state announce: 57s + 0-6s random. Class IDs: 0x0001 BluOS Player (musc.tcp), 0x0002 BluOS Server, 0x0003 multi-zone secondary (musp.tcp), 0x0008 BluOS Hub, 0xFFFF query all.

Provenance

source_domains:
  - bluos.io
source_urls:
  - https://bluos.io/wp-content/uploads/2025/06/BluOS-Custom-Integration-API_v1.7.pdf
retrieved_at: 2026-06-12T02:27:50.909Z
last_checked_at: 2026-07-21T23:32:31.932Z

Verification Summary

verdict: verified
checked_at: 2026-07-21T23:32:31.932Z
matched_actions: 40
action_count: 40
confidence: medium
summary: "All 40 spec actions map literally to documented BluOS HTTP endpoints; only two helper query endpoints are unrepresented, well under the short threshold. (7 unresolved item(s) noted in Known Gaps.)"

Known Gaps

- "/RadioBrowse?service=Capture"
- "/Settings?id=capture&schemaVersion=32"
- "RS-232 protocol listed by operator as \"Known protocol\" but source document exclusively describes BluOS HTTP API and LSDP UDP discovery. No serial commands, baud rate, or pinout found in source."
- "source documents queryable state (volume, mute, mode) but no settable"
- "source describes long-polling change notifications via etag/syncStat but"
- "source does not document user-defined multi-step sequences."
- "source contains no safety warnings, interlocks, or power-on sequencing."
- "firmware version compatibility not stated in source. Source distinguishes \"newer than v3.8.0 and older than v4.2.0\" vs \"v4.2.0 or newer\" only for /Play?inputIndex."
- "M15-specific quirks (e.g. which input types are physically present) not stated; source describes a generic BluOS player API."

From the AI4AV catalog (https://ai4av.net) · ODbL-1.0