Get the Latest Updates

Receive occasional drops of new designs, components, and random resources. No spam, just the good stuff.

Back

How I turned a €5 IKEA speaker into a voice assistant

An ESP32, a microphone, Home Assistant and Claude inside a €5 IKEA KALLSUP cube.

November 5, 2026, 23 min. read

Intro: why I bought it

It started with boredom. I was wandering around IKEA with nothing in particular to buy, and I saw the KALLSUP: a tiny Bluetooth speaker for €5.

I thought: cool, it definitely has a speaker inside. It's a Bluetooth speaker, so there's probably a board in there. It has a charging port, so there must be a battery too. Whatever. I only needed the speaker itself, and the little cube would make a nice enclosure.

So I figured I'd buy it and take a look. Worst case, I'd throw away five euros.

The finished speaker in my kitchen

Digging everything out

Opening it is easy. Unscrew the two screws on the bottom of the box and it comes apart.

Inside there is only one PCB, with a USB-C port and two buttons on it. It slides out easily. Carefully disconnect the speaker from the board. The speaker itself is screwed to the front panel, behind the holes.

The empty cube without the PCB
The speaker inside the cube

When you look at the PCB, a few things are worth noting:

  • The USB-C port. Most likely it charges the battery, and later it will be our power input.
  • The battery. Disconnect it. It is 750 mAh and about the size of a normal AA battery, with no model number printed on it. It is a perfectly decent battery, so I took it out to reuse in another project.
  • The buttons. You will need a multimeter to work out which pads carry voltage and which belong to the buttons. More on that in the next part.

Keep the PCB. We will be using it.

What you need

The first thing is to get your ESP ready. Here is the full shopping list:

  • ESP32-S3 board: XH-S3E, N16R8 (16 MB flash, 8 MB octal PSRAM)
  • Amplifier: MAX98357A (I2S)
  • Microphone: INMP441 (I2S MEMS)
  • Wires: a few Dupont cables
  • Pin headers: it is better to assemble everything on pins than to solder wires straight onto the boards
  • The IKEA cube, which brings its own speaker, USB-C power input and two buttons

That's all.

Wiring it up on the bench

This is the easy part: connect the pins, and that's it. Here is the full wiring, taken from the working config.

Microphone (INMP441)

INMP441 pinESP32-S3 pinNote
SCKGPIO4I2S bit clock
WSGPIO5I2S word select
SDGPIO6I2S data out of the mic
L/RGNDselects the left channel (channel: left in the config)
VDD3V3
GNDGND

Amplifier (MAX98357A) and speaker

MAX98357A pinESP32-S3 pinNote
BCLKGPIO7I2S bit clock
LRCGPIO8I2S word select
DINGPIO9I2S data into the amp
VIN3V3temporary, see below
GNDGND

The speaker connects to the amp's two output terminals (+ and −).

Power for the amp. At the beginning, power the amp from the 3V3 pin. The 5V pin on the ESP32 board does not provide 5 V: on this board it is input-only, and neither USB port outputs 5 V there. But 3.3 V is plenty for the speaker to speak out. Later we will bridge the 5 V from the IKEA PCB and move the amp over to it.

Pins you must not use. GPIO26 to GPIO37 are reserved for the octal PSRAM and flash on the N16R8 module.

First test: does it make a sound?

Before building anything clever, just test it. Here is a simple config: upload it through ESPHome and try it out. That's it.

Two small tests, one for each part. Test the speaker first, then the mic.

Test 1: speaker (a sine tone and a melody)

Every 20 seconds it plays a 440 Hz tone and then a short scale. If you hear it, the amp, the speaker and the I2S wiring are fine.

esphome:
  name: kallsup-test
  on_boot:
    priority: -100
    then:
      - delay: 3s
      - script.execute: play_tests

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  flash_size: 16MB
  framework:
    type: esp-idf

logger:
  level: DEBUG
  hardware_uart: UART0   # COM port. For the native USB port use USB_SERIAL_JTAG

i2s_audio:
  - id: i2s_out
    i2s_lrclk_pin: GPIO8   # LRC
    i2s_bclk_pin: GPIO7    # BCLK

speaker:
  - platform: i2s_audio
    id: spk
    i2s_audio_id: i2s_out
    dac_type: external
    i2s_dout_pin: GPIO9    # DIN
    channel: mono
    sample_rate: 16000
    bits_per_sample: 16bit

rtttl:
  speaker: spk
  id: melody
  gain: 30%

interval:
  - interval: 20s
    then:
      - script.execute: play_tests

script:
  - id: play_tests
    mode: single
    then:
      - logger.log: "Sine 440 Hz, 0.45 s"
      - speaker.play:
          data: !lambda |-
            const int sr = 16000, n = 7200;  // 0.45 s
            std::vector<uint8_t> b(n * 2);
            for (int i = 0; i < n; i++) {
              float env = 1.0f;                       // short fade in/out
              if (i < 800) env = i / 800.0f;
              if (i > n - 800) env = (n - i) / 800.0f;
              int16_t s = (int16_t)(6500 * env * sinf(2.0f * M_PI * 440.0f * i / sr));
              b[2*i] = s & 0xFF; b[2*i+1] = (s >> 8) & 0xFF;
            }
            return b;
      - delay: 2s
      - logger.log: "Melody"
      - rtttl.play: "scale:d=8,o=5,b=140:c,d,e,f,g,a,b,c6"

Test 2: microphone (a level meter in the log)

The speaker stays silent. The log prints the mic level twice a second as a little bar graph. Clap or talk and watch the bar move. If it never moves, check the wiring (my first mic was silent because of a solder bridge between SCK and GND).

esphome:
  name: kallsup-mic
  on_boot:
    priority: -100
    then:
      - delay: 2s
      - microphone.capture: mic

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  flash_size: 16MB
  framework:
    type: esp-idf

logger:
  level: INFO
  hardware_uart: UART0   # COM port. For the native USB port use USB_SERIAL_JTAG

i2s_audio:
  - id: i2s_in
    i2s_lrclk_pin: GPIO5   # WS
    i2s_bclk_pin: GPIO4    # SCK

microphone:
  - platform: i2s_audio
    id: mic
    i2s_audio_id: i2s_in
    adc_type: external
    i2s_din_pin: GPIO6     # SD
    channel: left          # L/R to GND = left channel
    sample_rate: 16000
    bits_per_sample: 32bit
    on_data:
      - lambda: |-
          static uint64_t sum = 0;
          static uint32_t cnt = 0;
          const int32_t *s = (const int32_t *) x.data();
          size_t n = x.size() / 4;
          for (size_t i = 0; i < n; i++) {
            int32_t v = s[i] >> 16;      // the mic sends 24 bit in a 32 bit word
            sum += (int64_t) v * v;
            cnt++;
          }
          if (cnt >= 8000) {             // about 0.5 s
            float rms = sqrtf((float) sum / cnt);
            float db = 20.0f * log10f(rms / 32768.0f + 1e-9f);
            int bars = (int) ((db + 80.0f) / 2.0f);
            if (bars < 0) bars = 0;
            if (bars > 40) bars = 40;
            char b[41];
            for (int i = 0; i < 40; i++) b[i] = i < bars ? '#' : '.';
            b[40] = 0;
            ESP_LOGI("mic", "%6.1f dBFS |%s|", db, b);
            sum = 0; cnt = 0;
          }

The logger gotcha

The dev board has two USB-C ports: one is a UART bridge (COM), the other is the native USB. The hardware_uart setting in the logger has to match the port you plug into: UART0 for COM, USB_SERIAL_JTAG for native USB. Pick the wrong one and the log is completely silent, which looks exactly like a dead board.

The full voice assistant

Once the speaker and the mic both work, flash the full voice assistant config. Here is how the pieces fit together:

  • On the ESP32: the wake word runs right on the board (micro_wake_word: okay nabu, hey jarvis, hey mycroft), so nothing streams to your network until you say it. After the wake word, the voice_assistant component streams your voice to Home Assistant.
  • In Home Assistant: Wyoming faster-whisper turns speech into text, Piper turns the answer back into speech, and Claude is the brain in the middle.
  • Beeps: a short Siri-style beep when it starts listening and another when it stops, generated with RTTTL, so there are no sound files.

The config (the Wi-Fi credentials, API key and OTA password are placeholders, use your own):

esphome:
  name: kallsup
  friendly_name: Nabu

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  flash_size: 16MB
  framework:
    type: esp-idf

psram:
  mode: octal
  speed: 80MHz

logger:
  level: DEBUG
  hardware_uart: USB_SERIAL_JTAG   # for the COM port use UART0

wifi:
  ssid: "<YOUR_WIFI_SSID>"
  password: "<YOUR_WIFI_PASSWORD>"
  power_save_mode: none            # faster response

api:
  encryption:
    key: "<YOUR_API_ENCRYPTION_KEY>"

ota:
  - platform: esphome
    password: "<YOUR_OTA_PASSWORD>"

i2s_audio:
  - id: i2s_out
    i2s_lrclk_pin: GPIO8
    i2s_bclk_pin: GPIO7
  - id: i2s_in
    i2s_lrclk_pin: GPIO5
    i2s_bclk_pin: GPIO4

microphone:
  - platform: i2s_audio
    id: mic
    i2s_audio_id: i2s_in
    adc_type: external
    i2s_din_pin: GPIO6
    channel: left
    sample_rate: 16000
    bits_per_sample: 32bit

speaker:
  - platform: i2s_audio
    id: spk
    i2s_audio_id: i2s_out
    dac_type: external
    i2s_dout_pin: GPIO9
    channel: mono
    sample_rate: 22050     # Piper's native rate; 16 kHz sounded muffled
    bits_per_sample: 16bit
    buffer_duration: 1s

# The media player downloads the answer from a URL, decodes and resamples it
media_player:
  - platform: speaker
    id: mp
    name: None
    volume_initial: 50%
    announcement_pipeline:
      speaker: spk
      format: WAV
      sample_rate: 22050
      num_channels: 1

# Short beeps, no sound files needed
rtttl:
  id: beep
  speaker: spk
  gain: 25%

globals:
  - id: g_last_ww
    type: std::string
    initial_value: '""'

script:
  # Beep, short pause, then listen (so the beep doesn't end up in the recording)
  - id: start_listening
    mode: single
    then:
      - micro_wake_word.stop:
      - rtttl.play: "start:d=32,o=6,b=180:c,e,g"
      - wait_until:
          condition:
            lambda: 'return !id(beep).is_playing();'
          timeout: 1s
      - delay: 150ms
      - voice_assistant.start:
          wake_word: !lambda 'return id(g_last_ww);'

# Wake word runs directly on the board, no server needed
micro_wake_word:
  id: mww
  microphone: mic
  stop_after_detection: false
  models:
    - model: okay_nabu
      id: okay_nabu
    - model: hey_jarvis
      id: hey_jarvis
    - model: hey_mycroft
      id: hey_mycroft
  vad:
  on_wake_word_detected:
    - lambda: 'id(g_last_ww) = wake_word;'
    - script.execute: start_listening

voice_assistant:
  id: va
  microphone: mic
  media_player: mp
  micro_wake_word: mww
  noise_suppression_level: 2
  auto_gain: 31dBFS
  volume_multiplier: 1.0     # 2.0 amplified the noise and Whisper hallucinated "The The The"
  on_stt_vad_end:
    - rtttl.play: "end:d=32,o=6,b=180:g,e,c"   # stopped listening
    - wait_until:
        condition:
          lambda: 'return !id(beep).is_playing();'
        timeout: 1s
    - media_player.volume_set:   # rtttl overwrites the speaker volume with its own gain
        id: mp
        volume: !lambda 'return id(mp).volume;'
  on_tts_start:
    - media_player.volume_set:   # same fix after the answer starts
        id: mp
        volume: !lambda 'return id(mp).volume;'
  on_client_connected:
    - micro_wake_word.start:
  on_end:
    # after the answer, go back to waiting for the wake word
    - wait_until:
        not:
          voice_assistant.is_running:
    - micro_wake_word.start:

# Start listening from Home Assistant (physical buttons come later)
button:
  - platform: template
    name: "Listen"
    icon: mdi:microphone
    on_press:
      - lambda: 'id(g_last_ww) = "";'
      - script.execute: start_listening

# Always-visible volume slider (HA only shows the media player slider while playing)
number:
  - platform: template
    name: "Volume"
    icon: mdi:volume-high
    min_value: 0
    max_value: 100
    step: 1
    unit_of_measurement: "%"
    mode: slider
    update_interval: 1s
    lambda: 'return id(mp).volume * 100.0f;'
    set_action:
      - media_player.volume_set:
          id: mp
          volume: !lambda 'return x / 100.0f;'

