Smart Home Steuerung am alten Wählscheibentelefon

Die Make (Ausgabe 2/2026) enthielt einen Vorschlag für die Nutzung eines alten Wählscheibentelefons mit einem Raspberry Pi und einem LLM inklusive Sprachausgabe. Insgesamt ein witziges Projekt, aber irgendwie war es mir ein bisschen zu viel „KI“.

Ich hatte jedoch noch so ein altes Schätzchen: Einen Fernsprechtischapparat FeTAp 611 in Grün. Es ist das Telefon meiner Schwiegereltern (und ja, die hatten damals tatsächlich eine dreistellige Telefonnummer!). Ich selbst habe es während meines Studiums in der Studentenbude genutzt. Da ich mich nur schwer von solchen Dingen trennen kann, fristete es seitdem sein Dasein irgendwo im Keller.

Der Make-Artikel inspirierte mich schließlich dazu, dem alten Teil wieder Leben einzuhauchen. Fast alle Geräte in unserem Haus werden mittlerweile über Home Assistant gesteuert – meist vollautomatisch über Sensoren oder Zeitpläne. Gelegentlich nutze ich aber auch das Tablet im Flur, auf dem noch eine alte FTUI von FHEM läuft (weil ich das Dashboard mit HA einfach nicht so schick hinbekomme), oder eben die Home Assistant App auf dem Handy.

Die Umsetzung

Mittels eines ESP32 WROOM wurde die notwendige Technik eingepflanzt. Das Schöne dabei: Das Innenleben blieb weitestgehend original, ich habe nichts ausgebaut.

  • Hörergabel: Den Schalter habe ich direkt auf der Rückseite der Platine angelötet.
  • Wählscheibe: Die Impulse werden einfach am ursprünglichen Stecker abgegriffen (Pin 1 & 2).
  • Audio: Für die Tonausgabe sorgt ein MAX98357. Lediglich für den Anschluss des alten Lautsprechers habe ich zwei Kabel durchgeschnitten und per Lüsterklemme verbunden (ja, es musste am Ende schnell gehen).

Das Telefon kann nun

  1. Sich ins WLAN einbinden
  2. Verbindung zu meinem MQTT-Server aufbauen
  3. WLAN und/oder MQTT-Status per LED anzeigen
  4. Den klassischen 425-Hz-Wählton ausgeben, sobald der Hörer abgehoben wird.
  5. Die gewählte Nummer an den MQTT-Server übermitteln

Anschlussbelegung

BauteilPin
LED26
Wählscheibe14
Hörer27
I2S_BCLK (MAX98357)18
I2S_LRC (MAX98357)19
I2S_DOUT (MAX98357)23

Der Code

Was bisher implementiert ist: Der ESP32 übernimmt das Management der WLAN-Verbindung, die MQTT-Kommunikation und die Dekodierung der Wählscheiben-Impulse. Über den MAX98357 wird der klassische Wählton via I2S direkt im Code generiert und mit einem leichten Rauschen unterlegt, um den alten analogen Charakter des Hörers beizubehalten.

#include <WiFi.h>
#include <PubSubClient.h>
#include <driver/i2s.h>

// ================== WLAN ==================
const char* ssid = "hier_die_ssid";
const char* password = "hier_das_wlan_passwort";

// ================== MQTT ==================
const char* mqtt_server = "192.168.xxx.xxx";
int mqtt_fail_count = 0;
const int mqtt_max_retries = 5;
bool mqtt_blocked = false;

// ================== LED ===================
const int ledPin = 26;

enum LedState {
  LED_WIFI_CONNECTING,
  LED_WIFI_CONNECTED,
  LED_MQTT_CONNECTING,
  LED_MQTT_CONNECTED
};

LedState currentState = LED_WIFI_CONNECTING;

// ================== Wifi ==================
WiFiClient espClient;
PubSubClient client(espClient);

// ================== Fetap ==================
const int dialPin = 14;
const int hookPin = 27;

// ================== I2S AUDIO ==================
#define I2S_BCLK 18
#define I2S_LRC  19
#define I2S_DOUT 23

bool toneActive = false;
uint32_t toneIndex = 0;

// ================== Waehlen ==================
volatile int pulses = 0;
volatile unsigned long lastPulseMicros = 0;
unsigned long lastActivity = 0;

