Skip to content

Commit 6387563

Browse files
fhirschmannclaude
andcommitted
feat: Apple HomeKit support (control + Television) via HomeSpan
Pair the player into the Apple Home app over the existing WiFi (no MFi chip needed). Exposes a "Controls" accessory (play/pause, volume, button-lock, loop, night-mode, battery) and a "Television" accessory (power + a Control-Center remote for transport and volume). State is mirrored back so changes from the buttons or RFID show up in the Home app. The HomeSpan poll task is pinned to core 0 so it never disturbs the audio pipeline on core 1; HomeKit-triggered actions are applied on core 1 to avoid cross-core races. Each device generates its own random pairing code on first boot (stored in NVS) instead of a shared hard-coded one. A HomeKit section in the web settings shows the pairing QR + setup code and lets you regenerate the code, reset the pairing and rename the device/remote. All new UI strings are translated (de/en/fr). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent def2d7e commit 6387563

12 files changed

Lines changed: 860 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
119119
| **Listening statistics**: per-day listening time (today / yesterday / 7 d / 30 d) in a 365-day NVS ring buffer, plus a most-played-cards top list — shown in the info dialog (`GET /info`, `GET /topcards`) | [`af6c8d3`](../../commit/af6c8d3) · [`c2e7d44`](../../commit/c2e7d44) |
120120
| **Full backup**: export/import all settings + RFID assignments + per-path EQ rules + listening stats as one JSON file; passwords are only included when explicitly ticked, so a shared backup doesn't leak credentials | [`4c90ff4`](../../commit/4c90ff4) · [`646ec5c`](../../commit/646ec5c) |
121121
| **Home Assistant MQTT discovery**: auto-registers all entities under one HA device — track/status/firmware/WiFi/battery sensors, volume & LED-brightness numbers, lock & ambient-light switches, equalizer select, transport/update/shutdown buttons | [`db73db1`](../../commit/db73db1) |
122+
| **Apple HomeKit + Siri** (`HOMEKIT_ENABLE`, via [HomeSpan](https://github.com/HomeSpan/HomeSpan)): pair the player straight into the Home app as a bridge with named tiles — **Playback** (play/pause + battery), **Volume** (brightness dimmer) and **Button-lock** — all controllable by Siri and usable in automations. State is mirrored back, so changes from buttons/RFID show up on the iPhone. Each device generates its **own random pairing code** on first boot (persisted in NVS) instead of a shared hard-coded one, so multiple ESPuinos — and everyone running this firmware — stay distinct and private. A dedicated **HomeKit settings section** shows the scannable pairing **QR code** + that setup code and a "reset pairing" button. Pairs over the existing WiFi (no MFi chip needed); the HAP server runs on its own port and the poll task is pinned to core 0 so it never disturbs the audio pipeline on core 1 | _pending_ |
122123
| **Control tab**: Repeat / Sleep-Timer (live countdown) / Night-Mode / button-lock / FTP start-stop and a Bluetooth-mode picker (Normal / Speaker / Headphones) right in the control tab; mobile-optimised (full-width louder/quieter buttons instead of the fiddly slider) | [`88a742c`](../../commit/88a742c) · [`520815a`](../../commit/520815a) · [`d57f24a`](../../commit/d57f24a) · [`aef1765`](../../commit/aef1765) |
123124
| **Battery-backed RTC (DS3231)**: optional real-time clock on the external I²C bus so the time stays correct without WiFi/NTP (seeds the clock at boot, NTP disciplines it); time + die-temperature in the info dialog and via MQTT/HA | [`bf90f66`](../../commit/bf90f66) |
124125
| **State-driven control LEDs**: the optional control LEDs can mirror a runtime state (key-lock / repeat mode / Bluetooth / battery level) instead of a static colour, per-slot configurable, with a master mute (command **121** / MQTT `control_leds`) | [`d12a496`](../../commit/d12a496) |
@@ -157,6 +158,10 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
157158

158159
<div align="center"><img src="docs/img/feat-control-bt.png" width="640" alt="Bluetooth-mode dropdown"></div>
159160

161+
**Apple HomeKit** — pair the player into the Home app straight from the settings: a scannable QR code + the per-device setup code, configurable device/remote names, and regenerate-code / reset-pairing buttons (codes below are placeholders) (_pending_):
162+
163+
<div align="center"><img src="docs/img/feat-homekit.png" width="360" alt="HomeKit settings section with pairing QR, setup code and name fields"></div>
164+
160165
**Settings tab** — WiFi / MQTT / FTP / Bluetooth merged with the general settings into one tab, laid out as a vertical UniFi-style sidebar with flat, consistent sections ([`8b01876`](../../commit/8b01876), [`32aabfd`](../../commit/32aabfd)):
161166

162167
<div align="center"><img src="docs/img/feat-settings.png" width="860" alt="Consolidated Settings tab with sub-tabs"></div>

‎docs/img/feat-homekit.png‎

280 KB
Loading

‎html/locales/de.json‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,5 +803,24 @@
803803
"lblPlay7d": "Letzte 7 Tage",
804804
"lblPlay30d": "Letzte 30 Tage",
805805
"lblTopCard": "Meistgespielt"
806+
},
807+
"homekit": {
808+
"title": "Apple HomeKit",
809+
"intro": "Füge diesen ESPuino zur Apple-Home-App hinzu, um Wiedergabe, Lautstärke und die Tastensperre per Siri und Automationen zu steuern. Koppelt über dein bestehendes WLAN – ohne Zusatzhardware.",
810+
"paired": "Gekoppelt",
811+
"notpaired": "Noch nicht gekoppelt",
812+
"setupcode": "Kopplungscode",
813+
"howto": "In der Home-App auf + → Gerät hinzufügen → Weitere Optionen… → ESPuino tippen, dann den QR-Code scannen (oder den Code manuell eingeben).",
814+
"devicename": "Gerätename",
815+
"tvname": "TV-/Fernbedienungs-Name",
816+
"tvnamehint": "(erscheint auf der Fernbedienung im Kontrollzentrum)",
817+
"savenames": "Namen speichern",
818+
"restartnote": "Wird nach einem Neustart des ESPuino übernommen.",
819+
"regen": "Code neu generieren",
820+
"reset": "Kopplung zurücksetzen",
821+
"actionsnote": "„Code neu generieren“ erzeugt einen neuen Kopplungscode + QR (der alte funktioniert dann nicht mehr; bereits gekoppelte Zuhause bleiben verbunden). „Kopplung zurücksetzen“ entfernt alle gekoppelten Controller, damit der Player in einem anderen Zuhause hinzugefügt werden kann.",
822+
"confirmreset": "Alle HomeKit-Kopplungen von diesem ESPuino entfernen?",
823+
"confirmregen": "Neuen Kopplungscode generieren? Der aktuelle Code und QR funktionieren dann für neue Kopplungen nicht mehr.",
824+
"namessaved": "Namen gespeichert. ESPuino jetzt neu starten, um sie zu übernehmen?"
806825
}
807826
}

‎html/locales/en.json‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,5 +803,24 @@
803803
"lblPlay7d": "Last 7 days",
804804
"lblPlay30d": "Last 30 days",
805805
"lblTopCard": "Most played"
806+
},
807+
"homekit": {
808+
"title": "Apple HomeKit",
809+
"intro": "Add this ESPuino to the Apple Home app to control playback, volume and the button-lock with Siri and automations. Pairs over your existing WiFi — no extra hardware needed.",
810+
"paired": "Paired",
811+
"notpaired": "Not paired yet",
812+
"setupcode": "Setup code",
813+
"howto": "In the Home app tap + → Add Accessory → More options… → ESPuino, then scan the QR code (or enter the code manually).",
814+
"devicename": "Device name",
815+
"tvname": "TV / remote name",
816+
"tvnamehint": "(shown on the Control-Center remote)",
817+
"savenames": "Save names",
818+
"restartnote": "Takes effect after a restart of the ESPuino.",
819+
"regen": "Regenerate code",
820+
"reset": "Reset pairing",
821+
"actionsnote": "“Regenerate code” creates a new pairing code + QR (the old one stops working; already-paired homes keep working). “Reset pairing” removes all paired controllers so the player can be added to a different Home.",
822+
"confirmreset": "Remove all HomeKit pairings from this ESPuino?",
823+
"confirmregen": "Generate a new pairing code? The current code and QR will stop working for new pairings.",
824+
"namessaved": "Names saved. Restart the ESPuino now to apply them?"
806825
}
807826
}

