├── docs ├── index.md ├── assets │ ├── logo.png │ ├── github.png │ ├── youtube.png │ ├── led_mqtt.gif │ ├── led_solid.gif │ └── led_wifi.gif ├── README.md ├── advanced-usage │ ├── branding.md │ ├── broadcast.md │ ├── ui-bundle.md │ ├── streaming-operator.md │ ├── standalone-mode.md │ ├── logging.md │ ├── built-in-led.md │ ├── deep-sleep.md │ ├── range-properties.md │ ├── resetting.md │ ├── magic-bytes.md │ ├── miscellaneous.md │ ├── custom-settings.md │ ├── input-handlers.md │ └── events.md ├── quickstart │ ├── what-is-it.md │ └── getting-started.md ├── others │ ├── limitations-and-known-issues.md │ ├── upgrade-guide-from-v1-to-v2.md │ ├── community-projects.md │ ├── homie-implementation-specifics.md │ ├── troubleshooting.md │ ├── ota-configuration-updates.md │ └── cpp-api-reference.md └── configuration │ ├── json-configuration-file.md │ └── http-json-api.md ├── .gitignore ├── scripts ├── ota_updater │ ├── requirements.txt │ ├── README.md │ └── ota_updater.py └── firmware_parser │ ├── README.md │ └── firmware_parser.py ├── banner.png ├── src ├── HomieRange.hpp ├── Homie.h ├── HomieBootMode.hpp ├── StreamingOperator.hpp ├── Homie │ ├── Uptime.hpp │ ├── Uptime.cpp │ ├── Utils │ │ ├── DeviceId.hpp │ │ ├── DeviceId.cpp │ │ ├── Helpers.hpp │ │ ├── Validation.hpp │ │ ├── Helpers.cpp │ │ └── Validation.cpp │ ├── Blinker.hpp │ ├── Boot │ │ ├── BootStandalone.hpp │ │ ├── Boot.hpp │ │ ├── Boot.cpp │ │ ├── BootConfig.hpp │ │ ├── BootStandalone.cpp │ │ ├── BootNormal.hpp │ │ └── BootConfig.cpp │ ├── Logger.hpp │ ├── ExponentialBackoffTimer.hpp │ ├── Timer.hpp │ ├── Logger.cpp │ ├── Strings.hpp │ ├── Blinker.cpp │ ├── Datatypes │ │ ├── Callbacks.hpp │ │ ├── Interface.cpp │ │ ├── ConfigStruct.hpp │ │ └── Interface.hpp │ ├── Timer.cpp │ ├── Constants.hpp │ ├── Config.hpp │ ├── ExponentialBackoffTimer.cpp │ ├── Limits.hpp │ └── Config.cpp ├── HomieEvent.hpp ├── SendingPromise.hpp ├── HomieSetting.hpp ├── HomieNode.cpp ├── HomieSetting.cpp ├── SendingPromise.cpp ├── Homie.hpp ├── HomieNode.hpp └── Homie.cpp ├── .circleci ├── assets │ ├── id_rsa.enc │ ├── circleci.ignore.yml │ ├── mkdocs.default.yml │ ├── docs_index_template.html │ └── generate_docs.py └── config.yml ├── homie-esp8266.cppcheck ├── Makefile ├── .editorconfig ├── data └── homie │ ├── example.config.json │ └── README.md ├── library.properties ├── examples ├── Broadcast │ └── Broadcast.ino ├── GlobalInputHandler │ └── GlobalInputHandler.ino ├── LightOnOff │ └── LightOnOff.ino ├── IteadSonoff │ └── IteadSonoff.ino ├── DoorSensor │ └── DoorSensor.ino ├── TemperatureSensor │ └── TemperatureSensor.ino ├── LedStrip │ └── LedStrip.ino ├── CustomSettings │ └── CustomSettings.ino ├── HookToEvents │ └── HookToEvents.ino ├── IteadSonoffButton │ └── IteadSonoffButton.ino └── SonoffDualShutters │ └── SonoffDualShutters.ino ├── .github └── ISSUE_TEMPLATE.md ├── LICENSE ├── library.json ├── keywords.txt ├── mkdocs.yml └── README.md /docs/index.md: -------------------------------------------------------------------------------- 1 | Welcome on the Homie for ESP8266 docs. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Output of mkdocs 2 | /site/ 3 | 4 | /config.json 5 | -------------------------------------------------------------------------------- /scripts/ota_updater/requirements.txt: -------------------------------------------------------------------------------- 1 | paho-mqtt >1.2.3,<=1.3.0 2 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenwest/homie-esp8266/develop/banner.png -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenwest/homie-esp8266/develop/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenwest/homie-esp8266/develop/docs/assets/github.png -------------------------------------------------------------------------------- /docs/assets/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenwest/homie-esp8266/develop/docs/assets/youtube.png -------------------------------------------------------------------------------- /docs/assets/led_mqtt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenwest/homie-esp8266/develop/docs/assets/led_mqtt.gif -------------------------------------------------------------------------------- /docs/assets/led_solid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenwest/homie-esp8266/develop/docs/assets/led_solid.gif -------------------------------------------------------------------------------- /docs/assets/led_wifi.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenwest/homie-esp8266/develop/docs/assets/led_wifi.gif -------------------------------------------------------------------------------- /src/HomieRange.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct HomieRange { 4 | bool isRange; 5 | uint16_t index; 6 | }; 7 | -------------------------------------------------------------------------------- /.circleci/assets/id_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenwest/homie-esp8266/develop/.circleci/assets/id_rsa.enc -------------------------------------------------------------------------------- /src/Homie.h: -------------------------------------------------------------------------------- 1 | #ifndef SRC_HOMIE_H_ 2 | #define SRC_HOMIE_H_ 3 | 4 | #include "Homie.hpp" 5 | 6 | #endif // SRC_HOMIE_H_ 7 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Docs 2 | ==== 3 | 4 | Docs are available: 5 | 6 | * Locally at [index.md](index.md) 7 | * Online at http://marvinroger.github.io/homie-esp8266/ 8 | -------------------------------------------------------------------------------- /src/HomieBootMode.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | enum class HomieBootMode : uint8_t { 4 | UNDEFINED = 0, 5 | STANDALONE = 1, 6 | CONFIGURATION = 2, 7 | NORMAL = 3 8 | }; 9 | -------------------------------------------------------------------------------- /homie-esp8266.cppcheck: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | cpplint: 2 | cpplint --repository=. --recursive --filter=-whitespace/line_length,-legal/copyright,-runtime/printf,-build/include,-build/namespace,-runtime/int,-whitespace/comments,-runtime/threadsafe_fn ./src 3 | .PHONY: cpplint 4 | -------------------------------------------------------------------------------- /scripts/firmware_parser/README.md: -------------------------------------------------------------------------------- 1 | Script: Firmware parser 2 | ======================= 3 | 4 | This will allow you to get information about the binary firmware file. 5 | 6 | ## Usage 7 | 8 | `python ./firmware_parser.py ~/firmware.bin` 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | [keywords.txt] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.circleci/assets/circleci.ignore.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/code 5 | docker: 6 | - image: circleci/python:2.7 7 | branches: 8 | ignore: 9 | - gh-pages 10 | steps: 11 | - checkout 12 | -------------------------------------------------------------------------------- /data/homie/example.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kitchen light", 3 | "wifi": { 4 | "ssid": "Network_1", 5 | "password": "I'm a Wi-Fi password!" 6 | }, 7 | "mqtt": { 8 | "host": "192.168.1.20", 9 | "port": 1883 10 | }, 11 | "ota": { 12 | "enabled": false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/StreamingOperator.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | template 4 | inline Print &operator <<(Print &stream, T arg) 5 | { stream.print(arg); return stream; } 6 | 7 | enum _EndLineCode { endl }; 8 | 9 | inline Print &operator <<(Print &stream, _EndLineCode arg) 10 | { stream.println(); return stream; } 11 | -------------------------------------------------------------------------------- /src/Homie/Uptime.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | namespace HomieInternals { 6 | class Uptime { 7 | public: 8 | Uptime(); 9 | void update(); 10 | uint64_t getSeconds() const; 11 | 12 | private: 13 | uint64_t _milliseconds; 14 | uint32_t _lastTick; 15 | }; 16 | } // namespace HomieInternals 17 | -------------------------------------------------------------------------------- /docs/advanced-usage/branding.md: -------------------------------------------------------------------------------- 1 | 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: 2 | 3 | ```c++ 4 | void setup() { 5 | Homie_setBrand("MyIoTSystem"); // before Homie.setup() 6 | // ... 7 | } 8 | ``` 9 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=Homie 2 | version=2.0.0 3 | author=Marvin Roger 4 | maintainer=Marvin Roger 5 | sentence=ESP8266 framework for Homie, a lightweight MQTT convention for the IoT 6 | paragraph=Like this project? Please star it on GitHub! 7 | category=Device Control 8 | url=https://github.com/marvinroger/homie-esp8266 9 | architectures=esp8266 10 | -------------------------------------------------------------------------------- /src/Homie/Uptime.cpp: -------------------------------------------------------------------------------- 1 | #include "Uptime.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | Uptime::Uptime() 6 | : _milliseconds(0) 7 | , _lastTick(0) { 8 | } 9 | 10 | void Uptime::update() { 11 | uint32_t now = millis(); 12 | _milliseconds += (now - _lastTick); 13 | _lastTick = now; 14 | } 15 | 16 | uint64_t Uptime::getSeconds() const { 17 | return (_milliseconds / 1000ULL); 18 | } 19 | -------------------------------------------------------------------------------- /src/Homie/Utils/DeviceId.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | #include 6 | 7 | #include "../Limits.hpp" 8 | 9 | namespace HomieInternals { 10 | class DeviceId { 11 | public: 12 | static void generate(); 13 | static const char* get(); 14 | 15 | private: 16 | static char _deviceId[MAX_MAC_STRING_LENGTH + 1]; 17 | }; 18 | } // namespace HomieInternals 19 | -------------------------------------------------------------------------------- /src/Homie/Blinker.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Datatypes/Interface.hpp" 5 | 6 | namespace HomieInternals { 7 | class Blinker { 8 | public: 9 | Blinker(); 10 | void start(float blinkPace); 11 | void stop(); 12 | 13 | private: 14 | Ticker _ticker; 15 | float _lastBlinkPace; 16 | 17 | static void _tick(uint8_t pin); 18 | }; 19 | } // namespace HomieInternals 20 | -------------------------------------------------------------------------------- /docs/advanced-usage/broadcast.md: -------------------------------------------------------------------------------- 1 | Your device can react to Homie broadcasts. To do that, you can use a broadcast handler: 2 | 3 | ```c++ 4 | bool broadcastHandler(const String& level, const String& value) { 5 | Serial << "Received broadcast level " << level << ": " << value << endl; 6 | return true; 7 | } 8 | 9 | void setup() { 10 | Homie.setBroadcastHandler(broadcastHandler); // before Homie.setup() 11 | // ... 12 | } 13 | ``` 14 | -------------------------------------------------------------------------------- /src/Homie/Utils/DeviceId.cpp: -------------------------------------------------------------------------------- 1 | #include "DeviceId.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | char DeviceId::_deviceId[]; // need to define the static variable 6 | 7 | void DeviceId::generate() { 8 | uint8_t mac[6]; 9 | WiFi.macAddress(mac); 10 | 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]); 11 | } 12 | 13 | const char* DeviceId::get() { 14 | return DeviceId::_deviceId; 15 | } 16 | -------------------------------------------------------------------------------- /docs/advanced-usage/ui-bundle.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | Indeed, you can serve the [configuration UI](http://setup.homie-esp8266.marvinroger.fr/) directly from your ESP8266. See [the data/homie folder](https://github.com/marvinroger/homie-esp8266/tree/develop/data/homie). 4 | -------------------------------------------------------------------------------- /examples/Broadcast/Broadcast.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | bool broadcastHandler(const String& level, const String& value) { 4 | Homie.getLogger() << "Received broadcast level " << level << ": " << value << endl; 5 | return true; 6 | } 7 | 8 | void setup() { 9 | Serial.begin(115200); 10 | Serial << endl << endl; 11 | Homie_setFirmware("broadcast-test", "1.0.0"); 12 | Homie.setBroadcastHandler(broadcastHandler); 13 | 14 | Homie.setup(); 15 | } 16 | 17 | void loop() { 18 | Homie.loop(); 19 | } 20 | -------------------------------------------------------------------------------- /src/Homie/Boot/BootStandalone.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | #include 6 | #include "../../StreamingOperator.hpp" 7 | #include "Boot.hpp" 8 | 9 | namespace HomieInternals { 10 | class BootStandalone : public Boot { 11 | public: 12 | BootStandalone(); 13 | ~BootStandalone(); 14 | void setup(); 15 | void loop(); 16 | 17 | private: 18 | bool _flaggedForConfig; 19 | Bounce _resetDebouncer; 20 | 21 | void _handleReset(); 22 | }; 23 | } // namespace HomieInternals 24 | -------------------------------------------------------------------------------- /src/Homie/Logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | namespace HomieInternals { 6 | class HomieClass; 7 | class Logger : public Print { 8 | friend HomieClass; 9 | 10 | public: 11 | Logger(); 12 | virtual size_t write(uint8_t character); 13 | virtual size_t write(const uint8_t* buffer, size_t size); 14 | 15 | private: 16 | void setPrinter(Print* printer); 17 | void setLogging(bool enable); 18 | 19 | bool _loggingEnabled; 20 | Print* _printer; 21 | }; 22 | } // namespace HomieInternals 23 | -------------------------------------------------------------------------------- /src/Homie/Boot/Boot.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | #include 6 | #include "../../StreamingOperator.hpp" 7 | #include "../Datatypes/Interface.hpp" 8 | #include "../Constants.hpp" 9 | #include "../Limits.hpp" 10 | #include "../Utils/Helpers.hpp" 11 | 12 | namespace HomieInternals { 13 | class Boot { 14 | public: 15 | explicit Boot(const char* name); 16 | virtual void setup(); 17 | virtual void loop(); 18 | 19 | protected: 20 | const char* _name; 21 | }; 22 | } // namespace HomieInternals 23 | -------------------------------------------------------------------------------- /src/Homie/ExponentialBackoffTimer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Timer.hpp" 4 | #include "Datatypes/Interface.hpp" 5 | 6 | namespace HomieInternals { 7 | class ExponentialBackoffTimer { 8 | public: 9 | ExponentialBackoffTimer(uint16_t initialInterval, uint8_t maxBackoff); 10 | void activate(); 11 | bool check(); 12 | void deactivate(); 13 | bool isActive() const; 14 | 15 | private: 16 | Timer _timer; 17 | 18 | uint16_t _initialInterval; 19 | uint8_t _maxBackoff; 20 | uint8_t _retryCount; 21 | }; 22 | } // namespace HomieInternals 23 | -------------------------------------------------------------------------------- /src/Homie/Timer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | namespace HomieInternals { 6 | class Timer { 7 | public: 8 | Timer(); 9 | void setInterval(uint32_t interval, bool tickAtBeginning = true); 10 | uint32_t getInterval(); 11 | bool check() const; 12 | void tick(); 13 | void reset(); 14 | void activate(); 15 | void deactivate(); 16 | bool isActive() const; 17 | 18 | private: 19 | uint32_t _initialTime; 20 | uint32_t _interval; 21 | bool _tickAtBeginning; 22 | bool _active; 23 | }; 24 | } // namespace HomieInternals 25 | -------------------------------------------------------------------------------- /src/Homie/Logger.cpp: -------------------------------------------------------------------------------- 1 | #include "Logger.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | Logger::Logger() 6 | : _loggingEnabled(true) 7 | , _printer(&Serial) { 8 | } 9 | 10 | void Logger::setLogging(bool enable) { 11 | _loggingEnabled = enable; 12 | } 13 | 14 | void Logger::setPrinter(Print* printer) { 15 | _printer = printer; 16 | } 17 | 18 | size_t Logger::write(uint8_t character) { 19 | if (_loggingEnabled) _printer->write(character); 20 | } 21 | 22 | size_t Logger::write(const uint8_t* buffer, size_t size) { 23 | if (_loggingEnabled) _printer->write(buffer, size); 24 | } 25 | -------------------------------------------------------------------------------- /docs/advanced-usage/streaming-operator.md: -------------------------------------------------------------------------------- 1 | Homie for ESP8266 includes a nice streaming operator to interact with `Print` objects. 2 | 3 | Imagine the following code: 4 | 5 | ```c++ 6 | int temperature = 32; 7 | Homie.getLogger().print("The current temperature is "); 8 | Homie.getLogger().print(temperature); 9 | Homie.getLogger().println(" °C."); 10 | ``` 11 | 12 | With the streaming operator, the following code will do exactly the same thing, without performance penalties: 13 | 14 | ```c++ 15 | int temperature = 32; 16 | Homie.getLogger() << "The current temperature is " << temperature << " °C." << endl; 17 | ``` 18 | -------------------------------------------------------------------------------- /src/Homie/Strings.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace HomieInternals { 4 | // config mode 5 | 6 | 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"; 7 | const char PROGMEM_CONFIG_APPLICATION_JSON[] PROGMEM = "application/json"; 8 | const char PROGMEM_CONFIG_JSON_FAILURE_BEGINNING[] PROGMEM = "{\"success\":false,\"error\":\""; 9 | const char PROGMEM_CONFIG_NETWORKS_FAILURE[] PROGMEM = "{\"error\": \"Initial Wi-Fi scan not finished yet\"}"; 10 | } 11 | -------------------------------------------------------------------------------- /data/homie/README.md: -------------------------------------------------------------------------------- 1 | `data/homie` folder 2 | =================== 3 | 4 | This folder contains the data you can upload to the SPIFFS of your ESP8266. 5 | This is optional. 6 | 7 | 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: 8 | 9 | 1. The `config.json` file, if you want to bypass the `configuration` mode. 10 | 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. 11 | -------------------------------------------------------------------------------- /docs/quickstart/what-is-it.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | You guessed it, the purpose of Homie for ESP8266 is to simplify the development of connected objects. 4 | -------------------------------------------------------------------------------- /src/Homie/Utils/Helpers.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | #include "../../StreamingOperator.hpp" 5 | #include "../Limits.hpp" 6 | #include 7 | 8 | namespace HomieInternals { 9 | class Helpers { 10 | public: 11 | static void abort(const String& message); 12 | static uint8_t rssiToPercentage(int32_t rssi); 13 | static void stringToBytes(const char* str, char sep, byte* bytes, int maxBytes, int base); 14 | static bool validateMacAddress(const char* mac); 15 | static bool validateMd5(const char* md5); 16 | static std::unique_ptr cloneString(const String& string); 17 | }; 18 | } // namespace HomieInternals 19 | -------------------------------------------------------------------------------- /src/Homie/Blinker.cpp: -------------------------------------------------------------------------------- 1 | #include "Blinker.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | Blinker::Blinker() 6 | : _lastBlinkPace(0) { 7 | } 8 | 9 | void Blinker::start(float blinkPace) { 10 | if (_lastBlinkPace != blinkPace) { 11 | _ticker.attach(blinkPace, _tick, Interface::get().led.pin); 12 | _lastBlinkPace = blinkPace; 13 | } 14 | } 15 | 16 | void Blinker::stop() { 17 | if (_lastBlinkPace != 0) { 18 | _ticker.detach(); 19 | _lastBlinkPace = 0; 20 | digitalWrite(Interface::get().led.pin, !Interface::get().led.on); 21 | } 22 | } 23 | 24 | void Blinker::_tick(uint8_t pin) { 25 | digitalWrite(pin, !digitalRead(pin)); 26 | } 27 | -------------------------------------------------------------------------------- /docs/others/limitations-and-known-issues.md: -------------------------------------------------------------------------------- 1 | # SSL support 2 | 3 | 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. 4 | 5 | # ADC readings 6 | 7 | [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. 8 | 9 | # Wi-Fi connection 10 | 11 | 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. 12 | -------------------------------------------------------------------------------- /examples/GlobalInputHandler/GlobalInputHandler.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | HomieNode lightNode("light", "switch"); 4 | 5 | bool globalInputHandler(const HomieNode& node, const String& property, const HomieRange& range, const String& value) { 6 | Homie.getLogger() << "Received on node " << node.getId() << ": " << property << " = " << value << endl; 7 | return true; 8 | } 9 | 10 | void setup() { 11 | Serial.begin(115200); 12 | Serial << endl << endl; 13 | Homie_setFirmware("global-input-handler", "1.0.0"); 14 | Homie.setGlobalInputHandler(globalInputHandler); 15 | 16 | lightNode.advertise("on").settable(); 17 | 18 | Homie.setup(); 19 | } 20 | 21 | void loop() { 22 | Homie.loop(); 23 | } 24 | -------------------------------------------------------------------------------- /docs/advanced-usage/standalone-mode.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | To enable this mode, call `Homie.setStandalone()`: 4 | 5 | ```c++ 6 | void setup() { 7 | Homie.setStandalone(); // before Homie.setup() 8 | // ... 9 | } 10 | ``` 11 | 12 | To actually configure the device, you have to reset it, the same way you would to go from `normal` mode to `configuration` mode. 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | The issue tracker is a great place to ask for enhancements and to report bugs. 2 | 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). 3 | 4 | Before submitting your issue, make sure: 5 | 6 | - [ ] 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) 7 | - [ ] 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 8 | 9 | Thanks! 10 | -------------------------------------------------------------------------------- /src/Homie/Boot/Boot.cpp: -------------------------------------------------------------------------------- 1 | #include "Boot.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | Boot::Boot(const char* name) 6 | : _name(name) { 7 | } 8 | 9 | 10 | void Boot::setup() { 11 | if (Interface::get().led.enabled) { 12 | pinMode(Interface::get().led.pin, OUTPUT); 13 | digitalWrite(Interface::get().led.pin, !Interface::get().led.on); 14 | } 15 | 16 | WiFi.persistent(true); // Persist data on SDK as it seems Wi-Fi connection is faster 17 | 18 | Interface::get().getLogger() << F("💡 Firmware ") << Interface::get().firmware.name << F(" (") << Interface::get().firmware.version << F(")") << endl; 19 | Interface::get().getLogger() << F("🔌 Booting into ") << _name << F(" mode 🔌") << endl; 20 | } 21 | 22 | void Boot::loop() { 23 | } 24 | -------------------------------------------------------------------------------- /docs/advanced-usage/logging.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | ```c++ 4 | void setup() { 5 | Homie.disableLogging(); // before Homie.setup() 6 | // ... 7 | } 8 | ``` 9 | 10 | !!! warning 11 | It's up to you to call `Serial.begin();`, whether logging is enabled or not. 12 | 13 | You can also change the `Print` instance to log to: 14 | 15 | ```c++ 16 | void setup() { 17 | Homie.setLoggingPrinter(&Serial2); // before Homie.setup() 18 | // ... 19 | } 20 | ``` 21 | 22 | You can use the logger from your code with the `getLogger()` client: 23 | 24 | ```c++ 25 | Homie.getLogger() << "Hey!" << endl; 26 | ``` 27 | -------------------------------------------------------------------------------- /.circleci/assets/mkdocs.default.yml: -------------------------------------------------------------------------------- 1 | site_name: Homie for ESP8266 2 | repo_name: 'marvinroger/homie-esp8266' 3 | repo_url: 'https://github.com/marvinroger/homie-esp8266' 4 | 5 | theme: material 6 | extra: 7 | palette: 8 | primary: red 9 | accent: red 10 | 11 | markdown_extensions: 12 | - meta 13 | - footnotes 14 | - codehilite 15 | - admonition 16 | - toc(permalink=true) 17 | - pymdownx.arithmatex 18 | - pymdownx.betterem(smart_enable=all) 19 | - pymdownx.caret 20 | - pymdownx.critic 21 | - pymdownx.emoji: 22 | emoji_generator: !!python/name:pymdownx.emoji.to_svg 23 | - pymdownx.inlinehilite 24 | - pymdownx.magiclink 25 | - pymdownx.mark 26 | - pymdownx.smartsymbols 27 | - pymdownx.superfences 28 | - pymdownx.tasklist(custom_checkbox=true) 29 | - pymdownx.tilde 30 | -------------------------------------------------------------------------------- /src/Homie/Datatypes/Callbacks.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../../HomieEvent.hpp" 5 | #include "../../HomieRange.hpp" 6 | 7 | class HomieNode; 8 | 9 | namespace HomieInternals { 10 | typedef std::function OperationFunction; 11 | 12 | typedef std::function GlobalInputHandler; 13 | typedef std::function NodeInputHandler; 14 | typedef std::function PropertyInputHandler; 15 | 16 | typedef std::function EventHandler; 17 | 18 | typedef std::function BroadcastHandler; 19 | } // namespace HomieInternals 20 | -------------------------------------------------------------------------------- /src/Homie/Datatypes/Interface.cpp: -------------------------------------------------------------------------------- 1 | #include "Interface.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | InterfaceData Interface::_interface; // need to define the static variable 6 | 7 | InterfaceData::InterfaceData() 8 | : brand{'\0'} 9 | , bootMode{HomieBootMode::UNDEFINED} 10 | , configurationAp { .secured = false, .password = {'\0'} } 11 | , firmware { .name = {'\0'}, .version = {'\0'} } 12 | , led { .enabled = false, .pin = 0, .on = 0 } 13 | , reset { .enabled = false, .idle = false, .triggerPin = 0, .triggerState = 0, .triggerTime = 0, .flaggedBySketch = false } 14 | , flaggedForSleep{false} 15 | , event{} 16 | , ready{false} 17 | , _logger{nullptr} 18 | , _blinker{nullptr} 19 | , _config{nullptr} 20 | , _mqttClient{nullptr} 21 | , _sendingPromise{nullptr} { 22 | } 23 | 24 | InterfaceData& Interface::get() { 25 | return _interface; 26 | } 27 | -------------------------------------------------------------------------------- /examples/LightOnOff/LightOnOff.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | const int PIN_RELAY = 5; 4 | 5 | HomieNode lightNode("light", "switch"); 6 | 7 | bool lightOnHandler(const HomieRange& range, const String& value) { 8 | if (value != "true" && value != "false") return false; 9 | 10 | bool on = (value == "true"); 11 | digitalWrite(PIN_RELAY, on ? HIGH : LOW); 12 | lightNode.setProperty("on").send(value); 13 | Homie.getLogger() << "Light is " << (on ? "on" : "off") << endl; 14 | 15 | return true; 16 | } 17 | 18 | void setup() { 19 | Serial.begin(115200); 20 | Serial << endl << endl; 21 | pinMode(PIN_RELAY, OUTPUT); 22 | digitalWrite(PIN_RELAY, LOW); 23 | 24 | Homie_setFirmware("awesome-relay", "1.0.0"); 25 | 26 | lightNode.advertise("on").settable(lightOnHandler); 27 | 28 | Homie.setup(); 29 | } 30 | 31 | void loop() { 32 | Homie.loop(); 33 | } 34 | -------------------------------------------------------------------------------- /src/HomieEvent.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | enum class HomieEventType : uint8_t { 7 | STANDALONE_MODE = 1, 8 | CONFIGURATION_MODE, 9 | NORMAL_MODE, 10 | OTA_STARTED, 11 | OTA_PROGRESS, 12 | OTA_SUCCESSFUL, 13 | OTA_FAILED, 14 | ABOUT_TO_RESET, 15 | WIFI_CONNECTED, 16 | WIFI_DISCONNECTED, 17 | MQTT_READY, 18 | MQTT_DISCONNECTED, 19 | MQTT_PACKET_ACKNOWLEDGED, 20 | READY_TO_SLEEP 21 | }; 22 | 23 | struct HomieEvent { 24 | HomieEventType type; 25 | /* WIFI_CONNECTED */ 26 | IPAddress ip; 27 | IPAddress mask; 28 | IPAddress gateway; 29 | /* WIFI_DISCONNECTED */ 30 | WiFiDisconnectReason wifiReason; 31 | /* MQTT_DISCONNECTED */ 32 | AsyncMqttClientDisconnectReason mqttReason; 33 | /* MQTT_PACKET_ACKNOWLEDGED */ 34 | uint16_t packetId; 35 | /* OTA_PROGRESS */ 36 | size_t sizeDone; 37 | size_t sizeTotal; 38 | }; 39 | -------------------------------------------------------------------------------- /docs/advanced-usage/built-in-led.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | 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. 4 | 5 | ```c++ 6 | void setup() { 7 | Homie.disableLedFeedback(); // before Homie.setup() 8 | // ... 9 | } 10 | ``` 11 | 12 | You may, instead of completely disable the LED control, set a new LED to control: 13 | 14 | ```c++ 15 | void setup() { 16 | Homie.setLedPin(16, HIGH); // before Homie.setup() -- 2nd param is the state of the pin when the LED is o 17 | // ... 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/advanced-usage/deep-sleep.md: -------------------------------------------------------------------------------- 1 | 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()`. 2 | 3 | ```c++ 4 | #include 5 | 6 | void onHomieEvent(const HomieEvent& event) { 7 | switch(event.type) { 8 | case HomieEventType::MQTT_READY: 9 | Homie.getLogger() << "MQTT connected, preparing for deep sleep..." << endl; 10 | Homie.prepareToSleep(); 11 | break; 12 | case HomieEventType::READY_TO_SLEEP: 13 | Homie.getLogger() << "Ready to sleep" << endl; 14 | ESP.deepSleep(); 15 | break; 16 | } 17 | } 18 | 19 | void setup() { 20 | Serial.begin(115200); 21 | Serial << endl << endl; 22 | Homie.onEvent(onHomieEvent); 23 | Homie.setup(); 24 | } 25 | 26 | void loop() { 27 | Homie.loop(); 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /src/Homie/Utils/Validation.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | #include 6 | #include 7 | #include "Helpers.hpp" 8 | #include "../Limits.hpp" 9 | #include "../../HomieSetting.hpp" 10 | 11 | namespace HomieInternals { 12 | struct ConfigValidationResult { 13 | bool valid; 14 | String reason; 15 | }; 16 | 17 | class Validation { 18 | public: 19 | static ConfigValidationResult validateConfig(const JsonObject& object); 20 | 21 | private: 22 | static ConfigValidationResult _validateConfigRoot(const JsonObject& object); 23 | static ConfigValidationResult _validateConfigWifi(const JsonObject& object); 24 | static ConfigValidationResult _validateConfigMqtt(const JsonObject& object); 25 | static ConfigValidationResult _validateConfigOta(const JsonObject& object); 26 | static ConfigValidationResult _validateConfigSettings(const JsonObject& object); 27 | }; 28 | } // namespace HomieInternals 29 | -------------------------------------------------------------------------------- /docs/advanced-usage/range-properties.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | ```c++ 4 | HomieNode stripNode("strip", "strip"); 5 | 6 | bool ledHandler(const HomieRange& range, const String& value) { 7 | Homie.getLogger() << "LED " << range.index << " set to " << value << endl; 8 | 9 | // Now, let's update the actual state of the given led 10 | stripNode.setProperty("led").setRange(range).send(value); 11 | } 12 | 13 | void setup() { 14 | stripNode.advertiseRange("led", 1, 100).settable(ledHandler); 15 | // before Homie.setup() 16 | } 17 | ``` 18 | 19 | See the following example for a concrete use case: 20 | 21 | [![GitHub logo](../assets/github.png) LedStrip](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/LedStrip/LedStrip.ino) 22 | -------------------------------------------------------------------------------- /examples/IteadSonoff/IteadSonoff.ino: -------------------------------------------------------------------------------- 1 | /* WARNING: untested */ 2 | 3 | #include 4 | 5 | const int PIN_RELAY = 12; 6 | const int PIN_LED = 13; 7 | const int PIN_BUTTON = 0; 8 | 9 | HomieNode switchNode("switch", "switch"); 10 | 11 | bool switchOnHandler(const HomieRange& range, const String& value) { 12 | if (value != "true" && value != "false") return false; 13 | 14 | bool on = (value == "true"); 15 | digitalWrite(PIN_RELAY, on ? HIGH : LOW); 16 | switchNode.setProperty("on").send(value); 17 | Homie.getLogger() << "Switch is " << (on ? "on" : "off") << endl; 18 | 19 | return true; 20 | } 21 | 22 | void setup() { 23 | Serial.begin(115200); 24 | Serial << endl << endl; 25 | pinMode(PIN_RELAY, OUTPUT); 26 | digitalWrite(PIN_RELAY, LOW); 27 | 28 | Homie_setFirmware("itead-sonoff", "1.0.0"); 29 | Homie.setLedPin(PIN_LED, LOW).setResetTrigger(PIN_BUTTON, LOW, 5000); 30 | 31 | switchNode.advertise("on").settable(switchOnHandler); 32 | 33 | Homie.setup(); 34 | } 35 | 36 | void loop() { 37 | Homie.loop(); 38 | } 39 | -------------------------------------------------------------------------------- /src/Homie/Timer.cpp: -------------------------------------------------------------------------------- 1 | #include "Timer.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | Timer::Timer() 6 | : _initialTime(0) 7 | , _interval(0) 8 | , _tickAtBeginning(false) 9 | , _active(true) { 10 | } 11 | 12 | void Timer::setInterval(uint32_t interval, bool tickAtBeginning) { 13 | _interval = interval; 14 | _tickAtBeginning = tickAtBeginning; 15 | 16 | this->reset(); 17 | } 18 | 19 | bool Timer::check() const { 20 | if (!_active) return false; 21 | 22 | if (_tickAtBeginning && _initialTime == 0) return true; 23 | if (millis() - _initialTime >= _interval) return true; 24 | 25 | return false; 26 | } 27 | 28 | void Timer::reset() { 29 | if (_tickAtBeginning) { 30 | _initialTime = 0; 31 | } else { 32 | this->tick(); 33 | } 34 | } 35 | 36 | void Timer::tick() { 37 | _initialTime = millis(); 38 | } 39 | 40 | void Timer::activate() { 41 | _active = true; 42 | } 43 | 44 | void Timer::deactivate() { 45 | _active = false; 46 | reset(); 47 | } 48 | 49 | bool Timer::isActive() const { 50 | return _active; 51 | } 52 | -------------------------------------------------------------------------------- /examples/DoorSensor/DoorSensor.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | const int PIN_DOOR = 16; 4 | 5 | Bounce debouncer = Bounce(); // Bounce is built into Homie, so you can use it without including it first 6 | int lastDoorValue = -1; 7 | 8 | HomieNode doorNode("door", "door"); 9 | 10 | void loopHandler() { 11 | int doorValue = debouncer.read(); 12 | 13 | if (doorValue != lastDoorValue) { 14 | Homie.getLogger() << "Door is now " << (doorValue ? "open" : "close") << endl; 15 | 16 | doorNode.setProperty("open").send(doorValue ? "true" : "false"); 17 | lastDoorValue = doorValue; 18 | } 19 | } 20 | 21 | void setup() { 22 | Serial.begin(115200); 23 | Serial << endl << endl; 24 | pinMode(PIN_DOOR, INPUT); 25 | digitalWrite(PIN_DOOR, HIGH); 26 | debouncer.attach(PIN_DOOR); 27 | debouncer.interval(50); 28 | 29 | Homie_setFirmware("awesome-door", "1.0.0"); 30 | Homie.setLoopFunction(loopHandler); 31 | 32 | doorNode.advertise("open"); 33 | 34 | Homie.setup(); 35 | } 36 | 37 | void loop() { 38 | Homie.loop(); 39 | debouncer.update(); 40 | } 41 | -------------------------------------------------------------------------------- /src/Homie/Datatypes/ConfigStruct.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../Constants.hpp" 4 | #include "../Limits.hpp" 5 | 6 | namespace HomieInternals { 7 | struct ConfigStruct { 8 | char name[MAX_FRIENDLY_NAME_LENGTH]; 9 | char deviceId[MAX_DEVICE_ID_LENGTH]; 10 | 11 | struct WiFi { 12 | char ssid[MAX_WIFI_SSID_LENGTH]; 13 | char password[MAX_WIFI_PASSWORD_LENGTH]; 14 | char bssid[MAX_MAC_STRING_LENGTH + 6]; 15 | uint16_t channel; 16 | char ip[MAX_IP_STRING_LENGTH]; 17 | char mask[MAX_IP_STRING_LENGTH]; 18 | char gw[MAX_IP_STRING_LENGTH]; 19 | char dns1[MAX_IP_STRING_LENGTH]; 20 | char dns2[MAX_IP_STRING_LENGTH]; 21 | } wifi; 22 | 23 | struct MQTT { 24 | struct Server { 25 | char host[MAX_HOSTNAME_LENGTH]; 26 | uint16_t port; 27 | } server; 28 | char baseTopic[MAX_MQTT_BASE_TOPIC_LENGTH]; 29 | bool auth; 30 | char username[MAX_MQTT_CREDS_LENGTH]; 31 | char password[MAX_MQTT_CREDS_LENGTH]; 32 | } mqtt; 33 | 34 | struct OTA { 35 | bool enabled; 36 | } ota; 37 | }; 38 | } // namespace HomieInternals 39 | -------------------------------------------------------------------------------- /docs/others/upgrade-guide-from-v1-to-v2.md: -------------------------------------------------------------------------------- 1 | This is an upgrade guide to upgrade your Homie devices from v1 to v2. 2 | 3 | ## New convention 4 | 5 | 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). 6 | 7 | ## API changes in the sketch 8 | 9 | 1. `Homie.setFirmware(name, version)` must be replaced by `Homie_setFirmware(name, version)` 10 | 2. `Homie.setBrand(brand)` must be replaced by `Homie_setBrand(brand)` 11 | 3. `Homie.registerNode()` must be removed, nodes are now automagically registered 12 | 4. If you've enabled Serial logging, `Serial.begin()` must be called explicitely in your sketch 13 | 5. Remove the `HOMIE_OTA_MODE` in your event handler, if you have one 14 | 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. 15 | 7. TODO 16 | -------------------------------------------------------------------------------- /examples/TemperatureSensor/TemperatureSensor.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | const int TEMPERATURE_INTERVAL = 300; 4 | 5 | unsigned long lastTemperatureSent = 0; 6 | 7 | HomieNode temperatureNode("temperature", "temperature"); 8 | 9 | void setupHandler() { 10 | temperatureNode.setProperty("unit").send("c"); 11 | } 12 | 13 | void loopHandler() { 14 | if (millis() - lastTemperatureSent >= TEMPERATURE_INTERVAL * 1000UL || lastTemperatureSent == 0) { 15 | float temperature = 22; // Fake temperature here, for the example 16 | Homie.getLogger() << "Temperature: " << temperature << " °C" << endl; 17 | temperatureNode.setProperty("degrees").send(String(temperature)); 18 | lastTemperatureSent = millis(); 19 | } 20 | } 21 | 22 | void setup() { 23 | Serial.begin(115200); 24 | Serial << endl << endl; 25 | Homie_setFirmware("awesome-temperature", "1.0.0"); 26 | Homie.setSetupFunction(setupHandler).setLoopFunction(loopHandler); 27 | 28 | temperatureNode.advertise("unit"); 29 | temperatureNode.advertise("degrees"); 30 | 31 | Homie.setup(); 32 | } 33 | 34 | void loop() { 35 | Homie.loop(); 36 | } 37 | -------------------------------------------------------------------------------- /src/Homie/Constants.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace HomieInternals { 6 | const char HOMIE_VERSION[] = "2.0.0"; 7 | const char HOMIE_ESP8266_VERSION[] = "2.0.0"; 8 | 9 | const IPAddress ACCESS_POINT_IP(192, 168, 123, 1); 10 | 11 | const uint16_t DEFAULT_MQTT_PORT = 1883; 12 | const char DEFAULT_MQTT_BASE_TOPIC[] = "homie/"; 13 | 14 | const uint8_t DEFAULT_RESET_PIN = 0; // == D3 on nodeMCU 15 | const uint8_t DEFAULT_RESET_STATE = LOW; 16 | const uint16_t DEFAULT_RESET_TIME = 5 * 1000; 17 | 18 | const char DEFAULT_BRAND[] = "Homie"; 19 | 20 | const uint16_t CONFIG_SCAN_INTERVAL = 20 * 1000; 21 | const uint32_t STATS_SEND_INTERVAL = 1 * 60 * 1000; 22 | const uint16_t MQTT_RECONNECT_INITIAL_INTERVAL = 1000; 23 | const uint8_t MQTT_RECONNECT_MAX_BACKOFF = 6; 24 | 25 | const float LED_WIFI_DELAY = 1; 26 | const float LED_MQTT_DELAY = 0.2; 27 | 28 | const char CONFIG_UI_BUNDLE_PATH[] = "/homie/ui_bundle.gz"; 29 | const char CONFIG_NEXT_BOOT_MODE_FILE_PATH[] = "/homie/NEXTMODE"; 30 | const char CONFIG_FILE_PATH[] = "/homie/config.json"; 31 | } // namespace HomieInternals 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Marvin Roger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Homie", 3 | "version": "2.0.0", 4 | "keywords": "iot, home, automation, mqtt, esp8266, async, sensor", 5 | "description": "ESP8266 framework for Homie, a lightweight MQTT convention for the IoT", 6 | "homepage": "http://marvinroger.github.io/homie-esp8266/", 7 | "license": "MIT", 8 | "authors": 9 | { 10 | "name": "Marvin Roger", 11 | "url": "https://www.marvinroger.fr", 12 | "maintainer": true 13 | }, 14 | "repository": 15 | { 16 | "type": "git", 17 | "url": "https://github.com/marvinroger/homie-esp8266.git", 18 | "branch": "master" 19 | }, 20 | "frameworks": "arduino", 21 | "platforms": "espressif8266", 22 | "dependencies": [ 23 | { 24 | "name": "ArduinoJson", 25 | "version": "^5.10.0" 26 | }, 27 | { 28 | "name": "AsyncMqttClient", 29 | "version": "^0.8.0" 30 | }, 31 | { 32 | "name": "Bounce2", 33 | "version": "^2.1.0" 34 | } 35 | ], 36 | "export": { 37 | "include": [ 38 | "LICENSE", 39 | "keywords.txt", 40 | "src/*", 41 | "examples/*" 42 | ] 43 | }, 44 | "examples": "examples/*.ino" 45 | } 46 | -------------------------------------------------------------------------------- /src/SendingPromise.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | #include "StreamingOperator.hpp" 5 | #include "Homie/Datatypes/Interface.hpp" 6 | #include "HomieRange.hpp" 7 | 8 | class HomieNode; 9 | 10 | namespace HomieInternals { 11 | class SendingPromise { 12 | friend ::HomieNode; 13 | 14 | public: 15 | SendingPromise(); 16 | SendingPromise& setQos(uint8_t qos); 17 | SendingPromise& setRetained(bool retained); 18 | SendingPromise& overwriteSetter(bool overwrite); 19 | SendingPromise& setRange(const HomieRange& range); 20 | SendingPromise& setRange(uint16_t rangeIndex); 21 | uint16_t send(const String& value); 22 | 23 | private: 24 | SendingPromise& setNode(const HomieNode& node); 25 | SendingPromise& setProperty(const String& property); 26 | const HomieNode* getNode() const; 27 | const String* getProperty() const; 28 | uint8_t getQos() const; 29 | HomieRange getRange() const; 30 | bool isRetained() const; 31 | bool doesOverwriteSetter() const; 32 | 33 | const HomieNode* _node; 34 | const String* _property; 35 | uint8_t _qos; 36 | bool _retained; 37 | bool _overwriteSetter; 38 | HomieRange _range; 39 | }; 40 | } // namespace HomieInternals 41 | -------------------------------------------------------------------------------- /src/Homie/Config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | #include 6 | #include "FS.h" 7 | #include "Datatypes/Interface.hpp" 8 | #include "Datatypes/ConfigStruct.hpp" 9 | #include "Utils/DeviceId.hpp" 10 | #include "Utils/Validation.hpp" 11 | #include "Constants.hpp" 12 | #include "Limits.hpp" 13 | #include "../HomieBootMode.hpp" 14 | #include "../HomieSetting.hpp" 15 | #include "../StreamingOperator.hpp" 16 | 17 | namespace HomieInternals { 18 | class Config { 19 | public: 20 | Config(); 21 | bool load(); 22 | inline const ConfigStruct& get() const; 23 | char* getSafeConfigFile() const; 24 | void erase(); 25 | void setHomieBootModeOnNextBoot(HomieBootMode bootMode); 26 | HomieBootMode getHomieBootModeOnNextBoot(); 27 | void write(const JsonObject& config); 28 | bool patch(const char* patch); 29 | void log() const; // print the current config to log output 30 | bool isValid() const; 31 | 32 | private: 33 | ConfigStruct _configStruct; 34 | bool _spiffsBegan; 35 | bool _valid; 36 | 37 | bool _spiffsBegin(); 38 | }; 39 | 40 | const ConfigStruct& Config::get() const { 41 | return _configStruct; 42 | } 43 | } // namespace HomieInternals 44 | -------------------------------------------------------------------------------- /src/Homie/ExponentialBackoffTimer.cpp: -------------------------------------------------------------------------------- 1 | #include "ExponentialBackoffTimer.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | ExponentialBackoffTimer::ExponentialBackoffTimer(uint16_t initialInterval, uint8_t maxBackoff) 6 | : _timer(Timer()) 7 | , _initialInterval(initialInterval) 8 | , _maxBackoff(maxBackoff) 9 | , _retryCount(0) { 10 | _timer.deactivate(); 11 | } 12 | 13 | bool ExponentialBackoffTimer::check() { 14 | if (_timer.check()) { 15 | if (_retryCount != _maxBackoff) _retryCount++; 16 | 17 | uint32_t fixedDelay = pow(_retryCount, 2) * _initialInterval; 18 | uint32_t randomDifference = random(0, (fixedDelay / 10) + 1); 19 | uint32_t nextInterval = fixedDelay - randomDifference; 20 | 21 | _timer.setInterval(nextInterval, false); 22 | return true; 23 | } else { 24 | return false; 25 | } 26 | } 27 | 28 | void ExponentialBackoffTimer::activate() { 29 | if (_timer.isActive()) return; 30 | 31 | _timer.setInterval(_initialInterval, false); 32 | _timer.activate(); 33 | _retryCount = 1; 34 | } 35 | 36 | void ExponentialBackoffTimer::deactivate() { 37 | _timer.deactivate(); 38 | } 39 | 40 | bool ExponentialBackoffTimer::isActive() const { 41 | return _timer.isActive(); 42 | } 43 | -------------------------------------------------------------------------------- /examples/LedStrip/LedStrip.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | const unsigned char NUMBER_OF_LED = 4; 4 | const unsigned char LED_PINS[NUMBER_OF_LED] = { 16, 5, 4, 0 }; 5 | 6 | HomieNode stripNode("strip", "strip"); 7 | 8 | bool stripLedHandler(const HomieRange& range, const String& value) { 9 | if (!range.isRange) return false; // if it's not a range 10 | 11 | if (range.index < 1 || range.index > NUMBER_OF_LED) return false; // if it's not a valid range 12 | 13 | if (value != "on" && value != "off") return false; // if the value is not valid 14 | 15 | bool on = (value == "on"); 16 | 17 | digitalWrite(LED_PINS[range.index - 1], on ? HIGH : LOW); 18 | stripNode.setProperty("led").setRange(range).send(value); // Update the state of the led 19 | Homie.getLogger() << "Led " << range.index << " is " << value << endl; 20 | 21 | return true; 22 | } 23 | 24 | void setup() { 25 | for (int i = 0; i < NUMBER_OF_LED; i++) { 26 | pinMode(LED_PINS[i], OUTPUT); 27 | digitalWrite(LED_PINS[i], LOW); 28 | } 29 | 30 | Serial.begin(115200); 31 | Serial << endl << endl; 32 | 33 | Homie_setFirmware("awesome-ledstrip", "1.0.0"); 34 | 35 | stripNode.advertiseRange("led", 1, NUMBER_OF_LED).settable(stripLedHandler); 36 | 37 | Homie.setup(); 38 | } 39 | 40 | void loop() { 41 | Homie.loop(); 42 | } 43 | -------------------------------------------------------------------------------- /docs/others/community-projects.md: -------------------------------------------------------------------------------- 1 | This page lists the projects made by the community to work with Homie. 2 | 3 | # [jpmens/homie-ota](https://github.com/jpmens/homie-ota) 4 | 5 | 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. 6 | 7 | # [stufisher/homie-control](https://github.com/stufisher/homie-control) 8 | 9 | homie-control provides a web UI to manage Homie devices as well as a series of virtual python devices to allow extended functionality. 10 | 11 | Its lets you do useful things like: 12 | 13 | * Historically log device properties 14 | * Schedule changes in event properties (i.e. water your garden once a day) 15 | * Execute profiles of property values (i.e. turn a series of lights on and off simultaneously) 16 | * Trigger property changes based on: 17 | * When a network device is dis/connected (i.e. your phone joins your wifi, turn the lights on) 18 | * Sunset / rise 19 | * When another property changes 20 | -------------------------------------------------------------------------------- /docs/advanced-usage/resetting.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | This behavior is configurable: 4 | 5 | ```c++ 6 | void setup() { 7 | Homie.setResetTrigger(1, LOW, 2000); // before Homie.setup() 8 | // ... 9 | } 10 | ``` 11 | 12 | The device will now reset if pin `1` is `LOW` for `2000`ms. You can also disable completely this reset trigger: 13 | 14 | ```c++ 15 | void setup() { 16 | Homie.disableResetTrigger(); // before Homie.setup() 17 | // ... 18 | } 19 | ``` 20 | 21 | In addition, you can also trigger a device reset from your sketch: 22 | 23 | ```c++ 24 | void loop() { 25 | Homie.reset(); 26 | } 27 | ``` 28 | 29 | 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. 30 | 31 | ```c++ 32 | Homie.setIdle(false); 33 | ``` 34 | 35 | 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. 36 | -------------------------------------------------------------------------------- /docs/advanced-usage/magic-bytes.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | 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. 4 | 5 | Values are encoded as such within the firmware binary: 6 | 7 | Type | Left boundary | Value | Right boundary 8 | ---- | ------------- | ----- | -------------- 9 | Homie magic bytes | *None* | `0x25 0x48 0x4F 0x4D 0x49 0x45 0x5F 0x45 0x53 0x50 0x38 0x32 0x36 0x36 0x5F 0x46 0x57 0x25` | *None* 10 | Firmware name | `0xBF 0x84 0xE4 0x13 0x54` | **actual firmware name** | `0x93 0x44 0x6B 0xA7 0x75` 11 | Firmware version | `0x6A 0x3F 0x3E 0x0E 0xE1` | **actual firmware version** | `0xB0 0x30 0x48 0xD4 0x1A` 12 | Firmware brand (only present if `Homie_setBrand()` called, Homie otherwise) | `0xFB 0x2A 0xF5 0x68 0xC0` | **actual firmware brand** | `0x6E 0x2F 0x0F 0xEB 0x2D` 13 | 14 | See the following script for a concrete use case: 15 | 16 | [![GitHub logo](../assets/github.png) firmware_parser.py](https://github.com/marvinroger/homie-esp8266/blob/develop/scripts/firmware_parser) 17 | -------------------------------------------------------------------------------- /.circleci/assets/docs_index_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Homie for ESP8266 docs 5 | 6 | 7 | 8 | 60 | 61 | 62 | 63 |
64 |

