Skip to content

Instantly share code, notes, and snippets.

@daemonp
Last active August 31, 2024 08:09
Show Gist options
  • Save daemonp/0ff629f34701b8da210fe932e4f17f8a to your computer and use it in GitHub Desktop.
Save daemonp/0ff629f34701b8da210fe932e4f17f8a to your computer and use it in GitHub Desktop.

McIntosh C2500 Integration with ESPHome and Home Assistant

The gubbins and configuration files for integrating a McIntosh C2500 amplifier (it should work with all rs232 McIntosh's) with Home Assistant using ESPHome.

ESPHome YAML (mcintosh.yaml)

# Filename: mcintosh.yaml

substitutions:
  device_name: mcintosh
  friendly_name: "McIntosh C2500"
  timezone: "Europe/London"

esphome:
  name: ${device_name}
  platform: ESP8266
  board: d1_mini
  includes:
    - mcintosh_media_player.h

wifi:
  ssid: iot
  password: !secret wifi_password
  ap:
    ssid: ${friendly_name}_AP

logger:
  level: VERBOSE
  baud_rate: 0
  hardware_uart: UART1

api:
  reboot_timeout: 300s

ota:
  platform: esphome

web_server:
  port: 80

time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: ${timezone}

uart:
  id: mcintosh_uart
  tx_pin: D3
  rx_pin: D4
  baud_rate: 115200

custom_component:
  - lambda: |-
      auto mcintosh_player = new McIntoshMediaPlayer(id(mcintosh_uart));
      App.register_component(mcintosh_player);
      return {mcintosh_player};
    id: mcintosh_player

sensor:
  - platform: template
    name: "McIntosh Volume"
    lambda: |-
      auto* player = static_cast<McIntoshMediaPlayer*>(id(mcintosh_player).get_component(0)); 
      return player->get_volume() * 100.0f;
    unit_of_measurement: "%"
    icon: "mdi:volume-high"
    update_interval: 60s

text_sensor:
  - platform: template
    name: "McIntosh Source"
    lambda: |-
      auto* player = static_cast<McIntoshMediaPlayer*>(id(mcintosh_player).get_component(0));
      return player->get_source();
    icon: "mdi:audio-input-rca"
    update_interval: 60s

number:
  - platform: template
    name: "McIntosh Volume Control"
    min_value: 0
    max_value: 100
    step: 1
    lambda: |-
      auto* player = static_cast<McIntoshMediaPlayer*>(id(mcintosh_player).get_component(0));
      return player->get_volume() * 100.0f;
    set_action:
      lambda: |-
        auto* player = static_cast<McIntoshMediaPlayer*>(id(mcintosh_player).get_component(0)); 
        player->set_volume(x / 100.0f);

select:
  - platform: template
    name: "McIntosh Source Select"
    options:
      - "MC"
      - "MM"
      - "CD1"
      - "CD2"
      - "DVD"
      - "AUX"
      - "SERVER"
      - "D2A"
      - "TUNER_AM"
      - "TUNER_FM"
    initial_option: "MC"
    optimistic: true
    set_action:
      lambda: |-
        auto* player = static_cast<McIntoshMediaPlayer*>(id(mcintosh_player).get_component(0));
        player->set_source(x);

button:
  - platform: template
    name: "McIntosh Volume Up"
    icon: "mdi:volume-plus"
    on_press:
      lambda: |-
        auto* player = static_cast<McIntoshMediaPlayer*>(id(mcintosh_player).get_component(0));
        player->volume_up();

  - platform: template
    name: "McIntosh Volume Down"
    icon: "mdi:volume-minus"
    on_press:
      lambda: |-
        auto* player = static_cast<McIntoshMediaPlayer*>(id(mcintosh_player).get_component(0));
        player->volume_down();

interval:
  - interval: 60s
    then:
      lambda: |-
        auto* player = static_cast<McIntoshMediaPlayer*>(id(mcintosh_player).get_component(0));
        player->query_state();

switch:
  - platform: template
    name: "McIntosh Power"
    icon: "mdi:power"
    optimistic: true
    turn_on_action:
      lambda: |-
          auto* player = static_cast<McIntoshMediaPlayer*>(id(mcintosh_player).get_component(0));
          player->power_on();
    turn_off_action:
      lambda: |-
          auto* player = static_cast<McIntoshMediaPlayer*>(id(mcintosh_player).get_component(0));
          player->power_off();

ESPHome C++ Code (mcintosh_media_player.h)

// Filename: mcintosh_media_player.h
#pragma once

#include "esphome.h"
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"

class McIntoshMediaPlayer : public Component {
 public:
  McIntoshMediaPlayer(uart::UARTComponent *parent) : uart_(parent) {}

  void setup() override {
    // Initial setup
    query_state();
  }

  void loop() override {
    if (uart_->available()) {
      std::string response;
      while (uart_->available()) {
        uint8_t byte;
        if (uart_->read_byte(&byte)) {
          response += (char)byte;
        }
      }
      process_response(response);
    }
  }

  void query_state() {
    uart_->write_str("(QRY)\r");
  }

  void set_volume(float volume) {
    int vol = volume * 100;
    char command[20];
    sprintf(command, "(VST Z1 %d)\r", vol);
    uart_->write_str(command);
  }

  void volume_up() {
    uart_->write_str("(VUP Z1)\r");
  }

  void volume_down() {
    uart_->write_str("(VDN Z1)\r");
  }

  void set_source(const std::string &source) {
    int input = source_to_input(source);
    char command[20];
    sprintf(command, "(INP Z1 %d)\r", input);
    uart_->write_str(command);
  }

  void power_on() {
    uart_->write_str("(PON Z1)\r");
    delay(100); 
    uart_->write_str("(PON Z1)\r");
  }

  void power_off() {
    uart_->write_str("(POF Z1)\r");
  }

  float get_volume() const { return volume_; }
  std::string get_source() const { return source_; }

 protected:
  uart::UARTComponent *uart_;
  float volume_ = 0.0f;
  std::string source_ = "UNKNOWN";

  void process_response(const std::string &response) {
    if (response.find("VST") != std::string::npos) {
      // Process volume status
      int vol;
      if (sscanf(response.c_str(), "(VST Z1 %d)", &vol) == 1) {
        volume_ = vol / 100.0f;
        ESP_LOGD("McIntosh", "Volume updated: %.2f", volume_);
      }
    } else if (response.find("INP") != std::string::npos) {
      // Process input status
      int input;
      if (sscanf(response.c_str(), "(INP Z1 %d)", &input) == 1) {
        source_ = input_to_source(input);
        ESP_LOGD("McIntosh", "Source updated: %s", source_.c_str());
      }
    }
  }

  std::string input_to_source(int input) {
    switch (input) {
      case 1: return "MC";
      case 2: return "MM";
      case 3: return "CD1";
      case 4: return "CD2";
      case 5: return "DVD";
      case 6: return "AUX";
      case 7: return "SERVER";
      case 8: return "D2A";
      case 9: return "TUNER_AM";
      case 10: return "TUNER_FM";
      default: return "UNKNOWN";
    }
  }

  int source_to_input(const std::string &source) {
    if (source == "MC") return 1;
    if (source == "MM") return 2;
    if (source == "CD1") return 3;
    if (source == "CD2") return 4;
    if (source == "DVD") return 5;
    if (source == "AUX") return 6;
    if (source == "SERVER") return 7;
    if (source == "D2A") return 8;
    if (source == "TUNER_AM") return 9;
    if (source == "TUNER_FM") return 10;
    return 1; // Default to MC if unknown
  }
};

Home Assistant YAML (configuration.yaml)

media_player:
  - platform: universal
    supported_features: 
      SUPPORT_VOLUME_SET | 
      SUPPORT_VOLUME_STEP | 
      SUPPORT_SELECT_SOURCE | 
      SUPPORT_TURN_ON | 
      SUPPORT_TURN_OFF
    name: McIntosh C2500
    volume_level: sensor.mcintosh_volume
    volume_set: number.mcintosh_volume_control
    source: sensor.mcintosh_source
    source_select: select.mcintosh_source_select
    turn_on: switch.mcintosh_power
    turn_off: switch.mcintosh_power
    attributes:
      state: switch.mcintosh_power
    commands:
      play_pause:
        service: media_player.media_play_pause
        target:
          entity_id: media_player.livingroom 
      volume_up: 
        service: number.set_value
        data_template:
          entity_id: number.mcintosh_volume_control
          value: "{{ (states('number.mcintosh_volume_control') | float) + 1 }}"
      volume_down:
        service: number.set_value
        data_template:
          entity_id: number.mcintosh_volume_control
          value: "{{ (states('number.mcintosh_volume_control') | float) - 1 }}"
      select_source:
        service: select.select_option
        data_template:
          entity_id: select.mcintosh_source_select
          option: "{{ source }}"
      turn_on:
        service: switch.turn_on
        target:
          entity_id: switch.mcintosh_power
      turn_off:
        service: switch.turn_off
        target:
          entity_id: switch.mcintosh_power

Lovelace UI YAML (card.yaml)

type: vertical-stack
cards:
  - type: entities
    title: McIntosh Controls
    entities:
      - entity: switch.mcintosh_power
        name: Power
      - entity: number.mcintosh_volume_control
        name: Volume
      - type: button
        name: Volume Up
        tap_action:
          action: call-service
          service: number.set_value
          service_data:
            entity_id: number.mcintosh_volume_control
            value: "{{ (states('number.mcintosh_volume_control') | float) + 1 }}"
      - type: button
        name: Volume Down
        tap_action:
          action: call-service
          service: number.set_value
          service_data:
            entity_id: number.mcintosh_volume_control
            value: "{{ (states('number.mcintosh_volume_control') | float) - 1 }}"
      - entity: select.mcintosh_source_select
        name: Source

Instructions:

  1. ESPHome:
    • Create a new ESPHome project and replace the contents of the mcintosh.yaml file with the code provided above.
    • Create a new file named mcintosh_media_player.h and paste the C++ code.
    • Adjust the substitutions (device name, Wi-Fi credentials, MQTT broker) according to your environment.
    • Flash the code to your ESP8266 device.

Notes:

  • This integration assumes that your McIntosh C2500 amplifier is connected to the ESP8266 via RS232 on pins D3 (TX) and D4 (RX).
  • The baud rate is set to 115200 in both the ESPHome and C++ code. Adjust if necessary.
  • This code provides basic functionality for volume control, source selection, and power on/off. You can extend it to include other features supported by your McIntosh C2500 amplifier.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment