diff --git a/content/blog/using-esp8266-macaddress-as-mqtt-unique-id.rst b/content/blog/using-esp8266-macaddress-as-mqtt-unique-id.rst new file mode 100644 index 0000000..97d198b --- /dev/null +++ b/content/blog/using-esp8266-macaddress-as-mqtt-unique-id.rst @@ -0,0 +1,331 @@ +Using the MAC address of an ESP8266’s WiFi adapter as the MQTT unique ID +######################################################################## + +:date: 2021-01-31T05:35:00Z +:category: blog +:tags: homeassistant,arduino +:url: blog/using-esp8266-macaddress-as-mqtt-unique-id/ +:save_as: blog/using-esp8266-macaddress-as-mqtt-unique-id/index.html +:status: published +:author: Gergely Polonkai + + +.. 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 is on my ToDo list for a looong time). 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 quicly 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. + +.. code-block:: c++ + + #include + #include + #include + #include + + #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 temperature("temperature", 0, mqtt); + HASensor 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): + +.. code-block:: c++ + + 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 dag deeper in the code of ``HADevice``. + +It has the following constructors: + +.. code-block:: c++ + + 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? + +The example code calls the second constructor, effectively converting the ``mac`` array (full of +zeroes) to a character string full of zeroes. + +The solution +============ + +.. _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: + +.. code-block:: c++ + + #include + #include + + #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).