‎html/locales/fr.json‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,5 +792,24 @@
792792
"lblPlay7d": "7 derniers jours",
793793
"lblPlay30d": "30 derniers jours",
794794
"lblTopCard": "Plus joué"
795+
},
796+
"homekit": {
797+
"title": "Apple HomeKit",
798+
"intro": "Ajoutez cet ESPuino à l’app Maison d’Apple pour contrôler la lecture, le volume et le verrouillage des touches avec Siri et des automatisations. Se jumelle via votre WiFi existant – sans matériel supplémentaire.",
799+
"paired": "Jumelé",
800+
"notpaired": "Pas encore jumelé",
801+
"setupcode": "Code de configuration",
802+
"howto": "Dans l’app Maison, touchez + → Ajouter un accessoire → Plus d’options… → ESPuino, puis scannez le QR code (ou saisissez le code manuellement).",
803+
"devicename": "Nom de l’appareil",
804+
"tvname": "Nom TV / télécommande",
805+
"tvnamehint": "(affiché sur la télécommande du centre de contrôle)",
806+
"savenames": "Enregistrer les noms",
807+
"restartnote": "Pris en compte après un redémarrage de l’ESPuino.",
808+
"regen": "Régénérer le code",
809+
"reset": "Réinitialiser le jumelage",
810+
"actionsnote": "« Régénérer le code » crée un nouveau code de jumelage + QR (l’ancien cesse de fonctionner ; les maisons déjà jumelées restent connectées). « Réinitialiser le jumelage » supprime tous les contrôleurs jumelés afin d’ajouter le lecteur à une autre Maison.",
811+
"confirmreset": "Supprimer tous les jumelages HomeKit de cet ESPuino ?",
812+
"confirmregen": "Générer un nouveau code de jumelage ? Le code et le QR actuels cesseront de fonctionner pour les nouveaux jumelages.",
813+
"namessaved": "Noms enregistrés. Redémarrer l’ESPuino maintenant pour les appliquer ?"
795814
}
796815
}

