Cambridge Audio CXN and CXA media_player

Hi there.

I recently bought a Cambridge Audio CXA81 amp and CXN streamer.
I noticed there was no integrations for either of them, so I decided to start writing my own. I have no Python experience whatsoever, and the resulting components were created by looking at the code of other components. It’s still early days but so far the components have been working without much issues.

If anyone is interested to test them out, please let me know here, and I will share what I have on github.

EDIT:
Here’s the CXA component: https://github.com/lievencoghe/cambridge_cxa/
And the CXN component: https://github.com/lievencoghe/cambridge_cxn/

Thanks,
Lieven

1 Like

Hi @lievencoghe,

You’ve done a great job, that I was looking at since last week after buying CXA81 myself :slight_smile: I also want to control it via HA. but don’t have much experience in Python, hence asking here.

What is your setup? do you have HA on same device or a separate rPi that is hooked to your CXA?

In my config I have RS232 connected directly to my HA instance on rPi 3B+ via USB and other end to CXA81. I’ve modified media_player.py to point to ‘tty=/dev/ttyUSB1’ as ttyUSB0 is used by zigbee controller, but the integration did not work, unfortunately. I believe it tries to ssh connect to itself and fails.

Is it possible in your integration to create an option to connect and control the amp directly via HA serial component instead of remotely ssh’ing to another device?

Regards,
Lauris

Excellent work!! I have an Azure 751r that I would love to control over RS232 (currently using broadlink) I had a go at butchering your code and adding in the commands I require (Im a mechanic so programming is not my forte). I think Ive changed the correct commands. However I cant see in your code where I can change the volume up and down command/replies.
How would I go about changing them? Thanks in advance

EDIT: Also your sound modes. Ive had a look through the documentation and cant find where you get 25 from in the amplifier commands

Hi !

I would really like to test that feature!
I have CXN

After copying the files in custom_components and configuring the configuration.yaml, I get this

Logger: homeassistant.components.media_player
Source: custom_components/cambridge_cxn/media_player.py:145
Integration: Lecteur multimédia (documentation, issues)
First occurred: 13:35:52 (1 occurrences)
Last logged: 13:35:52

Error while setting up cambridge_cxn platform for media_player
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/entity_platform.py", line 199, in _async_setup_platform
    await asyncio.shield(task)
  File "/usr/local/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/custom_components/cambridge_cxn/media_player.py", line 70, in setup_platform
    add_devices([CambridgeCXNDevice(host, name)])
  File "/config/custom_components/cambridge_cxn/media_player.py", line 97, in __init__
    self.update()
  File "/config/custom_components/cambridge_cxn/media_player.py", line 145, in update
    json.loads(
KeyError: 'volume_percent'

configuration.yaml

media_player:
  - platform: cambridge_cxn
    host: 192.168.1.157
    name: Cambridge CXN

Thanks !

Hi!

I am having the exact same issue as you.

Did you manage to find a solution?

Hi !
I found that the component doesn’t deal with the fact that I disabled the volume management on my cxn… Quick dirty fix; I deleted everything regarding the volume ! :stuck_out_tongue

here is my file /config/custom_components/cambridge_cxn/media_player.py

"""
Support for interface with a Cambridge Audio CXN media player.

For more details about this platform, please refer to the documentation at
https://github.com/lievencoghe/cambridge_cxn
"""

import json
import logging
import urllib.request
import requests
import uuid
import voluptuous as vol

from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA

from homeassistant.components.media_player.const import (
    SUPPORT_PAUSE,
    SUPPORT_PLAY,
    SUPPORT_STOP,
    SUPPORT_PREVIOUS_TRACK,
    SUPPORT_NEXT_TRACK,
    SUPPORT_SELECT_SOURCE,
    SUPPORT_TURN_OFF,
    SUPPORT_TURN_ON,
)

from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING, STATE_IDLE, STATE_STANDBY
import homeassistant.helpers.config_validation as cv

__version__ = "0.1"

_LOGGER = logging.getLogger(__name__)

SUPPORT_CXN = (
    SUPPORT_PAUSE
    | SUPPORT_PLAY
    | SUPPORT_STOP
    | SUPPORT_PREVIOUS_TRACK
    | SUPPORT_NEXT_TRACK
    | SUPPORT_SELECT_SOURCE
    | SUPPORT_TURN_OFF
    | SUPPORT_TURN_ON
)

DEFAULT_NAME = "Cambridge Audio CXN"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Required(CONF_HOST): cv.string,
        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    }
)


