From 5ab63a0880d7192578ac0092056b1377627a2158 Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Sun, 15 Feb 2026 15:33:02 +0100 Subject: [PATCH] manatee: Add bluetooth reset handling --- .../modules/home-assistant/default.nix | 204 +++++++++++++----- 1 file changed, 149 insertions(+), 55 deletions(-) diff --git a/hosts/manatee/modules/home-assistant/default.nix b/hosts/manatee/modules/home-assistant/default.nix index e59f395..0d0845f 100644 --- a/hosts/manatee/modules/home-assistant/default.nix +++ b/hosts/manatee/modules/home-assistant/default.nix @@ -6,6 +6,60 @@ }: let nginxEnabled = config.mod.nginx.enable; + + script = pkgs.writeShellScript "bt-reset" '' + set -euo pipefail + export PATH="${ + lib.makeBinPath [ + pkgs.bluez + pkgs.util-linux + pkgs.kmod + pkgs.gnugrep + pkgs.coreutils + ] + }" + + logger -t bt-reset "Starting Bluetooth adapter reset..." + + # Exit early if the adapter is already present and running + if hciconfig hci0 2>/dev/null | grep -q "UP RUNNING"; then + logger -t bt-reset "hci0 is already UP RUNNING — nothing to do" + exit 0 + fi + + # If hci0 exists but isn't UP, try bringing it up + if hciconfig hci0 2>/dev/null; then + logger -t bt-reset "hci0 exists but not running — bringing it up" + hciconfig hci0 up || true + sleep 2 + + if hciconfig hci0 2>/dev/null | grep -q "UP RUNNING"; then + logger -t bt-reset "hci0 is UP now" + systemctl restart bluetooth.service + logger -t bt-reset "bluetooth.service restarted — done" + exit 0 + fi + fi + + # Hard reset: reload the btusb kernel module (works for USB adapters) + logger -t bt-reset "hci0 missing — reloading btusb module..." + modprobe -r btusb 2>/dev/null || true + sleep 3 + modprobe btusb + sleep 3 + + if hciconfig hci0 2>/dev/null; then + hciconfig hci0 up + logger -t bt-reset "hci0 restored after module reload" + else + logger -t bt-reset "ERROR: hci0 not found after module reload" + exit 1 + fi + + # Restart the bluetooth systemd service so bluetoothd picks up the adapter + systemctl restart bluetooth.service + logger -t bt-reset "bluetooth.service restarted — done" + ''; in { hardware.bluetooth.enable = true; @@ -57,71 +111,111 @@ in }; }; }; + + # Trigger reset via udev when hci0 disappears + udev.extraRules = '' + ACTION=="remove", SUBSYSTEM=="bluetooth", KERNEL=="hci0", \ + TAG+="systemd", ENV{SYSTEMD_WANTS}+="bt-reset.service" + ''; }; - systemd.user = { - timers = { - "update-hetzner-ha-dns" = { - unitConfig = { - Description = "updates Hetzner DNS for home-assistant"; - }; + systemd = { + services = { + # Trigger reset on bluetoothd failure + bluetooth = { + unitConfig.OnFailure = [ "bt-reset.service" ]; + }; - timerConfig = { - Unit = "update-hetzner-ha-dns.service"; - OnCalendar = "*-*-* *:00/30:00"; - Persistent = true; - }; + bt-reset = { + description = "Reset Bluetooth adapter"; + after = [ "bluetooth.service" ]; - wantedBy = [ "timers.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = script; + + Restart = "on-failure"; + RestartSec = "10s"; + StartLimitIntervalSec = "120"; + StartLimitBurst = 3; + }; }; }; - services = { - "update-hetzner-ha-dns" = { - unitConfig = { - Description = "updates Hetzner DNS for home-assistant"; + timers.bt-reset = { + description = "Periodically reset Bluetooth adapter"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "5min"; # first run 5 min after boot + OnUnitActiveSec = "4h"; # then every 4 hours + RandomizedDelaySec = "5min"; + }; + }; + + user = { + timers = { + "update-hetzner-ha-dns" = { + unitConfig = { + Description = "updates Hetzner DNS for home-assistant"; + }; + + timerConfig = { + Unit = "update-hetzner-ha-dns.service"; + OnCalendar = "*-*-* *:00/30:00"; + Persistent = true; + }; + + wantedBy = [ "timers.target" ]; }; + }; - serviceConfig = { - Type = "exec"; - EnvironmentFile = config.age.secrets.hetzner-dns.path; + services = { + "update-hetzner-ha-dns" = { + unitConfig = { + Description = "updates Hetzner DNS for home-assistant"; + }; + + serviceConfig = { + Type = "exec"; + EnvironmentFile = config.age.secrets.hetzner-dns.path; + }; + + path = [ + pkgs.curl + pkgs.coreutils # For `cat` + pkgs.jq + ]; + + script = '' + LAST_IP_FILE="/tmp/hetzner-dns-ha-ip" + INTERFACE="enp3s0" + + CURRENT_IP=$(curl -s --fail --interface "$INTERFACE" ifconfig.me) + + LAST_IP="" + if [[ -f "$LAST_IP_FILE" ]]; then + LAST_IP=$(cat "$LAST_IP_FILE") + fi + + if [[ "$CURRENT_IP" == "$LAST_IP" ]]; then + echo "IP unchanged, NOOP update." + exit 0 + else + echo "Updating IP" + + JSON_BODY=$(jq -n --arg ip "$CURRENT_IP" '{records: [{value: $ip}]}') + + curl \ + --fail \ + -X POST \ + -H "Authorization: Bearer $HETZNER_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$JSON_BODY" \ + "https://api.hetzner.cloud/v1/zones/ppp.pm/rrsets/ha/A/actions/set_records" \ + && echo $CURRENT_IP > $LAST_IP_FILE + fi + ''; }; - - path = [ - pkgs.curl - pkgs.coreutils # For `cat` - pkgs.jq - ]; - - script = '' - LAST_IP_FILE="/tmp/hetzner-dns-ha-ip" - INTERFACE="enp3s0" - - CURRENT_IP=$(curl -s --fail --interface "$INTERFACE" ifconfig.me) - - LAST_IP="" - if [[ -f "$LAST_IP_FILE" ]]; then - LAST_IP=$(cat "$LAST_IP_FILE") - fi - - if [[ "$CURRENT_IP" == "$LAST_IP" ]]; then - echo "IP unchanged, NOOP update." - exit 0 - else - echo "Updating IP" - - JSON_BODY=$(jq -n --arg ip "$CURRENT_IP" '{records: [{value: $ip}]}') - - curl \ - --fail \ - -X POST \ - -H "Authorization: Bearer $HETZNER_API_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$JSON_BODY" \ - "https://api.hetzner.cloud/v1/zones/ppp.pm/rrsets/ha/A/actions/set_records" \ - && echo $CURRENT_IP > $LAST_IP_FILE - fi - ''; }; }; };