bool lastHookState = false;

// ================== Interrupt ==================
void IRAM_ATTR onPulse() {
  unsigned long now = micros();
  if (now - lastPulseMicros < 80000) return;
  pulses++;
  lastPulseMicros = now;
  lastActivity = millis();
}

// ================== SETUP ==================
void setup() {
  Serial.begin(115200);

  pinMode(dialPin, INPUT_PULLUP);
  attachInterrupt(dialPin, onPulse, FALLING);

  pinMode(hookPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  // ================== I2S INIT ==================
  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = 8000,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags = 0,
    .dma_buf_count = 8,
    .dma_buf_len = 64,
    .use_apll = false
  };

  i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_BCLK,
    .ws_io_num = I2S_LRC,
    .data_out_num = I2S_DOUT,
    .data_in_num = I2S_PIN_NO_CHANGE
  };

  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_0, &pin_config);

  // ================== WLAN ==================
  currentState = LED_WIFI_CONNECTING;
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    updateLED();
    delay(300);
  }

  currentState = LED_WIFI_CONNECTED;
  client.setServer(mqtt_server, 1883);

  Serial.println("Telefon bereit");
}

// ================== LOOP ==================
void loop() {
  if (!client.connected()) reconnect();
  client.loop();

  updateLED();

  bool hookReading = (digitalRead(hookPin) == LOW);

  // ================== HOOK CHANGE ==================
  if (hookReading != lastHookState) {
    delay(40);

    if (hookReading == (digitalRead(hookPin) == LOW)) {
      lastHookState = hookReading;

      if (hookReading) {
        Serial.println("Hörer ABGENOMMEN");
        pulses = 0;
        sendStatus(true);

        toneActive = true;
        toneIndex = 0;
      } else {
        Serial.println("Hörer AUFGELEGT");
        sendStatus(false);

        toneActive = false;
        i2s_zero_dma_buffer(I2S_NUM_0);
        i2s_stop(I2S_NUM_0);
        i2s_start(I2S_NUM_0);
      }
    }
  }

  // ================== Wählscheibe ==================
  unsigned long now = millis();

  if (lastHookState) {
    if (pulses > 0 && (now - lastActivity) > 900) {
      int number = pulses;
      if (number == 10) number = 0;

      Serial.print("Gewählt: ");
      Serial.println(number);

      sendMQTT(number);
      pulses = 0;
    }
  }

  // ================== 425 Hz Ton ==================
  if (toneActive) {
    size_t written;

    float freq = 425.0;
    float t = (float)toneIndex / 8000.0;

    int16_t sample =
      (int16_t)(2000 * sin(2 * PI * freq * t));

    sample += random(-300, 300);

    i2s_write(I2S_NUM_0, &sample, sizeof(sample), &written, portMAX_DELAY);

    toneIndex++;
    if (toneIndex > 8000) toneIndex = 0;
  }
}

// ================== MQTT ==================
void sendMQTT(int number) {
  if (!client.connected()) return;

  client.publish("fetap_611/nummer", String(number).c_str());
}

// ================== STATUS ==================
void sendStatus(bool onHook) {
  if (!client.connected()) return;

  client.publish("fetap_611/hoerer", onHook ? "ON" : "OFF");
  client.publish("fetap_611/nummer", "-1");
}

// ================== MQTT reconnect ==================
void reconnect() {
  if (mqtt_blocked) return;

  while (!client.connected()) {
    currentState = LED_MQTT_CONNECTING;

    if (client.connect("fetap_phone")) {
      mqtt_fail_count = 0;
      currentState = LED_MQTT_CONNECTED;
    } else {
      mqtt_fail_count++;
      if (mqtt_fail_count >= mqtt_max_retries) {
        mqtt_blocked = true;
        return;
      }
      delay(1500);
    }
  }
}

