Trying not to have my phone by my side constantly, but wanting access to Sonos Controls, I started looking for a hardware button that I could use to play and pause my Sonos, and turn the volume up and down. Just something to sit on my desk, nothing fancy, something simple and functional. A one (maybe two) trick pony.
Here was the path I travelled. Forgive me if I don’t get the lingo right. Hope it sets you on your own fun path.
Having coded, years and years ago, I thought I might step into the future and give a microcontroller a go. Something like an esp32 or such. I was thinking I wanted something that communicated with MQTT as I had set up a MQTT broker earlier to play with presence detection. And bluetooth would be good because it seems to be recognized a lot faster than wifi.
Then I came across the Espruino community and the Puck.JS – a “Javascript Bluetooth Beacon”. To quote their website, the Puck.js can “measure light, temperature, movement, magnetic fields and capacitance, can control Infrared devices, and has a clever tactile switch that turns the Puck into one big button”. Sounded great so I ordered it and got to work.
To think a few weeks ago I had no idea what an IDE was - well, still learning. Javascript however was something I had used before so off I went. The IDE is a Web IDE, and the tutorials are quite extensive.
More info/tutorials/forum at https://www.espruino.com/
Got the Puck.js connected to the IDE via bluetooth on my PC, wrote some Javascript to get the Puck to send out a ble advertisement based on various presses, installed Espruino hub to receive the bluetooth signals and translate them into MQTT with a Home Assistant discovery, then wrote an automation/script to turn the states of the sensors/devices created to turn play/pause my Sonos and turn the volume up and down. Thank you Gordon at Espruino for your patience and pointers.
- Here is the javascript I loaded onto the Puck.js via the Espruino IDE
//https://raw.githubusercontent.com/muet/EspruinoDocs/master/modules/SWButton.js
//This include recognizes a variety of presses
var SWBtn = require("SWButton");
var buttonState = 0;
var advData = {};
var idleTimeout;
// recognized button presses and add them as bluetooth services
// I chose these specific services as they were used by the Espruino hub, and seemed closest to my needs.
var mySWBtn = new SWBtn(function(k){
if (k === "S" ) { // single button press
buttonState = !buttonState;
advData[0x2A56] = buttonState; //UUID digital
advData[0x2A06] = 0; ///UUID alert_level 0-no level reset to 0 so HA automation can get a from/to state
}
else if (k === "L" ) { // long button press
advData[0x2A06] = 1; //UUID alert_level mid level
}
else if (k === "SS") { // double short press
advData[0x2A06] = 2; ///UUID alert_level high level
}
updateAdvertising(advData);
});
//start with advertising temperature and battery level
setInterval(function () {
advData[0x180F] = [E.getBattery()];
NRF.setAdvertising(advData);
}, 1*60*1000); // 1 min
function updateAdvertising(advData) {
NRF.setAdvertising(advData);
if (idleTimeout) clearTimeout(idleTimeout);
idleTimeout = setTimeout(function() {
idleTimeout = undefined;
advData[0x2A06] = 0; ///UUID alert_level 0-no level
NRF.setAdvertising(advData);
},2500);
}
- Installed Espruino Hub on a Raspberry Pi 3B+. Here is the Espruino hub config I used.
"// Set this to true to only publish MQTT messages for known devices":0,
"only_known_devices": true,
"known_devices" : {
"x0:x1:x2:x3:x4:x5": "fakekeys" // seemed to need this to have only the next devices to get discovered",
"AA:BB:CC:DD:EE:FF": "mypuck" // the mac address of your puck
},
"// skip advertise with a smaller signal":0,
"min_rssi" : -90,
"// How many seconds to wait for a packet before considering BLE connection":0,
"// broken and exiting. Higher values are useful with slowly advertising sensors.":0,
"// Setting a value of 0 disables the exit/restart.":0,
"ble_timeout": 20,
"// How many seconds to wait for emitting a presence event, after latest time polled":0,
"// Default is 60 seconds":0,
"presence_timeout" : 30,
"// Number of simultaneous bluetooth connection the device can handle (PI Zero=4)":0,
"max_connections" : 4,
"connection_timeout": 20,
"// MQTT path for history requests and output. Default is Empty (to disable).":0,
"//history_path": "/ble/hist/",
"// We can add our own custom advertising UUIDs here with names to help decode them":0,
"advertised_services" : {
"2a56" : {
"name" : "buttonState"
},
"2a06" : {
"name" : "volState"
}
},
"// Make this nonzero to enable the HTTP server on the given port.":0,
"// See README.md for more info on what it does":0,
"http_port" : 1888,
"// Set this to enable the HTTP proxy - it's off by default for safety":0,
"// since it would be possible to spoof MAC addresses and use your":0,
"// connection":0,
"// NOTE: Some Bluetooth adaptors will cause the error: Command Disallowed (0xc)":0,
"// when trying to connect if http_proxyis enabled.":0,
"http_proxy" : false,
"// If there are any addresses here, they are given access to the HTTP proxy":0,
"http_whitelist" : [
"aa:cc:ss:ee:qq:ss"
],
"mqtt_host": "mqtt://111.222.333.555", //the IP of your MQTT Broker
"mqtt_options": {
"username": "mqttusername",
"password": "mqttpassword",
"port": "mqttport"
},
"// Define the topic prefix under which the MQTT data will be posted. Defaults to /ble which is not adviced. For new installation, please activate the option below.":0,
"mqtt_prefix": "yourcustomtopic", // change this as you will
"// These are the types of MQTT topics that are created":0,
"// Send /ble/advertise/ad:dr:es:ss JSON with raw advertising data, as well as /ble/advertise/ad:dr:es:ss/rssi":0,
"// This is used by the localhost:1888/ide service to detect devices":0,
"mqtt_advertise": true,
"// Send /ble/advertise/ad:dr:es:ss/manufacturer/uuid raw manufacturer data as well as decoded /ble/advertise/ad:dr:es:ss/json_key for json-formatted 0x0590 advertising data":0,
"mqtt_advertise_manufacturer_data": false,
"// Send /ble/advertise/ad:dr:es:ss/uuid raw service data":0,
"mqtt_advertise_service_data": true,
"// Send /ble/json/ad:dr:es:ss/uuid for decoded service data - REQUIRED FOR HOMEASSISTANT":0,
"mqtt_format_json": true,
"// Send /ble/service_name/ad:dr:es:ss for decoded service data":0,
"mqtt_format_decoded_key_topic": true,
"// Whether to enable Home Assistant integration":0,
"homeassistant": true
}
This creates sensors and a puck device which are discovered by the HA MQTT Integration.
- Then I wrote an HA automation which takes the states of the sensors and translates 1 short press (ie 0x2A056=0) into toggle play/pause using the MQTT Binary sensor created by the espruino hub, 2 short press (ie 0x2A006=1) for volume down and 1 long press (ie 0x2A006=2) volume up. It uses trigger_IDs which really make automations so much easier - thanks @slackerLabs. Your videos turned me onto this new feature.
alias: '[Puck] Control Sonos Player'
description: 'An automation to play/pause sonos and turn vol up/down with Puck.JS button presses'
trigger:
- platform: state
entity_id: binary_sensor.yourmacaddress_2a56_digital
to: 'on'
from: 'off'
id: start
- platform: state
entity_id: binary_sensor.yourmacaddress_2a56_digital
to: 'off'
from: 'on'
id: stop
- platform: state
entity_id: sensor.yourmacaddress_2a06_alert
to: '1'
from: '0'
id: volume_up
- platform: state
entity_id: sensor.yourmacaddress_2a06_alert
from: '0'
id: volume_down
to: '2'
condition:
- condition: not
conditions:
- condition: state
entity_id: binary_sensor.yourmacaddress_2a56_digital
state: unknown
action:
- choose:
- conditions:
- condition: trigger
id: start
sequence:
- service: script.yourscript
data:
target_player: media_player.yourplayer
target_sonos_source: Your Sonos Favorite 1
target_trigger_state: start
- conditions:
- condition: trigger
id: stop
sequence:
- service: script.yourscript
data:
target_player: media_player.yourplayer
target_trigger_state: stop
- conditions:
- condition: trigger
id: volume_up
sequence:
- service: script.yourscript
data:
target_player: media_player.yourplayer
target_trigger_state: volume_up
- conditions:
- condition: trigger
id: volume_down
sequence:
- service: script.yourscript
data:
target_player: media_player.yourplayer
target_trigger_state: volume_down
default: []
mode: single
- And this is the yourscript script
alias: '[Puck] Control Sonos Player with Puck'
sequence:
- choose:
- conditions:
- condition: template
value_template: '{{ target_trigger_state == ''start'' }}'
sequence:
- service: media_player.select_source
data:
source: '{{ target_sonos_source }}'
entity_id: '{{ target_player }}'
- conditions:
- condition: template
value_template: '{{ target_trigger_state == ''stop'' }}'
sequence:
- service: media_player.media_stop
data:
entity_id: '{{ target_player }}'
- conditions:
- condition: template
value_template: '{{ target_trigger_state == ''volume_up'' }}'
sequence:
- service: media_player.volume_up
data:
entity_id: '{{ target_player }}'
- conditions:
- condition: template
value_template: '{{ target_trigger_state == ''volume_down'' }}'
sequence:
- service: media_player.volume_down
data:
entity_id: '{{ target_player }}'
default: []
fields:
target_player:
name: Target media player
description: Target media player of volume fade.
required: true
example: media_player.study
selector:
entity:
domain: media_player
target_trigger_state:
name: Target trigger state
description: Target trigger state
target_sonos_source:
name: Target Sonos favorites
description: Target music to play.
required: false
default: Your Sonos Favorite 1
selector:
select:
options:
- Your Sonos Favorite 1
- Your Sonos Favorite 2
mode: single
icon: mdi:music-circle
And it is something I now use every day. It is not perfect. Sometimes the timing of the bluetooth signals requires patience. As long as you pause a bit between vol up/down presses it does the trick.
If anyone has any improvement suggestions - please chip in.
Not sure if I can answer questions - but I will try. But the Espruino forum is active and very helpful - just as this one is.
Thanks - and have fun.