Compare commits
No commits in common. 'esphome' and 'main' have entirely different histories.
40 changed files with 1866 additions and 1020 deletions
@ -0,0 +1,76 @@ |
|||
--- |
|||
kind: pipeline |
|||
type: docker |
|||
name: default |
|||
|
|||
steps: |
|||
- name: build |
|||
image: python |
|||
environment: |
|||
PLATFORMIO_CI_SRC: "src" |
|||
WIFI_SSID: |
|||
from_secret: WIFI_SSID |
|||
WIFI_PASSWD: |
|||
from_secret: WIFI_PASSWD |
|||
WIFI_SSID2: |
|||
from_secret: WIFI_SSID2 |
|||
WIFI_PASSWD2: |
|||
from_secret: WIFI_PASSWD2 |
|||
WIFI_SSID3: |
|||
from_secret: WIFI_SSID3 |
|||
WIFI_PASSWD3: |
|||
from_secret: WIFI_PASSWD3 |
|||
MQTT_BROKER_URI: |
|||
from_secret: MQTT_BROKER_URI |
|||
MQTT_USERNAME: |
|||
from_secret: MQTT_USERNAME |
|||
MQTT_PASSWORD: |
|||
from_secret: MQTT_PASSWORD |
|||
commands: |
|||
- pip install -U platformio |
|||
- platformio run |
|||
- platformio ci --lib="script" --project-conf platformio.ini --board=lolin_d32_pro --build-dir="build" --keep-build-dir |
|||
# prepare deployment |
|||
- mkdir deploy |
|||
- cp .pio/build/lolin_d32_pro/firmware.bin deploy/${DRONE_REPO_NAME}.bin |
|||
- cp version.txt deploy/${DRONE_REPO_NAME}.bin.version |
|||
|
|||
- name: gitea_release |
|||
image: plugins/gitea-release |
|||
settings: |
|||
api_key: |
|||
from_secret: gitea_token |
|||
base_url: https://dev.xd0.de |
|||
files: |
|||
- .pio/build/lolin_d32_pro/*.bin |
|||
when: |
|||
event: tag |
|||
|
|||
- name: deploy |
|||
image: appleboy/drone-scp |
|||
settings: |
|||
host: 172.16.85.15 |
|||
target: /var/www/fwupdate/fota |
|||
source: |
|||
- deploy/${DRONE_REPO_NAME}.bin |
|||
- deploy/${DRONE_REPO_NAME}.bin.version |
|||
#rm: true |
|||
strip_components: 1 |
|||
username: |
|||
from_secret: ssh_username |
|||
password: |
|||
from_secret: ssh_password |
|||
port: |
|||
from_secret: ssh_port |
|||
when: |
|||
event: tag |
|||
|
|||
- name: notify |
|||
image: drillster/drone-email |
|||
settings: |
|||
host: mail.xd0.de |
|||
from: drone@xd0.de |
|||
recipients: |
|||
- hendrik+dev@xd0.de |
|||
when: |
|||
status: [ changed, failure ] |
@ -0,0 +1,18 @@ |
|||
.pio |
|||
|
|||
# old platformio |
|||
.pioenvs |
|||
.piolibdeps |
|||
.clang_complete |
|||
.gcc-flags.json |
|||
|
|||
# virtualenv |
|||
bin/ |
|||
include/ |
|||
lib/ |
|||
local/ |
|||
share/ |
|||
|
|||
# custom |
|||
version.txt |
|||
build/ |
@ -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 |
@ -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. |
@ -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 |
@ -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 |
@ -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 |
@ -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' |
@ -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 |
@ -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 |
@ -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 |
@ -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" |
@ -1,2 +0,0 @@ |
|||
web_server: |
|||
port: 80 |
@ -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 |
@ -1,4 +0,0 @@ |
|||
<<: !include ../secrets.yaml |
|||
|
|||
# You can also use Home Assistant secrets.yaml: |
|||
# <<: !include ../../secrets.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 |
@ -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); |
|||
} |
|||
}; |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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"}, |
|||
}; |
@ -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 |
@ -0,0 +1,60 @@ |
|||
;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 BMP280 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 |
|||
; ESP8266 Influxdb |
|||
|
|||
|
|||
monitor_speed = 115200 |
|||
upload_speed = 115200 |
|||
|
|||
extra_scripts = |
|||
pre:script/autoversioning.py |
|||
pre:lib/script/autoversioning.py |
@ -0,0 +1,19 @@ |
|||
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) |
|||
]) |
|||
|
|||
drone_semver = env["ENV"].get("DRONE_SEMVER") |
|||
if drone_semver: |
|||
env.Append(CPPDEFINES=[ |
|||
("DRONE_SEMVER", "\"" + drone_semver.replace("\"", "\\\"") + "\"") |
|||
]) |
@ -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" |
@ -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 |
@ -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 |
|||
|
@ -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 |
@ -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 |
@ -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); |
|||
|
|||
} |
@ -0,0 +1,25 @@ |
|||
#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 */ |
@ -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 |
|||
}; |
@ -0,0 +1,889 @@ |
|||
/*
|
|||
* 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_BMP280.h> |
|||
|
|||
#include "Adafruit_VEML6075.h" |
|||
#include <BH1750.h> |
|||
|
|||
#define ARDUINO_SAMD_VARIANT_COMPLIANCE |
|||
#include "SdsDustSensor.h" |
|||
|
|||
#include "network/XD0OTA.h" |
|||
#include "network/XD0MQTT.h" |
|||
|
|||
#include <ArduinoJson.h> |
|||
|
|||
#include "SensorHistory.h" |
|||
|
|||
#include "icons.h" |
|||
|
|||
extern "C" { |
|||
uint8_t temprature_sens_read(); |
|||
} |
|||
|
|||
static const char* TAG = "MAIN"; |
|||
|
|||
|
|||
#define TIME_TO_SLEEP 60 // seconds
|
|||
constexpr unsigned int dhcp_interval = 60*60; |
|||
|
|||
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 = 6; |
|||
|
|||
Adafruit_BME280 bme280; // I2C (also available: hardware SPI
|
|||
Adafruit_BME680 bme680; // I2C (also available: hardware SPI
|
|||
Adafruit_BMP280 bmp280; |
|||
//HardwareSerial Serial2(2);
|
|||
SdsDustSensor sds(Serial2); |
|||
Adafruit_VEML6075 uv = Adafruit_VEML6075(); |
|||
BH1750 lightMeter; |
|||
|
|||
constexpr unsigned int JSON_BUF_LEN = 512; |
|||
constexpr unsigned int JSON_CAPACITY = JSON_OBJECT_SIZE(16) + 0*JSON_ARRAY_SIZE(2) + 120; |
|||
XD0MQTT mqtt; |
|||
XD0OTA ota("esp32-weatherstation"); |
|||
|
|||
struct __attribute__((packed)) network_t { |
|||
uint32_t ip; |
|||
uint32_t dns; |
|||
uint32_t gateway; |
|||
uint32_t subnet; |
|||
char ssid[64]; |
|||
char password[64]; |
|||
int32_t channel; |
|||
time_t last_dhcp; |
|||
}; |
|||
RTC_DATA_ATTR network_t network; |
|||
|
|||
struct __attribute__((packed)) sensor_readings_t { |
|||
float temperature = NAN; // °C
|
|||
float humidity = NAN; // %H
|
|||
float pressure = NAN; // hPa
|
|||
float pressure_raw = NAN; // Pa
|
|||
uint32_t voc = 0; // Ohm
|
|||
float pm10 = NAN; // µg/m³
|
|||
float pm25 = NAN; // µg/m³
|
|||
float lux = NAN; // lx
|
|||
float uvi = NAN; |
|||
float uva = NAN; |
|||
float uvb = NAN; |
|||
float temperature_max = NAN; // °C
|
|||
float temperature_min = NAN; // °C
|
|||
float voltage = NAN; // V
|
|||
int8_t rssi = 0; // dBm
|
|||
time_t lastUpdate = 0; |
|||
} sensor_readings; |
|||
|
|||
sensor_readings_t sensors_a4cf1211c3e4, sensors_246f28d1fa5c, sensors_246f28d1a080, sensors_246f28d1eff4; |
|||
|
|||
SensorHistory history_pressure(30); |
|||
|
|||
RTC_DATA_ATTR time_t lastDisplayRefresh = 0; |
|||
struct __attribute__((packed)) sensors_active_t { |
|||
bool bme280 = false; |
|||
bool bme680 = false; |
|||
bool uv = false; |
|||
bool light = false; |
|||
bool sds = false; |
|||
}; |
|||
RTC_DATA_ATTR sensors_active_t sensors_active; |
|||
|
|||
float station_height = 0; |
|||
RTC_DATA_ATTR int bootCount = 0; |
|||
|
|||
|
|||
time_t getTimestamp() { |
|||
struct timeval tv; |
|||
gettimeofday(&tv, NULL); |
|||
return tv.tv_sec; |
|||
} |
|||
|
|||
|
|||
float mapf(float x, float in_min, float in_max, float out_min, float out_max) { |
|||
float newval = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; |
|||
if (newval > out_max) newval = out_max; |
|||
if (newval < out_min) newval = out_min; |
|||
return newval; |
|||
} |
|||
|
|||
|
|||
void poweroffDevices() { |
|||
display.powerOff(); |
|||
|
|||
if (sensors_active.bme680) { |
|||
bme680.setGasHeater(0, 0); |
|||
} |
|||
if (sensors_active.bme280) { |
|||
bme280.setSampling(Adafruit_BME280::MODE_SLEEP, |
|||
Adafruit_BME280::SAMPLING_X1, // temperature
|
|||
Adafruit_BME280::SAMPLING_X1, // pressure
|
|||
Adafruit_BME280::SAMPLING_X1, // humidity
|
|||
Adafruit_BME280::FILTER_OFF ); |
|||
} |
|||
if (sensors_active.light) { |
|||
static constexpr byte BH1750_I2CADDR = 0x23; |
|||
Wire.beginTransmission(BH1750_I2CADDR); |
|||
Wire.write(BH1750_POWER_DOWN); |
|||
byte ack = Wire.endTransmission(); |
|||
} |
|||
if (sensors_active.uv) { |
|||
uv.shutdown(true); |
|||
|
|||
} |
|||
|
|||
//if (sensors_active.sds) {
|
|||
// sds.sleep(); // use custom working period instead
|
|||
//}
|
|||
} |
|||
|
|||
|
|||
void gotoSleep(unsigned int sleep_time = TIME_TO_SLEEP) { |
|||
mqtt.end(); |
|||
WiFi.disconnect(); |
|||
WiFi.mode(WIFI_OFF); |
|||
|
|||
poweroffDevices(); |
|||
|
|||
//rtc_gpio_isolate(GPIO_NUM_12);
|
|||
|
|||
esp_sleep_enable_timer_wakeup(sleep_time * 1000000LL); |
|||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF); |
|||
ESP_LOGI(TAG, "going to to sleep for %d seconds", sleep_time); |
|||
Serial.flush(); |
|||
esp_deep_sleep_start(); |
|||
delay(1); |
|||
} |
|||
|
|||
void wifiConnect() { |
|||
WiFi.persistent(false); |
|||
WiFi.setHostname("esp32-weatherstation"); |
|||
|
|||
wifiMulti.addAP(WIFI_SSID, WIFI_PASSWD); |
|||
wifiMulti.addAP(WIFI_SSID2, WIFI_PASSWD2); |
|||
wifiMulti.addAP(WIFI_SSID3, WIFI_PASSWD3); |
|||
|
|||
IPAddress ip = IPAddress(network.ip); |
|||
IPAddress dns = IPAddress(network.dns); |
|||
IPAddress subnet = IPAddress(network.subnet); |
|||
IPAddress gateway = IPAddress(network.gateway); |
|||
|
|||
ESP_LOGD(TAG, "previous dhcp: %lu s ago", getTimestamp() - network.last_dhcp); |
|||
|
|||
if ( ip != INADDR_NONE && dns != INADDR_NONE && gateway != INADDR_NONE && subnet != INADDR_NONE |
|||
&& ((ip[0] == 192 && ip[1] == 168) || (ip[0] == 172 && ip[1] == 16)) |
|||
&& strlen(network.ssid) > 0 && strlen(network.password) > 0 |
|||
&& (getTimestamp() - network.last_dhcp < dhcp_interval) |
|||
) { |
|||
|
|||
ESP_LOGD("WiFi", "STATIC IP"); |
|||
WiFi.config(ip, gateway, subnet, dns); |
|||
WiFi.begin(network.ssid, network.password, network.channel); |
|||
for (int tries=0; WiFi.status() != WL_CONNECTED && tries < 10; tries++) { |
|||
ESP_LOGD("WiFi", "."); |
|||
delay(500); |
|||
} |
|||
} else { |
|||
ESP_LOGD("WiFi", "DHCP"); |
|||
for (int tries=0; wifiMulti.run() != WL_CONNECTED && tries < 20; tries++) { |
|||
ESP_LOGD("WiFi", "."); |
|||
delay(500); |
|||
} |
|||
|
|||
network.ip = (uint32_t)WiFi.localIP(); |
|||
network.dns = (uint32_t)WiFi.dnsIP(); |
|||
network.gateway = (uint32_t)WiFi.gatewayIP(); |
|||
network.subnet = (uint32_t)WiFi.subnetMask(); |
|||
strncpy(network.ssid, WiFi.SSID().c_str(), 64); |
|||
strncpy(network.password, WiFi.psk().c_str(), 64); |
|||
network.channel = WiFi.channel(); |
|||
network.last_dhcp = getTimestamp(); |
|||
} |
|||
|
|||
if(WiFi.status() == WL_CONNECTED) { |
|||
ESP_LOGD("WiFi", "connected"); |
|||
//ESP_LOGD("WiFi", (WiFi.localIP().toString().c_str()));
|
|||
|
|||
} else { |
|||
ESP_LOGE("WiFi", "could not connect to WiFi"); |
|||
ESP_LOGE(TAG, "restarting"); |
|||
ESP.restart(); |
|||
} |
|||
} |
|||
|
|||
bool obtain_time() { |
|||
ESP_LOGI(TAG, "syncing time"); |
|||
configTzTime("CET-1CEST,M3.5.0/2,M10.5.0/3", "de.pool.ntp.org"); |
|||
struct tm timeinfo; |
|||
return getLocalTime(&timeinfo, 5000); |
|||
} |
|||
|
|||
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; |
|||
if (display.epd2.hasFastPartialUpdate) { |
|||
display.setPartialWindow(0, 0, display.width(), display.height()); |
|||
} else { |
|||
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); |
|||
#if defined DRONE_SEMVER |
|||
display.print("Version "); |
|||
#define xstr(s) str(s) |
|||
#define str(s) #s |
|||
display.print(xstr(DRONE_SEMVER)); |
|||
#elif defined FW_VERSION |
|||
display.print("Build "); |
|||
display.print(FW_VERSION); |
|||
#endif |
|||
} |
|||
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 (sensors_active.bme280) { |
|||
bme280.takeForcedMeasurement(); |
|||
sensor_readings.temperature = bme280.readTemperature(); |
|||
sensor_readings.humidity = bme280.readHumidity(); |
|||
sensor_readings.pressure_raw = bme280.readPressure(); |
|||
ESP_LOGI(TAG, "Temperature : %8.2f °C", sensor_readings.temperature); |
|||
ESP_LOGI(TAG, "Pressure (Raw): %8.2f Pa", sensor_readings.pressure_raw); |
|||
ESP_LOGI(TAG, "Humidity : %8.2f %", sensor_readings.humidity); |
|||
} |
|||
if (sensors_active.bme680) { |
|||
bme680.endReading(); // ToDo
|
|||
if (bme680.performReading()) { |
|||
sensor_readings.temperature = bme680.temperature; |
|||
sensor_readings.humidity = bme680.humidity; |
|||
sensor_readings.pressure_raw = bme680.pressure; |
|||
sensor_readings.voc = bme680.gas_resistance; |
|||
ESP_LOGI(TAG, "Temperature : %8.2f °C", sensor_readings.temperature); |
|||
ESP_LOGI(TAG, "Pressure (Raw): %8.2f Pa", sensor_readings.pressure_raw); |
|||
ESP_LOGI(TAG, "Humidity : %8.2f %", sensor_readings.humidity); |
|||
ESP_LOGI(TAG, "VOC : %5lu Ohm", sensor_readings.voc); |
|||
} else { |
|||
ESP_LOGE(TAG, "Failed to perform BME680 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; |
|||
} |
|||
|
|||
// https://de.wikipedia.org/wiki/Barometrische_H%C3%B6henformel#Reduktion_auf_Meeresh%C3%B6he
|
|||
// https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
|
|||
float absolute_humidity = (6.112*exp((17.67*sensor_readings.temperature)/(sensor_readings.temperature+243.5))*(sensor_readings.humidity/100)*18.02)/((273.15+sensor_readings.temperature)*1000*0.08314); |
|||
float pressure_compensation_factor = exp((9.80665/(287.05*(sensor_readings.temperature+273.15+0.12*((absolute_humidity*461.5*(sensor_readings.temperature+273.15)/100)/100)+0.0065*(station_height/2))))*station_height); |
|||
|
|||
// individual sensor variance
|
|||
int pressure_sensor_deviation = 0; |
|||
if (ota.getMAC().equals("246f28d1a080")) { |
|||
pressure_sensor_deviation = -075; |
|||
} else if (ota.getMAC().equals("246f28d1fa5c")) { |
|||
pressure_sensor_deviation = -143; |
|||
} else if (ota.getMAC().equals("a4cf1211c3e4")) { |
|||
} |
|||
|
|||
sensor_readings.pressure = ((sensor_readings.pressure_raw + pressure_sensor_deviation) / 100.0F) * pressure_compensation_factor; |
|||
|
|||
history_pressure.addValue(sensor_readings.pressure); |
|||
|
|||
if (sensors_active.uv) { |
|||
sensor_readings.uvi = uv.readUVI(); |
|||
sensor_readings.uva = uv.readUVA(); |
|||
sensor_readings.uvb = uv.readUVB(); |
|||
ESP_LOGI(TAG, "UVI : %8.2f", sensor_readings.uvi); |
|||
ESP_LOGI(TAG, "UVA : %8.2f", sensor_readings.uva); |
|||
ESP_LOGI(TAG, "UVB : %8.2f", sensor_readings.uvb); |
|||
} |
|||
|
|||
if (sensors_active.light) { |
|||
sensor_readings.lux = lightMeter.readLightLevel(); |
|||
// auto-adjust sensitivity
|
|||
if (sensor_readings.lux < 0) { |
|||
ESP_LOGE(TAG, "Error reading light level"); |
|||
} else if (sensor_readings.lux > 40000.0) { |
|||
if (lightMeter.setMTreg(32)) { |
|||
ESP_LOGD(TAG, "Setting MTReg to low value for high light environment"); |
|||
} |
|||
} else if (sensor_readings.lux <= 10.0) { |
|||
if (lightMeter.setMTreg(138)) { |
|||
ESP_LOGD(TAG, "Setting MTReg to high value for low light environment"); |
|||
} |
|||
} else { // if (sensor_readings.lux > 10.0)
|
|||
if (lightMeter.setMTreg(69)) { |
|||
ESP_LOGD(TAG, "Setting MTReg to default value for normal light environment"); |
|||
} |
|||
} |
|||
ESP_LOGI(TAG, "Lux : %8.2f lx", sensor_readings.lux); |
|||
} |
|||
|
|||
if (sensors_active.sds) { |
|||
PmResult pm = sds.queryPm(); |
|||
if (pm.isOk()) { |
|||
sensor_readings.pm10 = pm.pm10; |
|||
sensor_readings.pm25 = pm.pm25; |
|||
ESP_LOGI(TAG, "PM10 : %8.2f µg/m³", sensor_readings.pm10); |
|||
ESP_LOGI(TAG, "PM2.5 : %8.2f µg/m³", sensor_readings.pm25); |
|||
} |
|||
} |
|||
|
|||
int battery = analogRead(_VBAT); |
|||
sensor_readings.voltage = (battery/4096.0)*2*3.42; |
|||
sensor_readings.rssi = WiFi.RSSI(); |
|||
|
|||
ESP_LOGI(TAG, "RSSI : %5d dBm", sensor_readings.rssi); |
|||
ESP_LOGI(TAG, "Battery : %5d ", battery); |
|||
ESP_LOGI(TAG, "Heap : %5lu", ESP.getFreeHeap()); |
|||
|
|||
sensor_readings.lastUpdate = getTimestamp(); |
|||
} |
|||
|
|||
|
|||
void receiveMqtt(const char* topic, const char* data, int data_len) { |
|||
sensor_readings_t* sensor = NULL; |
|||
|
|||
ESP_LOGI(TAG, "received MQTT message on subscribed topic \"%s\", len: %d", topic, data_len); |
|||
|
|||
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/246f28d1eff4") == topic) { |
|||
sensor = &sensors_246f28d1eff4; |
|||
} |
|||
|
|||
char* topic_last = strrchr(topic, '/'); |
|||
|
|||
if (topic_last) { |
|||
if (strcmp("/json", topic_last) == 0 && sensor) { |
|||
StaticJsonDocument<JSON_CAPACITY+120> jsonDoc; |
|||
DeserializationError err = deserializeJson(jsonDoc, data, data_len); |
|||
if (err) { |
|||
ESP_LOGW(TAG, "Error parsing JSON, code: %s", err.c_str()); |
|||
} else { |
|||
// got json
|
|||
if (jsonDoc.containsKey("temperature")) sensor->temperature = jsonDoc["temperature"].as<float>(); |
|||
if (jsonDoc.containsKey("humidity")) sensor->humidity = jsonDoc["humidity"].as<float>(); |
|||
if (jsonDoc.containsKey("pressure")) sensor->pressure = jsonDoc["pressure"].as<float>(); |
|||
if (jsonDoc.containsKey("voc")) sensor->voc = jsonDoc["voc"].as<uint32_t>(); |
|||
if (jsonDoc.containsKey("lux")) sensor->lux = jsonDoc["lux"].as<float>(); |
|||
if (jsonDoc.containsKey("uvi")) sensor->uvi = jsonDoc["uvi"].as<float>(); |
|||
if (jsonDoc.containsKey("uva")) sensor->uva = jsonDoc["uva"].as<float>(); |
|||
if (jsonDoc.containsKey("uvb")) sensor->uvb = jsonDoc["uvb"].as<float>(); |
|||
if (jsonDoc.containsKey("pm10")) sensor->pm10 = jsonDoc["pm10"].as<float>(); |
|||
if (jsonDoc.containsKey("pm2.5")) sensor->pm25 = jsonDoc["pm2.5"].as<float>(); |
|||
if (jsonDoc.containsKey("voltage")) sensor->voltage = jsonDoc["voltage"].as<float>(); |
|||
if (jsonDoc.containsKey("rssi")) sensor->rssi = jsonDoc["rssi"].as<int8_t>(); |
|||
if (jsonDoc.containsKey("timestamp")) sensor->lastUpdate = jsonDoc["timestamp"].as<time_t>(); |
|||
ESP_LOGI(TAG, "got new values from %s, timestamp: %lu", topic, sensor->lastUpdate); |
|||
ESP_LOGI(TAG, "%lu seconds ago", topic, getTimestamp() - sensor->lastUpdate); |
|||
} |
|||
} else if (strcmp("/command", topic_last) == 0) { |
|||
ESP_LOGW(TAG, "received command"); |
|||
if (data_len == 6 && strncmp("update", data, data_len) == 0) { |
|||
ESP_LOGI(TAG, "update command"); |
|||
int msg_id = mqtt.publish(topic, "", 0, 1, 1); |
|||
mqtt.waitForMsg(msg_id, 5000 / portTICK_PERIOD_MS); |
|||
ota.update(); |
|||
} else { |
|||
ESP_LOGW(TAG, "UNKNOWN COMMAND"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
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"); |
|||
|
|||
ESP_LOGD(TAG, "displayValues()"); |
|||
|
|||
if (display.epd2.hasFastPartialUpdate) { |
|||
display.setPartialWindow(0, 0, display.width(), display.height()); |
|||
} else { |
|||
display.setFullWindow(); |
|||
} |
|||
display.firstPage(); |
|||
do |
|||
{ |
|||
display.fillScreen(GxEPD_WHITE); |
|||
|
|||
// Title
|
|||
display.setCursor(30,y_offset+0); |
|||
display.print(timeStr); |
|||
display.drawLine(0,y_offset+10,display.width(), y_offset+10, GxEPD_BLACK); |
|||
|
|||
// Status
|
|||
display.setFont(NULL); |
|||
display.setCursor(190,y_offset+0); |
|||
if(WiFi.status() == WL_CONNECTED) { |
|||
display.print("W"); |
|||
} else { |
|||
display.print("_"); |
|||
} |
|||
if (sensor_readings.voltage <= 4.0F) { |
|||
display.setCursor(220,y_offset+0); |
|||
int battery_percent = mapf(sensor_readings.voltage, 3.3, 4.1, 0, 100); |
|||
display.printf("%3d", battery_percent); |
|||
display.print(" \%"); |
|||
} |
|||
|
|||
// Temp
|
|||
display.drawRect(0,y_offset+10,64,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(63,y_offset+10,58,50,GxEPD_BLACK); |
|||
display.setFont(NULL); |
|||
display.setCursor(68,y_offset+15); |
|||
display.print("Humidity"); |
|||
display.setFont(&FreeSansBold9pt7b); |
|||
display.setCursor(68,y_offset+40); |
|||
display.printf("%.1f", sensor_readings.humidity); |
|||
display.setFont(NULL); |
|||
display.print(" \%"); |
|||
display.setCursor(68,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(120,y_offset+10,66,50,GxEPD_BLACK); |
|||
display.setFont(NULL); |
|||
display.setCursor(125,y_offset+15); |
|||
display.print("Pressure"); |
|||
display.setFont(&FreeSansBold9pt7b); |
|||
display.setCursor(125,y_offset+40); |
|||
display.printf("%.1f", sensor_readings.pressure); |
|||
display.setFont(NULL); |
|||
//display.print(" hPa");
|
|||
float pressure_diff = history_pressure.getElement(0) - history_pressure.getFirst(); |
|||
display.setCursor(125,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(185,y_offset+10,250-186+1,122-10,GxEPD_BLACK); |
|||
display.setFont(NULL); |
|||
// VOC
|
|||
display.setCursor(190,y_offset+15); |
|||
display.println("-- VOC --"); |
|||
display.setCursor(190,y_offset+25); |
|||
display.printf("%.1f k\xe9", sensor_readings.voc / 1000.0F); |
|||
// PM
|
|||
float pm10, pm25; |
|||
if (sensors_active.sds) { |
|||
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(190,y_offset+37); |
|||
display.println("-- PM --"); |
|||
display.setCursor(190,y_offset+47); |
|||
display.printf("%.1f", pm10); |
|||
display.setCursor(220,y_offset+47); |
|||
display.printf("%.1f", pm25); |
|||
// Lux
|
|||
float lux; |
|||
if (sensors_active.light) { |
|||
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(190,y_offset+59); |
|||
display.println("-- Lux --"); |
|||
display.setCursor(190,y_offset+69); |
|||
display.printf("%.1f lx", lux); |
|||
// UV
|
|||
float uvi, uva, uvb; |
|||
if (sensors_active.uv) { |
|||
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(190,y_offset+80); |
|||
display.println("UV(I/A/B):"); |
|||
display.setCursor(190,y_offset+90); |
|||
display.printf("%.1f", uvi); |
|||
display.setCursor(190,y_offset+100); |
|||
display.printf("%.1f", uva); |
|||
display.setCursor(190,y_offset+110); |
|||
display.printf("%.1f", uvb); |
|||
|
|||
// other nodes
|
|||
display.setFont(NULL); |
|||
display.setCursor(0, y_offset+70); |
|||
if (!ota.getMAC().equals("246f28d1fa5c") && getTimestamp() - sensors_246f28d1fa5c.lastUpdate < 15*60) { |
|||
display.printf("246f28d1fa5c: %4.1f %4.1f %6.1f\n", sensors_246f28d1fa5c.temperature, sensors_246f28d1fa5c.humidity, sensors_246f28d1fa5c.pressure); |
|||
} |
|||
if (!ota.getMAC().equals("a4cf1211c3e4") && getTimestamp() - sensors_a4cf1211c3e4.lastUpdate < 15*60) { |
|||
display.printf("a4cf1211c3e4: %4.1f %4.1f %6.1f\n", sensors_a4cf1211c3e4.temperature, sensors_a4cf1211c3e4.humidity, sensors_a4cf1211c3e4.pressure); |
|||
} |
|||
if (!ota.getMAC().equals("246f28d1a080") && getTimestamp() - sensors_246f28d1a080.lastUpdate < 15*60) { |
|||
display.printf("246f28d1a080: %4.1f %4.1f %6.1f\n", sensors_246f28d1a080.temperature, sensors_246f28d1a080.humidity, sensors_246f28d1a080.pressure); |
|||
} |
|||
if (!ota.getMAC().equals("246f28d1eff4") && getTimestamp() - sensors_246f28d1eff4.lastUpdate < 15*60) { |
|||
display.printf("246f28d1eff4: %4.1f %4.1f %6.1f\n", sensors_246f28d1eff4.temperature, sensors_246f28d1eff4.humidity, sensors_246f28d1eff4.pressure); |
|||
} |
|||
|
|||
} |
|||
while (display.nextPage()); |
|||
|
|||
display.powerOff(); |
|||
} |
|||
|
|||
|
|||
void sendValues() { |
|||
for (int tries=0; mqtt.isConnected() == false && tries < 10; tries++) { |
|||
ESP_LOGD(TAG, "waiting for mqtt connection"); |
|||
delay(300); |
|||
} |
|||
|
|||
/* send values MQTT JSON */ |
|||
char buf[JSON_BUF_LEN]; |
|||
StaticJsonDocument<JSON_CAPACITY> jsonDoc; |
|||
if (sensors_active.bme280 || sensors_active.bme680) { |
|||
jsonDoc["temperature"] = sensor_readings.temperature; |
|||
jsonDoc["humidity"] = sensor_readings.humidity; |
|||
jsonDoc["pressure"] = sensor_readings.pressure; |
|||
} |
|||
if (sensors_active.bme680) { |
|||
jsonDoc["voc"] = sensor_readings.voc; |
|||
} |
|||
if (sensors_active.light) { |
|||
jsonDoc["lux"] = sensor_readings.lux; |
|||
} |
|||
if (sensors_active.uv) { |
|||
jsonDoc["uvi"] = sensor_readings.uvi; |
|||
jsonDoc["uva"] = sensor_readings.uva; |
|||
jsonDoc["uvb"] = sensor_readings.uvb; |
|||
} |
|||
if (sensors_active.sds) { |
|||
jsonDoc["pm10"] = sensor_readings.pm10; |
|||
jsonDoc["pm2.5"] = sensor_readings.pm25; |
|||
} |
|||
jsonDoc["voltage"] = sensor_readings.voltage; |
|||
jsonDoc["rssi"] = sensor_readings.rssi; |
|||
jsonDoc["timestamp"] = sensor_readings.lastUpdate; |
|||
serializeJson(jsonDoc, buf, JSON_BUF_LEN); |
|||
String topic_json = String("thomas/sensor/") + ota.getMAC() + String("/json"); |
|||
int msg_id = mqtt.publish(topic_json.c_str(), buf, strlen(buf), 1, 1); |
|||
mqtt.waitForMsg(msg_id, 5000 / portTICK_PERIOD_MS); |
|||
|
|||
} |
|||
|
|||
/**
|
|||
* \brief Setup function |
|||
* |
|||
* is run once on startup |
|||
*/ |
|||
void setup() |
|||
{ |
|||
Serial.begin(115200); |
|||
Serial2.begin(9600, SERIAL_8N1, /*rx*/15, /*tx*/2); // IMPORTANT: don't run with default pins 16, 17 as they are connected to PSRAM on boards that ship with it
|
|||
|
|||
esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause(); |
|||
++bootCount; |
|||
ESP_LOGI(TAG, "Boot number: %d", bootCount); |
|||
|
|||
ESP_LOGD(TAG, "setup hardware and sensors"); |
|||
|
|||
// initialize LED digital pin as an output.
|
|||
pinMode(LED_BUILTIN, OUTPUT); |
|||
digitalWrite(LED_BUILTIN, HIGH); |
|||
|
|||
pinMode(_VBAT, INPUT); |
|||
analogReadResolution(12); |
|||
analogSetAttenuation(ADC_11db); |
|||
adcAttachPin(_VBAT); |
|||
adcStart(_VBAT); |
|||
|
|||
Serial.print("MAC Address: "); |
|||
Serial.println(ota.getMAC()); |
|||
|
|||
#define BME_SDA 21 |
|||
#define BME_SCL 22 |
|||
Wire.begin(BME_SDA, BME_SCL); |
|||
if (bme280.begin()) { |
|||
sensors_active.bme280 = true; |
|||
} else { |
|||
ESP_LOGE(TAG, "Could not find a valid BME280 sensor, check wiring!"); |
|||
} |
|||
|
|||
if (bme680.begin()) { |
|||
sensors_active.bme680 = 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
|
|||
bme680.beginReading(); |
|||
} else { |
|||
ESP_LOGE(TAG, "Could not find a valid BME680 sensor, check wiring!"); |
|||
} |
|||
|
|||
if (bmp280.begin()) { |
|||
Serial.println("BMP280 found"); |
|||
} |
|||
|
|||
if (uv.begin()) { |
|||
sensors_active.uv = 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 { |
|||
ESP_LOGW(TAG, "Failed to communicate with VEML6075 sensor, check wiring?"); |
|||
} |
|||
|
|||
if (lightMeter.begin()) { |
|||
sensors_active.light = true; |
|||
lightMeter.setMTreg((byte) BH1750_DEFAULT_MTREG); |
|||
} else { |
|||
ESP_LOGW(TAG, "Failed to communicate with BH1750 sensor, check wiring?"); |
|||
} |
|||
|
|||
//sds.begin(); // don't call begin, only messes with Serial
|
|||
|
|||
if (wakeup_reason == ESP_SLEEP_WAKEUP_UNDEFINED || bootCount == 1) { |
|||
FirmwareVersionResult sds_fw = sds.queryFirmwareVersion(); |
|||
if (sds_fw.isOk()) { |
|||
sensors_active.sds = true; |
|||
|
|||
sds.setQueryReportingMode(); // ensures sensor is in 'query' reporting mode
|
|||
sds.setCustomWorkingPeriod(5); // sensor sends data every 3 minutes
|
|||
} else { |
|||
ESP_LOGW(TAG, "Failed to communicate with SDS011 sensor, check wiring?"); |
|||
} |
|||
} |
|||
|
|||
// initialize e-paper display
|
|||
SPI.begin(18, 19, 23, TFT_CS); // MISO is not connected to TFT_MISO!
|
|||
display.init(0, false, false); |
|||
display.setRotation(1); |
|||
|
|||
if (wakeup_reason == ESP_SLEEP_WAKEUP_UNDEFINED || bootCount == 1) { |
|||
// wakeup not caused by deep sleep
|
|||
display.clearScreen(); |
|||
display.refresh(); |
|||
lastDisplayRefresh = getTimestamp(); |
|||
helloWorld(); |
|||
display.powerOff(); |
|||
} else { |
|||
// wakeup by deep sleep
|
|||
// displayValues();
|
|||
} |
|||
|
|||
ESP_LOGD(TAG, "connecting to WiFi"); |
|||
|
|||
wifiConnect(); |
|||
|
|||
WiFi.waitForConnectResult(); |
|||
displayIcoPartial(ico_wifi16, display.width()-20, y_offset+0, ico_wifi16_width, ico_wifi16_height); |
|||
|
|||
if (wakeup_reason == ESP_SLEEP_WAKEUP_UNDEFINED || bootCount == 1) { |
|||
// wakeup not caused by deep sleep
|
|||
obtain_time(); |
|||
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/json", receiveMqtt); |
|||
if (!ota.getMAC().equals("246f28d1fa5c")) mqtt.subscribe("thomas/sensor/246f28d1fa5c/json", receiveMqtt); |
|||
if (!ota.getMAC().equals("246f28d1a080")) mqtt.subscribe("thomas/sensor/246f28d1a080/json", receiveMqtt); |
|||
if (!ota.getMAC().equals("246f28d1eff4")) mqtt.subscribe("thomas/sensor/246f28d1eff4/json", receiveMqtt); |
|||
|
|||
static String topic_command = String("thomas/sensor/") + ota.getMAC() + String("/command"); |
|||
mqtt.subscribe(topic_command.c_str(), receiveMqtt); |
|||
|
|||
if (WiFi.SSID() == "LNet") { |
|||
station_height = 135; |
|||
} else if (WiFi.SSID() == "Galaktisches Imperium") { |
|||
station_height = 30; |
|||
} else if (WiFi.SSID() == "nether.net") { |
|||
station_height = 111; |
|||
} |
|||
|
|||
ESP_LOGD(TAG, "setup done"); |
|||
} |
|||
|
|||
/**
|
|||
* \brief Arduino main loop |
|||
*/ |
|||
void loop() |
|||
{ |
|||
/* if(wifiMulti.run() != WL_CONNECTED) {
|
|||
Serial.println("WiFi not connected!"); |
|||
delay(1000); |
|||
}*/ |
|||
|
|||
/* Do a full refresh every hour */ |
|||
if (getTimestamp() - lastDisplayRefresh >= 60*60) { |
|||
lastDisplayRefresh = getTimestamp(); |
|||
display.clearScreen(); |
|||
display.refresh(); |
|||
} |
|||
|
|||
getSensorMeasurements(); |
|||
if (!mqtt.waitForConnectResult(5000)) { |
|||
ESP_LOGE(TAG, "failed to establish full connection"); |
|||
} |
|||
sendValues(); |
|||
delay(1); |
|||
displayValues(); |
|||
|
|||
int runtime = millis()/1000; |
|||
if (runtime < 0 || runtime >= TIME_TO_SLEEP) runtime = 0; |
|||
|
|||
gotoSleep(TIME_TO_SLEEP - runtime); |
|||
delay(2000); |
|||
} |
@ -0,0 +1,255 @@ |
|||
#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); |
|||
if (msg_id == -1) { |
|||
ESP_LOGE(TAG, "Failed subscribing to \"%s\"", subscription.topic); |
|||
} else { |
|||
connectWaitingForMsg.push_back(msg_id); |
|||
ESP_LOGI(TAG, "sent subscribe, 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); |
|||
connectWaitingForMsg.erase(std::remove(connectWaitingForMsg.begin(), connectWaitingForMsg.end(), event->msg_id), connectWaitingForMsg.end()); |
|||
wakeTask(event->msg_id); |
|||
break; |
|||
case MQTT_EVENT_UNSUBSCRIBED: |
|||
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); |
|||
wakeTask(event->msg_id); |
|||
break; |
|||
case MQTT_EVENT_PUBLISHED: |
|||
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); |
|||
wakeTask(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_) { |
|||
char topic[128]; |
|||
strncpy(topic, event->topic, event->topic_len); topic[event->topic_len] = '\0'; |
|||
// literal match
|
|||
if (event->topic_len == strlen(subscription.topic) && strncmp(event->topic, subscription.topic, event->topic_len) == 0) { |
|||
subscription.cb(topic, event->data, event->data_len); |
|||
// '#' wildcard
|
|||
} else if (subscription.topic[strlen(subscription.topic)-1] == '#'){ |
|||
if (event->topic_len >= strlen(subscription.topic) && strncmp(event->topic, subscription.topic, strlen(subscription.topic)-1) == 0) { |
|||
subscription.cb(topic, event->data, event->data_len); |
|||
} |
|||
} |
|||
// 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_LOGD(TAG, "[APP] Free memory: %d bytes", esp_get_free_heap_size()); |
|||
ESP_LOGI(TAG, "BROKER URI: %s\n", MQTT_BROKER_URI); |
|||
ESP_LOGI(TAG, "MQTT USERNAME: %s\n", MQTT_USERNAME); |
|||
ESP_LOGI(TAG, "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; |
|||
} |
|||
|
|||
void XD0MQTT::end(void) { |
|||
//for (const auto &subscription : subscriptions_) {
|
|||
// int msg_id = esp_mqtt_client_unsubscribe(client, subscription.topic);
|
|||
// ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id);
|
|||
//}
|
|||
subscriptions_.clear(); |
|||
esp_mqtt_client_stop(client); |
|||
waitingForMsg.clear(); |
|||
delay(10); |
|||
esp_mqtt_client_destroy(client); |
|||
} |
|||
|
|||
bool XD0MQTT::isConnected(void) { |
|||
return connected; |
|||
} |
|||
|
|||
int 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); |
|||
return msg_id; |
|||
} |
|||
|
|||
bool XD0MQTT::publishf(const char* topic, const char* format, ...) { |
|||
va_list argptr; |
|||
va_start(argptr, format); |
|||
bool ret = publishf2(topic, 1, 0, format, argptr); |
|||
va_end(argptr); |
|||
return ret; |
|||
|
|||
} |
|||
|
|||
bool XD0MQTT::publishf2(const char* topic, int qos, int retain, const char* format, ...) { |
|||
va_list argptr; |
|||
va_start(argptr, format); |
|||
bool ret = publishf2(topic, qos, retain, format, argptr); |
|||
va_end(argptr); |
|||
return ret; |
|||
} |
|||
|
|||
bool XD0MQTT::publishf2(const char* topic, int qos, int retain, const char* format, va_list argp) { |
|||
char data_str[64]; |
|||
vsprintf(data_str, format, argp); |
|||
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 cb_t &cb, int qos) { |
|||
subscription_t subscription = {topic, cb, qos}; |
|||
subscriptions_.push_back(subscription); |
|||
|
|||
if (connected) { |
|||
int msg_id = esp_mqtt_client_subscribe(client, subscription.topic, subscription.qos); |
|||
if (msg_id == -1) { |
|||
ESP_LOGE(TAG, "Failed subscribing to \"%s\"", subscription.topic); |
|||
return false; |
|||
} |
|||
} |
|||
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; |
|||
} |
|||
|
|||
bool XD0MQTT::waitForMsg(const int msgid, TickType_t delay) { |
|||
uint32_t ulNotifiedValue; BaseType_t ret; |
|||
waitingForMsg.insert(std::make_pair(msgid, xTaskGetCurrentTaskHandle())); |
|||
ret = xTaskNotifyWait( 0x00, // don't clear notification bits on entry
|
|||
ULONG_MAX, // clear all notification bits on exit
|
|||
&ulNotifiedValue, |
|||
delay ); |
|||
auto p = waitingForMsg.equal_range(msgid); |
|||
for (auto& it = p.first; it != p.second; ++it) { |
|||
if (it->second == xTaskGetCurrentTaskHandle()) { |
|||
waitingForMsg.erase(it); |
|||
break; |
|||
} |
|||
} |
|||
if (ret == pdFALSE) { |
|||
ESP_LOGI(TAG, "waitForMsg(): msg_id=%d timeout", msgid); |
|||
return false; |
|||
} else if (ulNotifiedValue == msgid) { |
|||
ESP_LOGI(TAG, "waitForMsg(): msg_id=%d successfully delivered", msgid); |
|||
return true; |
|||
} else { |
|||
ESP_LOGW(TAG, "waitForMsg(): msg_id=%d received unexpected notification value", msgid); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
bool XD0MQTT::wakeTask(const int msgid) { |
|||
int woken = 0; |
|||
auto p = waitingForMsg.equal_range(msgid); |
|||
for (auto& it = p.first; it != p.second; ++it) { |
|||
TaskHandle_t xTaskToNotify = it->second; |
|||
configASSERT( xTaskToNotify != NULL ); |
|||
if( xTaskNotify(xTaskToNotify, (uint32_t)msgid, eSetValueWithoutOverwrite) == pdPASS ) { |
|||
woken++; |
|||
} else { |
|||
ESP_LOGW(TAG, "woke task waiting for msg_id=%d, but it had other pending notifications", msgid); |
|||
} |
|||
} |
|||
if (woken > 0) return true; |
|||
return false; |
|||
} |
|||
|
|||
bool XD0MQTT::waitForConnectResult(int delay_ms) { |
|||
int start_time = millis(); |
|||
while(!isConnected() || connectWaitingForMsg.size() > 0) { |
|||
delay(10); |
|||
if (millis() - start_time >= delay_ms) { |
|||
ESP_LOGI(TAG, "mqtt still waiting for connection (%d subscriptions pending)\n", connectWaitingForMsg.size()); |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
@ -0,0 +1,76 @@ |
|||
/**
|
|||
* @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 <map> |
|||
|
|||
#include "mqtt_client.h" |
|||
|
|||
typedef std::function<void(char*, char*, int)> cb_t; |
|||
|
|||
struct subscription_t { |
|||
const char* topic; |
|||
cb_t cb; |
|||
int qos; |
|||
}; |
|||
|
|||
class XD0MQTT { |
|||
public: |
|||
XD0MQTT(void); |
|||
bool begin(void); |
|||
void end(void); |
|||
int publish(const char* topic, const char* data, int len, int qos=1, int retain=0); |
|||
bool publishf(const char* topic, const char* format, ...); |
|||
bool publishf2(const char* topic, int qos, int retain, const char* format, ...); |
|||
bool publishf2(const char* topic, int qos, int retain, const char* format, va_list argp); |
|||
bool subscribe(const char* topic, const 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 isConnected(void); |
|||
bool waitForConnectResult(int delay_ms = -1); |
|||
bool waitForMsg(const int msgid, TickType_t delay); |
|||
std::vector<subscription_t> subscriptions_; |
|||
private: |
|||
bool connected = false; |
|||
std::multimap<int, TaskHandle_t> waitingForMsg; |
|||
std::vector<int> connectWaitingForMsg; |
|||
|
|||
bool wakeTask(const int msgid); |
|||
// 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 */ |
@ -0,0 +1,138 @@ |
|||
#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, "de.pool.ntp.org", "time.nist.gov"); // UTC
|
|||
|
|||
ESP_LOGI(TAG, "Waiting for NTP time sync: "); |
|||
time_t now = time(nullptr); |
|||
int tries = 0; |
|||
while (now < 8 * 3600 * 2) { |
|||
yield(); |
|||
delay(500); |
|||
ESP_LOGI(TAG, "."); |
|||
now = time(nullptr); |
|||
tries++; |
|||
if (tries>15) return; |
|||
} |
|||
|
|||
struct tm timeinfo; |
|||
gmtime_r(&now, &timeinfo); |
|||
ESP_LOGI(TAG, "Current time: %s", asctime(&timeinfo)); |
|||
|
|||
// setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1);
|
|||
// tzset();
|
|||
// localtime_r(&now, &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; |
|||
} |
@ -0,0 +1,97 @@ |
|||
/**
|
|||
* @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> |
|||
|
|||
#ifndef FW_VERSION |
|||
#define FW_VERSION 0 |
|||
#endif |
|||
|
|||
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 = "0D 65 0C 18 81 95 32 48 5D FA 25 9F D5 AD AC 7B FB 70 0F 8A"; |
|||
const char* rootCACertificate = \ |
|||
"-----BEGIN CERTIFICATE-----\n" \ |
|||
"MIIDyzCCArOgAwIBAgIUJsJfCOA4jhaFLssvx2Qyn8Zz6XUwDQYJKoZIhvcNAQEL\n" \ |
|||
"BQAwdDELMAkGA1UEBhMCREUxHzAdBgNVBAgMFk5vcnRoIFJoaW5lLVdlc3RwaGFs\n" \ |
|||
"aWExDzANBgNVBAoMBnhkMC5kZTEYMBYGA1UEAwwPZnd1cGRhdGUueGQwLmRlMRkw\n" \ |
|||
"FwYJKoZIhvcNAQkBFgpkZXZAeGQwLmRlMCAXDTIwMDEwNDE3NTQwN1oYDzIwNzAw\n" \ |
|||
"MTA0MTc1NDA3WjB0MQswCQYDVQQGEwJERTEfMB0GA1UECAwWTm9ydGggUmhpbmUt\n" \ |
|||
"V2VzdHBoYWxpYTEPMA0GA1UECgwGeGQwLmRlMRgwFgYDVQQDDA9md3VwZGF0ZS54\n" \ |
|||
"ZDAuZGUxGTAXBgkqhkiG9w0BCQEWCmRldkB4ZDAuZGUwggEiMA0GCSqGSIb3DQEB\n" \ |
|||
"AQUAA4IBDwAwggEKAoIBAQCnXe8mQJWZSfFPNyYhUwWrjcPmWFjPzEiGFgRrvoev\n" \ |
|||
"MELoy0jbk5axEc9ovitmKmWwujYYn/xOfGyLN5Y7B0AJPPyWLh7KyvNO3QhyhlWi\n" \ |
|||
"DgEag5qtvu3DFqjF0pCO3mlf1TBcfvoEE9t9xpNB05Msq1w8sSuBPHk//6k0wgRz\n" \ |
|||
"5/3zthKXYhVykjMHIt318vQ3n44jum+BMb03yo+LwRg5vginPvXn9DAE12KEyXXm\n" \ |
|||
"s0nFj0qozhECzuNWMLGq91b/LOGOnnsNHgWII8TMRdaSvjCJY4AvIugBV5grDqrB\n" \ |
|||
"/zuL8L0Nr7ZDdD4MGrj6a91ZYG35r5TLgpNdqpQLWEnXAgMBAAGjUzBRMB0GA1Ud\n" \ |
|||
"DgQWBBQLHEb4HI9aDQyBL6bgzu8O6qmw5DAfBgNVHSMEGDAWgBQLHEb4HI9aDQyB\n" \ |
|||
"L6bgzu8O6qmw5DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAv\n" \ |
|||
"esVySFbV0TOf8BRLdzVabVA/viOKRiz3vfHckdO09ZZteiK9Z3yEgq0J4YhD7ode\n" \ |
|||
"ecbkpUt8MtU6zpYxpWYhuOTLUAF6/gcYWSTh6PmLEHUh31Zq4MuDCDoFMUMAv2hr\n" \ |
|||
"Xpb7yY8Zan0xZO8nm0l4LewM4oB64oOHFqVuYbqIm58AAMkIb/pnw4oaAKdXvMVV\n" \ |
|||
"zG3/sD+UdQOzDhgRaC4v0kTLpnexLldxcDuwpPRjrNZbb+bdS8BZWz855jiE0kmk\n" \ |
|||
"iOn7GveUb8VYEjcSCeOSa8DUmX3w553qunLkSZJPnSr5RCxZlRVRMHEOKg2YfEm/\n" \ |
|||
"fF8Uktfoam4FUPXaO++n\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; |
|||
} |
|||
|
|||
*/ |
@ -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…
Reference in new issue