diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6efa92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +owm_credentials.* +secrets.* \ No newline at end of file diff --git a/examples/Waveshare_7_5_T7_Sensors/LocalInterface.cpp b/examples/Waveshare_7_5_T7_Sensors/LocalInterface.cpp new file mode 100644 index 0000000..3c29e92 --- /dev/null +++ b/examples/Waveshare_7_5_T7_Sensors/LocalInterface.cpp @@ -0,0 +1,341 @@ +/////////////////////////////////////////////////////////////////////////////// +// LocalInterface.h +// +// Local Sensor Data Interface for ESP32-e-Paper-Weather-Display +// +// +// created: 10/2024 +// +// +// MIT License +// +// Copyright (c) 2024 Matthias Prinke +// +// 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. +// +// +// History: +// +// 20241010 Extracted from Waveshare_7_5_T7_Sensors.ino +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#include "LocalInterface.h" + +extern bool TouchTriggered(); + +extern local_sensors_t LocalSensors; + +#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) +static const int bleScanTime = 31; //!< BLE scan time in seconds +static std::vector knownBLEAddresses = KNOWN_BLE_ADDRESSES; +#endif + +#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) +static NimBLEScan *pBLEScan; +#endif + +#ifdef THEENGSDECODER_EN +class MyAdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks +{ + + int m_devices_found = 0; //!< Number of known devices found + + std::string convertServiceData(std::string deviceServiceData) + { + int serviceDataLength = (int)deviceServiceData.length(); + char spr[2 * serviceDataLength + 1]; + for (int i = 0; i < serviceDataLength; i++) + sprintf(spr + 2 * i, "%.2x", (unsigned char)deviceServiceData[i]); + spr[2 * serviceDataLength] = 0; + return spr; + } + + void onResult(BLEAdvertisedDevice *advertisedDevice) + { + TheengsDecoder decoder; + bool device_found = false; + unsigned idx; + JsonDocument doc; + + log_v("Advertised Device: %s", advertisedDevice->toString().c_str()); + JsonObject BLEdata = doc.to(); + String mac_adress = advertisedDevice->getAddress().toString().c_str(); + + BLEdata["id"] = (char *)mac_adress.c_str(); + for (idx = 0; idx < knownBLEAddresses.size(); idx++) + { + if (mac_adress == knownBLEAddresses[idx].c_str()) + { + log_v("BLE device found at index %d", idx); + device_found = true; + break; + } + } + + if (advertisedDevice->haveName()) + BLEdata["name"] = (char *)advertisedDevice->getName().c_str(); + + if (advertisedDevice->haveManufacturerData()) + { + char *manufacturerdata = BLEUtils::buildHexData(NULL, (uint8_t *)advertisedDevice->getManufacturerData().data(), advertisedDevice->getManufacturerData().length()); + BLEdata["manufacturerdata"] = manufacturerdata; + free(manufacturerdata); + } + + if (advertisedDevice->haveRSSI()) + BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); + + if (advertisedDevice->haveTXPower()) + BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); + + if (advertisedDevice->haveServiceData()) + { + int serviceDataCount = advertisedDevice->getServiceDataCount(); + for (int j = 0; j < serviceDataCount; j++) + { + std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); + BLEdata["servicedata"] = (char *)service_data.c_str(); + std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); + BLEdata["servicedatauuid"] = (char *)serviceDatauuid.c_str(); + } + } + + if (decoder.decodeBLEJson(BLEdata) && device_found) + { + if (CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG) + { + char buf[512]; + serializeJson(BLEdata, buf); + log_d("TheengsDecoder found device: %s", buf); + } + BLEdata.remove("manufacturerdata"); + BLEdata.remove("servicedata"); + + LocalSensors.ble_thsensor[idx].temperature = (float)BLEdata["tempc"]; + LocalSensors.ble_thsensor[idx].humidity = (float)BLEdata["hum"]; + LocalSensors.ble_thsensor[idx].batt_level = (uint8_t)BLEdata["batt"]; + LocalSensors.ble_thsensor[idx].rssi = (int)BLEdata["rssi"]; + LocalSensors.ble_thsensor[idx].valid = (LocalSensors.ble_thsensor[idx].batt_level > 0); + log_i("Temperature: %.1f°C", LocalSensors.ble_thsensor[idx].temperature); + log_i("Humidity: %.1f%%", LocalSensors.ble_thsensor[idx].humidity); + log_i("Battery level: %d%%", LocalSensors.ble_thsensor[idx].batt_level); + log_i("RSSI %ddBm", LocalSensors.ble_thsensor[idx].rssi = (int)BLEdata["rssi"]); + m_devices_found++; + log_d("BLE devices found: %d", m_devices_found); + } + + // Abort scanning by touch sensor + if (TouchTriggered()) + { + log_i("Touch interrupt!"); + pBLEScan->stop(); + } + + // Abort scanning because all known devices have been found + if (m_devices_found == knownBLEAddresses.size()) + { + log_i("All devices found."); + pBLEScan->stop(); + } + } +}; +#endif + +// Get local sensor data +void LocalInterface::GetLocalData(void) +{ + LocalSensors.ble_thsensor[0].valid = false; + LocalSensors.i2c_thpsensor[0].valid = false; + LocalSensors.i2c_co2sensor.valid = false; + +#if defined(SCD4X_EN) || defined(BME280_EN) + TwoWire myWire = TwoWire(0); + myWire.begin(I2C_SDA, I2C_SCL, 100000); +#endif + +#ifdef SCD4X_EN + SensirionI2CScd4x scd4x; + + uint16_t error; + char errorMessage[256]; + + scd4x.begin(myWire); + + // stop potential previously started measurement + error = scd4x.stopPeriodicMeasurement(); + if (error) + { + errorToString(error, errorMessage, 256); + log_e("Error trying to execute stopPeriodicMeasurement(): %s", errorMessage); + } + + // Start Measurement + error = scd4x.measureSingleShot(); + if (error) + { + errorToString(error, errorMessage, 256); + log_e("Error trying to execute measureSingleShot(): %s", errorMessage); + } + + log_d("First measurement takes ~5 sec..."); +#endif + +#ifdef MITHERMOMETER_EN + // Setup BLE Temperature/Humidity Sensors + ATC_MiThermometer miThermometer(knownBLEAddresses); //!< Mijia Bluetooth Low Energy Thermo-/Hygrometer + miThermometer.begin(); + + // Set sensor data invalid + miThermometer.resetData(); + + // Get sensor data - run BLE scan for + miThermometer.getData(bleScanTime); + + if (miThermometer.data[0].valid) + { + LocalSensors.ble_thsensor[0].valid = true; + LocalSensors.ble_thsensor[0].temperature = miThermometer.data[0].temperature / 100.0; + LocalSensors.ble_thsensor[0].humidity = miThermometer.data[0].humidity / 100.0; + LocalSensors.ble_thsensor[0].batt_level = miThermometer.data[0].batt_level; + } + miThermometer.clearScanResults(); +#endif + +#ifdef THEENGSDECODER_EN + + // From https://github.com/theengs/decoder/blob/development/examples/ESP32/ScanAndDecode/ScanAndDecode.ino: + // MyAdvertisedDeviceCallbacks are still triggered multiple times; this makes keeping track of received + // sensors difficult. Setting ScanFilterMode to CONFIG_BTDM_SCAN_DUPL_TYPE_DATA_DEVICE seems to + // restrict callback invocation to once per device as desired. + // NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DEVICE); + NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA_DEVICE); + NimBLEDevice::setScanDuplicateCacheSize(200); + NimBLEDevice::init(""); + + pBLEScan = NimBLEDevice::getScan(); // create new scan + // Set the callback for when devices are discovered, no duplicates. + pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks(), false); + pBLEScan->setActiveScan(true); // Set active scanning, this will get more data from the advertiser. + pBLEScan->setInterval(97); // How often the scan occurs / switches channels; in milliseconds, + pBLEScan->setWindow(37); // How long to scan during the interval; in milliseconds. + pBLEScan->setMaxResults(0); // do not store the scan results, use callback only. + pBLEScan->start(bleScanTime, false /* is_continue */); +#endif + + if (LocalSensors.ble_thsensor[0].valid) + { + log_d("Outdoor Air Temp.: % 3.1f °C", LocalSensors.ble_thsensor[0].temperature); + log_d("Outdoor Humidity: %3.1f %%", LocalSensors.ble_thsensor[0].humidity); + log_d("Outdoor Sensor Batt: %d %%", LocalSensors.ble_thsensor[0].batt_level); + } + else + { + log_d("Outdoor Air Temp.: --.- °C"); + log_d("Outdoor Humidity: -- %%"); + log_d("Outdoor Sensor Batt: -- %%"); + } + +#ifdef BME280_EN + pocketBME280 bme280; + bme280.setAddress(0x77); + log_v("BME280: start"); + if (bme280.begin(myWire)) + { + LocalSensors.i2c_thpsensor[0].valid = true; + bme280.startMeasurement(); + while (!bme280.isMeasuring()) + { + log_v("BME280: Waiting for Measurement to start"); + delay(1); + } + while (bme280.isMeasuring()) + { + log_v("BME280: Measurement in progress"); + delay(1); + } + LocalSensors.i2c_thpsensor[0].temperature = bme280.getTemperature() / 100.0; + LocalSensors.i2c_thpsensor[0].pressure = bme280.getPressure() / 100.0; + LocalSensors.i2c_thpsensor[0].humidity = bme280.getHumidity() / 1024.0; + log_d("Indoor Temperature: %.1f °C", LocalSensors.i2c_thpsensor[0].temperature); + log_d("Indoor Pressure: %.0f hPa", LocalSensors.i2c_thpsensor[0].pressure); + log_d("Indoor Humidity: %.0f %%rH", LocalSensors.i2c_thpsensor[0].humidity); + } + else + { + log_d("Indoor Temperature: --.- °C"); + log_d("Indoor Pressure: -- hPa"); + log_d("Indoor Humidity: -- %%rH"); + } +#endif + +#ifdef SCD4X_EN + if (LocalSensors.i2c_thpsensor[0].valid) + { + error = scd4x.setAmbientPressure((uint16_t)LocalSensors.i2c_thpsensor[0].pressure); + if (error) + { + errorToString(error, errorMessage, 256); + log_e("Error trying to execute setAmbientPressure(): %s", errorMessage); + } + } + // Read Measurement + bool isDataReady = false; + + for (int i = 0; i < 50; i++) + { + error = scd4x.getDataReadyFlag(isDataReady); + if (error) + { + errorToString(error, errorMessage, 256); + log_e("Error trying to execute getDataReadyFlag(): %s", errorMessage); + } + if (error || isDataReady) + { + break; + } + delay(100); + } + + if (isDataReady && !error) + { + error = scd4x.readMeasurement(LocalSensors.i2c_co2sensor.co2, LocalSensors.i2c_co2sensor.temperature, LocalSensors.i2c_co2sensor.humidity); + if (error) + { + errorToString(error, errorMessage, 256); + log_e("Error trying to execute readMeasurement(): %s", errorMessage); + } + else if (LocalSensors.i2c_co2sensor.co2 == 0) + { + log_e("Invalid sample detected, skipping."); + } + else + { + log_d("SCD4x CO2: %d ppm", LocalSensors.i2c_co2sensor.co2); + log_d("SCD4x Temperature: %4.1f °C", LocalSensors.i2c_co2sensor.temperature); + log_d("SCD4x Humidity: %3.1f %%rH", LocalSensors.i2c_co2sensor.humidity); + LocalSensors.i2c_co2sensor.valid = true; + } + } + scd4x.powerDown(); +#endif +} diff --git a/examples/Waveshare_7_5_T7_Sensors/LocalInterface.h b/examples/Waveshare_7_5_T7_Sensors/LocalInterface.h new file mode 100644 index 0000000..e4a042e --- /dev/null +++ b/examples/Waveshare_7_5_T7_Sensors/LocalInterface.h @@ -0,0 +1,120 @@ +/////////////////////////////////////////////////////////////////////////////// +// LocalInterface.h +// +// Local Sensor Data Interface for ESP32-e-Paper-Weather-Display +// +// +// created: 10/2024 +// +// +// MIT License +// +// Copyright (c) 2024 Matthias Prinke +// +// 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. +// +// +// History: +// +// 20241010 Extracted from Waveshare_7_5_T7_Sensors.ino +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _LOCAL_INTERFACE +#define _LOCAL_INTERFACE +#include +#include "config.h" + + +#ifdef MITHERMOMETER_EN +// BLE Temperature/Humidity Sensor +#include //!< https://github.com/matthias-bs/ATC_MiThermometer +#endif + +#ifdef THEENGSDECODER_EN +#include "NimBLEDevice.h" //!< https://github.com/h2zero/NimBLE-Arduino +#include "decoder.h" //!< https://github.com/theengs/decoder +#endif + +#ifdef BME280_EN +#include // https://github.com/angrest/pocketBME280 +#endif + +#ifdef SCD4X_EN +#include // https://github.com/Sensirion/arduino-i2c-scd4x +#endif + + + +// Local Sensor Data +struct LocalS +{ + struct + { + bool valid; //!< data valid + float temperature; //!< temperature in degC + float humidity; //!< humidity in % + uint8_t batt_level; //!< battery level in % + int rssi; //!< RSSI in dBm + } ble_thsensor[1]; + struct + { + bool valid; //!< data valid + float temperature; //!< temperature in degC + float humidity; //!< humidity in % + float pressure; //!< pressure in hPa + } i2c_thpsensor[1]; + struct + { + bool valid; //!< data valid + float temperature; //!< temperature in degC + float humidity; //!< humidity in % + uint16_t co2; //!< CO2 in ppm + } i2c_co2sensor; +}; + +typedef struct LocalS local_sensors_t; //!< Shortcut for struct LocalS + +struct LocalHistQData +{ + float temperature; //!< temperature in degC + float humidity; //!< humidity in % + float pressure; //!< pressure in hPa + bool th_valid; //!< temperature/humidity valid + bool p_valid; //!< pressure valid +}; + +typedef struct LocalHistQData local_hist_t; //!< Shortcut for struct LocalHistQData + +class LocalInterface +{ +public: + LocalInterface() { + }; + + /** + * \brief Get local sensor data + */ + void GetLocalData(void); +}; + +#endif \ No newline at end of file diff --git a/examples/Waveshare_7_5_T7_Sensors/MqttInterface.cpp b/examples/Waveshare_7_5_T7_Sensors/MqttInterface.cpp new file mode 100644 index 0000000..8dbe36a --- /dev/null +++ b/examples/Waveshare_7_5_T7_Sensors/MqttInterface.cpp @@ -0,0 +1,347 @@ +/////////////////////////////////////////////////////////////////////////////// +// MqttInterface.cpp +// +// MQTT Interface for ESP32-e-Paper-Weather-Display +// +// +// created: 10/2024 +// +// +// MIT License +// +// Copyright (c) 2024 Matthias Prinke +// +// 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. +// +// +// History: +// +// 20241010 Extracted from Waveshare_7_5_T7_Sensors.ino +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#include "MqttInterface.h" + +extern uint8_t StartWiFi(); +extern bool HistoryUpdateDue(); +extern void SaveLocalData(); +extern bool TouchTriggered(); +extern RTC_DATA_ATTR time_t LocalHistTStamp; + +static bool mqttMessageReceived = false; //!< Flag: MQTT message has been received + + +#ifdef SIMULATE_MQTT + static const char *MqttBuf = "{\"end_device_ids\":{\"device_id\":\"eui-9876b6000011c87b\",\"application_ids\":{\"application_id\":\"flora-lora\"},\"dev_eui\":\"9876B6000011C87B\",\"join_eui\":\"0000000000000000\",\"dev_addr\":\"260BFFCA\"},\"correlation_ids\":[\"as:up:01GH0PHSCTGKZ51EB8XCBBGHQD\",\"gs:conn:01GFQX269DVXYK9W6XF8NNZWDD\",\"gs:up:host:01GFQX26AXQM4QHEAPW48E8EWH\",\"gs:uplink:01GH0PHS6A65GBAPZB92XNGYAP\",\"ns:uplink:01GH0PHS6BEPXS9Y7DMDRNK84Y\",\"rpc:/ttn.lorawan.v3.GsNs/HandleUplink:01GH0PHS6BY76SY2VPRSHNDDRH\",\"rpc:/ttn.lorawan.v3.NsAs/HandleUplink:01GH0PHSCS7D3V8ERSKF0DTJ8H\"],\"received_at\":\"2022-11-04T06:51:44.409936969Z\",\"uplink_message\":{\"session_key_id\":\"AYRBaM/qASfqUi+BQK75Gg==\",\"f_port\":1,\"frm_payload\":\"PwOOWAgACAAIBwAAYEKAC28LAw0D4U0DwAoAAAAAwMxMP8DMTD/AzEw/AAAAAAAAAAAA\",\"decoded_payload\":{\"bytes\":{\"air_temp_c\":\"9.1\",\"battery_v\":2927,\"humidity\":88,\"indoor_humidity\":77,\"indoor_temp_c\":\"9.9\",\"rain_day\":\"0.8\",\"rain_hr\":\"0.0\",\"rain_mm\":\"56.0\",\"rain_mon\":\"0.8\",\"rain_week\":\"0.8\",\"soil_moisture\":10,\"soil_temp_c\":\"9.6\",\"status\":{\"ble_ok\":true,\"res\":false,\"rtc_sync_req\":false,\"runtime_expired\":true,\"s1_batt_ok\":true,\"s1_dec_ok\":true,\"ws_batt_ok\":true,\"ws_dec_ok\":true},\"supply_v\":2944,\"water_temp_c\":\"7.8\",\"wind_avg_meter_sec\":\"0.8\",\"wind_direction_deg\":\"180.0\",\"wind_gust_meter_sec\":\"0.8\"}},\"rx_metadata\":[{\"gateway_ids\":{\"gateway_id\":\"lora-db0fc\",\"eui\":\"3135323538002400\"},\"time\":\"2022-11-04T06:51:44.027496Z\",\"timestamp\":1403655780,\"rssi\":-104,\"channel_rssi\":-104,\"snr\":8.25,\"location\":{\"latitude\":52.27640735,\"longitude\":10.54058183,\"altitude\":65,\"source\":\"SOURCE_REGISTRY\"},\"uplink_token\":\"ChgKFgoKbG9yYS1kYjBmYxIIMTUyNTgAJAAQ5KyonQUaCwiA7ZKbBhCw6tpgIKDtnYPt67cC\",\"channel_index\":4,\"received_at\":\"2022-11-04T06:51:44.182146570Z\"}],\"settings\":{\"data_rate\":{\"lora\":{\"bandwidth\":125000,\"spreading_factor\":8,\"coding_rate\":\"4/5\"}},\"frequency\":\"867300000\",\"timestamp\":1403655780,\"time\":\"2022-11-04T06:51:44.027496Z\"},\"received_at\":\"2022-11-04T06:51:44.203702153Z\",\"confirmed\":true,\"consumed_airtime\":\"0.215552s\",\"locations\":{\"user\":{\"latitude\":52.24619,\"longitude\":10.50106,\"source\":\"SOURCE_REGISTRY\"}},\"network_ids\":{\"net_id\":\"000013\",\"tenant_id\":\"ttn\",\"cluster_id\":\"eu1\",\"cluster_address\":\"eu1.cloud.thethings.network\"}}}"; +#else + static char MqttBuf[MQTT_PAYLOAD_SIZE + 1]; //!< MQTT Payload Buffer +#endif + +/** + * \brief MQTT message reception callback function + * + * Sets the flag mqttMessageReceived and copies the received message to + * MqttBuf. + */ +static void mqttMessageCb(String &topic, String &payload) +{ + mqttMessageReceived = true; + log_d("Payload size: %d", payload.length()); +#ifndef SIMULATE_MQTT + strncpy(MqttBuf, payload.c_str(), payload.length()); +#endif +} + +// Connect to MQTT broker +bool MqttInterface::mqttConnect() +{ + log_d("Checking wifi..."); + if (StartWiFi() != WL_CONNECTED) + { + return false; + } + + log_i("MQTT connecting..."); + unsigned long start = millis(); + + MqttClient.begin(MQTT_HOST, MQTT_PORT, net); + MqttClient.setOptions(MQTT_KEEPALIVE /* keepAlive [s] */, MQTT_CLEAN_SESSION /* cleanSession */, MQTT_TIMEOUT * 1000 /* timeout [ms] */); + + while (!MqttClient.connect(HOSTNAME, MQTT_USER, MQTT_PASS)) + { + log_d("."); + if (millis() > start + MQTT_CONNECT_TIMEOUT * 1000) + { + log_i("Connect timeout!"); + return false; + } + delay(1000); + } + log_i("Connected!"); + + MqttClient.onMessage(mqttMessageCb); + + if (!MqttClient.subscribe(MQTT_SUB_IN)) + { + log_i("MQTT subscription failed!"); + return false; + } + return true; +} + +// Get data from MQTT broker +void MqttInterface::getMqttData(mqtt_sensors_t &MqttSensors) +{ + MqttSensors.valid = false; + + log_i("Waiting for MQTT message..."); + + // allocate the JsonDocument + JsonDocument doc; + + // LoRaWAN fPort + unsigned char f_port; + + do + { +#ifndef SIMULATE_MQTT + unsigned long start = millis(); + int count = 0; + while (!mqttMessageReceived) + { + MqttClient.loop(); + delay(10); + if (count++ == 1000) + { + log_d("."); + count = 0; + } + if (mqttMessageReceived) + break; + if (!MqttClient.connected()) + { + mqttConnect(); + } + if (TouchTriggered()) + { + log_i("Touch interrupt!"); + return; + } + if (millis() > start + MQTT_DATA_TIMEOUT * 1000) + { + log_i("Timeout!"); + MqttClient.disconnect(); + return; + } + // During this time-consuming loop, updating local history could be due + if (HistoryUpdateDue()) + { + time_t now = time(NULL); + if (now - LocalHistTStamp >= (HIST_UPDATE_RATE - HIST_UPDATE_TOL) * 60) + { + LocalHistTStamp = now; + SaveLocalData(); + } + } + } +#else + log_i("(Simulated MQTT incoming message)"); + MqttSensors.valid = true; +#endif + + log_i("done!"); + log_d("%s", MqttBuf); + + log_d("Creating JSON object..."); + + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, MqttBuf, MQTT_PAYLOAD_SIZE); + + // Test if parsing succeeds. + if (error) + { + log_i("deserializeJson() failed: %s", error.c_str()); + return; + } + else + { + log_d("Done!"); + } + + MqttClient.disconnect(); + MqttSensors.valid = true; + + const char *received_at = doc["received_at"]; + if (received_at) + { + strncpy(MqttSensors.received_at, received_at, 30); + } + f_port = doc["uplink_message"]["f_port"]; + } while (f_port != 1); + JsonVariant payload = doc["uplink_message"]["decoded_payload"]["bytes"]; + + MqttSensors.air_temp_c = payload[WS_TEMP_C].isNull() ? INV_TEMP : payload[WS_TEMP_C]; + MqttSensors.humidity = payload[WS_HUMIDITY].isNull() ? INV_UINT8 : payload[WS_HUMIDITY]; + MqttSensors.indoor_temp_c = payload[BLE0_TEMP_C].isNull() ? INV_TEMP : payload[BLE0_TEMP_C]; + MqttSensors.indoor_humidity = payload[BLE0_HUMIDITY].isNull() ? INV_UINT8 : payload[BLE0_HUMIDITY]; + MqttSensors.battery_v = payload[A0_VOLTAGE_MV].isNull() ? INV_UINT16 : payload[A0_VOLTAGE_MV]; + MqttSensors.rain_day = payload[WS_RAIN_DAILY_MM].isNull() ? INV_FLOAT : payload[WS_RAIN_DAILY_MM]; + MqttSensors.rain_hr = payload[WS_RAIN_HOURLY_MM].isNull() ? INV_FLOAT : payload[WS_RAIN_HOURLY_MM]; + MqttSensors.rain_mm = payload[WS_RAIN_MM].isNull() ? INV_FLOAT : payload[WS_RAIN_MM]; + MqttSensors.rain_month = payload[WS_RAIN_MONTHLY_MM].isNull() ? INV_FLOAT : payload[WS_RAIN_MONTHLY_MM]; + MqttSensors.rain_week = payload[WS_RAIN_WEEKLY_MM].isNull() ? INV_FLOAT : payload[WS_RAIN_WEEKLY_MM]; + MqttSensors.soil_moisture = payload[SOIL1_MOISTURE].isNull() ? INV_UINT8 : payload[SOIL1_MOISTURE]; + MqttSensors.soil_temp_c = payload[SOIL1_TEMP_C].isNull() ? INV_TEMP : payload[SOIL1_TEMP_C]; + MqttSensors.water_temp_c = payload[OW0_TEMP_C].isNull() ? INV_TEMP : payload[OW0_TEMP_C]; + MqttSensors.wind_avg_meter_sec = payload[WS_WIND_AVG_MS].isNull() ? INV_FLOAT : payload[WS_WIND_AVG_MS]; + MqttSensors.wind_direction_deg = payload[WS_WIND_DIR_DEG].isNull() ? INV_UINT16 : payload[WS_WIND_DIR_DEG]; + MqttSensors.wind_gust_meter_sec = payload[WS_WIND_GUST_MS].isNull() ? INV_FLOAT : payload[WS_WIND_GUST_MS]; + + // FIXME: This is a workaround for the time being + JsonObject status = payload["status"]; + bool ble_ok = MqttSensors.indoor_temp_c != INV_TEMP && MqttSensors.indoor_humidity != INV_UINT8; + // MqttSensors.status.ble_ok = status["ble_ok"] | ble_ok; + MqttSensors.status.ble_ok = ble_ok; + bool s1_dec_ok = MqttSensors.soil_temp_c != INV_TEMP && MqttSensors.soil_moisture != INV_UINT8; + // MqttSensors.status.s1_dec_ok = status["s1_dec_ok"] | s1_dec_ok; + MqttSensors.status.s1_dec_ok = s1_dec_ok; + bool ws_dec_ok = MqttSensors.air_temp_c != INV_TEMP && MqttSensors.rain_mm != INV_FLOAT; + // MqttSensors.status.ws_dec_ok = status["ws_dec_ok"] | ws_dec_ok; + MqttSensors.status.ws_dec_ok = ws_dec_ok; + + MqttSensors.status.s1_batt_ok = status["s1_batt_ok"]; + MqttSensors.status.ws_batt_ok = status["ws_batt_ok"]; + + // Sanity checks + if (MqttSensors.humidity == 0) + { + MqttSensors.status.ws_dec_ok = false; + } + MqttSensors.rain_hr_valid = (MqttSensors.rain_hr >= 0) && (MqttSensors.rain_hr < 300); + MqttSensors.rain_day_valid = (MqttSensors.rain_day >= 0) && (MqttSensors.rain_day < 1800); + + // If not valid, set value to zero to avoid any problems with auto-scale etc. + if (!MqttSensors.rain_hr_valid) + { + MqttSensors.rain_hr = 0; + } + if (!MqttSensors.rain_day_valid) + { + MqttSensors.rain_day = 0; + } + + log_i("MQTT data updated: %d", MqttSensors.valid ? 1 : 0); +} + + +bool MqttInterface::mqttUplink(WiFiClient &net, MQTTClient &MqttClient, local_sensors_t &data) +{ + char payload[21]; + char topic[41]; + + log_d("Checking wifi..."); + if (StartWiFi() != WL_CONNECTED) + { + return false; + } + + log_i("MQTT (publishing) connecting..."); + unsigned long start = millis(); + + MqttClient.begin(MQTT_HOST_P, MQTT_PORT_P, net); + MqttClient.setOptions(MQTT_KEEPALIVE /* keepAlive [s] */, MQTT_CLEAN_SESSION /* cleanSession */, MQTT_TIMEOUT * 1000 /* timeout [ms] */); + + while (!MqttClient.connect(HOSTNAME, MQTT_USER_P, MQTT_PASS_P)) + { + log_d("."); + if (millis() > start + MQTT_CONNECT_TIMEOUT * 1000) + { + log_i("Connect timeout!"); + return false; + } + delay(1000); + } + log_i("Connected!"); + + log_d("Publishing..."); +#if defined(SCD4X_EN) + if (data.i2c_co2sensor.valid) + { + snprintf(payload, 20, "%u", data.i2c_co2sensor.co2); + snprintf(topic, 40, "%s/sdc4x/co2", HOSTNAME); + MqttClient.publish(topic, payload); + + snprintf(payload, 20, "%3.1f", data.i2c_co2sensor.temperature); + snprintf(topic, 40, "%s/sdc4x/temperature", HOSTNAME); + MqttClient.publish(topic, payload); + + snprintf(payload, 20, "%3.0f", data.i2c_co2sensor.humidity); + snprintf(topic, 40, "%s/sdc4x/humidity", HOSTNAME); + MqttClient.publish(topic, payload); + } +#endif + +#if defined(BME280_EN) + if (data.i2c_thpsensor[0].valid) + { + snprintf(payload, 20, "%3.1f", data.i2c_thpsensor[0].temperature); + snprintf(topic, 40, "%s/bme280/temperature", HOSTNAME); + MqttClient.publish(topic, payload); + + snprintf(payload, 20, "%3.0f", data.i2c_thpsensor[0].humidity); + snprintf(topic, 40, "%s/bme280/humidity", HOSTNAME); + MqttClient.publish(topic, payload); + + snprintf(payload, 20, "%4.0f", data.i2c_thpsensor[0].pressure); + snprintf(topic, 40, "%s/bme280/pressure", HOSTNAME); + MqttClient.publish(topic, payload); + } +#endif + +#if defined(THEENGSDECODER_EN) || defined(THEENGSDECODER_EN) + if (data.ble_thsensor[0].valid) + { + snprintf(payload, 20, "%3.1f", data.ble_thsensor[0].temperature); + snprintf(topic, 40, "%s/ble/temperature", HOSTNAME); + MqttClient.publish(topic, payload); + + snprintf(payload, 20, "%3.0f", data.ble_thsensor[0].humidity); + snprintf(topic, 40, "%s/ble/humidity", HOSTNAME); + MqttClient.publish(topic, payload); + + snprintf(payload, 20, "%u", data.ble_thsensor[0].batt_level); + snprintf(topic, 40, "%s/ble/batt_level", HOSTNAME); + MqttClient.publish(topic, payload); + } +#endif + + for (int i = 0; i < 10; i++) + { + MqttClient.loop(); + delay(500); + } + + log_i("MQTT (publishing) disconnect."); + MqttClient.disconnect(); + + return true; +} diff --git a/examples/Waveshare_7_5_T7_Sensors/MqttInterface.h b/examples/Waveshare_7_5_T7_Sensors/MqttInterface.h new file mode 100644 index 0000000..82849ce --- /dev/null +++ b/examples/Waveshare_7_5_T7_Sensors/MqttInterface.h @@ -0,0 +1,180 @@ +/////////////////////////////////////////////////////////////////////////////// +// MqttInterface.h +// +// MQTT Interface for ESP32-e-Paper-Weather-Display +// +// +// created: 10/2024 +// +// +// MIT License +// +// Copyright (c) 2024 Matthias Prinke +// +// 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. +// +// +// History: +// +// 20241010 Extracted from Waveshare_7_5_T7_Sensors.ino +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _MQTT_INTERFACE +#define _MQTT_INTERFACE +#include +#include +#include +#include // https://github.com/256dpi/arduino-mqtt +#include // https://github.com/bblanchon/ArduinoJson needs version v6 or above +#include "config.h" +#include "secrets.h" +#include "LocalInterface.h" + +// MQTT Sensor Data +struct MqttS +{ + bool valid; //!< + char received_at[32]; //!< MQTT message received date/time + struct + { + unsigned int ws_batt_ok : 1; //!< weather sensor battery o.k. + unsigned int ws_dec_ok : 1; //!< weather sensor decoding o.k. + unsigned int s1_batt_ok : 1; //!< soil moisture sensor battery o.k. + unsigned int s1_dec_ok : 1; //!< soil moisture sensor dencoding o.k. + unsigned int ble_ok : 1; //!< BLE T-/H-sensor data o.k. + + } status; + float air_temp_c; //!< temperature in degC + uint8_t humidity; //!< humidity in % + float wind_direction_deg; //!< wind direction in deg + float wind_gust_meter_sec; //!< wind speed (gusts) in m/s + float wind_avg_meter_sec; //!< wind speed (avg) in m/s + float rain_mm; //!< rain gauge level in mm + uint16_t supply_v; //!< supply voltage in mV + uint16_t battery_v; //!< battery voltage in mV + float water_temp_c; //!< water temperature in degC + float indoor_temp_c; //!< indoor temperature in degC + uint8_t indoor_humidity; //!< indoor humidity in % + float soil_temp_c; //!< soil temperature in degC + uint8_t soil_moisture; //!< soil moisture in % + float rain_hr; //!< hourly precipitation in mm + bool rain_hr_valid; //!< hourly precipitation valid + float rain_day; //!< daily precipitation in mm + bool rain_day_valid; //!< daily precipitation valid + float rain_day_prev; //!< daily precipitation in mm, previous value + float rain_week; //!< weekly precipitation in mm + float rain_month; //!< monthly precipitatiion in mm +}; + +typedef struct MqttS mqtt_sensors_t; //!< Shortcut for struct Sensor +struct MqttHistQData +{ + float temperature; //!< temperature in degC + uint8_t humidity; //!< humidity in % + bool valid; //!< data valid +}; + +typedef struct MqttHistQData mqtt_hist_t; //!< Shortcut for struct MqttHistQData + +// TOPICS_NEW: BresserWeatherSensorLW +#define TOPICS_NEW + +#ifdef TOPICS_NEW +#define WS_TEMP_C "ws_temp_c" +#define WS_HUMIDITY "ws_humidity" +#define BLE0_TEMP_C "ble0_temp_c" +#define BLE0_HUMIDITY "ble0_humidity" +#define A0_VOLTAGE_MV "a0_voltage_mv" +#define WS_RAIN_DAILY_MM "ws_rain_daily_mm" +#define WS_RAIN_HOURLY_MM "ws_rain_hourly_mm" +#define WS_RAIN_MM "ws_rain_mm" +#define WS_RAIN_MONTHLY_MM "ws_rain_monthly_mm" +#define WS_RAIN_WEEKLY_MM "ws_rain_weekly_mm" +#define SOIL1_MOISTURE "soil1_moisture" +#define SOIL1_TEMP_C "soil1_temp_c" +#define OW0_TEMP_C "ow0_temp_c" +#define WS_WIND_AVG_MS "ws_wind_avg_ms" +#define WS_WIND_DIR_DEG "ws_wind_dir_deg" +#define WS_WIND_GUST_MS "ws_wind_gust_ms" +#else +#define WS_TEMP_C "air_temp_c" +#define WS_HUMIDITY "humidity" +#define BLE0_TEMP_C "indoor_temp_c" +#define BLE0_HUMIDITY "indoor_humidity" +#define A0_VOLTAGE_MV "battery_v" +#define WS_RAIN_DAILY_MM "rain_day" +#define WS_RAIN_HOURLY_MM "rain_hr" +#define WS_RAIN_MM "rain_mm" +#define WS_RAIN_MONTHLY_MM "rain_mon" +#define WS_RAIN_WEEKLY_MM "rain_week" +#define SOIL1_MOISTURE "soil_moisture" +#define SOIL1_TEMP_C "soil_temp_c" +#define OW0_TEMP_C "water_temp_c" +#define WS_WIND_AVG_MS "wind_avg_meter_sec" +#define WS_WIND_DIR_DEG "wind_direction_deg" +#define WS_WIND_GUST_MS "wind_gust_meter_sec" +#endif + +// Encoding of invalid values +// for floating point, see +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN +#define INV_FLOAT NAN +#define INV_UINT32 0xFFFFFFFF +#define INV_UINT16 0xFFFF +#define INV_UINT8 0xFF +#define INV_TEMP 327.67 + +class MqttInterface +{ +private: + WiFiClient net; + MQTTClient MqttClient; + +public: + /*! + * \brief Constructor + */ + MqttInterface(WiFiClient &_net, MQTTClient &_MqttClient) + { + net = _net; + MqttClient = _MqttClient; + }; + + /** + * \brief Connect to MQTT broker + * + * \return true if connection was succesful, otherwise false + */ + bool mqttConnect(); + + /** + * \brief Get MQTT data from broker + * + * \param net network connection + * \param MqttClient MQTT client object + */ + void getMqttData(mqtt_sensors_t &MqttSensors); + + bool mqttUplink(WiFiClient &net, MQTTClient &MqttClient, local_sensors_t &data); +}; +#endif \ No newline at end of file diff --git a/examples/Waveshare_7_5_T7_Sensors/RainHistory.h b/examples/Waveshare_7_5_T7_Sensors/RainHistory.h new file mode 100644 index 0000000..8cfeb15 --- /dev/null +++ b/examples/Waveshare_7_5_T7_Sensors/RainHistory.h @@ -0,0 +1,61 @@ +/////////////////////////////////////////////////////////////////////////////// +// RainHistory.h +// +// Rainfall history data definitions +// +// +// created: 10/2024 +// +// +// MIT License +// +// Copyright (c) 2024 Matthias Prinke +// +// 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. +// +// +// History: +// +// 20241011 Extracted from Waveshare_7_5_T7_Sensors.ino +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _RAIN_HISTORY_H +#define _RAIN_HISTORY_H + +struct RainHrHistQData +{ + float rain; //!< precipitation in mm + bool valid; //!< data valid +}; + +typedef struct RainHrHistQData rain_hr_hist_t; //!< Shortcut for struct RainHrHistQData + +struct RainDayHistQData +{ + float rain; //!< precipitation in mm + bool valid; //!< data valid +}; + +typedef struct RainDayHistQData rain_day_hist_t; //!< Shortcut for struct RainDayHistQData + +#endif \ No newline at end of file diff --git a/examples/Waveshare_7_5_T7_Sensors/Waveshare_7_5_T7_Sensors.ino b/examples/Waveshare_7_5_T7_Sensors/Waveshare_7_5_T7_Sensors.ino index 8d38020..d746a47 100644 --- a/examples/Waveshare_7_5_T7_Sensors/Waveshare_7_5_T7_Sensors.ino +++ b/examples/Waveshare_7_5_T7_Sensors/Waveshare_7_5_T7_Sensors.ino @@ -62,45 +62,23 @@ #include "owm_credentials.h" // See 'owm_credentials' tab and enter your OWM API key and set the Wifi SSID and PASSWORD #include // https://github.com/bblanchon/ArduinoJson needs version v6 or above #include // Built-in -#include // #include #include // Built-in #include // Built-in -#include // Built-in +//#include // Built-in #include // Built-in #include // https://github.com/256dpi/arduino-mqtt #include // https://github.com/SMFSW/cQueue +#include "MqttInterface.h" // MQTT sensor data interface +#include "LocalInterface.h" // Local sensor data interface (BLE and I2C) +#include "RainHistory.h" // Rainfall history data definitions #include "WeatherSymbols.h" // Functions for drawing weather symbols at runtime #include "bitmap_icons.h" // Icon bitmaps #include "bitmap_weather_report.h" // Picture shown on ScreenStart - -// #define SIMULATE_MQTT -// #define FORCE_LOW_BATTERY -// #define FORCE_NO_SIGNAL - -#define MQTT_PAYLOAD_SIZE 4096 -#define MQTT_CONNECT_TIMEOUT 30 -#define MQTT_DATA_TIMEOUT 600 -#define MQTT_KEEPALIVE 60 -#define MQTT_TIMEOUT 1800 -#define MQTT_CLEAN_SESSION false - -#define MQTT_HIST_SIZE 144 -#define RAIN_HR_HIST_SIZE 24 -#define RAIN_DAY_HIST_SIZE 29 -#define LOCAL_HIST_SIZE 144 -#define HIST_UPDATE_RATE 30 -#define HIST_UPDATE_TOL 5 - -// #define MITHERMOMETER_EN //!< Enable MiThermometer (BLE sensors) -#define THEENGSDECODER_EN //!< Enable Theengs Decoder (BLE sensors) -#define BME280_EN //!< Enable BME280 T/H/p-sensor (I2C) -#define SCD4X_EN //!< Enable SCD4x CO2-sensor (I2C) -// #define WATERTEMP_EN //!< Enable Wather Temperature Display (MQTT) -#define MITHERMOMETER_BATTALERT 6 //!< Low battery alert threshold [%] -#define WATER_TEMP_INVALID -30.0 //!< Water temperature invalid marker [°C] -#define I2C_SDA 21 //!< I2C Serial Data Pin -#define I2C_SCL 22 //!< I2C Serial Clock Pin +#include "bitmap_local.h" // Picture shown on ScreenLocal - replace by your own +#include "bitmap_remote.h" // Picture shown on ScreenMQTT - replace by your own +#include "utils.h" +#include "secrets.h" #define ENABLE_GxEPD2_display 0 #include //!< https://github.com/ZinggJM/GxEPD2 @@ -115,38 +93,6 @@ // #include "src/lang_nl.h" // Localization (Dutch) // #include "src/lang_pl.h" // Localisation (Polish) -// Encoding of invalid values -// for floating point, see -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN -#define INV_FLOAT NAN -#define INV_UINT32 0xFFFFFFFF -#define INV_UINT16 0xFFFF -#define INV_UINT8 0xFF -#define INV_TEMP 327.67 - -#ifdef MITHERMOMETER_EN -// BLE Temperature/Humidity Sensor -#include //!< https://github.com/matthias-bs/ATC_MiThermometer -#endif - -#ifdef THEENGSDECODER_EN -#include "NimBLEDevice.h" //!< https://github.com/h2zero/NimBLE-Arduino -#include "decoder.h" //!< https://github.com/theengs/decoder -#endif - -#ifdef BME280_EN -#include // https://github.com/angrest/pocketBME280 -#endif - -#ifdef SCD4X_EN -#include // https://github.com/Sensirion/arduino-i2c-scd4x -#endif - -// #define DISPLAY_3C -#define DISPLAY_BW -#define SCREEN_WIDTH 800 //!< EPD screen width -#define SCREEN_HEIGHT 480 //!< EPD screen height - long SleepDuration = 30; //!< Sleep time in minutes, aligned to the nearest minute boundary, so if 30 will always update at 00 or 30 past the hour (+ SleepOffset) long SleepOffset = -120; //!< Offset in seconds from SleepDuration; -120 will trigger wakeup 2 minutes earlier int WakeupTime = 7; //!< Don't wakeup until after 07:00 to save battery power @@ -181,17 +127,6 @@ static const uint8_t TOUCH_NEXT = 32; //!< Touch sensor right (next) static const uint8_t TOUCH_PREV = 33; //!< Touch sensor left (previous) static const uint8_t TOUCH_MID = 35; //!< Touch sensor middle -#ifdef SIMULATE_MQTT -const char *MqttBuf = "{\"end_device_ids\":{\"device_id\":\"eui-9876b6000011c87b\",\"application_ids\":{\"application_id\":\"flora-lora\"},\"dev_eui\":\"9876B6000011C87B\",\"join_eui\":\"0000000000000000\",\"dev_addr\":\"260BFFCA\"},\"correlation_ids\":[\"as:up:01GH0PHSCTGKZ51EB8XCBBGHQD\",\"gs:conn:01GFQX269DVXYK9W6XF8NNZWDD\",\"gs:up:host:01GFQX26AXQM4QHEAPW48E8EWH\",\"gs:uplink:01GH0PHS6A65GBAPZB92XNGYAP\",\"ns:uplink:01GH0PHS6BEPXS9Y7DMDRNK84Y\",\"rpc:/ttn.lorawan.v3.GsNs/HandleUplink:01GH0PHS6BY76SY2VPRSHNDDRH\",\"rpc:/ttn.lorawan.v3.NsAs/HandleUplink:01GH0PHSCS7D3V8ERSKF0DTJ8H\"],\"received_at\":\"2022-11-04T06:51:44.409936969Z\",\"uplink_message\":{\"session_key_id\":\"AYRBaM/qASfqUi+BQK75Gg==\",\"f_port\":1,\"frm_payload\":\"PwOOWAgACAAIBwAAYEKAC28LAw0D4U0DwAoAAAAAwMxMP8DMTD/AzEw/AAAAAAAAAAAA\",\"decoded_payload\":{\"bytes\":{\"air_temp_c\":\"9.1\",\"battery_v\":2927,\"humidity\":88,\"indoor_humidity\":77,\"indoor_temp_c\":\"9.9\",\"rain_day\":\"0.8\",\"rain_hr\":\"0.0\",\"rain_mm\":\"56.0\",\"rain_mon\":\"0.8\",\"rain_week\":\"0.8\",\"soil_moisture\":10,\"soil_temp_c\":\"9.6\",\"status\":{\"ble_ok\":true,\"res\":false,\"rtc_sync_req\":false,\"runtime_expired\":true,\"s1_batt_ok\":true,\"s1_dec_ok\":true,\"ws_batt_ok\":true,\"ws_dec_ok\":true},\"supply_v\":2944,\"water_temp_c\":\"7.8\",\"wind_avg_meter_sec\":\"0.8\",\"wind_direction_deg\":\"180.0\",\"wind_gust_meter_sec\":\"0.8\"}},\"rx_metadata\":[{\"gateway_ids\":{\"gateway_id\":\"lora-db0fc\",\"eui\":\"3135323538002400\"},\"time\":\"2022-11-04T06:51:44.027496Z\",\"timestamp\":1403655780,\"rssi\":-104,\"channel_rssi\":-104,\"snr\":8.25,\"location\":{\"latitude\":52.27640735,\"longitude\":10.54058183,\"altitude\":65,\"source\":\"SOURCE_REGISTRY\"},\"uplink_token\":\"ChgKFgoKbG9yYS1kYjBmYxIIMTUyNTgAJAAQ5KyonQUaCwiA7ZKbBhCw6tpgIKDtnYPt67cC\",\"channel_index\":4,\"received_at\":\"2022-11-04T06:51:44.182146570Z\"}],\"settings\":{\"data_rate\":{\"lora\":{\"bandwidth\":125000,\"spreading_factor\":8,\"coding_rate\":\"4/5\"}},\"frequency\":\"867300000\",\"timestamp\":1403655780,\"time\":\"2022-11-04T06:51:44.027496Z\"},\"received_at\":\"2022-11-04T06:51:44.203702153Z\",\"confirmed\":true,\"consumed_airtime\":\"0.215552s\",\"locations\":{\"user\":{\"latitude\":52.24619,\"longitude\":10.50106,\"source\":\"SOURCE_REGISTRY\"}},\"network_ids\":{\"net_id\":\"000013\",\"tenant_id\":\"ttn\",\"cluster_id\":\"eu1\",\"cluster_address\":\"eu1.cloud.thethings.network\"}}}"; -#else -char MqttBuf[MQTT_PAYLOAD_SIZE + 1]; //!< MQTT Payload Buffer -#endif - -#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) -const int bleScanTime = 31; //!< BLE scan time in seconds -std::vector knownBLEAddresses = KNOWN_BLE_ADDRESSES; -#endif - #if defined(DISPLAY_BW) GxEPD2_BW display(GxEPD2_750_T7(/*CS=*/EPD_CS, /*DC=*/EPD_DC, /*RST=*/EPD_RST, /*BUSY=*/EPD_BUSY)); // B/W display #elif defined(DISPLAY_3C) @@ -220,8 +155,8 @@ boolean LargeIcon = true, SmallIcon = false; #define Small 6 // For icon drawing, needs to be odd number for best effect String Time_str; //!< Curent time as string String Date_str; //!< Current date as stringstrings to hold time and received weather data -extern long _timezone; //!< -int wifi_signal = 0; //!< WiFi signal strength +extern long _timezone; //!< FIXME: Where defined? Needed? +int WiFiSignal = 0; //!< WiFi signal strength int CurrentHour = 0; //!< Current time - hour int CurrentMin = 0; //!< Current time - minutes int CurrentSec = 0; //!< Current time - seconds @@ -232,21 +167,9 @@ RTC_DATA_ATTR bool touchPrevTrig = false; //!< Flag: Left touch sensor has bee RTC_DATA_ATTR bool touchNextTrig = false; //!< Flag: Right touch sensor has been triggered RTC_DATA_ATTR bool touchMidTrig = false; //!< Flag: Middle touch sensor has been triggered -#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) -NimBLEScan *pBLEScan; -#endif - -bool mqttMessageReceived = false; //!< Flag: MQTT message has been received // ################ PROGRAM VARIABLES and OBJECTS ################ -#define max_readings 24 - -RTC_DATA_ATTR Forecast_record_type WxConditions[1]; //!< OWM Weather Conditions -Forecast_record_type WxForecast[max_readings]; //!< OWM Weather Forecast - -#include "common.h" - #define autoscale_on true #define autoscale_off false #define barchart_on true @@ -255,147 +178,42 @@ Forecast_record_type WxForecast[max_readings]; //!< OWM Weather Forecast const String Locations[] = LOCATIONS_TXT; //!< /Screen Titles // OWM Forecast Data +#define max_readings 24 +RTC_DATA_ATTR Forecast_record_type WxConditions[1]; //!< OWM Weather Conditions +Forecast_record_type WxForecast[max_readings]; //!< OWM Weather Forecast float pressure_readings[max_readings] = {0}; //!< OWM pressure readings float temperature_readings[max_readings] = {0}; //!< OWM temperature readings float humidity_readings[max_readings] = {0}; //!< OWM humidity readings float rain_readings[max_readings] = {0}; //!< OWM rain readings float snow_readings[max_readings] = {0}; //!< OWM snow readings -// MQTT Sensor Data -struct MqttS -{ - bool valid; //!< - char received_at[32]; //!< MQTT message received date/time - struct - { - unsigned int ws_batt_ok : 1; //!< weather sensor battery o.k. - unsigned int ws_dec_ok : 1; //!< weather sensor decoding o.k. - unsigned int s1_batt_ok : 1; //!< soil moisture sensor battery o.k. - unsigned int s1_dec_ok : 1; //!< soil moisture sensor dencoding o.k. - unsigned int ble_ok : 1; //!< BLE T-/H-sensor data o.k. - - } status; - float air_temp_c; //!< temperature in degC - uint8_t humidity; //!< humidity in % - float wind_direction_deg; //!< wind direction in deg - float wind_gust_meter_sec; //!< wind speed (gusts) in m/s - float wind_avg_meter_sec; //!< wind speed (avg) in m/s - float rain_mm; //!< rain gauge level in mm - uint16_t supply_v; //!< supply voltage in mV - uint16_t battery_v; //!< battery voltage in mV - float water_temp_c; //!< water temperature in degC - float indoor_temp_c; //!< indoor temperature in degC - uint8_t indoor_humidity; //!< indoor humidity in % - float soil_temp_c; //!< soil temperature in degC - uint8_t soil_moisture; //!< soil moisture in % - float rain_hr; //!< hourly precipitation in mm - bool rain_hr_valid; //!< hourly precipitation valid - float rain_day; //!< daily precipitation in mm - bool rain_day_valid; //!< daily precipitation valid - float rain_day_prev; //!< daily precipitation in mm, previous value - float rain_week; //!< weekly precipitation in mm - float rain_month; //!< monthly precipitatiion in mm -}; - -typedef struct MqttS mqtt_sensors_t; //!< Shortcut for struct Sensor -RTC_DATA_ATTR mqtt_sensors_t MqttSensors; //!< MQTT sensor data - -RTC_DATA_ATTR Queue_t MqttHistQCtrl; //!< MQTT Sensor Data History FIFO Control - -struct MqttHistQData -{ - float temperature; //!< temperature in degC - uint8_t humidity; //!< humidity in % - bool valid; //!< data valid -}; - -typedef struct MqttHistQData mqtt_hist_t; //!< Shortcut for struct MqttHistQData +#include "common.h" +// MQTT Sensor Data +RTC_DATA_ATTR mqtt_sensors_t MqttSensors; //!< MQTT sensor data +RTC_DATA_ATTR Queue_t MqttHistQCtrl; //!< MQTT Sensor Data History FIFO Control RTC_DATA_ATTR mqtt_hist_t MqttHist[MQTT_HIST_SIZE]; //toString().c_str()); - JsonObject BLEdata = doc.to(); - String mac_adress = advertisedDevice->getAddress().toString().c_str(); - - BLEdata["id"] = (char *)mac_adress.c_str(); - for (idx = 0; idx < knownBLEAddresses.size(); idx++) - { - if (mac_adress == knownBLEAddresses[idx].c_str()) - { - log_v("BLE device found at index %d", idx); - device_found = true; - break; - } - } - - if (advertisedDevice->haveName()) - BLEdata["name"] = (char *)advertisedDevice->getName().c_str(); - - if (advertisedDevice->haveManufacturerData()) - { - char *manufacturerdata = BLEUtils::buildHexData(NULL, (uint8_t *)advertisedDevice->getManufacturerData().data(), advertisedDevice->getManufacturerData().length()); - BLEdata["manufacturerdata"] = manufacturerdata; - free(manufacturerdata); - } - - if (advertisedDevice->haveRSSI()) - BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); - - if (advertisedDevice->haveTXPower()) - BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); - - if (advertisedDevice->haveServiceData()) - { - int serviceDataCount = advertisedDevice->getServiceDataCount(); - for (int j = 0; j < serviceDataCount; j++) - { - std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); - BLEdata["servicedata"] = (char *)service_data.c_str(); - std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); - BLEdata["servicedatauuid"] = (char *)serviceDatauuid.c_str(); - } - } - - if (decoder.decodeBLEJson(BLEdata) && device_found) - { - if (CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG) - { - char buf[512]; - serializeJson(BLEdata, buf); - log_d("TheengsDecoder found device: %s", buf); - } - BLEdata.remove("manufacturerdata"); - BLEdata.remove("servicedata"); - - LocalSensors.ble_thsensor[idx].temperature = (float)BLEdata["tempc"]; - LocalSensors.ble_thsensor[idx].humidity = (float)BLEdata["hum"]; - LocalSensors.ble_thsensor[idx].batt_level = (uint8_t)BLEdata["batt"]; - LocalSensors.ble_thsensor[idx].rssi = (int)BLEdata["rssi"]; - LocalSensors.ble_thsensor[idx].valid = (LocalSensors.ble_thsensor[idx].batt_level > 0); - log_i("Temperature: %.1f°C", LocalSensors.ble_thsensor[idx].temperature); - log_i("Humidity: %.1f%%", LocalSensors.ble_thsensor[idx].humidity); - log_i("Battery level: %d%%", LocalSensors.ble_thsensor[idx].batt_level); - log_i("RSSI %ddBm", LocalSensors.ble_thsensor[idx].rssi = (int)BLEdata["rssi"]); - m_devices_found++; - log_d("BLE devices found: %d", m_devices_found); - } - - // Abort scanning by touch sensor - if (TouchTriggered()) - { - log_i("Touch interrupt!"); - pBLEScan->stop(); - } - - // Abort scanning because all known devices have been found - if (m_devices_found == knownBLEAddresses.size()) - { - log_i("All devices found."); - pBLEScan->stop(); - } - } -}; -#endif - /** * \brief Arduino setup() */ @@ -533,7 +244,6 @@ void setup() Serial.begin(115200); Serial.setDebugOutput(true); - bool mqtt_connected = false; bool wifi_ok = false; if ((esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_EXT1) || TouchTriggered()) @@ -573,18 +283,18 @@ void setup() // Display start screen for 10 seconds if (ScreenNo == ScreenStart) { - #if defined(DISPLAY_3C) +#if defined(DISPLAY_3C) display.firstPage(); do { - #endif +#endif DisplayStartScreen(); - #if defined(DISPLAY_3C) +#if defined(DISPLAY_3C) } while (display.nextPage()); - #endif - #if defined(DISPLAY_BW) +#endif +#if defined(DISPLAY_BW) display.display(false); - #endif +#endif delay(10000L); ScreenNo = ScreenOWM; @@ -609,10 +319,6 @@ void setup() #if defined(DISPLAY_BW) display.display(true); #endif - // Add list of wifi networks - wifiMulti.addAP(ssid0, password0); - wifiMulti.addAP(ssid1, password1); - wifiMulti.addAP(ssid2, password2); wifi_ok = (StartWiFi() == WL_CONNECTED); @@ -687,7 +393,7 @@ void setup() } // Get local sensor data (Bluetooth, I²C, SPI, ...) - GetLocalData(); + localInterface.GetLocalData(); if (HistoryUpdateDue()) { @@ -766,8 +472,8 @@ void setup() // Fetch MQTT data MQTTClient MqttClient(MQTT_PAYLOAD_SIZE); - mqtt_connected = MqttConnect(net, MqttClient); - if (mqtt_connected) + MqttInterface mqttInterface(net, MqttClient); + if (mqttInterface.mqttConnect()) { // Show download icon int x = 88; @@ -800,7 +506,7 @@ void setup() } // no fast partial update // Get MQTT data (blocks until data is available) - GetMqttData(net, MqttClient); + mqttInterface.getMqttData(MqttSensors); if (display.epd2.hasFastPartialUpdate) { @@ -828,7 +534,7 @@ void setup() DisplayMQTTWeather(NULL); } } // no fast partial update - } // if (mqtt_connected) + } // if MQTT connected time_t t_now = time(NULL); log_i("Time since last MqttHist update: %ld min", (t_now - MqttHistTStamp) / 60); @@ -845,7 +551,7 @@ void setup() } SaveRainDayData(); - MqttUplink(net, MqttClient, LocalSensors); + mqttInterface.mqttUplink(net, MqttClient, LocalSensors); StopWiFi(); BeginSleep(); } @@ -916,35 +622,6 @@ inline bool TouchTriggered(void) return (touchPrevTrig || touchMidTrig || touchNextTrig); } -/** - * \brief Check if history update is due - * - * History is updated at an interval of HIST_UPDATE_RATE synchronized - * to the past full hour with a tolerance of HIST_UPDATE_TOL. - * - * \return true if update is due, otherwise false - */ -bool HistoryUpdateDue(void) -{ - int mins = (CurrentHour * 60 + CurrentMin) % HIST_UPDATE_RATE; - bool rv = (mins <= HIST_UPDATE_TOL) || (mins >= (HIST_UPDATE_RATE - HIST_UPDATE_TOL)); - return rv; -} - -/** - * \brief MQTT message reception callback function - * - * Sets the flag mqttMessageReceived and copies the received message to - * MqttBuf. - */ -void mqttMessageCb(String &topic, String &payload) -{ - mqttMessageReceived = true; - log_d("Payload size: %d", payload.length()); -#ifndef SIMULATE_MQTT - strncpy(MqttBuf, payload.c_str(), payload.length()); -#endif -} /** * \brief Display Open Weather Map (OWM) data @@ -965,11 +642,11 @@ void DisplayOWMWeather(const unsigned char *status_bitmap) { #endif display.fillScreen(GxEPD_WHITE); - #if defined(DISPLAY_BW) +#if defined(DISPLAY_BW) display.display(); - #endif +#endif DisplayGeneralInfoSection(); - DrawRSSI(705, 15, wifi_signal); + DrawRSSI(705, 15, WiFiSignal); DisplayDisplayWindSection(108, 146, WxConditions[0].Winddir, WxConditions[0].Windspeed, 81, true, TXT_WIND_SPEED_DIRECTION); DisplayMainWeatherSection(300, 100); // Centre section of display for Location, temperature, Weather report, current Wx Symbol and wind direction display.drawLine(0, 38, SCREEN_WIDTH - 3, 38, GxEPD_BLACK); @@ -983,8 +660,7 @@ void DisplayOWMWeather(const unsigned char *status_bitmap) { display.drawBitmap(x, y, status_bitmap, 48, 48, GxEPD_BLACK); } - - + #if defined(DISPLAY_3C) } while (display.nextPage()); #endif @@ -993,143 +669,6 @@ void DisplayOWMWeather(const unsigned char *status_bitmap) #endif } -/** - * \brief Connect to MQTT broker - * - * \param net network connection - * \param MqttClient MQTT client object - * - * \return true if connection was succesful, otherwise false - */ -bool MqttConnect(WiFiClient &net, MQTTClient &MqttClient) -{ - log_d("Checking wifi..."); - if (StartWiFi() != WL_CONNECTED) - { - return false; - } - - log_i("MQTT connecting..."); - unsigned long start = millis(); - - MqttClient.begin(MQTT_HOST, MQTT_PORT, net); - MqttClient.setOptions(MQTT_KEEPALIVE /* keepAlive [s] */, MQTT_CLEAN_SESSION /* cleanSession */, MQTT_TIMEOUT * 1000 /* timeout [ms] */); - - while (!MqttClient.connect(Hostname, MQTT_USER, MQTT_PASS)) - { - log_d("."); - if (millis() > start + MQTT_CONNECT_TIMEOUT * 1000) - { - log_i("Connect timeout!"); - return false; - } - delay(1000); - } - log_i("Connected!"); - - MqttClient.onMessage(mqttMessageCb); - - if (!MqttClient.subscribe(MQTT_SUB_IN)) - { - log_i("MQTT subscription failed!"); - return false; - } - return true; -} - -bool MqttUplink(WiFiClient &net, MQTTClient &MqttClient, local_sensors_t &data) -{ - char payload[21]; - char topic[41]; - - log_d("Checking wifi..."); - if (StartWiFi() != WL_CONNECTED) - { - return false; - } - - log_i("MQTT (publishing) connecting..."); - unsigned long start = millis(); - - MqttClient.begin(MQTT_HOST_P, MQTT_PORT_P, net); - MqttClient.setOptions(MQTT_KEEPALIVE /* keepAlive [s] */, MQTT_CLEAN_SESSION /* cleanSession */, MQTT_TIMEOUT * 1000 /* timeout [ms] */); - - while (!MqttClient.connect(Hostname, MQTT_USER_P, MQTT_PASS_P)) - { - log_d("."); - if (millis() > start + MQTT_CONNECT_TIMEOUT * 1000) - { - log_i("Connect timeout!"); - return false; - } - delay(1000); - } - log_i("Connected!"); - - log_d("Publishing..."); -#if defined(SCD4X_EN) - if (data.i2c_co2sensor.valid) - { - snprintf(payload, 20, "%u", data.i2c_co2sensor.co2); - snprintf(topic, 40, "%s/sdc4x/co2", Hostname); - MqttClient.publish(topic, payload); - - snprintf(payload, 20, "%3.1f", data.i2c_co2sensor.temperature); - snprintf(topic, 40, "%s/sdc4x/temperature", Hostname); - MqttClient.publish(topic, payload); - - snprintf(payload, 20, "%3.0f", data.i2c_co2sensor.humidity); - snprintf(topic, 40, "%s/sdc4x/humidity", Hostname); - MqttClient.publish(topic, payload); - } -#endif - -#if defined(BME280_EN) - if (data.i2c_thpsensor[0].valid) - { - snprintf(payload, 20, "%3.1f", data.i2c_thpsensor[0].temperature); - snprintf(topic, 40, "%s/bme280/temperature", Hostname); - MqttClient.publish(topic, payload); - - snprintf(payload, 20, "%3.0f", data.i2c_thpsensor[0].humidity); - snprintf(topic, 40, "%s/bme280/humidity", Hostname); - MqttClient.publish(topic, payload); - - snprintf(payload, 20, "%4.0f", data.i2c_thpsensor[0].pressure); - snprintf(topic, 40, "%s/bme280/pressure", Hostname); - MqttClient.publish(topic, payload); - } -#endif - -#if defined(THEENGSDECODER_EN) || defined(THEENGSDECODER_EN) - if (data.ble_thsensor[0].valid) - { - snprintf(payload, 20, "%3.1f", data.ble_thsensor[0].temperature); - snprintf(topic, 40, "%s/ble/temperature", Hostname); - MqttClient.publish(topic, payload); - - snprintf(payload, 20, "%3.0f", data.ble_thsensor[0].humidity); - snprintf(topic, 40, "%s/ble/humidity", Hostname); - MqttClient.publish(topic, payload); - - snprintf(payload, 20, "%u", data.ble_thsensor[0].batt_level); - snprintf(topic, 40, "%s/ble/batt_level", Hostname); - MqttClient.publish(topic, payload); - } -#endif - - for (int i = 0; i < 10; i++) - { - MqttClient.loop(); - delay(500); - } - - log_i("MQTT (publishing) disconnect."); - MqttClient.disconnect(); - - return true; -} - /** * \brief Find min/max temperature in MQTT history data * @@ -1182,175 +721,6 @@ void findMqttMinMaxTemp(float *t_min, float *t_max) *t_max = outdoorTMax; } -void convertUtcTimestamp(String time_str_utc, struct tm *ti_local, int tz_offset) -{ - struct tm received_at_utc; - memset(&received_at_utc, 0, sizeof(struct tm)); - memset(ti_local, 0, sizeof(struct tm)); - - // Read time string - strptime(time_str_utc.c_str(), "%Y-%m-%dT%H:%M:%S%z", &received_at_utc); - log_d("UTC: %d-%02d-%02d %02d:%02d DST: %d\n", received_at_utc.tm_year + 1900, received_at_utc.tm_mon + 1, received_at_utc.tm_mday, received_at_utc.tm_hour, received_at_utc.tm_min, received_at_utc.tm_isdst); - - // Convert time struct and calculate offset - time_t received_at = mktime(&received_at_utc) - tz_offset; - - // Convert time value to time struct (local time; with consideration of DST) - localtime_r(&received_at, ti_local); - char tbuf[26]; - strftime(tbuf, 25, "%Y-%m-%d %H:%M", ti_local); - log_d("Message received at: %s local time, DST: %d\n", tbuf, ti_local->tm_isdst); -} - -/** - * \brief Get MQTT data from broker - * - * \param net network connection - * \param MqttClient MQTT client object - */ -void GetMqttData(WiFiClient &net, MQTTClient &MqttClient) -{ - MqttSensors.valid = false; - - log_i("Waiting for MQTT message..."); - - // allocate the JsonDocument - JsonDocument doc; - - // LoRaWAN fPort - unsigned char f_port; - - do - { -#ifndef SIMULATE_MQTT - unsigned long start = millis(); - int count = 0; - while (!mqttMessageReceived) - { - MqttClient.loop(); - delay(10); - if (count++ == 1000) - { - log_d("."); - count = 0; - } - if (mqttMessageReceived) - break; - if (!MqttClient.connected()) - { - MqttConnect(net, MqttClient); - } - if (TouchTriggered()) - { - log_i("Touch interrupt!"); - return; - } - if (millis() > start + MQTT_DATA_TIMEOUT * 1000) - { - log_i("Timeout!"); - MqttClient.disconnect(); - return; - } - // During this time-consuming loop, updating local history could be due - if (HistoryUpdateDue()) - { - time_t now = time(NULL); - if (now - LocalHistTStamp >= (HIST_UPDATE_RATE - HIST_UPDATE_TOL) * 60) - { - LocalHistTStamp = now; - SaveLocalData(); - } - } - } -#else - log_i("(Simulated MQTT incoming message)"); - MqttSensors.valid = true; -#endif - - log_i("done!"); - log_d("%s", MqttBuf); - - log_d("Creating JSON object..."); - - // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, MqttBuf, MQTT_PAYLOAD_SIZE); - - // Test if parsing succeeds. - if (error) - { - log_i("deserializeJson() failed: %s", error.c_str()); - return; - } - else - { - log_d("Done!"); - } - - MqttClient.disconnect(); - MqttSensors.valid = true; - - const char *received_at = doc["received_at"]; - if (received_at) - { - strncpy(MqttSensors.received_at, received_at, 30); - } - f_port = doc["uplink_message"]["f_port"]; - } while (f_port != 1); - JsonVariant payload = doc["uplink_message"]["decoded_payload"]["bytes"]; - - MqttSensors.air_temp_c = payload[WS_TEMP_C].isNull() ? INV_TEMP : payload[WS_TEMP_C]; - MqttSensors.humidity = payload[WS_HUMIDITY].isNull() ? INV_UINT8 : payload[WS_HUMIDITY]; - MqttSensors.indoor_temp_c = payload[TH1_TEMP_C].isNull() ? INV_TEMP : payload[TH1_TEMP_C]; - MqttSensors.indoor_humidity = payload[TH1_HUMIDITY].isNull() ? INV_UINT8 : payload[TH1_HUMIDITY]; - MqttSensors.battery_v = payload[A0_VOLTAGE_MV].isNull() ? INV_UINT16 : payload[A0_VOLTAGE_MV]; - MqttSensors.rain_day = payload[WS_RAIN_DAILY_MM].isNull() ? INV_FLOAT : payload[WS_RAIN_DAILY_MM]; - MqttSensors.rain_hr = payload[WS_RAIN_HOURLY_MM].isNull() ? INV_FLOAT : payload[WS_RAIN_HOURLY_MM]; - MqttSensors.rain_mm = payload[WS_RAIN_MM].isNull() ? INV_FLOAT : payload[WS_RAIN_MM]; - MqttSensors.rain_month = payload[WS_RAIN_MONTHLY_MM].isNull() ? INV_FLOAT : payload[WS_RAIN_MONTHLY_MM]; - MqttSensors.rain_week = payload[WS_RAIN_WEEKLY_MM].isNull() ? INV_FLOAT : payload[WS_RAIN_WEEKLY_MM]; - MqttSensors.soil_moisture = payload[SOIL1_MOISTURE].isNull() ? INV_UINT8 : payload[SOIL1_MOISTURE]; - MqttSensors.soil_temp_c = payload[SOIL1_TEMP_C].isNull() ? INV_TEMP : payload[SOIL1_TEMP_C]; - MqttSensors.water_temp_c = payload[OW0_TEMP_C].isNull() ? INV_TEMP : payload[OW0_TEMP_C]; - MqttSensors.wind_avg_meter_sec = payload[WS_WIND_AVG_MS].isNull() ? INV_FLOAT : payload[WS_WIND_AVG_MS]; - MqttSensors.wind_direction_deg = payload[WS_WIND_DIR_DEG].isNull() ? INV_UINT16 : payload[WS_WIND_DIR_DEG]; - MqttSensors.wind_gust_meter_sec = payload[WS_WIND_GUST_MS].isNull() ? INV_FLOAT : payload[WS_WIND_GUST_MS]; - - // FIXME: This is a workaround for the time being - JsonObject status = payload["status"]; - bool ble_ok = MqttSensors.indoor_temp_c != INV_TEMP && MqttSensors.indoor_humidity != INV_UINT8; - // MqttSensors.status.ble_ok = status["ble_ok"] | ble_ok; - MqttSensors.status.ble_ok = ble_ok; - bool s1_dec_ok = MqttSensors.soil_temp_c != INV_TEMP && MqttSensors.soil_moisture != INV_UINT8; - // MqttSensors.status.s1_dec_ok = status["s1_dec_ok"] | s1_dec_ok; - MqttSensors.status.s1_dec_ok = s1_dec_ok; - bool ws_dec_ok = MqttSensors.air_temp_c != INV_TEMP && MqttSensors.rain_mm != INV_FLOAT; - // MqttSensors.status.ws_dec_ok = status["ws_dec_ok"] | ws_dec_ok; - MqttSensors.status.ws_dec_ok = ws_dec_ok; - - MqttSensors.status.s1_batt_ok = status["s1_batt_ok"]; - MqttSensors.status.ws_batt_ok = status["ws_batt_ok"]; - - // Sanity checks - if (MqttSensors.humidity == 0) - { - MqttSensors.status.ws_dec_ok = false; - } - MqttSensors.rain_hr_valid = (MqttSensors.rain_hr >= 0) && (MqttSensors.rain_hr < 300); - MqttSensors.rain_day_valid = (MqttSensors.rain_day >= 0) && (MqttSensors.rain_day < 1800); - - // If not valid, set value to zero to avoid any problems with auto-scale etc. - if (!MqttSensors.rain_hr_valid) - { - MqttSensors.rain_hr = 0; - } - if (!MqttSensors.rain_day_valid) - { - MqttSensors.rain_day = 0; - } - - log_i("MQTT data updated: %d", MqttSensors.valid ? 1 : 0); -} - /** * \brief Save MQTT data to history FIFO */ @@ -1424,9 +794,9 @@ void DisplayMQTTWeather(const unsigned char *status_bitmap) { #endif display.fillScreen(GxEPD_WHITE); - #if defined(DISPLAY_BW) +#if defined(DISPLAY_BW) display.display(); - #endif +#endif DisplayGeneralInfoSection(); DisplayMQTTDateTime(90, 225); display.drawBitmap(5, 25, epd_bitmap_remote, 220, 165, GxEPD_BLACK); @@ -1544,7 +914,7 @@ void DisplayMQTTWeather(const unsigned char *status_bitmap) display.drawBitmap(680, 100, epd_bitmap_signal_disconnected, 40, 40, GxEPD_BLACK); } DisplayMqttHistory(); - DrawRSSI(705, 15, wifi_signal); // Wi-Fi signal strength + DrawRSSI(705, 15, WiFiSignal); // Wi-Fi signal strength const int x = 88; const int y = 272; @@ -1560,187 +930,6 @@ void DisplayMQTTWeather(const unsigned char *status_bitmap) #endif } -/** - * \brief Get local sensor data - */ -void GetLocalData(void) -{ - LocalSensors.ble_thsensor[0].valid = false; - LocalSensors.i2c_thpsensor[0].valid = false; - LocalSensors.i2c_co2sensor.valid = false; - -#if defined(SCD4X_EN) || defined(BME280_EN) - TwoWire myWire = TwoWire(0); - myWire.begin(I2C_SDA, I2C_SCL, 100000); -#endif - -#ifdef SCD4X_EN - SensirionI2CScd4x scd4x; - - uint16_t error; - char errorMessage[256]; - - scd4x.begin(myWire); - - // stop potential previously started measurement - error = scd4x.stopPeriodicMeasurement(); - if (error) - { - errorToString(error, errorMessage, 256); - log_e("Error trying to execute stopPeriodicMeasurement(): %s", errorMessage); - } - - // Start Measurement - error = scd4x.measureSingleShot(); - if (error) - { - errorToString(error, errorMessage, 256); - log_e("Error trying to execute measureSingleShot(): %s", errorMessage); - } - - log_d("First measurement takes ~5 sec..."); -#endif - -#ifdef MITHERMOMETER_EN - // Setup BLE Temperature/Humidity Sensors - ATC_MiThermometer miThermometer(knownBLEAddresses); //!< Mijia Bluetooth Low Energy Thermo-/Hygrometer - miThermometer.begin(); - - // Set sensor data invalid - miThermometer.resetData(); - - // Get sensor data - run BLE scan for - miThermometer.getData(bleScanTime); - - if (miThermometer.data[0].valid) - { - LocalSensors.ble_thsensor[0].valid = true; - LocalSensors.ble_thsensor[0].temperature = miThermometer.data[0].temperature / 100.0; - LocalSensors.ble_thsensor[0].humidity = miThermometer.data[0].humidity / 100.0; - LocalSensors.ble_thsensor[0].batt_level = miThermometer.data[0].batt_level; - } - miThermometer.clearScanResults(); -#endif - -#ifdef THEENGSDECODER_EN - - // From https://github.com/theengs/decoder/blob/development/examples/ESP32/ScanAndDecode/ScanAndDecode.ino: - // MyAdvertisedDeviceCallbacks are still triggered multiple times; this makes keeping track of received - // sensors difficult. Setting ScanFilterMode to CONFIG_BTDM_SCAN_DUPL_TYPE_DATA_DEVICE seems to - // restrict callback invocation to once per device as desired. - // NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DEVICE); - NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA_DEVICE); - NimBLEDevice::setScanDuplicateCacheSize(200); - NimBLEDevice::init(""); - - pBLEScan = NimBLEDevice::getScan(); // create new scan - // Set the callback for when devices are discovered, no duplicates. - pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks(), false); - pBLEScan->setActiveScan(true); // Set active scanning, this will get more data from the advertiser. - pBLEScan->setInterval(97); // How often the scan occurs / switches channels; in milliseconds, - pBLEScan->setWindow(37); // How long to scan during the interval; in milliseconds. - pBLEScan->setMaxResults(0); // do not store the scan results, use callback only. - pBLEScan->start(bleScanTime, false /* is_continue */); -#endif - - if (LocalSensors.ble_thsensor[0].valid) - { - log_d("Outdoor Air Temp.: % 3.1f °C", LocalSensors.ble_thsensor[0].temperature); - log_d("Outdoor Humidity: %3.1f %%", LocalSensors.ble_thsensor[0].humidity); - log_d("Outdoor Sensor Batt: %d %%", LocalSensors.ble_thsensor[0].batt_level); - } - else - { - log_d("Outdoor Air Temp.: --.- °C"); - log_d("Outdoor Humidity: -- %%"); - log_d("Outdoor Sensor Batt: -- %%"); - } - -#ifdef BME280_EN - pocketBME280 bme280; - bme280.setAddress(0x77); - log_v("BME280: start"); - if (bme280.begin(myWire)) - { - LocalSensors.i2c_thpsensor[0].valid = true; - bme280.startMeasurement(); - while (!bme280.isMeasuring()) - { - log_v("BME280: Waiting for Measurement to start"); - delay(1); - } - while (bme280.isMeasuring()) - { - log_v("BME280: Measurement in progress"); - delay(1); - } - LocalSensors.i2c_thpsensor[0].temperature = bme280.getTemperature() / 100.0; - LocalSensors.i2c_thpsensor[0].pressure = bme280.getPressure() / 100.0; - LocalSensors.i2c_thpsensor[0].humidity = bme280.getHumidity() / 1024.0; - log_d("Indoor Temperature: %.1f °C", LocalSensors.i2c_thpsensor[0].temperature); - log_d("Indoor Pressure: %.0f hPa", LocalSensors.i2c_thpsensor[0].pressure); - log_d("Indoor Humidity: %.0f %%rH", LocalSensors.i2c_thpsensor[0].humidity); - } - else - { - log_d("Indoor Temperature: --.- °C"); - log_d("Indoor Pressure: -- hPa"); - log_d("Indoor Humidity: -- %%rH"); - } -#endif - -#ifdef SCD4X_EN - if (LocalSensors.i2c_thpsensor[0].valid) - { - error = scd4x.setAmbientPressure((uint16_t)LocalSensors.i2c_thpsensor[0].pressure); - if (error) - { - errorToString(error, errorMessage, 256); - log_e("Error trying to execute setAmbientPressure(): %s", errorMessage); - } - } - // Read Measurement - bool isDataReady = false; - - for (int i = 0; i < 50; i++) - { - error = scd4x.getDataReadyFlag(isDataReady); - if (error) - { - errorToString(error, errorMessage, 256); - log_e("Error trying to execute getDataReadyFlag(): %s", errorMessage); - } - if (error || isDataReady) - { - break; - } - delay(100); - } - - if (isDataReady && !error) - { - error = scd4x.readMeasurement(LocalSensors.i2c_co2sensor.co2, LocalSensors.i2c_co2sensor.temperature, LocalSensors.i2c_co2sensor.humidity); - if (error) - { - errorToString(error, errorMessage, 256); - log_e("Error trying to execute readMeasurement(): %s", errorMessage); - } - else if (LocalSensors.i2c_co2sensor.co2 == 0) - { - log_e("Invalid sample detected, skipping."); - } - else - { - log_d("SCD4x CO2: %d ppm", LocalSensors.i2c_co2sensor.co2); - log_d("SCD4x Temperature: %4.1f °C", LocalSensors.i2c_co2sensor.temperature); - log_d("SCD4x Humidity: %3.1f %%rH", LocalSensors.i2c_co2sensor.humidity); - LocalSensors.i2c_co2sensor.valid = true; - } - } - scd4x.powerDown(); -#endif -} - /** * \brief Save local sensor data to history FIFO */ @@ -1824,9 +1013,9 @@ void DisplayLocalWeather(const unsigned char *status_bitmap) { #endif display.fillScreen(GxEPD_WHITE); - #if defined(DISPLAY_BW) +#if defined(DISPLAY_BW) display.display(); - #endif +#endif DisplayGeneralInfoSection(); DisplayDateTime(90, 225); #ifdef SCD4X_EN @@ -1914,7 +1103,7 @@ void DisplayLocalWeather(const unsigned char *status_bitmap) #endif DisplayLocalHistory(); - DrawRSSI(705, 15, wifi_signal); // Wi-Fi signal strength + DrawRSSI(705, 15, WiFiSignal); // Wi-Fi signal strength const int x = 88; const int y = 272; @@ -2042,7 +1231,7 @@ void DisplayMQTTDateTime(int x, int y) return; convertUtcTimestamp(MqttSensors.received_at, &timeinfo, _timezone); - printTime(timeinfo, mqtt_date, mqtt_time, 32); + printTime(timeinfo, mqtt_date, mqtt_time, 32, TXT_UPDATED); u8g2Fonts.setFont(u8g2_font_helvB14_tf); drawString(x, y, mqtt_date, CENTER); @@ -2290,9 +1479,9 @@ void DisplayPrecipitationSection(int x, int y, int pwidth, int pdepth) void DisplayAstronomySection(int x, int y) { display.drawRect(x, y + 16, 191, 65, GxEPD_BLACK); - //display.drawLine(x, y + 16, x + 191, y + 16, GxEPD_BLACK); - //display.drawLine(x, y + 16 + 64, x + 191, y + 16 + 64, GxEPD_BLACK); - //display.drawLine(x, y + 16, x, y + 16 + 64, GxEPD_BLACK); + // display.drawLine(x, y + 16, x + 191, y + 16, GxEPD_BLACK); + // display.drawLine(x, y + 16 + 64, x + 191, y + 16 + 64, GxEPD_BLACK); + // display.drawLine(x, y + 16, x, y + 16 + 64, GxEPD_BLACK); u8g2Fonts.setFont(u8g2_font_helvB08_tf); drawString(x + 4, y + 24, ConvertUnixTime(WxConditions[0].Sunrise + WxConditions[0].Timezone).substring(0, 5) + " " + TXT_SUNRISE, LEFT); drawString(x + 4, y + 44, ConvertUnixTime(WxConditions[0].Sunset + WxConditions[0].Timezone).substring(0, 5) + " " + TXT_SUNSET, LEFT); @@ -2847,55 +2036,6 @@ void arrow(int x, int y, int asize, float aangle, int pwidth, int plength) display.fillTriangle(xx1, yy1, xx3, yy3, xx2, yy2, GxEPD_BLACK); } -/** - * \brief Start WiFi connection - * - * Establishes a WiFi connection with global settings ssid and password- - * If succesful, the global variable wifi_signal is updated with the RSSI. - * - * \return WiFi.status(); WL_CONNECTED if successful - */ -uint8_t StartWiFi() -{ - if (WiFi.status() == WL_CONNECTED) - { - return WL_CONNECTED; - } - - IPAddress dns(MY_DNS); - WiFi.disconnect(); - WiFi.mode(WIFI_STA); // switch off AP - // WiFi.setAutoConnect(true); // no longer valid - // WiFi.setAutoReconnect(false); // default: true - - uint8_t connectionStatus = wifiMulti.run(); - - if (connectionStatus == WL_CONNECTED) - { - String ssid = WiFi.SSID(); - wifi_signal = WiFi.RSSI(); // Get Wifi Signal strength now, because the WiFi will be turned off to save power! - log_i("WiFi connected to '%s'", ssid.c_str()); - log_i("WiFi connected at: %s", WiFi.localIP().toString().c_str()); - } - else - { - log_w("WiFi connection failed!"); - } - - return connectionStatus; -} - -/** - * \brief Stop WiFi connection - * - * Disconnects WiFi and switches off WiFi to save power. - */ -void StopWiFi() -{ - WiFi.disconnect(); - WiFi.mode(WIFI_OFF); -} - #if 0 /** * \brief Display status section @@ -2955,90 +2095,6 @@ void DrawRSSI(int x, int y, int rssi) } } -/** - * \brief Get time from NTP server and initialize/update RTC - * - * The global constants gmtOffset_sec, daylightOffset_sec and ntpServer are used. - * - * \return true if RTC initialization was successfully, false otherwise - */ -boolean SetupTime() -{ - configTime(gmtOffset_sec, daylightOffset_sec, ntpServer, "pool.ntp.org"); //(gmtOffset_sec, daylightOffset_sec, ntpServer) - setenv("TZ", Timezone, 1); // setenv()adds the "TZ" variable to the environment with a value TimeZone, only used if set to 1, 0 means no change - tzset(); // Set the TZ environment variable - delay(100); - bool TimeStatus = UpdateLocalTime(); - return TimeStatus; -} - -/** - * \brief Get local time (from RTC) and update global variables - * - * The global variables CurrentHour, CurrentMin, CurrentSec, CurrentDay - * and day_output/time_output are updated. - * date_output contains the localized date string, - * time_output contains the localized text TXT_UPDATED with the time string. - * - * \return true if RTC time was valid, false otherwise - */ -boolean UpdateLocalTime() -{ - struct tm timeinfo; - char time_output[32], date_output[32]; - while (!getLocalTime(&timeinfo, 10000)) - { // Wait for 10-sec for time to synchronise - log_w("Failed to obtain time"); - return false; - } - CurrentHour = timeinfo.tm_hour; - CurrentMin = timeinfo.tm_min; - CurrentSec = timeinfo.tm_sec; - CurrentDay = timeinfo.tm_mday; - printTime(timeinfo, time_output, date_output, 32); - - Date_str = date_output; - Time_str = time_output; - return true; -} - -/** - * \brief Print localized time to variables - * - * \param timeinfo date and time data structure - * \param date_output test output buffer for localized date - * \param time_output text output buffer for localized time - * \param max_size maximum text buffer size - * - * - */ -void printTime(struct tm &timeinfo, char *date_output, char *time_output, int max_size) -{ - char update_time[30]; - - // See http://www.cplusplus.com/reference/ctime/strftime/ - // Serial.println(&timeinfo, "%a %b %d %Y %H:%M:%S"); // Displays: Saturday, June 24 2017 14:05:49 - if (Units == "M") - { - if ((Language == "CZ") || (Language == "DE") || (Language == "PL") || (Language == "NL")) - { - sprintf(date_output, "%s, %02u. %s %04u", weekday_D[timeinfo.tm_wday], timeinfo.tm_mday, month_M[timeinfo.tm_mon], (timeinfo.tm_year) + 1900); // day_output >> So., 23. Juni 2019 << - } - else - { - sprintf(date_output, "%s %02u-%s-%04u", weekday_D[timeinfo.tm_wday], timeinfo.tm_mday, month_M[timeinfo.tm_mon], (timeinfo.tm_year) + 1900); - } - strftime(update_time, max_size, "%H:%M:%S", &timeinfo); // Creates: '14:05:49' - sprintf(time_output, "%s %s", TXT_UPDATED, update_time); - } - else - { - strftime(date_output, max_size, "%a %b-%d-%Y", &timeinfo); // Creates 'Sat May-31-2019' - strftime(update_time, sizeof(update_time), "%r", &timeinfo); // Creates: '02:05:49pm' - sprintf(time_output, "%s %s", TXT_UPDATED, update_time); - } -} - #if 0 /** * \brief Draw battery status @@ -3223,30 +2279,7 @@ void InitialiseDisplay() display.setFullWindow(); } // ######################################################################################### -/*String Translate_EN_DE(String text) { - if (text == "clear") return "klar"; - if (text == "sunny") return "sonnig"; - if (text == "mist") return "Nebel"; - if (text == "fog") return "Nebel"; - if (text == "rain") return "Regen"; - if (text == "shower") return "Regenschauer"; - if (text == "cloudy") return "wolkig"; - if (text == "clouds") return "Wolken"; - if (text == "drizzle") return "Nieselregen"; - if (text == "snow") return "Schnee"; - if (text == "thunderstorm") return "Gewitter"; - if (text == "light") return "leichter"; - if (text == "heavy") return "schwer"; - if (text == "mostly cloudy") return "größtenteils bewölkt"; - if (text == "overcast clouds") return "überwiegend bewölkt"; - if (text == "scattered clouds") return "aufgelockerte Bewölkung"; - if (text == "few clouds") return "ein paar Wolken"; - if (text == "clear sky") return "klarer Himmel"; - if (text == "broken clouds") return "aufgerissene Bewölkung"; - if (text == "light rain") return "leichter Regen"; - return text; - } -*/ + /* Version 16.11 diff --git a/examples/Waveshare_7_5_T7_Sensors/config.h b/examples/Waveshare_7_5_T7_Sensors/config.h new file mode 100644 index 0000000..9050ed4 --- /dev/null +++ b/examples/Waveshare_7_5_T7_Sensors/config.h @@ -0,0 +1,163 @@ +/////////////////////////////////////////////////////////////////////////////// +// config.h +// +// General configuration +// +// Note: Provide secrets in secrets.h! +// +// created: 10/2024 +// +// +// MIT License +// +// Copyright (c) 2024 Matthias Prinke +// +// 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. +// +// +// History: +// +// 20241010 Extracted from owm_credentials.h +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef CONFIG_H +#define CONFIG_H + +// Screen definitions +#define ScreenOWM 0 +#define ScreenLocal 1 +#define ScreenMQTT 2 +#define ScreenStart 3 + +#define TXT_START "Your Weather Station" +#define START_SCREEN ScreenStart +#define LAST_SCREEN ScreenMQTT + +// Locations / Screen Titles +#define LOCATIONS_TXT {"Forecast", "Local", "Remote", "Start"} + +// #define DISPLAY_3C +#define DISPLAY_BW +#define SCREEN_WIDTH 800 //!< EPD screen width +#define SCREEN_HEIGHT 480 //!< EPD screen height + +// #define MITHERMOMETER_EN //!< Enable MiThermometer (BLE sensors) +#define THEENGSDECODER_EN //!< Enable Theengs Decoder (BLE sensors) +#define BME280_EN //!< Enable BME280 T/H/p-sensor (I2C) +#define SCD4X_EN //!< Enable SCD4x CO2-sensor (I2C) +// #define WATERTEMP_EN //!< Enable Wather Temperature Display (MQTT) +#define MITHERMOMETER_BATTALERT 6 //!< Low battery alert threshold [%] +#define WATER_TEMP_INVALID -30.0 //!< Water temperature invalid marker [°C] +#define I2C_SDA 21 //!< I2C Serial Data Pin +#define I2C_SCL 22 //!< I2C Serial Clock Pin + +// Domain Name Server - separate bytes by comma! +#define MY_DNS 192,168,0,1 + +// Weather display's hostname +#define HOSTNAME "your_hostname" + +// TODO: Move to config.h +#define TIMEZONE "GMT0BST,M3.5.0/01,M10.5.0/02" // Choose your time zone from: https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv + // See below for examples +#define NTPSERVER "0.uk.pool.ntp.org" // Or, choose a time server close to you, but in most cases it's best to use pool.ntp.org to find an NTP server + // then the NTP system decides e.g. 0.pool.ntp.org, 1.pool.ntp.org as the NTP syem tries to find the closest available servers + // EU "0.europe.pool.ntp.org" + // US "0.north-america.pool.ntp.org" + // See: https://www.ntppool.org/en/ +#define GMT_OFFSET_SEC 0 // UK normal time is GMT, so GMT Offset is 0, for US (-5Hrs) is typically -18000, AU is typically (+8hrs) 28800 +#define DAYLIGHT_OFFSET_SEC 3600 // In the UK DST is +1hr or 3600-secs, other countries may use 2hrs 7200 or 30-mins 1800 or 5.5hrs 19800 Ahead of GMT use + offset behind - offset + +// Example time zones +// "MET-1METDST,M3.5.0/01,M10.5.0/02"; // Most of Europe +// "CET-1CEST,M3.5.0,M10.5.0/3"; // Central Europe +// "EST-2METDST,M3.5.0/01,M10.5.0/02"; // Most of Europe +// "EST5EDT,M3.2.0,M11.1.0"; // EST USA +// "CST6CDT,M3.2.0,M11.1.0"; // CST USA +// "MST7MDT,M4.1.0,M10.5.0"; // MST USA +// "NZST-12NZDT,M9.5.0,M4.1.0/3"; // Auckland +// "EET-2EEST,M3.5.5/0,M10.5.5/0"; // Asia +// "ACST-9:30ACDT,M10.1.0,M4.1.0/3": // Australia + +//!< List of known BLE sensors' MAC addresses (separate multiple entries by comma) + //#define KNOWN_BLE_ADDRESSES {"a4:c1:38:b8:1f:7f"} +#define KNOWN_BLE_ADDRESSES {"49:22:05:17:0c:1f"} + +// #define SIMULATE_MQTT +// #define FORCE_LOW_BATTERY +// #define FORCE_NO_SIGNAL + +// MQTT connection for subscribing (remote sensor data) +#define MQTT_PORT 1883 +#define MQTT_HOST "your_broker" +#define MQTT_SUB_IN "your/subscribe/topic" + +// MQTT connection for publishing (local sensor data) +#define MQTT_PORT_P 1883 +#define MQTT_HOST_P "your_broker_pub" + +#define MQTT_PAYLOAD_SIZE 4096 +#define MQTT_CONNECT_TIMEOUT 30 +#define MQTT_DATA_TIMEOUT 600 +#define MQTT_KEEPALIVE 60 +#define MQTT_TIMEOUT 1800 +#define MQTT_CLEAN_SESSION false + +#define MQTT_HIST_SIZE 144 +#define RAIN_HR_HIST_SIZE 24 +#define RAIN_DAY_HIST_SIZE 29 +#define LOCAL_HIST_SIZE 144 +#define HIST_UPDATE_RATE 30 +#define HIST_UPDATE_TOL 5 + +// TODO: Move to LocalSensors class +// Local Sensor Data +struct LocalS +{ + struct + { + bool valid; //!< data valid + float temperature; //!< temperature in degC + float humidity; //!< humidity in % + uint8_t batt_level; //!< battery level in % + int rssi; //!< RSSI in dBm + } ble_thsensor[1]; + struct + { + bool valid; //!< data valid + float temperature; //!< temperature in degC + float humidity; //!< humidity in % + float pressure; //!< pressure in hPa + } i2c_thpsensor[1]; + struct + { + bool valid; //!< data valid + float temperature; //!< temperature in degC + float humidity; //!< humidity in % + uint16_t co2; //!< CO2 in ppm + } i2c_co2sensor; +}; + +typedef struct LocalS local_sensors_t; //!< Shortcut for struct LocalS + +#endif diff --git a/examples/Waveshare_7_5_T7_Sensors/owm_credentials.h b/examples/Waveshare_7_5_T7_Sensors/owm_credentials.h index 16a0c90..6f98b4d 100644 --- a/examples/Waveshare_7_5_T7_Sensors/owm_credentials.h +++ b/examples/Waveshare_7_5_T7_Sensors/owm_credentials.h @@ -1,74 +1,48 @@ -// Change to your WiFi credentials -const char* ssid0 = "your_ssid0"; -const char* password0 = "your_password0"; -const char* ssid1 = "your_ssid1"; -const char* password1 = "your_password1"; -const char* ssid2 = "your_ssid2"; -const char* password2 = "your_password2"; +/////////////////////////////////////////////////////////////////////////////// +// MqttInterface.h +// +// MQTT Interface for ESP32-e-Paper-Weather-Display +// +// +// created: 10/2024 +// +// +// MIT License +// +// Copyright (c) 2024 Matthias Prinke +// +// 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. +// +// +// History: +// +// 20241010 Removed parts not related to OWM +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _OWM_CREDENTIALS_H +#define _OWM_CREDENTIALS_H -// Domain Name Server - separate bytes by comma! -#define MY_DNS 192,168,0,1 - -// Weather display's hostname -const char *Hostname = "your_hostname"; - -//!< List of known BLE sensors' MAC addresses (separate multiple entries by comma) -//#define KNOWN_BLE_ADDRESSES {"a4:c1:38:b8:1f:7f"} -#define KNOWN_BLE_ADDRESSES {"49:22:05:17:0c:1f"} - -// MQTT connection for subscribing (remote sensor data) -const int MQTT_PORT = 1883; -const char *MQTT_HOST = "your_broker"; -const char *MQTT_USER = "your_user"; // leave blank if no credentials used -const char *MQTT_PASS = "your_passwd"; // leave blank if no credentials used -const char *MQTT_SUB_IN = "your/subscribe/topic"; - -// TOPICS_NEW: BresserWeatherSensorLW -#define TOPICS_NEW - -#ifdef TOPICS_NEW - #define WS_TEMP_C "ws_temp_c" - #define WS_HUMIDITY "ws_humidity" - #define TH1_TEMP_C "th1_temp_c" - #define TH1_HUMIDITY "th1_humidity" - #define A0_VOLTAGE_MV "a0_voltage_mv" - #define WS_RAIN_DAILY_MM "ws_rain_daily_mm" - #define WS_RAIN_HOURLY_MM "ws_rain_hourly_mm" - #define WS_RAIN_MM "ws_rain_mm" - #define WS_RAIN_MONTHLY_MM "ws_rain_monthly_mm" - #define WS_RAIN_WEEKLY_MM "ws_rain_weekly_mm" - #define SOIL1_MOISTURE "soil1_moisture" - #define SOIL1_TEMP_C "soil1_temp_c" - #define OW0_TEMP_C "ow0_temp_c" - #define WS_WIND_AVG_MS "ws_wind_avg_ms" - #define WS_WIND_DIR_DEG "ws_wind_dir_deg" - #define WS_WIND_GUST_MS "ws_wind_gust_ms" -#else - #define WS_TEMP_C "air_temp_c" - #define WS_HUMIDITY "humidity" - #define TH1_TEMP_C "indoor_temp_c" - #define TH1_HUMIDITY "indoor_humidity" - #define A0_VOLTAGE_MV "battery_v" - #define WS_RAIN_DAILY_MM "rain_day" - #define WS_RAIN_HOURLY_MM "rain_hr" - #define WS_RAIN_MM "rain_mm" - #define WS_RAIN_MONTHLY_MM "rain_mon" - #define WS_RAIN_WEEKLY_MM "rain_week" - #define SOIL1_MOISTURE "soil_moisture" - #define SOIL1_TEMP_C "soil_temp_c" - #define OW0_TEMP_C "water_temp_c" - #define WS_WIND_AVG_MS "wind_avg_meter_sec" - #define WS_WIND_DIR_DEG "wind_direction_deg" - #define WS_WIND_GUST_MS "wind_gust_meter_sec" -#endif - -// MQTT connection for publishing (local sensor data) -const int MQTT_PORT_P = 1883; -const char *MQTT_HOST_P = "your_broker_pub"; -const char *MQTT_USER_P = "your_user_pub"; -const char *MQTT_PASS_P = "your passwd_pub"; - // Use your own API key by signing up for a free developer account at https://openweathermap.org/ String apikey = "your_API_key"; // See: https://openweathermap.org/ // It's free to get an API key, but don't take more than 60 readings/minute! const char server[] = "api.openweathermap.org"; @@ -84,41 +58,6 @@ String Language = "EN"; // NOTE: Only the wea // Korean (KR) Latvian (LA) Lithuanian (LT) Macedonian (MK) Slovak (SK) Slovenian (SL) Vietnamese (VI) String Hemisphere = "north"; // or "south" String Units = "M"; // Use 'M' for Metric or I for Imperial -const char* Timezone = "GMT0BST,M3.5.0/01,M10.5.0/02"; // Choose your time zone from: https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv - // See below for examples -const char* ntpServer = "0.uk.pool.ntp.org"; // Or, choose a time server close to you, but in most cases it's best to use pool.ntp.org to find an NTP server - // then the NTP system decides e.g. 0.pool.ntp.org, 1.pool.ntp.org as the NTP syem tries to find the closest available servers - // EU "0.europe.pool.ntp.org" - // US "0.north-america.pool.ntp.org" - // See: https://www.ntppool.org/en/ -int gmtOffset_sec = 0; // UK normal time is GMT, so GMT Offset is 0, for US (-5Hrs) is typically -18000, AU is typically (+8hrs) 28800 -int daylightOffset_sec = 3600; // In the UK DST is +1hr or 3600-secs, other countries may use 2hrs 7200 or 30-mins 1800 or 5.5hrs 19800 Ahead of GMT use + offset behind - offset - -// Example time zones -//const char* Timezone = "MET-1METDST,M3.5.0/01,M10.5.0/02"; // Most of Europe -//const char* Timezone = "CET-1CEST,M3.5.0,M10.5.0/3"; // Central Europe -//const char* Timezone = "EST-2METDST,M3.5.0/01,M10.5.0/02"; // Most of Europe -//const char* Timezone = "EST5EDT,M3.2.0,M11.1.0"; // EST USA -//const char* Timezone = "CST6CDT,M3.2.0,M11.1.0"; // CST USA -//const char* Timezone = "MST7MDT,M4.1.0,M10.5.0"; // MST USA -//const char* Timezone = "NZST-12NZDT,M9.5.0,M4.1.0/3"; // Auckland -//const char* Timezone = "EET-2EEST,M3.5.5/0,M10.5.5/0"; // Asia -//const char* Timezone = "ACST-9:30ACDT,M10.1.0,M4.1.0/3": // Australia - -// Personalization Options - -// Screen definitions -#define ScreenOWM 0 -#define ScreenLocal 1 -#define ScreenMQTT 2 -#define ScreenStart 3 - -#define TXT_START "Your Weather Station" -#define START_SCREEN ScreenStart -#define LAST_SCREEN ScreenMQTT -// Locations / Screen Titles -#define LOCATIONS_TXT {"Forecast", "Local", "Remote", "Start"} -#include "bitmap_local.h" // Picture shown on ScreenLocal - replace by your own -#include "bitmap_remote.h" // Picture shown on ScreenMQTT - replace by your own +#endif \ No newline at end of file diff --git a/examples/Waveshare_7_5_T7_Sensors/secrets.h b/examples/Waveshare_7_5_T7_Sensors/secrets.h new file mode 100644 index 0000000..971a0b4 --- /dev/null +++ b/examples/Waveshare_7_5_T7_Sensors/secrets.h @@ -0,0 +1,16 @@ +// Change to your WiFi credentials +#define ssid0 "your_ssid0" +#define password0 "your_password0" +#define ssid1 "your_ssid1" +#define password1 "your_password1" +#define ssid2 "your_ssid2" +#define password2 "your_password2" + +// MQTT broker for subscribing +// leave blank if no credentials used +#define MQTT_USER "your_user" +#define MQTT_PASS "your_passwd" + +// MQTT broker for publishing +#define MQTT_USER_P "your_user_pub" +#define MQTT_PASS_P "your passwd_pub" \ No newline at end of file diff --git a/examples/Waveshare_7_5_T7_Sensors/utils.cpp b/examples/Waveshare_7_5_T7_Sensors/utils.cpp new file mode 100644 index 0000000..a86b364 --- /dev/null +++ b/examples/Waveshare_7_5_T7_Sensors/utils.cpp @@ -0,0 +1,186 @@ +/////////////////////////////////////////////////////////////////////////////// +// utils.cpp +// +// Utility functions for ESP32-e-Paper-Weather-Display +// +// +// created: 10/2024 +// +// +// MIT License +// +// Copyright (c) 2024 Matthias Prinke +// +// 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. +// +// +// History: +// +// 20241010 Extracted from Waveshare_7_5_T7_Sensors.ino +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#include "utils.h" + +extern const char *weekday_D[]; +extern const char *month_M[]; +extern const String Language; +extern const String Units; +extern const String TXT_UPDATED; +extern String Time_str; +extern String Date_str; +extern int CurrentDay; +extern int CurrentHour; +extern int CurrentMin; +extern int CurrentSec; +extern int WiFiSignal; + +// WiFi connection to multiple alternative access points +static WiFiMulti wifiMulti; + +// Start WiFi connection +uint8_t StartWiFi() +{ + // Add list of wifi networks + wifiMulti.addAP(ssid0, password0); + wifiMulti.addAP(ssid1, password1); + wifiMulti.addAP(ssid2, password2); + + if (WiFi.status() == WL_CONNECTED) + { + return WL_CONNECTED; + } + + IPAddress dns(MY_DNS); + WiFi.disconnect(); + WiFi.mode(WIFI_STA); // switch off AP + + uint8_t connectionStatus = wifiMulti.run(); + + if (connectionStatus == WL_CONNECTED) + { + String ssid = WiFi.SSID(); + WiFiSignal = WiFi.RSSI(); // Get Wifi Signal strength now, because the WiFi will be turned off to save power! + log_i("WiFi connected to '%s'", ssid.c_str()); + log_i("WiFi connected at: %s", WiFi.localIP().toString().c_str()); + } + else + { + log_w("WiFi connection failed!"); + } + + return connectionStatus; +} + +// Disconnects WiFi and switches off WiFi to save power. +void StopWiFi() +{ + WiFi.disconnect(); + WiFi.mode(WIFI_OFF); +} + +// Check if history update is due +bool HistoryUpdateDue(void) +{ + int mins = (CurrentHour * 60 + CurrentMin) % HIST_UPDATE_RATE; + bool rv = (mins <= HIST_UPDATE_TOL) || (mins >= (HIST_UPDATE_RATE - HIST_UPDATE_TOL)); + return rv; +} + +void convertUtcTimestamp(String time_str_utc, struct tm *ti_local, int tz_offset) +{ + struct tm received_at_utc; + memset(&received_at_utc, 0, sizeof(struct tm)); + memset(ti_local, 0, sizeof(struct tm)); + + // Read time string + strptime(time_str_utc.c_str(), "%Y-%m-%dT%H:%M:%S%z", &received_at_utc); + log_d("UTC: %d-%02d-%02d %02d:%02d DST: %d\n", received_at_utc.tm_year + 1900, received_at_utc.tm_mon + 1, received_at_utc.tm_mday, received_at_utc.tm_hour, received_at_utc.tm_min, received_at_utc.tm_isdst); + + // Convert time struct and calculate offset + time_t received_at = mktime(&received_at_utc) - tz_offset; + + // Convert time value to time struct (local time; with consideration of DST) + localtime_r(&received_at, ti_local); + char tbuf[26]; + strftime(tbuf, 25, "%Y-%m-%d %H:%M", ti_local); + log_d("Message received at: %s local time, DST: %d\n", tbuf, ti_local->tm_isdst); +} + +// Get time from NTP server and initialize/update RTC +boolean SetupTime() +{ + configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTPSERVER, "pool.ntp.org"); // (gmtOffset_sec, daylightOffset_sec, ntpServer) + setenv("TZ", TIMEZONE, 1); // setenv() adds the "TZ" variable to the environment with a value TimeZone, only used if set to 1, 0 means no change + tzset(); // Set the TZ environment variable + delay(100); + return UpdateLocalTime(); +} + +// Get local time (from RTC) and update global variables +boolean UpdateLocalTime() +{ + struct tm timeinfo; + char time_output[32], date_output[32]; + while (!getLocalTime(&timeinfo, 10000)) + { // Wait for 10-sec for time to synchronise + log_w("Failed to obtain time"); + return false; + } + CurrentHour = timeinfo.tm_hour; + CurrentMin = timeinfo.tm_min; + CurrentSec = timeinfo.tm_sec; + CurrentDay = timeinfo.tm_mday; + printTime(timeinfo, time_output, date_output, 32, TXT_UPDATED); + + Date_str = date_output; + Time_str = time_output; + return true; +} + +// Print localized time to variables +void printTime(struct tm &timeinfo, char *date_output, char *time_output, int max_size, const String label) +{ + char update_time[30]; + + // See http://www.cplusplus.com/reference/ctime/strftime/ + // Serial.println(&timeinfo, "%a %b %d %Y %H:%M:%S"); // Displays: Saturday, June 24 2017 14:05:49 + if (Units == "M") + { + if ((Language == "CZ") || (Language == "DE") || (Language == "PL") || (Language == "NL")) + { + sprintf(date_output, "%s, %02u. %s %04u", weekday_D[timeinfo.tm_wday], timeinfo.tm_mday, month_M[timeinfo.tm_mon], (timeinfo.tm_year) + 1900); // day_output >> So., 23. Juni 2019 << + } + else + { + sprintf(date_output, "%s %02u-%s-%04u", weekday_D[timeinfo.tm_wday], timeinfo.tm_mday, month_M[timeinfo.tm_mon], (timeinfo.tm_year) + 1900); + } + strftime(update_time, max_size, "%H:%M:%S", &timeinfo); // Creates: '14:05:49' + } + else + { + strftime(date_output, max_size, "%a %b-%d-%Y", &timeinfo); // Creates 'Sat May-31-2019' + strftime(update_time, sizeof(update_time), "%r", &timeinfo); // Creates: '02:05:49pm' + + } + sprintf(time_output, "%s %s", label, update_time); +} \ No newline at end of file diff --git a/examples/Waveshare_7_5_T7_Sensors/utils.h b/examples/Waveshare_7_5_T7_Sensors/utils.h new file mode 100644 index 0000000..46b42d5 --- /dev/null +++ b/examples/Waveshare_7_5_T7_Sensors/utils.h @@ -0,0 +1,113 @@ +/////////////////////////////////////////////////////////////////////////////// +// utils.h +// +// Utility functions for ESP32-e-Paper-Weather-Display +// +// +// created: 10/2024 +// +// +// MIT License +// +// Copyright (c) 2024 Matthias Prinke +// +// 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. +// +// +// History: +// +// 20241010 Extracted from Waveshare_7_5_T7_Sensors.ino +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _UTILS_H +#define _UTILS_H +#include +#include +#include +#include +#include "config.h" +#include "secrets.h" + +/** + * \brief Start WiFi connection + * + * Establishes a WiFi connection with global settings ssid and password- + * If succesful, the global variable wifi_signal is updated with the RSSI. + * + * \return WiFi.status(); WL_CONNECTED if successful + */ +uint8_t StartWiFi(); + +/** + * \brief Stop WiFi connection + * + * Disconnects WiFi and switches off WiFi to save power. + */ +void StopWiFi(); + +/** + * \brief Check if history update is due + * + * History is updated at an interval of HIST_UPDATE_RATE synchronized + * to the past full hour with a tolerance of HIST_UPDATE_TOL. + * + * \return true if update is due, otherwise false + */ +bool HistoryUpdateDue(void); + +void convertUtcTimestamp(String time_str_utc, struct tm *ti_local, int tz_offset); + +/** + * \brief Get time from NTP server and initialize/update RTC + * + * The global constants gmtOffset_sec, daylightOffset_sec and ntpServer are used. + * + * \return true if RTC initialization was successfully, false otherwise + */ +boolean SetupTime(); + +/** + * \brief Get local time (from RTC) and update global variables + * + * The global variables CurrentHour, CurrentMin, CurrentSec, CurrentDay + * and day_output/time_output are updated. + * date_output contains the localized date string, + * time_output contains the localized text TXT_UPDATED with the time string. + * + * \return true if RTC time was valid, false otherwise + */ +boolean UpdateLocalTime(); + +/** + * \brief Print localized time to variables + * + * \param timeinfo date and time data structure + * \param date_output test output buffer for localized date + * \param time_output text output buffer for localized time + * \param max_size maximum text buffer size + * + * + */ +void printTime(struct tm &timeinfo, char *date_output, char *time_output, int max_size, const String label); + +#endif \ No newline at end of file