Homie for ESP8266 docs

65 | 66 | $versions_html 67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/CustomSettings/CustomSettings.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | const int DEFAULT_TEMPERATURE_INTERVAL = 300; 4 | 5 | unsigned long lastTemperatureSent = 0; 6 | 7 | HomieNode temperatureNode("temperature", "temperature"); 8 | 9 | HomieSetting temperatureIntervalSetting("temperatureInterval", "The temperature interval in seconds"); 10 | 11 | void setupHandler() { 12 | temperatureNode.setProperty("unit").send("c"); 13 | } 14 | 15 | void loopHandler() { 16 | if (millis() - lastTemperatureSent >= temperatureIntervalSetting.get() * 1000UL || lastTemperatureSent == 0) { 17 | float temperature = 22; // Fake temperature here, for the example 18 | Homie.getLogger() << "Temperature: " << temperature << " °C" << endl; 19 | temperatureNode.setProperty("degrees").send(String(temperature)); 20 | lastTemperatureSent = millis(); 21 | } 22 | } 23 | 24 | void setup() { 25 | Serial.begin(115200); 26 | Serial << endl << endl; 27 | Homie_setFirmware("temperature-setting", "1.0.0"); 28 | Homie.setSetupFunction(setupHandler).setLoopFunction(loopHandler); 29 | 30 | temperatureNode.advertise("unit"); 31 | temperatureNode.advertise("degrees"); 32 | 33 | temperatureIntervalSetting.setDefaultValue(DEFAULT_TEMPERATURE_INTERVAL).setValidator([] (long candidate) { 34 | return candidate > 0; 35 | }); 36 | 37 | Homie.setup(); 38 | } 39 | 40 | void loop() { 41 | Homie.loop(); 42 | } 43 | -------------------------------------------------------------------------------- /docs/others/homie-implementation-specifics.md: -------------------------------------------------------------------------------- 1 | The Homie `$implementation` identifier is `esp8266`. 2 | 3 | # Version 4 | 5 | * `$implementation/version`: Homie for ESP8266 version 6 | 7 | # Reset 8 | 9 | * `$implementation/reset`: You can publish a `true` to this topic to reset the device 10 | 11 | # Configuration 12 | 13 | * `$implementation/config`: The `configuration.json` is published there, with `wifi.password`, `mqtt.username` and `mqtt.password` fields stripped 14 | * `$implementation/config/set`: You can update the `configuration.json` by sending incremental JSON on this topic 15 | 16 | # OTA 17 | 18 | * `$implementation/ota/enabled`: `true` if OTA is enabled, `false` otherwise 19 | * `$implementation/ota/firmware`: If the update request is accepted, you must send the firmware payload to this topic 20 | * `$implementation/ota/status`: HTTP-like status code indicating the status of the OTA. Might be: 21 | 22 | Code|Description 23 | ----|----------- 24 | `200`|OTA successfully flashed 25 | `202`|OTA request / checksum accepted 26 | `206 465/349680`|OTA in progress. The data after the status code corresponds to `/` 27 | `304`|The current firmware is already up-to-date 28 | `400 BAD_FIRMWARE`|OTA error from your side. The identifier might be `BAD_FIRMWARE`, `BAD_CHECKSUM`, `NOT_ENOUGH_SPACE`, `NOT_REQUESTED` 29 | `403`|OTA not enabled 30 | `500 FLASH_ERROR`|OTA error on the ESP8266. The identifier might be `FLASH_ERROR` 31 | -------------------------------------------------------------------------------- /scripts/firmware_parser/firmware_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import re 4 | import sys 5 | 6 | if len(sys.argv) != 2: 7 | print("Please specify a file") 8 | sys.exit(1) 9 | 10 | regex_homie = re.compile(b"\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25") 11 | regex_name = re.compile(b"\xbf\x84\xe4\x13\x54(.+)\x93\x44\x6b\xa7\x75") 12 | regex_version = re.compile(b"\x6a\x3f\x3e\x0e\xe1(.+)\xb0\x30\x48\xd4\x1a") 13 | regex_brand = re.compile(b"\xfb\x2a\xf5\x68\xc0(.+)\x6e\x2f\x0f\xeb\x2d") 14 | 15 | try: 16 | firmware_file = open(sys.argv[1], "rb") 17 | except Exception as err: 18 | print("Error: {0}".format(err.strerror)) 19 | sys.exit(2) 20 | 21 | firmware_binary = firmware_file.read() 22 | firmware_file.close() 23 | 24 | regex_name_result = regex_name.search(firmware_binary) 25 | regex_version_result = regex_version.search(firmware_binary) 26 | 27 | if not regex_homie.search(firmware_binary) or not regex_name_result or not regex_version_result: 28 | print("Not a valid Homie firmware") 29 | sys.exit(3) 30 | 31 | 32 | regex_brand_result = regex_brand.search(firmware_binary) 33 | 34 | name = regex_name_result.group(1).decode() 35 | version = regex_version_result.group(1).decode() 36 | brand = regex_brand_result.group(1).decode() if regex_brand_result else "unset (default is Homie)" 37 | 38 | print("Name: {0}".format(name)) 39 | print("Version: {0}".format(version)) 40 | print("Brand: {0}".format(brand)) 41 | -------------------------------------------------------------------------------- /src/Homie/Boot/BootConfig.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "Boot.hpp" 12 | #include "../Constants.hpp" 13 | #include "../Limits.hpp" 14 | #include "../Datatypes/Interface.hpp" 15 | #include "../Timer.hpp" 16 | #include "../Utils/DeviceId.hpp" 17 | #include "../Utils/Validation.hpp" 18 | #include "../Utils/Helpers.hpp" 19 | #include "../Logger.hpp" 20 | #include "../Strings.hpp" 21 | #include "../../HomieSetting.hpp" 22 | #include "../../StreamingOperator.hpp" 23 | 24 | namespace HomieInternals { 25 | class BootConfig : public Boot { 26 | public: 27 | BootConfig(); 28 | ~BootConfig(); 29 | void setup(); 30 | void loop(); 31 | 32 | private: 33 | HTTPClient _httpClient; 34 | ESP8266WebServer _http; 35 | DNSServer _dns; 36 | uint8_t _ssidCount; 37 | bool _wifiScanAvailable; 38 | Timer _wifiScanTimer; 39 | bool _lastWifiScanEnded; 40 | char* _jsonWifiNetworks; 41 | bool _flaggedForReboot; 42 | uint32_t _flaggedForRebootAt; 43 | bool _proxyEnabled; 44 | char _apIpStr[MAX_IP_STRING_LENGTH]; 45 | 46 | void _onCaptivePortal(); 47 | void _onDeviceInfoRequest(); 48 | void _onNetworksRequest(); 49 | void _onConfigRequest(); 50 | void _generateNetworksJson(); 51 | void _onWifiConnectRequest(); 52 | void _onProxyControlRequest(); 53 | void _proxyHttpRequest(); 54 | void _onWifiStatusRequest(); 55 | }; 56 | } // namespace HomieInternals 57 | -------------------------------------------------------------------------------- /docs/advanced-usage/miscellaneous.md: -------------------------------------------------------------------------------- 1 | # Know if the device is configured / connected 2 | 3 | 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. 4 | 5 | ```c++ 6 | void loop() { 7 | if (Homie.isConfigured()) { 8 | // The device is configured, in normal mode 9 | if (Homie.isConnected()) { 10 | // The device is connected 11 | } else { 12 | // The device is not connected 13 | } 14 | } else { 15 | // The device is not configured, in either configuration or standalone mode 16 | } 17 | } 18 | ``` 19 | 20 | # Get access to the configuration 21 | 22 | You can get access to the configuration of the device. The representation of the configuration is: 23 | 24 | ```c++ 25 | struct ConfigStruct { 26 | char* name; 27 | char* deviceId; 28 | 29 | struct WiFi { 30 | char* ssid; 31 | char* password; 32 | } wifi; 33 | 34 | struct MQTT { 35 | struct Server { 36 | char* host; 37 | uint16_t port; 38 | } server; 39 | char* baseTopic; 40 | bool auth; 41 | char* username; 42 | char* password; 43 | } mqtt; 44 | 45 | struct OTA { 46 | bool enabled; 47 | } ota; 48 | }; 49 | ``` 50 | 51 | For example, to access the Wi-Fi SSID, you would do: 52 | 53 | ```c++ 54 | Homie.getConfiguration().wifi.ssid; 55 | ``` 56 | 57 | # Get access to the MQTT client 58 | 59 | You can get access to the underlying MQTT client. For example, to disconnect from the broker: 60 | 61 | ```c++ 62 | Homie.getMqttClient().disconnect(); 63 | ``` 64 | -------------------------------------------------------------------------------- /src/Homie/Limits.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace HomieInternals { 6 | const uint16_t MAX_JSON_CONFIG_FILE_SIZE = 1000; 7 | // 6 elements at root, 9 elements at wifi, 6 elements at mqtt, 1 element at ota, max 10 elements at settings 8 | 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(10); 9 | 10 | const uint8_t MAX_WIFI_SSID_LENGTH = 32 + 1; 11 | const uint8_t MAX_WIFI_PASSWORD_LENGTH = 64 + 1; 12 | const uint16_t MAX_HOSTNAME_LENGTH = 255 + 1; 13 | 14 | const uint8_t MAX_MQTT_CREDS_LENGTH = 32 + 1; 15 | const uint8_t MAX_MQTT_BASE_TOPIC_LENGTH = sizeof("shared-broker/username-lolipop/homie/sensors/"); 16 | const uint8_t MAX_MQTT_TOPIC_LENGTH = 128; 17 | 18 | const uint8_t MAX_FRIENDLY_NAME_LENGTH = sizeof("My awesome friendly name of the living room"); 19 | const uint8_t MAX_DEVICE_ID_LENGTH = sizeof("my-awesome-device-id-living-room"); 20 | 21 | const uint8_t MAX_BRAND_LENGTH = MAX_WIFI_SSID_LENGTH - sizeof("-0123abcd") + 1; 22 | const uint8_t MAX_FIRMWARE_NAME_LENGTH = sizeof("my-awesome-home-firmware-name"); 23 | const uint8_t MAX_FIRMWARE_VERSION_LENGTH = sizeof("v1.0.0-alpha+001"); 24 | 25 | const uint8_t MAX_NODE_ID_LENGTH = sizeof("my-super-awesome-node-id"); 26 | const uint8_t MAX_NODE_TYPE_LENGTH = sizeof("my-super-awesome-type"); 27 | const uint8_t MAX_NODE_PROPERTY_LENGTH = sizeof("my-super-awesome-property"); 28 | 29 | const uint8_t MAX_IP_STRING_LENGTH = sizeof("123.123.123.123"); 30 | 31 | const uint8_t MAX_MAC_STRING_LENGTH = 12; 32 | } // namespace HomieInternals 33 | -------------------------------------------------------------------------------- /docs/advanced-usage/custom-settings.md: -------------------------------------------------------------------------------- 1 | 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: 2 | 3 | ```c++ 4 | HomieSetting percentageSetting("percentage", "A simple percentage"); // id, description 5 | 6 | void setup() { 7 | percentageSetting.setDefaultValue(50).setValidator([] (long candidate) { 8 | return (candidate >= 0) && (candidate <= 100); 9 | }); 10 | 11 | Homie.setup(); 12 | } 13 | ``` 14 | 15 | An `HomieSetting` instance can be of the following types: 16 | 17 | Type | Value 18 | ---- | ----- 19 | `bool` | `true` or `false` 20 | `long` | An integer from `-2,147,483,648` to `2,147,483,647` 21 | `double` | A floating number that can fit into a `real64_t` 22 | `const char*` | Any string 23 | 24 | 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()`. 25 | 26 | For this example, if you want to provide the `percentage` setting, you will have to put in your configuration file: 27 | 28 | ```json 29 | { 30 | "settings": { 31 | "percentage": 75 32 | } 33 | } 34 | ``` 35 | 36 | See the following example for a concrete use case: 37 | 38 | [![GitHub logo](../assets/github.png) CustomSettings.ino](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/CustomSettings/CustomSettings.ino) 39 | -------------------------------------------------------------------------------- /scripts/ota_updater/README.md: -------------------------------------------------------------------------------- 1 | Script: OTA updater 2 | =================== 3 | 4 | This will allow you to send an OTA update to your device. 5 | 6 | ## Installation 7 | 8 | `pip install -r requirements.txt` 9 | 10 | ## Usage 11 | 12 | ```bash 13 | > scripts/ota_updater/ota_updater.py -h 14 | usage: ota_updater.py [-h] -l BROKER_HOST -p BROKER_PORT [-u BROKER_USERNAME] 15 | [-d BROKER_PASSWORD] [-t BASE_TOPIC] -i DEVICE_ID 16 | firmware 17 | 18 | ota firmware update scirpt for ESP8226 implemenation of the Homie mqtt IoT 19 | convention. 20 | 21 | positional arguments: 22 | firmware path to the firmware to be sent to the device 23 | 24 | arguments: 25 | -h, --help show this help message and exit 26 | -l BROKER_HOST, --broker-host BROKER_HOST 27 | host name or ip address of the mqtt broker 28 | -p BROKER_PORT, --broker-port BROKER_PORT 29 | port of the mqtt broker 30 | -u BROKER_USERNAME, --broker-username BROKER_USERNAME 31 | username used to authenticate with the mqtt broker 32 | -d BROKER_PASSWORD, --broker-password BROKER_PASSWORD 33 | password used to authenticate with the mqtt broker 34 | -t BASE_TOPIC, --base-topic BASE_TOPIC 35 | base topic of the homie devices on the broker 36 | -i DEVICE_ID, --device-id DEVICE_ID 37 | homie device id 38 | ``` 39 | 40 | * `BROKER_HOST` and `BROKER_PORT` defaults to 127.0.0.1 and 1883 respectively if not set. 41 | * `BROKER_USERNAME` and `BROKER_PASSWORD` are optional. 42 | * `BASE_TOPIC` defaults to `homie/` if not set 43 | -------------------------------------------------------------------------------- /src/HomieSetting.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Arduino.h" 6 | 7 | #include "./Homie/Datatypes/Callbacks.hpp" 8 | 9 | namespace HomieInternals { 10 | class HomieClass; 11 | class Config; 12 | class Validation; 13 | class BootConfig; 14 | 15 | class IHomieSetting { 16 | public: 17 | IHomieSetting() {} 18 | 19 | virtual bool isBool() const { return false; } 20 | virtual bool isLong() const { return false; } 21 | virtual bool isDouble() const { return false; } 22 | virtual bool isConstChar() const { return false; } 23 | 24 | static std::vector settings; 25 | }; 26 | } // namespace HomieInternals 27 | 28 | template 29 | class HomieSetting : public HomieInternals::IHomieSetting { 30 | friend HomieInternals::HomieClass; 31 | friend HomieInternals::Config; 32 | friend HomieInternals::Validation; 33 | friend HomieInternals::BootConfig; 34 | 35 | public: 36 | HomieSetting(const char* name, const char* description); 37 | T get() const; 38 | bool wasProvided() const; 39 | HomieSetting& setDefaultValue(T defaultValue); 40 | HomieSetting& setValidator(const std::function& validator); 41 | 42 | private: 43 | const char* _name; 44 | const char* _description; 45 | bool _required; 46 | bool _provided; 47 | T _value; 48 | std::function _validator; 49 | 50 | bool validate(T candidate) const; 51 | void set(T value); 52 | bool isRequired() const; 53 | const char* getName() const; 54 | const char* getDescription() const; 55 | 56 | bool isBool() const; 57 | bool isLong() const; 58 | bool isDouble() const; 59 | bool isConstChar() const; 60 | }; 61 | -------------------------------------------------------------------------------- /docs/others/troubleshooting.md: -------------------------------------------------------------------------------- 1 | ## 1. I see some garbage on the Serial monitor? 2 | 3 | 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: 4 | 5 | * Disable the serial logging, to have the LED working: 6 | 7 | ```c++ 8 | void setup() { 9 | Homie.enableLogging(false); // before Homie.setup() 10 | // ... 11 | } 12 | ``` 13 | 14 | * Disable the LED blinking, to have the serial line working: 15 | 16 | ```c++ 17 | void setup() { 18 | Homie.enableBuiltInLedIndicator(false); // before Homie.setup() 19 | // ... 20 | } 21 | ``` 22 | 23 | ## 2. I see an `abort` message on the Serial monitor? 24 | 25 | `abort()` is called by Homie for ESP8266 when the framework is used in a bad way. The possible causes are: 26 | 27 | * You are calling a function that is meant to be called before `Homie.setup()`, after `Homie.setup()` 28 | 29 | * 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. 30 | 31 | ## 3. The network is completely unstable... What's going on? 32 | 33 | 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). 34 | 35 | ## 4. My device resets itself without me doing anything? 36 | 37 | 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). 38 | -------------------------------------------------------------------------------- /src/Homie/Boot/BootStandalone.cpp: -------------------------------------------------------------------------------- 1 | #include "BootStandalone.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | BootStandalone::BootStandalone() 6 | : Boot("standalone") 7 | , _flaggedForConfig(false) { 8 | } 9 | 10 | BootStandalone::~BootStandalone() { 11 | } 12 | 13 | void BootStandalone::_handleReset() { 14 | if (Interface::get().reset.enabled) { 15 | _resetDebouncer.update(); 16 | 17 | if (_resetDebouncer.read() == Interface::get().reset.triggerState) { 18 | _flaggedForConfig = true; 19 | Interface::get().getLogger() << F("Flagged for configuration mode by pin") << endl; 20 | } 21 | } 22 | 23 | if (Interface::get().reset.flaggedBySketch) { 24 | _flaggedForConfig = true; 25 | Interface::get().getLogger() << F("Flagged for configuration mode by sketch") << endl; 26 | } 27 | } 28 | 29 | void BootStandalone::setup() { 30 | Boot::setup(); 31 | 32 | WiFi.mode(WIFI_OFF); 33 | 34 | if (Interface::get().reset.enabled) { 35 | pinMode(Interface::get().reset.triggerPin, INPUT_PULLUP); 36 | 37 | _resetDebouncer.attach(Interface::get().reset.triggerPin); 38 | _resetDebouncer.interval(Interface::get().reset.triggerTime); 39 | } 40 | } 41 | 42 | void BootStandalone::loop() { 43 | Boot::loop(); 44 | 45 | _handleReset(); 46 | 47 | if (_flaggedForConfig && Interface::get().reset.idle) { 48 | Interface::get().getLogger() << F("Device is idle") << endl; 49 | Interface::get().getConfig().setHomieBootModeOnNextBoot(HomieBootMode::CONFIGURATION); 50 | 51 | Interface::get().getLogger() << F("Triggering ABOUT_TO_RESET event...") << endl; 52 | Interface::get().event.type = HomieEventType::ABOUT_TO_RESET; 53 | Interface::get().eventHandler(Interface::get().event); 54 | 55 | Interface::get().getLogger() << F("↻ Rebooting into config mode...") << endl; 56 | Serial.flush(); 57 | ESP.restart(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Homie/Utils/Helpers.cpp: -------------------------------------------------------------------------------- 1 | #include "Helpers.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | void Helpers::abort(const String& message) { 6 | Serial.begin(115200); 7 | Serial << message << endl; 8 | Serial.flush(); 9 | ::abort(); 10 | } 11 | 12 | uint8_t Helpers::rssiToPercentage(int32_t rssi) { 13 | uint8_t quality; 14 | if (rssi <= -100) { 15 | quality = 0; 16 | } else if (rssi >= -50) { 17 | quality = 100; 18 | } else { 19 | quality = 2 * (rssi + 100); 20 | } 21 | 22 | return quality; 23 | } 24 | 25 | void Helpers::stringToBytes(const char* str, char sep, byte* bytes, int maxBytes, int base) { 26 | // taken from http://stackoverflow.com/a/35236734 27 | for (int i = 0; i < maxBytes; i++) { 28 | bytes[i] = strtoul(str, NULL, base); 29 | str = strchr(str, sep); 30 | if (str == NULL || *str == '\0') { 31 | break; 32 | } 33 | str++; 34 | } 35 | } 36 | 37 | bool Helpers::validateMacAddress(const char* mac) { 38 | // taken from http://stackoverflow.com/a/4792211 39 | int i = 0; 40 | int s = 0; 41 | while (*mac) { 42 | if (isxdigit(*mac)) { 43 | i++; 44 | } else if (*mac == ':' || *mac == '-') { 45 | if (i == 0 || i / 2 - 1 != s) 46 | break; 47 | ++s; 48 | } else { 49 | s = -1; 50 | } 51 | ++mac; 52 | } 53 | return (i == MAX_MAC_STRING_LENGTH && s == 5); 54 | } 55 | 56 | bool Helpers::validateMd5(const char* md5) { 57 | if (strlen(md5) != 32) return false; 58 | 59 | for (uint8_t i = 0; i < 32; i++) { 60 | char c = md5[i]; 61 | bool valid = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); 62 | if (!valid) return false; 63 | } 64 | 65 | return true; 66 | } 67 | 68 | std::unique_ptr Helpers::cloneString(const String& string) { 69 | size_t length = string.length(); 70 | std::unique_ptr copy(new char[length + 1]); 71 | memcpy(copy.get(), string.c_str(), length); 72 | copy.get()[length] = '\0'; 73 | 74 | return copy; 75 | } 76 | -------------------------------------------------------------------------------- /examples/HookToEvents/HookToEvents.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void onHomieEvent(const HomieEvent& event) { 4 | switch (event.type) { 5 | case HomieEventType::STANDALONE_MODE: 6 | Serial << "Standalone mode started" << endl; 7 | break; 8 | case HomieEventType::CONFIGURATION_MODE: 9 | Serial << "Configuration mode started" << endl; 10 | break; 11 | case HomieEventType::NORMAL_MODE: 12 | Serial << "Normal mode started" << endl; 13 | break; 14 | case HomieEventType::OTA_STARTED: 15 | Serial << "OTA started" << endl; 16 | break; 17 | case HomieEventType::OTA_PROGRESS: 18 | Serial << "OTA progress, " << event.sizeDone << "/" << event.sizeTotal << endl; 19 | break; 20 | case HomieEventType::OTA_FAILED: 21 | Serial << "OTA failed" << endl; 22 | break; 23 | case HomieEventType::OTA_SUCCESSFUL: 24 | Serial << "OTA successful" << endl; 25 | break; 26 | case HomieEventType::ABOUT_TO_RESET: 27 | Serial << "About to reset" << endl; 28 | break; 29 | case HomieEventType::WIFI_CONNECTED: 30 | Serial << "Wi-Fi connected, IP: " << event.ip << ", gateway: " << event.gateway << ", mask: " << event.mask << endl; 31 | break; 32 | case HomieEventType::WIFI_DISCONNECTED: 33 | Serial << "Wi-Fi disconnected, reason: " << (int8_t)event.wifiReason << endl; 34 | break; 35 | case HomieEventType::MQTT_READY: 36 | Serial << "MQTT connected" << endl; 37 | break; 38 | case HomieEventType::MQTT_DISCONNECTED: 39 | Serial << "MQTT disconnected, reason: " << (int8_t)event.mqttReason << endl; 40 | break; 41 | case HomieEventType::MQTT_PACKET_ACKNOWLEDGED: 42 | Serial << "MQTT packet acknowledged, packetId: " << event.packetId << endl; 43 | break; 44 | case HomieEventType::READY_TO_SLEEP: 45 | Serial << "Ready to sleep" << endl; 46 | break; 47 | } 48 | } 49 | 50 | void setup() { 51 | Serial.begin(115200); 52 | Serial << endl << endl; 53 | Homie.disableLogging(); 54 | Homie_setFirmware("events-test", "1.0.0"); 55 | Homie.onEvent(onHomieEvent); 56 | Homie.setup(); 57 | } 58 | 59 | void loop() { 60 | Homie.loop(); 61 | } 62 | -------------------------------------------------------------------------------- /examples/IteadSonoffButton/IteadSonoffButton.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Tested with "WiFi Smart Socket ESP8266 MQTT" 3 | * and "Sonoff - WiFi Wireless Smart Switch ESP8266 MQTT" 4 | * 5 | * The Relay could be toggeled with the physical pushbutton 6 | */ 7 | 8 | #include 9 | 10 | const int PIN_RELAY = 12; 11 | const int PIN_LED = 13; 12 | const int PIN_BUTTON = 0; 13 | 14 | unsigned long buttonDownTime = 0; 15 | byte lastButtonState = 1; 16 | byte buttonPressHandled = 0; 17 | 18 | HomieNode switchNode("switch", "switch"); 19 | 20 | bool switchOnHandler(HomieRange range, String value) { 21 | if (value != "true" && value != "false") return false; 22 | 23 | bool on = (value == "true"); 24 | digitalWrite(PIN_RELAY, on ? HIGH : LOW); 25 | switchNode.setProperty("on").send(value); 26 | Homie.getLogger() << "Switch is " << (on ? "on" : "off") << endl; 27 | 28 | return true; 29 | } 30 | 31 | void toggleRelay() { 32 | bool on = digitalRead(PIN_RELAY) == HIGH; 33 | digitalWrite(PIN_RELAY, on ? LOW : HIGH); 34 | switchNode.setProperty("on").send(on ? "false" : "true"); 35 | Homie.getLogger() << "Switch is " << (on ? "off" : "on") << endl; 36 | } 37 | 38 | void loopHandler() { 39 | byte buttonState = digitalRead(PIN_BUTTON); 40 | if ( buttonState != lastButtonState ) { 41 | if (buttonState == LOW) { 42 | buttonDownTime = millis(); 43 | buttonPressHandled = 0; 44 | } 45 | else { 46 | unsigned long dt = millis() - buttonDownTime; 47 | if ( dt >= 90 && dt <= 900 && buttonPressHandled == 0 ) { 48 | toggleRelay(); 49 | buttonPressHandled = 1; 50 | } 51 | } 52 | lastButtonState = buttonState; 53 | } 54 | } 55 | 56 | void setup() { 57 | Serial.begin(115200); 58 | Serial.println(); 59 | Serial.println(); 60 | pinMode(PIN_RELAY, OUTPUT); 61 | pinMode(PIN_BUTTON, INPUT); 62 | digitalWrite(PIN_RELAY, LOW); 63 | 64 | Homie_setFirmware("itead-sonoff-buton", "1.0.0"); 65 | Homie.setLedPin(PIN_LED, LOW).setResetTrigger(PIN_BUTTON, LOW, 5000); 66 | 67 | switchNode.advertise("on").settable(switchOnHandler); 68 | 69 | Homie.setLoopFunction(loopHandler); 70 | Homie.setup(); 71 | } 72 | 73 | void loop() { 74 | Homie.loop(); 75 | } 76 | -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # Datatypes (KEYWORD1) 3 | ####################################### 4 | 5 | Homie KEYWORD1 6 | HomieNode KEYWORD1 7 | HomieSetting KEYWORD1 8 | HomieEvent KEYWORD1 9 | HomieEventType KEYWORD1 10 | HomieRange KEYWORD1 11 | 12 | ####################################### 13 | # Methods and Functions (KEYWORD2) 14 | ####################################### 15 | 16 | Homie_setBrand KEYWORD2 17 | Homie_setFirmware KEYWORD2 18 | 19 | # Homie 20 | 21 | setup KEYWORD2 22 | loop KEYWORD2 23 | disableLogging KEYWORD2 24 | setLoggingPrinter KEYWORD2 25 | disableLedFeedback KEYWORD2 26 | setLedPin KEYWORD2 27 | setConfigurationApPassword KEYWORD2 28 | setGlobalInputHandler KEYWORD2 29 | setBroadcastHandler KEYWORD2 30 | onEvent KEYWORD2 31 | setResetTrigger KEYWORD2 32 | disableResetTrigger KEYWORD2 33 | setSetupFunction KEYWORD2 34 | setLoopFunction KEYWORD2 35 | setStandalone KEYWORD2 36 | reset KEYWORD2 37 | setIdle KEYWORD2 38 | isConfigured KEYWORD2 39 | isConnected KEYWORD2 40 | getConfiguration KEYWORD2 41 | getMqttClient KEYWORD2 42 | getLogger KEYWORD2 43 | prepareToSleep KEYWORD2 44 | 45 | # HomieNode 46 | 47 | getId KEYWORD2 48 | getType KEYWORD2 49 | advertise KEYWORD2 50 | advertiseRange KEYWORD2 51 | settable KEYWORD2 52 | setProperty KEYWORD2 53 | 54 | # HomieSetting 55 | 56 | get KEYWORD2 57 | wasProvided KEYWORD2 58 | setDefaultValue KEYWORD2 59 | setValidator KEYWORD2 60 | 61 | # HomieRange 62 | 63 | isRange KEYWORD2 64 | index KEYWORD2 65 | 66 | # SendingPromise 67 | 68 | setQos KEYWORD2 69 | setRetained KEYWORD2 70 | setRange KEYWORD2 71 | send KEYWORD2 72 | 73 | ####################################### 74 | # Constants (LITERAL1) 75 | ####################################### 76 | 77 | # HomieEventType 78 | 79 | STANDALONE_MODE LITERAL1 80 | CONFIGURATION_MODE LITERAL1 81 | NORMAL_MODE LITERAL1 82 | OTA_STARTED LITERAL1 83 | OTA_PROGRESS LITERAL1 84 | OTA_FAILED LITERAL1 85 | OTA_SUCCESSFUL LITERAL1 86 | ABOUT_TO_RESET LITERAL1 87 | WIFI_CONNECTED LITERAL1 88 | WIFI_DISCONNECTED LITERAL1 89 | MQTT_READY LITERAL1 90 | MQTT_DISCONNECTED LITERAL1 91 | MQTT_PACKET_ACKNOWLEDGED LITERAL1 92 | READY_TO_SLEEP LITERAL1 93 | 94 | # StreamingOperator 95 | 96 | endl LITERAL1 97 | -------------------------------------------------------------------------------- /src/Homie/Datatypes/Interface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../Logger.hpp" 5 | #include "../Blinker.hpp" 6 | #include "../Constants.hpp" 7 | #include "../Config.hpp" 8 | #include "../Limits.hpp" 9 | #include "./Callbacks.hpp" 10 | #include "../../HomieBootMode.hpp" 11 | #include "../../HomieNode.hpp" 12 | #include "../../SendingPromise.hpp" 13 | #include "../../HomieEvent.hpp" 14 | 15 | namespace HomieInternals { 16 | class Logger; 17 | class Blinker; 18 | class Config; 19 | class SendingPromise; 20 | class HomieClass; 21 | class InterfaceData { 22 | friend HomieClass; 23 | 24 | public: 25 | InterfaceData(); 26 | 27 | /***** User configurable data *****/ 28 | char brand[MAX_BRAND_LENGTH]; 29 | 30 | HomieBootMode bootMode; 31 | 32 | struct ConfigurationAP { 33 | bool secured; 34 | char password[MAX_WIFI_PASSWORD_LENGTH]; 35 | } configurationAp; 36 | 37 | struct Firmware { 38 | char name[MAX_FIRMWARE_NAME_LENGTH]; 39 | char version[MAX_FIRMWARE_VERSION_LENGTH]; 40 | } firmware; 41 | 42 | struct LED { 43 | bool enabled; 44 | uint8_t pin; 45 | uint8_t on; 46 | } led; 47 | 48 | struct Reset { 49 | bool enabled; 50 | bool idle; 51 | uint8_t triggerPin; 52 | uint8_t triggerState; 53 | uint16_t triggerTime; 54 | bool flaggedBySketch; 55 | } reset; 56 | 57 | bool flaggedForSleep; 58 | 59 | GlobalInputHandler globalInputHandler; 60 | BroadcastHandler broadcastHandler; 61 | OperationFunction setupFunction; 62 | OperationFunction loopFunction; 63 | EventHandler eventHandler; 64 | 65 | /***** Runtime data *****/ 66 | HomieEvent event; 67 | bool ready; 68 | Logger& getLogger() { return *_logger; } 69 | Blinker& getBlinker() { return *_blinker; } 70 | Config& getConfig() { return *_config; } 71 | AsyncMqttClient& getMqttClient() { return *_mqttClient; } 72 | SendingPromise& getSendingPromise() { return *_sendingPromise; } 73 | 74 | private: 75 | Logger* _logger; 76 | Blinker* _blinker; 77 | Config* _config; 78 | AsyncMqttClient* _mqttClient; 79 | SendingPromise* _sendingPromise; 80 | }; 81 | 82 | class Interface { 83 | public: 84 | static InterfaceData& get(); 85 | 86 | private: 87 | static InterfaceData _interface; 88 | }; 89 | } // namespace HomieInternals 90 | -------------------------------------------------------------------------------- /src/HomieNode.cpp: -------------------------------------------------------------------------------- 1 | #include "HomieNode.hpp" 2 | #include "Homie.hpp" 3 | 4 | using namespace HomieInternals; 5 | 6 | std::vector HomieNode::nodes; 7 | 8 | PropertyInterface::PropertyInterface() 9 | : _property(nullptr) { 10 | } 11 | 12 | void PropertyInterface::settable(const PropertyInputHandler& inputHandler) { 13 | _property->settable(inputHandler); 14 | } 15 | 16 | PropertyInterface& PropertyInterface::setProperty(Property* property) { 17 | _property = property; 18 | return *this; 19 | } 20 | 21 | HomieNode::HomieNode(const char* id, const char* type, const NodeInputHandler& inputHandler) 22 | : _id(id) 23 | , _type(type) 24 | , _properties() 25 | , _inputHandler(inputHandler) { 26 | if (strlen(id) + 1 > MAX_NODE_ID_LENGTH || strlen(type) + 1 > MAX_NODE_TYPE_LENGTH) { 27 | Helpers::abort(F("✖ HomieNode(): either the id or type string is too long")); 28 | return; // never reached, here for clarity 29 | } 30 | Homie._checkBeforeSetup(F("HomieNode::HomieNode")); 31 | 32 | HomieNode::nodes.push_back(this); 33 | } 34 | 35 | HomieNode::~HomieNode() { 36 | 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())")); 37 | return; // never reached, here for clarity 38 | } 39 | 40 | PropertyInterface& HomieNode::advertise(const char* property) { 41 | Property* propertyObject = new Property(property); 42 | 43 | _properties.push_back(propertyObject); 44 | 45 | return _propertyInterface.setProperty(propertyObject); 46 | } 47 | 48 | PropertyInterface& HomieNode::advertiseRange(const char* property, uint16_t lower, uint16_t upper) { 49 | Property* propertyObject = new Property(property, true, lower, upper); 50 | 51 | _properties.push_back(propertyObject); 52 | 53 | return _propertyInterface.setProperty(propertyObject); 54 | } 55 | 56 | SendingPromise& HomieNode::setProperty(const String& property) const { 57 | return Interface::get().getSendingPromise().setNode(*this).setProperty(property).setQos(1).setRetained(true).overwriteSetter(false).setRange({ .isRange = false, .index = 0 }); 58 | } 59 | 60 | bool HomieNode::handleInput(const String& property, const HomieRange& range, const String& value) { 61 | return _inputHandler(property, range, value); 62 | } 63 | 64 | const std::vector& HomieNode::getProperties() const { 65 | return _properties; 66 | } 67 | -------------------------------------------------------------------------------- /docs/advanced-usage/input-handlers.md: -------------------------------------------------------------------------------- 1 | There are four types of input handlers: 2 | 3 | * Global input handler. This unique handler will handle every changed settable properties for all nodes 4 | 5 | ```c++ 6 | bool globalInputHandler(const HomieNode& node, const String& property, const HomieRange& range, const String& value) { 7 | 8 | } 9 | 10 | void setup() { 11 | Homie.setGlobalInputHandler(globalInputHandler); // before Homie.setup() 12 | // ... 13 | } 14 | ``` 15 | 16 | * Node input handlers. This handler will handle every changed settable properties of a specific node 17 | 18 | ```c++ 19 | bool nodeInputHandler(const String& property, const HomieRange& range, const String& value) { 20 | 21 | } 22 | 23 | HomieNode node("id", "type", nodeInputHandler); 24 | ``` 25 | 26 | * Virtual callback from node input handler 27 | 28 | 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. 29 | 30 | ```c++ 31 | class RelaisNode : public HomieNode { 32 | public: 33 | RelaisNode(): HomieNode("Relais", "switch8"); 34 | 35 | protected: 36 | virtual bool handleInput(const String& property, const HomieRange& range, const String& value) { 37 | 38 | } 39 | }; 40 | ``` 41 | 42 | * Property input handlers. This handler will handle changes for a specific settable property of a specific node 43 | 44 | ```c++ 45 | bool propertyInputHandler(const HomieRange& range, const String& value) { 46 | 47 | } 48 | 49 | HomieNode node("id", "type"); 50 | 51 | void setup() { 52 | node.advertise("property").settable(propertyInputHandler); // before Homie.setup() 53 | // ... 54 | } 55 | ``` 56 | 57 | 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. 58 | 59 | 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. 60 | -------------------------------------------------------------------------------- /docs/configuration/json-configuration-file.md: -------------------------------------------------------------------------------- 1 | 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). 2 | 3 | Below is the format of the JSON configuration you will have to provide: 4 | 5 | ```json 6 | { 7 | "name": "The kitchen light", 8 | "device_id": "kitchen-light", 9 | "wifi": { 10 | "ssid": "Network_1", 11 | "password": "I'm a Wi-Fi password!", 12 | "bssid": "DE:AD:BE:EF:BA:BE", 13 | "channel": 1, 14 | "ip": "192.168.1.5", 15 | "mask": "255.255.255.0", 16 | "gw": "192.168.1.1", 17 | "dns1": "8.8.8.8", 18 | "dns2": "8.8.4.4" 19 | }, 20 | "mqtt": { 21 | "host": "192.168.1.10", 22 | "port": 1883, 23 | "base_topic": "devices/", 24 | "auth": true, 25 | "username": "user", 26 | "password": "pass" 27 | }, 28 | "ota": { 29 | "enabled": true 30 | }, 31 | "settings": { 32 | "percentage": 55 33 | } 34 | } 35 | ``` 36 | 37 | The above JSON contains every field that can be customized. 38 | 39 | Here are the rules: 40 | 41 | * `name`, `wifi.ssid`, `wifi.password`, `mqtt.host` and `ota.enabled` are mandatory 42 | * `wifi.password` can be `null` if connecting to an open network 43 | * If `mqtt.auth` is `true`, `mqtt.username` and `mqtt.password` must be provided 44 | * `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: 45 | - `bssid` and `channel` have to be defined together and these settings are independand of settings related to static IP 46 | - to define static IP, `ip` (IP address), `mask` (netmask) and `gw` (gateway) settings have to be defined at the same time 47 | - 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. 48 | 49 | 50 | Default values if not provided: 51 | 52 | * `device_id`: the hardware device ID (eg. `1a2b3c4d5e6f`) 53 | * `mqtt.port`: `1883` 54 | * `mqtt.base_topic`: `homie/` 55 | * `mqtt.auth`: `false` 56 | 57 | The `mqtt.host` field can be either an IP or an hostname. 58 | -------------------------------------------------------------------------------- /src/HomieSetting.cpp: -------------------------------------------------------------------------------- 1 | #include "HomieSetting.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | std::vector __attribute__((init_priority(101))) IHomieSetting::settings; 6 | 7 | template 8 | HomieSetting::HomieSetting(const char* name, const char* description) 9 | : _name(name) 10 | , _description(description) 11 | , _required(true) 12 | , _provided(false) 13 | , _value() 14 | , _validator([](T candidate) { return true; }) { 15 | IHomieSetting::settings.push_back(this); 16 | } 17 | 18 | template 19 | T HomieSetting::get() const { 20 | return _value; 21 | } 22 | 23 | template 24 | bool HomieSetting::wasProvided() const { 25 | return _provided; 26 | } 27 | 28 | template 29 | HomieSetting& HomieSetting::setDefaultValue(T defaultValue) { 30 | _value = defaultValue; 31 | _required = false; 32 | return *this; 33 | } 34 | 35 | template 36 | HomieSetting& HomieSetting::setValidator(const std::function& validator) { 37 | _validator = validator; 38 | return *this; 39 | } 40 | 41 | template 42 | bool HomieSetting::validate(T candidate) const { 43 | return _validator(candidate); 44 | } 45 | 46 | template 47 | void HomieSetting::set(T value) { 48 | _value = value; 49 | _provided = true; 50 | } 51 | 52 | template 53 | bool HomieSetting::isRequired() const { 54 | return _required; 55 | } 56 | 57 | template 58 | const char* HomieSetting::getName() const { 59 | return _name; 60 | } 61 | 62 | template 63 | const char* HomieSetting::getDescription() const { 64 | return _description; 65 | } 66 | 67 | template 68 | bool HomieSetting::isBool() const { return false; } 69 | 70 | template 71 | bool HomieSetting::isLong() const { return false; } 72 | 73 | template 74 | bool HomieSetting::isDouble() const { return false; } 75 | 76 | template 77 | bool HomieSetting::isConstChar() const { return false; } 78 | 79 | template<> 80 | bool HomieSetting::isBool() const { return true; } 81 | 82 | template<> 83 | bool HomieSetting::isLong() const { return true; } 84 | 85 | template<> 86 | bool HomieSetting::isDouble() const { return true; } 87 | 88 | template<> 89 | bool HomieSetting::isConstChar() const { return true; } 90 | 91 | template class HomieSetting; // Needed because otherwise undefined reference to 92 | template class HomieSetting; 93 | template class HomieSetting; 94 | template class HomieSetting; 95 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Homie for ESP8266 2 | repo_name: 'marvinroger/homie-esp8266' 3 | repo_url: 'https://github.com/marvinroger/homie-esp8266' 4 | edit_uri: edit/develop/docs 5 | 6 | pages: 7 | - Welcome: index.md 8 | - Quickstart: 9 | - What is it?: quickstart/what-is-it.md 10 | - Getting started: quickstart/getting-started.md 11 | - Advanced usage: 12 | - Built-in LED: advanced-usage/built-in-led.md 13 | - Branding: advanced-usage/branding.md 14 | - Events: advanced-usage/events.md 15 | - Logging: advanced-usage/logging.md 16 | - Streaming operator: advanced-usage/streaming-operator.md 17 | - Input handlers: advanced-usage/input-handlers.md 18 | - Broadcast: advanced-usage/broadcast.md 19 | - Custom settings: advanced-usage/custom-settings.md 20 | - Resetting: advanced-usage/resetting.md 21 | - Standalone mode: advanced-usage/standalone-mode.md 22 | - Magic bytes: advanced-usage/magic-bytes.md 23 | - Range properties: advanced-usage/range-properties.md 24 | - Deep sleep: advanced-usage/deep-sleep.md 25 | - Miscellaneous: advanced-usage/miscellaneous.md 26 | - UI Bundle: advanced-usage/ui-bundle.md 27 | - Configuration: 28 | - JSON configuration file: configuration/json-configuration-file.md 29 | - HTTP JSON API: configuration/http-json-api.md 30 | - Others: 31 | - OTA/configuration updates: others/ota-configuration-updates.md 32 | - Homie implementation specifics: others/homie-implementation-specifics.md 33 | - Limitations and known issues: others/limitations-and-known-issues.md 34 | - Troubleshooting: others/troubleshooting.md 35 | - C++ API reference: others/cpp-api-reference.md 36 | - Upgrade guide from v1 to v2: others/upgrade-guide-from-v1-to-v2.md 37 | - Community projects: others/community-projects.md 38 | 39 | theme: material 40 | extra: 41 | logo: assets/logo.png 42 | palette: 43 | primary: red 44 | accent: red 45 | feature: 46 | tabs: true 47 | social: 48 | - type: cog 49 | link: http://setup.homie-esp8266.marvinroger.fr 50 | 51 | markdown_extensions: 52 | - meta 53 | - footnotes 54 | - codehilite 55 | - admonition 56 | - toc(permalink=true) 57 | - pymdownx.arithmatex 58 | - pymdownx.betterem(smart_enable=all) 59 | - pymdownx.caret 60 | - pymdownx.critic 61 | - pymdownx.emoji: 62 | emoji_generator: !!python/name:pymdownx.emoji.to_svg 63 | - pymdownx.inlinehilite 64 | - pymdownx.magiclink 65 | - pymdownx.mark 66 | - pymdownx.smartsymbols 67 | - pymdownx.superfences 68 | - pymdownx.tasklist(custom_checkbox=true) 69 | - pymdownx.tilde 70 | -------------------------------------------------------------------------------- /.circleci/assets/generate_docs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | import urllib 6 | import urllib2 7 | import tempfile 8 | import zipfile 9 | import glob 10 | import subprocess 11 | import getopt 12 | import sys 13 | import shutil 14 | import os 15 | import string 16 | 17 | FIRST_RELEASE_ID=3084382 18 | VERSIONS = [ 19 | ('develop', 'develop branch (development)', 'develop'), 20 | ('master', 'branch master (stable)', 'stable') 21 | ] 22 | 23 | current_dir = os.path.dirname(__file__) 24 | output_dir = getopt.getopt(sys.argv[1:], 'o:')[0][0][1] 25 | github_releases = json.load(urllib2.urlopen('https://api.github.com/repos/marvinroger/homie-esp8266/releases')) 26 | 27 | def generate_docs(tag_name, description, destination_folder): 28 | print('Generating docs for ' + tag_name + ' (' + description + ') at /' + destination_folder + '...') 29 | zip_url = 'https://github.com/marvinroger/homie-esp8266/archive/' + tag_name + '.zip' 30 | zip_path = tempfile.mkstemp()[1] 31 | urllib.urlretrieve(zip_url, zip_path) 32 | 33 | zip_file = zipfile.ZipFile(zip_path, 'r') 34 | unzip_path = tempfile.mkdtemp() 35 | zip_file.extractall(unzip_path) 36 | src_path = glob.glob(unzip_path + '/*')[0] 37 | 38 | if not os.path.isfile(src_path + '/mkdocs.yml'): shutil.copy(current_dir + '/mkdocs.default.yml', src_path + '/mkdocs.yml') 39 | 40 | subprocess.call(['mkdocs', 'build'], cwd=src_path) 41 | shutil.copytree(src_path + '/site', output_dir + '/' + destination_folder) 42 | print('Done.') 43 | 44 | # Generate docs for branches 45 | 46 | for version in VERSIONS: 47 | generate_docs(version[0], version[1], version[2]) 48 | 49 | # Generate docs for releases 50 | 51 | for release in github_releases: 52 | if (release['id'] < FIRST_RELEASE_ID): continue 53 | 54 | tag_name = release['tag_name'] 55 | version = tag_name[1:] 56 | description = 'release ' + version 57 | 58 | VERSIONS.append((tag_name, description, version)) 59 | generate_docs(tag_name, description, version) 60 | 61 | # Generate index 62 | 63 | versions_html = '' 67 | 68 | docs_index_template_file = open(current_dir + '/docs_index_template.html') 69 | docs_index_template_html = docs_index_template_file.read() 70 | docs_index_template = string.Template(docs_index_template_html) 71 | docs_index = docs_index_template.substitute(versions_html=versions_html) 72 | 73 | docs_index_file = open(output_dir + '/index.html', 'w') 74 | docs_index_file.write(docs_index) 75 | docs_index_file.close() 76 | -------------------------------------------------------------------------------- /docs/advanced-usage/events.md: -------------------------------------------------------------------------------- 1 | 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: 2 | 3 | ```c++ 4 | void onHomieEvent(const HomieEvent& event) { 5 | switch(event.type) { 6 | case HomieEventType::STANDALONE_MODE: 7 | // Do whatever you want when standalone mode is started 8 | break; 9 | case HomieEventType::CONFIGURATION_MODE: 10 | // Do whatever you want when configuration mode is started 11 | break; 12 | case HomieEventType::NORMAL_MODE: 13 | // Do whatever you want when normal mode is started 14 | break; 15 | case HomieEventType::OTA_STARTED: 16 | // Do whatever you want when OTA is started 17 | break; 18 | case HomieEventType::OTA_PROGRESS: 19 | // Do whatever you want when OTA is in progress 20 | 21 | // You can use event.sizeDone and event.sizeTotal 22 | break; 23 | case HomieEventType::OTA_FAILED: 24 | // Do whatever you want when OTA is failed 25 | break; 26 | case HomieEventType::OTA_SUCCESSFUL: 27 | // Do whatever you want when OTA is successful 28 | break; 29 | case HomieEventType::ABOUT_TO_RESET: 30 | // Do whatever you want when the device is about to reset 31 | break; 32 | case HomieEventType::WIFI_CONNECTED: 33 | // Do whatever you want when Wi-Fi is connected in normal mode 34 | 35 | // You can use event.ip, event.gateway, event.mask 36 | break; 37 | case HomieEventType::WIFI_DISCONNECTED: 38 | // Do whatever you want when Wi-Fi is disconnected in normal mode 39 | 40 | // You can use event.wifiReason 41 | break; 42 | case HomieEventType::MQTT_READY: 43 | // Do whatever you want when MQTT is connected in normal mode 44 | break; 45 | case HomieEventType::MQTT_DISCONNECTED: 46 | // Do whatever you want when MQTT is disconnected in normal mode 47 | 48 | // You can use event.mqttReason 49 | break; 50 | case HomieEventType::MQTT_PACKET_ACKNOWLEDGED: 51 | // Do whatever you want when an MQTT packet with QoS > 0 is acknowledged by the broker 52 | 53 | // You can use event.packetId 54 | break; 55 | case HomieEventType::READY_TO_SLEEP: 56 | // After you've called `prepareToSleep()`, the event is triggered when MQTT is disconnected 57 | break; 58 | } 59 | } 60 | 61 | void setup() { 62 | Homie.onEvent(onHomieEvent); // before Homie.setup() 63 | // ... 64 | } 65 | ``` 66 | 67 | See the following example for a concrete use case: 68 | 69 | [![GitHub logo](../assets/github.png) HookToEvents.ino](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/HookToEvents/HookToEvents.ino) 70 | -------------------------------------------------------------------------------- /docs/others/ota-configuration-updates.md: -------------------------------------------------------------------------------- 1 | # OTA updates 2 | 3 | Homie for ESP8266 supports OTA, if enabled in the configuration, and if a compatible OTA entity is set up. 4 | 5 | There's a script that does just that: 6 | 7 | [![GitHub logo](../assets/github.png) ota_updater.py](https://github.com/marvinroger/homie-esp8266/blob/develop/scripts/ota_updater) 8 | 9 | It works this way: 10 | 11 | 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 12 | 2. The OTA entity publishes the latest available firmware payload to `$implementation/ota/firmware/`, either as binary or as a Base64 encoded string 13 | * If OTA is disabled, Homie for ESP8266 reports `403` to `$implementation/ota/status` and aborts the OTA 14 | * 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 15 | * If the checksum is not a valid MD5, Homie for ESP8266 reports `400 BAD_CHECKSUM` to `$implementation/ota/status` and aborts the OTA 16 | 3. Homie starts to flash the firmware 17 | * The firmware is updating. Homie for ESP8266 reports progress with `206 /` 18 | * When all bytes are flashed, the firmware is verified (including the MD5 if one was set) 19 | * Homie for ESP8266 either reports `200` on success, `400` if the firmware in invalid or `500` if there's an internal error 20 | 5. Homie for ESP8266 reboots on success as soon as the device is idle 21 | 22 | See [Homie implementation specifics](homie-implementation-specifics.md) for more details on status codes. 23 | 24 | ## OTA entities projects 25 | 26 | See [Community projects](community-projects.md). 27 | 28 | # Configuration updates 29 | 30 | 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`: 31 | 32 | ```json 33 | { 34 | "name": "Kitchen light", 35 | "wifi": { 36 | "ssid": "Network_1", 37 | "password": "I'm a Wi-Fi password!" 38 | }, 39 | "mqtt": { 40 | "host": "192.168.1.20", 41 | "port": 1883 42 | }, 43 | "ota": { 44 | "enabled": false 45 | }, 46 | "settings": { 47 | 48 | } 49 | } 50 | ``` 51 | 52 | You can update the name and Wi-Fi password by sending the following incremental JSON: 53 | 54 | ```json 55 | { 56 | "name": "Living room light", 57 | "wifi": { 58 | "password": "I'am a new Wi-Fi password!" 59 | } 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /src/SendingPromise.cpp: -------------------------------------------------------------------------------- 1 | #include "SendingPromise.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | SendingPromise::SendingPromise() 6 | : _node(nullptr) 7 | , _property(nullptr) 8 | , _qos(0) 9 | , _retained(false) 10 | , _overwriteSetter(false) 11 | , _range { .isRange = false, .index = 0 } { 12 | } 13 | 14 | SendingPromise& SendingPromise::setQos(uint8_t qos) { 15 | _qos = qos; 16 | return *this; 17 | } 18 | 19 | SendingPromise& SendingPromise::setRetained(bool retained) { 20 | _retained = retained; 21 | return *this; 22 | } 23 | 24 | SendingPromise& SendingPromise::overwriteSetter(bool overwrite) { 25 | _overwriteSetter = overwrite; 26 | return *this; 27 | } 28 | 29 | SendingPromise& SendingPromise::setRange(const HomieRange& range) { 30 | _range = range; 31 | return *this; 32 | } 33 | 34 | SendingPromise& SendingPromise::setRange(uint16_t rangeIndex) { 35 | HomieRange range; 36 | range.isRange = true; 37 | range.index = rangeIndex; 38 | _range = range; 39 | return *this; 40 | } 41 | 42 | uint16_t SendingPromise::send(const String& value) { 43 | if (!Interface::get().ready) { 44 | Interface::get().getLogger() << F("✖ setNodeProperty(): impossible now") << endl; 45 | return 0; 46 | } 47 | 48 | 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 49 | strcpy(topic, Interface::get().getConfig().get().mqtt.baseTopic); 50 | strcat(topic, Interface::get().getConfig().get().deviceId); 51 | strcat_P(topic, PSTR("/")); 52 | strcat(topic, _node->getId()); 53 | strcat_P(topic, PSTR("/")); 54 | strcat(topic, _property->c_str()); 55 | 56 | if (_range.isRange) { 57 | char rangeStr[5 + 1]; // max 65536 58 | itoa(_range.index, rangeStr, 10); 59 | strcat_P(topic, PSTR("_")); 60 | strcat(topic, rangeStr); 61 | } 62 | 63 | uint16_t packetId = Interface::get().getMqttClient().publish(topic, _qos, _retained, value.c_str()); 64 | 65 | if (_overwriteSetter) { 66 | strcat_P(topic, PSTR("/set")); 67 | Interface::get().getMqttClient().publish(topic, 1, true, value.c_str()); 68 | } 69 | 70 | delete[] topic; 71 | 72 | return packetId; 73 | } 74 | 75 | SendingPromise& SendingPromise::setNode(const HomieNode& node) { 76 | _node = &node; 77 | return *this; 78 | } 79 | 80 | SendingPromise& SendingPromise::setProperty(const String& property) { 81 | _property = &property; 82 | return *this; 83 | } 84 | 85 | const HomieNode* SendingPromise::getNode() const { 86 | return _node; 87 | } 88 | 89 | const String* SendingPromise::getProperty() const { 90 | return _property; 91 | } 92 | 93 | uint8_t SendingPromise::getQos() const { 94 | return _qos; 95 | } 96 | 97 | HomieRange SendingPromise::getRange() const { 98 | return _range; 99 | } 100 | 101 | bool SendingPromise::isRetained() const { 102 | return _retained; 103 | } 104 | 105 | bool SendingPromise::doesOverwriteSetter() const { 106 | return _overwriteSetter; 107 | } 108 | -------------------------------------------------------------------------------- /src/Homie.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | #include 6 | #include "Homie/Datatypes/Interface.hpp" 7 | #include "Homie/Constants.hpp" 8 | #include "Homie/Limits.hpp" 9 | #include "Homie/Utils/DeviceId.hpp" 10 | #include "Homie/Boot/Boot.hpp" 11 | #include "Homie/Boot/BootStandalone.hpp" 12 | #include "Homie/Boot/BootNormal.hpp" 13 | #include "Homie/Boot/BootConfig.hpp" 14 | #include "Homie/Logger.hpp" 15 | #include "Homie/Config.hpp" 16 | #include "Homie/Blinker.hpp" 17 | 18 | #include "SendingPromise.hpp" 19 | #include "HomieBootMode.hpp" 20 | #include "HomieEvent.hpp" 21 | #include "HomieNode.hpp" 22 | #include "HomieSetting.hpp" 23 | #include "StreamingOperator.hpp" 24 | 25 | #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); 26 | #define Homie_setBrand(brand) const char* __FLAGGED_BRAND = "\xfb\x2a\xf5\x68\xc0" brand "\x6e\x2f\x0f\xeb\x2d"; Homie.__setBrand(__FLAGGED_BRAND); 27 | 28 | namespace HomieInternals { 29 | class HomieClass { 30 | friend class ::HomieNode; 31 | friend SendingPromise; 32 | 33 | public: 34 | HomieClass(); 35 | ~HomieClass(); 36 | void setup(); 37 | void loop(); 38 | 39 | void __setFirmware(const char* name, const char* version); 40 | void __setBrand(const char* brand) const; 41 | 42 | HomieClass& disableLogging(); 43 | HomieClass& setLoggingPrinter(Print* printer); 44 | HomieClass& disableLedFeedback(); 45 | HomieClass& setLedPin(uint8_t pin, uint8_t on); 46 | HomieClass& setConfigurationApPassword(const char* password); 47 | HomieClass& setGlobalInputHandler(const GlobalInputHandler& globalInputHandler); 48 | HomieClass& setBroadcastHandler(const BroadcastHandler& broadcastHandler); 49 | HomieClass& onEvent(const EventHandler& handler); 50 | HomieClass& setResetTrigger(uint8_t pin, uint8_t state, uint16_t time); 51 | HomieClass& disableResetTrigger(); 52 | HomieClass& setSetupFunction(const OperationFunction& function); 53 | HomieClass& setLoopFunction(const OperationFunction& function); 54 | HomieClass& setHomieBootMode(HomieBootMode bootMode); 55 | HomieClass& setHomieBootModeOnNextBoot(HomieBootMode bootMode); 56 | 57 | static void reset(); 58 | void reboot(); 59 | static void setIdle(bool idle); 60 | static bool isConfigured(); 61 | static bool isConnected(); 62 | static const ConfigStruct& getConfiguration(); 63 | AsyncMqttClient& getMqttClient(); 64 | Logger& getLogger(); 65 | static void prepareToSleep(); 66 | 67 | private: 68 | bool _setupCalled; 69 | bool _firmwareSet; 70 | Boot* _boot; 71 | BootStandalone _bootStandalone; 72 | BootNormal _bootNormal; 73 | BootConfig _bootConfig; 74 | bool _flaggedForReboot; 75 | SendingPromise _sendingPromise; 76 | Logger _logger; 77 | Blinker _blinker; 78 | Config _config; 79 | AsyncMqttClient _mqttClient; 80 | 81 | void _checkBeforeSetup(const __FlashStringHelper* functionName) const; 82 | 83 | const char* __HOMIE_SIGNATURE; 84 | }; 85 | } // namespace HomieInternals 86 | 87 | extern HomieInternals::HomieClass Homie; 88 | -------------------------------------------------------------------------------- /src/HomieNode.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Arduino.h" 6 | #include "StreamingOperator.hpp" 7 | #include "Homie/Datatypes/Interface.hpp" 8 | #include "Homie/Datatypes/Callbacks.hpp" 9 | #include "Homie/Limits.hpp" 10 | #include "HomieRange.hpp" 11 | 12 | class HomieNode; 13 | 14 | namespace HomieInternals { 15 | class HomieClass; 16 | class Property; 17 | class BootNormal; 18 | class BootConfig; 19 | class SendingPromise; 20 | 21 | class PropertyInterface { 22 | friend ::HomieNode; 23 | 24 | public: 25 | PropertyInterface(); 26 | 27 | void settable(const PropertyInputHandler& inputHandler = [](const HomieRange& range, const String& value) { return false; }); 28 | 29 | private: 30 | PropertyInterface& setProperty(Property* property); 31 | 32 | Property* _property; 33 | }; 34 | 35 | class Property { 36 | friend BootNormal; 37 | 38 | public: 39 | 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; } 40 | void settable(const PropertyInputHandler& inputHandler) { _settable = true; _inputHandler = inputHandler; } 41 | 42 | private: 43 | const char* getProperty() const { return _id; } 44 | bool isSettable() const { return _settable; } 45 | bool isRange() const { return _range; } 46 | uint16_t getLower() const { return _lower; } 47 | uint16_t getUpper() const { return _upper; } 48 | PropertyInputHandler getInputHandler() const { return _inputHandler; } 49 | const char* _id; 50 | bool _range; 51 | uint16_t _lower; 52 | uint16_t _upper; 53 | bool _settable; 54 | PropertyInputHandler _inputHandler; 55 | }; 56 | } // namespace HomieInternals 57 | 58 | class HomieNode { 59 | friend HomieInternals::HomieClass; 60 | friend HomieInternals::BootNormal; 61 | friend HomieInternals::BootConfig; 62 | 63 | public: 64 | HomieNode(const char* id, const char* type, const HomieInternals::NodeInputHandler& nodeInputHandler = [](const String& property, const HomieRange& range, const String& value) { return false; }); 65 | virtual ~HomieNode(); 66 | 67 | const char* getId() const { return _id; } 68 | const char* getType() const { return _type; } 69 | 70 | HomieInternals::PropertyInterface& advertise(const char* property); 71 | HomieInternals::PropertyInterface& advertiseRange(const char* property, uint16_t lower, uint16_t upper); 72 | 73 | HomieInternals::SendingPromise& setProperty(const String& property) const; 74 | 75 | protected: 76 | virtual void setup() {} 77 | virtual void loop() {} 78 | virtual void onReadyToOperate() {} 79 | virtual bool handleInput(const String& property, const HomieRange& range, const String& value); 80 | 81 | private: 82 | const std::vector& getProperties() const; 83 | 84 | static HomieNode* find(const char* id) { 85 | for (HomieNode* iNode : HomieNode::nodes) { 86 | if (strcmp(id, iNode->getId()) == 0) return iNode; 87 | } 88 | 89 | return 0; 90 | } 91 | 92 | const char* _id; 93 | const char* _type; 94 | std::vector _properties; 95 | HomieInternals::NodeInputHandler _inputHandler; 96 | 97 | HomieInternals::PropertyInterface _propertyInterface; 98 | 99 | static std::vector nodes; 100 | }; 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![homie-esp8266 banner](banner.png) 2 | 3 | Homie for ESP8266 4 | ================= 5 | 6 | [![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) 7 | 8 | An Arduino for ESP8266 implementation of [Homie](https://github.com/marvinroger/homie), an MQTT convention for the IoT. 9 | 10 | ## Note for v1.x users 11 | 12 | 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). 13 | 14 | ## Download 15 | 16 | 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). 17 | 18 | ## Features 19 | 20 | * Automatic connection/reconnection to Wi-Fi/MQTT 21 | * [JSON configuration file](http://marvinroger.github.io/homie-esp8266/develop/configuration/json-configuration-file) to configure the device 22 | * [Cute HTTP API / Web UI / App](http://marvinroger.github.io/homie-esp8266/develop/configuration/http-json-api) to remotely send the configuration to the device and get information about it 23 | * [Custom settings](http://marvinroger.github.io/homie-esp8266/develop/advanced-usage/custom-settings) 24 | * [OTA over MQTT](http://marvinroger.github.io/homie-esp8266/develop/others/ota-configuration-updates) 25 | * [Magic bytes](http://marvinroger.github.io/homie-esp8266/develop/advanced-usage/magic-bytes) 26 | * Available in the [PlatformIO registry](http://platformio.org/#!/lib/show/555/Homie) 27 | * Pretty straightforward sketches, a simple light for example: 28 | 29 | ```c++ 30 | #include 31 | 32 | const int PIN_RELAY = 5; 33 | 34 | HomieNode lightNode("light", "switch"); 35 | 36 | bool lightOnHandler(const HomieRange& range, const String& value) { 37 | if (value != "true" && value != "false") return false; 38 | 39 | bool on = (value == "true"); 40 | digitalWrite(PIN_RELAY, on ? HIGH : LOW); 41 | lightNode.setProperty("on").send(value); 42 | Homie.getLogger() << "Light is " << (on ? "on" : "off") << endl; 43 | 44 | return true; 45 | } 46 | 47 | void setup() { 48 | Serial.begin(115200); 49 | Serial << endl << endl; 50 | pinMode(PIN_RELAY, OUTPUT); 51 | digitalWrite(PIN_RELAY, LOW); 52 | 53 | Homie_setFirmware("awesome-relay", "1.0.0"); 54 | 55 | lightNode.advertise("on").settable(lightOnHandler); 56 | 57 | Homie.setup(); 58 | } 59 | 60 | void loop() { 61 | Homie.loop(); 62 | } 63 | ``` 64 | 65 | ## Requirements, installation and usage 66 | 67 | The project is documented on http://marvinroger.github.io/homie-esp8266/ with a *Getting started* guide and every piece of information you will need. 68 | 69 | ## Donate 70 | 71 | 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: 72 | 73 | [![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) 74 | -------------------------------------------------------------------------------- /src/Homie/Boot/BootNormal.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "../../HomieNode.hpp" 12 | #include "../../HomieRange.hpp" 13 | #include "../../StreamingOperator.hpp" 14 | #include "../Constants.hpp" 15 | #include "../Limits.hpp" 16 | #include "../Datatypes/Interface.hpp" 17 | #include "../Utils/Helpers.hpp" 18 | #include "../Uptime.hpp" 19 | #include "../Timer.hpp" 20 | #include "../ExponentialBackoffTimer.hpp" 21 | #include "Boot.hpp" 22 | 23 | namespace HomieInternals { 24 | class BootNormal : public Boot { 25 | public: 26 | BootNormal(); 27 | ~BootNormal(); 28 | void setup(); 29 | void loop(); 30 | 31 | private: 32 | struct AdvertisementProgress { 33 | bool done = false; 34 | enum class GlobalStep { 35 | PUB_HOMIE, 36 | PUB_MAC, 37 | PUB_NAME, 38 | PUB_LOCALIP, 39 | PUB_STATS_INTERVAL, 40 | PUB_FW_NAME, 41 | PUB_FW_VERSION, 42 | PUB_FW_CHECKSUM, 43 | PUB_IMPLEMENTATION, 44 | PUB_IMPLEMENTATION_CONFIG, 45 | PUB_IMPLEMENTATION_VERSION, 46 | PUB_IMPLEMENTATION_OTA_ENABLED, 47 | PUB_NODES, 48 | SUB_IMPLEMENTATION_OTA, 49 | SUB_IMPLEMENTATION_RESET, 50 | SUB_IMPLEMENTATION_CONFIG_SET, 51 | SUB_SET, 52 | SUB_BROADCAST, 53 | PUB_ONLINE 54 | } globalStep; 55 | 56 | enum class NodeStep { 57 | PUB_TYPE, 58 | PUB_PROPERTIES 59 | } nodeStep; 60 | 61 | size_t currentNodeIndex; 62 | } _advertisementProgress; 63 | Uptime _uptime; 64 | Timer _statsTimer; 65 | ExponentialBackoffTimer _mqttReconnectTimer; 66 | bool _setupFunctionCalled; 67 | WiFiEventHandler _wifiGotIpHandler; 68 | WiFiEventHandler _wifiDisconnectedHandler; 69 | bool _mqttConnectNotified; 70 | bool _mqttDisconnectNotified; 71 | bool _otaOngoing; 72 | bool _flaggedForReset; 73 | bool _flaggedForReboot; 74 | Bounce _resetDebouncer; 75 | uint16_t _mqttOfflineMessageId; 76 | char _fwChecksum[32 + 1]; 77 | bool _otaIsBase64; 78 | base64_decodestate _otaBase64State; 79 | size_t _otaBase64Pads; 80 | size_t _otaSizeTotal; 81 | size_t _otaSizeDone; 82 | 83 | std::unique_ptr _mqttTopic; 84 | 85 | std::unique_ptr _mqttClientId; 86 | std::unique_ptr _mqttWillTopic; 87 | std::unique_ptr _mqttPayloadBuffer; 88 | std::unique_ptr _mqttTopicLevels; 89 | uint8_t _mqttTopicLevelsCount; 90 | 91 | void _handleReset(); 92 | void _wifiConnect(); 93 | void _onWifiGotIp(const WiFiEventStationModeGotIP& event); 94 | void _onWifiDisconnected(const WiFiEventStationModeDisconnected& event); 95 | void _mqttConnect(); 96 | void _advertise(); 97 | void _onMqttConnected(); 98 | void _onMqttDisconnected(AsyncMqttClientDisconnectReason reason); 99 | void _onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total); 100 | void _onMqttPublish(uint16_t id); 101 | void _prefixMqttTopic(); 102 | char* _prefixMqttTopic(PGM_P topic); 103 | bool _publishOtaStatus(int status, const char* info = nullptr); 104 | bool _publishOtaStatus_P(int status, PGM_P info); 105 | void _endOtaUpdate(bool success, uint8_t update_error = UPDATE_ERROR_OK); 106 | void _stringToBytes(const char* str, char sep, byte* bytes, int maxBytes, int base); 107 | }; 108 | } // namespace HomieInternals 109 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/code 5 | docker: 6 | - image: circleci/python:2.7 7 | steps: 8 | - checkout 9 | - run: 10 | name: install PlatformIO 11 | command: sudo pip install platformio 12 | - run: 13 | name: install current code as a PlatformIO library with all dependencies 14 | command: platformio lib -g install file://. 15 | - run: 16 | name: install exemples dependencies 17 | command: platformio lib -g install Shutters@^2.1.1 SonoffDual@1.1.0 18 | - run: platformio ci ./examples/CustomSettings --board=esp01 --board=nodemcuv2 19 | - run: platformio ci ./examples/DoorSensor --board=esp01 --board=nodemcuv2 20 | - run: platformio ci ./examples/HookToEvents --board=esp01 --board=nodemcuv2 21 | - run: platformio ci ./examples/IteadSonoff --board=esp01 --board=nodemcuv2 22 | - run: platformio ci ./examples/LightOnOff --board=esp01 --board=nodemcuv2 23 | - run: platformio ci ./examples/TemperatureSensor --board=esp01 --board=nodemcuv2 24 | - run: platformio ci ./examples/LedStrip --board=esp01 --board=nodemcuv2 25 | - run: platformio ci ./examples/Broadcast --board=esp01 --board=nodemcuv2 26 | - run: platformio ci ./examples/GlobalInputHandler --board=esp01 --board=nodemcuv2 27 | - run: platformio ci ./examples/SonoffDualShutters --board=esp01 --board=nodemcuv2 28 | 29 | lint: 30 | working_directory: ~/code 31 | docker: 32 | - image: circleci/python:2.7 33 | steps: 34 | - checkout 35 | - run: 36 | name: install cpplint 37 | command: sudo pip install cpplint 38 | - run: make cpplint 39 | 40 | generate_docs: 41 | working_directory: ~/code 42 | docker: 43 | - image: circleci/python:2.7 44 | steps: 45 | - checkout 46 | - run: 47 | name: install dependencies 48 | command: sudo pip install mkdocs==0.16.3 mkdocs-material==1.7.1 pygments==2.2.0 pymdown-extensions==3.4 49 | - run: 50 | name: generate and publish docs 51 | command: | 52 | openssl aes-256-cbc -d -in ./.circleci/assets/id_rsa.enc -k "${PRIVATE_KEY_ENCRYPT_KEY}" >> /tmp/deploy_rsa 53 | eval "$(ssh-agent -s)" 54 | chmod 600 /tmp/deploy_rsa 55 | ssh-add /tmp/deploy_rsa 56 | 57 | chmod +x ./.circleci/assets/generate_docs.py 58 | ./.circleci/assets/generate_docs.py -o /tmp/site 59 | 60 | # make sure we ignore the gh-pages branch 61 | mkdir /tmp/site/.circleci 62 | cp ./.circleci/assets/circleci.ignore.yml /tmp/site/.circleci/config.yml 63 | 64 | pushd /tmp/site 65 | git init 66 | git config --global user.name "circleci" 67 | git config --global user.email "sayhi@circleci.com" 68 | git remote add origin git@github.com:marvinroger/homie-esp8266.git 69 | git add . 70 | git commit -m ":package: Result of CircleCI build ${CIRCLE_BUILD_URL}" 71 | git push -f origin master:gh-pages 72 | popd 73 | 74 | workflows: 75 | version: 2 76 | lint_build_generatedocs: 77 | jobs: 78 | - lint: 79 | filters: 80 | branches: 81 | ignore: 82 | - gh-pages 83 | - build: 84 | filters: 85 | branches: 86 | ignore: 87 | - gh-pages 88 | - generate_docs: 89 | filters: 90 | branches: 91 | ignore: 92 | - gh-pages 93 | -------------------------------------------------------------------------------- /examples/SonoffDualShutters/SonoffDualShutters.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | # Homie enabled Sonoff Dual shutters 4 | 5 | Requires the Shutters library: 6 | https://github.com/marvinroger/arduino-shutters 7 | and the SonoffDual library: 8 | https://github.com/marvinroger/arduino-sonoff-dual 9 | 10 | ## Features 11 | 12 | * Do a short press to close shutters 13 | if level != 0 or open shutters if level == 0 14 | * Do a long press to reset 15 | 16 | */ 17 | 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | const unsigned long COURSE_TIME = 30 * 1000; 25 | const float CALIBRATION_RATIO = 0.1; 26 | 27 | const bool RELAY1_MOVE = true; 28 | const bool RELAY1_STOP = false; 29 | 30 | const bool RELAY2_UP = true; 31 | const bool RELAY2_DOWN = false; 32 | 33 | const byte SHUTTERS_EEPROM_POSITION = 0; 34 | 35 | HomieNode shuttersNode("shutters", "shutters"); 36 | 37 | // Shutters 38 | 39 | void shuttersUp() { 40 | SonoffDual.setRelays(RELAY1_MOVE, RELAY2_UP); 41 | } 42 | 43 | void shuttersDown() { 44 | SonoffDual.setRelays(RELAY1_MOVE, RELAY2_DOWN); 45 | } 46 | 47 | void shuttersHalt() { 48 | SonoffDual.setRelays(RELAY1_STOP, false); 49 | } 50 | 51 | uint8_t shuttersGetState() { 52 | return EEPROM.read(SHUTTERS_EEPROM_POSITION); 53 | } 54 | 55 | void shuttersSetState(uint8_t state) { 56 | EEPROM.write(SHUTTERS_EEPROM_POSITION, state); 57 | EEPROM.commit(); 58 | } 59 | 60 | Shutters shutters(COURSE_TIME, shuttersUp, shuttersDown, shuttersHalt, shuttersGetState, shuttersSetState, CALIBRATION_RATIO, onShuttersLevelReached); 61 | 62 | void onShuttersLevelReached(uint8_t level) { 63 | if (shutters.isIdle()) Homie.setIdle(true); // if idle, we've reached our target 64 | if (Homie.isConnected()) shuttersNode.setProperty("level").send(String(level)); 65 | } 66 | 67 | // Homie 68 | 69 | void onHomieEvent(const HomieEvent& event) { 70 | switch (event.type) { 71 | case HomieEventType::ABOUT_TO_RESET: 72 | shutters.reset(); 73 | break; 74 | } 75 | } 76 | 77 | bool shuttersLevelHandler(const HomieRange& range, const String& value) { 78 | for (byte i = 0; i < value.length(); i++) { 79 | if (isDigit(value.charAt(i)) == false) return false; 80 | } 81 | 82 | const unsigned long numericValue = value.toInt(); 83 | if (numericValue > 100) return false; 84 | 85 | // wanted value is valid 86 | 87 | if (shutters.isIdle() && numericValue == shutters.getCurrentLevel()) return true; // nothing to do 88 | 89 | Homie.setIdle(false); 90 | shutters.setLevel(numericValue); 91 | 92 | return true; 93 | } 94 | 95 | // Logic 96 | 97 | void setup() { 98 | SonoffDual.setup(); 99 | EEPROM.begin(4); 100 | shutters.begin(); 101 | 102 | Homie_setFirmware("sonoff-dual-shutters", "1.0.0"); 103 | Homie.disableLogging(); 104 | Homie.disableResetTrigger(); 105 | Homie.setLedPin(SonoffDual.LED_PIN, SonoffDual.LED_ON); 106 | Homie.onEvent(onHomieEvent); 107 | 108 | shuttersNode.advertise("level").settable(shuttersLevelHandler); 109 | 110 | Homie.setup(); 111 | } 112 | 113 | void loop() { 114 | shutters.loop(); 115 | Homie.loop(); 116 | SonoffDualButton buttonState = SonoffDual.handleButton(); 117 | if (buttonState == SonoffDualButton::LONG) { 118 | Homie.reset(); 119 | } else if (buttonState == SonoffDualButton::SHORT && shutters.isIdle()) { 120 | Homie.setIdle(false); 121 | 122 | if (shutters.getCurrentLevel() == 100) { 123 | shutters.setLevel(0); 124 | } else { 125 | shutters.setLevel(100); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /scripts/ota_updater/ota_updater.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division, print_function 4 | import paho.mqtt.client as mqtt 5 | import base64, sys, math 6 | from hashlib import md5 7 | 8 | # The callback for when the client receives a CONNACK response from the server. 9 | def on_connect(client, userdata, flags, rc): 10 | if rc != 0: 11 | print("Connection Failed with result code {}".format(rc)) 12 | client.disconnect() 13 | else: 14 | print("Connected with result code {}".format(rc)) 15 | 16 | # calcluate firmware md5 17 | firmware_md5 = md5(userdata['firmware']).hexdigest() 18 | userdata.update({'md5': firmware_md5}) 19 | 20 | # Subscribing in on_connect() means that if we lose the connection and 21 | # reconnect then subscriptions will be renewed. 22 | client.subscribe("{base_topic}{device_id}/$implementation/ota/status".format(**userdata)) 23 | client.subscribe("{base_topic}{device_id}/$implementation/ota/enabled".format(**userdata)) 24 | client.subscribe("{base_topic}{device_id}/$fw/#".format(**userdata)) 25 | 26 | # Wait for device info to come in and invoke the on_message callback where update will continue 27 | print("Waiting for device info...") 28 | 29 | 30 | # The callback for when a PUBLISH message is received from the server. 31 | def on_message(client, userdata, msg): 32 | # decode string for python2/3 compatiblity 33 | msg.payload = msg.payload.decode() 34 | 35 | if msg.topic.endswith('$implementation/ota/status'): 36 | status = int(msg.payload.split()[0]) 37 | 38 | if userdata.get("published"): 39 | if status == 206: # in progress 40 | # state in progress, print progress bar 41 | progress, total = [int(x) for x in msg.payload.split()[1].split('/')] 42 | bar_width = 30 43 | bar = int(bar_width*(progress/total)) 44 | print("\r[", '+'*bar, ' '*(bar_width-bar), "] ", msg.payload.split()[1], end='', sep='') 45 | if (progress == total): 46 | print() 47 | sys.stdout.flush() 48 | elif status == 304: # not modified 49 | print("Device firmware already up to date with md5 checksum: {}".format(userdata.get('md5'))) 50 | client.disconnect() 51 | elif status == 403: # forbidden 52 | print("Device ota disabled, aborting...") 53 | client.disconnect() 54 | 55 | elif msg.topic.endswith('$fw/checksum'): 56 | checksum = msg.payload 57 | 58 | if userdata.get("published"): 59 | if checksum == userdata.get('md5'): 60 | print("Device back online. Update Successful!") 61 | else: 62 | print("Expecting checksum {}, got {}, update failed!".format(userdata.get('md5'), checksum)) 63 | client.disconnect() 64 | else: 65 | if checksum != userdata.get('md5'): # save old md5 for comparison with new firmware 66 | userdata.update({'old_md5': checksum}) 67 | else: 68 | print("Device firmware already up to date with md5 checksum: {}".format(checksum)) 69 | client.disconnect() 70 | 71 | elif msg.topic.endswith('ota/enabled'): 72 | if msg.payload == 'true': 73 | userdata.update({'ota_enabled': True}) 74 | else: 75 | print("Device ota disabled, aborting...") 76 | client.disconnect() 77 | 78 | if ( not userdata.get("published") ) and ( userdata.get('ota_enabled') ) and \ 79 | ( 'old_md5' in userdata.keys() ) and ( userdata.get('md5') != userdata.get('old_md5') ): 80 | # push the firmware binary 81 | userdata.update({"published": True}) 82 | topic = "{base_topic}{device_id}/$implementation/ota/firmware/{md5}".format(**userdata) 83 | print("Publishing new firmware with checksum {}".format(userdata.get('md5'))) 84 | client.publish(topic, userdata['firmware']) 85 | 86 | 87 | def main(broker_host, broker_port, broker_username, broker_password, base_topic, device_id, firmware): 88 | # initialise mqtt client and register callbacks 89 | client = mqtt.Client() 90 | client.on_connect = on_connect 91 | client.on_message = on_message 92 | 93 | # set username and password if given 94 | if broker_username and broker_password: 95 | client.username_pw_set(broker_username, broker_password) 96 | 97 | # save data to be used in the callbacks 98 | client.user_data_set({ 99 | "base_topic": base_topic, 100 | "device_id": device_id, 101 | "firmware": firmware 102 | }) 103 | 104 | # start connection 105 | print("Connecting to mqtt broker {} on port {}".format(broker_host, broker_port)) 106 | client.connect(broker_host, broker_port, 60) 107 | 108 | # Blocking call that processes network traffic, dispatches callbacks and handles reconnecting. 109 | client.loop_forever() 110 | 111 | 112 | if __name__ == '__main__': 113 | import argparse 114 | 115 | parser = argparse.ArgumentParser( 116 | description='ota firmware update scirpt for ESP8226 implemenation of the Homie mqtt IoT convention.') 117 | 118 | # ensure base topic always ends with a '/' 119 | def base_topic_arg(s): 120 | s = str(s) 121 | if not s.endswith('/'): 122 | s = s + '/' 123 | return s 124 | 125 | # specify arguments 126 | parser.add_argument('-l', '--broker-host', type=str, required=False, 127 | help='host name or ip address of the mqtt broker', default="127.0.0.1") 128 | parser.add_argument('-p', '--broker-port', type=int, required=False, 129 | help='port of the mqtt broker', default=1883) 130 | parser.add_argument('-u', '--broker-username', type=str, required=False, 131 | help='username used to authenticate with the mqtt broker') 132 | parser.add_argument('-d', '--broker-password', type=str, required=False, 133 | help='password used to authenticate with the mqtt broker') 134 | parser.add_argument('-t', '--base-topic', type=base_topic_arg, required=False, 135 | help='base topic of the homie devices on the broker', default="homie/") 136 | parser.add_argument('-i', '--device-id', type=str, required=True, 137 | help='homie device id') 138 | parser.add_argument('firmware', type=argparse.FileType('rb'), 139 | help='path to the firmware to be sent to the device') 140 | 141 | # workaround for http://bugs.python.org/issue9694 142 | parser._optionals.title = "arguments" 143 | 144 | # get and validate arguments 145 | args = parser.parse_args() 146 | 147 | # read the contents of firmware into buffer 148 | firmware = args.firmware.read() 149 | args.firmware.close() 150 | 151 | # Invoke the business logic 152 | main(args.broker_host, args.broker_port, args.broker_username, 153 | args.broker_password, args.base_topic, args.device_id, firmware) 154 | -------------------------------------------------------------------------------- /docs/configuration/http-json-api.md: -------------------------------------------------------------------------------- 1 | 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`. 2 | 3 | If you don't want to mess with JSON, you have a Web UI / app available: 4 | 5 | * At http://setup.homie-esp8266.marvinroger.fr/ 6 | * As an [Android app](https://build.phonegap.com/apps/1906578/share) 7 | 8 | **Quick instructions to use the Web UI / app**: 9 | 10 | 1. Open the Web UI / app 11 | 2. Disconnect from your current Wi-Fi AP, and connect to the `Homie-xxxxxxxxxxxx` AP spawned in `configuration` mode 12 | 3. Follow the instructions 13 | 14 | You can see the sources of the Web UI [here](https://github.com/marvinroger/homie-esp8266-setup). 15 | 16 | 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: 17 | 18 | ```shell 19 | curl -X PUT http://192.168.123.1/config --header "Content-Type: application/json" -d @config.json 20 | ``` 21 | 22 | This will send the `./config.json` file to the device. 23 | 24 | # Error handling 25 | 26 | When everything went fine, a `2xx` HTTP code is returned, such as `200 OK`, `202 Accepted`, `204 No Content` and so on. 27 | 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. 28 | 29 | # Endpoints 30 | 31 | **API base address:** `http://192.168.123.1` 32 | 33 | !!! summary "GET `/heart`" 34 | 35 | This is useful to ensure we are connected to the device AP. 36 | 37 | ## Response 38 | 39 | 204 No Content 40 | 41 | -------------- 42 | 43 | !!! summary "GET `/device-info`" 44 | 45 | 46 | Get some information on the device. 47 | 48 | ## Response 49 | 50 | 200 OK (application/json) 51 | 52 | ```json 53 | { 54 | "hardware_device_id": "52a8fa5d", 55 | "homie_esp8266_version": "2.0.0", 56 | "firmware": { 57 | "name": "awesome-device", 58 | "version": "1.0.0" 59 | }, 60 | "nodes": [ 61 | { 62 | "id": "light", 63 | "type": "light" 64 | } 65 | ], 66 | "settings": [ 67 | { 68 | "name": "timeout", 69 | "description": "Timeout in seconds", 70 | "type": "ulong", 71 | "required": false, 72 | "default": 10 73 | } 74 | ] 75 | } 76 | ``` 77 | 78 | **Note about settings:** If a setting is no required, the `default` field is always present. `type` can be one of the following: 79 | 80 | * `bool`: a boolean 81 | * `ulong`: an unsigned long 82 | * `long`: a long 83 | * `double`: a double 84 | * `string`: a string 85 | 86 | -------------- 87 | 88 | !!! summary "GET `/networks`" 89 | 90 | Retrieve the Wi-Fi networks the device can see. 91 | 92 | ## Response 93 | 94 | !!! success "In case of success" 95 | 96 | 200 OK (application/json) 97 | 98 | ```json 99 | { 100 | "networks": [ 101 | { "ssid": "Network_2", "rssi": -82, "encryption": "wep" }, 102 | { "ssid": "Network_1", "rssi": -57, "encryption": "wpa" }, 103 | { "ssid": "Network_3", "rssi": -65, "encryption": "wpa2" }, 104 | { "ssid": "Network_5", "rssi": -94, "encryption": "none" }, 105 | { "ssid": "Network_4", "rssi": -89, "encryption": "auto" } 106 | ] 107 | } 108 | ``` 109 | 110 | !!! failure "In case the initial Wi-Fi scan is not finished on the device" 111 | 112 | 503 Service Unavailable (application/json) 113 | 114 | ```json 115 | { 116 | "error": "Initial Wi-Fi scan not finished yet" 117 | } 118 | ``` 119 | 120 | -------------- 121 | 122 | !!! summary "PUT `/config`" 123 | 124 | Save the config to the device. 125 | 126 | ## Request body 127 | 128 | (application/json) 129 | 130 | See [JSON configuration file](json-configuration-file.md). 131 | 132 | ## Response 133 | 134 | !!! success "In case of success" 135 | 136 | 200 OK (application/json) 137 | 138 | ```json 139 | { 140 | "success": true 141 | } 142 | ``` 143 | 144 | !!! failure "In case of error in the payload" 145 | 146 | 400 Bad Request (application/json) 147 | 148 | ```json 149 | { 150 | "success": false, 151 | "error": "Reason why the payload is invalid" 152 | } 153 | ``` 154 | 155 | !!! failure "In case the device already received a valid configuration and is waiting for reboot" 156 | 157 | 403 Forbidden (application/json) 158 | 159 | ```json 160 | { 161 | "success": false, 162 | "error": "Device already configured" 163 | } 164 | ``` 165 | 166 | -------------- 167 | 168 | !!! summary "GET `/wifi/connect`" 169 | 170 | 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`. 171 | 172 | ## Request body 173 | 174 | (application/json) 175 | 176 | ```json 177 | { 178 | "ssid": "My_SSID", 179 | "password": "my-passw0rd" 180 | } 181 | ``` 182 | 183 | ## Response 184 | 185 | !!! success "In case of success" 186 | 187 | 202 Accepted (application/json) 188 | 189 | ```json 190 | { 191 | "success": true 192 | } 193 | ``` 194 | 195 | !!! failure "In case of error in the payload" 196 | 197 | 400 Bad Request (application/json) 198 | 199 | ```json 200 | { 201 | "success": false, 202 | "error": "Reason why the payload is invalid" 203 | } 204 | ``` 205 | 206 | -------------- 207 | 208 | !!! summary "GET `/wifi/status`" 209 | 210 | Returns the current Wi-Fi connection status. 211 | 212 | Helpful when monitoring Wi-Fi connectivity after `PUT /wifi/connect`. 213 | 214 | ## Response 215 | 216 | 200 OK (application/json) 217 | 218 | ```json 219 | { 220 | "status": "connected" 221 | } 222 | ``` 223 | 224 | `status` might be one of the following: 225 | 226 | * `idle` 227 | * `connect_failed` 228 | * `connection_lost` 229 | * `no_ssid_available` 230 | * `connected` along with a `local_ip` field 231 | * `disconnected` 232 | 233 | -------------- 234 | 235 | !!! summary "PUT `/proxy/control`" 236 | 237 | Enable/disable the device to act as a transparent proxy between AP and Station networks. 238 | 239 | 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. 240 | 241 | 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`). 242 | 243 | HTTPS is not supported. 244 | 245 | **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. 246 | 247 | ## Request body 248 | 249 | (application/json) 250 | 251 | ```json 252 | { 253 | "enable": true 254 | } 255 | ``` 256 | 257 | ## Response 258 | 259 | !!! success "In case of success" 260 | 261 | 200 OK (application/json) 262 | 263 | ```json 264 | { 265 | "success": true 266 | } 267 | ``` 268 | 269 | !!! failure "In case of error in the payload" 270 | 271 | 400 Bad Request (application/json) 272 | 273 | ```json 274 | { 275 | "success": false, 276 | "error": "Reason why the payload is invalid" 277 | } 278 | ``` 279 | -------------------------------------------------------------------------------- /docs/quickstart/getting-started.md: -------------------------------------------------------------------------------- 1 | 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). 2 | 3 | To use Homie for ESP8266, you will need: 4 | 5 | * An ESP8266 6 | * The Arduino IDE for ESP8266 (version 2.3.0 minimum) 7 | * Basic knowledge of the Arduino environment (upload a sketch, import libraries, ...) 8 | * To understand [the Homie convention](https://github.com/marvinroger/homie) 9 | 10 | ## Installing Homie for ESP8266 11 | 12 | There are two ways to install Homie for ESP8266. 13 | 14 | ### 1a. For the Arduino IDE 15 | 16 | There is a YouTube video with instructions: 17 | 18 | [![YouTube logo](../assets/youtube.png) How to install Homie libraries on Arduino IDE](https://www.youtube.com/watch?v=bH3KfFfYUvg) 19 | 20 | 1. Download the [release corresponding to this documentation version](https://github.com/marvinroger/homie-esp8266/releases) 21 | 22 | 2. Load the `.zip` with **Sketch → Include Library → Add .ZIP Library** 23 | 24 | Homie for ESP8266 has 4 dependencies: 25 | 26 | * [ArduinoJson](https://github.com/bblanchon/ArduinoJson) >= 5.0.8 27 | * [Bounce2](https://github.com/thomasfredericks/Bounce2) 28 | * [ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) >= [c8ed544](https://github.com/me-no-dev/ESPAsyncTCP) 29 | * [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) 30 | 31 | 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. 32 | 33 | ### 1b. With [PlatformIO](http://platformio.org) 34 | 35 | In a terminal, run `platformio lib install 555`. 36 | 37 | !!! warning "Not yet released as stable" 38 | 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**: 39 | 40 | ``` 41 | lib_deps = git+https://github.com/marvinroger/homie-esp8266.git#develop 42 | ``` 43 | 44 | Dependencies are installed automatically. 45 | 46 | ## Bare minimum sketch 47 | 48 | ```c++ 49 | #include 50 | 51 | void setup() { 52 | Serial.begin(115200); 53 | Serial << endl << endl; 54 | 55 | Homie_setFirmware("bare-minimum", "1.0.0"); // The underscore is not a typo! See Magic bytes 56 | Homie.setup(); 57 | } 58 | 59 | void loop() { 60 | Homie.loop(); 61 | } 62 | ``` 63 | 64 | 65 | This is the bare minimum needed for Homie for ESP8266 to work correctly. 66 | 67 | !!! tip "LED" 68 | ![Solid LED](../assets/led_solid.gif) 69 | If you upload this sketch, you will notice the LED of the ESP8266 will light on. This is because you are in `configuration` mode. 70 | 71 | Homie for ESP8266 has 3 modes of operation: 72 | 73 | 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. 74 | 75 | 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)). 76 | 77 | 3. The `standalone` mode. See [Standalone mode](../advanced-usage/standalone-mode.md). 78 | 79 | !!! warning 80 | **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. 81 | 82 | ## Connecting to the AP and configuring the device 83 | 84 | Homie for ESP8266 has spawned a secure AP named `Homie-xxxxxxxxxxxx`, like `Homie-c631f278df44`. Connect to it. 85 | 86 | !!! tip "Hardware device ID" 87 | 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. 88 | 89 | 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. 90 | 91 | ## Understanding what happens in `normal` mode 92 | 93 | ### Visual codes 94 | 95 | When the device boots in `normal` mode, it will start blinking: 96 | 97 | !!! tip "LED" 98 | ![Slowly blinking LED](../assets/led_wifi.gif) 99 | Slowly when connecting to the Wi-Fi 100 | 101 | !!! tip "LED" 102 | ![Fast blinking LED](../assets/led_mqtt.gif) 103 | Faster when connecting to the MQTT broker 104 | 105 | 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. 106 | 107 | ### Under the hood 108 | 109 | Although the sketch looks like it does not do anything, it actually does quite a lot: 110 | 111 | * It automatically connects to the Wi-Fi and MQTT broker. No more network boilerplate code 112 | * It exposes the Homie device on MQTT (as `/`, e.g. `homie/c631f278df44`) 113 | * It subscribes to the special OTA and configuration topics, automatically flashing a sketch if available or updating the configuration 114 | * It checks for a button press on the ESP8266, to return to `configuration` mode 115 | 116 | ## Creating an useful sketch 117 | 118 | Now that we understand how Homie for ESP8266 works, let's create an useful sketch. We want to create a smart light. 119 | 120 | [![GitHub logo](../assets/github.png) LightOnOff.ino](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/LightOnOff/LightOnOff.ino) 121 | 122 | Alright, step by step: 123 | 124 | 1. We create a node with an ID of `light` and a type of `switch` with `HomieNode lightNode("light", "switch")` 125 | 2. We set the name and the version of the firmware with `Homie_setFirmware("awesome-light" ,"1.0.0");` 126 | 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 127 | 4. In the `lightOnHandler` function, we want to update the state of the `light` node. We do this with `lightNode.setProperty("on").send("true");` 128 | 129 | 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? 130 | 131 | ## Creating a sensor node 132 | 133 | 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. 134 | 135 | Fortunately, Homie for ESP8266 provides an easy way to do that. 136 | 137 | [![GitHub logo](../assets/github.png) TemperatureSensor.ino](https://github.com/marvinroger/homie-esp8266/blob/develop/examples/TemperatureSensor/TemperatureSensor.ino) 138 | 139 | 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. 140 | 141 | 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. 142 | -------------------------------------------------------------------------------- /docs/others/cpp-api-reference.md: -------------------------------------------------------------------------------- 1 | # Homie 2 | 3 | You don't have to instantiate an `Homie` instance, it is done internally. 4 | 5 | ```c++ 6 | void setup(); 7 | ``` 8 | 9 | Setup Homie. 10 | 11 | !!! warning "Mandatory!" 12 | Must be called once in `setup()`. 13 | 14 | ```c++ 15 | void loop(); 16 | ``` 17 | 18 | Handle Homie work. 19 | 20 | !!! warning "Mandatory!" 21 | Must be called once in `loop()`. 22 | 23 | ## Functions to call *before* `Homie.setup()` 24 | 25 | ```c++ 26 | void Homie_setFirmware(const char* name, const char* version); 27 | // This is not a typo 28 | ``` 29 | 30 | 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. 31 | 32 | !!! warning "Mandatory!" 33 | You need to set the firmware for your sketch to work. 34 | 35 | 36 | * **`name`**: Name of the firmware. Default value is `undefined` 37 | * **`version`**: Version of the firmware. Default value is `undefined` 38 | 39 | ```c++ 40 | void Homie_setBrand(const char* name); 41 | // This is not a typo 42 | ``` 43 | 44 | Set the brand of the device, used in the configuration AP, the device hostname and the MQTT client ID. 45 | 46 | * **`name`**: Name of the brand. Default value is `Homie` 47 | 48 | ```c++ 49 | Homie& disableLogging(); 50 | ``` 51 | 52 | Disable Homie logging. 53 | 54 | ```c++ 55 | Homie& setLoggingPrinter(Print* printer); 56 | ``` 57 | 58 | Set the Print instance used for logging. 59 | 60 | * **`printer`**: Print instance to log to. By default, `Serial` is used 61 | 62 | !!! warning 63 | It's up to you to call `Serial.begin()` 64 | 65 | ```c++ 66 | Homie& disableLedFeedback(); 67 | ``` 68 | 69 | Disable the built-in LED feedback indicating the Homie for ESP8266 state. 70 | 71 | ```c++ 72 | Homie& setLedPin(uint8_t pin, uint8_t on); 73 | ``` 74 | 75 | Set pin of the LED to control. 76 | 77 | * **`pin`**: LED to control 78 | * **`on`**: state when the light is on (HIGH or LOW) 79 | 80 | ```c++ 81 | Homie& setConfigurationApPassword(const char* password); 82 | ``` 83 | 84 | Set the configuration AP password. 85 | 86 | * **`password`**: the configuration AP password 87 | 88 | ```c++ 89 | Homie& setGlobalInputHandler(std::function handler); 90 | ``` 91 | 92 | Set input handler for subscribed properties. 93 | 94 | * **`handler`**: Global input handler 95 | * **`node`**: Name of the node getting updated 96 | * **`property`**: Property of the node getting updated 97 | * **`range`**: Range of the property of the node getting updated 98 | * **`value`**: Value of the new property 99 | 100 | ```c++ 101 | Homie& setBroadcastHandler(std::function handler); 102 | ``` 103 | 104 | Set broadcast handler. 105 | 106 | * **`handler`**: Broadcast handler 107 | * **`level`**: Level of the broadcast 108 | * **`value`**: Value of the broadcast 109 | 110 | ```c++ 111 | Homie& onEvent(std::function callback); 112 | ``` 113 | 114 | Set the event handler. Useful if you want to hook to Homie events. 115 | 116 | * **`callback`**: Event handler 117 | 118 | ```c++ 119 | Homie& setResetTrigger(uint8_t pin, uint8_t state, uint16_t time); 120 | ``` 121 | 122 | Set the reset trigger. By default, the device will reset when pin `0` is `LOW` for `5000`ms. 123 | 124 | * **`pin`**: Pin of the reset trigger 125 | * **`state`**: Reset when the pin reaches this state for the given time 126 | * **`time`**: Time necessary to reset 127 | 128 | ```c++ 129 | Homie& disableResetTrigger(); 130 | ``` 131 | 132 | Disable the reset trigger. 133 | 134 | ```c++ 135 | Homie& setSetupFunction(std::function callback); 136 | ``` 137 | 138 | You can provide the function that will be called when operating in `normal` mode. 139 | 140 | * **`callback`**: Setup function 141 | 142 | ```c++ 143 | Homie& setLoopFunction(std::function callback); 144 | ``` 145 | 146 | You can provide the function that will be looped in normal mode. 147 | 148 | * **`callback`**: Loop function 149 | 150 | ```c++ 151 | Homie& setStandalone(); 152 | ``` 153 | 154 | 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. 155 | 156 | ## Functions to call *after* `Homie.setup()` 157 | 158 | ```c++ 159 | void reset(); 160 | ``` 161 | 162 | Flag the device for reset. 163 | 164 | ```c++ 165 | void setIdle(bool idle); 166 | ``` 167 | 168 | 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. 169 | 170 | * **`idle`**: Device in an idle state or not 171 | 172 | ```c++ 173 | void prepareToSleep(); 174 | ``` 175 | 176 | 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. 177 | 178 | ```c++ 179 | bool isConfigured() const; 180 | ``` 181 | 182 | Is the device in `normal` mode, configured? 183 | 184 | ```c++ 185 | bool isConnected() const; 186 | ``` 187 | 188 | Is the device in `normal` mode, configured and connected? 189 | 190 | ```c++ 191 | const ConfigStruct& getConfiguration() const; 192 | ``` 193 | 194 | Get the configuration struct. 195 | 196 | !!! danger 197 | Be careful with this struct, never attempt to change it. 198 | 199 | ```c++ 200 | AsyncMqttClient& getMqttClient(); 201 | ``` 202 | 203 | Get the underlying `AsyncMqttClient` object. 204 | 205 | ```c++ 206 | Logger& getLogger(); 207 | ``` 208 | 209 | Get the underlying `Logger` object, which is only a wrapper around `Serial` by default. 210 | 211 | ------- 212 | 213 | # HomieNode 214 | 215 | ```c++ 216 | HomieNode(const char* id, const char* type, std::function handler = ); 217 | ``` 218 | 219 | Constructor of an HomieNode object. 220 | 221 | * **`id`**: ID of the node 222 | * **`type`**: Type of the node 223 | * **`handler`**: Optional. Input handler of the node 224 | 225 | ```c++ 226 | const char* getId() const; 227 | ``` 228 | 229 | Return the ID of the node. 230 | 231 | ```c++ 232 | const char* getType() const; 233 | ``` 234 | 235 | Return the type of the node. 236 | 237 | ```c++ 238 | PropertyInterface& advertise(const char* property); 239 | PropertyInterface& advertiseRange(const char* property, uint16_t lower, uint16_t upper); 240 | ``` 241 | 242 | Advertise a property / range property on the node. 243 | 244 | * **`property`**: Property to advertise 245 | * **`lower`**: Lower bound of the range 246 | * **`upper`**: Upper bound of the range 247 | 248 | This returns a reference to `PropertyInterface` on which you can call: 249 | 250 | ```c++ 251 | void settable(std::function handler) = ); 252 | ``` 253 | 254 | Make the property settable. 255 | 256 | * **`handler`**: Optional. Input handler of the property 257 | 258 | ```c++ 259 | SendingPromise& setProperty(const String& property); 260 | ``` 261 | 262 | Using this function, you can set the value of a node property, like a temperature for example. 263 | 264 | * **`property`**: Property to send 265 | 266 | This returns a reference to `SendingPromise`, on which you can call: 267 | 268 | ```c++ 269 | SendingPromise& setQos(uint8_t qos); // defaults to 1 270 | SendingPromise& setRetained(bool retained); // defaults to true 271 | SendingPromise& overwriteSetter(bool overwrite); // defaults to false 272 | SendingPromise& setRange(const HomieRange& range); // defaults to not a range 273 | SendingPromise& setRange(uint16_t rangeIndex); // defaults to not a range 274 | uint16_t send(const String& value); // finally send the property, return the packetId (or 0 if failure) 275 | ``` 276 | 277 | Method names should be self-explanatory. 278 | 279 | # HomieSetting 280 | 281 | ```c++ 282 | HomieSetting(const char* name, const char* description); 283 | ``` 284 | 285 | Constructor of an HomieSetting object. 286 | 287 | * **`T`**: Type of the setting. Either `bool`, `unsigned long`, `long`, `double` or `const char*` 288 | * **`name`**: Name of the setting 289 | * **`description`**: Description of the setting 290 | 291 | ```c++ 292 | T get() const; 293 | ``` 294 | 295 | 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. 296 | 297 | ```c++ 298 | bool wasProvided() const; 299 | ``` 300 | 301 | Return whether the setting was provided or not (otherwise `get()` would return the default value). 302 | 303 | Set the default value and make the setting optional. 304 | 305 | ```c++ 306 | HomieSetting& setDefaultValue(T defaultValue); 307 | ``` 308 | 309 | * **`defaultValue`**: The default value 310 | 311 | ```c++ 312 | HomieSetting& setValidator(std::function validator); 313 | ``` 314 | 315 | Set a validation function for the setting. The validator must return `true` if the candidate is correct, `false` otherwise. 316 | 317 | * **`validator`**: The validation function 318 | -------------------------------------------------------------------------------- /src/Homie.cpp: -------------------------------------------------------------------------------- 1 | #include "Homie.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | HomieClass::HomieClass() 6 | : _setupCalled(false) 7 | , _firmwareSet(false) 8 | , __HOMIE_SIGNATURE("\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25") { 9 | strlcpy(Interface::get().brand, DEFAULT_BRAND, MAX_BRAND_LENGTH); 10 | Interface::get().bootMode = HomieBootMode::UNDEFINED; 11 | Interface::get().configurationAp.secured = false; 12 | Interface::get().led.enabled = true; 13 | Interface::get().led.pin = BUILTIN_LED; 14 | Interface::get().led.on = LOW; 15 | Interface::get().reset.idle = true; 16 | Interface::get().reset.enabled = true; 17 | Interface::get().reset.triggerPin = DEFAULT_RESET_PIN; 18 | Interface::get().reset.triggerState = DEFAULT_RESET_STATE; 19 | Interface::get().reset.triggerTime = DEFAULT_RESET_TIME; 20 | Interface::get().reset.flaggedBySketch = false; 21 | Interface::get().flaggedForSleep = false; 22 | Interface::get().globalInputHandler = [](const HomieNode& node, const String& property, const HomieRange& range, const String& value) { return false; }; 23 | Interface::get().broadcastHandler = [](const String& level, const String& value) { return false; }; 24 | Interface::get().setupFunction = []() {}; 25 | Interface::get().loopFunction = []() {}; 26 | Interface::get().eventHandler = [](const HomieEvent& event) {}; 27 | Interface::get().ready = false; 28 | Interface::get()._mqttClient = &_mqttClient; 29 | Interface::get()._sendingPromise = &_sendingPromise; 30 | Interface::get()._blinker = &_blinker; 31 | Interface::get()._logger = &_logger; 32 | Interface::get()._config = &_config; 33 | 34 | DeviceId::generate(); 35 | } 36 | 37 | HomieClass::~HomieClass() { 38 | } 39 | 40 | void HomieClass::_checkBeforeSetup(const __FlashStringHelper* functionName) const { 41 | if (_setupCalled) { 42 | String message; 43 | message.concat(F("✖ ")); 44 | message.concat(functionName); 45 | message.concat(F("(): has to be called before setup()")); 46 | Helpers::abort(message); 47 | } 48 | } 49 | 50 | void HomieClass::setup() { 51 | _setupCalled = true; 52 | 53 | // Check if firmware is set 54 | 55 | if (!_firmwareSet) { 56 | Helpers::abort(F("✖ Firmware name must be set before calling setup()")); 57 | return; // never reached, here for clarity 58 | } 59 | 60 | // Check if default settings values are valid 61 | 62 | bool defaultSettingsValuesValid = true; 63 | for (IHomieSetting* iSetting : IHomieSetting::settings) { 64 | if (iSetting->isBool()) { 65 | HomieSetting* setting = static_cast*>(iSetting); 66 | if (!setting->isRequired() && !setting->validate(setting->get())) { 67 | defaultSettingsValuesValid = false; 68 | break; 69 | } 70 | } else if (iSetting->isLong()) { 71 | HomieSetting* setting = static_cast*>(iSetting); 72 | if (!setting->isRequired() && !setting->validate(setting->get())) { 73 | defaultSettingsValuesValid = false; 74 | break; 75 | } 76 | } else if (iSetting->isDouble()) { 77 | HomieSetting* setting = static_cast*>(iSetting); 78 | if (!setting->isRequired() && !setting->validate(setting->get())) { 79 | defaultSettingsValuesValid = false; 80 | break; 81 | } 82 | } else if (iSetting->isConstChar()) { 83 | HomieSetting* setting = static_cast*>(iSetting); 84 | if (!setting->isRequired() && !setting->validate(setting->get())) { 85 | defaultSettingsValuesValid = false; 86 | break; 87 | } 88 | } 89 | } 90 | 91 | if (!defaultSettingsValuesValid) { 92 | Helpers::abort(F("✖ Default setting value does not pass validator test")); 93 | return; // never reached, here for clarity 94 | } 95 | 96 | // boot mode set during this boot by application before Homie.setup() 97 | HomieBootMode _applicationHomieBootMode = Interface::get().bootMode; 98 | 99 | // boot mode set before resetting the device. If application has defined a boot mode, this will be ignored 100 | HomieBootMode _nextHomieBootMode = Interface::get().getConfig().getHomieBootModeOnNextBoot(); 101 | if (_nextHomieBootMode != HomieBootMode::UNDEFINED) { 102 | Interface::get().getConfig().setHomieBootModeOnNextBoot(HomieBootMode::UNDEFINED); 103 | } 104 | 105 | HomieBootMode _selectedHomieBootMode = HomieBootMode::CONFIGURATION; 106 | 107 | // select boot mode source 108 | if (_applicationHomieBootMode != HomieBootMode::UNDEFINED) { 109 | _selectedHomieBootMode = _applicationHomieBootMode; 110 | } else if (_nextHomieBootMode != HomieBootMode::UNDEFINED) { 111 | _selectedHomieBootMode = _nextHomieBootMode; 112 | } else { 113 | _selectedHomieBootMode = HomieBootMode::NORMAL; 114 | } 115 | 116 | // validate selected mode and fallback as needed 117 | if (_selectedHomieBootMode == HomieBootMode::NORMAL && !Interface::get().getConfig().load()) { 118 | Interface::get().getLogger() << F("Configuration invalid. Using CONFIG MODE") << endl; 119 | _selectedHomieBootMode = HomieBootMode::CONFIGURATION; 120 | } 121 | 122 | // run selected mode 123 | if (_selectedHomieBootMode == HomieBootMode::NORMAL) { 124 | _boot = &_bootNormal; 125 | Interface::get().event.type = HomieEventType::NORMAL_MODE; 126 | Interface::get().eventHandler(Interface::get().event); 127 | 128 | } else if (_selectedHomieBootMode == HomieBootMode::CONFIGURATION) { 129 | _boot = &_bootConfig; 130 | Interface::get().event.type = HomieEventType::CONFIGURATION_MODE; 131 | Interface::get().eventHandler(Interface::get().event); 132 | 133 | } else if (_selectedHomieBootMode == HomieBootMode::STANDALONE) { 134 | _boot = &_bootStandalone; 135 | Interface::get().event.type = HomieEventType::STANDALONE_MODE; 136 | Interface::get().eventHandler(Interface::get().event); 137 | 138 | } else { 139 | Helpers::abort(F("✖ Boot mode invalid")); 140 | return; // never reached, here for clarity 141 | } 142 | 143 | _boot->setup(); 144 | } 145 | 146 | void HomieClass::loop() { 147 | _boot->loop(); 148 | 149 | if (_flaggedForReboot && Interface::get().reset.idle) { 150 | Interface::get().getLogger() << F("Device is idle") << endl; 151 | Interface::get().getLogger() << F("Triggering ABOUT_TO_RESET event...") << endl; 152 | Interface::get().event.type = HomieEventType::ABOUT_TO_RESET; 153 | Interface::get().eventHandler(Interface::get().event); 154 | 155 | Interface::get().getLogger() << F("↻ Rebooting device...") << endl; 156 | Serial.flush(); 157 | ESP.restart(); 158 | } 159 | } 160 | 161 | HomieClass& HomieClass::disableLogging() { 162 | _checkBeforeSetup(F("disableLogging")); 163 | 164 | Interface::get().getLogger().setLogging(false); 165 | 166 | return *this; 167 | } 168 | 169 | HomieClass& HomieClass::setLoggingPrinter(Print* printer) { 170 | _checkBeforeSetup(F("setLoggingPrinter")); 171 | 172 | Interface::get().getLogger().setPrinter(printer); 173 | 174 | return *this; 175 | } 176 | 177 | HomieClass& HomieClass::disableLedFeedback() { 178 | _checkBeforeSetup(F("disableLedFeedback")); 179 | 180 | Interface::get().led.enabled = false; 181 | 182 | return *this; 183 | } 184 | 185 | HomieClass& HomieClass::setLedPin(uint8_t pin, uint8_t on) { 186 | _checkBeforeSetup(F("setLedPin")); 187 | 188 | Interface::get().led.pin = pin; 189 | Interface::get().led.on = on; 190 | 191 | return *this; 192 | } 193 | 194 | HomieClass& HomieClass::setConfigurationApPassword(const char* password) { 195 | _checkBeforeSetup(F("setConfigurationApPassword")); 196 | 197 | Interface::get().configurationAp.secured = true; 198 | strlcpy(Interface::get().configurationAp.password, password, MAX_WIFI_PASSWORD_LENGTH); 199 | return *this; 200 | } 201 | 202 | void HomieClass::__setFirmware(const char* name, const char* version) { 203 | _checkBeforeSetup(F("setFirmware")); 204 | if (strlen(name) + 1 - 10 > MAX_FIRMWARE_NAME_LENGTH || strlen(version) + 1 - 10 > MAX_FIRMWARE_VERSION_LENGTH) { 205 | Helpers::abort(F("✖ setFirmware(): either the name or version string is too long")); 206 | return; // never reached, here for clarity 207 | } 208 | 209 | strncpy(Interface::get().firmware.name, name + 5, strlen(name) - 10); 210 | Interface::get().firmware.name[strlen(name) - 10] = '\0'; 211 | strncpy(Interface::get().firmware.version, version + 5, strlen(version) - 10); 212 | Interface::get().firmware.version[strlen(version) - 10] = '\0'; 213 | _firmwareSet = true; 214 | } 215 | 216 | void HomieClass::__setBrand(const char* brand) const { 217 | _checkBeforeSetup(F("setBrand")); 218 | if (strlen(brand) + 1 - 10 > MAX_BRAND_LENGTH) { 219 | Helpers::abort(F("✖ setBrand(): the brand string is too long")); 220 | return; // never reached, here for clarity 221 | } 222 | 223 | strncpy(Interface::get().brand, brand + 5, strlen(brand) - 10); 224 | Interface::get().brand[strlen(brand) - 10] = '\0'; 225 | } 226 | 227 | void HomieClass::reset() { 228 | Interface::get().reset.flaggedBySketch = true; 229 | } 230 | 231 | void HomieClass::reboot() { 232 | _flaggedForReboot = true; 233 | } 234 | 235 | void HomieClass::setIdle(bool idle) { 236 | Interface::get().reset.idle = idle; 237 | } 238 | 239 | HomieClass& HomieClass::setGlobalInputHandler(const GlobalInputHandler& globalInputHandler) { 240 | _checkBeforeSetup(F("setGlobalInputHandler")); 241 | 242 | Interface::get().globalInputHandler = globalInputHandler; 243 | 244 | return *this; 245 | } 246 | 247 | HomieClass& HomieClass::setBroadcastHandler(const BroadcastHandler& broadcastHandler) { 248 | _checkBeforeSetup(F("setBroadcastHandler")); 249 | 250 | Interface::get().broadcastHandler = broadcastHandler; 251 | 252 | return *this; 253 | } 254 | 255 | HomieClass& HomieClass::setSetupFunction(const OperationFunction& function) { 256 | _checkBeforeSetup(F("setSetupFunction")); 257 | 258 | Interface::get().setupFunction = function; 259 | 260 | return *this; 261 | } 262 | 263 | HomieClass& HomieClass::setLoopFunction(const OperationFunction& function) { 264 | _checkBeforeSetup(F("setLoopFunction")); 265 | 266 | Interface::get().loopFunction = function; 267 | 268 | return *this; 269 | } 270 | 271 | HomieClass& HomieClass::setHomieBootMode(HomieBootMode bootMode) { 272 | _checkBeforeSetup(F("setHomieBootMode")); 273 | Interface::get().bootMode = bootMode; 274 | return *this; 275 | } 276 | 277 | HomieClass& HomieClass::setHomieBootModeOnNextBoot(HomieBootMode bootMode) { 278 | Interface::get().getConfig().setHomieBootModeOnNextBoot(bootMode); 279 | return *this; 280 | } 281 | 282 | bool HomieClass::isConfigured() { 283 | return Interface::get().getConfig().load(); 284 | } 285 | 286 | bool HomieClass::isConnected() { 287 | return Interface::get().ready; 288 | } 289 | 290 | HomieClass& HomieClass::onEvent(const EventHandler& handler) { 291 | _checkBeforeSetup(F("onEvent")); 292 | 293 | Interface::get().eventHandler = handler; 294 | 295 | return *this; 296 | } 297 | 298 | HomieClass& HomieClass::setResetTrigger(uint8_t pin, uint8_t state, uint16_t time) { 299 | _checkBeforeSetup(F("setResetTrigger")); 300 | 301 | Interface::get().reset.enabled = true; 302 | Interface::get().reset.triggerPin = pin; 303 | Interface::get().reset.triggerState = state; 304 | Interface::get().reset.triggerTime = time; 305 | 306 | return *this; 307 | } 308 | 309 | HomieClass& HomieClass::disableResetTrigger() { 310 | _checkBeforeSetup(F("disableResetTrigger")); 311 | 312 | Interface::get().reset.enabled = false; 313 | 314 | return *this; 315 | } 316 | 317 | const ConfigStruct& HomieClass::getConfiguration() { 318 | return Interface::get().getConfig().get(); 319 | } 320 | 321 | AsyncMqttClient& HomieClass::getMqttClient() { 322 | return _mqttClient; 323 | } 324 | 325 | Logger& HomieClass::getLogger() { 326 | return _logger; 327 | } 328 | 329 | void HomieClass::prepareToSleep() { 330 | if (Interface::get().ready) { 331 | Interface::get().flaggedForSleep = true; 332 | } else { 333 | Interface::get().getLogger() << F("Triggering READY_TO_SLEEP event...") << endl; 334 | Interface::get().event.type = HomieEventType::READY_TO_SLEEP; 335 | Interface::get().eventHandler(Interface::get().event); 336 | } 337 | } 338 | 339 | HomieClass Homie; 340 | -------------------------------------------------------------------------------- /src/Homie/Config.cpp: -------------------------------------------------------------------------------- 1 | #include "Config.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | Config::Config() 6 | : _configStruct() 7 | , _spiffsBegan(false) 8 | , _valid(false) { 9 | } 10 | 11 | bool Config::_spiffsBegin() { 12 | if (!_spiffsBegan) { 13 | _spiffsBegan = SPIFFS.begin(); 14 | if (!_spiffsBegan) Interface::get().getLogger() << F("✖ Cannot mount filesystem") << endl; 15 | } 16 | 17 | return _spiffsBegan; 18 | } 19 | 20 | bool Config::load() { 21 | if (!_spiffsBegin()) { return false; } 22 | 23 | _valid = false; 24 | 25 | if (!SPIFFS.exists(CONFIG_FILE_PATH)) { 26 | Interface::get().getLogger() << F("✖ ") << CONFIG_FILE_PATH << F(" doesn't exist") << endl; 27 | return false; 28 | } 29 | 30 | File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); 31 | if (!configFile) { 32 | Interface::get().getLogger() << F("✖ Cannot open config file") << endl; 33 | return false; 34 | } 35 | 36 | size_t configSize = configFile.size(); 37 | 38 | if (configSize > MAX_JSON_CONFIG_FILE_SIZE) { 39 | Interface::get().getLogger() << F("✖ Config file too big") << endl; 40 | return false; 41 | } 42 | 43 | char buf[MAX_JSON_CONFIG_FILE_SIZE]; 44 | configFile.readBytes(buf, configSize); 45 | configFile.close(); 46 | buf[configSize] = '\0'; 47 | 48 | StaticJsonBuffer jsonBuffer; 49 | JsonObject& parsedJson = jsonBuffer.parseObject(buf); 50 | if (!parsedJson.success()) { 51 | Interface::get().getLogger() << F("✖ Invalid JSON in the config file") << endl; 52 | return false; 53 | } 54 | 55 | ConfigValidationResult configValidationResult = Validation::validateConfig(parsedJson); 56 | if (!configValidationResult.valid) { 57 | Interface::get().getLogger() << F("✖ Config file is not valid, reason: ") << configValidationResult.reason << endl; 58 | return false; 59 | } 60 | 61 | const char* reqName = parsedJson["name"]; 62 | const char* reqWifiSsid = parsedJson["wifi"]["ssid"]; 63 | const char* reqWifiPassword = parsedJson["wifi"]["password"]; 64 | 65 | const char* reqMqttHost = parsedJson["mqtt"]["host"]; 66 | const char* reqDeviceId = DeviceId::get(); 67 | if (parsedJson.containsKey("device_id")) { 68 | reqDeviceId = parsedJson["device_id"]; 69 | } 70 | 71 | const char* reqWifiBssid = ""; 72 | if (parsedJson["wifi"].as().containsKey("bssid")) { 73 | reqWifiBssid = parsedJson["wifi"]["bssid"]; 74 | } 75 | uint16_t reqWifiChannel = 0; 76 | if (parsedJson["wifi"].as().containsKey("channel")) { 77 | reqWifiChannel = parsedJson["wifi"]["channel"]; 78 | } 79 | const char* reqWifiIp = ""; 80 | if (parsedJson["wifi"].as().containsKey("ip")) { 81 | reqWifiIp = parsedJson["wifi"]["ip"]; 82 | } 83 | const char* reqWifiMask = ""; 84 | if (parsedJson["wifi"].as().containsKey("mask")) { 85 | reqWifiMask = parsedJson["wifi"]["mask"]; 86 | } 87 | const char* reqWifiGw = ""; 88 | if (parsedJson["wifi"].as().containsKey("gw")) { 89 | reqWifiGw = parsedJson["wifi"]["gw"]; 90 | } 91 | const char* reqWifiDns1 = ""; 92 | if (parsedJson["wifi"].as().containsKey("dns1")) { 93 | reqWifiDns1 = parsedJson["wifi"]["dns1"]; 94 | } 95 | const char* reqWifiDns2 = ""; 96 | if (parsedJson["wifi"].as().containsKey("dns2")) { 97 | reqWifiDns2 = parsedJson["wifi"]["dns2"]; 98 | } 99 | 100 | uint16_t reqMqttPort = DEFAULT_MQTT_PORT; 101 | if (parsedJson["mqtt"].as().containsKey("port")) { 102 | reqMqttPort = parsedJson["mqtt"]["port"]; 103 | } 104 | const char* reqMqttBaseTopic = DEFAULT_MQTT_BASE_TOPIC; 105 | if (parsedJson["mqtt"].as().containsKey("base_topic")) { 106 | reqMqttBaseTopic = parsedJson["mqtt"]["base_topic"]; 107 | } 108 | bool reqMqttAuth = false; 109 | if (parsedJson["mqtt"].as().containsKey("auth")) { 110 | reqMqttAuth = parsedJson["mqtt"]["auth"]; 111 | } 112 | const char* reqMqttUsername = ""; 113 | if (parsedJson["mqtt"].as().containsKey("username")) { 114 | reqMqttUsername = parsedJson["mqtt"]["username"]; 115 | } 116 | const char* reqMqttPassword = ""; 117 | if (parsedJson["mqtt"].as().containsKey("password")) { 118 | reqMqttPassword = parsedJson["mqtt"]["password"]; 119 | } 120 | 121 | bool reqOtaEnabled = false; 122 | if (parsedJson["ota"].as().containsKey("enabled")) { 123 | reqOtaEnabled = parsedJson["ota"]["enabled"]; 124 | } 125 | 126 | strlcpy(_configStruct.name, reqName, MAX_FRIENDLY_NAME_LENGTH); 127 | strlcpy(_configStruct.deviceId, reqDeviceId, MAX_DEVICE_ID_LENGTH); 128 | strlcpy(_configStruct.wifi.ssid, reqWifiSsid, MAX_WIFI_SSID_LENGTH); 129 | if (reqWifiPassword) strlcpy(_configStruct.wifi.password, reqWifiPassword, MAX_WIFI_PASSWORD_LENGTH); 130 | strlcpy(_configStruct.wifi.bssid, reqWifiBssid, MAX_MAC_STRING_LENGTH + 6); 131 | _configStruct.wifi.channel = reqWifiChannel; 132 | strlcpy(_configStruct.wifi.ip, reqWifiIp, MAX_IP_STRING_LENGTH); 133 | strlcpy(_configStruct.wifi.gw, reqWifiGw, MAX_IP_STRING_LENGTH); 134 | strlcpy(_configStruct.wifi.mask, reqWifiMask, MAX_IP_STRING_LENGTH); 135 | strlcpy(_configStruct.wifi.dns1, reqWifiDns1, MAX_IP_STRING_LENGTH); 136 | strlcpy(_configStruct.wifi.dns2, reqWifiDns2, MAX_IP_STRING_LENGTH); 137 | strlcpy(_configStruct.mqtt.server.host, reqMqttHost, MAX_HOSTNAME_LENGTH); 138 | _configStruct.mqtt.server.port = reqMqttPort; 139 | strlcpy(_configStruct.mqtt.baseTopic, reqMqttBaseTopic, MAX_MQTT_BASE_TOPIC_LENGTH); 140 | _configStruct.mqtt.auth = reqMqttAuth; 141 | strlcpy(_configStruct.mqtt.username, reqMqttUsername, MAX_MQTT_CREDS_LENGTH); 142 | strlcpy(_configStruct.mqtt.password, reqMqttPassword, MAX_MQTT_CREDS_LENGTH); 143 | _configStruct.ota.enabled = reqOtaEnabled; 144 | 145 | /* Parse the settings */ 146 | 147 | JsonObject& settingsObject = parsedJson["settings"].as(); 148 | 149 | for (IHomieSetting* iSetting : IHomieSetting::settings) { 150 | if (iSetting->isBool()) { 151 | HomieSetting* setting = static_cast*>(iSetting); 152 | 153 | if (settingsObject.containsKey(setting->getName())) { 154 | setting->set(settingsObject[setting->getName()].as()); 155 | } 156 | } else if (iSetting->isLong()) { 157 | HomieSetting* setting = static_cast*>(iSetting); 158 | 159 | if (settingsObject.containsKey(setting->getName())) { 160 | setting->set(settingsObject[setting->getName()].as()); 161 | } 162 | } else if (iSetting->isDouble()) { 163 | HomieSetting* setting = static_cast*>(iSetting); 164 | 165 | if (settingsObject.containsKey(setting->getName())) { 166 | setting->set(settingsObject[setting->getName()].as()); 167 | } 168 | } else if (iSetting->isConstChar()) { 169 | HomieSetting* setting = static_cast*>(iSetting); 170 | 171 | if (settingsObject.containsKey(setting->getName())) { 172 | setting->set(strdup(settingsObject[setting->getName()].as())); 173 | } 174 | } 175 | } 176 | 177 | _valid = true; 178 | return true; 179 | } 180 | 181 | char* Config::getSafeConfigFile() const { 182 | File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); 183 | size_t configSize = configFile.size(); 184 | 185 | char buf[MAX_JSON_CONFIG_FILE_SIZE]; 186 | configFile.readBytes(buf, configSize); 187 | configFile.close(); 188 | buf[configSize] = '\0'; 189 | 190 | StaticJsonBuffer jsonBuffer; 191 | JsonObject& parsedJson = jsonBuffer.parseObject(buf); 192 | parsedJson["wifi"].as().remove("password"); 193 | parsedJson["mqtt"].as().remove("username"); 194 | parsedJson["mqtt"].as().remove("password"); 195 | 196 | size_t jsonBufferLength = parsedJson.measureLength() + 1; 197 | std::unique_ptr jsonString(new char[jsonBufferLength]); 198 | parsedJson.printTo(jsonString.get(), jsonBufferLength); 199 | 200 | return strdup(jsonString.get()); 201 | } 202 | 203 | void Config::erase() { 204 | if (!_spiffsBegin()) { return; } 205 | 206 | SPIFFS.remove(CONFIG_FILE_PATH); 207 | SPIFFS.remove(CONFIG_NEXT_BOOT_MODE_FILE_PATH); 208 | } 209 | 210 | void Config::setHomieBootModeOnNextBoot(HomieBootMode bootMode) { 211 | if (!_spiffsBegin()) { return; } 212 | 213 | if (bootMode == HomieBootMode::UNDEFINED) { 214 | SPIFFS.remove(CONFIG_NEXT_BOOT_MODE_FILE_PATH); 215 | } else { 216 | File bootModeFile = SPIFFS.open(CONFIG_NEXT_BOOT_MODE_FILE_PATH, "w"); 217 | if (!bootModeFile) { 218 | Interface::get().getLogger() << F("✖ Cannot open NEXTMODE file") << endl; 219 | return; 220 | } 221 | 222 | bootModeFile.printf("#%d", bootMode); 223 | bootModeFile.close(); 224 | Interface::get().getLogger().printf("Setting next boot mode to %d\n", bootMode); 225 | } 226 | } 227 | 228 | HomieBootMode Config::getHomieBootModeOnNextBoot() { 229 | if (!_spiffsBegin()) { return HomieBootMode::UNDEFINED; } 230 | 231 | File bootModeFile = SPIFFS.open(CONFIG_NEXT_BOOT_MODE_FILE_PATH, "r"); 232 | if (bootModeFile) { 233 | int v = bootModeFile.parseInt(); 234 | bootModeFile.close(); 235 | return static_cast(v); 236 | } else { 237 | return HomieBootMode::UNDEFINED; 238 | } 239 | } 240 | 241 | void Config::write(const JsonObject& config) { 242 | if (!_spiffsBegin()) { return; } 243 | 244 | SPIFFS.remove(CONFIG_FILE_PATH); 245 | 246 | File configFile = SPIFFS.open(CONFIG_FILE_PATH, "w"); 247 | if (!configFile) { 248 | Interface::get().getLogger() << F("✖ Cannot open config file") << endl; 249 | return; 250 | } 251 | 252 | config.printTo(configFile); 253 | configFile.close(); 254 | } 255 | 256 | bool Config::patch(const char* patch) { 257 | if (!_spiffsBegin()) { return false; } 258 | 259 | StaticJsonBuffer patchJsonBuffer; 260 | JsonObject& patchObject = patchJsonBuffer.parseObject(patch); 261 | 262 | if (!patchObject.success()) { 263 | Interface::get().getLogger() << F("✖ Invalid or too big JSON") << endl; 264 | return false; 265 | } 266 | 267 | File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); 268 | if (!configFile) { 269 | Interface::get().getLogger() << F("✖ Cannot open config file") << endl; 270 | return false; 271 | } 272 | 273 | size_t configSize = configFile.size(); 274 | 275 | char configJson[MAX_JSON_CONFIG_FILE_SIZE]; 276 | configFile.readBytes(configJson, configSize); 277 | configFile.close(); 278 | configJson[configSize] = '\0'; 279 | 280 | StaticJsonBuffer configJsonBuffer; 281 | JsonObject& configObject = configJsonBuffer.parseObject(configJson); 282 | 283 | for (JsonObject::iterator it = patchObject.begin(); it != patchObject.end(); ++it) { 284 | if (patchObject[it->key].is()) { 285 | JsonObject& subObject = patchObject[it->key].as(); 286 | for (JsonObject::iterator it2 = subObject.begin(); it2 != subObject.end(); ++it2) { 287 | if (!configObject.containsKey(it->key) || !configObject[it->key].is()) { 288 | String error = "✖ Config does not contain a "; 289 | error.concat(it->key); 290 | error.concat(" object"); 291 | Interface::get().getLogger() << error << endl; 292 | return false; 293 | } 294 | JsonObject& subConfigObject = configObject[it->key].as(); 295 | subConfigObject[it2->key] = it2->value; 296 | } 297 | } else { 298 | configObject[it->key] = it->value; 299 | } 300 | } 301 | 302 | ConfigValidationResult configValidationResult = Validation::validateConfig(configObject); 303 | if (!configValidationResult.valid) { 304 | Interface::get().getLogger() << F("✖ Config file is not valid, reason: ") << configValidationResult.reason << endl; 305 | return false; 306 | } 307 | 308 | write(configObject); 309 | 310 | return true; 311 | } 312 | 313 | bool Config::isValid() const { 314 | return this->_valid; 315 | } 316 | 317 | void Config::log() const { 318 | Interface::get().getLogger() << F("{} Stored configuration") << endl; 319 | Interface::get().getLogger() << F(" • Hardware device ID: ") << DeviceId::get() << endl; 320 | Interface::get().getLogger() << F(" • Device ID: ") << _configStruct.deviceId << endl; 321 | Interface::get().getLogger() << F(" • Name: ") << _configStruct.name << endl; 322 | 323 | Interface::get().getLogger() << F(" • Wi-Fi: ") << endl; 324 | Interface::get().getLogger() << F(" ◦ SSID: ") << _configStruct.wifi.ssid << endl; 325 | Interface::get().getLogger() << F(" ◦ Password not shown") << endl; 326 | if (strcmp_P(_configStruct.wifi.ip, PSTR("")) != 0) { 327 | Interface::get().getLogger() << F(" ◦ IP: ") << _configStruct.wifi.ip << endl; 328 | Interface::get().getLogger() << F(" ◦ Mask: ") << _configStruct.wifi.mask << endl; 329 | Interface::get().getLogger() << F(" ◦ Gateway: ") << _configStruct.wifi.gw << endl; 330 | } 331 | Interface::get().getLogger() << F(" • MQTT: ") << endl; 332 | Interface::get().getLogger() << F(" ◦ Host: ") << _configStruct.mqtt.server.host << endl; 333 | Interface::get().getLogger() << F(" ◦ Port: ") << _configStruct.mqtt.server.port << endl; 334 | Interface::get().getLogger() << F(" ◦ Base topic: ") << _configStruct.mqtt.baseTopic << endl; 335 | Interface::get().getLogger() << F(" ◦ Auth? ") << (_configStruct.mqtt.auth ? F("yes") : F("no")) << endl; 336 | if (_configStruct.mqtt.auth) { 337 | Interface::get().getLogger() << F(" ◦ Username: ") << _configStruct.mqtt.username << endl; 338 | Interface::get().getLogger() << F(" ◦ Password not shown") << endl; 339 | } 340 | 341 | Interface::get().getLogger() << F(" • OTA: ") << endl; 342 | Interface::get().getLogger() << F(" ◦ Enabled? ") << (_configStruct.ota.enabled ? F("yes") : F("no")) << endl; 343 | 344 | if (IHomieSetting::settings.size() > 0) { 345 | Interface::get().getLogger() << F(" • Custom settings: ") << endl; 346 | for (IHomieSetting* iSetting : IHomieSetting::settings) { 347 | Interface::get().getLogger() << F(" ◦ "); 348 | 349 | if (iSetting->isBool()) { 350 | HomieSetting* setting = static_cast*>(iSetting); 351 | Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); 352 | } else if (iSetting->isLong()) { 353 | HomieSetting* setting = static_cast*>(iSetting); 354 | Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); 355 | } else if (iSetting->isDouble()) { 356 | HomieSetting* setting = static_cast*>(iSetting); 357 | Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); 358 | } else if (iSetting->isConstChar()) { 359 | HomieSetting* setting = static_cast*>(iSetting); 360 | Interface::get().getLogger() << setting->getName() << F(": ") << setting->get() << F(" (") << (setting->wasProvided() ? F("set") : F("default")) << F(")"); 361 | } 362 | 363 | Interface::get().getLogger() << endl; 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/Homie/Utils/Validation.cpp: -------------------------------------------------------------------------------- 1 | #include "Validation.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | ConfigValidationResult Validation::validateConfig(const JsonObject& object) { 6 | ConfigValidationResult result; 7 | result = _validateConfigRoot(object); 8 | if (!result.valid) return result; 9 | result = _validateConfigWifi(object); 10 | if (!result.valid) return result; 11 | result = _validateConfigMqtt(object); 12 | if (!result.valid) return result; 13 | result = _validateConfigOta(object); 14 | if (!result.valid) return result; 15 | result = _validateConfigSettings(object); 16 | if (!result.valid) return result; 17 | 18 | result.valid = true; 19 | return result; 20 | } 21 | 22 | ConfigValidationResult Validation::_validateConfigRoot(const JsonObject& object) { 23 | ConfigValidationResult result; 24 | result.valid = false; 25 | if (!object.containsKey("name") || !object["name"].is()) { 26 | result.reason = F("name is not a string"); 27 | return result; 28 | } 29 | if (strlen(object["name"]) + 1 > MAX_FRIENDLY_NAME_LENGTH) { 30 | result.reason = F("name is too long"); 31 | return result; 32 | } 33 | if (object.containsKey("device_id")) { 34 | if (!object["device_id"].is()) { 35 | result.reason = F("device_id is not a string"); 36 | return result; 37 | } 38 | if (strlen(object["device_id"]) + 1 > MAX_DEVICE_ID_LENGTH) { 39 | result.reason = F("device_id is too long"); 40 | return result; 41 | } 42 | } 43 | 44 | const char* name = object["name"]; 45 | 46 | if (strcmp_P(name, PSTR("")) == 0) { 47 | result.reason = F("name is empty"); 48 | return result; 49 | } 50 | 51 | result.valid = true; 52 | return result; 53 | } 54 | 55 | ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) { 56 | ConfigValidationResult result; 57 | result.valid = false; 58 | 59 | if (!object.containsKey("wifi") || !object["wifi"].is()) { 60 | result.reason = F("wifi is not an object"); 61 | return result; 62 | } 63 | if (!object["wifi"].as().containsKey("ssid") || !object["wifi"]["ssid"].is()) { 64 | result.reason = F("wifi.ssid is not a string"); 65 | return result; 66 | } 67 | if (strlen(object["wifi"]["ssid"]) + 1 > MAX_WIFI_SSID_LENGTH) { 68 | result.reason = F("wifi.ssid is too long"); 69 | return result; 70 | } 71 | if (!object["wifi"].as().containsKey("password") || !object["wifi"]["password"].is()) { 72 | result.reason = F("wifi.password is not a string"); 73 | return result; 74 | } 75 | if (object["wifi"]["password"] && strlen(object["wifi"]["password"]) + 1 > MAX_WIFI_PASSWORD_LENGTH) { 76 | result.reason = F("wifi.password is too long"); 77 | return result; 78 | } 79 | // by benzino 80 | if (object["wifi"].as().containsKey("bssid") && !object["wifi"]["bssid"].is()) { 81 | result.reason = F("wifi.bssid is not a string"); 82 | return result; 83 | } 84 | if ( (object["wifi"].as().containsKey("bssid") && !object["wifi"].as().containsKey("channel")) || 85 | (!object["wifi"].as().containsKey("bssid") && object["wifi"].as().containsKey("channel")) ) { 86 | result.reason = F("wifi.channel_bssid channel and BSSID is required"); 87 | return result; 88 | } 89 | if (object["wifi"].as().containsKey("bssid") && !Helpers::validateMacAddress(object["wifi"].as().get("bssid"))) { 90 | result.reason = F("wifi.bssid is not valid mac"); 91 | return result; 92 | } 93 | if (object["wifi"].as().containsKey("channel") && !object["wifi"]["channel"].is()) { 94 | result.reason = F("wifi.channel is not an integer"); 95 | return result; 96 | } 97 | IPAddress ipAddress; 98 | if (object["wifi"].as().containsKey("ip") && !object["wifi"]["ip"].is()) { 99 | result.reason = F("wifi.ip is not a string"); 100 | return result; 101 | } 102 | if (object["wifi"]["ip"] && strlen(object["wifi"]["ip"]) + 1 > MAX_IP_STRING_LENGTH) { 103 | result.reason = F("wifi.ip is too long"); 104 | return result; 105 | } 106 | if (object["wifi"]["ip"] && !ipAddress.fromString(object["wifi"].as().get("ip"))) { 107 | result.reason = F("wifi.ip is not valid ip address"); 108 | return result; 109 | } 110 | if (object["wifi"].as().containsKey("mask") && !object["wifi"]["mask"].is()) { 111 | result.reason = F("wifi.mask is not a string"); 112 | return result; 113 | } 114 | if (object["wifi"]["mask"] && strlen(object["wifi"]["mask"]) + 1 > MAX_IP_STRING_LENGTH) { 115 | result.reason = F("wifi.mask is too long"); 116 | return result; 117 | } 118 | if (object["wifi"]["mask"] && !ipAddress.fromString(object["wifi"].as().get("mask"))) { 119 | result.reason = F("wifi.mask is not valid mask"); 120 | return result; 121 | } 122 | if (object["wifi"].as().containsKey("gw") && !object["wifi"]["gw"].is()) { 123 | result.reason = F("wifi.gw is not a string"); 124 | return result; 125 | } 126 | if (object["wifi"]["gw"] && strlen(object["wifi"]["gw"]) + 1 > MAX_IP_STRING_LENGTH) { 127 | result.reason = F("wifi.gw is too long"); 128 | return result; 129 | } 130 | if (object["wifi"]["gw"] && !ipAddress.fromString(object["wifi"].as().get("gw"))) { 131 | result.reason = F("wifi.gw is not valid gateway address"); 132 | return result; 133 | } 134 | if ( (object["wifi"].as().containsKey("ip") && (!object["wifi"].as().containsKey("mask") || !object["wifi"].as().containsKey("gw"))) || 135 | (object["wifi"].as().containsKey("gw") && (!object["wifi"].as().containsKey("mask") || !object["wifi"].as().containsKey("ip"))) || 136 | (object["wifi"].as().containsKey("mask") && (!object["wifi"].as().containsKey("ip") || !object["wifi"].as().containsKey("gw")))) { 137 | result.reason = F("wifi.staticip ip, gw and mask is required"); 138 | return result; 139 | } 140 | if (object["wifi"].as().containsKey("dns1") && !object["wifi"]["dns1"].is()) { 141 | result.reason = F("wifi.dns1 is not a string"); 142 | return result; 143 | } 144 | if (object["wifi"]["dns1"] && strlen(object["wifi"]["dns1"]) + 1 > MAX_IP_STRING_LENGTH) { 145 | result.reason = F("wifi.dns1 is too long"); 146 | return result; 147 | } 148 | if (object["wifi"]["dns1"] && !ipAddress.fromString(object["wifi"].as().get("dns1"))) { 149 | result.reason = F("wifi.dns1 is not valid dns address"); 150 | return result; 151 | } 152 | if (object["wifi"].as().containsKey("dns2") && !object["wifi"].as().containsKey("dns1")) { 153 | result.reason = F("wifi.dns2 no dns1 defined"); 154 | return result; 155 | } 156 | if (object["wifi"].as().containsKey("dns2") && !object["wifi"]["dns2"].is()) { 157 | result.reason = F("wifi.dns2 is not a string"); 158 | return result; 159 | } 160 | if (object["wifi"]["dns2"] && strlen(object["wifi"]["dns2"]) + 1 > MAX_IP_STRING_LENGTH) { 161 | result.reason = F("wifi.dns2 is too long"); 162 | return result; 163 | } 164 | if (object["wifi"]["dns2"] && !ipAddress.fromString(object["wifi"].as().get("dns2"))) { 165 | result.reason = F("wifi.dns2 is not valid dns address"); 166 | return result; 167 | } 168 | 169 | const char* wifiSsid = object["wifi"]["ssid"]; 170 | if (strcmp_P(wifiSsid, PSTR("")) == 0) { 171 | result.reason = F("wifi.ssid is empty"); 172 | return result; 173 | } 174 | 175 | result.valid = true; 176 | return result; 177 | } 178 | 179 | ConfigValidationResult Validation::_validateConfigMqtt(const JsonObject& object) { 180 | ConfigValidationResult result; 181 | result.valid = false; 182 | 183 | if (!object.containsKey("mqtt") || !object["mqtt"].is()) { 184 | result.reason = F("mqtt is not an object"); 185 | return result; 186 | } 187 | if (!object["mqtt"].as().containsKey("host") || !object["mqtt"]["host"].is()) { 188 | result.reason = F("mqtt.host is not a string"); 189 | return result; 190 | } 191 | if (strlen(object["mqtt"]["host"]) + 1 > MAX_HOSTNAME_LENGTH) { 192 | result.reason = F("mqtt.host is too long"); 193 | return result; 194 | } 195 | if (object["mqtt"].as().containsKey("port") && !object["mqtt"]["port"].is()) { 196 | result.reason = F("mqtt.port is not an integer"); 197 | return result; 198 | } 199 | if (object["mqtt"].as().containsKey("base_topic")) { 200 | if (!object["mqtt"]["base_topic"].is()) { 201 | result.reason = F("mqtt.base_topic is not a string"); 202 | return result; 203 | } 204 | 205 | if (strlen(object["mqtt"]["base_topic"]) + 1 > MAX_MQTT_BASE_TOPIC_LENGTH) { 206 | result.reason = F("mqtt.base_topic is too long"); 207 | return result; 208 | } 209 | } 210 | if (object["mqtt"].as().containsKey("auth")) { 211 | if (!object["mqtt"]["auth"].is()) { 212 | result.reason = F("mqtt.auth is not a boolean"); 213 | return result; 214 | } 215 | 216 | if (object["mqtt"]["auth"]) { 217 | if (!object["mqtt"].as().containsKey("username") || !object["mqtt"]["username"].is()) { 218 | result.reason = F("mqtt.username is not a string"); 219 | return result; 220 | } 221 | if (strlen(object["mqtt"]["username"]) + 1 > MAX_MQTT_CREDS_LENGTH) { 222 | result.reason = F("mqtt.username is too long"); 223 | return result; 224 | } 225 | if (!object["mqtt"].as().containsKey("password") || !object["mqtt"]["password"].is()) { 226 | result.reason = F("mqtt.password is not a string"); 227 | return result; 228 | } 229 | if (strlen(object["mqtt"]["password"]) + 1 > MAX_MQTT_CREDS_LENGTH) { 230 | result.reason = F("mqtt.password is too long"); 231 | return result; 232 | } 233 | } 234 | } 235 | 236 | const char* host = object["mqtt"]["host"]; 237 | if (strcmp_P(host, PSTR("")) == 0) { 238 | result.reason = F("mqtt.host is empty"); 239 | return result; 240 | } 241 | 242 | result.valid = true; 243 | return result; 244 | } 245 | 246 | ConfigValidationResult Validation::_validateConfigOta(const JsonObject& object) { 247 | ConfigValidationResult result; 248 | result.valid = false; 249 | 250 | if (!object.containsKey("ota") || !object["ota"].is()) { 251 | result.reason = F("ota is not an object"); 252 | return result; 253 | } 254 | if (!object["ota"].as().containsKey("enabled") || !object["ota"]["enabled"].is()) { 255 | result.reason = F("ota.enabled is not a boolean"); 256 | return result; 257 | } 258 | 259 | result.valid = true; 260 | return result; 261 | } 262 | 263 | ConfigValidationResult Validation::_validateConfigSettings(const JsonObject& object) { 264 | ConfigValidationResult result; 265 | result.valid = false; 266 | 267 | StaticJsonBuffer<0> emptySettingsBuffer; 268 | 269 | JsonObject* settingsObject = &(emptySettingsBuffer.createObject()); 270 | 271 | if (object.containsKey("settings") && object["settings"].is()) { 272 | settingsObject = &(object["settings"].as()); 273 | } 274 | 275 | for (IHomieSetting* iSetting : IHomieSetting::settings) { 276 | if (iSetting->isBool()) { 277 | HomieSetting* setting = static_cast*>(iSetting); 278 | 279 | if (settingsObject->containsKey(setting->getName())) { 280 | if (!(*settingsObject)[setting->getName()].is()) { 281 | result.reason = String(setting->getName()); 282 | result.reason.concat(F(" setting is not a boolean")); 283 | return result; 284 | } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { 285 | result.reason = String(setting->getName()); 286 | result.reason.concat(F(" setting does not pass the validator function")); 287 | return result; 288 | } 289 | } else if (setting->isRequired()) { 290 | result.reason = String(setting->getName()); 291 | result.reason.concat(F(" setting is missing")); 292 | return result; 293 | } 294 | } else if (iSetting->isLong()) { 295 | HomieSetting* setting = static_cast*>(iSetting); 296 | 297 | if (settingsObject->containsKey(setting->getName())) { 298 | if (!(*settingsObject)[setting->getName()].is()) { 299 | result.reason = String(setting->getName()); 300 | result.reason.concat(F(" setting is not a long")); 301 | return result; 302 | } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { 303 | result.reason = String(setting->getName()); 304 | result.reason.concat(F(" setting does not pass the validator function")); 305 | return result; 306 | } 307 | } else if (setting->isRequired()) { 308 | result.reason = String(setting->getName()); 309 | result.reason.concat(F(" setting is missing")); 310 | return result; 311 | } 312 | } else if (iSetting->isDouble()) { 313 | HomieSetting* setting = static_cast*>(iSetting); 314 | 315 | if (settingsObject->containsKey(setting->getName())) { 316 | if (!(*settingsObject)[setting->getName()].is()) { 317 | result.reason = String(setting->getName()); 318 | result.reason.concat(F(" setting is not a double")); 319 | return result; 320 | } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { 321 | result.reason = String(setting->getName()); 322 | result.reason.concat((" setting does not pass the validator function")); 323 | return result; 324 | } 325 | } else if (setting->isRequired()) { 326 | result.reason = String(setting->getName()); 327 | result.reason.concat(F(" setting is missing")); 328 | return result; 329 | } 330 | } else if (iSetting->isConstChar()) { 331 | HomieSetting* setting = static_cast*>(iSetting); 332 | 333 | if (settingsObject->containsKey(setting->getName())) { 334 | if (!(*settingsObject)[setting->getName()].is()) { 335 | result.reason = String(setting->getName()); 336 | result.reason.concat(F(" setting is not a const char*")); 337 | return result; 338 | } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { 339 | result.reason = String(setting->getName()); 340 | result.reason.concat(F(" setting does not pass the validator function")); 341 | return result; 342 | } 343 | } else if (setting->isRequired()) { 344 | result.reason = String(setting->getName()); 345 | result.reason.concat(F(" setting is missing")); 346 | return result; 347 | } 348 | } 349 | } 350 | 351 | result.valid = true; 352 | return result; 353 | } 354 | 355 | // bool Validation::_validateConfigWifiBssid(const char *mac) { 356 | // int i = 0; 357 | // int s = 0; 358 | // while (*mac) { 359 | // if (isxdigit(*mac)) { 360 | // i++; 361 | // } 362 | // else if (*mac == ':' || *mac == '-') { 363 | // if (i == 0 || i / 2 - 1 != s) 364 | // break; 365 | // ++s; 366 | // } 367 | // else { 368 | // s = -1; 369 | // } 370 | // ++mac; 371 | // } 372 | // return (i == MAX_MAC_STRING_LENGTH && s == 5); 373 | // } 374 | -------------------------------------------------------------------------------- /src/Homie/Boot/BootConfig.cpp: -------------------------------------------------------------------------------- 1 | #include "BootConfig.hpp" 2 | 3 | using namespace HomieInternals; 4 | 5 | BootConfig::BootConfig() 6 | : Boot("config") 7 | , _http(80) 8 | , _ssidCount(0) 9 | , _wifiScanAvailable(false) 10 | , _lastWifiScanEnded(true) 11 | , _jsonWifiNetworks() 12 | , _flaggedForReboot(false) 13 | , _flaggedForRebootAt(0) 14 | , _proxyEnabled(false) 15 | , _apIpStr {'\0'} { 16 | _wifiScanTimer.setInterval(CONFIG_SCAN_INTERVAL); 17 | } 18 | 19 | BootConfig::~BootConfig() { 20 | } 21 | 22 | void BootConfig::setup() { 23 | Boot::setup(); 24 | 25 | if (Interface::get().led.enabled) { 26 | digitalWrite(Interface::get().led.pin, Interface::get().led.on); 27 | } 28 | 29 | Interface::get().getLogger() << F("Device ID is ") << DeviceId::get() << endl; 30 | 31 | WiFi.mode(WIFI_AP_STA); 32 | 33 | char apName[MAX_WIFI_SSID_LENGTH]; 34 | strlcpy(apName, Interface::get().brand, MAX_WIFI_SSID_LENGTH - 1 - MAX_MAC_STRING_LENGTH); 35 | strcat_P(apName, PSTR("-")); 36 | strcat(apName, DeviceId::get()); 37 | 38 | WiFi.softAPConfig(ACCESS_POINT_IP, ACCESS_POINT_IP, IPAddress(255, 255, 255, 0)); 39 | if (Interface::get().configurationAp.secured) { 40 | WiFi.softAP(apName, Interface::get().configurationAp.password); 41 | } else { 42 | WiFi.softAP(apName); 43 | } 44 | 45 | snprintf(_apIpStr, MAX_IP_STRING_LENGTH, "%d.%d.%d.%d", ACCESS_POINT_IP[0], ACCESS_POINT_IP[1], ACCESS_POINT_IP[2], ACCESS_POINT_IP[3]); 46 | 47 | Interface::get().getLogger() << F("AP started as ") << apName << F(" with IP ") << _apIpStr << endl; 48 | _dns.setTTL(30); 49 | _dns.setErrorReplyCode(DNSReplyCode::NoError); 50 | _dns.start(53, F("*"), ACCESS_POINT_IP); 51 | 52 | _http.on("/heart", HTTP_GET, [this]() { 53 | Interface::get().getLogger() << F("Received heart request") << endl; 54 | _http.send(204); 55 | }); 56 | _http.on("/device-info", HTTP_GET, std::bind(&BootConfig::_onDeviceInfoRequest, this)); 57 | _http.on("/networks", HTTP_GET, std::bind(&BootConfig::_onNetworksRequest, this)); 58 | _http.on("/config", HTTP_PUT, std::bind(&BootConfig::_onConfigRequest, this)); 59 | _http.on("/config", HTTP_OPTIONS, [this]() { // CORS 60 | Interface::get().getLogger() << F("Received CORS request for /config") << endl; 61 | _http.sendContent(FPSTR(PROGMEM_CONFIG_CORS)); 62 | }); 63 | _http.on("/wifi/connect", HTTP_PUT, std::bind(&BootConfig::_onWifiConnectRequest, this)); 64 | _http.on("/wifi/connect", HTTP_OPTIONS, [this]() { // CORS 65 | Interface::get().getLogger() << F("Received CORS request for /wifi/connect") << endl; 66 | _http.sendContent(FPSTR(PROGMEM_CONFIG_CORS)); 67 | }); 68 | _http.on("/wifi/status", HTTP_GET, std::bind(&BootConfig::_onWifiStatusRequest, this)); 69 | _http.on("/proxy/control", HTTP_PUT, std::bind(&BootConfig::_onProxyControlRequest, this)); 70 | _http.onNotFound(std::bind(&BootConfig::_onCaptivePortal, this)); 71 | _http.begin(); 72 | } 73 | 74 | void BootConfig::_onWifiConnectRequest() { 75 | Interface::get().getLogger() << F("Received Wi-Fi connect request") << endl; 76 | StaticJsonBuffer parseJsonBuffer; 77 | std::unique_ptr bodyString = Helpers::cloneString(_http.arg("plain")); 78 | JsonObject& parsedJson = parseJsonBuffer.parseObject(bodyString.get()); 79 | if (!parsedJson.success()) { 80 | Interface::get().getLogger() << F("✖ Invalid or too big JSON") << endl; 81 | String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); 82 | errorJson.concat(F("Invalid or too big JSON\"}")); 83 | _http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); 84 | return; 85 | } 86 | 87 | if (!parsedJson.containsKey("ssid") || !parsedJson["ssid"].is() || !parsedJson.containsKey("password") || !parsedJson["password"].is()) { 88 | Interface::get().getLogger() << F("✖ SSID and password required") << endl; 89 | String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); 90 | errorJson.concat(F("SSID and password required\"}")); 91 | _http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); 92 | return; 93 | } 94 | 95 | Interface::get().getLogger() << F("Connecting to Wi-Fi") << endl; 96 | WiFi.begin(parsedJson["ssid"].as(), parsedJson["password"].as()); 97 | _http.send(202, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), F("{\"success\":true}")); 98 | } 99 | 100 | void BootConfig::_onWifiStatusRequest() { 101 | Interface::get().getLogger() << F("Received Wi-Fi status request") << endl; 102 | String json = ""; 103 | switch (WiFi.status()) { 104 | case WL_IDLE_STATUS: 105 | json = F("{\"status\":\"idle\"}"); 106 | break; 107 | case WL_CONNECT_FAILED: 108 | json = F("{\"status\":\"connect_failed\"}"); 109 | break; 110 | case WL_CONNECTION_LOST: 111 | json = F("{\"status\":\"connection_lost\"}"); 112 | break; 113 | case WL_NO_SSID_AVAIL: 114 | json = F("{\"status\":\"no_ssid_available\"}"); 115 | break; 116 | case WL_CONNECTED: 117 | json = "{\"status\":\"connected\",\"local_ip\":\"" + WiFi.localIP().toString() + "\"}"; 118 | break; 119 | case WL_DISCONNECTED: 120 | json = F("{\"status\":\"disconnected\"}"); 121 | break; 122 | default: 123 | json = F("{\"status\":\"other\"}"); 124 | break; 125 | } 126 | 127 | _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), json); 128 | } 129 | 130 | void BootConfig::_onProxyControlRequest() { 131 | Interface::get().getLogger() << F("Received proxy control request") << endl; 132 | StaticJsonBuffer parseJsonBuffer; 133 | std::unique_ptr bodyString = Helpers::cloneString(_http.arg("plain")); 134 | JsonObject& parsedJson = parseJsonBuffer.parseObject(bodyString.get()); // do not use plain String, else fails 135 | if (!parsedJson.success()) { 136 | Interface::get().getLogger() << F("✖ Invalid or too big JSON") << endl; 137 | String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); 138 | errorJson.concat(F("Invalid or too big JSON\"}")); 139 | _http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); 140 | return; 141 | } 142 | 143 | if (!parsedJson.containsKey("enable") || !parsedJson["enable"].is()) { 144 | Interface::get().getLogger() << F("✖ enable parameter is required") << endl; 145 | String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); 146 | errorJson.concat(F("enable parameter is required\"}")); 147 | _http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); 148 | return; 149 | } 150 | 151 | _proxyEnabled = parsedJson["enable"]; 152 | _http.send(202, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), F("{\"success\":true}")); 153 | } 154 | 155 | void BootConfig::_generateNetworksJson() { 156 | DynamicJsonBuffer generatedJsonBuffer(JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(_ssidCount) + (_ssidCount * JSON_OBJECT_SIZE(3))); // 1 at root, 3 in childrend 157 | JsonObject& json = generatedJsonBuffer.createObject(); 158 | 159 | JsonArray& networks = json.createNestedArray("networks"); 160 | for (int network = 0; network < _ssidCount; network++) { 161 | JsonObject& jsonNetwork = generatedJsonBuffer.createObject(); 162 | jsonNetwork["ssid"] = WiFi.SSID(network); 163 | jsonNetwork["rssi"] = WiFi.RSSI(network); 164 | switch (WiFi.encryptionType(network)) { 165 | case ENC_TYPE_WEP: 166 | jsonNetwork["encryption"] = "wep"; 167 | break; 168 | case ENC_TYPE_TKIP: 169 | jsonNetwork["encryption"] = "wpa"; 170 | break; 171 | case ENC_TYPE_CCMP: 172 | jsonNetwork["encryption"] = "wpa2"; 173 | break; 174 | case ENC_TYPE_NONE: 175 | jsonNetwork["encryption"] = "none"; 176 | break; 177 | case ENC_TYPE_AUTO: 178 | jsonNetwork["encryption"] = "auto"; 179 | break; 180 | } 181 | 182 | networks.add(jsonNetwork); 183 | } 184 | 185 | delete[] _jsonWifiNetworks; 186 | size_t jsonBufferLength = json.measureLength() + 1; 187 | _jsonWifiNetworks = new char[jsonBufferLength]; 188 | json.printTo(_jsonWifiNetworks, jsonBufferLength); 189 | } 190 | 191 | void BootConfig::_onCaptivePortal() { 192 | String host = _http.hostHeader(); 193 | if (host && !host.equals(_apIpStr)) { 194 | // redirect unknown host requests to self if not connected to Internet yet 195 | if (!_proxyEnabled) { 196 | Interface::get().getLogger() << F("Received captive portal request") << endl; 197 | // Catch any captive portal probe. 198 | // Every browser brand uses a different URL for this purpose 199 | // We MUST redirect all them to local webserver to prevent cache poisoning 200 | String redirectUrl = String("http://"); 201 | redirectUrl.concat(_apIpStr); 202 | _http.sendHeader(F("Location"), redirectUrl); 203 | _http.send(302, F("text/plain"), F("")); 204 | // perform transparent proxy to Internet if connected 205 | } else { 206 | _proxyHttpRequest(); 207 | } 208 | } else if (_http.uri() != "/" || !SPIFFS.exists(CONFIG_UI_BUNDLE_PATH)) { 209 | Interface::get().getLogger() << F("Received not found request") << endl; 210 | _http.send(404, F("text/plain"), F("UI bundle not loaded. See Configuration API usage: http://marvinroger.github.io/homie-esp8266/")); 211 | } else { 212 | Interface::get().getLogger() << F("Received UI request") << endl; 213 | File file = SPIFFS.open(CONFIG_UI_BUNDLE_PATH, "r"); 214 | _http.streamFile(file, F("text/html")); 215 | file.close(); 216 | } 217 | } 218 | 219 | void BootConfig::_proxyHttpRequest() { 220 | Interface::get().getLogger() << F("Received transparent proxy request") << endl; 221 | 222 | String url = String("http://"); 223 | url.concat(_http.hostHeader()); 224 | url.concat(_http.uri()); 225 | 226 | // send request to destination (as in incoming host header) 227 | _httpClient.setUserAgent(F("ESP8266-Homie")); 228 | _httpClient.begin(url); 229 | // copy headers 230 | for (int i = 0; i < _http.headers(); i++) { 231 | _httpClient.addHeader(_http.headerName(i), _http.header(i)); 232 | } 233 | 234 | String method = ""; 235 | switch (_http.method()) { 236 | case HTTP_GET: method = F("GET"); break; 237 | case HTTP_PUT: method = F("PUT"); break; 238 | case HTTP_POST: method = F("POST"); break; 239 | case HTTP_DELETE: method = F("DELETE"); break; 240 | case HTTP_OPTIONS: method = F("OPTIONS"); break; 241 | default: break; 242 | } 243 | 244 | Interface::get().getLogger() << F("Proxy sent request to destination") << endl; 245 | int _httpCode = _httpClient.sendRequest(method.c_str(), _http.arg("plain")); 246 | Interface::get().getLogger() << F("Destination response code = ") << _httpCode << endl; 247 | 248 | // bridge response to browser 249 | // copy response headers 250 | for (int i = 0; i < _httpClient.headers(); i++) { 251 | _http.sendHeader(_httpClient.headerName(i), _httpClient.header(i), false); 252 | } 253 | Interface::get().getLogger() << F("Bridging received destination contents to client") << endl; 254 | _http.send(_httpCode, _httpClient.header("Content-Type"), _httpClient.getString()); 255 | _httpClient.end(); 256 | } 257 | 258 | void BootConfig::_onDeviceInfoRequest() { 259 | Interface::get().getLogger() << F("Received device information request") << endl; 260 | auto numSettings = IHomieSetting::settings.size(); 261 | auto numNodes = HomieNode::nodes.size(); 262 | 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))); 263 | JsonObject& json = jsonBuffer.createObject(); 264 | json["hardware_device_id"] = DeviceId::get(); 265 | json["homie_esp8266_version"] = HOMIE_ESP8266_VERSION; 266 | JsonObject& firmware = json.createNestedObject("firmware"); 267 | firmware["name"] = Interface::get().firmware.name; 268 | firmware["version"] = Interface::get().firmware.version; 269 | 270 | JsonArray& nodes = json.createNestedArray("nodes"); 271 | for (HomieNode* iNode : HomieNode::nodes) { 272 | JsonObject& jsonNode = jsonBuffer.createObject(); 273 | jsonNode["id"] = iNode->getId(); 274 | jsonNode["type"] = iNode->getType(); 275 | nodes.add(jsonNode); 276 | } 277 | 278 | JsonArray& settings = json.createNestedArray("settings"); 279 | for (IHomieSetting* iSetting : IHomieSetting::settings) { 280 | JsonObject& jsonSetting = jsonBuffer.createObject(); 281 | if (iSetting->isBool()) { 282 | HomieSetting* setting = static_cast*>(iSetting); 283 | jsonSetting["name"] = setting->getName(); 284 | jsonSetting["description"] = setting->getDescription(); 285 | jsonSetting["type"] = "bool"; 286 | jsonSetting["required"] = setting->isRequired(); 287 | if (!setting->isRequired()) { 288 | jsonSetting["default"] = setting->get(); 289 | } 290 | } else if (iSetting->isLong()) { 291 | HomieSetting* setting = static_cast*>(iSetting); 292 | jsonSetting["name"] = setting->getName(); 293 | jsonSetting["description"] = setting->getDescription(); 294 | jsonSetting["type"] = "long"; 295 | jsonSetting["required"] = setting->isRequired(); 296 | if (!setting->isRequired()) { 297 | jsonSetting["default"] = setting->get(); 298 | } 299 | } else if (iSetting->isDouble()) { 300 | HomieSetting* setting = static_cast*>(iSetting); 301 | jsonSetting["name"] = setting->getName(); 302 | jsonSetting["description"] = setting->getDescription(); 303 | jsonSetting["type"] = "double"; 304 | jsonSetting["required"] = setting->isRequired(); 305 | if (!setting->isRequired()) { 306 | jsonSetting["default"] = setting->get(); 307 | } 308 | } else if (iSetting->isConstChar()) { 309 | HomieSetting* setting = static_cast*>(iSetting); 310 | jsonSetting["name"] = setting->getName(); 311 | jsonSetting["description"] = setting->getDescription(); 312 | jsonSetting["type"] = "string"; 313 | jsonSetting["required"] = setting->isRequired(); 314 | if (!setting->isRequired()) { 315 | jsonSetting["default"] = setting->get(); 316 | } 317 | } 318 | 319 | settings.add(jsonSetting); 320 | } 321 | 322 | size_t jsonBufferLength = json.measureLength() + 1; 323 | std::unique_ptr jsonString(new char[jsonBufferLength]); 324 | json.printTo(jsonString.get(), jsonBufferLength); 325 | _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), jsonString.get()); 326 | } 327 | 328 | void BootConfig::_onNetworksRequest() { 329 | Interface::get().getLogger() << F("Received networks request") << endl; 330 | if (_wifiScanAvailable) { 331 | _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), _jsonWifiNetworks); 332 | } else { 333 | _http.send(503, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_NETWORKS_FAILURE)); 334 | } 335 | } 336 | 337 | void BootConfig::_onConfigRequest() { 338 | Interface::get().getLogger() << F("Received config request") << endl; 339 | if (_flaggedForReboot) { 340 | Interface::get().getLogger() << F("✖ Device already configured") << endl; 341 | String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); 342 | errorJson.concat(F("Device already configured\"}")); 343 | _http.send(403, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); 344 | return; 345 | } 346 | 347 | StaticJsonBuffer parseJsonBuffer; 348 | std::unique_ptr bodyString = Helpers::cloneString(_http.arg("plain")); 349 | JsonObject& parsedJson = parseJsonBuffer.parseObject(bodyString.get()); // workaround, cannot pass raw String otherwise JSON parsing fails randomly 350 | if (!parsedJson.success()) { 351 | Interface::get().getLogger() << F("✖ Invalid or too big JSON") << endl; 352 | String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); 353 | errorJson.concat(F("Invalid or too big JSON\"}")); 354 | _http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); 355 | return; 356 | } 357 | 358 | ConfigValidationResult configValidationResult = Validation::validateConfig(parsedJson); 359 | if (!configValidationResult.valid) { 360 | Interface::get().getLogger() << F("✖ Config file is not valid, reason: ") << configValidationResult.reason << endl; 361 | String errorJson = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); 362 | errorJson.concat(F("Config file is not valid, reason: ")); 363 | errorJson.concat(configValidationResult.reason); 364 | errorJson.concat(F("\"}")); 365 | _http.send(400, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), errorJson); 366 | return; 367 | } 368 | 369 | Interface::get().getConfig().write(parsedJson); 370 | 371 | Interface::get().getLogger() << F("✔ Configured") << endl; 372 | 373 | _http.send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), F("{\"success\":true}")); 374 | 375 | _flaggedForReboot = true; // We don't reboot immediately, otherwise the response above is not sent 376 | _flaggedForRebootAt = millis(); 377 | } 378 | 379 | void BootConfig::loop() { 380 | Boot::loop(); 381 | 382 | _dns.processNextRequest(); 383 | _http.handleClient(); 384 | 385 | if (_flaggedForReboot) { 386 | if (millis() - _flaggedForRebootAt >= 3000UL) { 387 | Interface::get().getLogger() << F("↻ Rebooting into normal mode...") << endl; 388 | Serial.flush(); 389 | ESP.restart(); 390 | } 391 | 392 | return; 393 | } 394 | 395 | if (!_lastWifiScanEnded) { 396 | int8_t scanResult = WiFi.scanComplete(); 397 | 398 | switch (scanResult) { 399 | case WIFI_SCAN_RUNNING: 400 | return; 401 | case WIFI_SCAN_FAILED: 402 | Interface::get().getLogger() << F("✖ Wi-Fi scan failed") << endl; 403 | _ssidCount = 0; 404 | _wifiScanTimer.reset(); 405 | break; 406 | default: 407 | Interface::get().getLogger() << F("✔ Wi-Fi scan completed") << endl; 408 | _ssidCount = scanResult; 409 | _generateNetworksJson(); 410 | _wifiScanAvailable = true; 411 | break; 412 | } 413 | 414 | _lastWifiScanEnded = true; 415 | } 416 | 417 | if (_lastWifiScanEnded && _wifiScanTimer.check()) { 418 | Interface::get().getLogger() << F("Triggering Wi-Fi scan...") << endl; 419 | WiFi.scanNetworks(true); 420 | _wifiScanTimer.tick(); 421 | _lastWifiScanEnded = false; 422 | } 423 | } 424 | --------------------------------------------------------------------------------