Compare commits

...

No commits in common. 'esphome' and 'ir' have entirely different histories.
esphome ... ir

  1. 18
      .gitignore
  2. 39
      .gitlab-ci.yml
  3. 20
      LICENSE.md
  4. 63
      README.md
  5. 83
      common/base.yaml
  6. 27
      common/battery.yaml
  7. 8
      common/bh1750.yaml
  8. 45
      common/bme280.yaml
  9. 96
      common/bme680.yaml
  10. 238
      common/display-hendrik.yaml
  11. 294
      common/display.yaml
  12. 8
      common/experimental/ble.yaml
  13. 2
      common/experimental/webserver.yaml
  14. 15
      common/sds011.yaml
  15. 4
      common/secrets.yaml
  16. 19
      common/veml6075.yaml
  17. 43
      custom/veml6075_custom_sensor.h
  18. BIN
      fonts/Vera.ttf
  19. BIN
      fonts/VeraMono.ttf
  20. BIN
      fonts/materialdesignicons-webfont.ttf
  21. BIN
      fonts/slkscr.ttf
  22. 37
      fonts/weather_icon_map.h
  23. 15
      hendrik-test.yaml
  24. 57
      platformio.ini
  25. 13
      script/autoversioning.py
  26. 10
      secrets-example.yaml
  27. 11
      sensor-outdoor.yaml
  28. 9
      sensor-springer.yaml
  29. 8
      sensor-sz.yaml
  30. 8
      sensor-wz.yaml
  31. 314
      src/IR_RMT.cpp
  32. 53
      src/IR_RMT.h
  33. 77
      src/SensorHistory.cpp
  34. 26
      src/SensorHistory.h
  35. 50
      src/icons.h
  36. 681
      src/main.cpp
  37. 148
      src/network/XD0MQTT.cpp
  38. 66
      src/network/XD0MQTT.h
  39. 136
      src/network/XD0OTA.cpp
  40. 94
      src/network/XD0OTA.h
  41. 11
      test/README

18
.gitignore

@ -0,0 +1,18 @@
.pio
# old platformio
.pioenvs
.piolibdeps
.clang_complete
.gcc-flags.json
# virtualenv
bin/
include/
lib/
local/
share/
# custom
version.txt
build/

39
.gitlab-ci.yml

@ -0,0 +1,39 @@
stages:
- build
- deploy
build:
stage: build
image: python:2.7
before_script:
- pip install -U platformio
- platformio update
script:
# - date +%s > build/version.txt
- platformio ci --lib="script" --project-conf platformio.ini --board=lolin_d32_pro --build-dir="build" --keep-build-dir
variables: {PLATFORMIO_CI_SRC: "src"}
artifacts:
paths:
- build/.pio/build/lolin_d32_pro/*.bin
- build/version.txt
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk update && apk add openssh-client bash rsync
# - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- eval $(ssh-agent -s)
- echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add - > /dev/null
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "${SSH_HOST_KEY}" > ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- scp build/.pio/build/lolin_d32_pro/firmware.bin "${SSH_USER_HOST}:${DEPLOY_PATH}"
- scp build/version.txt "${SSH_USER_HOST}:${DEPLOY_PATH}".version
dependencies:
- build
when: manual
only:
- master

20
LICENSE.md

@ -0,0 +1,20 @@
Copyright (c) 2019 Hendrik Langer
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

63
README.md

@ -1,12 +1,9 @@
# esp32-weatherstation2
# esp32-weatherstation
Wetterstation v2 with ESP32 and various sensors (with ESPHome!)
[![Build Status](https://drone.xd0.de/api/badges/hendrik/esp32-weatherstation/status.svg)](https://drone.xd0.de/hendrik/esp32-weatherstation)
Wetterstation v2 with ESP32 and various sensors
## Hardware
* ESP32 µC
* Sensors
## Wiring
@ -67,8 +64,8 @@ Serial (HardwareSerial2)
SDS011 pin | ESP32 pin | Notes
-----------|-----------|----------
TXD | IO15 | (don't use IO16 together with PSRAM)
RXD | IO2 | (don't use IO17 together with PSRAM)
TXD | IO16 |
RXD | IO17 |
GND | GND |
25µm | nc |
5V | VUSB | 5V
@ -77,42 +74,14 @@ NC | nc |
## Build & Install
* Install ESPHome via Home-Assistant Supervisor
* Upload all needed files via the "File editor" into `config/esphome/` and subdirectories.
* rename secrets-example.yaml to secrets.yaml and fill in your credentials
* Choose your device and flash the firmware. Use ESPHome "Install" -> "Plug into this computer". (Subsequent updates can be done wirelessly.)
### Files
```
.
├── common (components, used by the devices)
│   ├── base.yaml
│   ├── battery.yaml
│   ├── bh1750.yaml
│   ├── bme280.yaml
│   ├── bme680.yaml
│   ├── display.yaml
│   ├── sds011.yaml
│   ├── secrets.yaml
│   ├── veml6075.yaml
│   └── experimental (experiments, skip these)
│      ├── ble.yaml
│      └── webserver.yaml
├── custom (used by custom components)
│   └── veml6075_custom_sensor.h
├── fonts (used by display component)
│   ├── materialdesignicons-webfont.ttf
│   ├── slkscr.ttf
│   ├── VeraMono.ttf
│   ├── Vera.ttf
│   └── weather_icon_map.h
├── secrets.yaml (edit example file)
│ (the individial device descriptions)
├── sensor-outdoor.yaml
├── sensor-springer.yaml
├── sensor-sz.yaml
└── sensor-wz.yaml
```bash
git clone https://dev.xd0.de/hendrik/esp32-weatherstation.git
cd esp32-weatherstation
virtualenv .
source bin/activate
pip install -U platformio
pio run
platformio run -t upload && platformio device monitor -b 115200
```
## Authors
@ -125,5 +94,9 @@ This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md
## Acknowledgments
* [ESPHome](https://esphome.io) - ESPHome is a system to control your ESP8266/ESP32 by simple yet powerful configuration files
* [Home Assistant](https://home-assistant.io) - Open source home automation that puts local control and privacy first
* [PlatformIO](https://platformio.org) - Cross-platform IDE
* [GxEPD2](https://github.com/ZinggJM/GxEPD2) - E-Paper display library
* [Adafruit BME680](https://github.com/adafruit/Adafruit_BME680) - BME680 sensor library
* [Nova Fitness SDS dust sensors arduino library](https://github.com/lewapek/sds-dust-sensors-arduino-library.git) - SDS011 Laser dust sensor library
* [BH1750](https://github.com/claws/BH1750/) - BH1750 sensor library
* [VEML6075](https://github.com/adafruit/Adafruit_VEML6075) - VEML6075 sensor library

83
common/base.yaml

@ -1,83 +0,0 @@
esphome:
name: ${node_name}
platform: ESP32
board: lolin_d32_pro
project:
name: "xd0.esp32weatherstation2"
version: "0.1.1"
includes:
- fonts/weather_icon_map.h
- custom/veml6075_custom_sensor.h
libraries:
- Wire
- SPI
- "https://github.com/adafruit/Adafruit_BusIO"
- "https://github.com/adafruit/Adafruit_VEML6075"
# platformio_options:
# lib_ldf_mode: chain+
# Enable logging
logger:
# level: VERY_VERBOSE
# Enable Home Assistant API
api:
ota:
password: !secret esphome_ota_password
wifi:
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_passwd
- ssid: !secret wifi_ssid2
password: !secret wifi_passwd2
power_save_mode: light
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: ${node_name}
password: !secret wifi_captive_portal_password
captive_portal:
time:
- platform: sntp
id: sntp_time
timezone: "Europe/Berlin"
mqtt:
broker: home.xd0.de
username: !secret esphome_mqtt_user
password: !secret esphome_mqtt_passwd
discovery: false
discovery_retain: false
topic_prefix: "thomas"
log_topic:
topic: log"thomas/${node_name}/debug"
level: INFO
# bus
i2c:
sda: 21
scl: 22
scan: true
spi:
clk_pin: 18
mosi_pin: 23
miso_pin: 19
status_led:
pin:
number: GPIO5
inverted: true
sensor:
# WiFi signal strength
- platform: wifi_signal
name: "${node_name} RSSI"
update_interval: 60s
# Example hall sensor
# - platform: esp32_hall
# name: "${node_name} ESP32 Hall Sensor"
# update_interval: 60s

27
common/battery.yaml

@ -1,27 +0,0 @@
sensor:
# Battery level
- platform: adc
pin: _VBAT
name: "${node_name} VBAT Voltage"
id: voltage
attenuation: 11db
filters:
- multiply: 1.71
# filters:
# - lambda: return ( x/4096.0 ) * 2 * 3.42;
update_interval: 60s
- platform: template
name: "${node_name} Battery Level"
id: "battery_level"
unit_of_measurement: '%'
icon: "mdi:battery"
device_class: "battery"
state_class: "measurement"
accuracy_decimals: 1
update_interval: 60s
lambda: |-
return ((id(voltage).state -3) /1.2 * 100.00);
#deep_sleep:
# run_duration: 30s
# sleep_duration: 10min

8
common/bh1750.yaml

@ -1,8 +0,0 @@
# BH1750 Ambient Light Sensor
sensor:
- platform: bh1750
name: "${node_name} Illuminance"
id: lux
address: 0x23
measurement_duration: 69
update_interval: 60s

45
common/bme280.yaml

@ -1,45 +0,0 @@
# BME280 Temperature+Pressure+Humidity Sensor
sensor:
- platform: bme280
temperature:
name: "${node_name} Temperature"
id: temperature
pressure:
name: "${node_name} Pressure"
id: pressure
humidity:
name: "${node_name} Humidity"
id: humidity
address: 0x77
update_interval: 60s
- platform: template
name: "${node_name} Equivalent sea level pressure"
id: pressure_sealevel
lambda: |-
const float STANDARD_ALTITUDE = ${altitude}; // in meters, see note
return id(pressure).state / powf(1 - ((0.0065 * STANDARD_ALTITUDE) /
(id(temperature).state + (0.0065 * STANDARD_ALTITUDE) + 273.15)), 5.257); // in hPa
update_interval: 60s
unit_of_measurement: 'hPa'
- platform: template
name: "${node_name} Absolute Humidity"
lambda: |-
const float mw = 18.01534; // molar mass of water g/mol
const float r = 8.31447215; // Universal gas constant J/mol/K
return (6.112 * powf(2.718281828, (17.67 * id(temperature).state) /
(id(temperature).state + 243.5)) * id(humidity).state * mw) /
((273.15 + id(temperature).state) * r); // in grams/m^3
accuracy_decimals: 2
update_interval: 60s
icon: 'mdi:water'
unit_of_measurement: 'g/m³'
- platform: template
name: "${node_name} Dew Point"
lambda: |-
return (243.5*(log(id(humidity).state/100)+((17.67*id(temperature).state)/
(243.5+id(temperature).state)))/(17.67-log(id(humidity).state/100)-
((17.67*id(temperature).state)/(243.5+id(temperature).state))));
accuracy_decimals: 1
update_interval: 60s
unit_of_measurement: °C
icon: 'mdi:thermometer-alert'

96
common/bme680.yaml

@ -1,96 +0,0 @@
# BME680 Temperature+Pressure+Humidity+Gas Sensor
sensor:
- platform: bme680_bsec
temperature:
name: "${node_name} Temperature"
id: temperature
filters:
- median:
window_size: 12
send_every: 12
pressure:
name: "${node_name} Pressure"
id: pressure
filters:
- median:
window_size: 12
send_every: 12
humidity:
name: "${node_name} Humidity"
id: humidity
filters:
- median:
window_size: 12
send_every: 12
gas_resistance:
name: "${node_name} Gas Resistance"
id: voc
filters:
- median:
window_size: 12
send_every: 12
iaq:
name: "${node_name} IAQ"
id: iaq
filters:
- median:
window_size: 12
send_every: 12
iaq_accuracy:
name: "${node_name} Numeric IAQ Accuracy"
id: iaq_accuracy
filters:
- median:
window_size: 12
send_every: 12
co2_equivalent:
name: "${node_name} CO2 Equivalent"
filters:
- median:
window_size: 12
send_every: 12
breath_voc_equivalent:
name: "${node_name} Breath VOC Equivalent"
filters:
- median:
window_size: 12
send_every: 12
- platform: template
name: "${node_name} Equivalent sea level pressure"
id: pressure_sealevel
lambda: |-
const float STANDARD_ALTITUDE = ${altitude}; // in meters, see note
return id(pressure).state / powf(1 - ((0.0065 * STANDARD_ALTITUDE) /
(id(temperature).state + (0.0065 * STANDARD_ALTITUDE) + 273.15)), 5.257); // in hPa
update_interval: 60s
unit_of_measurement: 'hPa'
- platform: template
name: "${node_name} Absolute Humidity"
lambda: |-
const float mw = 18.01534; // molar mass of water g/mol
const float r = 8.31447215; // Universal gas constant J/mol/K
return (6.112 * powf(2.718281828, (17.67 * id(temperature).state) /
(id(temperature).state + 243.5)) * id(humidity).state * mw) /
((273.15 + id(temperature).state) * r); // in grams/m^3
accuracy_decimals: 2
update_interval: 60s
icon: 'mdi:water'
unit_of_measurement: 'g/m³'
- platform: template
name: "${node_name} Dew Point"
lambda: |-
return (243.5*(log(id(humidity).state/100)+((17.67*id(temperature).state)/
(243.5+id(temperature).state)))/(17.67-log(id(humidity).state/100)-
((17.67*id(temperature).state)/(243.5+id(temperature).state))));
accuracy_decimals: 1
update_interval: 60s
unit_of_measurement: °C
icon: 'mdi:thermometer-alert'
text_sensor:
- platform: bme680_bsec
iaq_accuracy:
name: "${node_name} IAQ Accuracy"
bme680_bsec:
address: 0x77

238
common/display-hendrik.yaml

@ -1,238 +0,0 @@
# display #250x122 pixels (250x128)
font:
- file: 'fonts/Vera.ttf'
id: font1
size: 10
glyphs: '!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/²³µΩ€[]?'
- file: 'fonts/Vera.ttf'
id: font2
size: 20
# - file: 'fonts/VeraMono.ttf'
# id: font3
# size: 10
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font
size: 48
glyphs:
- "\U000F0590" # weather-cloudy
- "\U000F0F2F" # weather-cloudy-alert
- "\U000F0E6E" # weather-cloudy-arrow-right
- "\U000F0591" # weather-fog
- "\U000F0592" # weather-hail
- "\U000F0F30" # weather-hazy
- "\U000F0898" # weather-hurricane
- "\U000F0593" # weather-lightning
- "\U000F067E" # weather-lightning-rainy
- "\U000F0594" # weather-night
- "\U000F0F31" # weather-night-partly-cloudy
- "\U000F0595" # weather-partly-cloudy
- "\U000F0F32" # weather-partly-lightning
- "\U000F0F33" # weather-partly-rainy
- "\U000F0F34" # weather-partly-snowy
- "\U000F0F35" # weather-partly-snowy-rainy
- "\U000F0596" # weather-pouring
- "\U000F0597" # weather-rainy
- "\U000F0598" # weather-snowy
- "\U000F0F36" # weather-snowy-heavy
- "\U000F067F" # weather-snowy-rainy
- "\U000F0599" # weather-sunny
- "\U000F0F37" # weather-sunny-alert
- "\U000F14E4" # weather-sunny-off
- "\U000F059A" # weather-sunset
- "\U000F059B" # weather-sunset-down
- "\U000F059C" # weather-sunset-up
- "\U000F0F38" # weather-tornado
- "\U000F059D" # weather-windy
- "\U000F059E" # weather-windy-variant
display:
- platform: waveshare_epaper
cs_pin: 14
dc_pin: 27
reset_pin: 33
model: 2.13in-ttgo
rotation: 90
update_interval: 180s
full_update_every: 1
id: my_display
pages:
- id: page1
lambda: |-
it.print(10, 50, id(font2), "esp32weatherstation2");
- id: page2
lambda: |-
// Title
it.filled_rectangle(0,0,250,20, COLOR_ON);
it.print(5, 6, id(font1), COLOR_OFF, "${node_name}");
it.strftime(150, 6, id(font1), COLOR_OFF, "%Y-%m-%d %H:%M", id(sntp_time).now());
// Weather icon
if (id(weather_icon).has_state()) {
it.printf(0, 18, id(icon_font), weather_icon_map[id(weather_icon).state.c_str()].c_str());
}
it.printf(55, 25, id(font1), "T: %.1f°C", id(temperature_outside).state);
it.printf(55, 35, id(font1), "H: %.1f%%", id(humidity_outside).state);
it.printf(55, 45, id(font1), "%.0fmm / %.0f%%", id(precipitation).state, id(precipitation_probability).state);
// own measurements
it.filled_rectangle(0, 65, 59, 33, COLOR_ON);
it.printf(2, 65+4, id(font2), COLOR_OFF, "%.1f", id(temperature).state); it.printf(45, 65+4, id(font1), COLOR_OFF, "°C");
it.filled_rectangle(60, 65, 59, 33, COLOR_ON);
it.printf(62, 65+4, id(font2), COLOR_OFF, "%.1f", id(humidity).state); it.printf(105, 65+4, id(font1), COLOR_OFF, "%%");
it.filled_rectangle(120, 65, 59, 33, COLOR_ON);
it.printf(122, 65+4, id(font2), COLOR_OFF, "%.0f", id(pressure_sealevel).state); it.printf(178, 87, id(font1), COLOR_OFF, TextAlign::TOP_RIGHT, "hPa");
// humidity human readable
if (id(humidity).state < 30) {
it.printf(116, 87, id(font1), COLOR_OFF, TextAlign::TOP_RIGHT, "low");
} else if (id(humidity).state < 60) {
it.printf(116, 87, id(font1), COLOR_OFF, TextAlign::TOP_RIGHT, "comfort");
} else {
it.printf(116, 87, id(font1), COLOR_OFF, TextAlign::TOP_RIGHT, "high");
}
// graphs
it.graph(0, 98, id(temperature_graph));
it.graph(60, 98, id(humidity_graph));
it.graph(120, 98, id(pressure_graph));
// voc
it.circle(130, 42, 20);
it.line(115, 42, 145, 42);
it.filled_rectangle(130-10, 21, 20, 10, COLOR_OFF);
it.print(130, 26, id(font1), TextAlign::CENTER, "VOC");
if (id(iaq_accuracy).state >= 2) {
it.printf(130, 41, id(font1), TextAlign::BASELINE_CENTER, "%.0f", id(iaq).state);
if (id(iaq).state < 50) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "excellent");
} else if (id(iaq).state < 100) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "fine");
} else if (id(iaq).state < 150) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "moderate");
} else if (id(iaq).state < 200) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "poor");
} else if (id(iaq).state < 300) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "very poor");
} else if (id(iaq).state < 500) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "severe");
}
} else {
it.printf(130, 41, id(font1), TextAlign::BASELINE_CENTER, "%.0f \u03A9", id(voc).state);
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "VOC");
}
// climate.wz
it.circle(175, 42, 20);
it.line(160, 42, 190, 42);
it.filled_rectangle(175-10, 21, 20, 10, COLOR_OFF);
it.print(175, 26, id(font1), TextAlign::CENTER, "WZ");
it.printf(175, 41, id(font1), TextAlign::BASELINE_CENTER, "%.1f°C", id(temp_set_wz).state);
it.printf(175, 43, id(font1), TextAlign::TOP_CENTER, "%.1f°C", id(temp_wz).state);
// Benzinpreis
it.rectangle(200, 22, 40, 40);
it.line(205, 42, 235, 42);
it.filled_rectangle(220-10, 21, 20, 10, COLOR_OFF);
it.print(220, 26, id(font1), TextAlign::CENTER, "E5");
it.printf(220, 41, id(font1), TextAlign::BASELINE_CENTER, "%.3f€", id(benzinpreis).state);
it.printf(220, 43, id(font1), TextAlign::TOP_CENTER, "%.3f€", id(benzinpreis2).state);
// other values
it.print(184, 75, id(font1), "UVI:"); it.printf(248, 75, id(font1), TextAlign::TOP_RIGHT, "%.2f", id(uvi_outdoor).state);
it.print(184, 85, id(font1), "YNH:"); it.printf(248, 85, id(font1), TextAlign::TOP_RIGHT, "%.2f", id(yunohost_load).state);
// Springer
it.print(184, 105, id(font1), "Bat:");
if (id(battery_level).state >=0 && id(battery_level).state <= 100) {
it.printf(248, 105, id(font1), TextAlign::TOP_RIGHT, "%.0f%%", id(battery_level).state);
} else {
it.printf(248, 105, id(font1), TextAlign::TOP_RIGHT, "nc");
}
interval:
- interval: 30s
then:
- if:
condition:
- display.is_displaying_page: page1
then:
- display.page.show: page2
- component.update: my_display
graph:
# Show bare-minimum auto-ranged graph
- id: temperature_graph
duration: 1h
width: 59
height: 31
traces:
- sensor: temperature
line_type: SOLID
line_thickness: 2
# - sensor: temperature_outside
# line_type: DASHED
# line_thickness: 1
- id: humidity_graph
sensor: humidity
duration: 1h
width: 59
height: 31
- id: pressure_graph
sensor: pressure
duration: 1h
width: 59
height: 31
# Example configuration entry
sensor:
- platform: homeassistant
name: "Temperature Outdoor"
internal: true
entity_id: weather.openweathermap
attribute: temperature
id: temperature_outside
- platform: homeassistant
name: "Humidity Outdoor"
internal: true
entity_id: weather.openweathermap
attribute: humidity
id: humidity_outside
- platform: homeassistant
name: "Precipitation Propability Forecast"
internal: true
entity_id: sensor.openweathermap_forecast_precipitation
id: precipitation
- platform: homeassistant
name: "Precipitation Probability Forecast"
internal: true
entity_id: sensor.openweathermap_forecast_precipitation_probability
id: precipitation_probability
- platform: homeassistant
name: "UVI Outdoor"
internal: true
entity_id: sensor.openweathermap_uv_index
id: uvi_outdoor
- platform: homeassistant
name: "WZ Temp"
internal: true
entity_id: climate.leq1333417
attribute: current_temperature
id: temp_wz
- platform: homeassistant
name: "WZ Temp Set"
internal: true
entity_id: climate.leq1333417
attribute: temperature
id: temp_set_wz
- platform: homeassistant
name: "Benzinpreis"
internal: true
entity_id: sensor.tankerkoenig_sb_essen_frankenstrasse_74_e5
id: benzinpreis
- platform: homeassistant
name: "Benzinpreis2"
internal: true
entity_id: sensor.tankerkoenig_total_essen_e5
id: benzinpreis2
- platform: homeassistant
name: "Yunohost Load"
internal: true
entity_id: sensor.yunohost_load
id: yunohost_load
text_sensor:
- platform: homeassistant
id: weather_icon
internal: true
entity_id: weather.openweathermap

294
common/display.yaml

@ -1,294 +0,0 @@
# display #250x122 pixels (250x128)
font:
- file: 'fonts/Vera.ttf'
id: font1
size: 10
glyphs: '!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/²³µΩ€[]?'
- file: 'fonts/Vera.ttf'
id: font2
size: 20
# - file: 'fonts/VeraMono.ttf'
# id: font3
# size: 10
- file: 'fonts/materialdesignicons-webfont.ttf'
id: icon_font
size: 48
glyphs:
- "\U000F0590" # weather-cloudy
- "\U000F0F2F" # weather-cloudy-alert
- "\U000F0E6E" # weather-cloudy-arrow-right
- "\U000F0591" # weather-fog
- "\U000F0592" # weather-hail
- "\U000F0F30" # weather-hazy
- "\U000F0898" # weather-hurricane
- "\U000F0593" # weather-lightning
- "\U000F067E" # weather-lightning-rainy
- "\U000F0594" # weather-night
- "\U000F0F31" # weather-night-partly-cloudy
- "\U000F0595" # weather-partly-cloudy
- "\U000F0F32" # weather-partly-lightning
- "\U000F0F33" # weather-partly-rainy
- "\U000F0F34" # weather-partly-snowy
- "\U000F0F35" # weather-partly-snowy-rainy
- "\U000F0596" # weather-pouring
- "\U000F0597" # weather-rainy
- "\U000F0598" # weather-snowy
- "\U000F0F36" # weather-snowy-heavy
- "\U000F067F" # weather-snowy-rainy
- "\U000F0599" # weather-sunny
- "\U000F0F37" # weather-sunny-alert
- "\U000F14E4" # weather-sunny-off
- "\U000F059A" # weather-sunset
- "\U000F059B" # weather-sunset-down
- "\U000F059C" # weather-sunset-up
- "\U000F0F38" # weather-tornado
- "\U000F059D" # weather-windy
- "\U000F059E" # weather-windy-variant
display:
- platform: waveshare_epaper
cs_pin: 14
dc_pin: 27
reset_pin: 33
model: 2.13in-ttgo
rotation: 90
update_interval: 180s
full_update_every: 1
id: my_display
pages:
- id: page1
lambda: |-
it.print(10, 50, id(font2), "esp32weatherstation2");
- id: page2
lambda: |-
// Title
it.filled_rectangle(0,0,250,20, COLOR_ON);
it.print(5, 6, id(font1), COLOR_OFF, "${node_name}");
it.strftime(150, 6, id(font1), COLOR_OFF, "%Y-%m-%d %H:%M", id(sntp_time).now());
// Weather icon
if (id(weather_icon).has_state()) {
it.printf(0, 18, id(icon_font), weather_icon_map[id(weather_icon).state.c_str()].c_str());
}
it.printf(55, 25, id(font1), "T: %.1f°C", id(temperature_outside).state);
it.printf(55, 35, id(font1), "H: %.1f%%", id(humidity_outside).state);
it.printf(55, 45, id(font1), "%s", id(weather_icon).state.c_str());
// own measurements
it.filled_rectangle(0, 65, 59, 33, COLOR_ON);
it.printf(2, 65+4, id(font2), COLOR_OFF, "%.1f", id(temperature).state); it.printf(45, 65+4, id(font1), COLOR_OFF, "°C");
it.filled_rectangle(60, 65, 59, 33, COLOR_ON);
it.printf(62, 65+4, id(font2), COLOR_OFF, "%.1f", id(humidity).state); it.printf(105, 65+4, id(font1), COLOR_OFF, "%%");
it.filled_rectangle(120, 65, 59, 33, COLOR_ON);
it.printf(122, 65+4, id(font2), COLOR_OFF, "%.0f", id(pressure_sealevel).state); it.printf(178, 87, id(font1), COLOR_OFF, TextAlign::TOP_RIGHT, "hPa");
// humidity human readable
if (id(humidity).state < 30) {
it.printf(116, 87, id(font1), COLOR_OFF, TextAlign::TOP_RIGHT, "low");
} else if (id(humidity).state < 60) {
it.printf(116, 87, id(font1), COLOR_OFF, TextAlign::TOP_RIGHT, "comfort");
} else {
it.printf(116, 87, id(font1), COLOR_OFF, TextAlign::TOP_RIGHT, "high");
}
// graphs
it.graph(0, 98, id(temperature_graph));
it.graph(60, 98, id(humidity_graph));
it.graph(120, 98, id(pressure_graph));
// voc
it.circle(130, 42, 20);
it.line(115, 42, 145, 42);
it.filled_rectangle(130-10, 21, 20, 10, COLOR_OFF);
it.print(130, 26, id(font1), TextAlign::CENTER, "WZ");
if (id(iaq_accuracy_wz).state >= 2) {
it.printf(130, 41, id(font1), TextAlign::BASELINE_CENTER, "%.0f", id(iaq_wz).state);
if (id(iaq_wz).state < 50) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "excellent");
} else if (id(iaq_wz).state < 100) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "fine");
} else if (id(iaq_wz).state < 150) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "moderate");
} else if (id(iaq_wz).state < 200) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "poor");
} else if (id(iaq_wz).state < 300) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "very poor");
} else if (id(iaq_wz).state < 500) {
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "severe");
}
} else {
it.printf(130, 41, id(font1), TextAlign::BASELINE_CENTER, "%.0f \u03A9", id(voc_wz).state);
it.printf(130, 43, id(font1), TextAlign::TOP_CENTER, "VOC");
}
// voc2
it.circle(175, 42, 20);
it.line(160, 42, 190, 42);
it.filled_rectangle(175-10, 21, 20, 10, COLOR_OFF);
it.print(175, 26, id(font1), TextAlign::CENTER, "SZ");
if (id(iaq_accuracy_sz).state >= 2) {
it.printf(175, 41, id(font1), TextAlign::BASELINE_CENTER, "%.0f", id(iaq_sz).state);
if (id(iaq_sz).state < 50) {
it.printf(175, 43, id(font1), TextAlign::TOP_CENTER, "excellent");
} else if (id(iaq_sz).state < 100) {
it.printf(175, 43, id(font1), TextAlign::TOP_CENTER, "fine");
} else if (id(iaq_sz).state < 150) {
it.printf(175, 43, id(font1), TextAlign::TOP_CENTER, "moderate");
} else if (id(iaq_sz).state < 200) {
it.printf(175, 43, id(font1), TextAlign::TOP_CENTER, "poor");
} else if (id(iaq_sz).state < 300) {
it.printf(175, 43, id(font1), TextAlign::TOP_CENTER, "very poor");
} else if (id(iaq_sz).state < 500) {
it.printf(175, 43, id(font1), TextAlign::TOP_CENTER, "severe");
}
} else {
it.printf(175, 41, id(font1), TextAlign::BASELINE_CENTER, "%.0f \u03A9", id(voc_sz).state);
it.printf(175, 43, id(font1), TextAlign::TOP_CENTER, "VOC");
}
// Particulate Matter Sensor
it.rectangle(200, 22, 40, 40); // it.circle(220, 42, 20);
it.line(205, 42, 235, 42);
it.filled_rectangle(220-10, 21, 20, 10, COLOR_OFF);
it.print(220, 26, id(font1), TextAlign::CENTER, "PM");
it.printf(220, 41, id(font1), TextAlign::BASELINE_CENTER, "%.0f", id(pm2_outdoor).state);
it.printf(220, 43, id(font1), TextAlign::TOP_CENTER, "%.0f", id(pm10_outdoor).state);
// other values
it.print(184, 65, id(font1), "Lux:"); it.printf(248, 65, id(font1), TextAlign::TOP_RIGHT, "%.0f lx", id(lux_outdoor).state);
it.print(184, 75, id(font1), "UVI:"); it.printf(248, 75, id(font1), TextAlign::TOP_RIGHT, "%.0f", id(uvi_outdoor).state);
it.print(184, 85, id(font1), "UVA:"); it.printf(248, 85, id(font1), TextAlign::TOP_RIGHT, "%.0f", id(uva_outdoor).state);
it.print(184, 95, id(font1), "UVB:"); it.printf(248, 95, id(font1), TextAlign::TOP_RIGHT, "%.0f", id(uvb_outdoor).state);
// Springer
it.print(184, 105, id(font1), "Springer");
if (id(battery_springer).state >=0 && id(battery_springer).state <= 100) {
it.printf(248, 105, id(font1), TextAlign::TOP_RIGHT, "%.0f%%", id(battery_springer).state);
} else {
it.printf(248, 105, id(font1), TextAlign::TOP_RIGHT, "nc");
}
it.printf(184,115, id(font1), "%.1f°C, %.1f%%H, %.0fhPa", id(temperature_springer).state, id(humidity_springer).state, id(pressure_springer).state);
interval:
- interval: 30s
then:
- if:
condition:
- display.is_displaying_page: page1
then:
- display.page.show: page2
- component.update: my_display
graph:
# Show bare-minimum auto-ranged graph
- id: temperature_graph
duration: 1h
width: 59
height: 31
traces:
- sensor: temperature
line_type: SOLID
line_thickness: 2
# - sensor: temperature_outside
# line_type: DASHED
# line_thickness: 1
- id: humidity_graph
sensor: humidity
duration: 1h
width: 59
height: 31
- id: pressure_graph
sensor: pressure
duration: 1h
width: 59
height: 31
# Example configuration entry
sensor:
- platform: homeassistant
name: "Temperature Outdoor"
internal: true
entity_id: sensor.sensor_outdoor_temperature
id: temperature_outside
- platform: homeassistant
name: "Humidity Outdoor"
internal: true
entity_id: sensor.sensor_outdoor_humidity
id: humidity_outside
- platform: homeassistant
name: "WZ VOC"
internal: true
entity_id: sensor.sensor_wz_gas_resistance
id: voc_wz
- platform: homeassistant
name: "SZ VOC"
internal: true
entity_id: sensor.sensor_sz_gas_resistance
id: voc_sz
- platform: homeassistant
name: "WZ IAQ"
internal: true
entity_id: sensor.sensor_wz_iaq
id: iaq_wz
- platform: homeassistant
name: "SZ IAQ"
internal: true
entity_id: sensor.sensor_sz_iaq
id: iaq_sz
- platform: homeassistant
name: "WZ IAQ Accuracy"
internal: true
entity_id: sensor.sensor_wz_iaq_accuracy
id: iaq_accuracy_wz
- platform: homeassistant
name: "SZ IAQ Accuracy"
internal: true
entity_id: sensor.sensor_sz_iaq_accuracy
id: iaq_accuracy_sz
- platform: homeassistant
name: "PM2.5 Outdoor"
internal: true
entity_id: sensor.sensor_outdoor_particulate_matter_2_5um_concentration
id: pm2_outdoor
- platform: homeassistant
name: "PM10 Outdoor"
internal: true
entity_id: sensor.sensor_outdoor_particulate_matter_10_0um_concentration
id: pm10_outdoor
- platform: homeassistant
name: "UVI Outdoor"
internal: true
entity_id: sensor.sensor_outdoor_uvi
id: uvi_outdoor
- platform: homeassistant
name: "UVA Outdoor"
internal: true
entity_id: sensor.sensor_outdoor_uva
id: uva_outdoor
- platform: homeassistant
name: "UVB Outdoor"
internal: true
entity_id: sensor.sensor_outdoor_uvb
id: uvb_outdoor
- platform: homeassistant
name: "Illuminance Outdoor"
internal: true
entity_id: sensor.sensor_outdoor_illuminance
id: lux_outdoor
- platform: homeassistant
name: "Temperature Springer"
internal: true
entity_id: sensor.sensor_springer_temperature
id: temperature_springer
- platform: homeassistant
name: "Humidity Springer"
internal: true
entity_id: sensor.sensor_springer_humidity
id: humidity_springer
- platform: homeassistant
name: "Pressure Springer"
internal: true
entity_id: sensor.sensor_springer_equivalent_sea_level_pressure
id: pressure_springer
- platform: homeassistant
name: "Battery Springer"
internal: true
entity_id: sensor.sensor_springer_battery_level
id: battery_springer
text_sensor:
- platform: homeassistant
id: weather_icon
internal: true
entity_id: weather.openweathermap

8
common/experimental/ble.yaml

@ -1,8 +0,0 @@
# Bluetooth Low-Energy
# Example configuration entry
esp32_ble_tracker:
sensor:
- platform: ble_rssi
mac_address: FA:4E:84:FF:4D:16
name: "${node_name} MiBand Hendrik RSSI value"

2
common/experimental/webserver.yaml

@ -1,2 +0,0 @@
web_server:
port: 80

15
common/sds011.yaml

@ -1,15 +0,0 @@
uart:
rx_pin: 15
tx_pin: 2
baud_rate: 9600
# SDS 011 Particulate Matter Sensor
sensor:
- platform: sds011
pm_2_5:
name: "${node_name} Particulate Matter <2.5µm Concentration"
id: pm2
pm_10_0:
name: "${node_name} Particulate Matter <10.0µm Concentration"
id: pm10
update_interval: 10min

4
common/secrets.yaml

@ -1,4 +0,0 @@
<<: !include ../secrets.yaml
# You can also use Home Assistant secrets.yaml:
# <<: !include ../../secrets.yaml

19
common/veml6075.yaml

@ -1,19 +0,0 @@
# VEML6075 UV sensor
sensor:
- platform: custom
lambda: |-
auto veml6075 = new VEML6075CustomSensor();
App.register_component(veml6075);
return {veml6075->uva_sensor, veml6075->uvb_sensor, veml6075->uvi_sensor};
sensors:
- name: "${node_name} UVA"
id: uva
unit_of_measurement: "mW/cm²"
accuracy_decimals: 0
- name: "${node_name} UVB"
id: uvb
unit_of_measurement: "mW/cm²"
accuracy_decimals: 0
- name: "${node_name} UVI"
id: uvi
accuracy_decimals: 0

43
custom/veml6075_custom_sensor.h

@ -1,43 +0,0 @@
#include "esphome.h"
#include <Wire.h>
#include "Adafruit_VEML6075.h"
class VEML6075CustomSensor : public PollingComponent {
public:
Adafruit_VEML6075 uv = Adafruit_VEML6075();
Sensor *uva_sensor = new Sensor();
Sensor *uvb_sensor = new Sensor();
Sensor *uvi_sensor = new Sensor();
VEML6075CustomSensor() : PollingComponent(15000) {}
float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; }
void setup() override {
//Wire.begin(21, 22);
if (!uv.begin()) {
ESP_LOGE("custom", "VEML6075 init failed");
this->mark_failed();
return;
}
uv.setIntegrationTime(VEML6075_100MS);
//uv.setHighDynamic(true);
// Set the calibration coefficients
uv.setCoefficients(2.22, 1.33, // UVA_A and UVA_B coefficients
2.95, 1.74, // UVB_C and UVB_D coefficients
0.001461, 0.002591); // UVA and UVB responses
}
void update() override {
float uva = uv.readUVA();
float uvb = uv.readUVB();
float uvi = uv.readUVI();
ESP_LOGD("custom", "The value of sensor uva is: %.0f", uva);
ESP_LOGD("custom", "The value of sensor uvb is: %.0f", uvb);
uva_sensor->publish_state(uva);
uvb_sensor->publish_state(uvb);
uvi_sensor->publish_state(uvi);
}
};

BIN
fonts/Vera.ttf

Binary file not shown.

BIN
fonts/VeraMono.ttf

Binary file not shown.

BIN
fonts/materialdesignicons-webfont.ttf

Binary file not shown.

BIN
fonts/slkscr.ttf

Binary file not shown.

37
fonts/weather_icon_map.h

@ -1,37 +0,0 @@
#include <map>
std::map<std::string, std::string> weather_icon_map
{
{"cloudy", "\U000F0590"},
{"cloudy-alert", "\U000F0F2F"},
{"cloudy-arrow-right", "\U000F0E6E"},
{"fog", "\U000F0591"},
{"hail", "\U000F0592"},
{"hazy", "\U000F0F30"},
{"hurricane", "\U000F0898"},
{"lightning", "\U000F0593"},
{"lightning-rainy", "\U000F067E"},
{"night", "\U000F0594"},
{"clear-night", "\U000F0594"},
{"night-partly-cloudy", "\U000F0F31"},
{"partly-cloudy", "\U000F0595"},
{"partlycloudy", "\U000F0595"},
{"partly-lightning", "\U000F0F32"},
{"partly-rainy", "\U000F0F33"},
{"partly-snowy", "\U000F0F34"},
{"partly-snowy-rainy", "\U000F0F35"},
{"pouring", "\U000F0596"},
{"rainy", "\U000F0597"},
{"snowy", "\U000F0598"},
{"snowy-heavy", "\U000F0F36"},
{"snowy-rainy", "\U000F067F"},
{"sunny", "\U000F0599"},
{"sunny-alert", "\U000F0F37"},
{"sunny-off", "\U000F14E4"},
{"sunset", "\U000F059A"},
{"sunset-down", "\U000F059B"},
{"sunset-up", "\U000F059C"},
{"tornado", "\U000F0F38"},
{"windy", "\U000F059D"},
{"windy-variant", "\U000F059E"},
{"exceptional", "\U000F0599"},
};

15
hendrik-test.yaml

@ -1,15 +0,0 @@
substitutions:
node_name: hendrik-test
altitude: "111"
packages:
base: !include common/base.yaml
battery: !include common/battery.yaml
display: !include common/display-hendrik.yaml
webserver: !include common/experimental/webserver.yaml
env_sensor: !include common/bme680.yaml
# remote_package:
# url: https://dev.xd0.de/hendrik/esp32-weatherstation
# ref: esphome
# files: [common/base.yaml, common/bme680.yaml]
# refresh: 1d

57
platformio.ini

@ -0,0 +1,57 @@
;PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:lolin_d32_pro]
platform = espressif32
;platform = https://github.com/platformio/platform-espressif32.git
framework = arduino
board = lolin_d32_pro
;powersave
board_build.f_cpu = 160000000L
build_flags =
-DLOG_DEFAULT_LEVEL=ESP_LOG_VERBOSE
-DLOG_LOCAL_LEVEL=ESP_LOG_VERBOSE
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE
; -DBOARD_HAS_PSRAM
; -mfix-esp32-psram-cache-issue
-DWIFI_SSID="\"${sysenv.WIFI_SSID}\""
-DWIFI_PASSWD="\"${sysenv.WIFI_PASSWD}\""
-DWIFI_SSID2="\"${sysenv.WIFI_SSID2}\""
-DWIFI_PASSWD2="\"${sysenv.WIFI_PASSWD2}\""
-DWIFI_SSID3="\"${sysenv.WIFI_SSID3}\""
-DWIFI_PASSWD3="\"${sysenv.WIFI_PASSWD3}\""
-DMQTT_BROKER_URI="\"${sysenv.MQTT_BROKER_URI}\""
-DMQTT_USERNAME="\"${sysenv.MQTT_USERNAME}\""
-DMQTT_PASSWORD="\"${sysenv.MQTT_PASSWORD}\""
-DARDUINO_SAMD_VARIANT_COMPLIANCE
lib_deps =
GxEPD2
; https://github.com/wemos/LOLIN_EPD_Library
Adafruit GFX Library
Adafruit BME280 Library
Adafruit BME680 Library
Adafruit Unified Sensor
Adafruit BusIO
Adafruit VEML6075 Library
BH1750
; SDS011 sensor Library
; Nova Fitness Sds dust sensors library
https://github.com/lewapek/sds-dust-sensors-arduino-library.git
; ArduinoJSON
monitor_speed = 115200
upload_speed = 115200
extra_scripts =
pre:script/autoversioning.py
pre:lib/script/autoversioning.py

13
script/autoversioning.py

@ -0,0 +1,13 @@
Import("env")
import time
ver = int( time.time() )
f = open("version.txt", "w")
f.write(str(ver))
f.close()
env.Append(CPPDEFINES=[
("FW_VERSION", ver)
])

10
secrets-example.yaml

@ -1,10 +0,0 @@
# esphome secrets
wifi_ssid: ssid1
wifi_passwd: password1
wifi_ssid2: ssid2
wifi_passwd2: password2
esphome_ota_password: "1234567890abcdef1234567890abcdef"
wifi_captive_portal_password: "123456789abc"
esphome_mqtt_user: "mqtt-user"
esphome_mqtt_passwd: "mqtt-password"

11
sensor-outdoor.yaml

@ -1,11 +0,0 @@
substitutions:
node_name: sensor-outdoor
altitude: "30"
packages:
base: !include common/base.yaml
battery: !include common/battery.yaml
env_sensor: !include common/bme280.yaml
particulate: !include common/sds011.yaml
light: !include common/bh1750.yaml
uv: !include common/veml6075.yaml

9
sensor-springer.yaml

@ -1,9 +0,0 @@
substitutions:
node_name: sensor-springer
altitude: "30"
packages:
base: !include common/base.yaml
battery: !include common/battery.yaml
env_sensor: !include common/bme280.yaml

8
sensor-sz.yaml

@ -1,8 +0,0 @@
substitutions:
node_name: sensor-sz
altitude: "30"
packages:
base: !include common/base.yaml
env_sensor: !include common/bme680.yaml
display: !include common/display.yaml

8
sensor-wz.yaml

@ -1,8 +0,0 @@
substitutions:
node_name: sensor-wz
altitude: "30"
packages:
base: !include common/base.yaml
env_sensor: !include common/bme680.yaml
display: !include common/display.yaml

314
src/IR_RMT.cpp

@ -0,0 +1,314 @@
// https://github.com/espressif/esp-idf/blob/master/examples/peripherals/rmt_nec_tx_rx/main/infrared_nec_main.c
#include <Arduino.h>
#include <driver/rmt.h>
#include "IR_RMT.h"
static constexpr uint8_t CLK_DIV = 100; // Clock divisor (base clock is 80MHz)
static constexpr uint16_t TICK_10_US = (80000000 / CLK_DIV / 100000); // Number of clock ticks that represent 10us. 10 us = 1/100th msec.
static constexpr uint8_t NEC_DATA_ITEM_NUM = 34; // NEC code item number: header + 32bit data + end
#define NEC_HEADER_HIGH_US 9000 /*!< NEC protocol header: positive 9ms */
#define NEC_HEADER_LOW_US 4500 /*!< NEC protocol header: negative 4.5ms*/
#define NEC_BIT_ONE_HIGH_US 560 /*!< NEC protocol data bit 1: positive 0.56ms */
#define NEC_BIT_ONE_LOW_US (2250-NEC_BIT_ONE_HIGH_US) /*!< NEC protocol data bit 1: negative 1.69ms */
#define NEC_BIT_ZERO_HIGH_US 560 /*!< NEC protocol data bit 0: positive 0.56ms */
#define NEC_BIT_ZERO_LOW_US (1120-NEC_BIT_ZERO_HIGH_US) /*!< NEC protocol data bit 0: negative 0.56ms */
#define NEC_BIT_END 560 /*!< NEC protocol end: positive 0.56ms */
#define NEC_REPEAT_LOW_US 2250 /*!< NEC repeat header: negative 2.25ms*/
#define NEC_BIT_REPEAT_HIGH_US 560
#define NEC_BIT_MARGIN 40 /*!< NEC parse margin time */
namespace {
bool isInRange(rmt_item32_t item, int lowDuration, int highDuration, int tolerance) {
uint32_t lowValue = item.duration0 * 10 / TICK_10_US;
uint32_t highValue = item.duration1 * 10 / TICK_10_US;
/*
ESP_LOGD(tag, "lowValue=%d, highValue=%d, lowDuration=%d, highDuration=%d",
lowValue, highValue, lowDuration, highDuration);
*/
if (lowValue < (lowDuration - tolerance) || lowValue > (lowDuration + tolerance) ||
(highValue != 0 &&
(highValue < (highDuration - tolerance) || highValue > (highDuration + tolerance)))) {
return false;
}
return true;
}
bool NEC_is0(rmt_item32_t item) {
return isInRange(item, NEC_BIT_ZERO_HIGH_US, NEC_BIT_ZERO_LOW_US, NEC_BIT_MARGIN);
}
bool NEC_is1(rmt_item32_t item) {
return isInRange(item, NEC_BIT_ONE_HIGH_US, NEC_BIT_ONE_LOW_US, NEC_BIT_MARGIN);
}
ir_data_t decodeNEC(rmt_item32_t *data, int numItems) {
ir_data_t irData = {0,0};
// check for repeat
if (numItems == 2) {
if (isInRange(data[0], NEC_HEADER_HIGH_US, NEC_REPEAT_LOW_US, NEC_BIT_MARGIN)) {
Serial.println("repeat header");
if (isInRange(data[1], NEC_BIT_REPEAT_HIGH_US, 0, NEC_BIT_MARGIN)) {
irData = {REPEAT, REPEAT};
return irData;
}
}
}
if (!isInRange(data[0], NEC_HEADER_HIGH_US, NEC_HEADER_LOW_US, NEC_BIT_MARGIN) || numItems != 34) {
ESP_LOGD(tag, "Not an NEC");
irData = {INVALID_PROTOCOL, INVALID_PROTOCOL};
return irData; // 0
}
int i;
uint8_t address = 0, notAddress = 0, command = 0, notCommand = 0;
int accumCounter = 0;
uint8_t accumValue = 0;
for (i=1; i<numItems; i++) {
if (NEC_is0(data[i])) {
ESP_LOGD(tag, "%d: 0", i);
accumValue = accumValue >> 1;
} else if (NEC_is1(data[i])) {
ESP_LOGD(tag, "%d: 1", i);
accumValue = (accumValue >> 1) | 0x80;
} else {
ESP_LOGD(tag, "Unknown");
}
if (accumCounter == 7) {
accumCounter = 0;
ESP_LOGD(tag, "Byte: 0x%.2x", accumValue);
if (i==8) {
address = accumValue;
} else if (i==16) {
notAddress = accumValue;
} else if (i==24) {
command = accumValue;
} else if (i==32) {
notCommand = accumValue;
}
accumValue = 0;
} else {
accumCounter++;
}
}
ESP_LOGD(tag, "Address: 0x%.2x, NotAddress: 0x%.2x", address, notAddress ^ 0xff);
if (address != (notAddress ^ 0xff) || command != (notCommand ^ 0xff)) {
ESP_LOGD(tag, "Data mis match");
irData = {INVALID_CHECKSUM, INVALID_CHECKSUM};
return irData; // 0
}
ESP_LOGD(tag, "Address: 0x%.2x, Command: 0x%.2x", address, command);
irData.address = address;
irData.command = command;
return irData;
}
} /* anonymous namespace */
/*
* @brief Build register value of waveform for NEC one data bit
*/
inline void IR_RMT::nec_fill_item_level(rmt_item32_t* item, int high_us, int low_us)
{
item->level0 = inverted ? 0 : 1;
item->duration0 = (high_us) / 10 * TICK_10_US;
item->level1 = inverted ? 1 : 0;
item->duration1 = (low_us) / 10 * TICK_10_US;
}
/*
* @brief Generate NEC header value: active 9ms + negative 4.5ms
*/
void IR_RMT::nec_fill_item_header(rmt_item32_t* item)
{
nec_fill_item_level(item, NEC_HEADER_HIGH_US, NEC_HEADER_LOW_US);
}
/*
* @brief Generate NEC data bit 1: positive 0.56ms + negative 1.69ms
*/
void IR_RMT::nec_fill_item_bit_one(rmt_item32_t* item)
{
nec_fill_item_level(item, NEC_BIT_ONE_HIGH_US, NEC_BIT_ONE_LOW_US);
}
/*
* @brief Generate NEC data bit 0: positive 0.56ms + negative 0.56ms
*/
void IR_RMT::nec_fill_item_bit_zero(rmt_item32_t* item)
{
nec_fill_item_level(item, NEC_BIT_ZERO_HIGH_US, NEC_BIT_ZERO_LOW_US);
}
/*
* @brief Generate NEC end signal: positive 0.56ms
*/
void IR_RMT::nec_fill_item_end(rmt_item32_t* item)
{
nec_fill_item_level(item, NEC_BIT_END, 0x7fff);
}
/*
* @brief Build NEC 32bit waveform.
*/
void IR_RMT::nec_build_items(rmt_item32_t* item, uint16_t addr, uint16_t cmd_data)
{
nec_fill_item_header(item++);
for(int j = 0; j < 16; j++) {
if(addr & 0x1) {
nec_fill_item_bit_one(item);
} else {
nec_fill_item_bit_zero(item);
}
item++;
addr >>= 1;
}
for(int j = 0; j < 16; j++) {
if(cmd_data & 0x1) {
nec_fill_item_bit_one(item);
} else {
nec_fill_item_bit_zero(item);
}
item++;
cmd_data >>= 1;
}
nec_fill_item_end(item);
}
bool IR_RMT::send(uint16_t address, uint16_t command) {
int item_num = NEC_DATA_ITEM_NUM;
size_t size = (sizeof(rmt_item32_t) * item_num);
rmt_item32_t* item = (rmt_item32_t*)heap_caps_malloc(size, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
memset((void*) item, 0, size);
nec_build_items(item, ((~address) << 8) | address, ((~command) << 8) | command);
//To send data according to the waveform items.
rmt_write_items(tx_channel, item, item_num, true);
//Wait until sending is done.
//rmt_wait_tx_done(tx_channel, portMAX_DELAY);
//before we free the data, make sure sending is already done.
free(item);
return true;
}
bool IR_RMT::sendNEC32(uint32_t data) {
int item_num = NEC_DATA_ITEM_NUM;
size_t size = (sizeof(rmt_item32_t) * item_num);
rmt_item32_t* item = (rmt_item32_t*)heap_caps_malloc(size, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
memset((void*) item, 0, size);
nec_build_items(item, (data >> 16), data);
//To send data according to the waveform items.
rmt_write_items(tx_channel, item, item_num, true);
//Wait until sending is done.
//rmt_wait_tx_done(tx_channel, portMAX_DELAY);
//before we free the data, make sure sending is already done.
free(item);
return true;
}
IR_RMT::IR_RMT(gpio_num_t rx_pin, gpio_num_t tx_pin, bool inverted) {
this->rx_pin = rx_pin;
this->tx_pin = tx_pin;
this->rx_channel = RMT_CHANNEL_7;
this->tx_channel = RMT_CHANNEL_6;
this->running = false;
this->inverted = inverted;
}
bool IR_RMT::begin() {
if (rx_pin >= 0) {
rmt_config_t config;
config.rmt_mode = RMT_MODE_RX;
config.channel = rx_channel;
config.gpio_num = rx_pin;
config.mem_block_num = 1;
config.rx_config.filter_en = true;
config.rx_config.filter_ticks_thresh = 100; // 80000000/100 -> 800000 / 100 = 8000 = 125us
config.rx_config.idle_threshold = TICK_10_US * 100 * 20;
config.clk_div = CLK_DIV;
ESP_ERROR_CHECK(rmt_config(&config));
ESP_ERROR_CHECK(rmt_driver_install(config.channel, 1000, 0));
rmt_get_ringbuf_handle(rx_channel, &ringBuf);
rmt_rx_start(rx_channel, 1);
}
if (tx_pin >= 0) {
rmt_config_t rmt_tx;
rmt_tx.rmt_mode = RMT_MODE_TX;
rmt_tx.channel = tx_channel;
rmt_tx.gpio_num = tx_pin;
rmt_tx.mem_block_num = 1;
rmt_tx.clk_div = CLK_DIV;
rmt_tx.tx_config.loop_en = false;
rmt_tx.tx_config.carrier_duty_percent = 50;
rmt_tx.tx_config.carrier_freq_hz = 38000;
rmt_tx.tx_config.carrier_level = inverted ? RMT_CARRIER_LEVEL_LOW : RMT_CARRIER_LEVEL_HIGH;
rmt_tx.tx_config.carrier_en = true;
rmt_tx.tx_config.idle_level = inverted ? RMT_IDLE_LEVEL_HIGH : RMT_IDLE_LEVEL_LOW;
rmt_tx.tx_config.idle_output_en = true;
ESP_ERROR_CHECK(rmt_config(&rmt_tx));
ESP_ERROR_CHECK(rmt_driver_install(rmt_tx.channel, 0, 0));
//rmt_tx_start(tx_channel, true);
}
}
bool IR_RMT::stop() {
running = false;
rmt_tx_stop(tx_channel);
rmt_rx_stop(rx_channel);
}
IR_RMT::~IR_RMT() {
rmt_driver_uninstall(tx_channel);
rmt_driver_uninstall(rx_channel);
}
bool IR_RMT::register_callback(const ir_cb_t &cb) {
callbacks_.push_back(cb);
if (!rx_task_handle) {
xTaskCreate(&cTaskWrapper, "watch_ringbuf", 3072, this, tskIDLE_PRIORITY+2, &rx_task_handle);
}
}
void IR_RMT::task_watch_ringbuf(void* pvParameters) {
size_t itemSize;
running = true;
while(running && ringBuf) {
rmt_item32_t* data = (rmt_item32_t*) xRingbufferReceive(ringBuf, &itemSize, portMAX_DELAY);
int numItems = itemSize / sizeof(rmt_item32_t);
ir_data_t val = decodeNEC(data, numItems);
if (val.address != INVALID_CHECKSUM && val.address != INVALID_PROTOCOL) {
for (const auto &cb : callbacks_) {
cb(val);
}
}
vRingbufferReturnItem(ringBuf, (void*) data);
}
rx_task_handle = NULL;
vTaskDelete(NULL);
}
void IR_RMT::cTaskWrapper(void* parameters) {
static_cast<IR_RMT*>(parameters)->task_watch_ringbuf(NULL);
}

53
src/IR_RMT.h

@ -0,0 +1,53 @@
#ifndef _IR_RMT_H
#define _IR_RMT_H
#include <Arduino.h>
#include <functional>
#include <vector>
#include <driver/rmt.h>
struct ir_data_t {
uint16_t address;
uint16_t command;
};
typedef std::function<void(ir_data_t)> ir_cb_t;
class IR_RMT {
public:
IR_RMT(gpio_num_t rx_pin, gpio_num_t tx_pin, bool inverted = false);
~IR_RMT();
bool begin(void);
bool stop(void);
bool register_callback(const ir_cb_t &cb);
bool send(uint16_t address, uint16_t command);
bool sendNEC32(uint32_t data);
private:
gpio_num_t rx_pin;
gpio_num_t tx_pin;
rmt_channel_t rx_channel;
rmt_channel_t tx_channel;
RingbufHandle_t ringBuf;
TaskHandle_t rx_task_handle;
bool running;
bool inverted;
void task_watch_ringbuf(void*);
static void cTaskWrapper(void*);
std::vector<ir_cb_t> callbacks_;
protected:
void nec_fill_item_level(rmt_item32_t* item, int high_us, int low_us);
void nec_fill_item_header(rmt_item32_t* item);
void nec_fill_item_bit_one(rmt_item32_t* item);
void nec_fill_item_bit_zero(rmt_item32_t* item);
void nec_fill_item_end(rmt_item32_t* item);
void nec_build_items(rmt_item32_t* item, uint16_t addr, uint16_t cmd_data);
};
enum command_type {
REPEAT = 0x100,
INVALID_CHECKSUM = UINT16_MAX-1,
INVALID_PROTOCOL = UINT16_MAX };
#endif /* _IR_RMT_H */

77
src/SensorHistory.cpp

@ -0,0 +1,77 @@
#include "SensorHistory.h"
#include <Arduino.h>
//static const char *TAG = "SensorHistory";
SensorHistory::SensorHistory(const int size) {
_size = size;
_values = (float*) malloc(_size * sizeof(float));
clear();
}
SensorHistory::~SensorHistory()
{
if (_values != NULL) free(_values);
}
void SensorHistory::clear(void) {
_index = 0;
_cnt = 0;
for (int i = 0; i < _size; i++)
{
_values[i] = 0.0; // keeps addValue simpler
}
}
void SensorHistory::addValue(const float value) {
if (_values == NULL) return;
_values[_index] = value;
_index = (_index+1) % _size;
if (_cnt < _size) _cnt++;
}
float SensorHistory::getAverage(void) const {
if (_cnt == 0) return NAN;
float sum = 0;
for (int i = 0; i < _cnt; i++) {
sum += _values[i];
}
return sum / _cnt;
}
float SensorHistory::getMin(void) const {
if (_cnt == 0) return NAN;
float min = _values[0];
for (int i = 0; i < _cnt; i++) {
if (_values[i] < min) min = _values[i];
}
return min;
}
float SensorHistory::getMax(void) const {
if (_cnt == 0) return NAN;
float max = _values[0];
for (int i = 0; i < _cnt; i++) {
if (_values[i] > max) max = _values[i];
}
return max;
}
float SensorHistory::getElement(int index) const {
if (_cnt == 0) return NAN;
index = (_index-1 - index) % _size;
if (index < 0) index = _size+index;
if (index >= _cnt) return NAN;
return _values[index];
}
float SensorHistory::getFirst(void) const {
if (_cnt < _size) return _values[0];
return getElement(-1);
}

26
src/SensorHistory.h

@ -0,0 +1,26 @@
#ifndef _SENSOR_HISTORY_H
#define _SENSOR_HISTORY_H
class SensorHistory {
public:
explicit SensorHistory(const int);
~SensorHistory();
void clear();
void addValue(const float);
float getAverage() const;
float getMin() const;
float getMax() const;
float getElement(int) const;
float getFirst() const;
int getSize() const { return _size; };
int getCount() const { return _cnt; };
protected:
int _size;
int _index;
int _cnt;
float* _values;
};
#endif /* _SENSOR_HISTORY_H */

50
src/icons.h

@ -0,0 +1,50 @@
/* http://javl.github.io/image2cpp/ */
/* https://fontawesome.com/icons?d=gallery&m=free */
/* https://littlevgl.com/image-to-c-array */
#define ICO_HEIGHT 32
// 'wifi', 40x32px
static const uint8_t ico_wifi_height = 32;
static const uint8_t ico_wifi_width = 40;
static const uint8_t ico_wifi [] PROGMEM = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x7f, 0xff, 0xff,
0xe0, 0x00, 0x07, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xf0, 0x00,
0x00, 0x00, 0x0f, 0xe0, 0x01, 0xff, 0x80, 0x07, 0xc0, 0x0f, 0xff, 0xf0, 0x03, 0x80, 0x7f, 0xff,
0xfe, 0x01, 0x00, 0xff, 0xff, 0xff, 0x00, 0x83, 0xff, 0xff, 0xff, 0xc1, 0xc7, 0xff, 0x00, 0xff,
0xe3, 0xff, 0xf8, 0x00, 0x1f, 0xff, 0xff, 0xe0, 0x00, 0x07, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff,
0xff, 0x00, 0x00, 0x00, 0xff, 0xfe, 0x00, 0xff, 0x00, 0x7f, 0xff, 0x03, 0xff, 0xc0, 0xff, 0xff,
0x8f, 0xff, 0xf1, 0xff, 0xff, 0xdf, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xc3, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00,
0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff,
0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
};
// 'wifi, 20x16px
static const uint8_t ico_wifi16_height = 16;
static const uint8_t ico_wifi16_width = 20;
static const uint8_t ico_wifi16 [] PROGMEM = {
0xff, 0xff, 0xf0, 0xfc, 0x03, 0xf0, 0xf0, 0x00, 0xf0, 0xc0, 0x00, 0x30, 0x87, 0xfe, 0x10, 0x1f,
0xff, 0x80, 0xbe, 0x07, 0xd0, 0xf8, 0x01, 0xf0, 0xf0, 0x00, 0xf0, 0xf3, 0xfc, 0xf0, 0xff, 0xff,
0xf0, 0xff, 0x9f, 0xf0, 0xff, 0x0f, 0xf0, 0xff, 0x0f, 0xf0, 0xff, 0x9f, 0xf0, 0xff, 0xff, 0xf0
};
// 'user-circle', 31x32px
static const uint8_t ico_user_circle [] PROGMEM = {
0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x0f, 0xff, 0xfc, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x03, 0xff,
0x81, 0xff, 0x03, 0xfe, 0x0f, 0xff, 0x83, 0xf8, 0x7f, 0xff, 0xc3, 0xf1, 0xf8, 0x0f, 0xc7, 0xc3,
0xe0, 0x0f, 0x87, 0x0f, 0x80, 0x0f, 0x86, 0x3f, 0x1f, 0x1f, 0x8c, 0x7e, 0x3e, 0x3f, 0x10, 0xfc,
0x7c, 0x7e, 0x03, 0xf8, 0xf8, 0xfe, 0x07, 0xf1, 0xf1, 0xfc, 0x0f, 0xe0, 0x03, 0xf8, 0x1f, 0xe0,
0x0f, 0xf0, 0x3f, 0xe0, 0x3f, 0xe0, 0x7f, 0xff, 0xff, 0xc0, 0x7f, 0xff, 0xff, 0x08, 0xf8, 0x38,
0x3e, 0x31, 0xc0, 0x00, 0x1c, 0x61, 0x00, 0x00, 0x10, 0xe0, 0x1e, 0x3c, 0x03, 0xe0, 0xff, 0xfe,
0x0f, 0xc1, 0xff, 0xfc, 0x1f, 0xc1, 0xff, 0xf0, 0x7f, 0xc0, 0xff, 0x81, 0xff, 0xc0, 0x00, 0x07,
0xff, 0xe0, 0x00, 0x3f, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff
};
// 'thermometer-half', 16x32px
static const uint8_t ico_thermometer_half [] PROGMEM = {
0xfc, 0x3f, 0xf0, 0x0f, 0xe0, 0x07, 0xe3, 0xc7, 0xc7, 0xe3, 0xc7, 0xe3, 0xc7, 0xe3, 0xc7, 0xe3,
0xc7, 0xe3, 0xc7, 0xe3, 0xc7, 0xe3, 0xc7, 0xe3, 0xc6, 0x63, 0xc4, 0x23, 0xc4, 0x23, 0xc4, 0x23,
0xc4, 0x23, 0xc4, 0x23, 0xc4, 0x23, 0x84, 0x21, 0x8c, 0x31, 0x08, 0x10, 0x10, 0x08, 0x10, 0x08,
0x10, 0x08, 0x10, 0x08, 0x08, 0x10, 0x8c, 0x31, 0x83, 0xc1, 0xc0, 0x03, 0xe0, 0x07, 0xf8, 0x1f
};

681
src/main.cpp

@ -0,0 +1,681 @@
/*
* Blink
* Turns on an LED on for one second,
* then off for one second, repeatedly.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <Wire.h>
#include <SPI.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/Org_01.h>
#include "bitmaps/Bitmaps128x250.h"
#include <Adafruit_GFX.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BME280.h"
#include "Adafruit_BME680.h"
#include "Adafruit_VEML6075.h"
#include <BH1750.h>
#define ARDUINO_SAMD_VARIANT_COMPLIANCE
#include "SdsDustSensor.h"
#include "IR_RMT.h"
#include "network/XD0OTA.h"
#include "network/XD0MQTT.h"
#include "SensorHistory.h"
#include "icons.h"
extern "C" {
uint8_t temprature_sens_read();
}
static const char* TAG = "MAIN";
WiFiMulti wifiMulti;
GxEPD2_BW<GxEPD2_213_B72, GxEPD2_213_B72::HEIGHT> display(GxEPD2_213_B72(/*CS=SS*/ TFT_CS, /*DC=*/ TFT_DC, /*RST=*/ TFT_RST, /*BUSY=*/ -1)); // GDEH0213B72
static constexpr uint8_t y_offset = 8;
Adafruit_BME280 bme280; // I2C (also available: hardware SPI
Adafruit_BME680 bme680; // I2C (also available: hardware SPI
//HardwareSerial Serial2(2);
SdsDustSensor sds(Serial2);
Adafruit_VEML6075 uv = Adafruit_VEML6075();
BH1750 lightMeter;
static constexpr gpio_num_t PIN_IR_TX = (gpio_num_t)15;
IR_RMT ir((gpio_num_t)-1, PIN_IR_TX, true);
XD0OTA ota("esp32-weatherstation");
XD0MQTT mqtt;
struct __attribute__((packed)) sensor_readings_t {
float temperature = NAN; // °C
float humidity = NAN; // %H
float pressure = NAN; // Pa
uint32_t voc = 0; // Ohm
float pm10 = NAN; // µg/m³
float pm25 = NAN; // µg/m³
float lux = NAN;
float uvi = NAN;
float uva = NAN;
float uvb = NAN;
float temperature_max = NAN; // °C
float temperature_min = NAN; // °C
uint32_t lastUpdate = 0;
} sensor_readings;
sensor_readings_t sensors_a4cf1211c3e4, sensors_246f28d1fa5c, sensors_246f28d1a080, sensors_30aea47b0568;
SensorHistory history_pressure(30);
uint32_t lastDisplayUpdate = 0;
uint32_t lastDisplayRefresh = 0;
bool bme280_active = false;
bool bme680_active = false;
bool uv_active = false;
bool light_active = false;
bool sds_active = false;
void helloWorld()
{
const char HelloWorld[] = "IchbinsBens!";
//Serial.println("helloWorld");
display.setRotation(1);
display.setFont(&FreeMonoBold9pt7b);
display.setTextColor(GxEPD_BLACK);
int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds(HelloWorld, 0, 0, &tbx, &tby, &tbw, &tbh);
// center bounding box by transposition of origin:
uint16_t x = ((display.width() - tbw) / 2) - tbx;
uint16_t y = ((display.height() - tbh) / 2) - tby;
display.setFullWindow();
display.firstPage();
do
{
display.fillScreen(GxEPD_WHITE);
display.setCursor(x, y);
display.print(HelloWorld);
display.setCursor(5, display.height()-5);
display.setFont(&Org_01);
display.print(FW_VERSION);
}
while (display.nextPage());
//Serial.println("helloWorld done");
}
void displayIcoPartial(const uint8_t bitmap[], uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
display.setPartialWindow(x, y, w, h);
display.firstPage(); do {
display.drawInvertedBitmap(x, y, bitmap, w, h, GxEPD_BLACK);
} while (display.nextPage());
}
void getTime(char* ptr, size_t maxsize, const char* format) {
time_t now;
struct tm timeinfo;
time(&now); // update 'now' variable with current time
setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1);
tzset();
localtime_r(&now, &timeinfo);
strftime(ptr, maxsize, format, &timeinfo);
}
void getSensorMeasurements() {
if (bme280_active) {
bme280.takeForcedMeasurement();
sensor_readings.temperature = bme280.readTemperature();
sensor_readings.humidity = bme280.readHumidity();
sensor_readings.pressure = bme280.readPressure();
}
if (bme680_active) {
if (bme680.performReading()) {
sensor_readings.temperature = bme680.temperature;
sensor_readings.humidity = bme680.humidity;
sensor_readings.pressure = bme680.pressure;
sensor_readings.voc = bme680.gas_resistance;
} else {
Serial.println("Failed to perform reading :(");
}
}
if (sensor_readings.temperature > sensor_readings.temperature_max
|| isnan(sensor_readings.temperature_max)) {
sensor_readings.temperature_max = sensor_readings.temperature;
}
if (sensor_readings.temperature < sensor_readings.temperature_min
|| isnan(sensor_readings.temperature_min)) {
sensor_readings.temperature_min = sensor_readings.temperature;
}
history_pressure.addValue(sensor_readings.pressure / 100.0F);
if (uv_active) {
sensor_readings.uvi = uv.readUVI();
sensor_readings.uva = uv.readUVA();
sensor_readings.uvb = uv.readUVB();
}
if (light_active) {
sensor_readings.lux = lightMeter.readLightLevel();
}
if (sds_active) {
PmResult pm = sds.readPm();
if (pm.isOk()) {
sensor_readings.pm10 = pm.pm10;
sensor_readings.pm25 = pm.pm25;
}
}
sensor_readings.lastUpdate = millis();
}
void receiveMqtt(const char* topic, const char* data) {
sensor_readings_t* sensor = NULL;
if (strstr(topic, "thomas/sensor/a4cf1211c3e4") == topic) {
sensor = &sensors_a4cf1211c3e4;
} else if (strstr(topic, "thomas/sensor/246f28d1fa5c") == topic) {
sensor = &sensors_246f28d1fa5c;
} else if (strstr(topic, "thomas/sensor/246f28d1a080") == topic) {
sensor = &sensors_246f28d1a080;
} else if (strstr(topic, "thomas/sensor/30aea47b0568") == topic) {
sensor = &sensors_30aea47b0568;
}
char* topic_last = strrchr(topic, '/');
if (topic_last && sensor) {
if (strcmp("/temperature", topic_last) == 0) {
sensor->temperature = atof(data);
} else if (strcmp("/humidity", topic_last) == 0) {
sensor->humidity = atof(data);
} else if (strcmp("/pressure", topic_last) == 0) {
sensor->pressure = atof(data);
} else if (strcmp("/pm10", topic_last) == 0) {
sensor->pm10 = atof(data);
} else if (strcmp("/pm25", topic_last) == 0) {
sensor->pm25 = atof(data);
} else if (strcmp("/uvi", topic_last) == 0) {
sensor->uvi = atof(data);
} else if (strcmp("/uva", topic_last) == 0) {
sensor->uva = atof(data);
} else if (strcmp("/uvb", topic_last) == 0) {
sensor->uvb = atof(data);
} else if (strcmp("/voc", topic_last) == 0) {
sensor->voc = atof(data);
}
sensor->lastUpdate = millis();
}
}
void displayValues() {
display.setRotation(1);
display.setFont(NULL);
display.setTextColor(GxEPD_BLACK);
display.setTextSize(1);
display.setTextWrap(false);
char timeStr[40];
getTime(timeStr, sizeof(timeStr), "%d. %b %Y %H:%M:%S");
display.setFullWindow();
display.firstPage();
do
{
display.fillScreen(GxEPD_WHITE);
// Title
display.setCursor(30,y_offset+0);
display.println(timeStr);
display.drawLine(0,y_offset+10,display.width(), y_offset+10, GxEPD_BLACK);
// Temp
display.drawRect(0,y_offset+10,66,50,GxEPD_BLACK);
display.setFont(NULL);
display.setCursor(5,y_offset+15);
display.printf("max: %.1f", sensor_readings.temperature_max);
display.setFont(&FreeSansBold9pt7b);
display.setCursor(5,y_offset+40);
display.printf("%.1f", sensor_readings.temperature);
display.setFont(NULL);
display.print(" \xf7\x43");
display.setCursor(5,y_offset+45);
display.printf("min: %.1f", sensor_readings.temperature_min);
// Humidity
display.drawRect(65,y_offset+10,66,50,GxEPD_BLACK);
display.setFont(NULL);
display.setCursor(70,y_offset+15);
display.print("Humidity");
display.setFont(&FreeSansBold9pt7b);
display.setCursor(70,y_offset+40);
display.printf("%.1f", sensor_readings.humidity);
display.setFont(NULL);
display.print(" \%");
display.setCursor(70,y_offset+45);
if (sensor_readings.humidity < 30) {
display.print("low");
} else if (sensor_readings.humidity < 60) {
display.print("comfort");
} else {
display.print("high");
}
// Pressure
display.drawRect(130,y_offset+10,66,50,GxEPD_BLACK);
display.setFont(NULL);
display.setCursor(135,y_offset+15);
display.print("Pressure");
display.setFont(&FreeSansBold9pt7b);
display.setCursor(135,y_offset+40);
display.printf("%.1f", sensor_readings.pressure / 100.0F);
display.setFont(NULL);
//display.print(" hPa");
float pressure_diff = history_pressure.getElement(0) - history_pressure.getFirst();
display.setCursor(135,y_offset+45);
if (isnan(pressure_diff) || history_pressure.getCount() < history_pressure.getSize()) {
} else if (pressure_diff > -20 && pressure_diff < -0.6) {
display.print("Trend: \x19\x19");
} else if (pressure_diff < -0.1) {
display.print("Trend: \x19");
} else if (pressure_diff < 0.1) {
display.print("Trend: \x1a");
} else if (pressure_diff < 0.6) {
display.print("Trend: \x18");
} else if (pressure_diff < 20) {
display.print("Trend: \x18\x18");
} else {
display.print("?");
}
// Other
display.drawRect(195,y_offset+10,56,122-10,GxEPD_BLACK);
display.setFont(NULL);
// VOC
display.setCursor(200,y_offset+15);
display.println("VOC:");
display.setCursor(200,y_offset+25);
display.printf("%.1f k\xe9", sensor_readings.voc / 1000.0F);
// PM
float pm10, pm25;
if (sds_active) {
pm10 = sensor_readings.pm10;
pm25 = sensor_readings.pm25;
} else if (!isnan(sensors_a4cf1211c3e4.pm10) || !isnan(sensors_a4cf1211c3e4.pm25)) {
pm10 = sensors_a4cf1211c3e4.pm10;
pm25 = sensors_a4cf1211c3e4.pm25;
} else if (!isnan(sensors_246f28d1fa5c.pm10) || !isnan(sensors_246f28d1fa5c.pm25)) {
pm10 = sensors_246f28d1fa5c.pm10;
pm25 = sensors_246f28d1fa5c.pm25;
} else if (!isnan(sensors_246f28d1a080.pm10) || !isnan(sensors_246f28d1a080.pm25)) {
pm10 = sensors_246f28d1a080.pm10;
pm25 = sensors_246f28d1a080.pm25;
} else {
pm10 = NAN;
pm25 = NAN;
}
display.setCursor(200,y_offset+45);
display.println("PM10/2.5:");
display.setCursor(200,y_offset+55);
display.printf("%.1f", pm10);
display.setCursor(200,y_offset+65);
display.printf("%.1f", pm25);
// Lux
//display.setCursor(200,y_offset+80);
//display.println("Lux:");
//display.setCursor(200,y_offset+90);
//display.printf("%.1f", sensor_readings.lux);
// UV
float uvi, uva, uvb;
if (uv_active) {
uvi = sensor_readings.uvi;
uva = sensor_readings.uva;
uvb = sensor_readings.uvb;
} else if (!isnan(sensors_a4cf1211c3e4.uvi) || !isnan(sensors_a4cf1211c3e4.uva) || !isnan(sensors_a4cf1211c3e4.uvb)) {
uvi = sensors_a4cf1211c3e4.uvi;
uva = sensors_a4cf1211c3e4.uva;
uvb = sensors_a4cf1211c3e4.uvb;
} else if (!isnan(sensors_246f28d1fa5c.uvi) || !isnan(sensors_246f28d1fa5c.uva) || !isnan(sensors_246f28d1fa5c.uvb)) {
uvi = sensors_246f28d1fa5c.uvi;
uva = sensors_246f28d1fa5c.uva;
uvb = sensors_246f28d1fa5c.uvb;
} else if (!isnan(sensors_246f28d1a080.uvi) || !isnan(sensors_246f28d1a080.uva) || !isnan(sensors_246f28d1a080.uvb)) {
uvi = sensors_246f28d1a080.uvi;
uva = sensors_246f28d1a080.uva;
uvb = sensors_246f28d1a080.uvb;
} else {
uvi = NAN;
uva = NAN;
uvb = NAN;
}
display.setCursor(200,y_offset+80);
display.println("UV I/A/B:");
display.setCursor(200,y_offset+90);
display.printf("%.1f", uvi);
display.setCursor(200,y_offset+100);
display.printf("%.1f", uva);
display.setCursor(200,y_offset+110);
display.printf("%.1f", uvb);
float lux;
if (light_active) {
lux = sensor_readings.lux;
} else if (!isnan(sensors_a4cf1211c3e4.lux)) {
lux = sensors_a4cf1211c3e4.lux;
} else if (!isnan(sensors_246f28d1fa5c.lux)) {
lux = sensors_246f28d1fa5c.lux;
} else if (!isnan(sensors_246f28d1a080.lux)) {
lux = sensors_246f28d1a080.lux;
} else {
lux = NAN;
}
display.setCursor(200,y_offset+120);
display.println("LUX");
display.setCursor(200,y_offset+130);
display.printf("%.1f", lux);
// other nodes
display.setFont(NULL);
display.setCursor(0, y_offset+70);
if (!ota.getMAC().equals("246f28d1fa5c") && millis() - sensors_246f28d1fa5c.lastUpdate < 15*60*1000) {
display.printf("246f28d1fa5c: %4.1f %4.1f %6.1f\n", sensors_246f28d1fa5c.temperature, sensors_246f28d1fa5c.humidity, sensors_246f28d1fa5c.pressure);
}
if (!ota.getMAC().equals("a4cf1211c3e4") && millis() - sensors_a4cf1211c3e4.lastUpdate < 15*60*1000) {
display.printf("a4cf1211c3e4: %4.1f %4.1f %6.1f\n", sensors_a4cf1211c3e4.temperature, sensors_a4cf1211c3e4.humidity, sensors_a4cf1211c3e4.pressure);
}
if (!ota.getMAC().equals("246f28d1a080") && millis() - sensors_246f28d1a080.lastUpdate < 15*60*1000) {
display.printf("246f28d1a080: %4.1f %4.1f %6.1f\n", sensors_246f28d1a080.temperature, sensors_246f28d1a080.humidity, sensors_246f28d1a080.pressure);
}
if (!ota.getMAC().equals("30aea47b0568") && millis() - sensors_30aea47b0568.lastUpdate < 15*60*1000) {
display.printf("30aea47b0568: %4.1f %4.1f %6.1f\n", sensors_30aea47b0568.temperature, sensors_30aea47b0568.humidity, sensors_30aea47b0568.pressure);
}
}
while (display.nextPage());
display.powerOff();
}
void printValues() {
if (bme280_active || bme680_active) {
#define SEALEVELPRESSURE_HPA (1013.25)
Serial.print("Temperature = ");
Serial.print(sensor_readings.temperature);
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(sensor_readings.pressure / 100.0F);
Serial.println(" hPa");
Serial.print("Humidity = ");
Serial.print(sensor_readings.humidity);
Serial.println(" %");
}
if (bme680_active) {
Serial.print("VOC = ");
Serial.print(sensor_readings.voc / 1000.0F);
Serial.println(" hPa");
}
Serial.println();
if (uv_active) {
Serial.print("UV Index reading: "); Serial.println(sensor_readings.uvi);
Serial.print("Raw UVA reading: "); Serial.println(sensor_readings.uva);
Serial.print("Raw UVB reading: "); Serial.println(sensor_readings.uvb);
Serial.println();
}
if (sds_active) {
Serial.print("PM2.5 = ");
Serial.print(sensor_readings.pm25);
Serial.print(", PM10 = ");
Serial.println(sensor_readings.pm10);
}
Serial.print("Free HEAP: "); Serial.println(ESP.getFreeHeap());
}
void sendValues() {
/* send values MQTT */
if (bme280_active || bme680_active) {
String topic_temperature = String("thomas/sensor/") + ota.getMAC() + String("/temperature");
String topic_humidity = String("thomas/sensor/") + ota.getMAC() + String("/humidity");
String topic_pressure = String("thomas/sensor/") + ota.getMAC() + String("/pressure");
mqtt.publish(topic_temperature.c_str(), sensor_readings.temperature, "%.2f");
delay(10);
mqtt.publish(topic_humidity.c_str(), sensor_readings.humidity, "%.2f");
delay(10);
mqtt.publish(topic_pressure.c_str(), sensor_readings.pressure / 100.0F, "%.2f");
delay(10);
}
if (bme680_active) {
String topic_voc = String("thomas/sensor/") + ota.getMAC() + String("/voc");
mqtt.publish(topic_voc.c_str(), sensor_readings.voc / 1000.0F, "%.2f");
delay(10);
}
if (!bme280_active && !bme680_active) {
String topic_temperature = String("thomas/sensor/") + ota.getMAC() + String("/temperature");
float esp32_temperature = (temprature_sens_read() - 32) / 1.8;
char temperature[8]; sprintf(temperature, "%.2f", esp32_temperature-29.40);
mqtt.publish(topic_temperature.c_str(), temperature, strlen(temperature));
delay(10);
}
if (uv_active) {
String topic_uvi = String("thomas/sensor/") + ota.getMAC() + String("/uvi");
String topic_uva = String("thomas/sensor/") + ota.getMAC() + String("/uva");
String topic_uvb = String("thomas/sensor/") + ota.getMAC() + String("/uvb");
mqtt.publish(topic_uvi.c_str(), sensor_readings.uvi, "%.2f");
mqtt.publish(topic_uva.c_str(), sensor_readings.uva, "%.2f");
mqtt.publish(topic_uvb.c_str(), sensor_readings.uvb, "%.2f");
delay(10);
}
if (light_active) {
String topic_lux = String("thomas/sensor/") + ota.getMAC() + String("/lux");
mqtt.publish(topic_lux.c_str(), sensor_readings.lux, "%.2f");
delay(10);
}
if (sds_active) {
String topic_pm10 = String("thomas/sensor/") + ota.getMAC() + String("/pm10");
String topic_pm25 = String("thomas/sensor/") + ota.getMAC() + String("/pm25");
mqtt.publish(topic_pm10.c_str(), sensor_readings.pm10, "%.2f");
mqtt.publish(topic_pm25.c_str(), sensor_readings.pm25, "%.2f");
delay(10);
}
}
void ir_received(ir_data_t data) {
Serial.println("IR DATA RECEIVED");
}
/**
* \brief Setup function
*
* is run once on startup
*/
void setup()
{
Serial.begin(115200);
delay(10);
ESP_LOGD(TAG, "setup hardware and sensors");
// initialize LED digital pin as an output.
pinMode(LED_BUILTIN, OUTPUT);
// initialize e-paper display
SPI.begin(18, 19, 23, TFT_CS);
display.init();
#define BME_SDA 21
#define BME_SCL 22
Wire.begin(BME_SDA, BME_SCL);
if (bme280.begin()) {
bme280_active = true;
} else {
ESP_LOGE(TAG, "Could not find a valid BME280 sensor, check wiring!");
}
if (bme680.begin()) {
bme680_active = true;
// Set up oversampling and filter initialization
bme680.setTemperatureOversampling(BME680_OS_8X);
bme680.setHumidityOversampling(BME680_OS_2X);
bme680.setPressureOversampling(BME680_OS_4X);
bme680.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme680.setGasHeater(320, 150); // 320*C for 150 ms
} else {
ESP_LOGE(TAG, "Could not find a valid BME680 sensor, check wiring!");
}
if (uv.begin()) {
uv_active = true;
uv.setIntegrationTime(VEML6075_100MS); // Set the integration constant
uv.setHighDynamic(true); // Set the high dynamic mode
uv.setForcedMode(false);
// Set the calibration coefficients
uv.setCoefficients(2.22, 1.33, // UVA_A and UVA_B coefficients
2.95, 1.74, // UVB_C and UVB_D coefficients
0.001461, 0.002591); // UVA and UVB responses
} else {
Serial.println("Failed to communicate with VEML6075 sensor, check wiring?");
}
if (lightMeter.begin()) {
light_active = true;
} else {
Serial.println("Failed to communicate with BH1750 sensor, check wiring?");
}
sds.begin();
FirmwareVersionResult sds_fw = sds.queryFirmwareVersion();
if (sds_fw.isOk()) {
sds_active = true;
sds.setActiveReportingMode(); // ensures sensor is in 'active' reporting mode
sds.setCustomWorkingPeriod(5); // sensor sends data every 3 minutes
} else {
Serial.println("Failed to communicate with SDS011 sensor, check wiring?");
}
display.clearScreen();
display.refresh();
ESP_LOGD(TAG, "displaying welcome screen");
helloWorld();
display.powerOff();
ESP_LOGD(TAG, "connecting to WiFi");
WiFi.setHostname("esp32-weatherstation");
wifiMulti.addAP(WIFI_SSID, WIFI_PASSWD);
wifiMulti.addAP(WIFI_SSID2, WIFI_PASSWD2);
wifiMulti.addAP(WIFI_SSID3, WIFI_PASSWD3);
for (int tries=0; wifiMulti.run() != WL_CONNECTED && tries < 10; tries++) {
Serial.print(".");
delay(500);
}
if(wifiMulti.run() == WL_CONNECTED) {
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
displayIcoPartial(ico_wifi16, display.width()-20, y_offset+0, ico_wifi16_width, ico_wifi16_height);
}
ESP_LOGD(TAG, "trying to fetch over-the-air update");
if (WiFi.status() == WL_CONNECTED) {
ota.update();
}
WiFi.setSleep(true);
ESP_LOGD(TAG, "connecting to MQTT");
mqtt.begin();
if (!ota.getMAC().equals("a4cf1211c3e4")) mqtt.subscribe("thomas/sensor/a4cf1211c3e4/#", receiveMqtt);
if (!ota.getMAC().equals("246f28d1fa5c")) mqtt.subscribe("thomas/sensor/246f28d1fa5c/#", receiveMqtt);
if (!ota.getMAC().equals("246f28d1a080")) mqtt.subscribe("thomas/sensor/246f28d1a080/#", receiveMqtt);
if (!ota.getMAC().equals("30aea47b0568")) mqtt.subscribe("thomas/sensor/30aea47b0568/#", receiveMqtt);
/* temp: publish version */
delay(5000);
String topic_version = String("thomas/sensor/") + ota.getMAC() + String("/version");
const char* fw_version_str = String(FW_VERSION).c_str();
mqtt.publish(topic_version.c_str(), fw_version_str, strlen(fw_version_str));
ir.begin();
ir.register_callback(ir_received);
ESP_LOGD(TAG, "setup done");
}
/**
* \brief Arduino main loop
*/
void loop()
{
/* Do an e-paper display refresh every 1 minutes */
if (millis() - lastDisplayUpdate >= 1*60*1000) {
lastDisplayUpdate = millis();
/* Do a full refresh every hour */
if (millis() - lastDisplayRefresh >= 60*60*1000) {
lastDisplayRefresh = millis();
display.clearScreen();
display.refresh();
}
getSensorMeasurements();
displayValues();
printValues();
sendValues();
}
if(wifiMulti.run() != WL_CONNECTED) {
Serial.println("WiFi not connected!");
delay(1000);
}
delay(2000);
}

148
src/network/XD0MQTT.cpp

@ -0,0 +1,148 @@
#include "XD0MQTT.h"
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "esp_wifi.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_event.h"
#include "tcpip_adapter.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"
#include "lwip/sockets.h"
#include "lwip/dns.h"
#include "lwip/netdb.h"
#include "esp_log.h"
#include "mqtt_client.h"
#include "esp_tls.h"
static const char *TAG = "MQTT";
XD0MQTT::XD0MQTT() {}
esp_err_t XD0MQTT::mqtt_event_handler_cb(esp_mqtt_event_handle_t event)
{
esp_mqtt_client_handle_t client = event->client;
int msg_id;
//int mbedtls_err; esp_err_t err;
switch (event->event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
connected = true;
for (const auto &subscription : subscriptions_) {
int msg_id = esp_mqtt_client_subscribe(client, subscription.topic, subscription.qos);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
}
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
connected = false;
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "MQTT_EVENT_DATA");
ESP_LOGI(TAG, "TOPIC=%.*s\r\n", event->topic_len, event->topic);
ESP_LOGI(TAG, "DATA=%.*s\r\n", event->data_len, event->data);
for (const auto &subscription : subscriptions_) {
// literal match
if (strcmp(event->topic, subscription.topic) == 0) {
subscription.cb(event->topic, event->data);
// '#' wildcard
} else if (subscription.topic[strlen(subscription.topic)-1] == '#'){
if (strncmp(event->topic, subscription.topic, strlen(subscription.topic)-1) == 0) {
subscription.cb(event->topic, event->data);
}
}
// ToDo: '+' wildcard
}
break;
case MQTT_EVENT_ERROR:
ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
//mbedtls_err = 0;
//err = esp_tls_get_and_clear_last_error(event->error_handle, &mbedtls_err, NULL);
//ESP_LOGI(TAG, "Last esp error code: 0x%x", err);
//ESP_LOGI(TAG, "Last mbedtls failure: 0x%x", mbedtls_err);
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
break;
}
return ESP_OK;
}
//static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
static esp_err_t mqtt_event_handler(esp_mqtt_event_handle_t event) {
//esp_mqtt_client_handle_t client = event->client;
XD0MQTT *context = (XD0MQTT*)event->user_context;
context->mqtt_event_handler_cb(event);
//ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%d", base, event_id);
//mqtt_event_handler_cb(event_data);
}
bool XD0MQTT::begin(void) {
static esp_mqtt_client_config_t mqtt_cfg;
mqtt_cfg.uri = MQTT_BROKER_URI;
// mqtt_cfg.host = "home.xd0.de";
// mqtt_cfg.port = 8883;
mqtt_cfg.event_handle = mqtt_event_handler;
mqtt_cfg.cert_pem = (const char *)rootCACertificate;
mqtt_cfg.username = MQTT_USERNAME;
mqtt_cfg.password = MQTT_PASSWORD;
mqtt_cfg.user_context = (void*)this;
ESP_LOGI(TAG, "[APP] Free memory: %d bytes", esp_get_free_heap_size());
Serial.printf("BROKER URI: %s\n", MQTT_BROKER_URI);
Serial.printf("MQTT USERNAME: %s\n", MQTT_USERNAME);
Serial.printf("MQTT PASSWORD: %s\n", MQTT_PASSWORD);
client = esp_mqtt_client_init(&mqtt_cfg);
//esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, client);
esp_mqtt_client_start(client);
return true;
}
bool XD0MQTT::publish(const char* topic, const char* data, int len, int qos, int retain) {
int msg_id = esp_mqtt_client_publish(client, topic, data, len, qos, retain);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
}
bool XD0MQTT::publish(const char* topic, const float data, const char* format, int qos, int retain) {
char data_str[64];
sprintf(data_str, format, data);
int msg_id = esp_mqtt_client_publish(client, topic, data_str, strlen(data_str), qos, retain);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
}
bool XD0MQTT::subscribe(const char* topic, const xd0mqtt_cb_t &cb, int qos) {
subscription_t subscription = {topic, cb, qos};
subscriptions_.push_back(subscription);
return true;
}
bool XD0MQTT::unsubscribe(const char* topic) {
std::vector<subscription_t>::iterator it = subscriptions_.begin();
for (auto it = std::begin(subscriptions_); it != std::end(subscriptions_);) {
if (strcmp(topic, it->topic) == 0) {
it = subscriptions_.erase(it);
}
}
int msg_id = esp_mqtt_client_unsubscribe(client, topic);
ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id);
return true;
}

66
src/network/XD0MQTT.h

@ -0,0 +1,66 @@
/**
* @file XD0MQTT.h
*
* @brief MQTT via xd0.de broker
*
* @author Hendrik Langer <hendrik+dev@xd0.de>
* @version 0.2
*
*/
#ifndef _XD0MQTT_H
#define _XD0MQTT_H
#include <Arduino.h>
#include <functional>
#include <vector>
#include "mqtt_client.h"
typedef std::function<void(char*, char*)> xd0mqtt_cb_t;
struct subscription_t {
const char* topic;
xd0mqtt_cb_t cb;
int qos;
};
class XD0MQTT {
public:
XD0MQTT(void);
bool begin(void);
bool publish(const char* topic, const char* data, int len, int qos=1, int retain=0);
bool publish(const char* topic, const float data, const char* format, int qos=1, int retain=0);
bool subscribe(const char* topic, const xd0mqtt_cb_t &cb, int qos=1);
bool unsubscribe(const char* topic);
esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event);
bool connected = false;
std::vector<subscription_t> subscriptions_;
private:
// openssl s_client -showcerts -connect home.xd0.de:8883 </dev/null 2>/dev/null|openssl x509 -outform PEM >mqtt_xd0.de.pem
const char* rootCACertificate = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\n" \
"MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\n" \
"DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\n" \
"PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\n" \
"Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n" \
"AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\n" \
"rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\n" \
"OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\n" \
"xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n" \
"7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\n" \
"aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\n" \
"HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\n" \
"SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\n" \
"ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\n" \
"AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\n" \
"R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\n" \
"JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\n" \
"Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n" \
"-----END CERTIFICATE-----\n";
esp_mqtt_client_handle_t client;
};
#endif /* _XD0MQTT_H */

136
src/network/XD0OTA.cpp

@ -0,0 +1,136 @@
#include "XD0OTA.h"
#include <WiFi.h>
#include <WiFiClient.h>
#include <HTTPClient.h>
#include <HTTPUpdate.h>
#include <time.h>
static const char* TAG = "OTA";
XD0OTA::XD0OTA(String deviceName) : deviceName{deviceName} {}
// Set time via NTP, as required for x.509 validation
void XD0OTA::setClock() {
configTime(0, 0, "pool.ntp.org", "time.nist.gov"); // UTC
Serial.print(F("Waiting for NTP time sync: "));
time_t now = time(nullptr);
int tries = 0;
while (now < 8 * 3600 * 2) {
yield();
delay(500);
Serial.print(F("."));
now = time(nullptr);
tries++;
if (tries>15) return;
}
Serial.println(F(""));
struct tm timeinfo;
gmtime_r(&now, &timeinfo);
Serial.print(F("Current time: "));
Serial.print(asctime(&timeinfo));
}
String XD0OTA::getMAC() {
uint8_t mac[6];
char result[14];
WiFi.macAddress( mac );
snprintf( result, sizeof( result ), "%02x%02x%02x%02x%02x%02x", mac[ 0 ], mac[ 1 ], mac[ 2 ], mac[ 3 ], mac[ 4 ], mac[ 5 ] );
return String( result );
}
String XD0OTA::getUpdateURL(String file, String extension) {
String updateURL = String(fwUrlBase);
updateURL.concat(file);
updateURL.concat(extension);
return updateURL;
}
void XD0OTA::update(void) {
setClock();
// try device specific image first
String fwURL = getUpdateURL(getMAC(), ".bin");
int newVersion = checkForUpdates(fwURL + ".version");
if (newVersion < 1) {
ESP_LOGW(TAG, "[update] no device specific update found..\n");
fwURL = getUpdateURL(deviceName, ".bin");
newVersion = checkForUpdates(fwURL + ".version");
// try project specific image
if (newVersion < 1) {
ESP_LOGW(TAG, "[update] Error while looking for updates..\n");
return;
}
}
if( newVersion > FW_VERSION ) {
ESP_LOGW(TAG, "Preparing to update.\n" );
ESP_LOGW(TAG, "Firmware image URL: " );
ESP_LOGW(TAG, "%s\n", fwURL.c_str() );
WiFiClientSecure client;
client.setCACert(rootCACertificate);
client.setTimeout(12); // seconds
httpUpdate.setLedPin(LED_BUILTIN, HIGH);
t_httpUpdate_return ret = httpUpdate.update( client, fwURL );
switch(ret) {
case HTTP_UPDATE_FAILED:
ESP_LOGW(TAG, "HTTP_UPDATE_FAILED Error (%d): %s\n", httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str());
break;
case HTTP_UPDATE_NO_UPDATES:
ESP_LOGW(TAG, "HTTP_UPDATE_NO_UPDATES\n");
break;
case HTTP_UPDATE_OK:
ESP_LOGW(TAG, "[update] Update ok.\n"); // may not called we reboot the ESP
break;
}
} else {
ESP_LOGW(TAG, "[update] Already on latest version\n" );
}
}
int XD0OTA::checkForUpdates(String url) {
int newVersion = -1;
ESP_LOGW(TAG, "Checking for firmware updates.\n" );
ESP_LOGW(TAG, "Firmware version URL: " );
ESP_LOGW(TAG, "%s\n", url.c_str() );
WiFiClientSecure client;
client.setCACert(rootCACertificate);
client.setTimeout(5); // seconds
HTTPClient httpClient;
httpClient.begin( client, url );
int httpCode = httpClient.GET();
if( httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY ) {
String newFWVersion = httpClient.getString();
ESP_LOGW(TAG, "Current firmware version: " );
ESP_LOGW(TAG, "%s\n", String(FW_VERSION).c_str() );
ESP_LOGW(TAG, "Available firmware version: " );
ESP_LOGW(TAG, "%s\n", newFWVersion.c_str() );
newVersion = newFWVersion.toInt();
} else {
ESP_LOGW(TAG, "Firmware version check failed, got HTTP response code " );
ESP_LOGW(TAG, "%d\n", httpCode );
newVersion = -1;
}
httpClient.end();
client.stop();
return newVersion;
}