// ================== LED ==================
void updateLED() {
  static unsigned long lastBlink = 0;
  static bool state = false;

  unsigned long now = millis();

  switch (currentState) {

    case LED_WIFI_CONNECTING:
      if (now - lastBlink > 200) {
        state = !state;
        digitalWrite(ledPin, state);
        lastBlink = now;
      }
      break;

    case LED_WIFI_CONNECTED:
      if (now - lastBlink > 800) {
        state = !state;
        digitalWrite(ledPin, state);
        lastBlink = now;
      }
      break;

    case LED_MQTT_CONNECTING:
      if (now - lastBlink > 150) {
        state = !state;
        digitalWrite(ledPin, state);
        lastBlink = now;
      }
      break;

    case LED_MQTT_CONNECTED:
      digitalWrite(ledPin, HIGH);
      break;
  }
}

Die Herausforderung: Timing und Entprellung

Die Abfrage eines so alten mechanischen Geräts bringt ein paar Besonderheiten mit sich, die man im Code berücksichtigen muss. Hier sind zwei Punkte, bei denen man eventuell individuell nachjustieren muss:

  • Entprellen der Wählscheibe: Im Interrupt nutze ich eine Sperrzeit von 80ms. Das ist notwendig, da die mechanischen Kontakte beim Öffnen und Schließen kurz „prellen“. Wenn eure Wählscheibe falsche Ziffern erkennt, ist dieser Wert der erste Hebel, an dem ihr ansetzen solltet.
  • Der Hörerschalter (Gabel): Auch dieser Schalter ist mechanisch und neigt zum Prellen. Ich habe hier ein delay(40) eingebaut, um sicherzugehen, dass der Zustand stabil ist, bevor die MQTT-Meldung „Hörer abgenommen“ rausgeht.

Warum das Ganze?

Im Gegensatz zu moderner Digitaltechnik gibt die Wählscheibe einfach nur mechanische Impulse (Unterbrechungen) aus, etwa 10 Impulse pro Sekunde (10Hz). Eine gewählte „3“ sind drei kurze Unterbrechungen der Leitung. Da der ESP32 im Vergleich zur Wählscheibe sehr schnell ist, würde er ohne diese künstlichen „Gedenksekunden“ (Delays und Sperrzeiten) jedes mechanische Zittern als extra Impuls interpretieren.

Die aktuell im Code gewählten Zeiten und Mechanismen funktionieren bei meinem Telefon sehr zuverlässig.


Automatisierung in HA

Auf die Änderungen des entsprechenden MQTT-Topics reagiere ich mit einer Automatisierung im Home Assistant. Im folgenden Beispiel werden zwei Lampen bei Wahl der 1 oder der 2 getoggelt

alias: FeTap Steuerung
description: Schaltet Aktoren basierend auf Sensorwert 1-9
triggers:
  - entity_id: sensor.fetap_nummer
    trigger: state
conditions:
  - condition: template
    value_template: "{{ trigger.to_state.state in ['1','2','3','4','5','6','7','8','9'] }}"
actions:
  - choose:
      - conditions:
          - condition: state
            entity_id: sensor.fetap_nummer
            state: "1"
        sequence:
          - action: light.toggle
            target:
              entity_id:
                - light.lampe_fensterbank
      - conditions:
          - condition: state
            entity_id: sensor.fetap_nummer
            state: "2"
        sequence:
          - action: light.toggle
            target:
              entity_id:
                - light.lampe_couch
    default: []
mode: restart

Ausblick

Das war ein erster PoC was man mit dem alten Telefon alles machen kann. Der Quellcode ist nicht wirklich aufgeräumt und die WLAN Credentials noch direkt im Code enthalten. Ich glaube die alten Telefone haben auch mit dem 425Hz-Ton aufgehört sobald man gewählt hat.

Ideen für die nächsten Schritte:

  • WLAN-Credentials komfortabel über eine Webseite z.B. im SPIFFS/LittleFS konfigurieren.
  • Wählton deaktivieren, sobald die erste Ziffer gewählt wurde (originalgetreuer).
  • Ein kurzes akustisches Signal ausgeben, wenn das MQTT-Telegramm erfolgreich gesendet wurde.
  • Mittels Step-Up-Wandler die alte mechanische Klingel des Telefons für Benachrichtigungen integrieren.
  • Das Mikrofon in Betrieb nehmen (eventuell durch eine moderne Elektret-Kapsel ersetzen).
  • Ziffernfolgen implementieren (aktuell funktioniert das nur mit einzelnen Ziffern)

Viel Spaß beim Bauen und erweitern!

Gruß
Chris

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert