Skip to content
Published on

Controlling Govee Smart Lights with ESP32 and Motion Sensor

Automation
Authors

In this project, we'll explore how to control Govee smart lights using an ESP32 development board and a motion sensor. By integrating with the Govee API, we can programmatically turn lights on and off based on motion detection, enabling seamless home automation.


Govee smart lights offer a convenient way to add ambiance and automation to your home lighting. With their ability to change colors, brightness, and even sync with music or entertainment systems, these smart lights provide endless possibilities for customization. However, sometimes you may want to automate your lights based on motion detection, turning them on when someone enters a room and off when it's vacant, saving energy and adding an extra layer of convenience.

Hardware Requirements

To get started, you'll need the following hardware components:

Software Requirements

In addition to the hardware, you'll need the following software components:

  • Arduino IDE
  • ArduinoJson library
  • WiFi library

Setting up the Hardware

Connect the motion sensor, LED, and ESP32 board as follows:

  1. Connect the OUT pin of the AM312 motion sensor to GPIO pin 2 of the ESP32 board.
  2. Connect the positive leg of the LED to GPIO pin 13 of the ESP32 board, and the negative leg to a ground pin through a 330Ω resistor.
  3. Connect the ESP32 board to your computer via a USB cable.

With the hardware set up, you're ready to dive into the code and integrate with the Govee API.

Govee API Integration

The Govee API allows developers to interact with Govee smart devices programmatically. To use the API, you'll need to obtain an API key from the Govee developer portal.

In the code, we'll be using the following API endpoints:

  • /devices: Retrieve information about Govee devices associated with your account.
  • /control: Send commands to control Govee devices (turn on/off, change color, etc.).
  • /state: Fetch the current state (power, online status) of a Govee device.
How to get Govee API credentials
  1. Download the Govee Home App

  2. Navigate to the My Profile page

    • Open the Govee Home App and click on the 👤 icon to go to the My Profile page.
  3. Access Settings

    • On the My Profile page, click on the ⚙️ icon in the top-right corner to access the Settings.
  4. Apply for API Key

    • In the Settings, you should see an option to "Apply for API Key". Click on it.
  5. Fill in the Application Form

    • Enter your "Name" and "Reason for application" in the respective fields.
    • Possible reasons can include home automation, third-party integration, API Days Tutorial (education & research), or any other relevant use case.
  6. Accept the Govee Developer API Terms of Service

    • Read the Govee Developer API Terms of Service carefully, then click the checkbox to accept the terms.
  7. Submit the Application

    • After filling in all the required information and accepting the terms, click the "Submit" button to submit your API key application. You should receive an email with your credentials instantly.
  8. Copy the API Key

    • Once you receive your API key, copy it and store it securely. You'll need to use this key in your code to authenticate requests to the Govee API.

You can also refer to the official guide provided by Govee: https://developer.govee.com/reference/apply-you-govee-api-key

Code Walkthrough

Setup

The setup() function connects the ESP32 board to your WiFi network, initializes the pins for the motion sensor and LED, and fetches information about your Govee devices using the API.

cpp
void setup() {
  Serial.begin(115200);
  pinMode(motionPin, INPUT);
  pinMode(led1Pin, OUTPUT);
  pinMode(led2Pin, OUTPUT);

  // Connect to WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }

  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  delay(1000);
  getDevices(); // Fetch Govee device information
  delay(5000);
}

Loop

The loop() function continuously checks the state of the motion sensor. If motion is detected, it turns on an LED and starts a timer to keep track of the motion duration. After a certain amount of time (e.g., 4 seconds), it calls the printDevices() function to turn on the associated Govee devices. If no motion is detected, it turns off the LED and calls printDevices() to turn off the Govee devices after a certain idle duration.

cpp
void loop() {
  int motionState = digitalRead(motionPin);

  if (motionState == HIGH) {
    Serial.println("Motion detected!");
    time_in_motion++;
    time_idle = 0;
    if (time_in_motion == 4) {
      printDevices("on"); // Turn on Govee devices
    }
    turn1on2off(); // Visual feedback with LED

  } else {
    Serial.println("No motion!");
    time_in_motion = 0;
    time_idle++;
    if (time_idle == 4) {
      printDevices("off"); // Turn off Govee devices
    }
    turn2on1off(); // Visual feedback with LED
  }

  delay(1000); // 1s delay
}

