Skip to content

Add ADAM Audio integration#166511

Draft
Perhan35 wants to merge 1 commit intohome-assistant:devfrom
Perhan35:Perhan35/add-adam-audio-integration
Draft

Add ADAM Audio integration#166511
Perhan35 wants to merge 1 commit intohome-assistant:devfrom
Perhan35:Perhan35/add-adam-audio-integration

Conversation

@Perhan35
Copy link
Copy Markdown

@Perhan35 Perhan35 commented Mar 25, 2026

Proposed change

Adds ADAM Audio A-Series speakers integration with auto-discovery.
This integration will allow the user to command their speakers from Home Assistant, and without the need of the A Control App.
Tested and fully functioning locally with ADAM Audio A4V speakers.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Btw, this PR is proposed with the help of Claude and Gemini.

Copilot AI review requested due to automatic review settings March 25, 2026 18:40
Copy link
Copy Markdown

@home-assistant home-assistant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Perhan35

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant home-assistant bot marked this pull request as draft March 25, 2026 18:40
@home-assistant
Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Copy link
Copy Markdown

@home-assistant home-assistant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Perhan35 Perhan35 force-pushed the Perhan35/add-adam-audio-integration branch from cb9ed0c to 326f756 Compare March 25, 2026 18:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Home Assistant core integration for ADAM Audio A‑Series speakers, including config flow + zeroconf discovery, a polling coordinator + client wrapper, and switch/select/number platforms with tests.

Changes:

  • Introduce adam_audio integration with config flow (manual + zeroconf) and per-device coordinator/client.
  • Add Switch (mute/sleep), Select (input/voicing), and Number (EQ) entities plus an “All Speakers” group device.
  • Add full test suite for setup/unload, config flow, client behavior, and entity behavior; add dependency + generated metadata updates.

Reviewed changes

