├── .gitignore ├── onkyo-mqtt-test.ino ├── README.md └── onkyo-mqtt.ino /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /onkyo-mqtt-test.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // Wifi 8 | #define WIFI_SSID "" 9 | #define WIFI_PASSWORD "" 10 | 11 | // MQTT 12 | #define MQTT_SERVER "" 13 | #define MQTT_USER "" 14 | #define MQTT_PASSWORD "" 15 | #define MQTT_PORT 1883 16 | #define AMP_TOPIC "home/livingroom/amp" 17 | 18 | // RI 19 | #define ONKYO_PIN 5 20 | 21 | // OTA 22 | #define OTA_NAME "" 23 | 24 | int cmd = 0; 25 | bool pause = true; 26 | 27 | WiFiClient wifiClient; 28 | PubSubClient pubSubClient(wifiClient); 29 | OnkyoRI onkyoClient(ONKYO_PIN); 30 | 31 | void setup() { 32 | Serial.begin(115200); 33 | 34 | setupWifi(); 35 | setupOTA(); 36 | connectPubSub(); 37 | 38 | pubSubClient.publish("home/livingroom/amp/live", "Up", true); 39 | } 40 | 41 | void setupOTA() { 42 | ArduinoOTA.setHostname(OTA_NAME); 43 | 44 | ArduinoOTA.begin(); 45 | } 46 | 47 | void setupWifi() { 48 | delay(10); 49 | Serial.println(); 50 | Serial.print("Connecting to "); 51 | Serial.println(WIFI_SSID); 52 | 53 | WiFi.mode(WIFI_STA); 54 | WiFi.begin(WIFI_SSID, WIFI_PASSWORD); 55 | 56 | while (WiFi.status() != WL_CONNECTED) { 57 | delay(500); 58 | Serial.print("."); 59 | } 60 | 61 | Serial.println(""); 62 | Serial.println("WiFi connected"); 63 | Serial.println("IP address: "); 64 | Serial.println(WiFi.localIP()); 65 | } 66 | 67 | void connectPubSub() { 68 | pubSubClient.setServer(MQTT_SERVER, MQTT_PORT); 69 | pubSubClient.setCallback(callback); 70 | 71 | // Loop until we're reconnected 72 | while (!pubSubClient.connected()) { 73 | Serial.print("Attempting MQTT connection..."); 74 | // Attempt to connect 75 | if (pubSubClient.connect(MQTT_USER, MQTT_USER, MQTT_PASSWORD)) { 76 | Serial.println("connected"); 77 | pubSubClient.subscribe(AMP_TOPIC); 78 | } else { 79 | Serial.print("failed, rc="); 80 | Serial.print(pubSubClient.state()); 81 | Serial.println(" try again in 5 seconds"); 82 | // Wait 5 seconds before retrying 83 | delay(5000); 84 | } 85 | } 86 | } 87 | 88 | void loop() { 89 | if (!pubSubClient.connected()) { 90 | ESP.reset(); 91 | } 92 | 93 | ArduinoOTA.handle(); 94 | 95 | pubSubClient.loop(); 96 | 97 | if (!pause) { 98 | onkyoClient.send(cmd); 99 | 100 | pubSubClient.publish("home/livingroom/amp/live", String(cmd, HEX).c_str(), true); 101 | 102 | cmd++; 103 | } 104 | 105 | delay(500); 106 | } 107 | 108 | void callback(char* topic, byte* payload, unsigned int length) { 109 | Serial.print("Message arrived ["); 110 | Serial.print(topic); 111 | Serial.print("] "); 112 | 113 | char message[length + 1]; 114 | for (int i = 0; i < length; i++) { 115 | message[i] = (char)payload[i]; 116 | } 117 | message[length] = '\0'; 118 | Serial.print(message); 119 | Serial.print(" "); 120 | 121 | unsigned long cmd = strtoul(message, '\0', 16); 122 | Serial.println(cmd); 123 | 124 | if (String(message) == "p") { 125 | pause = !pause; 126 | } else { 127 | onkyoClient.send(cmd); 128 | pubSubClient.publish("home/livingroom/amp/live", message, true); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Onkyo-mqtt 2 | 3 | Controls an Onkyo A-9010 amplifier over MQTT using an ESP8266 dev board. Commands are sent over the amplifier's RI port. 4 | 5 | - [Onkyo-mqtt](#onkyo-mqtt) 6 | - [Setup](#setup) 7 | - [Usage](#usage) 8 | - [Home Assistant](#home-assistant) 9 | - [How does it work](#how-does-it-work) 10 | 11 | ## Setup 12 | 13 | 1. Download [this library](https://github.com/docbender/Onkyo-RI/tree/master/Onkyo_send_blocking) to `~/Documents/Arduino/libraries/OnkyoRI` 14 | 2. Fill in the required constants in `onkyo-mqtt.ino` 15 | 3. Flash `onkyo-mqtt.ino` to an ESP8266 dev board 16 | 4. Create a cable that has a 3.5mm mono audio jack at one end, and header pins at the other 17 | 5. Connect the ground of the 3.5mm jack to any GND on the dev board, connect the data to any of the GPIO pins 18 | - The labeling of the GPIO pins on the NodeMCU board [is a bit weird](https://iotbytes.wordpress.com/nodemcu-pinout/) 19 | - Change the `ONKYO_PIN` constant in `onkyo-mqtt.ino` to the desired pin. By default it's set to 5, which on the NodeMCU board is `D1` 20 | 6. Plug the 3.5mm jack in the amp's RI port 21 | 7. Depending on which sources you want to use (analogue or digital), change the RI-related switches at the back of the amp (see next section) 22 | 23 | ## Usage 24 | 25 | To send commands to the amplifier, publish the following messages to the `home/livingroom/amp/commands` topic: 26 | 27 | ```json 28 | { 29 | "state": "on" 30 | } 31 | ``` 32 | 33 | ```json 34 | { 35 | "state": "off" 36 | } 37 | ``` 38 | 39 | To turn the amp on and off. Both commands are idempotent, sending `on` to an amp that's already on doesn't do anything. 40 | 41 | ```json 42 | { 43 | "volume": "up" 44 | } 45 | ``` 46 | 47 | ```json 48 | { 49 | "volume": "down" 50 | } 51 | ``` 52 | 53 | To change the volume. 54 | 55 | ```json 56 | { 57 | "source": "dock" 58 | } 59 | ``` 60 | 61 | ```json 62 | { 63 | "source": "cd" 64 | } 65 | ``` 66 | 67 | To change the source. The actual sources that are targeted with these generic names can be chosen by switches at the back of the amp. CD is either line 1 or digital 1. Dock is either line 2 or digital 2. More information is available [on page 8 of the user manual](http://www.intl.onkyo.com/downloads/manuals/pdf/a-9010_manual_en.pdf). Both of the source commands are idempotent. 68 | 69 | Commands that change some state of the amplifier will also result in a message being published to either `home/livingroom/amp/state` or `home/livingroom/amp/source` topics. This is required to create a switch in Home Assistant. However, since there's no way to read the current state over RI, it is possible that what's on the topic is out of sync with the actual state of the amp (e.g. using the remote to change sources). 70 | 71 | Finally there's a bit of logging information being written to the `home/livingroom/amp/logs` topic to help debugging. 72 | 73 | ## Home Assistant 74 | 75 | To make your amp available in Home assistant, put the following in `configuration.yaml`: 76 | 77 | ```yaml 78 | switch: 79 | - platform: mqtt 80 | name: "Amp" 81 | state_topic: "home/livingroom/amp/state" 82 | command_topic: "home/livingroom/amp/commands" 83 | payload_on: '{"state":"on"}' 84 | payload_off: '{"state":"off"}' 85 | icon: 'mdi:speaker' 86 | ``` 87 | 88 | ## How does it work 89 | 90 | More information on how the RI protocol actually works can be found [on the library's repository](https://github.com/docbender/Onkyo-RI). However, the commands that are listed there are not valid for the A-9010. It seems like the RI implementation varies wildly between devices. 91 | 92 | I slightly modified the testing tool that's in the library's repository to scan through the entire command space to use MQTT. This modified version can be found in `onkyo-mqtt-test.ino`. It will loop over all commands, publishing the current one that's been sent to `home/livingroom/amp/live`. Controlling the process can be done by sending messages to the `home/livingroom/amp` topic: 93 | 94 | - *p* to pause/resume the scan 95 | - *Anything else* will be parsed as a hex string and sent to the amp (e.g. sending `2` will turn up the volume). 96 | 97 | Going through all the commands, I've found the following for the A-9010: 98 | 99 | | Command | Action | 100 | | ---- | ---- | 101 | | 0x002 | Volume up | 102 | | 0x003 | Volume down | 103 | | 0x004 | On/off toggle | 104 | | 0x005 | Muting toggle | 105 | | 0x020 | Change source to CD | 106 | | 0x02f | Turn on | 107 | | 0x0d5 | Next input | 108 | | 0x0d6 | Previous input | 109 | | 0x0da | Turn off | 110 | | 0x0e3 | Line in | 111 | | 0x170 | Change source to Dock | 112 | | 0x503 | Muting toggle | -------------------------------------------------------------------------------- /onkyo-mqtt.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // Wifi 8 | #define WIFI_SSID "" 9 | #define WIFI_PASSWORD "" 10 | 11 | // MQTT 12 | #define MQTT_SERVER "" 13 | #define MQTT_USER "" 14 | #define MQTT_PASSWORD "" 15 | #define MQTT_PORT 1883 16 | #define AMP_COMMANDS_TOPIC "home/livingroom/amp/commands" 17 | #define AMP_LOGS_TOPIC "home/livingroom/amp/logs" 18 | #define AMP_STATE_TOPIC "home/livingroom/amp/state" 19 | #define AMP_SOURCE_TOPIC "home/livingroom/amp/source" 20 | 21 | // RI 22 | #define ONKYO_PIN 5 23 | 24 | // OTA 25 | #define OTA_NAME "" 26 | #define OTA_PASSWORD "" 27 | 28 | WiFiClient wifiClient; 29 | PubSubClient pubSubClient(wifiClient); 30 | OnkyoRI onkyoClient(ONKYO_PIN); 31 | 32 | void setup() { 33 | Serial.begin(115200); 34 | 35 | setupWifi(); 36 | setupOTA(); 37 | connectPubSub(); 38 | 39 | pubSubClient.publish(AMP_LOGS_TOPIC, "Started", true); 40 | } 41 | 42 | void setupOTA() { 43 | ArduinoOTA.setHostname(OTA_NAME); 44 | ArduinoOTA.setPassword(OTA_PASSWORD); 45 | 46 | ArduinoOTA.begin(); 47 | } 48 | 49 | void setupWifi() { 50 | delay(10); 51 | Serial.println(); 52 | Serial.print("Connecting to "); 53 | Serial.println(WIFI_SSID); 54 | 55 | WiFi.mode(WIFI_STA); 56 | WiFi.begin(WIFI_SSID, WIFI_PASSWORD); 57 | 58 | while (WiFi.status() != WL_CONNECTED) { 59 | delay(500); 60 | Serial.print("."); 61 | } 62 | 63 | Serial.println(""); 64 | Serial.println("WiFi connected"); 65 | Serial.println("IP address: "); 66 | Serial.println(WiFi.localIP()); 67 | } 68 | 69 | void connectPubSub() { 70 | pubSubClient.setServer(MQTT_SERVER, MQTT_PORT); 71 | pubSubClient.setCallback(callback); 72 | 73 | // Loop until we're reconnected 74 | while (!pubSubClient.connected()) { 75 | Serial.print("Attempting MQTT connection..."); 76 | // Attempt to connect 77 | if (pubSubClient.connect(MQTT_USER, MQTT_USER, MQTT_PASSWORD)) { 78 | Serial.println("connected"); 79 | pubSubClient.subscribe(AMP_COMMANDS_TOPIC); 80 | } else { 81 | Serial.print("failed, rc="); 82 | Serial.print(pubSubClient.state()); 83 | Serial.println(" try again in 5 seconds"); 84 | // Wait 5 seconds before retrying 85 | delay(5000); 86 | } 87 | } 88 | } 89 | 90 | void loop() { 91 | if (!pubSubClient.connected()) { 92 | ESP.reset(); 93 | } 94 | 95 | ArduinoOTA.handle(); 96 | 97 | pubSubClient.loop(); 98 | 99 | delay(500); 100 | } 101 | 102 | void callback(char* topic, byte* payload, unsigned int length) { 103 | StaticJsonDocument<300> payloadDocument; 104 | StaticJsonDocument<300> resultDocument; 105 | DeserializationError err = deserializeJson(payloadDocument, payload); 106 | 107 | if (err) { 108 | String payloadString = String((char *) payload); 109 | pubSubClient.publish(AMP_LOGS_TOPIC, (String("Failed to parse JSON: ") + payloadString).c_str(), true); 110 | return; 111 | } 112 | 113 | String sendTopic; 114 | bool failedToParse = false; 115 | 116 | if (payloadDocument.containsKey("state")) { 117 | String state = payloadDocument["state"]; 118 | sendTopic = AMP_STATE_TOPIC; 119 | 120 | if (state == "on") { 121 | onkyoClient.send(0x2f); 122 | resultDocument["state"] = "on"; 123 | } else if (state == "off") { 124 | onkyoClient.send(0xda); 125 | resultDocument["state"] = "off"; 126 | } else if (state == "reset") { 127 | ESP.reset(); 128 | } else { 129 | failedToParse = true; 130 | } 131 | } else if (payloadDocument.containsKey("source")) { 132 | String source = payloadDocument["source"]; 133 | sendTopic = AMP_SOURCE_TOPIC; 134 | 135 | if (source == "cd") { 136 | onkyoClient.send(0x20); 137 | resultDocument["source"] = "cd"; 138 | } else if (source == "dock") { 139 | onkyoClient.send(0x170); 140 | resultDocument["source"] = "dock"; 141 | } else { 142 | failedToParse = true; 143 | } 144 | } else if (payloadDocument.containsKey("volume")) { 145 | String volume = payloadDocument["volume"]; 146 | 147 | if (volume == "up") { 148 | onkyoClient.send(0x2); 149 | delay(250); 150 | onkyoClient.send(0x2); 151 | } else if (volume == "down") { 152 | onkyoClient.send(0x3); 153 | delay(250); 154 | onkyoClient.send(0x3); 155 | } else { 156 | failedToParse = true; 157 | } 158 | } else { 159 | failedToParse = true; 160 | } 161 | 162 | if (failedToParse) { 163 | pubSubClient.publish(AMP_LOGS_TOPIC, String("Failed to do something with JSON: ").c_str(), true); 164 | return; 165 | } 166 | 167 | char outputBuffer[300]; 168 | serializeJson(resultDocument, outputBuffer); 169 | pubSubClient.publish(sendTopic.c_str(), outputBuffer, true); 170 | } 171 | --------------------------------------------------------------------------------