From f7bc6276506377e998155f56a80fb1a51961b90e Mon Sep 17 00:00:00 2001 From: Marvin Roger Date: Thu, 22 Sep 2016 10:40:10 +0200 Subject: [PATCH 01/51] Revert tree to v1.5.0 (dev now happens in develop branch) The new workflow is Git Flow --- .github/ISSUE_TEMPLATE.md | 7 - .travis.yml | 22 - Makefile | 3 - README.md | 32 +- banner.png | Bin 12993 -> 0 bytes data/homie/README.md | 10 - docs/1.-What-is-it.md | 5 + docs/2.-Getting-started.md | 184 +++++ docs/3.-Advanced-usage.md | 202 +++++ docs/4.-OTA.md | 22 + docs/5.-JSON-configuration-file.md | 66 ++ docs/6.-Configuration-API.md | 133 ++++ docs/7.-API-reference.md | 136 ++++ docs/8.-Limitations-and-known-issues.md | 24 + docs/9.-Troubleshooting.md | 38 + docs/README.md | 4 + docs/assets/led_mqtt.gif | Bin 0 -> 324 bytes docs/assets/led_solid.gif | Bin 0 -> 253 bytes docs/assets/led_wifi.gif | Bin 0 -> 324 bytes docs/index.md | 15 + ...example.config.json => example.config.json | 3 - examples/CustomSettings/CustomSettings.ino | 43 - examples/DoorSensor/DoorSensor.ino | 16 +- examples/HookToEvents/HookToEvents.ino | 31 +- examples/IteadSonoff/IteadSonoff.ino | 19 +- examples/LedStrip/LedStrip.ino | 99 +-- examples/LightOnOff/LightOnOff.ino | 16 +- .../TemperatureSensor/TemperatureSensor.ino | 22 +- firmware_parser.py | 38 - homie-esp8266.jpg | Bin 0 -> 47471 bytes keywords.txt | 62 +- library.json | 2 +- src/Homie.cpp | 362 +++------ src/Homie.h | 6 +- src/Homie.hpp | 135 +--- src/Homie/Blinker.cpp | 21 +- src/Homie/Blinker.hpp | 26 +- src/Homie/Boot/Boot.cpp | 29 +- src/Homie/Boot/Boot.hpp | 26 +- src/Homie/Boot/BootConfig.cpp | 354 +++------ src/Homie/Boot/BootConfig.hpp | 56 +- src/Homie/Boot/BootNormal.cpp | 741 ++++++++---------- src/Homie/Boot/BootNormal.hpp | 81 +- src/Homie/Boot/BootOta.cpp | 85 ++ src/Homie/Boot/BootOta.hpp | 22 + src/Homie/Boot/BootStandalone.cpp | 57 -- src/Homie/Boot/BootStandalone.hpp | 23 - src/Homie/Config.cpp | 402 +++++----- src/Homie/Config.hpp | 48 +- src/Homie/Constants.hpp | 35 +- src/Homie/Datatypes/Callbacks.hpp | 11 +- src/Homie/Datatypes/ConfigStruct.hpp | 56 +- src/Homie/Datatypes/Interface.hpp | 87 +- src/Homie/Datatypes/Subscription.hpp | 11 + src/Homie/Helpers.cpp | 217 +++-- src/Homie/Helpers.hpp | 45 +- src/Homie/Limits.hpp | 39 +- src/Homie/Logger.cpp | 22 +- src/Homie/Logger.hpp | 42 +- src/Homie/MqttClient.cpp | 120 +++ src/Homie/MqttClient.hpp | 44 ++ src/Homie/Strings.hpp | 2 +- src/Homie/Timer.cpp | 21 +- src/Homie/Timer.hpp | 26 +- src/Homie/Uptime.cpp | 13 +- src/Homie/Uptime.hpp | 20 +- src/HomieEvent.hpp | 22 +- src/HomieNode.cpp | 67 +- src/HomieNode.hpp | 105 +-- src/HomieRange.hpp | 6 - src/HomieSetting.cpp | 101 --- src/HomieSetting.hpp | 58 -- 72 files changed, 2557 insertions(+), 2341 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .travis.yml delete mode 100644 Makefile delete mode 100644 banner.png delete mode 100644 data/homie/README.md create mode 100644 docs/1.-What-is-it.md create mode 100644 docs/2.-Getting-started.md create mode 100644 docs/3.-Advanced-usage.md create mode 100644 docs/4.-OTA.md create mode 100644 docs/5.-JSON-configuration-file.md create mode 100644 docs/6.-Configuration-API.md create mode 100644 docs/7.-API-reference.md create mode 100644 docs/8.-Limitations-and-known-issues.md create mode 100644 docs/9.-Troubleshooting.md create mode 100644 docs/README.md create mode 100644 docs/assets/led_mqtt.gif create mode 100644 docs/assets/led_solid.gif create mode 100644 docs/assets/led_wifi.gif create mode 100644 docs/index.md rename data/homie/example.config.json => example.config.json (83%) delete mode 100644 examples/CustomSettings/CustomSettings.ino delete mode 100644 firmware_parser.py create mode 100644 homie-esp8266.jpg create mode 100644 src/Homie/Boot/BootOta.cpp create mode 100644 src/Homie/Boot/BootOta.hpp delete mode 100644 src/Homie/Boot/BootStandalone.cpp delete mode 100644 src/Homie/Boot/BootStandalone.hpp create mode 100644 src/Homie/Datatypes/Subscription.hpp create mode 100644 src/Homie/MqttClient.cpp create mode 100644 src/Homie/MqttClient.hpp delete mode 100644 src/HomieRange.hpp delete mode 100644 src/HomieSetting.cpp delete mode 100644 src/HomieSetting.hpp diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index e2021002..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,7 +0,0 @@ -Before submitting your issue, make sure: - -- [ ] You're using a [stable release](https://github.com/marvinroger/homie-esp8266/releases), not the Git development version which is, by definition, unstable -- [ ] You've read the documentation for *your* release, which is in the `docs/` folder of the `.zip` of the release you're using, especially the **Getting started** and **Troubleshooting** pages, which contain respectively the minimum required version of the dependencies, and some answsers to the most common problems -- [ ] You're using the examples bundled in *your* release, which are in the `examples/` folder of the `.zip` of the release you're using. Examples in the latest git revision might not be backward-compatible with your release - -Thanks! diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ac6ed850..00000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: python -python: - - "2.7" - -env: - - PLATFORMIO_CI_SRC=examples/CustomSettings - - PLATFORMIO_CI_SRC=examples/DoorSensor - - PLATFORMIO_CI_SRC=examples/HookToEvents - - PLATFORMIO_CI_SRC=examples/IteadSonoff - - PLATFORMIO_CI_SRC=examples/LightOnOff - - PLATFORMIO_CI_SRC=examples/TemperatureSensor - - PLATFORMIO_CI_SRC=examples/LedStrip - - CPPLINT=true - -install: - - pip install -U platformio - - pip install cpplint - # install current build as a library with all dependencies - - platformio lib -g install file://. - -script: - - if [[ "$CPPLINT" ]]; then make cpplint; else platformio ci --board=esp01 --board=nodemcuv2; fi diff --git a/Makefile b/Makefile deleted file mode 100644 index 494d0eb2..00000000 --- a/Makefile +++ /dev/null @@ -1,3 +0,0 @@ -cpplint: - cpplint --repository=. --recursive --filter=-whitespace/line_length,-legal/copyright,-runtime/printf,-build/include,-build/namespace,-runtime/int ./src -.PHONY: cpplint diff --git a/README.md b/README.md index 240c7621..8284202e 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,16 @@ -![homie-esp8266 banner](banner.png) - Homie for ESP8266 ================= -[![Build Status](https://img.shields.io/travis/marvinroger/homie-esp8266/master.svg?style=flat-square)](https://travis-ci.org/marvinroger/homie-esp8266) [![Latest Release](https://img.shields.io/badge/release-v1.5.0-yellow.svg?style=flat-square)](https://github.com/marvinroger/homie-esp8266/releases) - -An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. - -## Download +![homie-esp8266](homie-esp8266.jpg) -The Git repository contains the development version of Homie for ESP8266. Stable releases are available [on the releases page](https://github.com/marvinroger/homie-esp8266/releases). +An Arduino for ESP8266 implementation of [Homie](https://git.io/homieiot), an MQTT convention for the IoT. ## Features * Automatic connection/reconnection to Wi-Fi/MQTT -* [JSON configuration file](https://homie-esp8266.readme.io/v2.0.0/docs/json-configuration-file) to configure the device -* [Cute HTTP API / Web UI / App](https://homie-esp8266.readme.io/v2.0.0/docs/http-json-api) to remotely send the configuration to the device and get information about it -* [Custom settings](https://homie-esp8266.readme.io/v2.0.0/docs/custom-settings) -* [OTA over MQTT](https://homie-esp8266.readme.io/v2.0.0/docs/ota-configuration-updates) -* [Magic bytes](https://homie-esp8266.readme.io/v2.0.0/docs/magic-bytes) +* [Cute JSON configuration file](docs/5.-JSON-configuration-file.md) to configure the credentials of the device +* [Cute API / Web UI / App](docs/6.-Configuration-API.md) to remotely send the configuration to the device and get information about it +* [OTA support](docs/4.-OTA.md) * Available in the [PlatformIO registry](http://platformio.org/#!/lib/show/555/Homie) * Pretty straightforward sketches, a simple light for example: @@ -29,7 +21,7 @@ const int PIN_RELAY = 5; HomieNode lightNode("light", "switch"); -bool lightOnHandler(HomieRange range, String value) { +bool lightOnHandler(String value) { if (value == "true") { digitalWrite(PIN_RELAY, HIGH); Homie.setNodeProperty(lightNode, "on", "true"); // Update the state of the light @@ -46,16 +38,12 @@ bool lightOnHandler(HomieRange range, String value) { } void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); pinMode(PIN_RELAY, OUTPUT); digitalWrite(PIN_RELAY, LOW); - Homie_setFirmware("awesome-relay", "1.0.0"); - - lightNode.advertise("on").settable(lightOnHandler); - + Homie.setFirmware("awesome-relay", "1.0.0"); + lightNode.subscribe("on", lightOnHandler); + Homie.registerNode(lightNode); Homie.setup(); } @@ -66,4 +54,4 @@ void loop() { ## Requirements, installation and usage -The project is documented on https://homie-esp8266.readme.io with a *Getting started* guide and every piece of information you will need. +The project is documented on the [/docs folder](docs), with a *Getting started* guide and every piece of informations you will need. diff --git a/banner.png b/banner.png deleted file mode 100644 index 3a1ac1168dfe2ce755d49734e73d27d9c0b8bb90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12993 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 r`tkR)-P= 5.0.8 for `ArduinoJson`, >= 2.0 for `Bounce2`, >= 2.5 for `PubSubClient`. + +### 1b. With [PlatformIO](http://platformio.org) + +In a terminal, run `platformio lib install 555`. + +Dependencies are installed automatically. + +## Bare minimum sketch + +```c++ +#include + +void setup() { + Homie.setup(); +} + +void loop() { + Homie.loop(); +} +``` + +This is the bare minimum needed for Homie for ESP8266 to work correctly. +If you upload this sketch, you will notice the LED of the ESP8266 will light on ![LED solid](assets/led_solid.gif). This is because you are in `configuration` mode. + +Homie for ESP8266 has 3 modes of operation: + +1. The `configuration` mode is the initial one. It spawns an AP and an HTTP webserver exposing a JSON API. To interact with it, you have to connect to the AP. Then, an HTTP client can get the list of available Wi-Fi networks, and send the credentials (like the Wi-Fi SSID, the Wi-Fi password, ...). Once the device receives the credentials, it boots into `normal` mode. + +2. The `normal` mode is the mode the device will be most of the time. It connects to the Wi-Fi, to the MQTT, it sends initial informations to the Homie server (like the local IP, the version of the firmware currently running, ...) and it subscribes from the MQTT to properties change. The device can return to `configuration` mode in different ways (press of a button or custom function, see [3. Advanced usage](3.-Advanced-usage.md)). + +3. The `OTA` mode is triggered from the `normal` mode when the MQTT server sends a version different from the current firmware version. It will reach the OTA HTTP server and flash the latest firmware available. When it ends (either a success or a failure), it returns to `normal` mode. + +**Very important: As a rule of thumb, never block the device with blocking code for more than 50ms or so.** Otherwise, you may very probably experience unexpected behaviors. + +## Connecting to the AP and configuring the device + +Homie for ESP8266 has spawned a secure AP named `Homie-xxxxxxxx`. For example, if the AP is named `Homie-c631f278`, the AP password is `c631f278`. Connect to it. + +*Note*: This `c631f278` ID is unique to each device, and you cannot change it. If you reflash a new sketch, this ID won't change. + +Once connected, the webserver is available at `http://homie.config`. To bypass the built-in DNS server, you can reach directly `192.168.1.1`. You can then configure the device using the [Configuration API](6.-Configuration-API.md). When the device receives its configuration, it will reboot to `normal` mode. + +## Understanding what happens in `normal` mode + +### Visual codes + +When the device boots in `normal` mode, it will start blinking: + +* ![Wi-Fi LED blinking](assets/led_wifi.gif) Slowly when connecting to the Wi-Fi +* ![MQTT LED blinking](assets/led_mqtt.gif) Faster when connecting to the MQTT broker + +This way, you can have a quick feedback on what's going on. If both connections are established, the LED will stay off. Note the device will also blink during the automatic reconnection, if the connection to the Wi-Fi or the MQTT broker is lost. + +### Under the hood + +Although the sketch looks like it does not do anything, it actually does quite a lot: + +* It automatically connects to the Wi-Fi and MQTT broker. No more network boilerplate code +* It exposes the Homie device on MQTT (as devices / `device ID`, e.g. `devices/c631f278`). +* It subscribes to the special device property `$ota`, automatically rebooting in OTA mode if OTA is available +* It checks for a button press on the ESP8266, to return to `configuration` mode + +## Creating an useful sketch + +Now that we understand how Homie for ESP8266 works, let's create an useful sketch. We want to create a smart light. + +```c++ +#include + +const int PIN_RELAY = 5; + +HomieNode lightNode("light", "switch"); + +bool lightOnHandler(String value) { + if (value == "true") { + digitalWrite(PIN_RELAY, HIGH); + Homie.setNodeProperty(lightNode, "on", "true"); // Update the state of the light + Serial.println("Light is on"); + } else if (value == "false") { + digitalWrite(PIN_RELAY, LOW); + Homie.setNodeProperty(lightNode, "on", "false"); + Serial.println("Light is off"); + } else { + return false; + } + + return true; +} + +void setup() { + pinMode(PIN_RELAY, OUTPUT); + digitalWrite(PIN_RELAY, LOW); + + Homie.setFirmware("awesome-relay", "1.0.0"); + lightNode.subscribe("on", lightOnHandler); + Homie.registerNode(lightNode); + Homie.setup(); +} + +void loop() { + Homie.loop(); +} +``` + +Alright, step by step: + +1. We create a node with an ID of `light` and a type of `switch` with `HomieNode lightNode("light", "switch")` +2. We set the name and the version of the firmware with `Homie.setFirmware("awesome-light" ,"1.0.0");` +3. We want our `light` node to subscribe to the `on` property. We do that with `lightNode.subscribe("on", lightOnHandler);`. The `lightOnHandler` function will be called when the value of this property is changed +4. We tell Homie for ESP8266 to expose our `light` node by registering it. We do this with `Homie.registerNode(lightNode);` +5. In the `lightOnHandler` function, we want to update the state of the `light` node. We do this with `Homie.setNodeProperty(lightNode, "on", "true");` + +In about thirty SLOC, we have achieved to create a smart light, without any hard-coded credentials, with automatic reconnection in case of network failure, and with OTA support. Not bad, right? + +## Creating a sensor node + +In the previous example sketch, we were reacting on property changes. But what if we want, for example, to send a temperature every 5 minute? We could do this in the Arduino `loop()` function. But then, we would have to check if we are in `normal` mode, and we would have to ensure the network connection is up before sending any property. Boring. + +Fortunately, Homie for ESP8266 provides an easy way to do that. + +```c++ +#include + +const int TEMPERATURE_INTERVAL = 300; + +unsigned long lastTemperatureSent = 0; + +HomieNode temperatureNode("temperature", "temperature"); + +void setupHandler() { + // Do what you want to prepare your sensor +} + +void loopHandler() { + if (millis() - lastTemperatureSent >= TEMPERATURE_INTERVAL * 1000UL || lastTemperatureSent == 0) { + float temperature = 22; // Fake temperature here, for the example + Serial.print("Temperature: "); + Serial.print(temperature); + Serial.println(" °C"); + if (Homie.setNodeProperty(temperatureNode, "temperature", String(temperature), true)) { + lastTemperatureSent = millis(); + } else { + Serial.println("Sending failed"); + } + } +} + +void setup() { + Homie.setFirmware("awesome-temperature", "1.0.0"); + Homie.registerNode(temperatureNode); + Homie.setSetupFunction(setupHandler); + Homie.setLoopFunction(loopHandler); + Homie.setup(); +} + +void loop() { + Homie.loop(); +} +``` + +The only new things here are the `Homie.setSetupFunction(setupHandler);` and `Homie.setLoopFunction(loopHandler);` calls. The setup function will be called once, when the device is in `normal` mode and the network connection is up. The loop function will be called everytime, when the device is in `normal` mode and the network connection is up. This provides a nice level of abstraction. + +Now that you understand the basic usage of Homie for ESP8266, you can head on to the [Advanced usage](3.-Advanced-usage.md) page to learn about more powerful features like input handlers and the event system. diff --git a/docs/3.-Advanced-usage.md b/docs/3.-Advanced-usage.md new file mode 100644 index 00000000..f78012e2 --- /dev/null +++ b/docs/3.-Advanced-usage.md @@ -0,0 +1,202 @@ +# Advanced usage + +## Built-in LED + +By default, Homie for ESP8266 will blink the built-in LED to indicate its status. However, on some boards like the ESP-01, the built-in LED is actually the TX port, so it is fine if Serial is not enabled, but if you enable Serial, this is a problem. You can easily disable the built-in LED blinking. + +```c++ +void setup() { + Homie.enableBuiltInLedIndicator(false); // before Homie.setup() + // ... +} +``` + +You may, instead of completely disable the LED control, set a new LED to control: + +```c++ +void setup() { + Homie.setLedPin(16, HIGH); // before Homie.setup() -- 2nd param is the state when the LED is on + // ... +} +``` + +## Change the brand + +By default, Homie for ESP8266 will spawn a `Homie-XXXXXXXX` AP, will connect to the MQTT broker with the `Homie-XXXXXXXX` client ID, etc. You might want to change the `Homie` text: + +```c++ +void setup() { + Homie.setBrand("MyIoTSystem"); // before Homie.setup() + // ... +} +``` + +## Hook to Homie events + +You may want to hook to Homie events. Maybe you will want to blink a LED if the Wi-Fi connection is lost, or execute some code prior to a device reset to clear some EEPROM you're using. + +```c++ +void onHomieEvent(HomieEvent event) { + switch(event) { + case HOMIE_CONFIGURATION_MODE: + // Do whatever you want when configuration mode is started + break; + case HOMIE_NORMAL_MODE: + // Do whatever you want when normal mode is started + break; + case HOMIE_OTA_MODE: + // Do whatever you want when OTA mode is started + break; + case HOMIE_ABOUT_TO_RESET: + // Do whatever you want when the device is about to reset + break; + case HOMIE_WIFI_CONNECTED: + // Do whatever you want when Wi-Fi is connected in normal mode + break; + case HOMIE_WIFI_DISCONNECTED: + // Do whatever you want when Wi-Fi is disconnected in normal mode + break; + case HOMIE_MQTT_CONNECTED: + // Do whatever you want when MQTT is connected in normal mode + break; + case HOMIE_MQTT_DISCONNECTED: + // Do whatever you want when MQTT is disconnected in normal mode + break; + } +} + +void setup() { + Homie.onEvent(onHomieEvent); // before Homie.setup() + // ... +} +``` + +See the `HookToEvents` example for a concrete use case. + +## Serial / Logging + +By default, Homie for ESP8266 will output a lot of useful debug messages on the Serial. You may want to disable this behavior if you want to use the Serial line for anything else. + +```c++ +void setup() { + Homie.enableLogging(false); // before Homie.setup() + // ... +} +``` + +If logging is enabled `Serial.begin();` will be called internally at 115 200 baud in `Homie.setup()`. So don't initialize the Serial line yourself. + +## Input handlers + +There are three types of input handlers: + +* Global input handler. This unique handler will handle every changed subscribed properties for all registered nodes + +```c++ +bool globalInputHandler(String node, String property, String value) { + +} + +void setup() { + Homie.setGlobalInputHandler(globalInputHandler); // before Homie.setup() + // ... +} +``` +* Node input handlers. This handler will handle every changed subscribed properties of a specific node + +```c++ +bool nodeInputHandler(String property, String value) { + +} + +HomieNode node("id", "type", nodeInputHandler); +``` +* Property input handlers. This handler will handle changes for a specific property of a specific node + +```c++ +bool propertyInputHandler(String value) { + +} + +HomieNode node("id", "type"); + +void setup() { + node.subscribe("property", propertyInputHandler); // before Homie.setup() + // ... +} +``` + +You can see that input handlers return a boolean. An input handler can decide whether or not it handled the message and want to propagate it down to other input handlers. If an input handler returns `true`, the propagation is stopped, if it returns `false`, the propagation continues. The order of the propagation is global handler → node handler → property handler. + +For example, imagine you defined three input handlers: the global one, the node one, and the property one. If the global input handler returns `false`, the node input handler will be called. If the node input handler returns `true`, the propagation is stopped and the property input handler won't be called. + +## HomieNode + +You might want to create a node that subscribes to all properties. Just add a fourth parameter to the `HomieNode` constructor, set to `true`: + +```c++ +bool nodeInputHandler(String property, String value) { + +} + +HomieNode node("id", "type", nodeInputHandler, true); +``` + +See the `LedStrip` example for a concrete use case. + +## Reset + +Resetting the device means erasing the stored configuration and rebooting from `normal` mode to `configuration` mode. By default, you can do it by pressing 5 seconds the `FLASH` button of your ESP8266 board. + +This behavior is configurable: + +```c++ +void setup() { + Homie.setResetTrigger(1, LOW, 2000); // before Homie.setup() + // ... +} +``` + +The device will now reset if pin `1` is `LOW` for `2000`ms. You can also disable completely this reset trigger: + +```c++ +void setup() { + Homie.disableResetTrigger(); // before Homie.setup() + // ... +} +``` + +In addition, you can also provide your own function responsible for the device reset. This function will be looped: + +```c++ +bool resetFunction () { + return true; // If true is returned, the device will reset, if false, it won't +} + +void setup() { + Homie.setResetFunction(resetFunction); // before Homie.setup() + // ... +} +``` + +Sometimes, you might want to disable temporarily the ability to reset the device. For example, if your device is doing some background work like moving shutters, you will want to disable the ability to reset until the shutters are not moving anymore. + +```c++ +Homie.setResettable(false); +``` + +Note that if a reset is asked while `resettable` is set to false, the device will be flagged. In other words, when you will call `Homie.setResettable(true);` back, the device will immediately reset. + +## Know if device is in normal mode + +If, for some reason, you want to run some code in the Arduino `loop()` function, it might be useful for you to know if the device is in `normal` mode and if the network connection is up. + +```c++ +void loop() { + if (Homie.isReadyToOperate()) { + // normal mode and network connection up + } else { + // not in normal mode or network connection down + } +} +``` diff --git a/docs/4.-OTA.md b/docs/4.-OTA.md new file mode 100644 index 00000000..7edfddfc --- /dev/null +++ b/docs/4.-OTA.md @@ -0,0 +1,22 @@ +# OTA + +Homie for ESP8266 supports OTA, if enabled in the configuration, and if a compatible OTA server is set up. + +It works this way: + +1. The device receives an OTA notification from the MQTT broker, as defined in the Homie convention. If the version sent by the broker is different from the one set with `Homie.setFirmware()`, and if OTA is enabled in the configuration, the device will be flagged to reboot to `OTA` mode as soon as the device will be resettable (with `Homie.setResettable()`). +2. The device boots in `OTA` mode +3. The device reaches the OTA server and attempt to flash the new firmware +4. If the OTA fails or succeed, the device reboots to `normal` mode + +## Creating a compatible OTA server + +In `OTA` mode, the device sends a request to the host/path set in the configuration. This request contains the following headers: + +- `User-Agent`: `ESP8266-http-Update` +- `x-ESP8266-free-space`: space available on the ESP8266 in bytes +- `x-ESP8266-version`: `Device ID`=`Firmware name`=`Firmware version`=`OTA version target` (e.g. `c631f278=awesome-light=1.0.0=1.1.0`) + +Your server has to parse these headers. Based on the `x-ESP8266-version` header, it should decide what firmware it should send to the device. If no firmware is found, or if the firmware is bigger than the `x-ESP8266-free-space` header content, you can abort the OTA by sending a response with a `304` error code. To actually send the firmware, you must transfer the firmware file with a `200` code. For more bulletproof updates, you can also provide in the response the MD5 of your firmware file, in the `x-MD5` header. + +You have an example PHP in the [Arduino for ESP8266 doc](http://esp8266.github.io/Arduino/versions/2.1.0/doc/ota_updates/ota_updates.html#http-server) and a Node.js example in the [homie-server project](https://github.com/marvinroger/homie-server/blob/7b53ee9a1e5a053d311da139da8df8d3bdfd6f98/lib/servers/ota.js#L126). diff --git a/docs/5.-JSON-configuration-file.md b/docs/5.-JSON-configuration-file.md new file mode 100644 index 00000000..da86c2e0 --- /dev/null +++ b/docs/5.-JSON-configuration-file.md @@ -0,0 +1,66 @@ +# JSON configuration file + +To configure your device, you have two choices: manually flashing the configuration file to the SPIFFS at the `/homie/config.json` (see [Uploading files to file system](http://esp8266.github.io/Arduino/versions/2.1.0/doc/filesystem.html#uploading-files-to-file-system)), so you can bypass the `configuration` mode, or send it through the [Configuration API](6.-Configuration-API.md). + +Below is the format of the JSON configuration you will have to provide: + +```json +{ + "name": "The kitchen light", + "device_id": "kitchen-light", + "wifi": { + "ssid": "Network_1", + "password": "I'm a Wi-Fi password!" + }, + "mqtt": { + "host": "192.168.1.10", + "port": 1883, + "mdns": "mqtt", + "base_topic": "devices/", + "auth": true, + "username": "user", + "password": "pass", + "ssl": true, + "fingerprint": "CF 05 98 89 CA FF 8E D8 5E 5C E0 C2 E4 F7 E6 C3 C7 50 DD 5C" + }, + "ota": { + "enabled": true, + "host": "192.168.1.10", + "port": 80, + "mdns": "ota", + "path": "/custom_ota", + "ssl": true, + "fingerprint": "CF 05 98 89 CA FF 8E D8 5E 5C E0 C2 E4 F7 E6 C3 C7 50 DD 5C" + } +} +``` + +The above JSON contains every field that can be customized. + +Here are the rules: + +* `name`, `wifi.ssid`, `wifi.password`, `mqtt.host` (or `mqtt.mdns`) and `ota.enabled` are mandatory +* `wifi.password` can be `""` if connecting to an open network +* If `mqtt.auth` is `true`, `mqtt.username` and `mqtt.password` must be provided +* If a `mdns` field is set, the device will ignore the `host` and `port` fields and query for the corresponding mDNS service and get the first IP and port found + +Default values if not provided: + +* `device_id`: the hardware device ID (eg. `1a2b3c4d`) +* `mqtt.port`: `1883` +* `mqtt.base_topic`: `devices/` +* `mqtt.auth`: `false` +* `mqtt.ssl`: `false` +* `ota.host`: same as `mqtt.host` +* `ota.port`: `80` +* `ota.path`: `/ota` +* `ota.ssl`: `false` + +`host` fields can be either an IP or an hostname. + +The SSL fingerprints can be of the following format: + +* `CF 05 98 89 CA FF 8E D8 5E 5C E0 C2 E4 F7 E6 C3 C7 50 DD 5C` +* `CF:05:98:89:CA:FF:8E:D8:5E:5C:E0:C2:E4:F7:E6:C3:C7:50:DD:5C` +* `cf 05 98 89 ca ff 8e d8 5e 5c e0 c2 e4 f7 e6 c3 c7 50 dd 5c` +* `cf:05:98:89:ca:ff:8e:d8:5e:5c:e0:c2:e4:f7:e6:c3:c7:50:dd:5c` diff --git a/docs/6.-Configuration-API.md b/docs/6.-Configuration-API.md new file mode 100644 index 00000000..a7369082 --- /dev/null +++ b/docs/6.-Configuration-API.md @@ -0,0 +1,133 @@ +# Configuration API + +When in `configuration` mode, the device exposes a JSON API to send the configuration to it. When you send a valid configuration to the `/config` endpoint, the configuration file is stored in the file system at `/homie/config.json`. + +If you don't want to mess with JSON, you have a Web UI / app available: +* At [http://marvinroger.github.io/homie-esp8266](http://marvinroger.github.io/homie-esp8266) +* As an [Android app](https://build.phonegap.com/apps/1906578/share) + +**Quick instructions to use the Web UI / app**: + +1. Open the Web UI / app +2. Disconnect from your current Wi-Fi AP, and connect to the `Homie-xxxxxxxx` AP spawned in `configuration` mode +3. Follow the instructions + +You can see the sources of the Web UI [here](https://github.com/marvinroger/homie-esp8266/tree/configurator) and the built version [here](https://github.com/marvinroger/homie-esp8266/tree/gh-pages) + +Alternatively, you can use this curl command to send the config to the device: + +``` +curl -X PUT http://homie.config/config --header "Content-Type: application/json" -d @config.json +``` + +This will send the `./config.json` file to the device. + +## Error handling + +When everything went fine, a `200 OK` HTTP code is returned. +If anything goes wrong, a return code != 200 will be returned, with a JSON `error` field indicating the error. + +## API endpoints + +#### GET `/heart` + +This is useful to ensure we are connected to the device AP. + +##### Response + +200 OK (application/json) + +```json +{ "heart": "beat" } +``` + +#### GET `/device-info` + +Get some information on the device. + +##### Response + +200 OK (application/json) + +```json +{ + "device_id": "52a8fa5d", + "homie_version": "1.0.0", + "firmware": { + "name": "awesome-device", + "version": "1.0.0" + }, + "nodes": [ + { + "id": "light", + "type": "light" + } + ] +} +``` + +#### GET `/networks` + +Retrieve the Wi-Fi networks the device can see. + +##### Response + +* In case of success: + +200 OK (application/json) + +```json +{ + "networks": [ + { "ssid": "Network_2", "rssi": -82, "encryption": "wep" }, + { "ssid": "Network_1", "rssi": -57, "encryption": "wpa" }, + { "ssid": "Network_3", "rssi": -65, "encryption": "wpa2" }, + { "ssid": "Network_5", "rssi": -94, "encryption": "none" }, + { "ssid": "Network_4", "rssi": -89, "encryption": "auto" } + ] +} +``` + +* In case the initial Wi-Fi scan is not finished on the device: + +503 Service Unavailable (application/json) + +```json +{"error": "Initial Wi-Fi scan not finished yet"} +``` + +#### PUT `/config` + +Save the config to the device. + +##### Request body + +(application/json) + +See [JSON configuration file](5.-JSON-configuration-file.md). + +##### Response + +* In case of success: + +200 OK (application/json) + +```json +{ "success": true } +``` + +* In case of error in the payload: + +400 Bad Request (application/json) + +```json +{ "success": false, "error": "Reason why the payload is invalid" } +``` + +* In case the device already received a valid configuration and is waiting for reboot: + +403 Forbidden (application/json) + +```json +{ "success": false, "error": "Device already configured" } +``` diff --git a/docs/7.-API-reference.md b/docs/7.-API-reference.md new file mode 100644 index 00000000..c23cbfd4 --- /dev/null +++ b/docs/7.-API-reference.md @@ -0,0 +1,136 @@ +# API reference + +### Homie object + +You don't have to instantiate an `Homie` instance, it is done internally. + +#### void Homie.setup () + +Setup Homie. Must be called once in `setup()`. + +#### void Homie.loop () + +Handle Homie work. Must be called in `loop()`. + +#### void Homie.enableLogging (bool `enable`) + +Enable or disable Homie Serial logging. +If logging is enabled, `Serial.begin(115200)` will be called internally. + +* **`enable`**: Whether or not to enable logging. By default, logging is enabled + +#### void Homie.enableBuiltInLedIndicator (bool `enable`) + +Enable or disable the built-in LED to indicate the Homie state. + +* **`enable`**: Whether or not to enable built-in LED. By default, it is enabled + +#### void Homie.setLedPin (unsigned char `pin`, unsigned char `on`) + +Set pin of the LED to control. + +* **`pin`**: LED to control +* **`on`**: state when the light is on (HIGH or LOW) + +#### void Homie.setBrand (const char\* `name`) + +Set the brand of the device, used in the configuration AP, the device hostname and the MQTT client ID. + +* **`name`**: Name of the brand. Default value is `Homie` + +#### void Homie.setFirmware (const char\* `name`, const char\* `version`) + +Set the name and version of the firmware. This is useful for OTA, as Homie will check against the server if there is a newer version. + +* **`name`**: Name of the firmware. Default value is `undefined` +* **`version`**: Version of the firmware. Default value is `undefined` + +#### void Homie.registerNode (HomieNode `node`) + +Register a node. + +* **`node`**: node to register + +#### void Homie.setGlobalInputHandler (std::function `handler`) + +Set input handler for subscribed properties. + +* **`handler`**: Global input handler +* **`node`**: Name of the node getting updated +* **`property`**: Property of the node getting updated +* **`value`**: Value of the new property + +#### void Homie.onEvent (std::function `callback`) + +Set the event handler. Useful if you want to hook to Homie events. + +* **`callback`**: Event handler + +#### void Homie.setResetTrigger (unsigned char `pin`, unsigned char `state`, unsigned int `time`) + +Set the reset trigger. By default, the device will reset when pin `0` is `LOW` for `5000`ms. + +* **`pin`**: Pin of the reset trigger +* **`state`**: Reset when the pin reaches this state for the given time +* **`time`**: Time necessary to reset + +#### void Homie.disableResetTrigger () + +Disable the reset trigger. + +#### void Homie.setResetFunction (std::function `callback`) + +Set the reset function. This is a function that is going to be called at each loop iteration, which can trigger a device reset. If the function returns true, the device resets. Else, it does not. + +* **`callback`**: Reset function + +#### void Homie.setSetupFunction (std::function `callback`) + +You can provide the function that will be called when operating in `normal` mode. + +* **`callback`**: Setup function + +#### void Homie.setLoopFunction (std::function `callback`) + +You can provide the function that will be looped in normal mode. + +* **`callback`**: Loop function + +#### void Homie.setNodeProperty (HomieNode `node`, String `property`, String `value`, bool `retained` = true) + +Using this function, you can set the value of a node property, like a temperature for example. + +* **`node`**: HomieNode instance on which to set the property on +* **`property`**: Property to send +* **`value`**: Payload +* **`retained`**: Optional. Should the MQTT broker retain this value, or is it a one-shot value? + +#### void Homie.setResettable (bool `resettable`) + +Is the device resettable? This is useful at runtime, because you might want the device not to be resettable when you have another library that is doing some unfinished work, like moving shutters for example. + +* **`resettable`**: Is the device resettable? Default value is `true` + +#### bool Homie.isReadyToOperate () + +Is the device in normal mode, configured and connected? You should not need this function. But maybe you will. + +--- + +### HomieNode object + +#### void HomieNode (const char\* `id`, const char\* `type`, std::function `handler` = , bool `subscribeToAll` = false) + +Constructor of a HomieNode object. + +* **`id`**: ID of the node +* **`type`**: Type of the node +* **`handler`**: Optional. Input handler of the node +* **`subscribeToAll`**: Optional. Whether or not to call the handler for every properties, even the ones not registered + +#### void .subscribe (const char\* `property`, std::function `handler`) = ) + +Subscribes the node to the given property. + +* **`property`**: Property to subscribe to +* **`handler`**: Optional. Input handler of the property of the node diff --git a/docs/8.-Limitations-and-known-issues.md b/docs/8.-Limitations-and-known-issues.md new file mode 100644 index 00000000..47111fe9 --- /dev/null +++ b/docs/8.-Limitations-and-known-issues.md @@ -0,0 +1,24 @@ +# Limitations and known issues + +## Blocking Homie code + +In `configuration` and `normal` modes, Homie for ESP8266 code is designed to be non-blocking, so that you can do other tasks in the main `loop()`. However, the connection to the MQTT broker is blocking during ~5 seconds in case the server is unreachable. This is an Arduino for ESP8266 limitation, and we can't do anything on our side to solve this issue, not even a timeout. + +The `OTA` mode is blocking for obvious reason. + +## SSL fingerprint checking + +Adding a TLS fingerprint effectively pins the device to a particular certificate. Furthermore, as currently implemented by the ESP8266 `WifiSecureClient`, both `mqtt.host` and `ota.host` are verified against the server certificate's common name (CN) in the certificate subject or in the SANs (subjectAlternateName) contained in it, but not in their IP addresses. For example, if the certificate used by your server looks like this: + +``` +Subject: CN=tiggr.example.org, OU=generate-CA/emailAddress=nobody@example.net +... +X509v3 Subject Alternative Name: + IP Address:192.168.1.10, DNS:broker.example.org +``` + +Enabling fingerprint in Homie will work only if host is set to `tiggr.example.org` or `broker.example.org` and the correct fingerprint is used; setting host to the IP address will cause fingerprint verification to fail. + +## ADC readings + +[This is a known esp8266/Arduino issue](https://github.com/esp8266/Arduino/issues/1634) that polling `analogRead()` too frequently forces the Wi-Fi to disconnect. As a workaround, don't poll the ADC more than one time every 3ms. diff --git a/docs/9.-Troubleshooting.md b/docs/9.-Troubleshooting.md new file mode 100644 index 00000000..7c404fb5 --- /dev/null +++ b/docs/9.-Troubleshooting.md @@ -0,0 +1,38 @@ +# Troubleshooting + +## 1. I see some garbage on the Serial monitor? + +You are probably using a generic ESP8266. The problem with these modules is the built-in LED is tied to the serial line. You can do two things: + +* Disable the serial logging, to have the LED working: + +```c++ +void setup() { + Homie.enableLogging(false); // before Homie.setup() + // ... +} +``` +* Disable the the LED blinking, to have the serial line working: + +```c++ +void setup() { + Homie.enableBuiltInLedIndicator(false); // before Homie.setup() + // ... +} +``` + +## 2. I see an `abort` message on the Serial monitor? + +`abort()` is called by Homie for ESP8266 when the framework is used in a bad way. The possible causes are: + +* You are calling a function that is meant to be called before `Homie.setup()`, after `Homie.setup()` + +* One of the string you've used (in `setFirmware()`, `subscribe()`, etc.) is too long. Check the `Limits.hpp` file to see the max length possible for each string. + +## 3. The network is completely unstable... What's going on? + +The framework needs to work continuously (ie. `Homie.loop()` needs to be called very frequently). In other words, don't use `delay()` (see [avoid delay](http://playground.arduino.cc/Code/AvoidDelay)) or anything that might block the code for more than 50ms or so. There is also a known Arduino for ESP8266 issue with `analogRead()`, see [Limitations and known issues#adc-readings](8.-Limitations-and-known-issues.md#adc-readings). + +## 4. My device resets itself without me doing anything? + +You have probably connected a sensor to the default reset pin of the framework (D3 on NodeMCU, GPIO0 on other boards). See [Advanced usage#reset](3.-Advanced-usage.md#reset). diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..94fdf8f2 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,4 @@ +Homie for ESP8266 documentation +=============================== + +See [index.md](index.md) to view it locally, or http://marvinroger.viewdocs.io/homie-esp8266/ to view it online. diff --git a/docs/assets/led_mqtt.gif b/docs/assets/led_mqtt.gif new file mode 100644 index 0000000000000000000000000000000000000000..baf07ca0b8569fe046fa337d95197ffcf64a9794 GIT binary patch literal 324 zcmZ?wbhEHb6k!ly_`<;O_v43uzkj`JEcwn+B zeObNa$Jvwr|NZ^@>EoZ5&%f>2_O>$jfTi~T|Nj|?0E+*){aizWogD*Qjr0td8G%|9 zf3k3jFbFf~fV6|0!NA&-Agtj~WRuY)$GL!IQL}(ElTIF21s=#c1lYfL=o zFoA=+v&3tu9>>zgg{S69u`O#_a9dQwZvw|m>Ba?@UT6JY+voqBe@ATB_4mI^>+>ob z%NnX{^4OC)gwk}PdgFBidc*Y`on1ZLy?p!_jZDleEv#+rl~mNUv~=_g6hY2nMRS(; e1C_p%%y}8B*5$n3SD&By7y%q4AuZz9($qy literal 0 HcmV?d00001 diff --git a/docs/assets/led_solid.gif b/docs/assets/led_solid.gif new file mode 100644 index 0000000000000000000000000000000000000000..57aae4eece76e5a6785dcad50870050e9b821c44 GIT binary patch literal 253 zcmZ?wbh9u|6k!ly_`<;O_v43uzkj`JEcwn+B zeObNa$Jvwr|NZ^@>EoZ5&%f>2_O>$jfTcDAaX<&83}hDrYg2--hC`7}MwcAt0+vP1 z0?v$gf5h^zJnG>%c1WQ^qTj7C@u0&54(`qpucdk%OB)xSnk&V&tZBh*Q4zlh95baG z7hHOs^?Plf|8xEwv0c~S|1Pc1t86T5sIJLlPwEg#(~0Vh*A3_m*K>4s^>Fv{@nbYH WF|)L=wzXGMQPa}W(KAqFum%8pE@b%t literal 0 HcmV?d00001 diff --git a/docs/assets/led_wifi.gif b/docs/assets/led_wifi.gif new file mode 100644 index 0000000000000000000000000000000000000000..d7f21605de4fff6e5785144756df59bb152dee87 GIT binary patch literal 324 zcmZ?wbhEHb6k!ly_`<;O_v43uzkj`JEcwn+B zeObNa$Jvwr|NZ^@>EoZ5&%f>2_O>$jfTi~T|Nj|?0E+*){aizWogD*Qjr0td8G%|9 zf3k3lTIF21s=#c1lYfL=o zFoA=+v&3tu9>>zgg{S69u`O#_a9dQwZvw|m>Ba?@UT6JY+voqBe@ATB_4mI^>+>ob z%NnX{^4OC)gwk}PdgFBidc*Y`on1ZLy?p!_jZDleEv#+rl~mNUv~=_g6hY2nMRS(; e1C_p%%y}8B*5$n3SD&By7y%q4Auam?tDT3 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..1d24e90b --- /dev/null +++ b/docs/index.md @@ -0,0 +1,15 @@ +Welcome to the Homie for ESP8266 docs. This will help you to understand the framework and to use it in an effective manner. + +**

This documentation is valid for Homie v1.5.0

** + +----- + +#### 1. [What is it?](1.-What-is-it.md) +#### 2. [Getting started](2.-Getting-started.md) +#### 3. [Advanced usage](3.-Advanced-usage.md) +#### 4. [OTA](4.-OTA.md) +#### 5. [JSON configuration file](5.-JSON-configuration-file.md) +#### 6. [Configuration API](6.-Configuration-API.md) +#### 7. [API reference](7.-API-reference.md) +#### 8. [Limitations and known issues](8.-Limitations-and-known-issues.md) +#### 9. [Troubleshooting](9.-Troubleshooting.md) diff --git a/data/homie/example.config.json b/example.config.json similarity index 83% rename from data/homie/example.config.json rename to example.config.json index 2e37554a..825a8d9d 100644 --- a/data/homie/example.config.json +++ b/example.config.json @@ -10,8 +10,5 @@ }, "ota": { "enabled": false - }, - "settings": { - } } diff --git a/examples/CustomSettings/CustomSettings.ino b/examples/CustomSettings/CustomSettings.ino deleted file mode 100644 index 2a5db2fc..00000000 --- a/examples/CustomSettings/CustomSettings.ino +++ /dev/null @@ -1,43 +0,0 @@ -#include - -const int DEFAULT_TEMPERATURE_INTERVAL = 300; - -unsigned long lastTemperatureSent = 0; - -HomieNode temperatureNode("temperature", "temperature"); - -HomieSetting temperatureIntervalSetting("temperatureInterval", "The temperature interval in seconds"); - -void setupHandler() { - Homie.setNodeProperty(temperatureNode, "unit").setRetained(true).send("c"); -} - -void loopHandler() { - if (millis() - lastTemperatureSent >= temperatureIntervalSetting.get() * 1000UL || lastTemperatureSent == 0) { - float temperature = 22; // Fake temperature here, for the example - Serial.print("Temperature: "); - Serial.print(temperature); - Serial.println(" °C"); - Homie.setNodeProperty(temperatureNode, "degrees").send(String(temperature)); - lastTemperatureSent = millis(); - } -} - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - Homie_setFirmware("temperature-setting", "1.0.0"); - Homie.setSetupFunction(setupHandler).setLoopFunction(loopHandler); - - temperatureNode.advertise("unit"); - temperatureNode.advertise("degrees"); - - temperatureIntervalSetting.setDefaultValue(DEFAULT_TEMPERATURE_INTERVAL); - - Homie.setup(); -} - -void loop() { - Homie.loop(); -} diff --git a/examples/DoorSensor/DoorSensor.ino b/examples/DoorSensor/DoorSensor.ino index 5fa86e17..f91aa174 100644 --- a/examples/DoorSensor/DoorSensor.ino +++ b/examples/DoorSensor/DoorSensor.ino @@ -14,25 +14,23 @@ void loopHandler() { Serial.print("Door is now: "); Serial.println(doorValue ? "open" : "close"); - Homie.setNodeProperty(doorNode, "open").send(doorValue ? "true" : "false"); - lastDoorValue = doorValue; + if (Homie.setNodeProperty(doorNode, "open", doorValue ? "true" : "false", true)) { + lastDoorValue = doorValue; + } else { + Serial.println("Sending failed"); + } } } void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); pinMode(PIN_DOOR, INPUT); digitalWrite(PIN_DOOR, HIGH); debouncer.attach(PIN_DOOR); debouncer.interval(50); - Homie_setFirmware("awesome-door", "1.0.0"); + Homie.setFirmware("awesome-door", "1.0.0"); + Homie.registerNode(doorNode); Homie.setLoopFunction(loopHandler); - - doorNode.advertise("open"); - Homie.setup(); } diff --git a/examples/HookToEvents/HookToEvents.ino b/examples/HookToEvents/HookToEvents.ino index e0fc910b..4b0bc532 100644 --- a/examples/HookToEvents/HookToEvents.ino +++ b/examples/HookToEvents/HookToEvents.ino @@ -2,37 +2,28 @@ void onHomieEvent(HomieEvent event) { switch(event) { - case HomieEvent::STANDALONE_MODE: - Serial.println("Standalone mode started"); - break; - case HomieEvent::CONFIGURATION_MODE: + case HOMIE_CONFIGURATION_MODE: Serial.println("Configuration mode started"); break; - case HomieEvent::NORMAL_MODE: + case HOMIE_NORMAL_MODE: Serial.println("Normal mode started"); break; - case HomieEvent::OTA_STARTED: - Serial.println("OTA started"); - break; - case HomieEvent::OTA_FAILED: - Serial.println("OTA failed"); - break; - case HomieEvent::OTA_SUCCESSFUL: - Serial.println("OTA successful"); + case HOMIE_OTA_MODE: + Serial.println("OTA mode started"); break; - case HomieEvent::ABOUT_TO_RESET: + case HOMIE_ABOUT_TO_RESET: Serial.println("About to reset"); break; - case HomieEvent::WIFI_CONNECTED: + case HOMIE_WIFI_CONNECTED: Serial.println("Wi-Fi connected"); break; - case HomieEvent::WIFI_DISCONNECTED: + case HOMIE_WIFI_DISCONNECTED: Serial.println("Wi-Fi disconnected"); break; - case HomieEvent::MQTT_CONNECTED: + case HOMIE_MQTT_CONNECTED: Serial.println("MQTT connected"); break; - case HomieEvent::MQTT_DISCONNECTED: + case HOMIE_MQTT_DISCONNECTED: Serial.println("MQTT disconnected"); break; } @@ -42,8 +33,8 @@ void setup() { Serial.begin(115200); Serial.println(); Serial.println(); - Homie.disableLogging(); - Homie_setFirmware("events-test", "1.0.0"); + Homie.enableLogging(false); + Homie.setFirmware("events-test", "1.0.0"); Homie.onEvent(onHomieEvent); Homie.setup(); } diff --git a/examples/IteadSonoff/IteadSonoff.ino b/examples/IteadSonoff/IteadSonoff.ino index 23aa447d..b4b465be 100644 --- a/examples/IteadSonoff/IteadSonoff.ino +++ b/examples/IteadSonoff/IteadSonoff.ino @@ -8,14 +8,14 @@ const int PIN_BUTTON = 0; HomieNode switchNode("switch", "switch"); -bool switchOnHandler(HomieRange range, String value) { +bool switchOnHandler(String value) { if (value == "true") { digitalWrite(PIN_RELAY, HIGH); - Homie.setNodeProperty(switchNode, "on").send("true"); + Homie.setNodeProperty(switchNode, "on", "true"); Serial.println("Switch is on"); } else if (value == "false") { digitalWrite(PIN_RELAY, LOW); - Homie.setNodeProperty(switchNode, "on").send("false"); + Homie.setNodeProperty(switchNode, "on", "false"); Serial.println("Switch is off"); } else { return false; @@ -25,17 +25,14 @@ bool switchOnHandler(HomieRange range, String value) { } void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); pinMode(PIN_RELAY, OUTPUT); digitalWrite(PIN_RELAY, LOW); - Homie_setFirmware("itead-sonoff", "1.0.0"); - Homie.setLedPin(PIN_LED, LOW).setResetTrigger(PIN_BUTTON, LOW, 5000); - - switchNode.advertise("on").settable(switchOnHandler); - + Homie.setFirmware("itead-sonoff", "1.0.0"); + Homie.setLedPin(PIN_LED, LOW); + Homie.setResetTrigger(PIN_BUTTON, LOW, 5000); + switchNode.subscribe("on", switchOnHandler); + Homie.registerNode(switchNode); Homie.setup(); } diff --git a/examples/LedStrip/LedStrip.ino b/examples/LedStrip/LedStrip.ino index 77945fd9..c450c29f 100644 --- a/examples/LedStrip/LedStrip.ino +++ b/examples/LedStrip/LedStrip.ino @@ -1,46 +1,53 @@ -#include - -const unsigned char NUMBER_OF_LED = 4; -const unsigned char LED_PINS[NUMBER_OF_LED] = { 16, 5, 4, 0 }; - -HomieNode stripNode("strip", "strip"); - -bool stripLedHandler(HomieRange range, String value) { - if (!range.isRange) return false; // if it's not a range - - if (range.index < 1 || range.index > NUMBER_OF_LED) return false; // if it's not a valid number - - if (value != "on" && value != "off") return false; // if the value is not valid - - bool switchOn = value == "on"; - - digitalWrite(LED_PINS[range.index - 1], switchOn ? HIGH : LOW); - Homie.setNodeProperty(stripNode, "led").setRange(range).send(value); // Update the state of the led - Serial.print("Led "); - Serial.print(range.index); - Serial.print(" is "); - Serial.println(value); - - return true; -} - -void setup() { - for (int i = 0; i < NUMBER_OF_LED; i++) { - pinMode(LED_PINS[i], OUTPUT); - digitalWrite(LED_PINS[i], LOW); - } - - Serial.begin(115200); - Serial.println(); - Serial.println(); - - Homie_setFirmware("awesome-ledstrip", "1.0.0"); - - stripNode.advertiseRange("led", 1, NUMBER_OF_LED).settable(stripLedHandler); - - Homie.setup(); -} - -void loop() { - Homie.loop(); -} +#include + +const unsigned char NUMBER_OF_LED = 4; +const unsigned char LED_PINS[NUMBER_OF_LED] = { 16, 5, 4, 0 }; + +bool stripHandler(String, String); // forward declaration (needed for Arduino <= 1.6.8) +HomieNode stripNode("ledstrip", "ledstrip", stripHandler, true); // last true: subscribe to all properties + +bool stripHandler(String property, String value) { + for (int i = 0; i < property.length(); i++) { + if (!isDigit(property.charAt(i))) { + return false; + } + } + + int ledIndex = property.toInt(); + if (ledIndex < 0 || ledIndex > NUMBER_OF_LED - 1) { + return false; + } + + if (value == "true") { + digitalWrite(LED_PINS[ledIndex], HIGH); + Homie.setNodeProperty(stripNode, String(ledIndex), "true"); // Update the state of the led + Serial.print("Led "); + Serial.print(ledIndex); + Serial.println(" is on"); + } else if (value == "false") { + digitalWrite(LED_PINS[ledIndex], LOW); + Homie.setNodeProperty(stripNode, String(ledIndex), "false"); + Serial.print("Led "); + Serial.print(ledIndex); + Serial.println(" is off"); + } else { + return false; + } + + return true; +} + +void setup() { + for (int i = 0; i < NUMBER_OF_LED; i++) { + pinMode(LED_PINS[i], INPUT); + digitalWrite(LED_PINS[i], LOW); + } + + Homie.setFirmware("awesome-ledstrip", "1.0.0"); + Homie.registerNode(stripNode); + Homie.setup(); +} + +void loop() { + Homie.loop(); +} diff --git a/examples/LightOnOff/LightOnOff.ino b/examples/LightOnOff/LightOnOff.ino index f935fba6..fce676f3 100644 --- a/examples/LightOnOff/LightOnOff.ino +++ b/examples/LightOnOff/LightOnOff.ino @@ -4,14 +4,14 @@ const int PIN_RELAY = 5; HomieNode lightNode("light", "switch"); -bool lightOnHandler(HomieRange range, String value) { +bool lightOnHandler(String value) { if (value == "true") { digitalWrite(PIN_RELAY, HIGH); - Homie.setNodeProperty(lightNode, "on").send("true"); + Homie.setNodeProperty(lightNode, "on", "true"); // Update the state of the light Serial.println("Light is on"); } else if (value == "false") { digitalWrite(PIN_RELAY, LOW); - Homie.setNodeProperty(lightNode, "on").send("false"); + Homie.setNodeProperty(lightNode, "on", "false"); Serial.println("Light is off"); } else { return false; @@ -21,16 +21,12 @@ bool lightOnHandler(HomieRange range, String value) { } void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); pinMode(PIN_RELAY, OUTPUT); digitalWrite(PIN_RELAY, LOW); - Homie_setFirmware("awesome-relay", "1.0.0"); - - lightNode.advertise("on").settable(lightOnHandler); - + Homie.setFirmware("awesome-relay", "1.0.0"); + lightNode.subscribe("on", lightOnHandler); + Homie.registerNode(lightNode); Homie.setup(); } diff --git a/examples/TemperatureSensor/TemperatureSensor.ino b/examples/TemperatureSensor/TemperatureSensor.ino index f31b7f70..be2478fa 100644 --- a/examples/TemperatureSensor/TemperatureSensor.ino +++ b/examples/TemperatureSensor/TemperatureSensor.ino @@ -7,7 +7,7 @@ unsigned long lastTemperatureSent = 0; HomieNode temperatureNode("temperature", "temperature"); void setupHandler() { - Homie.setNodeProperty(temperatureNode, "unit").setRetained(true).send("c"); + Homie.setNodeProperty(temperatureNode, "unit", "c", true); } void loopHandler() { @@ -16,21 +16,19 @@ void loopHandler() { Serial.print("Temperature: "); Serial.print(temperature); Serial.println(" °C"); - Homie.setNodeProperty(temperatureNode, "degrees").send(String(temperature)); - lastTemperatureSent = millis(); + if (Homie.setNodeProperty(temperatureNode, "degrees", String(temperature), true)) { + lastTemperatureSent = millis(); + } else { + Serial.println("Temperature sending failed"); + } } } void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - Homie_setFirmware("awesome-temperature", "1.0.0"); - Homie.setSetupFunction(setupHandler).setLoopFunction(loopHandler); - - temperatureNode.advertise("unit"); - temperatureNode.advertise("degrees"); - + Homie.setFirmware("awesome-temperature", "1.0.0"); + Homie.registerNode(temperatureNode); + Homie.setSetupFunction(setupHandler); + Homie.setLoopFunction(loopHandler); Homie.setup(); } diff --git a/firmware_parser.py b/firmware_parser.py deleted file mode 100644 index 606c5afd..00000000 --- a/firmware_parser.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 - -import re -import sys - -if len(sys.argv) != 2: - print("Please specify a file") - sys.exit(1) - -regex_homie = re.compile(b"\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25") -regex_name = re.compile(b"\xbf\x84\xe4\x13\x54(.+)\x93\x44\x6b\xa7\x75") -regex_version = re.compile(b"\x6a\x3f\x3e\x0e\xe1(.+)\xb0\x30\x48\xd4\x1a") -regex_brand = re.compile(b"\xfb\x2a\xf5\x68\xc0(.+)\x6e\x2f\x0f\xeb\x2d") - -try: - firmware_file = open(sys.argv[1], "rb") -except Exception as err: - print("Error: {0}".format(err.strerror)) - sys.exit(2) - -firmware_binary = firmware_file.read() -firmware_file.close() - -if not regex_homie.search(firmware_binary): - print("Not a valid Homie firmware") - sys.exit(3) - -regex_name_result = regex_name.search(firmware_binary) -regex_version_result = regex_version.search(firmware_binary) -regex_brand_result = regex_brand.search(firmware_binary) - -name = regex_name_result.group(1).decode() if regex_name_result else "unset (default is undefined)" -version = regex_version_result.group(1).decode() if regex_version_result else "unset (default is undefined)" -brand = regex_brand_result.group(1).decode() if regex_brand_result else "unset (default is Homie)" - -print("Name: {0}".format(name)) -print("Version: {0}".format(version)) -print("Brand: {0}".format(brand)) \ No newline at end of file diff --git a/homie-esp8266.jpg b/homie-esp8266.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5266aebdaf05626bae6ddd6316c0b22f8c03357f GIT binary patch literal 47471 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 CFSbJf literal 0 HcmV?d00001 diff --git a/keywords.txt b/keywords.txt index a664ca0c..1b17bea0 100644 --- a/keywords.txt +++ b/keywords.txt @@ -4,68 +4,42 @@ Homie KEYWORD1 HomieNode KEYWORD1 -HomieSetting KEYWORD1 HomieEvent KEYWORD1 -HomieRange KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) ####################################### -Homie_setBrand KEYWORD2 -Homie_setFirmware KEYWORD2 - setup KEYWORD2 loop KEYWORD2 -disableLogging KEYWORD2 -setLoggingPrinter KEYWORD2 -disableLedFeedback KEYWORD2 +enableLogging KEYWORD2 +enableBuiltInLedIndicator KEYWORD2 setLedPin KEYWORD2 +setBrand KEYWORD2 +setFirmware KEYWORD2 +registerNode KEYWORD2 setGlobalInputHandler KEYWORD2 +setSetupFunction KEYWORD2 +setLoopFunction KEYWORD2 onEvent KEYWORD2 setResetTrigger KEYWORD2 disableResetTrigger KEYWORD2 setResetFunction KEYWORD2 -setSetupFunction KEYWORD2 -setLoopFunction KEYWORD2 -setStandalone KEYWORD2 +isReadyToOperate KEYWORD2 +setResettable KEYWORD2 setNodeProperty KEYWORD2 -setIdle KEYWORD2 -eraseConfiguration KEYWORD2 -isConfigured KEYWORD2 -isConnected KEYWORD2 -getConfiguration KEYWORD2 -getMqttClient KEYWORD2 - -advertise KEYWORD2 -advertiseRange KEYWORD2 -settable KEYWORD2 - -get KEYWORD2 -wasProvided KEYWORD2 -setDefaultValue KEYWORD2 -setValidator KEYWORD2 - -isRange KEYWORD2 -index KEYWORD2 -setQos KEYWORD2 -setRetained KEYWORD2 -setRange KEYWORD2 -send KEYWORD2 +subscribe KEYWORD2 ####################################### # Constants (LITERAL1) ####################################### -STANDALONE_MODE LITERAL1 -CONFIGURATION_MODE LITERAL1 -NORMAL_MODE LITERAL1 -OTA_STARTED LITERAL1 -OTA_FAILED LITERAL1 -OTA_SUCCESSFUL LITERAL1 -ABOUT_TO_RESET LITERAL1 -WIFI_CONNECTED LITERAL1 -WIFI_DISCONNECTED LITERAL1 -MQTT_CONNECTED LITERAL1 -MQTT_DISCONNECTED LITERAL1 +HOMIE_CONFIGURATION_MODE LITERAL1 +HOMIE_NORMAL_MODE LITERAL1 +HOMIE_OTA_MODE LITERAL1 +HOMIE_ABOUT_TO_RESET LITERAL1 +HOMIE_WIFI_CONNECTED LITERAL1 +HOMIE_WIFI_DISCONNECTED LITERAL1 +HOMIE_MQTT_CONNECTED LITERAL1 +HOMIE_MQTT_DISCONNECTED LITERAL1 diff --git a/library.json b/library.json index 683c225a..48284a37 100644 --- a/library.json +++ b/library.json @@ -17,7 +17,7 @@ "platforms": "espressif", "dependencies": [ { - "name": "ArduinoJson", + "name": "Json", "authors": "Benoit Blanchon", "frameworks": "arduino" }, diff --git a/src/Homie.cpp b/src/Homie.cpp index 71803c37..0c786877 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -2,132 +2,48 @@ using namespace HomieInternals; -SendingPromise::SendingPromise(HomieClass* homie) -: _homie(homie) -, _node(nullptr) -, _property(nullptr) -, _qos(0) -, _retained(false) { -} - -SendingPromise& SendingPromise::setQos(uint8_t qos) { - _qos = qos; -} - -SendingPromise& SendingPromise::setRetained(bool retained) { - _retained = retained; -} - -SendingPromise& SendingPromise::setRange(HomieRange range) { - _range = range; -} - -SendingPromise& SendingPromise::setRange(uint16_t rangeIndex) { - HomieRange range; - range.isRange = true; - range.index = rangeIndex; - _range = range; -} - -void SendingPromise::send(const String& value) { - if (!_homie->isConnected()) { - _homie->_logger.logln(F("✖ setNodeProperty(): impossible now")); - return; - } - - char* topic = new char[strlen(_homie->getConfiguration().mqtt.baseTopic) + strlen(_homie->getConfiguration().deviceId) + 1 + strlen(_node->getId()) + 1 + strlen(_property->c_str()) + 6 + 1]; // last + 6 for range _65536 - strcpy(topic, _homie->getConfiguration().mqtt.baseTopic); - strcat(topic, _homie->getConfiguration().deviceId); - strcat_P(topic, PSTR("/")); - strcat(topic, _node->getId()); - strcat_P(topic, PSTR("/")); - strcat(topic, _property->c_str()); - - if (_range.isRange) { - char rangeStr[5 + 1]; // max 65536 - itoa(_range.index, rangeStr, 10); - strcat_P(topic, PSTR("_")); - strcat(topic, rangeStr); - } - - _homie->getMqttClient().publish(topic, _qos, _retained, value.c_str()); - delete[] topic; -} - -SendingPromise& SendingPromise::setNode(const HomieNode& node) { - _node = &node; -} - -SendingPromise& SendingPromise::setProperty(const String& property) { - _property = &property; -} - -const HomieNode* SendingPromise::getNode() const { - return _node; -} - -const String* SendingPromise::getProperty() const { - return _property; -} - -uint8_t SendingPromise::getQos() const { - return _qos; -} - -HomieRange SendingPromise::getRange() const { - return _range; -} - -bool SendingPromise::isRetained() const { - return _retained; -} - -HomieClass::HomieClass() -: _setupCalled(false) -, _sendingPromise(this) -, __HOMIE_SIGNATURE("\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25") { - strcpy(_interface.brand, DEFAULT_BRAND); - _interface.standalone = false; - strcpy(_interface.firmware.name, DEFAULT_FW_NAME); - strcpy(_interface.firmware.version, DEFAULT_FW_VERSION); - _interface.led.enabled = true; - _interface.led.pin = BUILTIN_LED; - _interface.led.on = LOW; - _interface.reset.able = true; - _interface.reset.enabled = true; - _interface.reset.triggerPin = DEFAULT_RESET_PIN; - _interface.reset.triggerState = DEFAULT_RESET_STATE; - _interface.reset.triggerTime = DEFAULT_RESET_TIME; - _interface.reset.userFunction = []() { return false; }; - _interface.globalInputHandler = [](String node, String property, HomieRange range, String value) { return false; }; - _interface.setupFunction = []() {}; - _interface.loopFunction = []() {}; - _interface.eventHandler = [](HomieEvent event) {}; - _interface.connected = false; - _interface.logger = &_logger; - _interface.blinker = &_blinker; - _interface.config = &_config; - _interface.mqttClient = &_mqttClient; +HomieClass::HomieClass() : _setupCalled(false) { + strcpy(this->_interface.brand, DEFAULT_BRAND); + strcpy(this->_interface.firmware.name, DEFAULT_FW_NAME); + strcpy(this->_interface.firmware.version, DEFAULT_FW_VERSION); + this->_interface.led.enabled = true; + this->_interface.led.pin = BUILTIN_LED; + this->_interface.led.on = LOW; + this->_interface.reset.able = true; + this->_interface.reset.enabled = true; + this->_interface.reset.triggerPin = DEFAULT_RESET_PIN; + this->_interface.reset.triggerState = DEFAULT_RESET_STATE; + this->_interface.reset.triggerTime = DEFAULT_RESET_TIME; + this->_interface.reset.userFunction = []() { return false; }; + this->_interface.globalInputHandler = [](String node, String property, String value) { return false; }; + this->_interface.setupFunction = []() {}; + this->_interface.loopFunction = []() {}; + this->_interface.eventHandler = [](HomieEvent event) {}; + this->_interface.readyToOperate = false; + this->_interface.logger = &this->_logger; + this->_interface.blinker = &this->_blinker; + this->_interface.config = &this->_config; + this->_interface.mqttClient = &this->_mqttClient; Helpers::generateDeviceId(); - _config.attachInterface(&_interface); - _blinker.attachInterface(&_interface); + this->_config.attachInterface(&this->_interface); + this->_blinker.attachInterface(&this->_interface); + this->_mqttClient.attachInterface(&this->_interface); - _bootStandalone.attachInterface(&_interface); - _bootNormal.attachInterface(&_interface); - _bootConfig.attachInterface(&_interface); + this->_bootNormal.attachInterface(&this->_interface); + this->_bootOta.attachInterface(&this->_interface); + this->_bootConfig.attachInterface(&this->_interface); } HomieClass::~HomieClass() { } -void HomieClass::_checkBeforeSetup(const __FlashStringHelper* functionName) const { +void HomieClass::_checkBeforeSetup(const __FlashStringHelper* functionName) { if (_setupCalled) { - _logger.log(F("✖ ")); - _logger.log(functionName); - _logger.logln(F("(): has to be called before setup()")); - _logger.flush(); + this->_logger.log(F("✖ ")); + this->_logger.log(functionName); + this->_logger.logln(F("(): has to be called before setup()")); abort(); } } @@ -135,190 +51,164 @@ void HomieClass::_checkBeforeSetup(const __FlashStringHelper* functionName) cons void HomieClass::setup() { _setupCalled = true; - if (!_config.load()) { - if (_interface.standalone && !_config.canBypassStandalone()) { - _boot = &_bootStandalone; - _logger.logln(F("Triggering STANDALONE_MODE event...")); - _interface.eventHandler(HomieEvent::STANDALONE_MODE); - } else { - _boot = &_bootConfig; - _logger.logln(F("Triggering CONFIGURATION_MODE event...")); - _interface.eventHandler(HomieEvent::CONFIGURATION_MODE); - } + if (this->_logger.isEnabled()) { + Serial.begin(BAUD_RATE); + this->_logger.logln(); + this->_logger.logln(); + } + + if (!this->_config.load()) { + this->_boot = &this->_bootConfig; + this->_logger.logln(F("Triggering HOMIE_CONFIGURATION_MODE event...")); + this->_interface.eventHandler(HOMIE_CONFIGURATION_MODE); } else { - switch (_config.getBootMode()) { + switch (this->_config.getBootMode()) { case BOOT_NORMAL: - _boot = &_bootNormal; - _logger.logln(F("Triggering NORMAL_MODE event...")); - _interface.eventHandler(HomieEvent::NORMAL_MODE); + this->_boot = &this->_bootNormal; + this->_logger.logln(F("Triggering HOMIE_NORMAL_MODE event...")); + this->_interface.eventHandler(HOMIE_NORMAL_MODE); + break; + case BOOT_OTA: + this->_boot = &this->_bootOta; + this->_logger.logln(F("Triggering HOMIE_OTA_MODE event...")); + this->_interface.eventHandler(HOMIE_OTA_MODE); break; default: - _logger.logln(F("✖ The boot mode is invalid")); - _logger.flush(); + this->_logger.logln(F("✖ The boot mode is invalid")); abort(); break; } } - _boot->setup(); + this->_boot->setup(); } void HomieClass::loop() { - _boot->loop(); + this->_boot->loop(); } -HomieClass& HomieClass::disableLogging() { - _checkBeforeSetup(F("disableLogging")); - - _logger.setLogging(false); +void HomieClass::enableLogging(bool enable) { + this->_checkBeforeSetup(F("enableLogging")); - return *this; + this->_logger.setLogging(enable); } -HomieClass& HomieClass::setLoggingPrinter(Print* printer) { - _checkBeforeSetup(F("setLoggingPrinter")); +void HomieClass::enableBuiltInLedIndicator(bool enable) { + this->_checkBeforeSetup(F("enableBuiltInLedIndicator")); - _logger.setPrinter(printer); - - return *this; + this->_interface.led.enabled = enable; } -HomieClass& HomieClass::disableLedFeedback() { - _checkBeforeSetup(F("disableLedFeedback")); - - _interface.led.enabled = false; +void HomieClass::setLedPin(unsigned char pin, unsigned char on) { + this->_checkBeforeSetup(F("setLedPin")); - return *this; + this->_interface.led.pin = pin; + this->_interface.led.on = on; } -HomieClass& HomieClass::setLedPin(uint8_t pin, uint8_t on) { - _checkBeforeSetup(F("setLedPin")); - - _interface.led.pin = pin; - _interface.led.on = on; +void HomieClass::setFirmware(const char* name, const char* version) { + this->_checkBeforeSetup(F("setFirmware")); + if (strlen(name) + 1 > MAX_FIRMWARE_NAME_LENGTH || strlen(version) + 1 > MAX_FIRMWARE_VERSION_LENGTH) { + this->_logger.logln(F("✖ setFirmware(): either the name or version string is too long")); + abort(); + } - return *this; + strcpy(this->_interface.firmware.name, name); + strcpy(this->_interface.firmware.version, version); } -void HomieClass::__setFirmware(const char* name, const char* version) { - _checkBeforeSetup(F("setFirmware")); - if (strlen(name) + 1 - 10 > MAX_FIRMWARE_NAME_LENGTH || strlen(version) + 1 - 10 > MAX_FIRMWARE_VERSION_LENGTH) { - _logger.logln(F("✖ setFirmware(): either the name or version string is too long")); - _logger.flush(); +void HomieClass::setBrand(const char* name) { + this->_checkBeforeSetup(F("setBrand")); + if (strlen(name) + 1 > MAX_BRAND_LENGTH) { + this->_logger.logln(F("✖ setBrand(): the brand string is too long")); abort(); } - strncpy(_interface.firmware.name, name + 5, strlen(name) - 10); - _interface.firmware.name[strlen(name) - 10] = '\0'; - strncpy(_interface.firmware.version, version + 5, strlen(version) - 10); - _interface.firmware.version[strlen(version) - 10] = '\0'; + strcpy(this->_interface.brand, name); } -void HomieClass::__setBrand(const char* brand) { - _checkBeforeSetup(F("setBrand")); - if (strlen(brand) + 1 - 10 > MAX_BRAND_LENGTH) { - _logger.logln(F("✖ setBrand(): the brand string is too long")); - _logger.flush(); +void HomieClass::registerNode(const HomieNode& node) { + this->_checkBeforeSetup(F("registerNode")); + if (this->_interface.registeredNodesCount > MAX_REGISTERED_NODES_COUNT) { + Serial.println(F("✖ register(): the max registered nodes count has been reached")); abort(); } - strncpy(_interface.brand, brand + 5, strlen(brand) - 10); - _interface.brand[strlen(brand) - 10] = '\0'; + this->_interface.registeredNodes[this->_interface.registeredNodesCount++] = &node; } -void HomieClass::setIdle(bool idle) { - _interface.reset.able = idle; +bool HomieClass::isReadyToOperate() { + return this->_interface.readyToOperate; } -HomieClass& HomieClass::setGlobalInputHandler(GlobalInputHandler inputHandler) { - _checkBeforeSetup(F("setGlobalInputHandler")); - - _interface.globalInputHandler = inputHandler; - - return *this; +void HomieClass::setResettable(bool resettable) { + this->_interface.reset.able = resettable; } -HomieClass& HomieClass::setResetFunction(ResetFunction function) { - _checkBeforeSetup(F("setResetFunction")); - - _interface.reset.userFunction = function; +void HomieClass::setGlobalInputHandler(GlobalInputHandler inputHandler) { + this->_checkBeforeSetup(F("setGlobalInputHandler")); - return *this; + this->_interface.globalInputHandler = inputHandler; } -HomieClass& HomieClass::setSetupFunction(OperationFunction function) { - _checkBeforeSetup(F("setSetupFunction")); - - _interface.setupFunction = function; +void HomieClass::setResetFunction(ResetFunction function) { + this->_checkBeforeSetup(F("setResetFunction")); - return *this; + this->_interface.reset.userFunction = function; } -HomieClass& HomieClass::setLoopFunction(OperationFunction function) { - _checkBeforeSetup(F("setLoopFunction")); +void HomieClass::setSetupFunction(OperationFunction function) { + this->_checkBeforeSetup(F("setSetupFunction")); - _interface.loopFunction = function; - - return *this; + this->_interface.setupFunction = function; } -HomieClass& HomieClass::setStandalone() { - _checkBeforeSetup(F("setStandalone")); - - _interface.standalone = true; - - return *this; -} +void HomieClass::setLoopFunction(OperationFunction function) { + this->_checkBeforeSetup(F("setLoopFunction")); -bool HomieClass::isConfigured() const { - return _config.getBootMode() == BOOT_NORMAL; + this->_interface.loopFunction = function; } -bool HomieClass::isConnected() const { - return _interface.connected; -} - -HomieClass& HomieClass::onEvent(EventHandler handler) { - _checkBeforeSetup(F("onEvent")); - - _interface.eventHandler = handler; +void HomieClass::onEvent(EventHandler handler) { + this->_checkBeforeSetup(F("onEvent")); - return *this; + this->_interface.eventHandler = handler; } -HomieClass& HomieClass::setResetTrigger(uint8_t pin, uint8_t state, uint16_t time) { - _checkBeforeSetup(F("setResetTrigger")); +void HomieClass::setResetTrigger(unsigned char pin, unsigned char state, unsigned int time) { + this->_checkBeforeSetup(F("setResetTrigger")); - _interface.reset.enabled = true; - _interface.reset.triggerPin = pin; - _interface.reset.triggerState = state; - _interface.reset.triggerTime = time; - - return *this; + this->_interface.reset.enabled = true; + this->_interface.reset.triggerPin = pin; + this->_interface.reset.triggerState = state; + this->_interface.reset.triggerTime = time; } -HomieClass& HomieClass::disableResetTrigger() { - _checkBeforeSetup(F("disableResetTrigger")); - - _interface.reset.enabled = false; +void HomieClass::disableResetTrigger() { + this->_checkBeforeSetup(F("disableResetTrigger")); - return *this; + this->_interface.reset.enabled = false; } -void HomieClass::eraseConfiguration() { - _config.erase(); -} +bool HomieClass::setNodeProperty(const HomieNode& node, const char* property, const char* value, bool retained) { + if (!this->isReadyToOperate()) { + this->_logger.logln(F("✖ setNodeProperty(): impossible now")); + return false; + } -const ConfigStruct& HomieClass::getConfiguration() const { - return _config.get(); -} + strcpy(this->_mqttClient.getTopicBuffer(), this->_config.get().mqtt.baseTopic); + strcat(this->_mqttClient.getTopicBuffer(), this->_config.get().deviceId); + strcat_P(this->_mqttClient.getTopicBuffer(), PSTR("/")); + strcat(this->_mqttClient.getTopicBuffer(), node.getId()); + strcat_P(this->_mqttClient.getTopicBuffer(), PSTR("/")); + strcat(this->_mqttClient.getTopicBuffer(), property); -AsyncMqttClient& HomieClass::getMqttClient() { - return _mqttClient; -} + if (5 + 2 + strlen(this->_mqttClient.getTopicBuffer()) + strlen(value) + 1 > MQTT_MAX_PACKET_SIZE) { + this->_logger.logln(F("✖ setNodeProperty(): content to send is too long")); + return false; + } -void HomieClass::prepareForSleep() { - _boot->prepareForSleep(); + return this->_mqttClient.publish(value, retained); } HomieClass Homie; diff --git a/src/Homie.h b/src/Homie.h index 1f41262a..b61e8e7e 100644 --- a/src/Homie.h +++ b/src/Homie.h @@ -1,6 +1,6 @@ -#ifndef SRC_HOMIE_H_ -#define SRC_HOMIE_H_ +#ifndef Homie_h +#define Homie_h #include "Homie.hpp" -#endif // SRC_HOMIE_H_ +#endif diff --git a/src/Homie.hpp b/src/Homie.hpp index 03e3306c..214c47b9 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include "Homie/MqttClient.hpp" #include "Homie/Blinker.hpp" #include "Homie/Logger.hpp" #include "Homie/Config.hpp" @@ -8,102 +8,53 @@ #include "Homie/Limits.hpp" #include "Homie/Helpers.hpp" #include "Homie/Boot/Boot.hpp" -#include "Homie/Boot/BootStandalone.hpp" #include "Homie/Boot/BootNormal.hpp" #include "Homie/Boot/BootConfig.hpp" +#include "Homie/Boot/BootOta.hpp" #include "HomieNode.hpp" -#include "HomieSetting.hpp" - -#define Homie_setFirmware(name, version) const char* __FLAGGED_FW_NAME = "\xbf\x84\xe4\x13\x54" name "\x93\x44\x6b\xa7\x75"; const char* __FLAGGED_FW_VERSION = "\x6a\x3f\x3e\x0e\xe1" version "\xb0\x30\x48\xd4\x1a"; Homie.__setFirmware(__FLAGGED_FW_NAME, __FLAGGED_FW_VERSION); -#define Homie_setBrand(brand) const char* __FLAGGED_BRAND = "\xfb\x2a\xf5\x68\xc0" brand "\x6e\x2f\x0f\xeb\x2d"; Homie.__setBrand(__FLAGGED_BRAND); namespace HomieInternals { -class HomieClass; - -class SendingPromise { - friend HomieClass; - - public: - explicit SendingPromise(HomieClass* homie); - SendingPromise& setQos(uint8_t qos); - SendingPromise& setRetained(bool retained); - SendingPromise& setRange(HomieRange range); - SendingPromise& setRange(uint16_t rangeIndex); - void send(const String& value); - - private: - SendingPromise& setNode(const HomieNode& node); - SendingPromise& setProperty(const String& property); - const HomieNode* getNode() const; - const String* getProperty() const; - uint8_t getQos() const; - HomieRange getRange() const; - bool isRetained() const; - - HomieClass* _homie; - const HomieNode* _node; - const String* _property; - uint8_t _qos; - bool _retained; - HomieRange _range; -}; - -class HomieClass { - friend class ::HomieNode; - friend SendingPromise; - - public: - HomieClass(); - ~HomieClass(); - void setup(); - void loop(); - - void __setFirmware(const char* name, const char* version); - void __setBrand(const char* brand); - - HomieClass& disableLogging(); - HomieClass& setLoggingPrinter(Print* printer); - HomieClass& disableLedFeedback(); - HomieClass& setLedPin(uint8_t pin, uint8_t on); - HomieClass& setGlobalInputHandler(GlobalInputHandler globalInputHandler); - HomieClass& onEvent(EventHandler handler); - HomieClass& setResetTrigger(uint8_t pin, uint8_t state, uint16_t time); - HomieClass& disableResetTrigger(); - HomieClass& setResetFunction(ResetFunction function); - HomieClass& setSetupFunction(OperationFunction function); - HomieClass& setLoopFunction(OperationFunction function); - HomieClass& setStandalone(); - - SendingPromise& setNodeProperty(const HomieNode& node, const String& property) { - return _sendingPromise.setNode(node).setProperty(property).setQos(1).setRetained(true).setRange({ .isRange = false, .index = 0 }); - } - - void setIdle(bool idle); - void eraseConfiguration(); - bool isConfigured() const; - bool isConnected() const; - const ConfigStruct& getConfiguration() const; - AsyncMqttClient& getMqttClient(); - void prepareForSleep(); - - private: - bool _setupCalled; - Boot* _boot; - BootStandalone _bootStandalone; - BootNormal _bootNormal; - BootConfig _bootConfig; - SendingPromise _sendingPromise; - Interface _interface; - Logger _logger; - Blinker _blinker; - Config _config; - AsyncMqttClient _mqttClient; - - void _checkBeforeSetup(const __FlashStringHelper* functionName) const; - - const char* __HOMIE_SIGNATURE; -}; -} // namespace HomieInternals + class HomieClass { + public: + HomieClass(); + ~HomieClass(); + void setup(); + void loop(); + + void enableLogging(bool enable); + void enableBuiltInLedIndicator(bool enable); + void setLedPin(unsigned char pin, unsigned char on); + void setBrand(const char* name); + void setFirmware(const char* name, const char* version); + void registerNode(const HomieNode& node); + void setGlobalInputHandler(GlobalInputHandler globalInputHandler); + void setResettable(bool resettable); + void onEvent(EventHandler handler); + void setResetTrigger(unsigned char pin, unsigned char state, unsigned int time); + void disableResetTrigger(); + void setResetFunction(ResetFunction function); + void setSetupFunction(OperationFunction function); + void setLoopFunction(OperationFunction function); + bool isReadyToOperate(); + bool setNodeProperty(const HomieNode& node, const String& property, const String& value, bool retained = true) { + return this->setNodeProperty(node, property.c_str(), value.c_str(), retained); + } + bool setNodeProperty(const HomieNode& node, const char* property, const char* value, bool retained = true); + private: + bool _setupCalled; + Boot* _boot; + BootNormal _bootNormal; + BootConfig _bootConfig; + BootOta _bootOta; + Interface _interface; + Logger _logger; + Blinker _blinker; + Config _config; + MqttClient _mqttClient; + + void _checkBeforeSetup(const __FlashStringHelper* functionName); + }; +} extern HomieInternals::HomieClass Homie; diff --git a/src/Homie/Blinker.cpp b/src/Homie/Blinker.cpp index 25bb650d..3e76ecc4 100644 --- a/src/Homie/Blinker.cpp +++ b/src/Homie/Blinker.cpp @@ -4,28 +4,29 @@ using namespace HomieInternals; Blinker::Blinker() : _interface(nullptr) -, _lastBlinkPace(0) { +, _lastBlinkPace(0) +{ } void Blinker::attachInterface(Interface* interface) { - _interface = interface; + this->_interface = interface; } void Blinker::start(float blinkPace) { - if (_lastBlinkPace != blinkPace) { - _ticker.attach(blinkPace, _tick, _interface->led.pin); - _lastBlinkPace = blinkPace; + if (this->_lastBlinkPace != blinkPace) { + this->_ticker.attach(blinkPace, this->_tick, this->_interface->led.pin); + this->_lastBlinkPace = blinkPace; } } void Blinker::stop() { - if (_lastBlinkPace != 0) { - _ticker.detach(); - _lastBlinkPace = 0; - digitalWrite(_interface->led.pin, !_interface->led.on); + if (this->_lastBlinkPace != 0) { + this->_ticker.detach(); + this->_lastBlinkPace = 0; + digitalWrite(this->_interface->led.pin, !this->_interface->led.on); } } -void Blinker::_tick(uint8_t pin) { +void Blinker::_tick(unsigned char pin) { digitalWrite(pin, !digitalRead(pin)); } diff --git a/src/Homie/Blinker.hpp b/src/Homie/Blinker.hpp index 23a690b5..6c19a5ce 100644 --- a/src/Homie/Blinker.hpp +++ b/src/Homie/Blinker.hpp @@ -4,18 +4,18 @@ #include "Datatypes/Interface.hpp" namespace HomieInternals { -class Blinker { - public: - Blinker(); - void attachInterface(Interface* interface); - void start(float blinkPace); - void stop(); + class Blinker { + public: + Blinker(); + void attachInterface(Interface* interface); + void start(float blinkPace); + void stop(); - private: - Interface* _interface; - Ticker _ticker; - float _lastBlinkPace; + private: + Interface* _interface; + Ticker _ticker; + float _lastBlinkPace; - static void _tick(uint8_t pin); -}; -} // namespace HomieInternals + static void _tick(unsigned char pin); + }; +} diff --git a/src/Homie/Boot/Boot.cpp b/src/Homie/Boot/Boot.cpp index aec14e0f..be205a90 100644 --- a/src/Homie/Boot/Boot.cpp +++ b/src/Homie/Boot/Boot.cpp @@ -1,11 +1,11 @@ #include "Boot.hpp" -#include "../Config.hpp" using namespace HomieInternals; Boot::Boot(const char* name) : _interface(nullptr) -, _name(name) { +, _name(name) +{ } void Boot::attachInterface(Interface* interface) { @@ -13,20 +13,25 @@ void Boot::attachInterface(Interface* interface) { } void Boot::setup() { - if (_interface->led.enabled) { - pinMode(_interface->led.pin, OUTPUT); - digitalWrite(_interface->led.pin, !_interface->led.on); + if (this->_interface->led.enabled) { + pinMode(this->_interface->led.pin, OUTPUT); + digitalWrite(this->_interface->led.pin, !this->_interface->led.on); } - WiFi.persistent(true); // Persist data on SDK as it seems Wi-Fi connection is faster + WiFi.persistent(false); // Don't persist data on EEPROM since this is handled by Homie + WiFi.disconnect(); // Reset network state - _interface->logger->log(F("** Booting into ")); - _interface->logger->log(_name); - _interface->logger->logln(F(" mode **")); -} + char hostname[MAX_WIFI_SSID_LENGTH]; + strcpy(hostname, this->_interface->brand); + strcat_P(hostname, PSTR("-")); + strcat(hostname, Helpers::getDeviceId()); -void Boot::loop() { + WiFi.hostname(hostname); + + this->_interface->logger->log(F("** Booting into ")); + this->_interface->logger->log(this->_name); + this->_interface->logger->logln(F(" mode **")); } -void Boot::prepareForSleep() { +void Boot::loop() { } diff --git a/src/Homie/Boot/Boot.hpp b/src/Homie/Boot/Boot.hpp index 0b561070..c8ef20b1 100644 --- a/src/Homie/Boot/Boot.hpp +++ b/src/Homie/Boot/Boot.hpp @@ -1,7 +1,5 @@ #pragma once -#include "Arduino.h" - #include #include "../Datatypes/Interface.hpp" #include "../Constants.hpp" @@ -10,17 +8,15 @@ #include "../Helpers.hpp" namespace HomieInternals { -class Boot { - public: - explicit Boot(const char* name); - virtual void setup(); - virtual void loop(); - virtual void prepareForSleep(); - - void attachInterface(Interface* interface); + class Boot { + public: + explicit Boot(const char* name); + virtual void setup(); + virtual void loop(); - protected: - Interface* _interface; - const char* _name; -}; -} // namespace HomieInternals + void attachInterface(Interface* interface); + protected: + Interface* _interface; + const char* _name; + }; +} diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 9bd7f72f..e601175c 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -11,8 +11,8 @@ BootConfig::BootConfig() , _jsonWifiNetworks() , _flaggedForReboot(false) , _flaggedForRebootAt(0) -, _proxyEnabled(false) { - _wifiScanTimer.setInterval(CONFIG_SCAN_INTERVAL); +{ + this->_wifiScanTimer.setInterval(CONFIG_SCAN_INTERVAL); } BootConfig::~BootConfig() { @@ -21,109 +21,64 @@ BootConfig::~BootConfig() { void BootConfig::setup() { Boot::setup(); - if (_interface->led.enabled) { - digitalWrite(_interface->led.pin, _interface->led.on); + if (this->_interface->led.enabled) { + digitalWrite(this->_interface->led.pin, this->_interface->led.on); } const char* deviceId = Helpers::getDeviceId(); - _interface->logger->log(F("Device ID is ")); - _interface->logger->logln(deviceId); + this->_interface->logger->log(F("Device ID is ")); + this->_interface->logger->logln(deviceId); - WiFi.mode(WIFI_AP_STA); + WiFi.mode(WIFI_AP); char apName[MAX_WIFI_SSID_LENGTH]; - strcpy(apName, _interface->brand); + strcpy(apName, this->_interface->brand); strcat_P(apName, PSTR("-")); strcat(apName, Helpers::getDeviceId()); WiFi.softAPConfig(ACCESS_POINT_IP, ACCESS_POINT_IP, IPAddress(255, 255, 255, 0)); WiFi.softAP(apName, deviceId); - _interface->logger->log(F("AP started as ")); - _interface->logger->logln(apName); - _dns.setTTL(30); - _dns.setErrorReplyCode(DNSReplyCode::NoError); - _dns.start(53, F("*"), ACCESS_POINT_IP); + this->_interface->logger->log(F("AP started as ")); + this->_interface->logger->logln(apName); - _http.on("/heart", HTTP_GET, [this]() { - _interface->logger->logln(F("Received heart request")); - _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), F("{\"heart\":\"beat\"}")); + this->_dns.setTTL(300); + this->_dns.setErrorReplyCode(DNSReplyCode::ServerFailure); + this->_dns.start(53, F("homie.config"), ACCESS_POINT_IP); + + this->_http.on("/", HTTP_GET, [this]() { + this->_interface->logger->logln(F("Received index request")); + this->_http.send(200, F("text/plain"), F("See Configuration API usage: http://marvinroger.viewdocs.io/homie-esp8266/6.-Configuration-API")); }); - _http.on("/device-info", HTTP_GET, std::bind(&BootConfig::_onDeviceInfoRequest, this)); - _http.on("/networks", HTTP_GET, std::bind(&BootConfig::_onNetworksRequest, this)); - _http.on("/config", HTTP_PUT, std::bind(&BootConfig::_onConfigRequest, this)); - _http.on("/config", HTTP_OPTIONS, [this]() { // CORS - _interface->logger->logln(F("Received CORS request for /config")); - _http.sendContent(FPSTR(PROGMEM_CONFIG_CORS)); + this->_http.on("/heart", HTTP_GET, [this]() { + this->_interface->logger->logln(F("Received heart request")); + this->_http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), F("{\"heart\":\"beat\"}")); }); - _http.on("/wifi-connect", HTTP_POST, std::bind(&BootConfig::_onWifiConnectRequest, this)); - _http.on("/wifi-status", HTTP_GET, std::bind(&BootConfig::_onWifiStatusRequest, this)); - _http.on("/proxy-control", HTTP_POST, std::bind(&BootConfig::_onProxyControlRequest, this)); - _http.onNotFound(std::bind(&BootConfig::_onCaptivePortal, this)); - _http.begin(); -} - -void BootConfig::_onWifiConnectRequest() { - _interface->logger->logln(F("Received wifi connect request")); - String ssid = _http.arg("ssid"); - String pass = _http.arg("password"); - if (ssid && pass && ssid != "" && pass != "") { - _interface->logger->logln(F("Connecting to WiFi")); - WiFi.begin(ssid.c_str(), pass.c_str()); - _http.send(202, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), "{\"success\":true}"); - } else { - _interface->logger->logln(F("ssid/password required")); - _http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), "{\"success\":false, \"error\":\"ssid-password-required\"}"); - } -} - -void BootConfig::_onWifiStatusRequest() { - _interface->logger->logln(F("Received wifi status request")); - String json = ""; - switch (WiFi.status()) { - case WL_IDLE_STATUS: - json = "{\"status\":\"idle\"}"; break; - case WL_CONNECT_FAILED: - json = "{\"status\":\"connect-failed\"}"; break; - case WL_CONNECTION_LOST: - json = "{\"status\":\"connection-lost\"}"; break; - case WL_NO_SSID_AVAIL: - json = "{\"status\":\"no-ssid-avail\"}"; break; - case WL_CONNECTED: - json = "{\"status\":\"connected\", \"ip\":\""+ WiFi.localIP().toString() +"\"}"; break; - case WL_DISCONNECTED: - json = "{\"status\":\"disconnected\"}"; break; - default: - json = "{\"status\":\"other\"}"; break; - } - _interface->logger->log(F("WiFi status ")); - _interface->logger->logln(json); - _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), json); -} - -void BootConfig::_onProxyControlRequest() { - _interface->logger->logln(F("Received proxy control request")); - String enable = _http.arg("enable"); - _proxyEnabled = (enable == "true"); - if (_proxyEnabled) { - _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), "{\"message\":\"proxy-enabled\"}"); - } else { - _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), "{\"message\":\"proxy-disabled\"}"); - } - _interface->logger->log(F("Transparent proxy enabled=")); - _interface->logger->logln(_proxyEnabled); + this->_http.on("/device-info", HTTP_GET, std::bind(&BootConfig::_onDeviceInfoRequest, this)); + this->_http.on("/networks", HTTP_GET, std::bind(&BootConfig::_onNetworksRequest, this)); + this->_http.on("/config", HTTP_PUT, std::bind(&BootConfig::_onConfigRequest, this)); + this->_http.on("/config", HTTP_OPTIONS, [this]() { // CORS + this->_interface->logger->logln(F("Received CORS request for /config")); + this->_http.sendContent(FPSTR(PROGMEM_CONFIG_CORS)); + }); + this->_http.begin(); } void BootConfig::_generateNetworksJson() { - DynamicJsonBuffer generatedJsonBuffer = DynamicJsonBuffer(JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(_ssidCount) + (_ssidCount * JSON_OBJECT_SIZE(3))); // 1 at root, 3 in childrend + DynamicJsonBuffer generatedJsonBuffer = DynamicJsonBuffer(JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(this->_ssidCount) + (this->_ssidCount * JSON_OBJECT_SIZE(3))); // 1 at root, 3 in childrend JsonObject& json = generatedJsonBuffer.createObject(); + int jsonLength = 15; // {"networks":[]} JsonArray& networks = json.createNestedArray("networks"); - for (int network = 0; network < _ssidCount; network++) { + for (int network = 0; network < this->_ssidCount; network++) { + jsonLength += 36; // {"ssid":"","rssi":,"encryption":""}, JsonObject& jsonNetwork = generatedJsonBuffer.createObject(); + jsonLength += WiFi.SSID(network).length(); jsonNetwork["ssid"] = WiFi.SSID(network); + jsonLength += 4; jsonNetwork["rssi"] = WiFi.RSSI(network); + jsonLength += 4; switch (WiFi.encryptionType(network)) { case ENC_TYPE_WEP: jsonNetwork["encryption"] = "wep"; @@ -145,250 +100,143 @@ void BootConfig::_generateNetworksJson() { networks.add(jsonNetwork); } - delete[] _jsonWifiNetworks; - size_t jsonBufferLength = json.measureLength() + 1; - _jsonWifiNetworks = new char[jsonBufferLength]; - json.printTo(_jsonWifiNetworks, jsonBufferLength); -} + jsonLength++; // \0 -void BootConfig::_onCaptivePortal() { - String host = _http.hostHeader(); - if (host && !host.equalsIgnoreCase(F("homie.config"))) { - // redirect unknown host requests to self if not connected to Internet yet - if (!_proxyEnabled) { - _interface->logger->logln(F("Received captive portal request")); - // Catch any captive portal probe. - // Every browser brand uses a different URL for this purpose - // We MUST redirect all them to local webserver to prevent cache poisoning - _http.sendHeader(F("Location"), F("http://homie.config/")); - _http.send(302, F("text/plain"), F("")); - // perform transparent proxy to Internet if connected - } else { - _proxyHttpRequest(); - } - } else if (_http.uri() != "/" || !SPIFFS.exists(CONFIG_UI_BUNDLE_PATH)) { - _interface->logger->logln(F("Received not found request")); - _http.send(404, F("text/plain"), F("UI bundle not loaded. See Configuration API usage: http://marvinroger.viewdocs.io/homie-esp8266/6.-Configuration-API")); - } else { - _interface->logger->logln(F("Received UI request")); - File file = SPIFFS.open(CONFIG_UI_BUNDLE_PATH, "r"); - _http.streamFile(file, F("text/html")); - file.close(); - } -} - -void BootConfig::_proxyHttpRequest() { - _interface->logger->logln(F("Received transparent proxy request")); - - // send request to destination (as in incoming host header) - _httpClient.setUserAgent("ESP8266-Homie"); - _httpClient.begin("http://" + _http.hostHeader() + _http.uri()); - // copy headers - for (int i = 0; i < _http.headers(); i++) { - _httpClient.addHeader(_http.headerName(i), _http.header(i)); - } - - String method = ""; - switch (_http.method()) { - case HTTP_GET: method = "GET"; break; - case HTTP_PUT: method = "PUT"; break; - case HTTP_POST: method = "POST"; break; - case HTTP_DELETE: method = "DELETE"; break; - case HTTP_OPTIONS: method = "OPTIONS"; break; - default: break; - } - - _interface->logger->logln(F("Proxy sent request to destination")); - int _httpCode = _httpClient.sendRequest(method.c_str(), _http.arg("plain")); - _interface->logger->log(F("Destination response code=")); - _interface->logger->logln(_httpCode); - - // bridge response to browser - // copy response headers - for (int i = 0; i < _httpClient.headers(); i++) { - _http.sendHeader(_httpClient.headerName(i), _httpClient.header(i), false); - } - _interface->logger->logln(F("Bridging received destination contents to client")); - _http.send(_httpCode, _httpClient.header("Content-Type"), _httpClient.getString()); - _httpClient.end(); + delete[] this->_jsonWifiNetworks; + this->_jsonWifiNetworks = new char[jsonLength]; + json.printTo(this->_jsonWifiNetworks, jsonLength); } void BootConfig::_onDeviceInfoRequest() { - _interface->logger->logln(F("Received device info request")); - auto numSettings = IHomieSetting::settings.size(); - auto numNodes = HomieNode::nodes.size(); - DynamicJsonBuffer jsonBuffer = DynamicJsonBuffer(JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(2) + JSON_ARRAY_SIZE(numNodes) + (numNodes * JSON_OBJECT_SIZE(2)) + JSON_ARRAY_SIZE(numSettings) + (numSettings * JSON_OBJECT_SIZE(5))); + this->_interface->logger->logln(F("Received device info request")); + + DynamicJsonBuffer jsonBuffer = DynamicJsonBuffer(JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(2) + JSON_ARRAY_SIZE(this->_interface->registeredNodesCount) + (this->_interface->registeredNodesCount * JSON_OBJECT_SIZE(2))); + int jsonLength = 82; // {"device_id":"","homie_version":"","firmware":{"name":"","version":""},"nodes":[]} JsonObject& json = jsonBuffer.createObject(); + jsonLength += strlen(Helpers::getDeviceId()); json["device_id"] = Helpers::getDeviceId(); - json["homie_esp8266_version"] = HOMIE_ESP8266_VERSION; + jsonLength += strlen(VERSION); + json["homie_version"] = VERSION; JsonObject& firmware = json.createNestedObject("firmware"); - firmware["name"] = _interface->firmware.name; - firmware["version"] = _interface->firmware.version; + jsonLength += strlen(this->_interface->firmware.name); + firmware["name"] = this->_interface->firmware.name; + jsonLength += strlen(this->_interface->firmware.version); + firmware["version"] = this->_interface->firmware.version; JsonArray& nodes = json.createNestedArray("nodes"); - for (HomieNode* iNode : HomieNode::nodes) { + for (int i = 0; i < this->_interface->registeredNodesCount; i++) { + jsonLength += 20; // {"id":"","type":""}, + const HomieNode* node = this->_interface->registeredNodes[i]; JsonObject& jsonNode = jsonBuffer.createObject(); - jsonNode["id"] = iNode->getId(); - jsonNode["type"] = iNode->getType(); + jsonLength += strlen(node->getId()); + jsonNode["id"] = node->getId(); + jsonLength += strlen(node->getType()); + jsonNode["type"] = node->getType(); + nodes.add(jsonNode); } - JsonArray& settings = json.createNestedArray("settings"); - for (IHomieSetting* iSetting : IHomieSetting::settings) { - JsonObject& jsonSetting = jsonBuffer.createObject(); - if (iSetting->isBool()) { - HomieSetting* setting = static_cast*>(iSetting); - jsonSetting["name"] = setting->getName(); - jsonSetting["description"] = setting->getDescription(); - jsonSetting["type"] = "bool"; - jsonSetting["required"] = setting->isRequired(); - if (!setting->isRequired()) { - jsonSetting["default"] = setting->get(); - } - } else if (iSetting->isUnsignedLong()) { - HomieSetting* setting = static_cast*>(iSetting); - jsonSetting["name"] = setting->getName(); - jsonSetting["description"] = setting->getDescription(); - jsonSetting["type"] = "ulong"; - jsonSetting["required"] = setting->isRequired(); - if (!setting->isRequired()) { - jsonSetting["default"] = setting->get(); - } - } else if (iSetting->isLong()) { - HomieSetting* setting = static_cast*>(iSetting); - jsonSetting["name"] = setting->getName(); - jsonSetting["description"] = setting->getDescription(); - jsonSetting["type"] = "long"; - jsonSetting["required"] = setting->isRequired(); - if (!setting->isRequired()) { - jsonSetting["default"] = setting->get(); - } - } else if (iSetting->isDouble()) { - HomieSetting* setting = static_cast*>(iSetting); - jsonSetting["name"] = setting->getName(); - jsonSetting["description"] = setting->getDescription(); - jsonSetting["type"] = "double"; - jsonSetting["required"] = setting->isRequired(); - if (!setting->isRequired()) { - jsonSetting["default"] = setting->get(); - } - } else if (iSetting->isConstChar()) { - HomieSetting* setting = static_cast*>(iSetting); - jsonSetting["name"] = setting->getName(); - jsonSetting["description"] = setting->getDescription(); - jsonSetting["type"] = "string"; - jsonSetting["required"] = setting->isRequired(); - if (!setting->isRequired()) { - jsonSetting["default"] = setting->get(); - } - } - - settings.add(jsonSetting); - } + jsonLength++; // \0 - size_t jsonBufferLength = json.measureLength() + 1; - std::unique_ptr jsonString(new char[jsonBufferLength]); - json.printTo(jsonString.get(), jsonBufferLength); - _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), jsonString.get()); + std::unique_ptr jsonString(new char[jsonLength]); + json.printTo(jsonString.get(), jsonLength); + this->_http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), jsonString.get()); } void BootConfig::_onNetworksRequest() { - _interface->logger->logln(F("Received networks request")); - if (_wifiScanAvailable) { - _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), _jsonWifiNetworks); + this->_interface->logger->logln(F("Received networks request")); + if (this->_wifiScanAvailable) { + this->_http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), this->_jsonWifiNetworks); } else { - _http.send(503, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_NETWORKS_FAILURE)); + this->_http.send(503, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_NETWORKS_FAILURE)); } } void BootConfig::_onConfigRequest() { - _interface->logger->logln(F("Received config request")); - if (_flaggedForReboot) { - _interface->logger->logln(F("✖ Device already configured")); + this->_interface->logger->logln(F("Received config request")); + if (this->_flaggedForReboot) { + this->_interface->logger->logln(F("✖ Device already configured")); String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); errorJson.concat(F("Device already configured\"}")); - _http.send(403, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); + this->_http.send(403, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); return; } StaticJsonBuffer parseJsonBuffer; - char* bodyCharArray = strdup(_http.arg("plain").c_str()); - JsonObject& parsedJson = parseJsonBuffer.parseObject(bodyCharArray); // do not use plain String, else fails + char* bodyCharArray = strdup(this->_http.arg("plain").c_str()); + JsonObject& parsedJson = parseJsonBuffer.parseObject(bodyCharArray); // do not use plain String, else fails if (!parsedJson.success()) { - free(bodyCharArray); - _interface->logger->logln(F("✖ Invalid or too big JSON")); + this->_interface->logger->logln(F("✖ Invalid or too big JSON")); String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); errorJson.concat(F("Invalid or too big JSON\"}")); - _http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); + this->_http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); return; } ConfigValidationResult configValidationResult = Helpers::validateConfig(parsedJson); + free(bodyCharArray); if (!configValidationResult.valid) { - free(bodyCharArray); - _interface->logger->log(F("✖ Config file is not valid, reason: ")); - _interface->logger->logln(configValidationResult.reason); + this->_interface->logger->log(F("✖ Config file is not valid, reason: ")); + this->_interface->logger->logln(configValidationResult.reason); String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); errorJson.concat(F("Config file is not valid, reason: ")); errorJson.concat(configValidationResult.reason); errorJson.concat(F("\"}")); - _http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); + this->_http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); return; } - free(bodyCharArray); - _interface->config->write(_http.arg("plain").c_str()); + this->_interface->config->write(this->_http.arg("plain")); - _interface->logger->logln(F("✔ Configured")); + this->_interface->logger->logln(F("✔ Configured")); - _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), F("{\"success\":true}")); + this->_http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), F("{\"success\":true}")); - _flaggedForReboot = true; // We don't reboot immediately, otherwise the response above is not sent - _flaggedForRebootAt = millis(); + this->_flaggedForReboot = true; // We don't reboot immediately, otherwise the response above is not sent + this->_flaggedForRebootAt = millis(); } void BootConfig::loop() { Boot::loop(); - _dns.processNextRequest(); - _http.handleClient(); + this->_dns.processNextRequest(); + this->_http.handleClient(); - if (_flaggedForReboot) { - if (millis() - _flaggedForRebootAt >= 3000UL) { - _interface->logger->logln(F("↻ Rebooting into normal mode...")); - _interface->logger->flush(); + if (this->_flaggedForReboot) { + if (millis() - this->_flaggedForRebootAt >= 3000UL) { + this->_interface->logger->logln(F("↻ Rebooting into normal mode...")); ESP.restart(); } return; } - if (!_lastWifiScanEnded) { + if (!this->_lastWifiScanEnded) { int8_t scanResult = WiFi.scanComplete(); switch (scanResult) { case WIFI_SCAN_RUNNING: return; case WIFI_SCAN_FAILED: - _interface->logger->logln(F("✖ Wi-Fi scan failed")); - _ssidCount = 0; - _wifiScanTimer.reset(); + this->_interface->logger->logln(F("✖ Wi-Fi scan failed")); + this->_ssidCount = 0; + this->_wifiScanTimer.reset(); break; default: - _interface->logger->logln(F("✔ Wi-Fi scan completed")); - _ssidCount = scanResult; - _generateNetworksJson(); - _wifiScanAvailable = true; + this->_interface->logger->logln(F("✔ Wi-Fi scan completed")); + this->_ssidCount = scanResult; + this->_generateNetworksJson(); + this->_wifiScanAvailable = true; break; } - _lastWifiScanEnded = true; + this->_lastWifiScanEnded = true; } - if (_lastWifiScanEnded && _wifiScanTimer.check()) { - _interface->logger->logln(F("Triggering Wi-Fi scan...")); + if (this->_lastWifiScanEnded && this->_wifiScanTimer.check()) { + this->_interface->logger->logln(F("Triggering Wi-Fi scan...")); WiFi.scanNetworks(true); - _wifiScanTimer.tick(); - _lastWifiScanEnded = false; + this->_wifiScanTimer.tick(); + this->_lastWifiScanEnded = false; } } diff --git a/src/Homie/Boot/BootConfig.hpp b/src/Homie/Boot/BootConfig.hpp index 06f631e0..63764c86 100644 --- a/src/Homie/Boot/BootConfig.hpp +++ b/src/Homie/Boot/BootConfig.hpp @@ -1,11 +1,8 @@ #pragma once -#include "Arduino.h" - #include #include #include -#include #include #include #include "Boot.hpp" @@ -17,37 +14,28 @@ #include "../Helpers.hpp" #include "../Logger.hpp" #include "../Strings.hpp" -#include "../../HomieSetting.hpp" namespace HomieInternals { -class BootConfig : public Boot { - public: - BootConfig(); - ~BootConfig(); - void setup(); - void loop(); - - private: - HTTPClient _httpClient; - ESP8266WebServer _http; - DNSServer _dns; - uint8_t _ssidCount; - bool _wifiScanAvailable; - Timer _wifiScanTimer; - bool _lastWifiScanEnded; - char* _jsonWifiNetworks; - bool _flaggedForReboot; - uint32_t _flaggedForRebootAt; - bool _proxyEnabled; + class BootConfig : public Boot { + public: + BootConfig(); + ~BootConfig(); + void setup(); + void loop(); + private: + ESP8266WebServer _http; + DNSServer _dns; + unsigned char _ssidCount; + bool _wifiScanAvailable; + Timer _wifiScanTimer; + bool _lastWifiScanEnded; + char* _jsonWifiNetworks; + bool _flaggedForReboot; + unsigned long _flaggedForRebootAt; - void _onCaptivePortal(); - void _onDeviceInfoRequest(); - void _onNetworksRequest(); - void _onConfigRequest(); - void _generateNetworksJson(); - void _onWifiConnectRequest(); - void _onProxyControlRequest(); - void _proxyHttpRequest(); - void _onWifiStatusRequest(); -}; -} // namespace HomieInternals + void _onDeviceInfoRequest(); + void _onNetworksRequest(); + void _onConfigRequest(); + void _generateNetworksJson(); + }; +} diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index c7232686..55dcc833 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -5,115 +5,146 @@ using namespace HomieInternals; BootNormal::BootNormal() : Boot("normal") , _setupFunctionCalled(false) +, _wifiConnectNotified(false) +, _wifiDisconnectNotified(true) +, _mqttConnectNotified(false) , _mqttDisconnectNotified(true) +, _otaVersion {'\0'} , _flaggedForOta(false) , _flaggedForReset(false) -, _flaggedForReboot(false) -, _mqttTopic(nullptr) -, _mqttClientId(nullptr) -, _mqttWillTopic(nullptr) -, _mqttPayloadBuffer(nullptr) -, _flaggedForSleep(false) -, _mqttOfflineMessageId(0) { - _signalQualityTimer.setInterval(SIGNAL_QUALITY_SEND_INTERVAL); - _uptimeTimer.setInterval(UPTIME_SEND_INTERVAL); +{ + this->_wifiReconnectTimer.setInterval(WIFI_RECONNECT_INTERVAL); + this->_mqttReconnectTimer.setInterval(MQTT_RECONNECT_INTERVAL); + this->_signalQualityTimer.setInterval(SIGNAL_QUALITY_SEND_INTERVAL); + this->_uptimeTimer.setInterval(UPTIME_SEND_INTERVAL); } BootNormal::~BootNormal() { } -void BootNormal::_prefixMqttTopic() { - strcpy(_mqttTopic.get(), _interface->config->get().mqtt.baseTopic); - strcat(_mqttTopic.get(), _interface->config->get().deviceId); +void BootNormal::_fillMqttTopic(PGM_P topic) { + strcpy(this->_interface->mqttClient->getTopicBuffer(), this->_interface->config->get().mqtt.baseTopic); + strcat(this->_interface->mqttClient->getTopicBuffer(), this->_interface->config->get().deviceId); + strcat_P(this->_interface->mqttClient->getTopicBuffer(), topic); } -char* BootNormal::_prefixMqttTopic(PGM_P topic) { - _prefixMqttTopic(); - strcat_P(_mqttTopic.get(), topic); +bool BootNormal::_publishRetainedOrFail(const char* message) { + if (!this->_interface->mqttClient->publish(message, true)) { + this->_interface->mqttClient->disconnect(); + this->_interface->logger->logln(F(" Failed")); + return false; + } - return _mqttTopic.get(); + return true; } -void BootNormal::_wifiConnect() { - if (_interface->led.enabled) _interface->blinker->start(LED_WIFI_DELAY); - _interface->logger->logln(F("↕ Attempting to connect to Wi-Fi...")); - - if (WiFi.getMode() != WIFI_STA) WiFi.mode(WIFI_STA); - - WiFi.hostname(_interface->config->get().deviceId); - - if (WiFi.SSID() != _interface->config->get().wifi.ssid || WiFi.psk() != _interface->config->get().wifi.password) { - WiFi.begin(_interface->config->get().wifi.ssid, _interface->config->get().wifi.password); - WiFi.setAutoConnect(true); - WiFi.setAutoReconnect(true); +bool BootNormal::_subscribe1OrFail() { + if (!this->_interface->mqttClient->subscribe(1)) { + this->_interface->mqttClient->disconnect(); + this->_interface->logger->logln(F(" Failed")); + return false; } -} - -void BootNormal::_onWifiGotIp(const WiFiEventStationModeGotIP& event) { - if (_interface->led.enabled) _interface->blinker->stop(); - _interface->logger->logln(F("✔ Wi-Fi connected")); - _interface->logger->logln(F("Triggering WIFI_CONNECTED event...")); - _interface->eventHandler(HomieEvent::WIFI_CONNECTED); - MDNS.begin(_interface->config->get().deviceId); - _mqttConnect(); + return true; } -void BootNormal::_onWifiDisconnected(const WiFiEventStationModeDisconnected& event) { - _interface->connected = false; - if (_interface->led.enabled) _interface->blinker->start(LED_WIFI_DELAY); - _uptimeTimer.reset(); - _signalQualityTimer.reset(); - _interface->logger->logln(F("✖ Wi-Fi disconnected")); - _interface->logger->logln(F("Triggering WIFI_DISCONNECTED event...")); - _interface->eventHandler(HomieEvent::WIFI_DISCONNECTED); - - if (!_flaggedForSleep) { - _wifiConnect(); - } +void BootNormal::_wifiConnect() { + WiFi.mode(WIFI_STA); + WiFi.begin(this->_interface->config->get().wifi.ssid, this->_interface->config->get().wifi.password); } void BootNormal::_mqttConnect() { - if (_interface->led.enabled) _interface->blinker->start(LED_MQTT_DELAY); - _interface->logger->logln(F("↕ Attempting to connect to MQTT...")); - _interface->mqttClient->connect(); -} + const char* host = this->_interface->config->get().mqtt.server.host; + unsigned int port = this->_interface->config->get().mqtt.server.port; + if (this->_interface->config->get().mqtt.server.mdns.enabled) { + this->_interface->logger->log(F("Querying mDNS service ")); + this->_interface->logger->logln(this->_interface->config->get().mqtt.server.mdns.service); + MdnsQueryResult result = Helpers::mdnsQuery(this->_interface->config->get().mqtt.server.mdns.service); + if (result.success) { + host = result.ip.toString().c_str(); + port = result.port; + this->_interface->logger->log(F("✔ ")); + this->_interface->logger->log(F(" service found at ")); + this->_interface->logger->log(host); + this->_interface->logger->log(F(":")); + this->_interface->logger->logln(port); + } else { + this->_interface->logger->logln(F("✖ Service not found")); + return; + } + } -void BootNormal::_onMqttConnected() { - _mqttDisconnectNotified = false; - _interface->logger->logln(F("Sending initial information...")); - - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$homie")), 1, true, HOMIE_VERSION); - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$implementation")), 1, true, "esp8266"); - - for (HomieNode* iNode : HomieNode::nodes) { - std::unique_ptr subtopic = std::unique_ptr(new char[1 + strlen(iNode->getId()) + 12 + 1]); // /id/$properties - strcpy_P(subtopic.get(), PSTR("/")); - strcat(subtopic.get(), iNode->getId()); - strcat_P(subtopic.get(), PSTR("/$type")); - _interface->mqttClient->publish(_prefixMqttTopic(subtopic.get()), 1, true, iNode->getType()); - - strcpy_P(subtopic.get(), PSTR("/")); - strcat(subtopic.get(), iNode->getId()); - strcat_P(subtopic.get(), PSTR("/$properties")); - String properties; - for (Property* iProperty : iNode->getProperties()) { - properties.concat(iProperty->getProperty()); - if (iProperty->isRange()) { - properties.concat("["); - properties.concat(iProperty->getLower()); - properties.concat("-"); - properties.concat(iProperty->getUpper()); - properties.concat("]"); - } - if (iProperty->isSettable()) properties.concat(":settable"); - properties.concat(","); + this->_interface->mqttClient->setServer(host, port, this->_interface->config->get().mqtt.server.ssl.fingerprint); + this->_interface->mqttClient->setCallback(std::bind(&BootNormal::_mqttCallback, this, std::placeholders::_1, std::placeholders::_2)); + + char clientId[MAX_WIFI_SSID_LENGTH]; + strcpy(clientId, this->_interface->brand); + strcat_P(clientId, PSTR("-")); + strcat(clientId, this->_interface->config->get().deviceId); + + this->_fillMqttTopic(PSTR("/$online")); + if (this->_interface->mqttClient->connect(clientId, "false", 2, true, this->_interface->config->get().mqtt.auth, this->_interface->config->get().mqtt.username, this->_interface->config->get().mqtt.password)) { + this->_interface->logger->logln(F("Connected")); + this->_mqttSetup(); + } else { + this->_interface->logger->log(F("✖ Cannot connect, reason: ")); + switch (this->_interface->mqttClient->getState()) { + case MQTT_CONNECTION_TIMEOUT: + this->_interface->logger->logln(F("MQTT_CONNECTION_TIMEOUT")); + break; + case MQTT_CONNECTION_LOST: + this->_interface->logger->logln(F("MQTT_CONNECTION_LOST")); + break; + case MQTT_CONNECT_FAILED: + this->_interface->logger->logln(F("MQTT_CONNECT_FAILED")); + break; + case MQTT_DISCONNECTED: + this->_interface->logger->logln(F("MQTT_DISCONNECTED")); + break; + case MQTT_CONNECTED: + this->_interface->logger->logln(F("MQTT_CONNECTED (?)")); + break; + case MQTT_CONNECT_BAD_PROTOCOL: + this->_interface->logger->logln(F("MQTT_CONNECT_BAD_PROTOCOL")); + break; + case MQTT_CONNECT_BAD_CLIENT_ID: + this->_interface->logger->logln(F("MQTT_CONNECT_BAD_CLIENT_ID")); + break; + case MQTT_CONNECT_UNAVAILABLE: + this->_interface->logger->logln(F("MQTT_CONNECT_UNAVAILABLE")); + break; + case MQTT_CONNECT_BAD_CREDENTIALS: + this->_interface->logger->logln(F("MQTT_CONNECT_BAD_CREDENTIALS")); + break; + case MQTT_CONNECT_UNAUTHORIZED: + this->_interface->logger->logln(F("MQTT_CONNECT_UNAUTHORIZED")); + break; + default: + this->_interface->logger->logln(F("UNKNOWN")); } - if (iNode->getProperties().size() >= 1) properties.remove(properties.length() - 1); - _interface->mqttClient->publish(_prefixMqttTopic(subtopic.get()), 1, true, properties.c_str()); } +} + +void BootNormal::_mqttSetup() { + this->_interface->logger->log(F("Sending initial information... ")); + + this->_fillMqttTopic(PSTR("/$online")); + if (!this->_publishRetainedOrFail("true")) return; + + char nodes[MAX_REGISTERED_NODES_COUNT * (MAX_NODE_ID_LENGTH + 1 + MAX_NODE_ID_LENGTH + 1) - 1]; + strcpy_P(nodes, PSTR("")); + for (int i = 0; i < this->_interface->registeredNodesCount; i++) { + const HomieNode* node = this->_interface->registeredNodes[i]; + strcat(nodes, node->getId()); + strcat_P(nodes, PSTR(":")); + strcat(nodes, node->getType()); + if (i != this->_interface->registeredNodesCount - 1) strcat_P(nodes, PSTR(",")); + } + this->_fillMqttTopic(PSTR("/$nodes")); + if (!this->_publishRetainedOrFail(nodes)) return; - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$name")), 1, true, _interface->config->get().name); + this->_fillMqttTopic(PSTR("/$name")); + if (!this->_publishRetainedOrFail(this->_interface->config->get().name)) return; IPAddress localIp = WiFi.localIP(); char localIpStr[15 + 1]; @@ -129,412 +160,322 @@ void BootNormal::_onMqttConnected() { strcat_P(localIpStr, PSTR(".")); itoa(localIp[3], localIpPartStr, 10); strcat(localIpStr, localIpPartStr); - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$localip")), 1, true, localIpStr); + this->_fillMqttTopic(PSTR("/$localip")); + if (!this->_publishRetainedOrFail(localIpStr)) return; - char uptimeIntervalStr[3 + 1]; - itoa(UPTIME_SEND_INTERVAL / 1000, uptimeIntervalStr, 10); - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$uptime/interval")), 1, true, uptimeIntervalStr); - - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$fw/name")), 1, true, _interface->firmware.name); - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$fw/version")), 1, true, _interface->firmware.version); - - _interface->mqttClient->subscribe(_prefixMqttTopic(PSTR("/+/+/set")), 2); - - /* Implementation specific */ - - char* safeConfigFile = _interface->config->getSafeConfigFile(); - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$implementation/config")), 1, true, safeConfigFile); - free(safeConfigFile); - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$implementation/version")), 1, true, HOMIE_ESP8266_VERSION); - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$implementation/ota/enabled")), 1, true, _interface->config->get().ota.enabled ? "true" : "false"); - _interface->mqttClient->subscribe(_prefixMqttTopic(PSTR("/$implementation/reset")), 2); - _interface->mqttClient->subscribe(_prefixMqttTopic(PSTR("/$implementation/config/set")), 2); - - if (_interface->config->get().ota.enabled) { - _interface->mqttClient->subscribe(_prefixMqttTopic(PSTR("/$ota")), 2); - } + this->_fillMqttTopic(PSTR("/$fwname")); + if (!this->_publishRetainedOrFail(this->_interface->firmware.name)) return; - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$online")), 1, true, "true"); + this->_fillMqttTopic(PSTR("/$fwversion")); + if (!this->_publishRetainedOrFail(this->_interface->firmware.version)) return; - _interface->connected = true; - if (_interface->led.enabled) _interface->blinker->stop(); + this->_interface->logger->logln(F(" OK")); - _interface->logger->logln(F("✔ MQTT ready")); - _interface->logger->logln(F("Triggering MQTT_CONNECTED event...")); - _interface->eventHandler(HomieEvent::MQTT_CONNECTED); + this->_fillMqttTopic(PSTR("/+/+/set")); + this->_interface->logger->log(F("Subscribing to topics... ")); + if (!this->_subscribe1OrFail()) return; - for (HomieNode* iNode : HomieNode::nodes) { - iNode->onReadyToOperate(); - } + this->_fillMqttTopic(PSTR("/$reset")); + if (!this->_subscribe1OrFail()) return; - if (!_setupFunctionCalled) { - _interface->logger->logln(F("Calling setup function...")); - _interface->setupFunction(); - _setupFunctionCalled = true; + if (this->_interface->config->get().ota.enabled) { + this->_fillMqttTopic(PSTR("/$ota")); + if (!this->_subscribe1OrFail()) return; } -} -void BootNormal::_onMqttDisconnected(AsyncMqttClientDisconnectReason reason) { - _interface->connected = false; - if (!_mqttDisconnectNotified) { - _uptimeTimer.reset(); - _signalQualityTimer.reset(); - _interface->logger->logln(F("✖ MQTT disconnected")); - _interface->logger->logln(F("Triggering MQTT_DISCONNECTED event...")); - _interface->eventHandler(HomieEvent::MQTT_DISCONNECTED); - if (_flaggedForSleep) { - _interface->logger->logln(F("Triggering READY_FOR_SLEEP event...")); - _interface->eventHandler(HomieEvent::READY_FOR_SLEEP); - } - _mqttDisconnectNotified = true; - } - if (!_flaggedForSleep) { - _mqttConnect(); - } + this->_interface->logger->logln(F(" OK")); } -void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { - if (total == 0) return; // no empty message possible - - topic = topic + strlen(_interface->config->get().mqtt.baseTopic) + strlen(_interface->config->get().deviceId) + 1; // Remove devices/${id}/ --- +1 for / - - if (strcmp_P(topic, PSTR("$implementation/ota/payload")) == 0) { // If this is the $ota payload - if (_flaggedForOta) { - if (index == 0) { - Update.begin(total); - _interface->logger->logln(F("OTA started")); - _interface->logger->logln(F("Triggering OTA_STARTED event...")); - _interface->eventHandler(HomieEvent::OTA_STARTED); +void BootNormal::_mqttCallback(char* topic, char* payload) { + String message = String(payload); + String unified = String(topic); + unified.remove(0, strlen(this->_interface->config->get().mqtt.baseTopic) + strlen(this->_interface->config->get().deviceId) + 1); // Remove devices/${id}/ --- +1 for / + + // Device properties + if (this->_interface->config->get().ota.enabled && unified == "$ota") { + if (message != this->_interface->firmware.version) { + this->_interface->logger->log(F("✴ OTA available (version ")); + this->_interface->logger->log(message); + this->_interface->logger->logln(F(")")); + if (strlen(payload) + 1 <= MAX_FIRMWARE_VERSION_LENGTH) { + strcpy(this->_otaVersion, payload); + this->_flaggedForOta = true; + this->_interface->logger->logln(F("Flagged for OTA")); + } else { + this->_interface->logger->logln(F("Version string received is too long")); } - _interface->logger->log(F("Receiving OTA payload (")); - _interface->logger->log(index + len); - _interface->logger->log(F("/")); - _interface->logger->log(total); - _interface->logger->logln(F(")...")); - - Update.write(reinterpret_cast(payload), len); - - if (index + len == total) { - bool success = Update.end(); - - if (success) { - _interface->logger->logln(F("✔ OTA success")); - _interface->logger->logln(F("Triggering OTA_SUCCESSFUL event...")); - _interface->eventHandler(HomieEvent::OTA_SUCCESSFUL); - _flaggedForReboot = true; - } else { - _interface->logger->logln(F("✖ OTA failed")); - _interface->logger->logln(F("Triggering OTA_FAILED event...")); - _interface->eventHandler(HomieEvent::OTA_FAILED); - } - - _flaggedForOta = false; - _interface->mqttClient->unsubscribe(_prefixMqttTopic(PSTR("/$implementation/ota/payload"))); - } - } else { - _interface->logger->log(F("Receiving OTA payload but not requested, skipping...")); } return; - } - - if (_mqttPayloadBuffer == nullptr) _mqttPayloadBuffer = std::unique_ptr(new char[total + 1]); - - memcpy(_mqttPayloadBuffer.get() + index, payload, len); - - if (index + len != total) return; - _mqttPayloadBuffer.get()[total] = '\0'; - - if (strcmp_P(topic, PSTR("$ota")) == 0) { // If this is the $ota announcement - if (strcmp(_mqttPayloadBuffer.get(), _interface->firmware.version) != 0) { - _interface->logger->log(F("✴ OTA available (version ")); - _interface->logger->log(_mqttPayloadBuffer.get()); - _interface->logger->logln(F(")")); - - _interface->logger->logln(F("Subscribing to OTA payload...")); - _interface->mqttClient->subscribe(_prefixMqttTopic(PSTR("/$implementation/ota/payload")), 0); - _flaggedForOta = true; - } - - return; - } - - if (strcmp_P(topic, PSTR("$implementation/reset")) == 0 && strcmp(_mqttPayloadBuffer.get(), "true") == 0) { - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$implementation/reset")), 1, true, "false"); - _flaggedForReset = true; - _interface->logger->logln(F("Flagged for reset by network")); - return; - } - - if (strcmp_P(topic, PSTR("$implementation/config/set")) == 0) { - if (_interface->config->patch(_mqttPayloadBuffer.get())) { - _interface->logger->logln(F("✔ Configuration updated")); - _flaggedForReboot = true; - _interface->logger->logln(F("Flagged for reboot")); - } else { - _interface->logger->logln(F("✖ Configuration not updated")); - } + } else if (unified == "$reset" && message == "true") { + this->_fillMqttTopic(PSTR("/$reset")); + this->_interface->mqttClient->publish("false", true); + this->_flaggedForReset = true; + this->_interface->logger->logln(F("Flagged for reset by network")); return; } // Implicit node properties - topic[strlen(topic) - 4] = '\0'; // Remove /set - uint16_t separator = 0; - for (uint16_t i = 0; i < strlen(topic); i++) { - if (topic[i] == '/') { + unified.remove(unified.length() - 4, 4); // Remove /set + unsigned int separator = 0; + for (unsigned int i = 0; i < unified.length(); i++) { + if (unified.charAt(i) == '/') { separator = i; break; } } - char* node = topic; - node[separator] = '\0'; - char* property = topic + separator + 1; - HomieNode* homieNode = HomieNode::find(node); - if (!homieNode) { - _interface->logger->log(F("Node ")); - _interface->logger->log(node); - _interface->logger->logln(F(" not registered")); - return; - } - - int16_t rangeSeparator = -1; - for (uint16_t i = 0; i < strlen(property); i++) { - if (property[i] == '_') { - rangeSeparator = i; + String node = unified.substring(0, separator); + String property = unified.substring(separator + 1); + + int homieNodeIndex = -1; + for (int i = 0; i < this->_interface->registeredNodesCount; i++) { + const HomieNode* homieNode = this->_interface->registeredNodes[i]; + if (node == homieNode->getId()) { + homieNodeIndex = i; break; } } - bool isRange = false; - uint16_t rangeIndex = 0; - if (rangeSeparator != -1) { - isRange = true; - property[rangeSeparator] = '\0'; - char* rangeIndexStr = property + rangeSeparator + 1; - String rangeIndexTest = String(rangeIndexStr); - for (uint8_t i = 0; i < rangeIndexTest.length(); i++) { - if (!isDigit(rangeIndexTest.charAt(i))) { - _interface->logger->log(F("Range index ")); - _interface->logger->log(rangeIndexStr); - _interface->logger->logln(F(" is not valid")); - return; - } - } - rangeIndex = rangeIndexTest.toInt(); + + if (homieNodeIndex == -1) { + this->_interface->logger->log(F("Node ")); + this->_interface->logger->log(node); + this->_interface->logger->logln(F(" not registered")); + return; } - Property* propertyObject = nullptr; - for (Property* iProperty : homieNode->getProperties()) { - if (isRange) { - if (iProperty->isRange() && strcmp(property, iProperty->getProperty()) == 0) { - if (rangeIndex >= iProperty->getLower() && rangeIndex <= iProperty->getUpper()) { - propertyObject = iProperty; - break; - } else { - _interface->logger->log(F("Range index ")); - _interface->logger->log(rangeIndex); - _interface->logger->log(F(" is not within the bounds of ")); - _interface->logger->logln(property); - return; - } - } - } else if (strcmp(property, iProperty->getProperty()) == 0) { - propertyObject = iProperty; + const HomieNode* homieNode = this->_interface->registeredNodes[homieNodeIndex]; + + int homieNodePropertyIndex = -1; + for (int i = 0; i < homieNode->getSubscriptionsCount(); i++) { + Subscription subscription = homieNode->getSubscriptions()[i]; + if (property == subscription.property) { + homieNodePropertyIndex = i; break; } } - if (!propertyObject || !propertyObject->isSettable()) { - _interface->logger->log(F("Node ")); - _interface->logger->log(node); - _interface->logger->log(F(":")); - _interface->logger->logln(property); - _interface->logger->log(F(" property not settable")); + if (!homieNode->getSubscribeToAll() && homieNodePropertyIndex == -1) { + this->_interface->logger->log(F("Node ")); + this->_interface->logger->log(node); + this->_interface->logger->log(F(" not subscribed to ")); + this->_interface->logger->logln(property); return; } - HomieRange range; - range.isRange = isRange; - range.index = rangeIndex; - - _interface->logger->logln(F("Calling global input handler...")); - bool handled = _interface->globalInputHandler(String(node), String(property), range, String(_mqttPayloadBuffer.get())); + this->_interface->logger->logln(F("Calling global input handler...")); + bool handled = this->_interface->globalInputHandler(node, property, message); if (handled) return; - _interface->logger->logln(F("Calling node input handler...")); - handled = homieNode->handleInput(String(property), range, String(_mqttPayloadBuffer.get())); + this->_interface->logger->logln(F("Calling node input handler...")); + handled = homieNode->getInputHandler()(property, message); if (handled) return; - _interface->logger->logln(F("Calling property input handler...")); - handled = propertyObject->getInputHandler()(range, String(_mqttPayloadBuffer.get())); - - if (!handled) { - _interface->logger->logln(F("No handlers handled the following packet:")); - _interface->logger->log(F(" • Node ID: ")); - _interface->logger->logln(node); - _interface->logger->log(F(" • Property: ")); - _interface->logger->logln(property); - _interface->logger->log(F(" • Is range? ")); - if (isRange) { - _interface->logger->log(F("yes (")); - _interface->logger->log(rangeIndex); - _interface->logger->logln(F(")")); - } else { - _interface->logger->logln(F("no")); - } - _interface->logger->log(F(" • Value: ")); - _interface->logger->logln(_mqttPayloadBuffer.get()); + if (homieNodePropertyIndex != -1) { // might not if subscribed to all only + Subscription homieNodeSubscription = homieNode->getSubscriptions()[homieNodePropertyIndex]; + this->_interface->logger->logln(F("Calling property input handler...")); + handled = homieNodeSubscription.inputHandler(message); } -} -void BootNormal::_onMqttPublish(uint16_t id) { - if (_flaggedForSleep && id == _mqttOfflineMessageId) { - _interface->logger->logln(F("Offline message acknowledged. Disconnecting MQTT...")); - _interface->mqttClient->disconnect(); + if (!handled){ + this->_interface->logger->logln(F("No handlers handled the following packet:")); + this->_interface->logger->log(F(" • Node ID: ")); + this->_interface->logger->logln(node); + this->_interface->logger->log(F(" • Property: ")); + this->_interface->logger->logln(property); + this->_interface->logger->log(F(" • Value: ")); + this->_interface->logger->logln(message); } } void BootNormal::_handleReset() { - if (_interface->reset.enabled) { - _resetDebouncer.update(); + if (this->_interface->reset.enabled) { + this->_resetDebouncer.update(); - if (_resetDebouncer.read() == _interface->reset.triggerState) { - _flaggedForReset = true; - _interface->logger->logln(F("Flagged for reset by pin")); + if (this->_resetDebouncer.read() == this->_interface->reset.triggerState) { + this->_flaggedForReset = true; + this->_interface->logger->logln(F("Flagged for reset by pin")); } } - if (_interface->reset.userFunction()) { - _flaggedForReset = true; - _interface->logger->logln(F("Flagged for reset by function")); + if (this->_interface->reset.userFunction()) { + this->_flaggedForReset = true; + this->_interface->logger->logln(F("Flagged for reset by function")); } } void BootNormal::setup() { Boot::setup(); - Update.runAsync(true); + this->_interface->mqttClient->initMqtt(this->_interface->config->get().mqtt.server.ssl.enabled); - if (_interface->led.enabled) _interface->blinker->start(LED_WIFI_DELAY); + if (this->_interface->reset.enabled) { + pinMode(this->_interface->reset.triggerPin, INPUT_PULLUP); - // Generate topic buffer - size_t baseTopicLength = strlen(_interface->config->get().mqtt.baseTopic) + strlen(_interface->config->get().deviceId); - size_t longestSubtopicLength = 28 + 1; // /$implementation/ota/enabled - for (HomieNode* iNode : HomieNode::nodes) { - size_t nodeMaxTopicLength = 1 + strlen(iNode->getId()) + 12 + 1; // /id/$properties - if (nodeMaxTopicLength > longestSubtopicLength) longestSubtopicLength = nodeMaxTopicLength; + this->_resetDebouncer.attach(this->_interface->reset.triggerPin); + this->_resetDebouncer.interval(this->_interface->reset.triggerTime); + } - for (Property* iProperty : iNode->getProperties()) { - size_t propertyMaxTopicLength = 1 + strlen(iNode->getId()) + 1 + strlen(iProperty->getProperty()) + 1; - if (iProperty->isSettable()) propertyMaxTopicLength += 4; // /set + this->_interface->config->log(); - if (propertyMaxTopicLength > longestSubtopicLength) longestSubtopicLength = propertyMaxTopicLength; + if (this->_interface->config->get().mqtt.server.ssl.enabled) { + this->_interface->logger->log(F("SSL enabled: pushing CPU frequency to 160MHz... ")); + if (system_update_cpu_freq(SYS_CPU_160MHZ)) { + this->_interface->logger->logln(F("OK")); + } else { + this->_interface->logger->logln(F("Failure")); + this->_interface->logger->logln(F("Rebooting...")); + ESP.restart(); } } - _mqttTopic = std::unique_ptr(new char[baseTopicLength + longestSubtopicLength]); - - _wifiGotIpHandler = WiFi.onStationModeGotIP(std::bind(&BootNormal::_onWifiGotIp, this, std::placeholders::_1)); - _wifiDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&BootNormal::_onWifiDisconnected, this, std::placeholders::_1)); - - _interface->mqttClient->onConnect(std::bind(&BootNormal::_onMqttConnected, this)); - _interface->mqttClient->onDisconnect(std::bind(&BootNormal::_onMqttDisconnected, this, std::placeholders::_1)); - _interface->mqttClient->onMessage(std::bind(&BootNormal::_onMqttMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); - _interface->mqttClient->onPublish(std::bind(&BootNormal::_onMqttPublish, this, std::placeholders::_1)); +} - _interface->mqttClient->setServer(_interface->config->get().mqtt.server.host, _interface->config->get().mqtt.server.port); - _interface->mqttClient->setKeepAlive(10); - _mqttClientId = std::unique_ptr(new char[strlen(_interface->brand) + 1 + strlen(_interface->config->get().deviceId) + 1]); - strcpy(_mqttClientId.get(), _interface->brand); - strcat_P(_mqttClientId.get(), PSTR("-")); - strcat(_mqttClientId.get(), _interface->config->get().deviceId); - _interface->mqttClient->setClientId(_mqttClientId.get()); - char* mqttWillTopic = _prefixMqttTopic(PSTR("/$online")); - _mqttWillTopic = std::unique_ptr(new char[strlen(mqttWillTopic) + 1]); - memcpy(_mqttWillTopic.get(), mqttWillTopic, strlen(mqttWillTopic) + 1); - _interface->mqttClient->setWill(_mqttWillTopic.get(), 1, true, "false"); +void BootNormal::loop() { + Boot::loop(); - if (_interface->config->get().mqtt.auth) _interface->mqttClient->setCredentials(_interface->config->get().mqtt.username, _interface->config->get().mqtt.password); + this->_handleReset(); + if (this->_flaggedForReset && this->_interface->reset.able) { + this->_interface->logger->logln(F("Device is in a resettable state")); + this->_interface->config->erase(); + this->_interface->logger->logln(F("Configuration erased")); - if (_interface->reset.enabled) { - pinMode(_interface->reset.triggerPin, INPUT_PULLUP); + this->_interface->logger->logln(F("Triggering HOMIE_ABOUT_TO_RESET event...")); + this->_interface->eventHandler(HOMIE_ABOUT_TO_RESET); - _resetDebouncer.attach(_interface->reset.triggerPin); - _resetDebouncer.interval(_interface->reset.triggerTime); + this->_interface->logger->logln(F("↻ Rebooting into config mode...")); + ESP.restart(); } - _interface->config->log(); + if (this->_flaggedForOta && this->_interface->reset.able) { + this->_interface->logger->logln(F("Device is in a resettable state")); + this->_interface->config->setOtaMode(true, this->_otaVersion); - for (HomieNode* iNode : HomieNode::nodes) { - iNode->setup(); + this->_interface->logger->logln(F("↻ Rebooting into OTA mode...")); + ESP.restart(); } - _wifiConnect(); -} - -void BootNormal::loop() { - Boot::loop(); + this->_interface->readyToOperate = false; + + if (WiFi.status() != WL_CONNECTED) { + this->_wifiConnectNotified = false; + if (!this->_wifiDisconnectNotified) { + this->_wifiReconnectTimer.reset(); + this->_uptimeTimer.reset(); + this->_signalQualityTimer.reset(); + this->_interface->logger->logln(F("✖ Wi-Fi disconnected")); + this->_interface->logger->logln(F("Triggering HOMIE_WIFI_DISCONNECTED event...")); + this->_interface->eventHandler(HOMIE_WIFI_DISCONNECTED); + this->_wifiDisconnectNotified = true; + } - _handleReset(); + if (this->_wifiReconnectTimer.check()) { + this->_interface->logger->logln(F("↕ Attempting to connect to Wi-Fi...")); + this->_wifiReconnectTimer.tick(); + if (this->_interface->led.enabled) { + this->_interface->blinker->start(LED_WIFI_DELAY); + } + this->_wifiConnect(); + } + return; + } - if (_flaggedForReset && _interface->reset.able) { - _interface->logger->logln(F("Device is in a resettable state")); - _interface->config->erase(); - _interface->logger->logln(F("Configuration erased")); + this->_wifiDisconnectNotified = false; + if (!this->_wifiConnectNotified) { + this->_interface->logger->logln(F("✔ Wi-Fi connected")); + this->_interface->logger->logln(F("Triggering HOMIE_WIFI_CONNECTED event...")); + this->_interface->eventHandler(HOMIE_WIFI_CONNECTED); + this->_wifiConnectNotified = true; + } - _interface->logger->logln(F("Triggering ABOUT_TO_RESET event...")); - _interface->eventHandler(HomieEvent::ABOUT_TO_RESET); + if (!this->_interface->mqttClient->connected()) { + this->_mqttConnectNotified = false; + if (!this->_mqttDisconnectNotified) { + this->_mqttReconnectTimer.reset(); + this->_uptimeTimer.reset(); + this->_signalQualityTimer.reset(); + this->_interface->logger->logln(F("✖ MQTT disconnected")); + this->_interface->logger->logln(F("Triggering HOMIE_MQTT_DISCONNECTED event...")); + this->_interface->eventHandler(HOMIE_MQTT_DISCONNECTED); + this->_mqttDisconnectNotified = true; + } - _interface->logger->logln(F("↻ Rebooting into config mode...")); - _interface->logger->flush(); - ESP.restart(); + if (this->_mqttReconnectTimer.check()) { + this->_interface->logger->logln(F("↕ Attempting to connect to MQTT...")); + this->_mqttReconnectTimer.tick(); + if (this->_interface->led.enabled) { + this->_interface->blinker->start(LED_MQTT_DELAY); + } + this->_mqttConnect(); + } + return; + } else { + if (this->_interface->led.enabled) { + this->_interface->blinker->stop(); + } } - if (_flaggedForReboot && _interface->reset.able) { - _interface->logger->logln(F("Device is in a resettable state")); + this->_interface->readyToOperate = true; - _interface->logger->logln(F("↻ Rebooting...")); - _interface->logger->flush(); - ESP.restart(); + this->_mqttDisconnectNotified = false; + if (!this->_mqttConnectNotified) { + this->_interface->logger->logln(F("✔ MQTT ready")); + this->_interface->logger->logln(F("Triggering HOMIE_MQTT_CONNECTED event...")); + this->_interface->eventHandler(HOMIE_MQTT_CONNECTED); + this->_mqttConnectNotified = true; } - if (!_interface->connected) return; + if (!this->_setupFunctionCalled) { + this->_interface->logger->logln(F("Calling setup function...")); + this->_interface->setupFunction(); + this->_setupFunctionCalled = true; + } - if (_signalQualityTimer.check()) { - uint8_t quality = Helpers::rssiToPercentage(WiFi.RSSI()); + if (this->_signalQualityTimer.check()) { + int32_t rssi = WiFi.RSSI(); + unsigned char quality; + if (rssi <= -100) { + quality = 0; + } else if (rssi >= -50) { + quality = 100; + } else { + quality = 2 * (rssi + 100); + } char qualityStr[3 + 1]; itoa(quality, qualityStr, 10); - _interface->logger->log(F("Sending Wi-Fi signal quality (")); - _interface->logger->log(qualityStr); - _interface->logger->logln(F("%)...")); + this->_interface->logger->log(F("Sending Wi-Fi signal quality (")); + this->_interface->logger->log(qualityStr); + this->_interface->logger->log(F("%)... ")); - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$signal")), 1, true, qualityStr); - _signalQualityTimer.tick(); + this->_fillMqttTopic(PSTR("/$signal")); + if (this->_interface->mqttClient->publish(qualityStr, true)) { + this->_interface->logger->logln(F(" OK")); + this->_signalQualityTimer.tick(); + } else { + this->_interface->logger->logln(F(" Failure")); + } } - if (_uptimeTimer.check()) { - _uptime.update(); + if (this->_uptimeTimer.check()) { + this->_uptime.update(); char uptimeStr[10 + 1]; - itoa(_uptime.getSeconds(), uptimeStr, 10); + itoa(this->_uptime.getSeconds(), uptimeStr, 10); - _interface->logger->log(F("Sending uptime (")); - _interface->logger->log(_uptime.getSeconds()); - _interface->logger->logln(F("s)...")); + this->_interface->logger->log(F("Sending uptime (")); + this->_interface->logger->log(this->_uptime.getSeconds()); + this->_interface->logger->log(F("s)... ")); - _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$uptime/value")), 1, true, uptimeStr); - _uptimeTimer.tick(); + this->_fillMqttTopic(PSTR("/$uptime")); + if (this->_interface->mqttClient->publish(uptimeStr, true)) { + this->_interface->logger->logln(F(" OK")); + this->_uptimeTimer.tick(); + } else { + this->_interface->logger->logln(F(" Failure")); + } } - _interface->loopFunction(); - - for (HomieNode* iNode : HomieNode::nodes) { - iNode->loop(); - } -} + this->_interface->loopFunction(); -void BootNormal::prepareForSleep() { - _interface->logger->logln(F("Sending offline message...")); - _flaggedForSleep = true; - _mqttOfflineMessageId = _interface->mqttClient->publish(_prefixMqttTopic(PSTR("/$online")), 1, true, "false"); + this->_interface->mqttClient->loop(); } diff --git a/src/Homie/Boot/BootNormal.hpp b/src/Homie/Boot/BootNormal.hpp index 8285588a..851272be 100644 --- a/src/Homie/Boot/BootNormal.hpp +++ b/src/Homie/Boot/BootNormal.hpp @@ -1,17 +1,13 @@ #pragma once -#include "Arduino.h" - #include #include -#include -#include #include #include "../../HomieNode.hpp" -#include "../../HomieRange.hpp" #include "../Constants.hpp" #include "../Limits.hpp" #include "../Datatypes/Interface.hpp" +#include "../MqttClient.hpp" #include "../Helpers.hpp" #include "../Config.hpp" #include "../Blinker.hpp" @@ -20,46 +16,41 @@ #include "../Logger.hpp" #include "Boot.hpp" -namespace HomieInternals { -class BootNormal : public Boot { - public: - BootNormal(); - ~BootNormal(); - void setup(); - void loop(); - void prepareForSleep(); - - private: - Uptime _uptime; - Timer _signalQualityTimer; - Timer _uptimeTimer; - bool _setupFunctionCalled; - WiFiEventHandler _wifiGotIpHandler; - WiFiEventHandler _wifiDisconnectedHandler; - bool _mqttDisconnectNotified; - bool _flaggedForOta; - bool _flaggedForReset; - bool _flaggedForReboot; - Bounce _resetDebouncer; - bool _flaggedForSleep; - uint16_t _mqttOfflineMessageId; +extern "C" { + #include "user_interface.h" +} - std::unique_ptr _mqttTopic; +namespace HomieInternals { + class BootNormal : public Boot { + public: + BootNormal(); + ~BootNormal(); + void setup(); + void loop(); - std::unique_ptr _mqttClientId; - std::unique_ptr _mqttWillTopic; - std::unique_ptr _mqttPayloadBuffer; + private: + Uptime _uptime; + Timer _wifiReconnectTimer; + Timer _mqttReconnectTimer; + Timer _signalQualityTimer; + Timer _uptimeTimer; + bool _setupFunctionCalled; + bool _wifiConnectNotified; + bool _wifiDisconnectNotified; + bool _mqttConnectNotified; + bool _mqttDisconnectNotified; + char _otaVersion[MAX_FIRMWARE_VERSION_LENGTH]; + bool _flaggedForOta; + bool _flaggedForReset; + Bounce _resetDebouncer; - void _handleReset(); - void _wifiConnect(); - void _onWifiGotIp(const WiFiEventStationModeGotIP& event); - void _onWifiDisconnected(const WiFiEventStationModeDisconnected& event); - void _mqttConnect(); - void _onMqttConnected(); - void _onMqttDisconnected(AsyncMqttClientDisconnectReason reason); - void _onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total); - void _onMqttPublish(uint16_t id); - void _prefixMqttTopic(); - char* _prefixMqttTopic(PGM_P topic); -}; -} // namespace HomieInternals + void _handleReset(); + void _wifiConnect(); + void _mqttConnect(); + void _mqttSetup(); + void _mqttCallback(char* topic, char* message); + void _fillMqttTopic(PGM_P topic); + bool _publishRetainedOrFail(const char* message); + bool _subscribe1OrFail(); + }; +} diff --git a/src/Homie/Boot/BootOta.cpp b/src/Homie/Boot/BootOta.cpp new file mode 100644 index 00000000..9debade3 --- /dev/null +++ b/src/Homie/Boot/BootOta.cpp @@ -0,0 +1,85 @@ +#include "BootOta.hpp" + +using namespace HomieInternals; + +BootOta::BootOta() +: Boot("OTA") +{ +} + +BootOta::~BootOta() { +} + +void BootOta::setup() { + Boot::setup(); + + WiFi.mode(WIFI_STA); + + int wifiAttempts = 1; + while (WiFi.waitForConnectResult() != WL_CONNECTED) { + if (wifiAttempts++ <= 3) { + WiFi.begin(this->_interface->config->get().wifi.ssid, this->_interface->config->get().wifi.password); + this->_interface->logger->log(F("↕ Connecting to Wi-Fi (attempt ")); + this->_interface->logger->log(wifiAttempts); + this->_interface->logger->logln(F("/3)")); + } else { + this->_interface->logger->logln(F("✖ Connection failed")); + this->_interface->logger->logln(F("↻ Rebooting into normal mode...")); + this->_interface->config->setOtaMode(false); + ESP.restart(); + } + } + this->_interface->logger->logln(F("✔ Connected to Wi-Fi")); + + const char* host = this->_interface->config->get().ota.server.host; + unsigned int port = this->_interface->config->get().ota.server.port; + if (this->_interface->config->get().mqtt.server.mdns.enabled) { + this->_interface->logger->log(F("Querying mDNS service ")); + this->_interface->logger->logln(this->_interface->config->get().mqtt.server.mdns.service); + MdnsQueryResult result = Helpers::mdnsQuery(this->_interface->config->get().mqtt.server.mdns.service); + if (result.success) { + host = result.ip.toString().c_str(); + port = result.port; + this->_interface->logger->log(F("✔ ")); + this->_interface->logger->log(F(" service found at ")); + this->_interface->logger->log(host); + this->_interface->logger->log(F(":")); + this->_interface->logger->logln(port); + } else { + this->_interface->logger->logln(F("✖ Service not found")); + ESP.restart(); + } + } + + this->_interface->logger->logln(F("Starting OTA...")); + + + char dataToPass[(MAX_DEVICE_ID_LENGTH - 1) + 1 + (MAX_FIRMWARE_NAME_LENGTH - 1) + 1 + (MAX_FIRMWARE_VERSION_LENGTH - 1) + 1 + (MAX_FIRMWARE_VERSION_LENGTH - 1) + 1]; + strcpy(dataToPass, this->_interface->config->get().deviceId); + strcat(dataToPass, "="); + strcat(dataToPass, this->_interface->firmware.name); + strcat(dataToPass, "="); + strcat(dataToPass, this->_interface->firmware.version); + strcat(dataToPass, "="); + strcat(dataToPass, this->_interface->config->getOtaVersion()); + t_httpUpdate_return result = ESPhttpUpdate.update(host, port, this->_interface->config->get().ota.path, dataToPass, this->_interface->config->get().ota.server.ssl.enabled, this->_interface->config->get().ota.server.ssl.fingerprint, false); + switch (result) { + case HTTP_UPDATE_FAILED: + this->_interface->logger->logln(F("✖ Update failed")); + break; + case HTTP_UPDATE_NO_UPDATES: + this->_interface->logger->logln(F("✖ No updates")); + break; + case HTTP_UPDATE_OK: + this->_interface->logger->logln(F("✔ Success")); + break; + } + + this->_interface->logger->logln(F("↻ Rebooting into normal mode...")); + this->_interface->config->setOtaMode(false); + ESP.restart(); +} + +void BootOta::loop() { + Boot::loop(); +} diff --git a/src/Homie/Boot/BootOta.hpp b/src/Homie/Boot/BootOta.hpp new file mode 100644 index 00000000..e435a920 --- /dev/null +++ b/src/Homie/Boot/BootOta.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include "../Constants.hpp" +#include "../Datatypes/Interface.hpp" +#include "../Config.hpp" +#include "../Logger.hpp" +#include "Boot.hpp" + +namespace HomieInternals { + class BootOta : public Boot { + public: + BootOta(); + ~BootOta(); + void setup(); + void loop(); + + private: + }; +} diff --git a/src/Homie/Boot/BootStandalone.cpp b/src/Homie/Boot/BootStandalone.cpp deleted file mode 100644 index 6fe85a2f..00000000 --- a/src/Homie/Boot/BootStandalone.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "BootStandalone.hpp" - -using namespace HomieInternals; - -BootStandalone::BootStandalone() -: Boot("standalone") -, _flaggedForConfig(false) { -} - -BootStandalone::~BootStandalone() { -} - -void BootStandalone::_handleReset() { - if (_interface->reset.enabled) { - _resetDebouncer.update(); - - if (_resetDebouncer.read() == _interface->reset.triggerState) { - _flaggedForConfig = true; - _interface->logger->logln(F("Flagged for configuration mode by pin")); - } - } - - if (_interface->reset.userFunction()) { - _flaggedForConfig = true; - _interface->logger->logln(F("Flagged for configuration mode by function")); - } -} - -void BootStandalone::setup() { - Boot::setup(); - - if (_interface->reset.enabled) { - pinMode(_interface->reset.triggerPin, INPUT_PULLUP); - - _resetDebouncer.attach(_interface->reset.triggerPin); - _resetDebouncer.interval(_interface->reset.triggerTime); - } -} - -void BootStandalone::loop() { - Boot::loop(); - - _handleReset(); - - if (_flaggedForConfig && _interface->reset.able) { - _interface->logger->logln(F("Device is in a resettable state")); - _interface->config->bypassStandalone(); - _interface->logger->logln(F("Next reboot will bypass standalone mode")); - - _interface->logger->logln(F("Triggering ABOUT_TO_RESET event...")); - _interface->eventHandler(HomieEvent::ABOUT_TO_RESET); - - _interface->logger->logln(F("↻ Rebooting into config mode...")); - _interface->logger->flush(); - ESP.restart(); - } -} diff --git a/src/Homie/Boot/BootStandalone.hpp b/src/Homie/Boot/BootStandalone.hpp deleted file mode 100644 index 73a1ee70..00000000 --- a/src/Homie/Boot/BootStandalone.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "Arduino.h" - -#include -#include "Boot.hpp" -#include "../Config.hpp" - -namespace HomieInternals { -class BootStandalone : public Boot { - public: - BootStandalone(); - ~BootStandalone(); - void setup(); - void loop(); - - private: - bool _flaggedForConfig; - Bounce _resetDebouncer; - - void _handleReset(); -}; -} // namespace HomieInternals diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 5d9071a8..062f486a 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -4,79 +4,103 @@ using namespace HomieInternals; Config::Config() : _interface(nullptr) -, _bootMode() , _configStruct() -, _spiffsBegan(false) { +, _otaVersion {'\0'} +, _spiffsBegan(false) +{ } void Config::attachInterface(Interface* interface) { - _interface = interface; + this->_interface = interface; } bool Config::_spiffsBegin() { - if (!_spiffsBegan) { - _spiffsBegan = SPIFFS.begin(); - if (!_spiffsBegan) _interface->logger->logln(F("✖ Cannot mount filesystem")); + if (!this->_spiffsBegan) { + this->_spiffsBegan = SPIFFS.begin(); + if (!this->_spiffsBegan) this->_interface->logger->logln(F("✖ Cannot mount filesystem")); } - return _spiffsBegan; + return this->_spiffsBegan; } bool Config::load() { - if (!_spiffsBegin()) { return false; } + if (!this->_spiffsBegin()) { return false; } - _bootMode = BOOT_CONFIG; + this->_bootMode = BOOT_CONFIG; if (!SPIFFS.exists(CONFIG_FILE_PATH)) { - _interface->logger->log(F("✖ ")); - _interface->logger->log(CONFIG_FILE_PATH); - _interface->logger->logln(F(" doesn't exist")); + this->_interface->logger->log(F("✖ ")); + this->_interface->logger->log(CONFIG_FILE_PATH); + this->_interface->logger->logln(F(" doesn't exist")); return false; } File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); if (!configFile) { - _interface->logger->logln(F("✖ Cannot open config file")); + this->_interface->logger->logln(F("✖ Cannot open config file")); return false; } size_t configSize = configFile.size(); - if (configSize > MAX_JSON_CONFIG_FILE_SIZE) { - _interface->logger->logln(F("✖ Config file too big")); + if (configSize > MAX_JSON_CONFIG_FILE_BUFFER_SIZE) { + this->_interface->logger->logln(F("✖ Config file too big")); return false; } - char buf[MAX_JSON_CONFIG_FILE_SIZE]; + char buf[MAX_JSON_CONFIG_FILE_BUFFER_SIZE]; configFile.readBytes(buf, configSize); configFile.close(); StaticJsonBuffer jsonBuffer; JsonObject& parsedJson = jsonBuffer.parseObject(buf); if (!parsedJson.success()) { - _interface->logger->logln(F("✖ Invalid JSON in the config file")); + this->_interface->logger->logln(F("✖ Invalid JSON in the config file")); return false; } ConfigValidationResult configValidationResult = Helpers::validateConfig(parsedJson); if (!configValidationResult.valid) { - _interface->logger->log(F("✖ Config file is not valid, reason: ")); - _interface->logger->logln(configValidationResult.reason); + this->_interface->logger->log(F("✖ Config file is not valid, reason: ")); + this->_interface->logger->logln(configValidationResult.reason); return false; } - _bootMode = BOOT_NORMAL; + if (SPIFFS.exists(CONFIG_OTA_PATH)) { + this->_bootMode = BOOT_OTA; + + File otaFile = SPIFFS.open(CONFIG_OTA_PATH, "r"); + if (otaFile) { + size_t otaSize = otaFile.size(); + otaFile.readBytes(this->_otaVersion, otaSize); + otaFile.close(); + } else { + this->_interface->logger->logln(F("✖ Cannot open OTA file")); + } + } else { + this->_bootMode = BOOT_NORMAL; + } const char* reqName = parsedJson["name"]; const char* reqWifiSsid = parsedJson["wifi"]["ssid"]; const char* reqWifiPassword = parsedJson["wifi"]["password"]; - - const char* reqMqttHost = parsedJson["mqtt"]["host"]; + bool reqMqttMdns = false; + if (parsedJson["mqtt"].as().containsKey("mdns")) reqMqttMdns = true; + bool reqOtaMdns = false; + if (parsedJson["ota"].as().containsKey("mdns")) reqOtaMdns = true; + + const char* reqMqttHost = ""; + const char* reqMqttMdnsService = ""; + if (reqMqttMdns) { + reqMqttMdnsService = parsedJson["mqtt"]["mdns"]; + } else { + reqMqttHost = parsedJson["mqtt"]["host"]; + } const char* reqDeviceId = Helpers::getDeviceId(); if (parsedJson.containsKey("device_id")) { reqDeviceId = parsedJson["device_id"]; } - uint16_t reqMqttPort = DEFAULT_MQTT_PORT; + unsigned int reqMqttPort = DEFAULT_MQTT_PORT; if (parsedJson["mqtt"].as().containsKey("port")) { reqMqttPort = parsedJson["mqtt"]["port"]; } @@ -96,120 +120,87 @@ bool Config::load() { if (parsedJson["mqtt"].as().containsKey("password")) { reqMqttPassword = parsedJson["mqtt"]["password"]; } - + bool reqMqttSsl = false; + if (parsedJson["mqtt"].as().containsKey("ssl")) { + reqMqttSsl = parsedJson["mqtt"]["ssl"]; + } + const char* reqMqttFingerprint = ""; + if (parsedJson["mqtt"].as().containsKey("fingerprint")) { + reqMqttFingerprint = parsedJson["mqtt"]["fingerprint"]; + } bool reqOtaEnabled = false; if (parsedJson["ota"].as().containsKey("enabled")) { reqOtaEnabled = parsedJson["ota"]["enabled"]; } - - strcpy(_configStruct.name, reqName); - strcpy(_configStruct.wifi.ssid, reqWifiSsid); - strcpy(_configStruct.wifi.password, reqWifiPassword); - strcpy(_configStruct.deviceId, reqDeviceId); - strcpy(_configStruct.mqtt.server.host, reqMqttHost); - _configStruct.mqtt.server.port = reqMqttPort; - strcpy(_configStruct.mqtt.baseTopic, reqMqttBaseTopic); - _configStruct.mqtt.auth = reqMqttAuth; - strcpy(_configStruct.mqtt.username, reqMqttUsername); - strcpy(_configStruct.mqtt.password, reqMqttPassword); - _configStruct.ota.enabled = reqOtaEnabled; - - /* Parse the settings */ - - JsonObject& settingsObject = parsedJson["settings"].as(); - - for (IHomieSetting* iSetting : IHomieSetting::settings) { - if (iSetting->isBool()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject.containsKey(setting->getName())) { - setting->set(settingsObject[setting->getName()].as()); - } - } else if (iSetting->isUnsignedLong()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject.containsKey(setting->getName())) { - setting->set(settingsObject[setting->getName()].as()); - } - } else if (iSetting->isLong()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject.containsKey(setting->getName())) { - setting->set(settingsObject[setting->getName()].as()); - } - } else if (iSetting->isDouble()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject.containsKey(setting->getName())) { - setting->set(settingsObject[setting->getName()].as()); - } - } else if (iSetting->isConstChar()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject.containsKey(setting->getName())) { - setting->set(strdup(settingsObject[setting->getName()].as())); - } - } + const char* reqOtaHost = reqMqttHost; + const char* reqOtaMdnsService = ""; + if (reqOtaMdns) { + reqOtaMdnsService = parsedJson["ota"]["mdns"]; + } else if (parsedJson["ota"].as().containsKey("host")) { + reqOtaHost = parsedJson["ota"]["host"]; + } + unsigned int reqOtaPort = DEFAULT_OTA_PORT; + if (parsedJson["ota"].as().containsKey("port")) { + reqOtaPort = parsedJson["ota"]["port"]; + } + const char* reqOtaPath = DEFAULT_OTA_PATH; + if (parsedJson["ota"].as().containsKey("path")) { + reqOtaPath = parsedJson["ota"]["path"]; + } + bool reqOtaSsl = false; + if (parsedJson["ota"].as().containsKey("ssl")) { + reqOtaSsl = parsedJson["ota"]["ssl"]; } + const char* reqOtaFingerprint = ""; + if (parsedJson["ota"].as().containsKey("fingerprint")) { + reqOtaFingerprint = parsedJson["ota"]["fingerprint"]; + } + + strcpy(this->_configStruct.name, reqName); + strcpy(this->_configStruct.wifi.ssid, reqWifiSsid); + strcpy(this->_configStruct.wifi.password, reqWifiPassword); + strcpy(this->_configStruct.deviceId, reqDeviceId); + strcpy(this->_configStruct.mqtt.server.host, reqMqttHost); + this->_configStruct.mqtt.server.port = reqMqttPort; + this->_configStruct.mqtt.server.mdns.enabled = reqMqttMdns; + strcpy(this->_configStruct.mqtt.server.mdns.service, reqMqttMdnsService); + strcpy(this->_configStruct.mqtt.baseTopic, reqMqttBaseTopic); + this->_configStruct.mqtt.auth = reqMqttAuth; + strcpy(this->_configStruct.mqtt.username, reqMqttUsername); + strcpy(this->_configStruct.mqtt.password, reqMqttPassword); + this->_configStruct.mqtt.server.ssl.enabled = reqMqttSsl; + strcpy(this->_configStruct.mqtt.server.ssl.fingerprint, reqMqttFingerprint); + this->_configStruct.ota.enabled = reqOtaEnabled; + strcpy(this->_configStruct.ota.server.host, reqOtaHost); + this->_configStruct.ota.server.port = reqOtaPort; + this->_configStruct.ota.server.mdns.enabled = reqOtaMdns; + strcpy(this->_configStruct.ota.server.mdns.service, reqOtaMdnsService); + strcpy(this->_configStruct.ota.path, reqOtaPath); + this->_configStruct.ota.server.ssl.enabled = reqOtaSsl; + strcpy(this->_configStruct.ota.server.ssl.fingerprint, reqOtaFingerprint); return true; } -char* Config::getSafeConfigFile() const { - File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); - size_t configSize = configFile.size(); - - char buf[MAX_JSON_CONFIG_FILE_SIZE]; - configFile.readBytes(buf, configSize); - configFile.close(); - - StaticJsonBuffer jsonBuffer; - JsonObject& parsedJson = jsonBuffer.parseObject(buf); - parsedJson["wifi"].as().remove("password"); - parsedJson["mqtt"].as().remove("username"); - parsedJson["mqtt"].as().remove("password"); - - size_t jsonBufferLength = parsedJson.measureLength() + 1; - std::unique_ptr jsonString(new char[jsonBufferLength]); - parsedJson.printTo(jsonString.get(), jsonBufferLength); - - return strdup(jsonString.get()); +const ConfigStruct& Config::get() { + return this->_configStruct; } void Config::erase() { - if (!_spiffsBegin()) { return; } + if (!this->_spiffsBegin()) { return; } SPIFFS.remove(CONFIG_FILE_PATH); - SPIFFS.remove(CONFIG_BYPASS_STANDALONE_FILE_PATH); + SPIFFS.remove(CONFIG_OTA_PATH); } -void Config::bypassStandalone() { - if (!_spiffsBegin()) { return; } - - File bypassStandaloneFile = SPIFFS.open(CONFIG_BYPASS_STANDALONE_FILE_PATH, "w"); - if (!bypassStandaloneFile) { - _interface->logger->logln(F("✖ Cannot open bypass standalone file")); - return; - } - - bypassStandaloneFile.print("1"); - bypassStandaloneFile.close(); -} - -bool Config::canBypassStandalone() { - if (!_spiffsBegin()) { return false; } - - return SPIFFS.exists(CONFIG_BYPASS_STANDALONE_FILE_PATH); -} - -void Config::write(const char* config) { - if (!_spiffsBegin()) { return; } +void Config::write(const String& config) { + if (!this->_spiffsBegin()) { return; } SPIFFS.remove(CONFIG_FILE_PATH); File configFile = SPIFFS.open(CONFIG_FILE_PATH, "w"); if (!configFile) { - _interface->logger->logln(F("✖ Cannot open config file")); + this->_interface->logger->logln(F("✖ Cannot open config file")); return; } @@ -217,113 +208,108 @@ void Config::write(const char* config) { configFile.close(); } -bool Config::patch(const char* patch) { - if (!_spiffsBegin()) { return false; } - - StaticJsonBuffer patchJsonBuffer; - JsonObject& patchObject = patchJsonBuffer.parseObject(patch); +void Config::setOtaMode(bool enabled, const char* version) { + if (!this->_spiffsBegin()) { return; } - if (!patchObject.success()) { - _interface->logger->logln(F("✖ Invalid or too big JSON")); - return false; + if (enabled) { + File otaFile = SPIFFS.open(CONFIG_OTA_PATH, "w"); + if (!otaFile) { + this->_interface->logger->logln(F("✖ Cannot open OTA file")); + return; } - File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); - if (!configFile) { - _interface->logger->logln(F("✖ Cannot open config file")); - return false; - } - - size_t configSize = configFile.size(); - - char configJson[MAX_JSON_CONFIG_FILE_SIZE]; - configFile.readBytes(configJson, configSize); - configFile.close(); - - StaticJsonBuffer configJsonBuffer; - JsonObject& configObject = configJsonBuffer.parseObject(configJson); - - for (JsonObject::iterator it = patchObject.begin(); it != patchObject.end(); ++it) { - if (patchObject[it->key].is()) { - JsonObject& subObject = patchObject[it->key].as(); - for (JsonObject::iterator it2 = subObject.begin(); it2 != subObject.end(); ++it2) { - if (!configObject.containsKey(it->key) || !configObject[it->key].is()) { - String error = "✖ Config does not contain a "; - error.concat(it->key); - error.concat(" object"); - _interface->logger->logln(error); - return false; - } - JsonObject& subConfigObject = configObject[it->key].as(); - subConfigObject[it2->key] = it2->value; - } - } else { - configObject[it->key] = it->value; - } - } - - ConfigValidationResult configValidationResult = Helpers::validateConfig(configObject); - if (!configValidationResult.valid) { - _interface->logger->log(F("✖ Config file is not valid, reason: ")); - _interface->logger->logln(configValidationResult.reason); - return false; - } - - size_t finalBufferLength = configObject.measureLength() + 1; - std::unique_ptr finalConfigString(new char[finalBufferLength]); - configObject.printTo(finalConfigString.get(), finalBufferLength); - - write(finalConfigString.get()); + otaFile.print(version); + otaFile.close(); + } else { + SPIFFS.remove(CONFIG_OTA_PATH); + } +} - return true; +const char* Config::getOtaVersion() { + return this->_otaVersion; } -BootMode Config::getBootMode() const { - return _bootMode; +BootMode Config::getBootMode() { + return this->_bootMode; } -void Config::log() const { - _interface->logger->logln(F("{} Stored configuration:")); - _interface->logger->log(F(" • Hardware device ID: ")); - _interface->logger->logln(Helpers::getDeviceId()); - _interface->logger->log(F(" • Device ID: ")); - _interface->logger->logln(_configStruct.deviceId); - _interface->logger->log(F(" • Boot mode: ")); - switch (_bootMode) { +void Config::log() { + this->_interface->logger->logln(F("{} Stored configuration:")); + this->_interface->logger->log(F(" • Hardware device ID: ")); + this->_interface->logger->logln(Helpers::getDeviceId()); + this->_interface->logger->log(F(" • Device ID: ")); + this->_interface->logger->logln(this->_configStruct.deviceId); + this->_interface->logger->log(F(" • Boot mode: ")); + switch (this->_bootMode) { case BOOT_CONFIG: - _interface->logger->logln(F("configuration")); + this->_interface->logger->logln(F("configuration")); break; case BOOT_NORMAL: - _interface->logger->logln(F("normal")); + this->_interface->logger->logln(F("normal")); + break; + case BOOT_OTA: + this->_interface->logger->logln(F("OTA")); break; default: - _interface->logger->logln(F("unknown")); + this->_interface->logger->logln(F("unknown")); break; } - _interface->logger->log(F(" • Name: ")); - _interface->logger->logln(_configStruct.name); - - _interface->logger->logln(F(" • Wi-Fi")); - _interface->logger->log(F(" ◦ SSID: ")); - _interface->logger->logln(_configStruct.wifi.ssid); - _interface->logger->logln(F(" ◦ Password not shown")); - - _interface->logger->logln(F(" • MQTT")); - _interface->logger->log(F(" ◦ Host: ")); - _interface->logger->logln(_configStruct.mqtt.server.host); - _interface->logger->log(F(" ◦ Port: ")); - _interface->logger->logln(_configStruct.mqtt.server.port); - _interface->logger->log(F(" ◦ Base topic: ")); - _interface->logger->logln(_configStruct.mqtt.baseTopic); - _interface->logger->log(F(" ◦ Auth? ")); - _interface->logger->logln(_configStruct.mqtt.auth ? F("yes") : F("no")); - if (_configStruct.mqtt.auth) { - _interface->logger->log(F(" ◦ Username: ")); - _interface->logger->logln(_configStruct.mqtt.username); - _interface->logger->logln(F(" ◦ Password not shown")); + this->_interface->logger->log(F(" • Name: ")); + this->_interface->logger->logln(this->_configStruct.name); + + this->_interface->logger->logln(F(" • Wi-Fi")); + this->_interface->logger->log(F(" ◦ SSID: ")); + this->_interface->logger->logln(this->_configStruct.wifi.ssid); + this->_interface->logger->logln(F(" ◦ Password not shown")); + + this->_interface->logger->logln(F(" • MQTT")); + if (this->_configStruct.mqtt.server.mdns.enabled) { + this->_interface->logger->log(F(" ◦ mDNS: ")); + this->_interface->logger->log(this->_configStruct.mqtt.server.mdns.service); + } else { + this->_interface->logger->log(F(" ◦ Host: ")); + this->_interface->logger->logln(this->_configStruct.mqtt.server.host); + this->_interface->logger->log(F(" ◦ Port: ")); + this->_interface->logger->logln(this->_configStruct.mqtt.server.port); + } + this->_interface->logger->log(F(" ◦ Base topic: ")); + this->_interface->logger->logln(this->_configStruct.mqtt.baseTopic); + this->_interface->logger->log(F(" ◦ Auth? ")); + this->_interface->logger->logln(this->_configStruct.mqtt.auth ? F("yes") : F("no")); + if (this->_configStruct.mqtt.auth) { + this->_interface->logger->log(F(" ◦ Username: ")); + this->_interface->logger->logln(this->_configStruct.mqtt.username); + this->_interface->logger->logln(F(" ◦ Password not shown")); + } + this->_interface->logger->log(F(" ◦ SSL? ")); + this->_interface->logger->logln(this->_configStruct.mqtt.server.ssl.enabled ? F("yes") : F("no")); + if (this->_configStruct.mqtt.server.ssl.enabled) { + this->_interface->logger->log(F(" ◦ Fingerprint: ")); + if (strcmp_P(this->_configStruct.mqtt.server.ssl.fingerprint, PSTR("")) == 0) this->_interface->logger->logln(F("unset")); + else this->_interface->logger->logln(this->_configStruct.mqtt.server.ssl.fingerprint); } - _interface->logger->logln(F(" • OTA")); - _interface->logger->log(F(" ◦ Enabled? ")); - _interface->logger->logln(_configStruct.ota.enabled ? F("yes") : F("no")); + this->_interface->logger->logln(F(" • OTA")); + this->_interface->logger->log(F(" ◦ Enabled? ")); + this->_interface->logger->logln(this->_configStruct.ota.enabled ? F("yes") : F("no")); + if (this->_configStruct.ota.enabled) { + if (this->_configStruct.ota.server.mdns.enabled) { + this->_interface->logger->log(F(" ◦ mDNS: ")); + this->_interface->logger->log(this->_configStruct.ota.server.mdns.service); + } else { + this->_interface->logger->log(F(" ◦ Host: ")); + this->_interface->logger->logln(this->_configStruct.ota.server.host); + this->_interface->logger->log(F(" ◦ Port: ")); + this->_interface->logger->logln(this->_configStruct.ota.server.port); + } + this->_interface->logger->log(F(" ◦ Path: ")); + this->_interface->logger->logln(this->_configStruct.ota.path); + this->_interface->logger->log(F(" ◦ SSL? ")); + this->_interface->logger->logln(this->_configStruct.ota.server.ssl.enabled ? F("yes") : F("no")); + if (this->_configStruct.ota.server.ssl.enabled) { + this->_interface->logger->log(F(" ◦ Fingerprint: ")); + if (strcmp_P(this->_configStruct.ota.server.ssl.fingerprint, PSTR("")) == 0) this->_interface->logger->logln(F("unset")); + else this->_interface->logger->logln(this->_configStruct.ota.server.ssl.fingerprint); + } + } } diff --git a/src/Homie/Config.hpp b/src/Homie/Config.hpp index e2859294..faea7945 100644 --- a/src/Homie/Config.hpp +++ b/src/Homie/Config.hpp @@ -1,7 +1,5 @@ #pragma once -#include "Arduino.h" - #include #include "FS.h" #include "Datatypes/Interface.hpp" @@ -9,34 +7,28 @@ #include "Helpers.hpp" #include "Limits.hpp" #include "Logger.hpp" -#include "../HomieSetting.hpp" namespace HomieInternals { -class Config { - public: - Config(); - void attachInterface(Interface* interface); - bool load(); - inline const ConfigStruct& get() const; - char* getSafeConfigFile() const; - void erase(); - void bypassStandalone(); - bool canBypassStandalone(); - void write(const char* config); - bool patch(const char* patch); - BootMode getBootMode() const; - void log() const; // print the current config to log output - - private: - Interface* _interface; - BootMode _bootMode; - ConfigStruct _configStruct; - bool _spiffsBegan; + class Config { + public: + Config(); + void attachInterface(Interface* interface); + bool load(); + const ConfigStruct& get(); + void erase(); + void write(const String& config); + void setOtaMode(bool enabled, const char* version = ""); + const char* getOtaVersion(); + BootMode getBootMode(); + void log(); // print the current config to log output - bool _spiffsBegin(); -}; + private: + Interface* _interface; + BootMode _bootMode; + ConfigStruct _configStruct; + char _otaVersion[MAX_FIRMWARE_VERSION_LENGTH]; + bool _spiffsBegan; -const ConfigStruct& Config::get() const { - return _configStruct; + bool _spiffsBegin(); + }; } -} // namespace HomieInternals diff --git a/src/Homie/Constants.hpp b/src/Homie/Constants.hpp index 005dfc7c..a8f6692a 100644 --- a/src/Homie/Constants.hpp +++ b/src/Homie/Constants.hpp @@ -3,36 +3,39 @@ #include namespace HomieInternals { - const char HOMIE_VERSION[] = "2.0.0"; - const char HOMIE_ESP8266_VERSION[] = "2.0.0"; + const char VERSION[] = "1.5.0"; + const unsigned long BAUD_RATE = 115200; const IPAddress ACCESS_POINT_IP(192, 168, 1, 1); - const uint16_t DEFAULT_MQTT_PORT = 1883; + const unsigned int DEFAULT_MQTT_PORT = 1883; + const unsigned char DEFAULT_OTA_PORT = 80; const char DEFAULT_MQTT_BASE_TOPIC[] = "devices/"; + const char DEFAULT_OTA_PATH[] = "/ota"; - const uint8_t DEFAULT_RESET_PIN = 0; // == D3 on nodeMCU - const uint8_t DEFAULT_RESET_STATE = LOW; - const uint16_t DEFAULT_RESET_TIME = 5 * 1000; + const unsigned char DEFAULT_RESET_PIN = 0; // == D3 on nodeMCU + const unsigned char DEFAULT_RESET_STATE = LOW; + const unsigned int DEFAULT_RESET_TIME = 5 * 1000; const char DEFAULT_BRAND[] = "Homie"; const char DEFAULT_FW_NAME[] = "undefined"; const char DEFAULT_FW_VERSION[] = "undefined"; - const uint16_t CONFIG_SCAN_INTERVAL = 20 * 1000; - const uint32_t SIGNAL_QUALITY_SEND_INTERVAL = 5 * 60 * 1000; - const uint32_t UPTIME_SEND_INTERVAL = 2 * 60 * 1000; + const unsigned int CONFIG_SCAN_INTERVAL = 20 * 1000; + const unsigned int WIFI_RECONNECT_INTERVAL = 20 * 1000; + const unsigned int MQTT_RECONNECT_INTERVAL = 5 * 1000; + const unsigned long SIGNAL_QUALITY_SEND_INTERVAL = 5 * 60 * 1000; + const unsigned long UPTIME_SEND_INTERVAL = 2 * 60 * 1000; const float LED_WIFI_DELAY = 1; const float LED_MQTT_DELAY = 0.2; - const char CONFIG_UI_BUNDLE_PATH[] = "/homie/ui_bundle.gz"; - const char CONFIG_BYPASS_STANDALONE_FILE_PATH[] = "/homie/BYPASS_STANDALONE"; const char CONFIG_FILE_PATH[] = "/homie/config.json"; + const char CONFIG_OTA_PATH[] = "/homie/ota"; - enum BootMode : uint8_t { - BOOT_STANDALONE = 1, - BOOT_NORMAL, - BOOT_CONFIG + enum BootMode : unsigned char { + BOOT_NORMAL = 1, + BOOT_CONFIG, + BOOT_OTA }; -} // namespace HomieInternals +} diff --git a/src/Homie/Datatypes/Callbacks.hpp b/src/Homie/Datatypes/Callbacks.hpp index e4a33a32..ddc3e6ef 100644 --- a/src/Homie/Datatypes/Callbacks.hpp +++ b/src/Homie/Datatypes/Callbacks.hpp @@ -1,17 +1,16 @@ #pragma once -#include #include "../../HomieEvent.hpp" -#include "../../HomieRange.hpp" +#include namespace HomieInternals { typedef std::function OperationFunction; - typedef std::function GlobalInputHandler; - typedef std::function NodeInputHandler; - typedef std::function PropertyInputHandler; + typedef std::function GlobalInputHandler; + typedef std::function NodeInputHandler; + typedef std::function PropertyInputHandler; typedef std::function EventHandler; typedef std::function ResetFunction; -} // namespace HomieInternals +} diff --git a/src/Homie/Datatypes/ConfigStruct.hpp b/src/Homie/Datatypes/ConfigStruct.hpp index 59207fbd..c9c99a67 100644 --- a/src/Homie/Datatypes/ConfigStruct.hpp +++ b/src/Homie/Datatypes/ConfigStruct.hpp @@ -4,30 +4,40 @@ #include "../Limits.hpp" namespace HomieInternals { -struct ConfigStruct { - char name[MAX_FRIENDLY_NAME_LENGTH]; - char deviceId[MAX_DEVICE_ID_LENGTH]; + struct ConfigStruct { + char name[MAX_FRIENDLY_NAME_LENGTH]; + char deviceId[MAX_DEVICE_ID_LENGTH]; - struct WiFi { - char ssid[MAX_WIFI_SSID_LENGTH]; - char password[MAX_WIFI_PASSWORD_LENGTH]; - } wifi; + struct WiFi { + char ssid[MAX_WIFI_SSID_LENGTH]; + char password[MAX_WIFI_PASSWORD_LENGTH]; + } wifi; - struct Server { - char host[MAX_HOSTNAME_LENGTH]; - uint16_t port; - }; + struct Server { + char host[MAX_HOSTNAME_LENGTH]; + unsigned int port; + struct mDNS { + bool enabled; + char service[MAX_HOSTNAME_LENGTH]; + } mdns; + struct SSL { + bool enabled; + char fingerprint[MAX_FINGERPRINT_LENGTH]; + } ssl; + }; - struct MQTT { - Server server; - char baseTopic[MAX_MQTT_BASE_TOPIC_LENGTH]; - bool auth; - char username[MAX_MQTT_CREDS_LENGTH]; - char password[MAX_MQTT_CREDS_LENGTH]; - } mqtt; + struct MQTT { + Server server; + char baseTopic[MAX_MQTT_BASE_TOPIC_LENGTH]; + bool auth; + char username[MAX_MQTT_CREDS_LENGTH]; + char password[MAX_MQTT_CREDS_LENGTH]; + } mqtt; - struct OTA { - bool enabled; - } ota; -}; -} // namespace HomieInternals + struct OTA { + bool enabled; + Server server; + char path[MAX_OTA_PATH_LENGTH]; + } ota; + }; +} diff --git a/src/Homie/Datatypes/Interface.hpp b/src/Homie/Datatypes/Interface.hpp index ddb96a06..6892874b 100644 --- a/src/Homie/Datatypes/Interface.hpp +++ b/src/Homie/Datatypes/Interface.hpp @@ -1,51 +1,52 @@ #pragma once -#include #include "../Limits.hpp" #include "./Callbacks.hpp" #include "../../HomieNode.hpp" #include "../../HomieEvent.hpp" namespace HomieInternals { -class Logger; -class Blinker; -class Config; -struct Interface { - /***** User configurable data *****/ - char brand[MAX_BRAND_LENGTH]; - - bool standalone; - - struct Firmware { - char name[MAX_FIRMWARE_NAME_LENGTH]; - char version[MAX_FIRMWARE_VERSION_LENGTH]; - } firmware; - - struct LED { - bool enabled; - uint8_t pin; - uint8_t on; - } led; - - struct Reset { - bool enabled; - bool able; - uint8_t triggerPin; - uint8_t triggerState; - uint16_t triggerTime; - ResetFunction userFunction; - } reset; - - GlobalInputHandler globalInputHandler; - OperationFunction setupFunction; - OperationFunction loopFunction; - EventHandler eventHandler; - - /***** Runtime data *****/ - bool connected; - Logger* logger; - Blinker* blinker; - Config* config; - AsyncMqttClient* mqttClient; -}; -} // namespace HomieInternals + class Logger; + class Blinker; + class Config; + class MqttClient; + struct Interface { + /***** User configurable data *****/ + char brand[MAX_BRAND_LENGTH]; + + struct Firmware { + char name[MAX_FIRMWARE_NAME_LENGTH]; + char version[MAX_FIRMWARE_VERSION_LENGTH]; + } firmware; + + struct LED { + bool enabled; + unsigned char pin; + unsigned char on; + } led; + + struct Reset { + bool enabled; + bool able; + unsigned char triggerPin; + unsigned char triggerState; + unsigned int triggerTime; + ResetFunction userFunction; + } reset; + + const HomieNode* registeredNodes[MAX_REGISTERED_NODES_COUNT]; + unsigned char registeredNodesCount; + + GlobalInputHandler globalInputHandler; + OperationFunction setupFunction; + OperationFunction loopFunction; + EventHandler eventHandler; + + /***** Runtime data *****/ + bool readyToOperate; + Logger* logger; + Blinker* blinker; + Config* config; + MqttClient* mqttClient; + }; +} diff --git a/src/Homie/Datatypes/Subscription.hpp b/src/Homie/Datatypes/Subscription.hpp new file mode 100644 index 00000000..93c4448f --- /dev/null +++ b/src/Homie/Datatypes/Subscription.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "../Limits.hpp" +#include "./Callbacks.hpp" + +namespace HomieInternals { + struct Subscription { + char property[MAX_NODE_PROPERTY_LENGTH]; + PropertyInputHandler inputHandler; + }; +} diff --git a/src/Homie/Helpers.cpp b/src/Homie/Helpers.cpp index e1c94fb4..de051a37 100644 --- a/src/Homie/Helpers.cpp +++ b/src/Homie/Helpers.cpp @@ -2,7 +2,7 @@ using namespace HomieInternals; -char Helpers::_deviceId[] = ""; // need to define the static variable +char Helpers::_deviceId[] = ""; // need to define the static variable void Helpers::generateDeviceId() { char flashChipId[6 + 1]; @@ -15,17 +15,19 @@ const char* Helpers::getDeviceId() { return Helpers::_deviceId; } -uint8_t Helpers::rssiToPercentage(int32_t rssi) { - uint8_t quality; - if (rssi <= -100) { - quality = 0; - } else if (rssi >= -50) { - quality = 100; +MdnsQueryResult Helpers::mdnsQuery(const char* service) { + MdnsQueryResult result; + result.success = false; + int n = MDNS.queryService(service, "tcp"); + if (n == 0) { + return result; } else { - quality = 2 * (rssi + 100); + result.success = true; + result.ip = MDNS.IP(0); + result.port = MDNS.port(0); } - return quality; + return result; } ConfigValidationResult Helpers::validateConfig(const JsonObject& object) { @@ -38,16 +40,16 @@ ConfigValidationResult Helpers::validateConfig(const JsonObject& object) { if (!result.valid) return result; result = _validateConfigOta(object); if (!result.valid) return result; - result = _validateConfigSettings(object); - if (!result.valid) return result; result.valid = true; + result.reason = nullptr; return result; } ConfigValidationResult Helpers::_validateConfigRoot(const JsonObject& object) { ConfigValidationResult result; result.valid = false; + result.reason = nullptr; if (!object.containsKey("name") || !object["name"].is()) { result.reason = F("name is not a string"); return result; @@ -81,6 +83,7 @@ ConfigValidationResult Helpers::_validateConfigRoot(const JsonObject& object) { ConfigValidationResult Helpers::_validateConfigWifi(const JsonObject& object) { ConfigValidationResult result; result.valid = false; + result.reason = nullptr; if (!object.containsKey("wifi") || !object["wifi"].is()) { result.reason = F("wifi is not an object"); @@ -116,22 +119,36 @@ ConfigValidationResult Helpers::_validateConfigWifi(const JsonObject& object) { ConfigValidationResult Helpers::_validateConfigMqtt(const JsonObject& object) { ConfigValidationResult result; result.valid = false; + result.reason = nullptr; if (!object.containsKey("mqtt") || !object["mqtt"].is()) { result.reason = F("mqtt is not an object"); return result; } - if (!object["mqtt"].as().containsKey("host") || !object["mqtt"]["host"].is()) { - result.reason = F("mqtt.host is not a string"); - return result; - } - if (strlen(object["mqtt"]["host"]) + 1 > MAX_HOSTNAME_LENGTH) { - result.reason = F("mqtt.host is too long"); - return result; - } - if (object["mqtt"].as().containsKey("port") && !object["mqtt"]["port"].is()) { - result.reason = F("mqtt.port is not an integer"); - return result; + bool mdns = false; + if (object["mqtt"].as().containsKey("mdns")) { + if (!object["mqtt"]["mdns"].is()) { + result.reason = F("mqtt.mdns is not a string"); + return result; + } + if (strlen(object["mqtt"]["mdns"]) + 1 > MAX_HOSTNAME_LENGTH) { + result.reason = F("mqtt.mdns is too long"); + return result; + } + mdns = true; + } else { + if (!object["mqtt"].as().containsKey("host") || !object["mqtt"]["host"].is()) { + result.reason = F("mqtt.host is not a string"); + return result; + } + if (strlen(object["mqtt"]["host"]) + 1 > MAX_HOSTNAME_LENGTH) { + result.reason = F("mqtt.host is too long"); + return result; + } + if (object["mqtt"].as().containsKey("port") && !object["mqtt"]["port"].is()) { + result.reason = F("mqtt.port is not an unsigned integer"); + return result; + } } if (object["mqtt"].as().containsKey("base_topic")) { if (!object["mqtt"]["base_topic"].is()) { @@ -169,11 +186,32 @@ ConfigValidationResult Helpers::_validateConfigMqtt(const JsonObject& object) { } } } + if (object["mqtt"].as().containsKey("ssl")) { + if (!object["mqtt"]["ssl"].is()) { + result.reason = F("mqtt.ssl is not a boolean"); + return result; + } - const char* host = object["mqtt"]["host"]; - if (strcmp_P(host, PSTR("")) == 0) { - result.reason = F("mqtt.host is empty"); - return result; + if (object["mqtt"]["ssl"]) { + if (object["mqtt"].as().containsKey("fingerprint") && !object["mqtt"]["fingerprint"].is()) { + result.reason = F("mqtt.fingerprint is not a string"); + return result; + } + } + } + + if (mdns) { + const char* mdnsService = object["mqtt"]["mdns"]; + if (strcmp_P(mdnsService, PSTR("")) == 0) { + result.reason = F("mqtt.mdns is empty"); + return result; + } + } else { + const char* host = object["mqtt"]["host"]; + if (strcmp_P(host, PSTR("")) == 0) { + result.reason = F("mqtt.host is empty"); + return result; + } } result.valid = true; @@ -183,6 +221,7 @@ ConfigValidationResult Helpers::_validateConfigMqtt(const JsonObject& object) { ConfigValidationResult Helpers::_validateConfigOta(const JsonObject& object) { ConfigValidationResult result; result.valid = false; + result.reason = nullptr; if (!object.containsKey("ota") || !object["ota"].is()) { result.reason = F("ota is not an object"); @@ -192,113 +231,53 @@ ConfigValidationResult Helpers::_validateConfigOta(const JsonObject& object) { result.reason = F("ota.enabled is not a boolean"); return result; } - - result.valid = true; - return result; -} - -ConfigValidationResult Helpers::_validateConfigSettings(const JsonObject& object) { - ConfigValidationResult result; - result.valid = false; - - StaticJsonBuffer<0> emptySettingsBuffer; - - JsonObject* settingsObject = &(emptySettingsBuffer.createObject()); - - if (object.containsKey("settings") && object["settings"].is()) { - settingsObject = &(object["settings"].as()); - } - - for (IHomieSetting* iSetting : IHomieSetting::settings) { - if (iSetting->isBool()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject->containsKey(setting->getName())) { - if (!(*settingsObject)[setting->getName()].is()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is not a boolean")); - return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting does not pass the validator function")); - return result; - } - } else if (setting->isRequired()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is missing")); + if (object["ota"]["enabled"]) { + if (object["ota"].as().containsKey("mdns")) { + if (!object["ota"]["mdns"].is()) { + result.reason = F("ota.mdns is not a string"); return result; } - } else if (iSetting->isUnsignedLong()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject->containsKey(setting->getName())) { - if (!(*settingsObject)[setting->getName()].is()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is not an unsigned long")); - return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - result.reason = String(setting->getName()); - result.reason.concat((" setting does not pass the validator function")); - return result; - } - } else if (setting->isRequired()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is missing")); + if (strlen(object["ota"]["mdns"]) + 1 > MAX_HOSTNAME_LENGTH) { + result.reason = F("ota.mdns is too long"); return result; } - } else if (iSetting->isLong()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject->containsKey(setting->getName())) { - if (!(*settingsObject)[setting->getName()].is()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is not a long")); + } else { + if (object["ota"].as().containsKey("host")) { + if (!object["ota"]["host"].is()) { + result.reason = F("ota.host is not a string"); return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting does not pass the validator function")); + } + if (strlen(object["ota"]["host"]) + 1 > MAX_HOSTNAME_LENGTH) { + result.reason = F("ota.host is too long"); return result; } - } else if (setting->isRequired()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is missing")); + } + if (object["ota"].as().containsKey("port") && !object["ota"]["port"].is()) { + result.reason = F("ota.port is not an unsigned integer"); return result; } - } else if (iSetting->isDouble()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject->containsKey(setting->getName())) { - if (!(*settingsObject)[setting->getName()].is()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is not a double")); - return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - result.reason = String(setting->getName()); - result.reason.concat((" setting does not pass the validator function")); - return result; - } - } else if (setting->isRequired()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is missing")); + } + if (object["ota"].as().containsKey("path")) { + if (!object["ota"]["path"].is()) { + result.reason = F("ota.path is not a string"); + return result; + } + if (strlen(object["ota"]["path"]) + 1 > MAX_OTA_PATH_LENGTH) { + result.reason = F("ota.path is too long"); + return result; + } + } + if (object["ota"].as().containsKey("ssl")) { + if (!object["ota"]["ssl"].is()) { + result.reason = F("ota.ssl is not a boolean"); return result; } - } else if (iSetting->isConstChar()) { - HomieSetting* setting = static_cast*>(iSetting); - if (settingsObject->containsKey(setting->getName())) { - if (!(*settingsObject)[setting->getName()].is()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is not a const char*")); - return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting does not pass the validator function")); + if (object["ota"]["ssl"]) { + if (object["ota"].as().containsKey("fingerprint") && !object["ota"]["fingerprint"].is()) { + result.reason = F("ota.fingerprint is not a string"); return result; } - } else if (setting->isRequired()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is missing")); - return result; } } } diff --git a/src/Homie/Helpers.hpp b/src/Homie/Helpers.hpp index e1d0f6db..ddb0f93f 100644 --- a/src/Homie/Helpers.hpp +++ b/src/Homie/Helpers.hpp @@ -2,30 +2,35 @@ #include "Arduino.h" +#include #include #include "Limits.hpp" -#include "../HomieSetting.hpp" namespace HomieInternals { -struct ConfigValidationResult { - bool valid; - String reason; -}; + struct ConfigValidationResult { + bool valid; + const __FlashStringHelper* reason; + }; -class Helpers { - public: - static void generateDeviceId(); - static const char* getDeviceId(); - static uint8_t rssiToPercentage(int32_t rssi); - static ConfigValidationResult validateConfig(const JsonObject& object); + struct MdnsQueryResult { + bool success; + IPAddress ip; + unsigned int port; + }; - private: - static char _deviceId[8 + 1]; + class Helpers { + public: + static void generateDeviceId(); + static const char* getDeviceId(); + static MdnsQueryResult mdnsQuery(const char* service); + static ConfigValidationResult validateConfig(const JsonObject& object); - static ConfigValidationResult _validateConfigRoot(const JsonObject& object); - static ConfigValidationResult _validateConfigWifi(const JsonObject& object); - static ConfigValidationResult _validateConfigMqtt(const JsonObject& object); - static ConfigValidationResult _validateConfigOta(const JsonObject& object); - static ConfigValidationResult _validateConfigSettings(const JsonObject& object); -}; -} // namespace HomieInternals + private: + static char _deviceId[8 + 1]; + + static ConfigValidationResult _validateConfigRoot(const JsonObject& object); + static ConfigValidationResult _validateConfigWifi(const JsonObject& object); + static ConfigValidationResult _validateConfigMqtt(const JsonObject& object); + static ConfigValidationResult _validateConfigOta(const JsonObject& object); + }; +} diff --git a/src/Homie/Limits.hpp b/src/Homie/Limits.hpp index 76edf40f..1264faee 100644 --- a/src/Homie/Limits.hpp +++ b/src/Homie/Limits.hpp @@ -3,24 +3,31 @@ #include namespace HomieInternals { - const uint16_t MAX_JSON_CONFIG_FILE_SIZE = 1000; - const uint16_t MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE = JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(10); // Max 5 elements at root, 2 elements in nested, etc... the last 10 means 10 custom settings max + const unsigned int MAX_JSON_CONFIG_FILE_BUFFER_SIZE = 1000; + const unsigned int MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE = JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(8) + JSON_OBJECT_SIZE(6); // Max 5 elements at root, 2 elements in nested, etc... - const uint8_t MAX_WIFI_SSID_LENGTH = 32 + 1; - const uint8_t MAX_WIFI_PASSWORD_LENGTH = 64 + 1; - const uint16_t MAX_HOSTNAME_LENGTH = 255 + 1; + const unsigned char MAX_WIFI_SSID_LENGTH = 32 + 1; + const unsigned char MAX_WIFI_PASSWORD_LENGTH = 63 + 1; + const unsigned char MAX_HOSTNAME_LENGTH = sizeof("super.long.domain-name.superhost.com"); + const unsigned char MAX_FINGERPRINT_LENGTH = 59 + 1; - const uint8_t MAX_MQTT_CREDS_LENGTH = 32 + 1; - const uint8_t MAX_MQTT_BASE_TOPIC_LENGTH = sizeof("shared-broker/username-lolipop/homie/sensors/"); + const unsigned char MAX_MQTT_CREDS_LENGTH = 32 + 1; + const unsigned char MAX_MQTT_BASE_TOPIC_LENGTH = sizeof("shared-broker/username-lolipop/homie/sensors/"); + const unsigned char MAX_OTA_PATH_LENGTH = sizeof("/virtual-host/long-path/ota"); - const uint8_t MAX_FRIENDLY_NAME_LENGTH = sizeof("My awesome friendly name of the living room"); - const uint8_t MAX_DEVICE_ID_LENGTH = sizeof("my-awesome-device-id-living-room"); + const unsigned char MAX_FRIENDLY_NAME_LENGTH = sizeof("My awesome friendly name of the living room"); + const unsigned char MAX_DEVICE_ID_LENGTH = sizeof("my-awesome-device-id-living-room"); - const uint8_t MAX_BRAND_LENGTH = MAX_WIFI_SSID_LENGTH - sizeof("-0123abcd") + 1; - const uint8_t MAX_FIRMWARE_NAME_LENGTH = sizeof("my-awesome-home-firmware-name"); - const uint8_t MAX_FIRMWARE_VERSION_LENGTH = sizeof("v1.0.0-alpha+001"); + const unsigned char MAX_BRAND_LENGTH = MAX_WIFI_SSID_LENGTH - sizeof("-0123abcd") + 1; + const unsigned char MAX_FIRMWARE_NAME_LENGTH = sizeof("my-awesome-home-firmware-name"); + const unsigned char MAX_FIRMWARE_VERSION_LENGTH = sizeof("v1.0.0-alpha+001"); - const uint8_t MAX_NODE_ID_LENGTH = sizeof("my-super-awesome-node-id"); - const uint8_t MAX_NODE_TYPE_LENGTH = sizeof("my-super-awesome-type"); - const uint8_t MAX_NODE_PROPERTY_LENGTH = sizeof("my-super-awesome-property"); -} // namespace HomieInternals + const unsigned char MAX_NODE_ID_LENGTH = sizeof("my-super-awesome-node-id"); + const unsigned char MAX_NODE_TYPE_LENGTH = sizeof("my-super-awesome-type"); + const unsigned char MAX_NODE_PROPERTY_LENGTH = sizeof("my-super-awesome-property"); + + const unsigned char MAX_REGISTERED_NODES_COUNT = 5; + const unsigned char MAX_SUBSCRIPTIONS_COUNT_PER_NODE = 5; + + const unsigned char TOPIC_BUFFER_LENGTH = MAX_MQTT_BASE_TOPIC_LENGTH + MAX_DEVICE_ID_LENGTH + 1 + MAX_NODE_ID_LENGTH + 1 + MAX_NODE_PROPERTY_LENGTH + 4 + 1; +} diff --git a/src/Homie/Logger.cpp b/src/Homie/Logger.cpp index 1dc1bde2..5cae3b2f 100644 --- a/src/Homie/Logger.cpp +++ b/src/Homie/Logger.cpp @@ -4,25 +4,19 @@ using namespace HomieInternals; Logger::Logger() : _loggingEnabled(true) -, _printer(&Serial) { +{ } -void Logger::setLogging(bool enable) { - _loggingEnabled = enable; -} - -void Logger::setPrinter(Print* printer) { - _printer = printer; +bool Logger::isEnabled() { + return this->_loggingEnabled; } -void Logger::logln() const { - if (_loggingEnabled) { - _printer->println(); - } +void Logger::setLogging(bool enable) { + this->_loggingEnabled = enable; } -void Logger::flush() const { - if (_loggingEnabled && _printer == &Serial) { - static_cast(_printer)->flush(); +void Logger::logln() { + if (this->_loggingEnabled) { + Serial.println(); } } diff --git a/src/Homie/Logger.hpp b/src/Homie/Logger.hpp index 147f08f0..db559cd7 100644 --- a/src/Homie/Logger.hpp +++ b/src/Homie/Logger.hpp @@ -3,26 +3,24 @@ #include "Arduino.h" namespace HomieInternals { -class Logger { - public: - Logger(); - void setPrinter(Print* printer); - void setLogging(bool enable); - template void log(T value) const { - if (_loggingEnabled) { - _printer->print(value); - } - } - template void logln(T value) const { - if (_loggingEnabled) { - _printer->println(value); - } - } - void logln() const; - void flush() const; + class Logger { + public: + Logger(); + void setLogging(bool enable); + bool isEnabled(); + template void log(T value) { + if (this->_loggingEnabled) { + Serial.print(value); + } + } + template void logln(T value) { + if (this->_loggingEnabled) { + Serial.println(value); + } + } + void logln(); - private: - bool _loggingEnabled; - Print* _printer; -}; -} // namespace HomieInternals + private: + bool _loggingEnabled; + }; +} diff --git a/src/Homie/MqttClient.cpp b/src/Homie/MqttClient.cpp new file mode 100644 index 00000000..ef4bfc3a --- /dev/null +++ b/src/Homie/MqttClient.cpp @@ -0,0 +1,120 @@ +#include "MqttClient.hpp" + +using namespace HomieInternals; + +MqttClient::MqttClient() +: _interface(nullptr) +, _topicBuffer {'\0'} +, _secure(false) +, _host() +, _port(0) +, _fingerprint() +, _subscribeWithoutLoop(0) +{ +} + +MqttClient::~MqttClient() { +} + +void MqttClient::attachInterface(Interface* interface) { + this->_interface = interface; +} + +void MqttClient::initMqtt(bool secure) { + if (secure) { + this->_pubSubClient.setClient(this->_wifiClientSecure); + } else { + this->_pubSubClient.setClient(this->_wifiClient); + } + + this->_secure = secure; + + this->_pubSubClient.setCallback(std::bind(&MqttClient::_callback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +} + +char* MqttClient::getTopicBuffer() { + return this->_topicBuffer; +} + +void MqttClient::setCallback(std::function callback) { + _userCallback = callback; +} + +void MqttClient::setServer(const char* host, unsigned int port, const char* fingerprint) { + this->_host = host; + this->_port = port; + this->_fingerprint = fingerprint; + this->_pubSubClient.setServer(this->_host, port); +} + +bool MqttClient::connect(const char* clientId, const char* willMessage, unsigned char willQos, bool willRetain, bool auth, const char* username, const char* password) { + this->_wifiClient.stop(); // Ensure buffers are cleaned, otherwise exception + this->_wifiClientSecure.stop(); + + if (this->_secure && !(strcmp_P(this->_fingerprint, PSTR("")) == 0)) { + this->_interface->logger->logln(F("Checking certificate")); + if (!this->_wifiClientSecure.connect(this->_host, this->_port)) { + this->_wifiClientSecure.stop(); + return false; + } + + if (!this->_wifiClientSecure.verify(this->_fingerprint, this->_host)) { + this->_interface->logger->logln(F("✖ MQTT SSL certificate mismatch")); + this->_wifiClientSecure.stop(); + return false; + } + + this->_wifiClientSecure.stop(); + } + + bool result; + if (auth) { + result = this->_pubSubClient.connect(clientId, username, password, this->_topicBuffer, willQos, willRetain, willMessage); + } else { + result = this->_pubSubClient.connect(clientId, this->_topicBuffer, willQos, willRetain, willMessage); + } + + return result; +} + +int MqttClient::getState() { + return this->_pubSubClient.state(); +} + +void MqttClient::disconnect() { + this->_pubSubClient.disconnect(); +} + +bool MqttClient::publish(const char* message, bool retained) { + return this->_pubSubClient.publish(this->_topicBuffer, message, retained); +} + +bool MqttClient::subscribe(unsigned char qos) { + if (this->_subscribeWithoutLoop >= 5) { + this->loop(); // see knolleary/pubsublient#98 + } + + return this->_pubSubClient.subscribe(this->_topicBuffer, qos); +} + +void MqttClient::loop() { + this->_pubSubClient.loop(); + this->_subscribeWithoutLoop = 0; +} + +bool MqttClient::connected() { + return this->_pubSubClient.connected(); +} + +void MqttClient::_callback(char* topic, unsigned char* payload, unsigned int length) { + char buf[128]; + for (unsigned int i = 0; i < length; i++) { + char tempString[2]; + tempString[0] = (char)payload[i]; + tempString[1] = '\0'; + if (i == 0) strcpy(buf, tempString); + else strcat(buf, tempString); + } + + this->_userCallback(topic, buf); +} diff --git a/src/Homie/MqttClient.hpp b/src/Homie/MqttClient.hpp new file mode 100644 index 00000000..40ddeb5e --- /dev/null +++ b/src/Homie/MqttClient.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include "Datatypes/Interface.hpp" +#include "Logger.hpp" +#include "Constants.hpp" +#include "Limits.hpp" + +namespace HomieInternals { + class MqttClient { + public: + MqttClient(); + ~MqttClient(); + + void attachInterface(Interface* interface); + void initMqtt(bool secure); + char* getTopicBuffer(); + void setCallback(std::function callback); + void setServer(const char* host, unsigned int port, const char* fingerprint); + bool connect(const char* clientId, const char* willMessage, unsigned char willQos, bool willRetain, bool auth = false, const char* username = "", const char* password = ""); + int getState(); + void disconnect(); + bool publish(const char* message, bool retained); + bool subscribe(unsigned char qos); + void loop(); + bool connected(); + + private: + Interface* _interface; + WiFiClient _wifiClient; + WiFiClientSecure _wifiClientSecure; + PubSubClient _pubSubClient; + char _topicBuffer[TOPIC_BUFFER_LENGTH]; + bool _secure; + const char* _host; + unsigned int _port; + const char* _fingerprint; + unsigned char _subscribeWithoutLoop; + + void _callback(char* topic, unsigned char* payload, unsigned int length); + std::function _userCallback; + }; +} diff --git a/src/Homie/Strings.hpp b/src/Homie/Strings.hpp index 99806064..e8a06c9f 100644 --- a/src/Homie/Strings.hpp +++ b/src/Homie/Strings.hpp @@ -3,7 +3,7 @@ namespace HomieInternals { // config mode - const char PROGMEM_CONFIG_CORS[] PROGMEM = "HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: PUT\r\nAccess-Control-Allow-Headers: Content-Type, Origin, Referer, User-Agent\r\n\r\n"; + const char PROGMEM_CONFIG_CORS[] PROGMEM = "HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: PUT\r\nAccess-Control-Allow-Headers: Content-Type\r\n\r\n"; const char PROGMEM_CONFIG_APPLICATION_JSON[] PROGMEM = "application/json"; const char PROGMEM_CONFIG_JSON_FAILURE_BEGINNING[] PROGMEM = "{\"success\":false,\"error\":\""; const char PROGMEM_CONFIG_NETWORKS_FAILURE[] PROGMEM = "{\"error\": \"Initial Wi-Fi scan not finished yet\"}"; diff --git a/src/Homie/Timer.cpp b/src/Homie/Timer.cpp index 151c71ac..6ba4cc87 100644 --- a/src/Homie/Timer.cpp +++ b/src/Homie/Timer.cpp @@ -5,27 +5,28 @@ using namespace HomieInternals; Timer::Timer() : _initialTime(0) , _interval(0) -, _tickAtBeginning(false) { +, _tickAtBeginning(false) +{ } -void Timer::setInterval(uint32_t interval, bool tickAtBeginning) { - _interval = interval; - _tickAtBeginning = tickAtBeginning; +void Timer::setInterval(unsigned long interval, bool tickAtBeginning) { + this->_interval = interval; + this->_tickAtBeginning = tickAtBeginning; - if (!tickAtBeginning) _initialTime = millis(); + if (!tickAtBeginning) this->_initialTime = millis(); } -bool Timer::check() const { - if (_tickAtBeginning && _initialTime == 0) return true; - if (millis() - _initialTime >= _interval) return true; +bool Timer::check() { + if (this->_tickAtBeginning && this->_initialTime == 0) return true; + if (millis() - this->_initialTime >= this->_interval) return true; return false; } void Timer::reset() { - _initialTime = 0; + this->_initialTime = 0; } void Timer::tick() { - _initialTime = millis(); + this->_initialTime = millis(); } diff --git a/src/Homie/Timer.hpp b/src/Homie/Timer.hpp index dd1ce1df..82919f9d 100644 --- a/src/Homie/Timer.hpp +++ b/src/Homie/Timer.hpp @@ -3,17 +3,17 @@ #include "Arduino.h" namespace HomieInternals { -class Timer { - public: - Timer(); - void setInterval(uint32_t interval, bool tickAtBeginning = true); - bool check() const; - void tick(); - void reset(); + class Timer { + public: + Timer(); + void setInterval(unsigned long interval, bool tickAtBeginning = true); + bool check(); + void tick(); + void reset(); - private: - uint32_t _initialTime; - uint32_t _interval; - bool _tickAtBeginning; -}; -} // namespace HomieInternals + private: + unsigned long _initialTime; + unsigned long _interval; + bool _tickAtBeginning; + }; +} diff --git a/src/Homie/Uptime.cpp b/src/Homie/Uptime.cpp index a73e3234..dd773b62 100644 --- a/src/Homie/Uptime.cpp +++ b/src/Homie/Uptime.cpp @@ -4,15 +4,16 @@ using namespace HomieInternals; Uptime::Uptime() : _seconds(0) -, _lastTick(0) { +, _lastTick(0) +{ } void Uptime::update() { - uint32_t now = millis(); - _seconds += (now - _lastTick) / 1000UL; - _lastTick = now; + unsigned long now = millis(); + this->_seconds += (now - this->_lastTick) / 1000UL; + this->_lastTick = now; } -uint32_t Uptime::getSeconds() const { - return _seconds; +unsigned long Uptime::getSeconds() { + return this->_seconds; } diff --git a/src/Homie/Uptime.hpp b/src/Homie/Uptime.hpp index 4fde51b1..3a3d20de 100644 --- a/src/Homie/Uptime.hpp +++ b/src/Homie/Uptime.hpp @@ -3,14 +3,14 @@ #include "Arduino.h" namespace HomieInternals { -class Uptime { - public: - Uptime(); - void update(); - uint32_t getSeconds() const; + class Uptime { + public: + Uptime(); + void update(); + unsigned long getSeconds(); - private: - uint32_t _seconds; - uint32_t _lastTick; -}; -} // namespace HomieInternals + private: + unsigned long _seconds; + unsigned long _lastTick; + }; +} diff --git a/src/HomieEvent.hpp b/src/HomieEvent.hpp index e0bfd40d..ce09de36 100644 --- a/src/HomieEvent.hpp +++ b/src/HomieEvent.hpp @@ -1,16 +1,12 @@ #pragma once -enum class HomieEvent : uint8_t { - STANDALONE_MODE = 1, - CONFIGURATION_MODE, - NORMAL_MODE, - OTA_STARTED, - OTA_SUCCESSFUL, - OTA_FAILED, - ABOUT_TO_RESET, - WIFI_CONNECTED, - WIFI_DISCONNECTED, - MQTT_CONNECTED, - MQTT_DISCONNECTED, - READY_FOR_SLEEP +enum HomieEvent : unsigned char { + HOMIE_CONFIGURATION_MODE = 1, + HOMIE_NORMAL_MODE, + HOMIE_OTA_MODE, + HOMIE_ABOUT_TO_RESET, + HOMIE_WIFI_CONNECTED, + HOMIE_WIFI_DISCONNECTED, + HOMIE_MQTT_CONNECTED, + HOMIE_MQTT_DISCONNECTED }; diff --git a/src/HomieNode.cpp b/src/HomieNode.cpp index c12795a3..5891e8b5 100644 --- a/src/HomieNode.cpp +++ b/src/HomieNode.cpp @@ -1,58 +1,59 @@ #include "HomieNode.hpp" -#include "Homie.hpp" using namespace HomieInternals; -std::vector HomieNode::nodes; - -PropertyInterface::PropertyInterface() -: _property(nullptr) { -} - -void PropertyInterface::settable(PropertyInputHandler inputHandler) { - _property->settable(inputHandler); -} - -PropertyInterface& PropertyInterface::setProperty(Property* property) { - _property = property; - return *this; -} - -HomieNode::HomieNode(const char* id, const char* type, NodeInputHandler inputHandler) +HomieNode::HomieNode(const char* id, const char* type, NodeInputHandler inputHandler, bool subscribeToAll) : _id(id) , _type(type) -, _properties() +, _subscriptionsCount(0) +, _subscribeToAll(subscribeToAll) , _inputHandler(inputHandler) { if (strlen(id) + 1 > MAX_NODE_ID_LENGTH || strlen(type) + 1 > MAX_NODE_TYPE_LENGTH) { Serial.println(F("✖ HomieNode(): either the id or type string is too long")); - Serial.flush(); abort(); } - Homie._checkBeforeSetup(F("HomieNode::HomieNode")); - HomieNode::nodes.push_back(this); + this->_id = id; + this->_type = type; } -PropertyInterface& HomieNode::advertise(const char* property) { - Property* propertyObject = new Property(property); +void HomieNode::subscribe(const char* property, PropertyInputHandler inputHandler) { + if (strlen(property) + 1 > MAX_NODE_PROPERTY_LENGTH) { + Serial.println(F("✖ subscribe(): the property string is too long")); + abort(); + } - _properties.push_back(propertyObject); + if (this->_subscriptionsCount > MAX_SUBSCRIPTIONS_COUNT_PER_NODE) { + Serial.println(F("✖ subscribe(): the max subscription count has been reached")); + abort(); + } - return _propertyInterface.setProperty(propertyObject); + Subscription subscription; + strcpy(subscription.property, property); + subscription.inputHandler = inputHandler; + this->_subscriptions[this->_subscriptionsCount++] = subscription; } -PropertyInterface& HomieNode::advertiseRange(const char* property, uint16_t lower, uint16_t upper) { - Property* propertyObject = new Property(property, true, lower, upper); +const char* HomieNode::getId() const { + return this->_id; +} - _properties.push_back(propertyObject); +const char* HomieNode::getType() const { + return this->_type; +} + +const Subscription* HomieNode::getSubscriptions() const { + return this->_subscriptions; +} - return _propertyInterface.setProperty(propertyObject); +unsigned char HomieNode::getSubscriptionsCount() const { + return this->_subscriptionsCount; } -bool HomieNode::handleInput(String const &property, HomieRange range, String const &value) { - return _inputHandler(property, range, value); +bool HomieNode::getSubscribeToAll() const { + return this->_subscribeToAll; } -const std::vector& HomieNode::getProperties() const { - return _properties; +NodeInputHandler HomieNode::getInputHandler() const { + return this->_inputHandler; } diff --git a/src/HomieNode.hpp b/src/HomieNode.hpp index e216ea8c..77578478 100644 --- a/src/HomieNode.hpp +++ b/src/HomieNode.hpp @@ -1,94 +1,37 @@ #pragma once -#include -#include #include "Arduino.h" +#include "Homie/Datatypes/Subscription.hpp" #include "Homie/Datatypes/Callbacks.hpp" #include "Homie/Limits.hpp" -#include "HomieRange.hpp" - -class HomieNode; namespace HomieInternals { -class HomieClass; -class Property; -class BootNormal; -class BootConfig; - -class PropertyInterface { - friend ::HomieNode; - - public: - PropertyInterface(); - - void settable(PropertyInputHandler inputHandler = [](HomieRange range, String value) { return false; }); - - private: - PropertyInterface& setProperty(Property* property); - - Property* _property; -}; - -class Property { - friend BootNormal; - - public: - explicit Property(const char* id, bool range = false, uint16_t lower = 0, uint16_t upper = 0) { _id = strdup(id); _range = range; _lower = lower; _upper = upper; } - void settable(PropertyInputHandler inputHandler) { _settable = true; _inputHandler = inputHandler; } - - private: - const char* getProperty() const { return _id; } - bool isSettable() const { return _settable; } - bool isRange() const { return _range; } - uint16_t getLower() const { return _lower; } - uint16_t getUpper() const { return _upper; } - PropertyInputHandler getInputHandler() const { return _inputHandler; } - const char* _id; - bool _range; - uint16_t _lower; - uint16_t _upper; - bool _settable; - PropertyInputHandler _inputHandler; -}; -} // namespace HomieInternals + class HomieClass; + class BootNormal; + class BootConfig; +} class HomieNode { friend HomieInternals::HomieClass; friend HomieInternals::BootNormal; friend HomieInternals::BootConfig; - - public: - HomieNode(const char* id, const char* type, HomieInternals::NodeInputHandler nodeInputHandler = [](String property, HomieRange range, String value) { return false; }); - - const char* getId() const { return _id; } - const char* getType() const { return _type; } - - HomieInternals::PropertyInterface& advertise(const char* property); - HomieInternals::PropertyInterface& advertiseRange(const char* property, uint16_t lower, uint16_t upper); - - protected: - virtual void setup() {} - virtual void loop() {} - virtual void onReadyToOperate() {} - virtual bool handleInput(String const &property, HomieRange range, String const &value); - - private: - const std::vector& getProperties() const; - - static HomieNode* find(const char* id) { - for (HomieNode* iNode : HomieNode::nodes) { - if (strcmp(id, iNode->getId()) == 0) return iNode; - } - - return 0; - } - - const char* _id; - const char* _type; - std::vector _properties; - HomieInternals::NodeInputHandler _inputHandler; - - HomieInternals::PropertyInterface _propertyInterface; - - static std::vector nodes; + public: + HomieNode(const char* id, const char* type, HomieInternals::NodeInputHandler nodeInputHandler = [](String property, String value) { return false; }, bool subscribeToAll = false); + + void subscribe(const char* property, HomieInternals::PropertyInputHandler inputHandler = [](String value) { return false; }); + + private: + const char* getId() const; + const char* getType() const; + const HomieInternals::Subscription* getSubscriptions() const; + unsigned char getSubscriptionsCount() const; + bool getSubscribeToAll() const; + HomieInternals::NodeInputHandler getInputHandler() const; + + const char* _id; + const char* _type; + HomieInternals::Subscription _subscriptions[HomieInternals::MAX_SUBSCRIPTIONS_COUNT_PER_NODE]; + unsigned char _subscriptionsCount; + bool _subscribeToAll; + HomieInternals::NodeInputHandler _inputHandler; }; diff --git a/src/HomieRange.hpp b/src/HomieRange.hpp deleted file mode 100644 index c4eff8d3..00000000 --- a/src/HomieRange.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -struct HomieRange { - bool isRange; - uint16_t index; -}; diff --git a/src/HomieSetting.cpp b/src/HomieSetting.cpp deleted file mode 100644 index 2fb8ae77..00000000 --- a/src/HomieSetting.cpp +++ /dev/null @@ -1,101 +0,0 @@ -#include "HomieSetting.hpp" - -using namespace HomieInternals; - -std::vector IHomieSetting::settings; - -template -HomieSetting::HomieSetting(const char* name, const char* description) -: _name(name) -, _description(description) -, _required(true) -, _provided(false) -, _value() -, _validator([](T candidate) { return true; }) { - IHomieSetting::settings.push_back(this); -} - -template -T HomieSetting::get() const { - return _value; -} - -template -bool HomieSetting::wasProvided() const { - return _provided; -} - -template -HomieSetting& HomieSetting::setDefaultValue(T defaultValue) { - _value = defaultValue; - _required = false; - return *this; -} - -template -HomieSetting& HomieSetting::setValidator(std::function validator) { - _validator = validator; - return *this; -} - -template -bool HomieSetting::validate(T candidate) const { - return _validator(candidate); -} - -template -void HomieSetting::set(T value) { - _value = value; - _provided = true; -} - -template -bool HomieSetting::isRequired() const { - return _required; -} - -template -const char* HomieSetting::getName() const { - return _name; -} - -template -const char* HomieSetting::getDescription() const { - return _description; -} - -template -bool HomieSetting::isBool() const { return false; } - -template -bool HomieSetting::isUnsignedLong() const { return false; } - -template -bool HomieSetting::isLong() const { return false; } - -template -bool HomieSetting::isDouble() const { return false; } - -template -bool HomieSetting::isConstChar() const { return false; } - -template<> -bool HomieSetting::isBool() const { return true; } - -template<> -bool HomieSetting::isUnsignedLong() const { return true; } - -template<> -bool HomieSetting::isLong() const { return true; } - -template<> -bool HomieSetting::isDouble() const { return true; } - -template<> -bool HomieSetting::isConstChar() const { return true; } - -template class HomieSetting; // Needed because otherwise undefined reference to -template class HomieSetting; -template class HomieSetting; -template class HomieSetting; -template class HomieSetting; diff --git a/src/HomieSetting.hpp b/src/HomieSetting.hpp deleted file mode 100644 index b11befee..00000000 --- a/src/HomieSetting.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include -#include -#include "Arduino.h" - -namespace HomieInternals { -class Config; -class Helpers; -class BootConfig; - -class IHomieSetting { - public: - IHomieSetting() {} - - virtual bool isBool() const { return false; } - virtual bool isUnsignedLong() const { return false; } - virtual bool isLong() const { return false; } - virtual bool isDouble() const { return false; } - virtual bool isConstChar() const { return false; } - - static std::vector settings; -}; -} // namespace HomieInternals - -template -class HomieSetting : public HomieInternals::IHomieSetting { - friend HomieInternals::Config; - friend HomieInternals::Helpers; - friend HomieInternals::BootConfig; - - public: - HomieSetting(const char* name, const char* description); - T get() const; - bool wasProvided() const; - HomieSetting& setDefaultValue(T defaultValue); - HomieSetting& setValidator(std::function validator); - - private: - const char* _name; - const char* _description; - bool _required; - bool _provided; - T _value; - std::function _validator; - - bool validate(T candidate) const; - void set(T value); - bool isRequired() const; - const char* getName() const; - const char* getDescription() const; - - bool isBool() const; - bool isUnsignedLong() const; - bool isLong() const; - bool isDouble() const; - bool isConstChar() const; -}; From 528a4f77c6371366847ebf4def6aba942dfd0c4c Mon Sep 17 00:00:00 2001 From: Marvin Roger Date: Thu, 22 Sep 2016 10:53:45 +0200 Subject: [PATCH 02/51] Use new name of Json library (#144) --- library.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.json b/library.json index 48284a37..683c225a 100644 --- a/library.json +++ b/library.json @@ -17,7 +17,7 @@ "platforms": "espressif", "dependencies": [ { - "name": "Json", + "name": "ArduinoJson", "authors": "Benoit Blanchon", "frameworks": "arduino" }, From dad284b235c5540897585e81c56be58136121b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Blanchon?= Date: Thu, 29 Jun 2017 09:36:47 +0200 Subject: [PATCH 03/51] :bug: Fix compatibility with ArduinoJson 5.11.0 (#362) See issue #361 and https://github.com/bblanchon/ArduinoJson/issues/541. --- src/Homie/Boot/BootConfig.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index e601175c..22d253c0 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -66,7 +66,7 @@ void BootConfig::setup() { } void BootConfig::_generateNetworksJson() { - DynamicJsonBuffer generatedJsonBuffer = DynamicJsonBuffer(JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(this->_ssidCount) + (this->_ssidCount * JSON_OBJECT_SIZE(3))); // 1 at root, 3 in childrend + DynamicJsonBuffer generatedJsonBuffer(JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(this->_ssidCount) + (this->_ssidCount * JSON_OBJECT_SIZE(3))); // 1 at root, 3 in childrend JsonObject& json = generatedJsonBuffer.createObject(); int jsonLength = 15; // {"networks":[]} @@ -110,7 +110,7 @@ void BootConfig::_generateNetworksJson() { void BootConfig::_onDeviceInfoRequest() { this->_interface->logger->logln(F("Received device info request")); - DynamicJsonBuffer jsonBuffer = DynamicJsonBuffer(JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(2) + JSON_ARRAY_SIZE(this->_interface->registeredNodesCount) + (this->_interface->registeredNodesCount * JSON_OBJECT_SIZE(2))); + DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(2) + JSON_ARRAY_SIZE(this->_interface->registeredNodesCount) + (this->_interface->registeredNodesCount * JSON_OBJECT_SIZE(2))); int jsonLength = 82; // {"device_id":"","homie_version":"","firmware":{"name":"","version":""},"nodes":[]} JsonObject& json = jsonBuffer.createObject(); jsonLength += strlen(Helpers::getDeviceId()); From f06f111957dad48c2930df4c75b716f132cbf9d6 Mon Sep 17 00:00:00 2001 From: Nash Kaminski Date: Wed, 15 Nov 2017 03:53:33 -0600 Subject: [PATCH 04/51] :bug: Backport fix for issue #178 into master/stable branch (#416) * Backport config parsing fix, a964b78 from upstream * bodyCharArray no longer exists so do not try to free it --- src/Homie/Boot/BootConfig.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 22d253c0..8592a021 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -163,8 +163,11 @@ void BootConfig::_onConfigRequest() { } StaticJsonBuffer parseJsonBuffer; - char* bodyCharArray = strdup(this->_http.arg("plain").c_str()); - JsonObject& parsedJson = parseJsonBuffer.parseObject(bodyCharArray); // do not use plain String, else fails + size_t bodyLength = _http.arg("plain").length(); + std::unique_ptr bodyString(new char[bodyLength + 1]); + memcpy(bodyString.get(), _http.arg("plain").c_str(), bodyLength); + bodyString.get()[bodyLength] = '\0'; + JsonObject& parsedJson = parseJsonBuffer.parseObject(bodyString.get()); // workaround, cannot pass raw String otherwise JSON parsing fails randomly if (!parsedJson.success()) { this->_interface->logger->logln(F("✖ Invalid or too big JSON")); String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); @@ -174,7 +177,6 @@ void BootConfig::_onConfigRequest() { } ConfigValidationResult configValidationResult = Helpers::validateConfig(parsedJson); - free(bodyCharArray); if (!configValidationResult.valid) { this->_interface->logger->log(F("✖ Config file is not valid, reason: ")); this->_interface->logger->logln(configValidationResult.reason); From 6b63e573b553eafeddf37b390d7e7ca96dc3218a Mon Sep 17 00:00:00 2001 From: timpur Date: Sat, 23 Dec 2017 10:56:12 +1100 Subject: [PATCH 05/51] Refactor + Config changes - Refactored Firmware Name + Version + Brand - Refactor of Config + Added Write Validation + Load Validation + Moved all Validating to the Config - Remove Settings item via set to null + Revert if save of config fails --- src/Homie.cpp | 21 +-- src/Homie.hpp | 4 +- src/Homie/Boot/BootConfig.cpp | 129 +++++++------- src/Homie/Boot/BootNormal.cpp | 2 +- src/Homie/Config.cpp | 244 ++++++++++++++++++--------- src/Homie/Config.hpp | 16 +- src/Homie/Datatypes/ConfigStruct.hpp | 2 +- src/Homie/Datatypes/Interface.hpp | 2 +- src/Homie/Limits.hpp | 12 +- src/Homie/Strings.hpp | 6 +- src/Homie/Utils/DeviceId.cpp | 4 +- src/Homie/Utils/DeviceId.hpp | 4 +- src/Homie/Utils/Helpers.cpp | 10 +- src/Homie/Utils/Helpers.hpp | 2 + src/Homie/Utils/Validation.cpp | 27 +-- src/Homie/Utils/Validation.hpp | 3 + 16 files changed, 295 insertions(+), 193 deletions(-) diff --git a/src/Homie.cpp b/src/Homie.cpp index 6b769806..e6feddb8 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -6,7 +6,7 @@ HomieClass::HomieClass() : _setupCalled(false) , _firmwareSet(false) , __HOMIE_SIGNATURE("\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25") { - strlcpy(Interface::get().brand, DEFAULT_BRAND, MAX_BRAND_LENGTH); + strlcpy(Interface::get().brand, DEFAULT_BRAND, MAX_BRAND_STRING_LENGTH); Interface::get().bootMode = HomieBootMode::UNDEFINED; Interface::get().configurationAp.secured = false; Interface::get().led.enabled = true; @@ -203,27 +203,27 @@ HomieClass& HomieClass::setConfigurationApPassword(const char* password) { void HomieClass::__setFirmware(const char* name, const char* version) { _checkBeforeSetup(F("setFirmware")); - if (strlen(name) + 1 - 10 > MAX_FIRMWARE_NAME_LENGTH || strlen(version) + 1 - 10 > MAX_FIRMWARE_VERSION_LENGTH) { + if (strlen(name) + 1 > MAX_FIRMWARE_NAME_LENGTH || strlen(version) + 1 > MAX_FIRMWARE_VERSION_LENGTH) { Helpers::abort(F("✖ setFirmware(): either the name or version string is too long")); return; // never reached, here for clarity } - strncpy(Interface::get().firmware.name, name + 5, strlen(name) - 10); - Interface::get().firmware.name[strlen(name) - 10] = '\0'; - strncpy(Interface::get().firmware.version, version + 5, strlen(version) - 10); - Interface::get().firmware.version[strlen(version) - 10] = '\0'; + strncpy(Interface::get().firmware.name, name, MAX_FIRMWARE_NAME_LENGTH); + Interface::get().firmware.name[strlen(name)] = '\0'; + strncpy(Interface::get().firmware.version, version, MAX_FIRMWARE_VERSION_LENGTH); + Interface::get().firmware.version[strlen(version)] = '\0'; _firmwareSet = true; } void HomieClass::__setBrand(const char* brand) const { _checkBeforeSetup(F("setBrand")); - if (strlen(brand) + 1 - 10 > MAX_BRAND_LENGTH) { + if (strlen(brand) + 1 > MAX_BRAND_STRING_LENGTH) { Helpers::abort(F("✖ setBrand(): the brand string is too long")); return; // never reached, here for clarity } - strncpy(Interface::get().brand, brand + 5, strlen(brand) - 10); - Interface::get().brand[strlen(brand) - 10] = '\0'; + strncpy(Interface::get().brand, brand, MAX_BRAND_STRING_LENGTH); + Interface::get().brand[strlen(brand)] = '\0'; } void HomieClass::reset() { @@ -276,6 +276,7 @@ HomieClass& HomieClass::setLoopFunction(const OperationFunction& function) { HomieClass& HomieClass::setHomieBootMode(HomieBootMode bootMode) { _checkBeforeSetup(F("setHomieBootMode")); + Interface::get().bootMode = bootMode; return *this; } @@ -286,7 +287,7 @@ HomieClass& HomieClass::setHomieBootModeOnNextBoot(HomieBootMode bootMode) { } bool HomieClass::isConfigured() { - return Interface::get().getConfig().load(); + return Interface::get().getConfig().isValid(); } bool HomieClass::isConnected() { diff --git a/src/Homie.hpp b/src/Homie.hpp index 08461d40..f9267441 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -24,8 +24,8 @@ // Define DEBUG for debug -#define Homie_setFirmware(name, version) const char* __FLAGGED_FW_NAME = "\xbf\x84\xe4\x13\x54" name "\x93\x44\x6b\xa7\x75"; const char* __FLAGGED_FW_VERSION = "\x6a\x3f\x3e\x0e\xe1" version "\xb0\x30\x48\xd4\x1a"; Homie.__setFirmware(__FLAGGED_FW_NAME, __FLAGGED_FW_VERSION); -#define Homie_setBrand(brand) const char* __FLAGGED_BRAND = "\xfb\x2a\xf5\x68\xc0" brand "\x6e\x2f\x0f\xeb\x2d"; Homie.__setBrand(__FLAGGED_BRAND); +#define Homie_setFirmware(name, version) const char* __FLAGGED_FW_NAME = "\xbf\x84\xe4\x13\x54" name "\x93\x44\x6b\xa7\x75"; const char* __FLAGGED_FW_VERSION = "\x6a\x3f\x3e\x0e\xe1" version "\xb0\x30\x48\xd4\x1a"; Homie.__setFirmware(name, version); +#define Homie_setBrand(brand) const char* __FLAGGED_BRAND = "\xfb\x2a\xf5\x68\xc0" brand "\x6e\x2f\x0f\xeb\x2d"; Homie.__setBrand(brand); namespace HomieInternals { class HomieClass { diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 4fd44120..88dd6477 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -13,8 +13,7 @@ BootConfig::BootConfig() , _flaggedForReboot(false) , _flaggedForRebootAt(0) , _proxyEnabled(false) - , _apIpStr{ '\0' } -{ + , _apIpStr{ '\0' } { _wifiScanTimer.setInterval(CONFIG_SCAN_INTERVAL); } @@ -33,7 +32,7 @@ void BootConfig::setup() { WiFi.mode(WIFI_AP_STA); char apName[MAX_WIFI_SSID_LENGTH]; - strlcpy(apName, Interface::get().brand, MAX_WIFI_SSID_LENGTH - 1 - MAX_MAC_STRING_LENGTH); + strlcpy(apName, Interface::get().brand, MAX_BRAND_STRING_LENGTH); strcat_P(apName, PSTR("-")); strcat(apName, DeviceId::get()); @@ -92,19 +91,19 @@ void BootConfig::loop() { int8_t scanResult = WiFi.scanComplete(); switch (scanResult) { - case WIFI_SCAN_RUNNING: - return; - case WIFI_SCAN_FAILED: - Interface::get().getLogger() << F("✖ Wi-Fi scan failed") << endl; - _ssidCount = 0; - _wifiScanTimer.reset(); - break; - default: - Interface::get().getLogger() << F("✔ Wi-Fi scan completed") << endl; - _ssidCount = scanResult; - _generateNetworksJson(); - _wifiScanAvailable = true; - break; + case WIFI_SCAN_RUNNING: + return; + case WIFI_SCAN_FAILED: + Interface::get().getLogger() << F("✖ Wi-Fi scan failed") << endl; + _ssidCount = 0; + _wifiScanTimer.reset(); + break; + default: + Interface::get().getLogger() << F("✔ Wi-Fi scan completed") << endl; + _ssidCount = scanResult; + _generateNetworksJson(); + _wifiScanAvailable = true; + break; } _lastWifiScanEnded = true; @@ -148,28 +147,28 @@ void BootConfig::_onWifiStatusRequest(AsyncWebServerRequest *request) { //String json = ""; switch (WiFi.status()) { - case WL_IDLE_STATUS: - status = F("idle"); - break; - case WL_CONNECT_FAILED: - status = F("connect_failed"); - break; - case WL_CONNECTION_LOST: - status = F("connection_lost"); - break; - case WL_NO_SSID_AVAIL: - status = F("no_ssid_available"); - break; - case WL_CONNECTED: - status = F("connected"); - json["local_ip"] = WiFi.localIP().toString(); - break; - case WL_DISCONNECTED: - status = F("disconnected"); - break; - default: - status = F("other"); - break; + case WL_IDLE_STATUS: + status = F("idle"); + break; + case WL_CONNECT_FAILED: + status = F("connect_failed"); + break; + case WL_CONNECTION_LOST: + status = F("connection_lost"); + break; + case WL_NO_SSID_AVAIL: + status = F("no_ssid_available"); + break; + case WL_CONNECTED: + status = F("connected"); + json["local_ip"] = WiFi.localIP().toString(); + break; + case WL_DISCONNECTED: + status = F("disconnected"); + break; + default: + status = F("other"); + break; } json["status"] = status; @@ -209,21 +208,21 @@ void BootConfig::_generateNetworksJson() { jsonNetwork["ssid"] = WiFi.SSID(network); jsonNetwork["rssi"] = WiFi.RSSI(network); switch (WiFi.encryptionType(network)) { - case ENC_TYPE_WEP: - jsonNetwork["encryption"] = "wep"; - break; - case ENC_TYPE_TKIP: - jsonNetwork["encryption"] = "wpa"; - break; - case ENC_TYPE_CCMP: - jsonNetwork["encryption"] = "wpa2"; - break; - case ENC_TYPE_NONE: - jsonNetwork["encryption"] = "none"; - break; - case ENC_TYPE_AUTO: - jsonNetwork["encryption"] = "auto"; - break; + case ENC_TYPE_WEP: + jsonNetwork["encryption"] = "wep"; + break; + case ENC_TYPE_TKIP: + jsonNetwork["encryption"] = "wpa"; + break; + case ENC_TYPE_CCMP: + jsonNetwork["encryption"] = "wpa2"; + break; + case ENC_TYPE_NONE: + jsonNetwork["encryption"] = "none"; + break; + case ENC_TYPE_AUTO: + jsonNetwork["encryption"] = "auto"; + break; } networks.add(jsonNetwork); @@ -288,12 +287,12 @@ void BootConfig::_proxyHttpRequest(AsyncWebServerRequest *request) { String method = ""; switch (request->method()) { - case HTTP_GET: method = F("GET"); break; - case HTTP_PUT: method = F("PUT"); break; - case HTTP_POST: method = F("POST"); break; - case HTTP_DELETE: method = F("DELETE"); break; - case HTTP_OPTIONS: method = F("OPTIONS"); break; - default: break; + case HTTP_GET: method = F("GET"); break; + case HTTP_PUT: method = F("PUT"); break; + case HTTP_POST: method = F("POST"); break; + case HTTP_DELETE: method = F("DELETE"); break; + case HTTP_OPTIONS: method = F("OPTIONS"); break; + default: break; } Interface::get().getLogger() << F("Proxy sent request to destination") << endl; @@ -387,19 +386,13 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { DynamicJsonBuffer parseJsonBuffer(MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE); const char* body = (const char*)(request->_tempObject); JsonObject& parsedJson = parseJsonBuffer.parseObject(body); - if (!parsedJson.success()) { - __SendJSONError(request, F("✖ Invalid or too big JSON")); - return; - } - ConfigValidationResult configValidationResult = Validation::validateConfig(parsedJson); - if (!configValidationResult.valid) { - __SendJSONError(request, String(F("✖ Config file is not valid, reason: ")) + configValidationResult.reason); + ConfigValidationResult configWriteResult = Interface::get().getConfig().write(parsedJson); + if (!configWriteResult.valid) { + __SendJSONError(request, configWriteResult.reason); return; } - Interface::get().getConfig().write(parsedJson); - Interface::get().getLogger() << F("✔ Configured") << endl; request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_JSON_SUCCESS)); diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index 026489c5..bdc40ada 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -812,7 +812,7 @@ bool HomieInternals::BootNormal::__handleConfig(char * topic, char * payload, co && strcmp_P(_mqttTopicLevels.get()[3], PSTR("set")) == 0 ) { Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/config/set")), 1, true, ""); - if (Interface::get().getConfig().patch(_mqttPayloadBuffer.get())) { + if (Interface::get().getConfig().patch(_mqttPayloadBuffer.get()).valid) { Interface::get().getLogger() << F("✔ Configuration updated") << endl; _flaggedForReboot = true; Interface::get().getLogger() << F("Flagged for reboot") << endl; diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 0c6d5465..dfc85756 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -8,56 +8,39 @@ Config::Config() , _valid(false) { } +const ConfigStruct& Config::get() const { + return _configStruct; +} + bool Config::_spiffsBegin() { if (!_spiffsBegan) { _spiffsBegan = SPIFFS.begin(); - if (!_spiffsBegan) Interface::get().getLogger() << F("✖ Cannot mount filesystem") << endl; + if (!_spiffsBegan) Interface::get().getLogger() << FPSTR(PROGMEM_CONFIG_SPIFFS_NOT_FOUND) << endl; } return _spiffsBegan; } bool Config::load() { - if (!_spiffsBegin()) { return false; } - _valid = false; - if (!SPIFFS.exists(CONFIG_FILE_PATH)) { - Interface::get().getLogger() << F("✖ ") << CONFIG_FILE_PATH << F(" doesn't exist") << endl; - return false; - } - - File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); - if (!configFile) { - Interface::get().getLogger() << F("✖ Cannot open config file") << endl; - return false; - } - - size_t configSize = configFile.size(); - - if (configSize > MAX_JSON_CONFIG_FILE_SIZE) { - Interface::get().getLogger() << F("✖ Config file too big") << endl; - return false; - } - - char buf[MAX_JSON_CONFIG_FILE_SIZE]; - configFile.readBytes(buf, configSize); - configFile.close(); - buf[configSize] = '\0'; + Interface::get().getLogger() << F("↻ Config file loading") << endl; StaticJsonBuffer jsonBuffer; - JsonObject& parsedJson = jsonBuffer.parseObject(buf); - if (!parsedJson.success()) { - Interface::get().getLogger() << F("✖ Invalid JSON in the config file") << endl; + ConfigValidationResultOBJ loadResult = _loadConfigFile(&jsonBuffer); + if (!loadResult.valid) { + //Interface::get().getLogger() << loadResult.reason << endl; return false; } - - ConfigValidationResult configValidationResult = Validation::validateConfig(parsedJson); - if (!configValidationResult.valid) { - Interface::get().getLogger() << F("✖ Config file is not valid, reason: ") << configValidationResult.reason << endl; + JsonObject& parsedJson = *loadResult.config; + ConfigValidationResult validResult = validateConfig(parsedJson); + if (!validResult.valid) { + //Interface::get().getLogger() << validResult.reason << endl; return false; } + Interface::get().getLogger() << F("✔ Config file loaded and validated") << endl; + const char* reqName = parsedJson["name"]; const char* reqWifiSsid = parsedJson["wifi"]["ssid"]; const char* reqWifiPassword = parsedJson["wifi"]["password"]; @@ -132,7 +115,7 @@ bool Config::load() { _configStruct.deviceStatsInterval = regDeviceStatsInterval; strlcpy(_configStruct.wifi.ssid, reqWifiSsid, MAX_WIFI_SSID_LENGTH); if (reqWifiPassword) strlcpy(_configStruct.wifi.password, reqWifiPassword, MAX_WIFI_PASSWORD_LENGTH); - strlcpy(_configStruct.wifi.bssid, reqWifiBssid, MAX_MAC_STRING_LENGTH + 6); + strlcpy(_configStruct.wifi.bssid, reqWifiBssid, MAX_MAC_STRING_LENGTH); _configStruct.wifi.channel = reqWifiChannel; strlcpy(_configStruct.wifi.ip, reqWifiIp, MAX_IP_STRING_LENGTH); strlcpy(_configStruct.wifi.gw, reqWifiGw, MAX_IP_STRING_LENGTH); @@ -183,24 +166,25 @@ bool Config::load() { return true; } -char* Config::getSafeConfigFile() const { - File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); - size_t configSize = configFile.size(); - - char buf[MAX_JSON_CONFIG_FILE_SIZE]; - configFile.readBytes(buf, configSize); - configFile.close(); - buf[configSize] = '\0'; - +char* Config::getSafeConfigFile() { StaticJsonBuffer jsonBuffer; - JsonObject& parsedJson = jsonBuffer.parseObject(buf); - parsedJson["wifi"].as().remove("password"); - parsedJson["mqtt"].as().remove("username"); - parsedJson["mqtt"].as().remove("password"); + ConfigValidationResultOBJ configLoadResult = _loadConfigFile(&jsonBuffer); + if (!configLoadResult.valid) { + return nullptr; + } + JsonObject& configObject = *configLoadResult.config; + ConfigValidationResult configValidResult = validateConfig(configObject); + if (!configValidResult.valid) { + return nullptr; + } + + configObject["wifi"].as().remove("password"); + configObject["mqtt"].as().remove("username"); + configObject["mqtt"].as().remove("password"); - size_t jsonBufferLength = parsedJson.measureLength() + 1; + size_t jsonBufferLength = configObject.measureLength() + 1; std::unique_ptr jsonString(new char[jsonBufferLength]); - parsedJson.printTo(jsonString.get(), jsonBufferLength); + configObject.printTo(jsonString.get(), jsonBufferLength); return strdup(jsonString.get()); } @@ -243,47 +227,140 @@ HomieBootMode Config::getHomieBootModeOnNextBoot() { } } -void Config::write(const JsonObject& config) { - if (!_spiffsBegin()) { return; } +ConfigValidationResultOBJ Config::_loadConfigFile(StaticJsonBuffer* jsonBuffer) { + ConfigValidationResultOBJ result; + result.valid = false; - SPIFFS.remove(CONFIG_FILE_PATH); + if (!_spiffsBegin()) { + result.reason = FPSTR(PROGMEM_CONFIG_SPIFFS_NOT_FOUND); + return result; + } - File configFile = SPIFFS.open(CONFIG_FILE_PATH, "w"); + if (!SPIFFS.exists(CONFIG_FILE_PATH)) { + String error = F("✖ "); + error.concat(CONFIG_FILE_PATH); + error.concat(F(" doesn't exist")); + Interface::get().getLogger() << error << endl; + result.reason = error; + return result; + } + + File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); if (!configFile) { - Interface::get().getLogger() << F("✖ Cannot open config file") << endl; - return; + Interface::get().getLogger() << FPSTR(PROGMEM_CONFIG_FILE_NOT_FOUND) << endl; + result.reason = FPSTR(PROGMEM_CONFIG_FILE_NOT_FOUND); + return result; } - config.printTo(configFile); + size_t configSize = configFile.size(); + if (configSize > MAX_JSON_CONFIG_FILE_SIZE) { + configFile.close(); + Interface::get().getLogger() << FPSTR(PROGMEM_CONFIG_FILE_TOO_BIG) << endl; + result.reason = FPSTR(PROGMEM_CONFIG_FILE_TOO_BIG); + return result; + } + + char buf[MAX_JSON_CONFIG_FILE_SIZE]; + configFile.readBytes(buf, configSize); configFile.close(); + buf[configSize] = '\0'; + + result.valid = true; + result.config = &jsonBuffer->parseObject(buf); + + return result; } -bool Config::patch(const char* patch) { - if (!_spiffsBegin()) { return false; } +ConfigValidationResult Config::validateConfig(const JsonObject& parsedJson, bool skipValidation) { + ConfigValidationResult result; + result.valid = false; - StaticJsonBuffer patchJsonBuffer; - JsonObject& patchObject = patchJsonBuffer.parseObject(patch); + if (!parsedJson.success()) { + result.reason = F("✖ Invalid or too big JSON"); + return result; + } - if (!patchObject.success()) { - Interface::get().getLogger() << F("✖ Invalid or too big JSON") << endl; - return false; + if (!skipValidation) { + ConfigValidationResult configValidationResult = Validation::validateConfig(parsedJson); + if (!configValidationResult.valid) { + result.reason = String(F("✖ Config file is not valid, reason: ")) + configValidationResult.reason; + return result; + } } - File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); - if (!configFile) { - Interface::get().getLogger() << F("✖ Cannot open config file") << endl; - return false; + result.valid = true; + return result; +} + +ConfigValidationResult Config::write(const JsonObject& newConfig) { + ConfigValidationResult result; + result.valid = false; + + ConfigValidationResult validResult = validateConfig(newConfig); + if (!validResult.valid) { + result.reason = validResult.reason; + return result; } - size_t configSize = configFile.size(); + size_t configSize = newConfig.measureLength(); + if (configSize > MAX_JSON_CONFIG_FILE_SIZE) { + Interface::get().getLogger() << FPSTR(PROGMEM_CONFIG_FILE_TOO_BIG) << " Size: " << String(configSize) << endl; + result.reason = FPSTR(PROGMEM_CONFIG_FILE_TOO_BIG); + return result; + } + + if (!_spiffsBegin()) { + result.reason = FPSTR(PROGMEM_CONFIG_SPIFFS_NOT_FOUND); + return result; + } - char configJson[MAX_JSON_CONFIG_FILE_SIZE]; - configFile.readBytes(configJson, configSize); + SPIFFS.remove(CONFIG_FILE_PATH); + + File configFile = SPIFFS.open(CONFIG_FILE_PATH, "w"); + if (!configFile) { + Interface::get().getLogger() << FPSTR(PROGMEM_CONFIG_FILE_NOT_FOUND) << endl; + result.reason = FPSTR(PROGMEM_CONFIG_FILE_NOT_FOUND); + return result; + } + + // Write new config + newConfig.printTo(configFile); configFile.close(); - configJson[configSize] = '\0'; - StaticJsonBuffer configJsonBuffer; - JsonObject& configObject = configJsonBuffer.parseObject(configJson); + result.valid = true; + return result; +} + +ConfigValidationResult Config::patch(const char* patch) { + ConfigValidationResult result; + result.valid = false; + + StaticJsonBuffer patchJsonBuffer; + JsonObject& patchObject = patchJsonBuffer.parseObject(patch); + + // Validate Patch config + ConfigValidationResult patchValidResult = validateConfig(patchObject, true); + if (!patchValidResult.valid) { + String error = F("Patch JSON: "); + error.concat(patchValidResult.reason); + Interface::get().getLogger() << error << endl; + result.reason = error; + return result; + } + + // Validate current config + StaticJsonBuffer currentJsonBuffer; + ConfigValidationResultOBJ configLoadResult = _loadConfigFile(¤tJsonBuffer); + if (!configLoadResult.valid) { + result.reason = configLoadResult.reason; + return result; + } + JsonObject& configObject = *configLoadResult.config; + ConfigValidationResult configValidResult = validateConfig(configObject); + if (!configValidResult.valid) { + result.reason = configValidResult.reason; + return result; + } // To do alow object that dont currently exist to be added like settings. // if settings wasnt there origionally then it should be allowed to be added by incremental. @@ -292,11 +369,12 @@ bool Config::patch(const char* patch) { JsonObject& subObject = patchObject[it->key].as(); for (JsonObject::iterator it2 = subObject.begin(); it2 != subObject.end(); ++it2) { if (!configObject.containsKey(it->key) || !configObject[it->key].is()) { - String error = "✖ Config does not contain a "; + String error = F("✖ Config does not contain a "); error.concat(it->key); - error.concat(" object"); + error.concat(F(" object")); Interface::get().getLogger() << error << endl; - return false; + result.reason = error; + return result; } JsonObject& subConfigObject = configObject[it->key].as(); subConfigObject[it2->key] = it2->value; @@ -306,15 +384,17 @@ bool Config::patch(const char* patch) { } } - ConfigValidationResult configValidationResult = Validation::validateConfig(configObject); - if (!configValidationResult.valid) { - Interface::get().getLogger() << F("✖ Config file is not valid, reason: ") << configValidationResult.reason << endl; - return false; + ConfigValidationResult configWriteResult = write(configObject); + if (!configWriteResult.valid) { + String error = F("✖ Config file is not valid, reason: "); + error.concat(configWriteResult.reason); + Interface::get().getLogger() << error << endl; + result.reason = error; + return result; } - write(configObject); - - return true; + result.valid = true; + return result; } bool Config::isValid() const { diff --git a/src/Homie/Config.hpp b/src/Homie/Config.hpp index cfc95c6b..de6f652b 100644 --- a/src/Homie/Config.hpp +++ b/src/Homie/Config.hpp @@ -13,31 +13,31 @@ #include "../HomieBootMode.hpp" #include "../HomieSetting.hpp" #include "../StreamingOperator.hpp" +#include "Strings.hpp" namespace HomieInternals { class Config { public: Config(); bool load(); - inline const ConfigStruct& get() const; - char* getSafeConfigFile() const; + const ConfigStruct& get() const; + char* getSafeConfigFile(); void erase(); void setHomieBootModeOnNextBoot(HomieBootMode bootMode); HomieBootMode getHomieBootModeOnNextBoot(); - void write(const JsonObject& config); - bool patch(const char* patch); + ConfigValidationResult write(const JsonObject& config); + ConfigValidationResult patch(const char* patch); void log() const; // print the current config to log output bool isValid() const; + static ConfigValidationResult validateConfig(const JsonObject& parsedJson, bool skipValidation = false); + private: ConfigStruct _configStruct; bool _spiffsBegan; bool _valid; bool _spiffsBegin(); + ConfigValidationResultOBJ _loadConfigFile(StaticJsonBuffer* buf); }; - -const ConfigStruct& Config::get() const { - return _configStruct; -} } // namespace HomieInternals diff --git a/src/Homie/Datatypes/ConfigStruct.hpp b/src/Homie/Datatypes/ConfigStruct.hpp index 5d68c638..558974b8 100644 --- a/src/Homie/Datatypes/ConfigStruct.hpp +++ b/src/Homie/Datatypes/ConfigStruct.hpp @@ -12,7 +12,7 @@ struct ConfigStruct { struct WiFi { char ssid[MAX_WIFI_SSID_LENGTH]; char password[MAX_WIFI_PASSWORD_LENGTH]; - char bssid[MAX_MAC_STRING_LENGTH + 6]; + char bssid[MAX_MAC_STRING_LENGTH]; uint16_t channel; char ip[MAX_IP_STRING_LENGTH]; char mask[MAX_IP_STRING_LENGTH]; diff --git a/src/Homie/Datatypes/Interface.hpp b/src/Homie/Datatypes/Interface.hpp index 3e67a788..e9b97a4b 100644 --- a/src/Homie/Datatypes/Interface.hpp +++ b/src/Homie/Datatypes/Interface.hpp @@ -26,7 +26,7 @@ class InterfaceData { InterfaceData(); /***** User configurable data *****/ - char brand[MAX_BRAND_LENGTH]; + char brand[MAX_BRAND_STRING_LENGTH]; HomieBootMode bootMode; diff --git a/src/Homie/Limits.hpp b/src/Homie/Limits.hpp index e4686dc2..66d28cdc 100644 --- a/src/Homie/Limits.hpp +++ b/src/Homie/Limits.hpp @@ -10,6 +10,13 @@ namespace HomieInternals { // 6 elements at root, 9 elements at wifi, 6 elements at mqtt, 1 element at ota, max settings elements const uint16_t MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE = JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(9) + JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(MAX_CONFIG_SETTING_SIZE); + const uint8_t MAX_IP_LENGTH = 4; + const uint8_t MAX_IP_STRING_LENGTH = 16 + 1; + + const uint8_t MAX_MAC_LENGTH = 6; + const uint8_t MAX_MAC_STRING_LENGTH = (MAX_MAC_LENGTH * 2) + 1; + const uint8_t MAX_MAC_FORMATTED_STRING_LENGTH = (MAX_MAC_LENGTH * 2) + 5 + 1; + const uint8_t MAX_WIFI_SSID_LENGTH = 32 + 1; const uint8_t MAX_WIFI_PASSWORD_LENGTH = 64 + 1; const uint16_t MAX_HOSTNAME_LENGTH = 255 + 1; @@ -21,7 +28,7 @@ namespace HomieInternals { const uint8_t MAX_FRIENDLY_NAME_LENGTH = 64 + 1; const uint8_t MAX_DEVICE_ID_LENGTH = 32 + 1; - const uint8_t MAX_BRAND_LENGTH = MAX_WIFI_SSID_LENGTH - 10 - 1; + const uint8_t MAX_BRAND_STRING_LENGTH = (MAX_WIFI_SSID_LENGTH - 1) - (MAX_MAC_STRING_LENGTH - 1) - 1 + 1; const uint8_t MAX_FIRMWARE_NAME_LENGTH = 32 + 1; const uint8_t MAX_FIRMWARE_VERSION_LENGTH = 16 + 1; @@ -29,7 +36,4 @@ namespace HomieInternals { const uint8_t MAX_NODE_TYPE_LENGTH = 24 + 1; const uint8_t MAX_NODE_PROPERTY_LENGTH = 24 + 1; - const uint8_t MAX_IP_STRING_LENGTH = 16 + 1; - - const uint8_t MAX_MAC_STRING_LENGTH = 12; } // namespace HomieInternals diff --git a/src/Homie/Strings.hpp b/src/Homie/Strings.hpp index aa8c8a21..2fbfd5f4 100644 --- a/src/Homie/Strings.hpp +++ b/src/Homie/Strings.hpp @@ -8,4 +8,8 @@ namespace HomieInternals { const char PROGMEM_CONFIG_JSON_FAILURE_BEGINNING[] PROGMEM = "{\"success\":false,\"error\":\""; const char PROGMEM_CONFIG_JSON_FAILURE_END[] PROGMEM = "\"}"; -} + const char PROGMEM_CONFIG_SPIFFS_NOT_FOUND[] PROGMEM = "✖ Cannot mount filesystem"; + const char PROGMEM_CONFIG_FILE_NOT_FOUND[] PROGMEM = "✖ Cannot open config file"; + const char PROGMEM_CONFIG_FILE_TOO_BIG[] PROGMEM = "✖ Config file too big"; + +} // namespace HomieInternals diff --git a/src/Homie/Utils/DeviceId.cpp b/src/Homie/Utils/DeviceId.cpp index b3d8966b..1cdbc8e6 100644 --- a/src/Homie/Utils/DeviceId.cpp +++ b/src/Homie/Utils/DeviceId.cpp @@ -5,9 +5,9 @@ using namespace HomieInternals; char DeviceId::_deviceId[]; // need to define the static variable void DeviceId::generate() { - uint8_t mac[6]; + uint8_t mac[MAX_MAC_LENGTH]; WiFi.macAddress(mac); - snprintf(DeviceId::_deviceId, MAX_MAC_STRING_LENGTH+1 , "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + Helpers::macToString(mac, DeviceId::_deviceId); } const char* DeviceId::get() { diff --git a/src/Homie/Utils/DeviceId.hpp b/src/Homie/Utils/DeviceId.hpp index da5a42a5..f2c4c524 100644 --- a/src/Homie/Utils/DeviceId.hpp +++ b/src/Homie/Utils/DeviceId.hpp @@ -3,8 +3,8 @@ #include "Arduino.h" #include - #include "../Limits.hpp" +#include "../Utils/Helpers.hpp" namespace HomieInternals { class DeviceId { @@ -13,6 +13,6 @@ class DeviceId { static const char* get(); private: - static char _deviceId[MAX_MAC_STRING_LENGTH + 1]; + static char _deviceId[MAX_MAC_STRING_LENGTH]; }; } // namespace HomieInternals diff --git a/src/Homie/Utils/Helpers.cpp b/src/Homie/Utils/Helpers.cpp index 8c228680..ee7c9e4f 100644 --- a/src/Homie/Utils/Helpers.cpp +++ b/src/Homie/Utils/Helpers.cpp @@ -55,7 +55,7 @@ bool Helpers::validateMacAddress(const char* mac) { } ++mac; } - return (i == MAX_MAC_STRING_LENGTH && s == 5); + return (i == (MAX_MAC_LENGTH * 2) && s == 5); } bool Helpers::validateMd5(const char* md5) { @@ -82,3 +82,11 @@ std::unique_ptr Helpers::cloneString(const String& string) { void Helpers::ipToString(const IPAddress& ip, char * str) { snprintf(str, MAX_IP_STRING_LENGTH, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); } + +void HomieInternals::Helpers::macToString(const uint8_t mac[MAX_MAC_LENGTH], char * str) { + snprintf(str, MAX_MAC_STRING_LENGTH, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} + +void HomieInternals::Helpers::macToFormattedString(const uint8_t mac[MAX_MAC_LENGTH], char * str) { + snprintf(str, MAX_MAC_FORMATTED_STRING_LENGTH, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} diff --git a/src/Homie/Utils/Helpers.hpp b/src/Homie/Utils/Helpers.hpp index 1bf949cc..84d3261e 100644 --- a/src/Homie/Utils/Helpers.hpp +++ b/src/Homie/Utils/Helpers.hpp @@ -17,5 +17,7 @@ class Helpers { static bool validateMd5(const char* md5); static std::unique_ptr cloneString(const String& string); static void ipToString(const IPAddress& ip, char* str); + static void macToString(const uint8_t mac[MAX_MAC_LENGTH], char * str); + static void macToFormattedString(const uint8_t mac[MAX_MAC_LENGTH], char * str); }; } // namespace HomieInternals diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index 84f67696..8765bae8 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -276,11 +276,18 @@ ConfigValidationResult Validation::_validateConfigSettings(const JsonObject& obj settingsObject = &(object["settings"].as()); } - if (settingsObject->size() > MAX_CONFIG_SETTING_SIZE) {//max settings here and in isettings + if (settingsObject->size() > MAX_CONFIG_SETTING_SIZE) { result.reason = F("settings contains more elements than the set limit"); return result; } + // Remove nulls in settings + for (JsonPair kv : *settingsObject) { + if (kv.value.asString() == nullptr) { + settingsObject->remove(kv.key); + } + } + for (IHomieSetting* iSetting : IHomieSetting::settings) { enum class Issue { Type, @@ -289,15 +296,15 @@ ConfigValidationResult Validation::_validateConfigSettings(const JsonObject& obj }; auto setReason = [&result, &iSetting](Issue issue) { switch (issue) { - case Issue::Type: - result.reason = String(iSetting->getName()) + F(" setting is not a ") + String(iSetting->getType()); - break; - case Issue::Validator: - result.reason = String(iSetting->getName()) + F(" setting does not pass the validator function"); - break; - case Issue::Missing: - result.reason = String(iSetting->getName()) + F(" setting is missing"); - break; + case Issue::Type: + result.reason = String(iSetting->getName()) + F(" setting is not a ") + String(iSetting->getType()); + break; + case Issue::Validator: + result.reason = String(iSetting->getName()) + F(" setting does not pass the validator function"); + break; + case Issue::Missing: + result.reason = String(iSetting->getName()) + F(" setting is missing"); + break; } }; diff --git a/src/Homie/Utils/Validation.hpp b/src/Homie/Utils/Validation.hpp index efa39fc7..96c1f3fd 100644 --- a/src/Homie/Utils/Validation.hpp +++ b/src/Homie/Utils/Validation.hpp @@ -12,6 +12,9 @@ struct ConfigValidationResult { bool valid; String reason; }; +struct ConfigValidationResultOBJ : ConfigValidationResult { + JsonObject* config; +}; class Validation { public: From cd3c6e7014ceb5abdf5751a6ae2437a499991829 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Tue, 26 Dec 2017 23:28:34 +0100 Subject: [PATCH 06/51] Add last step to uibundle README (#460) * Add last step to uibundle README * Added Arduino Support for doc --- data/homie/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/homie/README.md b/data/homie/README.md index 868170ad..8ab0a3a9 100644 --- a/data/homie/README.md +++ b/data/homie/README.md @@ -4,7 +4,9 @@ This folder contains the data you can upload to the SPIFFS of your ESP8266. This is optional. -To upload files to the SPIFFS of your device, create a folder named `data` in your sketch directory. In this `data` folder, create an `homie` directory. You can put two files in it: +To upload files to the SPIFFS of your device, first create a folder named `data` in your sketch directory. In this `data` folder, create an `homie` directory. You can put two files in it: 1. The `config.json` file, if you want to bypass the `configuration` mode. 2. The `ui_bundle.gz` file, that you can download [here](http://setup.homie-esp8266.marvinroger.fr/ui_bundle.gz). If present, the configuration UI will be served directly from the ESP8266. + +Finally initiate the [SPIFFS upload process](http://docs.platformio.org/en/stable/platforms/espressif8266.html?highlight=spiffs#uploading-files-to-file-system-spiffs) via PlatformIO, or via the [Arduino IDE](http://esp8266.github.io/Arduino/versions/2.3.0/doc/filesystem.html#uploading-files-to-file-system) From 63fb96dc558319b01ab24dfe45547b0a49405a83 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Tue, 26 Dec 2017 23:37:54 +0100 Subject: [PATCH 07/51] Update updater script addressing quirks (#461) --- scripts/ota_updater/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/ota_updater/README.md b/scripts/ota_updater/README.md index 4cbff5b5..3ce065aa 100644 --- a/scripts/ota_updater/README.md +++ b/scripts/ota_updater/README.md @@ -1,7 +1,7 @@ Script: OTA updater =================== -This will allow you to send an OTA update to your device. +This script will allow you to send an OTA update to your device. ## Installation @@ -9,8 +9,7 @@ This will allow you to send an OTA update to your device. ## Usage -```bash -> scripts/ota_updater/ota_updater.py -h +```text usage: ota_updater.py [-h] -l BROKER_HOST -p BROKER_PORT [-u BROKER_USERNAME] [-d BROKER_PASSWORD] [-t BASE_TOPIC] -i DEVICE_ID firmware @@ -39,4 +38,11 @@ arguments: * `BROKER_HOST` and `BROKER_PORT` defaults to 127.0.0.1 and 1883 respectively if not set. * `BROKER_USERNAME` and `BROKER_PASSWORD` are optional. -* `BASE_TOPIC` defaults to `homie/` if not set +* `BASE_TOPIC` has to end with a slash, defaults to `homie/` if not set. + +### Example: + +```bash +python ota_updater.py -l localhost -u admin -d secure -t "homie/" -i "device-id" /path/to/firmware.bin +``` + From bbe347f186f14b77c772b1305ca630053ee60701 Mon Sep 17 00:00:00 2001 From: timpur Date: Wed, 27 Dec 2017 22:43:38 +1100 Subject: [PATCH 08/51] Fixed Config updates via patch. - Alow removal of setting items - Alow adding of settings items when settings object didnt originally exists in config. --- src/Homie/Boot/BootConfig.cpp | 1 + src/Homie/Boot/BootNormal.cpp | 4 +- src/Homie/Config.cpp | 90 ++++++++++++++++------------------ src/Homie/Config.hpp | 2 +- src/Homie/Utils/Validation.cpp | 8 +-- 5 files changed, 52 insertions(+), 53 deletions(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 88dd6477..7f135800 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -389,6 +389,7 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { ConfigValidationResult configWriteResult = Interface::get().getConfig().write(parsedJson); if (!configWriteResult.valid) { + Interface::get().getLogger() << F("✖ Error: ") << configWriteResult.reason << endl; __SendJSONError(request, configWriteResult.reason); return; } diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index bdc40ada..4767cd1f 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -812,12 +812,14 @@ bool HomieInternals::BootNormal::__handleConfig(char * topic, char * payload, co && strcmp_P(_mqttTopicLevels.get()[3], PSTR("set")) == 0 ) { Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/config/set")), 1, true, ""); - if (Interface::get().getConfig().patch(_mqttPayloadBuffer.get()).valid) { + ConfigValidationResult configPatchResult = Interface::get().getConfig().patch(_mqttPayloadBuffer.get()); + if (configPatchResult.valid) { Interface::get().getLogger() << F("✔ Configuration updated") << endl; _flaggedForReboot = true; Interface::get().getLogger() << F("Flagged for reboot") << endl; } else { Interface::get().getLogger() << F("✖ Configuration not updated") << endl; + Interface::get().getLogger() << F("Error: ") << configPatchResult.reason << endl; } return true; } diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index dfc85756..6546f5ea 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -29,15 +29,11 @@ bool Config::load() { StaticJsonBuffer jsonBuffer; ConfigValidationResultOBJ loadResult = _loadConfigFile(&jsonBuffer); if (!loadResult.valid) { - //Interface::get().getLogger() << loadResult.reason << endl; + Interface::get().getLogger() << loadResult.reason << endl; + loadResult.config->prettyPrintTo(Interface::get().getLogger()); return false; } JsonObject& parsedJson = *loadResult.config; - ConfigValidationResult validResult = validateConfig(parsedJson); - if (!validResult.valid) { - //Interface::get().getLogger() << validResult.reason << endl; - return false; - } Interface::get().getLogger() << F("✔ Config file loaded and validated") << endl; @@ -227,7 +223,7 @@ HomieBootMode Config::getHomieBootModeOnNextBoot() { } } -ConfigValidationResultOBJ Config::_loadConfigFile(StaticJsonBuffer* jsonBuffer) { +ConfigValidationResultOBJ Config::_loadConfigFile(StaticJsonBuffer* jsonBuffer, bool skipValidation) { ConfigValidationResultOBJ result; result.valid = false; @@ -237,17 +233,14 @@ ConfigValidationResultOBJ Config::_loadConfigFile(StaticJsonBuffer MAX_JSON_CONFIG_FILE_SIZE) { configFile.close(); - Interface::get().getLogger() << FPSTR(PROGMEM_CONFIG_FILE_TOO_BIG) << endl; result.reason = FPSTR(PROGMEM_CONFIG_FILE_TOO_BIG); return result; } - char buf[MAX_JSON_CONFIG_FILE_SIZE]; - configFile.readBytes(buf, configSize); - configFile.close(); - buf[configSize] = '\0'; + JsonObject& config = jsonBuffer->parseObject(configFile); + + ConfigValidationResult configValidationResult = validateConfig(config, skipValidation); + + if (!configValidationResult.valid) { + result.reason = configValidationResult.reason; + return result; + } result.valid = true; - result.config = &jsonBuffer->parseObject(buf); + result.config = &config; return result; } @@ -283,7 +279,8 @@ ConfigValidationResult Config::validateConfig(const JsonObject& parsedJson, bool if (!skipValidation) { ConfigValidationResult configValidationResult = Validation::validateConfig(parsedJson); if (!configValidationResult.valid) { - result.reason = String(F("✖ Config file is not valid, reason: ")) + configValidationResult.reason; + result.reason = F("✖ Config file is not valid, reason: "); + result.reason.concat(configValidationResult.reason); return result; } } @@ -304,8 +301,9 @@ ConfigValidationResult Config::write(const JsonObject& newConfig) { size_t configSize = newConfig.measureLength(); if (configSize > MAX_JSON_CONFIG_FILE_SIZE) { - Interface::get().getLogger() << FPSTR(PROGMEM_CONFIG_FILE_TOO_BIG) << " Size: " << String(configSize) << endl; result.reason = FPSTR(PROGMEM_CONFIG_FILE_TOO_BIG); + result.reason.concat(F(" Size: ")); + result.reason.concat(configSize); return result; } @@ -318,7 +316,6 @@ ConfigValidationResult Config::write(const JsonObject& newConfig) { File configFile = SPIFFS.open(CONFIG_FILE_PATH, "w"); if (!configFile) { - Interface::get().getLogger() << FPSTR(PROGMEM_CONFIG_FILE_NOT_FOUND) << endl; result.reason = FPSTR(PROGMEM_CONFIG_FILE_NOT_FOUND); return result; } @@ -341,7 +338,7 @@ ConfigValidationResult Config::patch(const char* patch) { // Validate Patch config ConfigValidationResult patchValidResult = validateConfig(patchObject, true); if (!patchValidResult.valid) { - String error = F("Patch JSON: "); + String error = F("✖ Patch Config file is not valid, reason: "); error.concat(patchValidResult.reason); Interface::get().getLogger() << error << endl; result.reason = error; @@ -352,41 +349,40 @@ ConfigValidationResult Config::patch(const char* patch) { StaticJsonBuffer currentJsonBuffer; ConfigValidationResultOBJ configLoadResult = _loadConfigFile(¤tJsonBuffer); if (!configLoadResult.valid) { - result.reason = configLoadResult.reason; + String error = F("✖ Old Config file is not valid, reason: "); + error.concat(patchValidResult.reason); + Interface::get().getLogger() << error << endl; + result.reason = error; return result; } JsonObject& configObject = *configLoadResult.config; - ConfigValidationResult configValidResult = validateConfig(configObject); - if (!configValidResult.valid) { - result.reason = configValidResult.reason; - return result; - } - // To do alow object that dont currently exist to be added like settings. - // if settings wasnt there origionally then it should be allowed to be added by incremental. - for (JsonObject::iterator it = patchObject.begin(); it != patchObject.end(); ++it) { - if (patchObject[it->key].is()) { - JsonObject& subObject = patchObject[it->key].as(); - for (JsonObject::iterator it2 = subObject.begin(); it2 != subObject.end(); ++it2) { - if (!configObject.containsKey(it->key) || !configObject[it->key].is()) { - String error = F("✖ Config does not contain a "); - error.concat(it->key); - error.concat(F(" object")); - Interface::get().getLogger() << error << endl; - result.reason = error; - return result; - } - JsonObject& subConfigObject = configObject[it->key].as(); - subConfigObject[it2->key] = it2->value; + for (const JsonPair& patchPair : patchObject) { + if (patchPair.value.is() && patchPair.value.size() > 0) { + if (!configObject.containsKey(patchPair.key)) { + configObject.createNestedObject(patchPair.key); + } + if (!configObject[patchPair.key].is()) { + configObject.remove(patchPair.key); + configObject.createNestedObject(patchPair.key); + } + JsonObject& patchSubObject = patchPair.value.as(); + JsonObject& configSubObject = configObject[patchPair.key].as(); + + for (const JsonPair& subPatchPair : patchSubObject) { + configSubObject[subPatchPair.key] = subPatchPair.value; } + } else if (patchPair.value.is()) { + // Not you handled + configObject[patchPair.key] = patchPair.value; } else { - configObject[it->key] = it->value; + configObject[patchPair.key] = patchPair.value; } } ConfigValidationResult configWriteResult = write(configObject); if (!configWriteResult.valid) { - String error = F("✖ Config file is not valid, reason: "); + String error = F("✖ New Config file is not valid, reason: "); error.concat(configWriteResult.reason); Interface::get().getLogger() << error << endl; result.reason = error; diff --git a/src/Homie/Config.hpp b/src/Homie/Config.hpp index de6f652b..cd00cced 100644 --- a/src/Homie/Config.hpp +++ b/src/Homie/Config.hpp @@ -38,6 +38,6 @@ class Config { bool _valid; bool _spiffsBegin(); - ConfigValidationResultOBJ _loadConfigFile(StaticJsonBuffer* buf); + ConfigValidationResultOBJ _loadConfigFile(StaticJsonBuffer* buf, bool skipValidation = false); }; } // namespace HomieInternals diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index 8765bae8..be2e13fb 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -61,7 +61,7 @@ ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) ConfigValidationResult result; result.valid = false; - if (!object.containsKey("wifi") || !object["wifi"].is()) { + if (!object.containsKey("wifi") || !object["wifi"].is()) { result.reason = F("wifi is not an object"); return result; } @@ -184,7 +184,7 @@ ConfigValidationResult Validation::_validateConfigMqtt(const JsonObject& object) ConfigValidationResult result; result.valid = false; - if (!object.containsKey("mqtt") || !object["mqtt"].is()) { + if (!object.containsKey("mqtt") || !object["mqtt"].is()) { result.reason = F("mqtt is not an object"); return result; } @@ -251,7 +251,7 @@ ConfigValidationResult Validation::_validateConfigOta(const JsonObject& object) ConfigValidationResult result; result.valid = false; - if (!object.containsKey("ota") || !object["ota"].is()) { + if (!object.containsKey("ota") || !object["ota"].is()) { result.reason = F("ota is not an object"); return result; } @@ -272,7 +272,7 @@ ConfigValidationResult Validation::_validateConfigSettings(const JsonObject& obj JsonObject* settingsObject = &(emptySettingsBuffer.createObject()); - if (object.containsKey("settings") && object["settings"].is()) { + if (object.containsKey("settings") && object["settings"].is()) { settingsObject = &(object["settings"].as()); } From 37f5b62851a90d356a60eef7887410bf7dd5d9fe Mon Sep 17 00:00:00 2001 From: timpur Date: Fri, 29 Dec 2017 20:25:50 +1100 Subject: [PATCH 09/51] Added ability to set settings via code and save to config ( #457 ) + Refactoring. --- docs/advanced-usage/custom-settings.md | 8 ++ docs/others/cpp-api-reference.md | 17 ++- docs/quickstart/getting-started.md | 2 +- src/Homie.cpp | 46 +++--- src/Homie/Boot/BootConfig.cpp | 42 +++--- src/Homie/Boot/BootNormal.cpp | 2 +- src/Homie/Config.cpp | 120 ++++++++-------- src/Homie/Config.hpp | 55 +++++++- src/Homie/Datatypes/Result.hpp | 15 ++ src/Homie/Limits.hpp | 3 +- src/Homie/Utils/Validation.cpp | 186 ++++++++++++------------- src/Homie/Utils/Validation.hpp | 21 +-- src/HomieSetting.cpp | 52 ++++--- src/HomieSetting.hpp | 33 +++-- 14 files changed, 344 insertions(+), 258 deletions(-) create mode 100644 src/Homie/Datatypes/Result.hpp diff --git a/docs/advanced-usage/custom-settings.md b/docs/advanced-usage/custom-settings.md index 03b29083..52b3d326 100644 --- a/docs/advanced-usage/custom-settings.md +++ b/docs/advanced-usage/custom-settings.md @@ -36,3 +36,11 @@ For this example, if you want to provide the `percentage` setting, you will have See the following example for a concrete use case: [![GitHub logo](../assets/github.png) CustomSettings.ino](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/CustomSettings/CustomSettings.ino) + + +You can also change the value of the setting at any time via updates to the config file or via code using: +```c++ +percentageSetting.set(50, true); +``` + +See the [API](http://marvinroger.github.io/homie-esp8266/docs/develop/others/cpp-api-reference/#homiesetting) for more information. diff --git a/docs/others/cpp-api-reference.md b/docs/others/cpp-api-reference.md index 1cabf21c..97dcff4a 100644 --- a/docs/others/cpp-api-reference.md +++ b/docs/others/cpp-api-reference.md @@ -1,4 +1,5 @@ # Homie +## Main You don't have to instantiate an `Homie` instance, it is done internally. @@ -20,7 +21,7 @@ Handle Homie work. !!! warning "Mandatory!" Must be called once in `loop()`. -## Functions to call *before* `Homie.setup()` +### Functions to call *before* `Homie.setup()` ```c++ void Homie_setFirmware(const char* name, const char* version); @@ -153,7 +154,7 @@ Homie& setStandalone(); This will mark the Homie firmware as standalone, meaning it will first boot in `standalone` mode. To configure it and boot to `configuration` mode, the device has to be resetted. -## Functions to call *after* `Homie.setup()` +### Functions to call *after* `Homie.setup()` ```c++ void reset(); @@ -179,7 +180,7 @@ Prepare the device for deep sleep. It ensures messages are sent and disconnects void doDeepSleep(uint32_t time_us = 0, RFMode mode = RF_DEFAULT); ``` -Puth the device into deep sleep. It ensures the Serial is flushed. +Puts the device into deep sleep. It ensures the Serial is flushed. ```c++ bool isConfigured() const; @@ -216,7 +217,7 @@ Get the underlying `Logger` object, which is only a wrapper around `Serial` by d ------- -# HomieNode +## HomieNode ```c++ HomieNode(const char* id, const char* type, std::function handler = ); @@ -282,7 +283,7 @@ uint16_t send(const String& value); // finally send the property, return the pa Method names should be self-explanatory. -# HomieSetting +## HomieSetting ```c++ HomieSetting(const char* name, const char* description); @@ -300,6 +301,12 @@ T get() const; Get the default value if the setting is optional and not provided, or the provided value if the setting is required or optional but provided. +```c++ +bool set(T value, bool saveToConfig = false); +``` + +Sets the setting to a new value. Use `saveToConfig` to eaither save this new value to the config file or just use it in memory. + ```c++ bool wasProvided() const; ``` diff --git a/docs/quickstart/getting-started.md b/docs/quickstart/getting-started.md index 709ef514..ebb9e894 100644 --- a/docs/quickstart/getting-started.md +++ b/docs/quickstart/getting-started.md @@ -23,7 +23,7 @@ There is a YouTube video with instructions: Homie for ESP8266 has 5 dependencies: -* [ArduinoJson](https://github.com/bblanchon/ArduinoJson) >= 5.0.8 +* [ArduinoJson](https://github.com/bblanchon/ArduinoJson) >= 5.10.0 * [Bounce2](https://github.com/thomasfredericks/Bounce2) * [ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) >= [c8ed544](https://github.com/me-no-dev/ESPAsyncTCP) * [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) diff --git a/src/Homie.cpp b/src/Homie.cpp index e6feddb8..48c39a19 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -64,38 +64,38 @@ void HomieClass::setup() { } // Check if default settings values are valid - bool defaultSettingsValuesValid = true; - for (IHomieSetting* iSetting : IHomieSetting::settings) { - if (iSetting->isBool()) { - HomieSetting* setting = static_cast*>(iSetting); - if (!setting->isRequired() && !setting->validate(setting->get())) { + + + for (IHomieSetting& iSetting : IHomieSetting::settings) { + bool defaultSettingsValuesValid = true; + if (iSetting.isBool()) { + HomieSetting& setting = static_cast&>(iSetting); + if (!setting.isRequired() && !setting._validate(setting.get())) { defaultSettingsValuesValid = false; - break; } - } else if (iSetting->isLong()) { - HomieSetting* setting = static_cast*>(iSetting); - if (!setting->isRequired() && !setting->validate(setting->get())) { + } else if (iSetting.isLong()) { + HomieSetting& setting = static_cast&>(iSetting); + if (!setting.isRequired() && !setting._validate(setting.get())) { defaultSettingsValuesValid = false; - break; } - } else if (iSetting->isDouble()) { - HomieSetting* setting = static_cast*>(iSetting); - if (!setting->isRequired() && !setting->validate(setting->get())) { + } else if (iSetting.isDouble()) { + HomieSetting& setting = static_cast&>(iSetting); + if (!setting.isRequired() && !setting._validate(setting.get())) { defaultSettingsValuesValid = false; - break; } - } else if (iSetting->isConstChar()) { - HomieSetting* setting = static_cast*>(iSetting); - if (!setting->isRequired() && !setting->validate(setting->get())) { + } else if (iSetting.isConstChar()) { + HomieSetting& setting = static_cast&>(iSetting); + if (!setting.isRequired() && !setting._validate(setting.get())) { defaultSettingsValuesValid = false; - break; } } - } - - if (!defaultSettingsValuesValid) { - Helpers::abort(F("✖ Default setting value does not pass validator test")); - return; // never reached, here for clarity + if (!defaultSettingsValuesValid) { + String error = F("✖ Default setting value does not pass validator test for "); + error.concat(iSetting.getName()); + error.concat(F(" setting")); + Helpers::abort(error); + return; // never reached, here for clarity + } } // boot mode set during this boot by application before Homie.setup() diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 7f135800..6a2ef813 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -332,28 +332,28 @@ void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { } JsonArray& settings = json.createNestedArray("settings"); - for (IHomieSetting* iSetting : IHomieSetting::settings) { + for (IHomieSetting& iSetting : IHomieSetting::settings) { JsonObject& jsonSetting = jsonBuffer.createObject(); - if (iSetting->getType() != "unknown") { - jsonSetting["name"] = iSetting->getName(); - jsonSetting["description"] = iSetting->getDescription(); - jsonSetting["type"] = iSetting->getType(); - jsonSetting["required"] = iSetting->isRequired(); - - if (!iSetting->isRequired()) { - if (iSetting->isBool()) { - HomieSetting* setting = static_cast*>(iSetting); - jsonSetting["default"] = setting->get(); - } else if (iSetting->isLong()) { - HomieSetting* setting = static_cast*>(iSetting); - jsonSetting["default"] = setting->get(); - } else if (iSetting->isDouble()) { - HomieSetting* setting = static_cast*>(iSetting); - jsonSetting["default"] = setting->get(); - } else if (iSetting->isConstChar()) { - HomieSetting* setting = static_cast*>(iSetting); - jsonSetting["default"] = setting->get(); + if (iSetting.getType() != "unknown") { + jsonSetting["name"] = iSetting.getName(); + jsonSetting["description"] = iSetting.getDescription(); + jsonSetting["type"] = iSetting.getType(); + jsonSetting["required"] = iSetting.isRequired(); + + if (!iSetting.isRequired()) { + if (iSetting.isBool()) { + HomieSetting& setting = static_cast&>(iSetting); + jsonSetting["default"] = setting.get(); + } else if (iSetting.isLong()) { + HomieSetting& setting = static_cast&>(iSetting); + jsonSetting["default"] = setting.get(); + } else if (iSetting.isDouble()) { + HomieSetting& setting = static_cast&>(iSetting); + jsonSetting["default"] = setting.get(); + } else if (iSetting.isConstChar()) { + HomieSetting& setting = static_cast&>(iSetting); + jsonSetting["default"] = setting.get(); } } } @@ -387,7 +387,7 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { const char* body = (const char*)(request->_tempObject); JsonObject& parsedJson = parseJsonBuffer.parseObject(body); - ConfigValidationResult configWriteResult = Interface::get().getConfig().write(parsedJson); + ValidationResult configWriteResult = Interface::get().getConfig().write(parsedJson); if (!configWriteResult.valid) { Interface::get().getLogger() << F("✖ Error: ") << configWriteResult.reason << endl; __SendJSONError(request, configWriteResult.reason); diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index 4767cd1f..5e298045 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -812,7 +812,7 @@ bool HomieInternals::BootNormal::__handleConfig(char * topic, char * payload, co && strcmp_P(_mqttTopicLevels.get()[3], PSTR("set")) == 0 ) { Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/config/set")), 1, true, ""); - ConfigValidationResult configPatchResult = Interface::get().getConfig().patch(_mqttPayloadBuffer.get()); + ValidationResult configPatchResult = Interface::get().getConfig().patch(_mqttPayloadBuffer.get()); if (configPatchResult.valid) { Interface::get().getLogger() << F("✔ Configuration updated") << endl; _flaggedForReboot = true; diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 6546f5ea..090a0df3 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -26,8 +26,8 @@ bool Config::load() { Interface::get().getLogger() << F("↻ Config file loading") << endl; - StaticJsonBuffer jsonBuffer; - ConfigValidationResultOBJ loadResult = _loadConfigFile(&jsonBuffer); + StaticJsonBuffer jsonBuffer; + ValidationResultOBJ loadResult = _loadConfigFile(jsonBuffer); if (!loadResult.valid) { Interface::get().getLogger() << loadResult.reason << endl; loadResult.config->prettyPrintTo(Interface::get().getLogger()); @@ -130,30 +130,30 @@ bool Config::load() { JsonObject& settingsObject = parsedJson["settings"].as(); - for (IHomieSetting* iSetting : IHomieSetting::settings) { - if (iSetting->isBool()) { - HomieSetting* setting = static_cast*>(iSetting); + for (IHomieSetting& iSetting : IHomieSetting::settings) { + if (iSetting.isBool()) { + HomieSetting& setting = static_cast&>(iSetting); - if (settingsObject.containsKey(setting->getName())) { - setting->set(settingsObject[setting->getName()].as()); + if (settingsObject.containsKey(setting.getName())) { + setting._set(settingsObject[setting.getName()].as()); } - } else if (iSetting->isLong()) { - HomieSetting* setting = static_cast*>(iSetting); + } else if (iSetting.isLong()) { + HomieSetting& setting = static_cast&>(iSetting); - if (settingsObject.containsKey(setting->getName())) { - setting->set(settingsObject[setting->getName()].as()); + if (settingsObject.containsKey(setting.getName())) { + setting._set(settingsObject[setting.getName()].as()); } - } else if (iSetting->isDouble()) { - HomieSetting* setting = static_cast*>(iSetting); + } else if (iSetting.isDouble()) { + HomieSetting& setting = static_cast&>(iSetting); - if (settingsObject.containsKey(setting->getName())) { - setting->set(settingsObject[setting->getName()].as()); + if (settingsObject.containsKey(setting.getName())) { + setting._set(settingsObject[setting.getName()].as()); } - } else if (iSetting->isConstChar()) { - HomieSetting* setting = static_cast*>(iSetting); + } else if (iSetting.isConstChar()) { + HomieSetting& setting = static_cast&>(iSetting); - if (settingsObject.containsKey(setting->getName())) { - setting->set(strdup(settingsObject[setting->getName()].as())); + if (settingsObject.containsKey(setting.getName())) { + setting._set(strdup(settingsObject[setting.getName()].as())); } } } @@ -163,13 +163,13 @@ bool Config::load() { } char* Config::getSafeConfigFile() { - StaticJsonBuffer jsonBuffer; - ConfigValidationResultOBJ configLoadResult = _loadConfigFile(&jsonBuffer); + StaticJsonBuffer jsonBuffer; + ValidationResultOBJ configLoadResult = _loadConfigFile(jsonBuffer); if (!configLoadResult.valid) { return nullptr; } JsonObject& configObject = *configLoadResult.config; - ConfigValidationResult configValidResult = validateConfig(configObject); + ValidationResult configValidResult = validateConfig(configObject); if (!configValidResult.valid) { return nullptr; } @@ -223,8 +223,8 @@ HomieBootMode Config::getHomieBootModeOnNextBoot() { } } -ConfigValidationResultOBJ Config::_loadConfigFile(StaticJsonBuffer* jsonBuffer, bool skipValidation) { - ConfigValidationResultOBJ result; +ValidationResultOBJ Config::_loadConfigFile(StaticJsonBuffer& jsonBuffer, bool skipValidation) { + ValidationResultOBJ result; result.valid = false; if (!_spiffsBegin()) { @@ -252,9 +252,9 @@ ConfigValidationResultOBJ Config::_loadConfigFile(StaticJsonBufferparseObject(configFile); + JsonObject& config = jsonBuffer.parseObject(configFile); - ConfigValidationResult configValidationResult = validateConfig(config, skipValidation); + ValidationResult configValidationResult = validateConfig(config, skipValidation); if (!configValidationResult.valid) { result.reason = configValidationResult.reason; @@ -267,8 +267,8 @@ ConfigValidationResultOBJ Config::_loadConfigFile(StaticJsonBuffer patchJsonBuffer; JsonObject& patchObject = patchJsonBuffer.parseObject(patch); // Validate Patch config - ConfigValidationResult patchValidResult = validateConfig(patchObject, true); + ValidationResult patchValidResult = validateConfig(patchObject, true); if (!patchValidResult.valid) { - String error = F("✖ Patch Config file is not valid, reason: "); - error.concat(patchValidResult.reason); - Interface::get().getLogger() << error << endl; - result.reason = error; + result.reason = F("✖ Patch Config file is not valid, reason: "); + result.reason.concat(patchValidResult.reason); return result; } // Validate current config - StaticJsonBuffer currentJsonBuffer; - ConfigValidationResultOBJ configLoadResult = _loadConfigFile(¤tJsonBuffer); + StaticJsonBuffer currentJsonBuffer; + ValidationResultOBJ configLoadResult = _loadConfigFile(currentJsonBuffer); if (!configLoadResult.valid) { - String error = F("✖ Old Config file is not valid, reason: "); - error.concat(patchValidResult.reason); - Interface::get().getLogger() << error << endl; - result.reason = error; + result.reason = F("✖ Old Config file is not valid, reason: "); + result.reason.concat(configLoadResult.reason); return result; } JsonObject& configObject = *configLoadResult.config; @@ -380,12 +376,10 @@ ConfigValidationResult Config::patch(const char* patch) { } } - ConfigValidationResult configWriteResult = write(configObject); + ValidationResult configWriteResult = write(configObject); if (!configWriteResult.valid) { - String error = F("✖ New Config file is not valid, reason: "); - error.concat(configWriteResult.reason); - Interface::get().getLogger() << error << endl; - result.reason = error; + result.reason = F("✖ New Config file is not valid, reason: "); + result.reason.concat(configWriteResult.reason); return result; } @@ -427,21 +421,21 @@ void Config::log() const { if (IHomieSetting::settings.size() > 0) { Interface::get().getLogger() << F(" • Custom settings: ") << endl; - for (IHomieSetting* iSetting : IHomieSetting::settings) { + for (IHomieSetting& iSetting : IHomieSetting::settings) { Interface::get().getLogger() << F(" ◦ "); - if (iSetting->isBool()) { - HomieSetting* setting = static_cast*>(iSetting); - Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); - } else if (iSetting->isLong()) { - HomieSetting* setting = static_cast*>(iSetting); - Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); - } else if (iSetting->isDouble()) { - HomieSetting* setting = static_cast*>(iSetting); - Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); - } else if (iSetting->isConstChar()) { - HomieSetting* setting = static_cast*>(iSetting); - Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); + if (iSetting.isBool()) { + HomieSetting& setting = static_cast&>(iSetting); + Interface::get().getLogger() << setting.getName() << F(": ") << setting.get() << F(" (") << (setting.wasProvided() ? F("set") : F("default")) << F(")"); + } else if (iSetting.isLong()) { + HomieSetting& setting = static_cast&>(iSetting); + Interface::get().getLogger() << setting.getName() << F(": ") << setting.get() << F(" (") << (setting.wasProvided() ? F("set") : F("default")) << F(")"); + } else if (iSetting.isDouble()) { + HomieSetting& setting = static_cast&>(iSetting); + Interface::get().getLogger() << setting.getName() << F(": ") << setting.get() << F(" (") << (setting.wasProvided() ? F("set") : F("default")) << F(")"); + } else if (iSetting.isConstChar()) { + HomieSetting& setting = static_cast&>(iSetting); + Interface::get().getLogger() << setting.getName() << F(": ") << setting.get() << F(" (") << (setting.wasProvided() ? F("set") : F("default")) << F(")"); } Interface::get().getLogger() << endl; diff --git a/src/Homie/Config.hpp b/src/Homie/Config.hpp index cd00cced..c49a06c5 100644 --- a/src/Homie/Config.hpp +++ b/src/Homie/Config.hpp @@ -14,10 +14,11 @@ #include "../HomieSetting.hpp" #include "../StreamingOperator.hpp" #include "Strings.hpp" +#include "./DataTypes/Result.hpp" namespace HomieInternals { class Config { - public: +public: Config(); bool load(); const ConfigStruct& get() const; @@ -25,19 +26,61 @@ class Config { void erase(); void setHomieBootModeOnNextBoot(HomieBootMode bootMode); HomieBootMode getHomieBootModeOnNextBoot(); - ConfigValidationResult write(const JsonObject& config); - ConfigValidationResult patch(const char* patch); + ValidationResult write(const JsonObject& config); + ValidationResult patch(const char* patch); + template + ValidationResult saveSetting(const char* name, T value); void log() const; // print the current config to log output bool isValid() const; - static ConfigValidationResult validateConfig(const JsonObject& parsedJson, bool skipValidation = false); + static ValidationResult validateConfig(const JsonObject& parsedJson, bool skipValidation = false); - private: +private: ConfigStruct _configStruct; bool _spiffsBegan; bool _valid; bool _spiffsBegin(); - ConfigValidationResultOBJ _loadConfigFile(StaticJsonBuffer* buf, bool skipValidation = false); + ValidationResultOBJ _loadConfigFile(StaticJsonBuffer& jsonBuffer, bool skipValidation = false); }; + } // namespace HomieInternals + +// Template Implementations +using namespace HomieInternals; + +template +ValidationResult Config::saveSetting(const char* name, T value) { + ValidationResult result; + result.valid = false; + + StaticJsonBuffer currentJsonBuffer; + ValidationResultOBJ configLoadResult = _loadConfigFile(currentJsonBuffer, true); + if (!configLoadResult.valid) { + result.reason = F("✖ Old Config file is not valid, reason: "); + result.reason.concat(configLoadResult.reason); + return result; + } + JsonObject& configObject = *configLoadResult.config; + + if (!configObject.containsKey("settings")) { + configObject.createNestedObject("settings"); + } + if (!configObject["settings"].is()) { + configObject.remove("settings"); + configObject.createNestedObject("settings"); + } + + JsonObject& settings = configObject["settings"].as(); + settings[name] = value; + + ValidationResult configWriteResult = write(configObject); + if (!configWriteResult.valid) { + result.reason = F("✖ New Config file is not valid, reason: "); + result.reason.concat(configWriteResult.reason); + return result; + } + + result.valid = true; + return result; +} diff --git a/src/Homie/Datatypes/Result.hpp b/src/Homie/Datatypes/Result.hpp new file mode 100644 index 00000000..3331152c --- /dev/null +++ b/src/Homie/Datatypes/Result.hpp @@ -0,0 +1,15 @@ +#pragma once + +namespace HomieInternals { +struct Result { + bool success; + String message; +}; +struct ValidationResult { + bool valid; + String reason; +}; +struct ValidationResultOBJ : ValidationResult { + JsonObject* config; +}; +} diff --git a/src/Homie/Limits.hpp b/src/Homie/Limits.hpp index 66d28cdc..b013de13 100644 --- a/src/Homie/Limits.hpp +++ b/src/Homie/Limits.hpp @@ -3,12 +3,13 @@ #include namespace HomieInternals { - const uint16_t MAX_JSON_CONFIG_FILE_SIZE = 1000; + const uint16_t MAX_JSON_CONFIG_FILE_SIZE = 500; // max setting elements const uint8_t MAX_CONFIG_SETTING_SIZE = 10; // 6 elements at root, 9 elements at wifi, 6 elements at mqtt, 1 element at ota, max settings elements const uint16_t MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE = JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(9) + JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(MAX_CONFIG_SETTING_SIZE); + const uint16_t MAX_JSON_CONFIG_ARDUINOJSON_FILE_BUFFER_SIZE = MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE + MAX_JSON_CONFIG_FILE_SIZE; const uint8_t MAX_IP_LENGTH = 4; const uint8_t MAX_IP_STRING_LENGTH = 16 + 1; diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index be2e13fb..aeabf5ac 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -2,8 +2,8 @@ using namespace HomieInternals; -ConfigValidationResult Validation::validateConfig(const JsonObject& object) { - ConfigValidationResult result; +ValidationResult Validation::validateConfig(const JsonObject& object) { + ValidationResult result; result = _validateConfigRoot(object); if (!result.valid) return result; result = _validateConfigWifi(object); @@ -19,8 +19,8 @@ ConfigValidationResult Validation::validateConfig(const JsonObject& object) { return result; } -ConfigValidationResult Validation::_validateConfigRoot(const JsonObject& object) { - ConfigValidationResult result; +ValidationResult Validation::_validateConfigRoot(const JsonObject& object) { + ValidationResult result; result.valid = false; if (!object.containsKey("name") || !object["name"].is()) { result.reason = F("name is not a string"); @@ -57,8 +57,8 @@ ConfigValidationResult Validation::_validateConfigRoot(const JsonObject& object) return result; } -ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) { - ConfigValidationResult result; +ValidationResult Validation::_validateConfigWifi(const JsonObject& object) { + ValidationResult result; result.valid = false; if (!object.containsKey("wifi") || !object["wifi"].is()) { @@ -180,8 +180,8 @@ ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) return result; } -ConfigValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { - ConfigValidationResult result; +ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { + ValidationResult result; result.valid = false; if (!object.containsKey("mqtt") || !object["mqtt"].is()) { @@ -247,8 +247,8 @@ ConfigValidationResult Validation::_validateConfigMqtt(const JsonObject& object) return result; } -ConfigValidationResult Validation::_validateConfigOta(const JsonObject& object) { - ConfigValidationResult result; +ValidationResult Validation::_validateConfigOta(const JsonObject& object) { + ValidationResult result; result.valid = false; if (!object.containsKey("ota") || !object["ota"].is()) { @@ -264,110 +264,110 @@ ConfigValidationResult Validation::_validateConfigOta(const JsonObject& object) return result; } -ConfigValidationResult Validation::_validateConfigSettings(const JsonObject& object) { - ConfigValidationResult result; +ValidationResult Validation::_validateConfigSettings(const JsonObject& object) { + ValidationResult result; result.valid = false; - StaticJsonBuffer<0> emptySettingsBuffer; - - JsonObject* settingsObject = &(emptySettingsBuffer.createObject()); + if (!object.containsKey("settings")) { + result.valid = true; + return result; + } - if (object.containsKey("settings") && object["settings"].is()) { - settingsObject = &(object["settings"].as()); + if (!object["settings"].is()) { + result.reason = F("settings is not an object"); + return result; } - if (settingsObject->size() > MAX_CONFIG_SETTING_SIZE) { + JsonObject& settingsObject = object["settings"].as(); + + if (settingsObject.size() > MAX_CONFIG_SETTING_SIZE) { result.reason = F("settings contains more elements than the set limit"); return result; } // Remove nulls in settings - for (JsonPair kv : *settingsObject) { - if (kv.value.asString() == nullptr) { - settingsObject->remove(kv.key); + for (JsonPair kv : settingsObject) { + bool hasValue = kv.value.is() || kv.value.is() || kv.value.is() || kv.value.as() != nullptr; + if (!hasValue) { + Interface::get().getLogger() << F("Removed ") << kv.key << F(" from settings") << endl; + settingsObject.remove(kv.key); } } - for (IHomieSetting* iSetting : IHomieSetting::settings) { - enum class Issue { - Type, - Validator, - Missing - }; - auto setReason = [&result, &iSetting](Issue issue) { - switch (issue) { - case Issue::Type: - result.reason = String(iSetting->getName()) + F(" setting is not a ") + String(iSetting->getType()); - break; - case Issue::Validator: - result.reason = String(iSetting->getName()) + F(" setting does not pass the validator function"); - break; - case Issue::Missing: - result.reason = String(iSetting->getName()) + F(" setting is missing"); - break; - } - }; - - if (iSetting->isBool()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject->containsKey(setting->getName())) { - if (!(*settingsObject)[setting->getName()].is()) { - setReason(Issue::Type); - return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - setReason(Issue::Validator); - return result; + enum class Issue { + None, + Type, + Validator, + Missing + }; + + for (IHomieSetting& iSetting : IHomieSetting::settings) { + + Issue issue = Issue::None; + + if (iSetting.isBool()) { + HomieSetting& setting = static_cast&>(iSetting); + + if (settingsObject.containsKey(setting.getName())) { + if (!settingsObject[setting.getName()].is()) { + issue = Issue::Type; + } else if (!setting._validate(settingsObject[setting.getName()].as())) { + issue = Issue::Validator; } - } else if (setting->isRequired()) { - setReason(Issue::Missing); - return result; + } else if (setting.isRequired()) { + issue = Issue::Missing; } - } else if (iSetting->isLong()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject->containsKey(setting->getName())) { - if (!(*settingsObject)[setting->getName()].is()) { - setReason(Issue::Type); - return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - setReason(Issue::Validator); - return result; + } else if (iSetting.isLong()) { + HomieSetting& setting = static_cast&>(iSetting); + + if (settingsObject.containsKey(setting.getName())) { + if (!settingsObject[setting.getName()].is()) { + issue = Issue::Type; + } else if (!setting._validate(settingsObject[setting.getName()].as())) { + issue = Issue::Validator; } - } else if (setting->isRequired()) { - setReason(Issue::Missing); - return result; + } else if (setting.isRequired()) { + issue = Issue::Missing; } - } else if (iSetting->isDouble()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject->containsKey(setting->getName())) { - if (!(*settingsObject)[setting->getName()].is()) { - setReason(Issue::Type); - return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - setReason(Issue::Validator); - return result; + } else if (iSetting.isDouble()) { + HomieSetting& setting = static_cast&>(iSetting); + + if (settingsObject.containsKey(setting.getName())) { + if (!settingsObject[setting.getName()].is()) { + issue = Issue::Type; + } else if (!setting._validate(settingsObject[setting.getName()].as())) { + issue = Issue::Validator; } - } else if (setting->isRequired()) { - setReason(Issue::Missing); - return result; + } else if (setting.isRequired()) { + issue = Issue::Missing; } - } else if (iSetting->isConstChar()) { - HomieSetting* setting = static_cast*>(iSetting); - - if (settingsObject->containsKey(setting->getName())) { - if (!(*settingsObject)[setting->getName()].is()) { - setReason(Issue::Type); - return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - setReason(Issue::Validator); - return result; + } else if (iSetting.isConstChar()) { + HomieSetting& setting = static_cast&>(iSetting); + + if (settingsObject.containsKey(setting.getName())) { + if (!settingsObject[setting.getName()].is()) { + issue = Issue::Type; + + } else if (!setting._validate(settingsObject[setting.getName()].as())) { + issue = Issue::Validator; } - } else if (setting->isRequired()) { - setReason(Issue::Missing); - return result; + } else if (setting.isRequired()) { + issue = Issue::Missing; + } + } + if (issue != Issue::None) { + switch (issue) { + case Issue::Type: + result.reason = String(iSetting.getName()) + F(" setting is not a ") + String(iSetting.getType()); + break; + case Issue::Validator: + result.reason = String(iSetting.getName()) + F(" setting does not pass the validator function"); + break; + case Issue::Missing: + result.reason = String(iSetting.getName()) + F(" setting is missing"); + break; } + return result; } } diff --git a/src/Homie/Utils/Validation.hpp b/src/Homie/Utils/Validation.hpp index 96c1f3fd..ee29190d 100644 --- a/src/Homie/Utils/Validation.hpp +++ b/src/Homie/Utils/Validation.hpp @@ -6,25 +6,18 @@ #include "Helpers.hpp" #include "../Limits.hpp" #include "../../HomieSetting.hpp" +#include "../Datatypes/Result.hpp" namespace HomieInternals { -struct ConfigValidationResult { - bool valid; - String reason; -}; -struct ConfigValidationResultOBJ : ConfigValidationResult { - JsonObject* config; -}; - class Validation { public: - static ConfigValidationResult validateConfig(const JsonObject& object); + static ValidationResult validateConfig(const JsonObject& object); private: - static ConfigValidationResult _validateConfigRoot(const JsonObject& object); - static ConfigValidationResult _validateConfigWifi(const JsonObject& object); - static ConfigValidationResult _validateConfigMqtt(const JsonObject& object); - static ConfigValidationResult _validateConfigOta(const JsonObject& object); - static ConfigValidationResult _validateConfigSettings(const JsonObject& object); + static ValidationResult _validateConfigRoot(const JsonObject& object); + static ValidationResult _validateConfigWifi(const JsonObject& object); + static ValidationResult _validateConfigMqtt(const JsonObject& object); + static ValidationResult _validateConfigOta(const JsonObject& object); + static ValidationResult _validateConfigSettings(const JsonObject& object); }; } // namespace HomieInternals diff --git a/src/HomieSetting.cpp b/src/HomieSetting.cpp index ea03f459..3f49782e 100644 --- a/src/HomieSetting.cpp +++ b/src/HomieSetting.cpp @@ -2,9 +2,9 @@ using namespace HomieInternals; -std::vector __attribute__((init_priority(101))) IHomieSetting::settings; +std::vector> __attribute__((init_priority(101))) IHomieSetting::settings; -HomieInternals::IHomieSetting::IHomieSetting(const char * name, const char * description) +IHomieSetting::IHomieSetting(const char * name, const char * description) : _name(name) , _description(description) , _required(true) @@ -30,7 +30,7 @@ HomieSetting::HomieSetting(const char* name, const char* description) : IHomieSetting(name, description) , _value() , _validator([](T candidate) { return true; }) { - IHomieSetting::settings.push_back(this); + IHomieSetting::settings.push_back(*this); } template @@ -43,6 +43,24 @@ bool HomieSetting::wasProvided() const { return _provided; } +template +bool HomieSetting::set(T value, bool saveToConfig) { + if (!_validate(value)) { + Interface::get().getLogger() << F("✖ Faild to set ") << _name << F(" setting. Reason: Did not pass validator") << endl; + return false; + } + _set(value); + if (saveToConfig) { + ValidationResult saveResult = Interface::get().getConfig().saveSetting(_name, _value); + if (!saveResult.valid) { + Interface::get().getLogger() << F("✖ Faild to save ") << _name << F(" setting to config file. Reason: ") << saveResult.reason << endl; + return false; + } + } + Interface::get().getLogger() << F("✔ Saved ") << _name << F(" setting to config file.") << endl; + return true; +} + template HomieSetting& HomieSetting::setDefaultValue(T defaultValue) { _value = defaultValue; @@ -57,47 +75,47 @@ HomieSetting& HomieSetting::setValidator(const std::function -bool HomieSetting::validate(T candidate) const { +bool HomieSetting::_validate(T candidate) const { return _validator(candidate); } template -void HomieSetting::set(T value) { +void HomieSetting::_set(T value) { _value = value; _provided = true; } template -bool HomieSetting::isBool() const { return false; } +bool HomieSetting::_isBool() const { return false; } template -bool HomieSetting::isLong() const { return false; } +bool HomieSetting::_isLong() const { return false; } template -bool HomieSetting::isDouble() const { return false; } +bool HomieSetting::_isDouble() const { return false; } template -bool HomieSetting::isConstChar() const { return false; } +bool HomieSetting::_isConstChar() const { return false; } template<> -bool HomieSetting::isBool() const { return true; } +bool HomieSetting::_isBool() const { return true; } template<> -const char* HomieSetting::getType() const { return "bool"; } +const char* HomieSetting::_getType() const { return "bool"; } template<> -bool HomieSetting::isLong() const { return true; } +bool HomieSetting::_isLong() const { return true; } template<> -const char* HomieSetting::getType() const { return "long"; } +const char* HomieSetting::_getType() const { return "long"; } template<> -bool HomieSetting::isDouble() const { return true; } +bool HomieSetting::_isDouble() const { return true; } template<> -const char* HomieSetting::getType() const { return "double"; } +const char* HomieSetting::_getType() const { return "double"; } template<> -bool HomieSetting::isConstChar() const { return true; } +bool HomieSetting::_isConstChar() const { return true; } template<> -const char* HomieSetting::getType() const { return "string"; } +const char* HomieSetting::_getType() const { return "string"; } // Needed because otherwise undefined reference to template class HomieSetting; diff --git a/src/HomieSetting.hpp b/src/HomieSetting.hpp index 0df2dc33..04f2f08a 100644 --- a/src/HomieSetting.hpp +++ b/src/HomieSetting.hpp @@ -3,8 +3,9 @@ #include #include #include "Arduino.h" - #include "./Homie/Datatypes/Callbacks.hpp" +#include "./Homie/Datatypes/Interface.hpp" +#include "./Homie/Datatypes/Result.hpp" namespace HomieInternals { class HomieClass; @@ -13,9 +14,12 @@ class Validation; class BootConfig; class IHomieSetting { - public: - static std::vector settings; + friend HomieInternals::HomieClass; + friend HomieInternals::Config; + friend HomieInternals::Validation; + friend HomieInternals::BootConfig; +public: bool isRequired() const; const char* getName() const; const char* getDescription() const; @@ -27,7 +31,9 @@ class IHomieSetting { virtual const char* getType() const { return "unknown"; } - protected: +protected: + static std::vector> settings; + explicit IHomieSetting(const char* name, const char* description); const char* _name; const char* _description; @@ -43,24 +49,25 @@ class HomieSetting : public HomieInternals::IHomieSetting { friend HomieInternals::Validation; friend HomieInternals::BootConfig; - public: +public: HomieSetting(const char* name, const char* description); T get() const; + bool set(T value, bool saveToConfig = false); bool wasProvided() const; HomieSetting& setDefaultValue(T defaultValue); HomieSetting& setValidator(const std::function& validator); - private: +private: T _value; std::function _validator; - bool validate(T candidate) const; - void set(T value); + bool _validate(T candidate) const; + void _set(T value); - bool isBool() const; - bool isLong() const; - bool isDouble() const; - bool isConstChar() const; + bool _isBool() const; + bool _isLong() const; + bool _isDouble() const; + bool _isConstChar() const; - const char* getType() const; + const char* _getType() const; }; From 494441dc27807df5b6e4ab855dfbbd902c522bae Mon Sep 17 00:00:00 2001 From: timpur Date: Fri, 29 Dec 2017 20:38:20 +1100 Subject: [PATCH 10/51] Lint Fixes --- src/Homie/Config.cpp | 10 +++++----- src/Homie/Config.hpp | 24 ++++++++++++------------ src/Homie/Datatypes/Result.hpp | 2 +- src/Homie/Utils/Validation.cpp | 1 - src/HomieSetting.hpp | 8 ++++---- 5 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 090a0df3..9002c998 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -27,7 +27,7 @@ bool Config::load() { Interface::get().getLogger() << F("↻ Config file loading") << endl; StaticJsonBuffer jsonBuffer; - ValidationResultOBJ loadResult = _loadConfigFile(jsonBuffer); + ValidationResultOBJ loadResult = _loadConfigFile(&jsonBuffer); if (!loadResult.valid) { Interface::get().getLogger() << loadResult.reason << endl; loadResult.config->prettyPrintTo(Interface::get().getLogger()); @@ -164,7 +164,7 @@ bool Config::load() { char* Config::getSafeConfigFile() { StaticJsonBuffer jsonBuffer; - ValidationResultOBJ configLoadResult = _loadConfigFile(jsonBuffer); + ValidationResultOBJ configLoadResult = _loadConfigFile(&jsonBuffer); if (!configLoadResult.valid) { return nullptr; } @@ -223,7 +223,7 @@ HomieBootMode Config::getHomieBootModeOnNextBoot() { } } -ValidationResultOBJ Config::_loadConfigFile(StaticJsonBuffer& jsonBuffer, bool skipValidation) { +ValidationResultOBJ Config::_loadConfigFile(StaticJsonBuffer* jsonBuffer, bool skipValidation) { ValidationResultOBJ result; result.valid = false; @@ -252,7 +252,7 @@ ValidationResultOBJ Config::_loadConfigFile(StaticJsonBufferparseObject(configFile); ValidationResult configValidationResult = validateConfig(config, skipValidation); @@ -345,7 +345,7 @@ ValidationResult Config::patch(const char* patch) { // Validate current config StaticJsonBuffer currentJsonBuffer; - ValidationResultOBJ configLoadResult = _loadConfigFile(currentJsonBuffer); + ValidationResultOBJ configLoadResult = _loadConfigFile(¤tJsonBuffer); if (!configLoadResult.valid) { result.reason = F("✖ Old Config file is not valid, reason: "); result.reason.concat(configLoadResult.reason); diff --git a/src/Homie/Config.hpp b/src/Homie/Config.hpp index c49a06c5..01ff05cc 100644 --- a/src/Homie/Config.hpp +++ b/src/Homie/Config.hpp @@ -4,21 +4,21 @@ #include #include "FS.h" -#include "Datatypes/Interface.hpp" -#include "Datatypes/ConfigStruct.hpp" -#include "Utils/DeviceId.hpp" -#include "Utils/Validation.hpp" -#include "Constants.hpp" -#include "Limits.hpp" +#include "./Datatypes/Interface.hpp" +#include "./Datatypes/ConfigStruct.hpp" +#include "./Utils/DeviceId.hpp" +#include "./Utils/Validation.hpp" +#include "./Constants.hpp" +#include "./Limits.hpp" #include "../HomieBootMode.hpp" #include "../HomieSetting.hpp" #include "../StreamingOperator.hpp" -#include "Strings.hpp" -#include "./DataTypes/Result.hpp" +#include "./Strings.hpp" +#include "./Datatypes/Result.hpp" namespace HomieInternals { class Config { -public: + public: Config(); bool load(); const ConfigStruct& get() const; @@ -35,13 +35,13 @@ class Config { static ValidationResult validateConfig(const JsonObject& parsedJson, bool skipValidation = false); -private: + private: ConfigStruct _configStruct; bool _spiffsBegan; bool _valid; bool _spiffsBegin(); - ValidationResultOBJ _loadConfigFile(StaticJsonBuffer& jsonBuffer, bool skipValidation = false); + ValidationResultOBJ _loadConfigFile(StaticJsonBuffer* jsonBuffer, bool skipValidation = false); }; } // namespace HomieInternals @@ -55,7 +55,7 @@ ValidationResult Config::saveSetting(const char* name, T value) { result.valid = false; StaticJsonBuffer currentJsonBuffer; - ValidationResultOBJ configLoadResult = _loadConfigFile(currentJsonBuffer, true); + ValidationResultOBJ configLoadResult = _loadConfigFile(¤tJsonBuffer, true); if (!configLoadResult.valid) { result.reason = F("✖ Old Config file is not valid, reason: "); result.reason.concat(configLoadResult.reason); diff --git a/src/Homie/Datatypes/Result.hpp b/src/Homie/Datatypes/Result.hpp index 3331152c..ce10673e 100644 --- a/src/Homie/Datatypes/Result.hpp +++ b/src/Homie/Datatypes/Result.hpp @@ -12,4 +12,4 @@ struct ValidationResult { struct ValidationResultOBJ : ValidationResult { JsonObject* config; }; -} +}// namespace HomieInternals diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index aeabf5ac..9344bdcc 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -302,7 +302,6 @@ ValidationResult Validation::_validateConfigSettings(const JsonObject& object) { }; for (IHomieSetting& iSetting : IHomieSetting::settings) { - Issue issue = Issue::None; if (iSetting.isBool()) { diff --git a/src/HomieSetting.hpp b/src/HomieSetting.hpp index 04f2f08a..70f60e95 100644 --- a/src/HomieSetting.hpp +++ b/src/HomieSetting.hpp @@ -19,7 +19,7 @@ class IHomieSetting { friend HomieInternals::Validation; friend HomieInternals::BootConfig; -public: + public: bool isRequired() const; const char* getName() const; const char* getDescription() const; @@ -31,7 +31,7 @@ class IHomieSetting { virtual const char* getType() const { return "unknown"; } -protected: + protected: static std::vector> settings; explicit IHomieSetting(const char* name, const char* description); @@ -49,7 +49,7 @@ class HomieSetting : public HomieInternals::IHomieSetting { friend HomieInternals::Validation; friend HomieInternals::BootConfig; -public: + public: HomieSetting(const char* name, const char* description); T get() const; bool set(T value, bool saveToConfig = false); @@ -57,7 +57,7 @@ class HomieSetting : public HomieInternals::IHomieSetting { HomieSetting& setDefaultValue(T defaultValue); HomieSetting& setValidator(const std::function& validator); -private: + private: T _value; std::function _validator; From 9faa2b9a9713a9158be7ce079ee8185aeb82cd9c Mon Sep 17 00:00:00 2001 From: timpur Date: Fri, 29 Dec 2017 20:47:36 +1100 Subject: [PATCH 11/51] Version Change --- library.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.json b/library.json index 7704c58e..40291dfa 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "Homie", - "version": "2.0.0", + "version": "2.1.0", "keywords": "iot, home, automation, mqtt, esp8266, async, sensor", "description": "ESP8266 framework for Homie, a lightweight MQTT convention for the IoT", "homepage": "http://marvinroger.github.io/homie-esp8266/", From 74a21d093b6f10301d17617d3112853eb43a66be Mon Sep 17 00:00:00 2001 From: timpur Date: Sat, 30 Dec 2017 08:15:22 +1100 Subject: [PATCH 12/51] Added getDeviceID --- docs/others/cpp-api-reference.md | 6 ++++++ src/Homie.cpp | 4 ++++ src/Homie.hpp | 1 + 3 files changed, 11 insertions(+) diff --git a/docs/others/cpp-api-reference.md b/docs/others/cpp-api-reference.md index 97dcff4a..f97c4320 100644 --- a/docs/others/cpp-api-reference.md +++ b/docs/others/cpp-api-reference.md @@ -215,6 +215,12 @@ Logger& getLogger(); Get the underlying `Logger` object, which is only a wrapper around `Serial` by default. +```c++ +String getDeviceID(); +``` + +Get the Device ID, which homie is using. Could be the config `device_id` or the inertnal device `mac`. + ------- ## HomieNode diff --git a/src/Homie.cpp b/src/Homie.cpp index 48c39a19..d79009d2 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -333,6 +333,10 @@ Logger& HomieClass::getLogger() { return _logger; } +String HomieInternals::HomieClass::getDeviceID() { + return String(Interface::get().getConfig().get().deviceId); +} + void HomieClass::prepareToSleep() { Interface::get().getLogger() << F("Flagged for sleep by sketch") << endl; if (Interface::get().ready) { diff --git a/src/Homie.hpp b/src/Homie.hpp index f9267441..96e69df0 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -64,6 +64,7 @@ class HomieClass { static const ConfigStruct& getConfiguration(); AsyncMqttClient& getMqttClient(); Logger& getLogger(); + static String getDeviceID(); static void prepareToSleep(); static void doDeepSleep(uint32_t time_us = 0, RFMode mode = RF_DEFAULT); From b0ac5ffbbab84f449c112c674e9addffcb4e5762 Mon Sep 17 00:00:00 2001 From: timpur Date: Sat, 30 Dec 2017 12:36:17 +1100 Subject: [PATCH 13/51] Add the ability to access settings before setup ( #372 ) --- docs/others/cpp-api-reference.md | 10 ++++ src/Homie.cpp | 93 +++++++++++++++++--------------- src/Homie.hpp | 3 ++ src/Homie/Config.cpp | 4 +- src/Homie/Utils/Validation.cpp | 14 ++--- 5 files changed, 74 insertions(+), 50 deletions(-) diff --git a/docs/others/cpp-api-reference.md b/docs/others/cpp-api-reference.md index f97c4320..0c7eb54c 100644 --- a/docs/others/cpp-api-reference.md +++ b/docs/others/cpp-api-reference.md @@ -46,6 +46,16 @@ Set the brand of the device, used in the configuration AP, the device hostname a * **`name`**: Name of the brand. Default value is `Homie` +```c++ +bool loadSettings(); +``` + +Validates settings and loads the config file setting values. Returns true if the config file load was successful and false if it failed. + +**Note**, if config file load faild then only default values will be present. + +**Note**, since this method loads the config file, you can also use `getConfiguration()` before `setup()`. + ```c++ Homie& disableLogging(); ``` diff --git a/src/Homie.cpp b/src/Homie.cpp index d79009d2..c9f3bad1 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -5,6 +5,7 @@ using namespace HomieInternals; HomieClass::HomieClass() : _setupCalled(false) , _firmwareSet(false) + , _loadedSettings(false) , __HOMIE_SIGNATURE("\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25") { strlcpy(Interface::get().brand, DEFAULT_BRAND, MAX_BRAND_STRING_LENGTH); Interface::get().bootMode = HomieBootMode::UNDEFINED; @@ -51,52 +52,14 @@ void HomieClass::_checkBeforeSetup(const __FlashStringHelper* functionName) cons void HomieClass::setup() { _setupCalled = true; - // Check if firmware is set + // check if firmware is set if (!_firmwareSet) { Helpers::abort(F("✖ Firmware name must be set before calling setup()")); return; // never reached, here for clarity } - // Check the max allowed setting elements - if (IHomieSetting::settings.size() > MAX_CONFIG_SETTING_SIZE) { - Helpers::abort(F("✖ Settings exceed set limit of elelement.")); - return; // never reached, here for clarity - } - - // Check if default settings values are valid - - - for (IHomieSetting& iSetting : IHomieSetting::settings) { - bool defaultSettingsValuesValid = true; - if (iSetting.isBool()) { - HomieSetting& setting = static_cast&>(iSetting); - if (!setting.isRequired() && !setting._validate(setting.get())) { - defaultSettingsValuesValid = false; - } - } else if (iSetting.isLong()) { - HomieSetting& setting = static_cast&>(iSetting); - if (!setting.isRequired() && !setting._validate(setting.get())) { - defaultSettingsValuesValid = false; - } - } else if (iSetting.isDouble()) { - HomieSetting& setting = static_cast&>(iSetting); - if (!setting.isRequired() && !setting._validate(setting.get())) { - defaultSettingsValuesValid = false; - } - } else if (iSetting.isConstChar()) { - HomieSetting& setting = static_cast&>(iSetting); - if (!setting.isRequired() && !setting._validate(setting.get())) { - defaultSettingsValuesValid = false; - } - } - if (!defaultSettingsValuesValid) { - String error = F("✖ Default setting value does not pass validator test for "); - error.concat(iSetting.getName()); - error.concat(F(" setting")); - Helpers::abort(error); - return; // never reached, here for clarity - } - } + // load settings & load config + if (!_loadedSettings) loadSettings(); // boot mode set during this boot by application before Homie.setup() HomieBootMode _applicationHomieBootMode = Interface::get().bootMode; @@ -119,7 +82,7 @@ void HomieClass::setup() { } // validate selected mode and fallback as needed - if (_selectedHomieBootMode == HomieBootMode::NORMAL && !Interface::get().getConfig().load()) { + if (_selectedHomieBootMode == HomieBootMode::NORMAL && !Interface::get().getConfig().isValid()) { Interface::get().getLogger() << F("Configuration invalid. Using CONFIG MODE") << endl; _selectedHomieBootMode = HomieBootMode::CONFIGURATION; } @@ -160,6 +123,52 @@ void HomieClass::loop() { } } +bool HomieInternals::HomieClass::loadSettings() { + // Check the max allowed setting elements + if (IHomieSetting::settings.size() > MAX_CONFIG_SETTING_SIZE) { + Helpers::abort(F("✖ Settings exceed set limit of elelement.")); + return false; // never reached, here for clarity + } + + // Check if default settings values are valid + for (IHomieSetting& iSetting : IHomieSetting::settings) { + bool defaultSettingsValuesValid = true; + if (iSetting.isBool()) { + HomieSetting& setting = static_cast&>(iSetting); + if (!setting.isRequired() && !setting._validate(setting.get())) { + defaultSettingsValuesValid = false; + } + } else if (iSetting.isLong()) { + HomieSetting& setting = static_cast&>(iSetting); + if (!setting.isRequired() && !setting._validate(setting.get())) { + defaultSettingsValuesValid = false; + } + } else if (iSetting.isDouble()) { + HomieSetting& setting = static_cast&>(iSetting); + if (!setting.isRequired() && !setting._validate(setting.get())) { + defaultSettingsValuesValid = false; + } + } else if (iSetting.isConstChar()) { + HomieSetting& setting = static_cast&>(iSetting); + if (!setting.isRequired() && !setting._validate(setting.get())) { + defaultSettingsValuesValid = false; + } + } + if (!defaultSettingsValuesValid) { + String error = F("✖ Default setting value does not pass validator test for "); + error.concat(iSetting.getName()); + error.concat(F(" setting")); + Helpers::abort(error); + return false; // never reached, here for clarity + } + } + + _loadedSettings = true; + + // load the config to get the config for settings; + return Interface::get().getConfig().load(); +} + HomieClass& HomieClass::disableLogging() { _checkBeforeSetup(F("disableLogging")); diff --git a/src/Homie.hpp b/src/Homie.hpp index 96e69df0..f9f9e5a2 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -38,6 +38,8 @@ class HomieClass { void setup(); void loop(); + bool loadSettings(); + void __setFirmware(const char* name, const char* version); void __setBrand(const char* brand) const; @@ -71,6 +73,7 @@ class HomieClass { private: bool _setupCalled; bool _firmwareSet; + bool _loadedSettings; Boot* _boot; BootStandalone _bootStandalone; BootNormal _bootNormal; diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 9002c998..34b3a902 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -29,8 +29,8 @@ bool Config::load() { StaticJsonBuffer jsonBuffer; ValidationResultOBJ loadResult = _loadConfigFile(&jsonBuffer); if (!loadResult.valid) { + Interface::get().getLogger() << F("✖ Config file Faild to load") << endl; Interface::get().getLogger() << loadResult.reason << endl; - loadResult.config->prettyPrintTo(Interface::get().getLogger()); return false; } JsonObject& parsedJson = *loadResult.config; @@ -436,6 +436,8 @@ void Config::log() const { } else if (iSetting.isConstChar()) { HomieSetting& setting = static_cast&>(iSetting); Interface::get().getLogger() << setting.getName() << F(": ") << setting.get() << F(" (") << (setting.wasProvided() ? F("set") : F("default")) << F(")"); + } else { + Interface::get().getLogger() << iSetting.getName() << F(": unknown type: ") << iSetting.getType(); } Interface::get().getLogger() << endl; diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index 9344bdcc..bfe9592c 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -298,7 +298,7 @@ ValidationResult Validation::_validateConfigSettings(const JsonObject& object) { None, Type, Validator, - Missing + Required }; for (IHomieSetting& iSetting : IHomieSetting::settings) { @@ -314,7 +314,7 @@ ValidationResult Validation::_validateConfigSettings(const JsonObject& object) { issue = Issue::Validator; } } else if (setting.isRequired()) { - issue = Issue::Missing; + issue = Issue::Required; } } else if (iSetting.isLong()) { HomieSetting& setting = static_cast&>(iSetting); @@ -326,7 +326,7 @@ ValidationResult Validation::_validateConfigSettings(const JsonObject& object) { issue = Issue::Validator; } } else if (setting.isRequired()) { - issue = Issue::Missing; + issue = Issue::Required; } } else if (iSetting.isDouble()) { HomieSetting& setting = static_cast&>(iSetting); @@ -338,7 +338,7 @@ ValidationResult Validation::_validateConfigSettings(const JsonObject& object) { issue = Issue::Validator; } } else if (setting.isRequired()) { - issue = Issue::Missing; + issue = Issue::Required; } } else if (iSetting.isConstChar()) { HomieSetting& setting = static_cast&>(iSetting); @@ -351,7 +351,7 @@ ValidationResult Validation::_validateConfigSettings(const JsonObject& object) { issue = Issue::Validator; } } else if (setting.isRequired()) { - issue = Issue::Missing; + issue = Issue::Required; } } if (issue != Issue::None) { @@ -362,8 +362,8 @@ ValidationResult Validation::_validateConfigSettings(const JsonObject& object) { case Issue::Validator: result.reason = String(iSetting.getName()) + F(" setting does not pass the validator function"); break; - case Issue::Missing: - result.reason = String(iSetting.getName()) + F(" setting is missing"); + case Issue::Required: + result.reason = String(iSetting.getName()) + F(" setting is missing. Required!"); break; } return result; From e64784b12dcd79fa992795dadc2eda7b4f12e258 Mon Sep 17 00:00:00 2001 From: timpur Date: Sat, 30 Dec 2017 13:08:55 +1100 Subject: [PATCH 14/51] Fix to Refactor error for Settings. --- src/HomieSetting.cpp | 24 ++++++++++++------------ src/HomieSetting.hpp | 14 +++++++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/HomieSetting.cpp b/src/HomieSetting.cpp index 3f49782e..18e1ea5b 100644 --- a/src/HomieSetting.cpp +++ b/src/HomieSetting.cpp @@ -86,36 +86,36 @@ void HomieSetting::_set(T value) { } template -bool HomieSetting::_isBool() const { return false; } +bool HomieSetting::isBool() const { return false; } template -bool HomieSetting::_isLong() const { return false; } +bool HomieSetting::isLong() const { return false; } template -bool HomieSetting::_isDouble() const { return false; } +bool HomieSetting::isDouble() const { return false; } template -bool HomieSetting::_isConstChar() const { return false; } +bool HomieSetting::isConstChar() const { return false; } template<> -bool HomieSetting::_isBool() const { return true; } +bool HomieSetting::isBool() const { return true; } template<> -const char* HomieSetting::_getType() const { return "bool"; } +const char* HomieSetting::getType() const { return "bool"; } template<> -bool HomieSetting::_isLong() const { return true; } +bool HomieSetting::isLong() const { return true; } template<> -const char* HomieSetting::_getType() const { return "long"; } +const char* HomieSetting::getType() const { return "long"; } template<> -bool HomieSetting::_isDouble() const { return true; } +bool HomieSetting::isDouble() const { return true; } template<> -const char* HomieSetting::_getType() const { return "double"; } +const char* HomieSetting::getType() const { return "double"; } template<> -bool HomieSetting::_isConstChar() const { return true; } +bool HomieSetting::isConstChar() const { return true; } template<> -const char* HomieSetting::_getType() const { return "string"; } +const char* HomieSetting::getType() const { return "string"; } // Needed because otherwise undefined reference to template class HomieSetting; diff --git a/src/HomieSetting.hpp b/src/HomieSetting.hpp index 70f60e95..670941d0 100644 --- a/src/HomieSetting.hpp +++ b/src/HomieSetting.hpp @@ -57,17 +57,17 @@ class HomieSetting : public HomieInternals::IHomieSetting { HomieSetting& setDefaultValue(T defaultValue); HomieSetting& setValidator(const std::function& validator); + bool isBool() const; + bool isLong() const; + bool isDouble() const; + bool isConstChar() const; + + const char* getType() const; + private: T _value; std::function _validator; bool _validate(T candidate) const; void _set(T value); - - bool _isBool() const; - bool _isLong() const; - bool _isDouble() const; - bool _isConstChar() const; - - const char* _getType() const; }; From 00843f3bbfca00de94fba98f780aba80a1ae0cd3 Mon Sep 17 00:00:00 2001 From: timpur Date: Sat, 30 Dec 2017 22:20:27 +1100 Subject: [PATCH 15/51] TLS support + fingerprint attribute in config file ( #399 ) Credit @adriancuzman --- docs/configuration/json-configuration-file.md | 5 ++++- src/Homie/Boot/BootNormal.cpp | 14 +++++++++++++- src/Homie/Config.cpp | 19 +++++++++++++++++++ src/Homie/Datatypes/ConfigStruct.hpp | 5 +++++ src/Homie/Limits.hpp | 1 + src/Homie/Utils/Helpers.cpp | 19 +++++++++++++++++-- src/Homie/Utils/Helpers.hpp | 6 ++++-- src/Homie/Utils/Validation.cpp | 17 +++++++++++++++++ 8 files changed, 80 insertions(+), 6 deletions(-) diff --git a/docs/configuration/json-configuration-file.md b/docs/configuration/json-configuration-file.md index 2924eef8..6cbd6d24 100644 --- a/docs/configuration/json-configuration-file.md +++ b/docs/configuration/json-configuration-file.md @@ -24,7 +24,9 @@ Below is the format of the JSON configuration you will have to provide: "base_topic": "devices/", "auth": true, "username": "user", - "password": "pass" + "password": "pass", + "ssl": true, + "ssl_fingerprint": "a27992d3420c89f293d351378ba5f5675f74fe3c" }, "ota": { "enabled": true @@ -46,6 +48,7 @@ Here are the rules: - `bssid` and `channel` have to be defined together and these settings are independand of settings related to static IP - to define static IP, `ip` (IP address), `mask` (netmask) and `gw` (gateway) settings have to be defined at the same time - to define second DNS `dns2` the first one `dns1` has to be defined. Set DNS without `ip`, `mask` and `gw` does not affect the configuration (dns server will be provided by DHCP). It is not required to set DNS servers. +* `ssl_fingerprint` if not required if `ssl` is enabled. Default values if not provided: diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index 5e298045..036c992c 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -62,6 +62,18 @@ void BootNormal::setup() { Interface::get().getMqttClient().onPublish(std::bind(&BootNormal::_onMqttPublish, this, std::placeholders::_1)); Interface::get().getMqttClient().setServer(Interface::get().getConfig().get().mqtt.server.host, Interface::get().getConfig().get().mqtt.server.port); + +#if ASYNC_TCP_SSL_ENABLED + Interface::get().getLogger() << "SSL is: " << Interface::get().getConfig().get().mqtt.server.ssl.enabled << endl; + Interface::get().getMqttClient().setSecure(Interface::get().getConfig().get().mqtt.server.ssl.enabled); + if (Interface::get().getConfig().get().mqtt.server.ssl.enabled && Interface::get().getConfig().get().mqtt.server.ssl.hasFingerprint) { + char hexBuf[MAX_FINGERPRINT_SIZE * 2 + 1]; + Helpers::byteArrayToHexString(Interface::get().getConfig().get().mqtt.server.ssl.fingerprint, hexBuf, MAX_FINGERPRINT_SIZE); + Interface::get().getLogger() << "Using fingerprint: " << hexBuf << endl; + Interface::get().getMqttClient().addServerFingerprint((const uint8_t*)Interface::get().getConfig().get().mqtt.server.ssl.fingerprint); + } +#endif + Interface::get().getMqttClient().setMaxTopicLength(MAX_MQTT_TOPIC_LENGTH); _mqttClientId = std::unique_ptr(new char[strlen(Interface::get().brand) + 1 + strlen(Interface::get().getConfig().get().deviceId) + 1]); strcpy(_mqttClientId.get(), Interface::get().brand); @@ -919,4 +931,4 @@ bool HomieInternals::BootNormal::__handleNodeProperty(char * topic, char * paylo } return false; -} + } diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 34b3a902..fdc19eeb 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -84,6 +84,14 @@ bool Config::load() { if (parsedJson["mqtt"].as().containsKey("port")) { reqMqttPort = parsedJson["mqtt"]["port"]; } + bool reqMqttSsl = false; + if (parsedJson["mqtt"].as().containsKey("ssl")) { + reqMqttSsl = parsedJson["mqtt"]["ssl"]; + } + const char* reqMqttFingerprint = ""; + if (parsedJson["mqtt"].as().containsKey("ssl_fingerprint")) { + reqMqttFingerprint = parsedJson["mqtt"]["ssl_fingerprint"]; + } const char* reqMqttBaseTopic = DEFAULT_MQTT_BASE_TOPIC; if (parsedJson["mqtt"].as().containsKey("base_topic")) { reqMqttBaseTopic = parsedJson["mqtt"]["base_topic"]; @@ -119,6 +127,11 @@ bool Config::load() { strlcpy(_configStruct.wifi.dns1, reqWifiDns1, MAX_IP_STRING_LENGTH); strlcpy(_configStruct.wifi.dns2, reqWifiDns2, MAX_IP_STRING_LENGTH); strlcpy(_configStruct.mqtt.server.host, reqMqttHost, MAX_HOSTNAME_LENGTH); + _configStruct.mqtt.server.ssl.enabled = reqMqttSsl; + if (strcmp_P(reqMqttFingerprint, PSTR("")) != 0) { + _configStruct.mqtt.server.ssl.hasFingerprint = true; + Helpers::hexStringToByteArray(reqMqttFingerprint, _configStruct.mqtt.server.ssl.fingerprint, MAX_FINGERPRINT_SIZE); + } _configStruct.mqtt.server.port = reqMqttPort; strlcpy(_configStruct.mqtt.baseTopic, reqMqttBaseTopic, MAX_MQTT_BASE_TOPIC_LENGTH); _configStruct.mqtt.auth = reqMqttAuth; @@ -409,6 +422,12 @@ void Config::log() const { Interface::get().getLogger() << F(" • MQTT: ") << endl; Interface::get().getLogger() << F(" ◦ Host: ") << _configStruct.mqtt.server.host << endl; Interface::get().getLogger() << F(" ◦ Port: ") << _configStruct.mqtt.server.port << endl; + Interface::get().getLogger() << F(" ◦ SSL enabled: ") << (_configStruct.mqtt.server.ssl.enabled ? "true" : "false") << endl; + if (_configStruct.mqtt.server.ssl.enabled && _configStruct.mqtt.server.ssl.hasFingerprint) { + char hexBuf[MAX_FINGERPRINT_SIZE * 2 + 1]; + Helpers::byteArrayToHexString(Interface::get().getConfig().get().mqtt.server.ssl.fingerprint, hexBuf, MAX_FINGERPRINT_SIZE); + Interface::get().getLogger() << F(" ◦ Fingerprint: ") << hexBuf << endl; + } Interface::get().getLogger() << F(" ◦ Base topic: ") << _configStruct.mqtt.baseTopic << endl; Interface::get().getLogger() << F(" ◦ Auth? ") << (_configStruct.mqtt.auth ? F("yes") : F("no")) << endl; if (_configStruct.mqtt.auth) { diff --git a/src/Homie/Datatypes/ConfigStruct.hpp b/src/Homie/Datatypes/ConfigStruct.hpp index 558974b8..a70c43ca 100644 --- a/src/Homie/Datatypes/ConfigStruct.hpp +++ b/src/Homie/Datatypes/ConfigStruct.hpp @@ -25,6 +25,11 @@ struct ConfigStruct { struct Server { char host[MAX_HOSTNAME_LENGTH]; uint16_t port; + struct { + bool enabled; + bool hasFingerprint; + uint8_t fingerprint[MAX_FINGERPRINT_SIZE]; + } ssl; } server; char baseTopic[MAX_MQTT_BASE_TOPIC_LENGTH]; bool auth; diff --git a/src/Homie/Limits.hpp b/src/Homie/Limits.hpp index b013de13..54cc01c0 100644 --- a/src/Homie/Limits.hpp +++ b/src/Homie/Limits.hpp @@ -21,6 +21,7 @@ namespace HomieInternals { const uint8_t MAX_WIFI_SSID_LENGTH = 32 + 1; const uint8_t MAX_WIFI_PASSWORD_LENGTH = 64 + 1; const uint16_t MAX_HOSTNAME_LENGTH = 255 + 1; + const uint8_t MAX_FINGERPRINT_SIZE = 20; const uint8_t MAX_MQTT_CREDS_LENGTH = 32 + 1; const uint8_t MAX_MQTT_BASE_TOPIC_LENGTH = 48 + 1; diff --git a/src/Homie/Utils/Helpers.cpp b/src/Homie/Utils/Helpers.cpp index ee7c9e4f..3dc523f0 100644 --- a/src/Homie/Utils/Helpers.cpp +++ b/src/Homie/Utils/Helpers.cpp @@ -83,10 +83,25 @@ void Helpers::ipToString(const IPAddress& ip, char * str) { snprintf(str, MAX_IP_STRING_LENGTH, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); } -void HomieInternals::Helpers::macToString(const uint8_t mac[MAX_MAC_LENGTH], char * str) { +void Helpers::macToString(const uint8_t mac[MAX_MAC_LENGTH], char * str) { snprintf(str, MAX_MAC_STRING_LENGTH, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); } -void HomieInternals::Helpers::macToFormattedString(const uint8_t mac[MAX_MAC_LENGTH], char * str) { +void Helpers::macToFormattedString(const uint8_t mac[MAX_MAC_LENGTH], char * str) { snprintf(str, MAX_MAC_FORMATTED_STRING_LENGTH, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); } + +void Helpers::hexStringToByteArray(const char* hexStr, uint8_t* hexArray, uint8_t size) { + for (uint8_t i = 0; i < size; i++) { + char hex[3]; + strncpy(hex, (hexStr + (i * 2)), 2); + hex[2] = '\0'; + hexArray[i] = (uint8_t)strtol((const char*)&hex, nullptr, 16); + } +} + +void Helpers::byteArrayToHexString(const uint8_t * hexArray, char* hexStr, uint8_t size) { + for (uint8_t i = 0; i < size; i++) { + snprintf((hexStr + (i * 2)), 3, "%02x", hexArray[i]); + } +} diff --git a/src/Homie/Utils/Helpers.hpp b/src/Homie/Utils/Helpers.hpp index 84d3261e..fd3764e1 100644 --- a/src/Homie/Utils/Helpers.hpp +++ b/src/Homie/Utils/Helpers.hpp @@ -1,14 +1,14 @@ #pragma once #include "Arduino.h" +#include #include #include "../../StreamingOperator.hpp" #include "../Limits.hpp" -#include namespace HomieInternals { class Helpers { - public: +public: static void abort(const String& message); static uint8_t rssiToPercentage(int32_t rssi); static void stringToBytes(const char* str, char sep, byte* bytes, int maxBytes, int base); @@ -19,5 +19,7 @@ class Helpers { static void ipToString(const IPAddress& ip, char* str); static void macToString(const uint8_t mac[MAX_MAC_LENGTH], char * str); static void macToFormattedString(const uint8_t mac[MAX_MAC_LENGTH], char * str); + static void hexStringToByteArray(const char* hexStr, uint8_t* hexArray, uint8_t size); + static void byteArrayToHexString(const uint8_t* hexArray, char* hexStr, uint8_t size); }; } // namespace HomieInternals diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index bfe9592c..5b201c52 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -200,6 +200,23 @@ ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { result.reason = F("mqtt.port is not an integer"); return result; } + if (object["mqtt"].as().containsKey("ssl")) { + if (!object["mqtt"]["ssl"].is()) { + result.reason = F("mqtt.ssl is not a bool"); + return result; + } + } + if (object["mqtt"].as().containsKey("ssl_fingerprint")) { + if (!object["mqtt"]["ssl_fingerprint"].is()) { + result.reason = F("mqtt.ssl_fingerprint is not a string"); + return result; + } + + if (strlen(object["mqtt"]["ssl_fingerprint"]) > MAX_FINGERPRINT_SIZE * 2) { + result.reason = F("mqtt.ssl_fingerprint is too long"); + return result; + } + } if (object["mqtt"].as().containsKey("base_topic")) { if (!object["mqtt"]["base_topic"].is()) { result.reason = F("mqtt.base_topic is not a string"); From 3a15a2bb8d73a7f561958150572c9b0963ee6b22 Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 1 Jan 2018 14:11:16 +1100 Subject: [PATCH 16/51] Minor Refactor of Limit Names --- src/Homie.cpp | 8 ++++---- src/Homie/Boot/BootConfig.cpp | 2 +- src/Homie/Boot/BootNormal.cpp | 2 +- src/Homie/Config.cpp | 16 +++++++-------- src/Homie/Datatypes/ConfigStruct.hpp | 16 +++++++-------- src/Homie/Datatypes/Interface.hpp | 6 +++--- src/Homie/Limits.hpp | 29 ++++++++++++++-------------- src/Homie/Utils/Helpers.hpp | 2 ++ src/Homie/Utils/Validation.cpp | 26 ++++++++++++------------- src/HomieNode.cpp | 2 +- 10 files changed, 55 insertions(+), 54 deletions(-) diff --git a/src/Homie.cpp b/src/Homie.cpp index c9f3bad1..19363952 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -206,20 +206,20 @@ HomieClass& HomieClass::setConfigurationApPassword(const char* password) { _checkBeforeSetup(F("setConfigurationApPassword")); Interface::get().configurationAp.secured = true; - strlcpy(Interface::get().configurationAp.password, password, MAX_WIFI_PASSWORD_LENGTH); + strlcpy(Interface::get().configurationAp.password, password, MAX_WIFI_PASSWORD_STRING_LENGTH); return *this; } void HomieClass::__setFirmware(const char* name, const char* version) { _checkBeforeSetup(F("setFirmware")); - if (strlen(name) + 1 > MAX_FIRMWARE_NAME_LENGTH || strlen(version) + 1 > MAX_FIRMWARE_VERSION_LENGTH) { + if (strlen(name) + 1 > MAX_FIRMWARE_NAME_STRING_LENGTH || strlen(version) + 1 > MAX_FIRMWARE_VERSION_STRING_LENGTH) { Helpers::abort(F("✖ setFirmware(): either the name or version string is too long")); return; // never reached, here for clarity } - strncpy(Interface::get().firmware.name, name, MAX_FIRMWARE_NAME_LENGTH); + strncpy(Interface::get().firmware.name, name, MAX_FIRMWARE_NAME_STRING_LENGTH); Interface::get().firmware.name[strlen(name)] = '\0'; - strncpy(Interface::get().firmware.version, version, MAX_FIRMWARE_VERSION_LENGTH); + strncpy(Interface::get().firmware.version, version, MAX_FIRMWARE_VERSION_STRING_LENGTH); Interface::get().firmware.version[strlen(version)] = '\0'; _firmwareSet = true; } diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 6a2ef813..adb5ec61 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -31,7 +31,7 @@ void BootConfig::setup() { WiFi.mode(WIFI_AP_STA); - char apName[MAX_WIFI_SSID_LENGTH]; + char apName[MAX_WIFI_SSID_STRING_LENGTH]; strlcpy(apName, Interface::get().brand, MAX_BRAND_STRING_LENGTH); strcat_P(apName, PSTR("-")); strcat(apName, DeviceId::get()); diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index 036c992c..21dcd025 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -74,7 +74,7 @@ void BootNormal::setup() { } #endif - Interface::get().getMqttClient().setMaxTopicLength(MAX_MQTT_TOPIC_LENGTH); + Interface::get().getMqttClient().setMaxTopicLength(MAX_MQTT_TOPIC_STRING_LENGTH); _mqttClientId = std::unique_ptr(new char[strlen(Interface::get().brand) + 1 + strlen(Interface::get().getConfig().get().deviceId) + 1]); strcpy(_mqttClientId.get(), Interface::get().brand); strcat_P(_mqttClientId.get(), PSTR("-")); diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index fdc19eeb..e01f0131 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -114,11 +114,11 @@ bool Config::load() { reqOtaEnabled = parsedJson["ota"]["enabled"]; } - strlcpy(_configStruct.name, reqName, MAX_FRIENDLY_NAME_LENGTH); - strlcpy(_configStruct.deviceId, reqDeviceId, MAX_DEVICE_ID_LENGTH); + strlcpy(_configStruct.name, reqName, MAX_FRIENDLY_NAME_STRING_LENGTH); + strlcpy(_configStruct.deviceId, reqDeviceId, MAX_DEVICE_ID_STRING_LENGTH); _configStruct.deviceStatsInterval = regDeviceStatsInterval; - strlcpy(_configStruct.wifi.ssid, reqWifiSsid, MAX_WIFI_SSID_LENGTH); - if (reqWifiPassword) strlcpy(_configStruct.wifi.password, reqWifiPassword, MAX_WIFI_PASSWORD_LENGTH); + strlcpy(_configStruct.wifi.ssid, reqWifiSsid, MAX_WIFI_SSID_STRING_LENGTH); + if (reqWifiPassword) strlcpy(_configStruct.wifi.password, reqWifiPassword, MAX_WIFI_PASSWORD_STRING_LENGTH); strlcpy(_configStruct.wifi.bssid, reqWifiBssid, MAX_MAC_STRING_LENGTH); _configStruct.wifi.channel = reqWifiChannel; strlcpy(_configStruct.wifi.ip, reqWifiIp, MAX_IP_STRING_LENGTH); @@ -126,17 +126,17 @@ bool Config::load() { strlcpy(_configStruct.wifi.mask, reqWifiMask, MAX_IP_STRING_LENGTH); strlcpy(_configStruct.wifi.dns1, reqWifiDns1, MAX_IP_STRING_LENGTH); strlcpy(_configStruct.wifi.dns2, reqWifiDns2, MAX_IP_STRING_LENGTH); - strlcpy(_configStruct.mqtt.server.host, reqMqttHost, MAX_HOSTNAME_LENGTH); + strlcpy(_configStruct.mqtt.server.host, reqMqttHost, MAX_HOSTNAME_STRING_LENGTH); _configStruct.mqtt.server.ssl.enabled = reqMqttSsl; if (strcmp_P(reqMqttFingerprint, PSTR("")) != 0) { _configStruct.mqtt.server.ssl.hasFingerprint = true; Helpers::hexStringToByteArray(reqMqttFingerprint, _configStruct.mqtt.server.ssl.fingerprint, MAX_FINGERPRINT_SIZE); } _configStruct.mqtt.server.port = reqMqttPort; - strlcpy(_configStruct.mqtt.baseTopic, reqMqttBaseTopic, MAX_MQTT_BASE_TOPIC_LENGTH); + strlcpy(_configStruct.mqtt.baseTopic, reqMqttBaseTopic, MAX_MQTT_BASE_TOPIC_STRING_LENGTH); _configStruct.mqtt.auth = reqMqttAuth; - strlcpy(_configStruct.mqtt.username, reqMqttUsername, MAX_MQTT_CREDS_LENGTH); - strlcpy(_configStruct.mqtt.password, reqMqttPassword, MAX_MQTT_CREDS_LENGTH); + strlcpy(_configStruct.mqtt.username, reqMqttUsername, MAX_MQTT_CREDS_STRING_LENGTH); + strlcpy(_configStruct.mqtt.password, reqMqttPassword, MAX_MQTT_CREDS_STRING_LENGTH); _configStruct.ota.enabled = reqOtaEnabled; /* Parse the settings */ diff --git a/src/Homie/Datatypes/ConfigStruct.hpp b/src/Homie/Datatypes/ConfigStruct.hpp index a70c43ca..0e3d9033 100644 --- a/src/Homie/Datatypes/ConfigStruct.hpp +++ b/src/Homie/Datatypes/ConfigStruct.hpp @@ -5,13 +5,13 @@ namespace HomieInternals { struct ConfigStruct { - char name[MAX_FRIENDLY_NAME_LENGTH]; - char deviceId[MAX_DEVICE_ID_LENGTH]; + char name[MAX_FRIENDLY_NAME_STRING_LENGTH]; + char deviceId[MAX_DEVICE_ID_STRING_LENGTH]; uint16_t deviceStatsInterval; struct WiFi { - char ssid[MAX_WIFI_SSID_LENGTH]; - char password[MAX_WIFI_PASSWORD_LENGTH]; + char ssid[MAX_WIFI_SSID_STRING_LENGTH]; + char password[MAX_WIFI_PASSWORD_STRING_LENGTH]; char bssid[MAX_MAC_STRING_LENGTH]; uint16_t channel; char ip[MAX_IP_STRING_LENGTH]; @@ -23,7 +23,7 @@ struct ConfigStruct { struct MQTT { struct Server { - char host[MAX_HOSTNAME_LENGTH]; + char host[MAX_HOSTNAME_STRING_LENGTH]; uint16_t port; struct { bool enabled; @@ -31,10 +31,10 @@ struct ConfigStruct { uint8_t fingerprint[MAX_FINGERPRINT_SIZE]; } ssl; } server; - char baseTopic[MAX_MQTT_BASE_TOPIC_LENGTH]; + char baseTopic[MAX_MQTT_BASE_TOPIC_STRING_LENGTH]; bool auth; - char username[MAX_MQTT_CREDS_LENGTH]; - char password[MAX_MQTT_CREDS_LENGTH]; + char username[MAX_MQTT_CREDS_STRING_LENGTH]; + char password[MAX_MQTT_CREDS_STRING_LENGTH]; } mqtt; struct OTA { diff --git a/src/Homie/Datatypes/Interface.hpp b/src/Homie/Datatypes/Interface.hpp index e9b97a4b..0432d954 100644 --- a/src/Homie/Datatypes/Interface.hpp +++ b/src/Homie/Datatypes/Interface.hpp @@ -32,12 +32,12 @@ class InterfaceData { struct ConfigurationAP { bool secured; - char password[MAX_WIFI_PASSWORD_LENGTH]; + char password[MAX_WIFI_PASSWORD_STRING_LENGTH]; } configurationAp; struct Firmware { - char name[MAX_FIRMWARE_NAME_LENGTH]; - char version[MAX_FIRMWARE_VERSION_LENGTH]; + char name[MAX_FIRMWARE_NAME_STRING_LENGTH]; + char version[MAX_FIRMWARE_VERSION_STRING_LENGTH]; } firmware; struct LED { diff --git a/src/Homie/Limits.hpp b/src/Homie/Limits.hpp index 54cc01c0..fb32d7db 100644 --- a/src/Homie/Limits.hpp +++ b/src/Homie/Limits.hpp @@ -18,24 +18,25 @@ namespace HomieInternals { const uint8_t MAX_MAC_STRING_LENGTH = (MAX_MAC_LENGTH * 2) + 1; const uint8_t MAX_MAC_FORMATTED_STRING_LENGTH = (MAX_MAC_LENGTH * 2) + 5 + 1; - const uint8_t MAX_WIFI_SSID_LENGTH = 32 + 1; - const uint8_t MAX_WIFI_PASSWORD_LENGTH = 64 + 1; - const uint16_t MAX_HOSTNAME_LENGTH = 255 + 1; + const uint8_t MAX_WIFI_SSID_STRING_LENGTH = 32 + 1; + const uint8_t MAX_WIFI_PASSWORD_STRING_LENGTH = 64 + 1; + const uint16_t MAX_HOSTNAME_STRING_LENGTH = 255 + 1; const uint8_t MAX_FINGERPRINT_SIZE = 20; + const uint8_t MAX_FINGERPRINT_STRING_LENGTH = (MAX_FINGERPRINT_SIZE *2 ) + 1; - const uint8_t MAX_MQTT_CREDS_LENGTH = 32 + 1; - const uint8_t MAX_MQTT_BASE_TOPIC_LENGTH = 48 + 1; - const uint8_t MAX_MQTT_TOPIC_LENGTH = 128 + 1; + const uint8_t MAX_MQTT_CREDS_STRING_LENGTH = 32 + 1; + const uint8_t MAX_MQTT_BASE_TOPIC_STRING_LENGTH = 48 + 1; + const uint8_t MAX_MQTT_TOPIC_STRING_LENGTH = 128 + 1; - const uint8_t MAX_FRIENDLY_NAME_LENGTH = 64 + 1; - const uint8_t MAX_DEVICE_ID_LENGTH = 32 + 1; + const uint8_t MAX_FRIENDLY_NAME_STRING_LENGTH = 64 + 1; + const uint8_t MAX_DEVICE_ID_STRING_LENGTH = 32 + 1; - const uint8_t MAX_BRAND_STRING_LENGTH = (MAX_WIFI_SSID_LENGTH - 1) - (MAX_MAC_STRING_LENGTH - 1) - 1 + 1; - const uint8_t MAX_FIRMWARE_NAME_LENGTH = 32 + 1; - const uint8_t MAX_FIRMWARE_VERSION_LENGTH = 16 + 1; + const uint8_t MAX_BRAND_STRING_LENGTH = (MAX_WIFI_SSID_STRING_LENGTH - 1) - ((MAX_MAC_STRING_LENGTH - 1) - 1) + 1; + const uint8_t MAX_FIRMWARE_NAME_STRING_LENGTH = 32 + 1; + const uint8_t MAX_FIRMWARE_VERSION_STRING_LENGTH = 16 + 1; - const uint8_t MAX_NODE_ID_LENGTH = 24 + 1; - const uint8_t MAX_NODE_TYPE_LENGTH = 24 + 1; - const uint8_t MAX_NODE_PROPERTY_LENGTH = 24 + 1; + const uint8_t MAX_NODE_ID_STRING_LENGTH = 24 + 1; + const uint8_t MAX_NODE_TYPE_STRING_LENGTH = 24 + 1; + const uint8_t MAX_NODE_PROPERTY_STRING_LENGTH = 24 + 1; } // namespace HomieInternals diff --git a/src/Homie/Utils/Helpers.hpp b/src/Homie/Utils/Helpers.hpp index fd3764e1..3cbc27fc 100644 --- a/src/Homie/Utils/Helpers.hpp +++ b/src/Homie/Utils/Helpers.hpp @@ -19,7 +19,9 @@ class Helpers { static void ipToString(const IPAddress& ip, char* str); static void macToString(const uint8_t mac[MAX_MAC_LENGTH], char * str); static void macToFormattedString(const uint8_t mac[MAX_MAC_LENGTH], char * str); + // size in bytes static void hexStringToByteArray(const char* hexStr, uint8_t* hexArray, uint8_t size); + // size in bytes static void byteArrayToHexString(const uint8_t* hexArray, char* hexStr, uint8_t size); }; } // namespace HomieInternals diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index 5b201c52..31f4a2d3 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -26,7 +26,7 @@ ValidationResult Validation::_validateConfigRoot(const JsonObject& object) { result.reason = F("name is not a string"); return result; } - if (strlen(object["name"]) + 1 > MAX_FRIENDLY_NAME_LENGTH) { + if (strlen(object["name"]) + 1 > MAX_FRIENDLY_NAME_STRING_LENGTH) { result.reason = F("name is too long"); return result; } @@ -35,7 +35,7 @@ ValidationResult Validation::_validateConfigRoot(const JsonObject& object) { result.reason = F("device_id is not a string"); return result; } - if (strlen(object["device_id"]) + 1 > MAX_DEVICE_ID_LENGTH) { + if (strlen(object["device_id"]) + 1 > MAX_DEVICE_ID_STRING_LENGTH) { result.reason = F("device_id is too long"); return result; } @@ -69,7 +69,7 @@ ValidationResult Validation::_validateConfigWifi(const JsonObject& object) { result.reason = F("wifi.ssid is not a string"); return result; } - if (strlen(object["wifi"]["ssid"]) + 1 > MAX_WIFI_SSID_LENGTH) { + if (strlen(object["wifi"]["ssid"]) + 1 > MAX_WIFI_SSID_STRING_LENGTH) { result.reason = F("wifi.ssid is too long"); return result; } @@ -77,7 +77,7 @@ ValidationResult Validation::_validateConfigWifi(const JsonObject& object) { result.reason = F("wifi.password is not a string"); return result; } - if (object["wifi"]["password"] && strlen(object["wifi"]["password"]) + 1 > MAX_WIFI_PASSWORD_LENGTH) { + if (object["wifi"]["password"] && strlen(object["wifi"]["password"]) + 1 > MAX_WIFI_PASSWORD_STRING_LENGTH) { result.reason = F("wifi.password is too long"); return result; } @@ -192,7 +192,7 @@ ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { result.reason = F("mqtt.host is not a string"); return result; } - if (strlen(object["mqtt"]["host"]) + 1 > MAX_HOSTNAME_LENGTH) { + if (strlen(object["mqtt"]["host"]) + 1 > MAX_HOSTNAME_STRING_LENGTH) { result.reason = F("mqtt.host is too long"); return result; } @@ -200,11 +200,9 @@ ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { result.reason = F("mqtt.port is not an integer"); return result; } - if (object["mqtt"].as().containsKey("ssl")) { - if (!object["mqtt"]["ssl"].is()) { - result.reason = F("mqtt.ssl is not a bool"); - return result; - } + if (object["mqtt"].as().containsKey("ssl") && !object["mqtt"]["ssl"].is()) { + result.reason = F("mqtt.ssl is not a bool"); + return result; } if (object["mqtt"].as().containsKey("ssl_fingerprint")) { if (!object["mqtt"]["ssl_fingerprint"].is()) { @@ -212,7 +210,7 @@ ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { return result; } - if (strlen(object["mqtt"]["ssl_fingerprint"]) > MAX_FINGERPRINT_SIZE * 2) { + if (strlen(object["mqtt"]["ssl_fingerprint"]) + 1 > MAX_FINGERPRINT_STRING_LENGTH) { result.reason = F("mqtt.ssl_fingerprint is too long"); return result; } @@ -223,7 +221,7 @@ ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { return result; } - if (strlen(object["mqtt"]["base_topic"]) + 1 > MAX_MQTT_BASE_TOPIC_LENGTH) { + if (strlen(object["mqtt"]["base_topic"]) + 1 > MAX_MQTT_BASE_TOPIC_STRING_LENGTH) { result.reason = F("mqtt.base_topic is too long"); return result; } @@ -239,7 +237,7 @@ ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { result.reason = F("mqtt.username is not a string"); return result; } - if (strlen(object["mqtt"]["username"]) + 1 > MAX_MQTT_CREDS_LENGTH) { + if (strlen(object["mqtt"]["username"]) + 1 > MAX_MQTT_CREDS_STRING_LENGTH) { result.reason = F("mqtt.username is too long"); return result; } @@ -247,7 +245,7 @@ ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { result.reason = F("mqtt.password is not a string"); return result; } - if (strlen(object["mqtt"]["password"]) + 1 > MAX_MQTT_CREDS_LENGTH) { + if (strlen(object["mqtt"]["password"]) + 1 > MAX_MQTT_CREDS_STRING_LENGTH) { result.reason = F("mqtt.password is too long"); return result; } diff --git a/src/HomieNode.cpp b/src/HomieNode.cpp index de66f4ed..f5d96d1e 100644 --- a/src/HomieNode.cpp +++ b/src/HomieNode.cpp @@ -23,7 +23,7 @@ HomieNode::HomieNode(const char* id, const char* type, const NodeInputHandler& i , _type(type) , _properties() , _inputHandler(inputHandler) { - if (strlen(id) + 1 > MAX_NODE_ID_LENGTH || strlen(type) + 1 > MAX_NODE_TYPE_LENGTH) { + if (strlen(id) + 1 > MAX_NODE_ID_STRING_LENGTH || strlen(type) + 1 > MAX_NODE_TYPE_STRING_LENGTH) { Helpers::abort(F("✖ HomieNode(): either the id or type string is too long")); return; // never reached, here for clarity } From 2a8a8f135926dd2fc22a0276669f0f06c0c554dc Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 1 Jan 2018 14:20:57 +1100 Subject: [PATCH 17/51] Refactor SSL to only compile when enabled (`#if ASYNC_TCP_SSL_ENABLED`) --- src/Homie/Boot/BootNormal.cpp | 2 +- src/Homie/Config.cpp | 6 +++++- src/Homie/Datatypes/ConfigStruct.hpp | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index 21dcd025..e2d8358b 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -67,7 +67,7 @@ void BootNormal::setup() { Interface::get().getLogger() << "SSL is: " << Interface::get().getConfig().get().mqtt.server.ssl.enabled << endl; Interface::get().getMqttClient().setSecure(Interface::get().getConfig().get().mqtt.server.ssl.enabled); if (Interface::get().getConfig().get().mqtt.server.ssl.enabled && Interface::get().getConfig().get().mqtt.server.ssl.hasFingerprint) { - char hexBuf[MAX_FINGERPRINT_SIZE * 2 + 1]; + char hexBuf[MAX_FINGERPRINT_STRING_LENGTH]; Helpers::byteArrayToHexString(Interface::get().getConfig().get().mqtt.server.ssl.fingerprint, hexBuf, MAX_FINGERPRINT_SIZE); Interface::get().getLogger() << "Using fingerprint: " << hexBuf << endl; Interface::get().getMqttClient().addServerFingerprint((const uint8_t*)Interface::get().getConfig().get().mqtt.server.ssl.fingerprint); diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index e01f0131..920d073e 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -127,11 +127,13 @@ bool Config::load() { strlcpy(_configStruct.wifi.dns1, reqWifiDns1, MAX_IP_STRING_LENGTH); strlcpy(_configStruct.wifi.dns2, reqWifiDns2, MAX_IP_STRING_LENGTH); strlcpy(_configStruct.mqtt.server.host, reqMqttHost, MAX_HOSTNAME_STRING_LENGTH); +#if ASYNC_TCP_SSL_ENABLED _configStruct.mqtt.server.ssl.enabled = reqMqttSsl; if (strcmp_P(reqMqttFingerprint, PSTR("")) != 0) { _configStruct.mqtt.server.ssl.hasFingerprint = true; Helpers::hexStringToByteArray(reqMqttFingerprint, _configStruct.mqtt.server.ssl.fingerprint, MAX_FINGERPRINT_SIZE); } +#endif _configStruct.mqtt.server.port = reqMqttPort; strlcpy(_configStruct.mqtt.baseTopic, reqMqttBaseTopic, MAX_MQTT_BASE_TOPIC_STRING_LENGTH); _configStruct.mqtt.auth = reqMqttAuth; @@ -422,12 +424,14 @@ void Config::log() const { Interface::get().getLogger() << F(" • MQTT: ") << endl; Interface::get().getLogger() << F(" ◦ Host: ") << _configStruct.mqtt.server.host << endl; Interface::get().getLogger() << F(" ◦ Port: ") << _configStruct.mqtt.server.port << endl; +#if ASYNC_TCP_SSL_ENABLED Interface::get().getLogger() << F(" ◦ SSL enabled: ") << (_configStruct.mqtt.server.ssl.enabled ? "true" : "false") << endl; if (_configStruct.mqtt.server.ssl.enabled && _configStruct.mqtt.server.ssl.hasFingerprint) { - char hexBuf[MAX_FINGERPRINT_SIZE * 2 + 1]; + char hexBuf[MAX_FINGERPRINT_STRING_LENGTH]; Helpers::byteArrayToHexString(Interface::get().getConfig().get().mqtt.server.ssl.fingerprint, hexBuf, MAX_FINGERPRINT_SIZE); Interface::get().getLogger() << F(" ◦ Fingerprint: ") << hexBuf << endl; } +#endif Interface::get().getLogger() << F(" ◦ Base topic: ") << _configStruct.mqtt.baseTopic << endl; Interface::get().getLogger() << F(" ◦ Auth? ") << (_configStruct.mqtt.auth ? F("yes") : F("no")) << endl; if (_configStruct.mqtt.auth) { diff --git a/src/Homie/Datatypes/ConfigStruct.hpp b/src/Homie/Datatypes/ConfigStruct.hpp index 0e3d9033..69cbb932 100644 --- a/src/Homie/Datatypes/ConfigStruct.hpp +++ b/src/Homie/Datatypes/ConfigStruct.hpp @@ -25,11 +25,13 @@ struct ConfigStruct { struct Server { char host[MAX_HOSTNAME_STRING_LENGTH]; uint16_t port; +#if ASYNC_TCP_SSL_ENABLED struct { bool enabled; bool hasFingerprint; uint8_t fingerprint[MAX_FINGERPRINT_SIZE]; } ssl; +#endif } server; char baseTopic[MAX_MQTT_BASE_TOPIC_STRING_LENGTH]; bool auth; From 87b01f03cd4766b9b5102d34655b098f31ff53b9 Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 1 Jan 2018 15:04:06 +1100 Subject: [PATCH 18/51] Uupdated Homie log messages --- src/Homie/Boot/BootNormal.cpp | 14 ++++++++------ src/Homie/Config.cpp | 12 +++++++----- src/Homie/Utils/Validation.cpp | 4 ++-- src/HomieSetting.cpp | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index e2d8358b..7db5ce3f 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -64,13 +64,15 @@ void BootNormal::setup() { Interface::get().getMqttClient().setServer(Interface::get().getConfig().get().mqtt.server.host, Interface::get().getConfig().get().mqtt.server.port); #if ASYNC_TCP_SSL_ENABLED - Interface::get().getLogger() << "SSL is: " << Interface::get().getConfig().get().mqtt.server.ssl.enabled << endl; - Interface::get().getMqttClient().setSecure(Interface::get().getConfig().get().mqtt.server.ssl.enabled); - if (Interface::get().getConfig().get().mqtt.server.ssl.enabled && Interface::get().getConfig().get().mqtt.server.ssl.hasFingerprint) { + bool ssl = Interface::get().getConfig().get().mqtt.server.ssl.enabled; + Interface::get().getLogger() << (ssl ? F("🔒 ") : F("🔓 ")) << F("SSL is: ") << (ssl ? F("enabled") : F("disabled")) << endl; + Interface::get().getMqttClient().setSecure(ssl); + if (ssl && Interface::get().getConfig().get().mqtt.server.ssl.hasFingerprint) { char hexBuf[MAX_FINGERPRINT_STRING_LENGTH]; - Helpers::byteArrayToHexString(Interface::get().getConfig().get().mqtt.server.ssl.fingerprint, hexBuf, MAX_FINGERPRINT_SIZE); - Interface::get().getLogger() << "Using fingerprint: " << hexBuf << endl; - Interface::get().getMqttClient().addServerFingerprint((const uint8_t*)Interface::get().getConfig().get().mqtt.server.ssl.fingerprint); + const uint8_t* fingerprint = (const uint8_t*)&Interface::get().getConfig().get().mqtt.server.ssl.fingerprint; + Helpers::byteArrayToHexString(fingerprint, hexBuf, MAX_FINGERPRINT_SIZE); + Interface::get().getLogger() << F("🔐 Using fingerprint: ") << hexBuf << endl; + Interface::get().getMqttClient().addServerFingerprint(fingerprint); } #endif diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 920d073e..9b1c1132 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -29,7 +29,7 @@ bool Config::load() { StaticJsonBuffer jsonBuffer; ValidationResultOBJ loadResult = _loadConfigFile(&jsonBuffer); if (!loadResult.valid) { - Interface::get().getLogger() << F("✖ Config file Faild to load") << endl; + Interface::get().getLogger() << F("✖ Config file faild to load") << endl; Interface::get().getLogger() << loadResult.reason << endl; return false; } @@ -339,6 +339,8 @@ ValidationResult Config::write(const JsonObject& newConfig) { newConfig.printTo(configFile); configFile.close(); + Interface::get().getLogger() << F("💾 Saved config file.") << endl; + result.valid = true; return result; } @@ -408,7 +410,7 @@ bool Config::isValid() const { void Config::log() const { Interface::get().getLogger() << F("{} Stored configuration") << endl; - Interface::get().getLogger() << F(" • Hardware device ID: ") << DeviceId::get() << endl; + Interface::get().getLogger() << F(" • Hardware Device ID: ") << DeviceId::get() << endl; Interface::get().getLogger() << F(" • Device ID: ") << _configStruct.deviceId << endl; Interface::get().getLogger() << F(" • Name: ") << _configStruct.name << endl; Interface::get().getLogger() << F(" • Device Stats Interval: ") << _configStruct.deviceStatsInterval << F(" sec") << endl; @@ -425,14 +427,14 @@ void Config::log() const { Interface::get().getLogger() << F(" ◦ Host: ") << _configStruct.mqtt.server.host << endl; Interface::get().getLogger() << F(" ◦ Port: ") << _configStruct.mqtt.server.port << endl; #if ASYNC_TCP_SSL_ENABLED - Interface::get().getLogger() << F(" ◦ SSL enabled: ") << (_configStruct.mqtt.server.ssl.enabled ? "true" : "false") << endl; + Interface::get().getLogger() << F(" ◦ SSL Enabled?: ") << (_configStruct.mqtt.server.ssl.enabled ? F("yes") : F("no")) << endl; if (_configStruct.mqtt.server.ssl.enabled && _configStruct.mqtt.server.ssl.hasFingerprint) { char hexBuf[MAX_FINGERPRINT_STRING_LENGTH]; Helpers::byteArrayToHexString(Interface::get().getConfig().get().mqtt.server.ssl.fingerprint, hexBuf, MAX_FINGERPRINT_SIZE); Interface::get().getLogger() << F(" ◦ Fingerprint: ") << hexBuf << endl; } #endif - Interface::get().getLogger() << F(" ◦ Base topic: ") << _configStruct.mqtt.baseTopic << endl; + Interface::get().getLogger() << F(" ◦ Base Topic: ") << _configStruct.mqtt.baseTopic << endl; Interface::get().getLogger() << F(" ◦ Auth? ") << (_configStruct.mqtt.auth ? F("yes") : F("no")) << endl; if (_configStruct.mqtt.auth) { Interface::get().getLogger() << F(" ◦ Username: ") << _configStruct.mqtt.username << endl; @@ -443,7 +445,7 @@ void Config::log() const { Interface::get().getLogger() << F(" ◦ Enabled? ") << (_configStruct.ota.enabled ? F("yes") : F("no")) << endl; if (IHomieSetting::settings.size() > 0) { - Interface::get().getLogger() << F(" • Custom settings: ") << endl; + Interface::get().getLogger() << F(" • Custom Settings: ") << endl; for (IHomieSetting& iSetting : IHomieSetting::settings) { Interface::get().getLogger() << F(" ◦ "); diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index 31f4a2d3..490bffea 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -210,8 +210,8 @@ ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { return result; } - if (strlen(object["mqtt"]["ssl_fingerprint"]) + 1 > MAX_FINGERPRINT_STRING_LENGTH) { - result.reason = F("mqtt.ssl_fingerprint is too long"); + if (strlen(object["mqtt"]["ssl_fingerprint"]) + 1 != MAX_FINGERPRINT_STRING_LENGTH) { + result.reason = F("mqtt.ssl_fingerprint is not the right length"); return result; } } diff --git a/src/HomieSetting.cpp b/src/HomieSetting.cpp index 18e1ea5b..1402c0fe 100644 --- a/src/HomieSetting.cpp +++ b/src/HomieSetting.cpp @@ -57,7 +57,7 @@ bool HomieSetting::set(T value, bool saveToConfig) { return false; } } - Interface::get().getLogger() << F("✔ Saved ") << _name << F(" setting to config file.") << endl; + Interface::get().getLogger() << F("💾 Saved ") << _name << F(" setting to config file.") << endl; return true; } From ef478e04afa6f49d3f6c6173d947a315b88386c7 Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 1 Jan 2018 15:06:43 +1100 Subject: [PATCH 19/51] Lint Issues --- src/Homie/Limits.hpp | 2 +- src/Homie/Utils/Helpers.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Homie/Limits.hpp b/src/Homie/Limits.hpp index fb32d7db..08809e56 100644 --- a/src/Homie/Limits.hpp +++ b/src/Homie/Limits.hpp @@ -22,7 +22,7 @@ namespace HomieInternals { const uint8_t MAX_WIFI_PASSWORD_STRING_LENGTH = 64 + 1; const uint16_t MAX_HOSTNAME_STRING_LENGTH = 255 + 1; const uint8_t MAX_FINGERPRINT_SIZE = 20; - const uint8_t MAX_FINGERPRINT_STRING_LENGTH = (MAX_FINGERPRINT_SIZE *2 ) + 1; + const uint8_t MAX_FINGERPRINT_STRING_LENGTH = (MAX_FINGERPRINT_SIZE *2) + 1; const uint8_t MAX_MQTT_CREDS_STRING_LENGTH = 32 + 1; const uint8_t MAX_MQTT_BASE_TOPIC_STRING_LENGTH = 48 + 1; diff --git a/src/Homie/Utils/Helpers.hpp b/src/Homie/Utils/Helpers.hpp index 3cbc27fc..9adef699 100644 --- a/src/Homie/Utils/Helpers.hpp +++ b/src/Homie/Utils/Helpers.hpp @@ -8,7 +8,7 @@ namespace HomieInternals { class Helpers { -public: + public: static void abort(const String& message); static uint8_t rssiToPercentage(int32_t rssi); static void stringToBytes(const char* str, char sep, byte* bytes, int maxBytes, int base); From 9f05e09538161d054b580d26213c4573f9c3dc2e Mon Sep 17 00:00:00 2001 From: Myles McNamara Date: Wed, 3 Jan 2018 16:22:25 -0500 Subject: [PATCH 20/51] Update API for /wifi/connect from GET to PUT (#468) Docs show `/wifi/connect` as `GET` when it should actually be `PUT` --- docs/configuration/http-json-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/http-json-api.md b/docs/configuration/http-json-api.md index 9287d093..c18f66e3 100644 --- a/docs/configuration/http-json-api.md +++ b/docs/configuration/http-json-api.md @@ -158,7 +158,7 @@ If anything goes wrong, a return code != 2xx will be returned, with a JSON `erro -------------- -??? summary "GET `/wifi/connect`" +??? summary "PUT `/wifi/connect`" Initiates the connection of the device to the Wi-Fi network while in configuation mode. This request is not synchronous and the result (Wi-Fi connected or not) must be obtained by with `GET /wifi/status`. ## Request body From 6495a3e75238a10807f877f9f316b71d04698309 Mon Sep 17 00:00:00 2001 From: timpur Date: Sun, 7 Jan 2018 20:45:53 +1100 Subject: [PATCH 21/51] Added Restart + Fixups --- docs/advanced-usage/custom-settings.md | 8 +- docs/advanced-usage/miscellaneous.md | 6 ++ docs/others/homie-implementation-specifics.md | 4 + src/Homie.cpp | 2 +- src/Homie/Boot/BootNormal.cpp | 76 +++++++++++++------ src/Homie/Boot/BootNormal.hpp | 2 + 6 files changed, 71 insertions(+), 27 deletions(-) diff --git a/docs/advanced-usage/custom-settings.md b/docs/advanced-usage/custom-settings.md index 52b3d326..0baae6f2 100644 --- a/docs/advanced-usage/custom-settings.md +++ b/docs/advanced-usage/custom-settings.md @@ -37,10 +37,16 @@ See the following example for a concrete use case: [![GitHub logo](../assets/github.png) CustomSettings.ino](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/CustomSettings/CustomSettings.ino) - You can also change the value of the setting at any time via updates to the config file or via code using: + ```c++ percentageSetting.set(50, true); ``` +You can also load the value of setthings from the config file before homie is setup. This is useful when you need these settings to setup nodes before you call `Homie.setup`. + +```c++ +Homie.loadSettings(); +``` + See the [API](http://marvinroger.github.io/homie-esp8266/docs/develop/others/cpp-api-reference/#homiesetting) for more information. diff --git a/docs/advanced-usage/miscellaneous.md b/docs/advanced-usage/miscellaneous.md index ae698c92..ab235da3 100644 --- a/docs/advanced-usage/miscellaneous.md +++ b/docs/advanced-usage/miscellaneous.md @@ -25,6 +25,7 @@ You can get access to the configuration of the device. The representation of the struct ConfigStruct { char* name; char* deviceId; + uint16_t deviceStatsInterval; struct WiFi { char* ssid; @@ -35,6 +36,11 @@ struct ConfigStruct { struct Server { char* host; uint16_t port; + struct { + bool enabled; + bool hasFingerprint; + uint8_t fingerprint[MAX_FINGERPRINT_SIZE]; + } ssl; } server; char* baseTopic; bool auth; diff --git a/docs/others/homie-implementation-specifics.md b/docs/others/homie-implementation-specifics.md index 0f1fbc67..27a8efcc 100644 --- a/docs/others/homie-implementation-specifics.md +++ b/docs/others/homie-implementation-specifics.md @@ -8,6 +8,10 @@ The Homie `$implementation` identifier is `esp8266`. * `$implementation/reset`: You can publish a `true` to this topic to reset the device +# Restart + +* `$implementation/restart`: You can publish a `true` to this topic to restart the device + # Configuration * `$implementation/config`: The `configuration.json` is published there, with `wifi.password`, `mqtt.username` and `mqtt.password` fields stripped diff --git a/src/Homie.cpp b/src/Homie.cpp index 19363952..cc040cd7 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -123,7 +123,7 @@ void HomieClass::loop() { } } -bool HomieInternals::HomieClass::loadSettings() { +bool HomieClass::loadSettings() { // Check the max allowed setting elements if (IHomieSetting::settings.size() > MAX_CONFIG_SETTING_SIZE) { Helpers::abort(F("✖ Settings exceed set limit of elelement.")); diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index 7db5ce3f..535b68c0 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -451,6 +451,10 @@ void BootNormal::_advertise() { break; case AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_RESET: packetId = Interface::get().getMqttClient().subscribe(_prefixMqttTopic(PSTR("/$implementation/reset")), 1); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_RESTART; + break; + case AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_RESTART: + packetId = Interface::get().getMqttClient().subscribe(_prefixMqttTopic(PSTR("/$implementation/restart")), 1); if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_CONFIG_SET; break; case AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_CONFIG_SET: @@ -548,11 +552,15 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa if (__handleResets(topic, payload, properties, len, index, total)) return; - // 6. handle config set + // 6. handle restarts + if (__handleRestarts(topic, payload, properties, len, index, total)) + return; + + // 7. handle config set if (__handleConfig(topic, payload, properties, len, index, total)) return; - // 7. here, we're sure we have a node property + // 8. here, we're sure we have a node property if (__handleNodeProperty(topic, payload, properties, len, index, total)) return; } @@ -593,7 +601,7 @@ void BootNormal::__splitTopic(char* topic) { } } -bool HomieInternals::BootNormal::__fillPayloadBuffer(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { +bool BootNormal::__fillPayloadBuffer(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { // Reallocate Buffer everytime a new message is received if (_mqttPayloadBuffer == nullptr || index == 0) _mqttPayloadBuffer = std::unique_ptr(new char[total + 1]); @@ -608,7 +616,7 @@ bool HomieInternals::BootNormal::__fillPayloadBuffer(char * topic, char * payloa return false; } -bool HomieInternals::BootNormal::__handleOTAUpdates(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { +bool BootNormal::__handleOTAUpdates(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { if ( _mqttTopicLevelsCount == 5 && strcmp(_mqttTopicLevels.get()[0], Interface::get().getConfig().get().deviceId) == 0 @@ -784,7 +792,7 @@ bool HomieInternals::BootNormal::__handleOTAUpdates(char* topic, char* payload, return false; } -bool HomieInternals::BootNormal::__handleBroadcasts(char * topic, char * payload, const AsyncMqttClientMessageProperties & properties, size_t len, size_t index, size_t total) { +bool BootNormal::__handleBroadcasts(char * topic, char * payload, const AsyncMqttClientMessageProperties & properties, size_t len, size_t index, size_t total) { if ( _mqttTopicLevelsCount == 2 && strcmp_P(_mqttTopicLevels.get()[0], PSTR("$broadcast")) == 0 @@ -802,45 +810,63 @@ bool HomieInternals::BootNormal::__handleBroadcasts(char * topic, char * payload return false; } -bool HomieInternals::BootNormal::__handleResets(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { +bool BootNormal::__handleResets(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { if ( _mqttTopicLevelsCount == 3 && strcmp_P(_mqttTopicLevels.get()[1], PSTR("$implementation")) == 0 && strcmp_P(_mqttTopicLevels.get()[2], PSTR("reset")) == 0 - && strcmp_P(_mqttPayloadBuffer.get(), PSTR("true")) == 0 ) { - Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/reset")), 1, true, "false"); - Interface::get().getLogger() << F("Flagged for reset by network") << endl; - Interface::get().disable = true; - Interface::get().reset.resetFlag = true; - return true; + if(strcmp_P(_mqttPayloadBuffer.get(), PSTR("true")) == 0){ + Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/reset")), 1, true, "false"); + Interface::get().getLogger() << F("Flagged for reset by network") << endl; + Interface::get().disable = true; + Interface::get().reset.resetFlag = true; + } + return true; } return false; } -bool HomieInternals::BootNormal::__handleConfig(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { +bool BootNormal::__handleRestarts(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total){ + if ( + _mqttTopicLevelsCount == 3 + && strcmp_P(_mqttTopicLevels.get()[1], PSTR("$implementation")) == 0 + && strcmp_P(_mqttTopicLevels.get()[2], PSTR("restart")) == 0 + ) { + if(strcmp_P(_mqttPayloadBuffer.get(), PSTR("true")) == 0){ + Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/restart")), 1, true, "false"); + Interface::get().getLogger() << F("Flagged for restart by network") << endl; + Interface::get().disable = true; + _flaggedForReboot = true; + } + return true; + } + return false; +} + +bool BootNormal::__handleConfig(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { if ( _mqttTopicLevelsCount == 4 && strcmp_P(_mqttTopicLevels.get()[1], PSTR("$implementation")) == 0 && strcmp_P(_mqttTopicLevels.get()[2], PSTR("config")) == 0 && strcmp_P(_mqttTopicLevels.get()[3], PSTR("set")) == 0 ) { - Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/config/set")), 1, true, ""); - ValidationResult configPatchResult = Interface::get().getConfig().patch(_mqttPayloadBuffer.get()); - if (configPatchResult.valid) { - Interface::get().getLogger() << F("✔ Configuration updated") << endl; - _flaggedForReboot = true; - Interface::get().getLogger() << F("Flagged for reboot") << endl; - } else { - Interface::get().getLogger() << F("✖ Configuration not updated") << endl; - Interface::get().getLogger() << F("Error: ") << configPatchResult.reason << endl; - } - return true; + Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/config/set")), 1, true, ""); + ValidationResult configPatchResult = Interface::get().getConfig().patch(_mqttPayloadBuffer.get()); + if (configPatchResult.valid) { + Interface::get().getLogger() << F("✔ Configuration updated") << endl; + _flaggedForReboot = true; + Interface::get().getLogger() << F("Flagged for reboot") << endl; + } else { + Interface::get().getLogger() << F("✖ Configuration not updated") << endl; + Interface::get().getLogger() << F("✖ Error: ") << configPatchResult.reason << endl; + } + return true; } return false; } -bool HomieInternals::BootNormal::__handleNodeProperty(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { +bool BootNormal::__handleNodeProperty(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { // initialize HomieRange HomieRange range; range.isRange = false; diff --git a/src/Homie/Boot/BootNormal.hpp b/src/Homie/Boot/BootNormal.hpp index d9937134..90faddb0 100644 --- a/src/Homie/Boot/BootNormal.hpp +++ b/src/Homie/Boot/BootNormal.hpp @@ -47,6 +47,7 @@ class BootNormal : public Boot { PUB_NODES, SUB_IMPLEMENTATION_OTA, SUB_IMPLEMENTATION_RESET, + SUB_IMPLEMENTATION_RESTART, SUB_IMPLEMENTATION_CONFIG_SET, SUB_SET, SUB_BROADCAST, @@ -106,6 +107,7 @@ class BootNormal : public Boot { bool __handleOTAUpdates(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); bool __handleBroadcasts(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); bool __handleResets(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleRestarts(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); bool __handleConfig(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); bool __handleNodeProperty(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); }; From 73bb2ab4f997c3da669c63dc4f80a52204d20c64 Mon Sep 17 00:00:00 2001 From: timpur Date: Tue, 9 Jan 2018 20:53:51 +1100 Subject: [PATCH 22/51] Fix #446 CORS Issue --- src/Homie/Boot/BootConfig.cpp | 35 ++++++++++++++--------------------- src/Homie/Boot/BootConfig.hpp | 2 +- src/Homie/Boot/BootNormal.cpp | 6 +++--- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index adb5ec61..48c14899 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -44,12 +44,13 @@ void BootConfig::setup() { } Helpers::ipToString(ACCESS_POINT_IP, _apIpStr); - Interface::get().getLogger() << F("AP started as ") << apName << F(" with IP ") << _apIpStr << endl; + _dns.setTTL(30); _dns.setErrorReplyCode(DNSReplyCode::NoError); _dns.start(53, F("*"), ACCESS_POINT_IP); + __setCORS(); _http.on("/heart", HTTP_GET, [this](AsyncWebServerRequest *request) { Interface::get().getLogger() << F("Received heart request") << endl; request->send(204); @@ -57,18 +58,17 @@ void BootConfig::setup() { _http.on("/device-info", HTTP_GET, [this](AsyncWebServerRequest *request) { _onDeviceInfoRequest(request); }); _http.on("/networks", HTTP_GET, [this](AsyncWebServerRequest *request) { _onNetworksRequest(request); }); _http.on("/config", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onConfigRequest(request); }).onBody(BootConfig::__parsePost); - _http.on("/config", HTTP_OPTIONS, [this](AsyncWebServerRequest *request) { // CORS - Interface::get().getLogger() << F("Received CORS request for /config") << endl; - __sendCORS(request); - }); _http.on("/wifi/connect", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onWifiConnectRequest(request); }).onBody(BootConfig::__parsePost); - _http.on("/wifi/connect", HTTP_OPTIONS, [this](AsyncWebServerRequest *request) { // CORS - Interface::get().getLogger() << F("Received CORS request for /wifi/connect") << endl; - __sendCORS(request); - }); _http.on("/wifi/status", HTTP_GET, [this](AsyncWebServerRequest *request) { _onWifiStatusRequest(request); }); _http.on("/proxy/control", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onProxyControlRequest(request); }).onBody(BootConfig::__parsePost); - _http.onNotFound([this](AsyncWebServerRequest *request) { _onCaptivePortal(request); }); + _http.onNotFound([this](AsyncWebServerRequest *request) { + if ( request->method() == HTTP_OPTIONS ) { + Interface::get().getLogger() << F("Received CORS request for ")<< request->url() << endl; + request->send(200); + } else { + _onCaptivePortal(request); + } + }); _http.begin(); } @@ -403,12 +403,10 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { _flaggedForRebootAt = millis(); } -void BootConfig::__sendCORS(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response = request->beginResponse(204); - response->addHeader(F("Access-Control-Allow-Origin"), F("*")); - response->addHeader(F("Access-Control-Allow-Methods"), F("PUT")); - response->addHeader(F("Access-Control-Allow-Headers"), F("Content-Type, Origin, Referer, User-Agent")); - request->send(response); +void BootConfig::__setCORS() { + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Origin"), F("*")); + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Methods"), F("GET, PUT")); + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Headers"), F("Content-Type, Origin, Referer, User-Agent")); } void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { @@ -423,11 +421,6 @@ void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size strcat(buff, (const char*)data); } } -static const String ConfigJSONError(const String error) { - const String BEGINNING = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); - const String END = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_END)); - return BEGINNING + error + END; -} void HomieInternals::BootConfig::__SendJSONError(AsyncWebServerRequest * request, String msg, int16_t code) { Interface::get().getLogger() << msg << endl; diff --git a/src/Homie/Boot/BootConfig.hpp b/src/Homie/Boot/BootConfig.hpp index 54a4c59f..6893de60 100644 --- a/src/Homie/Boot/BootConfig.hpp +++ b/src/Homie/Boot/BootConfig.hpp @@ -55,7 +55,7 @@ class BootConfig : public Boot { void _onWifiStatusRequest(AsyncWebServerRequest *request); // Helpers - static void __sendCORS(AsyncWebServerRequest *request); + static void __setCORS(); static const int MAX_POST_SIZE = 1500; static void __parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); static void __SendJSONError(AsyncWebServerRequest *request, String msg, int16_t code = 400); diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index 535b68c0..d882054d 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -816,7 +816,7 @@ bool BootNormal::__handleResets(char * topic, char * payload, const AsyncMqttCli && strcmp_P(_mqttTopicLevels.get()[1], PSTR("$implementation")) == 0 && strcmp_P(_mqttTopicLevels.get()[2], PSTR("reset")) == 0 ) { - if(strcmp_P(_mqttPayloadBuffer.get(), PSTR("true")) == 0){ + if ( strcmp_P(_mqttPayloadBuffer.get(), PSTR("true") ) == 0 ) { Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/reset")), 1, true, "false"); Interface::get().getLogger() << F("Flagged for reset by network") << endl; Interface::get().disable = true; @@ -827,13 +827,13 @@ bool BootNormal::__handleResets(char * topic, char * payload, const AsyncMqttCli return false; } -bool BootNormal::__handleRestarts(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total){ +bool BootNormal::__handleRestarts(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { if ( _mqttTopicLevelsCount == 3 && strcmp_P(_mqttTopicLevels.get()[1], PSTR("$implementation")) == 0 && strcmp_P(_mqttTopicLevels.get()[2], PSTR("restart")) == 0 ) { - if(strcmp_P(_mqttPayloadBuffer.get(), PSTR("true")) == 0){ + if ( strcmp_P(_mqttPayloadBuffer.get(), PSTR("true") ) == 0 ) { Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/restart")), 1, true, "false"); Interface::get().getLogger() << F("Flagged for restart by network") << endl; Interface::get().disable = true; From 852a4d652db181746a01c4e23414cf05e7c3cd66 Mon Sep 17 00:00:00 2001 From: timpur Date: Thu, 11 Jan 2018 20:22:09 +1100 Subject: [PATCH 23/51] Config Mode - get current config --- src/Homie/Boot/BootConfig.cpp | 67 +++++++++++++++++++++-------------- src/Homie/Boot/BootConfig.hpp | 3 +- src/Homie/Config.cpp | 4 +++ src/Homie/Config.hpp | 1 + 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 48c14899..565e1498 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -57,10 +57,11 @@ void BootConfig::setup() { }); _http.on("/device-info", HTTP_GET, [this](AsyncWebServerRequest *request) { _onDeviceInfoRequest(request); }); _http.on("/networks", HTTP_GET, [this](AsyncWebServerRequest *request) { _onNetworksRequest(request); }); + _http.on("/config", HTTP_GET, [this](AsyncWebServerRequest *request) { _onCurrentConfig(request); }); _http.on("/config", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onConfigRequest(request); }).onBody(BootConfig::__parsePost); - _http.on("/wifi/connect", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onWifiConnectRequest(request); }).onBody(BootConfig::__parsePost); + _http.on("/wifi/connect", HTTP_POST, [this](AsyncWebServerRequest *request) { _onWifiConnectRequest(request); }).onBody(BootConfig::__parsePost); _http.on("/wifi/status", HTTP_GET, [this](AsyncWebServerRequest *request) { _onWifiStatusRequest(request); }); - _http.on("/proxy/control", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onProxyControlRequest(request); }).onBody(BootConfig::__parsePost); + _http.on("/proxy/control", HTTP_POST, [this](AsyncWebServerRequest *request) { _onProxyControlRequest(request); }).onBody(BootConfig::__parsePost); _http.onNotFound([this](AsyncWebServerRequest *request) { if ( request->method() == HTTP_OPTIONS ) { Interface::get().getLogger() << F("Received CORS request for ")<< request->url() << endl; @@ -123,12 +124,12 @@ void BootConfig::_onWifiConnectRequest(AsyncWebServerRequest *request) { const char* body = (const char*)(request->_tempObject); JsonObject& parsedJson = parseJsonBuffer.parseObject(body); if (!parsedJson.success()) { - __SendJSONError(request, F("✖ Invalid or too big JSON")); + __SendJSONError(request, 400, F("✖ Invalid or too big JSON")); return; } if (!parsedJson.containsKey("ssid") || !parsedJson["ssid"].is() || !parsedJson.containsKey("password") || !parsedJson["password"].is()) { - __SendJSONError(request, F("✖ SSID and password required")); + __SendJSONError(request, 400, F("✖ SSID and password required")); return; } @@ -143,39 +144,36 @@ void BootConfig::_onWifiStatusRequest(AsyncWebServerRequest *request) { DynamicJsonBuffer generatedJsonBuffer(JSON_OBJECT_SIZE(2)); JsonObject& json = generatedJsonBuffer.createObject(); - String status; - //String json = ""; switch (WiFi.status()) { case WL_IDLE_STATUS: - status = F("idle"); + json["status"] = F("idle"); break; case WL_CONNECT_FAILED: - status = F("connect_failed"); + json["status"] = F("connect_failed"); break; case WL_CONNECTION_LOST: - status = F("connection_lost"); + json["status"] = F("connection_lost"); break; case WL_NO_SSID_AVAIL: - status = F("no_ssid_available"); + json["status"] = F("no_ssid_available"); break; case WL_CONNECTED: - status = F("connected"); + json["status"] = F("connected"); json["local_ip"] = WiFi.localIP().toString(); break; case WL_DISCONNECTED: - status = F("disconnected"); + json["status"] = F("disconnected"); break; default: - status = F("other"); + json["status"] = F("other"); break; } - json["status"] = status; - String output; - json.printTo(output); + AsyncResponseStream* response = request->beginResponseStream(FPSTR(PROGMEM_CONFIG_APPLICATION_JSON)); + json.printTo(*response); - request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), output); + request->send(response); } void BootConfig::_onProxyControlRequest(AsyncWebServerRequest *request) { @@ -184,12 +182,12 @@ void BootConfig::_onProxyControlRequest(AsyncWebServerRequest *request) { const char* body = (const char*)(request->_tempObject); JsonObject& parsedJson = parseJsonBuffer.parseObject(body); // do not use plain String, else fails if (!parsedJson.success()) { - __SendJSONError(request, F("✖ Invalid or too big JSON")); + __SendJSONError(request, 400, F("✖ Invalid or too big JSON")); return; } if (!parsedJson.containsKey("enable") || !parsedJson["enable"].is()) { - __SendJSONError(request, F("✖ enable parameter is required")); + __SendJSONError(request, 400, F("✖ enable parameter is required")); return; } @@ -361,10 +359,10 @@ void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { settings.add(jsonSetting); } - String output; - json.printTo(output); + AsyncResponseStream *response = request->beginResponseStream(FPSTR(PROGMEM_CONFIG_APPLICATION_JSON)); + json.printTo(*response); - request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), output); + request->send(response); } void BootConfig::_onNetworksRequest(AsyncWebServerRequest *request) { @@ -372,14 +370,31 @@ void BootConfig::_onNetworksRequest(AsyncWebServerRequest *request) { if (_wifiScanAvailable) { request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), _jsonWifiNetworks); } else { - __SendJSONError(request, F("Initial Wi-Fi scan not finished yet"), 503); + __SendJSONError(request, 503, F("Initial Wi-Fi scan not finished yet")); + } +} + +void BootConfig::_onCurrentConfig(AsyncWebServerRequest *request) { + StaticJsonBuffer jsonBuffer; + ValidationResultOBJ result = Interface::get().getConfig().getJsonObject(&jsonBuffer); + + if (!result.valid) { + Interface::get().getLogger() << F("✖ Error: ") << result.reason << endl; + __SendJSONError(request, 500, result.reason); + return; } + + JsonObject& json = *result.config; + AsyncResponseStream *response = request->beginResponseStream(FPSTR(PROGMEM_CONFIG_APPLICATION_JSON)); + json.printTo(*response); + + request->send(response); } void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { Interface::get().getLogger() << F("Received config request") << endl; if (_flaggedForReboot) { - __SendJSONError(request, F("✖ Device already configured"), 403); + __SendJSONError(request, 403, F("✖ Device already configured")); return; } @@ -390,7 +405,7 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { ValidationResult configWriteResult = Interface::get().getConfig().write(parsedJson); if (!configWriteResult.valid) { Interface::get().getLogger() << F("✖ Error: ") << configWriteResult.reason << endl; - __SendJSONError(request, configWriteResult.reason); + __SendJSONError(request, 500, configWriteResult.reason); return; } @@ -422,7 +437,7 @@ void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size } } -void HomieInternals::BootConfig::__SendJSONError(AsyncWebServerRequest * request, String msg, int16_t code) { +void HomieInternals::BootConfig::__SendJSONError(AsyncWebServerRequest * request, int16_t code, String msg) { Interface::get().getLogger() << msg << endl; const String BEGINNING = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); const String END = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_END)); diff --git a/src/Homie/Boot/BootConfig.hpp b/src/Homie/Boot/BootConfig.hpp index 6893de60..238f5676 100644 --- a/src/Homie/Boot/BootConfig.hpp +++ b/src/Homie/Boot/BootConfig.hpp @@ -53,11 +53,12 @@ class BootConfig : public Boot { void _onProxyControlRequest(AsyncWebServerRequest *request); void _proxyHttpRequest(AsyncWebServerRequest *request); void _onWifiStatusRequest(AsyncWebServerRequest *request); + void _onCurrentConfig(AsyncWebServerRequest *request); // Helpers static void __setCORS(); static const int MAX_POST_SIZE = 1500; static void __parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); - static void __SendJSONError(AsyncWebServerRequest *request, String msg, int16_t code = 400); + static void __SendJSONError(AsyncWebServerRequest *request, int16_t code, String msg); }; } // namespace HomieInternals diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 9b1c1132..81e70030 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -12,6 +12,10 @@ const ConfigStruct& Config::get() const { return _configStruct; } +ValidationResultOBJ Config::getJsonObject(StaticJsonBuffer* jsonBuffer) { + return _loadConfigFile(jsonBuffer, true); +} + bool Config::_spiffsBegin() { if (!_spiffsBegan) { _spiffsBegan = SPIFFS.begin(); diff --git a/src/Homie/Config.hpp b/src/Homie/Config.hpp index 01ff05cc..5e00fdc7 100644 --- a/src/Homie/Config.hpp +++ b/src/Homie/Config.hpp @@ -22,6 +22,7 @@ class Config { Config(); bool load(); const ConfigStruct& get() const; + ValidationResultOBJ getJsonObject(StaticJsonBuffer* jsonBuffer); char* getSafeConfigFile(); void erase(); void setHomieBootModeOnNextBoot(HomieBootMode bootMode); From 90257225ba4bde6bf21cc2717119366d0971aa50 Mon Sep 17 00:00:00 2001 From: timpur Date: Thu, 18 Jan 2018 07:19:29 +1100 Subject: [PATCH 24/51] Refactor internal flags + OneButton + Switch between modes using HomieButton + Added ability to get current config in config mode (towards #433) +General refactor --- docs/configuration/http-json-api.md | 4 +-- library.json | 19 ++++++---- src/Homie.cpp | 43 ++++++++++------------ src/Homie.h | 1 + src/Homie.hpp | 7 ++-- src/Homie/Boot/BootConfig.cpp | 16 +++++---- src/Homie/Boot/BootConfig.hpp | 4 +++ src/Homie/Boot/BootNormal.cpp | 43 ++++++++++------------ src/Homie/Boot/BootNormal.hpp | 4 ++- src/Homie/Boot/BootStandalone.cpp | 5 ++- src/Homie/Boot/BootStandalone.hpp | 3 ++ src/Homie/Config.cpp | 33 ++++++++++------- src/Homie/Config.hpp | 5 +-- src/Homie/Datatypes/Callbacks.hpp | 2 +- src/Homie/Datatypes/Interface.cpp | 5 ++- src/Homie/Datatypes/Interface.hpp | 15 ++++---- src/Homie/Utils/HomieButton.cpp | 56 +++++++++++++++++++++++++++++ src/Homie/Utils/HomieButton.hpp | 30 ++++++++++++++++ src/Homie/Utils/ResetHandler.cpp | 39 ++++++++++---------- src/Homie/Utils/ResetHandler.hpp | 12 ++++--- src/HomieEvent.hpp | 1 + src/HomieFirmwareConfig.h | 9 +++++ 22 files changed, 238 insertions(+), 118 deletions(-) create mode 100644 src/Homie/Utils/HomieButton.cpp create mode 100644 src/Homie/Utils/HomieButton.hpp create mode 100644 src/HomieFirmwareConfig.h diff --git a/docs/configuration/http-json-api.md b/docs/configuration/http-json-api.md index c18f66e3..15065b2d 100644 --- a/docs/configuration/http-json-api.md +++ b/docs/configuration/http-json-api.md @@ -158,7 +158,7 @@ If anything goes wrong, a return code != 2xx will be returned, with a JSON `erro -------------- -??? summary "PUT `/wifi/connect`" +??? summary "POST `/wifi/connect`" Initiates the connection of the device to the Wi-Fi network while in configuation mode. This request is not synchronous and the result (Wi-Fi connected or not) must be obtained by with `GET /wifi/status`. ## Request body @@ -221,7 +221,7 @@ If anything goes wrong, a return code != 2xx will be returned, with a JSON `erro -------------- -??? summary "PUT `/proxy/control`" +??? summary "POST `/proxy/control`" Enable/disable the device to act as a transparent proxy between AP and Station networks. All requests that don't collide with existing API paths will be bridged to the destination according to the `Host` HTTP header. The destination host is called using the existing Wi-Fi connection (established after a `PUT /wifi/connect`) and all contents are bridged back to the connection made to the AP side. diff --git a/library.json b/library.json index 40291dfa..473a6999 100644 --- a/library.json +++ b/library.json @@ -5,14 +5,12 @@ "description": "ESP8266 framework for Homie, a lightweight MQTT convention for the IoT", "homepage": "http://marvinroger.github.io/homie-esp8266/", "license": "MIT", - "authors": - { + "authors": { "name": "Marvin Roger", "url": "https://www.marvinroger.fr", "maintainer": true }, - "repository": - { + "repository": { "type": "git", "url": "https://github.com/marvinroger/homie-esp8266.git", "branch": "master" @@ -32,6 +30,9 @@ "name": "Bounce2", "version": "^2.1.0" }, + { + "name": "OneButton" + }, { "name": "ESP Async WebServer" } @@ -43,5 +44,11 @@ "src/*", "examples/*" ] - } -} + }, + "build": { + "libCompatMode": 2, + "flags":[ + "-D HOMIE_FIRMWARE_HOMIE_BUTTON=1" + ] + } +} \ No newline at end of file diff --git a/src/Homie.cpp b/src/Homie.cpp index cc040cd7..52bddb0d 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -13,14 +13,15 @@ HomieClass::HomieClass() Interface::get().led.enabled = true; Interface::get().led.pin = BUILTIN_LED; Interface::get().led.on = LOW; - Interface::get().reset.idle = true; Interface::get().reset.enabled = true; Interface::get().reset.triggerPin = DEFAULT_RESET_PIN; Interface::get().reset.triggerState = DEFAULT_RESET_STATE; Interface::get().reset.triggerTime = DEFAULT_RESET_TIME; - Interface::get().reset.resetFlag = false; - Interface::get().disable = false; - Interface::get().flaggedForSleep = false; + Interface::get().flags.idle = true; + Interface::get().flags.disable = false; + Interface::get().flags.reboot = false; + Interface::get().flags.reset = false; + Interface::get().flags.sleep = false; Interface::get().globalInputHandler = [](const HomieNode& node, const String& property, const HomieRange& range, const String& value) { return false; }; Interface::get().broadcastHandler = [](const String& level, const String& value) { return false; }; Interface::get().setupFunction = []() {}; @@ -105,22 +106,14 @@ void HomieClass::setup() { return; // never reached, here for clarity } + // Set the boot mode + Interface::get().bootMode = _selectedHomieBootMode; + _boot->setup(); } void HomieClass::loop() { _boot->loop(); - - if (_flaggedForReboot && Interface::get().reset.idle) { - Interface::get().getLogger() << F("Device is idle") << endl; - Interface::get().getLogger() << F("Triggering ABOUT_TO_RESET event...") << endl; - Interface::get().event.type = HomieEventType::ABOUT_TO_RESET; - Interface::get().eventHandler(Interface::get().event); - - Interface::get().getLogger() << F("↻ Rebooting device...") << endl; - Serial.flush(); - ESP.restart(); - } } bool HomieClass::loadSettings() { @@ -237,18 +230,18 @@ void HomieClass::__setBrand(const char* brand) const { void HomieClass::reset() { Interface::get().getLogger() << F("Flagged for reset by sketch") << endl; - Interface::get().disable = true; - Interface::get().reset.resetFlag = true; + Interface::get().flags.disable = true; + Interface::get().flags.reset = true; } void HomieClass::reboot() { Interface::get().getLogger() << F("Flagged for reboot by sketch") << endl; - Interface::get().disable = true; - _flaggedForReboot = true; + Interface::get().flags.disable = true; + Interface::get().flags.reboot = true; } void HomieClass::setIdle(bool idle) { - Interface::get().reset.idle = idle; + Interface::get().flags.idle = idle; } HomieClass& HomieClass::setGlobalInputHandler(const GlobalInputHandler& globalInputHandler) { @@ -267,7 +260,7 @@ HomieClass& HomieClass::setBroadcastHandler(const BroadcastHandler& broadcastHan return *this; } -HomieClass& HomieClass::setSetupFunction(const OperationFunction& function) { +HomieClass& HomieClass::setSetupFunction(const CallbackFunction& function) { _checkBeforeSetup(F("setSetupFunction")); Interface::get().setupFunction = function; @@ -275,7 +268,7 @@ HomieClass& HomieClass::setSetupFunction(const OperationFunction& function) { return *this; } -HomieClass& HomieClass::setLoopFunction(const OperationFunction& function) { +HomieClass& HomieClass::setLoopFunction(const CallbackFunction& function) { _checkBeforeSetup(F("setLoopFunction")); Interface::get().loopFunction = function; @@ -349,10 +342,10 @@ String HomieInternals::HomieClass::getDeviceID() { void HomieClass::prepareToSleep() { Interface::get().getLogger() << F("Flagged for sleep by sketch") << endl; if (Interface::get().ready) { - Interface::get().disable = true; - Interface::get().flaggedForSleep = true; + Interface::get().flags.disable = true; + Interface::get().flags.sleep = true; } else { - Interface::get().disable = true; + Interface::get().flags.disable = true; Interface::get().getLogger() << F("Triggering READY_TO_SLEEP event...") << endl; Interface::get().event.type = HomieEventType::READY_TO_SLEEP; Interface::get().eventHandler(Interface::get().event); diff --git a/src/Homie.h b/src/Homie.h index 1f41262a..2018ef16 100644 --- a/src/Homie.h +++ b/src/Homie.h @@ -1,6 +1,7 @@ #ifndef SRC_HOMIE_H_ #define SRC_HOMIE_H_ +#include "HomieFirmwareConfig.h" #include "Homie.hpp" #endif // SRC_HOMIE_H_ diff --git a/src/Homie.hpp b/src/Homie.hpp index f9f9e5a2..cbf12758 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -1,8 +1,8 @@ #pragma once #include "Arduino.h" - #include "AsyncMqttClient.h" + #include "Homie/Datatypes/Interface.hpp" #include "Homie/Constants.hpp" #include "Homie/Limits.hpp" @@ -53,8 +53,8 @@ class HomieClass { HomieClass& onEvent(const EventHandler& handler); HomieClass& setResetTrigger(uint8_t pin, uint8_t state, uint16_t time); HomieClass& disableResetTrigger(); - HomieClass& setSetupFunction(const OperationFunction& function); - HomieClass& setLoopFunction(const OperationFunction& function); + HomieClass& setSetupFunction(const CallbackFunction& function); + HomieClass& setLoopFunction(const CallbackFunction& function); HomieClass& setHomieBootMode(HomieBootMode bootMode); HomieClass& setHomieBootModeOnNextBoot(HomieBootMode bootMode); @@ -78,7 +78,6 @@ class HomieClass { BootStandalone _bootStandalone; BootNormal _bootNormal; BootConfig _bootConfig; - bool _flaggedForReboot; SendingPromise _sendingPromise; Logger _logger; Blinker _blinker; diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 565e1498..19206d12 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -70,6 +70,12 @@ void BootConfig::setup() { _onCaptivePortal(request); } }); + + ResetHandler::attach(); + #if HOMIE_FIRMWARE_HOMIE_BUTTON + HomieButton::attach(); + #endif + _http.begin(); } @@ -80,9 +86,7 @@ void BootConfig::loop() { if (_flaggedForReboot) { if (millis() - _flaggedForRebootAt >= 3000UL) { - Interface::get().getLogger() << F("↻ Rebooting into normal mode...") << endl; - Serial.flush(); - ESP.restart(); + Interface::get().flags.reboot = true; } return; @@ -376,7 +380,7 @@ void BootConfig::_onNetworksRequest(AsyncWebServerRequest *request) { void BootConfig::_onCurrentConfig(AsyncWebServerRequest *request) { StaticJsonBuffer jsonBuffer; - ValidationResultOBJ result = Interface::get().getConfig().getJsonObject(&jsonBuffer); + ValidationResultOBJ result = Interface::get().getConfig().getSafeConfigFile(&jsonBuffer); if (!result.valid) { Interface::get().getLogger() << F("✖ Error: ") << result.reason << endl; @@ -413,14 +417,14 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_JSON_SUCCESS)); - Interface::get().disable = true; + Interface::get().flags.disable = true; _flaggedForReboot = true; // We don't reboot immediately, otherwise the response above is not sent _flaggedForRebootAt = millis(); } void BootConfig::__setCORS() { DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Origin"), F("*")); - DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Methods"), F("GET, PUT")); + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Methods"), F("GET, POST, PUT")); DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Headers"), F("Content-Type, Origin, Referer, User-Agent")); } diff --git a/src/Homie/Boot/BootConfig.hpp b/src/Homie/Boot/BootConfig.hpp index 238f5676..8a4fcfa0 100644 --- a/src/Homie/Boot/BootConfig.hpp +++ b/src/Homie/Boot/BootConfig.hpp @@ -21,6 +21,10 @@ #include "../Strings.hpp" #include "../../HomieSetting.hpp" #include "../../StreamingOperator.hpp" +#include "../Utils/ResetHandler.hpp" +#if HOMIE_FIRMWARE_HOMIE_BUTTON +#include "../Utils/HomieButton.hpp" +#endif namespace HomieInternals { class BootConfig : public Boot { diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index d882054d..ed4d10a3 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -9,7 +9,6 @@ BootNormal::BootNormal() , _mqttConnectNotified(false) , _mqttDisconnectNotified(true) , _otaOngoing(false) - , _flaggedForReboot(false) , _mqttOfflineMessageId(0) , _otaIsBase64(false) , _otaBase64Pads(0) @@ -89,7 +88,10 @@ void BootNormal::setup() { if (Interface::get().getConfig().get().mqtt.auth) Interface::get().getMqttClient().setCredentials(Interface::get().getConfig().get().mqtt.username, Interface::get().getConfig().get().mqtt.password); - ResetHandler::Attach(); + ResetHandler::attach(); +#if HOMIE_FIRMWARE_HOMIE_BUTTON + HomieButton::attach(); +#endif Interface::get().getConfig().log(); @@ -103,14 +105,6 @@ void BootNormal::setup() { void BootNormal::loop() { Boot::loop(); - if (_flaggedForReboot && Interface::get().reset.idle) { - Interface::get().getLogger() << F("Device is idle") << endl; - - Interface::get().getLogger() << F("↻ Rebooting...") << endl; - Serial.flush(); - ESP.restart(); - } - if (_mqttReconnectTimer.check()) { _mqttConnect(); return; @@ -152,7 +146,7 @@ void BootNormal::loop() { // here, we have notified the sketch we are ready - if (_mqttOfflineMessageId == 0 && Interface::get().flaggedForSleep) { + if (_mqttOfflineMessageId == 0 && Interface::get().flags.sleep) { Interface::get().getLogger() << F("Device in preparation to sleep...") << endl; _mqttOfflineMessageId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$online")), 1, true, "false"); } @@ -211,7 +205,7 @@ void BootNormal::_endOtaUpdate(bool success, uint8_t update_error) { Interface::get().eventHandler(Interface::get().event); _publishOtaStatus(200); // 200 OK - _flaggedForReboot = true; + Interface::get().flags.reboot = true; } else { int code; String info; @@ -254,7 +248,7 @@ void BootNormal::_endOtaUpdate(bool success, uint8_t update_error) { } void BootNormal::_wifiConnect() { - if (!Interface::get().disable) { + if (!Interface::get().flags.disable) { if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_WIFI_DELAY); Interface::get().getLogger() << F("↕ Attempting to connect to Wi-Fi...") << endl; @@ -325,7 +319,7 @@ void BootNormal::_onWifiDisconnected(const WiFiEventStationModeDisconnected& eve } void BootNormal::_mqttConnect() { - if (!Interface::get().disable) { + if (!Interface::get().flags.disable) { if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_MQTT_DELAY); Interface::get().getLogger() << F("↕ Attempting to connect to MQTT...") << endl; Interface::get().getMqttClient().connect(); @@ -380,9 +374,9 @@ void BootNormal::_advertise() { break; case AdvertisementProgress::GlobalStep::PUB_IMPLEMENTATION_CONFIG: { - char* safeConfigFile = Interface::get().getConfig().getSafeConfigFile(); - packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/config")), 1, true, safeConfigFile); - free(safeConfigFile); + std::unique_ptr safeConfigFile = Interface::get().getConfig().getSafeConfigFileSTR(); + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/config")), 1, true, safeConfigFile.get()); + safeConfigFile.release(); if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_IMPLEMENTATION_VERSION; break; } @@ -506,7 +500,7 @@ void BootNormal::_onMqttDisconnected(AsyncMqttClientDisconnectReason reason) { _mqttDisconnectNotified = true; - if (Interface::get().flaggedForSleep) { + if (Interface::get().flags.sleep) { _mqttOfflineMessageId = 0; Interface::get().getLogger() << F("Triggering READY_TO_SLEEP event...") << endl; Interface::get().event.type = HomieEventType::READY_TO_SLEEP; @@ -570,7 +564,7 @@ void BootNormal::_onMqttPublish(uint16_t id) { Interface::get().event.packetId = id; Interface::get().eventHandler(Interface::get().event); - if (Interface::get().flaggedForSleep && id == _mqttOfflineMessageId) { + if (Interface::get().flags.sleep && id == _mqttOfflineMessageId) { Interface::get().getLogger() << F("Offline message acknowledged. Disconnecting MQTT...") << endl; Interface::get().getMqttClient().disconnect(); } @@ -819,8 +813,8 @@ bool BootNormal::__handleResets(char * topic, char * payload, const AsyncMqttCli if ( strcmp_P(_mqttPayloadBuffer.get(), PSTR("true") ) == 0 ) { Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/reset")), 1, true, "false"); Interface::get().getLogger() << F("Flagged for reset by network") << endl; - Interface::get().disable = true; - Interface::get().reset.resetFlag = true; + Interface::get().flags.disable = true; + Interface::get().flags.reset = true; } return true; } @@ -836,8 +830,8 @@ bool BootNormal::__handleRestarts(char* topic, char* payload, const AsyncMqttCli if ( strcmp_P(_mqttPayloadBuffer.get(), PSTR("true") ) == 0 ) { Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/restart")), 1, true, "false"); Interface::get().getLogger() << F("Flagged for restart by network") << endl; - Interface::get().disable = true; - _flaggedForReboot = true; + Interface::get().flags.disable = true; + Interface::get().flags.reboot = true; } return true; } @@ -855,7 +849,8 @@ bool BootNormal::__handleConfig(char * topic, char * payload, const AsyncMqttCli ValidationResult configPatchResult = Interface::get().getConfig().patch(_mqttPayloadBuffer.get()); if (configPatchResult.valid) { Interface::get().getLogger() << F("✔ Configuration updated") << endl; - _flaggedForReboot = true; + Interface::get().flags.disable = true; + Interface::get().flags.reboot = true; Interface::get().getLogger() << F("Flagged for reboot") << endl; } else { Interface::get().getLogger() << F("✖ Configuration not updated") << endl; diff --git a/src/Homie/Boot/BootNormal.hpp b/src/Homie/Boot/BootNormal.hpp index 90faddb0..5f728b8f 100644 --- a/src/Homie/Boot/BootNormal.hpp +++ b/src/Homie/Boot/BootNormal.hpp @@ -19,6 +19,9 @@ #include "../ExponentialBackoffTimer.hpp" #include "Boot.hpp" #include "../Utils/ResetHandler.hpp" +#if HOMIE_FIRMWARE_HOMIE_BUTTON +#include "../Utils/HomieButton.hpp" +#endif namespace HomieInternals { class BootNormal : public Boot { @@ -70,7 +73,6 @@ class BootNormal : public Boot { bool _mqttConnectNotified; bool _mqttDisconnectNotified; bool _otaOngoing; - bool _flaggedForReboot; uint16_t _mqttOfflineMessageId; char _fwChecksum[32 + 1]; bool _otaIsBase64; diff --git a/src/Homie/Boot/BootStandalone.cpp b/src/Homie/Boot/BootStandalone.cpp index bd326782..a5a67a2d 100644 --- a/src/Homie/Boot/BootStandalone.cpp +++ b/src/Homie/Boot/BootStandalone.cpp @@ -14,7 +14,10 @@ void BootStandalone::setup() { WiFi.mode(WIFI_OFF); - ResetHandler::Attach(); + ResetHandler::attach(); +#if HOMIE_FIRMWARE_HOMIE_BUTTON + HomieButton::attach(); +#endif } void BootStandalone::loop() { diff --git a/src/Homie/Boot/BootStandalone.hpp b/src/Homie/Boot/BootStandalone.hpp index 2d3f24ea..276e9a63 100644 --- a/src/Homie/Boot/BootStandalone.hpp +++ b/src/Homie/Boot/BootStandalone.hpp @@ -5,6 +5,9 @@ #include "Boot.hpp" #include "../../StreamingOperator.hpp" #include "../Utils/ResetHandler.hpp" +#if HOMIE_FIRMWARE_HOMIE_BUTTON +#include "../Utils/HomieButton.hpp" +#endif namespace HomieInternals { class BootStandalone : public Boot { diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 81e70030..9be9de27 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -12,10 +12,6 @@ const ConfigStruct& Config::get() const { return _configStruct; } -ValidationResultOBJ Config::getJsonObject(StaticJsonBuffer* jsonBuffer) { - return _loadConfigFile(jsonBuffer, true); -} - bool Config::_spiffsBegin() { if (!_spiffsBegan) { _spiffsBegan = SPIFFS.begin(); @@ -181,27 +177,38 @@ bool Config::load() { return true; } -char* Config::getSafeConfigFile() { - StaticJsonBuffer jsonBuffer; - ValidationResultOBJ configLoadResult = _loadConfigFile(&jsonBuffer); +ValidationResultOBJ Config::getConfigFile(StaticJsonBuffer* jsonBuffer) { + return _loadConfigFile(jsonBuffer, true); +} + +ValidationResultOBJ Config::getSafeConfigFile(StaticJsonBuffer* jsonBuffer) { + ValidationResultOBJ configLoadResult = getConfigFile(jsonBuffer); if (!configLoadResult.valid) { - return nullptr; + return configLoadResult; } JsonObject& configObject = *configLoadResult.config; - ValidationResult configValidResult = validateConfig(configObject); - if (!configValidResult.valid) { - return nullptr; - } configObject["wifi"].as().remove("password"); configObject["mqtt"].as().remove("username"); configObject["mqtt"].as().remove("password"); + return configLoadResult; +} + + +std::unique_ptr Config::getSafeConfigFileSTR() { + StaticJsonBuffer jsonBuffer; + ValidationResultOBJ configLoadResult = getSafeConfigFile(&jsonBuffer); + if (!configLoadResult.valid) { + return std::unique_ptr(nullptr); + } + JsonObject& configObject = *configLoadResult.config; + size_t jsonBufferLength = configObject.measureLength() + 1; std::unique_ptr jsonString(new char[jsonBufferLength]); configObject.printTo(jsonString.get(), jsonBufferLength); - return strdup(jsonString.get()); + return jsonString; } void Config::erase() { diff --git a/src/Homie/Config.hpp b/src/Homie/Config.hpp index 5e00fdc7..b5217495 100644 --- a/src/Homie/Config.hpp +++ b/src/Homie/Config.hpp @@ -22,8 +22,9 @@ class Config { Config(); bool load(); const ConfigStruct& get() const; - ValidationResultOBJ getJsonObject(StaticJsonBuffer* jsonBuffer); - char* getSafeConfigFile(); + ValidationResultOBJ getConfigFile(StaticJsonBuffer* jsonBuffer); + ValidationResultOBJ getSafeConfigFile(StaticJsonBuffer* jsonBuffer); + std::unique_ptr getSafeConfigFileSTR(); void erase(); void setHomieBootModeOnNextBoot(HomieBootMode bootMode); HomieBootMode getHomieBootModeOnNextBoot(); diff --git a/src/Homie/Datatypes/Callbacks.hpp b/src/Homie/Datatypes/Callbacks.hpp index 9e519235..a4d6b55a 100644 --- a/src/Homie/Datatypes/Callbacks.hpp +++ b/src/Homie/Datatypes/Callbacks.hpp @@ -7,7 +7,7 @@ class HomieNode; namespace HomieInternals { - typedef std::function OperationFunction; + typedef std::function CallbackFunction; typedef std::function GlobalInputHandler; typedef std::function NodeInputHandler; diff --git a/src/Homie/Datatypes/Interface.cpp b/src/Homie/Datatypes/Interface.cpp index 290ce923..bdee211b 100644 --- a/src/Homie/Datatypes/Interface.cpp +++ b/src/Homie/Datatypes/Interface.cpp @@ -10,9 +10,8 @@ InterfaceData::InterfaceData() , configurationAp{ .secured = false, .password = {'\0'} } , firmware{ .name = {'\0'}, .version = {'\0'} } , led{ .enabled = false, .pin = 0, .on = 0 } - , reset{ .enabled = false, .idle = false, .triggerPin = 0, .triggerState = 0, .triggerTime = 0, .resetFlag = false } - , disable{ false } - , flaggedForSleep{ false } + , reset{ .enabled = false, .triggerPin = 0, .triggerState = 0, .triggerTime = 0 } + , flags{.idle = false, .disable = false, .reboot = false, .reset = false, .sleep = false } , event{} , ready{ false } , _logger{ nullptr } diff --git a/src/Homie/Datatypes/Interface.hpp b/src/Homie/Datatypes/Interface.hpp index 0432d954..97b7dc32 100644 --- a/src/Homie/Datatypes/Interface.hpp +++ b/src/Homie/Datatypes/Interface.hpp @@ -48,20 +48,23 @@ class InterfaceData { struct Reset { bool enabled; - bool idle; uint8_t triggerPin; uint8_t triggerState; uint16_t triggerTime; - bool resetFlag; } reset; - bool disable; - bool flaggedForSleep; + struct Flags { + bool idle; + bool disable; + bool reboot; + bool reset; + bool sleep; + } flags; GlobalInputHandler globalInputHandler; BroadcastHandler broadcastHandler; - OperationFunction setupFunction; - OperationFunction loopFunction; + CallbackFunction setupFunction; + CallbackFunction loopFunction; EventHandler eventHandler; /***** Runtime data *****/ diff --git a/src/Homie/Utils/HomieButton.cpp b/src/Homie/Utils/HomieButton.cpp new file mode 100644 index 00000000..4bed81a7 --- /dev/null +++ b/src/Homie/Utils/HomieButton.cpp @@ -0,0 +1,56 @@ +#include "HomieButton.hpp" + +using namespace HomieInternals; + +Ticker HomieButton::_homieButtonTicker; +OneButton* HomieButton::_homieButton; +CallbackFunction HomieButton::_userClickFunc = [](){}; + +void HomieButton::attach() { + Interface::get().getLogger() << F("Homie button is enabled") << endl; + + _homieButtonTicker.attach_ms(10, _tick); + + _homieButton = new OneButton(Interface::get().reset.triggerPin, !Interface::get().reset.triggerState); + _homieButton->setPressTicks(Interface::get().reset.triggerTime); + + _homieButton->attachClick(_clickFunc); + _homieButton->attachDoubleClick(_doubleClickFunc); + _homieButton->attachLongPressStart(_longPressStartFunc); +} + +void HomieButton::addClickHandler(CallbackFunction func) { + _userClickFunc = func; +} + +void HomieButton::_tick() { + _homieButton->tick(); +} + +void HomieButton::_clickFunc() { + _userClickFunc(); +} + +void HomieButton::_doubleClickFunc() { + // change Modes + if (Interface::get().bootMode == HomieBootMode::NORMAL) { + Interface::get().getConfig().setHomieBootModeOnNextBoot(HomieBootMode::CONFIGURATION); + Interface::get().getLogger() << F("Changed next boot mode to congig") << endl; + Interface::get().flags.disable = true; + Interface::get().flags.reboot = true; + } else if (Interface::get().bootMode == HomieBootMode::CONFIGURATION) { + Interface::get().getConfig().setHomieBootModeOnNextBoot(HomieBootMode::NORMAL); + Interface::get().getLogger() << F("Changed next boot mode to normal") << endl; + Interface::get().flags.disable = true; + Interface::get().flags.reboot = true; + } +} + +void HomieButton::_longPressStartFunc() { + // reset device + if (Interface::get().reset.enabled && !Interface::get().flags.reset) { + Interface::get().getLogger() << F("Flagged for reset by pin") << endl; + Interface::get().flags.disable = true; + Interface::get().flags.reset = true; + } +} diff --git a/src/Homie/Utils/HomieButton.hpp b/src/Homie/Utils/HomieButton.hpp new file mode 100644 index 00000000..0ba8fcf0 --- /dev/null +++ b/src/Homie/Utils/HomieButton.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "Arduino.h" + +#include +#include +#include "../../StreamingOperator.hpp" +#include "../Datatypes/Interface.hpp" +#include "../Datatypes/Callbacks.hpp" + +namespace HomieInternals { +class HomieButton{ + public: + static void attach(); + static void addClickHandler(CallbackFunction func); + + private: + // Disable creating an instance of this object + HomieButton() {} + + static Ticker _homieButtonTicker; + static OneButton* _homieButton; + static CallbackFunction _userClickFunc; + + static void _tick(); + static void _clickFunc(); + static void _doubleClickFunc(); + static void _longPressStartFunc(); +}; +} \ No newline at end of file diff --git a/src/Homie/Utils/ResetHandler.cpp b/src/Homie/Utils/ResetHandler.cpp index f4fd1c11..d30d6507 100644 --- a/src/Homie/Utils/ResetHandler.cpp +++ b/src/Homie/Utils/ResetHandler.cpp @@ -2,35 +2,36 @@ using namespace HomieInternals; -Ticker ResetHandler::_resetBTNTicker; -Bounce ResetHandler::_resetBTNDebouncer; Ticker ResetHandler::_resetTicker; bool ResetHandler::_sentReset = false; +bool ResetHandler::_sentRestart = false; -void ResetHandler::Attach() { - if (Interface::get().reset.enabled) { - pinMode(Interface::get().reset.triggerPin, INPUT_PULLUP); - _resetBTNDebouncer.attach(Interface::get().reset.triggerPin); - _resetBTNDebouncer.interval(Interface::get().reset.triggerTime); - - _resetBTNTicker.attach_ms(10, _tick); - _resetTicker.attach_ms(100, _handleReset); - } +void ResetHandler::attach() { + _resetTicker.attach_ms(100, _tick); } void ResetHandler::_tick() { - if (!Interface::get().reset.resetFlag && Interface::get().reset.enabled) { - _resetBTNDebouncer.update(); - if (_resetBTNDebouncer.read() == Interface::get().reset.triggerState) { - Interface::get().getLogger() << F("Flagged for reset by pin") << endl; - Interface::get().disable = true; - Interface::get().reset.resetFlag = true; - } + _handleReboot(); + _handleReset(); +} + +void ResetHandler::_handleReboot() { + if (Interface::get().flags.reboot && !_sentRestart && Interface::get().flags.idle) { + Interface::get().getLogger() << F("Device is idle") << endl; + + Interface::get().getLogger() << F("Triggering ABOUT_TO_RESTART event...") << endl; + Interface::get().event.type = HomieEventType::ABOUT_TO_RESTART; + Interface::get().eventHandler(Interface::get().event); + + Interface::get().getLogger() << F("↻ Rebooting...") << endl; + Serial.flush(); + ESP.restart(); + _sentRestart = true; } } void ResetHandler::_handleReset() { - if (Interface::get().reset.resetFlag && !_sentReset && Interface::get().reset.idle) { + if (Interface::get().flags.reset && !_sentReset && Interface::get().flags.idle) { Interface::get().getLogger() << F("Device is idle") << endl; Interface::get().getConfig().erase(); diff --git a/src/Homie/Utils/ResetHandler.hpp b/src/Homie/Utils/ResetHandler.hpp index 8ece92b5..7e764de2 100644 --- a/src/Homie/Utils/ResetHandler.hpp +++ b/src/Homie/Utils/ResetHandler.hpp @@ -10,16 +10,18 @@ namespace HomieInternals { class ResetHandler { public: - static void Attach(); + static void attach(); private: - // Disallow creating an instance of this object + // Disable creating an instance of this object ResetHandler() {} - static Ticker _resetBTNTicker; - static Bounce _resetBTNDebouncer; - static void _tick(); + static Ticker _resetTicker; static bool _sentReset; + static bool _sentRestart; + + static void _tick(); + static void _handleReboot(); static void _handleReset(); }; } // namespace HomieInternals diff --git a/src/HomieEvent.hpp b/src/HomieEvent.hpp index c514db74..bc3ed972 100644 --- a/src/HomieEvent.hpp +++ b/src/HomieEvent.hpp @@ -12,6 +12,7 @@ enum class HomieEventType : uint8_t { OTA_SUCCESSFUL, OTA_FAILED, ABOUT_TO_RESET, + ABOUT_TO_RESTART, WIFI_CONNECTED, WIFI_DISCONNECTED, MQTT_READY, diff --git a/src/HomieFirmwareConfig.h b/src/HomieFirmwareConfig.h new file mode 100644 index 00000000..6577eb64 --- /dev/null +++ b/src/HomieFirmwareConfig.h @@ -0,0 +1,9 @@ + +#ifndef LIBRARIES_HOMIE_CONFIG_H +#define LIBRARIES_HOMIE_CONFIG_H + +#ifndef HOMIE_FIRMWARE_HOMIE_BUTTON +#define HOMIE_FIRMWARE_HOMIE_BUTTON 1 +#endif + +#endif // \ No newline at end of file From 02a054a140392f4f9540a81f5191fa3553e07087 Mon Sep 17 00:00:00 2001 From: timpur Date: Wed, 21 Feb 2018 21:59:52 +1100 Subject: [PATCH 25/51] Inital Patch Via Config File #433 --- docs/configuration/http-json-api.md | 93 ++++++++++++++++++- docs/configuration/json-configuration-file.md | 5 +- src/Homie/Boot/BootConfig.cpp | 34 ++++++- src/Homie/Boot/BootConfig.hpp | 4 +- src/Homie/Config.cpp | 21 +++-- src/Homie/Config.hpp | 3 + src/Homie/Constants.hpp | 2 +- src/Homie/Utils/HomieButton.cpp | 6 +- src/Homie/Utils/Validation.cpp | 48 +++++++--- src/Homie/Utils/Validation.hpp | 1 + 10 files changed, 181 insertions(+), 36 deletions(-) diff --git a/docs/configuration/http-json-api.md b/docs/configuration/http-json-api.md index 15065b2d..fd3f7af9 100644 --- a/docs/configuration/http-json-api.md +++ b/docs/configuration/http-json-api.md @@ -48,8 +48,11 @@ If anything goes wrong, a return code != 2xx will be returned, with a JSON `erro ```json { - "hardware_device_id": "52a8fa5d", - "homie_esp8266_version": "2.0.0", + "device_hardware_id": "52a8fa5d", + "homie_version": "2.0.0", + "homie_esp8266_version": "2.1.0", + "device_config_state": false, + "device_config_state_error": "ERROR MESSAGE", "firmware": { "name": "awesome-device", "version": "1.0.0" @@ -116,6 +119,60 @@ If anything goes wrong, a return code != 2xx will be returned, with a JSON `erro -------------- +??? summary "GET `/config`" + Retrieve the Config file of the device. + + ## Response + + !!! success "In case of success" + `200 OK (application/json)` + + ```json + { + "name": "The kitchen light", + "device_id": "kitchen-light", + "device_stats_interval": 60, + "wifi": { + "ssid": "Network_1", + "password": "I'm a Wi-Fi password!", + "bssid": "DE:AD:BE:EF:BA:BE", + "channel": 1, + "ip": "192.168.1.5", + "mask": "255.255.255.0", + "gw": "192.168.1.1", + "dns1": "8.8.8.8", + "dns2": "8.8.4.4" + }, + "mqtt": { + "host": "192.168.1.10", + "port": 1883, + "base_topic": "devices/", + "auth": true, + "username": "user", + "password": "pass", + "ssl": true, + "ssl_fingerprint": "a27992d3420c89f293d351378ba5f5675f74fe3c" + }, + "ota": { + "enabled": true + }, + "settings": { + "percentage": 55 + } + } + ``` + + !!! failure "In case there was something wrong loading the current config." + `500 Service Unavailable (application/json)` + + ```json + { + "error": "ERROR MESSAGE" + } + ``` + +-------------- + ??? summary "PUT `/config`" Save the config to the device. @@ -158,6 +215,38 @@ If anything goes wrong, a return code != 2xx will be returned, with a JSON `erro -------------- +??? summary "POST `/config/patch`" + Save the config to the device via incremental patches. + + ## Request body + + `(application/json)` + + See [JSON configuration file](json-configuration-file.md). + + ## Response + + !!! success "In case of success" + `200 OK (application/json)` + + ```json + { + "success": true + } + ``` + + !!! failure "In case of error in the payload" + `400 Bad Request (application/json)` + + ```json + { + "success": false, + "error": "Reason why the payload is invalid" + } + ``` + +-------------- + ??? summary "POST `/wifi/connect`" Initiates the connection of the device to the Wi-Fi network while in configuation mode. This request is not synchronous and the result (Wi-Fi connected or not) must be obtained by with `GET /wifi/status`. diff --git a/docs/configuration/json-configuration-file.md b/docs/configuration/json-configuration-file.md index 6cbd6d24..d8a667df 100644 --- a/docs/configuration/json-configuration-file.md +++ b/docs/configuration/json-configuration-file.md @@ -47,8 +47,8 @@ Here are the rules: * `bssid`, `channel`, `ip`, `mask`, `gw`, `dns1`, `dns2` are not mandatory and are only needed to if there is a requirement to specify particular AP or set Static IP address. There are some rules which needs to be satisfied: - `bssid` and `channel` have to be defined together and these settings are independand of settings related to static IP - to define static IP, `ip` (IP address), `mask` (netmask) and `gw` (gateway) settings have to be defined at the same time - - to define second DNS `dns2` the first one `dns1` has to be defined. Set DNS without `ip`, `mask` and `gw` does not affect the configuration (dns server will be provided by DHCP). It is not required to set DNS servers. -* `ssl_fingerprint` if not required if `ssl` is enabled. + - to define second DNS `dns2` the first one `dns1` has to be defined. Set `ip`, `mask` and `gw` without DNS, does not affect the configuration (dns server will be provided by DHCP). It is not required to set DNS servers. +* `ssl_fingerprint` is not required if `ssl` is enabled. Default values if not provided: @@ -58,5 +58,6 @@ Default values if not provided: * `mqtt.port`: `1883` * `mqtt.base_topic`: `homie/` * `mqtt.auth`: `false` +* `mqtt.ssl`: `false` The `mqtt.host` field can be either an IP or an hostname. diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 19206d12..592dcb75 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -59,6 +59,7 @@ void BootConfig::setup() { _http.on("/networks", HTTP_GET, [this](AsyncWebServerRequest *request) { _onNetworksRequest(request); }); _http.on("/config", HTTP_GET, [this](AsyncWebServerRequest *request) { _onCurrentConfig(request); }); _http.on("/config", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onConfigRequest(request); }).onBody(BootConfig::__parsePost); + _http.on("/config/patch", HTTP_POST, [this](AsyncWebServerRequest *request) { _onPatchConfigRequest(request); }).onBody(BootConfig::__parsePost); _http.on("/wifi/connect", HTTP_POST, [this](AsyncWebServerRequest *request) { _onWifiConnectRequest(request); }).onBody(BootConfig::__parsePost); _http.on("/wifi/status", HTTP_GET, [this](AsyncWebServerRequest *request) { _onWifiStatusRequest(request); }); _http.on("/proxy/control", HTTP_POST, [this](AsyncWebServerRequest *request) { _onProxyControlRequest(request); }).onBody(BootConfig::__parsePost); @@ -208,7 +209,9 @@ void BootConfig::_generateNetworksJson() { for (int network = 0; network < _ssidCount; network++) { JsonObject& jsonNetwork = generatedJsonBuffer.createObject(); jsonNetwork["ssid"] = WiFi.SSID(network); + jsonNetwork["bssid"] = WiFi.BSSIDstr(network); jsonNetwork["rssi"] = WiFi.RSSI(network); + jsonNetwork["signal"] = Helpers::rssiToPercentage(WiFi.RSSI(network)); switch (WiFi.encryptionType(network)) { case ENC_TYPE_WEP: jsonNetwork["encryption"] = "wep"; @@ -315,12 +318,17 @@ void BootConfig::_proxyHttpRequest(AsyncWebServerRequest *request) { void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { Interface::get().getLogger() << F("Received device information request") << endl; + auto numSettings = IHomieSetting::settings.size(); auto numNodes = HomieNode::nodes.size(); DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(2) + JSON_ARRAY_SIZE(numNodes) + (numNodes * JSON_OBJECT_SIZE(2)) + JSON_ARRAY_SIZE(numSettings) + (numSettings * JSON_OBJECT_SIZE(5))); JsonObject& json = jsonBuffer.createObject(); - json["hardware_device_id"] = DeviceId::get(); + json["homie_version"] = HOMIE_VERSION; json["homie_esp8266_version"] = HOMIE_ESP8266_VERSION; + json["device_hardware_id"] = DeviceId::get(); + auto configValidationResult = Interface::get().getConfig().isConfigFileValid(); + json["device_config_state"] = configValidationResult.valid; + if (!configValidationResult.valid) json["device_config_state_error"] = configValidationResult.reason; JsonObject& firmware = json.createNestedObject("firmware"); firmware["name"] = Interface::get().firmware.name; firmware["version"] = Interface::get().firmware.version; @@ -397,14 +405,14 @@ void BootConfig::_onCurrentConfig(AsyncWebServerRequest *request) { void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { Interface::get().getLogger() << F("Received config request") << endl; - if (_flaggedForReboot) { + if (Interface::get().getConfig().isConfigFileValid().valid) { __SendJSONError(request, 403, F("✖ Device already configured")); return; } - DynamicJsonBuffer parseJsonBuffer(MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE); + StaticJsonBuffer jsonBuffer; const char* body = (const char*)(request->_tempObject); - JsonObject& parsedJson = parseJsonBuffer.parseObject(body); + JsonObject& parsedJson = jsonBuffer.parseObject(body); ValidationResult configWriteResult = Interface::get().getConfig().write(parsedJson); if (!configWriteResult.valid) { @@ -422,6 +430,24 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { _flaggedForRebootAt = millis(); } +void BootConfig::_onPatchConfigRequest(AsyncWebServerRequest *request) { + const char* body = (const char*)(request->_tempObject); + ValidationResult configPatchResult = Interface::get().getConfig().patch(body); + if (!configPatchResult.valid) { + Interface::get().getLogger() << F("✖ Error: ") << configPatchResult.reason << endl; + __SendJSONError(request, 500, configPatchResult.reason); + return; + } + + Interface::get().getLogger() << F("✔ Configured") << endl; + + request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_JSON_SUCCESS)); + + Interface::get().flags.disable = true; + _flaggedForReboot = true; // We don't reboot immediately, otherwise the response above is not sent + _flaggedForRebootAt = millis(); +} + void BootConfig::__setCORS() { DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Origin"), F("*")); DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Methods"), F("GET, POST, PUT")); diff --git a/src/Homie/Boot/BootConfig.hpp b/src/Homie/Boot/BootConfig.hpp index 8a4fcfa0..1ba9d83e 100644 --- a/src/Homie/Boot/BootConfig.hpp +++ b/src/Homie/Boot/BootConfig.hpp @@ -51,13 +51,15 @@ class BootConfig : public Boot { void _onCaptivePortal(AsyncWebServerRequest *request); void _onDeviceInfoRequest(AsyncWebServerRequest *request); void _onNetworksRequest(AsyncWebServerRequest *request); + void _onCurrentConfig(AsyncWebServerRequest *request); void _onConfigRequest(AsyncWebServerRequest *request); + void _onPatchConfigRequest(AsyncWebServerRequest *request); void _generateNetworksJson(); void _onWifiConnectRequest(AsyncWebServerRequest *request); void _onProxyControlRequest(AsyncWebServerRequest *request); void _proxyHttpRequest(AsyncWebServerRequest *request); void _onWifiStatusRequest(AsyncWebServerRequest *request); - void _onCurrentConfig(AsyncWebServerRequest *request); + // Helpers static void __setCORS(); diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 9be9de27..0fc67650 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -84,14 +84,6 @@ bool Config::load() { if (parsedJson["mqtt"].as().containsKey("port")) { reqMqttPort = parsedJson["mqtt"]["port"]; } - bool reqMqttSsl = false; - if (parsedJson["mqtt"].as().containsKey("ssl")) { - reqMqttSsl = parsedJson["mqtt"]["ssl"]; - } - const char* reqMqttFingerprint = ""; - if (parsedJson["mqtt"].as().containsKey("ssl_fingerprint")) { - reqMqttFingerprint = parsedJson["mqtt"]["ssl_fingerprint"]; - } const char* reqMqttBaseTopic = DEFAULT_MQTT_BASE_TOPIC; if (parsedJson["mqtt"].as().containsKey("base_topic")) { reqMqttBaseTopic = parsedJson["mqtt"]["base_topic"]; @@ -108,6 +100,14 @@ bool Config::load() { if (parsedJson["mqtt"].as().containsKey("password")) { reqMqttPassword = parsedJson["mqtt"]["password"]; } + bool reqMqttSsl = false; + if (parsedJson["mqtt"].as().containsKey("ssl")) { + reqMqttSsl = parsedJson["mqtt"]["ssl"]; + } + const char* reqMqttFingerprint = ""; + if (parsedJson["mqtt"].as().containsKey("ssl_fingerprint")) { + reqMqttFingerprint = parsedJson["mqtt"]["ssl_fingerprint"]; + } bool reqOtaEnabled = false; if (parsedJson["ota"].as().containsKey("enabled")) { @@ -419,6 +419,11 @@ bool Config::isValid() const { return this->_valid; } +ValidationResult Config::isConfigFileValid() { + StaticJsonBuffer jsonBuffer; + return (ValidationResult)_loadConfigFile(&jsonBuffer); +} + void Config::log() const { Interface::get().getLogger() << F("{} Stored configuration") << endl; Interface::get().getLogger() << F(" • Hardware Device ID: ") << DeviceId::get() << endl; diff --git a/src/Homie/Config.hpp b/src/Homie/Config.hpp index b5217495..eabec685 100644 --- a/src/Homie/Config.hpp +++ b/src/Homie/Config.hpp @@ -16,6 +16,8 @@ #include "./Strings.hpp" #include "./Datatypes/Result.hpp" +// TODO: Config Remove of sensitive data Should have a default value so we know it actually exists. + namespace HomieInternals { class Config { public: @@ -34,6 +36,7 @@ class Config { ValidationResult saveSetting(const char* name, T value); void log() const; // print the current config to log output bool isValid() const; + ValidationResult isConfigFileValid(); static ValidationResult validateConfig(const JsonObject& parsedJson, bool skipValidation = false); diff --git a/src/Homie/Constants.hpp b/src/Homie/Constants.hpp index 20f4ffc3..55dac04d 100644 --- a/src/Homie/Constants.hpp +++ b/src/Homie/Constants.hpp @@ -4,7 +4,7 @@ namespace HomieInternals { const char HOMIE_VERSION[] = "2.0.0"; - const char HOMIE_ESP8266_VERSION[] = "2.0.0"; + const char HOMIE_ESP8266_VERSION[] = "2.1.0"; const IPAddress ACCESS_POINT_IP(192, 168, 123, 1); diff --git a/src/Homie/Utils/HomieButton.cpp b/src/Homie/Utils/HomieButton.cpp index 4bed81a7..e3571a38 100644 --- a/src/Homie/Utils/HomieButton.cpp +++ b/src/Homie/Utils/HomieButton.cpp @@ -35,12 +35,12 @@ void HomieButton::_doubleClickFunc() { // change Modes if (Interface::get().bootMode == HomieBootMode::NORMAL) { Interface::get().getConfig().setHomieBootModeOnNextBoot(HomieBootMode::CONFIGURATION); - Interface::get().getLogger() << F("Changed next boot mode to congig") << endl; + Interface::get().getLogger() << F("Changed next boot mode to congig, by homie button") << endl; Interface::get().flags.disable = true; Interface::get().flags.reboot = true; } else if (Interface::get().bootMode == HomieBootMode::CONFIGURATION) { Interface::get().getConfig().setHomieBootModeOnNextBoot(HomieBootMode::NORMAL); - Interface::get().getLogger() << F("Changed next boot mode to normal") << endl; + Interface::get().getLogger() << F("Changed next boot mode to normal, by homie button") << endl; Interface::get().flags.disable = true; Interface::get().flags.reboot = true; } @@ -49,7 +49,7 @@ void HomieButton::_doubleClickFunc() { void HomieButton::_longPressStartFunc() { // reset device if (Interface::get().reset.enabled && !Interface::get().flags.reset) { - Interface::get().getLogger() << F("Flagged for reset by pin") << endl; + Interface::get().getLogger() << F("Flagged for reset by homie button") << endl; Interface::get().flags.disable = true; Interface::get().flags.reset = true; } diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index 490bffea..c7e172c4 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -3,6 +3,8 @@ using namespace HomieInternals; ValidationResult Validation::validateConfig(const JsonObject& object) { + _removeNullConfigItems("Config", (JsonObject&)object); + ValidationResult result; result = _validateConfigRoot(object); if (!result.valid) return result; @@ -19,6 +21,22 @@ ValidationResult Validation::validateConfig(const JsonObject& object) { return result; } +void Validation::_removeNullConfigItems(const char* name, JsonObject& object) { + auto hasValue = [](JsonVariant& val) { + return val.is() || val.is() || val.is() || val.as() != nullptr; + }; + + for (JsonPair kv : object) { + if (kv.value.is()) { + _removeNullConfigItems(kv.key, kv.value.as()); + } else if (!hasValue(kv.value)) { + Interface::get().getLogger() << F("Removed ") << kv.key << F(" from ") << name << endl; + object.remove(kv.key); + } + } +} + + ValidationResult Validation::_validateConfigRoot(const JsonObject& object) { ValidationResult result; result.valid = false; @@ -200,21 +218,6 @@ ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { result.reason = F("mqtt.port is not an integer"); return result; } - if (object["mqtt"].as().containsKey("ssl") && !object["mqtt"]["ssl"].is()) { - result.reason = F("mqtt.ssl is not a bool"); - return result; - } - if (object["mqtt"].as().containsKey("ssl_fingerprint")) { - if (!object["mqtt"]["ssl_fingerprint"].is()) { - result.reason = F("mqtt.ssl_fingerprint is not a string"); - return result; - } - - if (strlen(object["mqtt"]["ssl_fingerprint"]) + 1 != MAX_FINGERPRINT_STRING_LENGTH) { - result.reason = F("mqtt.ssl_fingerprint is not the right length"); - return result; - } - } if (object["mqtt"].as().containsKey("base_topic")) { if (!object["mqtt"]["base_topic"].is()) { result.reason = F("mqtt.base_topic is not a string"); @@ -251,6 +254,21 @@ ValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { } } } + if (object["mqtt"].as().containsKey("ssl") && !object["mqtt"]["ssl"].is()) { + result.reason = F("mqtt.ssl is not a bool"); + return result; + } + if (object["mqtt"].as().containsKey("ssl_fingerprint")) { + if (!object["mqtt"]["ssl_fingerprint"].is()) { + result.reason = F("mqtt.ssl_fingerprint is not a string"); + return result; + } + + if (strlen(object["mqtt"]["ssl_fingerprint"]) + 1 != MAX_FINGERPRINT_STRING_LENGTH) { + result.reason = F("mqtt.ssl_fingerprint is not the right length"); + return result; + } + } const char* host = object["mqtt"]["host"]; if (strcmp_P(host, PSTR("")) == 0) { diff --git a/src/Homie/Utils/Validation.hpp b/src/Homie/Utils/Validation.hpp index ee29190d..9197dc05 100644 --- a/src/Homie/Utils/Validation.hpp +++ b/src/Homie/Utils/Validation.hpp @@ -14,6 +14,7 @@ class Validation { static ValidationResult validateConfig(const JsonObject& object); private: + static void _removeNullConfigItems(const char* name, JsonObject& object); static ValidationResult _validateConfigRoot(const JsonObject& object); static ValidationResult _validateConfigWifi(const JsonObject& object); static ValidationResult _validateConfigMqtt(const JsonObject& object); From 57ecb0d0a90ca5fb1a59e0878a58ebe93743d43a Mon Sep 17 00:00:00 2001 From: timpur Date: Thu, 22 Feb 2018 23:04:29 +1100 Subject: [PATCH 26/51] fix for #477 and Homie Button Config in arduino SDK --- src/Homie.h | 1 - src/Homie/Boot/BootConfig.cpp | 24 +++++++++++-------- src/Homie/Boot/BootConfig.hpp | 2 ++ src/Homie/Boot/BootNormal.hpp | 2 ++ src/Homie/Boot/BootStandalone.hpp | 1 + ...rmwareConfig.h => HomieFirmwareConfig.hpp} | 6 +---- 6 files changed, 20 insertions(+), 16 deletions(-) rename src/{HomieFirmwareConfig.h => HomieFirmwareConfig.hpp} (51%) diff --git a/src/Homie.h b/src/Homie.h index 2018ef16..1f41262a 100644 --- a/src/Homie.h +++ b/src/Homie.h @@ -1,7 +1,6 @@ #ifndef SRC_HOMIE_H_ #define SRC_HOMIE_H_ -#include "HomieFirmwareConfig.h" #include "Homie.hpp" #endif // SRC_HOMIE_H_ diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 592dcb75..cce48700 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -410,8 +410,9 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { return; } - StaticJsonBuffer jsonBuffer; const char* body = (const char*)(request->_tempObject); + + StaticJsonBuffer jsonBuffer; JsonObject& parsedJson = jsonBuffer.parseObject(body); ValidationResult configWriteResult = Interface::get().getConfig().write(parsedJson); @@ -455,15 +456,18 @@ void BootConfig::__setCORS() { } void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { - if (!index && total > MAX_POST_SIZE) { - Interface::get().getLogger() << "Request is to large to be processed." << endl; - } else if (!index && total <= MAX_POST_SIZE) { - char* buff = new char[total + 1]; - strcpy(buff, (const char*)data); - request->_tempObject = buff; - } else if (total <= MAX_POST_SIZE) { - char* buff = reinterpret_cast(request->_tempObject); - strcat(buff, (const char*)data); + if (total > MAX_POST_SIZE) { + Interface::get().getLogger() << F("Request is to large to be processed.") << endl; + } else { + if (index == 0) { + request->_tempObject = new char[total + 1]; + } + void* buff = request->_tempObject + index; + memcpy(buff, data, len); + if (index + len == total) { + void* buff = request->_tempObject + total; + *(char*)buff = 0; + } } } diff --git a/src/Homie/Boot/BootConfig.hpp b/src/Homie/Boot/BootConfig.hpp index 1ba9d83e..800bde7a 100644 --- a/src/Homie/Boot/BootConfig.hpp +++ b/src/Homie/Boot/BootConfig.hpp @@ -9,6 +9,8 @@ #include #include #include + +#include "../../HomieFirmwareConfig.hpp" #include "Boot.hpp" #include "../Constants.hpp" #include "../Limits.hpp" diff --git a/src/Homie/Boot/BootNormal.hpp b/src/Homie/Boot/BootNormal.hpp index 5f728b8f..96e393aa 100644 --- a/src/Homie/Boot/BootNormal.hpp +++ b/src/Homie/Boot/BootNormal.hpp @@ -7,6 +7,8 @@ #include #include #include + +#include "../../HomieFirmwareConfig.hpp" #include "../../HomieNode.hpp" #include "../../HomieRange.hpp" #include "../../StreamingOperator.hpp" diff --git a/src/Homie/Boot/BootStandalone.hpp b/src/Homie/Boot/BootStandalone.hpp index 276e9a63..9fdfac27 100644 --- a/src/Homie/Boot/BootStandalone.hpp +++ b/src/Homie/Boot/BootStandalone.hpp @@ -2,6 +2,7 @@ #include "Arduino.h" +#include "../../HomieFirmwareConfig.hpp" #include "Boot.hpp" #include "../../StreamingOperator.hpp" #include "../Utils/ResetHandler.hpp" diff --git a/src/HomieFirmwareConfig.h b/src/HomieFirmwareConfig.hpp similarity index 51% rename from src/HomieFirmwareConfig.h rename to src/HomieFirmwareConfig.hpp index 6577eb64..7b7c8347 100644 --- a/src/HomieFirmwareConfig.h +++ b/src/HomieFirmwareConfig.hpp @@ -1,9 +1,5 @@ - -#ifndef LIBRARIES_HOMIE_CONFIG_H -#define LIBRARIES_HOMIE_CONFIG_H +#pragma once #ifndef HOMIE_FIRMWARE_HOMIE_BUTTON #define HOMIE_FIRMWARE_HOMIE_BUTTON 1 #endif - -#endif // \ No newline at end of file From de3c3b63804091081a933becd25993caa53a4267 Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 26 Feb 2018 08:44:38 +1100 Subject: [PATCH 27/51] Small Fix ups + fix #462 --- docs/others/cpp-api-reference.md | 8 +++++++- src/Homie.hpp | 2 +- src/Homie/Boot/BootConfig.cpp | 6 +++--- src/Homie/Config.cpp | 8 ++++---- src/Homie/Config.hpp | 3 ++- src/Homie/Utils/Validation.cpp | 11 +---------- 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/others/cpp-api-reference.md b/docs/others/cpp-api-reference.md index 0c7eb54c..86ec8b10 100644 --- a/docs/others/cpp-api-reference.md +++ b/docs/others/cpp-api-reference.md @@ -166,11 +166,17 @@ This will mark the Homie firmware as standalone, meaning it will first boot in ` ### Functions to call *after* `Homie.setup()` +```c++ +void reboot(); +``` + +Flag the device for reboot (restart). + ```c++ void reset(); ``` -Flag the device for reset. +Flag the device for reset (Wipes config). ```c++ void setIdle(bool idle); diff --git a/src/Homie.hpp b/src/Homie.hpp index cbf12758..58ef3d7e 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -59,7 +59,7 @@ class HomieClass { HomieClass& setHomieBootModeOnNextBoot(HomieBootMode bootMode); static void reset(); - void reboot(); + static void reboot(); static void setIdle(bool idle); static bool isConfigured(); static bool isConnected(); diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index cce48700..6d9366b8 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -462,11 +462,11 @@ void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size if (index == 0) { request->_tempObject = new char[total + 1]; } - void* buff = request->_tempObject + index; + char* buff = reinterpret_cast(request->_tempObject) + index; memcpy(buff, data, len); if (index + len == total) { - void* buff = request->_tempObject + total; - *(char*)buff = 0; + char* buff = reinterpret_cast(request->_tempObject) + total; + *buff = '\0'; } } } diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 0fc67650..a787e282 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -224,7 +224,7 @@ void Config::setHomieBootModeOnNextBoot(HomieBootMode bootMode) { if (bootMode == HomieBootMode::UNDEFINED) { SPIFFS.remove(CONFIG_NEXT_BOOT_MODE_FILE_PATH); } else { - File bootModeFile = SPIFFS.open(CONFIG_NEXT_BOOT_MODE_FILE_PATH, "w"); + fs::File bootModeFile = SPIFFS.open(CONFIG_NEXT_BOOT_MODE_FILE_PATH, "w"); if (!bootModeFile) { Interface::get().getLogger() << F("✖ Cannot open NEXTMODE file") << endl; return; @@ -239,7 +239,7 @@ void Config::setHomieBootModeOnNextBoot(HomieBootMode bootMode) { HomieBootMode Config::getHomieBootModeOnNextBoot() { if (!_spiffsBegin()) { return HomieBootMode::UNDEFINED; } - File bootModeFile = SPIFFS.open(CONFIG_NEXT_BOOT_MODE_FILE_PATH, "r"); + fs::File bootModeFile = SPIFFS.open(CONFIG_NEXT_BOOT_MODE_FILE_PATH, "r"); if (bootModeFile) { int v = bootModeFile.parseInt(); bootModeFile.close(); @@ -265,7 +265,7 @@ ValidationResultOBJ Config::_loadConfigFile(StaticJsonBuffer +#define FS_NO_GLOBALS #include "FS.h" + #include "./Datatypes/Interface.hpp" #include "./Datatypes/ConfigStruct.hpp" #include "./Utils/DeviceId.hpp" diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index c7e172c4..77a856cc 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -311,22 +311,13 @@ ValidationResult Validation::_validateConfigSettings(const JsonObject& object) { return result; } - JsonObject& settingsObject = object["settings"].as(); + const JsonObject& settingsObject = object["settings"].as(); if (settingsObject.size() > MAX_CONFIG_SETTING_SIZE) { result.reason = F("settings contains more elements than the set limit"); return result; } - // Remove nulls in settings - for (JsonPair kv : settingsObject) { - bool hasValue = kv.value.is() || kv.value.is() || kv.value.is() || kv.value.as() != nullptr; - if (!hasValue) { - Interface::get().getLogger() << F("Removed ") << kv.key << F(" from settings") << endl; - settingsObject.remove(kv.key); - } - } - enum class Issue { None, Type, From b5e8926cf9a5a234e755abd2b67b3ff4e7be02de Mon Sep 17 00:00:00 2001 From: Tim P Date: Mon, 12 Mar 2018 12:11:28 +1100 Subject: [PATCH 28/51] Show Homie version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7699b1d1..172c82ec 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Homie for ESP8266 [![Build Status](https://img.shields.io/circleci/project/github/marvinroger/homie-esp8266/develop.svg?style=flat-square)](https://circleci.com/gh/marvinroger/homie-esp8266) [![Latest Release](https://img.shields.io/badge/release-v2.0.0-yellow.svg?style=flat-square)](https://github.com/marvinroger/homie-esp8266/releases) [![Gitter](https://img.shields.io/gitter/room/Homie/ESP8266.svg?style=flat-square)](https://gitter.im/homie-iot/ESP8266) -An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. +An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. Currently Homie for ESP8266 implements [Homie 2.0](https://github.com/marvinroger/homie/releases/tag/v2.0) ## Note for v1.x users From ffebed077af03f843ad3e4e3e918759a78290c5a Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 12 Mar 2018 21:14:32 +1100 Subject: [PATCH 29/51] Lint + Small Fixed --- docs/others/cpp-api-reference.md | 10 ++- .../IteadSonoffButton/IteadSonoffButton.ino | 72 +++++++------------ src/Homie.cpp | 6 +- src/Homie.hpp | 2 + src/Homie/Config.hpp | 2 +- src/Homie/Constants.hpp | 2 +- src/Homie/Datatypes/ConfigStruct.hpp | 2 +- src/Homie/Datatypes/Interface.hpp | 2 +- src/Homie/Utils/HomieButton.cpp | 6 +- src/Homie/Utils/HomieButton.hpp | 6 +- src/Homie/Utils/Validation.cpp | 11 +-- src/Homie/Utils/Validation.hpp | 2 +- src/HomieEvent.hpp | 2 +- src/SendingPromise.cpp | 8 ++- src/SendingPromise.hpp | 1 + 15 files changed, 65 insertions(+), 69 deletions(-) diff --git a/docs/others/cpp-api-reference.md b/docs/others/cpp-api-reference.md index 86ec8b10..57f605d1 100644 --- a/docs/others/cpp-api-reference.md +++ b/docs/others/cpp-api-reference.md @@ -130,7 +130,7 @@ Set the event handler. Useful if you want to hook to Homie events. Homie& setResetTrigger(uint8_t pin, uint8_t state, uint16_t time); ``` -Set the reset trigger. By default, the device will reset when pin `0` is `LOW` for `5000`ms. +Set the reset trigger and the Homie Button. By default, the device will reset when pin `0` is `LOW` for `5000`ms. This Homie button can also be used by the user with `setHomieButtonClick(fnc)` and also used to swap between boot modes. This is usefull to update the config. * **`pin`**: Pin of the reset trigger * **`state`**: Reset when the pin reaches this state for the given time @@ -164,6 +164,14 @@ Homie& setStandalone(); This will mark the Homie firmware as standalone, meaning it will first boot in `standalone` mode. To configure it and boot to `configuration` mode, the device has to be resetted. +```c++ + HomieClass& setHomieButtonClick(std::function function); +``` + +You can provide the function that will be called when the homie button is clicked. + +* **`callback`**: Loop function + ### Functions to call *after* `Homie.setup()` ```c++ diff --git a/examples/IteadSonoffButton/IteadSonoffButton.ino b/examples/IteadSonoffButton/IteadSonoffButton.ino index 3279a2cb..1f45b486 100644 --- a/examples/IteadSonoffButton/IteadSonoffButton.ino +++ b/examples/IteadSonoffButton/IteadSonoffButton.ino @@ -6,70 +6,46 @@ */ #include +#include -const int PIN_RELAY = 12; -const int PIN_LED = 13; -const int PIN_BUTTON = 0; +#define PIN_RELAY 12 +#define PIN_LED 13 +#define EXT_BUTTON_PIN 0 +#define EXT_BUTTON_ACTIVE_STATE 1 -unsigned long buttonDownTime = 0; -byte lastButtonState = 1; -byte buttonPressHandled = 0; +bool relayState; -HomieNode switchNode("switch", "switch"); - -bool switchOnHandler(HomieRange range, String value) { - if (value != "true" && value != "false") return false; - - bool on = (value == "true"); - digitalWrite(PIN_RELAY, on ? HIGH : LOW); - switchNode.setProperty("on").send(value); - Homie.getLogger() << "Switch is " << (on ? "on" : "off") << endl; - - return true; -} - -void toggleRelay() { - bool on = digitalRead(PIN_RELAY) == HIGH; - digitalWrite(PIN_RELAY, on ? LOW : HIGH); - switchNode.setProperty("on").send(on ? "false" : "true"); - Homie.getLogger() << "Switch is " << (on ? "off" : "on") << endl; -} - -void loopHandler() { - byte buttonState = digitalRead(PIN_BUTTON); - if ( buttonState != lastButtonState ) { - if (buttonState == LOW) { - buttonDownTime = millis(); - buttonPressHandled = 0; - } - else { - unsigned long dt = millis() - buttonDownTime; - if ( dt >= 90 && dt <= 900 && buttonPressHandled == 0 ) { - toggleRelay(); - buttonPressHandled = 1; - } - } - lastButtonState = buttonState; - } -} +OneButton externalButton(EXT_BUTTON_PIN, EXT_BUTTON_ACTIVE_STATE); +SwitchNode relayNode("relay"); void setup() { Serial.begin(115200); Serial.println(); Serial.println(); pinMode(PIN_RELAY, OUTPUT); - pinMode(PIN_BUTTON, INPUT); - digitalWrite(PIN_RELAY, LOW); + setRelayState(LOW); Homie_setFirmware("itead-sonoff-buton", "1.0.0"); - Homie.setLedPin(PIN_LED, LOW).setResetTrigger(PIN_BUTTON, LOW, 5000); + Homie.setLedPin(PIN_LED, LOW); - switchNode.advertise("on").settable(switchOnHandler); + relayNode.setCallback([](const bool val) { + setRelayState(val); + }); + externalButton.attachClick([]() { + setRelayState(!relayState, true); + }); - Homie.setLoopFunction(loopHandler); Homie.setup(); } void loop() { Homie.loop(); + externalButton.tick(); +} + +void setRelayState(const bool val, const bool overwriteSetter = false) { + relayState = val; + digitalWrite(PIN_RELAY, relayState); + relayNode.setValue(relayState, overwriteSetter); + Homie.getLogger() << "Switch is " << (relayState ? "on" : "off") << endl; } diff --git a/src/Homie.cpp b/src/Homie.cpp index 52bddb0d..25fef7a2 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -304,7 +304,7 @@ HomieClass& HomieClass::onEvent(const EventHandler& handler) { return *this; } -HomieClass& HomieClass::setResetTrigger(uint8_t pin, uint8_t state, uint16_t time) { +HomieClass& HomieClass::setResetTrigger(uint8_t pin, bool state, uint16_t time) { _checkBeforeSetup(F("setResetTrigger")); Interface::get().reset.enabled = true; @@ -323,6 +323,10 @@ HomieClass& HomieClass::disableResetTrigger() { return *this; } +HomieClass& HomieClass::setHomieButtonClick(CallbackFunction& function) { + HomieButton::setClickHandler(function); +} + const ConfigStruct& HomieClass::getConfiguration() { return Interface::get().getConfig().get(); } diff --git a/src/Homie.hpp b/src/Homie.hpp index 58ef3d7e..74bbbde5 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -14,6 +14,7 @@ #include "Homie/Logger.hpp" #include "Homie/Config.hpp" #include "Homie/Blinker.hpp" +#include "Homie/Utils/HomieButton.hpp" #include "SendingPromise.hpp" #include "HomieBootMode.hpp" @@ -57,6 +58,7 @@ class HomieClass { HomieClass& setLoopFunction(const CallbackFunction& function); HomieClass& setHomieBootMode(HomieBootMode bootMode); HomieClass& setHomieBootModeOnNextBoot(HomieBootMode bootMode); + HomieClass& setHomieButtonClick(CallbackFunction& function); static void reset(); static void reboot(); diff --git a/src/Homie/Config.hpp b/src/Homie/Config.hpp index b13c900b..b7fd6d90 100644 --- a/src/Homie/Config.hpp +++ b/src/Homie/Config.hpp @@ -17,7 +17,7 @@ #include "./Strings.hpp" #include "./Datatypes/Result.hpp" -// TODO: Config Remove of sensitive data Should have a default value so we know it actually exists. +// TODO(timpur): Config Remove of sensitive data Should have a default value so we know it actually exists. namespace HomieInternals { class Config { diff --git a/src/Homie/Constants.hpp b/src/Homie/Constants.hpp index 55dac04d..f98356c8 100644 --- a/src/Homie/Constants.hpp +++ b/src/Homie/Constants.hpp @@ -12,7 +12,7 @@ namespace HomieInternals { const char DEFAULT_MQTT_BASE_TOPIC[] = "homie/"; const uint8_t DEFAULT_RESET_PIN = 0; // == D3 on nodeMCU - const uint8_t DEFAULT_RESET_STATE = LOW; + const bool DEFAULT_RESET_STATE = LOW; const uint16_t DEFAULT_RESET_TIME = 5 * 1000; const char DEFAULT_BRAND[] = "Homie"; diff --git a/src/Homie/Datatypes/ConfigStruct.hpp b/src/Homie/Datatypes/ConfigStruct.hpp index 69cbb932..5e2bb618 100644 --- a/src/Homie/Datatypes/ConfigStruct.hpp +++ b/src/Homie/Datatypes/ConfigStruct.hpp @@ -43,4 +43,4 @@ struct ConfigStruct { bool enabled; } ota; }; -} // namespace HomieInternals +} // namespace HomieInternals diff --git a/src/Homie/Datatypes/Interface.hpp b/src/Homie/Datatypes/Interface.hpp index 97b7dc32..dcfa7314 100644 --- a/src/Homie/Datatypes/Interface.hpp +++ b/src/Homie/Datatypes/Interface.hpp @@ -49,7 +49,7 @@ class InterfaceData { struct Reset { bool enabled; uint8_t triggerPin; - uint8_t triggerState; + bool triggerState; uint16_t triggerTime; } reset; diff --git a/src/Homie/Utils/HomieButton.cpp b/src/Homie/Utils/HomieButton.cpp index e3571a38..0a587aed 100644 --- a/src/Homie/Utils/HomieButton.cpp +++ b/src/Homie/Utils/HomieButton.cpp @@ -11,7 +11,7 @@ void HomieButton::attach() { _homieButtonTicker.attach_ms(10, _tick); - _homieButton = new OneButton(Interface::get().reset.triggerPin, !Interface::get().reset.triggerState); + _homieButton = new OneButton(Interface::get().reset.triggerPin, !Interface::get().reset.triggerState); // TODO(timpur): Work out why this is inverted _homieButton->setPressTicks(Interface::get().reset.triggerTime); _homieButton->attachClick(_clickFunc); @@ -19,8 +19,8 @@ void HomieButton::attach() { _homieButton->attachLongPressStart(_longPressStartFunc); } -void HomieButton::addClickHandler(CallbackFunction func) { - _userClickFunc = func; +void HomieButton::setClickHandler(CallbackFunction& function) { + _userClickFunc = function; } void HomieButton::_tick() { diff --git a/src/Homie/Utils/HomieButton.hpp b/src/Homie/Utils/HomieButton.hpp index 0ba8fcf0..333e62e4 100644 --- a/src/Homie/Utils/HomieButton.hpp +++ b/src/Homie/Utils/HomieButton.hpp @@ -12,7 +12,7 @@ namespace HomieInternals { class HomieButton{ public: static void attach(); - static void addClickHandler(CallbackFunction func); + static void setClickHandler(CallbackFunction& function); private: // Disable creating an instance of this object @@ -21,10 +21,10 @@ class HomieButton{ static Ticker _homieButtonTicker; static OneButton* _homieButton; static CallbackFunction _userClickFunc; - + static void _tick(); static void _clickFunc(); static void _doubleClickFunc(); static void _longPressStartFunc(); }; -} \ No newline at end of file +} // namespace HomieInternals diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index 77a856cc..52fb0183 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -3,7 +3,8 @@ using namespace HomieInternals; ValidationResult Validation::validateConfig(const JsonObject& object) { - _removeNullConfigItems("Config", (JsonObject&)object); + JsonObject& objectPTR = (JsonObject&)object; + _removeNullConfigItems("Config", &objectPTR); ValidationResult result; result = _validateConfigRoot(object); @@ -21,17 +22,17 @@ ValidationResult Validation::validateConfig(const JsonObject& object) { return result; } -void Validation::_removeNullConfigItems(const char* name, JsonObject& object) { +void Validation::_removeNullConfigItems(const char* name, JsonObject* object) { auto hasValue = [](JsonVariant& val) { return val.is() || val.is() || val.is() || val.as() != nullptr; }; - for (JsonPair kv : object) { + for (JsonPair kv : *object) { if (kv.value.is()) { - _removeNullConfigItems(kv.key, kv.value.as()); + _removeNullConfigItems(kv.key, &kv.value.as()); } else if (!hasValue(kv.value)) { Interface::get().getLogger() << F("Removed ") << kv.key << F(" from ") << name << endl; - object.remove(kv.key); + object->remove(kv.key); } } } diff --git a/src/Homie/Utils/Validation.hpp b/src/Homie/Utils/Validation.hpp index 9197dc05..da7fdbab 100644 --- a/src/Homie/Utils/Validation.hpp +++ b/src/Homie/Utils/Validation.hpp @@ -14,7 +14,7 @@ class Validation { static ValidationResult validateConfig(const JsonObject& object); private: - static void _removeNullConfigItems(const char* name, JsonObject& object); + static void _removeNullConfigItems(const char* name, JsonObject* object); static ValidationResult _validateConfigRoot(const JsonObject& object); static ValidationResult _validateConfigWifi(const JsonObject& object); static ValidationResult _validateConfigMqtt(const JsonObject& object); diff --git a/src/HomieEvent.hpp b/src/HomieEvent.hpp index bc3ed972..5e946037 100644 --- a/src/HomieEvent.hpp +++ b/src/HomieEvent.hpp @@ -12,7 +12,7 @@ enum class HomieEventType : uint8_t { OTA_SUCCESSFUL, OTA_FAILED, ABOUT_TO_RESET, - ABOUT_TO_RESTART, + ABOUT_TO_RESTART, WIFI_CONNECTED, WIFI_DISCONNECTED, MQTT_READY, diff --git a/src/SendingPromise.cpp b/src/SendingPromise.cpp index bfdf8d44..96ee28b2 100644 --- a/src/SendingPromise.cpp +++ b/src/SendingPromise.cpp @@ -40,6 +40,10 @@ SendingPromise& SendingPromise::setRange(uint16_t rangeIndex) { } uint16_t SendingPromise::send(const String& value) { + return send(value.c_str()); +} + +uint16_t SendingPromise::send(const char* value) { if (!Interface::get().ready) { Interface::get().getLogger() << F("✖ setNodeProperty(): impossible now") << endl; return 0; @@ -60,11 +64,11 @@ uint16_t SendingPromise::send(const String& value) { strcat(topic, rangeStr); } - uint16_t packetId = Interface::get().getMqttClient().publish(topic, _qos, _retained, value.c_str()); + uint16_t packetId = Interface::get().getMqttClient().publish(topic, _qos, _retained, value); if (_overwriteSetter) { strcat_P(topic, PSTR("/set")); - Interface::get().getMqttClient().publish(topic, 1, true, value.c_str()); + Interface::get().getMqttClient().publish(topic, 1, true, value); } delete[] topic; diff --git a/src/SendingPromise.hpp b/src/SendingPromise.hpp index 353d9144..026b7be1 100644 --- a/src/SendingPromise.hpp +++ b/src/SendingPromise.hpp @@ -19,6 +19,7 @@ class SendingPromise { SendingPromise& setRange(const HomieRange& range); SendingPromise& setRange(uint16_t rangeIndex); uint16_t send(const String& value); + uint16_t send(const char* value); private: SendingPromise& setNode(const HomieNode& node); From 5288d7a6ea2b1f4e772968a106e3c91309c3c0fb Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 12 Mar 2018 21:23:03 +1100 Subject: [PATCH 30/51] Touch ups + restore example --- .../IteadSonoffButton/IteadSonoffButton.ino | 72 ++++++++++++------- library.json | 4 -- src/Homie/Utils/ResetHandler.hpp | 1 - 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/examples/IteadSonoffButton/IteadSonoffButton.ino b/examples/IteadSonoffButton/IteadSonoffButton.ino index 1f45b486..3279a2cb 100644 --- a/examples/IteadSonoffButton/IteadSonoffButton.ino +++ b/examples/IteadSonoffButton/IteadSonoffButton.ino @@ -6,46 +6,70 @@ */ #include -#include -#define PIN_RELAY 12 -#define PIN_LED 13 -#define EXT_BUTTON_PIN 0 -#define EXT_BUTTON_ACTIVE_STATE 1 +const int PIN_RELAY = 12; +const int PIN_LED = 13; +const int PIN_BUTTON = 0; -bool relayState; +unsigned long buttonDownTime = 0; +byte lastButtonState = 1; +byte buttonPressHandled = 0; -OneButton externalButton(EXT_BUTTON_PIN, EXT_BUTTON_ACTIVE_STATE); -SwitchNode relayNode("relay"); +HomieNode switchNode("switch", "switch"); + +bool switchOnHandler(HomieRange range, String value) { + if (value != "true" && value != "false") return false; + + bool on = (value == "true"); + digitalWrite(PIN_RELAY, on ? HIGH : LOW); + switchNode.setProperty("on").send(value); + Homie.getLogger() << "Switch is " << (on ? "on" : "off") << endl; + + return true; +} + +void toggleRelay() { + bool on = digitalRead(PIN_RELAY) == HIGH; + digitalWrite(PIN_RELAY, on ? LOW : HIGH); + switchNode.setProperty("on").send(on ? "false" : "true"); + Homie.getLogger() << "Switch is " << (on ? "off" : "on") << endl; +} + +void loopHandler() { + byte buttonState = digitalRead(PIN_BUTTON); + if ( buttonState != lastButtonState ) { + if (buttonState == LOW) { + buttonDownTime = millis(); + buttonPressHandled = 0; + } + else { + unsigned long dt = millis() - buttonDownTime; + if ( dt >= 90 && dt <= 900 && buttonPressHandled == 0 ) { + toggleRelay(); + buttonPressHandled = 1; + } + } + lastButtonState = buttonState; + } +} void setup() { Serial.begin(115200); Serial.println(); Serial.println(); pinMode(PIN_RELAY, OUTPUT); - setRelayState(LOW); + pinMode(PIN_BUTTON, INPUT); + digitalWrite(PIN_RELAY, LOW); Homie_setFirmware("itead-sonoff-buton", "1.0.0"); - Homie.setLedPin(PIN_LED, LOW); + Homie.setLedPin(PIN_LED, LOW).setResetTrigger(PIN_BUTTON, LOW, 5000); - relayNode.setCallback([](const bool val) { - setRelayState(val); - }); - externalButton.attachClick([]() { - setRelayState(!relayState, true); - }); + switchNode.advertise("on").settable(switchOnHandler); + Homie.setLoopFunction(loopHandler); Homie.setup(); } void loop() { Homie.loop(); - externalButton.tick(); -} - -void setRelayState(const bool val, const bool overwriteSetter = false) { - relayState = val; - digitalWrite(PIN_RELAY, relayState); - relayNode.setValue(relayState, overwriteSetter); - Homie.getLogger() << "Switch is " << (relayState ? "on" : "off") << endl; } diff --git a/library.json b/library.json index 473a6999..de7b86a9 100644 --- a/library.json +++ b/library.json @@ -26,10 +26,6 @@ "name": "AsyncMqttClient", "version": "^0.8.0" }, - { - "name": "Bounce2", - "version": "^2.1.0" - }, { "name": "OneButton" }, diff --git a/src/Homie/Utils/ResetHandler.hpp b/src/Homie/Utils/ResetHandler.hpp index 7e764de2..3b7e38c3 100644 --- a/src/Homie/Utils/ResetHandler.hpp +++ b/src/Homie/Utils/ResetHandler.hpp @@ -3,7 +3,6 @@ #include "Arduino.h" #include -#include #include "../../StreamingOperator.hpp" #include "../Datatypes/Interface.hpp" From da58af95a74ccb5856123a273d829494867ec802 Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 12 Mar 2018 21:33:50 +1100 Subject: [PATCH 31/51] Small Fix --- src/Homie.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Homie.hpp b/src/Homie.hpp index 74bbbde5..ced182ed 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -52,7 +52,7 @@ class HomieClass { HomieClass& setGlobalInputHandler(const GlobalInputHandler& globalInputHandler); HomieClass& setBroadcastHandler(const BroadcastHandler& broadcastHandler); HomieClass& onEvent(const EventHandler& handler); - HomieClass& setResetTrigger(uint8_t pin, uint8_t state, uint16_t time); + HomieClass& setResetTrigger(uint8_t pin, bool state, uint16_t time); HomieClass& disableResetTrigger(); HomieClass& setSetupFunction(const CallbackFunction& function); HomieClass& setLoopFunction(const CallbackFunction& function); From 3497d015b21279185a3ddcf7a2f01985929af505 Mon Sep 17 00:00:00 2001 From: timpur Date: Tue, 13 Mar 2018 08:23:06 +1100 Subject: [PATCH 32/51] Fix to Homie Button on click reg --- src/Homie.cpp | 2 +- src/Homie.hpp | 2 +- src/Homie/Utils/HomieButton.cpp | 2 +- src/Homie/Utils/HomieButton.hpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Homie.cpp b/src/Homie.cpp index 25fef7a2..41cb52c8 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -323,7 +323,7 @@ HomieClass& HomieClass::disableResetTrigger() { return *this; } -HomieClass& HomieClass::setHomieButtonClick(CallbackFunction& function) { +HomieClass& HomieClass::setHomieButtonClick(const CallbackFunction& function) { HomieButton::setClickHandler(function); } diff --git a/src/Homie.hpp b/src/Homie.hpp index ced182ed..118da573 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -58,7 +58,7 @@ class HomieClass { HomieClass& setLoopFunction(const CallbackFunction& function); HomieClass& setHomieBootMode(HomieBootMode bootMode); HomieClass& setHomieBootModeOnNextBoot(HomieBootMode bootMode); - HomieClass& setHomieButtonClick(CallbackFunction& function); + HomieClass& setHomieButtonClick(const CallbackFunction& function); static void reset(); static void reboot(); diff --git a/src/Homie/Utils/HomieButton.cpp b/src/Homie/Utils/HomieButton.cpp index 0a587aed..eab35077 100644 --- a/src/Homie/Utils/HomieButton.cpp +++ b/src/Homie/Utils/HomieButton.cpp @@ -19,7 +19,7 @@ void HomieButton::attach() { _homieButton->attachLongPressStart(_longPressStartFunc); } -void HomieButton::setClickHandler(CallbackFunction& function) { +void HomieButton::setClickHandler(const CallbackFunction& function) { _userClickFunc = function; } diff --git a/src/Homie/Utils/HomieButton.hpp b/src/Homie/Utils/HomieButton.hpp index 333e62e4..55d046a8 100644 --- a/src/Homie/Utils/HomieButton.hpp +++ b/src/Homie/Utils/HomieButton.hpp @@ -12,7 +12,7 @@ namespace HomieInternals { class HomieButton{ public: static void attach(); - static void setClickHandler(CallbackFunction& function); + static void setClickHandler(const CallbackFunction& function); private: // Disable creating an instance of this object From b884730da68eeabb6a17ad68a7f78b750e5bdd40 Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 19 Mar 2018 08:18:06 +1100 Subject: [PATCH 33/51] Fix #446 CORS Issue --- src/Homie/Boot/BootConfig.cpp | 28 +++++++++++++--------------- src/Homie/Boot/BootConfig.hpp | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 4fd44120..6cf96167 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -51,6 +51,7 @@ void BootConfig::setup() { _dns.setErrorReplyCode(DNSReplyCode::NoError); _dns.start(53, F("*"), ACCESS_POINT_IP); + __setCORS(); _http.on("/heart", HTTP_GET, [this](AsyncWebServerRequest *request) { Interface::get().getLogger() << F("Received heart request") << endl; request->send(204); @@ -58,18 +59,17 @@ void BootConfig::setup() { _http.on("/device-info", HTTP_GET, [this](AsyncWebServerRequest *request) { _onDeviceInfoRequest(request); }); _http.on("/networks", HTTP_GET, [this](AsyncWebServerRequest *request) { _onNetworksRequest(request); }); _http.on("/config", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onConfigRequest(request); }).onBody(BootConfig::__parsePost); - _http.on("/config", HTTP_OPTIONS, [this](AsyncWebServerRequest *request) { // CORS - Interface::get().getLogger() << F("Received CORS request for /config") << endl; - __sendCORS(request); - }); _http.on("/wifi/connect", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onWifiConnectRequest(request); }).onBody(BootConfig::__parsePost); - _http.on("/wifi/connect", HTTP_OPTIONS, [this](AsyncWebServerRequest *request) { // CORS - Interface::get().getLogger() << F("Received CORS request for /wifi/connect") << endl; - __sendCORS(request); - }); _http.on("/wifi/status", HTTP_GET, [this](AsyncWebServerRequest *request) { _onWifiStatusRequest(request); }); _http.on("/proxy/control", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onProxyControlRequest(request); }).onBody(BootConfig::__parsePost); - _http.onNotFound([this](AsyncWebServerRequest *request) { _onCaptivePortal(request); }); + _http.onNotFound([this](AsyncWebServerRequest *request) { + if ( request->method() == HTTP_OPTIONS ) { + Interface::get().getLogger() << F("Received CORS request for ")<< request->url() << endl; + request->send(200); + } else { + _onCaptivePortal(request); + } + }); _http.begin(); } @@ -409,12 +409,10 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { _flaggedForRebootAt = millis(); } -void BootConfig::__sendCORS(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response = request->beginResponse(204); - response->addHeader(F("Access-Control-Allow-Origin"), F("*")); - response->addHeader(F("Access-Control-Allow-Methods"), F("PUT")); - response->addHeader(F("Access-Control-Allow-Headers"), F("Content-Type, Origin, Referer, User-Agent")); - request->send(response); +void BootConfig::__setCORS() { + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Origin"), F("*")); + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Methods"), F("GET, PUT")); + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Headers"), F("Content-Type, Origin, Referer, User-Agent")); } void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { diff --git a/src/Homie/Boot/BootConfig.hpp b/src/Homie/Boot/BootConfig.hpp index 54a4c59f..6893de60 100644 --- a/src/Homie/Boot/BootConfig.hpp +++ b/src/Homie/Boot/BootConfig.hpp @@ -55,7 +55,7 @@ class BootConfig : public Boot { void _onWifiStatusRequest(AsyncWebServerRequest *request); // Helpers - static void __sendCORS(AsyncWebServerRequest *request); + static void __setCORS(); static const int MAX_POST_SIZE = 1500; static void __parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); static void __SendJSONError(AsyncWebServerRequest *request, String msg, int16_t code = 400); From f3cc8902187f2a6d0ebc38646893bcfff3919687 Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 19 Mar 2018 08:20:41 +1100 Subject: [PATCH 34/51] Fix for #477 --- src/Homie/Boot/BootConfig.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 6cf96167..b016e799 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -416,15 +416,18 @@ void BootConfig::__setCORS() { } void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { - if (!index && total > MAX_POST_SIZE) { - Interface::get().getLogger() << "Request is to large to be processed." << endl; - } else if (!index && total <= MAX_POST_SIZE) { - char* buff = new char[total + 1]; - strcpy(buff, (const char*)data); - request->_tempObject = buff; - } else if (total <= MAX_POST_SIZE) { - char* buff = reinterpret_cast(request->_tempObject); - strcat(buff, (const char*)data); + if (total > MAX_POST_SIZE) { + Interface::get().getLogger() << F("Request is to large to be processed.") << endl; + } else { + if (index == 0) { + request->_tempObject = new char[total + 1]; + } + void* buff = request->_tempObject + index; + memcpy(buff, data, len); + if (index + len == total) { + void* buff = request->_tempObject + total; + *(char*)buff = 0; + } } } static const String ConfigJSONError(const String error) { From 07219fc8c9afadba662d2bc040d098a390a9eef5 Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 19 Mar 2018 08:37:53 +1100 Subject: [PATCH 35/51] Fix Lint --- src/Homie/Boot/BootConfig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index b016e799..0ea07192 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -426,7 +426,7 @@ void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size memcpy(buff, data, len); if (index + len == total) { void* buff = request->_tempObject + total; - *(char*)buff = 0; + *reinterpret_cast(buff) = 0; } } } From 95d95669756931e9918450260364a473d6d1cbda Mon Sep 17 00:00:00 2001 From: Tim P Date: Mon, 19 Mar 2018 08:44:33 +1100 Subject: [PATCH 36/51] Fix #446 #477 (#501) * Fix #446 CORS Issue * Fix for #477 * Fix Lint --- src/Homie/Boot/BootConfig.cpp | 49 ++++++++++++++++++----------------- src/Homie/Boot/BootConfig.hpp | 2 +- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 4fd44120..0ea07192 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -51,6 +51,7 @@ void BootConfig::setup() { _dns.setErrorReplyCode(DNSReplyCode::NoError); _dns.start(53, F("*"), ACCESS_POINT_IP); + __setCORS(); _http.on("/heart", HTTP_GET, [this](AsyncWebServerRequest *request) { Interface::get().getLogger() << F("Received heart request") << endl; request->send(204); @@ -58,18 +59,17 @@ void BootConfig::setup() { _http.on("/device-info", HTTP_GET, [this](AsyncWebServerRequest *request) { _onDeviceInfoRequest(request); }); _http.on("/networks", HTTP_GET, [this](AsyncWebServerRequest *request) { _onNetworksRequest(request); }); _http.on("/config", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onConfigRequest(request); }).onBody(BootConfig::__parsePost); - _http.on("/config", HTTP_OPTIONS, [this](AsyncWebServerRequest *request) { // CORS - Interface::get().getLogger() << F("Received CORS request for /config") << endl; - __sendCORS(request); - }); _http.on("/wifi/connect", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onWifiConnectRequest(request); }).onBody(BootConfig::__parsePost); - _http.on("/wifi/connect", HTTP_OPTIONS, [this](AsyncWebServerRequest *request) { // CORS - Interface::get().getLogger() << F("Received CORS request for /wifi/connect") << endl; - __sendCORS(request); - }); _http.on("/wifi/status", HTTP_GET, [this](AsyncWebServerRequest *request) { _onWifiStatusRequest(request); }); _http.on("/proxy/control", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onProxyControlRequest(request); }).onBody(BootConfig::__parsePost); - _http.onNotFound([this](AsyncWebServerRequest *request) { _onCaptivePortal(request); }); + _http.onNotFound([this](AsyncWebServerRequest *request) { + if ( request->method() == HTTP_OPTIONS ) { + Interface::get().getLogger() << F("Received CORS request for ")<< request->url() << endl; + request->send(200); + } else { + _onCaptivePortal(request); + } + }); _http.begin(); } @@ -409,24 +409,25 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { _flaggedForRebootAt = millis(); } -void BootConfig::__sendCORS(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response = request->beginResponse(204); - response->addHeader(F("Access-Control-Allow-Origin"), F("*")); - response->addHeader(F("Access-Control-Allow-Methods"), F("PUT")); - response->addHeader(F("Access-Control-Allow-Headers"), F("Content-Type, Origin, Referer, User-Agent")); - request->send(response); +void BootConfig::__setCORS() { + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Origin"), F("*")); + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Methods"), F("GET, PUT")); + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Headers"), F("Content-Type, Origin, Referer, User-Agent")); } void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { - if (!index && total > MAX_POST_SIZE) { - Interface::get().getLogger() << "Request is to large to be processed." << endl; - } else if (!index && total <= MAX_POST_SIZE) { - char* buff = new char[total + 1]; - strcpy(buff, (const char*)data); - request->_tempObject = buff; - } else if (total <= MAX_POST_SIZE) { - char* buff = reinterpret_cast(request->_tempObject); - strcat(buff, (const char*)data); + if (total > MAX_POST_SIZE) { + Interface::get().getLogger() << F("Request is to large to be processed.") << endl; + } else { + if (index == 0) { + request->_tempObject = new char[total + 1]; + } + void* buff = request->_tempObject + index; + memcpy(buff, data, len); + if (index + len == total) { + void* buff = request->_tempObject + total; + *reinterpret_cast(buff) = 0; + } } } static const String ConfigJSONError(const String error) { diff --git a/src/Homie/Boot/BootConfig.hpp b/src/Homie/Boot/BootConfig.hpp index 54a4c59f..6893de60 100644 --- a/src/Homie/Boot/BootConfig.hpp +++ b/src/Homie/Boot/BootConfig.hpp @@ -55,7 +55,7 @@ class BootConfig : public Boot { void _onWifiStatusRequest(AsyncWebServerRequest *request); // Helpers - static void __sendCORS(AsyncWebServerRequest *request); + static void __setCORS(); static const int MAX_POST_SIZE = 1500; static void __parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); static void __SendJSONError(AsyncWebServerRequest *request, String msg, int16_t code = 400); From b7b6776794f84bfdf7c01c36a0f5f2212cf7027c Mon Sep 17 00:00:00 2001 From: Tim P Date: Mon, 19 Mar 2018 21:07:02 +1100 Subject: [PATCH 37/51] Fix Safari not displaying the config bundle HTML page (Fix #476) (#502) * Fix Safari not displaying the config bundle HTML page Safari cannot deal with gzip files that have a "*.gz" file extension. Simply faking the filename solves the problem though. --- src/Homie/Boot/BootConfig.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 0ea07192..f788858a 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -260,8 +260,7 @@ void BootConfig::_onCaptivePortal(AsyncWebServerRequest *request) { } else if (request->url() == "/" && SPIFFS.exists(CONFIG_UI_BUNDLE_PATH)) { // Respond with UI Interface::get().getLogger() << F("UI bundle found") << endl; - AsyncWebServerResponse *response = request->beginResponse(SPIFFS, CONFIG_UI_BUNDLE_PATH, F("text/html")); - response->addHeader("Content-Encoding", "gzip"); + AsyncWebServerResponse *response = request->beginResponse(SPIFFS.open(CONFIG_UI_BUNDLE_PATH, "r"), F("index.html"), F("text/html")); request->send(response); } else { // Faild to find request From 03938e197eb2e481435248a477f31355f38aafe7 Mon Sep 17 00:00:00 2001 From: Tim P Date: Mon, 19 Mar 2018 21:17:42 +1100 Subject: [PATCH 38/51] Update Readme Homie Version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 172c82ec..946f244f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Homie for ESP8266 [![Build Status](https://img.shields.io/circleci/project/github/marvinroger/homie-esp8266/develop.svg?style=flat-square)](https://circleci.com/gh/marvinroger/homie-esp8266) [![Latest Release](https://img.shields.io/badge/release-v2.0.0-yellow.svg?style=flat-square)](https://github.com/marvinroger/homie-esp8266/releases) [![Gitter](https://img.shields.io/gitter/room/Homie/ESP8266.svg?style=flat-square)](https://gitter.im/homie-iot/ESP8266) -An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. Currently Homie for ESP8266 implements [Homie 2.0](https://github.com/marvinroger/homie/releases/tag/v2.0) +An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. Currently Homie for ESP8266 implements [Homie 2.0](https://github.com/marvinroger/homie/releases/tag/v2.0.0) ## Note for v1.x users From 7f2acdb2b5ada519cd759673e4cc235570f29c58 Mon Sep 17 00:00:00 2001 From: timpur Date: Mon, 19 Mar 2018 21:37:07 +1100 Subject: [PATCH 39/51] Fix Warnings --- src/Homie.cpp | 2 +- src/Homie/Boot/BootConfig.cpp | 15 +++++---------- src/Homie/Logger.cpp | 6 ++++-- src/Homie/Timer.cpp | 2 +- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Homie.cpp b/src/Homie.cpp index 6b769806..ea45fb13 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -10,7 +10,7 @@ HomieClass::HomieClass() Interface::get().bootMode = HomieBootMode::UNDEFINED; Interface::get().configurationAp.secured = false; Interface::get().led.enabled = true; - Interface::get().led.pin = BUILTIN_LED; + Interface::get().led.pin = LED_BUILTIN; Interface::get().led.on = LOW; Interface::get().reset.idle = true; Interface::get().reset.enabled = true; diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index f788858a..9332b6ab 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -281,7 +281,7 @@ void BootConfig::_proxyHttpRequest(AsyncWebServerRequest *request) { _httpClient.setUserAgent(F("ESP8266-Homie")); _httpClient.begin(url); // copy headers - for (int i = 0; i < request->headers(); i++) { + for (size_t i = 0; i < request->headers(); i++) { _httpClient.addHeader(request->headerName(i), request->header(i)); } @@ -335,7 +335,7 @@ void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { for (IHomieSetting* iSetting : IHomieSetting::settings) { JsonObject& jsonSetting = jsonBuffer.createObject(); - if (iSetting->getType() != "unknown") { + if (String(iSetting->getType()) != "unknown") { jsonSetting["name"] = iSetting->getName(); jsonSetting["description"] = iSetting->getDescription(); jsonSetting["type"] = iSetting->getType(); @@ -421,19 +421,14 @@ void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size if (index == 0) { request->_tempObject = new char[total + 1]; } - void* buff = request->_tempObject + index; + char* buff = reinterpret_cast(request->_tempObject) + index; memcpy(buff, data, len); if (index + len == total) { - void* buff = request->_tempObject + total; - *reinterpret_cast(buff) = 0; + char* buff = reinterpret_cast(request->_tempObject) + total; + *buff = '\0'; } } } -static const String ConfigJSONError(const String error) { - const String BEGINNING = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); - const String END = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_END)); - return BEGINNING + error + END; -} void HomieInternals::BootConfig::__SendJSONError(AsyncWebServerRequest * request, String msg, int16_t code) { Interface::get().getLogger() << msg << endl; diff --git a/src/Homie/Logger.cpp b/src/Homie/Logger.cpp index d3fc70e6..e1291637 100644 --- a/src/Homie/Logger.cpp +++ b/src/Homie/Logger.cpp @@ -16,9 +16,11 @@ void Logger::setPrinter(Print* printer) { } size_t Logger::write(uint8_t character) { - if (_loggingEnabled) _printer->write(character); + if (_loggingEnabled) return _printer->write(character); + return 0; } size_t Logger::write(const uint8_t* buffer, size_t size) { - if (_loggingEnabled) _printer->write(buffer, size); + if (_loggingEnabled) return _printer->write(buffer, size); + return 0; } diff --git a/src/Homie/Timer.cpp b/src/Homie/Timer.cpp index 063b051f..8e13519a 100644 --- a/src/Homie/Timer.cpp +++ b/src/Homie/Timer.cpp @@ -17,7 +17,7 @@ void Timer::setInterval(uint32_t interval, bool tickAtBeginning) { } uint32_t HomieInternals::Timer::getInterval() { - _interval; + return _interval; } bool Timer::check() const { From 901076f69cbf9e73b86a2c4683f023552cc08730 Mon Sep 17 00:00:00 2001 From: Tim P Date: Mon, 19 Mar 2018 23:01:43 +1100 Subject: [PATCH 40/51] Fix Warnings (#503) --- src/Homie.cpp | 2 +- src/Homie/Boot/BootConfig.cpp | 15 +++++---------- src/Homie/Logger.cpp | 6 ++++-- src/Homie/Timer.cpp | 2 +- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Homie.cpp b/src/Homie.cpp index 6b769806..ea45fb13 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -10,7 +10,7 @@ HomieClass::HomieClass() Interface::get().bootMode = HomieBootMode::UNDEFINED; Interface::get().configurationAp.secured = false; Interface::get().led.enabled = true; - Interface::get().led.pin = BUILTIN_LED; + Interface::get().led.pin = LED_BUILTIN; Interface::get().led.on = LOW; Interface::get().reset.idle = true; Interface::get().reset.enabled = true; diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index f788858a..9332b6ab 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -281,7 +281,7 @@ void BootConfig::_proxyHttpRequest(AsyncWebServerRequest *request) { _httpClient.setUserAgent(F("ESP8266-Homie")); _httpClient.begin(url); // copy headers - for (int i = 0; i < request->headers(); i++) { + for (size_t i = 0; i < request->headers(); i++) { _httpClient.addHeader(request->headerName(i), request->header(i)); } @@ -335,7 +335,7 @@ void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { for (IHomieSetting* iSetting : IHomieSetting::settings) { JsonObject& jsonSetting = jsonBuffer.createObject(); - if (iSetting->getType() != "unknown") { + if (String(iSetting->getType()) != "unknown") { jsonSetting["name"] = iSetting->getName(); jsonSetting["description"] = iSetting->getDescription(); jsonSetting["type"] = iSetting->getType(); @@ -421,19 +421,14 @@ void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size if (index == 0) { request->_tempObject = new char[total + 1]; } - void* buff = request->_tempObject + index; + char* buff = reinterpret_cast(request->_tempObject) + index; memcpy(buff, data, len); if (index + len == total) { - void* buff = request->_tempObject + total; - *reinterpret_cast(buff) = 0; + char* buff = reinterpret_cast(request->_tempObject) + total; + *buff = '\0'; } } } -static const String ConfigJSONError(const String error) { - const String BEGINNING = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); - const String END = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_END)); - return BEGINNING + error + END; -} void HomieInternals::BootConfig::__SendJSONError(AsyncWebServerRequest * request, String msg, int16_t code) { Interface::get().getLogger() << msg << endl; diff --git a/src/Homie/Logger.cpp b/src/Homie/Logger.cpp index d3fc70e6..e1291637 100644 --- a/src/Homie/Logger.cpp +++ b/src/Homie/Logger.cpp @@ -16,9 +16,11 @@ void Logger::setPrinter(Print* printer) { } size_t Logger::write(uint8_t character) { - if (_loggingEnabled) _printer->write(character); + if (_loggingEnabled) return _printer->write(character); + return 0; } size_t Logger::write(const uint8_t* buffer, size_t size) { - if (_loggingEnabled) _printer->write(buffer, size); + if (_loggingEnabled) return _printer->write(buffer, size); + return 0; } diff --git a/src/Homie/Timer.cpp b/src/Homie/Timer.cpp index 063b051f..8e13519a 100644 --- a/src/Homie/Timer.cpp +++ b/src/Homie/Timer.cpp @@ -17,7 +17,7 @@ void Timer::setInterval(uint32_t interval, bool tickAtBeginning) { } uint32_t HomieInternals::Timer::getInterval() { - _interval; + return _interval; } bool Timer::check() const { From 9e83c6aea1a838d858ed0c87f145a6670bcdcaf4 Mon Sep 17 00:00:00 2001 From: timpur Date: Tue, 20 Mar 2018 08:04:24 +1100 Subject: [PATCH 41/51] Minor Change from string to strcmp --- src/Homie/Boot/BootConfig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 9332b6ab..a18a625d 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -335,7 +335,7 @@ void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { for (IHomieSetting* iSetting : IHomieSetting::settings) { JsonObject& jsonSetting = jsonBuffer.createObject(); - if (String(iSetting->getType()) != "unknown") { + if (strcmp(iSetting->getType()), "unknown") != 0) { jsonSetting["name"] = iSetting->getName(); jsonSetting["description"] = iSetting->getDescription(); jsonSetting["type"] = iSetting->getType(); From bfba03d36ee058cf4d071866b3befd96af520daa Mon Sep 17 00:00:00 2001 From: Tim P Date: Wed, 21 Mar 2018 09:05:26 +1100 Subject: [PATCH 42/51] Update Readme - Homie Convention --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 946f244f..cb97782a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ Homie for ESP8266 [![Build Status](https://img.shields.io/circleci/project/github/marvinroger/homie-esp8266/develop.svg?style=flat-square)](https://circleci.com/gh/marvinroger/homie-esp8266) [![Latest Release](https://img.shields.io/badge/release-v2.0.0-yellow.svg?style=flat-square)](https://github.com/marvinroger/homie-esp8266/releases) [![Gitter](https://img.shields.io/gitter/room/Homie/ESP8266.svg?style=flat-square)](https://gitter.im/homie-iot/ESP8266) -An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. Currently Homie for ESP8266 implements [Homie 2.0](https://github.com/marvinroger/homie/releases/tag/v2.0.0) +An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. + +Currently Homie for ESP8266 implements [Homie 2.0.0](https://github.com/marvinroger/homie/releases/tag/v2.0.0) ## Note for v1.x users From e229bc1062177e612737ef90988de024c105a0d1 Mon Sep 17 00:00:00 2001 From: timpur Date: Tue, 27 Mar 2018 08:09:17 +1100 Subject: [PATCH 43/51] Update to use homie convention v2.0.1 --- README.md | 2 +- src/Homie/Boot/BootNormal.cpp | 20 ++++++++++++++++---- src/Homie/Boot/BootNormal.hpp | 3 ++- src/Homie/Constants.hpp | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cb97782a..a4299de2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Homie for ESP8266 An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. -Currently Homie for ESP8266 implements [Homie 2.0.0](https://github.com/marvinroger/homie/releases/tag/v2.0.0) +Currently Homie for ESP8266 implements [Homie 2.0.1](https://github.com/marvinroger/homie/releases/tag/v2.0.1) ## Note for v1.x users diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index 026489c5..6176bd17 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -323,14 +323,14 @@ void BootNormal::_advertise() { switch (_advertisementProgress.globalStep) { case AdvertisementProgress::GlobalStep::PUB_HOMIE: packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$homie")), 1, true, HOMIE_VERSION); - if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_MAC; - break; - case AdvertisementProgress::GlobalStep::PUB_MAC: - packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$mac")), 1, true, WiFi.macAddress().c_str()); if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_NAME; break; case AdvertisementProgress::GlobalStep::PUB_NAME: packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$name")), 1, true, Interface::get().getConfig().get().name); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_MAC; + break; + case AdvertisementProgress::GlobalStep::PUB_MAC: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$mac")), 1, true, WiFi.macAddress().c_str()); if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_LOCALIP; break; case AdvertisementProgress::GlobalStep::PUB_LOCALIP: @@ -339,6 +339,18 @@ void BootNormal::_advertise() { char localIpStr[MAX_IP_STRING_LENGTH]; Helpers::ipToString(localIp, localIpStr); packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$localip")), 1, true, localIpStr); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_NODES_ATTR; + break; + } + case AdvertisementProgress::GlobalStep::PUB_NODES_ATTR: + { + String nodes; + for (HomieNode* node : HomieNode::nodes) { + nodes.concat(node->getId()); + nodes.concat(F(",")); + } + if (HomieNode::nodes.size() >= 1) nodes.remove(nodes.length() - 1); + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$nodes")), 1, true, nodes.c_str()); if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_STATS_INTERVAL; break; } diff --git a/src/Homie/Boot/BootNormal.hpp b/src/Homie/Boot/BootNormal.hpp index d9937134..8d119715 100644 --- a/src/Homie/Boot/BootNormal.hpp +++ b/src/Homie/Boot/BootNormal.hpp @@ -33,9 +33,10 @@ class BootNormal : public Boot { bool done = false; enum class GlobalStep { PUB_HOMIE, - PUB_MAC, PUB_NAME, + PUB_MAC, PUB_LOCALIP, + PUB_NODES_ATTR, PUB_STATS_INTERVAL, PUB_FW_NAME, PUB_FW_VERSION, diff --git a/src/Homie/Constants.hpp b/src/Homie/Constants.hpp index 20f4ffc3..5b26686b 100644 --- a/src/Homie/Constants.hpp +++ b/src/Homie/Constants.hpp @@ -3,7 +3,7 @@ #include namespace HomieInternals { - const char HOMIE_VERSION[] = "2.0.0"; + const char HOMIE_VERSION[] = "2.0.1"; const char HOMIE_ESP8266_VERSION[] = "2.0.0"; const IPAddress ACCESS_POINT_IP(192, 168, 123, 1); From b17b6262396d5bef8659e2f90f7c68233fe47b51 Mon Sep 17 00:00:00 2001 From: timpur Date: Tue, 27 Mar 2018 08:19:10 +1100 Subject: [PATCH 44/51] Minor Fix --- src/Homie/Boot/BootConfig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index e806a3a3..e85869d6 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -344,7 +344,7 @@ void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { for (IHomieSetting& iSetting : IHomieSetting::settings) { JsonObject& jsonSetting = jsonBuffer.createObject(); - if (strcmp(iSetting->getType(), "unknown") != 0) { + if (strcmp(iSetting.getType(), "unknown") != 0) { jsonSetting["name"] = iSetting.getName(); jsonSetting["description"] = iSetting.getDescription(); jsonSetting["type"] = iSetting.getType(); From 99161ce03c7ff1c8d3dc1fcc52abbf081212b68f Mon Sep 17 00:00:00 2001 From: Tim P Date: Fri, 30 Mar 2018 21:22:06 +1100 Subject: [PATCH 45/51] Update to Homie Convention v2.0.1 (#507) --- README.md | 2 +- src/Homie/Boot/BootConfig.cpp | 2 +- src/Homie/Boot/BootNormal.cpp | 20 ++++++++++++++++---- src/Homie/Boot/BootNormal.hpp | 3 ++- src/Homie/Constants.hpp | 2 +- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cb97782a..a4299de2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Homie for ESP8266 An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. -Currently Homie for ESP8266 implements [Homie 2.0.0](https://github.com/marvinroger/homie/releases/tag/v2.0.0) +Currently Homie for ESP8266 implements [Homie 2.0.1](https://github.com/marvinroger/homie/releases/tag/v2.0.1) ## Note for v1.x users diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 9332b6ab..d0f5bf28 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -335,7 +335,7 @@ void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { for (IHomieSetting* iSetting : IHomieSetting::settings) { JsonObject& jsonSetting = jsonBuffer.createObject(); - if (String(iSetting->getType()) != "unknown") { + if (strcmp(iSetting->getType(), "unknown") != 0) { jsonSetting["name"] = iSetting->getName(); jsonSetting["description"] = iSetting->getDescription(); jsonSetting["type"] = iSetting->getType(); diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index 026489c5..6176bd17 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -323,14 +323,14 @@ void BootNormal::_advertise() { switch (_advertisementProgress.globalStep) { case AdvertisementProgress::GlobalStep::PUB_HOMIE: packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$homie")), 1, true, HOMIE_VERSION); - if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_MAC; - break; - case AdvertisementProgress::GlobalStep::PUB_MAC: - packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$mac")), 1, true, WiFi.macAddress().c_str()); if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_NAME; break; case AdvertisementProgress::GlobalStep::PUB_NAME: packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$name")), 1, true, Interface::get().getConfig().get().name); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_MAC; + break; + case AdvertisementProgress::GlobalStep::PUB_MAC: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$mac")), 1, true, WiFi.macAddress().c_str()); if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_LOCALIP; break; case AdvertisementProgress::GlobalStep::PUB_LOCALIP: @@ -339,6 +339,18 @@ void BootNormal::_advertise() { char localIpStr[MAX_IP_STRING_LENGTH]; Helpers::ipToString(localIp, localIpStr); packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$localip")), 1, true, localIpStr); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_NODES_ATTR; + break; + } + case AdvertisementProgress::GlobalStep::PUB_NODES_ATTR: + { + String nodes; + for (HomieNode* node : HomieNode::nodes) { + nodes.concat(node->getId()); + nodes.concat(F(",")); + } + if (HomieNode::nodes.size() >= 1) nodes.remove(nodes.length() - 1); + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$nodes")), 1, true, nodes.c_str()); if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_STATS_INTERVAL; break; } diff --git a/src/Homie/Boot/BootNormal.hpp b/src/Homie/Boot/BootNormal.hpp index d9937134..8d119715 100644 --- a/src/Homie/Boot/BootNormal.hpp +++ b/src/Homie/Boot/BootNormal.hpp @@ -33,9 +33,10 @@ class BootNormal : public Boot { bool done = false; enum class GlobalStep { PUB_HOMIE, - PUB_MAC, PUB_NAME, + PUB_MAC, PUB_LOCALIP, + PUB_NODES_ATTR, PUB_STATS_INTERVAL, PUB_FW_NAME, PUB_FW_VERSION, diff --git a/src/Homie/Constants.hpp b/src/Homie/Constants.hpp index 20f4ffc3..5b26686b 100644 --- a/src/Homie/Constants.hpp +++ b/src/Homie/Constants.hpp @@ -3,7 +3,7 @@ #include namespace HomieInternals { - const char HOMIE_VERSION[] = "2.0.0"; + const char HOMIE_VERSION[] = "2.0.1"; const char HOMIE_ESP8266_VERSION[] = "2.0.0"; const IPAddress ACCESS_POINT_IP(192, 168, 123, 1); From 05fcbfda14a01fb753723341137f79655573ca29 Mon Sep 17 00:00:00 2001 From: timpur Date: Sun, 1 Apr 2018 17:29:25 +1000 Subject: [PATCH 46/51] Add getHomieBootMode --- docs/others/cpp-api-reference.md | 6 ++++++ src/Homie.cpp | 4 ++++ src/Homie.hpp | 1 + 3 files changed, 11 insertions(+) diff --git a/docs/others/cpp-api-reference.md b/docs/others/cpp-api-reference.md index 57f605d1..77fe3788 100644 --- a/docs/others/cpp-api-reference.md +++ b/docs/others/cpp-api-reference.md @@ -245,6 +245,12 @@ String getDeviceID(); Get the Device ID, which homie is using. Could be the config `device_id` or the inertnal device `mac`. +```c++ +HomieBootMode getHomieBootMode(); +``` + +Get the boot mode of homie: `UNDEFINED,STANDALONE,CONFIGURATION,NORMAL`. + ------- ## HomieNode diff --git a/src/Homie.cpp b/src/Homie.cpp index 04ed2dd3..6ce01170 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -362,4 +362,8 @@ void HomieClass::doDeepSleep(uint32_t time_us, RFMode mode) { ESP.deepSleep(time_us, mode); } +HomieBootMode HomieClass::getHomieBootMode() { + return Interface::get().bootMode; +} + HomieClass Homie; diff --git a/src/Homie.hpp b/src/Homie.hpp index 118da573..66113260 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -71,6 +71,7 @@ class HomieClass { static String getDeviceID(); static void prepareToSleep(); static void doDeepSleep(uint32_t time_us = 0, RFMode mode = RF_DEFAULT); + static HomieBootMode getHomieBootMode(); private: bool _setupCalled; From c105381fdcb328f2371ad4457f3796227671a258 Mon Sep 17 00:00:00 2001 From: timpur Date: Sun, 27 May 2018 14:35:50 +1000 Subject: [PATCH 47/51] Revert Beaking chnage --- docs/configuration/http-json-api.md | 2 +- src/Homie/Boot/BootConfig.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/http-json-api.md b/docs/configuration/http-json-api.md index fd3f7af9..72dcc72d 100644 --- a/docs/configuration/http-json-api.md +++ b/docs/configuration/http-json-api.md @@ -48,7 +48,7 @@ If anything goes wrong, a return code != 2xx will be returned, with a JSON `erro ```json { - "device_hardware_id": "52a8fa5d", + "hardware_device_id": "52a8fa5d", "homie_version": "2.0.0", "homie_esp8266_version": "2.1.0", "device_config_state": false, diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index e85869d6..9989ba7e 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -324,7 +324,7 @@ void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { JsonObject& json = jsonBuffer.createObject(); json["homie_version"] = HOMIE_VERSION; json["homie_esp8266_version"] = HOMIE_ESP8266_VERSION; - json["device_hardware_id"] = DeviceId::get(); + json["hardware_device_id"] = DeviceId::get(); auto configValidationResult = Interface::get().getConfig().isConfigFileValid(); json["device_config_state"] = configValidationResult.valid; if (!configValidationResult.valid) json["device_config_state_error"] = configValidationResult.reason; From d014eefb91914b86d6a74d9ce7b748f338e4dda2 Mon Sep 17 00:00:00 2001 From: timpur Date: Sat, 2 Jun 2018 11:06:09 +1000 Subject: [PATCH 48/51] Release 2.0.0 (Force Merge) --- .circleci/assets/circleci.ignore.yml | 11 + .circleci/assets/configurator_v1.html | 33 + .circleci/assets/docs_index_template.html | 75 ++ .circleci/assets/generate_docs.py | 130 ++ .circleci/assets/id_rsa.enc | Bin 0 -> 3264 bytes .circleci/assets/mkdocs.default.yml | 29 + .circleci/config.yml | 101 ++ .editorconfig | 12 + .github/ISSUE_TEMPLATE.md | 9 + .gitignore | 7 +- Makefile | 3 + README.md | 100 +- banner.png | Bin 0 -> 13044 bytes data/homie/README.md | 12 + .../homie/example.config.json | 28 +- docs/1.-What-is-it.md | 5 - docs/2.-Getting-started.md | 184 --- docs/3.-Advanced-usage.md | 202 --- docs/4.-OTA.md | 22 - docs/5.-JSON-configuration-file.md | 66 - docs/6.-Configuration-API.md | 133 -- docs/7.-API-reference.md | 136 -- docs/8.-Limitations-and-known-issues.md | 24 - docs/README.md | 11 +- docs/advanced-usage/branding.md | 8 + docs/advanced-usage/broadcast.md | 13 + docs/advanced-usage/built-in-led.md | 19 + docs/advanced-usage/custom-settings.md | 38 + docs/advanced-usage/deep-sleep.md | 29 + docs/advanced-usage/events.md | 69 + docs/advanced-usage/input-handlers.md | 63 + docs/advanced-usage/logging.md | 26 + docs/advanced-usage/magic-bytes.md | 16 + docs/advanced-usage/miscellaneous.md | 63 + docs/advanced-usage/range-properties.md | 31 + docs/advanced-usage/resetting.md | 35 + docs/advanced-usage/standalone-mode.md | 12 + docs/advanced-usage/streaming-operator.md | 17 + docs/advanced-usage/ui-bundle.md | 3 + docs/assets/github.png | Bin 0 -> 2944 bytes docs/assets/logo.png | Bin 0 -> 26042 bytes docs/assets/youtube.png | Bin 0 -> 2635 bytes docs/configuration/http-json-api.md | 264 ++++ docs/configuration/json-configuration-file.md | 59 + docs/index.md | 16 +- docs/others/community-projects.md | 19 + docs/others/cpp-api-reference.md | 323 +++++ docs/others/homie-implementation-specifics.md | 30 + docs/others/limitations-and-known-issues.md | 11 + docs/others/ota-configuration-updates.md | 61 + .../troubleshooting.md} | 9 +- docs/others/upgrade-guide-from-v1-to-v2.md | 15 + docs/quickstart/getting-started.md | 142 ++ docs/quickstart/what-is-it.md | 3 + examples/Broadcast/Broadcast.ino | 19 + examples/CustomSettings/CustomSettings.ino | 42 + examples/DoorSensor/DoorSensor.ino | 20 +- .../GlobalInputHandler/GlobalInputHandler.ino | 23 + examples/HookToEvents/HookToEvents.ino | 61 +- examples/IteadSonoff/IteadSonoff.ino | 31 +- .../IteadSonoffButton/IteadSonoffButton.ino | 75 ++ examples/LedStrip/LedStrip.ino | 51 +- examples/LightOnOff/LightOnOff.ino | 28 +- .../SonoffDualShutters/SonoffDualShutters.ino | 128 ++ .../TemperatureSensor/TemperatureSensor.ino | 25 +- homie-esp8266.cppcheck | 12 +- homie-esp8266.jpg | Bin 47471 -> 0 bytes keywords.txt | 142 +- library.json | 38 +- library.properties | 2 +- mkdocs.yml | 76 ++ scripts/firmware_parser/README.md | 8 + scripts/firmware_parser/firmware_parser.py | 40 + scripts/ota_updater/README.md | 48 + scripts/ota_updater/ota_updater.py | 155 +++ scripts/ota_updater/requirements.txt | 1 + src/Homie.cpp | 406 ++++-- src/Homie.h | 6 +- src/Homie.hpp | 124 +- src/Homie/Blinker.cpp | 24 +- src/Homie/Blinker.hpp | 24 +- src/Homie/Boot/Boot.cpp | 28 +- src/Homie/Boot/Boot.hpp | 26 +- src/Homie/Boot/BootConfig.cpp | 517 ++++--- src/Homie/Boot/BootConfig.hpp | 72 +- src/Homie/Boot/BootNormal.cpp | 1195 ++++++++++++----- src/Homie/Boot/BootNormal.hpp | 143 +- src/Homie/Boot/BootOta.cpp | 85 -- src/Homie/Boot/BootOta.hpp | 22 - src/Homie/Boot/BootStandalone.cpp | 22 + src/Homie/Boot/BootStandalone.hpp | 17 + src/Homie/Config.cpp | 479 ++++--- src/Homie/Config.hpp | 53 +- src/Homie/Constants.hpp | 40 +- src/Homie/Datatypes/Callbacks.hpp | 17 +- src/Homie/Datatypes/ConfigStruct.hpp | 60 +- src/Homie/Datatypes/Interface.cpp | 27 + src/Homie/Datatypes/Interface.hpp | 127 +- src/Homie/Datatypes/Subscription.hpp | 11 - src/Homie/ExponentialBackoffTimer.cpp | 42 + src/Homie/ExponentialBackoffTimer.hpp | 22 + src/Homie/Helpers.cpp | 287 ---- src/Homie/Helpers.hpp | 36 - src/Homie/Limits.hpp | 68 +- src/Homie/Logger.cpp | 22 +- src/Homie/Logger.hpp | 36 +- src/Homie/MqttClient.cpp | 120 -- src/Homie/MqttClient.hpp | 44 - src/Homie/Strings.hpp | 5 +- src/Homie/Timer.cpp | 43 +- src/Homie/Timer.hpp | 31 +- src/Homie/Uptime.cpp | 15 +- src/Homie/Uptime.hpp | 20 +- src/Homie/Utils/DeviceId.cpp | 15 + src/Homie/Utils/DeviceId.hpp | 18 + src/Homie/Utils/Helpers.cpp | 84 ++ src/Homie/Utils/Helpers.hpp | 21 + src/Homie/Utils/ResetHandler.cpp | 51 + src/Homie/Utils/ResetHandler.hpp | 25 + src/Homie/Utils/Validation.cpp | 389 ++++++ src/Homie/Utils/Validation.hpp | 27 + src/HomieBootMode.hpp | 8 + src/HomieEvent.hpp | 44 +- src/HomieNode.cpp | 75 +- src/HomieNode.hpp | 111 +- src/HomieRange.hpp | 6 + src/HomieSetting.cpp | 106 ++ src/HomieSetting.hpp | 66 + src/SendingPromise.cpp | 107 ++ src/SendingPromise.hpp | 40 + src/StreamingOperator.hpp | 10 + 131 files changed, 6303 insertions(+), 2968 deletions(-) create mode 100644 .circleci/assets/circleci.ignore.yml create mode 100644 .circleci/assets/configurator_v1.html create mode 100644 .circleci/assets/docs_index_template.html create mode 100644 .circleci/assets/generate_docs.py create mode 100644 .circleci/assets/id_rsa.enc create mode 100644 .circleci/assets/mkdocs.default.yml create mode 100644 .circleci/config.yml create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 Makefile create mode 100644 banner.png create mode 100644 data/homie/README.md rename example.config.json => data/homie/example.config.json (93%) delete mode 100644 docs/1.-What-is-it.md delete mode 100644 docs/2.-Getting-started.md delete mode 100644 docs/3.-Advanced-usage.md delete mode 100644 docs/4.-OTA.md delete mode 100644 docs/5.-JSON-configuration-file.md delete mode 100644 docs/6.-Configuration-API.md delete mode 100644 docs/7.-API-reference.md delete mode 100644 docs/8.-Limitations-and-known-issues.md create mode 100644 docs/advanced-usage/branding.md create mode 100644 docs/advanced-usage/broadcast.md create mode 100644 docs/advanced-usage/built-in-led.md create mode 100644 docs/advanced-usage/custom-settings.md create mode 100644 docs/advanced-usage/deep-sleep.md create mode 100644 docs/advanced-usage/events.md create mode 100644 docs/advanced-usage/input-handlers.md create mode 100644 docs/advanced-usage/logging.md create mode 100644 docs/advanced-usage/magic-bytes.md create mode 100644 docs/advanced-usage/miscellaneous.md create mode 100644 docs/advanced-usage/range-properties.md create mode 100644 docs/advanced-usage/resetting.md create mode 100644 docs/advanced-usage/standalone-mode.md create mode 100644 docs/advanced-usage/streaming-operator.md create mode 100644 docs/advanced-usage/ui-bundle.md create mode 100644 docs/assets/github.png create mode 100644 docs/assets/logo.png create mode 100644 docs/assets/youtube.png create mode 100644 docs/configuration/http-json-api.md create mode 100644 docs/configuration/json-configuration-file.md create mode 100644 docs/others/community-projects.md create mode 100644 docs/others/cpp-api-reference.md create mode 100644 docs/others/homie-implementation-specifics.md create mode 100644 docs/others/limitations-and-known-issues.md create mode 100644 docs/others/ota-configuration-updates.md rename docs/{9.-Troubleshooting.md => others/troubleshooting.md} (81%) create mode 100644 docs/others/upgrade-guide-from-v1-to-v2.md create mode 100644 docs/quickstart/getting-started.md create mode 100644 docs/quickstart/what-is-it.md create mode 100644 examples/Broadcast/Broadcast.ino create mode 100644 examples/CustomSettings/CustomSettings.ino create mode 100644 examples/GlobalInputHandler/GlobalInputHandler.ino create mode 100644 examples/IteadSonoffButton/IteadSonoffButton.ino create mode 100644 examples/SonoffDualShutters/SonoffDualShutters.ino delete mode 100644 homie-esp8266.jpg create mode 100644 mkdocs.yml create mode 100644 scripts/firmware_parser/README.md create mode 100644 scripts/firmware_parser/firmware_parser.py create mode 100644 scripts/ota_updater/README.md create mode 100644 scripts/ota_updater/ota_updater.py create mode 100644 scripts/ota_updater/requirements.txt delete mode 100644 src/Homie/Boot/BootOta.cpp delete mode 100644 src/Homie/Boot/BootOta.hpp create mode 100644 src/Homie/Boot/BootStandalone.cpp create mode 100644 src/Homie/Boot/BootStandalone.hpp create mode 100644 src/Homie/Datatypes/Interface.cpp delete mode 100644 src/Homie/Datatypes/Subscription.hpp create mode 100644 src/Homie/ExponentialBackoffTimer.cpp create mode 100644 src/Homie/ExponentialBackoffTimer.hpp delete mode 100644 src/Homie/Helpers.cpp delete mode 100644 src/Homie/Helpers.hpp delete mode 100644 src/Homie/MqttClient.cpp delete mode 100644 src/Homie/MqttClient.hpp create mode 100644 src/Homie/Utils/DeviceId.cpp create mode 100644 src/Homie/Utils/DeviceId.hpp create mode 100644 src/Homie/Utils/Helpers.cpp create mode 100644 src/Homie/Utils/Helpers.hpp create mode 100644 src/Homie/Utils/ResetHandler.cpp create mode 100644 src/Homie/Utils/ResetHandler.hpp create mode 100644 src/Homie/Utils/Validation.cpp create mode 100644 src/Homie/Utils/Validation.hpp create mode 100644 src/HomieBootMode.hpp create mode 100644 src/HomieRange.hpp create mode 100644 src/HomieSetting.cpp create mode 100644 src/HomieSetting.hpp create mode 100644 src/SendingPromise.cpp create mode 100644 src/SendingPromise.hpp create mode 100644 src/StreamingOperator.hpp diff --git a/.circleci/assets/circleci.ignore.yml b/.circleci/assets/circleci.ignore.yml new file mode 100644 index 00000000..59c18e15 --- /dev/null +++ b/.circleci/assets/circleci.ignore.yml @@ -0,0 +1,11 @@ +version: 2 +jobs: + build: + working_directory: ~/code + docker: + - image: circleci/python:2.7 + branches: + ignore: + - gh-pages + steps: + - checkout diff --git a/.circleci/assets/configurator_v1.html b/.circleci/assets/configurator_v1.html new file mode 100644 index 00000000..cd634062 --- /dev/null +++ b/.circleci/assets/configurator_v1.html @@ -0,0 +1,33 @@ + + + + + + + + Set up your Homie for ESP8266 device + + + + + + + + + + +
+
+ + + + diff --git a/.circleci/assets/docs_index_template.html b/.circleci/assets/docs_index_template.html new file mode 100644 index 00000000..6c874f08 --- /dev/null +++ b/.circleci/assets/docs_index_template.html @@ -0,0 +1,75 @@ + + + + Homie for ESP8266 docs + + + + + + + +
+

Homie for ESP8266 docs

+ +

Configurators

+ + $configurators_html + +

Documentation

+ + $documentation_html +
+ + diff --git a/.circleci/assets/generate_docs.py b/.circleci/assets/generate_docs.py new file mode 100644 index 00000000..ecfc2fe4 --- /dev/null +++ b/.circleci/assets/generate_docs.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import json +import urllib +import urllib2 +import tempfile +import zipfile +import glob +import subprocess +import getopt +import sys +import shutil +import os +import string + +FIRST_RELEASE_ID=3084382 +DOCS_PATH = 'docs' +DOCS_BRANCHES = [ + { 'tag': 'develop', 'description': 'develop branch (development)', 'path': 'develop' }, + { 'tag': 'master', 'description': 'master branch (stable)', 'path': 'stable' } +] +CONFIGURATORS_PATH = 'configurators' +CONFIGURATORS_VERSIONS = [ + { 'title': 'v2', 'description': 'For Homie v2.x.x', 'path': 'v2', 'url': 'https://github.com/marvinroger/homie-esp8266-setup/raw/gh-pages/ui_bundle.html' }, + { 'title': 'v1', 'description': 'For Homie v1.x.x', 'path': 'v1', 'file': '/configurator_v1.html' } +] + +current_dir = os.path.dirname(__file__) +output_dir = getopt.getopt(sys.argv[1:], 'o:')[0][0][1] +github_releases = json.load(urllib2.urlopen('https://api.github.com/repos/marvinroger/homie-esp8266/releases')) + +def generate_docs(data): + print('Generating docs for ' + data['tag'] + ' (' + data['description'] + ') at /' + data['path'] + '...') + zip_url = 'https://github.com/marvinroger/homie-esp8266/archive/' + data['tag'] + '.zip' + zip_path = tempfile.mkstemp()[1] + urllib.urlretrieve(zip_url, zip_path) + + zip_file = zipfile.ZipFile(zip_path, 'r') + unzip_path = tempfile.mkdtemp() + zip_file.extractall(unzip_path) + src_path = glob.glob(unzip_path + '/*')[0] + + if not os.path.isfile(src_path + '/mkdocs.yml'): shutil.copy(current_dir + '/mkdocs.default.yml', src_path + '/mkdocs.yml') + + subprocess.call(['mkdocs', 'build'], cwd=src_path) + shutil.copytree(src_path + '/site', output_dir + '/' + DOCS_PATH + '/' + data['path']) + print('Done.') + +def generate_configurators(data): + print('Generating configurator for ' + data['title'] + ' (' + data['description'] + ') at /' + data['path'] + '...') + file_path = None + if 'file' in data: + file_path = current_dir + data['file'] + else: # url + file_path = tempfile.mkstemp()[1] + urllib.urlretrieve(data['url'], file_path) + + prefix_output = output_dir + '/' + CONFIGURATORS_PATH + '/' + data['path'] + try: + os.makedirs(prefix_output) + except: + pass + + shutil.copy(file_path, prefix_output + '/index.html') + + print('Done.') + +shutil.rmtree(output_dir, ignore_errors=True) + +# Generate docs + +generated_docs = [] + +# Generate docs for branches + +for branch in DOCS_BRANCHES: + generated_docs.append(branch) + generate_docs(branch) + +# Generate docs for releases + +for release in github_releases: + if (release['id'] < FIRST_RELEASE_ID): continue + + tag_name = release['tag_name'] + version = tag_name[1:] + description = 'release ' + version + + data = { + 'tag': tag_name, + 'description': description, + 'path': version + } + + generated_docs.append(data) + generate_docs(data) + +# Generate documentation html + +documentation_html = '' + +# Generate configurators + +generated_configurators = [] + +for version in CONFIGURATORS_VERSIONS: + generated_configurators.append(version) + generate_configurators(version) + +# Generate configurators html + +configurators_html = '' + +# Generate index + +docs_index_template_file = open(current_dir + '/docs_index_template.html') +docs_index_template_html = docs_index_template_file.read() +docs_index_template = string.Template(docs_index_template_html) +docs_index = docs_index_template.substitute(documentation_html=documentation_html, configurators_html=configurators_html) + +docs_index_file = open(output_dir + '/index.html', 'w') +docs_index_file.write(docs_index) +docs_index_file.close() diff --git a/.circleci/assets/id_rsa.enc b/.circleci/assets/id_rsa.enc new file mode 100644 index 0000000000000000000000000000000000000000..c8b23381e7ef4f4a7ee738260702a6574f466b6c GIT binary patch literal 3264 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 literal 0 HcmV?d00001 diff --git a/.circleci/assets/mkdocs.default.yml b/.circleci/assets/mkdocs.default.yml new file mode 100644 index 00000000..2a31cff2 --- /dev/null +++ b/.circleci/assets/mkdocs.default.yml @@ -0,0 +1,29 @@ +site_name: Homie for ESP8266 +repo_name: 'marvinroger/homie-esp8266' +repo_url: 'https://github.com/marvinroger/homie-esp8266' + +theme: + name: material + palette: + primary: red + accent: red + +markdown_extensions: + - meta + - footnotes + - codehilite + - admonition + - toc(permalink=true) + - pymdownx.arithmatex + - pymdownx.betterem(smart_enable=all) + - pymdownx.caret + - pymdownx.critic + - pymdownx.emoji: + emoji_generator: !!python/name:pymdownx.emoji.to_svg + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences + - pymdownx.tasklist(custom_checkbox=true) + - pymdownx.tilde diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..8fee20fc --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,101 @@ +version: 2 +jobs: + build: + working_directory: ~/code + docker: + - image: circleci/python:2.7 + steps: + - checkout + - run: + name: install PlatformIO + command: sudo pip install -U https://github.com/platformio/platformio-core/archive/develop.zip + - run: + name: install current code as a PlatformIO library with all dependencies + command: platformio lib -g install file://. + - run: + name: install staging version of Arduino Core for ESP8266 + command: platformio platform install https://github.com/platformio/platform-espressif8266.git#feature/stage + - run: + name: install exemples dependencies + command: platformio lib -g install Shutters@2.1.1 SonoffDual@1.1.0 + - run: platformio ci ./examples/CustomSettings --board=esp01 --board=nodemcuv2 + - run: platformio ci ./examples/DoorSensor --board=esp01 --board=nodemcuv2 + - run: platformio ci ./examples/HookToEvents --board=esp01 --board=nodemcuv2 + - run: platformio ci ./examples/IteadSonoff --board=esp01 --board=nodemcuv2 + - run: platformio ci ./examples/LightOnOff --board=esp01 --board=nodemcuv2 + - run: platformio ci ./examples/TemperatureSensor --board=esp01 --board=nodemcuv2 + - run: platformio ci ./examples/LedStrip --board=esp01 --board=nodemcuv2 + - run: platformio ci ./examples/Broadcast --board=esp01 --board=nodemcuv2 + - run: platformio ci ./examples/GlobalInputHandler --board=esp01 --board=nodemcuv2 + - run: platformio ci ./examples/SonoffDualShutters --board=esp01 --board=nodemcuv2 + + lint: + working_directory: ~/code + docker: + - image: circleci/python:2.7 + steps: + - checkout + - run: + name: install cpplint + command: sudo pip install cpplint + - run: make cpplint + + generate_docs: + working_directory: ~/code + docker: + - image: circleci/python:2.7 + steps: + - checkout + - run: + name: install dependencies + command: sudo pip install mkdocs==0.17.2 mkdocs-material==2.2.0 pygments==2.2.0 pymdown-extensions==4.5.1 + - run: + name: generate and publish docs + command: | + if [ -z ${PRIVATE_KEY_ENCRYPT_KEY+x} ] + then + echo "Fork detected. Ignoring..." + exit 0 + fi + + openssl aes-256-cbc -d -in ./.circleci/assets/id_rsa.enc -k "${PRIVATE_KEY_ENCRYPT_KEY}" >> /tmp/deploy_rsa + eval "$(ssh-agent -s)" + chmod 600 /tmp/deploy_rsa + ssh-add /tmp/deploy_rsa + + chmod +x ./.circleci/assets/generate_docs.py + ./.circleci/assets/generate_docs.py -o /tmp/site + + # make sure we ignore the gh-pages branch + mkdir /tmp/site/.circleci + cp ./.circleci/assets/circleci.ignore.yml /tmp/site/.circleci/config.yml + + pushd /tmp/site + git init + git config --global user.name "circleci" + git config --global user.email "sayhi@circleci.com" + git remote add origin git@github.com:marvinroger/homie-esp8266.git + git add . + git commit -m ":package: Result of CircleCI build ${CIRCLE_BUILD_URL}" + git push -f origin master:gh-pages + popd + +workflows: + version: 2 + lint_build_generatedocs: + jobs: + - lint: + filters: + branches: + ignore: + - gh-pages + - build: + filters: + branches: + ignore: + - gh-pages + - generate_docs: + filters: + branches: + ignore: + - gh-pages diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..3dccc6fc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[keywords.txt] +indent_style = tab diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..c23dcb1c --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,9 @@ +The issue tracker is a great place to ask for enhancements and to report bugs. +If you have some questions or if you need help, some people might help you on the [Gitter room](https://gitter.im/homie-iot/ESP8266). + +Before submitting your issue, make sure: + +- [ ] You've read the documentation for *your* release (in the `docs/` folder or at http://marvinroger.github.io/homie-esp8266/) which contains some answsers to the most common problems (notably the `Limitations and know issues` and `Troubleshooting` pages) +- [ ] You're using the examples bundled in *your* release, which are in the `examples/` folder of the `.zip` of the release you're using. Examples might not be backward-compatible + +Thanks! diff --git a/.gitignore b/.gitignore index 2af98cab..ebb6f739 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -/config.json +# Output of mkdocs +/site/ + +/config.json +*.filters +*.vcxitems diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..c8c1cc9b --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +cpplint: + cpplint --repository=. --recursive --filter=-whitespace/line_length,-legal/copyright,-runtime/printf,-build/include,-build/namespace,-runtime/int,-whitespace/comments,-runtime/threadsafe_fn ./src +.PHONY: cpplint diff --git a/README.md b/README.md index 8284202e..a4299de2 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,70 @@ +![homie-esp8266 banner](banner.png) + Homie for ESP8266 ================= -![homie-esp8266](homie-esp8266.jpg) +[![Build Status](https://img.shields.io/circleci/project/github/marvinroger/homie-esp8266/develop.svg?style=flat-square)](https://circleci.com/gh/marvinroger/homie-esp8266) [![Latest Release](https://img.shields.io/badge/release-v2.0.0-yellow.svg?style=flat-square)](https://github.com/marvinroger/homie-esp8266/releases) [![Gitter](https://img.shields.io/gitter/room/Homie/ESP8266.svg?style=flat-square)](https://gitter.im/homie-iot/ESP8266) + +An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. + +Currently Homie for ESP8266 implements [Homie 2.0.1](https://github.com/marvinroger/homie/releases/tag/v2.0.1) + +## Note for v1.x users + +The old configurator is not available online anymore. You can download it [here](https://github.com/marvinroger/homie-esp8266/releases/download/v1.5.0/homie-esp8266-v1-setup.zip). + +## Download + +The Git repository contains the development version of Homie for ESP8266. Stable releases are available [on the releases page](https://github.com/marvinroger/homie-esp8266/releases). + +## Using with PlatformIO + +[PlatformIO](http://platformio.org) is an open source ecosystem for IoT development with cross platform build system, library manager and full support for Espressif ESP8266 development. It works on the popular host OS: Mac OS X, Windows, Linux 32/64, Linux ARM (like Raspberry Pi, BeagleBone, CubieBoard). + +1. Install [PlatformIO IDE](http://platformio.org/platformio-ide) +2. Create new project using "PlatformIO Home > New Project" +3. Open [Project Configuration File `platformio.ini`](http://docs.platformio.org/page/projectconf.html) + +### Stable version + +4. Add "Homie" to project using `platformio.ini` and [lib_deps](http://docs.platformio.org/page/projectconf/section_env_library.html#lib-deps) option: +```ini +[env:myboard] +platform = espressif8266 +board = ... +framework = arduino +lib_deps = Homie +``` + +### Development version + +4. Update dev/platform to staging version: + - [Instruction for Espressif 8266](http://docs.platformio.org/en/latest/platforms/espressif8266.html#using-arduino-framework-with-staging-version) +5. Add development version of "Homie" to project using `platformio.ini` and [lib_deps](http://docs.platformio.org/page/projectconf/section_env_library.html#lib-deps) option: +```ini +[env:myboard] +platform = ... +board = ... +framework = arduino + +; the latest development branch +lib_deps = https://github.com/marvinroger/homie-esp8266.git + +; or tagged version +lib_deps = https://github.com/marvinroger/homie-esp8266.git#v2.0.0-beta.2 +``` -An Arduino for ESP8266 implementation of [Homie](https://git.io/homieiot), an MQTT convention for the IoT. +----- +Happy coding with PlatformIO! ## Features * Automatic connection/reconnection to Wi-Fi/MQTT -* [Cute JSON configuration file](docs/5.-JSON-configuration-file.md) to configure the credentials of the device -* [Cute API / Web UI / App](docs/6.-Configuration-API.md) to remotely send the configuration to the device and get information about it -* [OTA support](docs/4.-OTA.md) +* [JSON configuration file](http://marvinroger.github.io/homie-esp8266/docs/develop/configuration/json-configuration-file) to configure the device +* [Cute HTTP API / Web UI / App](http://marvinroger.github.io/homie-esp8266/docs/develop/configuration/http-json-api) to remotely send the configuration to the device and get information about it +* [Custom settings](http://marvinroger.github.io/homie-esp8266/docs/develop/advanced-usage/custom-settings) +* [OTA over MQTT](http://marvinroger.github.io/homie-esp8266/docs/develop/others/ota-configuration-updates) +* [Magic bytes](http://marvinroger.github.io/homie-esp8266/docs/develop/advanced-usage/magic-bytes) * Available in the [PlatformIO registry](http://platformio.org/#!/lib/show/555/Homie) * Pretty straightforward sketches, a simple light for example: @@ -21,29 +75,27 @@ const int PIN_RELAY = 5; HomieNode lightNode("light", "switch"); -bool lightOnHandler(String value) { - if (value == "true") { - digitalWrite(PIN_RELAY, HIGH); - Homie.setNodeProperty(lightNode, "on", "true"); // Update the state of the light - Serial.println("Light is on"); - } else if (value == "false") { - digitalWrite(PIN_RELAY, LOW); - Homie.setNodeProperty(lightNode, "on", "false"); - Serial.println("Light is off"); - } else { - return false; - } +bool lightOnHandler(const HomieRange& range, const String& value) { + if (value != "true" && value != "false") return false; + + bool on = (value == "true"); + digitalWrite(PIN_RELAY, on ? HIGH : LOW); + lightNode.setProperty("on").send(value); + Homie.getLogger() << "Light is " << (on ? "on" : "off") << endl; return true; } void setup() { + Serial.begin(115200); + Serial << endl << endl; pinMode(PIN_RELAY, OUTPUT); digitalWrite(PIN_RELAY, LOW); - Homie.setFirmware("awesome-relay", "1.0.0"); - lightNode.subscribe("on", lightOnHandler); - Homie.registerNode(lightNode); + Homie_setFirmware("awesome-relay", "1.0.0"); + + lightNode.advertise("on").settable(lightOnHandler); + Homie.setup(); } @@ -54,4 +106,10 @@ void loop() { ## Requirements, installation and usage -The project is documented on the [/docs folder](docs), with a *Getting started* guide and every piece of informations you will need. +The project is documented on http://marvinroger.github.io/homie-esp8266/ with a *Getting started* guide and every piece of information you will need. + +## Donate + +I am a student and maintaining Homie for ESP8266 takes time. **I am not in need and I will continue to maintain this project as much as I can even without donations**. Consider this as a way to tip the project if you like it. :wink: + +[![Donate button](https://www.paypal.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JSGTYJPMNRC74) diff --git a/banner.png b/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..caf80ccc87a4b0d6732d71b55ff0c888b9811dbe GIT binary patch literal 13044 zcmd^mWm_E0*6tv|T@u`#L4#|s!3nOx8Qk3o!QCN1aCf)hZXvikf#4QA*gLSFz2EcU z?DGdsUtG}DT}x}Ns=HP-VTuZps7M4z00010T1s3Q0D$R&+?OK2Kz^&m%ppMj;5$ob zI;+^3I=dM~3QZ zp#}g1gx&28jjh1WWJX{!3tK^o)7DN3G7A$y3Jp$qR(X3du(^elrz2R!Q$f|()5@6F zghE(|Ou(HF!oUXXY)IyAV{Pli=PpR`%9jsv|MHlHg6y@6vy~vlzm(FHS0od&a|Dxd zGIKE*v%P00L3%JbdDuD|x-;22QU05QIM~V9 z(Zb%@!p@fLg`=U7or|*|1w_+-nP6l8AGWqm|F{X_Fcx=1dloik))$lh-BDit|J~Kb z=0ClioRz`}2QSXbc%RQ_2@n_IzTFU_)m+M^!sJ>wkMu(cI42 z&dJ=)o=i;a#cQ0*oMdzw7PcmKZcg;CRPyqC(zZ^{hPK9FX>mac2p?t(3llz8kO&W} zC>Jk}gcvIun*Lx~3s(jg544wB==GcI5=D ztt_9PpS3@V?rTav?C%l~5U8psdwY4M`rA;G5i84s1o`=zBAHG@yzA=f7n@RBTicIa zEFvNzc6)RGF3$Dz^|Q0HCnhHD?ZHqG;|BzMG0@dqAN~p)9{C|IF8&nGP!Q&vmzQ^VbQEks+tJl?d3h=F;R7EhJ3Tcy z2m}guHs09SSQ?WQ5tZmGjNstlU}s{;@n$Ry0A9B>Qjro#d=ONZARixxyB+C|_pHPsV*ia>gwX+@`?JU zr>!E^lbnP|T}t@J&j|;6J27t3iK*#7i(!_k)PBCcF?RGnE91!s@k#>Ncv*>?>+9~O z$HhUC*AtbI4&3js(78C?kC(?vg2eAP*G$z6z1&^fJ38B9nJ7sJl@;ES61)XTN=isb z5WOR205VC6iw?O^J#}MiN-CqFpixYdFe_{%L_t$2vSXLF ziU)3w)nR?Nk`iZgHBu|N-4(7OM}p~dZ{Q#M^3B~%?E5C`K1`MAsn1_yF`IWcN4zz= z4;}GWl_&T6GP{C84=1Nj8RvG{epQ}MrrPT2>PtV&HkBXlXHlAApB1joe_ZBs_09r5 zi%iS5)=fz!Uh#aw0N`=>Tv}@XUDZCL`Gf;$30`)6yA+=NvAfX&=*ljs5=@^SoU?7_ z0Kc&#wV1D8Q;9c4<8Y}$OFXgqr0vJp*m4^{l)&92D1Hi&SIF^q2nQp6r zK*6kR?!R+uJW~w{A^QF0y6WmPJaGzLkKASbK5iO`OK&FPhXql!7VUNxMTAFYEt1LY zS=x@R*#Kg-8s$bR!rE92vdbRuD2UYSWKrT~u4WSpx+~l6Bla~}dkd|x(qChQt;3;<6L+x;wEDcK$vk<%nA!A*XX$jXAcr#;tyf}KfSF+W`G=+4Na3apv{wp>E(?78 zs*|5X?gGTGEYUaK;Nz2?-o0mnXb3Sa7ceee(GT^)&H|oQFN)IG)N|?gn8iQZ*!}EE zKs>hx;>jRFyhq&C`kq~{-`^fi2)YYmpeGa%7dyF zm)>d;&qn)|Q#e34Gw!@=5Wh!8Gi);PJ%kPHXGkzQL-DLdUFNb@2@46k)4Zx2UrV!+ z2<5f1>J`Z8a(3Rr0l3VOUdN>74g-(f87nbFA9%E!cvPVh^wR_n48s{ zz7IN_>vD(~}RYh}C5WIM1 zJ@;*-FtPjA>`{C|^2Wd7ES>hNeDj77U=1||0|4jaK+Bnu@mT0K12F|1LNofE^dmCVi>GTH&c1Xatn;FG6~I6U zMPiEN3DzN5So@NNxvL4i+{l*W5E~fwTZRLB-U|mKzocFATHCs!I}~i^%bVaHtsCqI zA+&c$5Rb^)1Xof*A~)*ls4u2uct8Ys5Bz2)-|#p2(~b z-5j&mg(R#qYS69#^1C+>E_sUXmwO{`A~wE2CYSw!!T22eIb(Y^NT>Sg!Pa;A;DWJ5 z)L%4E1{esifPfEV06bV|s4g-L01C7};Gc&8Rs>k+&m#Xk+}O{q`e>zU+qAqFHd?GE z1qVmf4>FeMH2E;4Z^&_tmW>iLcN`}(LBD3KN{&6;6&kjTCw_`eGn9PO9Hb2A88zVD zy)D&~FRU~Y8aA^xKm)Uo&uSb0mwwO8NI+!r&@_RT{-*scb8suf_<{4zuj^Cb zkg85b+^^(n?LMAPY8AtQslY0vRPRuMIxSL5P=4$?Xj^ME0CTS|P#w#>_EXH%!@=oql28DI#{x!%>zep6L}c_MjghpPCm| zNZJf+^}pA*Baw&7i~emMmZ)Pf#QMXT{v)SAmL)JY3_q2OAB`KlZ65fI)9sGYC2%Hfb=~!$I|T*f4xs-%w!Ew9K%abq)aFcl=fC zWjqiTn^fUJhP6e}w&TuV#pF7iHkLBR4^zl^faZ60I0%4!O7mQ^9e@nU#$Qqrgc>x{ z&wE7YU24T!k*iiMLi_$o$MfD$cM6H-gET%oe{luuTZd_He`2^+`N*bpVD^~7(netq zwNuQ!K^JI6O2GjAPhjbGJ$+{YoeA)SerMhso zu6A<$mX^79G5!vHR^MYIoyF*;W)IUHCM*Zos>f$Ez#v0*+pJP7>j1h-iEh?#|NFmN zY^+Zh3fXvg@XeyEkyt$O1O1zL3mQ5>#s-N6+$l}8Mh4*0rH7W@JE0XiNSbnWflDrF z=~hgLgR}P==H*1(7%8qew|#Y1X}(Qb+q-X>2_RyxAQ5|`QePq_kg@XyV0FJ|2+849 zk}1Yd5mj&uBpSJ^%Ao+h$nV9^qRC^zG-dJIq&E$pReD zE(eW7-D4ApBKm}-E_EhlsY?OC1XjZX6&fYy z$n^U8If?{v`nSOivs-~Msh2r{40ZFl~Otz^PSLIWvx87vIWDUMqAurjErw!E0?@TBlDlD*>OX$ zoejIAeZ1_+;D}qdJF%cWO1uo_8G2pMAXklS)Sohi8F zAwV0w@}7^u8pJ(}WpaoKKO2771sd79#sFF{*7HQ+7PHHHsaB96&3;3ynB1S-WN_|S z4x`kM@5y@miE+oZ5vk!yy$CyKIDaQ}%#foQU)7?CM_`XoxidJF^kw~a_5(}@uKsiY zcE<*`hxXDZ|2mSF69}k(H_0JcE${mCC&mKKVk}BGr^6>D%F!(O#g$ufuF_xrXf% z^YilD2R9_8k%m(=CjA1N<+#IW6!;$4}w@OQ!<-}dSMw0a8mBo234z|Xj zuxaS#a+ukqAT;Wnsh?Y>hg1{!1=>f|YNOvv!LvHxS=;E>WT1koTsa@Tsv0k|yX$Ye z;x}u#esr8&%UBxG;-4R1-mB8T?Jm?mu_wOYDGYhb#~{4WV7#f9;<}s6-}%Ej6~iEz zb14`hCLtgHdZa?>(T6Ma76Y-F&rSmPFBtFaU@#{4V>>0>r?Jzl+sSa4C-e4{~ z`r}#RcOS`D6Y)4KT?Io-C6l|$qbN$E{mlm4ySs-t<(NwZmMeE8T)8;x{jJ9` z&h|KL__wl9FW4Oav(-JSqu=P?E9Ox#g*&mk$`$^~wswii5F{PboRQ1qiy3&n`NFHp z6)mP8@F(%@sg-SNNq6Xrx?UGFa9>3LO~m~GZep!Q>(n9T{JVhq=o^MZf>H4hG+f!y z(iEcHqi^b4oH93l(@Rq`Mp_-DFN25;ebl-9Jd)&e_&}Y+?|O1;py#uESarG(R64cQ1s zZMzCXJj(pbx`vEE)w3XNJsCXCAaB%u`vJ?KbbYO9^(y{#EG61)x7ZO#-s?oO;%|oDXK`?@GQvUsCwl-4+wI-`jM1-O7ug z?Cu-!oT`pXC%PT9FDNBm+p)yOoXIYnrNy5FaPciRiN{ZA-`^=yh5#zHOo0^U$>QkI zhDWY&=nbZ4I%zc?NmQM+^~Y_M9r72@?k@xF4=onsJKt&JjH_M0a>x>Q;|vddV6VBF zb3#|`S_b3)fhV#S`oPv|d@@fG+33yQRnr@`heBEmb@&-#F|qGBT}0FbKZbcT6IMUi zmw^(^=Jvgc~DRFH4(1D&!HwHx>>yN>0-|`$5 zi}Zc(c3Q+J0SU7()Zf2{Q*37cjlm8lP*Ys5>C)i$IMd%N(1nRQ5|xG{0fcd$3o22pBWLuY3_NLhtX(bCv}jvC^? zE*L*IqR+j=o1+@yQBks$>lj()L}&)^L@F6PMW1xtnECG+Cr_(fuGjgRj)H;wgZ10) z-NSok7uODyZ1z!ZjPd2crVA#x+Ml#@t%g`AKNiaozNlIoRqD$sUxYcTIn z@bRwTs%h!U?GF|ktA?MdhA>sJo?HEA8I)~B*G*7)ZY#)bfgow96@57}dPWXmKAnSs$`!ZZq+9{j~|A&H~)#O~*6qVez_^Y%W=JSlJp=Q3$#TI`K?;@N-!jE4JD11vX=+Gar1#_ps^z}>fe{Z4^#ITQt-uV$x0L_ccWf_=RfE^Z0=s-o+5%u~a`Xh88NeTTdtP;G^yZ z{$|23Y0r(~eA#KzIFNf7OWIi%$}|T3BpnPt!xw&uZC(R?QnPJ;sw2C97WgbtmNUIR#RyJsMIDW8_a@y6)Vtjuq zT4=ypTy^D}zmJ$TvZ|39?q8wlzAfoShM~~c^&_W}Yqemj5T{FGiczxi%q2XSa!uK} zd}1iD-;K4)Nu%-+`xPTZGF-YHhQ#;cg^{12@K3_{!a~HOn*nY!C_rdl5C5ggP=h^_h!w?v;CY!7iMaXRxrYaGJw1Emawkmoa5sH8n|hFWZErCQ90{M)5? ztY_UAu8hW{*CLEhna)!{7VTAQiiUTy&prB@r==H`fs4QPI3o%%n+x>DiS3Fojt1~t zoMrXk!;JR6rEJ%*ySU!Rgpp+fpz?++*1Kf=Iwt{p}TsYAHO3ZBa^(;hM+fWmJ>?7 zk!tv%?a2uCV=g1^DUBpj#B+LJ%PPz$rb4C3m83kW+Xd|Qb_?3-^|!DY*CvGoSmrYU z%|^op60+wTDNBW0I$6KpQNuYpZ!JpQNw|4zU%XB{L0N?~9=P0SW6qH+ADtMicFEnwaOFP?W3oqsUaNZHy;R!erb z!u!)Tnv6q%?v3WLLD+-^_{D^;*&{*~0#YexM_4Mu(-nTQ;&*Atmoy5*d+}N4%u|Yn zK-$a=sgK2SDEvuiI6r?oRu%VF`^q}m0JEkpnR+HBn=-(A-;GUr*_D*ssP2a|$%SOk{rCFS|?EYb?DzB#k!D*L}mp zmWm|svIgRGtrs1XG$U9_2{FKdYAV!-QHn*`l0;^Un7fee@Z9U_xl2{f3;l~*G^jZ> zdk^%2N;_}z&DMG%!sz{hJFY5`y}t{kzRAduQ&EOvWyt9yS_TPi+3laIhW0T(ed_$< zp06-ln6T6Z1DPPCp&s@cM^y2SNZ(ZRSt7{ZU$sxhim$Jp9uju_%7H1hS7q86Bi^je z;U_?&3{6*?i;n&f!sX^;YI%jq|EHd6O)IRPmRm{sC&*Gpfk&LpURraG?yWR9<*<_D znrlegXaWfp5I}S2?g*iy@weSd(jLeNQIav}hB2bm<%=9jLA_RR=!`X0a`9Pxf1h`!6kGPjeMRk) zkX^T080HHCzF`rFrKF#ib%R;Kd@q$=&>kH9^~4frz|?Gs8p7`hUF^7u8Je4-Sq{VE zW5)CWjfaS*yY=mL?o_BP{es+&HPZ#>+k5_&k>9~N7soBDN z242X^TVy7N8WAjkG-HH_{J7BO9Py&_4=9kim1Z)O`gxTy<^e@`^4H`f4DXEKW7GWD z2fp%?=Z8C&tnx*t`{~cj%Dcw|5A|%O#VYvBx(C}}4M&QU0(I3Ol6??`R7zQtW$oj( zm+;>${`I@9)91nEp29BcjzQm!iGh59v(KGO1PAmj1of^ndTOYWsJ?h%Zy=HvL$6kG z*NG>K{fQ3pbPt6z0_Dt9<50ccW(j&-Umi!!61U}F)Hcgv?P1~HDkzE5#GCU~6+<`s z?Jq>j(Xjo{0$0af=hel+KS6tJq%~n*Tt*;#r*FCDm)@4#d1`FLYUwP<&mUV?|1t%c zD_;>dBKncv-MzTwM@pla4`^3P=krz-8pxHdeoqf8b&O^|Zq0}NKZ@7b+i;Xem&)f9 zFA+qg<*GO2aFw5+g;XR}igRP_wmC{0R{7m0=|As<2tRDPJ^i+!SqoJ&ug=Zh$Sy-CH zO!z}Cu|N=?n_?)S!vg{AlC}PeTdvj4f_KvzdLH{pDOj?t@8vQuV2VIS^k00peI)}Q z98d^qAehEP@Qe|mJ}|TupjW57jVqyEuua>>!&d;PwKKtcBK| zyXTKG?yHA&^lU|%;#~N3fK87mwP0N;w zvQ%(kCHhk=aRj0?0y2Nwi9Ku(y_+5ntNb22pP5VimejM(x`F;rAV8y+OjH4bgwSc+ z?-Wbi0i;YhkBxUPtJ~!+=hFu?A{hNcd`~-b`_+b+IpRwGrp?b=mYT%Jp}}gwnjF?@ zZ=m12naj+Bvic~= zYa0ss%Lz02^c3s-S)-(vxO9a$wWqR^s^Cs|{eCT`1AG46`d!ttG@g(&dsHpgYD!)D zUTdQ%p79e5^le_zZp@rao7^SxE8@-KFnT=M2_*5@p2Q`!ZH3?1tmeD67zmy%+hrDZ zWBSE4j?O^HcMlHM%&`0bRB`nnG|506dD3*d%;;N_3S7NT$XLf7uzxw(D6(62T<8Bm7D__MFAuJy&ReYwzd?@R*aRTb*pN~AE#uv7h&J7cf8~D9!(%HI_hHG#j z)hniw05?fYb*IxXhCYIXh7ZK*ZpHl2P-WUSYVdr}#KZ?D(Vq#bcnRs6-^;$c>1x!4 zV5%VDb9+(Tr80|Y`k2V0C~+KK^(q0?K)_r52 zaxuNMUAtDb4g;^kik$ZvtJqtMBpI-=@vkIM!C*AIj_?;k$Rs=fb=zG80vWac z8Tsh?_yg#5$-ja+aRu(p_vSUA_RVjUh@y@@?BksRA zv~uFJSQ^>#1kb`?LShqk{&3o(~e!jdOT9dlmL+wCp6`L#mz}#4(?J=^xK&_gCU#eiOTE6;RvLDkf ziv~=7J*D*~zU++GR;F#KMf3Hbcb!<8o>nn%8uW}NaUrS4y(BuWAqY{*6gwWUGXhyp z81v&D?(-5jwZ;^hQchmR`l%K?*if(~{?S91O9-6)GC#?n92&B16jbfYAx$4H`0zxy zs}rv6Tz2z%0!#6E$6aaBD=Vq8g0RVYMM=8K(|xoTx6+5ONuce(a@aG`t$gYZBYx5a zWyi)cVO<0+HEAiN$-8=9OF8R+w6v|9LIQfu+2(&z|Ha%1mf7f@EI_<(4=FE-!O z#CvgRs_|lXyh%D!9q>|sruaOlx||%9if-e)=vpzpoOGz&$>=oAY&JZ=bT71gIK}*9 zOxJy~;o$0rT2_P7#mUifL$8bVX8#p(gN@itbO~P!3BB8>J2kaigDCRs-tAWzg@q>a zlVV(gkGsMl4+tAH`rjS>VmJ=yvENv@XXmh@utm){&%+I90Z=d}ugs+-t~VfDs_ zA37THOE$Ax6?!|?AWD^vd)pw;lF=o}J1@dEK7Nf9l}{>~HoNV)RdAfM-q^=U<^+kH zuF;IGN^_c)lItqElngpt>C;1KsK3okmMDTJLQBz;*uv<03r;hI7$KiRyq+l`&b1nj zcRns(z>n;0ubH$JO@OBI$9`?boC!$dMKMVDMkN~~BR3S3PR&t-sOkX6b}hEq8@z++ zdu&w`uYWQp^6|=OYYtiA@Ot&L*0y)%TS4aZ*OLE>l+>N<_|vYI7B9tMgd3uySiG$S5Yvm-9_qN-;~JDtXNa! zL}2>s=irn?C1T-yL{ui+VySXP60vJp6R+haX1;N-kz+TZ(?$-#`ZcePCdn^YDu62`VLHb z%Et-G49@N-mGK+cKuf*+&9II?E0XY0zhFK+cT&%JV0YTBtU9b}(Eu5-QN(0Q4?K6D zh7L+U$5)x6GPj>k3xs8U1Nm-3&Qp-x;#56#tDqTGG*@E>1WvyyG{71E@X(vNJ%Zx#+^SxZYTfB5i)_FRfX@j z;M~27Ez|N@?y4ELjP1?YE*fq5%Lh{fszRH3sdD;GThwV@V>$Yd2dH)9^Lj$}VWcUC zaXQ|=hb6OsH+SASe+puL4j?*_vX_LX11k}?F_fb>_eeK*GZK0W!wPU+Eawp0!;};URH(kejVej<~zs%Llix}2sc&kSQTd= zz}{nz)b_0rUe>VpVVEPV`uh8im73wwT*4P+P0Yzn_soemr_C!kin(otK zM_PEy3a2lGX%ZsP8Y&>PEk9@xOlY^7l!(`_y zb9P8DZJMwMly_b-brvwrCTgIY^ae>REY{kjoslB*vKwJN_Cfs5t}= 5.0.8 for `ArduinoJson`, >= 2.0 for `Bounce2`, >= 2.5 for `PubSubClient`. - -### 1b. With [PlatformIO](http://platformio.org) - -In a terminal, run `platformio lib install 555`. - -Dependencies are installed automatically. - -## Bare minimum sketch - -```c++ -#include - -void setup() { - Homie.setup(); -} - -void loop() { - Homie.loop(); -} -``` - -This is the bare minimum needed for Homie for ESP8266 to work correctly. -If you upload this sketch, you will notice the LED of the ESP8266 will light on ![LED solid](assets/led_solid.gif). This is because you are in `configuration` mode. - -Homie for ESP8266 has 3 modes of operation: - -1. The `configuration` mode is the initial one. It spawns an AP and an HTTP webserver exposing a JSON API. To interact with it, you have to connect to the AP. Then, an HTTP client can get the list of available Wi-Fi networks, and send the credentials (like the Wi-Fi SSID, the Wi-Fi password, ...). Once the device receives the credentials, it boots into `normal` mode. - -2. The `normal` mode is the mode the device will be most of the time. It connects to the Wi-Fi, to the MQTT, it sends initial informations to the Homie server (like the local IP, the version of the firmware currently running, ...) and it subscribes from the MQTT to properties change. The device can return to `configuration` mode in different ways (press of a button or custom function, see [3. Advanced usage](3.-Advanced-usage.md)). - -3. The `OTA` mode is triggered from the `normal` mode when the MQTT server sends a version different from the current firmware version. It will reach the OTA HTTP server and flash the latest firmware available. When it ends (either a success or a failure), it returns to `normal` mode. - -**Very important: As a rule of thumb, never block the device with blocking code for more than 50ms or so.** Otherwise, you may very probably experience unexpected behaviors. - -## Connecting to the AP and configuring the device - -Homie for ESP8266 has spawned a secure AP named `Homie-xxxxxxxx`. For example, if the AP is named `Homie-c631f278`, the AP password is `c631f278`. Connect to it. - -*Note*: This `c631f278` ID is unique to each device, and you cannot change it. If you reflash a new sketch, this ID won't change. - -Once connected, the webserver is available at `http://homie.config`. To bypass the built-in DNS server, you can reach directly `192.168.1.1`. You can then configure the device using the [Configuration API](6.-Configuration-API.md). When the device receives its configuration, it will reboot to `normal` mode. - -## Understanding what happens in `normal` mode - -### Visual codes - -When the device boots in `normal` mode, it will start blinking: - -* ![Wi-Fi LED blinking](assets/led_wifi.gif) Slowly when connecting to the Wi-Fi -* ![MQTT LED blinking](assets/led_mqtt.gif) Faster when connecting to the MQTT broker - -This way, you can have a quick feedback on what's going on. If both connections are established, the LED will stay off. Note the device will also blink during the automatic reconnection, if the connection to the Wi-Fi or the MQTT broker is lost. - -### Under the hood - -Although the sketch looks like it does not do anything, it actually does quite a lot: - -* It automatically connects to the Wi-Fi and MQTT broker. No more network boilerplate code -* It exposes the Homie device on MQTT (as devices / `device ID`, e.g. `devices/c631f278`). -* It subscribes to the special device property `$ota`, automatically rebooting in OTA mode if OTA is available -* It checks for a button press on the ESP8266, to return to `configuration` mode - -## Creating an useful sketch - -Now that we understand how Homie for ESP8266 works, let's create an useful sketch. We want to create a smart light. - -```c++ -#include - -const int PIN_RELAY = 5; - -HomieNode lightNode("light", "switch"); - -bool lightOnHandler(String value) { - if (value == "true") { - digitalWrite(PIN_RELAY, HIGH); - Homie.setNodeProperty(lightNode, "on", "true"); // Update the state of the light - Serial.println("Light is on"); - } else if (value == "false") { - digitalWrite(PIN_RELAY, LOW); - Homie.setNodeProperty(lightNode, "on", "false"); - Serial.println("Light is off"); - } else { - return false; - } - - return true; -} - -void setup() { - pinMode(PIN_RELAY, OUTPUT); - digitalWrite(PIN_RELAY, LOW); - - Homie.setFirmware("awesome-relay", "1.0.0"); - lightNode.subscribe("on", lightOnHandler); - Homie.registerNode(lightNode); - Homie.setup(); -} - -void loop() { - Homie.loop(); -} -``` - -Alright, step by step: - -1. We create a node with an ID of `light` and a type of `switch` with `HomieNode lightNode("light", "switch")` -2. We set the name and the version of the firmware with `Homie.setFirmware("awesome-light" ,"1.0.0");` -3. We want our `light` node to subscribe to the `on` property. We do that with `lightNode.subscribe("on", lightOnHandler);`. The `lightOnHandler` function will be called when the value of this property is changed -4. We tell Homie for ESP8266 to expose our `light` node by registering it. We do this with `Homie.registerNode(lightNode);` -5. In the `lightOnHandler` function, we want to update the state of the `light` node. We do this with `Homie.setNodeProperty(lightNode, "on", "true");` - -In about thirty SLOC, we have achieved to create a smart light, without any hard-coded credentials, with automatic reconnection in case of network failure, and with OTA support. Not bad, right? - -## Creating a sensor node - -In the previous example sketch, we were reacting on property changes. But what if we want, for example, to send a temperature every 5 minute? We could do this in the Arduino `loop()` function. But then, we would have to check if we are in `normal` mode, and we would have to ensure the network connection is up before sending any property. Boring. - -Fortunately, Homie for ESP8266 provides an easy way to do that. - -```c++ -#include - -const int TEMPERATURE_INTERVAL = 300; - -unsigned long lastTemperatureSent = 0; - -HomieNode temperatureNode("temperature", "temperature"); - -void setupHandler() { - // Do what you want to prepare your sensor -} - -void loopHandler() { - if (millis() - lastTemperatureSent >= TEMPERATURE_INTERVAL * 1000UL || lastTemperatureSent == 0) { - float temperature = 22; // Fake temperature here, for the example - Serial.print("Temperature: "); - Serial.print(temperature); - Serial.println(" °C"); - if (Homie.setNodeProperty(temperatureNode, "temperature", String(temperature), true)) { - lastTemperatureSent = millis(); - } else { - Serial.println("Sending failed"); - } - } -} - -void setup() { - Homie.setFirmware("awesome-temperature", "1.0.0"); - Homie.registerNode(temperatureNode); - Homie.setSetupFunction(setupHandler); - Homie.setLoopFunction(loopHandler); - Homie.setup(); -} - -void loop() { - Homie.loop(); -} -``` - -The only new things here are the `Homie.setSetupFunction(setupHandler);` and `Homie.setLoopFunction(loopHandler);` calls. The setup function will be called once, when the device is in `normal` mode and the network connection is up. The loop function will be called everytime, when the device is in `normal` mode and the network connection is up. This provides a nice level of abstraction. - -Now that you understand the basic usage of Homie for ESP8266, you can head on to the [Advanced usage](3.-Advanced-usage.md) page to learn about more powerful features like input handlers and the event system. diff --git a/docs/3.-Advanced-usage.md b/docs/3.-Advanced-usage.md deleted file mode 100644 index f78012e2..00000000 --- a/docs/3.-Advanced-usage.md +++ /dev/null @@ -1,202 +0,0 @@ -# Advanced usage - -## Built-in LED - -By default, Homie for ESP8266 will blink the built-in LED to indicate its status. However, on some boards like the ESP-01, the built-in LED is actually the TX port, so it is fine if Serial is not enabled, but if you enable Serial, this is a problem. You can easily disable the built-in LED blinking. - -```c++ -void setup() { - Homie.enableBuiltInLedIndicator(false); // before Homie.setup() - // ... -} -``` - -You may, instead of completely disable the LED control, set a new LED to control: - -```c++ -void setup() { - Homie.setLedPin(16, HIGH); // before Homie.setup() -- 2nd param is the state when the LED is on - // ... -} -``` - -## Change the brand - -By default, Homie for ESP8266 will spawn a `Homie-XXXXXXXX` AP, will connect to the MQTT broker with the `Homie-XXXXXXXX` client ID, etc. You might want to change the `Homie` text: - -```c++ -void setup() { - Homie.setBrand("MyIoTSystem"); // before Homie.setup() - // ... -} -``` - -## Hook to Homie events - -You may want to hook to Homie events. Maybe you will want to blink a LED if the Wi-Fi connection is lost, or execute some code prior to a device reset to clear some EEPROM you're using. - -```c++ -void onHomieEvent(HomieEvent event) { - switch(event) { - case HOMIE_CONFIGURATION_MODE: - // Do whatever you want when configuration mode is started - break; - case HOMIE_NORMAL_MODE: - // Do whatever you want when normal mode is started - break; - case HOMIE_OTA_MODE: - // Do whatever you want when OTA mode is started - break; - case HOMIE_ABOUT_TO_RESET: - // Do whatever you want when the device is about to reset - break; - case HOMIE_WIFI_CONNECTED: - // Do whatever you want when Wi-Fi is connected in normal mode - break; - case HOMIE_WIFI_DISCONNECTED: - // Do whatever you want when Wi-Fi is disconnected in normal mode - break; - case HOMIE_MQTT_CONNECTED: - // Do whatever you want when MQTT is connected in normal mode - break; - case HOMIE_MQTT_DISCONNECTED: - // Do whatever you want when MQTT is disconnected in normal mode - break; - } -} - -void setup() { - Homie.onEvent(onHomieEvent); // before Homie.setup() - // ... -} -``` - -See the `HookToEvents` example for a concrete use case. - -## Serial / Logging - -By default, Homie for ESP8266 will output a lot of useful debug messages on the Serial. You may want to disable this behavior if you want to use the Serial line for anything else. - -```c++ -void setup() { - Homie.enableLogging(false); // before Homie.setup() - // ... -} -``` - -If logging is enabled `Serial.begin();` will be called internally at 115 200 baud in `Homie.setup()`. So don't initialize the Serial line yourself. - -## Input handlers - -There are three types of input handlers: - -* Global input handler. This unique handler will handle every changed subscribed properties for all registered nodes - -```c++ -bool globalInputHandler(String node, String property, String value) { - -} - -void setup() { - Homie.setGlobalInputHandler(globalInputHandler); // before Homie.setup() - // ... -} -``` -* Node input handlers. This handler will handle every changed subscribed properties of a specific node - -```c++ -bool nodeInputHandler(String property, String value) { - -} - -HomieNode node("id", "type", nodeInputHandler); -``` -* Property input handlers. This handler will handle changes for a specific property of a specific node - -```c++ -bool propertyInputHandler(String value) { - -} - -HomieNode node("id", "type"); - -void setup() { - node.subscribe("property", propertyInputHandler); // before Homie.setup() - // ... -} -``` - -You can see that input handlers return a boolean. An input handler can decide whether or not it handled the message and want to propagate it down to other input handlers. If an input handler returns `true`, the propagation is stopped, if it returns `false`, the propagation continues. The order of the propagation is global handler → node handler → property handler. - -For example, imagine you defined three input handlers: the global one, the node one, and the property one. If the global input handler returns `false`, the node input handler will be called. If the node input handler returns `true`, the propagation is stopped and the property input handler won't be called. - -## HomieNode - -You might want to create a node that subscribes to all properties. Just add a fourth parameter to the `HomieNode` constructor, set to `true`: - -```c++ -bool nodeInputHandler(String property, String value) { - -} - -HomieNode node("id", "type", nodeInputHandler, true); -``` - -See the `LedStrip` example for a concrete use case. - -## Reset - -Resetting the device means erasing the stored configuration and rebooting from `normal` mode to `configuration` mode. By default, you can do it by pressing 5 seconds the `FLASH` button of your ESP8266 board. - -This behavior is configurable: - -```c++ -void setup() { - Homie.setResetTrigger(1, LOW, 2000); // before Homie.setup() - // ... -} -``` - -The device will now reset if pin `1` is `LOW` for `2000`ms. You can also disable completely this reset trigger: - -```c++ -void setup() { - Homie.disableResetTrigger(); // before Homie.setup() - // ... -} -``` - -In addition, you can also provide your own function responsible for the device reset. This function will be looped: - -```c++ -bool resetFunction () { - return true; // If true is returned, the device will reset, if false, it won't -} - -void setup() { - Homie.setResetFunction(resetFunction); // before Homie.setup() - // ... -} -``` - -Sometimes, you might want to disable temporarily the ability to reset the device. For example, if your device is doing some background work like moving shutters, you will want to disable the ability to reset until the shutters are not moving anymore. - -```c++ -Homie.setResettable(false); -``` - -Note that if a reset is asked while `resettable` is set to false, the device will be flagged. In other words, when you will call `Homie.setResettable(true);` back, the device will immediately reset. - -## Know if device is in normal mode - -If, for some reason, you want to run some code in the Arduino `loop()` function, it might be useful for you to know if the device is in `normal` mode and if the network connection is up. - -```c++ -void loop() { - if (Homie.isReadyToOperate()) { - // normal mode and network connection up - } else { - // not in normal mode or network connection down - } -} -``` diff --git a/docs/4.-OTA.md b/docs/4.-OTA.md deleted file mode 100644 index 7edfddfc..00000000 --- a/docs/4.-OTA.md +++ /dev/null @@ -1,22 +0,0 @@ -# OTA - -Homie for ESP8266 supports OTA, if enabled in the configuration, and if a compatible OTA server is set up. - -It works this way: - -1. The device receives an OTA notification from the MQTT broker, as defined in the Homie convention. If the version sent by the broker is different from the one set with `Homie.setFirmware()`, and if OTA is enabled in the configuration, the device will be flagged to reboot to `OTA` mode as soon as the device will be resettable (with `Homie.setResettable()`). -2. The device boots in `OTA` mode -3. The device reaches the OTA server and attempt to flash the new firmware -4. If the OTA fails or succeed, the device reboots to `normal` mode - -## Creating a compatible OTA server - -In `OTA` mode, the device sends a request to the host/path set in the configuration. This request contains the following headers: - -- `User-Agent`: `ESP8266-http-Update` -- `x-ESP8266-free-space`: space available on the ESP8266 in bytes -- `x-ESP8266-version`: `Device ID`=`Firmware name`=`Firmware version`=`OTA version target` (e.g. `c631f278=awesome-light=1.0.0=1.1.0`) - -Your server has to parse these headers. Based on the `x-ESP8266-version` header, it should decide what firmware it should send to the device. If no firmware is found, or if the firmware is bigger than the `x-ESP8266-free-space` header content, you can abort the OTA by sending a response with a `304` error code. To actually send the firmware, you must transfer the firmware file with a `200` code. For more bulletproof updates, you can also provide in the response the MD5 of your firmware file, in the `x-MD5` header. - -You have an example PHP in the [Arduino for ESP8266 doc](http://esp8266.github.io/Arduino/versions/2.1.0/doc/ota_updates/ota_updates.html#http-server) and a Node.js example in the [homie-server project](https://github.com/marvinroger/homie-server/blob/7b53ee9a1e5a053d311da139da8df8d3bdfd6f98/lib/servers/ota.js#L126). diff --git a/docs/5.-JSON-configuration-file.md b/docs/5.-JSON-configuration-file.md deleted file mode 100644 index da86c2e0..00000000 --- a/docs/5.-JSON-configuration-file.md +++ /dev/null @@ -1,66 +0,0 @@ -# JSON configuration file - -To configure your device, you have two choices: manually flashing the configuration file to the SPIFFS at the `/homie/config.json` (see [Uploading files to file system](http://esp8266.github.io/Arduino/versions/2.1.0/doc/filesystem.html#uploading-files-to-file-system)), so you can bypass the `configuration` mode, or send it through the [Configuration API](6.-Configuration-API.md). - -Below is the format of the JSON configuration you will have to provide: - -```json -{ - "name": "The kitchen light", - "device_id": "kitchen-light", - "wifi": { - "ssid": "Network_1", - "password": "I'm a Wi-Fi password!" - }, - "mqtt": { - "host": "192.168.1.10", - "port": 1883, - "mdns": "mqtt", - "base_topic": "devices/", - "auth": true, - "username": "user", - "password": "pass", - "ssl": true, - "fingerprint": "CF 05 98 89 CA FF 8E D8 5E 5C E0 C2 E4 F7 E6 C3 C7 50 DD 5C" - }, - "ota": { - "enabled": true, - "host": "192.168.1.10", - "port": 80, - "mdns": "ota", - "path": "/custom_ota", - "ssl": true, - "fingerprint": "CF 05 98 89 CA FF 8E D8 5E 5C E0 C2 E4 F7 E6 C3 C7 50 DD 5C" - } -} -``` - -The above JSON contains every field that can be customized. - -Here are the rules: - -* `name`, `wifi.ssid`, `wifi.password`, `mqtt.host` (or `mqtt.mdns`) and `ota.enabled` are mandatory -* `wifi.password` can be `""` if connecting to an open network -* If `mqtt.auth` is `true`, `mqtt.username` and `mqtt.password` must be provided -* If a `mdns` field is set, the device will ignore the `host` and `port` fields and query for the corresponding mDNS service and get the first IP and port found - -Default values if not provided: - -* `device_id`: the hardware device ID (eg. `1a2b3c4d`) -* `mqtt.port`: `1883` -* `mqtt.base_topic`: `devices/` -* `mqtt.auth`: `false` -* `mqtt.ssl`: `false` -* `ota.host`: same as `mqtt.host` -* `ota.port`: `80` -* `ota.path`: `/ota` -* `ota.ssl`: `false` - -`host` fields can be either an IP or an hostname. - -The SSL fingerprints can be of the following format: - -* `CF 05 98 89 CA FF 8E D8 5E 5C E0 C2 E4 F7 E6 C3 C7 50 DD 5C` -* `CF:05:98:89:CA:FF:8E:D8:5E:5C:E0:C2:E4:F7:E6:C3:C7:50:DD:5C` -* `cf 05 98 89 ca ff 8e d8 5e 5c e0 c2 e4 f7 e6 c3 c7 50 dd 5c` -* `cf:05:98:89:ca:ff:8e:d8:5e:5c:e0:c2:e4:f7:e6:c3:c7:50:dd:5c` diff --git a/docs/6.-Configuration-API.md b/docs/6.-Configuration-API.md deleted file mode 100644 index a7369082..00000000 --- a/docs/6.-Configuration-API.md +++ /dev/null @@ -1,133 +0,0 @@ -# Configuration API - -When in `configuration` mode, the device exposes a JSON API to send the configuration to it. When you send a valid configuration to the `/config` endpoint, the configuration file is stored in the file system at `/homie/config.json`. - -If you don't want to mess with JSON, you have a Web UI / app available: -* At [http://marvinroger.github.io/homie-esp8266](http://marvinroger.github.io/homie-esp8266) -* As an [Android app](https://build.phonegap.com/apps/1906578/share) - -**Quick instructions to use the Web UI / app**: - -1. Open the Web UI / app -2. Disconnect from your current Wi-Fi AP, and connect to the `Homie-xxxxxxxx` AP spawned in `configuration` mode -3. Follow the instructions - -You can see the sources of the Web UI [here](https://github.com/marvinroger/homie-esp8266/tree/configurator) and the built version [here](https://github.com/marvinroger/homie-esp8266/tree/gh-pages) - -Alternatively, you can use this curl command to send the config to the device: - -``` -curl -X PUT http://homie.config/config --header "Content-Type: application/json" -d @config.json -``` - -This will send the `./config.json` file to the device. - -## Error handling - -When everything went fine, a `200 OK` HTTP code is returned. -If anything goes wrong, a return code != 200 will be returned, with a JSON `error` field indicating the error. - -## API endpoints - -#### GET `/heart` - -This is useful to ensure we are connected to the device AP. - -##### Response - -200 OK (application/json) - -```json -{ "heart": "beat" } -``` - -#### GET `/device-info` - -Get some information on the device. - -##### Response - -200 OK (application/json) - -```json -{ - "device_id": "52a8fa5d", - "homie_version": "1.0.0", - "firmware": { - "name": "awesome-device", - "version": "1.0.0" - }, - "nodes": [ - { - "id": "light", - "type": "light" - } - ] -} -``` - -#### GET `/networks` - -Retrieve the Wi-Fi networks the device can see. - -##### Response - -* In case of success: - -200 OK (application/json) - -```json -{ - "networks": [ - { "ssid": "Network_2", "rssi": -82, "encryption": "wep" }, - { "ssid": "Network_1", "rssi": -57, "encryption": "wpa" }, - { "ssid": "Network_3", "rssi": -65, "encryption": "wpa2" }, - { "ssid": "Network_5", "rssi": -94, "encryption": "none" }, - { "ssid": "Network_4", "rssi": -89, "encryption": "auto" } - ] -} -``` - -* In case the initial Wi-Fi scan is not finished on the device: - -503 Service Unavailable (application/json) - -```json -{"error": "Initial Wi-Fi scan not finished yet"} -``` - -#### PUT `/config` - -Save the config to the device. - -##### Request body - -(application/json) - -See [JSON configuration file](5.-JSON-configuration-file.md). - -##### Response - -* In case of success: - -200 OK (application/json) - -```json -{ "success": true } -``` - -* In case of error in the payload: - -400 Bad Request (application/json) - -```json -{ "success": false, "error": "Reason why the payload is invalid" } -``` - -* In case the device already received a valid configuration and is waiting for reboot: - -403 Forbidden (application/json) - -```json -{ "success": false, "error": "Device already configured" } -``` diff --git a/docs/7.-API-reference.md b/docs/7.-API-reference.md deleted file mode 100644 index c23cbfd4..00000000 --- a/docs/7.-API-reference.md +++ /dev/null @@ -1,136 +0,0 @@ -# API reference - -### Homie object - -You don't have to instantiate an `Homie` instance, it is done internally. - -#### void Homie.setup () - -Setup Homie. Must be called once in `setup()`. - -#### void Homie.loop () - -Handle Homie work. Must be called in `loop()`. - -#### void Homie.enableLogging (bool `enable`) - -Enable or disable Homie Serial logging. -If logging is enabled, `Serial.begin(115200)` will be called internally. - -* **`enable`**: Whether or not to enable logging. By default, logging is enabled - -#### void Homie.enableBuiltInLedIndicator (bool `enable`) - -Enable or disable the built-in LED to indicate the Homie state. - -* **`enable`**: Whether or not to enable built-in LED. By default, it is enabled - -#### void Homie.setLedPin (unsigned char `pin`, unsigned char `on`) - -Set pin of the LED to control. - -* **`pin`**: LED to control -* **`on`**: state when the light is on (HIGH or LOW) - -#### void Homie.setBrand (const char\* `name`) - -Set the brand of the device, used in the configuration AP, the device hostname and the MQTT client ID. - -* **`name`**: Name of the brand. Default value is `Homie` - -#### void Homie.setFirmware (const char\* `name`, const char\* `version`) - -Set the name and version of the firmware. This is useful for OTA, as Homie will check against the server if there is a newer version. - -* **`name`**: Name of the firmware. Default value is `undefined` -* **`version`**: Version of the firmware. Default value is `undefined` - -#### void Homie.registerNode (HomieNode `node`) - -Register a node. - -* **`node`**: node to register - -#### void Homie.setGlobalInputHandler (std::function `handler`) - -Set input handler for subscribed properties. - -* **`handler`**: Global input handler -* **`node`**: Name of the node getting updated -* **`property`**: Property of the node getting updated -* **`value`**: Value of the new property - -#### void Homie.onEvent (std::function `callback`) - -Set the event handler. Useful if you want to hook to Homie events. - -* **`callback`**: Event handler - -#### void Homie.setResetTrigger (unsigned char `pin`, unsigned char `state`, unsigned int `time`) - -Set the reset trigger. By default, the device will reset when pin `0` is `LOW` for `5000`ms. - -* **`pin`**: Pin of the reset trigger -* **`state`**: Reset when the pin reaches this state for the given time -* **`time`**: Time necessary to reset - -#### void Homie.disableResetTrigger () - -Disable the reset trigger. - -#### void Homie.setResetFunction (std::function `callback`) - -Set the reset function. This is a function that is going to be called at each loop iteration, which can trigger a device reset. If the function returns true, the device resets. Else, it does not. - -* **`callback`**: Reset function - -#### void Homie.setSetupFunction (std::function `callback`) - -You can provide the function that will be called when operating in `normal` mode. - -* **`callback`**: Setup function - -#### void Homie.setLoopFunction (std::function `callback`) - -You can provide the function that will be looped in normal mode. - -* **`callback`**: Loop function - -#### void Homie.setNodeProperty (HomieNode `node`, String `property`, String `value`, bool `retained` = true) - -Using this function, you can set the value of a node property, like a temperature for example. - -* **`node`**: HomieNode instance on which to set the property on -* **`property`**: Property to send -* **`value`**: Payload -* **`retained`**: Optional. Should the MQTT broker retain this value, or is it a one-shot value? - -#### void Homie.setResettable (bool `resettable`) - -Is the device resettable? This is useful at runtime, because you might want the device not to be resettable when you have another library that is doing some unfinished work, like moving shutters for example. - -* **`resettable`**: Is the device resettable? Default value is `true` - -#### bool Homie.isReadyToOperate () - -Is the device in normal mode, configured and connected? You should not need this function. But maybe you will. - ---- - -### HomieNode object - -#### void HomieNode (const char\* `id`, const char\* `type`, std::function `handler` = , bool `subscribeToAll` = false) - -Constructor of a HomieNode object. - -* **`id`**: ID of the node -* **`type`**: Type of the node -* **`handler`**: Optional. Input handler of the node -* **`subscribeToAll`**: Optional. Whether or not to call the handler for every properties, even the ones not registered - -#### void .subscribe (const char\* `property`, std::function `handler`) = ) - -Subscribes the node to the given property. - -* **`property`**: Property to subscribe to -* **`handler`**: Optional. Input handler of the property of the node diff --git a/docs/8.-Limitations-and-known-issues.md b/docs/8.-Limitations-and-known-issues.md deleted file mode 100644 index 47111fe9..00000000 --- a/docs/8.-Limitations-and-known-issues.md +++ /dev/null @@ -1,24 +0,0 @@ -# Limitations and known issues - -## Blocking Homie code - -In `configuration` and `normal` modes, Homie for ESP8266 code is designed to be non-blocking, so that you can do other tasks in the main `loop()`. However, the connection to the MQTT broker is blocking during ~5 seconds in case the server is unreachable. This is an Arduino for ESP8266 limitation, and we can't do anything on our side to solve this issue, not even a timeout. - -The `OTA` mode is blocking for obvious reason. - -## SSL fingerprint checking - -Adding a TLS fingerprint effectively pins the device to a particular certificate. Furthermore, as currently implemented by the ESP8266 `WifiSecureClient`, both `mqtt.host` and `ota.host` are verified against the server certificate's common name (CN) in the certificate subject or in the SANs (subjectAlternateName) contained in it, but not in their IP addresses. For example, if the certificate used by your server looks like this: - -``` -Subject: CN=tiggr.example.org, OU=generate-CA/emailAddress=nobody@example.net -... -X509v3 Subject Alternative Name: - IP Address:192.168.1.10, DNS:broker.example.org -``` - -Enabling fingerprint in Homie will work only if host is set to `tiggr.example.org` or `broker.example.org` and the correct fingerprint is used; setting host to the IP address will cause fingerprint verification to fail. - -## ADC readings - -[This is a known esp8266/Arduino issue](https://github.com/esp8266/Arduino/issues/1634) that polling `analogRead()` too frequently forces the Wi-Fi to disconnect. As a workaround, don't poll the ADC more than one time every 3ms. diff --git a/docs/README.md b/docs/README.md index 94fdf8f2..aa561b45 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,7 @@ -Homie for ESP8266 documentation -=============================== - -See [index.md](index.md) to view it locally, or http://marvinroger.viewdocs.io/homie-esp8266/ to view it online. +Docs +==== + +Docs are available: + +* Locally at [index.md](index.md) +* Online at http://marvinroger.github.io/homie-esp8266/ diff --git a/docs/advanced-usage/branding.md b/docs/advanced-usage/branding.md new file mode 100644 index 00000000..a2a1e1ee --- /dev/null +++ b/docs/advanced-usage/branding.md @@ -0,0 +1,8 @@ +By default, Homie for ESP8266 will spawn an `Homie-xxxxxxxxxxxx` AP and will connect to the MQTT broker with the `Homie-xxxxxxxxxxxx` client ID. You might want to change the `Homie` text: + +```c++ +void setup() { + Homie_setBrand("MyIoTSystem"); // before Homie.setup() + // ... +} +``` diff --git a/docs/advanced-usage/broadcast.md b/docs/advanced-usage/broadcast.md new file mode 100644 index 00000000..f77e452b --- /dev/null +++ b/docs/advanced-usage/broadcast.md @@ -0,0 +1,13 @@ +Your device can react to Homie broadcasts. To do that, you can use a broadcast handler: + +```c++ +bool broadcastHandler(const String& level, const String& value) { + Serial << "Received broadcast level " << level << ": " << value << endl; + return true; +} + +void setup() { + Homie.setBroadcastHandler(broadcastHandler); // before Homie.setup() + // ... +} +``` diff --git a/docs/advanced-usage/built-in-led.md b/docs/advanced-usage/built-in-led.md new file mode 100644 index 00000000..d5fc54a0 --- /dev/null +++ b/docs/advanced-usage/built-in-led.md @@ -0,0 +1,19 @@ +By default, Homie for ESP8266 will blink the built-in LED to indicate its status. Note it does not indicate activity, only the status of the device (in `configuration` mode, connecting to Wi-Fi or connecting to MQTT), see [Getting started](../quickstart/getting-started.md) for more information. + +However, on some boards like the ESP-01, the built-in LED is actually the TX port, so it is fine if Serial is not enabled, but if you enable Serial, this is a problem. You can easily disable the built-in LED blinking. + +```c++ +void setup() { + Homie.disableLedFeedback(); // before Homie.setup() + // ... +} +``` + +You may, instead of completely disable the LED control, set a new LED to control: + +```c++ +void setup() { + Homie.setLedPin(16, HIGH); // before Homie.setup() -- 2nd param is the state of the pin when the LED is o + // ... +} +``` diff --git a/docs/advanced-usage/custom-settings.md b/docs/advanced-usage/custom-settings.md new file mode 100644 index 00000000..03b29083 --- /dev/null +++ b/docs/advanced-usage/custom-settings.md @@ -0,0 +1,38 @@ +Homie for ESP8266 lets you implement custom settings that can be set from the JSON configuration file and the Configuration API. Below is an example of how to use this feature: + +```c++ +HomieSetting percentageSetting("percentage", "A simple percentage"); // id, description + +void setup() { + percentageSetting.setDefaultValue(50).setValidator([] (long candidate) { + return (candidate >= 0) && (candidate <= 100); + }); + + Homie.setup(); +} +``` + +An `HomieSetting` instance can be of the following types: + +Type | Value +---- | ----- +`bool` | `true` or `false` +`long` | An integer from `-2,147,483,648` to `2,147,483,647` +`double` | A floating number that can fit into a `real64_t` +`const char*` | Any string + +By default, a setting is mandatory (you have to set it in the configuration file). If you give it a default value with `setDefaultValue()`, the setting becomes optional. You can validate a setting by giving a validator function to `setValidator()`. To get the setting from your code, use `get()`. To get whether the value returned is the optional one or the one provided, use `wasProvided()`. + +For this example, if you want to provide the `percentage` setting, you will have to put in your configuration file: + +```json +{ + "settings": { + "percentage": 75 + } +} +``` + +See the following example for a concrete use case: + +[![GitHub logo](../assets/github.png) CustomSettings.ino](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/CustomSettings/CustomSettings.ino) diff --git a/docs/advanced-usage/deep-sleep.md b/docs/advanced-usage/deep-sleep.md new file mode 100644 index 00000000..734da88b --- /dev/null +++ b/docs/advanced-usage/deep-sleep.md @@ -0,0 +1,29 @@ +Before deep sleeping, you will want to ensure that all messages are sent, including the `$online → false`. To do that, you can call `Homie.prepareToSleep()`. This will disconnect everything cleanly, so that you can call `ESP.deepSleep()`. + +```c++ +#include + +void onHomieEvent(const HomieEvent& event) { + switch(event.type) { + case HomieEventType::MQTT_READY: + Homie.getLogger() << "MQTT connected, preparing for deep sleep..." << endl; + Homie.prepareToSleep(); + break; + case HomieEventType::READY_TO_SLEEP: + Homie.getLogger() << "Ready to sleep" << endl; + Homie.doDeepSleep(); + break; + } +} + +void setup() { + Serial.begin(115200); + Serial << endl << endl; + Homie.onEvent(onHomieEvent); + Homie.setup(); +} + +void loop() { + Homie.loop(); +} +``` diff --git a/docs/advanced-usage/events.md b/docs/advanced-usage/events.md new file mode 100644 index 00000000..7ab6ce48 --- /dev/null +++ b/docs/advanced-usage/events.md @@ -0,0 +1,69 @@ +You may want to hook to Homie events. Maybe you will want to control an RGB LED if the Wi-Fi connection is lost, or execute some code prior to a device reset, for example to clear some EEPROM you're using: + +```c++ +void onHomieEvent(const HomieEvent& event) { + switch(event.type) { + case HomieEventType::STANDALONE_MODE: + // Do whatever you want when standalone mode is started + break; + case HomieEventType::CONFIGURATION_MODE: + // Do whatever you want when configuration mode is started + break; + case HomieEventType::NORMAL_MODE: + // Do whatever you want when normal mode is started + break; + case HomieEventType::OTA_STARTED: + // Do whatever you want when OTA is started + break; + case HomieEventType::OTA_PROGRESS: + // Do whatever you want when OTA is in progress + + // You can use event.sizeDone and event.sizeTotal + break; + case HomieEventType::OTA_FAILED: + // Do whatever you want when OTA is failed + break; + case HomieEventType::OTA_SUCCESSFUL: + // Do whatever you want when OTA is successful + break; + case HomieEventType::ABOUT_TO_RESET: + // Do whatever you want when the device is about to reset + break; + case HomieEventType::WIFI_CONNECTED: + // Do whatever you want when Wi-Fi is connected in normal mode + + // You can use event.ip, event.gateway, event.mask + break; + case HomieEventType::WIFI_DISCONNECTED: + // Do whatever you want when Wi-Fi is disconnected in normal mode + + // You can use event.wifiReason + break; + case HomieEventType::MQTT_READY: + // Do whatever you want when MQTT is connected in normal mode + break; + case HomieEventType::MQTT_DISCONNECTED: + // Do whatever you want when MQTT is disconnected in normal mode + + // You can use event.mqttReason + break; + case HomieEventType::MQTT_PACKET_ACKNOWLEDGED: + // Do whatever you want when an MQTT packet with QoS > 0 is acknowledged by the broker + + // You can use event.packetId + break; + case HomieEventType::READY_TO_SLEEP: + // After you've called `prepareToSleep()`, the event is triggered when MQTT is disconnected + break; + } +} + +void setup() { + Homie.onEvent(onHomieEvent); // before Homie.setup() + // ... +} +``` + +See the following example for a concrete use case: + +[![GitHub logo](../assets/github.png) HookToEvents.ino](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/HookToEvents/HookToEvents.ino) diff --git a/docs/advanced-usage/input-handlers.md b/docs/advanced-usage/input-handlers.md new file mode 100644 index 00000000..0efe4124 --- /dev/null +++ b/docs/advanced-usage/input-handlers.md @@ -0,0 +1,63 @@ +There are four types of input handlers: + +* Global input handler. This unique handler will handle every changed settable properties for all nodes + +```c++ +bool globalInputHandler(const HomieNode& node, const String& property, const HomieRange& range, const String& value) { + +} + +void setup() { + Homie.setGlobalInputHandler(globalInputHandler); // before Homie.setup() + // ... +} +``` + +* Node input handlers. This handler will handle every changed settable properties of a specific node + +```c++ +bool nodeInputHandler(const String& property, const HomieRange& range, const String& value) { + +} + +HomieNode node("id", "type", nodeInputHandler); +``` + +* Virtual callback from node input handler + +You can create your own class derived from HomieNode that implements the virtual method `bool HomieNode::handleInput(const String& property, const String& value)`. The default node input handler then automatically calls your callback. + +```c++ +class RelaisNode : public HomieNode { + public: + RelaisNode(): HomieNode("Relais", "switch8"); + + protected: + virtual bool handleInput(const String& property, const HomieRange& range, const String& value) { + + } +}; +``` + +* Property input handlers. This handler will handle changes for a specific settable property of a specific node + +```c++ +bool propertyInputHandler(const HomieRange& range, const String& value) { + +} + +HomieNode node("id", "type"); + +void setup() { + node.advertise("property").settable(propertyInputHandler); // before Homie.setup() + // ... +} +``` + +You can see that input handlers return a boolean. An input handler can decide whether or not it handled the message and want to propagate it down to other input handlers. If an input handler returns `true`, the propagation is stopped, if it returns `false`, the propagation continues. The order of propagation is global handler → node handler → property handler. + +For example, imagine you defined three input handlers: the global one, the node one, and the property one. If the global input handler returns `false`, the node input handler will be called. If the node input handler returns `true`, the propagation is stopped and the property input handler won't be called. You can think of it as middlewares. + + +!!! warning + Homie uses [ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) for network communication that make uses of asynchronous callback from the ESP8266 framework for incoming network packets. Thus the input handler runs in a different task than the `loopHandler()`. So keep in mind that the network task may interrupt your loop at any time. diff --git a/docs/advanced-usage/logging.md b/docs/advanced-usage/logging.md new file mode 100644 index 00000000..e1b48ece --- /dev/null +++ b/docs/advanced-usage/logging.md @@ -0,0 +1,26 @@ +By default, Homie for ESP8266 will output a lot of useful debug messages on the Serial. You may want to disable this behavior if you want to use the Serial line for anything else. + +```c++ +void setup() { + Homie.disableLogging(); // before Homie.setup() + // ... +} +``` + +!!! warning + It's up to you to call `Serial.begin();`, whether logging is enabled or not. + +You can also change the `Print` instance to log to: + +```c++ +void setup() { + Homie.setLoggingPrinter(&Serial2); // before Homie.setup() + // ... +} +``` + +You can use the logger from your code with the `getLogger()` client: + +```c++ +Homie.getLogger() << "Hey!" << endl; +``` diff --git a/docs/advanced-usage/magic-bytes.md b/docs/advanced-usage/magic-bytes.md new file mode 100644 index 00000000..418ac85b --- /dev/null +++ b/docs/advanced-usage/magic-bytes.md @@ -0,0 +1,16 @@ +Homie for ESP8266 firmwares contain magic bytes allowing you to check if a firmware is actually an Homie for ESP8266 firmware, and if so, to get the name, the version and the brand of the firmware. + +You might be wondering why `Homie_setFirmware()` instead of `Homie.setFirmware()`, this is because we use [special macros](https://github.com/marvinroger/homie-esp8266/blob/8935639bc649a6c71ce817ea4f732988506d020e/src/Homie.hpp#L23-L24) to embed the magic bytes. + +Values are encoded as such within the firmware binary: + +Type | Left boundary | Value | Right boundary +---- | ------------- | ----- | -------------- +Homie magic bytes | *None* | `0x25 0x48 0x4F 0x4D 0x49 0x45 0x5F 0x45 0x53 0x50 0x38 0x32 0x36 0x36 0x5F 0x46 0x57 0x25` | *None* +Firmware name | `0xBF 0x84 0xE4 0x13 0x54` | **actual firmware name** | `0x93 0x44 0x6B 0xA7 0x75` +Firmware version | `0x6A 0x3F 0x3E 0x0E 0xE1` | **actual firmware version** | `0xB0 0x30 0x48 0xD4 0x1A` +Firmware brand (only present if `Homie_setBrand()` called, Homie otherwise) | `0xFB 0x2A 0xF5 0x68 0xC0` | **actual firmware brand** | `0x6E 0x2F 0x0F 0xEB 0x2D` + +See the following script for a concrete use case: + +[![GitHub logo](../assets/github.png) firmware_parser.py](https://github.com/marvinroger/homie-esp8266/blob/develop/scripts/firmware_parser) diff --git a/docs/advanced-usage/miscellaneous.md b/docs/advanced-usage/miscellaneous.md new file mode 100644 index 00000000..ae698c92 --- /dev/null +++ b/docs/advanced-usage/miscellaneous.md @@ -0,0 +1,63 @@ +# Know if the device is configured / connected + +If, for some reason, you want to run some code in the Arduino `loop()` function, it might be useful for you to know if the device is in configured (so in `normal` mode) and if the network connection is up. + +```c++ +void loop() { + if (Homie.isConfigured()) { + // The device is configured, in normal mode + if (Homie.isConnected()) { + // The device is connected + } else { + // The device is not connected + } + } else { + // The device is not configured, in either configuration or standalone mode + } +} +``` + +# Get access to the configuration + +You can get access to the configuration of the device. The representation of the configuration is: + +```c++ +struct ConfigStruct { + char* name; + char* deviceId; + + struct WiFi { + char* ssid; + char* password; + } wifi; + + struct MQTT { + struct Server { + char* host; + uint16_t port; + } server; + char* baseTopic; + bool auth; + char* username; + char* password; + } mqtt; + + struct OTA { + bool enabled; + } ota; +}; +``` + +For example, to access the Wi-Fi SSID, you would do: + +```c++ +Homie.getConfiguration().wifi.ssid; +``` + +# Get access to the MQTT client + +You can get access to the underlying MQTT client. For example, to disconnect from the broker: + +```c++ +Homie.getMqttClient().disconnect(); +``` diff --git a/docs/advanced-usage/range-properties.md b/docs/advanced-usage/range-properties.md new file mode 100644 index 00000000..d2a7f5a4 --- /dev/null +++ b/docs/advanced-usage/range-properties.md @@ -0,0 +1,31 @@ +In all the previous examples you have seen, node properties were advertised one-by-one (e.g. `temperature`, `unit`...). But what if you have a LED strip with, say, 100 properties, one for each LED? You won't advertise these 100 LEDs one-by-one. This is what range properties are meant for. + +```c++ +HomieNode stripNode("strip", "strip"); + +bool ledHandler(const HomieRange& range, const String& value) { + Homie.getLogger() << "LED " << range.index << " set to " << value << endl; + + // Now, let's update the actual state of the given led + stripNode.setProperty("led").setRange(range).send(value); +} + +void setup() { + stripNode.advertiseRange("led", 1, 100).settable(ledHandler); + // before Homie.setup() +} +``` + +On the mqtt broker you will see the following message show up: +``` +topic message +-------------------------------------------------------- +homie//strip/$type strip +homie//strip/$properties led[1-100]:settable +``` + +You can then publish the value `on` to topic `homie//strip/led_1/set` to turn on led number 1. + +See the following example for a concrete use case: + +[![GitHub logo](../assets/github.png) LedStrip](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/LedStrip/LedStrip.ino) diff --git a/docs/advanced-usage/resetting.md b/docs/advanced-usage/resetting.md new file mode 100644 index 00000000..d7bdf81b --- /dev/null +++ b/docs/advanced-usage/resetting.md @@ -0,0 +1,35 @@ +Resetting the device means erasing the stored configuration and rebooting from `normal` mode to `configuration` mode. By default, you can do it by pressing for 5 seconds the `FLASH` button of your ESP8266 board. + +This behavior is configurable: + +```c++ +void setup() { + Homie.setResetTrigger(1, LOW, 2000); // before Homie.setup() + // ... +} +``` + +The device will now reset if pin `1` is `LOW` for `2000`ms. You can also disable completely this reset trigger: + +```c++ +void setup() { + Homie.disableResetTrigger(); // before Homie.setup() + // ... +} +``` + +In addition, you can also trigger a device reset from your sketch: + +```c++ +void loop() { + Homie.reset(); +} +``` + +This will reset the device as soon as it is idle. Indeed, sometimes, you might want to disable temporarily the ability to reset the device. For example, if your device is doing some background work like moving shutters, you will want to disable the ability to reset until the shutters are not moving anymore. + +```c++ +Homie.setIdle(false); +``` + +Note that if a reset is asked while the device is not idle, the device will be flagged. In other words, when you will call `Homie.setIdle(true);` back, the device will immediately reset. diff --git a/docs/advanced-usage/standalone-mode.md b/docs/advanced-usage/standalone-mode.md new file mode 100644 index 00000000..8f584025 --- /dev/null +++ b/docs/advanced-usage/standalone-mode.md @@ -0,0 +1,12 @@ +Homie for ESP8266 has a special mode named `standalone`. It was a [requested feature](https://github.com/marvinroger/homie-esp8266/issues/125) to implement a way not to boot into `configuration` mode on initial boot, so that a device can work without being configured first. It was already possible in `configuration` mode, but the device would spawn an AP which would make it insecure. + +To enable this mode, call `Homie.setStandalone()`: + +```c++ +void setup() { + Homie.setStandalone(); // before Homie.setup() + // ... +} +``` + +To actually configure the device, you have to reset it, the same way you would to go from `normal` mode to `configuration` mode. diff --git a/docs/advanced-usage/streaming-operator.md b/docs/advanced-usage/streaming-operator.md new file mode 100644 index 00000000..f420cac6 --- /dev/null +++ b/docs/advanced-usage/streaming-operator.md @@ -0,0 +1,17 @@ +Homie for ESP8266 includes a nice streaming operator to interact with `Print` objects. + +Imagine the following code: + +```c++ +int temperature = 32; +Homie.getLogger().print("The current temperature is "); +Homie.getLogger().print(temperature); +Homie.getLogger().println(" °C."); +``` + +With the streaming operator, the following code will do exactly the same thing, without performance penalties: + +```c++ +int temperature = 32; +Homie.getLogger() << "The current temperature is " << temperature << " °C." << endl; +``` diff --git a/docs/advanced-usage/ui-bundle.md b/docs/advanced-usage/ui-bundle.md new file mode 100644 index 00000000..8e089116 --- /dev/null +++ b/docs/advanced-usage/ui-bundle.md @@ -0,0 +1,3 @@ +The Homie for ESP8266 configuration AP implements a captive portal. When connecting to it, you will be prompted to connect, and your Web browser will open. By default, it will show an empty page with a text saying to install an `ui_bundle.gz` file. + +Indeed, you can serve the [configuration UI](http://marvinroger.github.io/homie-esp8266/configurators/v2/) directly from your ESP8266. See [the data/homie folder](https://github.com/marvinroger/homie-esp8266/tree/develop/data/homie). diff --git a/docs/assets/github.png b/docs/assets/github.png new file mode 100644 index 0000000000000000000000000000000000000000..fd168a46742ae4d95535e8c669f7d270091f35b5 GIT binary patch literal 2944 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 literal 0 HcmV?d00001 diff --git a/docs/assets/youtube.png b/docs/assets/youtube.png new file mode 100644 index 0000000000000000000000000000000000000000..ffb6613dea44e0c7c71e039be21e472cecc3cddc GIT binary patch literal 2635 zcmV-R3bgf!P)k*SJXQ4w9;>PcfF_qg@J{*LQ>X$;L@B1eD^ek%R8pi& zloC;riXJ`k)L1YZXhlS)70{sJmy-AWhxua4k zFZNe0ssQ@)`Hv{k%|y5itAEREG%0{5Kze^^FnVmEKeV6%IIym3nVsXx6c}XHQ}9}%3u2ng>ULS8~Lm&p91`~#r#7A`b_-; znyiT8;Yt`@w;M=iT0i3+;8DQqD&`-x#$WxIaBXL?`(d4>t!I4l+(O|y0A0Pj(M(nt ze9b^H|LvL9opuP=pU+>cOn>23aFWO@oB*Q4+fV&qc-kI-n7(I?-*^~cPSg8lc>cJ7 zVsSTsi!)1QGDS?g_viB`weLvk5RfGp%UKMJ7SPb&h)5b?DIewsQ1c z6+H*gdqrf_b*2@Gw7^P~cb#Lo^hMEdsT|$ux3r$C&>x6m!gZ#VM1ed5=yDxoxotgi z2hOU1xHMfG$>`>G}a& zST6s&w;X=Lr?gjq$6e=fg@8_0)utpge=yM-hTm@|Iv0w&02y(escx8k=6QyRF_X?Omn)?(ygRO?=cwX7h>W|=^mYm0 zS>rF#j!LE68-}+jdmEwnF=iQhWi?=ctN@m%w_JX`6oy|)IoYg=U&#s|5p*_I0B_Uw zw?_Wk8-}0KRJKC&_pb9SmK8vqY#$o>dnt-OCM1^#5V_8?Pz_T6u7|ZzfcF=>Kh45F zXJFM_Vz=R5>)^RU;p+W_dl=ZV+D2Eh0(ct+3Wdv3m3x`6-F23wvI1Cw{(OFy0(+Ej zDX;Q>q^tlIVW6jLtEAo@5H4S3d-_>f0W84ZipBF1=sl_68m85Dl%Jawz#R5>cW)q% z?nuBN#}!l?PgJOQ97`vpF=VMA_h zrE+66O>bvHL);^|7$Q{l6*5jub_93|@Yk-(e@;h-?f#jE)X<8*R7!PLYEdqCLTY@cx!Vc1t-xRJJu{+{j&HRU}-xWaXo6^}9G z6KT{4Qv``nvA*K0J18`NWl^ zdVltjOqNpe9yoFNqyp%TqP-CPQ?`*zR#frW_QAoE&iS0ygNl5)6*i?xBZ>*Q?bIhv zD}Zfp4IhBw?rbudtSI!3QW*a8)aPgH52^s|FXn&2pzD@DnpuAGvr>8J^E0iPu@fx; z+FOo3tBMDgH<(#&s(5f)dFY1O*3a6DngHny!<(SEzM+OlZx$j|h2Gd3hBwh1;~3A| zjlL9yzf8&Btcsrii0c~CEDHf1hv&bqw;cUwzSl15o{)V#J!b}zTo1uD!c=lSJgYf@ z$e0rUR)Gil!f?S?cbD9N&vkUH^K$+U6~0hN&ts*tK{#80v#c(HS^PhwH`hI(luTF1jek9g@ih@VeYyFH*wfDQsWLD~+W9ZGEg+CZ{K zmDiAJPMC5mniHm07HtJnt4w}xVUbo$&Tq6@rnpH&WP+Ki5SajRRS4rOIu53)LdGF< z6hvbni~)L-fQka63LYh3Wa?`;AsPwdIAUPvy~D%9a~;zcYt?P33h>h#Hnf~MIvS*L z97rw~Xp#h)rU8N=5D_1q1ggF-BEF~Usi(fG=d-G(svdjliF%%@dIXRH%0!|_g$bf5 z2@$12loF+hq?CdvVWN~NN#cYO^1PUs6U1>WzF(tiHBMSuYU7=qwM!2ktktuTj4{R- tV~jDz7-Nhv#u#IaF~%5Uj4@_m{twuW*r<{bA^-pY002ovPDHLkV1fbV)0_YR literal 0 HcmV?d00001 diff --git a/docs/configuration/http-json-api.md b/docs/configuration/http-json-api.md new file mode 100644 index 00000000..c18f66e3 --- /dev/null +++ b/docs/configuration/http-json-api.md @@ -0,0 +1,264 @@ +When in `configuration` mode, the device exposes a HTTP JSON API to send the configuration to it. When you send a valid configuration to the `/config` endpoint, the configuration file is stored in the filesystem at `/homie/config.json`. + +If you don't want to mess with JSON, you have a Web UI / app available: + +* At http://marvinroger.github.io/homie-esp8266/configurators/v2/ +* As an [Android app](https://build.phonegap.com/apps/1906578/share) + +**Quick instructions to use the Web UI / app**: + +1. Open the Web UI / app +2. Disconnect from your current Wi-Fi AP, and connect to the `Homie-xxxxxxxxxxxx` AP spawned in `configuration` mode +3. Follow the instructions + +You can see the sources of the Web UI [here](https://github.com/marvinroger/homie-esp8266-setup). + +Alternatively, you can use this `curl` command to send the configuration to the device. You must connect to the device in `configuration` mode (i.e. the device is an Access Point). This method will not work if not in `configuration` mode: + +```shell +curl -X PUT http://192.168.123.1/config --header "Content-Type: application/json" -d @config.json +``` + +This will send the `./config.json` file to the device. + +# Error handling + +When everything went fine, a `2xx` HTTP code is returned, such as `200 OK`, `202 Accepted`, `204 No Content` and so on. +If anything goes wrong, a return code != 2xx will be returned, with a JSON `error` field indicating the error, such as `500 Internal Server error`, `400 Bad request` and so on. + +# Endpoints + +**API base address:** `http://192.168.123.1` + +??? summary "GET `/heart`" + This is useful to ensure we are connected to the device AP. + + ## Response + + `204 No Content` + +-------------- + +??? summary "GET `/device-info`" + Get some information on the device. + + ## Response + + `200 OK (application/json)` + + ```json + { + "hardware_device_id": "52a8fa5d", + "homie_esp8266_version": "2.0.0", + "firmware": { + "name": "awesome-device", + "version": "1.0.0" + }, + "nodes": [ + { + "id": "light", + "type": "light" + } + ], + "settings": [ + { + "name": "timeout", + "description": "Timeout in seconds", + "type": "ulong", + "required": false, + "default": 10 + } + ] + } + ``` + + `type` can be one of the following: + + * `bool`: a boolean + * `ulong`: an unsigned long + * `long`: a long + * `double`: a double + * `string`: a string + + !!! note "Note about settings" + If a setting is not required, the `default` field will always be set. + +-------------- + +??? summary "GET `/networks`" + Retrieve the Wi-Fi networks the device can see. + + ## Response + + !!! success "In case of success" + `200 OK (application/json)` + + ```json + { + "networks": [ + { "ssid": "Network_2", "rssi": -82, "encryption": "wep" }, + { "ssid": "Network_1", "rssi": -57, "encryption": "wpa" }, + { "ssid": "Network_3", "rssi": -65, "encryption": "wpa2" }, + { "ssid": "Network_5", "rssi": -94, "encryption": "none" }, + { "ssid": "Network_4", "rssi": -89, "encryption": "auto" } + ] + } + ``` + + !!! failure "In case the initial Wi-Fi scan is not finished on the device" + `503 Service Unavailable (application/json)` + + ```json + { + "error": "Initial Wi-Fi scan not finished yet" + } + ``` + +-------------- + +??? summary "PUT `/config`" + Save the config to the device. + + ## Request body + + `(application/json)` + + See [JSON configuration file](json-configuration-file.md). + + ## Response + + !!! success "In case of success" + `200 OK (application/json)` + + ```json + { + "success": true + } + ``` + + !!! failure "In case of error in the payload" + `400 Bad Request (application/json)` + + ```json + { + "success": false, + "error": "Reason why the payload is invalid" + } + ``` + + !!! failure "In case the device already received a valid configuration and is waiting for reboot" + `403 Forbidden (application/json)` + + ```json + { + "success": false, + "error": "Device already configured" + } + ``` + +-------------- + +??? summary "PUT `/wifi/connect`" + Initiates the connection of the device to the Wi-Fi network while in configuation mode. This request is not synchronous and the result (Wi-Fi connected or not) must be obtained by with `GET /wifi/status`. + + ## Request body + + `(application/json)` + + ```json + { + "ssid": "My_SSID", + "password": "my-passw0rd" + } + ``` + + ## Response + + !!! success "In case of success" + `202 Accepted (application/json)` + + ```json + { + "success": true + } + ``` + + !!! failure "In case of error in the payload" + `400 Bad Request (application/json)` + + ```json + { + "success": false, + "error": "Reason why the payload is invalid" + } + ``` + +-------------- + +??? summary "GET `/wifi/status`" + Returns the current Wi-Fi connection status. + + Helpful when monitoring Wi-Fi connectivity after `PUT /wifi/connect`. + + ## Response + + `200 OK (application/json)` + + ```json + { + "status": "connected" + } + ``` + + `status` might be one of the following: + + * `idle` + * `connect_failed` + * `connection_lost` + * `no_ssid_available` + * `connected` along with a `local_ip` field + * `disconnected` + +-------------- + +??? summary "PUT `/proxy/control`" + Enable/disable the device to act as a transparent proxy between AP and Station networks. + + All requests that don't collide with existing API paths will be bridged to the destination according to the `Host` HTTP header. The destination host is called using the existing Wi-Fi connection (established after a `PUT /wifi/connect`) and all contents are bridged back to the connection made to the AP side. + + This feature can be used to help captive portals to perform cloud API calls during device enrollment using the ESP8266 Wi-Fi AP connection without having to patch the Homie firmware. By using the transparent proxy, all operations can be performed by the custom JavaScript running on the browser (in SPIFFS location `/data/homie/ui_bundle.gz`). + + HTTPS is not supported. + + **Important**: The HTTP requests and responses must be kept as small as possible because all contents are transported using RAM memory, which is very limited. + + ## Request body + + `(application/json)` + + ```json + { + "enable": true + } + ``` + + ## Response + + ??? success "In case of success" + `200 OK (application/json)` + + ```json + { + "success": true + } + ``` + + ??? failure "In case of error in the payload" + `400 Bad Request (application/json)` + + ```json + { + "success": false, + "error": "Reason why the payload is invalid" + } + ``` diff --git a/docs/configuration/json-configuration-file.md b/docs/configuration/json-configuration-file.md new file mode 100644 index 00000000..2924eef8 --- /dev/null +++ b/docs/configuration/json-configuration-file.md @@ -0,0 +1,59 @@ +To configure your device, you have two choices: manually flashing the configuration file to the SPIFFS at the `/homie/config.json` (see [Uploading files to file system](http://esp8266.github.io/Arduino/versions/2.3.0/doc/filesystem.html#uploading-files-to-file-system)), so you can bypass the `configuration` mode, or send it through the [HTTP JSON API](http-json-api.md). + +Below is the format of the JSON configuration you will have to provide: + +```json +{ + "name": "The kitchen light", + "device_id": "kitchen-light", + "device_stats_interval": 60, + "wifi": { + "ssid": "Network_1", + "password": "I'm a Wi-Fi password!", + "bssid": "DE:AD:BE:EF:BA:BE", + "channel": 1, + "ip": "192.168.1.5", + "mask": "255.255.255.0", + "gw": "192.168.1.1", + "dns1": "8.8.8.8", + "dns2": "8.8.4.4" + }, + "mqtt": { + "host": "192.168.1.10", + "port": 1883, + "base_topic": "devices/", + "auth": true, + "username": "user", + "password": "pass" + }, + "ota": { + "enabled": true + }, + "settings": { + "percentage": 55 + } +} +``` + +The above JSON contains every field that can be customized. + +Here are the rules: + +* `name`, `wifi.ssid`, `wifi.password`, `mqtt.host` and `ota.enabled` are mandatory +* `wifi.password` can be `null` if connecting to an open network +* If `mqtt.auth` is `true`, `mqtt.username` and `mqtt.password` must be provided +* `bssid`, `channel`, `ip`, `mask`, `gw`, `dns1`, `dns2` are not mandatory and are only needed to if there is a requirement to specify particular AP or set Static IP address. There are some rules which needs to be satisfied: + - `bssid` and `channel` have to be defined together and these settings are independand of settings related to static IP + - to define static IP, `ip` (IP address), `mask` (netmask) and `gw` (gateway) settings have to be defined at the same time + - to define second DNS `dns2` the first one `dns1` has to be defined. Set DNS without `ip`, `mask` and `gw` does not affect the configuration (dns server will be provided by DHCP). It is not required to set DNS servers. + + +Default values if not provided: + +* `device_id`: the hardware device ID (eg. `1a2b3c4d5e6f`) +* `device_stats_interval`: 60 seconds +* `mqtt.port`: `1883` +* `mqtt.base_topic`: `homie/` +* `mqtt.auth`: `false` + +The `mqtt.host` field can be either an IP or an hostname. diff --git a/docs/index.md b/docs/index.md index 1d24e90b..74a4a83e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,15 +1 @@ -Welcome to the Homie for ESP8266 docs. This will help you to understand the framework and to use it in an effective manner. - -**

This documentation is valid for Homie v1.5.0

** - ------ - -#### 1. [What is it?](1.-What-is-it.md) -#### 2. [Getting started](2.-Getting-started.md) -#### 3. [Advanced usage](3.-Advanced-usage.md) -#### 4. [OTA](4.-OTA.md) -#### 5. [JSON configuration file](5.-JSON-configuration-file.md) -#### 6. [Configuration API](6.-Configuration-API.md) -#### 7. [API reference](7.-API-reference.md) -#### 8. [Limitations and known issues](8.-Limitations-and-known-issues.md) -#### 9. [Troubleshooting](9.-Troubleshooting.md) +Welcome on the Homie for ESP8266 docs. diff --git a/docs/others/community-projects.md b/docs/others/community-projects.md new file mode 100644 index 00000000..faceb1a5 --- /dev/null +++ b/docs/others/community-projects.md @@ -0,0 +1,19 @@ +This page lists the projects made by the community to work with Homie. + +# [jpmens/homie-ota](https://github.com/jpmens/homie-ota) + +homie-ota is written in Python. It provides an OTA server for Homie devices as well as a simple inventory which can be useful to keep track of Homie devices. homie-ota also enables you to trigger an OTA update (over MQTT, using the Homie convention) from within its inventory. New firmware can be uploaded to homie-ota which detects firmware name (fwname) and version (fwversion) from the uploaded binary blob, thanks to an idea and code contributed by Marvin. + +# [stufisher/homie-control](https://github.com/stufisher/homie-control) + +homie-control provides a web UI to manage Homie devices as well as a series of virtual python devices to allow extended functionality. + +Its lets you do useful things like: + +* Historically log device properties +* Schedule changes in event properties (i.e. water your garden once a day) +* Execute profiles of property values (i.e. turn a series of lights on and off simultaneously) +* Trigger property changes based on: + * When a network device is dis/connected (i.e. your phone joins your wifi, turn the lights on) + * Sunset / rise + * When another property changes diff --git a/docs/others/cpp-api-reference.md b/docs/others/cpp-api-reference.md new file mode 100644 index 00000000..1cabf21c --- /dev/null +++ b/docs/others/cpp-api-reference.md @@ -0,0 +1,323 @@ +# Homie + +You don't have to instantiate an `Homie` instance, it is done internally. + +```c++ +void setup(); +``` + +Setup Homie. + +!!! warning "Mandatory!" + Must be called once in `setup()`. + +```c++ +void loop(); +``` + +Handle Homie work. + +!!! warning "Mandatory!" + Must be called once in `loop()`. + +## Functions to call *before* `Homie.setup()` + +```c++ +void Homie_setFirmware(const char* name, const char* version); +// This is not a typo +``` + +Set the name and version of the firmware. This is useful for OTA, as Homie will check against the server if there is a newer version. + +!!! warning "Mandatory!" + You need to set the firmware for your sketch to work. + + +* **`name`**: Name of the firmware. Default value is `undefined` +* **`version`**: Version of the firmware. Default value is `undefined` + +```c++ +void Homie_setBrand(const char* name); +// This is not a typo +``` + +Set the brand of the device, used in the configuration AP, the device hostname and the MQTT client ID. + +* **`name`**: Name of the brand. Default value is `Homie` + +```c++ +Homie& disableLogging(); +``` + +Disable Homie logging. + +```c++ +Homie& setLoggingPrinter(Print* printer); +``` + +Set the Print instance used for logging. + +* **`printer`**: Print instance to log to. By default, `Serial` is used + +!!! warning + It's up to you to call `Serial.begin()` + +```c++ +Homie& disableLedFeedback(); +``` + +Disable the built-in LED feedback indicating the Homie for ESP8266 state. + +```c++ +Homie& setLedPin(uint8_t pin, uint8_t on); +``` + +Set pin of the LED to control. + +* **`pin`**: LED to control +* **`on`**: state when the light is on (HIGH or LOW) + +```c++ +Homie& setConfigurationApPassword(const char* password); +``` + +Set the configuration AP password. + +* **`password`**: the configuration AP password + +```c++ +Homie& setGlobalInputHandler(std::function handler); +``` + +Set input handler for subscribed properties. + +* **`handler`**: Global input handler +* **`node`**: Name of the node getting updated +* **`property`**: Property of the node getting updated +* **`range`**: Range of the property of the node getting updated +* **`value`**: Value of the new property + +```c++ +Homie& setBroadcastHandler(std::function handler); +``` + +Set broadcast handler. + +* **`handler`**: Broadcast handler +* **`level`**: Level of the broadcast +* **`value`**: Value of the broadcast + +```c++ +Homie& onEvent(std::function callback); +``` + +Set the event handler. Useful if you want to hook to Homie events. + +* **`callback`**: Event handler + +```c++ +Homie& setResetTrigger(uint8_t pin, uint8_t state, uint16_t time); +``` + +Set the reset trigger. By default, the device will reset when pin `0` is `LOW` for `5000`ms. + +* **`pin`**: Pin of the reset trigger +* **`state`**: Reset when the pin reaches this state for the given time +* **`time`**: Time necessary to reset + +```c++ +Homie& disableResetTrigger(); +``` + +Disable the reset trigger. + +```c++ +Homie& setSetupFunction(std::function callback); +``` + +You can provide the function that will be called when operating in `normal` mode. + +* **`callback`**: Setup function + +```c++ +Homie& setLoopFunction(std::function callback); +``` + +You can provide the function that will be looped in normal mode. + +* **`callback`**: Loop function + +```c++ +Homie& setStandalone(); +``` + +This will mark the Homie firmware as standalone, meaning it will first boot in `standalone` mode. To configure it and boot to `configuration` mode, the device has to be resetted. + +## Functions to call *after* `Homie.setup()` + +```c++ +void reset(); +``` + +Flag the device for reset. + +```c++ +void setIdle(bool idle); +``` + +Set the device as idle or not. This is useful at runtime, because you might want the device not to be resettable when you have another library that is doing some unfinished work, like moving shutters for example. + +* **`idle`**: Device in an idle state or not + +```c++ +void prepareToSleep(); +``` + +Prepare the device for deep sleep. It ensures messages are sent and disconnects cleanly from the MQTT broker, triggering a `READY_TO_SLEEP` event when done. + +```c++ +void doDeepSleep(uint32_t time_us = 0, RFMode mode = RF_DEFAULT); +``` + +Puth the device into deep sleep. It ensures the Serial is flushed. + +```c++ +bool isConfigured() const; +``` + +Is the device in `normal` mode, configured? + +```c++ +bool isConnected() const; +``` + +Is the device in `normal` mode, configured and connected? + +```c++ +const ConfigStruct& getConfiguration() const; +``` + +Get the configuration struct. + +!!! danger + Be careful with this struct, never attempt to change it. + +```c++ +AsyncMqttClient& getMqttClient(); +``` + +Get the underlying `AsyncMqttClient` object. + +```c++ +Logger& getLogger(); +``` + +Get the underlying `Logger` object, which is only a wrapper around `Serial` by default. + +------- + +# HomieNode + +```c++ +HomieNode(const char* id, const char* type, std::function handler = ); +``` + +Constructor of an HomieNode object. + +* **`id`**: ID of the node +* **`type`**: Type of the node +* **`handler`**: Optional. Input handler of the node + +```c++ +const char* getId() const; +``` + +Return the ID of the node. + +```c++ +const char* getType() const; +``` + +Return the type of the node. + +```c++ +PropertyInterface& advertise(const char* property); +PropertyInterface& advertiseRange(const char* property, uint16_t lower, uint16_t upper); +``` + +Advertise a property / range property on the node. + +* **`property`**: Property to advertise +* **`lower`**: Lower bound of the range +* **`upper`**: Upper bound of the range + +This returns a reference to `PropertyInterface` on which you can call: + +```c++ +void settable(std::function handler) = ); +``` + +Make the property settable. + +* **`handler`**: Optional. Input handler of the property + +```c++ +SendingPromise& setProperty(const String& property); +``` + +Using this function, you can set the value of a node property, like a temperature for example. + +* **`property`**: Property to send + +This returns a reference to `SendingPromise`, on which you can call: + +```c++ +SendingPromise& setQos(uint8_t qos); // defaults to 1 +SendingPromise& setRetained(bool retained); // defaults to true +SendingPromise& overwriteSetter(bool overwrite); // defaults to false +SendingPromise& setRange(const HomieRange& range); // defaults to not a range +SendingPromise& setRange(uint16_t rangeIndex); // defaults to not a range +uint16_t send(const String& value); // finally send the property, return the packetId (or 0 if failure) +``` + +Method names should be self-explanatory. + +# HomieSetting + +```c++ +HomieSetting(const char* name, const char* description); +``` + +Constructor of an HomieSetting object. + +* **`T`**: Type of the setting. Either `bool`, `unsigned long`, `long`, `double` or `const char*` +* **`name`**: Name of the setting +* **`description`**: Description of the setting + +```c++ +T get() const; +``` + +Get the default value if the setting is optional and not provided, or the provided value if the setting is required or optional but provided. + +```c++ +bool wasProvided() const; +``` + +Return whether the setting was provided or not (otherwise `get()` would return the default value). + +Set the default value and make the setting optional. + +```c++ +HomieSetting& setDefaultValue(T defaultValue); +``` + +* **`defaultValue`**: The default value + +```c++ +HomieSetting& setValidator(std::function validator); +``` + +Set a validation function for the setting. The validator must return `true` if the candidate is correct, `false` otherwise. + +* **`validator`**: The validation function diff --git a/docs/others/homie-implementation-specifics.md b/docs/others/homie-implementation-specifics.md new file mode 100644 index 00000000..0f1fbc67 --- /dev/null +++ b/docs/others/homie-implementation-specifics.md @@ -0,0 +1,30 @@ +The Homie `$implementation` identifier is `esp8266`. + +# Version + +* `$implementation/version`: Homie for ESP8266 version + +# Reset + +* `$implementation/reset`: You can publish a `true` to this topic to reset the device + +# Configuration + +* `$implementation/config`: The `configuration.json` is published there, with `wifi.password`, `mqtt.username` and `mqtt.password` fields stripped +* `$implementation/config/set`: You can update the `configuration.json` by sending incremental JSON on this topic + +# OTA + +* `$implementation/ota/enabled`: `true` if OTA is enabled, `false` otherwise +* `$implementation/ota/firmware`: If the update request is accepted, you must send the firmware payload to this topic +* `$implementation/ota/status`: HTTP-like status code indicating the status of the OTA. Might be: + +Code|Description +----|----------- +`200`|OTA successfully flashed +`202`|OTA request / checksum accepted +`206 465/349680`|OTA in progress. The data after the status code corresponds to `/` +`304`|The current firmware is already up-to-date +`400 BAD_FIRMWARE`|OTA error from your side. The identifier might be `BAD_FIRMWARE`, `BAD_CHECKSUM`, `NOT_ENOUGH_SPACE`, `NOT_REQUESTED` +`403`|OTA not enabled +`500 FLASH_ERROR`|OTA error on the ESP8266. The identifier might be `FLASH_ERROR` diff --git a/docs/others/limitations-and-known-issues.md b/docs/others/limitations-and-known-issues.md new file mode 100644 index 00000000..4849a012 --- /dev/null +++ b/docs/others/limitations-and-known-issues.md @@ -0,0 +1,11 @@ +# SSL support + +In Homie for ESP8266 v1.x, SSL was possible but it was not reliable. Due to the asynchronous nature of the v2.x, SSL is not available anymore. + +# ADC readings + +[This is a known esp8266/Arduino issue](https://github.com/esp8266/Arduino/issues/1634) that polling `analogRead()` too frequently forces the Wi-Fi to disconnect. As a workaround, don't poll the ADC more than one time every 3ms. + +# Wi-Fi connection + +If you encouter any issues with the Wi-Fi, try changing the flash size build parameter, or try to erase the flash. See [#158](https://github.com/marvinroger/homie-esp8266/issues/158) for more information. diff --git a/docs/others/ota-configuration-updates.md b/docs/others/ota-configuration-updates.md new file mode 100644 index 00000000..100ea920 --- /dev/null +++ b/docs/others/ota-configuration-updates.md @@ -0,0 +1,61 @@ +# OTA updates + +Homie for ESP8266 supports OTA, if enabled in the configuration, and if a compatible OTA entity is set up. + +There's a script that does just that: + +[![GitHub logo](../assets/github.png) ota_updater.py](https://github.com/marvinroger/homie-esp8266/blob/develop/scripts/ota_updater) + +It works this way: + +1. During startup of the Homie for ESP8266 device, it reports the current firmware's MD5 to `$fw/checksum` (in addition to `$fw/name` and `$fw/version`). The OTA entity may or may not use this information to automatically schedule OTA updates +2. The OTA entity publishes the latest available firmware payload to `$implementation/ota/firmware/`, either as binary or as a Base64 encoded string + * If OTA is disabled, Homie for ESP8266 reports `403` to `$implementation/ota/status` and aborts the OTA + * If OTA is enabled and the latest available checksum is the same as what is currently running, Homie for ESP8266 reports `304` and aborts the OTA + * If the checksum is not a valid MD5, Homie for ESP8266 reports `400 BAD_CHECKSUM` to `$implementation/ota/status` and aborts the OTA +3. Homie starts to flash the firmware + * The firmware is updating. Homie for ESP8266 reports progress with `206 /` + * When all bytes are flashed, the firmware is verified (including the MD5 if one was set) + * Homie for ESP8266 either reports `200` on success, `400` if the firmware in invalid or `500` if there's an internal error +5. Homie for ESP8266 reboots on success as soon as the device is idle + +See [Homie implementation specifics](homie-implementation-specifics.md) for more details on status codes. + +## OTA entities projects + +See [Community projects](community-projects.md). + +# Configuration updates + +In `normal` mode, you can get the current `config.json`, published on `$implementation/config` with `wifi.password`, `mqtt.username` and `mqtt.password` stripped. You can update the configuration on-the-fly by publishing incremental JSON updates to `$implementation/config/set`. For example, given the following `config.json`: + +```json +{ + "name": "Kitchen light", + "wifi": { + "ssid": "Network_1", + "password": "I'm a Wi-Fi password!" + }, + "mqtt": { + "host": "192.168.1.20", + "port": 1883 + }, + "ota": { + "enabled": false + }, + "settings": { + + } +} +``` + +You can update the name and Wi-Fi password by sending the following incremental JSON: + +```json +{ + "name": "Living room light", + "wifi": { + "password": "I'am a new Wi-Fi password!" + } +} +``` diff --git a/docs/9.-Troubleshooting.md b/docs/others/troubleshooting.md similarity index 81% rename from docs/9.-Troubleshooting.md rename to docs/others/troubleshooting.md index 7c404fb5..0dc71b79 100644 --- a/docs/9.-Troubleshooting.md +++ b/docs/others/troubleshooting.md @@ -1,5 +1,3 @@ -# Troubleshooting - ## 1. I see some garbage on the Serial monitor? You are probably using a generic ESP8266. The problem with these modules is the built-in LED is tied to the serial line. You can do two things: @@ -12,7 +10,8 @@ void setup() { // ... } ``` -* Disable the the LED blinking, to have the serial line working: + +* Disable the LED blinking, to have the serial line working: ```c++ void setup() { @@ -31,8 +30,8 @@ void setup() { ## 3. The network is completely unstable... What's going on? -The framework needs to work continuously (ie. `Homie.loop()` needs to be called very frequently). In other words, don't use `delay()` (see [avoid delay](http://playground.arduino.cc/Code/AvoidDelay)) or anything that might block the code for more than 50ms or so. There is also a known Arduino for ESP8266 issue with `analogRead()`, see [Limitations and known issues#adc-readings](8.-Limitations-and-known-issues.md#adc-readings). +The framework needs to work continuously (ie. `Homie.loop()` needs to be called very frequently). In other words, don't use `delay()` (see [avoid delay](http://playground.arduino.cc/Code/AvoidDelay)) or anything that might block the code for more than 50ms or so. There is also a known Arduino for ESP8266 issue with `analogRead()`, see [Limitations and known issues](limitations-and-known-issues.md#adc-readings). ## 4. My device resets itself without me doing anything? -You have probably connected a sensor to the default reset pin of the framework (D3 on NodeMCU, GPIO0 on other boards). See [Advanced usage#reset](3.-Advanced-usage.md#reset). +You have probably connected a sensor to the default reset pin of the framework (D3 on NodeMCU, GPIO0 on other boards). See [Resetting](../advanced-usage/resetting.md). diff --git a/docs/others/upgrade-guide-from-v1-to-v2.md b/docs/others/upgrade-guide-from-v1-to-v2.md new file mode 100644 index 00000000..e7a69526 --- /dev/null +++ b/docs/others/upgrade-guide-from-v1-to-v2.md @@ -0,0 +1,15 @@ +This is an upgrade guide to upgrade your Homie devices from v1 to v2. + +## New convention + +The Homie convention has been revised to v2 to be more extensible and introspectable. Be sure to [check it out](https://github.com/marvinroger/homie/tree/v2). + +## API changes in the sketch + +1. `Homie.setFirmware(name, version)` must be replaced by `Homie_setFirmware(name, version)` +2. `Homie.setBrand(brand)` must be replaced by `Homie_setBrand(brand)` +3. `Homie.registerNode()` must be removed, nodes are now automagically registered +4. If you've enabled Serial logging, `Serial.begin()` must be called explicitely in your sketch +5. Remove the `HOMIE_OTA_MODE` in your event handler, if you have one +6. The `Homie.setNodeProperty()` signature changed completely. If you had `Homie.setNodeProperty(node, "property", "value", true)`, the new equivalent syntax is `Homie.setNodeProperty(node, "property").setRetained(true).send("value")`. Note the `setRetained()` is not even required as messages are retained by default. +7. TODO diff --git a/docs/quickstart/getting-started.md b/docs/quickstart/getting-started.md new file mode 100644 index 00000000..709ef514 --- /dev/null +++ b/docs/quickstart/getting-started.md @@ -0,0 +1,142 @@ +This *Getting Started* guide assumes you have an ESP8266 board with an user-configurable LED, and an user programmable button, like a NodeMCU DevKit 1.0, for example. These restrictions can be lifted (see next pages). + +To use Homie for ESP8266, you will need: + +* An ESP8266 +* The Arduino IDE for ESP8266 (version 2.3.0 minimum) +* Basic knowledge of the Arduino environment (upload a sketch, import libraries, ...) +* To understand [the Homie convention](https://github.com/marvinroger/homie) + +## Installing Homie for ESP8266 + +There are two ways to install Homie for ESP8266. + +### 1a. For the Arduino IDE + +There is a YouTube video with instructions: + +[![YouTube logo](../assets/youtube.png) How to install Homie libraries on Arduino IDE](https://www.youtube.com/watch?v=bH3KfFfYUvg) + +1. Download the [release corresponding to this documentation version](https://github.com/marvinroger/homie-esp8266/releases) + +2. Load the `.zip` with **Sketch → Include Library → Add .ZIP Library** + +Homie for ESP8266 has 5 dependencies: + +* [ArduinoJson](https://github.com/bblanchon/ArduinoJson) >= 5.0.8 +* [Bounce2](https://github.com/thomasfredericks/Bounce2) +* [ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) >= [c8ed544](https://github.com/me-no-dev/ESPAsyncTCP) +* [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) +* [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) + +Some of them are available through the Arduino IDE, with **Sketch → Include Library → Manage Libraries**. For the others, install it by downloading the `.zip` on GitHub. + +### 1b. With [PlatformIO](http://platformio.org) + +In a terminal, run `platformio lib install 555`. + +!!! warning "Not yet released as stable" + The above command is for when the v2 is stable and released. Currently, the latest stable version is 1.5. In the meantime, use the develop branch to get started with the v2, add this in your **platformio.ini**: + + ``` + lib_deps = git+https://github.com/marvinroger/homie-esp8266.git#develop + ``` + +Dependencies are installed automatically. + +## Bare minimum sketch + +```c++ +#include + +void setup() { + Serial.begin(115200); + Serial << endl << endl; + + Homie_setFirmware("bare-minimum", "1.0.0"); // The underscore is not a typo! See Magic bytes + Homie.setup(); +} + +void loop() { + Homie.loop(); +} +``` + + +This is the bare minimum needed for Homie for ESP8266 to work correctly. + +!!! tip "LED" + ![Solid LED](../assets/led_solid.gif) + If you upload this sketch, you will notice the LED of the ESP8266 will light on. This is because you are in `configuration` mode. + +Homie for ESP8266 has 3 modes of operation: + +1. By default, the `configuration` mode is the initial one. It spawns an AP and an HTTP webserver exposing a JSON API. To interact with it, you have to connect to the AP. Then, an HTTP client can get the list of available Wi-Fi networks and send the configuration (like the Wi-Fi SSID, the Wi-Fi password, some settings...). Once the device receives the credentials, it boots into `normal` mode. + +2. The `normal` mode is the mode the device will be most of the time. It connects to the Wi-Fi, to the MQTT, it sends initial informations to the Homie server (like the local IP, the version of the firmware currently running...) and it subscribes to the needed MQTT topics. It automatically reconnects to the Wi-Fi and the MQTT when the connection is lost. It also handle the OTA. The device can return to `configuration` mode in different ways (press of a button or custom function, see [Resetting](../advanced-usage/resetting.md)). + +3. The `standalone` mode. See [Standalone mode](../advanced-usage/standalone-mode.md). + +!!! warning + **As a rule of thumb, never block the device with blocking code for more than 50ms or so.** Otherwise, you may very probably experience unexpected behaviors. + +## Connecting to the AP and configuring the device + +Homie for ESP8266 has spawned a secure AP named `Homie-xxxxxxxxxxxx`, like `Homie-c631f278df44`. Connect to it. + +!!! tip "Hardware device ID" + This `c631f278df44` ID is unique to each device, and you cannot change it (this is actually the MAC address of the station mode). If you flash a new sketch, this ID won't change. + +Once connected, the webserver is available at `http://192.168.123.1`. Every domain name is resolved by the built-in DNS server to this address. You can then configure the device using the [HTTP JSON API](../configuration/http-json-api.md). When the device receives its configuration, it will reboot into `normal` mode. + +## Understanding what happens in `normal` mode + +### Visual codes + +When the device boots in `normal` mode, it will start blinking: + +!!! tip "LED" + ![Slowly blinking LED](../assets/led_wifi.gif) + Slowly when connecting to the Wi-Fi + +!!! tip "LED" + ![Fast blinking LED](../assets/led_mqtt.gif) + Faster when connecting to the MQTT broker + +This way, you can have a quick feedback on what's going on. If both connections are established, the LED will stay off. Note the device will also blink during the automatic reconnection, if the connection to the Wi-Fi or the MQTT broker is lost. + +### Under the hood + +Although the sketch looks like it does not do anything, it actually does quite a lot: + +* It automatically connects to the Wi-Fi and MQTT broker. No more network boilerplate code +* It exposes the Homie device on MQTT (as `/`, e.g. `homie/c631f278df44`) +* It subscribes to the special OTA and configuration topics, automatically flashing a sketch if available or updating the configuration +* It checks for a button press on the ESP8266, to return to `configuration` mode + +## Creating an useful sketch + +Now that we understand how Homie for ESP8266 works, let's create an useful sketch. We want to create a smart light. + +[![GitHub logo](../assets/github.png) LightOnOff.ino](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/LightOnOff/LightOnOff.ino) + +Alright, step by step: + +1. We create a node with an ID of `light` and a type of `switch` with `HomieNode lightNode("light", "switch")` +2. We set the name and the version of the firmware with `Homie_setFirmware("awesome-light" ,"1.0.0");` +3. We want our `light` node to advertise an `on` property, which is settable. We do that with `lightNode.advertise("on").settable(lightOnHandler);`. The `lightOnHandler` function will be called when the value of this property is changed +4. In the `lightOnHandler` function, we want to update the state of the `light` node. We do this with `lightNode.setProperty("on").send("true");` + +In about thirty SLOC, we have achieved to create a smart light, without any hard-coded credentials, with automatic reconnection in case of network failure, and with OTA support. Not bad, right? + +## Creating a sensor node + +In the previous example sketch, we were reacting to property changes. But what if we want, for example, to send a temperature every 5 minutes? We could do this in the Arduino `loop()` function. But then, we would have to check if we are in `normal` mode, and we would have to ensure the network connection is up before being able to send anything. Boring. + +Fortunately, Homie for ESP8266 provides an easy way to do that. + +[![GitHub logo](../assets/github.png) TemperatureSensor.ino](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/TemperatureSensor/TemperatureSensor.ino) + +The only new things here are the `Homie.setSetupFunction(setupHandler);` and `Homie.setLoopFunction(loopHandler);` calls. The setup function will be called once, when the device is in `normal` mode and the network connection is up. The loop function will be called everytime, when the device is in `normal` mode and the network connection is up. This provides a nice level of abstraction. + +Now that you understand the basic usage of Homie for ESP8266, you can head on to the next pages to learn about more powerful features like input handlers, the event system and custom settings. diff --git a/docs/quickstart/what-is-it.md b/docs/quickstart/what-is-it.md new file mode 100644 index 00000000..87303ee2 --- /dev/null +++ b/docs/quickstart/what-is-it.md @@ -0,0 +1,3 @@ +Homie for ESP8266 is an ESP8266 for Arduino implementation of [Homie](https://github.com/marvinroger/homie), a thin and simple MQTT convention for the IoT. More than that, it's also a full-featured framework to get started with your IoT project very quickly. Simply put, you don't have to manage yourself the connection/reconnection to the Wi-Fi/MQTT. You don't even have to hard-code credentials in your sketch: this can be done using a simple JSON API. Everything is handled internally, by Homie for ESP8266. + +You guessed it, the purpose of Homie for ESP8266 is to simplify the development of connected objects. diff --git a/examples/Broadcast/Broadcast.ino b/examples/Broadcast/Broadcast.ino new file mode 100644 index 00000000..91e1ecf1 --- /dev/null +++ b/examples/Broadcast/Broadcast.ino @@ -0,0 +1,19 @@ +#include + +bool broadcastHandler(const String& level, const String& value) { + Homie.getLogger() << "Received broadcast level " << level << ": " << value << endl; + return true; +} + +void setup() { + Serial.begin(115200); + Serial << endl << endl; + Homie_setFirmware("broadcast-test", "1.0.0"); + Homie.setBroadcastHandler(broadcastHandler); + + Homie.setup(); +} + +void loop() { + Homie.loop(); +} diff --git a/examples/CustomSettings/CustomSettings.ino b/examples/CustomSettings/CustomSettings.ino new file mode 100644 index 00000000..56dad63e --- /dev/null +++ b/examples/CustomSettings/CustomSettings.ino @@ -0,0 +1,42 @@ +#include + +const int DEFAULT_TEMPERATURE_INTERVAL = 300; + +unsigned long lastTemperatureSent = 0; + +HomieNode temperatureNode("temperature", "temperature"); + +HomieSetting temperatureIntervalSetting("temperatureInterval", "The temperature interval in seconds"); + +void setupHandler() { + temperatureNode.setProperty("unit").send("c"); +} + +void loopHandler() { + if (millis() - lastTemperatureSent >= temperatureIntervalSetting.get() * 1000UL || lastTemperatureSent == 0) { + float temperature = 22; // Fake temperature here, for the example + Homie.getLogger() << "Temperature: " << temperature << " °C" << endl; + temperatureNode.setProperty("degrees").send(String(temperature)); + lastTemperatureSent = millis(); + } +} + +void setup() { + Serial.begin(115200); + Serial << endl << endl; + Homie_setFirmware("temperature-setting", "1.0.0"); + Homie.setSetupFunction(setupHandler).setLoopFunction(loopHandler); + + temperatureNode.advertise("unit"); + temperatureNode.advertise("degrees"); + + temperatureIntervalSetting.setDefaultValue(DEFAULT_TEMPERATURE_INTERVAL).setValidator([] (long candidate) { + return candidate > 0; + }); + + Homie.setup(); +} + +void loop() { + Homie.loop(); +} diff --git a/examples/DoorSensor/DoorSensor.ino b/examples/DoorSensor/DoorSensor.ino index f91aa174..617d4740 100644 --- a/examples/DoorSensor/DoorSensor.ino +++ b/examples/DoorSensor/DoorSensor.ino @@ -11,26 +11,26 @@ void loopHandler() { int doorValue = debouncer.read(); if (doorValue != lastDoorValue) { - Serial.print("Door is now: "); - Serial.println(doorValue ? "open" : "close"); - - if (Homie.setNodeProperty(doorNode, "open", doorValue ? "true" : "false", true)) { - lastDoorValue = doorValue; - } else { - Serial.println("Sending failed"); - } + Homie.getLogger() << "Door is now " << (doorValue ? "open" : "close") << endl; + + doorNode.setProperty("open").send(doorValue ? "true" : "false"); + lastDoorValue = doorValue; } } void setup() { + Serial.begin(115200); + Serial << endl << endl; pinMode(PIN_DOOR, INPUT); digitalWrite(PIN_DOOR, HIGH); debouncer.attach(PIN_DOOR); debouncer.interval(50); - Homie.setFirmware("awesome-door", "1.0.0"); - Homie.registerNode(doorNode); + Homie_setFirmware("awesome-door", "1.0.0"); Homie.setLoopFunction(loopHandler); + + doorNode.advertise("open"); + Homie.setup(); } diff --git a/examples/GlobalInputHandler/GlobalInputHandler.ino b/examples/GlobalInputHandler/GlobalInputHandler.ino new file mode 100644 index 00000000..841612db --- /dev/null +++ b/examples/GlobalInputHandler/GlobalInputHandler.ino @@ -0,0 +1,23 @@ +#include + +HomieNode lightNode("light", "switch"); + +bool globalInputHandler(const HomieNode& node, const String& property, const HomieRange& range, const String& value) { + Homie.getLogger() << "Received on node " << node.getId() << ": " << property << " = " << value << endl; + return true; +} + +void setup() { + Serial.begin(115200); + Serial << endl << endl; + Homie_setFirmware("global-input-handler", "1.0.0"); + Homie.setGlobalInputHandler(globalInputHandler); + + lightNode.advertise("on").settable(); + + Homie.setup(); +} + +void loop() { + Homie.loop(); +} diff --git a/examples/HookToEvents/HookToEvents.ino b/examples/HookToEvents/HookToEvents.ino index 4b0bc532..5e5f9e6e 100644 --- a/examples/HookToEvents/HookToEvents.ino +++ b/examples/HookToEvents/HookToEvents.ino @@ -1,40 +1,57 @@ #include -void onHomieEvent(HomieEvent event) { - switch(event) { - case HOMIE_CONFIGURATION_MODE: - Serial.println("Configuration mode started"); +void onHomieEvent(const HomieEvent& event) { + switch (event.type) { + case HomieEventType::STANDALONE_MODE: + Serial << "Standalone mode started" << endl; break; - case HOMIE_NORMAL_MODE: - Serial.println("Normal mode started"); + case HomieEventType::CONFIGURATION_MODE: + Serial << "Configuration mode started" << endl; break; - case HOMIE_OTA_MODE: - Serial.println("OTA mode started"); + case HomieEventType::NORMAL_MODE: + Serial << "Normal mode started" << endl; break; - case HOMIE_ABOUT_TO_RESET: - Serial.println("About to reset"); + case HomieEventType::OTA_STARTED: + Serial << "OTA started" << endl; break; - case HOMIE_WIFI_CONNECTED: - Serial.println("Wi-Fi connected"); + case HomieEventType::OTA_PROGRESS: + Serial << "OTA progress, " << event.sizeDone << "/" << event.sizeTotal << endl; break; - case HOMIE_WIFI_DISCONNECTED: - Serial.println("Wi-Fi disconnected"); + case HomieEventType::OTA_FAILED: + Serial << "OTA failed" << endl; break; - case HOMIE_MQTT_CONNECTED: - Serial.println("MQTT connected"); + case HomieEventType::OTA_SUCCESSFUL: + Serial << "OTA successful" << endl; break; - case HOMIE_MQTT_DISCONNECTED: - Serial.println("MQTT disconnected"); + case HomieEventType::ABOUT_TO_RESET: + Serial << "About to reset" << endl; + break; + case HomieEventType::WIFI_CONNECTED: + Serial << "Wi-Fi connected, IP: " << event.ip << ", gateway: " << event.gateway << ", mask: " << event.mask << endl; + break; + case HomieEventType::WIFI_DISCONNECTED: + Serial << "Wi-Fi disconnected, reason: " << (int8_t)event.wifiReason << endl; + break; + case HomieEventType::MQTT_READY: + Serial << "MQTT connected" << endl; + break; + case HomieEventType::MQTT_DISCONNECTED: + Serial << "MQTT disconnected, reason: " << (int8_t)event.mqttReason << endl; + break; + case HomieEventType::MQTT_PACKET_ACKNOWLEDGED: + Serial << "MQTT packet acknowledged, packetId: " << event.packetId << endl; + break; + case HomieEventType::READY_TO_SLEEP: + Serial << "Ready to sleep" << endl; break; } } void setup() { Serial.begin(115200); - Serial.println(); - Serial.println(); - Homie.enableLogging(false); - Homie.setFirmware("events-test", "1.0.0"); + Serial << endl << endl; + Homie.disableLogging(); + Homie_setFirmware("events-test", "1.0.0"); Homie.onEvent(onHomieEvent); Homie.setup(); } diff --git a/examples/IteadSonoff/IteadSonoff.ino b/examples/IteadSonoff/IteadSonoff.ino index b4b465be..0110d7d1 100644 --- a/examples/IteadSonoff/IteadSonoff.ino +++ b/examples/IteadSonoff/IteadSonoff.ino @@ -8,31 +8,28 @@ const int PIN_BUTTON = 0; HomieNode switchNode("switch", "switch"); -bool switchOnHandler(String value) { - if (value == "true") { - digitalWrite(PIN_RELAY, HIGH); - Homie.setNodeProperty(switchNode, "on", "true"); - Serial.println("Switch is on"); - } else if (value == "false") { - digitalWrite(PIN_RELAY, LOW); - Homie.setNodeProperty(switchNode, "on", "false"); - Serial.println("Switch is off"); - } else { - return false; - } +bool switchOnHandler(const HomieRange& range, const String& value) { + if (value != "true" && value != "false") return false; + + bool on = (value == "true"); + digitalWrite(PIN_RELAY, on ? HIGH : LOW); + switchNode.setProperty("on").send(value); + Homie.getLogger() << "Switch is " << (on ? "on" : "off") << endl; return true; } void setup() { + Serial.begin(115200); + Serial << endl << endl; pinMode(PIN_RELAY, OUTPUT); digitalWrite(PIN_RELAY, LOW); - Homie.setFirmware("itead-sonoff", "1.0.0"); - Homie.setLedPin(PIN_LED, LOW); - Homie.setResetTrigger(PIN_BUTTON, LOW, 5000); - switchNode.subscribe("on", switchOnHandler); - Homie.registerNode(switchNode); + Homie_setFirmware("itead-sonoff", "1.0.0"); + Homie.setLedPin(PIN_LED, LOW).setResetTrigger(PIN_BUTTON, LOW, 5000); + + switchNode.advertise("on").settable(switchOnHandler); + Homie.setup(); } diff --git a/examples/IteadSonoffButton/IteadSonoffButton.ino b/examples/IteadSonoffButton/IteadSonoffButton.ino new file mode 100644 index 00000000..3279a2cb --- /dev/null +++ b/examples/IteadSonoffButton/IteadSonoffButton.ino @@ -0,0 +1,75 @@ +/* + * Tested with "WiFi Smart Socket ESP8266 MQTT" + * and "Sonoff - WiFi Wireless Smart Switch ESP8266 MQTT" + * + * The Relay could be toggeled with the physical pushbutton +*/ + +#include + +const int PIN_RELAY = 12; +const int PIN_LED = 13; +const int PIN_BUTTON = 0; + +unsigned long buttonDownTime = 0; +byte lastButtonState = 1; +byte buttonPressHandled = 0; + +HomieNode switchNode("switch", "switch"); + +bool switchOnHandler(HomieRange range, String value) { + if (value != "true" && value != "false") return false; + + bool on = (value == "true"); + digitalWrite(PIN_RELAY, on ? HIGH : LOW); + switchNode.setProperty("on").send(value); + Homie.getLogger() << "Switch is " << (on ? "on" : "off") << endl; + + return true; +} + +void toggleRelay() { + bool on = digitalRead(PIN_RELAY) == HIGH; + digitalWrite(PIN_RELAY, on ? LOW : HIGH); + switchNode.setProperty("on").send(on ? "false" : "true"); + Homie.getLogger() << "Switch is " << (on ? "off" : "on") << endl; +} + +void loopHandler() { + byte buttonState = digitalRead(PIN_BUTTON); + if ( buttonState != lastButtonState ) { + if (buttonState == LOW) { + buttonDownTime = millis(); + buttonPressHandled = 0; + } + else { + unsigned long dt = millis() - buttonDownTime; + if ( dt >= 90 && dt <= 900 && buttonPressHandled == 0 ) { + toggleRelay(); + buttonPressHandled = 1; + } + } + lastButtonState = buttonState; + } +} + +void setup() { + Serial.begin(115200); + Serial.println(); + Serial.println(); + pinMode(PIN_RELAY, OUTPUT); + pinMode(PIN_BUTTON, INPUT); + digitalWrite(PIN_RELAY, LOW); + + Homie_setFirmware("itead-sonoff-buton", "1.0.0"); + Homie.setLedPin(PIN_LED, LOW).setResetTrigger(PIN_BUTTON, LOW, 5000); + + switchNode.advertise("on").settable(switchOnHandler); + + Homie.setLoopFunction(loopHandler); + Homie.setup(); +} + +void loop() { + Homie.loop(); +} diff --git a/examples/LedStrip/LedStrip.ino b/examples/LedStrip/LedStrip.ino index c450c29f..c74d530a 100644 --- a/examples/LedStrip/LedStrip.ino +++ b/examples/LedStrip/LedStrip.ino @@ -3,48 +3,37 @@ const unsigned char NUMBER_OF_LED = 4; const unsigned char LED_PINS[NUMBER_OF_LED] = { 16, 5, 4, 0 }; -bool stripHandler(String, String); // forward declaration (needed for Arduino <= 1.6.8) -HomieNode stripNode("ledstrip", "ledstrip", stripHandler, true); // last true: subscribe to all properties - -bool stripHandler(String property, String value) { - for (int i = 0; i < property.length(); i++) { - if (!isDigit(property.charAt(i))) { - return false; - } - } +HomieNode stripNode("strip", "strip"); - int ledIndex = property.toInt(); - if (ledIndex < 0 || ledIndex > NUMBER_OF_LED - 1) { - return false; - } +bool stripLedHandler(const HomieRange& range, const String& value) { + if (!range.isRange) return false; // if it's not a range - if (value == "true") { - digitalWrite(LED_PINS[ledIndex], HIGH); - Homie.setNodeProperty(stripNode, String(ledIndex), "true"); // Update the state of the led - Serial.print("Led "); - Serial.print(ledIndex); - Serial.println(" is on"); - } else if (value == "false") { - digitalWrite(LED_PINS[ledIndex], LOW); - Homie.setNodeProperty(stripNode, String(ledIndex), "false"); - Serial.print("Led "); - Serial.print(ledIndex); - Serial.println(" is off"); - } else { - return false; - } + if (range.index < 1 || range.index > NUMBER_OF_LED) return false; // if it's not a valid range + + if (value != "on" && value != "off") return false; // if the value is not valid + + bool on = (value == "on"); + + digitalWrite(LED_PINS[range.index - 1], on ? HIGH : LOW); + stripNode.setProperty("led").setRange(range).send(value); // Update the state of the led + Homie.getLogger() << "Led " << range.index << " is " << value << endl; return true; } void setup() { for (int i = 0; i < NUMBER_OF_LED; i++) { - pinMode(LED_PINS[i], INPUT); + pinMode(LED_PINS[i], OUTPUT); digitalWrite(LED_PINS[i], LOW); } - Homie.setFirmware("awesome-ledstrip", "1.0.0"); - Homie.registerNode(stripNode); + Serial.begin(115200); + Serial << endl << endl; + + Homie_setFirmware("awesome-ledstrip", "1.0.0"); + + stripNode.advertiseRange("led", 1, NUMBER_OF_LED).settable(stripLedHandler); + Homie.setup(); } diff --git a/examples/LightOnOff/LightOnOff.ino b/examples/LightOnOff/LightOnOff.ino index fce676f3..42d43838 100644 --- a/examples/LightOnOff/LightOnOff.ino +++ b/examples/LightOnOff/LightOnOff.ino @@ -4,29 +4,27 @@ const int PIN_RELAY = 5; HomieNode lightNode("light", "switch"); -bool lightOnHandler(String value) { - if (value == "true") { - digitalWrite(PIN_RELAY, HIGH); - Homie.setNodeProperty(lightNode, "on", "true"); // Update the state of the light - Serial.println("Light is on"); - } else if (value == "false") { - digitalWrite(PIN_RELAY, LOW); - Homie.setNodeProperty(lightNode, "on", "false"); - Serial.println("Light is off"); - } else { - return false; - } +bool lightOnHandler(const HomieRange& range, const String& value) { + if (value != "true" && value != "false") return false; + + bool on = (value == "true"); + digitalWrite(PIN_RELAY, on ? HIGH : LOW); + lightNode.setProperty("on").send(value); + Homie.getLogger() << "Light is " << (on ? "on" : "off") << endl; return true; } void setup() { + Serial.begin(115200); + Serial << endl << endl; pinMode(PIN_RELAY, OUTPUT); digitalWrite(PIN_RELAY, LOW); - Homie.setFirmware("awesome-relay", "1.0.0"); - lightNode.subscribe("on", lightOnHandler); - Homie.registerNode(lightNode); + Homie_setFirmware("awesome-relay", "1.0.0"); + + lightNode.advertise("on").settable(lightOnHandler); + Homie.setup(); } diff --git a/examples/SonoffDualShutters/SonoffDualShutters.ino b/examples/SonoffDualShutters/SonoffDualShutters.ino new file mode 100644 index 00000000..09e12c61 --- /dev/null +++ b/examples/SonoffDualShutters/SonoffDualShutters.ino @@ -0,0 +1,128 @@ +/* + +# Homie enabled Sonoff Dual shutters + +Requires the Shutters library: +https://github.com/marvinroger/arduino-shutters +and the SonoffDual library: +https://github.com/marvinroger/arduino-sonoff-dual + +## Features + +* Do a short press to close shutters +if level != 0 or open shutters if level == 0 +* Do a long press to reset + +*/ + +#include + +#include +#include +#include + +const unsigned long COURSE_TIME = 30 * 1000; +const float CALIBRATION_RATIO = 0.1; + +const bool RELAY1_MOVE = true; +const bool RELAY1_STOP = false; + +const bool RELAY2_UP = true; +const bool RELAY2_DOWN = false; + +const byte SHUTTERS_EEPROM_POSITION = 0; + +HomieNode shuttersNode("shutters", "shutters"); + +// Shutters + +void shuttersUp() { + SonoffDual.setRelays(RELAY1_MOVE, RELAY2_UP); +} + +void shuttersDown() { + SonoffDual.setRelays(RELAY1_MOVE, RELAY2_DOWN); +} + +void shuttersHalt() { + SonoffDual.setRelays(RELAY1_STOP, false); +} + +uint8_t shuttersGetState() { + return EEPROM.read(SHUTTERS_EEPROM_POSITION); +} + +void shuttersSetState(uint8_t state) { + EEPROM.write(SHUTTERS_EEPROM_POSITION, state); + EEPROM.commit(); +} + +Shutters shutters(COURSE_TIME, shuttersUp, shuttersDown, shuttersHalt, shuttersGetState, shuttersSetState, CALIBRATION_RATIO, onShuttersLevelReached); + +void onShuttersLevelReached(uint8_t level) { + if (shutters.isIdle()) Homie.setIdle(true); // if idle, we've reached our target + if (Homie.isConnected()) shuttersNode.setProperty("level").send(String(level)); +} + +// Homie + +void onHomieEvent(const HomieEvent& event) { + switch (event.type) { + case HomieEventType::ABOUT_TO_RESET: + shutters.reset(); + break; + } +} + +bool shuttersLevelHandler(const HomieRange& range, const String& value) { + for (byte i = 0; i < value.length(); i++) { + if (isDigit(value.charAt(i)) == false) return false; + } + + const unsigned long numericValue = value.toInt(); + if (numericValue > 100) return false; + + // wanted value is valid + + if (shutters.isIdle() && numericValue == shutters.getCurrentLevel()) return true; // nothing to do + + Homie.setIdle(false); + shutters.setLevel(numericValue); + + return true; +} + +// Logic + +void setup() { + SonoffDual.setup(); + EEPROM.begin(4); + shutters.begin(); + + Homie_setFirmware("sonoff-dual-shutters", "1.0.0"); + Homie.disableLogging(); + Homie.disableResetTrigger(); + Homie.setLedPin(SonoffDual.LED_PIN, SonoffDual.LED_ON); + Homie.onEvent(onHomieEvent); + + shuttersNode.advertise("level").settable(shuttersLevelHandler); + + Homie.setup(); +} + +void loop() { + shutters.loop(); + Homie.loop(); + SonoffDualButton buttonState = SonoffDual.handleButton(); + if (buttonState == SonoffDualButton::LONG) { + Homie.reset(); + } else if (buttonState == SonoffDualButton::SHORT && shutters.isIdle()) { + Homie.setIdle(false); + + if (shutters.getCurrentLevel() == 100) { + shutters.setLevel(0); + } else { + shutters.setLevel(100); + } + } +} diff --git a/examples/TemperatureSensor/TemperatureSensor.ino b/examples/TemperatureSensor/TemperatureSensor.ino index be2478fa..0c598121 100644 --- a/examples/TemperatureSensor/TemperatureSensor.ino +++ b/examples/TemperatureSensor/TemperatureSensor.ino @@ -7,28 +7,27 @@ unsigned long lastTemperatureSent = 0; HomieNode temperatureNode("temperature", "temperature"); void setupHandler() { - Homie.setNodeProperty(temperatureNode, "unit", "c", true); + temperatureNode.setProperty("unit").send("c"); } void loopHandler() { if (millis() - lastTemperatureSent >= TEMPERATURE_INTERVAL * 1000UL || lastTemperatureSent == 0) { float temperature = 22; // Fake temperature here, for the example - Serial.print("Temperature: "); - Serial.print(temperature); - Serial.println(" °C"); - if (Homie.setNodeProperty(temperatureNode, "degrees", String(temperature), true)) { - lastTemperatureSent = millis(); - } else { - Serial.println("Temperature sending failed"); - } + Homie.getLogger() << "Temperature: " << temperature << " °C" << endl; + temperatureNode.setProperty("degrees").send(String(temperature)); + lastTemperatureSent = millis(); } } void setup() { - Homie.setFirmware("awesome-temperature", "1.0.0"); - Homie.registerNode(temperatureNode); - Homie.setSetupFunction(setupHandler); - Homie.setLoopFunction(loopHandler); + Serial.begin(115200); + Serial << endl << endl; + Homie_setFirmware("awesome-temperature", "1.0.0"); + Homie.setSetupFunction(setupHandler).setLoopFunction(loopHandler); + + temperatureNode.advertise("unit"); + temperatureNode.advertise("degrees"); + Homie.setup(); } diff --git a/homie-esp8266.cppcheck b/homie-esp8266.cppcheck index 1b4f5852..da200864 100644 --- a/homie-esp8266.cppcheck +++ b/homie-esp8266.cppcheck @@ -1,6 +1,6 @@ - - - - - - + + + + + + diff --git a/homie-esp8266.jpg b/homie-esp8266.jpg deleted file mode 100644 index 5266aebdaf05626bae6ddd6316c0b22f8c03357f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47471 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 CFSbJf diff --git a/keywords.txt b/keywords.txt index 1b17bea0..a983208d 100644 --- a/keywords.txt +++ b/keywords.txt @@ -1,45 +1,97 @@ -####################################### -# Datatypes (KEYWORD1) -####################################### - -Homie KEYWORD1 -HomieNode KEYWORD1 -HomieEvent KEYWORD1 - -####################################### -# Methods and Functions (KEYWORD2) -####################################### - -setup KEYWORD2 -loop KEYWORD2 -enableLogging KEYWORD2 -enableBuiltInLedIndicator KEYWORD2 -setLedPin KEYWORD2 -setBrand KEYWORD2 -setFirmware KEYWORD2 -registerNode KEYWORD2 -setGlobalInputHandler KEYWORD2 -setSetupFunction KEYWORD2 -setLoopFunction KEYWORD2 -onEvent KEYWORD2 -setResetTrigger KEYWORD2 -disableResetTrigger KEYWORD2 -setResetFunction KEYWORD2 -isReadyToOperate KEYWORD2 -setResettable KEYWORD2 -setNodeProperty KEYWORD2 - -subscribe KEYWORD2 - -####################################### -# Constants (LITERAL1) -####################################### - -HOMIE_CONFIGURATION_MODE LITERAL1 -HOMIE_NORMAL_MODE LITERAL1 -HOMIE_OTA_MODE LITERAL1 -HOMIE_ABOUT_TO_RESET LITERAL1 -HOMIE_WIFI_CONNECTED LITERAL1 -HOMIE_WIFI_DISCONNECTED LITERAL1 -HOMIE_MQTT_CONNECTED LITERAL1 -HOMIE_MQTT_DISCONNECTED LITERAL1 +####################################### +# Datatypes (KEYWORD1) +####################################### + +Homie KEYWORD1 +HomieNode KEYWORD1 +HomieSetting KEYWORD1 +HomieEvent KEYWORD1 +HomieEventType KEYWORD1 +HomieRange KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +Homie_setBrand KEYWORD2 +Homie_setFirmware KEYWORD2 + +# Homie + +setup KEYWORD2 +loop KEYWORD2 +disableLogging KEYWORD2 +setLoggingPrinter KEYWORD2 +disableLedFeedback KEYWORD2 +setLedPin KEYWORD2 +setConfigurationApPassword KEYWORD2 +setGlobalInputHandler KEYWORD2 +setBroadcastHandler KEYWORD2 +onEvent KEYWORD2 +setResetTrigger KEYWORD2 +disableResetTrigger KEYWORD2 +setSetupFunction KEYWORD2 +setLoopFunction KEYWORD2 +setStandalone KEYWORD2 +reset KEYWORD2 +setIdle KEYWORD2 +isConfigured KEYWORD2 +isConnected KEYWORD2 +getConfiguration KEYWORD2 +getMqttClient KEYWORD2 +getLogger KEYWORD2 +prepareToSleep KEYWORD2 +doDeepSleep KEYWORD2 + +# HomieNode + +getId KEYWORD2 +getType KEYWORD2 +advertise KEYWORD2 +advertiseRange KEYWORD2 +settable KEYWORD2 +setProperty KEYWORD2 + +# HomieSetting + +get KEYWORD2 +wasProvided KEYWORD2 +setDefaultValue KEYWORD2 +setValidator KEYWORD2 + +# HomieRange + +isRange KEYWORD2 +index KEYWORD2 + +# SendingPromise + +setQos KEYWORD2 +setRetained KEYWORD2 +setRange KEYWORD2 +send KEYWORD2 + +####################################### +# Constants (LITERAL1) +####################################### + +# HomieEventType + +STANDALONE_MODE LITERAL1 +CONFIGURATION_MODE LITERAL1 +NORMAL_MODE LITERAL1 +OTA_STARTED LITERAL1 +OTA_PROGRESS LITERAL1 +OTA_FAILED LITERAL1 +OTA_SUCCESSFUL LITERAL1 +ABOUT_TO_RESET LITERAL1 +WIFI_CONNECTED LITERAL1 +WIFI_DISCONNECTED LITERAL1 +MQTT_READY LITERAL1 +MQTT_DISCONNECTED LITERAL1 +MQTT_PACKET_ACKNOWLEDGED LITERAL1 +READY_TO_SLEEP LITERAL1 + +# StreamingOperator + +endl LITERAL1 diff --git a/library.json b/library.json index 683c225a..7704c58e 100644 --- a/library.json +++ b/library.json @@ -1,35 +1,47 @@ { "name": "Homie", - "keywords": "iot, home, automation, mqtt, esp8266", + "version": "2.0.0", + "keywords": "iot, home, automation, mqtt, esp8266, async, sensor", "description": "ESP8266 framework for Homie, a lightweight MQTT convention for the IoT", + "homepage": "http://marvinroger.github.io/homie-esp8266/", + "license": "MIT", "authors": { "name": "Marvin Roger", - "url": "https://www.marvinroger.fr" + "url": "https://www.marvinroger.fr", + "maintainer": true }, "repository": { "type": "git", - "url": "https://github.com/marvinroger/homie-esp8266.git" + "url": "https://github.com/marvinroger/homie-esp8266.git", + "branch": "master" }, - "version": "1.5.0", "frameworks": "arduino", - "platforms": "espressif", + "platforms": "espressif8266", "dependencies": [ { "name": "ArduinoJson", - "authors": "Benoit Blanchon", - "frameworks": "arduino" + "version": "^5.10.0" }, { - "name": "PubSubClient", - "authors": "Nick O'Leary", - "frameworks": "arduino" + "name": "AsyncMqttClient", + "version": "^0.8.0" }, { "name": "Bounce2", - "authors": "Thomas O Fredericks", - "frameworks": "arduino" + "version": "^2.1.0" + }, + { + "name": "ESP Async WebServer" } - ] + ], + "export": { + "include": [ + "LICENSE", + "keywords.txt", + "src/*", + "examples/*" + ] + } } diff --git a/library.properties b/library.properties index 610b53b5..3b58c122 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=Homie -version=1.5.0 +version=2.0.0 author=Marvin Roger maintainer=Marvin Roger sentence=ESP8266 framework for Homie, a lightweight MQTT convention for the IoT diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..1158a5bd --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,76 @@ +site_name: Homie for ESP8266 +site_description: The Homie for ESP8266 documentation. +site_author: Marvin ROGER + +repo_name: 'marvinroger/homie-esp8266' +repo_url: 'https://github.com/marvinroger/homie-esp8266' + +edit_uri: edit/develop/docs + +pages: + - Welcome: index.md + - Quickstart: + - What is it?: quickstart/what-is-it.md + - Getting started: quickstart/getting-started.md + - Advanced usage: + - Built-in LED: advanced-usage/built-in-led.md + - Branding: advanced-usage/branding.md + - Events: advanced-usage/events.md + - Logging: advanced-usage/logging.md + - Streaming operator: advanced-usage/streaming-operator.md + - Input handlers: advanced-usage/input-handlers.md + - Broadcast: advanced-usage/broadcast.md + - Custom settings: advanced-usage/custom-settings.md + - Resetting: advanced-usage/resetting.md + - Standalone mode: advanced-usage/standalone-mode.md + - Magic bytes: advanced-usage/magic-bytes.md + - Range properties: advanced-usage/range-properties.md + - Deep sleep: advanced-usage/deep-sleep.md + - Miscellaneous: advanced-usage/miscellaneous.md + - UI Bundle: advanced-usage/ui-bundle.md + - Configuration: + - JSON configuration file: configuration/json-configuration-file.md + - HTTP JSON API: configuration/http-json-api.md + - Others: + - OTA/configuration updates: others/ota-configuration-updates.md + - Homie implementation specifics: others/homie-implementation-specifics.md + - Limitations and known issues: others/limitations-and-known-issues.md + - Troubleshooting: others/troubleshooting.md + - C++ API reference: others/cpp-api-reference.md + - Upgrade guide from v1 to v2: others/upgrade-guide-from-v1-to-v2.md + - Community projects: others/community-projects.md + +theme: + name: material + palette: + primary: red + accent: red + logo: assets/logo.png + feature: + tabs: true + +extra: + social: + - type: cog + link: http://marvinroger.github.io/homie-esp8266/configurators/v2/ + +markdown_extensions: + - meta + - footnotes + - codehilite + - admonition + - toc(permalink=true) + - pymdownx.arithmatex + - pymdownx.betterem(smart_enable=all) + - pymdownx.caret + - pymdownx.critic + - pymdownx.details + - pymdownx.emoji: + emoji_generator: !!python/name:pymdownx.emoji.to_svg + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences + - pymdownx.tasklist(custom_checkbox=true) + - pymdownx.tilde diff --git a/scripts/firmware_parser/README.md b/scripts/firmware_parser/README.md new file mode 100644 index 00000000..1cb6ddf9 --- /dev/null +++ b/scripts/firmware_parser/README.md @@ -0,0 +1,8 @@ +Script: Firmware parser +======================= + +This will allow you to get information about the binary firmware file. + +## Usage + +`python ./firmware_parser.py ~/firmware.bin` diff --git a/scripts/firmware_parser/firmware_parser.py b/scripts/firmware_parser/firmware_parser.py new file mode 100644 index 00000000..9ed43e69 --- /dev/null +++ b/scripts/firmware_parser/firmware_parser.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +import re +import sys + +if len(sys.argv) != 2: + print("Please specify a file") + sys.exit(1) + +regex_homie = re.compile(b"\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25") +regex_name = re.compile(b"\xbf\x84\xe4\x13\x54(.+)\x93\x44\x6b\xa7\x75") +regex_version = re.compile(b"\x6a\x3f\x3e\x0e\xe1(.+)\xb0\x30\x48\xd4\x1a") +regex_brand = re.compile(b"\xfb\x2a\xf5\x68\xc0(.+)\x6e\x2f\x0f\xeb\x2d") + +try: + firmware_file = open(sys.argv[1], "rb") +except Exception as err: + print("Error: {0}".format(err.strerror)) + sys.exit(2) + +firmware_binary = firmware_file.read() +firmware_file.close() + +regex_name_result = regex_name.search(firmware_binary) +regex_version_result = regex_version.search(firmware_binary) + +if not regex_homie.search(firmware_binary) or not regex_name_result or not regex_version_result: + print("Not a valid Homie firmware") + sys.exit(3) + + +regex_brand_result = regex_brand.search(firmware_binary) + +name = regex_name_result.group(1).decode() +version = regex_version_result.group(1).decode() +brand = regex_brand_result.group(1).decode() if regex_brand_result else "unset (default is Homie)" + +print("Name: {0}".format(name)) +print("Version: {0}".format(version)) +print("Brand: {0}".format(brand)) diff --git a/scripts/ota_updater/README.md b/scripts/ota_updater/README.md new file mode 100644 index 00000000..3ce065aa --- /dev/null +++ b/scripts/ota_updater/README.md @@ -0,0 +1,48 @@ +Script: OTA updater +=================== + +This script will allow you to send an OTA update to your device. + +## Installation + +`pip install -r requirements.txt` + +## Usage + +```text +usage: ota_updater.py [-h] -l BROKER_HOST -p BROKER_PORT [-u BROKER_USERNAME] + [-d BROKER_PASSWORD] [-t BASE_TOPIC] -i DEVICE_ID + firmware + +ota firmware update scirpt for ESP8226 implemenation of the Homie mqtt IoT +convention. + +positional arguments: + firmware path to the firmware to be sent to the device + +arguments: + -h, --help show this help message and exit + -l BROKER_HOST, --broker-host BROKER_HOST + host name or ip address of the mqtt broker + -p BROKER_PORT, --broker-port BROKER_PORT + port of the mqtt broker + -u BROKER_USERNAME, --broker-username BROKER_USERNAME + username used to authenticate with the mqtt broker + -d BROKER_PASSWORD, --broker-password BROKER_PASSWORD + password used to authenticate with the mqtt broker + -t BASE_TOPIC, --base-topic BASE_TOPIC + base topic of the homie devices on the broker + -i DEVICE_ID, --device-id DEVICE_ID + homie device id +``` + +* `BROKER_HOST` and `BROKER_PORT` defaults to 127.0.0.1 and 1883 respectively if not set. +* `BROKER_USERNAME` and `BROKER_PASSWORD` are optional. +* `BASE_TOPIC` has to end with a slash, defaults to `homie/` if not set. + +### Example: + +```bash +python ota_updater.py -l localhost -u admin -d secure -t "homie/" -i "device-id" /path/to/firmware.bin +``` + diff --git a/scripts/ota_updater/ota_updater.py b/scripts/ota_updater/ota_updater.py new file mode 100644 index 00000000..2b0f1f5b --- /dev/null +++ b/scripts/ota_updater/ota_updater.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python + +from __future__ import division, print_function +import paho.mqtt.client as mqtt +import base64, sys, math +from hashlib import md5 + +# The callback for when the client receives a CONNACK response from the server. +def on_connect(client, userdata, flags, rc): + if rc != 0: + print("Connection Failed with result code {}".format(rc)) + client.disconnect() + else: + print("Connected with result code {}".format(rc)) + + # calcluate firmware md5 + firmware_md5 = md5(userdata['firmware']).hexdigest() + userdata.update({'md5': firmware_md5}) + + # Subscribing in on_connect() means that if we lose the connection and + # reconnect then subscriptions will be renewed. + client.subscribe("{base_topic}{device_id}/$implementation/ota/status".format(**userdata)) + client.subscribe("{base_topic}{device_id}/$implementation/ota/enabled".format(**userdata)) + client.subscribe("{base_topic}{device_id}/$fw/#".format(**userdata)) + + # Wait for device info to come in and invoke the on_message callback where update will continue + print("Waiting for device info...") + + +# The callback for when a PUBLISH message is received from the server. +def on_message(client, userdata, msg): + # decode string for python2/3 compatiblity + msg.payload = msg.payload.decode() + + if msg.topic.endswith('$implementation/ota/status'): + status = int(msg.payload.split()[0]) + + if userdata.get("published"): + if status == 206: # in progress + # state in progress, print progress bar + progress, total = [int(x) for x in msg.payload.split()[1].split('/')] + bar_width = 30 + bar = int(bar_width*(progress/total)) + print("\r[", '+'*bar, ' '*(bar_width-bar), "] ", msg.payload.split()[1], end='', sep='') + if (progress == total): + print() + sys.stdout.flush() + elif status == 304: # not modified + print("Device firmware already up to date with md5 checksum: {}".format(userdata.get('md5'))) + client.disconnect() + elif status == 403: # forbidden + print("Device ota disabled, aborting...") + client.disconnect() + + elif msg.topic.endswith('$fw/checksum'): + checksum = msg.payload + + if userdata.get("published"): + if checksum == userdata.get('md5'): + print("Device back online. Update Successful!") + else: + print("Expecting checksum {}, got {}, update failed!".format(userdata.get('md5'), checksum)) + client.disconnect() + else: + if checksum != userdata.get('md5'): # save old md5 for comparison with new firmware + userdata.update({'old_md5': checksum}) + else: + print("Device firmware already up to date with md5 checksum: {}".format(checksum)) + client.disconnect() + + elif msg.topic.endswith('ota/enabled'): + if msg.payload == 'true': + userdata.update({'ota_enabled': True}) + else: + print("Device ota disabled, aborting...") + client.disconnect() + + if ( not userdata.get("published") ) and ( userdata.get('ota_enabled') ) and \ + ( 'old_md5' in userdata.keys() ) and ( userdata.get('md5') != userdata.get('old_md5') ): + # push the firmware binary + userdata.update({"published": True}) + topic = "{base_topic}{device_id}/$implementation/ota/firmware/{md5}".format(**userdata) + print("Publishing new firmware with checksum {}".format(userdata.get('md5'))) + client.publish(topic, userdata['firmware']) + + +def main(broker_host, broker_port, broker_username, broker_password, base_topic, device_id, firmware): + # initialise mqtt client and register callbacks + client = mqtt.Client() + client.on_connect = on_connect + client.on_message = on_message + + # set username and password if given + if broker_username and broker_password: + client.username_pw_set(broker_username, broker_password) + + # save data to be used in the callbacks + client.user_data_set({ + "base_topic": base_topic, + "device_id": device_id, + "firmware": firmware + }) + + # start connection + print("Connecting to mqtt broker {} on port {}".format(broker_host, broker_port)) + client.connect(broker_host, broker_port, 60) + + # Blocking call that processes network traffic, dispatches callbacks and handles reconnecting. + client.loop_forever() + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser( + description='ota firmware update scirpt for ESP8226 implemenation of the Homie mqtt IoT convention.') + + # ensure base topic always ends with a '/' + def base_topic_arg(s): + s = str(s) + if not s.endswith('/'): + s = s + '/' + return s + + # specify arguments + parser.add_argument('-l', '--broker-host', type=str, required=False, + help='host name or ip address of the mqtt broker', default="127.0.0.1") + parser.add_argument('-p', '--broker-port', type=int, required=False, + help='port of the mqtt broker', default=1883) + parser.add_argument('-u', '--broker-username', type=str, required=False, + help='username used to authenticate with the mqtt broker') + parser.add_argument('-d', '--broker-password', type=str, required=False, + help='password used to authenticate with the mqtt broker') + parser.add_argument('-t', '--base-topic', type=base_topic_arg, required=False, + help='base topic of the homie devices on the broker', default="homie/") + parser.add_argument('-i', '--device-id', type=str, required=True, + help='homie device id') + parser.add_argument('firmware', type=argparse.FileType('rb'), + help='path to the firmware to be sent to the device') + + # workaround for http://bugs.python.org/issue9694 + parser._optionals.title = "arguments" + + # get and validate arguments + args = parser.parse_args() + + # read the contents of firmware into buffer + fw_buffer = args.firmware.read() + args.firmware.close() + firmware = bytearray() + firmware.extend(fw_buffer) + + # Invoke the business logic + main(args.broker_host, args.broker_port, args.broker_username, + args.broker_password, args.base_topic, args.device_id, firmware) diff --git a/scripts/ota_updater/requirements.txt b/scripts/ota_updater/requirements.txt new file mode 100644 index 00000000..75ccf28c --- /dev/null +++ b/scripts/ota_updater/requirements.txt @@ -0,0 +1 @@ +paho-mqtt >1.2.3,<=1.3.0 diff --git a/src/Homie.cpp b/src/Homie.cpp index 0c786877..ea45fb13 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -2,213 +2,353 @@ using namespace HomieInternals; -HomieClass::HomieClass() : _setupCalled(false) { - strcpy(this->_interface.brand, DEFAULT_BRAND); - strcpy(this->_interface.firmware.name, DEFAULT_FW_NAME); - strcpy(this->_interface.firmware.version, DEFAULT_FW_VERSION); - this->_interface.led.enabled = true; - this->_interface.led.pin = BUILTIN_LED; - this->_interface.led.on = LOW; - this->_interface.reset.able = true; - this->_interface.reset.enabled = true; - this->_interface.reset.triggerPin = DEFAULT_RESET_PIN; - this->_interface.reset.triggerState = DEFAULT_RESET_STATE; - this->_interface.reset.triggerTime = DEFAULT_RESET_TIME; - this->_interface.reset.userFunction = []() { return false; }; - this->_interface.globalInputHandler = [](String node, String property, String value) { return false; }; - this->_interface.setupFunction = []() {}; - this->_interface.loopFunction = []() {}; - this->_interface.eventHandler = [](HomieEvent event) {}; - this->_interface.readyToOperate = false; - this->_interface.logger = &this->_logger; - this->_interface.blinker = &this->_blinker; - this->_interface.config = &this->_config; - this->_interface.mqttClient = &this->_mqttClient; - - Helpers::generateDeviceId(); - - this->_config.attachInterface(&this->_interface); - this->_blinker.attachInterface(&this->_interface); - this->_mqttClient.attachInterface(&this->_interface); - - this->_bootNormal.attachInterface(&this->_interface); - this->_bootOta.attachInterface(&this->_interface); - this->_bootConfig.attachInterface(&this->_interface); +HomieClass::HomieClass() + : _setupCalled(false) + , _firmwareSet(false) + , __HOMIE_SIGNATURE("\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25") { + strlcpy(Interface::get().brand, DEFAULT_BRAND, MAX_BRAND_LENGTH); + Interface::get().bootMode = HomieBootMode::UNDEFINED; + Interface::get().configurationAp.secured = false; + Interface::get().led.enabled = true; + Interface::get().led.pin = LED_BUILTIN; + Interface::get().led.on = LOW; + Interface::get().reset.idle = true; + Interface::get().reset.enabled = true; + Interface::get().reset.triggerPin = DEFAULT_RESET_PIN; + Interface::get().reset.triggerState = DEFAULT_RESET_STATE; + Interface::get().reset.triggerTime = DEFAULT_RESET_TIME; + Interface::get().reset.resetFlag = false; + Interface::get().disable = false; + Interface::get().flaggedForSleep = false; + Interface::get().globalInputHandler = [](const HomieNode& node, const String& property, const HomieRange& range, const String& value) { return false; }; + Interface::get().broadcastHandler = [](const String& level, const String& value) { return false; }; + Interface::get().setupFunction = []() {}; + Interface::get().loopFunction = []() {}; + Interface::get().eventHandler = [](const HomieEvent& event) {}; + Interface::get().ready = false; + Interface::get()._mqttClient = &_mqttClient; + Interface::get()._sendingPromise = &_sendingPromise; + Interface::get()._blinker = &_blinker; + Interface::get()._logger = &_logger; + Interface::get()._config = &_config; + + DeviceId::generate(); } HomieClass::~HomieClass() { } -void HomieClass::_checkBeforeSetup(const __FlashStringHelper* functionName) { +void HomieClass::_checkBeforeSetup(const __FlashStringHelper* functionName) const { if (_setupCalled) { - this->_logger.log(F("✖ ")); - this->_logger.log(functionName); - this->_logger.logln(F("(): has to be called before setup()")); - abort(); + String message; + message.concat(F("✖ ")); + message.concat(functionName); + message.concat(F("(): has to be called before setup()")); + Helpers::abort(message); } } void HomieClass::setup() { _setupCalled = true; - if (this->_logger.isEnabled()) { - Serial.begin(BAUD_RATE); - this->_logger.logln(); - this->_logger.logln(); + // Check if firmware is set + if (!_firmwareSet) { + Helpers::abort(F("✖ Firmware name must be set before calling setup()")); + return; // never reached, here for clarity } - if (!this->_config.load()) { - this->_boot = &this->_bootConfig; - this->_logger.logln(F("Triggering HOMIE_CONFIGURATION_MODE event...")); - this->_interface.eventHandler(HOMIE_CONFIGURATION_MODE); - } else { - switch (this->_config.getBootMode()) { - case BOOT_NORMAL: - this->_boot = &this->_bootNormal; - this->_logger.logln(F("Triggering HOMIE_NORMAL_MODE event...")); - this->_interface.eventHandler(HOMIE_NORMAL_MODE); + // Check the max allowed setting elements + if (IHomieSetting::settings.size() > MAX_CONFIG_SETTING_SIZE) { + Helpers::abort(F("✖ Settings exceed set limit of elelement.")); + return; // never reached, here for clarity + } + + // Check if default settings values are valid + bool defaultSettingsValuesValid = true; + for (IHomieSetting* iSetting : IHomieSetting::settings) { + if (iSetting->isBool()) { + HomieSetting* setting = static_cast*>(iSetting); + if (!setting->isRequired() && !setting->validate(setting->get())) { + defaultSettingsValuesValid = false; + break; + } + } else if (iSetting->isLong()) { + HomieSetting* setting = static_cast*>(iSetting); + if (!setting->isRequired() && !setting->validate(setting->get())) { + defaultSettingsValuesValid = false; break; - case BOOT_OTA: - this->_boot = &this->_bootOta; - this->_logger.logln(F("Triggering HOMIE_OTA_MODE event...")); - this->_interface.eventHandler(HOMIE_OTA_MODE); + } + } else if (iSetting->isDouble()) { + HomieSetting* setting = static_cast*>(iSetting); + if (!setting->isRequired() && !setting->validate(setting->get())) { + defaultSettingsValuesValid = false; break; - default: - this->_logger.logln(F("✖ The boot mode is invalid")); - abort(); + } + } else if (iSetting->isConstChar()) { + HomieSetting* setting = static_cast*>(iSetting); + if (!setting->isRequired() && !setting->validate(setting->get())) { + defaultSettingsValuesValid = false; break; + } } } - this->_boot->setup(); + if (!defaultSettingsValuesValid) { + Helpers::abort(F("✖ Default setting value does not pass validator test")); + return; // never reached, here for clarity + } + + // boot mode set during this boot by application before Homie.setup() + HomieBootMode _applicationHomieBootMode = Interface::get().bootMode; + + // boot mode set before resetting the device. If application has defined a boot mode, this will be ignored + HomieBootMode _nextHomieBootMode = Interface::get().getConfig().getHomieBootModeOnNextBoot(); + if (_nextHomieBootMode != HomieBootMode::UNDEFINED) { + Interface::get().getConfig().setHomieBootModeOnNextBoot(HomieBootMode::UNDEFINED); + } + + HomieBootMode _selectedHomieBootMode = HomieBootMode::CONFIGURATION; + + // select boot mode source + if (_applicationHomieBootMode != HomieBootMode::UNDEFINED) { + _selectedHomieBootMode = _applicationHomieBootMode; + } else if (_nextHomieBootMode != HomieBootMode::UNDEFINED) { + _selectedHomieBootMode = _nextHomieBootMode; + } else { + _selectedHomieBootMode = HomieBootMode::NORMAL; + } + + // validate selected mode and fallback as needed + if (_selectedHomieBootMode == HomieBootMode::NORMAL && !Interface::get().getConfig().load()) { + Interface::get().getLogger() << F("Configuration invalid. Using CONFIG MODE") << endl; + _selectedHomieBootMode = HomieBootMode::CONFIGURATION; + } + + // run selected mode + if (_selectedHomieBootMode == HomieBootMode::NORMAL) { + _boot = &_bootNormal; + Interface::get().event.type = HomieEventType::NORMAL_MODE; + Interface::get().eventHandler(Interface::get().event); + } else if (_selectedHomieBootMode == HomieBootMode::CONFIGURATION) { + _boot = &_bootConfig; + Interface::get().event.type = HomieEventType::CONFIGURATION_MODE; + Interface::get().eventHandler(Interface::get().event); + } else if (_selectedHomieBootMode == HomieBootMode::STANDALONE) { + _boot = &_bootStandalone; + Interface::get().event.type = HomieEventType::STANDALONE_MODE; + Interface::get().eventHandler(Interface::get().event); + } else { + Helpers::abort(F("✖ Boot mode invalid")); + return; // never reached, here for clarity + } + + _boot->setup(); } void HomieClass::loop() { - this->_boot->loop(); + _boot->loop(); + + if (_flaggedForReboot && Interface::get().reset.idle) { + Interface::get().getLogger() << F("Device is idle") << endl; + Interface::get().getLogger() << F("Triggering ABOUT_TO_RESET event...") << endl; + Interface::get().event.type = HomieEventType::ABOUT_TO_RESET; + Interface::get().eventHandler(Interface::get().event); + + Interface::get().getLogger() << F("↻ Rebooting device...") << endl; + Serial.flush(); + ESP.restart(); + } } -void HomieClass::enableLogging(bool enable) { - this->_checkBeforeSetup(F("enableLogging")); +HomieClass& HomieClass::disableLogging() { + _checkBeforeSetup(F("disableLogging")); - this->_logger.setLogging(enable); + Interface::get().getLogger().setLogging(false); + + return *this; } -void HomieClass::enableBuiltInLedIndicator(bool enable) { - this->_checkBeforeSetup(F("enableBuiltInLedIndicator")); +HomieClass& HomieClass::setLoggingPrinter(Print* printer) { + _checkBeforeSetup(F("setLoggingPrinter")); + + Interface::get().getLogger().setPrinter(printer); - this->_interface.led.enabled = enable; + return *this; } -void HomieClass::setLedPin(unsigned char pin, unsigned char on) { - this->_checkBeforeSetup(F("setLedPin")); +HomieClass& HomieClass::disableLedFeedback() { + _checkBeforeSetup(F("disableLedFeedback")); + + Interface::get().led.enabled = false; - this->_interface.led.pin = pin; - this->_interface.led.on = on; + return *this; } -void HomieClass::setFirmware(const char* name, const char* version) { - this->_checkBeforeSetup(F("setFirmware")); - if (strlen(name) + 1 > MAX_FIRMWARE_NAME_LENGTH || strlen(version) + 1 > MAX_FIRMWARE_VERSION_LENGTH) { - this->_logger.logln(F("✖ setFirmware(): either the name or version string is too long")); - abort(); - } +HomieClass& HomieClass::setLedPin(uint8_t pin, uint8_t on) { + _checkBeforeSetup(F("setLedPin")); + + Interface::get().led.pin = pin; + Interface::get().led.on = on; - strcpy(this->_interface.firmware.name, name); - strcpy(this->_interface.firmware.version, version); + return *this; } -void HomieClass::setBrand(const char* name) { - this->_checkBeforeSetup(F("setBrand")); - if (strlen(name) + 1 > MAX_BRAND_LENGTH) { - this->_logger.logln(F("✖ setBrand(): the brand string is too long")); - abort(); +HomieClass& HomieClass::setConfigurationApPassword(const char* password) { + _checkBeforeSetup(F("setConfigurationApPassword")); + + Interface::get().configurationAp.secured = true; + strlcpy(Interface::get().configurationAp.password, password, MAX_WIFI_PASSWORD_LENGTH); + return *this; +} + +void HomieClass::__setFirmware(const char* name, const char* version) { + _checkBeforeSetup(F("setFirmware")); + if (strlen(name) + 1 - 10 > MAX_FIRMWARE_NAME_LENGTH || strlen(version) + 1 - 10 > MAX_FIRMWARE_VERSION_LENGTH) { + Helpers::abort(F("✖ setFirmware(): either the name or version string is too long")); + return; // never reached, here for clarity } - strcpy(this->_interface.brand, name); + strncpy(Interface::get().firmware.name, name + 5, strlen(name) - 10); + Interface::get().firmware.name[strlen(name) - 10] = '\0'; + strncpy(Interface::get().firmware.version, version + 5, strlen(version) - 10); + Interface::get().firmware.version[strlen(version) - 10] = '\0'; + _firmwareSet = true; } -void HomieClass::registerNode(const HomieNode& node) { - this->_checkBeforeSetup(F("registerNode")); - if (this->_interface.registeredNodesCount > MAX_REGISTERED_NODES_COUNT) { - Serial.println(F("✖ register(): the max registered nodes count has been reached")); - abort(); +void HomieClass::__setBrand(const char* brand) const { + _checkBeforeSetup(F("setBrand")); + if (strlen(brand) + 1 - 10 > MAX_BRAND_LENGTH) { + Helpers::abort(F("✖ setBrand(): the brand string is too long")); + return; // never reached, here for clarity } - this->_interface.registeredNodes[this->_interface.registeredNodesCount++] = &node; + strncpy(Interface::get().brand, brand + 5, strlen(brand) - 10); + Interface::get().brand[strlen(brand) - 10] = '\0'; +} + +void HomieClass::reset() { + Interface::get().getLogger() << F("Flagged for reset by sketch") << endl; + Interface::get().disable = true; + Interface::get().reset.resetFlag = true; } -bool HomieClass::isReadyToOperate() { - return this->_interface.readyToOperate; +void HomieClass::reboot() { + Interface::get().getLogger() << F("Flagged for reboot by sketch") << endl; + Interface::get().disable = true; + _flaggedForReboot = true; } -void HomieClass::setResettable(bool resettable) { - this->_interface.reset.able = resettable; +void HomieClass::setIdle(bool idle) { + Interface::get().reset.idle = idle; } -void HomieClass::setGlobalInputHandler(GlobalInputHandler inputHandler) { - this->_checkBeforeSetup(F("setGlobalInputHandler")); +HomieClass& HomieClass::setGlobalInputHandler(const GlobalInputHandler& globalInputHandler) { + _checkBeforeSetup(F("setGlobalInputHandler")); - this->_interface.globalInputHandler = inputHandler; + Interface::get().globalInputHandler = globalInputHandler; + + return *this; } -void HomieClass::setResetFunction(ResetFunction function) { - this->_checkBeforeSetup(F("setResetFunction")); +HomieClass& HomieClass::setBroadcastHandler(const BroadcastHandler& broadcastHandler) { + _checkBeforeSetup(F("setBroadcastHandler")); + + Interface::get().broadcastHandler = broadcastHandler; - this->_interface.reset.userFunction = function; + return *this; } -void HomieClass::setSetupFunction(OperationFunction function) { - this->_checkBeforeSetup(F("setSetupFunction")); +HomieClass& HomieClass::setSetupFunction(const OperationFunction& function) { + _checkBeforeSetup(F("setSetupFunction")); + + Interface::get().setupFunction = function; - this->_interface.setupFunction = function; + return *this; } -void HomieClass::setLoopFunction(OperationFunction function) { - this->_checkBeforeSetup(F("setLoopFunction")); +HomieClass& HomieClass::setLoopFunction(const OperationFunction& function) { + _checkBeforeSetup(F("setLoopFunction")); - this->_interface.loopFunction = function; + Interface::get().loopFunction = function; + + return *this; } -void HomieClass::onEvent(EventHandler handler) { - this->_checkBeforeSetup(F("onEvent")); +HomieClass& HomieClass::setHomieBootMode(HomieBootMode bootMode) { + _checkBeforeSetup(F("setHomieBootMode")); + Interface::get().bootMode = bootMode; + return *this; +} - this->_interface.eventHandler = handler; +HomieClass& HomieClass::setHomieBootModeOnNextBoot(HomieBootMode bootMode) { + Interface::get().getConfig().setHomieBootModeOnNextBoot(bootMode); + return *this; } -void HomieClass::setResetTrigger(unsigned char pin, unsigned char state, unsigned int time) { - this->_checkBeforeSetup(F("setResetTrigger")); +bool HomieClass::isConfigured() { + return Interface::get().getConfig().load(); +} - this->_interface.reset.enabled = true; - this->_interface.reset.triggerPin = pin; - this->_interface.reset.triggerState = state; - this->_interface.reset.triggerTime = time; +bool HomieClass::isConnected() { + return Interface::get().ready; } -void HomieClass::disableResetTrigger() { - this->_checkBeforeSetup(F("disableResetTrigger")); +HomieClass& HomieClass::onEvent(const EventHandler& handler) { + _checkBeforeSetup(F("onEvent")); - this->_interface.reset.enabled = false; + Interface::get().eventHandler = handler; + + return *this; } -bool HomieClass::setNodeProperty(const HomieNode& node, const char* property, const char* value, bool retained) { - if (!this->isReadyToOperate()) { - this->_logger.logln(F("✖ setNodeProperty(): impossible now")); - return false; - } +HomieClass& HomieClass::setResetTrigger(uint8_t pin, uint8_t state, uint16_t time) { + _checkBeforeSetup(F("setResetTrigger")); + + Interface::get().reset.enabled = true; + Interface::get().reset.triggerPin = pin; + Interface::get().reset.triggerState = state; + Interface::get().reset.triggerTime = time; - strcpy(this->_mqttClient.getTopicBuffer(), this->_config.get().mqtt.baseTopic); - strcat(this->_mqttClient.getTopicBuffer(), this->_config.get().deviceId); - strcat_P(this->_mqttClient.getTopicBuffer(), PSTR("/")); - strcat(this->_mqttClient.getTopicBuffer(), node.getId()); - strcat_P(this->_mqttClient.getTopicBuffer(), PSTR("/")); - strcat(this->_mqttClient.getTopicBuffer(), property); + return *this; +} + +HomieClass& HomieClass::disableResetTrigger() { + _checkBeforeSetup(F("disableResetTrigger")); + + Interface::get().reset.enabled = false; + + return *this; +} + +const ConfigStruct& HomieClass::getConfiguration() { + return Interface::get().getConfig().get(); +} - if (5 + 2 + strlen(this->_mqttClient.getTopicBuffer()) + strlen(value) + 1 > MQTT_MAX_PACKET_SIZE) { - this->_logger.logln(F("✖ setNodeProperty(): content to send is too long")); - return false; +AsyncMqttClient& HomieClass::getMqttClient() { + return _mqttClient; +} + +Logger& HomieClass::getLogger() { + return _logger; +} + +void HomieClass::prepareToSleep() { + Interface::get().getLogger() << F("Flagged for sleep by sketch") << endl; + if (Interface::get().ready) { + Interface::get().disable = true; + Interface::get().flaggedForSleep = true; + } else { + Interface::get().disable = true; + Interface::get().getLogger() << F("Triggering READY_TO_SLEEP event...") << endl; + Interface::get().event.type = HomieEventType::READY_TO_SLEEP; + Interface::get().eventHandler(Interface::get().event); } +} - return this->_mqttClient.publish(value, retained); +void HomieClass::doDeepSleep(uint32_t time_us, RFMode mode) { + Interface::get().getLogger() << F("💤 Device is deep sleeping...") << endl; + Serial.flush(); + ESP.deepSleep(time_us, mode); } HomieClass Homie; diff --git a/src/Homie.h b/src/Homie.h index b61e8e7e..1f41262a 100644 --- a/src/Homie.h +++ b/src/Homie.h @@ -1,6 +1,6 @@ -#ifndef Homie_h -#define Homie_h +#ifndef SRC_HOMIE_H_ +#define SRC_HOMIE_H_ #include "Homie.hpp" -#endif +#endif // SRC_HOMIE_H_ diff --git a/src/Homie.hpp b/src/Homie.hpp index 214c47b9..08461d40 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -1,60 +1,90 @@ #pragma once -#include "Homie/MqttClient.hpp" -#include "Homie/Blinker.hpp" -#include "Homie/Logger.hpp" -#include "Homie/Config.hpp" +#include "Arduino.h" + +#include "AsyncMqttClient.h" +#include "Homie/Datatypes/Interface.hpp" #include "Homie/Constants.hpp" #include "Homie/Limits.hpp" -#include "Homie/Helpers.hpp" +#include "Homie/Utils/DeviceId.hpp" #include "Homie/Boot/Boot.hpp" +#include "Homie/Boot/BootStandalone.hpp" #include "Homie/Boot/BootNormal.hpp" #include "Homie/Boot/BootConfig.hpp" -#include "Homie/Boot/BootOta.hpp" +#include "Homie/Logger.hpp" +#include "Homie/Config.hpp" +#include "Homie/Blinker.hpp" +#include "SendingPromise.hpp" +#include "HomieBootMode.hpp" +#include "HomieEvent.hpp" #include "HomieNode.hpp" +#include "HomieSetting.hpp" +#include "StreamingOperator.hpp" + +// Define DEBUG for debug + +#define Homie_setFirmware(name, version) const char* __FLAGGED_FW_NAME = "\xbf\x84\xe4\x13\x54" name "\x93\x44\x6b\xa7\x75"; const char* __FLAGGED_FW_VERSION = "\x6a\x3f\x3e\x0e\xe1" version "\xb0\x30\x48\xd4\x1a"; Homie.__setFirmware(__FLAGGED_FW_NAME, __FLAGGED_FW_VERSION); +#define Homie_setBrand(brand) const char* __FLAGGED_BRAND = "\xfb\x2a\xf5\x68\xc0" brand "\x6e\x2f\x0f\xeb\x2d"; Homie.__setBrand(__FLAGGED_BRAND); namespace HomieInternals { - class HomieClass { - public: - HomieClass(); - ~HomieClass(); - void setup(); - void loop(); - - void enableLogging(bool enable); - void enableBuiltInLedIndicator(bool enable); - void setLedPin(unsigned char pin, unsigned char on); - void setBrand(const char* name); - void setFirmware(const char* name, const char* version); - void registerNode(const HomieNode& node); - void setGlobalInputHandler(GlobalInputHandler globalInputHandler); - void setResettable(bool resettable); - void onEvent(EventHandler handler); - void setResetTrigger(unsigned char pin, unsigned char state, unsigned int time); - void disableResetTrigger(); - void setResetFunction(ResetFunction function); - void setSetupFunction(OperationFunction function); - void setLoopFunction(OperationFunction function); - bool isReadyToOperate(); - bool setNodeProperty(const HomieNode& node, const String& property, const String& value, bool retained = true) { - return this->setNodeProperty(node, property.c_str(), value.c_str(), retained); - } - bool setNodeProperty(const HomieNode& node, const char* property, const char* value, bool retained = true); - private: - bool _setupCalled; - Boot* _boot; - BootNormal _bootNormal; - BootConfig _bootConfig; - BootOta _bootOta; - Interface _interface; - Logger _logger; - Blinker _blinker; - Config _config; - MqttClient _mqttClient; - - void _checkBeforeSetup(const __FlashStringHelper* functionName); - }; -} +class HomieClass { + friend class ::HomieNode; + friend SendingPromise; + + public: + HomieClass(); + ~HomieClass(); + void setup(); + void loop(); + + void __setFirmware(const char* name, const char* version); + void __setBrand(const char* brand) const; + + HomieClass& disableLogging(); + HomieClass& setLoggingPrinter(Print* printer); + HomieClass& disableLedFeedback(); + HomieClass& setLedPin(uint8_t pin, uint8_t on); + HomieClass& setConfigurationApPassword(const char* password); + HomieClass& setGlobalInputHandler(const GlobalInputHandler& globalInputHandler); + HomieClass& setBroadcastHandler(const BroadcastHandler& broadcastHandler); + HomieClass& onEvent(const EventHandler& handler); + HomieClass& setResetTrigger(uint8_t pin, uint8_t state, uint16_t time); + HomieClass& disableResetTrigger(); + HomieClass& setSetupFunction(const OperationFunction& function); + HomieClass& setLoopFunction(const OperationFunction& function); + HomieClass& setHomieBootMode(HomieBootMode bootMode); + HomieClass& setHomieBootModeOnNextBoot(HomieBootMode bootMode); + + static void reset(); + void reboot(); + static void setIdle(bool idle); + static bool isConfigured(); + static bool isConnected(); + static const ConfigStruct& getConfiguration(); + AsyncMqttClient& getMqttClient(); + Logger& getLogger(); + static void prepareToSleep(); + static void doDeepSleep(uint32_t time_us = 0, RFMode mode = RF_DEFAULT); + + private: + bool _setupCalled; + bool _firmwareSet; + Boot* _boot; + BootStandalone _bootStandalone; + BootNormal _bootNormal; + BootConfig _bootConfig; + bool _flaggedForReboot; + SendingPromise _sendingPromise; + Logger _logger; + Blinker _blinker; + Config _config; + AsyncMqttClient _mqttClient; + + void _checkBeforeSetup(const __FlashStringHelper* functionName) const; + + const char* __HOMIE_SIGNATURE; +}; +} // namespace HomieInternals extern HomieInternals::HomieClass Homie; diff --git a/src/Homie/Blinker.cpp b/src/Homie/Blinker.cpp index 3e76ecc4..ecd18523 100644 --- a/src/Homie/Blinker.cpp +++ b/src/Homie/Blinker.cpp @@ -3,30 +3,24 @@ using namespace HomieInternals; Blinker::Blinker() -: _interface(nullptr) -, _lastBlinkPace(0) -{ -} - -void Blinker::attachInterface(Interface* interface) { - this->_interface = interface; +: _lastBlinkPace(0) { } void Blinker::start(float blinkPace) { - if (this->_lastBlinkPace != blinkPace) { - this->_ticker.attach(blinkPace, this->_tick, this->_interface->led.pin); - this->_lastBlinkPace = blinkPace; + if (_lastBlinkPace != blinkPace) { + _ticker.attach(blinkPace, _tick, Interface::get().led.pin); + _lastBlinkPace = blinkPace; } } void Blinker::stop() { - if (this->_lastBlinkPace != 0) { - this->_ticker.detach(); - this->_lastBlinkPace = 0; - digitalWrite(this->_interface->led.pin, !this->_interface->led.on); + if (_lastBlinkPace != 0) { + _ticker.detach(); + _lastBlinkPace = 0; + digitalWrite(Interface::get().led.pin, !Interface::get().led.on); } } -void Blinker::_tick(unsigned char pin) { +void Blinker::_tick(uint8_t pin) { digitalWrite(pin, !digitalRead(pin)); } diff --git a/src/Homie/Blinker.hpp b/src/Homie/Blinker.hpp index 6c19a5ce..cf0aa302 100644 --- a/src/Homie/Blinker.hpp +++ b/src/Homie/Blinker.hpp @@ -4,18 +4,16 @@ #include "Datatypes/Interface.hpp" namespace HomieInternals { - class Blinker { - public: - Blinker(); - void attachInterface(Interface* interface); - void start(float blinkPace); - void stop(); +class Blinker { + public: + Blinker(); + void start(float blinkPace); + void stop(); - private: - Interface* _interface; - Ticker _ticker; - float _lastBlinkPace; + private: + Ticker _ticker; + float _lastBlinkPace; - static void _tick(unsigned char pin); - }; -} + static void _tick(uint8_t pin); +}; +} // namespace HomieInternals diff --git a/src/Homie/Boot/Boot.cpp b/src/Homie/Boot/Boot.cpp index be205a90..22f81fc5 100644 --- a/src/Homie/Boot/Boot.cpp +++ b/src/Homie/Boot/Boot.cpp @@ -3,34 +3,20 @@ using namespace HomieInternals; Boot::Boot(const char* name) -: _interface(nullptr) -, _name(name) -{ +: _name(name) { } -void Boot::attachInterface(Interface* interface) { - _interface = interface; -} void Boot::setup() { - if (this->_interface->led.enabled) { - pinMode(this->_interface->led.pin, OUTPUT); - digitalWrite(this->_interface->led.pin, !this->_interface->led.on); + if (Interface::get().led.enabled) { + pinMode(Interface::get().led.pin, OUTPUT); + digitalWrite(Interface::get().led.pin, !Interface::get().led.on); } - WiFi.persistent(false); // Don't persist data on EEPROM since this is handled by Homie - WiFi.disconnect(); // Reset network state - - char hostname[MAX_WIFI_SSID_LENGTH]; - strcpy(hostname, this->_interface->brand); - strcat_P(hostname, PSTR("-")); - strcat(hostname, Helpers::getDeviceId()); - - WiFi.hostname(hostname); + WiFi.persistent(true); // Persist data on SDK as it seems Wi-Fi connection is faster - this->_interface->logger->log(F("** Booting into ")); - this->_interface->logger->log(this->_name); - this->_interface->logger->logln(F(" mode **")); + Interface::get().getLogger() << F("💡 Firmware ") << Interface::get().firmware.name << F(" (") << Interface::get().firmware.version << F(")") << endl; + Interface::get().getLogger() << F("🔌 Booting into ") << _name << F(" mode 🔌") << endl; } void Boot::loop() { diff --git a/src/Homie/Boot/Boot.hpp b/src/Homie/Boot/Boot.hpp index c8ef20b1..10eea2ce 100644 --- a/src/Homie/Boot/Boot.hpp +++ b/src/Homie/Boot/Boot.hpp @@ -1,22 +1,22 @@ #pragma once +#include "Arduino.h" + #include +#include "../../StreamingOperator.hpp" #include "../Datatypes/Interface.hpp" #include "../Constants.hpp" #include "../Limits.hpp" -#include "../Logger.hpp" -#include "../Helpers.hpp" +#include "../Utils/Helpers.hpp" namespace HomieInternals { - class Boot { - public: - explicit Boot(const char* name); - virtual void setup(); - virtual void loop(); +class Boot { + public: + explicit Boot(const char* name); + virtual void setup(); + virtual void loop(); - void attachInterface(Interface* interface); - protected: - Interface* _interface; - const char* _name; - }; -} + protected: + const char* _name; +}; +} // namespace HomieInternals diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index 8592a021..d0f5bf28 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -3,16 +3,19 @@ using namespace HomieInternals; BootConfig::BootConfig() -: Boot("config") -, _http(80) -, _ssidCount(0) -, _wifiScanAvailable(false) -, _lastWifiScanEnded(true) -, _jsonWifiNetworks() -, _flaggedForReboot(false) -, _flaggedForRebootAt(0) + : Boot("config") + , _http(80) + , _httpClient() + , _ssidCount(0) + , _wifiScanAvailable(false) + , _lastWifiScanEnded(true) + , _jsonWifiNetworks() + , _flaggedForReboot(false) + , _flaggedForRebootAt(0) + , _proxyEnabled(false) + , _apIpStr{ '\0' } { - this->_wifiScanTimer.setInterval(CONFIG_SCAN_INTERVAL); + _wifiScanTimer.setInterval(CONFIG_SCAN_INTERVAL); } BootConfig::~BootConfig() { @@ -21,224 +24,416 @@ BootConfig::~BootConfig() { void BootConfig::setup() { Boot::setup(); - if (this->_interface->led.enabled) { - digitalWrite(this->_interface->led.pin, this->_interface->led.on); + if (Interface::get().led.enabled) { + digitalWrite(Interface::get().led.pin, Interface::get().led.on); } - const char* deviceId = Helpers::getDeviceId(); + Interface::get().getLogger() << F("Device ID is ") << DeviceId::get() << endl; - this->_interface->logger->log(F("Device ID is ")); - this->_interface->logger->logln(deviceId); - - WiFi.mode(WIFI_AP); + WiFi.mode(WIFI_AP_STA); char apName[MAX_WIFI_SSID_LENGTH]; - strcpy(apName, this->_interface->brand); + strlcpy(apName, Interface::get().brand, MAX_WIFI_SSID_LENGTH - 1 - MAX_MAC_STRING_LENGTH); strcat_P(apName, PSTR("-")); - strcat(apName, Helpers::getDeviceId()); + strcat(apName, DeviceId::get()); WiFi.softAPConfig(ACCESS_POINT_IP, ACCESS_POINT_IP, IPAddress(255, 255, 255, 0)); - WiFi.softAP(apName, deviceId); + if (Interface::get().configurationAp.secured) { + WiFi.softAP(apName, Interface::get().configurationAp.password); + } else { + WiFi.softAP(apName); + } - this->_interface->logger->log(F("AP started as ")); - this->_interface->logger->logln(apName); + Helpers::ipToString(ACCESS_POINT_IP, _apIpStr); - this->_dns.setTTL(300); - this->_dns.setErrorReplyCode(DNSReplyCode::ServerFailure); - this->_dns.start(53, F("homie.config"), ACCESS_POINT_IP); + Interface::get().getLogger() << F("AP started as ") << apName << F(" with IP ") << _apIpStr << endl; + _dns.setTTL(30); + _dns.setErrorReplyCode(DNSReplyCode::NoError); + _dns.start(53, F("*"), ACCESS_POINT_IP); - this->_http.on("/", HTTP_GET, [this]() { - this->_interface->logger->logln(F("Received index request")); - this->_http.send(200, F("text/plain"), F("See Configuration API usage: http://marvinroger.viewdocs.io/homie-esp8266/6.-Configuration-API")); + __setCORS(); + _http.on("/heart", HTTP_GET, [this](AsyncWebServerRequest *request) { + Interface::get().getLogger() << F("Received heart request") << endl; + request->send(204); }); - this->_http.on("/heart", HTTP_GET, [this]() { - this->_interface->logger->logln(F("Received heart request")); - this->_http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), F("{\"heart\":\"beat\"}")); - }); - this->_http.on("/device-info", HTTP_GET, std::bind(&BootConfig::_onDeviceInfoRequest, this)); - this->_http.on("/networks", HTTP_GET, std::bind(&BootConfig::_onNetworksRequest, this)); - this->_http.on("/config", HTTP_PUT, std::bind(&BootConfig::_onConfigRequest, this)); - this->_http.on("/config", HTTP_OPTIONS, [this]() { // CORS - this->_interface->logger->logln(F("Received CORS request for /config")); - this->_http.sendContent(FPSTR(PROGMEM_CONFIG_CORS)); + _http.on("/device-info", HTTP_GET, [this](AsyncWebServerRequest *request) { _onDeviceInfoRequest(request); }); + _http.on("/networks", HTTP_GET, [this](AsyncWebServerRequest *request) { _onNetworksRequest(request); }); + _http.on("/config", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onConfigRequest(request); }).onBody(BootConfig::__parsePost); + _http.on("/wifi/connect", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onWifiConnectRequest(request); }).onBody(BootConfig::__parsePost); + _http.on("/wifi/status", HTTP_GET, [this](AsyncWebServerRequest *request) { _onWifiStatusRequest(request); }); + _http.on("/proxy/control", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onProxyControlRequest(request); }).onBody(BootConfig::__parsePost); + _http.onNotFound([this](AsyncWebServerRequest *request) { + if ( request->method() == HTTP_OPTIONS ) { + Interface::get().getLogger() << F("Received CORS request for ")<< request->url() << endl; + request->send(200); + } else { + _onCaptivePortal(request); + } }); - this->_http.begin(); + _http.begin(); +} + +void BootConfig::loop() { + Boot::loop(); + + _dns.processNextRequest(); + + if (_flaggedForReboot) { + if (millis() - _flaggedForRebootAt >= 3000UL) { + Interface::get().getLogger() << F("↻ Rebooting into normal mode...") << endl; + Serial.flush(); + ESP.restart(); + } + + return; + } + + if (!_lastWifiScanEnded) { + int8_t scanResult = WiFi.scanComplete(); + + switch (scanResult) { + case WIFI_SCAN_RUNNING: + return; + case WIFI_SCAN_FAILED: + Interface::get().getLogger() << F("✖ Wi-Fi scan failed") << endl; + _ssidCount = 0; + _wifiScanTimer.reset(); + break; + default: + Interface::get().getLogger() << F("✔ Wi-Fi scan completed") << endl; + _ssidCount = scanResult; + _generateNetworksJson(); + _wifiScanAvailable = true; + break; + } + + _lastWifiScanEnded = true; + } + + if (_lastWifiScanEnded && _wifiScanTimer.check()) { + Interface::get().getLogger() << F("Triggering Wi-Fi scan...") << endl; + WiFi.scanNetworks(true); + _wifiScanTimer.tick(); + _lastWifiScanEnded = false; + } +} + +void BootConfig::_onWifiConnectRequest(AsyncWebServerRequest *request) { + Interface::get().getLogger() << F("Received Wi-Fi connect request") << endl; + DynamicJsonBuffer parseJsonBuffer(JSON_OBJECT_SIZE(2)); + const char* body = (const char*)(request->_tempObject); + JsonObject& parsedJson = parseJsonBuffer.parseObject(body); + if (!parsedJson.success()) { + __SendJSONError(request, F("✖ Invalid or too big JSON")); + return; + } + + if (!parsedJson.containsKey("ssid") || !parsedJson["ssid"].is() || !parsedJson.containsKey("password") || !parsedJson["password"].is()) { + __SendJSONError(request, F("✖ SSID and password required")); + return; + } + + Interface::get().getLogger() << F("Connecting to Wi-Fi") << endl; + WiFi.begin(parsedJson["ssid"].as(), parsedJson["password"].as()); + + request->send(202, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_JSON_SUCCESS)); +} + +void BootConfig::_onWifiStatusRequest(AsyncWebServerRequest *request) { + Interface::get().getLogger() << F("Received Wi-Fi status request") << endl; + + DynamicJsonBuffer generatedJsonBuffer(JSON_OBJECT_SIZE(2)); + JsonObject& json = generatedJsonBuffer.createObject(); + String status; + + //String json = ""; + switch (WiFi.status()) { + case WL_IDLE_STATUS: + status = F("idle"); + break; + case WL_CONNECT_FAILED: + status = F("connect_failed"); + break; + case WL_CONNECTION_LOST: + status = F("connection_lost"); + break; + case WL_NO_SSID_AVAIL: + status = F("no_ssid_available"); + break; + case WL_CONNECTED: + status = F("connected"); + json["local_ip"] = WiFi.localIP().toString(); + break; + case WL_DISCONNECTED: + status = F("disconnected"); + break; + default: + status = F("other"); + break; + } + + json["status"] = status; + String output; + json.printTo(output); + + request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), output); +} + +void BootConfig::_onProxyControlRequest(AsyncWebServerRequest *request) { + Interface::get().getLogger() << F("Received proxy control request") << endl; + DynamicJsonBuffer parseJsonBuffer(JSON_OBJECT_SIZE(1)); + const char* body = (const char*)(request->_tempObject); + JsonObject& parsedJson = parseJsonBuffer.parseObject(body); // do not use plain String, else fails + if (!parsedJson.success()) { + __SendJSONError(request, F("✖ Invalid or too big JSON")); + return; + } + + if (!parsedJson.containsKey("enable") || !parsedJson["enable"].is()) { + __SendJSONError(request, F("✖ enable parameter is required")); + return; + } + + _proxyEnabled = parsedJson["enable"]; + + request->send(202, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_JSON_SUCCESS)); } void BootConfig::_generateNetworksJson() { - DynamicJsonBuffer generatedJsonBuffer(JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(this->_ssidCount) + (this->_ssidCount * JSON_OBJECT_SIZE(3))); // 1 at root, 3 in childrend + DynamicJsonBuffer generatedJsonBuffer(JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(_ssidCount) + (_ssidCount * JSON_OBJECT_SIZE(3))); // 1 at root, 3 in childrend JsonObject& json = generatedJsonBuffer.createObject(); - int jsonLength = 15; // {"networks":[]} JsonArray& networks = json.createNestedArray("networks"); - for (int network = 0; network < this->_ssidCount; network++) { - jsonLength += 36; // {"ssid":"","rssi":,"encryption":""}, + for (int network = 0; network < _ssidCount; network++) { JsonObject& jsonNetwork = generatedJsonBuffer.createObject(); - jsonLength += WiFi.SSID(network).length(); jsonNetwork["ssid"] = WiFi.SSID(network); - jsonLength += 4; jsonNetwork["rssi"] = WiFi.RSSI(network); - jsonLength += 4; switch (WiFi.encryptionType(network)) { - case ENC_TYPE_WEP: - jsonNetwork["encryption"] = "wep"; - break; - case ENC_TYPE_TKIP: - jsonNetwork["encryption"] = "wpa"; - break; - case ENC_TYPE_CCMP: - jsonNetwork["encryption"] = "wpa2"; - break; - case ENC_TYPE_NONE: - jsonNetwork["encryption"] = "none"; - break; - case ENC_TYPE_AUTO: - jsonNetwork["encryption"] = "auto"; - break; + case ENC_TYPE_WEP: + jsonNetwork["encryption"] = "wep"; + break; + case ENC_TYPE_TKIP: + jsonNetwork["encryption"] = "wpa"; + break; + case ENC_TYPE_CCMP: + jsonNetwork["encryption"] = "wpa2"; + break; + case ENC_TYPE_NONE: + jsonNetwork["encryption"] = "none"; + break; + case ENC_TYPE_AUTO: + jsonNetwork["encryption"] = "auto"; + break; } networks.add(jsonNetwork); } - jsonLength++; // \0 + String output; + json.printTo(output); + _jsonWifiNetworks = output; +} - delete[] this->_jsonWifiNetworks; - this->_jsonWifiNetworks = new char[jsonLength]; - json.printTo(this->_jsonWifiNetworks, jsonLength); +void BootConfig::_onCaptivePortal(AsyncWebServerRequest *request) { + String host = request->host(); + Interface::get().getLogger() << F("Received captive portal request: "); + if (host && !host.equals(_apIpStr)) { + // redirect unknown host requests to self if not connected to Internet yet + if (!_proxyEnabled) { + // Catch any captive portal probe. + // Every browser brand uses a different URL for this purpose + // We MUST redirect all them to local webserver to prevent cache poisoning + String redirectUrl = String("http://"); + redirectUrl.concat(_apIpStr); + Interface::get().getLogger() << F("Redirect: ") << redirectUrl << endl; + request->redirect(redirectUrl); + } else { + // perform transparent proxy to Internet if connected + Interface::get().getLogger() << F("Proxy") << endl; + _proxyHttpRequest(request); + } + } else if (request->url() == "/" && !SPIFFS.exists(CONFIG_UI_BUNDLE_PATH)) { + // UI File not found + String msg = String(F("UI bundle not loaded. See Configuration API usage: http://marvinroger.github.io/homie-esp8266/")); + Interface::get().getLogger() << msg << endl; + request->send(404, F("text/plain"), msg); + } else if (request->url() == "/" && SPIFFS.exists(CONFIG_UI_BUNDLE_PATH)) { + // Respond with UI + Interface::get().getLogger() << F("UI bundle found") << endl; + AsyncWebServerResponse *response = request->beginResponse(SPIFFS.open(CONFIG_UI_BUNDLE_PATH, "r"), F("index.html"), F("text/html")); + request->send(response); + } else { + // Faild to find request + String msg = String(F("Request NOT found for url: ")) + request->url(); + Interface::get().getLogger() << msg << endl; + request->send(404, F("text/plain"), msg); + } } -void BootConfig::_onDeviceInfoRequest() { - this->_interface->logger->logln(F("Received device info request")); +void BootConfig::_proxyHttpRequest(AsyncWebServerRequest *request) { + Interface::get().getLogger() << F("Received transparent proxy request") << endl; + + String url = String("http://"); + url.concat(request->host()); + url.concat(request->url()); - DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(2) + JSON_ARRAY_SIZE(this->_interface->registeredNodesCount) + (this->_interface->registeredNodesCount * JSON_OBJECT_SIZE(2))); - int jsonLength = 82; // {"device_id":"","homie_version":"","firmware":{"name":"","version":""},"nodes":[]} + // send request to destination (as in incoming host header) + _httpClient.setUserAgent(F("ESP8266-Homie")); + _httpClient.begin(url); + // copy headers + for (size_t i = 0; i < request->headers(); i++) { + _httpClient.addHeader(request->headerName(i), request->header(i)); + } + + String method = ""; + switch (request->method()) { + case HTTP_GET: method = F("GET"); break; + case HTTP_PUT: method = F("PUT"); break; + case HTTP_POST: method = F("POST"); break; + case HTTP_DELETE: method = F("DELETE"); break; + case HTTP_OPTIONS: method = F("OPTIONS"); break; + default: break; + } + + Interface::get().getLogger() << F("Proxy sent request to destination") << endl; + const char* body = (const char*)(request->_tempObject); + int _httpCode = _httpClient.sendRequest(method.c_str(), body); + Interface::get().getLogger() << F("Destination response code = ") << _httpCode << endl; + + // bridge response to browser + // copy response headers + Interface::get().getLogger() << F("Bridging received destination contents to client") << endl; + AsyncWebServerResponse* response = request->beginResponse(_httpCode, _httpClient.header("Content-Type"), _httpClient.getString()); + for (int i = 0; i < _httpClient.headers(); i++) { + response->addHeader(_httpClient.headerName(i), _httpClient.header(i)); + } + request->send(response); + _httpClient.end(); +} + +void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { + Interface::get().getLogger() << F("Received device information request") << endl; + auto numSettings = IHomieSetting::settings.size(); + auto numNodes = HomieNode::nodes.size(); + DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(2) + JSON_ARRAY_SIZE(numNodes) + (numNodes * JSON_OBJECT_SIZE(2)) + JSON_ARRAY_SIZE(numSettings) + (numSettings * JSON_OBJECT_SIZE(5))); JsonObject& json = jsonBuffer.createObject(); - jsonLength += strlen(Helpers::getDeviceId()); - json["device_id"] = Helpers::getDeviceId(); - jsonLength += strlen(VERSION); - json["homie_version"] = VERSION; + json["hardware_device_id"] = DeviceId::get(); + json["homie_esp8266_version"] = HOMIE_ESP8266_VERSION; JsonObject& firmware = json.createNestedObject("firmware"); - jsonLength += strlen(this->_interface->firmware.name); - firmware["name"] = this->_interface->firmware.name; - jsonLength += strlen(this->_interface->firmware.version); - firmware["version"] = this->_interface->firmware.version; + firmware["name"] = Interface::get().firmware.name; + firmware["version"] = Interface::get().firmware.version; JsonArray& nodes = json.createNestedArray("nodes"); - for (int i = 0; i < this->_interface->registeredNodesCount; i++) { - jsonLength += 20; // {"id":"","type":""}, - const HomieNode* node = this->_interface->registeredNodes[i]; + for (HomieNode* iNode : HomieNode::nodes) { JsonObject& jsonNode = jsonBuffer.createObject(); - jsonLength += strlen(node->getId()); - jsonNode["id"] = node->getId(); - jsonLength += strlen(node->getType()); - jsonNode["type"] = node->getType(); - + jsonNode["id"] = iNode->getId(); + jsonNode["type"] = iNode->getType(); nodes.add(jsonNode); } - jsonLength++; // \0 + JsonArray& settings = json.createNestedArray("settings"); + for (IHomieSetting* iSetting : IHomieSetting::settings) { + JsonObject& jsonSetting = jsonBuffer.createObject(); + + if (strcmp(iSetting->getType(), "unknown") != 0) { + jsonSetting["name"] = iSetting->getName(); + jsonSetting["description"] = iSetting->getDescription(); + jsonSetting["type"] = iSetting->getType(); + jsonSetting["required"] = iSetting->isRequired(); + + if (!iSetting->isRequired()) { + if (iSetting->isBool()) { + HomieSetting* setting = static_cast*>(iSetting); + jsonSetting["default"] = setting->get(); + } else if (iSetting->isLong()) { + HomieSetting* setting = static_cast*>(iSetting); + jsonSetting["default"] = setting->get(); + } else if (iSetting->isDouble()) { + HomieSetting* setting = static_cast*>(iSetting); + jsonSetting["default"] = setting->get(); + } else if (iSetting->isConstChar()) { + HomieSetting* setting = static_cast*>(iSetting); + jsonSetting["default"] = setting->get(); + } + } + } + + settings.add(jsonSetting); + } - std::unique_ptr jsonString(new char[jsonLength]); - json.printTo(jsonString.get(), jsonLength); - this->_http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), jsonString.get()); + String output; + json.printTo(output); + + request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), output); } -void BootConfig::_onNetworksRequest() { - this->_interface->logger->logln(F("Received networks request")); - if (this->_wifiScanAvailable) { - this->_http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), this->_jsonWifiNetworks); +void BootConfig::_onNetworksRequest(AsyncWebServerRequest *request) { + Interface::get().getLogger() << F("Received networks request") << endl; + if (_wifiScanAvailable) { + request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), _jsonWifiNetworks); } else { - this->_http.send(503, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_NETWORKS_FAILURE)); + __SendJSONError(request, F("Initial Wi-Fi scan not finished yet"), 503); } } -void BootConfig::_onConfigRequest() { - this->_interface->logger->logln(F("Received config request")); - if (this->_flaggedForReboot) { - this->_interface->logger->logln(F("✖ Device already configured")); - String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); - errorJson.concat(F("Device already configured\"}")); - this->_http.send(403, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); +void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { + Interface::get().getLogger() << F("Received config request") << endl; + if (_flaggedForReboot) { + __SendJSONError(request, F("✖ Device already configured"), 403); return; } - StaticJsonBuffer parseJsonBuffer; - size_t bodyLength = _http.arg("plain").length(); - std::unique_ptr bodyString(new char[bodyLength + 1]); - memcpy(bodyString.get(), _http.arg("plain").c_str(), bodyLength); - bodyString.get()[bodyLength] = '\0'; - JsonObject& parsedJson = parseJsonBuffer.parseObject(bodyString.get()); // workaround, cannot pass raw String otherwise JSON parsing fails randomly + DynamicJsonBuffer parseJsonBuffer(MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE); + const char* body = (const char*)(request->_tempObject); + JsonObject& parsedJson = parseJsonBuffer.parseObject(body); if (!parsedJson.success()) { - this->_interface->logger->logln(F("✖ Invalid or too big JSON")); - String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); - errorJson.concat(F("Invalid or too big JSON\"}")); - this->_http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); + __SendJSONError(request, F("✖ Invalid or too big JSON")); return; } - ConfigValidationResult configValidationResult = Helpers::validateConfig(parsedJson); + ConfigValidationResult configValidationResult = Validation::validateConfig(parsedJson); if (!configValidationResult.valid) { - this->_interface->logger->log(F("✖ Config file is not valid, reason: ")); - this->_interface->logger->logln(configValidationResult.reason); - String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); - errorJson.concat(F("Config file is not valid, reason: ")); - errorJson.concat(configValidationResult.reason); - errorJson.concat(F("\"}")); - this->_http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); + __SendJSONError(request, String(F("✖ Config file is not valid, reason: ")) + configValidationResult.reason); return; } - this->_interface->config->write(this->_http.arg("plain")); + Interface::get().getConfig().write(parsedJson); - this->_interface->logger->logln(F("✔ Configured")); + Interface::get().getLogger() << F("✔ Configured") << endl; - this->_http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), F("{\"success\":true}")); + request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_JSON_SUCCESS)); - this->_flaggedForReboot = true; // We don't reboot immediately, otherwise the response above is not sent - this->_flaggedForRebootAt = millis(); + Interface::get().disable = true; + _flaggedForReboot = true; // We don't reboot immediately, otherwise the response above is not sent + _flaggedForRebootAt = millis(); } -void BootConfig::loop() { - Boot::loop(); - - this->_dns.processNextRequest(); - this->_http.handleClient(); +void BootConfig::__setCORS() { + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Origin"), F("*")); + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Methods"), F("GET, PUT")); + DefaultHeaders::Instance().addHeader(F("Access-Control-Allow-Headers"), F("Content-Type, Origin, Referer, User-Agent")); +} - if (this->_flaggedForReboot) { - if (millis() - this->_flaggedForRebootAt >= 3000UL) { - this->_interface->logger->logln(F("↻ Rebooting into normal mode...")); - ESP.restart(); +void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + if (total > MAX_POST_SIZE) { + Interface::get().getLogger() << F("Request is to large to be processed.") << endl; + } else { + if (index == 0) { + request->_tempObject = new char[total + 1]; } - - return; - } - - if (!this->_lastWifiScanEnded) { - int8_t scanResult = WiFi.scanComplete(); - - switch (scanResult) { - case WIFI_SCAN_RUNNING: - return; - case WIFI_SCAN_FAILED: - this->_interface->logger->logln(F("✖ Wi-Fi scan failed")); - this->_ssidCount = 0; - this->_wifiScanTimer.reset(); - break; - default: - this->_interface->logger->logln(F("✔ Wi-Fi scan completed")); - this->_ssidCount = scanResult; - this->_generateNetworksJson(); - this->_wifiScanAvailable = true; - break; + char* buff = reinterpret_cast(request->_tempObject) + index; + memcpy(buff, data, len); + if (index + len == total) { + char* buff = reinterpret_cast(request->_tempObject) + total; + *buff = '\0'; } - - this->_lastWifiScanEnded = true; } +} - if (this->_lastWifiScanEnded && this->_wifiScanTimer.check()) { - this->_interface->logger->logln(F("Triggering Wi-Fi scan...")); - WiFi.scanNetworks(true); - this->_wifiScanTimer.tick(); - this->_lastWifiScanEnded = false; - } +void HomieInternals::BootConfig::__SendJSONError(AsyncWebServerRequest * request, String msg, int16_t code) { + Interface::get().getLogger() << msg << endl; + const String BEGINNING = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); + const String END = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_END)); + String errorJson = BEGINNING + msg + END; + request->send(code, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); } diff --git a/src/Homie/Boot/BootConfig.hpp b/src/Homie/Boot/BootConfig.hpp index 63764c86..6893de60 100644 --- a/src/Homie/Boot/BootConfig.hpp +++ b/src/Homie/Boot/BootConfig.hpp @@ -1,41 +1,63 @@ #pragma once +#include "Arduino.h" + #include #include -#include +#include +#include +#include #include #include #include "Boot.hpp" -#include "../Config.hpp" #include "../Constants.hpp" #include "../Limits.hpp" #include "../Datatypes/Interface.hpp" #include "../Timer.hpp" -#include "../Helpers.hpp" +#include "../Utils/DeviceId.hpp" +#include "../Utils/Validation.hpp" +#include "../Utils/Helpers.hpp" #include "../Logger.hpp" #include "../Strings.hpp" +#include "../../HomieSetting.hpp" +#include "../../StreamingOperator.hpp" namespace HomieInternals { - class BootConfig : public Boot { - public: - BootConfig(); - ~BootConfig(); - void setup(); - void loop(); - private: - ESP8266WebServer _http; - DNSServer _dns; - unsigned char _ssidCount; - bool _wifiScanAvailable; - Timer _wifiScanTimer; - bool _lastWifiScanEnded; - char* _jsonWifiNetworks; - bool _flaggedForReboot; - unsigned long _flaggedForRebootAt; +class BootConfig : public Boot { + public: + BootConfig(); + ~BootConfig(); + void setup(); + void loop(); + + private: + AsyncWebServer _http; + HTTPClient _httpClient; + DNSServer _dns; + uint8_t _ssidCount; + bool _wifiScanAvailable; + Timer _wifiScanTimer; + bool _lastWifiScanEnded; + String _jsonWifiNetworks; + bool _flaggedForReboot; + uint32_t _flaggedForRebootAt; + bool _proxyEnabled; + char _apIpStr[MAX_IP_STRING_LENGTH]; + + void _onCaptivePortal(AsyncWebServerRequest *request); + void _onDeviceInfoRequest(AsyncWebServerRequest *request); + void _onNetworksRequest(AsyncWebServerRequest *request); + void _onConfigRequest(AsyncWebServerRequest *request); + void _generateNetworksJson(); + void _onWifiConnectRequest(AsyncWebServerRequest *request); + void _onProxyControlRequest(AsyncWebServerRequest *request); + void _proxyHttpRequest(AsyncWebServerRequest *request); + void _onWifiStatusRequest(AsyncWebServerRequest *request); - void _onDeviceInfoRequest(); - void _onNetworksRequest(); - void _onConfigRequest(); - void _generateNetworksJson(); - }; -} + // Helpers + static void __setCORS(); + static const int MAX_POST_SIZE = 1500; + static void __parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + static void __SendJSONError(AsyncWebServerRequest *request, String msg, int16_t code = 400); +}; +} // namespace HomieInternals diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index 55dcc833..6176bd17 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -3,479 +3,930 @@ using namespace HomieInternals; BootNormal::BootNormal() -: Boot("normal") -, _setupFunctionCalled(false) -, _wifiConnectNotified(false) -, _wifiDisconnectNotified(true) -, _mqttConnectNotified(false) -, _mqttDisconnectNotified(true) -, _otaVersion {'\0'} -, _flaggedForOta(false) -, _flaggedForReset(false) -{ - this->_wifiReconnectTimer.setInterval(WIFI_RECONNECT_INTERVAL); - this->_mqttReconnectTimer.setInterval(MQTT_RECONNECT_INTERVAL); - this->_signalQualityTimer.setInterval(SIGNAL_QUALITY_SEND_INTERVAL); - this->_uptimeTimer.setInterval(UPTIME_SEND_INTERVAL); + : Boot("normal") + , _mqttReconnectTimer(MQTT_RECONNECT_INITIAL_INTERVAL, MQTT_RECONNECT_MAX_BACKOFF) + , _setupFunctionCalled(false) + , _mqttConnectNotified(false) + , _mqttDisconnectNotified(true) + , _otaOngoing(false) + , _flaggedForReboot(false) + , _mqttOfflineMessageId(0) + , _otaIsBase64(false) + , _otaBase64Pads(0) + , _otaSizeTotal(0) + , _otaSizeDone(0) + , _mqttTopic(nullptr) + , _mqttClientId(nullptr) + , _mqttWillTopic(nullptr) + , _mqttPayloadBuffer(nullptr) + , _mqttTopicLevels(nullptr) + , _mqttTopicLevelsCount(0) { + strlcpy(_fwChecksum, ESP.getSketchMD5().c_str(), sizeof(_fwChecksum)); + _fwChecksum[sizeof(_fwChecksum) - 1] = '\0'; } BootNormal::~BootNormal() { } -void BootNormal::_fillMqttTopic(PGM_P topic) { - strcpy(this->_interface->mqttClient->getTopicBuffer(), this->_interface->config->get().mqtt.baseTopic); - strcat(this->_interface->mqttClient->getTopicBuffer(), this->_interface->config->get().deviceId); - strcat_P(this->_interface->mqttClient->getTopicBuffer(), topic); -} +void BootNormal::setup() { + Boot::setup(); + + Update.runAsync(true); + + _statsTimer.setInterval(Interface::get().getConfig().get().deviceStatsInterval * 1000); + + if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_WIFI_DELAY); + + // Generate topic buffer + size_t baseTopicLength = strlen(Interface::get().getConfig().get().mqtt.baseTopic) + strlen(Interface::get().getConfig().get().deviceId); + size_t longestSubtopicLength = 29 + 1; // /$implementation/ota/firmware + for (HomieNode* iNode : HomieNode::nodes) { + size_t nodeMaxTopicLength = 1 + strlen(iNode->getId()) + 12 + 1; // /id/$properties + if (nodeMaxTopicLength > longestSubtopicLength) longestSubtopicLength = nodeMaxTopicLength; -bool BootNormal::_publishRetainedOrFail(const char* message) { - if (!this->_interface->mqttClient->publish(message, true)) { - this->_interface->mqttClient->disconnect(); - this->_interface->logger->logln(F(" Failed")); - return false; + for (Property* iProperty : iNode->getProperties()) { + size_t propertyMaxTopicLength = 1 + strlen(iNode->getId()) + 1 + strlen(iProperty->getProperty()) + 1; + if (iProperty->isSettable()) propertyMaxTopicLength += 4; // /set + + if (propertyMaxTopicLength > longestSubtopicLength) longestSubtopicLength = propertyMaxTopicLength; + } } + _mqttTopic = std::unique_ptr(new char[baseTopicLength + longestSubtopicLength]); + + _wifiGotIpHandler = WiFi.onStationModeGotIP(std::bind(&BootNormal::_onWifiGotIp, this, std::placeholders::_1)); + _wifiDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&BootNormal::_onWifiDisconnected, this, std::placeholders::_1)); + + Interface::get().getMqttClient().onConnect(std::bind(&BootNormal::_onMqttConnected, this)); + Interface::get().getMqttClient().onDisconnect(std::bind(&BootNormal::_onMqttDisconnected, this, std::placeholders::_1)); + Interface::get().getMqttClient().onMessage(std::bind(&BootNormal::_onMqttMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + Interface::get().getMqttClient().onPublish(std::bind(&BootNormal::_onMqttPublish, this, std::placeholders::_1)); + + Interface::get().getMqttClient().setServer(Interface::get().getConfig().get().mqtt.server.host, Interface::get().getConfig().get().mqtt.server.port); + Interface::get().getMqttClient().setMaxTopicLength(MAX_MQTT_TOPIC_LENGTH); + _mqttClientId = std::unique_ptr(new char[strlen(Interface::get().brand) + 1 + strlen(Interface::get().getConfig().get().deviceId) + 1]); + strcpy(_mqttClientId.get(), Interface::get().brand); + strcat_P(_mqttClientId.get(), PSTR("-")); + strcat(_mqttClientId.get(), Interface::get().getConfig().get().deviceId); + Interface::get().getMqttClient().setClientId(_mqttClientId.get()); + char* mqttWillTopic = _prefixMqttTopic(PSTR("/$online")); + _mqttWillTopic = std::unique_ptr(new char[strlen(mqttWillTopic) + 1]); + memcpy(_mqttWillTopic.get(), mqttWillTopic, strlen(mqttWillTopic) + 1); + Interface::get().getMqttClient().setWill(_mqttWillTopic.get(), 1, true, "false"); + + if (Interface::get().getConfig().get().mqtt.auth) Interface::get().getMqttClient().setCredentials(Interface::get().getConfig().get().mqtt.username, Interface::get().getConfig().get().mqtt.password); - return true; + ResetHandler::Attach(); + + Interface::get().getConfig().log(); + + for (HomieNode* iNode : HomieNode::nodes) { + iNode->setup(); + } + + _wifiConnect(); } -bool BootNormal::_subscribe1OrFail() { - if (!this->_interface->mqttClient->subscribe(1)) { - this->_interface->mqttClient->disconnect(); - this->_interface->logger->logln(F(" Failed")); - return false; +void BootNormal::loop() { + Boot::loop(); + + if (_flaggedForReboot && Interface::get().reset.idle) { + Interface::get().getLogger() << F("Device is idle") << endl; + + Interface::get().getLogger() << F("↻ Rebooting...") << endl; + Serial.flush(); + ESP.restart(); + } + + if (_mqttReconnectTimer.check()) { + _mqttConnect(); + return; + } + + if (!Interface::get().getMqttClient().connected()) return; + + // here, we are connected to the broker + + if (!_advertisementProgress.done) { + _advertise(); + return; + } + + // here, we finished the advertisement + + if (!_mqttConnectNotified) { + Interface::get().ready = true; + if (Interface::get().led.enabled) Interface::get().getBlinker().stop(); + + Interface::get().getLogger() << F("✔ MQTT ready") << endl; + Interface::get().getLogger() << F("Triggering MQTT_READY event...") << endl; + Interface::get().event.type = HomieEventType::MQTT_READY; + Interface::get().eventHandler(Interface::get().event); + + for (HomieNode* iNode : HomieNode::nodes) { + iNode->onReadyToOperate(); + } + + if (!_setupFunctionCalled) { + Interface::get().getLogger() << F("Calling setup function...") << endl; + Interface::get().setupFunction(); + _setupFunctionCalled = true; + } + + _mqttConnectNotified = true; + return; + } + + // here, we have notified the sketch we are ready + + if (_mqttOfflineMessageId == 0 && Interface::get().flaggedForSleep) { + Interface::get().getLogger() << F("Device in preparation to sleep...") << endl; + _mqttOfflineMessageId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$online")), 1, true, "false"); + } + + if (_statsTimer.check()) { + uint8_t quality = Helpers::rssiToPercentage(WiFi.RSSI()); + char qualityStr[3 + 1]; + itoa(quality, qualityStr, 10); + Interface::get().getLogger() << F("〽 Sending statistics...") << endl; + Interface::get().getLogger() << F(" • Wi-Fi signal quality: ") << qualityStr << F("%") << endl; + uint16_t signalPacketId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$stats/signal")), 1, true, qualityStr); + + _uptime.update(); + char uptimeStr[20 + 1]; + itoa(_uptime.getSeconds(), uptimeStr, 10); + Interface::get().getLogger() << F(" • Uptime: ") << uptimeStr << F("s") << endl; + uint16_t uptimePacketId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$stats/uptime")), 1, true, uptimeStr); + + if (signalPacketId != 0 && uptimePacketId != 0) _statsTimer.tick(); } - return true; + Interface::get().loopFunction(); + + for (HomieNode* iNode : HomieNode::nodes) { + iNode->loop(); + } } -void BootNormal::_wifiConnect() { - WiFi.mode(WIFI_STA); - WiFi.begin(this->_interface->config->get().wifi.ssid, this->_interface->config->get().wifi.password); +void BootNormal::_prefixMqttTopic() { + strcpy(_mqttTopic.get(), Interface::get().getConfig().get().mqtt.baseTopic); + strcat(_mqttTopic.get(), Interface::get().getConfig().get().deviceId); } -void BootNormal::_mqttConnect() { - const char* host = this->_interface->config->get().mqtt.server.host; - unsigned int port = this->_interface->config->get().mqtt.server.port; - if (this->_interface->config->get().mqtt.server.mdns.enabled) { - this->_interface->logger->log(F("Querying mDNS service ")); - this->_interface->logger->logln(this->_interface->config->get().mqtt.server.mdns.service); - MdnsQueryResult result = Helpers::mdnsQuery(this->_interface->config->get().mqtt.server.mdns.service); - if (result.success) { - host = result.ip.toString().c_str(); - port = result.port; - this->_interface->logger->log(F("✔ ")); - this->_interface->logger->log(F(" service found at ")); - this->_interface->logger->log(host); - this->_interface->logger->log(F(":")); - this->_interface->logger->logln(port); - } else { - this->_interface->logger->logln(F("✖ Service not found")); - return; - } +char* BootNormal::_prefixMqttTopic(PGM_P topic) { + _prefixMqttTopic(); + strcat_P(_mqttTopic.get(), topic); + + return _mqttTopic.get(); +} + +bool BootNormal::_publishOtaStatus(int status, const char* info) { + String payload(status); + if (info) { + payload.concat(F(" ")); + payload.concat(info); } - this->_interface->mqttClient->setServer(host, port, this->_interface->config->get().mqtt.server.ssl.fingerprint); - this->_interface->mqttClient->setCallback(std::bind(&BootNormal::_mqttCallback, this, std::placeholders::_1, std::placeholders::_2)); + return Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/ota/status")), 0, true, payload.c_str()) != 0; +} - char clientId[MAX_WIFI_SSID_LENGTH]; - strcpy(clientId, this->_interface->brand); - strcat_P(clientId, PSTR("-")); - strcat(clientId, this->_interface->config->get().deviceId); +void BootNormal::_endOtaUpdate(bool success, uint8_t update_error) { + if (success) { + Interface::get().getLogger() << F("✔ OTA succeeded") << endl; + Interface::get().getLogger() << F("Triggering OTA_SUCCESSFUL event...") << endl; + Interface::get().event.type = HomieEventType::OTA_SUCCESSFUL; + Interface::get().eventHandler(Interface::get().event); - this->_fillMqttTopic(PSTR("/$online")); - if (this->_interface->mqttClient->connect(clientId, "false", 2, true, this->_interface->config->get().mqtt.auth, this->_interface->config->get().mqtt.username, this->_interface->config->get().mqtt.password)) { - this->_interface->logger->logln(F("Connected")); - this->_mqttSetup(); + _publishOtaStatus(200); // 200 OK + _flaggedForReboot = true; } else { - this->_interface->logger->log(F("✖ Cannot connect, reason: ")); - switch (this->_interface->mqttClient->getState()) { - case MQTT_CONNECTION_TIMEOUT: - this->_interface->logger->logln(F("MQTT_CONNECTION_TIMEOUT")); - break; - case MQTT_CONNECTION_LOST: - this->_interface->logger->logln(F("MQTT_CONNECTION_LOST")); + int code; + String info; + switch (update_error) { + case UPDATE_ERROR_SIZE: // new firmware size is zero + case UPDATE_ERROR_MAGIC_BYTE: // new firmware does not have 0xE9 in first byte + case UPDATE_ERROR_NEW_FLASH_CONFIG: // bad new flash config (does not match flash ID) + code = 400; // 400 Bad Request + info.concat(F("BAD_FIRMWARE")); break; - case MQTT_CONNECT_FAILED: - this->_interface->logger->logln(F("MQTT_CONNECT_FAILED")); + case UPDATE_ERROR_MD5: + code = 400; // 400 Bad Request + info.concat(F("BAD_CHECKSUM")); break; - case MQTT_DISCONNECTED: - this->_interface->logger->logln(F("MQTT_DISCONNECTED")); + case UPDATE_ERROR_SPACE: + code = 400; // 400 Bad Request + info.concat(F("NOT_ENOUGH_SPACE")); break; - case MQTT_CONNECTED: - this->_interface->logger->logln(F("MQTT_CONNECTED (?)")); - break; - case MQTT_CONNECT_BAD_PROTOCOL: - this->_interface->logger->logln(F("MQTT_CONNECT_BAD_PROTOCOL")); - break; - case MQTT_CONNECT_BAD_CLIENT_ID: - this->_interface->logger->logln(F("MQTT_CONNECT_BAD_CLIENT_ID")); - break; - case MQTT_CONNECT_UNAVAILABLE: - this->_interface->logger->logln(F("MQTT_CONNECT_UNAVAILABLE")); - break; - case MQTT_CONNECT_BAD_CREDENTIALS: - this->_interface->logger->logln(F("MQTT_CONNECT_BAD_CREDENTIALS")); - break; - case MQTT_CONNECT_UNAUTHORIZED: - this->_interface->logger->logln(F("MQTT_CONNECT_UNAUTHORIZED")); + case UPDATE_ERROR_WRITE: + case UPDATE_ERROR_ERASE: + case UPDATE_ERROR_READ: + code = 500; // 500 Internal Server Error + info.concat(F("FLASH_ERROR")); break; default: - this->_interface->logger->logln(F("UNKNOWN")); + code = 500; // 500 Internal Server Error + info.concat(F("INTERNAL_ERROR ")); + info.concat(update_error); + break; } + _publishOtaStatus(code, info.c_str()); + + Interface::get().getLogger() << F("✖ OTA failed (") << code << F(" ") << info << F(")") << endl; + + Interface::get().getLogger() << F("Triggering OTA_FAILED event...") << endl; + Interface::get().event.type = HomieEventType::OTA_FAILED; + Interface::get().eventHandler(Interface::get().event); } + _otaOngoing = false; } -void BootNormal::_mqttSetup() { - this->_interface->logger->log(F("Sending initial information... ")); +void BootNormal::_wifiConnect() { + if (!Interface::get().disable) { + if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_WIFI_DELAY); + Interface::get().getLogger() << F("↕ Attempting to connect to Wi-Fi...") << endl; + + if (WiFi.getMode() != WIFI_STA) WiFi.mode(WIFI_STA); + + WiFi.hostname(Interface::get().getConfig().get().deviceId); + if (strcmp_P(Interface::get().getConfig().get().wifi.ip, PSTR("")) != 0) { // on _validateConfigWifi there is a requirement for mask and gateway + IPAddress convertedIp; + convertedIp.fromString(Interface::get().getConfig().get().wifi.ip); + IPAddress convertedMask; + convertedMask.fromString(Interface::get().getConfig().get().wifi.mask); + IPAddress convertedGateway; + convertedGateway.fromString(Interface::get().getConfig().get().wifi.gw); + + if (strcmp_P(Interface::get().getConfig().get().wifi.dns1, PSTR("")) != 0) { + IPAddress convertedDns1; + convertedDns1.fromString(Interface::get().getConfig().get().wifi.dns1); + if ((strcmp_P(Interface::get().getConfig().get().wifi.dns2, PSTR("")) != 0)) { // on _validateConfigWifi there is requirement that we need dns1 if we want to define dns2 + IPAddress convertedDns2; + convertedDns2.fromString(Interface::get().getConfig().get().wifi.dns2); + WiFi.config(convertedIp, convertedGateway, convertedMask, convertedDns1, convertedDns2); + } else { + WiFi.config(convertedIp, convertedGateway, convertedMask, convertedDns1); + } + } else { + WiFi.config(convertedIp, convertedGateway, convertedMask); + } + } - this->_fillMqttTopic(PSTR("/$online")); - if (!this->_publishRetainedOrFail("true")) return; + if (strcmp_P(Interface::get().getConfig().get().wifi.bssid, PSTR("")) != 0) { + byte bssidBytes[6]; + Helpers::stringToBytes(Interface::get().getConfig().get().wifi.bssid, ':', bssidBytes, 6, 16); + WiFi.begin(Interface::get().getConfig().get().wifi.ssid, Interface::get().getConfig().get().wifi.password, Interface::get().getConfig().get().wifi.channel, bssidBytes); + } else { + WiFi.begin(Interface::get().getConfig().get().wifi.ssid, Interface::get().getConfig().get().wifi.password); + } - char nodes[MAX_REGISTERED_NODES_COUNT * (MAX_NODE_ID_LENGTH + 1 + MAX_NODE_ID_LENGTH + 1) - 1]; - strcpy_P(nodes, PSTR("")); - for (int i = 0; i < this->_interface->registeredNodesCount; i++) { - const HomieNode* node = this->_interface->registeredNodes[i]; - strcat(nodes, node->getId()); - strcat_P(nodes, PSTR(":")); - strcat(nodes, node->getType()); - if (i != this->_interface->registeredNodesCount - 1) strcat_P(nodes, PSTR(",")); - } - this->_fillMqttTopic(PSTR("/$nodes")); - if (!this->_publishRetainedOrFail(nodes)) return; - - this->_fillMqttTopic(PSTR("/$name")); - if (!this->_publishRetainedOrFail(this->_interface->config->get().name)) return; - - IPAddress localIp = WiFi.localIP(); - char localIpStr[15 + 1]; - char localIpPartStr[3 + 1]; - itoa(localIp[0], localIpPartStr, 10); - strcpy(localIpStr, localIpPartStr); - strcat_P(localIpStr, PSTR(".")); - itoa(localIp[1], localIpPartStr, 10); - strcat(localIpStr, localIpPartStr); - strcat_P(localIpStr, PSTR(".")); - itoa(localIp[2], localIpPartStr, 10); - strcat(localIpStr, localIpPartStr); - strcat_P(localIpStr, PSTR(".")); - itoa(localIp[3], localIpPartStr, 10); - strcat(localIpStr, localIpPartStr); - this->_fillMqttTopic(PSTR("/$localip")); - if (!this->_publishRetainedOrFail(localIpStr)) return; - - this->_fillMqttTopic(PSTR("/$fwname")); - if (!this->_publishRetainedOrFail(this->_interface->firmware.name)) return; - - this->_fillMqttTopic(PSTR("/$fwversion")); - if (!this->_publishRetainedOrFail(this->_interface->firmware.version)) return; - - this->_interface->logger->logln(F(" OK")); - - this->_fillMqttTopic(PSTR("/+/+/set")); - this->_interface->logger->log(F("Subscribing to topics... ")); - if (!this->_subscribe1OrFail()) return; - - this->_fillMqttTopic(PSTR("/$reset")); - if (!this->_subscribe1OrFail()) return; - - if (this->_interface->config->get().ota.enabled) { - this->_fillMqttTopic(PSTR("/$ota")); - if (!this->_subscribe1OrFail()) return; + WiFi.setAutoConnect(true); + WiFi.setAutoReconnect(true); } +} - this->_interface->logger->logln(F(" OK")); +void BootNormal::_onWifiGotIp(const WiFiEventStationModeGotIP& event) { + if (Interface::get().led.enabled) Interface::get().getBlinker().stop(); + Interface::get().getLogger() << F("✔ Wi-Fi connected, IP: ") << event.ip << endl; + Interface::get().getLogger() << F("Triggering WIFI_CONNECTED event...") << endl; + Interface::get().event.type = HomieEventType::WIFI_CONNECTED; + Interface::get().event.ip = event.ip; + Interface::get().event.mask = event.mask; + Interface::get().event.gateway = event.gw; + Interface::get().eventHandler(Interface::get().event); + MDNS.begin(Interface::get().getConfig().get().deviceId); + + _mqttConnect(); } -void BootNormal::_mqttCallback(char* topic, char* payload) { - String message = String(payload); - String unified = String(topic); - unified.remove(0, strlen(this->_interface->config->get().mqtt.baseTopic) + strlen(this->_interface->config->get().deviceId) + 1); // Remove devices/${id}/ --- +1 for / - - // Device properties - if (this->_interface->config->get().ota.enabled && unified == "$ota") { - if (message != this->_interface->firmware.version) { - this->_interface->logger->log(F("✴ OTA available (version ")); - this->_interface->logger->log(message); - this->_interface->logger->logln(F(")")); - if (strlen(payload) + 1 <= MAX_FIRMWARE_VERSION_LENGTH) { - strcpy(this->_otaVersion, payload); - this->_flaggedForOta = true; - this->_interface->logger->logln(F("Flagged for OTA")); - } else { - this->_interface->logger->logln(F("Version string received is too long")); - } - } - return; - } else if (unified == "$reset" && message == "true") { - this->_fillMqttTopic(PSTR("/$reset")); - this->_interface->mqttClient->publish("false", true); - this->_flaggedForReset = true; - this->_interface->logger->logln(F("Flagged for reset by network")); - return; +void BootNormal::_onWifiDisconnected(const WiFiEventStationModeDisconnected& event) { + Interface::get().ready = false; + if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_WIFI_DELAY); + _statsTimer.reset(); + Interface::get().getLogger() << F("✖ Wi-Fi disconnected") << endl; + Interface::get().getLogger() << F("Triggering WIFI_DISCONNECTED event...") << endl; + Interface::get().event.type = HomieEventType::WIFI_DISCONNECTED; + Interface::get().event.wifiReason = event.reason; + Interface::get().eventHandler(Interface::get().event); + + _wifiConnect(); +} + +void BootNormal::_mqttConnect() { + if (!Interface::get().disable) { + if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_MQTT_DELAY); + Interface::get().getLogger() << F("↕ Attempting to connect to MQTT...") << endl; + Interface::get().getMqttClient().connect(); } +} - // Implicit node properties - unified.remove(unified.length() - 4, 4); // Remove /set - unsigned int separator = 0; - for (unsigned int i = 0; i < unified.length(); i++) { - if (unified.charAt(i) == '/') { - separator = i; +void BootNormal::_advertise() { + uint16_t packetId; + switch (_advertisementProgress.globalStep) { + case AdvertisementProgress::GlobalStep::PUB_HOMIE: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$homie")), 1, true, HOMIE_VERSION); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_NAME; + break; + case AdvertisementProgress::GlobalStep::PUB_NAME: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$name")), 1, true, Interface::get().getConfig().get().name); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_MAC; + break; + case AdvertisementProgress::GlobalStep::PUB_MAC: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$mac")), 1, true, WiFi.macAddress().c_str()); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_LOCALIP; + break; + case AdvertisementProgress::GlobalStep::PUB_LOCALIP: + { + IPAddress localIp = WiFi.localIP(); + char localIpStr[MAX_IP_STRING_LENGTH]; + Helpers::ipToString(localIp, localIpStr); + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$localip")), 1, true, localIpStr); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_NODES_ATTR; break; } - } - String node = unified.substring(0, separator); - String property = unified.substring(separator + 1); - - int homieNodeIndex = -1; - for (int i = 0; i < this->_interface->registeredNodesCount; i++) { - const HomieNode* homieNode = this->_interface->registeredNodes[i]; - if (node == homieNode->getId()) { - homieNodeIndex = i; + case AdvertisementProgress::GlobalStep::PUB_NODES_ATTR: + { + String nodes; + for (HomieNode* node : HomieNode::nodes) { + nodes.concat(node->getId()); + nodes.concat(F(",")); + } + if (HomieNode::nodes.size() >= 1) nodes.remove(nodes.length() - 1); + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$nodes")), 1, true, nodes.c_str()); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_STATS_INTERVAL; break; } - } - - if (homieNodeIndex == -1) { - this->_interface->logger->log(F("Node ")); - this->_interface->logger->log(node); - this->_interface->logger->logln(F(" not registered")); - return; - } - - const HomieNode* homieNode = this->_interface->registeredNodes[homieNodeIndex]; - - int homieNodePropertyIndex = -1; - for (int i = 0; i < homieNode->getSubscriptionsCount(); i++) { - Subscription subscription = homieNode->getSubscriptions()[i]; - if (property == subscription.property) { - homieNodePropertyIndex = i; + case AdvertisementProgress::GlobalStep::PUB_STATS_INTERVAL: + char statsIntervalStr[3 + 1]; + itoa(STATS_SEND_INTERVAL_SEC / 1000, statsIntervalStr, 10); + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$stats/interval")), 1, true, statsIntervalStr); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_FW_NAME; + break; + case AdvertisementProgress::GlobalStep::PUB_FW_NAME: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$fw/name")), 1, true, Interface::get().firmware.name); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_FW_VERSION; + break; + case AdvertisementProgress::GlobalStep::PUB_FW_VERSION: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$fw/version")), 1, true, Interface::get().firmware.version); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_FW_CHECKSUM; + break; + case AdvertisementProgress::GlobalStep::PUB_FW_CHECKSUM: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$fw/checksum")), 1, true, _fwChecksum); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_IMPLEMENTATION; + break; + case AdvertisementProgress::GlobalStep::PUB_IMPLEMENTATION: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation")), 1, true, "esp8266"); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_IMPLEMENTATION_CONFIG; + break; + case AdvertisementProgress::GlobalStep::PUB_IMPLEMENTATION_CONFIG: + { + char* safeConfigFile = Interface::get().getConfig().getSafeConfigFile(); + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/config")), 1, true, safeConfigFile); + free(safeConfigFile); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_IMPLEMENTATION_VERSION; + break; + } + case AdvertisementProgress::GlobalStep::PUB_IMPLEMENTATION_VERSION: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/version")), 1, true, HOMIE_ESP8266_VERSION); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_IMPLEMENTATION_OTA_ENABLED; + break; + case AdvertisementProgress::GlobalStep::PUB_IMPLEMENTATION_OTA_ENABLED: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/ota/enabled")), 1, true, Interface::get().getConfig().get().ota.enabled ? "true" : "false"); + if (packetId != 0) { + if (HomieNode::nodes.size()) { // skip if no nodes to publish + _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_NODES; + _advertisementProgress.nodeStep = AdvertisementProgress::NodeStep::PUB_TYPE; + _advertisementProgress.currentNodeIndex = 0; + } else { + _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_OTA; + } + } + break; + case AdvertisementProgress::GlobalStep::PUB_NODES: + { + HomieNode* node = HomieNode::nodes[_advertisementProgress.currentNodeIndex]; + std::unique_ptr subtopic = std::unique_ptr(new char[1 + strlen(node->getId()) + 12 + 1]); // /id/$properties + switch (_advertisementProgress.nodeStep) { + case AdvertisementProgress::NodeStep::PUB_TYPE: + strcpy_P(subtopic.get(), PSTR("/")); + strcat(subtopic.get(), node->getId()); + strcat_P(subtopic.get(), PSTR("/$type")); + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(subtopic.get()), 1, true, node->getType()); + if (packetId != 0) _advertisementProgress.nodeStep = AdvertisementProgress::NodeStep::PUB_PROPERTIES; + break; + case AdvertisementProgress::NodeStep::PUB_PROPERTIES: + strcpy_P(subtopic.get(), PSTR("/")); + strcat(subtopic.get(), node->getId()); + strcat_P(subtopic.get(), PSTR("/$properties")); + String properties; + for (Property* iProperty : node->getProperties()) { + properties.concat(iProperty->getProperty()); + if (iProperty->isRange()) { + properties.concat("["); + properties.concat(iProperty->getLower()); + properties.concat("-"); + properties.concat(iProperty->getUpper()); + properties.concat("]"); + } + if (iProperty->isSettable()) properties.concat(":settable"); + properties.concat(","); + } + if (node->getProperties().size() >= 1) properties.remove(properties.length() - 1); + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(subtopic.get()), 1, true, properties.c_str()); + if (packetId != 0) { + if (_advertisementProgress.currentNodeIndex < HomieNode::nodes.size() - 1) { + _advertisementProgress.currentNodeIndex++; + _advertisementProgress.nodeStep = AdvertisementProgress::NodeStep::PUB_TYPE; + } else { + _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_OTA; + } + } + break; + } + break; + } + case AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_OTA: + packetId = Interface::get().getMqttClient().subscribe(_prefixMqttTopic(PSTR("/$implementation/ota/firmware/+")), 1); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_RESET; + break; + case AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_RESET: + packetId = Interface::get().getMqttClient().subscribe(_prefixMqttTopic(PSTR("/$implementation/reset")), 1); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_CONFIG_SET; + break; + case AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_CONFIG_SET: + packetId = Interface::get().getMqttClient().subscribe(_prefixMqttTopic(PSTR("/$implementation/config/set")), 1); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::SUB_SET; + break; + case AdvertisementProgress::GlobalStep::SUB_SET: + packetId = Interface::get().getMqttClient().subscribe(_prefixMqttTopic(PSTR("/+/+/set")), 2); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::SUB_BROADCAST; + break; + case AdvertisementProgress::GlobalStep::SUB_BROADCAST: + { + String broadcast_topic(Interface::get().getConfig().get().mqtt.baseTopic); + broadcast_topic.concat("$broadcast/+"); + packetId = Interface::get().getMqttClient().subscribe(broadcast_topic.c_str(), 2); + if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_ONLINE; break; } + case AdvertisementProgress::GlobalStep::PUB_ONLINE: + packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$online")), 1, true, "true"); + if (packetId != 0) _advertisementProgress.done = true; + break; } +} - if (!homieNode->getSubscribeToAll() && homieNodePropertyIndex == -1) { - this->_interface->logger->log(F("Node ")); - this->_interface->logger->log(node); - this->_interface->logger->log(F(" not subscribed to ")); - this->_interface->logger->logln(property); - return; - } +void BootNormal::_onMqttConnected() { + _mqttDisconnectNotified = false; + _mqttReconnectTimer.deactivate(); - this->_interface->logger->logln(F("Calling global input handler...")); - bool handled = this->_interface->globalInputHandler(node, property, message); - if (handled) return; + Interface::get().getLogger() << F("Sending initial information...") << endl; - this->_interface->logger->logln(F("Calling node input handler...")); - handled = homieNode->getInputHandler()(property, message); - if (handled) return; + _advertise(); +} - if (homieNodePropertyIndex != -1) { // might not if subscribed to all only - Subscription homieNodeSubscription = homieNode->getSubscriptions()[homieNodePropertyIndex]; - this->_interface->logger->logln(F("Calling property input handler...")); - handled = homieNodeSubscription.inputHandler(message); - } +void BootNormal::_onMqttDisconnected(AsyncMqttClientDisconnectReason reason) { + Interface::get().ready = false; + _mqttConnectNotified = false; + _advertisementProgress.done = false; + _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_HOMIE; + _advertisementProgress.nodeStep = AdvertisementProgress::NodeStep::PUB_TYPE; + _advertisementProgress.currentNodeIndex = 0; + if (!_mqttDisconnectNotified) { + _statsTimer.reset(); + Interface::get().getLogger() << F("✖ MQTT disconnected") << endl; + Interface::get().getLogger() << F("Triggering MQTT_DISCONNECTED event...") << endl; + Interface::get().event.type = HomieEventType::MQTT_DISCONNECTED; + Interface::get().event.mqttReason = reason; + Interface::get().eventHandler(Interface::get().event); + + _mqttDisconnectNotified = true; + + if (Interface::get().flaggedForSleep) { + _mqttOfflineMessageId = 0; + Interface::get().getLogger() << F("Triggering READY_TO_SLEEP event...") << endl; + Interface::get().event.type = HomieEventType::READY_TO_SLEEP; + Interface::get().eventHandler(Interface::get().event); + + return; + } + + _mqttConnect(); - if (!handled){ - this->_interface->logger->logln(F("No handlers handled the following packet:")); - this->_interface->logger->log(F(" • Node ID: ")); - this->_interface->logger->logln(node); - this->_interface->logger->log(F(" • Property: ")); - this->_interface->logger->logln(property); - this->_interface->logger->log(F(" • Value: ")); - this->_interface->logger->logln(message); + } else { + _mqttReconnectTimer.activate(); } } -void BootNormal::_handleReset() { - if (this->_interface->reset.enabled) { - this->_resetDebouncer.update(); +void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { + if (total == 0) return; // no empty message possible - if (this->_resetDebouncer.read() == this->_interface->reset.triggerState) { - this->_flaggedForReset = true; - this->_interface->logger->logln(F("Flagged for reset by pin")); - } + // split topic on each "/" + if (index == 0) { + __splitTopic(topic); } - if (this->_interface->reset.userFunction()) { - this->_flaggedForReset = true; - this->_interface->logger->logln(F("Flagged for reset by function")); - } -} + // 1. Handle OTA firmware (not copied to payload buffer) + if (__handleOTAUpdates(topic, payload, properties, len, index, total)) + return; -void BootNormal::setup() { - Boot::setup(); + // 2. Fill Payload Buffer + if (__fillPayloadBuffer(topic, payload, properties, len, index, total)) + return; + + /* Arrived here, the payload is complete */ - this->_interface->mqttClient->initMqtt(this->_interface->config->get().mqtt.server.ssl.enabled); + // 3. handle broadcasts + if (__handleBroadcasts(topic, payload, properties, len, index, total)) + return; - if (this->_interface->reset.enabled) { - pinMode(this->_interface->reset.triggerPin, INPUT_PULLUP); + // 4.all following messages are only for this deviceId + if (strcmp(_mqttTopicLevels.get()[0], Interface::get().getConfig().get().deviceId) != 0) + return; - this->_resetDebouncer.attach(this->_interface->reset.triggerPin); - this->_resetDebouncer.interval(this->_interface->reset.triggerTime); - } + // 5. handle reset + if (__handleResets(topic, payload, properties, len, index, total)) + return; - this->_interface->config->log(); + // 6. handle config set + if (__handleConfig(topic, payload, properties, len, index, total)) + return; - if (this->_interface->config->get().mqtt.server.ssl.enabled) { - this->_interface->logger->log(F("SSL enabled: pushing CPU frequency to 160MHz... ")); - if (system_update_cpu_freq(SYS_CPU_160MHZ)) { - this->_interface->logger->logln(F("OK")); - } else { - this->_interface->logger->logln(F("Failure")); - this->_interface->logger->logln(F("Rebooting...")); - ESP.restart(); - } - } + // 7. here, we're sure we have a node property + if (__handleNodeProperty(topic, payload, properties, len, index, total)) + return; } -void BootNormal::loop() { - Boot::loop(); +void BootNormal::_onMqttPublish(uint16_t id) { + Interface::get().event.type = HomieEventType::MQTT_PACKET_ACKNOWLEDGED; + Interface::get().event.packetId = id; + Interface::get().eventHandler(Interface::get().event); - this->_handleReset(); + if (Interface::get().flaggedForSleep && id == _mqttOfflineMessageId) { + Interface::get().getLogger() << F("Offline message acknowledged. Disconnecting MQTT...") << endl; + Interface::get().getMqttClient().disconnect(); + } +} - if (this->_flaggedForReset && this->_interface->reset.able) { - this->_interface->logger->logln(F("Device is in a resettable state")); - this->_interface->config->erase(); - this->_interface->logger->logln(F("Configuration erased")); +// _onMqttMessage Helpers - this->_interface->logger->logln(F("Triggering HOMIE_ABOUT_TO_RESET event...")); - this->_interface->eventHandler(HOMIE_ABOUT_TO_RESET); +void BootNormal::__splitTopic(char* topic) { + // split topic on each "/" + char* afterBaseTopic = topic + strlen(Interface::get().getConfig().get().mqtt.baseTopic); - this->_interface->logger->logln(F("↻ Rebooting into config mode...")); - ESP.restart(); + uint8_t topicLevelsCount = 1; + for (uint8_t i = 0; i < strlen(afterBaseTopic); i++) { + if (afterBaseTopic[i] == '/') topicLevelsCount++; } - if (this->_flaggedForOta && this->_interface->reset.able) { - this->_interface->logger->logln(F("Device is in a resettable state")); - this->_interface->config->setOtaMode(true, this->_otaVersion); + _mqttTopicLevels = std::unique_ptr(new char*[topicLevelsCount]); + _mqttTopicLevelsCount = topicLevelsCount; - this->_interface->logger->logln(F("↻ Rebooting into OTA mode...")); - ESP.restart(); + const char* delimiter = "/"; + uint8_t topicLevelIndex = 0; + + char* token = strtok(afterBaseTopic, delimiter); + while (token != nullptr) { + _mqttTopicLevels[topicLevelIndex++] = token; + + token = strtok(nullptr, delimiter); } +} - this->_interface->readyToOperate = false; - - if (WiFi.status() != WL_CONNECTED) { - this->_wifiConnectNotified = false; - if (!this->_wifiDisconnectNotified) { - this->_wifiReconnectTimer.reset(); - this->_uptimeTimer.reset(); - this->_signalQualityTimer.reset(); - this->_interface->logger->logln(F("✖ Wi-Fi disconnected")); - this->_interface->logger->logln(F("Triggering HOMIE_WIFI_DISCONNECTED event...")); - this->_interface->eventHandler(HOMIE_WIFI_DISCONNECTED); - this->_wifiDisconnectNotified = true; - } +bool HomieInternals::BootNormal::__fillPayloadBuffer(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { + // Reallocate Buffer everytime a new message is received + if (_mqttPayloadBuffer == nullptr || index == 0) _mqttPayloadBuffer = std::unique_ptr(new char[total + 1]); + + // copy payload into buffer + memcpy(_mqttPayloadBuffer.get() + index, payload, len); + + // return if payload buffer is not complete + if (index + len != total) + return true; + // terminate buffer + _mqttPayloadBuffer.get()[total] = '\0'; + return false; +} + +bool HomieInternals::BootNormal::__handleOTAUpdates(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { + if ( + _mqttTopicLevelsCount == 5 + && strcmp(_mqttTopicLevels.get()[0], Interface::get().getConfig().get().deviceId) == 0 + && strcmp_P(_mqttTopicLevels.get()[1], PSTR("$implementation")) == 0 + && strcmp_P(_mqttTopicLevels.get()[2], PSTR("ota")) == 0 + && strcmp_P(_mqttTopicLevels.get()[3], PSTR("firmware")) == 0 + ) { + if (index == 0) { + Interface::get().getLogger() << F("Receiving OTA payload") << endl; + if (!Interface::get().getConfig().get().ota.enabled) { + _publishOtaStatus(403); // 403 Forbidden + Interface::get().getLogger() << F("✖ Aborting, OTA not enabled") << endl; + return true; + } - if (this->_wifiReconnectTimer.check()) { - this->_interface->logger->logln(F("↕ Attempting to connect to Wi-Fi...")); - this->_wifiReconnectTimer.tick(); - if (this->_interface->led.enabled) { - this->_interface->blinker->start(LED_WIFI_DELAY); + char* firmwareMd5 = _mqttTopicLevels.get()[4]; + if (!Helpers::validateMd5(firmwareMd5)) { + _endOtaUpdate(false, UPDATE_ERROR_MD5); + Interface::get().getLogger() << F("✖ Aborting, invalid MD5") << endl; + return true; + } else if (strcmp(firmwareMd5, _fwChecksum) == 0) { + _publishOtaStatus(304); // 304 Not Modified + Interface::get().getLogger() << F("✖ Aborting, firmware is the same") << endl; + return true; + } else { + Update.setMD5(firmwareMd5); + _publishOtaStatus(202); + _otaOngoing = true; + + Interface::get().getLogger() << F("↕ OTA started") << endl; + Interface::get().getLogger() << F("Triggering OTA_STARTED event...") << endl; + Interface::get().event.type = HomieEventType::OTA_STARTED; + Interface::get().eventHandler(Interface::get().event); } - this->_wifiConnect(); + } else if (!_otaOngoing) { + return true; // we've not validated the checksum } - return; - } - this->_wifiDisconnectNotified = false; - if (!this->_wifiConnectNotified) { - this->_interface->logger->logln(F("✔ Wi-Fi connected")); - this->_interface->logger->logln(F("Triggering HOMIE_WIFI_CONNECTED event...")); - this->_interface->eventHandler(HOMIE_WIFI_CONNECTED); - this->_wifiConnectNotified = true; - } + // here, we need to flash the payload - if (!this->_interface->mqttClient->connected()) { - this->_mqttConnectNotified = false; - if (!this->_mqttDisconnectNotified) { - this->_mqttReconnectTimer.reset(); - this->_uptimeTimer.reset(); - this->_signalQualityTimer.reset(); - this->_interface->logger->logln(F("✖ MQTT disconnected")); - this->_interface->logger->logln(F("Triggering HOMIE_MQTT_DISCONNECTED event...")); - this->_interface->eventHandler(HOMIE_MQTT_DISCONNECTED); - this->_mqttDisconnectNotified = true; + if (index == 0) { + // Autodetect if firmware is binary or base64-encoded. ESP firmware always has a magic first byte 0xE9. + if (*payload == 0xE9) { + _otaIsBase64 = false; + Interface::get().getLogger() << F("Firmware is binary") << endl; + } else { + // Base64-decode first two bytes. Compare decoded value against magic byte. + char plain[2]; // need 12 bits + base64_init_decodestate(&_otaBase64State); + int l = base64_decode_block(payload, 2, plain, &_otaBase64State); + if ((l == 1) && (plain[0] == 0xE9)) { + _otaIsBase64 = true; + _otaBase64Pads = 0; + Interface::get().getLogger() << F("Firmware is base64-encoded") << endl; + if (total % 4) { + // Base64 encoded length not a multiple of 4 bytes + _endOtaUpdate(false, UPDATE_ERROR_MAGIC_BYTE); + return true; + } + + // Restart base64-decoder + base64_init_decodestate(&_otaBase64State); + } else { + // Bad firmware format + _endOtaUpdate(false, UPDATE_ERROR_MAGIC_BYTE); + return true; + } + } + _otaSizeDone = 0; + _otaSizeTotal = _otaIsBase64 ? base64_decode_expected_len(total) : total; + bool success = Update.begin(_otaSizeTotal); + if (!success) { + // Detected error during begin (e.g. size == 0 or size > space) + _endOtaUpdate(false, Update.getError()); + return true; + } } - if (this->_mqttReconnectTimer.check()) { - this->_interface->logger->logln(F("↕ Attempting to connect to MQTT...")); - this->_mqttReconnectTimer.tick(); - if (this->_interface->led.enabled) { - this->_interface->blinker->start(LED_MQTT_DELAY); + size_t write_len; + if (_otaIsBase64) { + // Base64-firmware: Make sure there are no non-base64 characters in the payload. + // libb64/cdecode.c doesn't ignore such characters if the compiler treats `char` + // as `unsigned char`. + size_t bin_len = 0; + char* p = payload; + for (size_t i = 0; i < len; i++) { + char c = *p++; + bool b64 = ((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z')) || ((c >= '0') && (c <= '9')) || (c == '+') || (c == '/'); + if (b64) { + bin_len++; + } else if (c == '=') { + // Ignore "=" padding (but only at the end and only up to 2) + if (index + i < total - 2) { + _endOtaUpdate(false, UPDATE_ERROR_MAGIC_BYTE); + return true; + } + // Note the number of pad characters at the end + _otaBase64Pads++; + } else { + // Non-base64 character in firmware + _endOtaUpdate(false, UPDATE_ERROR_MAGIC_BYTE); + return true; + } + } + if (bin_len > 0) { + // Decode base64 payload in-place. base64_decode_block() can decode in-place, + // except for the first two base64-characters which make one binary byte plus + // 4 extra bits (saved in _otaBase64State). So we "manually" decode the first + // two characters into a temporary buffer and manually merge that back into + // the payload. This one is a little tricky, but it saves us from having to + // dynamically allocate some 800 bytes of memory for every payload chunk. + size_t dec_len = bin_len > 1 ? 2 : 1; + char c; + write_len = (size_t)base64_decode_block(payload, dec_len, &c, &_otaBase64State); + *payload = c; + + if (bin_len > 1) { + write_len += (size_t)base64_decode_block((const char*)payload + dec_len, bin_len - dec_len, payload + write_len, &_otaBase64State); + } + } else { + write_len = 0; } - this->_mqttConnect(); + } else { + // Binary firmware + write_len = len; } - return; - } else { - if (this->_interface->led.enabled) { - this->_interface->blinker->stop(); + if (write_len > 0) { + bool success = Update.write(reinterpret_cast(payload), write_len) > 0; + if (success) { + // Flash write successful. + _otaSizeDone += write_len; + if (_otaIsBase64 && (index + len == total)) { + // Having received the last chunk of base64 encoded firmware, we can now determine + // the real size of the binary firmware from the number of padding character ("="): + // If we have received 1 pad character, real firmware size modulo 3 was 2. + // If we have received 2 pad characters, real firmware size modulo 3 was 1. + // Correct the total firmware length accordingly. + _otaSizeTotal -= _otaBase64Pads; + } + + String progress(_otaSizeDone); + progress.concat(F("/")); + progress.concat(_otaSizeTotal); + Interface::get().getLogger() << F("Receiving OTA firmware (") << progress << F(")...") << endl; + + Interface::get().event.type = HomieEventType::OTA_PROGRESS; + Interface::get().event.sizeDone = _otaSizeDone; + Interface::get().event.sizeTotal = _otaSizeTotal; + Interface::get().eventHandler(Interface::get().event); + + _publishOtaStatus(206, progress.c_str()); // 206 Partial Content + + // Done with the update? + if (index + len == total) { + // With base64-coded firmware, we may have provided a length off by one or two + // to Update.begin() because the base64-coded firmware may use padding (one or + // two "=") at the end. In case of base64, total length was adjusted above. + // Check the real length here and ask Update::end() to skip this test. + if ((_otaIsBase64) && (_otaSizeDone != _otaSizeTotal)) { + _endOtaUpdate(false, UPDATE_ERROR_SIZE); + return true; + } + success = Update.end(_otaIsBase64); + _endOtaUpdate(success, Update.getError()); + } + } else { + // Error erasing or writing flash + _endOtaUpdate(false, Update.getError()); + } } + return true; } + return false; +} - this->_interface->readyToOperate = true; - - this->_mqttDisconnectNotified = false; - if (!this->_mqttConnectNotified) { - this->_interface->logger->logln(F("✔ MQTT ready")); - this->_interface->logger->logln(F("Triggering HOMIE_MQTT_CONNECTED event...")); - this->_interface->eventHandler(HOMIE_MQTT_CONNECTED); - this->_mqttConnectNotified = true; +bool HomieInternals::BootNormal::__handleBroadcasts(char * topic, char * payload, const AsyncMqttClientMessageProperties & properties, size_t len, size_t index, size_t total) { + if ( + _mqttTopicLevelsCount == 2 + && strcmp_P(_mqttTopicLevels.get()[0], PSTR("$broadcast")) == 0 + ) { + String broadcastLevel(_mqttTopicLevels.get()[1]); + Interface::get().getLogger() << F("📢 Calling broadcast handler...") << endl; + bool handled = Interface::get().broadcastHandler(broadcastLevel, _mqttPayloadBuffer.get()); + if (!handled) { + Interface::get().getLogger() << F("The following broadcast was not handled:") << endl; + Interface::get().getLogger() << F(" • Level: ") << broadcastLevel << endl; + Interface::get().getLogger() << F(" • Value: ") << _mqttPayloadBuffer.get() << endl; + } + return true; } + return false; +} - if (!this->_setupFunctionCalled) { - this->_interface->logger->logln(F("Calling setup function...")); - this->_interface->setupFunction(); - this->_setupFunctionCalled = true; +bool HomieInternals::BootNormal::__handleResets(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { + if ( + _mqttTopicLevelsCount == 3 + && strcmp_P(_mqttTopicLevels.get()[1], PSTR("$implementation")) == 0 + && strcmp_P(_mqttTopicLevels.get()[2], PSTR("reset")) == 0 + && strcmp_P(_mqttPayloadBuffer.get(), PSTR("true")) == 0 + ) { + Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/reset")), 1, true, "false"); + Interface::get().getLogger() << F("Flagged for reset by network") << endl; + Interface::get().disable = true; + Interface::get().reset.resetFlag = true; + return true; } + return false; +} - if (this->_signalQualityTimer.check()) { - int32_t rssi = WiFi.RSSI(); - unsigned char quality; - if (rssi <= -100) { - quality = 0; - } else if (rssi >= -50) { - quality = 100; +bool HomieInternals::BootNormal::__handleConfig(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { + if ( + _mqttTopicLevelsCount == 4 + && strcmp_P(_mqttTopicLevels.get()[1], PSTR("$implementation")) == 0 + && strcmp_P(_mqttTopicLevels.get()[2], PSTR("config")) == 0 + && strcmp_P(_mqttTopicLevels.get()[3], PSTR("set")) == 0 + ) { + Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/config/set")), 1, true, ""); + if (Interface::get().getConfig().patch(_mqttPayloadBuffer.get())) { + Interface::get().getLogger() << F("✔ Configuration updated") << endl; + _flaggedForReboot = true; + Interface::get().getLogger() << F("Flagged for reboot") << endl; } else { - quality = 2 * (rssi + 100); + Interface::get().getLogger() << F("✖ Configuration not updated") << endl; } + return true; + } + return false; +} - char qualityStr[3 + 1]; - itoa(quality, qualityStr, 10); +bool HomieInternals::BootNormal::__handleNodeProperty(char * topic, char * payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) { + // initialize HomieRange + HomieRange range; + range.isRange = false; + range.index = 0; + + char* node = _mqttTopicLevels.get()[1]; + char* property = _mqttTopicLevels.get()[2]; + HomieNode* homieNode = HomieNode::find(node); + if (!homieNode) { + Interface::get().getLogger() << F("Node ") << node << F(" not registered") << endl; + return true; + } - this->_interface->logger->log(F("Sending Wi-Fi signal quality (")); - this->_interface->logger->log(qualityStr); - this->_interface->logger->log(F("%)... ")); +#ifdef DEBUG + Interface::get().getLogger() << F("Recived network message for ") << homieNode->getId() << endl; +#endif // DEBUG - this->_fillMqttTopic(PSTR("/$signal")); - if (this->_interface->mqttClient->publish(qualityStr, true)) { - this->_interface->logger->logln(F(" OK")); - this->_signalQualityTimer.tick(); - } else { - this->_interface->logger->logln(F(" Failure")); + int16_t rangeSeparator = -1; + for (uint16_t i = 0; i < strlen(property); i++) { + if (property[i] == '_') { + rangeSeparator = i; + break; } } + if (rangeSeparator != -1) { + range.isRange = true; + property[rangeSeparator] = '\0'; + char* rangeIndexStr = property + rangeSeparator + 1; + String rangeIndexTest = String(rangeIndexStr); + for (uint8_t i = 0; i < rangeIndexTest.length(); i++) { + if (!isDigit(rangeIndexTest.charAt(i))) { + Interface::get().getLogger() << F("Range index ") << rangeIndexStr << F(" is not valid") << endl; + return true; + } + } + range.index = rangeIndexTest.toInt(); + } - if (this->_uptimeTimer.check()) { - this->_uptime.update(); - - char uptimeStr[10 + 1]; - itoa(this->_uptime.getSeconds(), uptimeStr, 10); + Property* propertyObject = nullptr; + for (Property* iProperty : homieNode->getProperties()) { + if (range.isRange) { + if (iProperty->isRange() && strcmp(property, iProperty->getProperty()) == 0) { + if (range.index >= iProperty->getLower() && range.index <= iProperty->getUpper()) { + propertyObject = iProperty; + break; + } else { + Interface::get().getLogger() << F("Range index ") << range.index << F(" is not within the bounds of ") << property << endl; + return true; + } + } + } else if (strcmp(property, iProperty->getProperty()) == 0) { + propertyObject = iProperty; + break; + } + } - this->_interface->logger->log(F("Sending uptime (")); - this->_interface->logger->log(this->_uptime.getSeconds()); - this->_interface->logger->log(F("s)... ")); + if (!propertyObject || !propertyObject->isSettable()) { + Interface::get().getLogger() << F("Node ") << node << F(": ") << property << F(" property not settable") << endl; + return true; + } - this->_fillMqttTopic(PSTR("/$uptime")); - if (this->_interface->mqttClient->publish(uptimeStr, true)) { - this->_interface->logger->logln(F(" OK")); - this->_uptimeTimer.tick(); +#ifdef DEBUG + Interface::get().getLogger() << F("Calling global input handler...") << endl; +#endif // DEBUG + bool handled = Interface::get().globalInputHandler(*homieNode, String(property), range, String(_mqttPayloadBuffer.get())); + if (handled) return true; + +#ifdef DEBUG + Interface::get().getLogger() << F("Calling node input handler...") << endl; +#endif // DEBUG + handled = homieNode->handleInput(String(property), range, String(_mqttPayloadBuffer.get())); + if (handled) return true; + +#ifdef DEBUG + Interface::get().getLogger() << F("Calling property input handler...") << endl; +#endif // DEBUG + handled = propertyObject->getInputHandler()(range, String(_mqttPayloadBuffer.get())); + + if (!handled) { + Interface::get().getLogger() << F("No handlers handled the following packet:") << endl; + Interface::get().getLogger() << F(" • Node ID: ") << node << endl; + Interface::get().getLogger() << F(" • Property: ") << property << endl; + Interface::get().getLogger() << F(" • Is range? "); + if (range.isRange) { + Interface::get().getLogger() << F("yes (") << range.index << F(")") << endl; } else { - this->_interface->logger->logln(F(" Failure")); + Interface::get().getLogger() << F("no") << endl; } + Interface::get().getLogger() << F(" • Value: ") << _mqttPayloadBuffer.get() << endl; } - this->_interface->loopFunction(); - - this->_interface->mqttClient->loop(); + return false; } diff --git a/src/Homie/Boot/BootNormal.hpp b/src/Homie/Boot/BootNormal.hpp index 851272be..8d119715 100644 --- a/src/Homie/Boot/BootNormal.hpp +++ b/src/Homie/Boot/BootNormal.hpp @@ -1,56 +1,113 @@ #pragma once +#include "Arduino.h" + #include +#include #include -#include +#include +#include #include "../../HomieNode.hpp" +#include "../../HomieRange.hpp" +#include "../../StreamingOperator.hpp" #include "../Constants.hpp" #include "../Limits.hpp" #include "../Datatypes/Interface.hpp" -#include "../MqttClient.hpp" -#include "../Helpers.hpp" -#include "../Config.hpp" -#include "../Blinker.hpp" +#include "../Utils/Helpers.hpp" #include "../Uptime.hpp" #include "../Timer.hpp" -#include "../Logger.hpp" +#include "../ExponentialBackoffTimer.hpp" #include "Boot.hpp" - -extern "C" { - #include "user_interface.h" -} +#include "../Utils/ResetHandler.hpp" namespace HomieInternals { - class BootNormal : public Boot { - public: - BootNormal(); - ~BootNormal(); - void setup(); - void loop(); - - private: - Uptime _uptime; - Timer _wifiReconnectTimer; - Timer _mqttReconnectTimer; - Timer _signalQualityTimer; - Timer _uptimeTimer; - bool _setupFunctionCalled; - bool _wifiConnectNotified; - bool _wifiDisconnectNotified; - bool _mqttConnectNotified; - bool _mqttDisconnectNotified; - char _otaVersion[MAX_FIRMWARE_VERSION_LENGTH]; - bool _flaggedForOta; - bool _flaggedForReset; - Bounce _resetDebouncer; - - void _handleReset(); - void _wifiConnect(); - void _mqttConnect(); - void _mqttSetup(); - void _mqttCallback(char* topic, char* message); - void _fillMqttTopic(PGM_P topic); - bool _publishRetainedOrFail(const char* message); - bool _subscribe1OrFail(); - }; -} +class BootNormal : public Boot { + public: + BootNormal(); + ~BootNormal(); + void setup(); + void loop(); + + private: + struct AdvertisementProgress { + bool done = false; + enum class GlobalStep { + PUB_HOMIE, + PUB_NAME, + PUB_MAC, + PUB_LOCALIP, + PUB_NODES_ATTR, + PUB_STATS_INTERVAL, + PUB_FW_NAME, + PUB_FW_VERSION, + PUB_FW_CHECKSUM, + PUB_IMPLEMENTATION, + PUB_IMPLEMENTATION_CONFIG, + PUB_IMPLEMENTATION_VERSION, + PUB_IMPLEMENTATION_OTA_ENABLED, + PUB_NODES, + SUB_IMPLEMENTATION_OTA, + SUB_IMPLEMENTATION_RESET, + SUB_IMPLEMENTATION_CONFIG_SET, + SUB_SET, + SUB_BROADCAST, + PUB_ONLINE + } globalStep; + + enum class NodeStep { + PUB_TYPE, + PUB_PROPERTIES + } nodeStep; + + size_t currentNodeIndex; + } _advertisementProgress; + Uptime _uptime; + Timer _statsTimer; + ExponentialBackoffTimer _mqttReconnectTimer; + bool _setupFunctionCalled; + WiFiEventHandler _wifiGotIpHandler; + WiFiEventHandler _wifiDisconnectedHandler; + bool _mqttConnectNotified; + bool _mqttDisconnectNotified; + bool _otaOngoing; + bool _flaggedForReboot; + uint16_t _mqttOfflineMessageId; + char _fwChecksum[32 + 1]; + bool _otaIsBase64; + base64_decodestate _otaBase64State; + size_t _otaBase64Pads; + size_t _otaSizeTotal; + size_t _otaSizeDone; + + std::unique_ptr _mqttTopic; + + std::unique_ptr _mqttClientId; + std::unique_ptr _mqttWillTopic; + std::unique_ptr _mqttPayloadBuffer; + std::unique_ptr _mqttTopicLevels; + uint8_t _mqttTopicLevelsCount; + + void _wifiConnect(); + void _onWifiGotIp(const WiFiEventStationModeGotIP& event); + void _onWifiDisconnected(const WiFiEventStationModeDisconnected& event); + void _mqttConnect(); + void _advertise(); + void _onMqttConnected(); + void _onMqttDisconnected(AsyncMqttClientDisconnectReason reason); + void _onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total); + void _onMqttPublish(uint16_t id); + void _prefixMqttTopic(); + char* _prefixMqttTopic(PGM_P topic); + bool _publishOtaStatus(int status, const char* info = nullptr); + void _endOtaUpdate(bool success, uint8_t update_error = UPDATE_ERROR_OK); + + // _onMqttMessage Helpers + void __splitTopic(char* topic); + bool __fillPayloadBuffer(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleOTAUpdates(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleBroadcasts(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleResets(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleConfig(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleNodeProperty(char* topic, char* payload, const AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); +}; +} // namespace HomieInternals diff --git a/src/Homie/Boot/BootOta.cpp b/src/Homie/Boot/BootOta.cpp deleted file mode 100644 index 9debade3..00000000 --- a/src/Homie/Boot/BootOta.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "BootOta.hpp" - -using namespace HomieInternals; - -BootOta::BootOta() -: Boot("OTA") -{ -} - -BootOta::~BootOta() { -} - -void BootOta::setup() { - Boot::setup(); - - WiFi.mode(WIFI_STA); - - int wifiAttempts = 1; - while (WiFi.waitForConnectResult() != WL_CONNECTED) { - if (wifiAttempts++ <= 3) { - WiFi.begin(this->_interface->config->get().wifi.ssid, this->_interface->config->get().wifi.password); - this->_interface->logger->log(F("↕ Connecting to Wi-Fi (attempt ")); - this->_interface->logger->log(wifiAttempts); - this->_interface->logger->logln(F("/3)")); - } else { - this->_interface->logger->logln(F("✖ Connection failed")); - this->_interface->logger->logln(F("↻ Rebooting into normal mode...")); - this->_interface->config->setOtaMode(false); - ESP.restart(); - } - } - this->_interface->logger->logln(F("✔ Connected to Wi-Fi")); - - const char* host = this->_interface->config->get().ota.server.host; - unsigned int port = this->_interface->config->get().ota.server.port; - if (this->_interface->config->get().mqtt.server.mdns.enabled) { - this->_interface->logger->log(F("Querying mDNS service ")); - this->_interface->logger->logln(this->_interface->config->get().mqtt.server.mdns.service); - MdnsQueryResult result = Helpers::mdnsQuery(this->_interface->config->get().mqtt.server.mdns.service); - if (result.success) { - host = result.ip.toString().c_str(); - port = result.port; - this->_interface->logger->log(F("✔ ")); - this->_interface->logger->log(F(" service found at ")); - this->_interface->logger->log(host); - this->_interface->logger->log(F(":")); - this->_interface->logger->logln(port); - } else { - this->_interface->logger->logln(F("✖ Service not found")); - ESP.restart(); - } - } - - this->_interface->logger->logln(F("Starting OTA...")); - - - char dataToPass[(MAX_DEVICE_ID_LENGTH - 1) + 1 + (MAX_FIRMWARE_NAME_LENGTH - 1) + 1 + (MAX_FIRMWARE_VERSION_LENGTH - 1) + 1 + (MAX_FIRMWARE_VERSION_LENGTH - 1) + 1]; - strcpy(dataToPass, this->_interface->config->get().deviceId); - strcat(dataToPass, "="); - strcat(dataToPass, this->_interface->firmware.name); - strcat(dataToPass, "="); - strcat(dataToPass, this->_interface->firmware.version); - strcat(dataToPass, "="); - strcat(dataToPass, this->_interface->config->getOtaVersion()); - t_httpUpdate_return result = ESPhttpUpdate.update(host, port, this->_interface->config->get().ota.path, dataToPass, this->_interface->config->get().ota.server.ssl.enabled, this->_interface->config->get().ota.server.ssl.fingerprint, false); - switch (result) { - case HTTP_UPDATE_FAILED: - this->_interface->logger->logln(F("✖ Update failed")); - break; - case HTTP_UPDATE_NO_UPDATES: - this->_interface->logger->logln(F("✖ No updates")); - break; - case HTTP_UPDATE_OK: - this->_interface->logger->logln(F("✔ Success")); - break; - } - - this->_interface->logger->logln(F("↻ Rebooting into normal mode...")); - this->_interface->config->setOtaMode(false); - ESP.restart(); -} - -void BootOta::loop() { - Boot::loop(); -} diff --git a/src/Homie/Boot/BootOta.hpp b/src/Homie/Boot/BootOta.hpp deleted file mode 100644 index e435a920..00000000 --- a/src/Homie/Boot/BootOta.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include -#include -#include "../Constants.hpp" -#include "../Datatypes/Interface.hpp" -#include "../Config.hpp" -#include "../Logger.hpp" -#include "Boot.hpp" - -namespace HomieInternals { - class BootOta : public Boot { - public: - BootOta(); - ~BootOta(); - void setup(); - void loop(); - - private: - }; -} diff --git a/src/Homie/Boot/BootStandalone.cpp b/src/Homie/Boot/BootStandalone.cpp new file mode 100644 index 00000000..bd326782 --- /dev/null +++ b/src/Homie/Boot/BootStandalone.cpp @@ -0,0 +1,22 @@ +#include "BootStandalone.hpp" + +using namespace HomieInternals; + +BootStandalone::BootStandalone() + : Boot("standalone") { +} + +BootStandalone::~BootStandalone() { +} + +void BootStandalone::setup() { + Boot::setup(); + + WiFi.mode(WIFI_OFF); + + ResetHandler::Attach(); +} + +void BootStandalone::loop() { + Boot::loop(); +} diff --git a/src/Homie/Boot/BootStandalone.hpp b/src/Homie/Boot/BootStandalone.hpp new file mode 100644 index 00000000..2d3f24ea --- /dev/null +++ b/src/Homie/Boot/BootStandalone.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "Arduino.h" + +#include "Boot.hpp" +#include "../../StreamingOperator.hpp" +#include "../Utils/ResetHandler.hpp" + +namespace HomieInternals { +class BootStandalone : public Boot { + public: + BootStandalone(); + ~BootStandalone(); + void setup(); + void loop(); +}; +} // namespace HomieInternals diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 062f486a..0c6d5465 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -3,104 +3,105 @@ using namespace HomieInternals; Config::Config() -: _interface(nullptr) -, _configStruct() -, _otaVersion {'\0'} -, _spiffsBegan(false) -{ -} - -void Config::attachInterface(Interface* interface) { - this->_interface = interface; + : _configStruct() + , _spiffsBegan(false) + , _valid(false) { } bool Config::_spiffsBegin() { - if (!this->_spiffsBegan) { - this->_spiffsBegan = SPIFFS.begin(); - if (!this->_spiffsBegan) this->_interface->logger->logln(F("✖ Cannot mount filesystem")); + if (!_spiffsBegan) { + _spiffsBegan = SPIFFS.begin(); + if (!_spiffsBegan) Interface::get().getLogger() << F("✖ Cannot mount filesystem") << endl; } - return this->_spiffsBegan; + return _spiffsBegan; } bool Config::load() { - if (!this->_spiffsBegin()) { return false; } + if (!_spiffsBegin()) { return false; } - this->_bootMode = BOOT_CONFIG; + _valid = false; if (!SPIFFS.exists(CONFIG_FILE_PATH)) { - this->_interface->logger->log(F("✖ ")); - this->_interface->logger->log(CONFIG_FILE_PATH); - this->_interface->logger->logln(F(" doesn't exist")); + Interface::get().getLogger() << F("✖ ") << CONFIG_FILE_PATH << F(" doesn't exist") << endl; return false; } File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); if (!configFile) { - this->_interface->logger->logln(F("✖ Cannot open config file")); + Interface::get().getLogger() << F("✖ Cannot open config file") << endl; return false; } size_t configSize = configFile.size(); - if (configSize > MAX_JSON_CONFIG_FILE_BUFFER_SIZE) { - this->_interface->logger->logln(F("✖ Config file too big")); + if (configSize > MAX_JSON_CONFIG_FILE_SIZE) { + Interface::get().getLogger() << F("✖ Config file too big") << endl; return false; } - char buf[MAX_JSON_CONFIG_FILE_BUFFER_SIZE]; + char buf[MAX_JSON_CONFIG_FILE_SIZE]; configFile.readBytes(buf, configSize); configFile.close(); + buf[configSize] = '\0'; StaticJsonBuffer jsonBuffer; JsonObject& parsedJson = jsonBuffer.parseObject(buf); if (!parsedJson.success()) { - this->_interface->logger->logln(F("✖ Invalid JSON in the config file")); + Interface::get().getLogger() << F("✖ Invalid JSON in the config file") << endl; return false; } - ConfigValidationResult configValidationResult = Helpers::validateConfig(parsedJson); + ConfigValidationResult configValidationResult = Validation::validateConfig(parsedJson); if (!configValidationResult.valid) { - this->_interface->logger->log(F("✖ Config file is not valid, reason: ")); - this->_interface->logger->logln(configValidationResult.reason); + Interface::get().getLogger() << F("✖ Config file is not valid, reason: ") << configValidationResult.reason << endl; return false; } - if (SPIFFS.exists(CONFIG_OTA_PATH)) { - this->_bootMode = BOOT_OTA; - - File otaFile = SPIFFS.open(CONFIG_OTA_PATH, "r"); - if (otaFile) { - size_t otaSize = otaFile.size(); - otaFile.readBytes(this->_otaVersion, otaSize); - otaFile.close(); - } else { - this->_interface->logger->logln(F("✖ Cannot open OTA file")); - } - } else { - this->_bootMode = BOOT_NORMAL; - } - const char* reqName = parsedJson["name"]; const char* reqWifiSsid = parsedJson["wifi"]["ssid"]; const char* reqWifiPassword = parsedJson["wifi"]["password"]; - bool reqMqttMdns = false; - if (parsedJson["mqtt"].as().containsKey("mdns")) reqMqttMdns = true; - bool reqOtaMdns = false; - if (parsedJson["ota"].as().containsKey("mdns")) reqOtaMdns = true; - - const char* reqMqttHost = ""; - const char* reqMqttMdnsService = ""; - if (reqMqttMdns) { - reqMqttMdnsService = parsedJson["mqtt"]["mdns"]; - } else { - reqMqttHost = parsedJson["mqtt"]["host"]; - } - const char* reqDeviceId = Helpers::getDeviceId(); + + const char* reqMqttHost = parsedJson["mqtt"]["host"]; + const char* reqDeviceId = DeviceId::get(); if (parsedJson.containsKey("device_id")) { reqDeviceId = parsedJson["device_id"]; } - unsigned int reqMqttPort = DEFAULT_MQTT_PORT; + uint16_t regDeviceStatsInterval = STATS_SEND_INTERVAL_SEC; //device_stats_interval + if (parsedJson.containsKey(F("device_stats_interval"))) { + regDeviceStatsInterval = parsedJson[F("device_stats_interval")]; + } + + const char* reqWifiBssid = ""; + if (parsedJson["wifi"].as().containsKey("bssid")) { + reqWifiBssid = parsedJson["wifi"]["bssid"]; + } + uint16_t reqWifiChannel = 0; + if (parsedJson["wifi"].as().containsKey("channel")) { + reqWifiChannel = parsedJson["wifi"]["channel"]; + } + const char* reqWifiIp = ""; + if (parsedJson["wifi"].as().containsKey("ip")) { + reqWifiIp = parsedJson["wifi"]["ip"]; + } + const char* reqWifiMask = ""; + if (parsedJson["wifi"].as().containsKey("mask")) { + reqWifiMask = parsedJson["wifi"]["mask"]; + } + const char* reqWifiGw = ""; + if (parsedJson["wifi"].as().containsKey("gw")) { + reqWifiGw = parsedJson["wifi"]["gw"]; + } + const char* reqWifiDns1 = ""; + if (parsedJson["wifi"].as().containsKey("dns1")) { + reqWifiDns1 = parsedJson["wifi"]["dns1"]; + } + const char* reqWifiDns2 = ""; + if (parsedJson["wifi"].as().containsKey("dns2")) { + reqWifiDns2 = parsedJson["wifi"]["dns2"]; + } + + uint16_t reqMqttPort = DEFAULT_MQTT_PORT; if (parsedJson["mqtt"].as().containsKey("port")) { reqMqttPort = parsedJson["mqtt"]["port"]; } @@ -120,196 +121,254 @@ bool Config::load() { if (parsedJson["mqtt"].as().containsKey("password")) { reqMqttPassword = parsedJson["mqtt"]["password"]; } - bool reqMqttSsl = false; - if (parsedJson["mqtt"].as().containsKey("ssl")) { - reqMqttSsl = parsedJson["mqtt"]["ssl"]; - } - const char* reqMqttFingerprint = ""; - if (parsedJson["mqtt"].as().containsKey("fingerprint")) { - reqMqttFingerprint = parsedJson["mqtt"]["fingerprint"]; - } + bool reqOtaEnabled = false; if (parsedJson["ota"].as().containsKey("enabled")) { reqOtaEnabled = parsedJson["ota"]["enabled"]; } - const char* reqOtaHost = reqMqttHost; - const char* reqOtaMdnsService = ""; - if (reqOtaMdns) { - reqOtaMdnsService = parsedJson["ota"]["mdns"]; - } else if (parsedJson["ota"].as().containsKey("host")) { - reqOtaHost = parsedJson["ota"]["host"]; - } - unsigned int reqOtaPort = DEFAULT_OTA_PORT; - if (parsedJson["ota"].as().containsKey("port")) { - reqOtaPort = parsedJson["ota"]["port"]; - } - const char* reqOtaPath = DEFAULT_OTA_PATH; - if (parsedJson["ota"].as().containsKey("path")) { - reqOtaPath = parsedJson["ota"]["path"]; - } - bool reqOtaSsl = false; - if (parsedJson["ota"].as().containsKey("ssl")) { - reqOtaSsl = parsedJson["ota"]["ssl"]; - } - const char* reqOtaFingerprint = ""; - if (parsedJson["ota"].as().containsKey("fingerprint")) { - reqOtaFingerprint = parsedJson["ota"]["fingerprint"]; - } - - strcpy(this->_configStruct.name, reqName); - strcpy(this->_configStruct.wifi.ssid, reqWifiSsid); - strcpy(this->_configStruct.wifi.password, reqWifiPassword); - strcpy(this->_configStruct.deviceId, reqDeviceId); - strcpy(this->_configStruct.mqtt.server.host, reqMqttHost); - this->_configStruct.mqtt.server.port = reqMqttPort; - this->_configStruct.mqtt.server.mdns.enabled = reqMqttMdns; - strcpy(this->_configStruct.mqtt.server.mdns.service, reqMqttMdnsService); - strcpy(this->_configStruct.mqtt.baseTopic, reqMqttBaseTopic); - this->_configStruct.mqtt.auth = reqMqttAuth; - strcpy(this->_configStruct.mqtt.username, reqMqttUsername); - strcpy(this->_configStruct.mqtt.password, reqMqttPassword); - this->_configStruct.mqtt.server.ssl.enabled = reqMqttSsl; - strcpy(this->_configStruct.mqtt.server.ssl.fingerprint, reqMqttFingerprint); - this->_configStruct.ota.enabled = reqOtaEnabled; - strcpy(this->_configStruct.ota.server.host, reqOtaHost); - this->_configStruct.ota.server.port = reqOtaPort; - this->_configStruct.ota.server.mdns.enabled = reqOtaMdns; - strcpy(this->_configStruct.ota.server.mdns.service, reqOtaMdnsService); - strcpy(this->_configStruct.ota.path, reqOtaPath); - this->_configStruct.ota.server.ssl.enabled = reqOtaSsl; - strcpy(this->_configStruct.ota.server.ssl.fingerprint, reqOtaFingerprint); + strlcpy(_configStruct.name, reqName, MAX_FRIENDLY_NAME_LENGTH); + strlcpy(_configStruct.deviceId, reqDeviceId, MAX_DEVICE_ID_LENGTH); + _configStruct.deviceStatsInterval = regDeviceStatsInterval; + strlcpy(_configStruct.wifi.ssid, reqWifiSsid, MAX_WIFI_SSID_LENGTH); + if (reqWifiPassword) strlcpy(_configStruct.wifi.password, reqWifiPassword, MAX_WIFI_PASSWORD_LENGTH); + strlcpy(_configStruct.wifi.bssid, reqWifiBssid, MAX_MAC_STRING_LENGTH + 6); + _configStruct.wifi.channel = reqWifiChannel; + strlcpy(_configStruct.wifi.ip, reqWifiIp, MAX_IP_STRING_LENGTH); + strlcpy(_configStruct.wifi.gw, reqWifiGw, MAX_IP_STRING_LENGTH); + strlcpy(_configStruct.wifi.mask, reqWifiMask, MAX_IP_STRING_LENGTH); + strlcpy(_configStruct.wifi.dns1, reqWifiDns1, MAX_IP_STRING_LENGTH); + strlcpy(_configStruct.wifi.dns2, reqWifiDns2, MAX_IP_STRING_LENGTH); + strlcpy(_configStruct.mqtt.server.host, reqMqttHost, MAX_HOSTNAME_LENGTH); + _configStruct.mqtt.server.port = reqMqttPort; + strlcpy(_configStruct.mqtt.baseTopic, reqMqttBaseTopic, MAX_MQTT_BASE_TOPIC_LENGTH); + _configStruct.mqtt.auth = reqMqttAuth; + strlcpy(_configStruct.mqtt.username, reqMqttUsername, MAX_MQTT_CREDS_LENGTH); + strlcpy(_configStruct.mqtt.password, reqMqttPassword, MAX_MQTT_CREDS_LENGTH); + _configStruct.ota.enabled = reqOtaEnabled; + + /* Parse the settings */ + + JsonObject& settingsObject = parsedJson["settings"].as(); + + for (IHomieSetting* iSetting : IHomieSetting::settings) { + if (iSetting->isBool()) { + HomieSetting* setting = static_cast*>(iSetting); + + if (settingsObject.containsKey(setting->getName())) { + setting->set(settingsObject[setting->getName()].as()); + } + } else if (iSetting->isLong()) { + HomieSetting* setting = static_cast*>(iSetting); + + if (settingsObject.containsKey(setting->getName())) { + setting->set(settingsObject[setting->getName()].as()); + } + } else if (iSetting->isDouble()) { + HomieSetting* setting = static_cast*>(iSetting); + + if (settingsObject.containsKey(setting->getName())) { + setting->set(settingsObject[setting->getName()].as()); + } + } else if (iSetting->isConstChar()) { + HomieSetting* setting = static_cast*>(iSetting); + + if (settingsObject.containsKey(setting->getName())) { + setting->set(strdup(settingsObject[setting->getName()].as())); + } + } + } + + _valid = true; return true; } -const ConfigStruct& Config::get() { - return this->_configStruct; +char* Config::getSafeConfigFile() const { + File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); + size_t configSize = configFile.size(); + + char buf[MAX_JSON_CONFIG_FILE_SIZE]; + configFile.readBytes(buf, configSize); + configFile.close(); + buf[configSize] = '\0'; + + StaticJsonBuffer jsonBuffer; + JsonObject& parsedJson = jsonBuffer.parseObject(buf); + parsedJson["wifi"].as().remove("password"); + parsedJson["mqtt"].as().remove("username"); + parsedJson["mqtt"].as().remove("password"); + + size_t jsonBufferLength = parsedJson.measureLength() + 1; + std::unique_ptr jsonString(new char[jsonBufferLength]); + parsedJson.printTo(jsonString.get(), jsonBufferLength); + + return strdup(jsonString.get()); } void Config::erase() { - if (!this->_spiffsBegin()) { return; } + if (!_spiffsBegin()) { return; } SPIFFS.remove(CONFIG_FILE_PATH); - SPIFFS.remove(CONFIG_OTA_PATH); + SPIFFS.remove(CONFIG_NEXT_BOOT_MODE_FILE_PATH); } -void Config::write(const String& config) { - if (!this->_spiffsBegin()) { return; } +void Config::setHomieBootModeOnNextBoot(HomieBootMode bootMode) { + if (!_spiffsBegin()) { return; } + + if (bootMode == HomieBootMode::UNDEFINED) { + SPIFFS.remove(CONFIG_NEXT_BOOT_MODE_FILE_PATH); + } else { + File bootModeFile = SPIFFS.open(CONFIG_NEXT_BOOT_MODE_FILE_PATH, "w"); + if (!bootModeFile) { + Interface::get().getLogger() << F("✖ Cannot open NEXTMODE file") << endl; + return; + } + + bootModeFile.printf("#%d", bootMode); + bootModeFile.close(); + Interface::get().getLogger().printf("Setting next boot mode to %d\n", bootMode); + } +} + +HomieBootMode Config::getHomieBootModeOnNextBoot() { + if (!_spiffsBegin()) { return HomieBootMode::UNDEFINED; } + + File bootModeFile = SPIFFS.open(CONFIG_NEXT_BOOT_MODE_FILE_PATH, "r"); + if (bootModeFile) { + int v = bootModeFile.parseInt(); + bootModeFile.close(); + return static_cast(v); + } else { + return HomieBootMode::UNDEFINED; + } +} + +void Config::write(const JsonObject& config) { + if (!_spiffsBegin()) { return; } SPIFFS.remove(CONFIG_FILE_PATH); File configFile = SPIFFS.open(CONFIG_FILE_PATH, "w"); if (!configFile) { - this->_interface->logger->logln(F("✖ Cannot open config file")); + Interface::get().getLogger() << F("✖ Cannot open config file") << endl; return; } - configFile.print(config); + config.printTo(configFile); configFile.close(); } -void Config::setOtaMode(bool enabled, const char* version) { - if (!this->_spiffsBegin()) { return; } +bool Config::patch(const char* patch) { + if (!_spiffsBegin()) { return false; } - if (enabled) { - File otaFile = SPIFFS.open(CONFIG_OTA_PATH, "w"); - if (!otaFile) { - this->_interface->logger->logln(F("✖ Cannot open OTA file")); - return; + StaticJsonBuffer patchJsonBuffer; + JsonObject& patchObject = patchJsonBuffer.parseObject(patch); + + if (!patchObject.success()) { + Interface::get().getLogger() << F("✖ Invalid or too big JSON") << endl; + return false; + } + + File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); + if (!configFile) { + Interface::get().getLogger() << F("✖ Cannot open config file") << endl; + return false; + } + + size_t configSize = configFile.size(); + + char configJson[MAX_JSON_CONFIG_FILE_SIZE]; + configFile.readBytes(configJson, configSize); + configFile.close(); + configJson[configSize] = '\0'; + + StaticJsonBuffer configJsonBuffer; + JsonObject& configObject = configJsonBuffer.parseObject(configJson); + + // To do alow object that dont currently exist to be added like settings. + // if settings wasnt there origionally then it should be allowed to be added by incremental. + for (JsonObject::iterator it = patchObject.begin(); it != patchObject.end(); ++it) { + if (patchObject[it->key].is()) { + JsonObject& subObject = patchObject[it->key].as(); + for (JsonObject::iterator it2 = subObject.begin(); it2 != subObject.end(); ++it2) { + if (!configObject.containsKey(it->key) || !configObject[it->key].is()) { + String error = "✖ Config does not contain a "; + error.concat(it->key); + error.concat(" object"); + Interface::get().getLogger() << error << endl; + return false; + } + JsonObject& subConfigObject = configObject[it->key].as(); + subConfigObject[it2->key] = it2->value; + } + } else { + configObject[it->key] = it->value; } + } - otaFile.print(version); - otaFile.close(); - } else { - SPIFFS.remove(CONFIG_OTA_PATH); + ConfigValidationResult configValidationResult = Validation::validateConfig(configObject); + if (!configValidationResult.valid) { + Interface::get().getLogger() << F("✖ Config file is not valid, reason: ") << configValidationResult.reason << endl; + return false; } -} -const char* Config::getOtaVersion() { - return this->_otaVersion; + write(configObject); + + return true; } -BootMode Config::getBootMode() { - return this->_bootMode; +bool Config::isValid() const { + return this->_valid; } -void Config::log() { - this->_interface->logger->logln(F("{} Stored configuration:")); - this->_interface->logger->log(F(" • Hardware device ID: ")); - this->_interface->logger->logln(Helpers::getDeviceId()); - this->_interface->logger->log(F(" • Device ID: ")); - this->_interface->logger->logln(this->_configStruct.deviceId); - this->_interface->logger->log(F(" • Boot mode: ")); - switch (this->_bootMode) { - case BOOT_CONFIG: - this->_interface->logger->logln(F("configuration")); - break; - case BOOT_NORMAL: - this->_interface->logger->logln(F("normal")); - break; - case BOOT_OTA: - this->_interface->logger->logln(F("OTA")); - break; - default: - this->_interface->logger->logln(F("unknown")); - break; - } - this->_interface->logger->log(F(" • Name: ")); - this->_interface->logger->logln(this->_configStruct.name); - - this->_interface->logger->logln(F(" • Wi-Fi")); - this->_interface->logger->log(F(" ◦ SSID: ")); - this->_interface->logger->logln(this->_configStruct.wifi.ssid); - this->_interface->logger->logln(F(" ◦ Password not shown")); - - this->_interface->logger->logln(F(" • MQTT")); - if (this->_configStruct.mqtt.server.mdns.enabled) { - this->_interface->logger->log(F(" ◦ mDNS: ")); - this->_interface->logger->log(this->_configStruct.mqtt.server.mdns.service); - } else { - this->_interface->logger->log(F(" ◦ Host: ")); - this->_interface->logger->logln(this->_configStruct.mqtt.server.host); - this->_interface->logger->log(F(" ◦ Port: ")); - this->_interface->logger->logln(this->_configStruct.mqtt.server.port); - } - this->_interface->logger->log(F(" ◦ Base topic: ")); - this->_interface->logger->logln(this->_configStruct.mqtt.baseTopic); - this->_interface->logger->log(F(" ◦ Auth? ")); - this->_interface->logger->logln(this->_configStruct.mqtt.auth ? F("yes") : F("no")); - if (this->_configStruct.mqtt.auth) { - this->_interface->logger->log(F(" ◦ Username: ")); - this->_interface->logger->logln(this->_configStruct.mqtt.username); - this->_interface->logger->logln(F(" ◦ Password not shown")); - } - this->_interface->logger->log(F(" ◦ SSL? ")); - this->_interface->logger->logln(this->_configStruct.mqtt.server.ssl.enabled ? F("yes") : F("no")); - if (this->_configStruct.mqtt.server.ssl.enabled) { - this->_interface->logger->log(F(" ◦ Fingerprint: ")); - if (strcmp_P(this->_configStruct.mqtt.server.ssl.fingerprint, PSTR("")) == 0) this->_interface->logger->logln(F("unset")); - else this->_interface->logger->logln(this->_configStruct.mqtt.server.ssl.fingerprint); - } - - this->_interface->logger->logln(F(" • OTA")); - this->_interface->logger->log(F(" ◦ Enabled? ")); - this->_interface->logger->logln(this->_configStruct.ota.enabled ? F("yes") : F("no")); - if (this->_configStruct.ota.enabled) { - if (this->_configStruct.ota.server.mdns.enabled) { - this->_interface->logger->log(F(" ◦ mDNS: ")); - this->_interface->logger->log(this->_configStruct.ota.server.mdns.service); - } else { - this->_interface->logger->log(F(" ◦ Host: ")); - this->_interface->logger->logln(this->_configStruct.ota.server.host); - this->_interface->logger->log(F(" ◦ Port: ")); - this->_interface->logger->logln(this->_configStruct.ota.server.port); - } - this->_interface->logger->log(F(" ◦ Path: ")); - this->_interface->logger->logln(this->_configStruct.ota.path); - this->_interface->logger->log(F(" ◦ SSL? ")); - this->_interface->logger->logln(this->_configStruct.ota.server.ssl.enabled ? F("yes") : F("no")); - if (this->_configStruct.ota.server.ssl.enabled) { - this->_interface->logger->log(F(" ◦ Fingerprint: ")); - if (strcmp_P(this->_configStruct.ota.server.ssl.fingerprint, PSTR("")) == 0) this->_interface->logger->logln(F("unset")); - else this->_interface->logger->logln(this->_configStruct.ota.server.ssl.fingerprint); +void Config::log() const { + Interface::get().getLogger() << F("{} Stored configuration") << endl; + Interface::get().getLogger() << F(" • Hardware device ID: ") << DeviceId::get() << endl; + Interface::get().getLogger() << F(" • Device ID: ") << _configStruct.deviceId << endl; + Interface::get().getLogger() << F(" • Name: ") << _configStruct.name << endl; + Interface::get().getLogger() << F(" • Device Stats Interval: ") << _configStruct.deviceStatsInterval << F(" sec") << endl; + + Interface::get().getLogger() << F(" • Wi-Fi: ") << endl; + Interface::get().getLogger() << F(" ◦ SSID: ") << _configStruct.wifi.ssid << endl; + Interface::get().getLogger() << F(" ◦ Password not shown") << endl; + if (strcmp_P(_configStruct.wifi.ip, PSTR("")) != 0) { + Interface::get().getLogger() << F(" ◦ IP: ") << _configStruct.wifi.ip << endl; + Interface::get().getLogger() << F(" ◦ Mask: ") << _configStruct.wifi.mask << endl; + Interface::get().getLogger() << F(" ◦ Gateway: ") << _configStruct.wifi.gw << endl; + } + Interface::get().getLogger() << F(" • MQTT: ") << endl; + Interface::get().getLogger() << F(" ◦ Host: ") << _configStruct.mqtt.server.host << endl; + Interface::get().getLogger() << F(" ◦ Port: ") << _configStruct.mqtt.server.port << endl; + Interface::get().getLogger() << F(" ◦ Base topic: ") << _configStruct.mqtt.baseTopic << endl; + Interface::get().getLogger() << F(" ◦ Auth? ") << (_configStruct.mqtt.auth ? F("yes") : F("no")) << endl; + if (_configStruct.mqtt.auth) { + Interface::get().getLogger() << F(" ◦ Username: ") << _configStruct.mqtt.username << endl; + Interface::get().getLogger() << F(" ◦ Password not shown") << endl; + } + + Interface::get().getLogger() << F(" • OTA: ") << endl; + Interface::get().getLogger() << F(" ◦ Enabled? ") << (_configStruct.ota.enabled ? F("yes") : F("no")) << endl; + + if (IHomieSetting::settings.size() > 0) { + Interface::get().getLogger() << F(" • Custom settings: ") << endl; + for (IHomieSetting* iSetting : IHomieSetting::settings) { + Interface::get().getLogger() << F(" ◦ "); + + if (iSetting->isBool()) { + HomieSetting* setting = static_cast*>(iSetting); + Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); + } else if (iSetting->isLong()) { + HomieSetting* setting = static_cast*>(iSetting); + Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); + } else if (iSetting->isDouble()) { + HomieSetting* setting = static_cast*>(iSetting); + Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); + } else if (iSetting->isConstChar()) { + HomieSetting* setting = static_cast*>(iSetting); + Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); + } + + Interface::get().getLogger() << endl; } } } diff --git a/src/Homie/Config.hpp b/src/Homie/Config.hpp index faea7945..cfc95c6b 100644 --- a/src/Homie/Config.hpp +++ b/src/Homie/Config.hpp @@ -1,34 +1,43 @@ #pragma once +#include "Arduino.h" + #include #include "FS.h" #include "Datatypes/Interface.hpp" #include "Datatypes/ConfigStruct.hpp" -#include "Helpers.hpp" +#include "Utils/DeviceId.hpp" +#include "Utils/Validation.hpp" +#include "Constants.hpp" #include "Limits.hpp" -#include "Logger.hpp" +#include "../HomieBootMode.hpp" +#include "../HomieSetting.hpp" +#include "../StreamingOperator.hpp" namespace HomieInternals { - class Config { - public: - Config(); - void attachInterface(Interface* interface); - bool load(); - const ConfigStruct& get(); - void erase(); - void write(const String& config); - void setOtaMode(bool enabled, const char* version = ""); - const char* getOtaVersion(); - BootMode getBootMode(); - void log(); // print the current config to log output +class Config { + public: + Config(); + bool load(); + inline const ConfigStruct& get() const; + char* getSafeConfigFile() const; + void erase(); + void setHomieBootModeOnNextBoot(HomieBootMode bootMode); + HomieBootMode getHomieBootModeOnNextBoot(); + void write(const JsonObject& config); + bool patch(const char* patch); + void log() const; // print the current config to log output + bool isValid() const; + + private: + ConfigStruct _configStruct; + bool _spiffsBegan; + bool _valid; - private: - Interface* _interface; - BootMode _bootMode; - ConfigStruct _configStruct; - char _otaVersion[MAX_FIRMWARE_VERSION_LENGTH]; - bool _spiffsBegan; + bool _spiffsBegin(); +}; - bool _spiffsBegin(); - }; +const ConfigStruct& Config::get() const { + return _configStruct; } +} // namespace HomieInternals diff --git a/src/Homie/Constants.hpp b/src/Homie/Constants.hpp index a8f6692a..5b26686b 100644 --- a/src/Homie/Constants.hpp +++ b/src/Homie/Constants.hpp @@ -3,39 +3,29 @@ #include namespace HomieInternals { - const char VERSION[] = "1.5.0"; - const unsigned long BAUD_RATE = 115200; + const char HOMIE_VERSION[] = "2.0.1"; + const char HOMIE_ESP8266_VERSION[] = "2.0.0"; - const IPAddress ACCESS_POINT_IP(192, 168, 1, 1); + const IPAddress ACCESS_POINT_IP(192, 168, 123, 1); - const unsigned int DEFAULT_MQTT_PORT = 1883; - const unsigned char DEFAULT_OTA_PORT = 80; - const char DEFAULT_MQTT_BASE_TOPIC[] = "devices/"; - const char DEFAULT_OTA_PATH[] = "/ota"; + const uint16_t DEFAULT_MQTT_PORT = 1883; + const char DEFAULT_MQTT_BASE_TOPIC[] = "homie/"; - const unsigned char DEFAULT_RESET_PIN = 0; // == D3 on nodeMCU - const unsigned char DEFAULT_RESET_STATE = LOW; - const unsigned int DEFAULT_RESET_TIME = 5 * 1000; + const uint8_t DEFAULT_RESET_PIN = 0; // == D3 on nodeMCU + const uint8_t DEFAULT_RESET_STATE = LOW; + const uint16_t DEFAULT_RESET_TIME = 5 * 1000; const char DEFAULT_BRAND[] = "Homie"; - const char DEFAULT_FW_NAME[] = "undefined"; - const char DEFAULT_FW_VERSION[] = "undefined"; - const unsigned int CONFIG_SCAN_INTERVAL = 20 * 1000; - const unsigned int WIFI_RECONNECT_INTERVAL = 20 * 1000; - const unsigned int MQTT_RECONNECT_INTERVAL = 5 * 1000; - const unsigned long SIGNAL_QUALITY_SEND_INTERVAL = 5 * 60 * 1000; - const unsigned long UPTIME_SEND_INTERVAL = 2 * 60 * 1000; + const uint16_t CONFIG_SCAN_INTERVAL = 20 * 1000; + const uint32_t STATS_SEND_INTERVAL_SEC = 1 * 60; + const uint16_t MQTT_RECONNECT_INITIAL_INTERVAL = 1000; + const uint8_t MQTT_RECONNECT_MAX_BACKOFF = 6; const float LED_WIFI_DELAY = 1; const float LED_MQTT_DELAY = 0.2; + const char CONFIG_UI_BUNDLE_PATH[] = "/homie/ui_bundle.gz"; + const char CONFIG_NEXT_BOOT_MODE_FILE_PATH[] = "/homie/NEXTMODE"; const char CONFIG_FILE_PATH[] = "/homie/config.json"; - const char CONFIG_OTA_PATH[] = "/homie/ota"; - - enum BootMode : unsigned char { - BOOT_NORMAL = 1, - BOOT_CONFIG, - BOOT_OTA - }; -} +} // namespace HomieInternals diff --git a/src/Homie/Datatypes/Callbacks.hpp b/src/Homie/Datatypes/Callbacks.hpp index ddc3e6ef..9e519235 100644 --- a/src/Homie/Datatypes/Callbacks.hpp +++ b/src/Homie/Datatypes/Callbacks.hpp @@ -1,16 +1,19 @@ #pragma once -#include "../../HomieEvent.hpp" #include +#include "../../HomieEvent.hpp" +#include "../../HomieRange.hpp" + +class HomieNode; namespace HomieInternals { typedef std::function OperationFunction; - typedef std::function GlobalInputHandler; - typedef std::function NodeInputHandler; - typedef std::function PropertyInputHandler; + typedef std::function GlobalInputHandler; + typedef std::function NodeInputHandler; + typedef std::function PropertyInputHandler; - typedef std::function EventHandler; + typedef std::function EventHandler; - typedef std::function ResetFunction; -} + typedef std::function BroadcastHandler; +} // namespace HomieInternals diff --git a/src/Homie/Datatypes/ConfigStruct.hpp b/src/Homie/Datatypes/ConfigStruct.hpp index c9c99a67..5d68c638 100644 --- a/src/Homie/Datatypes/ConfigStruct.hpp +++ b/src/Homie/Datatypes/ConfigStruct.hpp @@ -4,40 +4,36 @@ #include "../Limits.hpp" namespace HomieInternals { - struct ConfigStruct { - char name[MAX_FRIENDLY_NAME_LENGTH]; - char deviceId[MAX_DEVICE_ID_LENGTH]; +struct ConfigStruct { + char name[MAX_FRIENDLY_NAME_LENGTH]; + char deviceId[MAX_DEVICE_ID_LENGTH]; + uint16_t deviceStatsInterval; - struct WiFi { - char ssid[MAX_WIFI_SSID_LENGTH]; - char password[MAX_WIFI_PASSWORD_LENGTH]; - } wifi; + struct WiFi { + char ssid[MAX_WIFI_SSID_LENGTH]; + char password[MAX_WIFI_PASSWORD_LENGTH]; + char bssid[MAX_MAC_STRING_LENGTH + 6]; + uint16_t channel; + char ip[MAX_IP_STRING_LENGTH]; + char mask[MAX_IP_STRING_LENGTH]; + char gw[MAX_IP_STRING_LENGTH]; + char dns1[MAX_IP_STRING_LENGTH]; + char dns2[MAX_IP_STRING_LENGTH]; + } wifi; + struct MQTT { struct Server { char host[MAX_HOSTNAME_LENGTH]; - unsigned int port; - struct mDNS { - bool enabled; - char service[MAX_HOSTNAME_LENGTH]; - } mdns; - struct SSL { - bool enabled; - char fingerprint[MAX_FINGERPRINT_LENGTH]; - } ssl; - }; + uint16_t port; + } server; + char baseTopic[MAX_MQTT_BASE_TOPIC_LENGTH]; + bool auth; + char username[MAX_MQTT_CREDS_LENGTH]; + char password[MAX_MQTT_CREDS_LENGTH]; + } mqtt; - struct MQTT { - Server server; - char baseTopic[MAX_MQTT_BASE_TOPIC_LENGTH]; - bool auth; - char username[MAX_MQTT_CREDS_LENGTH]; - char password[MAX_MQTT_CREDS_LENGTH]; - } mqtt; - - struct OTA { - bool enabled; - Server server; - char path[MAX_OTA_PATH_LENGTH]; - } ota; - }; -} + struct OTA { + bool enabled; + } ota; +}; +} // namespace HomieInternals diff --git a/src/Homie/Datatypes/Interface.cpp b/src/Homie/Datatypes/Interface.cpp new file mode 100644 index 00000000..290ce923 --- /dev/null +++ b/src/Homie/Datatypes/Interface.cpp @@ -0,0 +1,27 @@ +#include "Interface.hpp" + +using namespace HomieInternals; + +InterfaceData Interface::_interface; // need to define the static variable + +InterfaceData::InterfaceData() + : brand{ '\0' } + , bootMode{ HomieBootMode::UNDEFINED } + , configurationAp{ .secured = false, .password = {'\0'} } + , firmware{ .name = {'\0'}, .version = {'\0'} } + , led{ .enabled = false, .pin = 0, .on = 0 } + , reset{ .enabled = false, .idle = false, .triggerPin = 0, .triggerState = 0, .triggerTime = 0, .resetFlag = false } + , disable{ false } + , flaggedForSleep{ false } + , event{} + , ready{ false } + , _logger{ nullptr } + , _blinker{ nullptr } + , _config{ nullptr } + , _mqttClient{ nullptr } + , _sendingPromise{ nullptr } { +} + +InterfaceData& Interface::get() { + return _interface; +} diff --git a/src/Homie/Datatypes/Interface.hpp b/src/Homie/Datatypes/Interface.hpp index 6892874b..3e67a788 100644 --- a/src/Homie/Datatypes/Interface.hpp +++ b/src/Homie/Datatypes/Interface.hpp @@ -1,52 +1,91 @@ #pragma once +#include +#include "../Logger.hpp" +#include "../Blinker.hpp" +#include "../Constants.hpp" +#include "../Config.hpp" #include "../Limits.hpp" #include "./Callbacks.hpp" +#include "../../HomieBootMode.hpp" #include "../../HomieNode.hpp" +#include "../../SendingPromise.hpp" #include "../../HomieEvent.hpp" namespace HomieInternals { - class Logger; - class Blinker; - class Config; - class MqttClient; - struct Interface { - /***** User configurable data *****/ - char brand[MAX_BRAND_LENGTH]; - - struct Firmware { - char name[MAX_FIRMWARE_NAME_LENGTH]; - char version[MAX_FIRMWARE_VERSION_LENGTH]; - } firmware; - - struct LED { - bool enabled; - unsigned char pin; - unsigned char on; - } led; - - struct Reset { - bool enabled; - bool able; - unsigned char triggerPin; - unsigned char triggerState; - unsigned int triggerTime; - ResetFunction userFunction; - } reset; - - const HomieNode* registeredNodes[MAX_REGISTERED_NODES_COUNT]; - unsigned char registeredNodesCount; - - GlobalInputHandler globalInputHandler; - OperationFunction setupFunction; - OperationFunction loopFunction; - EventHandler eventHandler; - - /***** Runtime data *****/ - bool readyToOperate; - Logger* logger; - Blinker* blinker; - Config* config; - MqttClient* mqttClient; - }; -} +class Logger; +class Blinker; +class Config; +class SendingPromise; +class HomieClass; + +class InterfaceData { + friend HomieClass; + + public: + InterfaceData(); + + /***** User configurable data *****/ + char brand[MAX_BRAND_LENGTH]; + + HomieBootMode bootMode; + + struct ConfigurationAP { + bool secured; + char password[MAX_WIFI_PASSWORD_LENGTH]; + } configurationAp; + + struct Firmware { + char name[MAX_FIRMWARE_NAME_LENGTH]; + char version[MAX_FIRMWARE_VERSION_LENGTH]; + } firmware; + + struct LED { + bool enabled; + uint8_t pin; + uint8_t on; + } led; + + struct Reset { + bool enabled; + bool idle; + uint8_t triggerPin; + uint8_t triggerState; + uint16_t triggerTime; + bool resetFlag; + } reset; + + bool disable; + bool flaggedForSleep; + + GlobalInputHandler globalInputHandler; + BroadcastHandler broadcastHandler; + OperationFunction setupFunction; + OperationFunction loopFunction; + EventHandler eventHandler; + + /***** Runtime data *****/ + HomieEvent event; + bool ready; + Logger& getLogger() { return *_logger; } + Blinker& getBlinker() { return *_blinker; } + Config& getConfig() { return *_config; } + AsyncMqttClient& getMqttClient() { return *_mqttClient; } + SendingPromise& getSendingPromise() { return *_sendingPromise; } + + private: + Logger* _logger; + Blinker* _blinker; + Config* _config; + AsyncMqttClient* _mqttClient; + SendingPromise* _sendingPromise; +}; + +class Interface { + public: + static InterfaceData& get(); + + private: + static InterfaceData _interface; +}; +} // namespace HomieInternals diff --git a/src/Homie/Datatypes/Subscription.hpp b/src/Homie/Datatypes/Subscription.hpp deleted file mode 100644 index 93c4448f..00000000 --- a/src/Homie/Datatypes/Subscription.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include "../Limits.hpp" -#include "./Callbacks.hpp" - -namespace HomieInternals { - struct Subscription { - char property[MAX_NODE_PROPERTY_LENGTH]; - PropertyInputHandler inputHandler; - }; -} diff --git a/src/Homie/ExponentialBackoffTimer.cpp b/src/Homie/ExponentialBackoffTimer.cpp new file mode 100644 index 00000000..d4cf6fd3 --- /dev/null +++ b/src/Homie/ExponentialBackoffTimer.cpp @@ -0,0 +1,42 @@ +#include "ExponentialBackoffTimer.hpp" + +using namespace HomieInternals; + +ExponentialBackoffTimer::ExponentialBackoffTimer(uint16_t initialInterval, uint8_t maxBackoff) +: _timer(Timer()) +, _initialInterval(initialInterval) +, _maxBackoff(maxBackoff) +, _retryCount(0) { + _timer.deactivate(); +} + +bool ExponentialBackoffTimer::check() { + if (_timer.check()) { + if (_retryCount != _maxBackoff) _retryCount++; + + uint32_t fixedDelay = pow(_retryCount, 2) * _initialInterval; + uint32_t randomDifference = random(0, (fixedDelay / 10) + 1); + uint32_t nextInterval = fixedDelay - randomDifference; + + _timer.setInterval(nextInterval, false); + return true; + } else { + return false; + } +} + +void ExponentialBackoffTimer::activate() { + if (_timer.isActive()) return; + + _timer.setInterval(_initialInterval, false); + _timer.activate(); + _retryCount = 1; +} + +void ExponentialBackoffTimer::deactivate() { + _timer.deactivate(); +} + +bool ExponentialBackoffTimer::isActive() const { + return _timer.isActive(); +} diff --git a/src/Homie/ExponentialBackoffTimer.hpp b/src/Homie/ExponentialBackoffTimer.hpp new file mode 100644 index 00000000..5d7d251d --- /dev/null +++ b/src/Homie/ExponentialBackoffTimer.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "Timer.hpp" +#include "Datatypes/Interface.hpp" + +namespace HomieInternals { +class ExponentialBackoffTimer { + public: + ExponentialBackoffTimer(uint16_t initialInterval, uint8_t maxBackoff); + void activate(); + bool check(); + void deactivate(); + bool isActive() const; + + private: + Timer _timer; + + uint16_t _initialInterval; + uint8_t _maxBackoff; + uint8_t _retryCount; +}; +} // namespace HomieInternals diff --git a/src/Homie/Helpers.cpp b/src/Homie/Helpers.cpp deleted file mode 100644 index de051a37..00000000 --- a/src/Homie/Helpers.cpp +++ /dev/null @@ -1,287 +0,0 @@ -#include "Helpers.hpp" - -using namespace HomieInternals; - -char Helpers::_deviceId[] = ""; // need to define the static variable - -void Helpers::generateDeviceId() { - char flashChipId[6 + 1]; - sprintf(flashChipId, "%06x", ESP.getFlashChipId()); - - sprintf(Helpers::_deviceId, "%06x%s", ESP.getChipId(), flashChipId + strlen(flashChipId) - 2); -} - -const char* Helpers::getDeviceId() { - return Helpers::_deviceId; -} - -MdnsQueryResult Helpers::mdnsQuery(const char* service) { - MdnsQueryResult result; - result.success = false; - int n = MDNS.queryService(service, "tcp"); - if (n == 0) { - return result; - } else { - result.success = true; - result.ip = MDNS.IP(0); - result.port = MDNS.port(0); - } - - return result; -} - -ConfigValidationResult Helpers::validateConfig(const JsonObject& object) { - ConfigValidationResult result; - result = _validateConfigRoot(object); - if (!result.valid) return result; - result = _validateConfigWifi(object); - if (!result.valid) return result; - result = _validateConfigMqtt(object); - if (!result.valid) return result; - result = _validateConfigOta(object); - if (!result.valid) return result; - - result.valid = true; - result.reason = nullptr; - return result; -} - -ConfigValidationResult Helpers::_validateConfigRoot(const JsonObject& object) { - ConfigValidationResult result; - result.valid = false; - result.reason = nullptr; - if (!object.containsKey("name") || !object["name"].is()) { - result.reason = F("name is not a string"); - return result; - } - if (strlen(object["name"]) + 1 > MAX_FRIENDLY_NAME_LENGTH) { - result.reason = F("name is too long"); - return result; - } - if (object.containsKey("device_id")) { - if (!object["device_id"].is()) { - result.reason = F("device_id is not a string"); - return result; - } - if (strlen(object["device_id"]) + 1 > MAX_DEVICE_ID_LENGTH) { - result.reason = F("device_id is too long"); - return result; - } - } - - const char* name = object["name"]; - - if (strcmp_P(name, PSTR("")) == 0) { - result.reason = F("name is empty"); - return result; - } - - result.valid = true; - return result; -} - -ConfigValidationResult Helpers::_validateConfigWifi(const JsonObject& object) { - ConfigValidationResult result; - result.valid = false; - result.reason = nullptr; - - if (!object.containsKey("wifi") || !object["wifi"].is()) { - result.reason = F("wifi is not an object"); - return result; - } - if (!object["wifi"].as().containsKey("ssid") || !object["wifi"]["ssid"].is()) { - result.reason = F("wifi.ssid is not a string"); - return result; - } - if (strlen(object["wifi"]["ssid"]) + 1 > MAX_WIFI_SSID_LENGTH) { - result.reason = F("wifi.ssid is too long"); - return result; - } - if (!object["wifi"].as().containsKey("password") || !object["wifi"]["password"].is()) { - result.reason = F("wifi.password is not a string"); - return result; - } - if (strlen(object["wifi"]["password"]) + 1 > MAX_WIFI_PASSWORD_LENGTH) { - result.reason = F("wifi.password is too long"); - return result; - } - - const char* wifiSsid = object["wifi"]["ssid"]; - if (strcmp_P(wifiSsid, PSTR("")) == 0) { - result.reason = F("wifi.ssid is empty"); - return result; - } - - result.valid = true; - return result; -} - -ConfigValidationResult Helpers::_validateConfigMqtt(const JsonObject& object) { - ConfigValidationResult result; - result.valid = false; - result.reason = nullptr; - - if (!object.containsKey("mqtt") || !object["mqtt"].is()) { - result.reason = F("mqtt is not an object"); - return result; - } - bool mdns = false; - if (object["mqtt"].as().containsKey("mdns")) { - if (!object["mqtt"]["mdns"].is()) { - result.reason = F("mqtt.mdns is not a string"); - return result; - } - if (strlen(object["mqtt"]["mdns"]) + 1 > MAX_HOSTNAME_LENGTH) { - result.reason = F("mqtt.mdns is too long"); - return result; - } - mdns = true; - } else { - if (!object["mqtt"].as().containsKey("host") || !object["mqtt"]["host"].is()) { - result.reason = F("mqtt.host is not a string"); - return result; - } - if (strlen(object["mqtt"]["host"]) + 1 > MAX_HOSTNAME_LENGTH) { - result.reason = F("mqtt.host is too long"); - return result; - } - if (object["mqtt"].as().containsKey("port") && !object["mqtt"]["port"].is()) { - result.reason = F("mqtt.port is not an unsigned integer"); - return result; - } - } - if (object["mqtt"].as().containsKey("base_topic")) { - if (!object["mqtt"]["base_topic"].is()) { - result.reason = F("mqtt.base_topic is not a string"); - return result; - } - - if (strlen(object["mqtt"]["base_topic"]) + 1 > MAX_MQTT_BASE_TOPIC_LENGTH) { - result.reason = F("mqtt.base_topic is too long"); - return result; - } - } - if (object["mqtt"].as().containsKey("auth")) { - if (!object["mqtt"]["auth"].is()) { - result.reason = F("mqtt.auth is not a boolean"); - return result; - } - - if (object["mqtt"]["auth"]) { - if (!object["mqtt"].as().containsKey("username") || !object["mqtt"]["username"].is()) { - result.reason = F("mqtt.username is not a string"); - return result; - } - if (strlen(object["mqtt"]["username"]) + 1 > MAX_MQTT_CREDS_LENGTH) { - result.reason = F("mqtt.username is too long"); - return result; - } - if (!object["mqtt"].as().containsKey("password") || !object["mqtt"]["password"].is()) { - result.reason = F("mqtt.password is not a string"); - return result; - } - if (strlen(object["mqtt"]["password"]) + 1 > MAX_MQTT_CREDS_LENGTH) { - result.reason = F("mqtt.password is too long"); - return result; - } - } - } - if (object["mqtt"].as().containsKey("ssl")) { - if (!object["mqtt"]["ssl"].is()) { - result.reason = F("mqtt.ssl is not a boolean"); - return result; - } - - if (object["mqtt"]["ssl"]) { - if (object["mqtt"].as().containsKey("fingerprint") && !object["mqtt"]["fingerprint"].is()) { - result.reason = F("mqtt.fingerprint is not a string"); - return result; - } - } - } - - if (mdns) { - const char* mdnsService = object["mqtt"]["mdns"]; - if (strcmp_P(mdnsService, PSTR("")) == 0) { - result.reason = F("mqtt.mdns is empty"); - return result; - } - } else { - const char* host = object["mqtt"]["host"]; - if (strcmp_P(host, PSTR("")) == 0) { - result.reason = F("mqtt.host is empty"); - return result; - } - } - - result.valid = true; - return result; -} - -ConfigValidationResult Helpers::_validateConfigOta(const JsonObject& object) { - ConfigValidationResult result; - result.valid = false; - result.reason = nullptr; - - if (!object.containsKey("ota") || !object["ota"].is()) { - result.reason = F("ota is not an object"); - return result; - } - if (!object["ota"].as().containsKey("enabled") || !object["ota"]["enabled"].is()) { - result.reason = F("ota.enabled is not a boolean"); - return result; - } - if (object["ota"]["enabled"]) { - if (object["ota"].as().containsKey("mdns")) { - if (!object["ota"]["mdns"].is()) { - result.reason = F("ota.mdns is not a string"); - return result; - } - if (strlen(object["ota"]["mdns"]) + 1 > MAX_HOSTNAME_LENGTH) { - result.reason = F("ota.mdns is too long"); - return result; - } - } else { - if (object["ota"].as().containsKey("host")) { - if (!object["ota"]["host"].is()) { - result.reason = F("ota.host is not a string"); - return result; - } - if (strlen(object["ota"]["host"]) + 1 > MAX_HOSTNAME_LENGTH) { - result.reason = F("ota.host is too long"); - return result; - } - } - if (object["ota"].as().containsKey("port") && !object["ota"]["port"].is()) { - result.reason = F("ota.port is not an unsigned integer"); - return result; - } - } - if (object["ota"].as().containsKey("path")) { - if (!object["ota"]["path"].is()) { - result.reason = F("ota.path is not a string"); - return result; - } - if (strlen(object["ota"]["path"]) + 1 > MAX_OTA_PATH_LENGTH) { - result.reason = F("ota.path is too long"); - return result; - } - } - if (object["ota"].as().containsKey("ssl")) { - if (!object["ota"]["ssl"].is()) { - result.reason = F("ota.ssl is not a boolean"); - return result; - } - - if (object["ota"]["ssl"]) { - if (object["ota"].as().containsKey("fingerprint") && !object["ota"]["fingerprint"].is()) { - result.reason = F("ota.fingerprint is not a string"); - return result; - } - } - } - } - - result.valid = true; - return result; -} diff --git a/src/Homie/Helpers.hpp b/src/Homie/Helpers.hpp deleted file mode 100644 index ddb0f93f..00000000 --- a/src/Homie/Helpers.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include "Arduino.h" - -#include -#include -#include "Limits.hpp" - -namespace HomieInternals { - struct ConfigValidationResult { - bool valid; - const __FlashStringHelper* reason; - }; - - struct MdnsQueryResult { - bool success; - IPAddress ip; - unsigned int port; - }; - - class Helpers { - public: - static void generateDeviceId(); - static const char* getDeviceId(); - static MdnsQueryResult mdnsQuery(const char* service); - static ConfigValidationResult validateConfig(const JsonObject& object); - - private: - static char _deviceId[8 + 1]; - - static ConfigValidationResult _validateConfigRoot(const JsonObject& object); - static ConfigValidationResult _validateConfigWifi(const JsonObject& object); - static ConfigValidationResult _validateConfigMqtt(const JsonObject& object); - static ConfigValidationResult _validateConfigOta(const JsonObject& object); - }; -} diff --git a/src/Homie/Limits.hpp b/src/Homie/Limits.hpp index 1264faee..e4686dc2 100644 --- a/src/Homie/Limits.hpp +++ b/src/Homie/Limits.hpp @@ -1,33 +1,35 @@ -#pragma once - -#include - -namespace HomieInternals { - const unsigned int MAX_JSON_CONFIG_FILE_BUFFER_SIZE = 1000; - const unsigned int MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE = JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(8) + JSON_OBJECT_SIZE(6); // Max 5 elements at root, 2 elements in nested, etc... - - const unsigned char MAX_WIFI_SSID_LENGTH = 32 + 1; - const unsigned char MAX_WIFI_PASSWORD_LENGTH = 63 + 1; - const unsigned char MAX_HOSTNAME_LENGTH = sizeof("super.long.domain-name.superhost.com"); - const unsigned char MAX_FINGERPRINT_LENGTH = 59 + 1; - - const unsigned char MAX_MQTT_CREDS_LENGTH = 32 + 1; - const unsigned char MAX_MQTT_BASE_TOPIC_LENGTH = sizeof("shared-broker/username-lolipop/homie/sensors/"); - const unsigned char MAX_OTA_PATH_LENGTH = sizeof("/virtual-host/long-path/ota"); - - const unsigned char MAX_FRIENDLY_NAME_LENGTH = sizeof("My awesome friendly name of the living room"); - const unsigned char MAX_DEVICE_ID_LENGTH = sizeof("my-awesome-device-id-living-room"); - - const unsigned char MAX_BRAND_LENGTH = MAX_WIFI_SSID_LENGTH - sizeof("-0123abcd") + 1; - const unsigned char MAX_FIRMWARE_NAME_LENGTH = sizeof("my-awesome-home-firmware-name"); - const unsigned char MAX_FIRMWARE_VERSION_LENGTH = sizeof("v1.0.0-alpha+001"); - - const unsigned char MAX_NODE_ID_LENGTH = sizeof("my-super-awesome-node-id"); - const unsigned char MAX_NODE_TYPE_LENGTH = sizeof("my-super-awesome-type"); - const unsigned char MAX_NODE_PROPERTY_LENGTH = sizeof("my-super-awesome-property"); - - const unsigned char MAX_REGISTERED_NODES_COUNT = 5; - const unsigned char MAX_SUBSCRIPTIONS_COUNT_PER_NODE = 5; - - const unsigned char TOPIC_BUFFER_LENGTH = MAX_MQTT_BASE_TOPIC_LENGTH + MAX_DEVICE_ID_LENGTH + 1 + MAX_NODE_ID_LENGTH + 1 + MAX_NODE_PROPERTY_LENGTH + 4 + 1; -} +#pragma once + +#include + +namespace HomieInternals { + const uint16_t MAX_JSON_CONFIG_FILE_SIZE = 1000; + + // max setting elements + const uint8_t MAX_CONFIG_SETTING_SIZE = 10; + // 6 elements at root, 9 elements at wifi, 6 elements at mqtt, 1 element at ota, max settings elements + const uint16_t MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE = JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(9) + JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(MAX_CONFIG_SETTING_SIZE); + + const uint8_t MAX_WIFI_SSID_LENGTH = 32 + 1; + const uint8_t MAX_WIFI_PASSWORD_LENGTH = 64 + 1; + const uint16_t MAX_HOSTNAME_LENGTH = 255 + 1; + + const uint8_t MAX_MQTT_CREDS_LENGTH = 32 + 1; + const uint8_t MAX_MQTT_BASE_TOPIC_LENGTH = 48 + 1; + const uint8_t MAX_MQTT_TOPIC_LENGTH = 128 + 1; + + const uint8_t MAX_FRIENDLY_NAME_LENGTH = 64 + 1; + const uint8_t MAX_DEVICE_ID_LENGTH = 32 + 1; + + const uint8_t MAX_BRAND_LENGTH = MAX_WIFI_SSID_LENGTH - 10 - 1; + const uint8_t MAX_FIRMWARE_NAME_LENGTH = 32 + 1; + const uint8_t MAX_FIRMWARE_VERSION_LENGTH = 16 + 1; + + const uint8_t MAX_NODE_ID_LENGTH = 24 + 1; + const uint8_t MAX_NODE_TYPE_LENGTH = 24 + 1; + const uint8_t MAX_NODE_PROPERTY_LENGTH = 24 + 1; + + const uint8_t MAX_IP_STRING_LENGTH = 16 + 1; + + const uint8_t MAX_MAC_STRING_LENGTH = 12; +} // namespace HomieInternals diff --git a/src/Homie/Logger.cpp b/src/Homie/Logger.cpp index 5cae3b2f..e1291637 100644 --- a/src/Homie/Logger.cpp +++ b/src/Homie/Logger.cpp @@ -4,19 +4,23 @@ using namespace HomieInternals; Logger::Logger() : _loggingEnabled(true) -{ +, _printer(&Serial) { } -bool Logger::isEnabled() { - return this->_loggingEnabled; +void Logger::setLogging(bool enable) { + _loggingEnabled = enable; } -void Logger::setLogging(bool enable) { - this->_loggingEnabled = enable; +void Logger::setPrinter(Print* printer) { + _printer = printer; +} + +size_t Logger::write(uint8_t character) { + if (_loggingEnabled) return _printer->write(character); + return 0; } -void Logger::logln() { - if (this->_loggingEnabled) { - Serial.println(); - } +size_t Logger::write(const uint8_t* buffer, size_t size) { + if (_loggingEnabled) return _printer->write(buffer, size); + return 0; } diff --git a/src/Homie/Logger.hpp b/src/Homie/Logger.hpp index db559cd7..801e3d49 100644 --- a/src/Homie/Logger.hpp +++ b/src/Homie/Logger.hpp @@ -3,24 +3,20 @@ #include "Arduino.h" namespace HomieInternals { - class Logger { - public: - Logger(); - void setLogging(bool enable); - bool isEnabled(); - template void log(T value) { - if (this->_loggingEnabled) { - Serial.print(value); - } - } - template void logln(T value) { - if (this->_loggingEnabled) { - Serial.println(value); - } - } - void logln(); +class HomieClass; +class Logger : public Print { + friend HomieClass; - private: - bool _loggingEnabled; - }; -} + public: + Logger(); + virtual size_t write(uint8_t character); + virtual size_t write(const uint8_t* buffer, size_t size); + + private: + void setPrinter(Print* printer); + void setLogging(bool enable); + + bool _loggingEnabled; + Print* _printer; +}; +} // namespace HomieInternals diff --git a/src/Homie/MqttClient.cpp b/src/Homie/MqttClient.cpp deleted file mode 100644 index ef4bfc3a..00000000 --- a/src/Homie/MqttClient.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include "MqttClient.hpp" - -using namespace HomieInternals; - -MqttClient::MqttClient() -: _interface(nullptr) -, _topicBuffer {'\0'} -, _secure(false) -, _host() -, _port(0) -, _fingerprint() -, _subscribeWithoutLoop(0) -{ -} - -MqttClient::~MqttClient() { -} - -void MqttClient::attachInterface(Interface* interface) { - this->_interface = interface; -} - -void MqttClient::initMqtt(bool secure) { - if (secure) { - this->_pubSubClient.setClient(this->_wifiClientSecure); - } else { - this->_pubSubClient.setClient(this->_wifiClient); - } - - this->_secure = secure; - - this->_pubSubClient.setCallback(std::bind(&MqttClient::_callback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); -} - -char* MqttClient::getTopicBuffer() { - return this->_topicBuffer; -} - -void MqttClient::setCallback(std::function callback) { - _userCallback = callback; -} - -void MqttClient::setServer(const char* host, unsigned int port, const char* fingerprint) { - this->_host = host; - this->_port = port; - this->_fingerprint = fingerprint; - this->_pubSubClient.setServer(this->_host, port); -} - -bool MqttClient::connect(const char* clientId, const char* willMessage, unsigned char willQos, bool willRetain, bool auth, const char* username, const char* password) { - this->_wifiClient.stop(); // Ensure buffers are cleaned, otherwise exception - this->_wifiClientSecure.stop(); - - if (this->_secure && !(strcmp_P(this->_fingerprint, PSTR("")) == 0)) { - this->_interface->logger->logln(F("Checking certificate")); - if (!this->_wifiClientSecure.connect(this->_host, this->_port)) { - this->_wifiClientSecure.stop(); - return false; - } - - if (!this->_wifiClientSecure.verify(this->_fingerprint, this->_host)) { - this->_interface->logger->logln(F("✖ MQTT SSL certificate mismatch")); - this->_wifiClientSecure.stop(); - return false; - } - - this->_wifiClientSecure.stop(); - } - - bool result; - if (auth) { - result = this->_pubSubClient.connect(clientId, username, password, this->_topicBuffer, willQos, willRetain, willMessage); - } else { - result = this->_pubSubClient.connect(clientId, this->_topicBuffer, willQos, willRetain, willMessage); - } - - return result; -} - -int MqttClient::getState() { - return this->_pubSubClient.state(); -} - -void MqttClient::disconnect() { - this->_pubSubClient.disconnect(); -} - -bool MqttClient::publish(const char* message, bool retained) { - return this->_pubSubClient.publish(this->_topicBuffer, message, retained); -} - -bool MqttClient::subscribe(unsigned char qos) { - if (this->_subscribeWithoutLoop >= 5) { - this->loop(); // see knolleary/pubsublient#98 - } - - return this->_pubSubClient.subscribe(this->_topicBuffer, qos); -} - -void MqttClient::loop() { - this->_pubSubClient.loop(); - this->_subscribeWithoutLoop = 0; -} - -bool MqttClient::connected() { - return this->_pubSubClient.connected(); -} - -void MqttClient::_callback(char* topic, unsigned char* payload, unsigned int length) { - char buf[128]; - for (unsigned int i = 0; i < length; i++) { - char tempString[2]; - tempString[0] = (char)payload[i]; - tempString[1] = '\0'; - if (i == 0) strcpy(buf, tempString); - else strcat(buf, tempString); - } - - this->_userCallback(topic, buf); -} diff --git a/src/Homie/MqttClient.hpp b/src/Homie/MqttClient.hpp deleted file mode 100644 index 40ddeb5e..00000000 --- a/src/Homie/MqttClient.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include -#include "Datatypes/Interface.hpp" -#include "Logger.hpp" -#include "Constants.hpp" -#include "Limits.hpp" - -namespace HomieInternals { - class MqttClient { - public: - MqttClient(); - ~MqttClient(); - - void attachInterface(Interface* interface); - void initMqtt(bool secure); - char* getTopicBuffer(); - void setCallback(std::function callback); - void setServer(const char* host, unsigned int port, const char* fingerprint); - bool connect(const char* clientId, const char* willMessage, unsigned char willQos, bool willRetain, bool auth = false, const char* username = "", const char* password = ""); - int getState(); - void disconnect(); - bool publish(const char* message, bool retained); - bool subscribe(unsigned char qos); - void loop(); - bool connected(); - - private: - Interface* _interface; - WiFiClient _wifiClient; - WiFiClientSecure _wifiClientSecure; - PubSubClient _pubSubClient; - char _topicBuffer[TOPIC_BUFFER_LENGTH]; - bool _secure; - const char* _host; - unsigned int _port; - const char* _fingerprint; - unsigned char _subscribeWithoutLoop; - - void _callback(char* topic, unsigned char* payload, unsigned int length); - std::function _userCallback; - }; -} diff --git a/src/Homie/Strings.hpp b/src/Homie/Strings.hpp index e8a06c9f..aa8c8a21 100644 --- a/src/Homie/Strings.hpp +++ b/src/Homie/Strings.hpp @@ -3,8 +3,9 @@ namespace HomieInternals { // config mode - const char PROGMEM_CONFIG_CORS[] PROGMEM = "HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: PUT\r\nAccess-Control-Allow-Headers: Content-Type\r\n\r\n"; const char PROGMEM_CONFIG_APPLICATION_JSON[] PROGMEM = "application/json"; + const char PROGMEM_CONFIG_JSON_SUCCESS[] PROGMEM = "{\"success\":true}"; const char PROGMEM_CONFIG_JSON_FAILURE_BEGINNING[] PROGMEM = "{\"success\":false,\"error\":\""; - const char PROGMEM_CONFIG_NETWORKS_FAILURE[] PROGMEM = "{\"error\": \"Initial Wi-Fi scan not finished yet\"}"; + const char PROGMEM_CONFIG_JSON_FAILURE_END[] PROGMEM = "\"}"; + } diff --git a/src/Homie/Timer.cpp b/src/Homie/Timer.cpp index 6ba4cc87..8e13519a 100644 --- a/src/Homie/Timer.cpp +++ b/src/Homie/Timer.cpp @@ -6,27 +6,50 @@ Timer::Timer() : _initialTime(0) , _interval(0) , _tickAtBeginning(false) -{ +, _active(true) { } -void Timer::setInterval(unsigned long interval, bool tickAtBeginning) { - this->_interval = interval; - this->_tickAtBeginning = tickAtBeginning; +void Timer::setInterval(uint32_t interval, bool tickAtBeginning) { + _interval = interval; + _tickAtBeginning = tickAtBeginning; - if (!tickAtBeginning) this->_initialTime = millis(); + this->reset(); } -bool Timer::check() { - if (this->_tickAtBeginning && this->_initialTime == 0) return true; - if (millis() - this->_initialTime >= this->_interval) return true; +uint32_t HomieInternals::Timer::getInterval() { + return _interval; +} + +bool Timer::check() const { + if (!_active) return false; + + if (_tickAtBeginning && _initialTime == 0) return true; + if (millis() - _initialTime >= _interval) return true; return false; } void Timer::reset() { - this->_initialTime = 0; + if (_tickAtBeginning) { + _initialTime = 0; + } else { + this->tick(); + } } void Timer::tick() { - this->_initialTime = millis(); + _initialTime = millis(); +} + +void Timer::activate() { + _active = true; +} + +void Timer::deactivate() { + _active = false; + reset(); +} + +bool Timer::isActive() const { + return _active; } diff --git a/src/Homie/Timer.hpp b/src/Homie/Timer.hpp index 82919f9d..b7afa253 100644 --- a/src/Homie/Timer.hpp +++ b/src/Homie/Timer.hpp @@ -3,17 +3,22 @@ #include "Arduino.h" namespace HomieInternals { - class Timer { - public: - Timer(); - void setInterval(unsigned long interval, bool tickAtBeginning = true); - bool check(); - void tick(); - void reset(); +class Timer { + public: + Timer(); + void setInterval(uint32_t interval, bool tickAtBeginning = true); + uint32_t getInterval(); + bool check() const; + void tick(); + void reset(); + void activate(); + void deactivate(); + bool isActive() const; - private: - unsigned long _initialTime; - unsigned long _interval; - bool _tickAtBeginning; - }; -} + private: + uint32_t _initialTime; + uint32_t _interval; + bool _tickAtBeginning; + bool _active; +}; +} // namespace HomieInternals diff --git a/src/Homie/Uptime.cpp b/src/Homie/Uptime.cpp index dd773b62..2365b275 100644 --- a/src/Homie/Uptime.cpp +++ b/src/Homie/Uptime.cpp @@ -3,17 +3,16 @@ using namespace HomieInternals; Uptime::Uptime() -: _seconds(0) -, _lastTick(0) -{ +: _milliseconds(0) +, _lastTick(0) { } void Uptime::update() { - unsigned long now = millis(); - this->_seconds += (now - this->_lastTick) / 1000UL; - this->_lastTick = now; + uint32_t now = millis(); + _milliseconds += (now - _lastTick); + _lastTick = now; } -unsigned long Uptime::getSeconds() { - return this->_seconds; +uint64_t Uptime::getSeconds() const { + return (_milliseconds / 1000ULL); } diff --git a/src/Homie/Uptime.hpp b/src/Homie/Uptime.hpp index 3a3d20de..a43efd33 100644 --- a/src/Homie/Uptime.hpp +++ b/src/Homie/Uptime.hpp @@ -3,14 +3,14 @@ #include "Arduino.h" namespace HomieInternals { - class Uptime { - public: - Uptime(); - void update(); - unsigned long getSeconds(); +class Uptime { + public: + Uptime(); + void update(); + uint64_t getSeconds() const; - private: - unsigned long _seconds; - unsigned long _lastTick; - }; -} + private: + uint64_t _milliseconds; + uint32_t _lastTick; +}; +} // namespace HomieInternals diff --git a/src/Homie/Utils/DeviceId.cpp b/src/Homie/Utils/DeviceId.cpp new file mode 100644 index 00000000..b3d8966b --- /dev/null +++ b/src/Homie/Utils/DeviceId.cpp @@ -0,0 +1,15 @@ +#include "DeviceId.hpp" + +using namespace HomieInternals; + +char DeviceId::_deviceId[]; // need to define the static variable + +void DeviceId::generate() { + uint8_t mac[6]; + WiFi.macAddress(mac); + snprintf(DeviceId::_deviceId, MAX_MAC_STRING_LENGTH+1 , "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} + +const char* DeviceId::get() { + return DeviceId::_deviceId; +} diff --git a/src/Homie/Utils/DeviceId.hpp b/src/Homie/Utils/DeviceId.hpp new file mode 100644 index 00000000..da5a42a5 --- /dev/null +++ b/src/Homie/Utils/DeviceId.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "Arduino.h" + +#include + +#include "../Limits.hpp" + +namespace HomieInternals { +class DeviceId { + public: + static void generate(); + static const char* get(); + + private: + static char _deviceId[MAX_MAC_STRING_LENGTH + 1]; +}; +} // namespace HomieInternals diff --git a/src/Homie/Utils/Helpers.cpp b/src/Homie/Utils/Helpers.cpp new file mode 100644 index 00000000..8c228680 --- /dev/null +++ b/src/Homie/Utils/Helpers.cpp @@ -0,0 +1,84 @@ +#include "Helpers.hpp" + +using namespace HomieInternals; + +void Helpers::abort(const String& message) { + Serial.begin(115200); + Serial << message << endl; + Serial.flush(); + ::abort(); +} + +uint8_t Helpers::rssiToPercentage(int32_t rssi) { + uint8_t quality; + if (rssi <= -100) { + quality = 0; + } else if (rssi >= -50) { + quality = 100; + } else { + quality = 2 * (rssi + 100); + } + + return quality; +} + +void Helpers::stringToBytes(const char* str, char sep, byte* bytes, int maxBytes, int base) { + // taken from http://stackoverflow.com/a/35236734 + for (int i = 0; i < maxBytes; i++) { + bytes[i] = strtoul(str, NULL, base); + str = strchr(str, sep); + if (str == NULL || *str == '\0') { + break; + } + str++; + } +} + +bool Helpers::validateIP(const char* ip) { + IPAddress test; + return test.fromString(ip); +} + +bool Helpers::validateMacAddress(const char* mac) { + // taken from http://stackoverflow.com/a/4792211 + int i = 0; + int s = 0; + while (*mac) { + if (isxdigit(*mac)) { + i++; + } else if (*mac == ':' || *mac == '-') { + if (i == 0 || i / 2 - 1 != s) + break; + ++s; + } else { + s = -1; + } + ++mac; + } + return (i == MAX_MAC_STRING_LENGTH && s == 5); +} + +bool Helpers::validateMd5(const char* md5) { + if (strlen(md5) != 32) return false; + + for (uint8_t i = 0; i < 32; i++) { + char c = md5[i]; + bool valid = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + if (!valid) return false; + } + + return true; +} + +std::unique_ptr Helpers::cloneString(const String& string) { + size_t length = string.length(); + std::unique_ptr copy(new char[length + 1]); + memcpy(copy.get(), string.c_str(), length); + copy.get()[length] = '\0'; + + return copy; +} + +void Helpers::ipToString(const IPAddress& ip, char * str) { + snprintf(str, MAX_IP_STRING_LENGTH, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); +} diff --git a/src/Homie/Utils/Helpers.hpp b/src/Homie/Utils/Helpers.hpp new file mode 100644 index 00000000..1bf949cc --- /dev/null +++ b/src/Homie/Utils/Helpers.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "Arduino.h" +#include +#include "../../StreamingOperator.hpp" +#include "../Limits.hpp" +#include + +namespace HomieInternals { +class Helpers { + public: + static void abort(const String& message); + static uint8_t rssiToPercentage(int32_t rssi); + static void stringToBytes(const char* str, char sep, byte* bytes, int maxBytes, int base); + static bool validateIP(const char* ip); + static bool validateMacAddress(const char* mac); + static bool validateMd5(const char* md5); + static std::unique_ptr cloneString(const String& string); + static void ipToString(const IPAddress& ip, char* str); +}; +} // namespace HomieInternals diff --git a/src/Homie/Utils/ResetHandler.cpp b/src/Homie/Utils/ResetHandler.cpp new file mode 100644 index 00000000..f4fd1c11 --- /dev/null +++ b/src/Homie/Utils/ResetHandler.cpp @@ -0,0 +1,51 @@ +#include "ResetHandler.hpp" + +using namespace HomieInternals; + +Ticker ResetHandler::_resetBTNTicker; +Bounce ResetHandler::_resetBTNDebouncer; +Ticker ResetHandler::_resetTicker; +bool ResetHandler::_sentReset = false; + +void ResetHandler::Attach() { + if (Interface::get().reset.enabled) { + pinMode(Interface::get().reset.triggerPin, INPUT_PULLUP); + _resetBTNDebouncer.attach(Interface::get().reset.triggerPin); + _resetBTNDebouncer.interval(Interface::get().reset.triggerTime); + + _resetBTNTicker.attach_ms(10, _tick); + _resetTicker.attach_ms(100, _handleReset); + } +} + +void ResetHandler::_tick() { + if (!Interface::get().reset.resetFlag && Interface::get().reset.enabled) { + _resetBTNDebouncer.update(); + if (_resetBTNDebouncer.read() == Interface::get().reset.triggerState) { + Interface::get().getLogger() << F("Flagged for reset by pin") << endl; + Interface::get().disable = true; + Interface::get().reset.resetFlag = true; + } + } +} + +void ResetHandler::_handleReset() { + if (Interface::get().reset.resetFlag && !_sentReset && Interface::get().reset.idle) { + Interface::get().getLogger() << F("Device is idle") << endl; + + Interface::get().getConfig().erase(); + Interface::get().getLogger() << F("Configuration erased") << endl; + + // Set boot mode + Interface::get().getConfig().setHomieBootModeOnNextBoot(HomieBootMode::CONFIGURATION); + + Interface::get().getLogger() << F("Triggering ABOUT_TO_RESET event...") << endl; + Interface::get().event.type = HomieEventType::ABOUT_TO_RESET; + Interface::get().eventHandler(Interface::get().event); + + Interface::get().getLogger() << F("↻ Rebooting into config mode...") << endl; + Serial.flush(); + ESP.restart(); + _sentReset = true; + } +} diff --git a/src/Homie/Utils/ResetHandler.hpp b/src/Homie/Utils/ResetHandler.hpp new file mode 100644 index 00000000..8ece92b5 --- /dev/null +++ b/src/Homie/Utils/ResetHandler.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "Arduino.h" + +#include +#include +#include "../../StreamingOperator.hpp" +#include "../Datatypes/Interface.hpp" + +namespace HomieInternals { +class ResetHandler { + public: + static void Attach(); + + private: + // Disallow creating an instance of this object + ResetHandler() {} + static Ticker _resetBTNTicker; + static Bounce _resetBTNDebouncer; + static void _tick(); + static Ticker _resetTicker; + static bool _sentReset; + static void _handleReset(); +}; +} // namespace HomieInternals diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp new file mode 100644 index 00000000..84f67696 --- /dev/null +++ b/src/Homie/Utils/Validation.cpp @@ -0,0 +1,389 @@ +#include "Validation.hpp" + +using namespace HomieInternals; + +ConfigValidationResult Validation::validateConfig(const JsonObject& object) { + ConfigValidationResult result; + result = _validateConfigRoot(object); + if (!result.valid) return result; + result = _validateConfigWifi(object); + if (!result.valid) return result; + result = _validateConfigMqtt(object); + if (!result.valid) return result; + result = _validateConfigOta(object); + if (!result.valid) return result; + result = _validateConfigSettings(object); + if (!result.valid) return result; + + result.valid = true; + return result; +} + +ConfigValidationResult Validation::_validateConfigRoot(const JsonObject& object) { + ConfigValidationResult result; + result.valid = false; + if (!object.containsKey("name") || !object["name"].is()) { + result.reason = F("name is not a string"); + return result; + } + if (strlen(object["name"]) + 1 > MAX_FRIENDLY_NAME_LENGTH) { + result.reason = F("name is too long"); + return result; + } + if (object.containsKey("device_id")) { + if (!object["device_id"].is()) { + result.reason = F("device_id is not a string"); + return result; + } + if (strlen(object["device_id"]) + 1 > MAX_DEVICE_ID_LENGTH) { + result.reason = F("device_id is too long"); + return result; + } + } + + const char* name = object["name"]; + + if (strcmp_P(name, PSTR("")) == 0) { + result.reason = F("name is empty"); + return result; + } + + if (object.containsKey(F("device_stats_interval")) && !object[F("device_stats_interval")].is()) { + result.reason = F("device_stats_interval is not an integer"); + return result; + } + + result.valid = true; + return result; +} + +ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) { + ConfigValidationResult result; + result.valid = false; + + if (!object.containsKey("wifi") || !object["wifi"].is()) { + result.reason = F("wifi is not an object"); + return result; + } + if (!object["wifi"].as().containsKey("ssid") || !object["wifi"]["ssid"].is()) { + result.reason = F("wifi.ssid is not a string"); + return result; + } + if (strlen(object["wifi"]["ssid"]) + 1 > MAX_WIFI_SSID_LENGTH) { + result.reason = F("wifi.ssid is too long"); + return result; + } + if (!object["wifi"].as().containsKey("password") || !object["wifi"]["password"].is()) { + result.reason = F("wifi.password is not a string"); + return result; + } + if (object["wifi"]["password"] && strlen(object["wifi"]["password"]) + 1 > MAX_WIFI_PASSWORD_LENGTH) { + result.reason = F("wifi.password is too long"); + return result; + } + // by benzino + if (object["wifi"].as().containsKey("bssid") && !object["wifi"]["bssid"].is()) { + result.reason = F("wifi.bssid is not a string"); + return result; + } + if ((object["wifi"].as().containsKey("bssid") && !object["wifi"].as().containsKey("channel")) || + (!object["wifi"].as().containsKey("bssid") && object["wifi"].as().containsKey("channel"))) { + result.reason = F("wifi.channel_bssid channel and BSSID is required"); + return result; + } + if (object["wifi"].as().containsKey("bssid") && !Helpers::validateMacAddress(object["wifi"].as().get("bssid"))) { + result.reason = F("wifi.bssid is not valid mac"); + return result; + } + if (object["wifi"].as().containsKey("channel") && !object["wifi"]["channel"].is()) { + result.reason = F("wifi.channel is not an integer"); + return result; + } + if (object["wifi"].as().containsKey("ip") && !object["wifi"]["ip"].is()) { + result.reason = F("wifi.ip is not a string"); + return result; + } + if (object["wifi"]["ip"] && strlen(object["wifi"]["ip"]) + 1 > MAX_IP_STRING_LENGTH) { + result.reason = F("wifi.ip is too long"); + return result; + } + if (object["wifi"]["ip"] && !Helpers::validateIP(object["wifi"].as().get("ip"))) { + result.reason = F("wifi.ip is not valid ip address"); + return result; + } + if (object["wifi"].as().containsKey("mask") && !object["wifi"]["mask"].is()) { + result.reason = F("wifi.mask is not a string"); + return result; + } + if (object["wifi"]["mask"] && strlen(object["wifi"]["mask"]) + 1 > MAX_IP_STRING_LENGTH) { + result.reason = F("wifi.mask is too long"); + return result; + } + if (object["wifi"]["mask"] && !Helpers::validateIP(object["wifi"].as().get("mask"))) { + result.reason = F("wifi.mask is not valid mask"); + return result; + } + if (object["wifi"].as().containsKey("gw") && !object["wifi"]["gw"].is()) { + result.reason = F("wifi.gw is not a string"); + return result; + } + if (object["wifi"]["gw"] && strlen(object["wifi"]["gw"]) + 1 > MAX_IP_STRING_LENGTH) { + result.reason = F("wifi.gw is too long"); + return result; + } + if (object["wifi"]["gw"] && !Helpers::validateIP(object["wifi"].as().get("gw"))) { + result.reason = F("wifi.gw is not valid gateway address"); + return result; + } + if ((object["wifi"].as().containsKey("ip") && (!object["wifi"].as().containsKey("mask") || !object["wifi"].as().containsKey("gw"))) || + (object["wifi"].as().containsKey("gw") && (!object["wifi"].as().containsKey("mask") || !object["wifi"].as().containsKey("ip"))) || + (object["wifi"].as().containsKey("mask") && (!object["wifi"].as().containsKey("ip") || !object["wifi"].as().containsKey("gw")))) { + result.reason = F("wifi.staticip ip, gw and mask is required"); + return result; + } + if (object["wifi"].as().containsKey("dns1") && !object["wifi"]["dns1"].is()) { + result.reason = F("wifi.dns1 is not a string"); + return result; + } + if (object["wifi"]["dns1"] && strlen(object["wifi"]["dns1"]) + 1 > MAX_IP_STRING_LENGTH) { + result.reason = F("wifi.dns1 is too long"); + return result; + } + if (object["wifi"]["dns1"] && !Helpers::validateIP(object["wifi"].as().get("dns1"))) { + result.reason = F("wifi.dns1 is not valid dns address"); + return result; + } + if (object["wifi"].as().containsKey("dns2") && !object["wifi"].as().containsKey("dns1")) { + result.reason = F("wifi.dns2 no dns1 defined"); + return result; + } + if (object["wifi"].as().containsKey("dns2") && !object["wifi"]["dns2"].is()) { + result.reason = F("wifi.dns2 is not a string"); + return result; + } + if (object["wifi"]["dns2"] && strlen(object["wifi"]["dns2"]) + 1 > MAX_IP_STRING_LENGTH) { + result.reason = F("wifi.dns2 is too long"); + return result; + } + if (object["wifi"]["dns2"] && !Helpers::validateIP(object["wifi"].as().get("dns2"))) { + result.reason = F("wifi.dns2 is not valid dns address"); + return result; + } + + const char* wifiSsid = object["wifi"]["ssid"]; + if (strcmp_P(wifiSsid, PSTR("")) == 0) { + result.reason = F("wifi.ssid is empty"); + return result; + } + + result.valid = true; + return result; +} + +ConfigValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { + ConfigValidationResult result; + result.valid = false; + + if (!object.containsKey("mqtt") || !object["mqtt"].is()) { + result.reason = F("mqtt is not an object"); + return result; + } + if (!object["mqtt"].as().containsKey("host") || !object["mqtt"]["host"].is()) { + result.reason = F("mqtt.host is not a string"); + return result; + } + if (strlen(object["mqtt"]["host"]) + 1 > MAX_HOSTNAME_LENGTH) { + result.reason = F("mqtt.host is too long"); + return result; + } + if (object["mqtt"].as().containsKey("port") && !object["mqtt"]["port"].is()) { + result.reason = F("mqtt.port is not an integer"); + return result; + } + if (object["mqtt"].as().containsKey("base_topic")) { + if (!object["mqtt"]["base_topic"].is()) { + result.reason = F("mqtt.base_topic is not a string"); + return result; + } + + if (strlen(object["mqtt"]["base_topic"]) + 1 > MAX_MQTT_BASE_TOPIC_LENGTH) { + result.reason = F("mqtt.base_topic is too long"); + return result; + } + } + if (object["mqtt"].as().containsKey("auth")) { + if (!object["mqtt"]["auth"].is()) { + result.reason = F("mqtt.auth is not a boolean"); + return result; + } + + if (object["mqtt"]["auth"]) { + if (!object["mqtt"].as().containsKey("username") || !object["mqtt"]["username"].is()) { + result.reason = F("mqtt.username is not a string"); + return result; + } + if (strlen(object["mqtt"]["username"]) + 1 > MAX_MQTT_CREDS_LENGTH) { + result.reason = F("mqtt.username is too long"); + return result; + } + if (!object["mqtt"].as().containsKey("password") || !object["mqtt"]["password"].is()) { + result.reason = F("mqtt.password is not a string"); + return result; + } + if (strlen(object["mqtt"]["password"]) + 1 > MAX_MQTT_CREDS_LENGTH) { + result.reason = F("mqtt.password is too long"); + return result; + } + } + } + + const char* host = object["mqtt"]["host"]; + if (strcmp_P(host, PSTR("")) == 0) { + result.reason = F("mqtt.host is empty"); + return result; + } + + result.valid = true; + return result; +} + +ConfigValidationResult Validation::_validateConfigOta(const JsonObject& object) { + ConfigValidationResult result; + result.valid = false; + + if (!object.containsKey("ota") || !object["ota"].is()) { + result.reason = F("ota is not an object"); + return result; + } + if (!object["ota"].as().containsKey("enabled") || !object["ota"]["enabled"].is()) { + result.reason = F("ota.enabled is not a boolean"); + return result; + } + + result.valid = true; + return result; +} + +ConfigValidationResult Validation::_validateConfigSettings(const JsonObject& object) { + ConfigValidationResult result; + result.valid = false; + + StaticJsonBuffer<0> emptySettingsBuffer; + + JsonObject* settingsObject = &(emptySettingsBuffer.createObject()); + + if (object.containsKey("settings") && object["settings"].is()) { + settingsObject = &(object["settings"].as()); + } + + if (settingsObject->size() > MAX_CONFIG_SETTING_SIZE) {//max settings here and in isettings + result.reason = F("settings contains more elements than the set limit"); + return result; + } + + for (IHomieSetting* iSetting : IHomieSetting::settings) { + enum class Issue { + Type, + Validator, + Missing + }; + auto setReason = [&result, &iSetting](Issue issue) { + switch (issue) { + case Issue::Type: + result.reason = String(iSetting->getName()) + F(" setting is not a ") + String(iSetting->getType()); + break; + case Issue::Validator: + result.reason = String(iSetting->getName()) + F(" setting does not pass the validator function"); + break; + case Issue::Missing: + result.reason = String(iSetting->getName()) + F(" setting is missing"); + break; + } + }; + + if (iSetting->isBool()) { + HomieSetting* setting = static_cast*>(iSetting); + + if (settingsObject->containsKey(setting->getName())) { + if (!(*settingsObject)[setting->getName()].is()) { + setReason(Issue::Type); + return result; + } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { + setReason(Issue::Validator); + return result; + } + } else if (setting->isRequired()) { + setReason(Issue::Missing); + return result; + } + } else if (iSetting->isLong()) { + HomieSetting* setting = static_cast*>(iSetting); + + if (settingsObject->containsKey(setting->getName())) { + if (!(*settingsObject)[setting->getName()].is()) { + setReason(Issue::Type); + return result; + } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { + setReason(Issue::Validator); + return result; + } + } else if (setting->isRequired()) { + setReason(Issue::Missing); + return result; + } + } else if (iSetting->isDouble()) { + HomieSetting* setting = static_cast*>(iSetting); + + if (settingsObject->containsKey(setting->getName())) { + if (!(*settingsObject)[setting->getName()].is()) { + setReason(Issue::Type); + return result; + } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { + setReason(Issue::Validator); + return result; + } + } else if (setting->isRequired()) { + setReason(Issue::Missing); + return result; + } + } else if (iSetting->isConstChar()) { + HomieSetting* setting = static_cast*>(iSetting); + + if (settingsObject->containsKey(setting->getName())) { + if (!(*settingsObject)[setting->getName()].is()) { + setReason(Issue::Type); + return result; + } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { + setReason(Issue::Validator); + return result; + } + } else if (setting->isRequired()) { + setReason(Issue::Missing); + return result; + } + } + } + + result.valid = true; + return result; +} + +// bool Validation::_validateConfigWifiBssid(const char *mac) { +// int i = 0; +// int s = 0; +// while (*mac) { +// if (isxdigit(*mac)) { +// i++; +// } +// else if (*mac == ':' || *mac == '-') { +// if (i == 0 || i / 2 - 1 != s) +// break; +// ++s; +// } +// else { +// s = -1; +// } +// ++mac; +// } +// return (i == MAX_MAC_STRING_LENGTH && s == 5); +// } diff --git a/src/Homie/Utils/Validation.hpp b/src/Homie/Utils/Validation.hpp new file mode 100644 index 00000000..efa39fc7 --- /dev/null +++ b/src/Homie/Utils/Validation.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "Arduino.h" + +#include +#include "Helpers.hpp" +#include "../Limits.hpp" +#include "../../HomieSetting.hpp" + +namespace HomieInternals { +struct ConfigValidationResult { + bool valid; + String reason; +}; + +class Validation { + public: + static ConfigValidationResult validateConfig(const JsonObject& object); + + private: + static ConfigValidationResult _validateConfigRoot(const JsonObject& object); + static ConfigValidationResult _validateConfigWifi(const JsonObject& object); + static ConfigValidationResult _validateConfigMqtt(const JsonObject& object); + static ConfigValidationResult _validateConfigOta(const JsonObject& object); + static ConfigValidationResult _validateConfigSettings(const JsonObject& object); +}; +} // namespace HomieInternals diff --git a/src/HomieBootMode.hpp b/src/HomieBootMode.hpp new file mode 100644 index 00000000..f2ea7664 --- /dev/null +++ b/src/HomieBootMode.hpp @@ -0,0 +1,8 @@ +#pragma once + +enum class HomieBootMode : uint8_t { + UNDEFINED = 0, + STANDALONE = 1, + CONFIGURATION = 2, + NORMAL = 3 +}; diff --git a/src/HomieEvent.hpp b/src/HomieEvent.hpp index ce09de36..c514db74 100644 --- a/src/HomieEvent.hpp +++ b/src/HomieEvent.hpp @@ -1,12 +1,38 @@ #pragma once -enum HomieEvent : unsigned char { - HOMIE_CONFIGURATION_MODE = 1, - HOMIE_NORMAL_MODE, - HOMIE_OTA_MODE, - HOMIE_ABOUT_TO_RESET, - HOMIE_WIFI_CONNECTED, - HOMIE_WIFI_DISCONNECTED, - HOMIE_MQTT_CONNECTED, - HOMIE_MQTT_DISCONNECTED +#include +#include + +enum class HomieEventType : uint8_t { + STANDALONE_MODE = 1, + CONFIGURATION_MODE, + NORMAL_MODE, + OTA_STARTED, + OTA_PROGRESS, + OTA_SUCCESSFUL, + OTA_FAILED, + ABOUT_TO_RESET, + WIFI_CONNECTED, + WIFI_DISCONNECTED, + MQTT_READY, + MQTT_DISCONNECTED, + MQTT_PACKET_ACKNOWLEDGED, + READY_TO_SLEEP +}; + +struct HomieEvent { + HomieEventType type; + /* WIFI_CONNECTED */ + IPAddress ip; + IPAddress mask; + IPAddress gateway; + /* WIFI_DISCONNECTED */ + WiFiDisconnectReason wifiReason; + /* MQTT_DISCONNECTED */ + AsyncMqttClientDisconnectReason mqttReason; + /* MQTT_PACKET_ACKNOWLEDGED */ + uint16_t packetId; + /* OTA_PROGRESS */ + size_t sizeDone; + size_t sizeTotal; }; diff --git a/src/HomieNode.cpp b/src/HomieNode.cpp index 5891e8b5..de66f4ed 100644 --- a/src/HomieNode.cpp +++ b/src/HomieNode.cpp @@ -1,59 +1,66 @@ #include "HomieNode.hpp" +#include "Homie.hpp" using namespace HomieInternals; -HomieNode::HomieNode(const char* id, const char* type, NodeInputHandler inputHandler, bool subscribeToAll) +std::vector HomieNode::nodes; + +PropertyInterface::PropertyInterface() +: _property(nullptr) { +} + +void PropertyInterface::settable(const PropertyInputHandler& inputHandler) { + _property->settable(inputHandler); +} + +PropertyInterface& PropertyInterface::setProperty(Property* property) { + _property = property; + return *this; +} + +HomieNode::HomieNode(const char* id, const char* type, const NodeInputHandler& inputHandler) : _id(id) , _type(type) -, _subscriptionsCount(0) -, _subscribeToAll(subscribeToAll) +, _properties() , _inputHandler(inputHandler) { if (strlen(id) + 1 > MAX_NODE_ID_LENGTH || strlen(type) + 1 > MAX_NODE_TYPE_LENGTH) { - Serial.println(F("✖ HomieNode(): either the id or type string is too long")); - abort(); + Helpers::abort(F("✖ HomieNode(): either the id or type string is too long")); + return; // never reached, here for clarity } + Homie._checkBeforeSetup(F("HomieNode::HomieNode")); - this->_id = id; - this->_type = type; + HomieNode::nodes.push_back(this); } -void HomieNode::subscribe(const char* property, PropertyInputHandler inputHandler) { - if (strlen(property) + 1 > MAX_NODE_PROPERTY_LENGTH) { - Serial.println(F("✖ subscribe(): the property string is too long")); - abort(); - } +HomieNode::~HomieNode() { + Helpers::abort(F("✖✖ ~HomieNode(): Destruction of HomieNode object not possible\n Hint: Don't create HomieNode objects as a local variable (e.g. in setup())")); + return; // never reached, here for clarity +} - if (this->_subscriptionsCount > MAX_SUBSCRIPTIONS_COUNT_PER_NODE) { - Serial.println(F("✖ subscribe(): the max subscription count has been reached")); - abort(); - } +PropertyInterface& HomieNode::advertise(const char* property) { + Property* propertyObject = new Property(property); - Subscription subscription; - strcpy(subscription.property, property); - subscription.inputHandler = inputHandler; - this->_subscriptions[this->_subscriptionsCount++] = subscription; -} + _properties.push_back(propertyObject); -const char* HomieNode::getId() const { - return this->_id; + return _propertyInterface.setProperty(propertyObject); } -const char* HomieNode::getType() const { - return this->_type; -} +PropertyInterface& HomieNode::advertiseRange(const char* property, uint16_t lower, uint16_t upper) { + Property* propertyObject = new Property(property, true, lower, upper); + + _properties.push_back(propertyObject); -const Subscription* HomieNode::getSubscriptions() const { - return this->_subscriptions; + return _propertyInterface.setProperty(propertyObject); } -unsigned char HomieNode::getSubscriptionsCount() const { - return this->_subscriptionsCount; +SendingPromise& HomieNode::setProperty(const String& property) const { + return Interface::get().getSendingPromise().setNode(*this).setProperty(property).setQos(1).setRetained(true).overwriteSetter(false).setRange({ .isRange = false, .index = 0 }); } -bool HomieNode::getSubscribeToAll() const { - return this->_subscribeToAll; +bool HomieNode::handleInput(const String& property, const HomieRange& range, const String& value) { + return _inputHandler(property, range, value); } -NodeInputHandler HomieNode::getInputHandler() const { - return this->_inputHandler; +const std::vector& HomieNode::getProperties() const { + return _properties; } diff --git a/src/HomieNode.hpp b/src/HomieNode.hpp index 77578478..695eca8e 100644 --- a/src/HomieNode.hpp +++ b/src/HomieNode.hpp @@ -1,37 +1,100 @@ #pragma once +#include +#include #include "Arduino.h" -#include "Homie/Datatypes/Subscription.hpp" +#include "StreamingOperator.hpp" +#include "Homie/Datatypes/Interface.hpp" #include "Homie/Datatypes/Callbacks.hpp" #include "Homie/Limits.hpp" +#include "HomieRange.hpp" + +class HomieNode; namespace HomieInternals { - class HomieClass; - class BootNormal; - class BootConfig; -} +class HomieClass; +class Property; +class BootNormal; +class BootConfig; +class SendingPromise; + +class PropertyInterface { + friend ::HomieNode; + + public: + PropertyInterface(); + + void settable(const PropertyInputHandler& inputHandler = [](const HomieRange& range, const String& value) { return false; }); + + private: + PropertyInterface& setProperty(Property* property); + + Property* _property; +}; + +class Property { + friend BootNormal; + + public: + explicit Property(const char* id, bool range = false, uint16_t lower = 0, uint16_t upper = 0) { _id = strdup(id); _range = range; _lower = lower; _upper = upper; _settable = false; } + void settable(const PropertyInputHandler& inputHandler) { _settable = true; _inputHandler = inputHandler; } + + private: + const char* getProperty() const { return _id; } + bool isSettable() const { return _settable; } + bool isRange() const { return _range; } + uint16_t getLower() const { return _lower; } + uint16_t getUpper() const { return _upper; } + PropertyInputHandler getInputHandler() const { return _inputHandler; } + const char* _id; + bool _range; + uint16_t _lower; + uint16_t _upper; + bool _settable; + PropertyInputHandler _inputHandler; +}; +} // namespace HomieInternals class HomieNode { friend HomieInternals::HomieClass; friend HomieInternals::BootNormal; friend HomieInternals::BootConfig; - public: - HomieNode(const char* id, const char* type, HomieInternals::NodeInputHandler nodeInputHandler = [](String property, String value) { return false; }, bool subscribeToAll = false); - - void subscribe(const char* property, HomieInternals::PropertyInputHandler inputHandler = [](String value) { return false; }); - - private: - const char* getId() const; - const char* getType() const; - const HomieInternals::Subscription* getSubscriptions() const; - unsigned char getSubscriptionsCount() const; - bool getSubscribeToAll() const; - HomieInternals::NodeInputHandler getInputHandler() const; - - const char* _id; - const char* _type; - HomieInternals::Subscription _subscriptions[HomieInternals::MAX_SUBSCRIPTIONS_COUNT_PER_NODE]; - unsigned char _subscriptionsCount; - bool _subscribeToAll; - HomieInternals::NodeInputHandler _inputHandler; + + public: + HomieNode(const char* id, const char* type, const HomieInternals::NodeInputHandler& nodeInputHandler = [](const String& property, const HomieRange& range, const String& value) { return false; }); + virtual ~HomieNode(); + + const char* getId() const { return _id; } + const char* getType() const { return _type; } + + HomieInternals::PropertyInterface& advertise(const char* property); + HomieInternals::PropertyInterface& advertiseRange(const char* property, uint16_t lower, uint16_t upper); + + HomieInternals::SendingPromise& setProperty(const String& property) const; + + protected: + virtual void setup() {} + virtual void loop() {} + virtual void onReadyToOperate() {} + virtual bool handleInput(const String& property, const HomieRange& range, const String& value); + + private: + const std::vector& getProperties() const; + + static HomieNode* find(const char* id) { + for (HomieNode* iNode : HomieNode::nodes) { + if (strcmp(id, iNode->getId()) == 0) return iNode; + } + + return 0; + } + + const char* _id; + const char* _type; + std::vector _properties; + HomieInternals::NodeInputHandler _inputHandler; + + HomieInternals::PropertyInterface _propertyInterface; + + static std::vector nodes; }; diff --git a/src/HomieRange.hpp b/src/HomieRange.hpp new file mode 100644 index 00000000..c4eff8d3 --- /dev/null +++ b/src/HomieRange.hpp @@ -0,0 +1,6 @@ +#pragma once + +struct HomieRange { + bool isRange; + uint16_t index; +}; diff --git a/src/HomieSetting.cpp b/src/HomieSetting.cpp new file mode 100644 index 00000000..ea03f459 --- /dev/null +++ b/src/HomieSetting.cpp @@ -0,0 +1,106 @@ +#include "HomieSetting.hpp" + +using namespace HomieInternals; + +std::vector __attribute__((init_priority(101))) IHomieSetting::settings; + +HomieInternals::IHomieSetting::IHomieSetting(const char * name, const char * description) + : _name(name) + , _description(description) + , _required(true) + , _provided(false) { +} + +bool IHomieSetting::isRequired() const { + return _required; +} + +const char* IHomieSetting::getName() const { + return _name; +} + +const char* IHomieSetting::getDescription() const { + return _description; +} + + + +template +HomieSetting::HomieSetting(const char* name, const char* description) + : IHomieSetting(name, description) + , _value() + , _validator([](T candidate) { return true; }) { + IHomieSetting::settings.push_back(this); +} + +template +T HomieSetting::get() const { + return _value; +} + +template +bool HomieSetting::wasProvided() const { + return _provided; +} + +template +HomieSetting& HomieSetting::setDefaultValue(T defaultValue) { + _value = defaultValue; + _required = false; + return *this; +} + +template +HomieSetting& HomieSetting::setValidator(const std::function& validator) { + _validator = validator; + return *this; +} + +template +bool HomieSetting::validate(T candidate) const { + return _validator(candidate); +} + +template +void HomieSetting::set(T value) { + _value = value; + _provided = true; +} + +template +bool HomieSetting::isBool() const { return false; } + +template +bool HomieSetting::isLong() const { return false; } + +template +bool HomieSetting::isDouble() const { return false; } + +template +bool HomieSetting::isConstChar() const { return false; } + +template<> +bool HomieSetting::isBool() const { return true; } +template<> +const char* HomieSetting::getType() const { return "bool"; } + +template<> +bool HomieSetting::isLong() const { return true; } +template<> +const char* HomieSetting::getType() const { return "long"; } + +template<> +bool HomieSetting::isDouble() const { return true; } +template<> +const char* HomieSetting::getType() const { return "double"; } + +template<> +bool HomieSetting::isConstChar() const { return true; } +template<> +const char* HomieSetting::getType() const { return "string"; } + +// Needed because otherwise undefined reference to +template class HomieSetting; +template class HomieSetting; +template class HomieSetting; +template class HomieSetting; diff --git a/src/HomieSetting.hpp b/src/HomieSetting.hpp new file mode 100644 index 00000000..0df2dc33 --- /dev/null +++ b/src/HomieSetting.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include "Arduino.h" + +#include "./Homie/Datatypes/Callbacks.hpp" + +namespace HomieInternals { +class HomieClass; +class Config; +class Validation; +class BootConfig; + +class IHomieSetting { + public: + static std::vector settings; + + bool isRequired() const; + const char* getName() const; + const char* getDescription() const; + + virtual bool isBool() const { return false; } + virtual bool isLong() const { return false; } + virtual bool isDouble() const { return false; } + virtual bool isConstChar() const { return false; } + + virtual const char* getType() const { return "unknown"; } + + protected: + explicit IHomieSetting(const char* name, const char* description); + const char* _name; + const char* _description; + bool _required; + bool _provided; +}; +} // namespace HomieInternals + +template +class HomieSetting : public HomieInternals::IHomieSetting { + friend HomieInternals::HomieClass; + friend HomieInternals::Config; + friend HomieInternals::Validation; + friend HomieInternals::BootConfig; + + public: + HomieSetting(const char* name, const char* description); + T get() const; + bool wasProvided() const; + HomieSetting& setDefaultValue(T defaultValue); + HomieSetting& setValidator(const std::function& validator); + + private: + T _value; + std::function _validator; + + bool validate(T candidate) const; + void set(T value); + + bool isBool() const; + bool isLong() const; + bool isDouble() const; + bool isConstChar() const; + + const char* getType() const; +}; diff --git a/src/SendingPromise.cpp b/src/SendingPromise.cpp new file mode 100644 index 00000000..bfdf8d44 --- /dev/null +++ b/src/SendingPromise.cpp @@ -0,0 +1,107 @@ +#include "SendingPromise.hpp" + +using namespace HomieInternals; + +SendingPromise::SendingPromise() +: _node(nullptr) +, _property(nullptr) +, _qos(0) +, _retained(false) +, _overwriteSetter(false) +, _range { .isRange = false, .index = 0 } { +} + +SendingPromise& SendingPromise::setQos(uint8_t qos) { + _qos = qos; + return *this; +} + +SendingPromise& SendingPromise::setRetained(bool retained) { + _retained = retained; + return *this; +} + +SendingPromise& SendingPromise::overwriteSetter(bool overwrite) { + _overwriteSetter = overwrite; + return *this; +} + +SendingPromise& SendingPromise::setRange(const HomieRange& range) { + _range = range; + return *this; +} + +SendingPromise& SendingPromise::setRange(uint16_t rangeIndex) { + HomieRange range; + range.isRange = true; + range.index = rangeIndex; + _range = range; + return *this; +} + +uint16_t SendingPromise::send(const String& value) { + if (!Interface::get().ready) { + Interface::get().getLogger() << F("✖ setNodeProperty(): impossible now") << endl; + return 0; + } + + char* topic = new char[strlen(Interface::get().getConfig().get().mqtt.baseTopic) + strlen(Interface::get().getConfig().get().deviceId) + 1 + strlen(_node->getId()) + 1 + strlen(_property->c_str()) + 6 + 4 + 1]; // last + 6 for range _65536, last + 4 for /set + strcpy(topic, Interface::get().getConfig().get().mqtt.baseTopic); + strcat(topic, Interface::get().getConfig().get().deviceId); + strcat_P(topic, PSTR("/")); + strcat(topic, _node->getId()); + strcat_P(topic, PSTR("/")); + strcat(topic, _property->c_str()); + + if (_range.isRange) { + char rangeStr[5 + 1]; // max 65536 + itoa(_range.index, rangeStr, 10); + strcat_P(topic, PSTR("_")); + strcat(topic, rangeStr); + } + + uint16_t packetId = Interface::get().getMqttClient().publish(topic, _qos, _retained, value.c_str()); + + if (_overwriteSetter) { + strcat_P(topic, PSTR("/set")); + Interface::get().getMqttClient().publish(topic, 1, true, value.c_str()); + } + + delete[] topic; + + return packetId; +} + +SendingPromise& SendingPromise::setNode(const HomieNode& node) { + _node = &node; + return *this; +} + +SendingPromise& SendingPromise::setProperty(const String& property) { + _property = &property; + return *this; +} + +const HomieNode* SendingPromise::getNode() const { + return _node; +} + +const String* SendingPromise::getProperty() const { + return _property; +} + +uint8_t SendingPromise::getQos() const { + return _qos; +} + +HomieRange SendingPromise::getRange() const { + return _range; +} + +bool SendingPromise::isRetained() const { + return _retained; +} + +bool SendingPromise::doesOverwriteSetter() const { + return _overwriteSetter; +} diff --git a/src/SendingPromise.hpp b/src/SendingPromise.hpp new file mode 100644 index 00000000..353d9144 --- /dev/null +++ b/src/SendingPromise.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "Arduino.h" +#include "StreamingOperator.hpp" +#include "Homie/Datatypes/Interface.hpp" +#include "HomieRange.hpp" + +class HomieNode; + +namespace HomieInternals { +class SendingPromise { + friend ::HomieNode; + + public: + SendingPromise(); + SendingPromise& setQos(uint8_t qos); + SendingPromise& setRetained(bool retained); + SendingPromise& overwriteSetter(bool overwrite); + SendingPromise& setRange(const HomieRange& range); + SendingPromise& setRange(uint16_t rangeIndex); + uint16_t send(const String& value); + + private: + SendingPromise& setNode(const HomieNode& node); + SendingPromise& setProperty(const String& property); + const HomieNode* getNode() const; + const String* getProperty() const; + uint8_t getQos() const; + HomieRange getRange() const; + bool isRetained() const; + bool doesOverwriteSetter() const; + + const HomieNode* _node; + const String* _property; + uint8_t _qos; + bool _retained; + bool _overwriteSetter; + HomieRange _range; +}; +} // namespace HomieInternals diff --git a/src/StreamingOperator.hpp b/src/StreamingOperator.hpp new file mode 100644 index 00000000..ed4f8d29 --- /dev/null +++ b/src/StreamingOperator.hpp @@ -0,0 +1,10 @@ +#pragma once + +template +inline Print &operator <<(Print &stream, T arg) +{ stream.print(arg); return stream; } + +enum _EndLineCode { endl }; + +inline Print &operator <<(Print &stream, _EndLineCode arg) +{ stream.println(); return stream; } From de317292ea61cbd3eadbba9b9a8521fe7d458dc3 Mon Sep 17 00:00:00 2001 From: timpur Date: Sat, 2 Jun 2018 12:38:30 +1000 Subject: [PATCH 49/51] update file permissions --- .circleci/assets/configurator_v1.html | 0 scripts/ota_updater/ota_updater.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .circleci/assets/configurator_v1.html mode change 100644 => 100755 scripts/ota_updater/ota_updater.py diff --git a/.circleci/assets/configurator_v1.html b/.circleci/assets/configurator_v1.html old mode 100644 new mode 100755 diff --git a/scripts/ota_updater/ota_updater.py b/scripts/ota_updater/ota_updater.py old mode 100644 new mode 100755 From f4396c41eeb650a595757bf954d602953b97d8bb Mon Sep 17 00:00:00 2001 From: timpur Date: Sat, 2 Jun 2018 12:38:30 +1000 Subject: [PATCH 50/51] update file permissions --- .circleci/assets/configurator_v1.html | 0 scripts/ota_updater/ota_updater.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .circleci/assets/configurator_v1.html mode change 100644 => 100755 scripts/ota_updater/ota_updater.py diff --git a/.circleci/assets/configurator_v1.html b/.circleci/assets/configurator_v1.html old mode 100644 new mode 100755 diff --git a/scripts/ota_updater/ota_updater.py b/scripts/ota_updater/ota_updater.py old mode 100644 new mode 100755 From 53c08db236be05f5c4c510d583f1a2e2c7828386 Mon Sep 17 00:00:00 2001 From: timpur Date: Sat, 2 Jun 2018 13:04:33 +1000 Subject: [PATCH 51/51] Update CI Build --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8fee20fc..faa220b2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,13 +8,13 @@ jobs: - checkout - run: name: install PlatformIO - command: sudo pip install -U https://github.com/platformio/platformio-core/archive/develop.zip + command: sudo pip install -U platformio - run: name: install current code as a PlatformIO library with all dependencies command: platformio lib -g install file://. - run: - name: install staging version of Arduino Core for ESP8266 - command: platformio platform install https://github.com/platformio/platform-espressif8266.git#feature/stage + name: install Arduino Core for ESP8266 + command: platformio platform install espressif8266 - run: name: install exemples dependencies command: platformio lib -g install Shutters@2.1.1 SonoffDual@1.1.0