A · Battery bypass

Run the tablet with no battery — the right way

An eleven-year-old lithium pouch, sealed to a wall, permanently warm, is the one genuine hazard in this whole build. The fix is to remove the cell and feed the tablet directly. The trick that makes it reliable — and the part I got wrong the first time — is to keep the pack's own little circuit board.

That PCB glued to the cell is the pack's BMS — battery management. It carries three things the tablet depends on: a protection circuit (over/under-voltage and over-current MOSFETs in series with the cell), an NTC thermistor for temperature, and usually a small fuel-gauge or ID chip. The one thing it does not contain is the charger itself — that CC/CV charging IC lives on the tablet's mainboard.

So feeding the bare mainboard connector is flaky: the tablet misses its thermistor and gauge and often refuses to boot, which is why the crude version needs a faked 10 kΩ resistor. Keep the BMS in circuit instead and feed your supply into its cell pads, and the tablet sees a completely normal battery — nothing to fake.

01

Keep the management board; lose the pouch

Open the pack and separate the lithium pouch from its BMS PCB, leaving the board and its mainboard connector intact. The pouch is done — discharge and dispose of it safely. Everything the tablet talks to stays on that board.

02

Feed ~4.0 V into the cell pads

Wire a regulated supply to the two pads where the pouch's + and − were soldered — not the mainboard connector. Set it to about 4.0 V: above the 3.8 V nominal, safely below the 4.2 V ceiling. The board passes it through its protection FETs to the tablet exactly as the cell would, and the mainboard charger sees a "nearly full" battery and simply idles.

03

Size the supply for the peaks

Use a buck converter that can source the tablet's >2 A current spikes (screen + CPU), fed from a beefy 5 V / 3 A or a 9–12 V brick — never the tablet's own micro-USB port, which can't run the system directly and browns out.

04

Know what changes

