Take over the action button on a rooted Echo Dot (2nd Generation) and present the Dot to Home Assistant as an ESPHome device, while stock Alexa keeps running.
Home Assistant adopts it with its own first-party ESPHome integration: no custom component, no MQTT, and no Home Assistant credential on the Dot. The Dot reports a press and a hold of the action button to Home Assistant, reports what it can read about itself over the same connection, takes the button back from Home Assistant and hands it over again, and chimes on the device itself for every press it takes.
This runs on a device you own and have already rooted. Nothing here is supported by anyone, and a FireOS update can invalidate any of it. Rooting voided Amazon's warranty, and the flashing that gets you there can brick the Dot; both are behind you before anything here runs.
Alexa commands are optional, and what they risk is the account, not the Dot. They reach an undocumented endpoint with a credential that carries the whole Amazon account, and that endpoint is the Alexa app's own rather than an interface Amazon offers, so driving it may sit outside Amazon's terms. Read Alexa commands before building MapDump. Without the jar the daemon never offers them.
The Dot also announces itself to Music Assistant, and plays. It advertises
_sendspin._tcp.local. on tcp/8928, joins a group as a player@v1, keeps its
clock against the server's and plays the audio it is sent on the frame that
audio's own timestamp names. It reports available: false until that clock has
converged, which takes about a fifth of a second, and stays false on a Dot whose
speaker could not be opened at all. Anything that can reach the Dot on wlan0
can take that session, because the pairing flow is not implemented and the
fallback key is a published constant; what is behind it is playback and nothing
that reads off the device, but see SECURITY.md for what it does
hold. switch.<name>_sendspin turns the whole thing off -- the advert, the port
and any session -- and the setting survives a reboot.
number.<name>_sendspin_output_delay is the output delay, the one figure a music
server can also set: the last end to set it wins, and the number shows what is
applied whichever did.
What has not been settled is whether the Dot is in step with a second speaker
to better than a few milliseconds. One Dot can measure everything but that, so
grouping it with another Sendspin player and listening is the check that is left;
docs/sendspin.md says why and Music Assistant's own per-player delay is the dial
for whatever is left over.
Taking the action button takes it from Alexa. Stopping a timer or an alarm with
it, press-to-talk, and holding it to enter setup mode all stop working while the
daemon holds it. The Action button mode select gives it back without stopping
anything else, so the Dot keeps reporting and keeps its entities while Alexa
has her button. Mute and the volume keys are untouched throughout.
The other projects on this hardware all replace Alexa. This one does not.
EchoMuse gives the Dot "a second life as a fully local, open-source voice assistant and media player for Home Assistant", replacing the Alexa firmware with a Go server. It is also the practical route to a rooted Dot, because its docs cover the amonet-biscuit unlock.
echolocal is the closest neighbour: the same Dot, also Go, also speaking the ESPHome native API, with local wake word detection, LED ring control, a media player and a Bluetooth proxy.
EchoGo sits lowest: "A Go SDK for your Echo Dot 2. Gen", giving programmatic control of the LEDs, microphone, speaker and buttons. If you want to write the device's software yourself, that is the toolkit.
overdub keeps stock Alexa running and adds to her. The action button is the only thing taken, and if the daemon dies it goes straight back to her.
For a local voice satellite with Amazon out of the picture, use one of those. To keep the Echo you have, with her voice, her music, her timers and the mute button, and add a Home Assistant button and a handful of entities, use this one.
- an Echo Dot (2nd Generation) you have rooted, with Magisk. Model RS03QR, codename biscuit, FireOS 5.5.5.4. The model number is printed on the underside, and everything here was measured on that one
- Magisk 17.3, or another that keeps
service.dinsidemagisk.imgat/sbin/.core/img/.core/service.d. That is the only pathinstall.shwrites the boot script to, and it fails there rather than guessing. A Magisk that uses/data/adb/service.dneeds that path changed first adb, on a development machine. A release needs nothing else- to build it yourself instead: Go 1.25 or later, and an Android NDK for the
chime. It is played through OpenSL ES, so the daemon is cgo and
build.shwill not run without one.brew install --cask android-ndkon macOS, or a release from developer.android.com/ndk elsewhere, withANDROID_NDK_HOMEpointing at it - Home Assistant on the same subnet as the Dot
The buttons on this hardware:
| Node | Name | Keycodes |
|---|---|---|
/dev/input/event1 |
mtk-kpd |
138 action ("dot"), 113 mute |
/dev/input/event2 |
keys (gpio-keys) |
115 volume up, 114 volume down |
The daemon opens event1 and takes keycode 138 from it, re-emitting the rest.
event2 is listed for orientation only: nothing here opens it. To check them on
another device, FireOS already ships the tools:
adb shell su -c 'cat /proc/bus/input/devices' # names, handlers, key bitmaps
adb shell su -c getevent # events, without grabbingEchoMuse's debloat step suppresses the Alexa stack this runs beside, so a Dot rooted through it needs that undone first. Nothing was uninstalled, so nothing needs reinstalling:
deploy/restore-amazon.sh # then rebootEverything comes back except com.amazon.device.software.ota, left hidden on
purpose: an OTA rewrites boot.img, removing Magisk and taking root and overdub
with it. EchoMuse's own payload is moved to /data/local/echomuse-disabled/
rather than deleted, except the service.d debloat hook, which is removed: it
is what suppresses the stack again on every boot.
Skip this if the Dot never had EchoMuse. It restores every package that is currently hidden or disabled, not just EchoMuse's, so running it on a Dot where you have suppressed things yourself will undo that too.
Each release carries one tarball
holding the scripts, the binary and mapdump.jar, already built. Unpack it and
install from it; the rest of this file applies unchanged from there:
tar xf overdub-v1.0.0.tar.gz
overdub-v1.0.0/deploy/install.sh kitchenSHA256SUMS sits beside the tarball, and the build is attested, so GitHub can
be asked which workflow run and which commit produced the file you have:
shasum -a 256 -c SHA256SUMS # sha256sum -c, where you have that instead
gh attestation verify overdub-v1.0.0.tar.gz --repo bboe/overdubA released binary says what it is, which one built here does not:
adb shell 'su -c "/data/local/bin/overdub -version"'The Dot's linker prints four WARNING: linker: lines of its own first, and adb
merges them into the same stream. The version is the last line:
overdub v1.0.0
Build it yourself instead for anything you want to change, or if you would
rather not run a binary somebody else compiled. The tarball carries no
build.sh, and install.sh builds when it finds one and installs what is in
build/overdub when it does not.
This section needs the repository. The release tarball carries neither
build.sh nor deploy/mapdump/build.sh, because it carries what they produce.
./build.shGOARCH=arm is not optional, and build.sh pins it. A build for any other word
size fails to compile rather than producing a daemon that misreads every input
event: internal/evdev asserts the 32-bit timeval this device has.
GOOS=android is not optional either, and is the less obvious of the two: the
chime is cgo against OpenSL ES, and Go's linux runtime hangs before main
against Bionic rather than failing. build.sh sets it, finds the NDK compiler,
and makes the empty libpthread stub Bionic needs and cgo asks for. Set
ANDROID_NDK_HOME if the NDK is not where it looks; it says so if it cannot
find one.
Alexa commands additionally need mapdump.jar, which is not in the repository
and is not built by build.sh. Skip this if you do not want them. $SDK is your
Android SDK root, and deploy/mapdump/build.sh names both jars, where to
download them, and the JDK it needs:
ANDROID_JAR=$SDK/platforms/android-22/android.jar \
R8_JAR=$SDK/build-tools/34.0.0/lib/d8.jar deploy/mapdump/build.shdeploy/install.sh kitchen # binary, boot script, keyinstall.sh picks the jar up from deploy/mapdump/mapdump.jar if it is there
and says so either way; nothing else about the install changes.
Installing under a different name needs a reboot to finish. The boot script
is read once at boot, so the loop that respawns the daemon keeps the old name
until then; the install prints REBOOT REQUIRED when it sees that.
More than one Dot on adb means telling it which. install.sh uses plain
adb, so ANDROID_SERIAL picks the target:
adb devices # serials
ANDROID_SERIAL=<serial> deploy/install.sh kitchen| File | Where |
|---|---|
overdub |
/data/local/bin/ |
overdub.sh |
Magisk service.d, inside magisk.img on Magisk 17.3 |
.overdub-noise-key |
/data/local/bin/, mode 600, generated if absent |
adb_keys |
/data/local/bin/, from ~/.android/adbkey.pub or $ADBKEY; enables Secure, and removed if there is no key to push |
The name is required, and must be unique on your network. Every entity id
Home Assistant creates is prefixed with it, so a duplicate collides there, and
adding a second Dot under a name already in use stops the flow with a conflict
menu rather than completing. Lowercase letters, digits, - and _, at most 63
characters, and not starting or ending with -. Those are ESPHome's own naming
rules, and install.sh rejects anything else.
Changing it later is allowed. Home Assistant identifies the device by its MAC rather than its name, so a rename is accepted on the next connection and the stored name is updated in place; the entity ids keep the prefix they were created with. The display name is separate, and you set it in Home Assistant afterwards.
<name> below is therefore the name Home Assistant knows the device by,
which is this one until somebody changes it there. Renaming the device in Home
Assistant offers to rename its entity ids to match, so a Dot installed as
kitchen and renamed afterwards answers to the new name in every id in the
table and to neither name in the daemon's own log, which goes on saying what
-name was given.
Reboot to start the daemon. It is supervised, and failure is fail-open: if it dies the grab is released and the action button goes back to Alexa.
The boot script runs the daemon. To run it by hand, as root and by full path,
because /data/local/bin is on nobody's PATH:
adb shell 'su -c "/data/local/bin/overdub -name kitchen"'| Flag | ||
|---|---|---|
-name |
required | unique device name Home Assistant identifies the Dot by |
Everything else is fixed in the binary, none of it having a second sensible
value here: event1 and keycode 138, mtk-kpd for the clone, wlan0 and
tcp/6053.
deploy/uninstall.shThe order matters if you interrupt it: the boot script goes first, so a reboot
part way through leaves a Dot with nothing running rather than a supervisor
respawning a half-deleted install. The daemon gets SIGTERM rather than being
killed outright, so it gives the button back and destroys its uinput clones on
the way out.
Everything goes: the boot script, the binary, the API key, the Sendspin identity,
mapdump.jar and the directory it sits in, the log the boot script writes, and
the two properties that remember whether Sendspin was switched off and the
output delay it was left at -- so installing
again starts with it on rather than inheriting a decision nothing on the device
explains.
/data/local/bin goes with them if nothing else is left in it. Removing the jar
revokes nothing: see Alexa commands. The Sendspin identity
matters as much as the API key does -- the pairing token is derived from it, so a
copy left behind stays valid for a Dot that no longer runs this.
The tcp/6053 and tcp/8928 rules the daemon opened go too, once the daemon is confirmed gone, so no reboot is needed. An uninstall that reports trouble stops before that step and leaves them in the chain.
Amazon's stack is untouched, because installing never touched it. Home Assistant will show the device as unavailable; delete it there when you are done.
The Dot announces itself, so Home Assistant discovers it: it appears under
Settings -> Devices & Services as an ESPHome device named after -name,
and adding it asks only for the encryption key the installer printed.
If it does not appear, add it by hand at Add integration -> ESPHome with the
Dot's address and port 6053; adb shell ip -4 addr show wlan0 gives the
address. Discovery is multicast and does not cross subnets, so a Home Assistant
on a different network segment needs the address either way.
The key is the whole of the access control. ESPHome has no peer allowlist, so anything that can route to the Dot may open a connection. What the key guards is what that connection reaches, not whether it is made: a peer without the key learns the device name and holds one of eight slots for ten seconds, and eight of them can keep Home Assistant off the Dot for as long as they care to. The firewall rule matches the interface rather than a source range, so that reach is wider than the local subnet, and a VPN client on another one is inside it. SECURITY.md has the measurement.
A DHCP reservation is still worth setting. Home Assistant stores the address it found, and re-finds the device by name after a change, but only while discovery reaches it.
The device is the server and Home Assistant dials in, the reverse of most integrations. It needs inbound reach to tcp/6053, which the daemon opens on the Dot's own firewall.
| Entity | Kind | Notes |
|---|---|---|
event.<name>_action_button |
none | Home Assistant's standard button types: press_end, multi_press_end, long_press_start, long_press_end; an event, so it has no state between presses |
sensor.<name>_uptime |
diagnostic | seconds since boot |
sensor.<name>_wifi_signal |
diagnostic | dBm; a reading that is not a signal is reported as missing rather than as zero |
sensor.<name>_volume |
diagnostic | percent of the speaker's own scale, which is 30 steps here; a muted stream reads as zero |
sensor.<name>_cpu_temperature |
diagnostic | °C, from the SoC's own thermal zone |
sensor.<name>_memory_available |
diagnostic | MiB the kernel says an allocation could get, which is not the same as free |
sensor.<name>_jack_volume |
diagnostic | percent, for the 3.5mm output rather than the speaker; a muted stream reads as zero here too |
binary_sensor.<name>_audio_jack |
diagnostic | whether anything is in the 3.5mm socket |
binary_sensor.<name>_speaker_playing |
diagnostic | whether audio is coming out, by either wired route; sound shorter than about a second and a half is not reported, and bluetooth is not seen at all |
binary_sensor.<name>_alexa_registered |
diagnostic | whether the Dot holds an Amazon account, which is what setup gives it; a Dot that was never set up, or that deregistered itself, reads off while the button and the rest of this list go on working |
media_player.<name>_speaker |
none | the volume, the control that changes it, and playback: it sets the level of whichever route is live, and plays an mp3 you give it by handing the URL to Alexa's own synthesizer |
switch.<name>_microphone_muted |
none | whether the microphone is muted, and the control that changes it; muting from here presses the mute key, so it is the mute the button performs, ring included |
event.<name>_mute_button |
none | the microphone mute key, reported the same way the action button is |
select.<name>_mute_button_mode |
config | what the daemon does with the mute key; ships in monitor |
select.<name>_action_button_mode |
config | what the daemon does with the action button: intercept, monitor or pass through |
text.<name>_alexa_command |
config | a box that runs what you type on the Echo as though it had been spoken; listed only where mapdump.jar is installed and the Dot is registered |
select.<name>_network_adb |
config | adb over the network on tcp/5555: Off, Insecure, and Secure when a key was installed |
switch.<name>_sendspin |
config | whether the Dot offers Sendspin at all: off withdraws the mDNS advert, closes tcp/8928, deletes its firewall rule and ends any session in progress. Survives a reboot. Listed only where the Dot could read its Sendspin identity |
number.<name>_sendspin_output_delay |
config | milliseconds the Dot plays a server's audio ahead of the moment it was stamped for, 0 to 5,000. A music server can set the same figure and the last one set wins; whichever did, this is what is applied. Survives a reboot, works with the switch off, and is listed under the same condition |
Uptime, signal and the registration are read once a minute, and again when Home Assistant subscribes. Both volumes, the jack, the temperature, the memory and the microphone are read every two and a half seconds, whether the speaker is playing every half second, and all of it only while something is subscribed -- except uptime and signal, which are cheap enough to read either way. So the registration has no state to show on the first connect after a restart, until that subscriber's own reading arrives a moment later.
The microphone switch carries one caution: unmuting needs only the API key, not a hand on the Dot, so anything holding that key can turn the microphone back on. Everything is sent only when it changes, so the uptime arrives every minute, the others when they move, and a quiet short tick costs the reads and no traffic at all.
That is why a volume you have just turned appears within a few seconds, whether you turned it with the buttons, from an app, or by asking Alexa.
Setting it from Home Assistant goes the other way down the same path: the daemon asks Android for the level outright, so it lands exactly where you put the slider and every other reader of the volume agrees with it afterwards. It moves the route you are hearing, so with headphones in the socket the slider moves the socket's level and leaves the speaker's alone.
It is silent, and it used to be heard: the daemon pressed the volume keys one per step, and Android ticks on every adjustment. An automation setting the volume at four in the morning no longer wakes the room.
Music Assistant can set it too, over Sendspin, and the same is true there. A Dot that offers no volume is left out of the group volume a server works out, so these are now part of it.
volume is the speaker's own level and jack_volume is the socket's. Android
keeps a level per route and switches between them when you plug something in,
so between those two, the one you are hearing is whichever audio_jack says.
Both are reported whatever is plugged in, because both are real levels the
device would return to. Mute is not per route, so muting reads as zero on both
at once.
A bluetooth speaker is a third route and is not reported at all. Pair one and
Android tracks its level separately again, so neither of these readings is what
you are hearing and audio_jack does not say so.
media_player.play_media hands the URL to Alexa's synthesizer, which fetches and
plays it the way she plays her own speech -- mixed and ducked against whatever
else is going on, rather than fighting it. Two rules come from her rather than
from here:
- The clip must be CBR mp3 at 48 kbps, 24 kHz, mono. Anything variable is
refused by her demuxer, and the refusal reads exactly like a file that is not
there. Home Assistant's
tts.speaktakespreferred_bitrate: 48from 2026.9, which is the whole of the setup on newer versions. - The URL must be
http://, with no comma or double quote in it. The intent that carries it is a comma-separated array and hand-built JSON, so those two characters end it early.httpsis not refused by Alexa so much as unverified here.
The Dot fetches the clip itself, so whatever serves it has to be reachable from
the Dot, which is the opposite direction from the one Home Assistant uses to
reach the Dot. A /local/ file served by Home Assistant is the ordinary case.
action: tts.speak
target:
entity_id: tts.home_assistant_cloud
data:
media_player_entity_id: media_player.kitchen_speaker
message: "The back door has been open for ten minutes"
options:
preferred_bitrate: 48The entity reports playing while Alexa is actually playing rather than from
the moment you ask, because the daemon learns it from her playback log; expect
roughly two thirds of a second before it moves. If she never plays it at all,
the entity gives up and goes back to idle rather than sticking.
When nothing plays, the daemon's log is the wrong place to look. It records what was asked for and nothing else, because everything after that is hers: she fetches the clip, she decodes it, and she is where it fails. Her log is where the reason is:
adb shell 'su -c "logcat -d -v brief -s tts-Server tts-Playback"'Three failures look alike from Home Assistant and are easy to tell apart there:
cannot estimate length of the next mp3 frameis the encoding -- a variable bitrate her demuxer will not take.Playback ended: ... FAILEDwith nothing before it is usually the fetch: a 404, or a URL the Dot cannot route to.- No lines at all means the request never reached her.
The routing one catches people out, because it fails in complete silence. The Dot fetches the clip itself, and a Dot on an IoT network often cannot open a connection to the machine serving it even though that machine reaches the Dot perfectly well. Ask the Dot rather than assuming:
adb shell 'su -c "curl -sS -m 5 -o /dev/null -w %{http_code} http://<host>:8123/"'200 means the route is open. A curl error and 000 is the answer: open that
one route, and everything else already works.
The mode selects are the only entities Home Assistant writes to. There is one per button -- the action button and the microphone mute -- and each has three settings:
| mode | Alexa | Home Assistant |
|---|---|---|
intercept |
nothing | events |
monitor |
answers the press as usual | events |
pass through |
answers the press as usual | nothing |
The action button ships in intercept, which is what the daemon is for. Mute
ships in monitor: taking it by default would leave a Dot that cannot be
muted by the button that says mute on it, and monitor is additive -- Alexa still
mutes, and your automation still fires. pass through gives the
button back -- press-to-talk is measured, and timers and setup mode follow the
same key path -- while the daemon keeps running and keeps reporting everything
else. monitor is both at once: press-to-talk still works and your automation
fires too. Only an intercepted press chimes, because in monitor Alexa answers it
herself and two acknowledgements for one press is worse than none.
Changing it takes no reboot and no reinstall. A press arriving mid-change belongs to whichever mode was set when the key went down, so nothing is ever half-delivered.
The daemon starts in intercept, and the mode is not remembered across a
restart: a Dot that reboots comes back holding its button whatever was set
before. Home Assistant will restore it if you ask it to, with an
automation on homeassistant_start or on the device becoming available.
audio_jack is on whenever the socket is occupied and nothing more. The
detection is electrical and stops at the contacts: a bare cable with nothing on
the far end reads the same as headphones, and unplugging the far end of a
connected cable is invisible to it.
action_button is the button itself. It reports Home Assistant's standard
button gestures rather than names of its own, so an automation written for any
other button works here.
Quick presses are collected into a run, which fires multi_press_end once with
its count about a third of a second after you stop pressing. A single press
fires press_end, and waits out that same third of a second first: nothing
knows a press was single until it has.
Holding past six hundred milliseconds, Alexa's own threshold for a long press,
fires long_press_start while you are still holding, so an automation can
run for as long as the button is down. Letting go fires long_press_end. A hold
ends any run in front of it, and that run is reported first.
If the daemon is busy enough to notice the threshold late, it falls back to the duration the release carries and sends both at once. The hold is still reported; it is not reported early.
The chime does not wait. It sounds as the button goes down, once per press, so
four presses are four chimes and one multi_press_end.
An EventResponse carries a type and nothing else, so the numbers arrive beside
it as an esphome.overdub_pressed event on Home Assistant's bus. It always
carries event_type, device and button -- every button fires the same bus
event, so button is what tells them apart and an automation wants it in its
trigger. multi_press_count comes with multi_press_end, held_ms with
long_press_end. Both are integers, so
{{ trigger.event.data.multi_press_count == 7 }} works without a cast. The
blueprint in ha/ wires the gestures up.
Every button fires the same bus event, so an automation that filters only on
device_id runs for all of them: with mute in monitor, a press of the mute key
would run an action meant for the action button. Filter on button as well. The
blueprint in ha/ does, but Home Assistant does not update a blueprint you
have already imported -- re-import it after upgrading, or the mute key will run
your single-press action.
long_press_start and long_press_end are not a guaranteed pair. Events carry
no state, so if Home Assistant misses the release -- a restart, a reconnect --
whatever the hold started keeps running. Give such an automation its own timeout.
It is an event rather than a sensor, so it has no state to read: an automation triggers on it, and the dashboard shows no value between presses. A Home Assistant that was not connected does not learn about a press afterwards.
Only a press the daemon reports does any of this. In pass through the button
is Alexa's and a press is unreported. The chime tells intercept from the other
two rather than telling you the daemon is alive: monitor reports the press
without chiming, so silence there is the mode working as asked.
The mode selects are still the only entities Home Assistant writes to. Everything else reports, the button included, and together they are the connection proved end to end in both directions.
With mapdump.jar installed, the Dot gains a text box on its device page and a
matching action. Either runs text on the Echo as though somebody had said it:
action: esphome.kitchen_send_command
data:
text: play dance party music on Amazon MusicTwo things have to be true: the jar has to be installed, and the Dot has to be registered to an Amazon account. If either is missing, neither the box nor the action is advertised at all, rather than being offered and failing on every call, and the daemon log says which one it was. The one exception is a Dot that cannot be asked -- if the registration reading itself fails, the box is offered rather than hidden, because the jar is the switch somebody chose and a reading that did not happen is not an answer. The box shows the last command it was given, so an automation that sent one can be seen to have sent it.
Registration is the credential, which is why it gates the feature.
binary_sensor.<name>_alexa_registered reports it, and a Dot that was factory
reset, or rooted and restored, usually is not registered.
Registering one is Alexa's own process, and the daemon's only part in it is to get out of the way:
- Set
Action button modetopass through, which gives Alexa her button back without stopping anything else here. - Hold the action button until the ring turns orange. That is Alexa's own long press, and it is what the daemon was intercepting.
- Add the device in the Alexa app. The Dot brings up its own Wi-Fi network to
finish, so it leaves your LAN and Home Assistant shows it as unavailable
until setup ends.
adbover USB is unaffected;adbover the network goes with the LAN. - Set the button mode back to
intercept.
The daemon needs nothing else: it asks again every five minutes, so the command box appears on its own once the Dot is registered, with no restart and no reinstall.
This is an unofficial endpoint, reached with an account-level credential.
/api/behaviors/previewis undocumented and its request shape has drifted before.What MapDump reads is the OAuth refresh token this Echo was registered with, and the daemon trades it for cookies scoped to
.amazon.comrather than to an Alexa subdomain. Measured, what comes back isat-main,sess-at-main,session-id,session-token,ubid-mainandx-main-- the set a browser signed in to amazon.com carries. So while the daemon runs it holds a signed-in session on the retail account, order history and addresses included: the breadth alexa_media_player gets by asking you to log in, arrived at from the other end. Not AWS, which is a separate sign-in. The token and the cookies are kept in memory and never written to disk, and the only lock on the port that reaches them is the API key.The failure mode to watch for is not an error from the daemon but the Dot quietly deregistering. To revoke what it holds, deregister the Dot in the Alexa app or under Manage Your Content and Devices; uninstalling removes the jar but revokes nothing already extracted.
Network ADB turns adb on over the network, on tcp/5555, so the Dot can be
worked on without a cable. It has three positions, and a reboot always returns
it to the first:
| Position | What it does |
|---|---|
Off |
adbd stops listening on the network, and tcp/5555 is closed again |
Insecure |
adb is open to the local network, and anyone on it may connect |
Secure |
adb is open, but only a client holding the installed key may connect |
Connect with adb connect <address>:5555. ANDROID_SERIAL then picks that
target for install.sh as readily as a cable does.
Off and Insecure are the network only. Neither touches adb over USB, so
Off is not a way to lock the Dot down against someone holding it: it closes
tcp/5555 and nothing else.
Secure reaches the cable as well. ro.adb.secure is a setting on adbd
rather than on one transport, so while it is on, every adb connection is
challenged. A machine that does not hold the installed key gets unauthorized
over USB too, and this Dot has no screen to show the prompt a phone would.
Changing position restarts adbd, which drops every live adb session -- the one
you may be watching from included. Do not change it from an install that is
running over adb.
Secure is offered only if deploy/install.sh found a public key to install --
~/.android/adbkey.pub, or whatever ADBKEY names. Without one it would refuse
every machine including yours, so it is not listed at all. That key becomes the
only one adbd will accept: a Dot that had authorised other machines over USB
stops accepting them.
Secure is authentication, not a sandbox.
ro.secure=1on this build, so adbd runs asshelland root still comes fromsu. A client holding the key reaches root exactly as it did before. It decides who may connect, not what they may do, and it is not a reason to expose tcp/5555 beyond your own network.
If a key ever locks you out, set the control back to Off or Insecure from
Home Assistant. That path is the ESPHome API on tcp/6053 and owes nothing to
adb. Failing that, power-cycle the Dot.
Uninstalling does not turn it off. The position lives in the property store and
the firewall chain rather than on disk, and uninstall.sh may itself be running
over the connection it would cut, so it says so and leaves it to a reboot.
Installing with no key while the Dot is in Secure leaves the control reporting
a position it no longer offers, and Home Assistant marks that state invalid.
Both halves are true: the Dot really is in Secure, and Secure really cannot
be chosen with no key to install. Nothing is broken by it, and a reboot clears
it, since ro.adb.secure does not survive one.
The API speaks ESPHome's Noise_NNpsk0_25519_ChaChaPoly_SHA256, and speaks
nothing else. There is no plaintext mode, because this build does not implement
one, and no peer allowlist, because ESPHome has no such concept: the device is
the server, and the client authenticates with a pre-shared key.
deploy/install.sh generates that key when the device has none, the way
ESPHome's own tooling does, and prints it once:
Generated an API encryption key. Paste it into Home Assistant's
ESPHome integration. The installer keeps no copy:
kR2b...
Keep it. The installer does not take a key of your own, and re-running it leaves an existing one alone, so reinstalling does not lock Home Assistant out of a Dot it was already talking to.
To rotate: delete /data/local/bin/.overdub-noise-key on the Dot, install
again, and paste the new key into Home Assistant.
adb shell 'su -c "cat /data/local/tmp/overdub.log"' # truncated per boot
adb shell 'su -c "iptables -L INPUT -n -v | grep 6053"' # packet counter
adb shell 'su -c "logcat -d -v brief -s tts-Server tts-Playback"' # Alexa on playback| Symptom | Look at |
|---|---|
| you need the Dot's address to add it | adb shell ip -4 addr show wlan0 |
| Home Assistant times out adding the Dot | the tcp/6053 packet counter. Zero means the traffic never arrived |
Home Assistant logs Unexpected device found |
the stored address now answers with a different MAC, so it is a different device: set a DHCP reservation |
| Home Assistant says the key is invalid | the daemon log. handshake failed means the key it sent is not the one on the Dot |
| Home Assistant says the device requires encryption | it has no key stored for this Dot; give it the one the installer printed |
nothing starts, and the log ends no such file or directory (deploy/install.sh generates one) |
there is no key on the device: rerun deploy/install.sh <name> |
nothing starts, and the log says the key decodes to N bytes |
the key on the device is corrupt. A reinstall keeps an existing key, so delete it first: adb shell 'su -c "rm -f /data/local/bin/.overdub-noise-key"' |
nothing starts, and the log says NAME is unset |
the boot script was installed by hand; rerun deploy/install.sh <name> |
| mute stopped working | the clone's name. Android picks a keylayout by device name, so it must be mtk-kpd |
| every keycode looks wrong | the build. GOARCH=arm is required |
| the button does not chime | the mode first: only intercept chimes. Then the daemon log, where presses will be silent means the audio player did not start. It needs nothing of Alexa's stack, only the device's own libOpenSLES.so |
| the button does nothing, and the device is otherwise online | the Action button mode select, then the daemon log, which names the address that changed it |
| the button rings Alexa rather than chiming | the mode. pass through is Alexa's alone; monitor is hers and reported, and only intercept chimes |
docs/ is the engineering record: what was measured on the hardware, and what
each decision is defending against.
- Hardware: the input nodes and keycodes, and how to test against a live Dot
- Hard constraints: what cannot change, and why
- The button: the grab, the clone, the modes, and what a press reports
- The Home Assistant API: the entities, the polls, and the encryption
- Finding the Dot: the mDNS responder, and what it advertises for
- Network adb, and the microphone: the two things Home Assistant can switch on the Dot itself
- Audio: what it takes to make a sound here, and the chime
- Things that fail silently: the failures that report success
- Deployment: installing and removing it
BSD 2-Clause; LICENSE.txt carries the terms.
The chime is original to this repository, so the licence covers it as it covers
the code. It is not a recording and not an asset: internal/audio/chime.go
renders it at startup from two sine tones, 880 Hz then 1320 Hz, faded out over
the last tenth of a second. It was an mp3 built by ffmpeg until the daemon
learned to make the sound itself, and those are that clip's own numbers.
This licence covers the code in this repository and nothing else. This repository contains no Amazon code. The Amazon names it does carry identify things already installed on the device, so that this software can interoperate with them: functional names, not copied implementation.
overdub is not affiliated with, endorsed by, or supported by Amazon.