‎html/management.html‎

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2446,6 +2446,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
24462446
<a class="nav-item nav-link" id="nav-ftp-tab" data-visible="false" data-bs-toggle="tab" data-bs-target="#nav-ftp" href="#nav-ftp" role="tab" aria-controls="nav-ftp"><i class="fas fa-folder"></i> <span class="d-none d-md-inline" data-i18n="nav.ftp"></span></a>
24472447
<a class="nav-item nav-link" id="nav-sync-tab" data-bs-toggle="tab" data-bs-target="#nav-sync" href="#nav-sync" role="tab" aria-controls="nav-sync"><i class="fas fa-sync"></i> <span class="d-none d-md-inline" data-i18n="nav.sync"></span></a>
24482448
<a class="nav-item nav-link" id="nav-bt-tab" data-visible="false" data-bs-toggle="tab" data-bs-target="#nav-bt" href="#nav-bt" role="tab" aria-controls="nav-bt"><i class="fab fa-bluetooth"></i> <span class="d-none d-md-inline" data-i18n="nav.bluetooth"></span></a>
2449+
<a class="nav-item nav-link" id="nav-homekit-tab" data-visible="false" data-bs-toggle="tab" data-bs-target="#nav-homekit" href="#nav-homekit" role="tab" aria-controls="nav-homekit"><i class="fab fa-apple"></i> <span class="d-none d-md-inline">HomeKit</span></a>
24492450
</div>
24502451
</nav>
24512452
<div class="tab-content" id="settings-subcontent">
@@ -3339,7 +3340,41 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
33393340
</fieldset>
33403341
</div>
33413342
</div>
3342-
<div class="tab-pane fade" id="nav-bt" role="tabpanel" aria-labelledby="nav-bt-tab">
3343+
<div class="tab-pane fade" id="nav-homekit" role="tabpanel" aria-labelledby="nav-homekit-tab">
3344+
<div class="container" id="homekitConfig">
3345+
<div class="mb-3 col-md-12">
3346+
<legend><i class="fab fa-apple"></i> <span data-i18n="homekit.title"></span></legend>
3347+
<p class="text-secondary" data-i18n="homekit.intro"></p>
3348+
<div id="homekitStatus" class="mb-3"></div>
3349+
<div class="row align-items-center g-3">
3350+
<div class="col-auto text-center">
3351+
<img id="homekitQr" alt="HomeKit pairing QR code" width="220" height="220" style="background:#fff;border-radius:10px;padding:10px;">
3352+
</div>
3353+
<div class="col">
3354+
<label class="form-label mb-1" data-i18n="homekit.setupcode"></label>
3355+
<div id="homekitCode" class="h4 font-monospace mb-2">–</div>
3356+
<p class="small text-secondary mb-0" data-i18n="homekit.howto"></p>
3357+
</div>
3358+
</div>
3359+
<hr>
3360+
<div class="mb-3">
3361+
<label class="form-label mb-1" for="homekitDeviceName" data-i18n="homekit.devicename"></label>
3362+
<input type="text" class="form-control" id="homekitDeviceName" maxlength="32" placeholder="ESPuino">
3363+
<label class="form-label mb-1 mt-2" for="homekitTvName"><span data-i18n="homekit.tvname"></span> <span class="text-secondary small" data-i18n="homekit.tvnamehint"></span></label>
3364+
<input type="text" class="form-control" id="homekitTvName" maxlength="32" placeholder="ESPuino">
3365+
<button type="button" class="btn btn-primary mt-2" id="homekitNamesBtn" onclick="saveHomeKitNames()"><i class="fas fa-save"></i> <span data-i18n="homekit.savenames"></span></button>
3366+
<p class="small text-secondary mt-1 mb-0" data-i18n="homekit.restartnote"></p>
3367+
</div>
3368+
<hr>
3369+
<div class="d-grid gap-2 d-md-block">
3370+
<button type="button" class="btn btn-outline-secondary" id="homekitRegenBtn" onclick="regenHomeKit()"><i class="fas fa-sync"></i> <span data-i18n="homekit.regen"></span></button>
3371+
<button type="button" class="btn btn-outline-danger" id="homekitResetBtn" onclick="resetHomeKit()"><i class="fas fa-unlink"></i> <span data-i18n="homekit.reset"></span></button>
3372+
</div>
3373+
<p class="small text-secondary mt-2 mb-0" data-i18n="homekit.actionsnote"></p>
3374+
</div>
3375+
</div>
3376+
</div>
3377+
<div class="tab-pane fade" id="nav-bt" role="tabpanel" aria-labelledby="nav-bt-tab">
33433378
<div class="container" id="btConfig">
33443379
<form action="#btConfig" method="POST" onsubmit="btSettings('btConfig'); return false">
33453380
<div class="mb-3 col-md-12">
@@ -5403,7 +5438,78 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
54035438
// Auto-save the General + Buttons tabs on any change (delegated, so the dynamically
54045439
// populated button dropdowns are covered too). No submit button on these tabs.
54055440
$('#settings-general, #settings-buttons, #settings-rfid, #settings-leds, #settings-power').on('change slideStop', 'input:not([data-no-autosave]), select:not([data-no-autosave]), textarea', autoSaveGeneralSettings);
5441+
initHomeKit();
54065442
});
5443+
5444+
// ---- Apple HomeKit settings section -------------------------------------
5445+
function renderHomeKitStatus(hk) {
5446+
const el = document.getElementById('homekitStatus');
5447+
if (!el) return;
5448+
el.innerHTML = hk && hk.paired
5449+
? '<span class="badge bg-success"><i class="fas fa-check"></i> ' + i18next.t('homekit.paired') + '</span>'
5450+
: '<span class="badge bg-secondary"><i class="fas fa-circle-notch"></i> ' + i18next.t('homekit.notpaired') + '</span>';
5451+
const qr = document.getElementById('homekitQr');
5452+
if (qr) qr.src = "http://" + host + "/homekit/qr.svg?ts=" + Date.now();
5453+
}
5454+
async function fetchHomeKit() {
5455+
return await (await fetch("http://" + host + "/homekit", { cache: "no-store" })).json();
5456+
}
5457+
async function initHomeKit() {
5458+
let hk;
5459+
try {
5460+
hk = await fetchHomeKit();
5461+
} catch (e) {
5462+
return; // feature compiled out -> leave the tab hidden
5463+
}
5464+
if (!hk || hk.enabled !== true) return;
5465+
document.getElementById('nav-homekit-tab').setAttribute('data-visible', true);
5466+
document.getElementById('homekitCode').textContent = hk.code || "–";
5467+
document.getElementById('homekitDeviceName').value = hk.deviceName || "";
5468+
document.getElementById('homekitTvName').value = hk.tvName || "";
5469+
renderHomeKitStatus(hk);
5470+
// Refresh pairing status whenever the user opens the tab.
5471+
document.getElementById('nav-homekit-tab').addEventListener('shown.bs.tab', async () => {
5472+
try { renderHomeKitStatus(await fetchHomeKit()); } catch (e) { /* ignore */ }
5473+
});
5474+
}
5475+
async function resetHomeKit() {
5476+
if (!confirm(i18next.t('homekit.confirmreset'))) return;
5477+
const btn = document.getElementById('homekitResetBtn');
5478+
if (btn) btn.disabled = true;
5479+
try {
5480+
await fetch("http://" + host + "/homekit/reset", { method: "POST" });
5481+
renderHomeKitStatus(await fetchHomeKit());
5482+
} catch (e) { /* ignore */ }
5483+
if (btn) btn.disabled = false;
5484+
}
5485+
async function saveHomeKitNames() {
5486+
const device = document.getElementById('homekitDeviceName').value.trim();
5487+
const tv = document.getElementById('homekitTvName').value.trim();
5488+
const btn = document.getElementById('homekitNamesBtn');
5489+
if (btn) btn.disabled = true;
5490+
try {
5491+
const body = new URLSearchParams({ device: device, tv: tv });
5492+
await fetch("http://" + host + "/homekit/names", { method: "POST", body: body });
5493+
if (confirm(i18next.t('homekit.namessaved'))) {
5494+
await fetch("http://" + host + "/restart", { method: "POST" });
5495+
}
5496+
} catch (e) { /* ignore */ }
5497+
if (btn) btn.disabled = false;
5498+
}
5499+
async function regenHomeKit() {
5500+
if (!confirm(i18next.t('homekit.confirmregen'))) return;
5501+
const btn = document.getElementById('homekitRegenBtn');
5502+
if (btn) btn.disabled = true;
5503+
try {
5504+
await fetch("http://" + host + "/homekit/regencode", { method: "POST" });
5505+
// the new code is computed on the device (SRP) -> give it a moment
5506+
await new Promise(r => setTimeout(r, 2500));
5507+
const hk = await fetchHomeKit();
5508+
document.getElementById('homekitCode').textContent = hk.code || "–";
5509+
renderHomeKitStatus(hk);
5510+
} catch (e) { /* ignore */ }
5511+
if (btn) btn.disabled = false;
5512+
}
54075513
const rfidListSavedAssignments = document.getElementById("rfidListSavedAssignments");
54085514
async function rebuildRFIDList() {
54095515
var rfidList = await (await fetch("http://" + host + "/rfid")).json();

‎platformio.ini‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ lib_deps =
4848
https://github.com/tueddy/natsort.git#ebbf6604c573c5315daa8fa77da8f047f202bd63 ; avoid warnings, fork from https://github.com/sourcefrog/natsort.git#cdd8df9
4949
olikraus/U8g2@^2.35.9 ; OLED display (SH1106/SSD1306), only compiled when OLED_ENABLE is set
5050
adafruit/RTClib@^2.1.4 ; battery-backed RTC (DS3231), only compiled when RTC_ENABLE is set
51+
https://github.com/HomeSpan/HomeSpan.git ; Apple HomeKit accessory (control + Siri), only compiled when HOMEKIT_ENABLE is set
5152

5253
board_build.embed_txtfiles =
5354
managed_components/espressif__esp_insights/server_certs/https_server.crt

0 commit comments

Comments
 (0)