Don't exceed ~4.2 V or the board's own over-voltage protection cuts out (that's a safety feature working). And the fuel gauge now reads a fixed percentage forever — cosmetic, but it means the charge-trend tile and the whole power-management system on the wall panel become optional. With nothing left to drain, the panel just runs bright all day.

Advanced, and at your own risk. This means opening the tablet and soldering to a live lithium battery's board. Confirm polarity with a meter before you connect anything — the two pads reading 3.7–4.2 V are + and −. Disconnect and tape off the old pouch before it goes anywhere near the wall. Done right, there's no battery left to drain or swell — which is exactly why wall-panel builders do it.

B · Alarm remote relay bridge

Letting software press the key fob

The alarm's radio hub is slow to wake, so a voice "disarm" can lag. The fix is to press the buttons on a spare enrolled key fob electronically — an ESP32 drives four isolated relays, each closing across one button, so Home Assistant and Google can Arm, Disarm, Part-arm and Panic. The fob keeps floating on its own power; nothing on its side ever touches the ESP32's ground.

How it works

Each relay is an electronic finger. A GPIO goes high, the relay's coil pulls in, and its contact closes across the two pads of one button — electrically identical to pressing it. The contact is a dry, isolated switch: it shares no copper with the coil or the ESP32, so it bridges the fob's button without dragging the fob's ground onto anything else. That isolation isn't a nicety here — as the note below explains, it's the whole reason this works.

Why a relay — and not an optocoupler or a transistor. This is the lesson that cost the most bench time. The fob floats: its button "common" sits behind a reverse-protection diode, not at true ground. Anything that shares a ground with the ESP32 — a common-ground opto board, or a bare transistor — bonds the fob's common to the system ground the moment it switches, and that jams the remote dead every time. We proved it twice before the penny dropped. A relay's contacts are galvanically isolated from its coil and the ESP32, so they bridge a button's two pads exactly like a fingertip, with nothing shared. Isolation here is the whole trick, not an optional extra.

B · Wiring

Two worlds, one press

Wire colour map

Two worlds that never meet. On the control side the colours are triggers and everything shares one ground. On the isolated fob side the same colour is that button's signal and one grey serves the shared common. Red and black are power only.

FromWireToPurpose
Control side — shares the ESP32 / 12 V ground
12 V PSU +redRelay DC+ and buck input +Coil power
12 V PSU −blackRelay DC− and buck input −0 V (= ESP32 GND)
Buck Type-CUSB-CESP32 USB-C port5 V for the ESP32
ESP32 D23yellowRelay IN1Arm trigger
ESP32 D22greenRelay IN2Disarm trigger
ESP32 D21blueRelay IN3Part-arm trigger
ESP32 D19whiteRelay IN4Panic trigger
ESP32 GNDblackRelay DC−Logic ground (shared)
Isolated fob side — floats, touches nothing above
Relay COM1yellowArm button, signal padOne side of the press
Relay COM2greenDisarm button, signal padOne side of the press
Relay COM3bluePart-arm button, signal padOne side of the press
Relay COM4whitePanic button, signal padOne side of the press
Relay NO1–4greyThe fob's shared common "G" padOther side of the press

The one rule that makes or breaks it: the fob's grey "G" wires (relay NO) go only to the fob's own common pad — never to the 12 V −, the ESP32 GND, or the relay's DC−. The instant those grounds meet, the remote jams. Leave the fob on its own cell (or give it a completely separate, isolated 12 V supply) so its ground stays its own.

One channel, end to end

D23 high → IN1 → the coil pulls in → the contact closes COM-to-NO, bridging the Arm button's signal pad to the fob's common pad. That's exactly the short a continuity probe makes across those two points to press the button. The other three channels are identical, each with its own colour.

Where everything plugs in

ESP32 · right-hand header

  • D23 yellow → IN1 · Arm
  • D22 green → IN2 · Disarm
  • D21 blue → IN3 · Part-arm
  • D19 white → IN4 · Panic
  • GND black → relay DC−

Relay module (4-ch, 12 V)

  • DC+red → +12 V (coil power)
  • DC−black → 0 V = ESP32 GND
  • IN1-4colour in from D23/D22/D21/D19
  • COMxthat button's signal pad (colour)
  • NOxgrey → the shared common "G"
  • jumperset High-level trigger (3.3 V high = on)

Remote PCB

  • SW1Arm · top
  • SW2Disarm
  • SW3Part-arm
  • SW4Panic · bottom
  • presssignal pad → COM · shared common → NO
  • powerown cell / isolated 12 V — ground stays apart
B · Hardware

The build, in photographs

Your hardware, annotated

The two ends of the isolated side, drawn onto your own photos. Positions are close, but always confirm against the silkscreen and a continuity check before you commit a joint.

Alarm remote: fob case and opened PCB, coloured markers on the four button solder points
The remote. Each button has a signal pad and a shared common "G"; a continuity probe across those two presses the button — that's the pair each relay closes. Function colour to the signal pad → relay COM; one grey to any common pad → relay NO (all four share it). Top switch is Arm, bottom is Panic. Leave the fob floating on its own power.
ESP32 DevKit with D23, D22, D21, D19 and GND marked on the right-hand header
The ESP32. All five control connections on the right-hand header, USB-C at the bottom. D23 and D22 at the top, skip TX0/RX0, then D21 and D19. GND goes to the relay board's DC− — check the silkscreen so black lands on GND, not 3V3.

The finished bridge

Built, wired, and driving the real alarm — the isolated relay quietly presses the fob on command.

The full bridge on the bench: 12 V supply, alarm key fob, four-channel relay board and ESP32 all wired together, one relay LED lit
The whole bridge. The 12 V supply (left) feeds the fob and, through a buck, the ESP32 (right). The ESP's four GPIOs drive the isolated relay board in the centre, whose contacts press the fob's four buttons. A lit green channel LED is a button being held.
Close-up of the four-channel SONGLE 12 V relay board with coloured signal wires on the screw terminals and one channel LED lit green
The isolated relay board. Four SONGLE 12 V relays. Each colour lands on a COM, its grey on NO; the dry contact bridges a button to its common exactly like a fingertip — with the fob's ground kept completely separate. Green LED = that channel energised.
The opened 868 MHz key fob PCB with yellow, blue, white and grey signal wires soldered to the four button pads plus power leads
The fob, tapped. A colour on each button's signal pad (yellow, blue, white, grey) and its own 12 V power. The 868 MHz transmitter, crystal and coil antenna do the rest — it's a real enrolled remote, so the alarm trusts it and the rolling code just works.
Close-up of the ESP32 dev board, USB-C connected, red power LED lit, GPIO labels visible
The ESP32. The brain. D23/D22/D21/D19 drive the relay inputs; the USB-C lead now carries both power and the serial control link to the Pi — there's no Wi-Fi in the control path any more.

Wired in — and it talks. The bridge is tied into the GlobalGuard panel in Home Assistant, so arm, part-arm, disarm and panic all fire the fob instantly — from the wall dashboard, and by voice through Google ("Night Security", "Day Security", "House Panic", and a PIN-gated disarm). The alarm's own radio hub is only read for status now; every command goes the fast way, through the remote.

The road here

The four-channel PC817 optocoupler board that was tried first and abandoned
The dead end. The first attempt used this 4-channel PC817 optocoupler board. On this unit all eight ground screws proved to be one net — a common-ground board, not truly isolating — so bridging a button dragged the fob's common onto the system ground and jammed the remote. The phototransistor outputs metered open even when driven. The fix was to stop chasing the opto and switch to a genuinely isolated relay contact. Kept here because the "why" matters more than the win.
B · Build

Building the bridge

Build order

01

Set the relay board, then bench-test a click

Set the High/Low jumper to High-level trigger. Power DC+/DC− from 12 V, tie DC− to the ESP32 GND, and touch IN1 to 3V3 — the relay should click and a meter across COM–NO should close. Repeat for all four before anything goes near the fob.

02

Keep the fob floating

Leave the fob on its own cell (or a separate, isolated 12 V supply). Its ground must never bond to the ESP32/PSU ground — that single mistake is what jams it. Nothing you wire on the isolated side touches the control side's 0 V.

03

Find each button's two press points

For each button, identify the signal pad and the shared common "G" — a continuity probe across the two triggers that button. Those two points are the relay's COM and NO. On this remote the common sits behind a reverse-protection diode, which is exactly why it must stay isolated.

04

Wire the isolated side

Per channel: COMx → the signal pad in its function colour; NOx → the shared common with grey (one grey serves all four). Screw terminals, or short solder tails if you tap the pads directly. Keep joints small so the case still closes.

05

Wire the control side

DC+ → 12 V, DC− → 0 V. IN1←D23, IN2←D22, IN3←D21, IN4←D19, one black from ESP32 GND to DC−. Every colour is one clean trigger from a GPIO to an IN.

06

Power up in order

12 V to the relay DC+/DC− and to the buck. Meter the buck's USB-C output — confirm ~5 V — before plugging the ESP32. Then buck USB-C → ESP32.

07

Flash the firmware

Load the ESPHome config below over USB once, then adopt in Home Assistant — four button.alarm_remote_bridge_* entities appear. After that it's fully over-the-air.

08

Test each in a safe state

Press each button in HA where a stray Arm or Panic won't wake the street. You should hear the relay click, see the fob's own LED blink, and watch the alarm respond. Panic holds the relay closed for 3.5 s (a sustained press) — the panel needs that; the other three are a quick 600 ms tap.

Panic is a held press, not a tap. Arm, Disarm and Part-arm register from a single clean 600 ms press. Panic is different: the panel wants the button held down for a few seconds before it trips, so Panic latches the relay for 3.5 s — a real thumb-on-the-button hold — then releases.

B · Control

Driven over a wire, not Wi-Fi

How it's driven now — a hardwired serial link

The Wi-Fi had to go. For weeks the bridge talked to Home Assistant over Wi-Fi, and for weeks it kept dropping — blamed in turn on the mesh, the channel, a reboot timer. The real cause only showed on the ESP's own serial console: parked next to the Pi, the Wi-Fi there is −84 dBm, too weak to finish the auth handshake, so it looped on Auth Expired and never reached the network. Every arm and disarm needed that network, so nothing landed — while the relays, fob and wiring were fine the whole time. The fix: keep the ESP plugged into the Pi's USB and make the wire the control path. No Wi-Fi, no API, no OTA — a serial line can't silently fall off the network.

The firmware became a pure serial build. logger: baud_rate: 0 frees UART0 — the same GPIO1/GPIO3 the USB port uses — and a 30 ms loop reads whole-line commands (ARM, DISARM, PART, PANIC, PING), presses the matching button and acknowledges each (OK ARM, PONG, or ERR). The hold times live here too: a 600 ms tap for arm and part-arm, a longer 2 s for disarm (the fob wants ~1.5 s), and a 3.5 s hold for panic.

alarm-remote-bridge.yamlUSB serial · no Wi-Fi
esphome:
  name: alarm-remote-bridge
  on_boot:
    priority: -100
    then: [ lambda: 'id(cmd_uart).write_str("READY\n");' ]
esp32:
  board: esp32dev
  framework: { type: arduino }

# NO wifi / api / ota — the control path is the wired USB serial line to the Pi.
logger:
  baud_rate: 0            # free UART0 (GPIO1/3 = the USB port) for our own use

uart:
  id: cmd_uart
  tx_pin: GPIO1
  rx_pin: GPIO3
  baud_rate: 115200

globals:
  - { id: cmd_buf, type: std::string }

# relay outputs — LOW at boot, so a reset can never fire the alarm.
output:
  - { platform: gpio, id: out_arm,    pin: GPIO23 }
  - { platform: gpio, id: out_disarm, pin: GPIO22 }
  - { platform: gpio, id: out_part,   pin: GPIO21 }
  - { platform: gpio, id: out_panic,  pin: GPIO19 }

# each button carries an id so the serial loop can press it
button:
  - { platform: template, id: btn_arm,    name: Arm,      on_press: [ output.turn_on: out_arm,    delay: 600ms,  output.turn_off: out_arm ] }
  - { platform: template, id: btn_disarm, name: Disarm,   on_press: [ output.turn_on: out_disarm, delay: 2000ms, output.turn_off: out_disarm ] }
  - { platform: template, id: btn_part,   name: Part-arm, on_press: [ output.turn_on: out_part,   delay: 600ms,  output.turn_off: out_part ] }
  - { platform: template, id: btn_panic,  name: Panic,    on_press: [ output.turn_on: out_panic,  delay: 3500ms, output.turn_off: out_panic ] }

# read whole-line commands off USB serial and act on them, acking each one
interval:
  - interval: 30ms
    then:
      - lambda: |-
          while (id(cmd_uart).available()) {
            uint8_t c; id(cmd_uart).read_byte(&c);
            if (c == '\n' || c == '\r') {
              std::string cmd = id(cmd_buf); id(cmd_buf) = "";
              if      (cmd == "ARM")    { id(btn_arm).press();    id(cmd_uart).write_str("OK ARM\n"); }
              else if (cmd == "DISARM") { id(btn_disarm).press(); id(cmd_uart).write_str("OK DISARM\n"); }
              else if (cmd == "PART")   { id(btn_part).press();   id(cmd_uart).write_str("OK PART\n"); }
              else if (cmd == "PANIC")  { id(btn_panic).press();  id(cmd_uart).write_str("OK PANIC\n"); }
              else if (cmd == "PING")   { id(cmd_uart).write_str("PONG\n"); }
              else if (!cmd.empty())    { id(cmd_uart).write_str(("ERR " + cmd + "\n").c_str()); }
            } else if (id(cmd_buf).size() < 16) {
              id(cmd_buf) += (char) c;
            }
          }

The Pi side — a presser daemon, with a failsafe

One small Python service on the Pi (gg-esp-presser) is the only thing that holds the serial port. Home Assistant publishes arm / disarm / part / panic to an MQTT topic; the daemon writes the command down the wire, waits for the ESP's OK (three tries), and pings every 30 s so a binary_sensor shows the link live. Opening the port resets the ESP, so it's opened once and kept.

And a failsafe on top. A fob press is instant, but the panel only reports the new state to the hub six to nine seconds later — so after every arm or disarm the daemon re-reads the panel and, if it hasn't complied, presses again (up to three times). Disarm re-presses eagerly: a spare disarm is harmless and getting in fast matters most. Arm waits for the panel's report before retrying, so it can't disturb the exit countdown. Only if it still fails does your phone get a plain "use the keypad" alert. A truly instant (~1 s) confirmation is exactly what the tone-ACK mod below is for.

Flashing: because the control path is the serial port, there's no OTA — reflash over USB, and stop the gg-esp-presser service first (it holds the port). D23/D22/D21/D19 are still chosen deliberately: they sit low at boot, so the ESP can never ghost-press a button while it resets.

Think twice about voice disarm. Anyone your speaker can hear — including someone shouting through a letterbox — could say "disarm". Keep Disarm app- and dashboard-only, or require Google's PIN confirmation for it. Arm, Part-arm and Panic carry no such risk.

Sanity checklist before the first live press

  • ✓ Fob left floating — its ground touches nothing on the ESP32/PSU side
  • ✓ Relay board on High-level trigger; every channel clicks on the bench
  • ✓ COM → signal pad, NO → the shared common "G"
  • ✓ DC+ = 12 V, DC− = ESP32 GND; buck 5 V metered before the ESP32
  • ✓ Each trigger is one colour GPIO → INx; each press is COMx → NOx
  • ✓ Alarm in a safe state for the first live press
C · Tone ACK

Hear the panel confirm planned · parts to order

The relay presses the fob in milliseconds — but the alarm's hub only reports the new state six to nine seconds later, so even the failsafe above can't be certain an arm or disarm landed until then. The panel itself says so instantly, out loud: a quick chirp on arm, a longer tone on disarm. This mod gives the bridge an electrical ear on the panel's own sounder, so Home Assistant learns the truth in tens of milliseconds instead of seconds. It's drawn and costed — it just isn't built yet.

Every "arm" today is a small act of faith for a few long seconds. Voice, dashboard or fob fires the press; Home Assistant paints the house armed; and if the radio hop was missed, the fob cell was flat, or the panel was already mid-delay, you don't find out until the hub finally reports — or until you come home to a surprise. The failsafe re-presses to cover it, but it's working blind against a 6–9 second clock.

The panel already broadcasts the one fact we want. Its little internal sounder chirps the instant it accepts a command. If we can hear that, the loop closes: press, hear the beep, done — or hear nothing, and press again within a second. The blind timer becomes a real acknowledgement.

The measured floor

6–9 s
hub report lag
<100 ms
beep → HA · target

A fob press is instant; the hub only sees the event 6–9 seconds later. No software check can beat that — the sounder can.

Why the speaker, and not the fob. A two-way fob would flash a confirm LED we could simply tap — but this fob is one-way: it transmits and never hears back, so its LED only means "sending", not "the panel agreed". The sounder is the only local evidence the panel actually accepted anything, so that's what we listen to. And it stays a second ESP, at the panel — the relay bridge now lives at the Pi on its wire, and the tap has to be where the sounder is. Wi-Fi is fine for this job: a missed beep just falls back to the hub poll, so it can never block an arm or disarm the way weak Wi-Fi did on the control path.

The tones on this panel

Measured at the panel: arm is three quick blips, then the exit-delay countdown settles into a slower, evenly-spaced bleep; disarm is a single longer tone. Everything else — a key press, the long countdown, a full siren — has to be told apart and ignored. We classify the shape of the sound, not its pitch.

The tap — high-Z, then a wall of plastic

Same discipline as the relay bridge: the panel's copper and the ESP's copper never share a ground. Sense the sounder in parallel through a high-impedance envelope detector, then cross an optocoupler — nothing else ever touches the panel.

Never put a pin straight across the sounder. The driver is a few volts of 2–4 kHz square wave on a confirm, and a lot more on a full alarm. Sense it in parallel through the high-impedance envelope, then isolate — the same rule that stopped the fob jamming when we first tried a common-ground opto on its buttons. The panel's sounder ground must never meet the ESP ground.

C · Build

Circuit, parts & firmware

Parts — pennies on top of a spare ESP32

QtyPartWhyUK ballpark
1ESP32 dev boardA second one, living at the panel£4–£7
1PC817 optocouplerThe isolation wall~£0.50
11N5819 Schottky (or 1N4148)Rectify the tonepennies
2100 nF ceramic ("104")C1 couple · C2 envelopepennies
1 ea4.7 k · 100 k · 10 k · 1 k resistorSeries · bleed · pull-up · LED limitpennies
optLM393 + 10 k potCleaner threshold if the confirm tone is shy£1–£2
—Wire, heatshrink, 2-pin plugSo the tap unplugs when the panel comes off the wall~£2

The idea, in firmware

This ESP sits at the panel with Wi-Fi, so — unlike the serial bridge — it can run ordinary ESPHome and publish straight to Home Assistant. It doesn't decide on the first edge: it counts pulse widths, waits for the sound to stop, then classifies. Three tuned numbers, learnt once from the log.

alarm-tone-ack.yamlclassify on the silence
substitutions:
  disarm_min_ms: "600"      # one solid tone this long → DISARM
  disarm_max_ms: "1500"
  pulse_min_ms:  "40"       # a "blip" is between these
  pulse_max_ms:  "300"
  arm_pulses:    "3"        # ≥3 blips in the window → ARM
  window_ms:     "1500"
  ignore_after:  "4500"     # still sounding? countdown / siren → ignore

# GPIO27 = opto collector (inverted, debounced)
#   rising edge  → start / continue a burst
#   falling edge → count a pulse if 40–300 ms wide
#   ~1.5 s after the last edge, decide:
#      one tone 600–1500 ms       → DISARM
#      ≥3 short pulses, then quiet → ARM
#      anything past 4.5 s          → ignore
# then a short cooldown so the alarm strobe can't retrigger it.

# exposes to Home Assistant:
#   binary_sensor.panel_confirmed_armed
#   binary_sensor.panel_confirmed_disarmed
#   text_sensor.last_tone_decision   (ARM / DISARM / ignored)

The beep leads; the hub reconciles. When the tone fires, Home Assistant commits the alarm state and the status lights immediately — and the slow hub poll becomes a back-check only: accept it when it agrees, log it when it doesn't, but never let a late reading overwrite a beep that just said DISARM. Fast and certain, instead of one or the other.

When it's built — the bench order

01

Put the alarm in service mode

You're working on a live panel. Put the wirefree siren into service mode from the menu so a slip doesn't wake the street, and keep the fob bridge powered so Home Assistant can disarm if it trips.

02

Find the sounder pads

Slide the panel off its bracket and locate the small internal piezo — two short wires or pads near the front of the board. Confirm with a meter on AC while you disarm from the fob: a burst of a couple of volts. It's not the 868 MHz antenna, and not the battery pack.

03

Solder a parallel tap

Two flying leads across the sounder pads to a 2-pin plug — sense only, never cut the loop, so the panel can still come off the wall later.

04

Build the envelope off the board

C1 → R1 → D1 → C2/R2 → opto LED, on a scrap of protoboard that lives with the ESP, not inside the panel. If a disarm tone reads under ~1.2 V at the diode, add the LM393.

05

Meter the isolation, then flash

Continuity from the panel's sounder ground to ESP GND must read open before power. Then flash the YAML, open the log, and read span / high / pulses off a real disarm and a real arm.

06

Write the three numbers

Drop your panel's real windows into the substitutions, watch a few arms and disarms confirm in the log, then hand the tone the job of committing HA state — with the hub demoted to reconciliation.

Status: designed, costed, and waiting on a parts order — nothing built yet, so there are no annotated photos here yet. Until it's in, the 6–9 second failsafe on the relay bridge stands. If day-to-day the failsafe proves nuisance-free, this stays a nice-to-have; the day the confirm lag grates, this is the upgrade that erases it.