Using the MAC address of an ESP8266’s WiFi adapter as the MQTT unique ID

Note

This is a long story. If you are interested only in the solution, click here

The story

I recently dug my nose in the Arduino world, my goal being to build my own weather station. After experimenting with Arduinos and other boards of different sizes, i finally settled using ESP8266 based NodeMCU boards. They are cheap, have a well developed and maintained library for the Arduino IDE, and can be accessed via WiFi so no cabling is needed throughout my house.

The first version was based on Prometheus, running on a Raspberry Pi. Prometheus periodically queried the station for the weather data, put it on a graph, and all was set…

Except keeping a web server constantly up is really power consuming so it can’t be operated from a battery; i had to install the station near a mains socket, which is in a place in wind shade, providing less accurate temperature readings. It also takes away a precious wall socket, of which i have only two outside.

I got a little stuck in an X-Y problem, and started looking for another solution, based on a Prometheus Push Gateway. While browsing documentation and generally hanging around the Interwebz, i stumbled upon the Home Assistant project (which was on my ToDo list for a looong time anyway). Without hesitating, i quickly backed up the SD card of my Raspberry Pi and installed Hassio on it. I never looked back since.

With Home Assistant installed and configured, i installed and configured the Mosquitto add-on and started tinkering with my ESP boards.

Logging the “weather” of my office (and adding a doorbell to the mix)

Since i didn’t want to dismantle the Prometheus based station yet (even though there was no-one to query it except me calling http get http://192.168.0.12/metrics manually), i decided to measure the temperature and humidity of my home office environment.

The circuit is really simple:

  • Take an ESP8266 board and a DHT22 sensor (you can go with a DHT11, it doesn’t really matter)
  • Connect the GND, VCC, and OUT pins of the DHT sensor to the GND, 3V3, and D5 (any other pin can work, though)
  • Install the code below, and behold!

OK, i had a bit of precaution, and installed Mosquitto on my local machine, so i could test it before sending data directly to Hassio.

Meanwhile, i was expecting some package deliveries. I trusted the dogs that they will bark whenever someone stops at our gate, which i can clearly hear in my office. Well, it turned out they don’t do that if it’s raining, and since my smartphone rebooted for some unknown reason, the delivery guy could not reach me and left; he will try to deliver the package again on Monday. All this happened because i don’t have a doorbell.

So while at it, i quickly installed a push button on our gate, led the wire to my office (a good 5 meters or so), and did some more soldering:

  • Connect the GND pin of the ESP board to D4 through a 10kΩ resistor
  • Connect one pin of the push button to the 3V3 pin of the ESP board
  • Connect the other pin of the push button to the D4 pin of the ESP board

This way D4 is pulled down (ie. remains low) all the time, and is pulled up (becomes high) when someone pushes the button.

All is good, now to the coding part!

The code

The code below is already tailored to be used with Home Assistant. While testing, BROKER_ADDR pointed to my PC, and BROKER_USERNAME and BROKER_PASSWORD were not defined.

To compile it, you will need the Arduino IDE (or any toolchain that can compile Arduino code), the ESP8266 board files for Arduino IDE, the Adafruit DHT library, and the home-assistant-integration library.

#include <Adafruit_Sensor.h>
#include <ESP8266WiFi.h>
#include <ArduinoHA.h>
#include <DHT.h>

#define LED_PIN         BUILTIN_LED
#define BROKER_ADDR     IPAddress(192, 168, 0, 16)
#define BROKER_USERNAME "homeassistant"
#define BROKER_PASSWORD "verySecureHomeAssistantMQTTPassword"
#define BROKER_NAME     "office-weather-station"
#define WIFI_SSID       "MyHomeWiFi"
#define WIFI_PASSWORD   "MyHomeWiFiPassword"
#define DHT_PIN         D5
#define DHT_TYPE        DHT22
#define DOORBELL_PIN    D4
#define UPDATE_INTERVAL 5000
#define UNIQUE_ID       "84cca8aa2673"
#define BUZZER_PIN      D7

WiFiClient client;
HADevice device(UNIQUE_ID);
HAMqtt mqtt(client, device);
HASwitch led("led", false, mqtt);
HABinarySensor doorbell("doorbell", false, mqtt);
HASensor<double> temperature("temperature", 0, mqtt);
HASensor<double> humidity("humidity", 0, mqtt);
unsigned long last_update = 0;
DHT dht(DHT_PIN, DHT_TYPE);
bool doorbell_pushed = false;
unsigned long doorbell_push_time = 0;
bool ringing = false;

void onSwitchStateChanged(bool state, HASwitch* s)
{
    digitalWrite(LED_PIN, (state ? LOW : HIGH));
}

void setup() {
    sensor_t sensor;
    Serial.begin(9600);
    Serial.println("Starting...");

    dht.begin();

    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, HIGH);

    pinMode(DOORBELL_PIN, INPUT);
    pinMode(BUZZER_PIN, OUTPUT);

    // connect to wifi
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    while (WiFi.status() != WL_CONNECTED) {
        Serial.print(".");
        delay(500); // waiting for the connection
    }

    Serial.println();
    Serial.print("Connected to the network, IP address: ");
    Serial.println(WiFi.localIP());

    // set device's details (optional)
    device.setName(BROKER_NAME);
    device.setSoftwareVersion("0.3.0");

    // handle switch state
    led.onStateChanged(onSwitchStateChanged);

    temperature.setUnitOfMeasurement("°C");
    humidity.setUnitOfMeasurement("%");

    mqtt.begin(BROKER_ADDR
#if defined(BROKER_USERNAME) && defined(BROKER_PASSWORD)
               , BROKER_USERNAME, BROKER_PASSWORD
#endif
              );
}

void loop() {
    sensors_event_t event;
    unsigned long now = millis();
    float temp_value;
    float humid_value;
    int doorbell_state = digitalRead(DOORBELL_PIN);

    mqtt.loop();

    if ((doorbell_state == HIGH) && !doorbell_pushed) {
        doorbell_pushed = true;
        doorbell_push_time = now;
    } else if ((doorbell_state == LOW) && doorbell_pushed) {
        doorbell_pushed = false;
        doorbell.setState(false);
        ringing = false;
        Serial.println("Doorbell released");
        analogWrite(BUZZER_PIN, 0);
        delay(100);
    }

    if (doorbell_pushed && !ringing && (now - doorbell_push_time > 100)) {
        ringing = true;
        Serial.println("Doorbell pushed");
        doorbell.setState(true);
        analogWrite(BUZZER_PIN, 255);
    }

    if ((now - last_update) >= UPDATE_INTERVAL) {
        last_update = now;

        temp_value = dht.readTemperature();
        temperature.setValue(temp_value);
        Serial.print("Read temperature ");
        Serial.println(temp_value);

        humid_value = dht.readHumidity();
        humidity.setValue(humid_value);
        Serial.print("Read humidity ");
        Serial.println(humid_value);
    }
}

Nice, isn’t it? Well, can you see that UNIQUE_ID thing? That’s the ugly part, and that’s what this article is about.

Into the depths of the home-assistant-integration

The NodeMCU example of the library goes like this (only the relevant parts are here):

byte mac[6];
WiFiClient client;
HADevice device(mac, sizeof(mac));
HAMqtt mqtt(client, device);
HASwitch led("led", false, mqtt); // you can use custom name in place of "led"

void setup() {
    WiFi.macAddress(mac);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    mqtt.begin(BROKER_ADDRESS);
}

Now guess what the unique ID of the device will be. I’ll wait…

Was your answer “the MAC address of the ESP board’s WiFi chip”? Yeah, mine too. Except it will be 000000000000. If you want to install one station in your house, that’s not a big deal. But i want one outside, one in my office, in the kitchen, the bedroom, bathroom, and so one. Having the same unique ID makes it not-so-unique in this case. So I dug deeper in the code of HADevice.

It has the following constructors:

HADevice::HADevice(const char *uniqueId) :
    _uniqueId(uniqueId);
    HADEVICE_INIT
{}

uint16_t HADevice::HADevice(const byte *uniqueId, const uint16_t &length) :
    _uniqueId(HAUtils::byteArrayToStr(uniqueId, length)),
    HADEVICE_INIT
{}

Meanwhile, the WiFi.macAddress(mac) line calls a function that gets the MAC address of the WiFi chip, and stores the bytes in the mac array.

So what happens? How does the unique ID become a string of zeroes? Well, the example code calls the second constructor, effectively converting the mac array (full of zeroes) to a character string full of zeroes.

The solution

As you can’t get the MAC address of the WiFi card outside of a function like setup(), we can’t rely on the second constructor form. However, the first form is more interesting if you look at the code behind it: if you provide a string as the unique ID, it will be used without any mangling. So let’s update our code a bit:

#include <ESP8266WiFi.h>
#include <ArduinoHA.h>

#define LED_PIN         D0
#define BROKER_ADDR     IPAddress(192, 168, 0, 17)
#define WIFI_SSID       "MyNetwork"
#define WIFI_PASSWORD   "MyPassword"

char macaddress[13];
WiFiClient client;
HADevice device(macaddress);
HAMqtt mqtt(client, device);
HASwitch led("led", false, mqtt); // you can use custom name in place of "led"

void onSwitchStateChanged(bool state, HASwitch* s)
{
    digitalWrite(LED_PIN, (state ? HIGH : LOW));
}

void setup() {
    byte mac[6];

    Serial.begin(9600);
    Serial.println("Starting...");

    WiFi.macAddress(mac);
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);

    // connect to wifi
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    while (WiFi.status() != WL_CONNECTED) {
        Serial.print(".");
        delay(500); // waiting for the connection
    }
    Serial.println();
    Serial.println("Connected to the network");

    snprintf(macaddress, 13,
             "%02x%02x%02x%02x%02x%02x",
             mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

    // set device's details (optional)
    device.setName("NodeMCU");
    device.setSoftwareVersion("1.0.0");

    // handle switch state
    led.onStateChanged(onSwitchStateChanged);

    mqtt.begin(BROKER_ADDR);
}

void loop() {
    mqtt.loop();
}

And now you have a unique(ish) ID (well, unless you start tinkering with MAC addresses on you network, but then you are on your own). And if you want to update the unique ID while the software is still running, you can do that, too. But i won’t help you with such perversions.

links

social