sensor:
  - platform: wifi_signal
    name: "WiFi signal"
    update_interval: 60s

Flash it and find it in Home Assistant

Flash it from the ESPHome Device Builder in Home Assistant (that is where your real Wi-Fi credentials live). Home Assistant will pick the board up immediately as a new ESPHome device. Add it, and you will see the media player, the assist satellite, the volume slider and the Wi-Fi signal.

Give it a brain: Claude

Connect Anthropic to Home Assistant and use Claude as the conversation agent. I let an MCP server set the whole thing up in Home Assistant, using a Claude API key from Anthropic. I use Claude Haiku, because it is the fastest model and plenty smart for a kitchen assistant. Then create a voice pipeline that uses a speech-to-text engine, Piper and Claude, assign it to the satellite, and test it.

My pipeline is called "Kuchyň" (the kitchen) and uses English. For speech-to-text I started with Whisper and later switched to NVIDIA's Parakeet (see the gotchas below). For the voice I use Piper's en_US-ryan-medium, slowed down a bit so it is easier to follow.

Gotchas I hit on the way

  • speaker.play on a stopped I2S speaker silently drops the data. For the answers, use media_player: platform: speaker and point voice_assistant at it. A bare speaker: gave no output at all.
  • RTTTL resets the speaker volume to its own gain, which is why the config restores the media player volume after each beep.
  • Muffled sound: at 16 kHz everything was dull. Playback at 22.05 kHz, which is Piper's native rate, was a big improvement.
  • Whisper hallucinated "The The The" when I boosted the mic with volume_multiplier: 2.0. Leave it at 1.0.
  • Choppy, laggy answers were Piper, not Wi-Fi. I blamed my weak Wi-Fi (about −69 dBm), but when I measured Piper directly, the ryan-high voice generated speech slower than it plays (about 1.5 times real time with the container limited to 2 CPUs), so the speaker kept waiting for the next piece. Giving the Piper container all 6 cores and switching to ryan-medium brought it down to roughly 0.1 to 0.3 times real time, and the stuttering is gone.
  • Whisper was the slowest and least accurate part. On my CPU-only server, Whisper small.en took about 4 seconds after I stopped talking, and a smaller model mis-heard me. NVIDIA's Parakeet (the 0.6B v2 model, English only) did the same job in 0.4 to 0.9 seconds and transcribed my test sentences correctly. You don't need a new image: the same rhasspy/wyoming-whisper image can run it. Start a second container next to Whisper (a different port), add it in Home Assistant as another Wyoming integration, and switch the pipeline to it. The first start downloads about 600 MB, so the container looks stuck for a couple of minutes:
  wyoming-parakeet:
    image: rhasspy/wyoming-whisper
    container_name: wyoming-parakeet
    command: --stt-library onnx-asr --model nemo-parakeet-tdt-0.6b-v2 --language en
    volumes:
      - ./parakeet-data:/data
    ports:
      - "10301:10300"
    restart: unless-stopped
  • The voice talks too fast. Piper has a --length-scale option on the server (the command of the Piper container). 1.0 is normal; I use 1.5, which makes a sentence about 30 percent longer. It reacts less than the number suggests: 1.15 gave only about 5 percent, so just try a few values and listen.

Bringing the IKEA PCB back to life

Disclaimer: I'm not an electrician. I probably did some terrible things here, so everything in this part is at your own risk.

Step 1: the buttons

The buttons don't work unless you desolder the resistors next to them. The two that matter are R4 and R2, the pull-up resistors on the button lines. A third resistor sits right beside them and I desoldered that one too, but it turned out to be unrelated, so you can leave it alone. The original chip is still powered through them and disturbs the signal, so the buttons read as permanently pressed. Desoldering those resistors isolates the buttons from the rest of the PCB, and that is all we need.

After that, each button is just a switch. One side goes to a GPIO on the ESP32 (Play to GPIO10, Bluetooth to GPIO11) and the other side to a common ground. The ESP32 uses its internal pull-ups, so no extra parts are needed.

The IKEA PCB with the buttons wired to the ESP32

Step 2: power

Flip the PCB over. On the other side there are about five test pads: ground, USB data (two pads), VCC 5 V and a battery pad.

  • 5 V pad: this is the one we want. The battery pad only gives about 4.9 V, and we want a proper 5 V.
  • Ground pad: skip it. The buttons will share a common ground with the ESP32 anyway.