def setup_platform(hass, config, add_devices, discovery_info=None):
    host = config.get(CONF_HOST)
    name = config.get(CONF_NAME)

    if host is None:
        _LOGGER.error("No Cambridge CXN IP address found in configuration file")
        return

    add_devices([CambridgeCXNDevice(host, name)])


class CambridgeCXNDevice(MediaPlayerDevice):
    def __init__(self, host, name):
        """Initialize the Cambridge CXN."""
        _LOGGER.info("Setting up Cambridge CXN")
        self._host = host
        self._mediasource = ""
        self._name = name
        self._pwstate = "NETWORK"
        self._should_setup_sources = True
        self._source_list = None
        self._source_list_reverse = None
        self._state = STATE_OFF
        self._media_title = None
        self._media_artist = None
        self._artwork_url = None

        _LOGGER.debug(
            "Set up Cambridge CXN with IP: %s", host,
        )

        self.update()

    def _setup_sources(self):
        _LOGGER.debug("Setting up CXN sources")
        sources = json.loads(
            urllib.request.urlopen(
                "http://" + self._host + "/smoip/system/sources"
            ).read()
        )["data"]
        sources2 = sources.get("sources")
        self._source_list = {}
        self._source_list_reverse = {}
        for i in sources2:
            _LOGGER.debug("Setting up CXN sources... %s", i["id"])
            source = i["id"]
            configured_name = i["name"]
            self._source_list[source] = configured_name
            self._source_list_reverse[configured_name] = source

    def media_play_pause(self):
        self.url_command("smoip/zone/play_control?action=toggle")

    def media_next_track(self):
        self.url_command("smoip/zone/play_control?skip_track=1")

    def media_previous_track(self):
        self.url_command("smoip/zone/play_control?skip_track=-1")

    def update(self):
        self._pwstate = json.loads(
            urllib.request.urlopen(
                "http://" + self._host + "/smoip/system/power"
            ).read()
        )["data"]["power"]
        self._mediasource = json.loads(
            urllib.request.urlopen("http://" + self._host + "/smoip/zone/state").read()
        )["data"]["source"]
        playstate = urllib.request.urlopen("http://" + self._host + "/smoip/zone/play_state").read()
        try:
            self._media_title = json.loads(playstate)["data"]["metadata"]["title"] 
        except:
            self._media_title = None
        try:
            self._media_artist = json.loads(playstate)["data"]["metadata"]["artist"]
        except:
            self._media_artist = None
        try:
            urllib.request.urlretrieve(json.loads(playstate)["data"]["metadata"]["art_url"], "/config/www/cxn-artwork.jpg")
            self._artwork_url = "/local/cxn-artwork.jpg?" + str(uuid.uuid4())
        except:
            self._artwork_url = None
        self._state = json.loads(playstate)["data"]["state"]
        
        if self._should_setup_sources:
            self._setup_sources()
            self._should_setup_sources = False

    def url_command(self, command):
        """Establish a telnet connection and sends `command`."""
        _LOGGER.debug("Sending command: %s", command)
        urllib.request.urlopen("http://" + self._host + "/" + command).read()

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def source_list(self):
        return sorted(list(self._source_list.values()))

    @property
    def state(self):
        if self._pwstate == "NETWORK":
            return STATE_OFF
        if self._pwstate == "ON":
            if self._state == "play":
                return STATE_PLAYING
            elif self._state == "pause":
                return STATE_PAUSED
            elif self._state == "stop":
                return STATE_IDLE
            else:
                return STATE_ON
        return None

    @property
    def supported_features(self):
        return SUPPORT_CXN

    @property
    def media_title(self):
        return self._media_title

    @property
    def media_artist(self):
        return self._media_artist

    @property
    def media_image_url(self):
        _LOGGER.debug("CXN Artwork URL: %s", self._artwork_url)
        return self._artwork_url

    def select_source(self, source):
        reverse_source = self._source_list_reverse[source]
        if reverse_source in [
            "AIRPLAY",
            "CAST",
            "IR",
            "MEDIA_PLAYER",
            "SPDIF_COAX",
            "SPDIF_TOSLINK",
            "SPOTIFY",
            "USB_AUDIO",
            "ROON"
        ]:
            self.url_command("smoip/zone/state?source=" + reverse_source)
        else:
            self.url_command("smoip/zone/recall_preset?preset=" + reverse_source)

    #def set_volume_level(self, volume):
       # vol_str = "smoip/zone/state?volume_percent=" + str(int(volume * 100))
       # self.url_command(vol_str)

    def turn_on(self):
        self.url_command("smoip/system/power?power=ON")

    def turn_off(self):
        self.url_command("smoip/system/power?power=NETWORK")

