Pioneer AVR integration (supporting config flow and asyncio)

Quick follow-up to the ILV post above! I really wanted a few more things working properly, and honestly one of them was scratching a personal itch: the graphic EQ (“Finger EQ”) curve doesn’t stick — close and reopen the app, or just power the receiver off and back on, and it’s gone, back to flat, 9 sliders to redo from scratch. That annoyed me enough to dig further, and it turned into a nice bit of extra reverse-engineering. All on the same branch as te previous post.

Channel mute (CMU) — separate from the level itself, each speaker channel can be individually muted. Wire format is the same value-first/command-last convention as ILV (<channel><0|1>CMU to set, ?CMU<channel> to query). Implemented as its own property (channel_mute) with per-channel switch entities in Home Assistant, alongside the existing level number entities.

Found and fixed a refresh-loop crash — once ILV is in the picture, its bulk decode reports a SW1 field that the library’s own per-channel CLV refresh loop doesn’t recognise as a valid channel name, so periodic status polling threw a ValueError every cycle on this receiver. Turned out to be two things stacked: this receiver’s model auto-detection (?P/?RGD) also intermittently fails on some boots (separate, still-open, self-heals on retry), which meant a first attempt at gating the fix behind the ILV routing param wasn’t quite enough on its own — needed an unconditional filter as a backstop too.

Graphic EQ (ATB) — this is the one I really wanted, and it took two capture sessions to get right, worth sharing since I got it wrong the first time round:

  • 9-band graphic EQ, same (code−50)×0.5 dB scale as everything else on this protocol.
  • First capture only ever caught the response echo, so I couldn’t tell if SET was value-first (like ILV/CLV) or command-first (like the response) — shipped it read-only rather than guess.
  • Second capture drove all 9 bands to both slider extremes one at a time and caught genuine outbound SET frames: it’s value-first/command-last, same as everything else — "<22-char-data>ATB".
  • Range is confirmed ±6dB (code 38–62), not ±12dB like the channel levels — every band hit exactly the same two extreme codes, consistently.
  • The app’s own “Reset” button just sends the same SET with every band back at 0dB — not a separate command.
  • Now fully read+write in the library, exposed as 9 number entities in Home Assistant.

Turns out Save/Load for the graphic EQ isn’t a receiver feature at all — and this is where it got fun. A third capture (build a curve, hit Save, change it, hit Load) showed “Load” just re-sends the exact same curve as a normal ATB SET. No dedicated command, no wire traffic for “Save” whatsoever — the app is just remembering it locally on the phone. Which explains why it doesn’t survive closing the app — and separately, the receiver itself doesn’t retain the curve across a power cycle either, so even leaving the app alone, a standby off/on wipes it back to flat. So I built the equivalent entirely on the Home Assistant side instead (a couple of helper entities + scripts, below) — and unlike either the app or the receiver, it actually persists, and can be wired into an automation to fire automatically and re-apply your saved curve the instant the receiver powers back on. Small thing, but it fixed a genuine annoyance for me.

Also found and fixed a pre-existing bug, unrelated to any of the above — while capturing the AV Scaler / output-resolution feature (VTC), which is already fully implemented in aiopioneer, I noticed set_video_resolution throws AVRCommandUnavailableError for every single value, on every model. VideoResolution’s own code_map uses 2-digit zero-padded codes ("00".."09") but the default PARAM_VIDEO_RESOLUTION_MODES list was bare single digits (["0","1","3",...]), so the guard comparing the two could never match. Confirmed live before/after — this isn’t VSX-924-specific, it’ll affect anyone trying to change video resolution via this library. One-line fix, already on the branch.

Since Save/Load turned out to be app-side only, here’s the Home Assistant config I used to replicate it (and make it actually stick!) — nothing library-specific, just plain input_number helpers + scripts, in case it’s useful to anyone else:

# configuration.yaml

input_number:
  pioneer_graphic_eq_saved_63hz:
    name: Pioneer Graphic EQ Saved 63Hz
    min: -6
    max: 6
    step: 0.5
    initial: 0
  # ...repeat for 125hz, 250hz, 500hz, 1khz, 2khz, 4khz, 8khz, 16khz

# scripts.yaml
pioneer_save_graphic_eq:
  alias: Pioneer VSX-924 - Save graphic EQ
  sequence:
    - service: input_number.set_value
      target:
        entity_id: input_number.pioneer_graphic_eq_saved_63hz
      data:
        value: "{{ states('number.<your_entity>_graphic_eq_63hz') }}"
    # ...repeat per band
pioneer_load_graphic_eq:
  alias: Pioneer VSX-924 - Load graphic EQ
  sequence:
    - service: number.set_value
      target:
        entity_id: number.<your_entity>_graphic_eq_63hz
      data:
        value: "{{ states('input_number.pioneer_graphic_eq_saved_63hz') }}"
    # ...repeat per band, IN ORDER (see note below)
pioneer_reset_graphic_eq:
  alias: Pioneer VSX-924 - Reset graphic EQ
  sequence:
    - service: number.set_value
      target:
        entity_id: number.<your_entity>_graphic_eq_63hz
      data:
        value: 0
    # ...repeat per band, IN ORDER (see note below)

Gotcha that cost me a debugging round: don’t target all 9 band entities from a single number.set_value call (or use scene.turn_on for Load) — Home Assistant fires a multi-entity target concurrently, not sequentially. Since every ATB SET sends the full 9-band state built from the most recently known values, concurrent writes race: most get encoded from a stale snapshot before earlier ones’ responses land, and only one band’s change actually survives. Each step above needs to be its own separate sequential step (9 steps, one entity each) so every write waits for the previous one’s response before the next reads state. Worth knowing if you’re setting any group of related values on this AVR at once from HA — not specific to the EQ.

Still not implemented: the SSG/SSS/SSC memory-preset family spotted in an earlier capture (5 numbered slots) — that one’s for something else, probably channel levels or speaker distance presets, not looked into yet.

Happy to split any of this into separate PRs if that’s easier to review than one big branch — the ILV/CMU/SW1 fix, the ATB graphic EQ, and the video-resolution param fix are all fairly independent of each other.