Functions

The code includes several functions to interact with the Govee API and control devices.

getDevices()

This function sends a GET request to the /devices endpoint of the Govee API to fetch information about your Govee devices.

cpp
void getDevices() {
  HTTPClient http;
  http.begin(url);
  http.addHeader("Govee-API-Key", apiKey);
  int httpCode = http.GET();

  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    deserializeJson(doc, payload);
    getDevicesSuccess = true; // Successful API call

    // Print device information
    JsonArray devices = doc["data"]["devices"];
    for (JsonObject device : devices) {
      const char *deviceName = device["deviceName"];
      // ...
      Serial.println("Device Name: " + String(deviceName));
      // ...
    }
  }

  http.end();
}

printDevices()

This function prints information about Govee devices and controls them based on their current state and the desired action (on or off).

cpp
void printDevices(const String &action) {
  if (getDevicesSuccess) {
    JsonArray devices = doc["data"]["devices"];
    for (JsonObject device : devices) {
      const char *deviceName = device["deviceName"];
      const char *device_address = device["device"];
      const char *model = device["model"];

      DeviceState state = getDeviceState(device_address, model);

      Serial.println("Device Name: " + String(deviceName));
      Serial.println("Power State: " + state.powerState);
      Serial.println("Online: " + String(state.online ? "Yes" : "No"));

      if (state.online) {
        if (action != state.powerState) {
          controlDevice(device_address, model, "turn", action);
          Serial.println("[ACTION] Turn " + String(action));
        } else {
          Serial.println("do nothing...");
        }
      }

      Serial.println("------");
    }
  } else {
    Serial.println("API call was not successful.");
  }
}

controlDevice()

This function sends a PUT request to the /control endpoint of the Govee API to turn a device on or off.

cpp
void controlDevice(const String &device, const String &model, const String &cmdName, const String &cmdValue) {
  HTTPClient http;
  http.begin(control_endpoint);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("Govee-API-Key", apiKey);

  StaticJsonDocument<200> jsonBody;
  jsonBody["device"] = device;
  jsonBody["model"] = model;

  JsonObject cmdObject = jsonBody.createNestedObject("cmd");
  cmdObject["name"] = cmdName;
  cmdObject["value"] = cmdValue;

  String jsonString;
  serializeJson(jsonBody, jsonString);

  int httpCode = http.PUT(jsonString);

  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    Serial.println(payload);
  } else {
    Serial.printf("[HTTP] PUT request failed, error: %s\n", http.errorToString(httpCode).c_str());
  }

  http.end();
}

getDeviceState()

This function sends a GET request to the /state endpoint of the Govee API to fetch the current state (power, online status) of a Govee device.

cpp
DeviceState getDeviceState(const String &device, const String &model) {
  HTTPClient http;
  String apiUrl = state_endpoint + "?device=" + device + "&model=" + model;
  http.begin(apiUrl);
  http.addHeader("Govee-API-Key", apiKey);

  DeviceState result;

  int httpCode = http.GET();

  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();

    DynamicJsonDocument doc(1024);
    deserializeJson(doc, payload);

    JsonObject data = doc["data"];
    result.powerState = data["properties"][1]["powerState"].as<String>();
    result.online = data["properties"][0]["online"];
  } else {
    Serial.printf("[HTTP] GET request failed, error: %s\n", http.errorToString(httpCode).c_str());
  }

  http.end();
  return result;
}

Testing and Demonstration

To test the project, follow these steps:

  1. Open the Arduino IDE and upload the code to your ESP32 board.
  2. Verify that the LED turns on and off correctly based on motion detection.
  3. Simulate motion detection by waving your hand in front of the motion sensor. After a few seconds, your Govee devices should turn on.
  4. Wait for the idle duration (e.g., 4 seconds), and your Govee devices should turn off.

You can monitor the serial output in the Arduino IDE to see the device information and the actions being performed.

