Skip to content

Commit e771ab1

Browse files
rjwatskasedy
andauthored
Settings placeholder substitution (#164)
* Use text formatting for default factory values to produce dynamic names. Header files contains duplicates of factory values defined in factory_settings.ini Removed them to simplify the code. * Use text formatting for default factory values to produce dynamic names. Header files contains duplicates of factory values defined in factory_settings.ini Removed them to simplify the code. * Configured the WiFi host name to contain the device id by default * Removed possibility to use placeholders for FACTORY_WIFI_SSID factory setting. * Update README.md Updated documentation * Use text formatting for default factory values to produce dynamic names. Header files contains duplicates of factory values defined in factory_settings.ini Removed them to simplify the code. * Configured the WiFi host name to contain the device id by default * Removed possibility to use placeholders for FACTORY_WIFI_SSID factory setting. * Added a space to the end of the file to comply project code style * fix typos clang formatting use 2 spaces in ini files use ${platform}-${chip_id} for hostname put chip id in brackets in AP SSID * restore (and update) factory setting ifndefs - this is so src can be built without an exaustive set build-time defines - standardize ordering of defines: factory settings, paths, config * format and modify comment * escape spaces in pio defines experiment with removing $'s from our format strings (they are being substituted with empty values by pio) * fix formatting in readme rename FactoryValue to SettingValue, put in own header give example of direct usage of FactorySetting::format in README.md * auto format * use hash to delimit placeholders * fix factory_settings.ini * remove flash string helpers * format ini file * use MAC address instead of chip id for properly unique identifier * use lower case hex encoding for unique id use chip id and unique id for more secure secret * fix comment * Use random values for JWT secret Arduino uses the ESP random number generator for "true random" numbers on both esp32 and esp8266 This makes a better JWT secret and may be useful for other factory defaults too In addition a modification has been made to force the FSPersistance to save the file if applying defaults * Don't use spaces in default AP SSID * restore helpful comment in factory_settings.ini fix default defines Co-authored-by: kasedy <[email protected]>
1 parent 6e22893 commit e771ab1

13 files changed

+191
-123
lines changed

README.md

+30-14
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,12 @@ Many of the framework's built in features may be enabled or disabled as required
169169
Customize the settings as you see fit. A value of 0 will disable the specified feature:
170170

171171
```ini
172-
-D FT_PROJECT=1
173-
-D FT_SECURITY=1
174-
-D FT_MQTT=1
175-
-D FT_NTP=1
176-
-D FT_OTA=1
177-
-D FT_UPLOAD_FIRMWARE=1
172+
-D FT_PROJECT=1
173+
-D FT_SECURITY=1
174+
-D FT_MQTT=1
175+
-D FT_NTP=1
176+
-D FT_OTA=1
177+
-D FT_UPLOAD_FIRMWARE=1
178178
```
179179

180180
Flag | Description
@@ -193,9 +193,9 @@ The framework has built-in factory settings which act as default values for the
193193
Customize the settings as you see fit, for example you might configure your home WiFi network as the factory default:
194194

195195
```ini
196-
-D FACTORY_WIFI_SSID=\"My Awesome WiFi Network\"
197-
-D FACTORY_WIFI_PASSWORD=\"secret\"
198-
-D FACTORY_WIFI_HOSTNAME=\"awesome_light_controller\"
196+
-D FACTORY_WIFI_SSID=\"My Awesome WiFi Network\"
197+
-D FACTORY_WIFI_PASSWORD=\"secret\"
198+
-D FACTORY_WIFI_HOSTNAME=\"awesome_light_controller\"
199199
```
200200

201201
### Default access point settings
@@ -221,15 +221,31 @@ It is recommended that you change the user credentials from their defaults bette
221221
Changing factory time zone setting is a common requirement. This requires a little effort because the time zone name and POSIX format are stored as separate values for the moment. The time zone names and POSIX formats are contained in the UI code in [TZ.tsx](interface/src/ntp/TZ.tsx). Take the appropriate pair of values from there, for example, for Los Angeles you would use:
222222

223223
```ini
224-
-D FACTORY_NTP_TIME_ZONE_LABEL=\"America/Los_Angeles\"
225-
-D FACTORY_NTP_TIME_ZONE_FORMAT=\"PST8PDT,M3.2.0,M11.1.0\"
224+
-D FACTORY_NTP_TIME_ZONE_LABEL=\"America/Los_Angeles\"
225+
-D FACTORY_NTP_TIME_ZONE_FORMAT=\"PST8PDT,M3.2.0,M11.1.0\"
226226
```
227227

228-
### Device ID factory defaults
228+
### Placeholder substitution
229229

230-
If not overridden with a build flag, the framework will use the device ID to generate factory defaults for settings such as the JWT secret and MQTT client ID.
230+
Various settings support placeholder substitution, indicated by comments in [factory_settings.ini](factory_settings.ini). This can be particularly useful where settings need to be unique, such as the Access Point SSID or MQTT client id. The following placeholders are supported:
231231

232-
> **Tip**: Random values are generally better defaults for these settings, so it is recommended you leave these flags undefined.
232+
Placeholder | Substituted value
233+
----------- | -----------------
234+
#{platform} | The microcontroller platform, e.g. "esp32" or "esp8266"
235+
#{unique_id} | A unique identifier derived from the MAC address, e.g. "0b0a859d6816"
236+
#{random} | A random number encoded as a hex string, e.g. "55722f94"
237+
238+
You may use SettingValue::format in your own code if you require the use of these placeholders. This is demonstrated in the demo project:
239+
240+
```cpp
241+
static StateUpdateResult update(JsonObject& root, LightMqttSettings& settings) {
242+
settings.mqttPath = root["mqtt_path"] | SettingValue::format("homeassistant/light/#{unique_id}");
243+
settings.name = root["name"] | SettingValue::format("light-#{unique_id}");
244+
settings.uniqueId = root["unique_id"] | SettingValue::format("light-#{unique_id}");
245+
return StateUpdateResult::CHANGED;
246+
}
247+
};
248+
```
233249
234250
## Building for different devices
235251

factory_settings.ini

+44-41
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,51 @@
1+
; The indicated settings support placeholder substitution as follows:
2+
;
3+
; #{platform} - The microcontroller platform, e.g. "esp32" or "esp8266"
4+
; #{unique_id} - A unique identifier derived from the MAC address, e.g. "0b0a859d6816"
5+
; #{random} - A random number encoded as a hex string, e.g. "55722f94"
6+
17
[factory_settings]
2-
build_flags =
3-
; WiFi settings
4-
-D FACTORY_WIFI_SSID=\"\"
5-
-D FACTORY_WIFI_PASSWORD=\"\"
6-
; if unspecified the devices hardware ID will be used
7-
; -D FACTORY_WIFI_HOSTNAME=\"esp-react\"
8+
build_flags =
9+
; WiFi settings
10+
-D FACTORY_WIFI_SSID=\"\"
11+
-D FACTORY_WIFI_PASSWORD=\"\"
12+
-D FACTORY_WIFI_HOSTNAME=\"#{platform}-#{unique_id}\" ; supports placeholders
813

9-
; Access point settings
10-
-D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED
11-
-D FACTORY_AP_SSID=\"ESP8266-React\" ; 1-64 characters
12-
-D FACTORY_AP_PASSWORD=\"esp-react\" ; 8-64 characters
13-
-D FACTORY_AP_LOCAL_IP=\"192.168.4.1\"
14-
-D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"
15-
-D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"
14+
; Access point settings
15+
-D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED
16+
-D FACTORY_AP_SSID=\"ESP8266-React-#{unique_id}\" ; 1-64 characters, supports placeholders
17+
-D FACTORY_AP_PASSWORD=\"esp-react\" ; 8-64 characters
18+
-D FACTORY_AP_LOCAL_IP=\"192.168.4.1\"
19+
-D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"
20+
-D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"
1621

17-
; User credentials for admin and guest user
18-
-D FACTORY_ADMIN_USERNAME=\"admin\"
19-
-D FACTORY_ADMIN_PASSWORD=\"admin\"
20-
-D FACTORY_GUEST_USERNAME=\"guest\"
21-
-D FACTORY_GUEST_PASSWORD=\"guest\"
22+
; User credentials for admin and guest user
23+
-D FACTORY_ADMIN_USERNAME=\"admin\"
24+
-D FACTORY_ADMIN_PASSWORD=\"admin\"
25+
-D FACTORY_GUEST_USERNAME=\"guest\"
26+
-D FACTORY_GUEST_PASSWORD=\"guest\"
2227

23-
; NTP settings
24-
-D FACTORY_NTP_ENABLED=true
25-
-D FACTORY_NTP_TIME_ZONE_LABEL=\"Europe/London\"
26-
-D FACTORY_NTP_TIME_ZONE_FORMAT=\"GMT0BST,M3.5.0/1,M10.5.0\"
27-
-D FACTORY_NTP_SERVER=\"time.google.com\"
28+
; NTP settings
29+
-D FACTORY_NTP_ENABLED=true
30+
-D FACTORY_NTP_TIME_ZONE_LABEL=\"Europe/London\"
31+
-D FACTORY_NTP_TIME_ZONE_FORMAT=\"GMT0BST,M3.5.0/1,M10.5.0\"
32+
-D FACTORY_NTP_SERVER=\"time.google.com\"
2833

29-
; OTA settings
30-
-D FACTORY_OTA_PORT=8266
31-
-D FACTORY_OTA_PASSWORD=\"esp-react\"
32-
-D FACTORY_OTA_ENABLED=true
34+
; OTA settings
35+
-D FACTORY_OTA_PORT=8266
36+
-D FACTORY_OTA_PASSWORD=\"esp-react\"
37+
-D FACTORY_OTA_ENABLED=true
3338

34-
; MQTT settings
35-
-D FACTORY_MQTT_ENABLED=false
36-
-D FACTORY_MQTT_HOST=\"test.mosquitto.org\"
37-
-D FACTORY_MQTT_PORT=1883
38-
-D FACTORY_MQTT_USERNAME=\"\"
39-
-D FACTORY_MQTT_PASSWORD=\"\"
40-
; if unspecified the devices hardware ID will be used
41-
;-D FACTORY_MQTT_CLIENT_ID=\"esp-react\"
42-
-D FACTORY_MQTT_KEEP_ALIVE=60
43-
-D FACTORY_MQTT_CLEAN_SESSION=true
44-
-D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
39+
; MQTT settings
40+
-D FACTORY_MQTT_ENABLED=false
41+
-D FACTORY_MQTT_HOST=\"test.mosquitto.org\"
42+
-D FACTORY_MQTT_PORT=1883
43+
-D FACTORY_MQTT_USERNAME=\"\" ; supports placeholders
44+
-D FACTORY_MQTT_PASSWORD=\"\"
45+
-D FACTORY_MQTT_CLIENT_ID=\"#{platform}-#{unique_id}\" ; supports placeholders
46+
-D FACTORY_MQTT_KEEP_ALIVE=60
47+
-D FACTORY_MQTT_CLEAN_SESSION=true
48+
-D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
4549

46-
; JWT Secret
47-
; if unspecified the devices hardware ID will be used
48-
; -D FACTORY_JWT_SECRET=\"esp8266-react\"
50+
; JWT Secret
51+
-D FACTORY_JWT_SECRET=\"#{random}-#{random}\" ; supports placeholders

features.ini

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[features]
22
build_flags =
3-
-D FT_PROJECT=1
4-
-D FT_SECURITY=1
5-
-D FT_MQTT=1
6-
-D FT_NTP=1
7-
-D FT_OTA=1
8-
-D FT_UPLOAD_FIRMWARE=1
3+
-D FT_PROJECT=1
4+
-D FT_SECURITY=1
5+
-D FT_MQTT=1
6+
-D FT_NTP=1
7+
-D FT_OTA=1
8+
-D FT_UPLOAD_FIRMWARE=1

lib/framework/APSettingsService.h

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
#ifndef APSettingsConfig_h
22
#define APSettingsConfig_h
33

4+
#include <SettingValue.h>
45
#include <HttpEndpoint.h>
56
#include <FSPersistence.h>
67
#include <JsonUtils.h>
78

89
#include <DNSServer.h>
910
#include <IPAddress.h>
1011

11-
#define MANAGE_NETWORK_DELAY 10000
12-
13-
#define AP_MODE_ALWAYS 0
14-
#define AP_MODE_DISCONNECTED 1
15-
#define AP_MODE_NEVER 2
16-
17-
#define DNS_PORT 53
18-
1912
#ifndef FACTORY_AP_PROVISION_MODE
2013
#define FACTORY_AP_PROVISION_MODE AP_MODE_DISCONNECTED
2114
#endif
2215

2316
#ifndef FACTORY_AP_SSID
24-
#define FACTORY_AP_SSID "ESP8266-React"
17+
#define FACTORY_AP_SSID "ESP8266-React-#{unique_id}"
2518
#endif
2619

2720
#ifndef FACTORY_AP_PASSWORD
@@ -43,6 +36,14 @@
4336
#define AP_SETTINGS_FILE "/config/apSettings.json"
4437
#define AP_SETTINGS_SERVICE_PATH "/rest/apSettings"
4538

39+
#define MANAGE_NETWORK_DELAY 10000
40+
41+
#define AP_MODE_ALWAYS 0
42+
#define AP_MODE_DISCONNECTED 1
43+
#define AP_MODE_NEVER 2
44+
45+
#define DNS_PORT 53
46+
4647
enum APNetworkStatus { ACTIVE = 0, INACTIVE, LINGERING };
4748

4849
class APSettings {
@@ -79,7 +80,7 @@ class APSettings {
7980
default:
8081
newSettings.provisionMode = AP_MODE_ALWAYS;
8182
}
82-
newSettings.ssid = root["ssid"] | FACTORY_AP_SSID;
83+
newSettings.ssid = root["ssid"] | SettingValue::format(FACTORY_AP_SSID);
8384
newSettings.password = root["password"] | FACTORY_AP_PASSWORD;
8485

8586
JsonUtils::readIP(root, "local_ip", newSettings.localIP, FACTORY_AP_LOCAL_IP);

lib/framework/ESPUtils.h

-17
This file was deleted.

lib/framework/FSPersistence.h

+4-2
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ class FSPersistence {
3838
settingsFile.close();
3939
}
4040

41-
// If we reach here we have not been successful in loading the config,
42-
// hard-coded emergency defaults are now applied.
41+
// If we reach here we have not been successful in loading the config and hard-coded defaults are now applied.
42+
// The settings are then written back to the file system so the defaults persist between resets. This last step is
43+
// required as in some cases defaults contain randomly generated values which would otherwise be modified on reset.
4344
applyDefaults();
45+
writeToFS();
4446
}
4547

4648
bool writeToFS() {

lib/framework/MqttSettingsService.h

+8-16
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@
55
#include <HttpEndpoint.h>
66
#include <FSPersistence.h>
77
#include <AsyncMqttClient.h>
8-
#include <ESPUtils.h>
9-
10-
#define MQTT_RECONNECTION_DELAY 5000
11-
12-
#define MQTT_SETTINGS_FILE "/config/mqttSettings.json"
13-
#define MQTT_SETTINGS_SERVICE_PATH "/rest/mqttSettings"
8+
#include <SettingValue.h>
149

1510
#ifndef FACTORY_MQTT_ENABLED
1611
#define FACTORY_MQTT_ENABLED false
@@ -33,7 +28,7 @@
3328
#endif
3429

3530
#ifndef FACTORY_MQTT_CLIENT_ID
36-
#define FACTORY_MQTT_CLIENT_ID generateClientId()
31+
#define FACTORY_MQTT_CLIENT_ID "#{platform}-#{unique_id}"
3732
#endif
3833

3934
#ifndef FACTORY_MQTT_KEEP_ALIVE
@@ -48,13 +43,10 @@
4843
#define FACTORY_MQTT_MAX_TOPIC_LENGTH 128
4944
#endif
5045

51-
static String generateClientId() {
52-
#ifdef ESP32
53-
return ESPUtils::defaultDeviceValue("esp32-");
54-
#elif defined(ESP8266)
55-
return ESPUtils::defaultDeviceValue("esp8266-");
56-
#endif
57-
}
46+
#define MQTT_SETTINGS_FILE "/config/mqttSettings.json"
47+
#define MQTT_SETTINGS_SERVICE_PATH "/rest/mqttSettings"
48+
49+
#define MQTT_RECONNECTION_DELAY 5000
5850

5951
class MqttSettings {
6052
public:
@@ -91,9 +83,9 @@ class MqttSettings {
9183
settings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED;
9284
settings.host = root["host"] | FACTORY_MQTT_HOST;
9385
settings.port = root["port"] | FACTORY_MQTT_PORT;
94-
settings.username = root["username"] | FACTORY_MQTT_USERNAME;
86+
settings.username = root["username"] | SettingValue::format(FACTORY_MQTT_USERNAME);
9587
settings.password = root["password"] | FACTORY_MQTT_PASSWORD;
96-
settings.clientId = root["client_id"] | FACTORY_MQTT_CLIENT_ID;
88+
settings.clientId = root["client_id"] | SettingValue::format(FACTORY_MQTT_CLIENT_ID);
9789
settings.keepAlive = root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE;
9890
settings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION;
9991
settings.maxTopicLength = root["max_topic_length"] | FACTORY_MQTT_MAX_TOPIC_LENGTH;

lib/framework/SecurityManager.h

-5
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,9 @@
44
#include <Features.h>
55
#include <ArduinoJsonJWT.h>
66
#include <ESPAsyncWebServer.h>
7-
#include <ESPUtils.h>
87
#include <AsyncJson.h>
98
#include <list>
109

11-
#ifndef FACTORY_JWT_SECRET
12-
#define FACTORY_JWT_SECRET ESPUtils::defaultDeviceValue()
13-
#endif
14-
1510
#define ACCESS_TOKEN_PARAMATER "access_token"
1611

1712
#define AUTHORIZATION_HEADER "Authorization"

lib/framework/SecuritySettingsService.h

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
#ifndef SecuritySettingsService_h
22
#define SecuritySettingsService_h
33

4+
#include <SettingValue.h>
45
#include <Features.h>
56
#include <SecurityManager.h>
67
#include <HttpEndpoint.h>
78
#include <FSPersistence.h>
89

10+
#ifndef FACTORY_JWT_SECRET
11+
#define FACTORY_JWT_SECRET "#{random}-#{random}"
12+
#endif
13+
914
#ifndef FACTORY_ADMIN_USERNAME
1015
#define FACTORY_ADMIN_USERNAME "admin"
1116
#endif
@@ -48,7 +53,7 @@ class SecuritySettings {
4853

4954
static StateUpdateResult update(JsonObject& root, SecuritySettings& settings) {
5055
// secret
51-
settings.jwtSecret = root["jwt_secret"] | FACTORY_JWT_SECRET;
56+
settings.jwtSecret = root["jwt_secret"] | SettingValue::format(FACTORY_JWT_SECRET);
5257

5358
// users
5459
settings.users.clear();
@@ -103,7 +108,7 @@ class SecuritySettingsService : public SecurityManager {
103108
SecuritySettingsService(AsyncWebServer* server, FS* fs);
104109
~SecuritySettingsService();
105110

106-
// minimal set of functions to support framework with security settings disabled
111+
// minimal set of functions to support framework with security settings disabled
107112
Authentication authenticateRequest(AsyncWebServerRequest* request);
108113
ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate);
109114
ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate);

0 commit comments

Comments
 (0)