diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp
index 5dff6765fb7c4a6b19d9dc631af15c3474d9840b..b573c4b49d2bc03d08cfafad6d78ef6277ae439a 100644
--- a/ampel-firmware/co2_sensor.cpp
+++ b/ampel-firmware/co2_sensor.cpp
@@ -15,6 +15,7 @@ namespace config {
   const float temperature_offset = -3.0;  // [K] Temperature measured by sensor is usually at least 3K too high.
 #endif
   bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
+  const bool debug_sensor_states = false; // If true, log state transitions over serial console
 }
 
 namespace sensor {
@@ -24,12 +25,39 @@ namespace sensor {
   float humidity = 0;
   char timestamp[23];
   int16_t stable_measurements = 0;
-  uint32_t waiting_color = color::blue;
-  bool should_calibrate = false;
+
+  /**
+   * Define sensor states
+   * INITIAL -> initial state
+   * BOOTUP -> state after initializing the sensor, i.e. after scd.begin()
+   * READY -> sensor does output valid information (> 0 ppm) and no other condition takes place
+   * NEEDS_CALIBRATION -> sensor measurements are too low (< 250 ppm)
+   * PREPARE_CALIBRATION -> forced calibration was initiated, waiting for stable measurements
+   * CALIBRATION -> the sensor does calibrate itself
+   */
+  enum state {
+    INITIAL,
+    BOOTUP,
+    READY,
+    NEEDS_CALIBRATION,
+    PREPARE_CALIBRATION_UNSTABLE,
+    PREPARE_CALIBRATION_STABLE,
+    CALIBRATION
+  };
+  const char *state_names[] = {
+      "INITIAL",
+      "BOOTUP",
+      "READY",
+      "NEEDS_CALIBRATION",
+      "PREPARE_CALIBRATION_UNSTABLE",
+      "PREPARE_CALIBRATION_STABLE",
+      "CALIBRATION" };
+  state current_state = INITIAL;
+  void switchState(state);
 
   void initialize() {
 #if defined(ESP8266)
-    Wire.begin(12, 14);  // ESP8266 - D6, D5;
+    Wire.begin(12, 14); // ESP8266 - D6, D5;
 #endif
 #if defined(ESP32)
     Wire.begin(21, 22); // ESP32
@@ -49,6 +77,8 @@ namespace sensor {
       ESP.restart();
     }
 
+    switchState(BOOTUP);
+
     // SCD30 has its own timer.
     //NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
     Serial.println();
@@ -91,19 +121,21 @@ namespace sensor {
     previous_measurement_at = now;
   }
 
-  void countStableMeasurements() {
+  bool countStableMeasurements() {
+    // Returns true, if a sufficient number of stable measurements has been observed.
     static int16_t previous_co2 = 0;
     if (co2 > (previous_co2 - config::max_deviation_during_calibration)
         && co2 < (previous_co2 + config::max_deviation_during_calibration)) {
       stable_measurements++;
       Serial.print(F("Number of stable measurements : "));
       Serial.println(stable_measurements);
-      waiting_color = color::green;
+      switchState(PREPARE_CALIBRATION_STABLE);
     } else {
       stable_measurements = 0;
-      waiting_color = color::red;
+      switchState(PREPARE_CALIBRATION_UNSTABLE);
     }
     previous_co2 = co2;
+    return (stable_measurements == config::enough_stable_measurements);
   }
 
   void startCalibrationProcess() {
@@ -115,10 +147,11 @@ namespace sensor {
     scd30.setMeasurementInterval(2); // [s] The change will only take effect after next measurement.
     Serial.println(F("Waiting until the measurements are stable for at least 2 minutes."));
     Serial.println(F("It could take a very long time."));
-    should_calibrate = true;
+    switchState(PREPARE_CALIBRATION_UNSTABLE);
   }
 
   void calibrateAndRestart() {
+    switchState(CALIBRATION);
     Serial.print(F("Calibrating SCD30 now..."));
     scd30.setAltitudeCompensation(config::altitude_above_sea_level);
     scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
@@ -137,12 +170,41 @@ namespace sensor {
     Serial.println(humidity, 1);
   }
 
-  void displayCO2OnLedRing() {
-    if (co2 < 250) {
-      // Sensor should be calibrated.
-      led_effects::showWaitingLED(color::magenta);
+  void switchState(state new_state) {
+    if (new_state == current_state) {
       return;
     }
+    if (config::debug_sensor_states) {
+      Serial.print(F("Changing sensor state: "));
+      Serial.print(state_names[current_state]);
+      Serial.print(" -> ");
+      Serial.println(state_names[new_state]);
+    }
+    current_state = new_state;
+  }
+
+  void switchStateForCurrentPPM() {
+    if (co2 == 0) {
+      // NOTE: Data is available, but it's sometimes erroneous: the sensor outputs
+      // zero ppm but non-zero temperature and non-zero humidity.
+      Serial.println(F("Invalid sensor data - CO2 concentration supposedly 0 ppm"));
+      switchState(BOOTUP);
+    } else if ((current_state == PREPARE_CALIBRATION_UNSTABLE) || (current_state == PREPARE_CALIBRATION_STABLE)) {
+      // Check for pre-calibration states first, because we do not want to
+      // leave them before calibration is done.
+      bool ready_for_calibration = countStableMeasurements();
+      if (ready_for_calibration) {
+        calibrateAndRestart();
+      }
+    } else if (co2 < 250) {
+      // Sensor should be calibrated.
+      switchState(NEEDS_CALIBRATION);
+    } else {
+      switchState(READY);
+    }
+  }
+
+  void displayCO2OnLedRing() {
     /**
      * Display data, even if it's "old" (with breathing).
      * A short delay is required in order to let background tasks run on the ESP8266.
@@ -157,6 +219,30 @@ namespace sensor {
     }
   }
 
+  void showState() {
+    switch (current_state) {
+    case BOOTUP:
+      led_effects::showWaitingLED(color::blue);
+      break;
+    case READY:
+      displayCO2OnLedRing();
+      break;
+    case NEEDS_CALIBRATION:
+      led_effects::showWaitingLED(color::magenta);
+      break;
+    case PREPARE_CALIBRATION_UNSTABLE:
+      led_effects::showWaitingLED(color::red);
+      break;
+    case PREPARE_CALIBRATION_STABLE:
+      led_effects::showWaitingLED(color::green);
+      break;
+    case CALIBRATION: // Nothing to do, will restart soon.
+      break;
+    default:
+      Serial.println(F("Encountered unknown sensor state")); // This should not happen.
+    }
+  }
+
   /** Gets fresh data if available, checks calibration status, displays CO2 levels.
    * Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa)
    */
@@ -169,34 +255,15 @@ namespace sensor {
       co2 = scd30.getCO2();
       temperature = scd30.getTemperature();
       humidity = scd30.getHumidity();
-    }
 
-    //NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
-    if (co2 <= 0) {
-      // No measurement yet. Waiting.
-      led_effects::showWaitingLED(color::blue);
-      return false;
-    }
+      switchStateForCurrentPPM();
 
-    /**
-     * Fresh data. Log it and send it if needed.
-     */
-    if (freshData) {
-      if (should_calibrate) {
-        countStableMeasurements();
-      }
+      // Log every time fresh data is available.
       logToSerial();
     }
 
-    if (should_calibrate) {
-      if (stable_measurements == config::enough_stable_measurements) {
-        calibrateAndRestart();
-      }
-      led_effects::showWaitingLED(waiting_color);
-      return false;
-    }
+    showState();
 
-    displayCO2OnLedRing();
     return freshData;
   }
 
@@ -207,6 +274,7 @@ namespace sensor {
     Serial.print(F("DEBUG. Setting CO2 to "));
     co2 = fakeCo2;
     Serial.println(co2);
+    switchStateForCurrentPPM();
   }
 
   void setAutoCalibration(int32_t autoCalibration) {