Battery powered weather station with ESP8266 and BME280

I used to use web services such as OpenWeatherMap to monitor outside temperature in openHAB. I found out that sometimes they’re not very accurate and naturally they fail when the internet is down. So I decided to measure the temperature myself. After some searching, I decided to settle with battery powered ESP8266 wireless solution. The advantages are the following:

  • no wires;
  • low cost;
  • easy programming via familiar Arduino IDE;
  • long battery operation with deep sleep mode.

I’ve chosen ESP-01 module mostly due to its popularity and low cost. However, to reduce power consumption it requires two hardware modifications: removing power LED (which is always on otherwise) and soldering pin #8 of ESP8266 chip to RST pin of the module. The latter is especially tricky—the chip pins are very tiny—so if you’re not comfortable you should probably choose ESP-03 module instead.

To measure temperature I use BME280 environmental sensors which also provide humidity and pressure readings. I prefer them over DHT-22 which are less accurate, very slow and generally less reliable. You can get BME280s in neat ready to use modules.

To program ESP-01 I used UART module I already had which supports 3.3V, but I’d highly recommend a specialized one. To enter programming mode, you need to connect GPIO0 to GND and CH_PD pin to VCC, and this module has a special switch for that. This makes debugging via serial connection much easier.

I need the setup to operate in cold temperatures (down to -20°C), so I chose 3×AA Ni-MH rechargeable batteries which are ubiquitous and behave better in such environment than Li-ion, for example. Three batteries connected in series give more than 4V fully charged which is above 3–3.6V datasheet range but seem to work nevertheless.
To summarize, you’ll need the following:

  • ESP-01 (or ESP-03, see above) module;
  • BME280 sensor module;
  • 3.3V USB UART;
  • 3×AA Ni-MH batteries with holder;
  • soldering iron, wires (breadboard, jump wires, etc.—optional);
  • PC with USB port and Arduino IDE.

On the software side, I used existing openHAB installation. To communicate with ESP-01 it needs running MQTT broker and corresponding addon. Fortunately, broker installation is simple:

wget http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key
sudo apt-key add mosquitto-repo.gpg.key
cd /etc/apt/sources.list.d/
sudo wget http://repo.mosquitto.org/debian/mosquitto-wheezy.list
sudo apt-get install mosquitto
sudo systemctl enable mosquitto
sudo systemctl start mosquitto

openHAB addon can (probably) be installed via Paper UI, but I used legacy config:

sudo nano /etc/openhab2/services/addons.cfg

binding = ..., mqtt1

sudo rm /var/lib/openhab2/config/org/openhab/mqtt.config
sudo nano /etc/openhab2/services/mqtt.cfg

mosquitto.url=tcp://localhost:1883

I found that the best way to debug Mosquitto is to run it in terminal:

sudo systemctl stop mosquitto
sudo mosquitto -p 1883 -v

I did the wiring and debugging in following steps:

  1. Connected ESP-01 to UART and check if it works. I used WiFiScan example for that.
  2. I started with a great sketch by Marcel Akkerman but replaced BME280 code with dummy random values. Don’t forget to create config.h file with your parameters!
  3. When I got that running, I enabled BME280 code and connected the sensor. I was pleasantly surprised when it worked and started sending data.
  4. Then I soldered deep sleep pins and brutally destroyed power LED on ESP-01 and added ESP.deepSleep(10*1000*1000) call to the sketch which sends it to sleep for just 10 seconds. For debugging purposes, I disabled BME280 readout again and was removing GND wire from CH_PD pin of ESP as soon as the sketch was 100% uploaded. This way I could see the serial output.
  5. When I was happy with results, I increased the interval to 900 seconds, disabled serial output and enabled BME280 again.

As the last step, I used RX (GPIO3) pin on ESP-01 to power BME280 sensor. That prevents it from draining battery when the deep sleep is active.
Here’s the final wiring scheme:

AA battery powered ESP-01 with BME280 and deep sleep - scheme

And here’s the sketch for ESP-01:

// ESP-01 with BME-280 via MQTT with deep sleep
// Based on https://github.com/akkerman/espthp

#include <string>
#include <esp8266wifi.h>
#include <pubsubclient.h>
#include <adafruit_sensor.h>
#include <adafruit_bme280.h>
#include <ticker.h>
#include "config.h"
#define STATUS_DISCONNECTED "disconnected"
#define STATUS_ONLINE "online"
#define DEEP_SLEEP_S 900  // 900 s = 15 min.
#define BME280ON  // Comment to disable BME280 sensor readout.
//#define DEBUG  // Comment to disable debug serial output.
#ifdef DEBUG
  #define DPRINT(...)    Serial.print(__VA_ARGS__)
  #define DPRINTLN(...)  Serial.println(__VA_ARGS__)
