← Ars Magna Lab

A restored Córdoba foosball table

Smart foosball(with Home Assistant)

Infrared beams in the ball return lanes and an ESP32 running ESPHome that keeps track of the match. The scoreboard is a dashboard on a tablet, with a commentator and music, it's voice controlled with Assist and goals are celebrated with WLED. It runs on batteries, hidden inside the cabinet, and the goals haven't been touched.

ESPHomeWLEDAssistDashboardESP32IR beams
The foosball table mid-game, with the tablet next to it showing 2-1

01

What it does

You play with 11 balls and the first to 6 wins. At 5-5, the last ball is served from the centre: that's “la buena”, the decider. The ESP32 knows these rules and publishes the phase of the match in sensor.futbolin_fase. Everything else (scoreboard, sound and lights) reacts to that phase.

Kick-off

“New game” by voice. Scoreboard back to zero and the commentator introduces the clásico.

normal
Goal

Counted automatically. A wave of light in the team's colour and a line from the commentator.

event.futbolin_gol
Match point

When a team reaches 5, the strips breathe in its colour.

bola_partido_a
The decider

5-5: duelling waves, a golden heartbeat and a banner on the scoreboard.

la_buena
Victory

A light party and the winner's screen. Extra goals no longer count.

fin_a

02

A classic Córdoba

It's an old Córdoba foosball table, black and restored. That set the rules for the build:

  • Same as ever on the outside. No screens stuck on, no new buttons. The only visible thing is a main switch underneath.
  • The goals stay untouched. The ball is solid and flies in: any sensor inside the goal would end up broken or bouncing the ball back onto the pitch.
  • No cables while playing. Everything runs on batteries.
  • No buttons on the table. Everything is controlled from Home Assistant.

The abacus with red and blue beads is still in its place, just in case.

The restored Córdoba foosball table, black, with white and maroon players
The Córdoba, restored. Nothing new shows on the outside.
Brass plate from the Barcelona Industry Office: Justo A. Córdoba brand, parlour football, table type, approved in 1950
The original plate: “fútbol de salón” (parlour football), table type, approved by the Spanish security authorities in 1950.
The same foosball table before restoration, painted red and with worn players
How it looked in 2022, before the restoration.
Close-up of the players before the restoration, with worn paint
The players, with decades of matches behind them.

03

How does it know it's a goal?

The obvious place to look would be the goal, but it's better to look afterwards. When the ball goes in, it drops through a slot into a wooden lane that rolls it down to the ball tray. By then it's no longer flying: it rolls slowly and always along the same path.

Each lane has an infrared beam, with an emitter on one side and a receiver opposite. When the ball passes, it breaks the beam for a few tens of milliseconds, and that's a goal.

As unobtrusive as possible

Zero holes

Two wooden wedges glued inside the lane. Neither the goal nor the lane has been drilled.

Removable

The wedges come unglued and the sensors use connectors. The table goes back to how it was.

Out of the way

Each sensor is sheltered behind its wedge. The ball never touches it and the ramp guides it without slowing it down.

Nothing slips by

The beam crosses the lane wall to wall, so the ball breaks it wherever it rolls.

What the firmware does

Cross-section of the lane with the emitter and receiver on either side of the ball, and the signal on GPIO 32: the beam is broken for about 35 ms by a goal, the 400 ms lockout ignores bounces and very short breaks are discarded as noise
The beam seen from the front and the signal the ESP32 gets from a goal. The diagrams are in Spanish.
The ball return lane with the two wooden wedges glued to the walls and the sensors behind them
The two wedges in the lane, with a sensor behind each one.
The ball passes between the wedges and breaks the beam. Real speed.

04

Architecture

The key decision: the ESP32 runs the match, not Home Assistant. It counts, works out the phase and stores it in flash (restore_value) so a reboot or an OTA update doesn't lose it.

From there it reports two ways. To HA, through the ESPHome native API. To WLED, directly through the JSON API ({"ps": 3}), bypassing HA, so the effect fires instantly.

If the WiFi or Home Assistant goes down, the ESP32 keeps counting, and when it's back the tablet shows the right score. HA holds no match logic at all: it only displays the entities and presses the buttons.

05

What Home Assistant sees

The ESPHome device is called futbolin, so every entity ends up as <domain>.futbolin_*. The entity names are in Spanish. The teams are A and B: at home, A is Real Madrid and B is Barça.

State

EntityWhat it is
sensor.futbolin_goles_a / _bGoals for each team, from 0 to 6
sensor.futbolin_fasereposo (idle), normal, bola_partido_a/_b (match point), la_buena (decider), fin_a/_b (end)
event.futbolin_golgol_a or gol_b, only for goals seen by the beams. It's the natural trigger for automations
sensor.futbolin_bateriaBattery %, computed from the highest voltage in the last 10 min so the strips' current spikes don't make it jump
binary_sensor.futbolin_autotestResult of the beams' self-test

Controls

EntityWhat it does
button.futbolin_nueva_partida0-0, let's play
button.futbolin_gol_a_mas / _menosCorrects the score. The phase is recalculated: a −1 from 6-5 brings match point back
button.futbolin_anular_partidaBack to reposo
switch.futbolin_efectos_luzTurns the strip effects on or off
number.futbolin_luces_bateria_minimaBattery % below which the lights are cut
switch.futbolin_modo_enchufadoTo play from a 12 V power supply instead of batteries: hides the battery and silences its warnings

06

The scoreboard: a dashboard with a commentator

The scoreboard is an HA dashboard built from a single button-card with custom HTML: dot-matrix digits, the column of 11 balls showing the order of the goals, the “GOAL!” animation, the decider banner and the winner's screen. While it's open, HA disables the tablet's screensaver.

