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 
#include 
#include 
#include 
#include 
#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
  dtostrf(t, 5, 1, temperature);
  dtostrf(h, 5, 1, humidity);
  dtostrf(p, 5, 1, pressure);
}
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;
  dtostrf(v, 5, 2, vcc);
}
void callback(char* topic, byte* payload, unsigned int length) {
  std::string s( reinterpret_cast(payload), length );
  DPRINT("message arrived: ");
  DPRINT(topic);
  DPRINT(" - ");
  DPRINT(s.c_str());
  DPRINTLN();
  if (s == "all" || s == chipId.c_str()) {
    publish();
  }
}

The configuration file:

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

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:

String SVoltageESP1 "ESP voltage [%s V]" {mqtt="<[mosquitto:raw/4896388/vcc:state:default]"}
String STemperatureESP1 "Outside temperature (ESP1) [%s °C]" {mqtt="<[mosquitto:raw/4896388/temperature:state:default]"}
String SHumidityESP1 "Outside humidity (ESP1) [%s%%]" {mqtt="<[mosquitto:raw/4896388/humidity:state:default]"}
String SPressureESP1 "Pressure (ESP1) [%s hPa]" {mqtt="<[mosquitto:raw/4896388/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)
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

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.

19 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. ;)

Your comment: