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 | [ 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 | [ 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 | [ 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 | [ 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 | 
2 |
3 | Homie for ESP8266
4 | =================
5 |
6 | [](https://circleci.com/gh/marvinroger/homie-esp8266) [](https://github.com/marvinroger/homie-esp8266/releases) [](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 | [](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 | [ 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 | 
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 | 
99 | Slowly when connecting to the Wi-Fi
100 |
101 | !!! tip "LED"
102 | 
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 | [ 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 | [ 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