The commentator

Every goal is called by a commentator who follows the match: the first goal, the equaliser, the streak, the comeback, match point, the goal that saves the match… There are 348 lines, generated in advance in Spanish with an OpenAI TTS voice. HA works out the moment of the match as a sensor attribute and the dashboard picks a random line, never repeating the previous one.

The tablet showing the scoreboard at 0-0, propped on the foosball table next to the abacus
The scoreboard on the tablet, next to the good old abacus.

The music trick

With music playing on the tablet, the commentator has to duck it, not stop it. Android only ducks the music if the new sound requests “transient, may duck” audio focus, and the WebView only requests it that way for media shorter than 5 s. With a longer line, the music paused and never came back.

The fix: while the commentator speaks, an <audio> element loops 1 s of silence, which requests the short focus. The voice plays separately through Web Audio, which requests no focus at all. When it's done, the silence's src is emptied and load() is called: with pause() alone, the focus is never released and the music stays ducked.

07

Voice control with Assist

The table has no buttons: everything is done with Assist from the tablet. The phrases are conversation triggers in an automation that presses the ESPHome buttons. They're matched locally before reaching the LLM, so the table doesn't need to be exposed to Assist.

triggers:
  - trigger: conversation
    command:
      - "gol del Madrid"
      - "gol de los blancos"
actions:
  - action: button.press
    target:
      entity_id: button.futbolin_gol_a_mas
PhraseAction
«Nueva partida», «echamos una» (new game, let's play one)New game and scoreboard on the tablet
«Gol del Barça» (Barça goal)+1
«Ese no vale el del Madrid» (Madrid's one doesn't count)−1
«¿Cómo vamos?» (what's the score?)Replies with the score and the phase
«Anula la partida» (cancel the game)Back to idle
«Pon la música» (play the music)Music on the tablet

The phrases are in Spanish, the language of our Assist at home.

08

Lights with WLED

There's an LED strip along each side of the pitch (12 V WS2811, 28 pixels each), driven by a Gledopto controller running WLED. Each strip is a segment. On batteries they draw too much to stay on for a whole match, so they only play effects of a few seconds and then switch themselves off again.

Each moment is a preset (or a playlist of presets) that the ESP32 fires through the JSON API. The presets come out of a generator script, so they're versioned with the rest of the code.

The 12 V bus powers the Gledopto running WLED, which has two outputs: strip A on GPIO 16 (pixels 0 to 27) and strip B on GPIO 2 (pixels 28 to 55)
Each strip goes to its own Gledopto output and is a WLED segment.
PresetWhenWhat it does
gol_a / gol_bGoalA wave in the team's colour towards the opposing goal and fireworks, 3 s
bola_partido_*A team reaches 5The strips breathe in its colour for 8 s
la_buena5-5Duelling waves, each from its own goal, and a golden heartbeat
fin_a / fin_bWinning goalA party in the winner's colour, 10 s
avisoJam or failed self-testSolid red until it's resolved

On low battery, the ESP32 cuts the lights by itself: if the % stays below number.futbolin_luces_bateria_minima for 60 s, it switches them off, and doesn't turn them back on until the battery is 10 points above the minimum. That margin stops the cut-off from bouncing, because the effects themselves pull the voltage down.

09

Wiring and batteries

All the electronics sit on a plywood panel next to the batteries, in the fixed part of the cabinet: the ESP32, the buck converter that steps 12 V down to 5 V, a terminal strip acting as the 12 V bus and the interface board for the beams. The strips take 12 V directly.

Each lane gets a 5-wire cable ending in two JST-SM connectors, one for the receiver and one for the emitter. The pull-up resistors are soldered at the ESP32 end of the cable, and a 100k/20k divider brings the 12 V down to GPIO 34 to compute the battery %.

Overall schematic: two batteries with their fuses, the switch, the 12 V bus, the Gledopto with the strips, the 5 V buck converter, the ESP32 and the interface with the cables to both lanes
The overall schematic. In red, the +12 V.

The ESP32 and the beams

Wiring a beam to the ESP32: 3.3 V and GND through screw terminals, the receiver signal to GPIO 32 with a 5.1 kΩ pull-up and 1 kΩ in series, the emitter to GPIO 14 and the battery divider to GPIO 34
How a beam connects to the ESP32, with the battery divider on GPIO 34.

The batteries

Two 12 V 7 Ah lead-acid batteries in parallel, each with its own 5 A fuse, and a main switch under the cabinet. On paper they're good for about 7 hours of play (still to be measured). To play plugged in, you take the batteries out and connect a 12 V power supply in their place.

Battery wiring: each one with its 5 A fuse holder, both positives to the switch and from there to the 12 V terminals, which feed the Gledopto, the buck converter and the interface; the negatives to the GND terminal
Each battery has its fuse next to the terminal and both positives join at the switch.
The foosball table with the playing field tilted up and the electronics inside the cabinet
With the playing field lifted: it all fits inside the cabinet.
The electronics box, with the ESP32 on its terminal board, the buck converter and the connections
The ESP32 on its terminal board, the buck converter and the beam connections.
The two lead-acid batteries and the Gledopto controller inside a blue box
The two batteries, with their fuses, and the Gledopto.
The main switch, a round push button with a red ring screwed under the cabinet
The main switch: everything powers up from under the cabinet.

The code

It's all on GitHub, open source: the ESPHome firmware, the Home Assistant package with the scoreboard dashboard and the Assist phrases, the WLED presets with their generator, the commentary generator and the diagrams.