Conclusion

In this project, we've explored how to integrate an ESP32 board, a motion sensor, and the Govee API to create a home automation system that controls Govee smart lights based on motion detection. By leveraging the Govee API, we can programmatically turn devices on and off, enabling seamless automation and energy-saving capabilities.

This project serves as a starting point for further exploration of home automation and the Govee ecosystem. You can extend the functionality by adding more sensors, integrating with other smart home platforms, or exploring additional features offered by the Govee API.

Additional Resources

Full Source code

Happy automating!

Full source code
cpp
#include <WiFi.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>
HTTPClient http;

// Parse JSON response
DynamicJsonDocument doc(1024);

const int motionPin = 2;  // GPIO pin connected the OUT pin of the AM312 sensor to
const int led1Pin = 13;   // Replace with the pin of the first LED (green)
const int led2Pin = 15;   // Replace with the pin of the second LED (red)
int time_idle = 0;
int time_in_motion = 0;

const char *ssid = "wifi-ssid";
const char *password = "wifi-password";

const char *apiKey = "govee-api-key"; // get api key from govee
const char *url = "https://developer-api.govee.com/v1/devices";
const char *control_endpoint = "https://developer-api.govee.com/v1/devices/control";
const String &state_endpoint = "https://developer-api.govee.com/v1/devices/state";


// devices that we want to turn on and off
const char *devicesToControl[] = { "[1] Smart LED Bulb", "[2] Smart LED Bulb", "[3] Smart LED Bulb", "[4] Smart LED Bulb" };
bool getDevicesSuccess = false;


struct DeviceState {
  String powerState;
  bool online;
};


void setup() {
  Serial.begin(115200);
  pinMode(motionPin, INPUT);

  pinMode(led1Pin, OUTPUT);
  pinMode(led2Pin, OUTPUT);

  delay(10);

  // Connect to Wi-Fi
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  delay(1000);
  getDevices();
  delay(5000);
}

// Function to turn on LED1 and turn off LED2
void turn1on2off() {
  digitalWrite(led1Pin, HIGH);
  digitalWrite(led2Pin, LOW);
}

// Function to turn on LED2 and turn off LED1
void turn2on1off() {
  digitalWrite(led1Pin, LOW);
  digitalWrite(led2Pin, HIGH);
}

void loop() {
  int motionState = digitalRead(motionPin);

  if (motionState == HIGH) {
    Serial.print("Motion detected! - ");
    Serial.print("Motion lasted ");
    Serial.print(time_in_motion);
    Serial.println(" s.");
    time_in_motion++;
    time_idle = 0;
    if(time_in_motion == 4) {
      printDevices("on");
    }
    turn1on2off();

  } else {
    Serial.print("No motion! - ");
    Serial.print("Idleness lasted ");
    Serial.print(time_idle);
    Serial.println(" s.");
    time_in_motion = 0;
    time_idle++;
    if(time_idle == 4) {
      printDevices("off");
    }
    turn2on1off();
  }

  delay(1000);  // 1s delay
}

void getDevices() {
  HTTPClient http;

  Serial.print("Sending GET request to Govee API...");

  // Your Govee API URL
  http.begin(url);

  // Set headers
  http.addHeader("Govee-API-Key", apiKey);

  // Send the request
  int httpCode = http.GET();

  if (httpCode > 0) {
    Serial.printf("[HTTP] GET... code: %d\n", httpCode);

    if (httpCode == HTTP_CODE_OK) {
      String payload = http.getString();
      Serial.println(payload);
      deserializeJson(doc, payload);

      getDevicesSuccess = true;  // a successful API call

      int numDevicesToControl = sizeof(devicesToControl) / sizeof(devicesToControl[0]);


      // Extract and print device information
      JsonArray devices = doc["data"]["devices"];
      for (JsonObject device : devices) {
        const char *deviceName = device["deviceName"];
        const char *device_address = device["device"];
        for (int i = 0; i < numDevicesToControl; i++) {

          if (strcmp(deviceName, devicesToControl[i]) == 0) {

            const char *model = device["model"];
            bool controllable = device["controllable"];
            bool retrievable = device["retrievable"];

            Serial.println("Device Name: " + String(deviceName) + " " + String(device_address));
            Serial.println("Model: " + String(model));
            Serial.println("Controllable: " + String(controllable ? "Yes" : "No"));
            Serial.println("Retrievable: " + String(retrievable ? "Yes" : "No"));
            Serial.println("------");
            break;
          }
        }
      }
    }
  } else {
    Serial.printf("[HTTP] GET request failed, error: %s\n", http.errorToString(httpCode).c_str());
  }

  // Free resources
  http.end();
}

void printDevices(const String &action) {
  // Check if the API call was successful before printing devices
  if (getDevicesSuccess) {
    // Extract and print device information
    JsonArray devices = doc["data"]["devices"];
    for (JsonObject device : devices) {
      const char *deviceName = device["deviceName"];
        const char *device_address = device["device"];
      const char *model = device["model"];
      bool controllable = device["controllable"];
      bool retrievable = device["retrievable"];

      DeviceState state = getDeviceState(device_address, model);

      Serial.println("Device Name: " + String(deviceName));
      Serial.println("Model: " + String(model));

      Serial.println("Power State: " + state.powerState);
      Serial.println("Online: " + String(state.online ? "Yes" : "No"));
      if(state.online) {
        // then we can either turn on or off

        if(action != state.powerState) {
          controlDevice(device_address, model, "turn", action);
          Serial.println("[ACTION] Turn " + String(action));
        } else {
          Serial.println("do nothing...");
        }
      }

      // Serial.println("Controllable: " + String(controllable ? "Yes" : "No"));
      // Serial.println("Retrievable: " + String(retrievable ? "Yes" : "No"));
      Serial.println("------");
    }
  } else {
    Serial.println("API call was not successful. Check the previous logs for details.");
  }
}

void controlDevice(const String &device, const String &model, const String &cmdName, const String &cmdValue) {
  HTTPClient http;

  Serial.print("[CONTROL DEVICE]");
  http.begin(control_endpoint);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("Govee-API-Key", apiKey);

  // Create JSON request body
  StaticJsonDocument<200> jsonBody;
  jsonBody["device"] = device;
  jsonBody["model"] = model;

  JsonObject cmdObject = jsonBody.createNestedObject("cmd");
  cmdObject["name"] = cmdName;
  cmdObject["value"] = cmdValue;

  // Serialize JSON to string
  String jsonString;
  serializeJson(jsonBody, jsonString);

  // Send the request
  int httpCode = http.PUT(jsonString);

  if (httpCode > 0) {
    // HTTP header has been sent and the response status code received
    Serial.printf("[HTTP] PUT... code: %d\n", httpCode);

    // File found at the server
    if (httpCode == HTTP_CODE_OK) {
      String payload = http.getString();
      Serial.println(payload);
    }
  } else {
    Serial.printf("[HTTP] PUT request failed, error: %s\n", http.errorToString(httpCode).c_str());
  }

  // Free resources
  http.end();
}

DeviceState getDeviceState(const String &device, const String &model) {
  HTTPClient http;

  Serial.print("[GET STATE]");

  String apiUrl = state_endpoint + "?device=" + device + "&model=" + model;
  http.begin(apiUrl);

  // Set headers
  http.addHeader("Govee-API-Key", apiKey);

  DeviceState result;  // Create a struct to store the result

  // Send the request
  int httpCode = http.GET();

  if (httpCode > 0) {
    Serial.printf("[HTTP] GET... code: %d\n", httpCode);

    if (httpCode == HTTP_CODE_OK) {
      String payload = http.getString();
      Serial.println(payload);

      // Parse JSON response
      DynamicJsonDocument doc(1024);
      deserializeJson(doc, payload);

      // Extract device state information
      JsonObject data = doc["data"];
      result.powerState = data["properties"][1]["powerState"].as<String>();
      result.online = data["properties"][0]["online"];
    }
  } else {
    Serial.printf("[HTTP] GET request failed, error: %s\n", http.errorToString(httpCode).c_str());
  }

  // Free resources
  http.end();

  return result;
}