#else
  #define DPRINT(...)
  #define DPRINTLN(...)
#endif

ADC_MODE(ADC_VCC);

unsigned long previousMillis = 0;
const long interval = 10 * 60 * 1000;
const String chipId = String(ESP.getChipId());
const String hostName = "ESP-01_" + chipId;
const String baseTopic = "raw/" + chipId + "/";
const String tempTopic = baseTopic + "temperature";
const String humiTopic = baseTopic + "humidity";
const String presTopic = baseTopic + "pressure";
const String willTopic = baseTopic + "status";
const String vccTopic  = baseTopic + "vcc";
const String ipTopic   = baseTopic + "ip";
IPAddress ip;
WiFiClient WiFiClient;
PubSubClient client(WiFiClient);

#ifdef BME280ON
  Adafruit_BME280 bme; // I2C
#endif

Ticker ticker;
char temperature[6];
char humidity[6];
char pressure[7];
char vcc[10];
void setup() {
#ifdef DEBUG
  Serial.begin(9600);
#else
  pinMode(3, OUTPUT);  // Power up BME280 when powered via RX (GPIO3).
  digitalWrite(3, HIGH);
#endif
  delay(10);
#ifdef BME280ON
  Wire.begin(0, 2);
  Wire.setClock(100000);
  if (!bme.begin(0x76)) {
    DPRINTLN("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
#endif
  DPRINTLN();
  DPRINT("Connecting to ");
  DPRINTLN(WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.hostname(hostName);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    DPRINT(".");
  }
  DPRINTLN();
  DPRINTLN("WiFi connected");
  DPRINT("IP address: ");
  ip = WiFi.localIP();
  DPRINTLN(ip);
  client.setServer(MQTT_IP, MQTT_PORT);
  client.setCallback(callback);
  ticker.attach(600.0, publish);
  bool publishNow = false;
  yield();
  if (!client.connected()) {
    DPRINTLN("Attempting MQTT connection...");
    if (client.connect(chipId.c_str(), willTopic.c_str(), 1, true, STATUS_DISCONNECTED)) {
      DPRINTLN("MQTT client connected.");
      client.publish(willTopic.c_str(), STATUS_ONLINE, true);
      client.publish(ipTopic.c_str(), ip.toString().c_str(), true);
      client.subscribe("config/publish");
      publishNow = true;
    } else {
      DPRINT("failed, rc=");
      DPRINTLN(client.state());
    }
  }
  bmeRead();
  vccRead();
#ifndef DEBUG
  digitalWrite(3, LOW);
#endif
  if (client.connected()) {
    client.loop();
    if (publishNow) {
      publish();
      publishNow = false;
    }
  }
  client.disconnect();
  delay(500); // Needed to properly close connection.
  WiFi.disconnect();
  DPRINTLN("Going to deep sleep...");
  ESP.deepSleep(DEEP_SLEEP_S * 1000 * 1000);
}

void loop() {
}

void bmeRead() {
#ifdef BME280ON
  float t = bme.readTemperature();
  float h = bme.readHumidity();
  float p = bme.readPressure() / 100.0F;
#else
  float t = 20 + random(5);
  float h = 50 + random(10);
  float p = 1013 + random(10);
#endif
  sprintf(temperature, "%.1f", t);
  sprintf(humidity, "%.1f", h);
  sprintf(pressure, "%.1f", p);
}

void printMeasurements() {
  DPRINT("t,h,p: ");
  DPRINT(temperature); DPRINT(", ");
  DPRINT(humidity);    DPRINT(", ");
  DPRINT(pressure);    DPRINTLN();
  DPRINT("vcc:   ");   DPRINTLN(vcc);
}

void publish() {
  printMeasurements();
  if (client.connected()) {
    DPRINTLN("publishing values");
    DPRINTLN(baseTopic);
    client.publish(tempTopic.c_str(), temperature);
    client.publish(humiTopic.c_str(), humidity);
    client.publish(presTopic.c_str(), pressure);
    client.publish(vccTopic.c_str(), vcc);
  } else {
    DPRINTLN("client not connected, not publishing");
  }
}

void vccRead() {
  float v  = ESP.getVcc() / 1000.0;
  sprintf(vcc, "%.2f", v);
}

void callback(char* topic, byte* payload, unsigned int length) {
  std::string s( reinterpret_cast<char const*>(payload), length );
  Serial.print("message arrived: ");
  Serial.print(topic);
  Serial.print(" - ");
  Serial.print(s.c_str());
  Serial.println();
  if (s == "all" || s == chipId.c_str()) {
    publish();
  }
}

The configuration file config.h:

#define WIFI_SSID ""
#define WIFI_PASS ""
#define MQTT_IP "192.168.0.107"
#define MQTT_PORT 1883

If you use password authentication in MQTT, add the credentials to the connect function in the sketch:

client.connect(chipId.c_str(), "user", "password", willTopic.c_str(), 1, true, STATUS_DISCONNECTED)

openHAB

Unfortunately, MQTT operates with strings and to draw plots and create conditions in openHAB we need numbers. So I created a set of hacky rules to convert values on each change:

mqtt.items:

String SVoltageESP1 "ESP voltage [%s V]" {mqtt="<[mosquitto:raw/4896388/vcc:state:default]"}
String STemperatureESP1 "Outside temperature (ESP1) [%s °C]" {mqtt="<[mosquitto:raw/1234567/temperature:state:default]"}
String SHumidityESP1 "Outside humidity (ESP1) [%s%%]" {mqtt="<[mosquitto:raw/1234567/humidity:state:default]"}
String SPressureESP1 "Pressure (ESP1) [%s hPa]" {mqtt="<[mosquitto:raw/1234567/pressure:state:default]"}
Number VoltageESP1 "Voltage (ESP1) [%.2f V]"  (Voltages)
Number TemperatureESP1 "Outside temperature [%.1f °C]"  (Temperatures)
Number HumidityESP1 "Outside humidity [%.1f%%]"  (Humidities)
Number PressureESP1 "Outside pressure [%.1f hPa]"  (Pressures)

mqtt.rules:

rule "VoltageESP1 String to Number"
when
  Item SVoltageESP1 changed
then
  VoltageESP1.postUpdate(Float::parseFloat(String::format("%s",SVoltageESP1.state).replace(' ','')))
end

rule "TemperatureESP1 String to Number"
when
  Item STemperatureESP1 changed
then
  TemperatureESP1.postUpdate(Float::parseFloat(String::format("%s",STemperatureESP1.state).replace(' ','')))
end

rule "HumidityESP1 String to Number"
when
    Item SHumidityESP1 changed
then
  HumidityESP1.postUpdate(Float::parseFloat(String::format("%s",SHumidityESP1.state).replace(' ','')))
end

rule "PressureESP1 String to Number"
when
    Item SPressureESP1 changed
then
  PressureESP1.postUpdate(Float::parseFloat(String::format("%s",SPressureESP1.state).replace(' ','')))
end

Home Assistant

To use the data in Home Assistant you should enable MQTT integration and put the following values to the sensor section of configuration.yaml:

sensor:
  - platform: mqtt
    name: "esp01_temperature"
    unique_id: "esp01_temperature"
    unit_of_measurement: "°C"
    device_class: temperature
    state_topic: "raw/1234567/temperature"
  - platform: mqtt
    name: "esp01_humidity"
    unique_id: "esp01_humidity"
    device_class: humidity
    unit_of_measurement: "%"
    state_topic: "raw/1234567/humidity"
  - platform: mqtt
    name: "esp01_pressure"
    unique_id: "esp01_pressure"
    device_class: pressure
    unit_of_measurement: "hPa"
    state_topic: "raw/1234567/pressure"
  - platform: mqtt
    name: "esp01_voltage"
    unique_id: "esp01_voltage"
    device_class: voltage
    unit_of_measurement: "V"
    state_topic: "raw/1234567/vcc"

Don’t forget to replace 1234567 with the chip Id of your ESP or hard-code your own baseTopic in the sketch.

The first test with some old used batteries showed that the setup worked down to at least 2.3V.

UPD Nov’18: So far with a set of few years old 2000 mAh AA NiMH batteries the setup works for 2 months+ in summer and ~1 month in winter.

UPD: I replaced dtostrf with sprintf to remove spaces from the published strings, since some software has troubles converting them back to numbers.

UPD: Added Home Assistant config.

UPD: In 2022 Home Assistant introduced new config format for MQTT so the relevant section should now look like this:

mqtt:
  sensor:
    - name: "esp01_temperature"
      unique_id: "esp01_temperature"
      state_topic: "raw/1234567/temperature"
      device_class: temperature
      unit_of_measurement: "°C"
    - name: "esp01_humidity"
      unique_id: "esp01_humidity"
      device_class: humidity
      unit_of_measurement: "%"
      state_topic: "raw/1234567/humidity"
    - name: "esp01_pressure"
      unique_id: "esp01_pressure"
      device_class: pressure
      unit_of_measurement: "hPa"
      state_topic: "raw/1234567/pressure"
    - name: "esp01_voltage"
      unique_id: "esp01_voltage"
      device_class: voltage
      unit_of_measurement: "V"
      state_topic: "raw/1234567/vcc"

UPD: I’ve replaced AA batteries with a 3.2 V 1800 mAh LiFePO4 battery and it works pretty well. The downside is you need a special charger for those, the regular Li-ion won’t do. See the voltage plot below (taken in the temperature range of -14° to +7°C):

45 comments:

  1. Marco says:

    Hi,
    I try to use your sketch but I use a username an password to connect mqtt. Unfortunately I am not a programmer is there a line where I just can add password and username?
    Thank you & regards
    Marco

  2. Kim says:

    So I’m completely new to this!
    What should I do when I get:
    In function ‘void setup()’:
    sketch_aug14b:80: error: ‘WIFI_SSID’ was not declared in this scope
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    ^
    sketch_aug14b:80: error: ‘WIFI_PASS’ was not declared in this scope
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    ^
    sketch_aug14b:93: error: ‘MQTT_IP’ was not declared in this scope
    client.setServer(MQTT_IP, MQTT_PORT);
    ^
    sketch_aug14b:93: error: ‘MQTT_PORT’ was not declared in this scope
    client.setServer(MQTT_IP, MQTT_PORT);
    ^
    exit status 1
    ‘WIFI_SSID’ was not declared in this scope

  3. Adrian says:

    “I’ll update this post with long term testing results.”
    This.

    • glsk says:

      Fair point! So far with a set of few years old 2000 mAh AA NiMH batteries the setup works for 2 months+ in summer and ~1 month in winter.

  4. Leo Diaconu says:

    hi,
    Great project.
    Can you share some sketch with an web interface to change the network settings ?
    It is not practical to connect the device to pc every time you change your SSID or password.
    Thanks!

  5. neonew says:

    Thank you for this excellent article! Best tutorial I’ve found, including all youtube videos.
    Maybe you can add a photo of your final setup?

    • glsk says:

      Haha, no way, sorry. 🙂 It’s just too ugly to show — basically it’s the scheme shown above stuffed in a plastic box with a hole for the sensor to react faster.

  6. Kimmo Hassinen says:

    Is there way to monitor battery levels somehow? Like putting alarm or something when 10% or less is battery left.

  7. InLeon says:

    Hi,
    thanks for your great project, what I’m exactly looking for.
    But I have a problem: I can read only the values from the esp out of the MQTT server, but not from the BME. These values shows always “nan” (not a number).
    When I connect the BME directly to VCC everything is fine, but not when powering it with GPIO3.
    I am using 3xEneloop (~3,9V). GPIO3 switches every 10 seconds between 3.7V and 3.5V, but never goes down to 0V as I expected.
    Do you have any idea?
    Best regards
    Ingo

    • glsk says:

      Now that I think of it, maybe powering off BME280 doesn’t make too much sense in the first place. The sensor itself consumes just 0.1 uA in sleep mode, that’s less than 0.1 mAh per month. I never did any long term tests myself, maybe you want to try?
      Anyway, a stupid question: are you sure ‘digitalWrite(3, LOW)’ is actually being called before going to sleep? In my sketch it’s executed only if DEBUG is not defined.

      • InLeon says:

        Hi glsk, yes, I do call ‘digitalWrite(3, LOW)’ because DEBUG is not defined. That’s strange, isn’t it?
        Regarding the powering of BME280, 0.1 uA is not very much. I think I have to use it in MODE_FORCED, so it goes to sleep mode after a read. I have no experience with it, but I will try and tell you about it.
        Ingo

  8. Iulian Gheorghiu says:

    Hi!
    I’m trying to achieve something similar to the above but using a ds18b20 temperature sensor instead, and I’m facing some issues powering the esp-01 :(. For some reason, it doesn’t power on 3 regular AA batteries (Duracell, not rechargeable) connected in series with a pull down regulator (AMS1117-3.3 3.3V) and 2 electrolytic capacitors (100uf, 10uf). Still, when I connect the entire setup on RPi it powers on perfectly fine. I even tried with 4 AA batteries and it powered on, but I don’t think this is the way to go since replacing 4 batteries is not cost-effective. The output voltage on those 3 AA is about 4.6V and the pull down regulator reduces it to a steady 3.24V. Connecting the ESP-01, reduces the voltage even more to about 2V.
    Am I missing a point here, should I have gone with rechargeable batteries instead?
    Thanks & really appreciate this tutorial! :thumbsup:

    • glsk says:

      That’s odd. My ESP-01 works just fine in ~2.5-4.2V range. According to the datasheet the maximum dropout voltage of AMS1117 is 1.3V, may this be the reason of your troubles (4.6 – 1.3 = 3.3)?
      BTW, are you sure this regulator won’t drain the battery too quickly? Since DS18B20 can go up to 5.5V I’d try powering your setup with 3 AAs directly. But better have a spare ESP-01 just in case. 😉

  9. Dirk says:

    Hi,

    I try to use your sketch but i get an error. In the last function. See below.

    main:163:45: error: expected ‘>’ before ‘*=’ token
    std::string s( reinterpret_cast(payload), length );
    ^
    main:163:45: error: expected ‘(‘ before ‘*=’ token
    main:163:45: error: expected primary-expression before ‘*=’ token
    main:163:58: error: comparison between distinct pointer types ‘const char*’ and ‘byte* {aka unsigned char*}’ lacks a cast [-fpermissive]
    std::string s( reinterpret_cast(payload), length );
    ^
    I can’t really understand the problem.

    Best regards
    Dirk

    • glsk says:

      Thank you very much for noticing this. This part of code got messed up during my WordPress upgrade. I’ve fixed this part, please check if everything else is OK.

  10. mehmet tarhan says:

    exit status 1
    esp8266wifi.h: No such file or directory

    Where is the library. Error always comes.

  11. Fred says:

    hi

    i have this on my serie monitor

    10:49:10.269 -> ets Jan 8 2013,rst cause:2, boot mode:(3,6)
    10:49:10.303 ->
    10:49:10.303 -> load 0x4010f000, len 3664, room 16
    10:49:10.303 -> tail 0
    10:49:10.303 -> chksum 0xee
    10:49:10.303 -> csum 0xee
    10:49:10.303 -> v39c79d9b
    10:49:10.303 -> ~ld

  12. Сергей says:

    Thanks for the project. I really want to repeat it. But when compiling, it gives an error in: std::string s(reinterpret_cast(payload), length); (153:14: error: variable ‘std::string s’ has initializer but incomplete type
    153 | std::string s(reinterpret_cast(payload), length);
    | ^
    exit status 1
    variable ‘std::string s’ has initializer but incomplete type)
    How to fix it?

  13. Сергей says:

    Thanks a lot! There are no errors. Also explain how and where to use mqtt.items: and mqtt.rules:?

    • glsk says:

      Thanks, I’ll update the post!
      As for the files you mentioned, they are Openhab config files for MQTT. I’ve switched to Home Assistant a while ago, so those may be outdated, sorry about that.

  14. Сергей says:

    Thanks for answering. I don’t understand where to insert mqtt.items: and mqtt.rules: code? Where should they be registered or created?

  15. Сергей says:

    Home Assistant (OpenHab)MQTT.

  16. Сергей says:

    My MQTT broker is assembled on ESP 12f. I want to use your code to connect to it.

    • glsk says:

      If you put your MQTT broker IP address and port in config.h, the broker will receive and distribute weather data. Then it’s up to your MQTT-compatible smart home system (Openhab/HA/etc.) to grab weather station data and display and process it.

  17. Сергей says:

    I understand it. I don’t understand what to do with mqtt.items: and mqtt.rules:, the code in the article. I created the config.h

    • glsk says:

      mqtt.items and mqtt.rules are config files for an older version of Openhab. If you use Openhab, unfortunately I cannot help you: I don’t use it any more. If you use Home Assistant, I can try to add a brief howto to the post some time later.

  18. Сергей says:

    I will be using Home Assistant. I would be very grateful if you write a guide. Thank you for your communication.

  19. daks says:

    hello, this is very nice project. Can you help me to connect esp with HomeAssistant
    I tired to set MQTT_USER and MQTT_PASSWORD but stil not have connection to my MQTT BROKER on HA.

    • daks says:

      I found it, just add username and password for broker in this line
      if (client.connect(chipId.c_str(), username, password, willTopic.c_str(), 1, true, STATUS_DISCONNECTED)

      • glsk says:

        Thanks for reporting this, I’ve added info on MQTT authentication to the main post.

Your comment: