Compare commits

...

No commits in common. 'esphome' and 'ci-test' have entirely different histories.

  1. 14
      .gitignore
  2. 41
      .gitlab-ci.yml
  3. 0
      .temp-clang-tidy.cpp
  4. 20
      LICENSE.md
  5. 120
      README.md
  6. 83
      common/base.yaml
  7. 27
      common/battery.yaml
  8. 8
      common/bh1750.yaml
  9. 45
      common/bme280.yaml
  10. 96
      common/bme680.yaml
  11. 238
      common/display-hendrik.yaml
  12. 294
      common/display.yaml
  13. 8
      common/experimental/ble.yaml
  14. 2
      common/experimental/webserver.yaml
  15. 15
      common/sds011.yaml
  16. 4
      common/secrets.yaml
  17. 19
      common/veml6075.yaml
  18. 7
      compile_commands.json
  19. 43
      custom/veml6075_custom_sensor.h
  20. BIN
      fonts/Vera.ttf
  21. BIN
      fonts/VeraMono.ttf
  22. BIN
      fonts/materialdesignicons-webfont.ttf
  23. BIN
      fonts/slkscr.ttf
  24. 37
      fonts/weather_icon_map.h
  25. 15
      hendrik-test.yaml
  26. 23
      platformio.ini
  27. 94
      script/build_compile_commands.py
  28. 170
      script/clang-tidy
  29. 139
      script/helpers.py
  30. BIN
      script/helpers.pyc
  31. 10
      secrets-example.yaml
  32. 11
      sensor-outdoor.yaml
  33. 9
      sensor-springer.yaml
  34. 8
      sensor-sz.yaml
  35. 8
      sensor-wz.yaml
  36. 28
      src/main.cpp
  37. 0
      src/main.h
  38. 11
      test/README

14
.gitignore

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

41
.gitlab-ci.yml

@ -0,0 +1,41 @@
stages:
- build
- deploy
build:
stage: build
image: python:2.7
before_script:
- pip install -U platformio
- platformio update
script:
- platformio ci --project-conf platformio.ini --board=lolin_d32_pro --build-dir build --keep-build-dir
# - cp ~/.platformio/packages/framework-arduinoespressif32/tools/sdk/bin/bootloader_dio_40m.bin build/.pio/build/lolin_d32_pro/
# - cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/default.bin build/.pio/build/lolin_d32_pro/
variables: {PLATFORMIO_CI_SRC: "src"}
artifacts:
paths:
- build/.pio/build/lolin_d32_pro/*.bin
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}"
dependencies:
- build
when: manual
only:
- master
include:
- template: Code-Quality.gitlab-ci.yml

0
.temp-clang-tidy.cpp

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.

120
README.md

@ -1,118 +1,23 @@
# 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
### Display
Connect to TFT LCD Port with 10P SH1.0 cable
E-Paper pin | ESP32 pin | Notes
------------|-----------|------------
3V3 | |
BUSY | nc |
CS | IO14 |
SCK | IO18 |
MOSI | IO23 | MISO: IO19
DC | IO27 |
RST | IO33 |
GND | |
### BME680
I2C
BME680 pin | ESP32 pin | Notes
-----------|-----------|----------
VCC | 3V |
GND | GND |
SCL | IO22 |
SDA | IO21 |
SDO | nc | SPI only
CS | nc | SPI only
### BH1750 (GY-302)
I2C
GY-302 pin | ESP32 pin | Notes
-----------|-----------|----------
VCC | 3V |
GND | GND |
SCL | IO22 |
SDA | IO21 |
ADDR | nc | select addr: GND: 0x23 or VCC: 0x5C
### VEML6075
I2C
GY-VEML6075 pin | ESP32 pin | Notes
-----------|-----------|----------
VIN | 3V |
GND | GND |
SCL | IO22 |
SDA | IO21 |
### SDS011
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)
GND | GND |
25µm | nc |
5V | VUSB | 5V
1µm | nc |
NC | nc |
tbd
## 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
platformio run -t upload && platformio device monitor -b 115200
pio run
```
## Authors
@ -125,5 +30,4 @@ 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

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

7
compile_commands.json

File diff suppressed because one or more lines are too long

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

23
platformio.ini

@ -0,0 +1,23 @@
;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
framework = arduino
board = lolin_d32_pro
build_flags =
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
;lib_deps =
; ArduinoJSON
monitor_speed = 115200

94
script/build_compile_commands.py

@ -0,0 +1,94 @@
#!/usr/bin/env python
import codecs
import json
import os.path
import re
import subprocess
import sys
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..')))
basepath = os.path.join(root_path, 'esphome')
temp_header_file = os.path.join(root_path, '.temp-clang-tidy.cpp')
def shlex_quote(s):
if not s:
return u"''"
if re.search(r'[^\w@%+=:,./-]', s) is None:
return s
return u"'" + s.replace(u"'", u"'\"'\"'") + u"'"
def build_all_include():
# Build a cpp file that includes all header files in this repo.
# Otherwise header-only integrations would not be tested by clang-tidy
headers = []
for path in walk_files(basepath):
filetypes = ('.h',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
path = os.path.relpath(path, root_path)
include_p = path.replace(os.path.sep, '/')
headers.append('#include "{}"'.format(include_p))
headers.sort()
headers.append('')
content = '\n'.join(headers)
with codecs.open(temp_header_file, 'w', encoding='utf-8') as f:
f.write(content)
def build_compile_commands():
gcc_flags_json = os.path.join(root_path, '.gcc-flags.json')
if not os.path.isfile(gcc_flags_json):
print("Could not find {} file which is required for clang-tidy.")
print('Please run "pio init --ide atom" in the root esphome folder to generate that file.')
sys.exit(1)
with codecs.open(gcc_flags_json, 'r', encoding='utf-8') as f:
gcc_flags = json.load(f)
exec_path = gcc_flags['execPath']
include_paths = gcc_flags['gccIncludePaths'].split(',')
includes = ['-I{}'.format(p) for p in include_paths]
cpp_flags = gcc_flags['gccDefaultCppFlags'].split(' ')
defines = [flag for flag in cpp_flags if flag.startswith('-D')]
command = [exec_path]
command.extend(includes)
command.extend(defines)
command.append('-std=gnu++11')
command.append('-Wall')
command.append('-Wno-delete-non-virtual-dtor')
command.append('-Wno-unused-variable')
command.append('-Wunreachable-code')
source_files = []
for path in walk_files(basepath):
filetypes = ('.cpp',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
source_files.append(os.path.abspath(path))
source_files.append(temp_header_file)
source_files.sort()
compile_commands = [{
'directory': root_path,
'command': ' '.join(shlex_quote(x) for x in (command + ['-o', p + '.o', '-c', p])),
'file': p
} for p in source_files]
compile_commands_json = os.path.join(root_path, 'compile_commands.json')
if os.path.isfile(compile_commands_json):
with codecs.open(compile_commands_json, 'r', encoding='utf-8') as f:
try:
if json.load(f) == compile_commands:
return
except:
pass
with codecs.open(compile_commands_json, 'w', encoding='utf-8') as f:
json.dump(compile_commands, f, indent=2)
def walk_files(path):
for root, _, files in os.walk(path):
for name in files:
yield os.path.join(root, name)
if __name__ == "__main__":
build_compile_commands()
build_all_include()

170
script/clang-tidy

@ -0,0 +1,170 @@
#!/usr/bin/env python
from __future__ import print_function
import multiprocessing
import os
import re
import pexpect
import shutil
import subprocess
import sys
import tempfile
import argparse
import click
import threading
sys.path.append(os.path.dirname(__file__))
from helpers import basepath, shlex_quote, get_output, build_compile_commands, \
build_all_include, temp_header_file, git_ls_files, filter_changed
is_py2 = sys.version[0] == '2'
if is_py2:
import Queue as queue
else:
import queue as queue
def run_tidy(args, tmpdir, queue, lock, failed_files):
print("tidy")
while True:
path = queue.get()
invocation = ['clang-tidy-7', '-header-filter=^{}/.*'.format(re.escape(basepath))]
if tmpdir is not None:
invocation.append('-export-fixes')
# Get a temporary file. We immediately close the handle so clang-tidy can
# overwrite it.
(handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
os.close(handle)
invocation.append(name)
invocation.append('-p=.')
if args.quiet:
invocation.append('-quiet')
for arg in ['-Wfor-loop-analysis', '-Wshadow-field', '-Wshadow-field-in-constructor']:
invocation.append('-extra-arg={}'.format(arg))
invocation.append(os.path.abspath(path))
invocation_s = ' '.join(shlex_quote(x) for x in invocation)
# Use pexpect for a pseudy-TTY with colored output
output, rc = pexpect.run(invocation_s, withexitstatus=True, encoding='utf-8',
timeout=15*60)
with lock:
if rc != 0:
print()
print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path))
print(invocation_s)
print(output)
print()
failed_files.append(path)
queue.task_done()
def progress_bar_show(value):
if value is None:
return ''
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-j', '--jobs', type=int,
default=multiprocessing.cpu_count(),
help='number of tidy instances to be run in parallel.')
parser.add_argument('files', nargs='*', default=[],
help='files to be processed (regex on path)')
parser.add_argument('--fix', action='store_true', help='apply fix-its')
parser.add_argument('-q', '--quiet', action='store_false',
help='Run clang-tidy in quiet mode')
parser.add_argument('-c', '--changed', action='store_true',
help='Only run on changed files')
parser.add_argument('--all-headers', action='store_true',
help='Create a dummy file that checks all headers')
args = parser.parse_args()
try:
get_output('clang-tidy-7', '-version')
except:
print("""
Oops. It looks like clang-tidy is not installed.
Please check you can run "clang-tidy-7 -version" in your terminal and install
clang-tidy (v7) if necessary.
Note you can also upload your code as a pull request on GitHub and see the CI check
output to apply clang-tidy.
""")
return 1
build_all_include()
build_compile_commands()
print("test")
files = []
for path in git_ls_files():
filetypes = ('.cpp',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
path = os.path.relpath(path, os.getcwd())
files.append(path)
# Match against re
file_name_re = re.compile('|'.join(args.files))
files = [p for p in files if file_name_re.search(p)]
if args.changed:
files = filter_changed(files)
files.sort()
if args.all_headers:
files.insert(0, temp_header_file)
tmpdir = None
if args.fix:
tmpdir = tempfile.mkdtemp()
failed_files = []
return_code = 0
try:
task_queue = queue.Queue(args.jobs)
lock = threading.Lock()
for _ in range(args.jobs):
t = threading.Thread(target=run_tidy,
args=(args, tmpdir, task_queue, lock, failed_files))
t.daemon = True
t.start()
# Fill the queue with files.
with click.progressbar(files, width=30, file=sys.stderr,
item_show_func=progress_bar_show) as bar:
for name in bar:
task_queue.put(name)
# Wait for all threads to be done.
task_queue.join()
return_code = len(failed_files)
except KeyboardInterrupt:
print()
print('Ctrl-C detected, goodbye.')
if tmpdir:
shutil.rmtree(tmpdir)
os.kill(0, 9)
print("finished")
if args.fix and failed_files:
print('Applying fixes ...')
try:
subprocess.call(['clang-apply-replacements-7', tmpdir])
except:
print('Error applying fixes.\n', file=sys.stderr)
raise
sys.exit(return_code)
if __name__ == '__main__':
main()

139
script/helpers.py

@ -0,0 +1,139 @@
import codecs
import json
import os.path
import re
import subprocess
import sys
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..')))
basepath = os.path.join(root_path, 'src')
temp_header_file = os.path.join(root_path, '.temp-clang-tidy.cpp')
def shlex_quote(s):
if not s:
return u"''"
if re.search(r'[^\w@%+=:,./-]', s) is None:
return s
return u"'" + s.replace(u"'", u"'\"'\"'") + u"'"
def build_all_include():
# Build a cpp file that includes all header files in this repo.
# Otherwise header-only integrations would not be tested by clang-tidy
headers = []
for path in walk_files(basepath):
filetypes = ('.h',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
path = os.path.relpath(path, root_path)
include_p = path.replace(os.path.sep, '/')
headers.append('#include "{}"'.format(include_p))
headers.sort()
headers.append('')
content = '\n'.join(headers)
with codecs.open(temp_header_file, 'w', encoding='utf-8') as f:
f.write(content)
def build_compile_commands():
gcc_flags_json = os.path.join(root_path, '.gcc-flags.json')
if not os.path.isfile(gcc_flags_json):
print("Could not find {} file which is required for clang-tidy.")
print('Please run "pio init --ide atom" in the root esphome folder to generate that file.')
sys.exit(1)
with codecs.open(gcc_flags_json, 'r', encoding='utf-8') as f:
gcc_flags = json.load(f)
exec_path = gcc_flags['execPath']
include_paths = gcc_flags['gccIncludePaths'].split(',')
includes = ['-isystem{}'.format(p) for p in include_paths]
cpp_flags = gcc_flags['gccDefaultCppFlags'].split(' ')
defines = [flag for flag in cpp_flags if flag.startswith('-D')]
command = [exec_path]
command.extend(includes)
command.extend(defines)
command.append('-std=gnu++11')
command.append('-Wall')
command.append('-Wno-delete-non-virtual-dtor')
command.append('-Wno-unused-variable')
command.append('-Wunreachable-code')
source_files = []
for path in walk_files(basepath):
filetypes = ('.cpp',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
source_files.append(os.path.abspath(path))
source_files.append(temp_header_file)
source_files.sort()
compile_commands = [{
'directory': root_path,
'command': ' '.join(shlex_quote(x) for x in (command + ['-o', p + '.o', '-c', p])),
'file': p
} for p in source_files]
compile_commands_json = os.path.join(root_path, 'compile_commands.json')
if os.path.isfile(compile_commands_json):
with codecs.open(compile_commands_json, 'r', encoding='utf-8') as f:
try:
if json.load(f) == compile_commands:
return
except:
pass
with codecs.open(compile_commands_json, 'w', encoding='utf-8') as f:
json.dump(compile_commands, f, indent=2)
def walk_files(path):
for root, _, files in os.walk(path):
for name in files:
yield os.path.join(root, name)
def get_output(*args):
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate()
return output.decode('utf-8')
def splitlines_no_ends(string):
return [s.strip() for s in string.splitlines()]
def changed_files():
for remote in ('upstream', 'origin'):
command = ['git', 'merge-base', '{}/dev'.format(remote), 'HEAD']
try:
merge_base = splitlines_no_ends(get_output(*command))[0]
break
except:
pass
else:
raise ValueError("Git not configured")
command = ['git', 'diff', merge_base, '--name-only']
changed = splitlines_no_ends(get_output(*command))
changed = [os.path.relpath(f, os.getcwd()) for f in changed]
changed.sort()
return changed
def filter_changed(files):
changed = changed_files()
files = [f for f in files if f in changed]
print("Changed files:")
if not files:
print(" No changed files!")
for c in files:
print(" {}".format(c))
return files
def git_ls_files():
command = ['git', 'ls-files', '-s']
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
output, err = proc.communicate()
lines = [x.split() for x in output.decode('utf-8').splitlines()]
return {
s[3].strip(): int(s[0]) for s in lines
}

BIN
script/helpers.pyc

Binary file not shown.

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

28
src/main.cpp

@ -0,0 +1,28 @@
/*
* Blink
* Turns on an LED on for one second,
* then off for one second, repeatedly.
*/
#include <Arduino.h>
// Set LED_BUILTIN if it is not defined by Arduino framework
// #define LED_BUILTIN 2
void setup()
{
// initialize LED digital pin as an output.
pinMode(LED_BUILTIN, OUTPUT);
}
void loop()
{
// turn the LED on (HIGH is the voltage level)
digitalWrite(LED_BUILTIN, HIGH);
// wait for a second
delay(1000);
// turn the LED off by making the voltage LOW
digitalWrite(LED_BUILTIN, LOW);
// wait for a second
delay(1000);
}

0
src/main.h

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