Carefully solder a wire to the 5 V pad. Use a proper thick one, 24 AWG because this wire carries the power for everything, so it has to be a really good one.

The 5 V wire soldered to the test pad

That wire feeds two things, so you need two wires:

  1. One goes to the 5Vin pin of the ESP32.
  2. One goes to the VIN of the amplifier, which until now ran from the 3V3 pin. Moving it to 5 V gives the speaker more headroom. On 3V3 the sound clipped at fairly low levels.

Now USB-C on the IKEA PCB powers the whole thing. The dev board's own USB ports are only for flashing and logs.

Step 3: connect the buttons to the ESP32

Now the buttons. Sometimes you just don't want to shout at your speaker, so it's nice to have something you can press. I won't use them very often, but it would be a pity to leave two perfectly good buttons unused.

They are classic tactile buttons, so the wiring is always diagonal: one leg is the signal and the diagonally opposite leg is ground. It doesn't really matter which way round you choose, because it most likely works either way. What worked for me: the signal wire on the bottom-left leg and the ground wire on the upper-right leg, and the same on the other button.

  • Signal wires: Play (S1) to GPIO10, Bluetooth (S2) to GPIO11.
  • Ground: join the ground legs of both buttons into one common ground, and take it to a GND pin on the ESP32. Note that this ground is shared with the ground of the USB power input, so it is the same ground that powers everything.

Then test them. Flash the config below and press each button: you should see the log line and the binary sensor flip in Home Assistant.

Step 4: use them in the config

Add this to the ESPHome config. Both pins use the internal pull-up and are inverted, because a pressed button pulls the pin to ground. A 30 ms debounce filter on each stops accidental double presses.

binary_sensor:
  - platform: gpio
    id: btn_s1
    name: "Play"
    pin:
      number: GPIO10
      mode:
        input: true
        pullup: true
      inverted: true
    filters:
      - delayed_on: 30ms
      - delayed_off: 30ms
    on_press:
      - logger.log: "Play pressed"
      - lambda: 'id(g_last_ww) = "";'
      - script.execute: start_listening

  - platform: gpio
    id: btn_s2
    name: "Bluetooth"
    pin:
      number: GPIO11
      mode:
        input: true
        pullup: true
      inverted: true
    filters:
      - delayed_on: 30ms
      - delayed_off: 30ms
    on_press:
      - logger.log: "Bluetooth pressed"
      - voice_assistant.stop:
      - media_player.stop:
          id: mp
      - micro_wake_word.start:

I kept the original names on the buttons, Play and Bluetooth. Play starts listening, the same as saying the wake word. Bluetooth is the stop button: it cancels the assistant, stops whatever is playing and goes back to waiting for the wake word.

Step 5: the LED (a failure)

This part of the project is a failure, so I'll tell it as one.

The original PCB has an LED that blinks whenever it has power. Even with the buttons isolated and all the wires soldered, the moment I plugged the USB-C into the wall, the LED started blinking. It was looking for a Bluetooth connection or something like that. It was annoying, and I wanted to get rid of it, or better, take control of it.

So I scouted around the PCB looking for a way to hack it. I found three resistors in the LED circuit, R12, R15 and R17. When I tested them, the colors turned out to be:

ResistorLED color
R17red
R15green
R12blue

It is an RGB LED. The plan was simple: desolder those three resistors to separate the LED from the original chip, then add my own resistors on the ESP32 side and drive the LED from there, for example as a status light for the assistant (listening, thinking, speaking).

While desoldering the resistors, I ripped off the pads as well. That was the end of the idea. The LED can no longer be driven, so the project failed here. That one is on me.

The only good news is that I disconnected the LED, so at least it stopped blinking. It is still sitting there, useless but quiet.

I keep an old config with an RGB LED on GPIO12 to GPIO14 as a backup, in case I want a status light later. Another option is the RGB LED that is already on the ESP32 board.

Step 6: fitting everything into a tiny box

Now comes the worst part: how to fit all of this into a small box.

You will need a tool that cuts plastic (a rotary tool with a cutting disc, for example) because you have to remove a lot of plastic inside the cube.

My final setup: cut away the unneeded plastic, including the cover and the back cover, then fit everything in carefully. You will most likely take it apart and put it back together several times, as I did, so don't glue anything yet. Just put things inside and find out what fits.

The ESP32 will most likely fit diagonally in the box. After that, you can tuck the rest of the parts wherever there is space.

The speaker and the wires inside the cube
Everything fitted in, with the ESP32 diagonally

Again, disclaimer: I'm not an electrician, so do this at your own risk.

The microphone hole

The microphone is its own little problem. I couldn't find a place for it in the head of the cube, so I drilled a hole in the right side of the cube, the same size as the speaker holes. Then I used double-sided tape to stick the microphone over the hole.

Why the right side? My speaker sits in a corner of the kitchen and I mostly talk to it from the right. So this is a personal choice. Put the hole where you will stand.

Marking the hole with a paper template
The marked spot on the side of the cube
Drilling the hole
The finished microphone holes

The ideal place would be the top, where the buttons are, because it would be the most universal. But I couldn't find any space there. I just couldn't. Maybe I'll try again, but no luck so far.

At first the assistant sometimes misheard me, and I blamed my accent and the cheap microphone. It turned out the speech-to-text model was the weak point, not the mic: a small Whisper turned "speed of light" into "Peter Flight". After I switched to Parakeet it understands me correctly, with the same microphone and the same hole.

How well does it listen?

The wake word works pretty well. Not only "okay nabu" but also "hey jarvis" and "hey mycroft" are picked up reliably. For a short question, the answer starts about five seconds after I stop talking: roughly 0.6 seconds for speech-to-text, one to two seconds for Claude, and two to three seconds for Piper to speak the first sentence.

Tips I really like

The best part of using Claude as the brain is that you can write instructions for the assistant in Home Assistant, in the conversation agent's settings. It then follows them every time. Two that I use daily:

Morning news. I always wanted to hear the morning news. Home Assistant reads the BBC World and a Czech news RSS feed. Claude picks the most important stories (not just the newest ones) and I get a short news-anchor style briefing: "Good morning, here is what is happening…", three world stories with a line on why they matter, and two from the Czech Republic translated into English. The picking happens once, only when the feed changes, and the result is cached in a helper. When I ask "what's the news", a plain automation with a conversation trigger answers from that cache, so it costs zero tokens and no waiting for the model.

Recipes. When I ask for a recipe, the instructions tell it to:

  • give me one short sentence about the dish, then the ingredients, and only then the steps, two or three at a time, asking "Ready for the next steps?" so I can cook hands-free,
  • always convert the units to the metric system (grams, milliliters, degrees Celsius), because most recipes online use cups and Fahrenheit and that is annoying,
  • and spell out the units in words so they sound right when spoken: "degrees Celsius", not "C", and "half a teaspoon", not "1/2".

An example of what that looks like in the instructions (adapt it to your taste):

You are a voice assistant in a kitchen. Answer in English, briefly, in plain text, because everything is read aloud. Use metric units only and spell them out. For recipes: one short sentence about the dish, then the ingredients, then the steps two or three at a time, asking if I am ready for the next ones. For news, call get_news and speak like a radio news anchor: a short intro, three stories (world first, then the Czech Republic). If you offer to continue, end with a short question.

Keeping the conversation going. If the assistant's answer ends with a question (my instructions tell it to do that when it offers to continue, for example "Ready for the next steps?"), the speaker starts listening again by itself, without the wake word. That is what makes step-by-step recipes pleasant. When the answer is not a question, it goes back to waiting for the wake word.

Future improvements

It works, but it is far from finished. Here is my to-do list:

  1. Fix the wires. I just took two long wires and stuffed them in, and it's a mess inside. I need to cut them to length and route them properly.
  2. Better holes for the microphone. Recognition is good now, but the mic sits behind a single hole next to a speaker in a tiny box, so I want to test other positions and add a foam gasket to keep the speaker out of the mic.
  3. Better Wi-Fi. The board sits around a corner in the kitchen and the signal is weak (about −69 dBm). I first blamed this for the choppy audio, but the real cause was a slow Piper voice (see the gotchas), so it is no longer urgent. My board seems to be the version with a PCB antenna and no antenna connector, so a real external antenna would need a different board with a connector.
  4. Customize Home Assistant more. I want this to be far more customizable than a HomePod or the other stock smart speakers. I'll keep experimenting with the Home Assistant settings.

I'll share the results here when I get to them.

Back