94
src/network/XD0OTA.h

@ -0,0 +1,94 @@
/**
* @file XD0OTA.h
*
* @brief https Over-the-Air updates via xd0.de
*
* @author Hendrik Langer <hendrik+dev@xd0.de>
* @version 0.2
*
*/
#ifndef _XD0OTA_H
#define _XD0OTA_H
#include <Arduino.h>
class XD0OTA {
public:
XD0OTA(String deviceName);
void update(void);
int checkForUpdates(String url);
String getMAC(void);
private:
String deviceName;
const char* fwUrlBase = "https://fwupdate.xd0.de:444/fota/";
const char* httpsFingerprint = "37 42 61 B9 E6 EE 22 36 D1 59 67 7D 55 53 6E A4 C7 AA 60 26";
const char* rootCACertificate = \
"-----BEGIN CERTIFICATE-----\n" \
"MIID8DCCAtigAwIBAgIJAPSONy8RRejRMA0GCSqGSIb3DQEBCwUAMIGLMQswCQYD\n" \
"VQQGEwJERTEeMBwGA1UECAwVTm9ydGhyaGluZS1XZXN0cGhhbGlhMQ4wDAYDVQQH\n" \
"DAVFc3NlbjEPMA0GA1UECgwGeGQwLmRlMRgwFgYDVQQDDA9md3VwZGF0ZS54ZDAu\n" \
"ZGUxITAfBgkqhkiG9w0BCQEWEmhlbmRyaWsrZGV2QHhkMC5kZTAgFw0xODA2MTYx\n" \
"MTAzMTVaGA8yMDY4MDYxNjExMDMxNVowgYsxCzAJBgNVBAYTAkRFMR4wHAYDVQQI\n" \
"DBVOb3J0aHJoaW5lLVdlc3RwaGFsaWExDjAMBgNVBAcMBUVzc2VuMQ8wDQYDVQQK\n" \
"DAZ4ZDAuZGUxGDAWBgNVBAMMD2Z3dXBkYXRlLnhkMC5kZTEhMB8GCSqGSIb3DQEJ\n" \
"ARYSaGVuZHJpaytkZXZAeGQwLmRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" \
"CgKCAQEAx37S1YgaG74IhTysaaZQMFI50TDeGkxxdpNoIBX0UBeVtKI/3u3MAqBz\n" \
"kKTDHFu4IQDj0PvBdlqPFGdSinFgrIvr49uAr+alNKUtkuSTT7nXI0fzqAxv1taj\n" \
"0mNhigVvYikX2BUU/rNLnQclyBdPNVsOf9cv0t5+UcOHRt6oEwk5nFtG7s7k4+wu\n" \
"wRdGlLy2LwLihYFon4LHAs05JW3qs0IQI4etc8E2JWjF2YwBg3+ooyzUFFIGjPSl\n" \
"Lpi7WvAAR19HITbt5FJXQkFZnFxnfbQv/5f7n8vWfFmzYsEgvldwMZv+Eg6wPb2h\n" \
"rgH7T6RSb55JrZE/JUY5C6pKvTJ3AwIDAQABo1MwUTAdBgNVHQ4EFgQUCnZywNj+\n" \
"djz6n0sIARPx8dp+7bQwHwYDVR0jBBgwFoAUCnZywNj+djz6n0sIARPx8dp+7bQw\n" \
"DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAto7IGXpNYTiPUgnA\n" \
"DE6osdgSV1yVYJj75v+Y8aUGgQ93Ipl/0+PQL99wbGgjDhfxGADLtljwoEAz/fep\n" \
"RqCh8swjL34XV9XjMzfhEMDCybSO6mK7ZmCKwhz9yBaK/Qjdj0YUoLhJ9Huzb70m\n" \
"lGzbOhY4JJKFVaA5AcZhYHqjCmzHCVJ/H0zeuPGyKutbvSx23a24LmebfY4q2D62\n" \
"U85ox3Ojek6Mc8J4V+RjORygDGAO4gZClEhAza4koAg7lCO/kSSk5PrXdlz2dqtA\n" \
"D5Npv9M5363apnO1VlVR+OuO1NEJusRK1aWk9RLZsTPxzwOWwdkifXxUEJ+f8mGn\n" \
"o+6SCw==\n" \
"-----END CERTIFICATE-----\n";
String getUpdateURL(String file, String extension);
void setClock(void);
};
#endif /* _XD0OTA_H */
/*
MAKE SURE TO RESET THE ESP8266 ONCE AFTER NORMAL FLASHING! OTHERWISE OTA WON'T WORK!
openssl req -x509 -nodes -days 18263 -newkey rsa:2048 -keyout /etc/ssl/private/xd0-fwupdate-selfsigned.key -out /etc/ssl/certs/xd0-fwupdate-selfsigned.crt
openssl x509 -noout -fingerprint -sha1 -inform pem -in /etc/ssl/certs/xd0-fwupdate-selfsigned.crt
scp .pioenvs/nodemcuv2/firmware.bin user@webserver:/var/www/fwupdate/fota/macaddress.bin
and change macaddress.version
server {
listen 444 ssl;
listen [::]:444 ssl;
server_name fwupdate.xd0.de;
# SSL configuration
ssl_certificate /etc/ssl/certs/xd0-fwupdate-selfsigned.crt;
ssl_certificate_key /etc/ssl/private/xd0-fwupdate-selfsigned.key;
ssl_buffer_size 4k;
root /var/www/fwupdate;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
access_log /var/log/nginx/fwupdate_access.log;
error_log /var/log/nginx/fwupdate_error.log;
}
*/

11
test/README

@ -0,0 +1,11 @@
This directory is intended for PIO Unit Testing and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PIO Unit Testing:
- https://docs.platformio.org/page/plus/unit-testing.html
Loading…
Cancel
Save