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!