Copilot reviewed 22 out of 25 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
homeassistant/components/adam_audio/init.py Integration setup/unload, platform forwarding, coordinator registry
homeassistant/components/adam_audio/client.py Executor-based UDP client wrapper with retry + state polling
homeassistant/components/adam_audio/config_flow.py Manual + zeroconf config flow and connection probing
homeassistant/components/adam_audio/const.py Integration constants and entity option mappings
homeassistant/components/adam_audio/coordinator.py DataUpdateCoordinator implementation + device metadata
homeassistant/components/adam_audio/entity.py Base per-device + group entity classes
homeassistant/components/adam_audio/number.py EQ number entities (per-device + group)
homeassistant/components/adam_audio/select.py Input/voicing select entities (per-device + group)
homeassistant/components/adam_audio/switch.py Mute/sleep switch entities (per-device + group)
homeassistant/components/adam_audio/manifest.json Integration manifest, requirements, zeroconf match
homeassistant/components/adam_audio/strings.json UI strings for flow + entities
homeassistant/components/adam_audio/quality_scale.yaml Quality scale declaration
tests/components/adam_audio/* New tests + fixtures for config flow, setup, entities, client
requirements_all.txt / requirements_test_all.txt Add pyadamaudiocontroller==1.0.0
homeassistant/generated/* Generated integration + config_flow + zeroconf registries
CODEOWNERS Add code ownership for integration + tests

Comment on lines +40 to +47
# Create group entities exactly once per HA lifecycle.
if not integration_data.group_switches_added:
integration_data.group_switches_added = True
entities += [
AdamAudioGroupSleepSwitch(hass),
AdamAudioGroupMuteSwitch(hass),
]

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the "All Speakers" group switches aren’t tied to a single config entry; gating creation behind group_switches_added can make the group entities disappear permanently if the entry that first created them is unloaded while other entries remain loaded.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +146
async def async_turn_on(self, **kwargs: Any) -> None:
"""Mute all speakers."""
coordinators = self._coordinators()
await asyncio.gather(*(c.client.async_set_mute(True) for c in coordinators))
for c in coordinators:
c.async_set_updated_data(c.client.state)
self.async_write_ha_state()
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle partial failures when fanning out group commands by using asyncio.gather(..., return_exceptions=True) (and then logging/marking failures) so one offline speaker doesn’t cause the whole service call to raise and skip updating the remaining speakers.

Copilot uses AI. Check for mistakes.
TREBLE_MIN = -1
TREBLE_MAX = 1
EQ_STEP = 1
EQ_UNIT = ""
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expose the EQ controls’ unit as decibels instead of an empty string; EQ_UNIT = "" makes the Number entities unit-less even though the values are documented as dB steps.

Suggested change
EQ_UNIT = ""
EQ_UNIT = "dB"

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +55
if not integration_data.group_selects_added:
integration_data.group_selects_added = True
entities += [
AdamAudioGroupVoicingSelect(hass),
AdamAudioGroupInputSelect(hass),
]
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the "All Speakers" group selects aren’t tied to a single config entry; gating creation behind group_selects_added can make the group entities disappear if the entry that originally created them is unloaded while other entries remain loaded.

Suggested change
if not integration_data.group_selects_added:
integration_data.group_selects_added = True
entities += [
AdamAudioGroupVoicingSelect(hass),
AdamAudioGroupInputSelect(hass),
]
entities += [
AdamAudioGroupVoicingSelect(hass),
AdamAudioGroupInputSelect(hass),
]

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +143
async def async_select_option(self, option: str) -> None:
"""Set the input source on all speakers."""
value = INPUT_TO_INT[option]
coordinators = self._coordinators()
await asyncio.gather(*(c.client.async_set_input(value) for c in coordinators))
for c in coordinators:
c.async_set_updated_data(c.client.state)
self.async_write_ha_state()
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle partial failures when fanning out group select changes by using asyncio.gather(..., return_exceptions=True) and dealing with exceptions so one unreachable speaker doesn’t fail the entire operation and leave the group state inconsistent.

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +128
integration_data: AdamAudioIntegrationData = hass.data[DOMAIN]

entities: list[NumberEntity] = [
AdamAudioNumber(coordinator, desc) for desc in _NUMBER_DESCRIPTORS
]

if not integration_data.group_numbers_added:
integration_data.group_numbers_added = True
entities += [AdamAudioGroupNumber(hass, desc) for desc in _NUMBER_DESCRIPTORS]

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the "All Speakers" group numbers aren’t tied to a single config entry; gating creation behind group_numbers_added can make the group entities disappear if the entry that originally created them is unloaded while other entries remain loaded.

Suggested change
integration_data: AdamAudioIntegrationData = hass.data[DOMAIN]
entities: list[NumberEntity] = [
AdamAudioNumber(coordinator, desc) for desc in _NUMBER_DESCRIPTORS
]
if not integration_data.group_numbers_added:
integration_data.group_numbers_added = True
entities += [AdamAudioGroupNumber(hass, desc) for desc in _NUMBER_DESCRIPTORS]
entities: list[NumberEntity] = [
AdamAudioNumber(coordinator, desc) for desc in _NUMBER_DESCRIPTORS
] + [AdamAudioGroupNumber(hass, desc) for desc in _NUMBER_DESCRIPTORS]

Copilot uses AI. Check for mistakes.
Comment on lines +224 to +226
await asyncio.gather(
*(getattr(c.client, self._desc.setter_name)(value) for c in coordinators)
)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle partial failures when fanning out group number updates by using asyncio.gather(..., return_exceptions=True) and processing exceptions so one speaker error doesn’t abort the whole call and prevent other speakers from updating.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +65
self.device_unique_id: str = entry.data[CONF_DEVICE_NAME]
self.device_description: str = entry.data.get(CONF_DESCRIPTION, "ADAM Audio")
self.device_serial: str = entry.data.get(CONF_SERIAL, "")
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a truly unique identifier (preferably the device serial when available) for device_unique_id since it feeds entity unique_ids; relying on CONF_DEVICE_NAME risks collisions if the reported name is not globally unique across speakers.

Suggested change
self.device_unique_id: str = entry.data[CONF_DEVICE_NAME]
self.device_description: str = entry.data.get(CONF_DESCRIPTION, "ADAM Audio")
self.device_serial: str = entry.data.get(CONF_SERIAL, "")
self.device_description: str = entry.data.get(CONF_DESCRIPTION, "ADAM Audio")
self.device_serial: str = entry.data.get(CONF_SERIAL, "")
# Use a stable, truly unique identifier (prefer serial, fall back to name)
self.device_unique_id: str = self.device_serial or entry.data[CONF_DEVICE_NAME]

Copilot uses AI. Check for mistakes.
• After each SET, a read-back verification confirms the device accepted the
change. If verification fails, the command is retried up to MAX_RETRIES
times with RETRY_DELAY seconds between attempts.
• ``async_fetch_state()`` polls all 9 GET commands from the device and
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Align the client docstring with the actual implementation; the module docs mention polling 9 GET commands, but _fetch_state_blocking currently expects/consumes 8 responses (mute/sleep/input/voicing/bass/desk/presence/treble).

Suggested change
``async_fetch_state()`` polls all 9 GET commands from the device and
``async_fetch_state()`` polls all 8 GET commands from the device and

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@home-assistant home-assistant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Perhan35

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants