Control Sonos Player with Puck.js hardware button

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.

  1. 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);
}
  1. 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.

  1. 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
  1. 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.

2 Likes