Dice activated voice assistant

I made a voice assistant that only listens when you roll higher than a certain number. You may be wondering why? Well, I don’t have a good answer for you. I did not ask myself if I should do this, I only asked if I could do it. Such is the folly of creativity.

The components I used

You could combine the first two into one device. But I prefer to have separate devices so I can use the Dice BLE client for other purposes (perhaps you can force your guests to roll a six in order to turn on the lights).

Preparing the ESP32 BLE client.

First, I should mention that I have become so spoiled by Home Assistant that I immediately expected the dice to be auto-discovered by home assistant through my BLE proxies. Sadly, it was not to be. I then considered writing a custom integration but in the end decided to make an ESPHOME BLE client for the GoDice.

Luckily, the creators of the GoDice (Particula) provide some nice examples using the GoDice here. I simply took their python example and adapted to ESPHOME. Here is the yaml for the ESPHOME dice client:

esphome:
  name: dice-bleclient
  friendly_name: Dice bluetooth client
  
esp32:
  board: esp32dev
  framework:
    type: arduino

logger:

api:

ota:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

captive_portal:

esp32_ble_tracker:

ble_client:
  - mac_address: DC:53:1D:57:85:BE
    id: dice1
    on_connect:
      then:
        - lambda: |-
            ESP_LOGD("ble_client_lambda", "Connected to Dice 1");
        - delay: 1s
        - ble_client.ble_write:
            id: dice1
            service_uuid: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
            characteristic_uuid: 6e400002-b5a3-f393-e0a9-e50e24dcca9e
            # List of bytes to write.
            value: [0x10, 0x03, 0x28, 0x32, 0x00, 0x00, 0xff, 0x01, 0x00]
    
button:
  - platform: template
    name: "pulse blue"
    on_press:
      - ble_client.ble_write:
          id: dice1
          service_uuid: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
          characteristic_uuid: 6e400002-b5a3-f393-e0a9-e50e24dcca9e
          # List of bytes to write.
          value: [0x10, 0x03, 0x28, 0x32, 0x00, 0x00, 0xff, 0x01, 0x00]

sensor:
  - platform: ble_client
    type: characteristic
    ble_client_id: dice1
    name: "Dice state"
    id: dice_state
    service_uuid: '6e400001-b5a3-f393-e0a9-e50e24dcca9e'
    characteristic_uuid: '6e400003-b5a3-f393-e0a9-e50e24dcca9e'
    notify: True
    lambda: |-
      int16_t first_byte = x[0];
      
      // Rolling if birst byte is 82
      if (first_byte == 82) {
        return 0.0;
      }

      // Get second and third byte
      int16_t second_byte = x[1];
      int16_t third_byte = x[2];

      // It's a message with a battery update, skip parsing it and return -1
      if (first_byte == 66 && second_byte == 97 && third_byte == 116) {
        return -1.0;
      }

      // It is a message with a color update, skip parsing and return -2
      if (first_byte == 67 && second_byte == 111 && third_byte == 108) {
        return -2.0;
      }

      // Define NUM_VECTORS inside the loop
      const int NUM_VECTORS = 6;

      // define d_6 vectors of dice
      int d6Vectors[NUM_VECTORS][3] = {
        {-64, 0, 0},
        {0, 0, 64},
        {0, 64, 0},
        {0, -64, 0},
        {0, 0, -64},
        {64, 0, 0}
      };

      // This is a roll, so parse the bytes
      if (first_byte == 83) {
        // Get 2nd, 3rd, and fourth bytes and unpack
        int8_t x_v, y_v, z_v;

        // Unpack bytes into unsigned 8-bit integers
        x_v = static_cast<int8_t>(x[1]);
        y_v = static_cast<int8_t>(x[2]);
        z_v = static_cast<int8_t>(x[3]);

        // Define xyz array outside the loop
        int xyz[3] = {x_v, y_v, z_v};

        float distances[NUM_VECTORS];


        for (int i = 0; i < NUM_VECTORS; i++) {
          distances[i] = 0;

          for (int j = 0; j < 3; j++) {
            distances[i] += pow(static_cast<float>(xyz[j]) - d6Vectors[i][j], 2);
          }

          distances[i] = sqrt(distances[i]);
        }

        int idxMinDistance = 0;

        for (int i = 1; i < NUM_VECTORS; i++) {
          if (distances[i] < distances[idxMinDistance]) {
            idxMinDistance = i;
          }
        }

        int rolledValue = idxMinDistance + 1; 


        ESP_LOGD("ble_client_lambda", "The rolled value is :%d", rolledValue);
        return float(rolledValue);
      }

      return -10.0;

You will need to find the mac address of your GoDice using any BLE scanner app on your smart phone and replace it. The most important part is the dice state sensor. I have configured it so that when the dice is being rolled a state of “0” is shown. When the dice is rolled, the state should match the dice. For any other movements, or during charging, the dice state should be a negative integer. So for our voice assistant we only care when the state is between 1-6. Here is how it looks in my home assistant dashboard. Note that I added a pulse blue button so I can check if the dice is connected.

For the next part, we are going to need the sensor entity name of our ESPHOME BLE client. For me that was sensor.dice_bleclient_dice_state.

Preparing the voice assistant

This part was a bit simpler to figure out. I used the original logic of the M5 for the year of the voice, when we had to press the button to activate the voice assistant.

To get the M5 to communicate with the dice we need to add a couple things to the ESPHOME ready made projects yaml file.

First I added a text sensor from home assistant that brings in the BLE client state:

text_sensor:
  - platform: homeassistant
    internal: True
    id: dice_state
    entity_id: sensor.dice_bleclient_dice_state
    on_value:
      then:
        - if:
            condition:
              lambda: |-
                return id(dice_state).state >= id(dice_cutoff).state;
            then:
              - if:
                  condition:
                    not: voice_assistant.is_running
                  then:
                    - voice_assistant.start:

Notice that I have a dice_cutoff variable in the lambda. This is so I can configure the lowest number needed for activation directly in Home Assistant. This is done like this.

# dice selection
select:
  - platform: template
    entity_category: config
    name: Dice cutoff
    id: dice_cutoff
    options:
     - "1"
     - "2"
     - "3"
     - "4"
     - "5"
     - "6"
    initial_option: "3"
    optimistic: true
    set_action:
      - logger.log:
          format: "Chosen cutoff: %s"
          args: ["x.c_str()"]

The final piece of the puzzle was to add a toggle for enabling the dice throw. Here I added a new switch that turns off the wake word.

  - platform: template
    name: Use dice throw
    id: use_dice
    optimistic: true
    entity_category: config
    restore_mode: RESTORE_DEFAULT_OFF
    on_turn_on:
    - switch.turn_off: use_wake_word

Small note: I also added a switch.turn_off: use_dice to the use wake word switch so that they become mutually exclusive. The modified device looks like this in Home Assistant:

For ease of use I also added the complete yaml for the BLE client and the voice assistant.

And finally, here is a video of me using it! I was too eager on the second throw. Enjoy!

Nice one! Silly, but very nice!

1 Like