1 Like

Thank you worked like a charm! I did some minor tweaks and left volume up/down that allows me to control the volume of my CXA Amp :smile:

def volume_up(self):
        self.url_command("smoip/zone/state?volume_step_change=+1")

    def volume_down(self):
        self.url_command("smoip/zone/state?volume_step_change=-1")

Brand new to Home Assistant and I find myself in the same position, trying to get my CXA and CXN to work with in. The above suggestions are brilliant, I have the CXN now responding to commands. Question: Is a Raspberry Pi with RS232 connection necessary to control the CXA, or can it be controlled via the CXN?

CXN can control only the power and the volume of CXA (when Pre-Amp mode is enabled in CXN). I’m not a dev myself, but I wish the lievencoghe RS232 CXA integration would work from the same HA instance rather than ssh’ing it to another machine. It would be to much to have another rPi dedicated just for CXA control :slight_smile:

As a workaround i created myself HA switches that send RS232 commands to CXA, but a native integration with feedback statuses would be an awesome feature!

Please help me, its not working for me :c

Platform error media_player.cambridge_cxa - Integration ‘cambridge_cxa’ not found.

1 Like

I have the same problem with
Platform error media_player.cambridge_cxa - Integration ‘cambridge_cxa’ not found.

Have anyone please tip for this? Thanks much!!

Updated to work with 2022.4.6

Also HACS compatible

Any chance you could say what changes need to be made to lievencoghe’s CXA component to get it working as well? I’d be willing to make changes and test this out, but I wouldn’t know how to figure out what to fix.

Added data to manifest.json. Also updated MediaPlayerEntity.

Here you go. Please test and let us know if it work. I don’t have a CXA to test.

1 Like

That’s awesome, thank you!

I tested this a fair amount yesterday with mixed success. I can confirm that Home Assistant can make the SSH connection and that the serial commands do work over this SSH connection.

The problem I am running into is that I start Home Assistant, I get the error:

Setup of platform cambridge_cxa is taking longer than 60 seconds. Startup will proceed without waiting any longer.

I’ll skip the troubleshooting I did, but I believe the issue is with this component occurs when it it setting itself up, it sends a serial command to get the current power state of the amp. The issue is that the amp does not give any response to the #01,01 command. That causes the setup of this component to hang and never complete.

I have verified this behavior outside of the HA component. When I manually send the #01,01 command, I get no response. For comparison, sending the commands to change inputs does cause the amp to produce a response. However, I’ve never seen the amp respond to the power state command (tested when amp was on and off).

For reference, here is the CXA81/CXA61 serial connection documentation. The relevant bit starts on page 4, and from what I can tell, it clearly lists how to query the amp’s power state (1st table, 1st row) and how the amp should respond (2nd table, 1st row).

Not sure why the amp is not responding to that command. Everything else seems to work, that makes me think that there must be a different way to get that data (rather than thinking the amp is broken or documentation is wrong).

I’ll try to play around with this some more, I feel like it’s really close to working.

Hi,
I installed this Cambridge_cxn component via HACS. Then restarted HA.
In the HACS integrations the Cambridge Audio CXN integration is visible.

Assumed I would see a kind of a custom media_player card, but don’t see one.

Any idea to solve this, did I forget something?

André

Have you added to config.yaml?

Also, theres no integration card. You should see a new media_player though in dev tools.

Adding the config did the trick! Thanks.

rgds, André

Thanks for creating this @lievencoghe . Is there any documentation on the CXN endpoints? I’m having an issue with the back/next options where I receive an error when this code runs “self.url_command(“smoip/zone/play_control?skip_track=1”)”:

{
"code": 114,
"message": "'skip_track' not available"
}

Hello everyone,

AWESOME WORK!!! may I ask what is needed to get this up and running? I raspberry pi with a vanilla Debian deployed on it?

I have a Volumio instance running on a Raspberry which sits nearby the CXA. It would be awesome if I could use that device and spare a new one.

Also, one question: the only component that can be installed through HACS is only the one for the CXN model, right?
thanks