diff --git a/README.md b/README.md index a664a3d..075108d 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,9 @@ The following diagram visualises how the framework's modular components fit toge #### Stateful service -The [StatefulService.h](lib/framework/StatefulService.h) class is a responsible for managing state and interfacing with code which wants to change or respond to changes in that state. You can define a data class to hold some state, then build a StatefulService class to manage its state: +The [StatefulService.h](lib/framework/StatefulService.h) class is responsible for managing state. It has an API which allows other code to update or respond to updates in the state it manages. You can define a data class to hold state, then build a StatefulService class to manage it. After that you may attach HTTP endpoints, WebSockets or MQTT topics to the StatefulService instance to provide commonly required features. + +Here is a simple example of a state class and a StatefulService to manage it: ```cpp class LightState { @@ -369,7 +371,8 @@ You may listen for changes to state by registering an update handler callback. I // register an update handler update_handler_id_t myUpdateHandler = lightStateService.addUpdateHandler( [&](const String& originId) { - Serial.println("The light's state has been updated"); + Serial.print("The light's state has been updated by: "); + Serial.println(originId); } ); @@ -377,7 +380,7 @@ update_handler_id_t myUpdateHandler = lightStateService.addUpdateHandler( lightStateService.removeUpdateHandler(myUpdateHandler); ``` -An "originId" is passed to the update handler which may be used to identify the origin of the update. The default origin values the framework provides are: +An "originId" is passed to the update handler which may be used to identify the origin of an update. The default origin values the framework provides are: Origin | Description --------------------- | ----------- @@ -393,17 +396,35 @@ lightStateService.read([&](LightState& state) { }); ``` -StatefulService also exposes an update function which allows the caller to update the state with a callback. This approach automatically calls the registered update handlers when complete. The example below turns on the lights using the arbitrary origin "timer": +StatefulService also exposes an update function which allows the caller to update the state with a callback. This function automatically calls the registered update handlers if the state has been changed. The example below changes the state of the light (turns it on) using the arbitrary origin "timer" and returns the "CHANGED" state update result, indicating that a change was made: ```cpp lightStateService.update([&](LightState& state) { - state.on = true; // turn on the lights! + if (state.on) { + return StateUpdateResult::UNCHANGED; // lights were already on, return UNCHANGED + } + state.on = true; // turn on the lights + return StateUpdateResult::CHANGED; // notify StatefulService by returning CHANGED }, "timer"); ``` +There are three possible return values for an update function which are as follows: + +Origin | Description +----------------------------- | --------------------------------------------------------------------------- +StateUpdateResult::CHANGED | The update changed the state, propagation should take place if required +StateUpdateResult::UNCHANGED | The state was unchanged, propagation should not take place +StateUpdateResult::ERROR | There was an error updating the state, propagation should not take place + #### Serialization -When transmitting state over HTTP, WebSockets, or MQTT it must to be marshalled into a serializable form (JSON). The framework uses ArduinoJson for serialization and the functions defined in [JsonSerializer.h](lib/framework/JsonSerializer.h) and [JsonDeserializer.h](lib/framework/JsonDeserializer.h) facilitate this. +When reading or updating state from an external source (HTTP, WebSockets, or MQTT for example) the state must be marshalled into a serializable form (JSON). SettingsService provides two callback patterns which facilitate this internally: + +Callback | Signature | Purpose +---------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------- +JsonStateReader | void read(T& settings, JsonObject& root) | Reading the state object into a JsonObject +JsonStateUpdater | StateUpdateResult update(JsonObject& root, T& settings) | Updating the state from a JsonObject, returning the appropriate StateUpdateResult + The static functions below can be used to facilitate the serialization/deserialization of the light state: @@ -413,32 +434,33 @@ class LightState { bool on = false; uint8_t brightness = 255; - static void serialize(LightState& state, JsonObject& root) { + static void read(LightState& state, JsonObject& root) { root["on"] = state.on; root["brightness"] = state.brightness; } - static void deserialize(JsonObject& root, LightState& state) { + static StateUpdateResult update(JsonObject& root, LightState& state) { state.on = root["on"] | false; state.brightness = root["brightness"] | 255; + return StateUpdateResult::CHANGED; } }; ``` For convenience, the StatefulService class provides overloads of its `update` and `read` functions which utilize these functions. -Copy the state to a JsonObject using a serializer: +Read the state to a JsonObject using a serializer: ```cpp JsonObject jsonObject = jsonDocument.to(); -lightStateService->read(jsonObject, serializer); +lightStateService->read(jsonObject, LightState::read); ``` Update the state from a JsonObject using a deserializer: ```cpp JsonObject jsonObject = jsonDocument.as(); -lightStateService->update(jsonObject, deserializer, "timer"); +lightStateService->update(jsonObject, LightState::update, "timer"); ``` #### Endpoints @@ -451,7 +473,7 @@ The code below demonstrates how to extend the LightStateService class to provide class LightStateService : public StatefulService { public: LightStateService(AsyncWebServer* server) : - _httpEndpoint(LightState::serialize, LightState::deserialize, this, server, "/rest/lightState") { + _httpEndpoint(LightState::read, LightState::update, this, server, "/rest/lightState") { } private: @@ -471,7 +493,7 @@ The code below demonstrates how to extend the LightStateService class to provide class LightStateService : public StatefulService { public: LightStateService(FS* fs) : - _fsPersistence(LightState::serialize, LightState::deserialize, this, fs, "/config/lightState.json") { + _fsPersistence(LightState::read, LightState::update, this, fs, "/config/lightState.json") { } private: @@ -489,7 +511,7 @@ The code below demonstrates how to extend the LightStateService class to provide class LightStateService : public StatefulService { public: LightStateService(AsyncWebServer* server) : - _webSocket(LightState::serialize, LightState::deserialize, this, server, "/ws/lightState"), { + _webSocket(LightState::read, LightState::update, this, server, "/ws/lightState"), { } private: @@ -508,15 +530,16 @@ The framework includes an MQTT client which can be configured via the UI. MQTT r The code below demonstrates how to extend the LightStateService class to interface with MQTT: ```cpp + class LightStateService : public StatefulService { public: LightStateService(AsyncMqttClient* mqttClient) : - _mqttPubSub(LightState::serialize, - LightState::deserialize, - this, - mqttClient, - "homeassistant/light/my_light/set", - "homeassistant/light/my_light/state") { + _mqttPubSub(LightState::read, + LightState::update, + this, + mqttClient, + "homeassistant/light/my_light/set", + "homeassistant/light/my_light/state") { } private: diff --git a/lib/framework/APSettingsService.cpp b/lib/framework/APSettingsService.cpp index 980621f..677e972 100644 --- a/lib/framework/APSettingsService.cpp +++ b/lib/framework/APSettingsService.cpp @@ -1,13 +1,8 @@ #include APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _httpEndpoint(APSettings::serialize, - APSettings::deserialize, - this, - server, - AP_SETTINGS_SERVICE_PATH, - securityManager), - _fsPersistence(APSettings::serialize, APSettings::deserialize, this, fs, AP_SETTINGS_FILE), + _httpEndpoint(APSettings::read, APSettings::update, this, server, AP_SETTINGS_SERVICE_PATH, securityManager), + _fsPersistence(APSettings::read, APSettings::update, this, fs, AP_SETTINGS_FILE), _dnsServer(nullptr), _lastManaged(0) { addUpdateHandler([&](const String& originId) { reconfigureAP(); }, false); diff --git a/lib/framework/APSettingsService.h b/lib/framework/APSettingsService.h index 2366183..45fb547 100644 --- a/lib/framework/APSettingsService.h +++ b/lib/framework/APSettingsService.h @@ -36,13 +36,13 @@ class APSettings { String ssid; String password; - static void serialize(APSettings& settings, JsonObject& root) { + static void read(APSettings& settings, JsonObject& root) { root["provision_mode"] = settings.provisionMode; root["ssid"] = settings.ssid; root["password"] = settings.password; } - static void deserialize(JsonObject& root, APSettings& settings) { + static StateUpdateResult update(JsonObject& root, APSettings& settings) { settings.provisionMode = root["provision_mode"] | FACTORY_AP_PROVISION_MODE; switch (settings.provisionMode) { case AP_MODE_ALWAYS: @@ -54,6 +54,7 @@ class APSettings { } settings.ssid = root["ssid"] | FACTORY_AP_SSID; settings.password = root["password"] | FACTORY_AP_PASSWORD; + return StateUpdateResult::CHANGED; } }; diff --git a/lib/framework/FSPersistence.h b/lib/framework/FSPersistence.h index 915ed18..bba8cda 100644 --- a/lib/framework/FSPersistence.h +++ b/lib/framework/FSPersistence.h @@ -2,21 +2,19 @@ #define FSPersistence_h #include -#include -#include #include template class FSPersistence { public: - FSPersistence(JsonSerializer jsonSerializer, - JsonDeserializer jsonDeserializer, + FSPersistence(JsonStateReader stateReader, + JsonStateUpdater stateUpdater, StatefulService* statefulService, FS* fs, char const* filePath, size_t bufferSize = DEFAULT_BUFFER_SIZE) : - _jsonSerializer(jsonSerializer), - _jsonDeserializer(jsonDeserializer), + _stateReader(stateReader), + _stateUpdater(stateUpdater), _statefulService(statefulService), _fs(fs), _filePath(filePath), @@ -33,7 +31,7 @@ class FSPersistence { DeserializationError error = deserializeJson(jsonDocument, settingsFile); if (error == DeserializationError::Ok && jsonDocument.is()) { JsonObject jsonObject = jsonDocument.as(); - _statefulService->updateWithoutPropagation(jsonObject, _jsonDeserializer); + _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater); settingsFile.close(); return; } @@ -49,7 +47,7 @@ class FSPersistence { // create and populate a new json object DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize); JsonObject jsonObject = jsonDocument.to(); - _statefulService->read(jsonObject, _jsonSerializer); + _statefulService->read(jsonObject, _stateReader); // serialize it to filesystem File settingsFile = _fs->open(_filePath, "w"); @@ -79,21 +77,21 @@ class FSPersistence { } private: - JsonSerializer _jsonSerializer; - JsonDeserializer _jsonDeserializer; + JsonStateReader _stateReader; + JsonStateUpdater _stateUpdater; StatefulService* _statefulService; - FS* _fs; + FS* _fs; char const* _filePath; size_t _bufferSize; update_handler_id_t _updateHandlerId; protected: - // We assume the deserializer supplies sensible defaults if an empty object + // We assume the updater supplies sensible defaults if an empty object // is supplied, this virtual function allows that to be changed. virtual void applyDefaults() { DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize); JsonObject jsonObject = jsonDocument.as(); - _statefulService->updateWithoutPropagation(jsonObject, _jsonDeserializer); + _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater); } }; diff --git a/lib/framework/HttpEndpoint.h b/lib/framework/HttpEndpoint.h index 2e747e2..f45e716 100644 --- a/lib/framework/HttpEndpoint.h +++ b/lib/framework/HttpEndpoint.h @@ -8,46 +8,44 @@ #include #include -#include -#include #define HTTP_ENDPOINT_ORIGIN_ID "http" template class HttpGetEndpoint { public: - HttpGetEndpoint(JsonSerializer jsonSerializer, + HttpGetEndpoint(JsonStateReader stateReader, StatefulService* statefulService, AsyncWebServer* server, const String& servicePath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN, size_t bufferSize = DEFAULT_BUFFER_SIZE) : - _jsonSerializer(jsonSerializer), _statefulService(statefulService), _bufferSize(bufferSize) { + _stateReader(stateReader), _statefulService(statefulService), _bufferSize(bufferSize) { server->on(servicePath.c_str(), HTTP_GET, securityManager->wrapRequest(std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1), authenticationPredicate)); } - HttpGetEndpoint(JsonSerializer jsonSerializer, + HttpGetEndpoint(JsonStateReader stateReader, StatefulService* statefulService, AsyncWebServer* server, const String& servicePath, size_t bufferSize = DEFAULT_BUFFER_SIZE) : - _jsonSerializer(jsonSerializer), _statefulService(statefulService), _bufferSize(bufferSize) { + _stateReader(stateReader), _statefulService(statefulService), _bufferSize(bufferSize) { server->on(servicePath.c_str(), HTTP_GET, std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1)); } protected: - JsonSerializer _jsonSerializer; + JsonStateReader _stateReader; StatefulService* _statefulService; size_t _bufferSize; void fetchSettings(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(false, _bufferSize); JsonObject jsonObject = response->getRoot().to(); - _statefulService->read(jsonObject, _jsonSerializer); + _statefulService->read(jsonObject, _stateReader); response->setLength(); request->send(response); @@ -57,16 +55,16 @@ class HttpGetEndpoint { template class HttpPostEndpoint { public: - HttpPostEndpoint(JsonSerializer jsonSerializer, - JsonDeserializer jsonDeserializer, + HttpPostEndpoint(JsonStateReader stateReader, + JsonStateUpdater stateUpdater, StatefulService* statefulService, AsyncWebServer* server, const String& servicePath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN, size_t bufferSize = DEFAULT_BUFFER_SIZE) : - _jsonSerializer(jsonSerializer), - _jsonDeserializer(jsonDeserializer), + _stateReader(stateReader), + _stateUpdater(stateUpdater), _statefulService(statefulService), _updateHandler( servicePath, @@ -79,14 +77,14 @@ class HttpPostEndpoint { server->addHandler(&_updateHandler); } - HttpPostEndpoint(JsonSerializer jsonSerializer, - JsonDeserializer jsonDeserializer, + HttpPostEndpoint(JsonStateReader stateReader, + JsonStateUpdater stateUpdater, StatefulService* statefulService, AsyncWebServer* server, const String& servicePath, size_t bufferSize = DEFAULT_BUFFER_SIZE) : - _jsonSerializer(jsonSerializer), - _jsonDeserializer(jsonDeserializer), + _stateReader(stateReader), + _stateUpdater(stateUpdater), _statefulService(statefulService), _updateHandler(servicePath, std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2), @@ -97,56 +95,54 @@ class HttpPostEndpoint { } protected: - JsonSerializer _jsonSerializer; - JsonDeserializer _jsonDeserializer; + JsonStateReader _stateReader; + JsonStateUpdater _stateUpdater; StatefulService* _statefulService; AsyncCallbackJsonWebHandler _updateHandler; size_t _bufferSize; void updateSettings(AsyncWebServerRequest* request, JsonVariant& json) { - if (json.is()) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, _bufferSize); - - // use callback to update the settings once the response is complete - request->onDisconnect([this]() { _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); }); - - // update the settings, deferring the call to the update handlers to when the response is complete - _statefulService->updateWithoutPropagation([&](T& settings) { - JsonObject jsonObject = json.as(); - _jsonDeserializer(jsonObject, settings); - jsonObject = response->getRoot().to(); - _jsonSerializer(settings, jsonObject); - }); - - // write the response to the client - response->setLength(); - request->send(response); - } else { + if (!json.is()) { request->send(400); + return; } + JsonObject jsonObject = json.as(); + StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater); + if (outcome == StateUpdateResult::ERROR) { + request->send(400); + return; + } + if (outcome == StateUpdateResult::CHANGED) { + request->onDisconnect([this]() { _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); }); + } + AsyncJsonResponse* response = new AsyncJsonResponse(false, _bufferSize); + jsonObject = response->getRoot().to(); + _statefulService->read(jsonObject, _stateReader); + response->setLength(); + request->send(response); } }; template class HttpEndpoint : public HttpGetEndpoint, public HttpPostEndpoint { public: - HttpEndpoint(JsonSerializer jsonSerializer, - JsonDeserializer jsonDeserializer, + HttpEndpoint(JsonStateReader stateReader, + JsonStateUpdater stateUpdater, StatefulService* statefulService, AsyncWebServer* server, const String& servicePath, SecurityManager* securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN, size_t bufferSize = DEFAULT_BUFFER_SIZE) : - HttpGetEndpoint(jsonSerializer, + HttpGetEndpoint(stateReader, statefulService, server, servicePath, securityManager, authenticationPredicate, bufferSize), - HttpPostEndpoint(jsonSerializer, - jsonDeserializer, + HttpPostEndpoint(stateReader, + stateUpdater, statefulService, server, servicePath, @@ -155,14 +151,14 @@ class HttpEndpoint : public HttpGetEndpoint, public HttpPostEndpoint { bufferSize) { } - HttpEndpoint(JsonSerializer jsonSerializer, - JsonDeserializer jsonDeserializer, + HttpEndpoint(JsonStateReader stateReader, + JsonStateUpdater stateUpdater, StatefulService* statefulService, AsyncWebServer* server, const String& servicePath, size_t bufferSize = DEFAULT_BUFFER_SIZE) : - HttpGetEndpoint(jsonSerializer, statefulService, server, servicePath, bufferSize), - HttpPostEndpoint(jsonSerializer, jsonDeserializer, statefulService, server, servicePath, bufferSize) { + HttpGetEndpoint(stateReader, statefulService, server, servicePath, bufferSize), + HttpPostEndpoint(stateReader, stateUpdater, statefulService, server, servicePath, bufferSize) { } }; diff --git a/lib/framework/JsonDeserializer.h b/lib/framework/JsonDeserializer.h deleted file mode 100644 index 630ba53..0000000 --- a/lib/framework/JsonDeserializer.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef JsonDeserializer_h -#define JsonDeserializer_h - -#include - -template -using JsonDeserializer = void (*)(JsonObject& root, T& settings); - -#endif // end JsonDeserializer diff --git a/lib/framework/JsonSerializer.h b/lib/framework/JsonSerializer.h deleted file mode 100644 index 993d5aa..0000000 --- a/lib/framework/JsonSerializer.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef JsonSerializer_h -#define JsonSerializer_h - -#include - -template -using JsonSerializer = void (*)(T& settings, JsonObject& root); - -#endif // end JsonSerializer diff --git a/lib/framework/MqttPubSub.h b/lib/framework/MqttPubSub.h index 959a582..c3ed3f1 100644 --- a/lib/framework/MqttPubSub.h +++ b/lib/framework/MqttPubSub.h @@ -2,8 +2,6 @@ #define MqttPubSub_h #include -#include -#include #include #define MQTT_ORIGIN_ID "mqtt" @@ -31,12 +29,12 @@ class MqttConnector { template class MqttPub : virtual public MqttConnector { public: - MqttPub(JsonSerializer jsonSerializer, + MqttPub(JsonStateReader stateReader, StatefulService* statefulService, AsyncMqttClient* mqttClient, const String& pubTopic = "", size_t bufferSize = DEFAULT_BUFFER_SIZE) : - MqttConnector(statefulService, mqttClient, bufferSize), _jsonSerializer(jsonSerializer), _pubTopic(pubTopic) { + MqttConnector(statefulService, mqttClient, bufferSize), _stateReader(stateReader), _pubTopic(pubTopic) { MqttConnector::_statefulService->addUpdateHandler([&](const String& originId) { publish(); }, false); } @@ -51,7 +49,7 @@ class MqttPub : virtual public MqttConnector { } private: - JsonSerializer _jsonSerializer; + JsonStateReader _stateReader; String _pubTopic; void publish() { @@ -59,7 +57,7 @@ class MqttPub : virtual public MqttConnector { // serialize to json doc DynamicJsonDocument json(MqttConnector::_bufferSize); JsonObject jsonObject = json.to(); - MqttConnector::_statefulService->read(jsonObject, _jsonSerializer); + MqttConnector::_statefulService->read(jsonObject, _stateReader); // serialize to string String payload; @@ -74,14 +72,12 @@ class MqttPub : virtual public MqttConnector { template class MqttSub : virtual public MqttConnector { public: - MqttSub(JsonDeserializer jsonDeserializer, + MqttSub(JsonStateUpdater stateUpdater, StatefulService* statefulService, AsyncMqttClient* mqttClient, const String& subTopic = "", size_t bufferSize = DEFAULT_BUFFER_SIZE) : - MqttConnector(statefulService, mqttClient, bufferSize), - _jsonDeserializer(jsonDeserializer), - _subTopic(subTopic) { + MqttConnector(statefulService, mqttClient, bufferSize), _stateUpdater(stateUpdater), _subTopic(subTopic) { MqttConnector::_mqttClient->onMessage(std::bind(&MqttSub::onMqttMessage, this, std::placeholders::_1, @@ -110,7 +106,7 @@ class MqttSub : virtual public MqttConnector { } private: - JsonDeserializer _jsonDeserializer; + JsonStateUpdater _stateUpdater; String _subTopic; void subscribe() { @@ -135,7 +131,7 @@ class MqttSub : virtual public MqttConnector { DeserializationError error = deserializeJson(json, payload, len); if (!error && json.is()) { JsonObject jsonObject = json.as(); - MqttConnector::_statefulService->update(jsonObject, _jsonDeserializer, MQTT_ORIGIN_ID); + MqttConnector::_statefulService->update(jsonObject, _stateUpdater, MQTT_ORIGIN_ID); } } }; @@ -143,16 +139,16 @@ class MqttSub : virtual public MqttConnector { template class MqttPubSub : public MqttPub, public MqttSub { public: - MqttPubSub(JsonSerializer jsonSerializer, - JsonDeserializer jsonDeserializer, + MqttPubSub(JsonStateReader stateReader, + JsonStateUpdater stateUpdater, StatefulService* statefulService, AsyncMqttClient* mqttClient, const String& pubTopic = "", const String& subTopic = "", size_t bufferSize = DEFAULT_BUFFER_SIZE) : MqttConnector(statefulService, mqttClient, bufferSize), - MqttPub(jsonSerializer, statefulService, mqttClient, pubTopic, bufferSize), - MqttSub(jsonDeserializer, statefulService, mqttClient, subTopic, bufferSize) { + MqttPub(stateReader, statefulService, mqttClient, pubTopic, bufferSize), + MqttSub(stateUpdater, statefulService, mqttClient, subTopic, bufferSize) { } public: diff --git a/lib/framework/MqttSettingsService.cpp b/lib/framework/MqttSettingsService.cpp index 29ce527..a9932ae 100644 --- a/lib/framework/MqttSettingsService.cpp +++ b/lib/framework/MqttSettingsService.cpp @@ -21,13 +21,8 @@ static char* retainCstr(const char* cstr, char** ptr) { } MqttSettingsService::MqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _httpEndpoint(MqttSettings::serialize, - MqttSettings::deserialize, - this, - server, - MQTT_SETTINGS_SERVICE_PATH, - securityManager), - _fsPersistence(MqttSettings::serialize, MqttSettings::deserialize, this, fs, MQTT_SETTINGS_FILE), + _httpEndpoint(MqttSettings::read, MqttSettings::update, this, server, MQTT_SETTINGS_SERVICE_PATH, securityManager), + _fsPersistence(MqttSettings::read, MqttSettings::update, this, fs, MQTT_SETTINGS_FILE), _retainedHost(nullptr), _retainedClientId(nullptr), _retainedUsername(nullptr), diff --git a/lib/framework/MqttSettingsService.h b/lib/framework/MqttSettingsService.h index 5535329..e6a0357 100644 --- a/lib/framework/MqttSettingsService.h +++ b/lib/framework/MqttSettingsService.h @@ -75,7 +75,7 @@ class MqttSettings { bool cleanSession; uint16_t maxTopicLength; - static void serialize(MqttSettings& settings, JsonObject& root) { + static void read(MqttSettings& settings, JsonObject& root) { root["enabled"] = settings.enabled; root["host"] = settings.host; root["port"] = settings.port; @@ -87,7 +87,7 @@ class MqttSettings { root["max_topic_length"] = settings.maxTopicLength; } - static void deserialize(JsonObject& root, MqttSettings& settings) { + static StateUpdateResult update(JsonObject& root, MqttSettings& settings) { settings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED; settings.host = root["host"] | FACTORY_MQTT_HOST; settings.port = root["port"] | FACTORY_MQTT_PORT; @@ -97,6 +97,7 @@ class MqttSettings { settings.keepAlive = root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE; settings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION; settings.maxTopicLength = root["max_topic_length"] | FACTORY_MQTT_MAX_TOPIC_LENGTH; + return StateUpdateResult::CHANGED; } }; diff --git a/lib/framework/NTPSettingsService.cpp b/lib/framework/NTPSettingsService.cpp index baa8c9d..40f3772 100644 --- a/lib/framework/NTPSettingsService.cpp +++ b/lib/framework/NTPSettingsService.cpp @@ -1,13 +1,8 @@ #include NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _httpEndpoint(NTPSettings::serialize, - NTPSettings::deserialize, - this, - server, - NTP_SETTINGS_SERVICE_PATH, - securityManager), - _fsPersistence(NTPSettings::serialize, NTPSettings::deserialize, this, fs, NTP_SETTINGS_FILE) { + _httpEndpoint(NTPSettings::read, NTPSettings::update, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager), + _fsPersistence(NTPSettings::read, NTPSettings::update, this, fs, NTP_SETTINGS_FILE) { #ifdef ESP32 WiFi.onEvent( std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), diff --git a/lib/framework/NTPSettingsService.h b/lib/framework/NTPSettingsService.h index c2974bc..5749e3b 100644 --- a/lib/framework/NTPSettingsService.h +++ b/lib/framework/NTPSettingsService.h @@ -37,18 +37,19 @@ class NTPSettings { String tzFormat; String server; - static void serialize(NTPSettings& settings, JsonObject& root) { + static void read(NTPSettings& settings, JsonObject& root) { root["enabled"] = settings.enabled; root["server"] = settings.server; root["tz_label"] = settings.tzLabel; root["tz_format"] = settings.tzFormat; } - static void deserialize(JsonObject& root, NTPSettings& settings) { + static StateUpdateResult update(JsonObject& root, NTPSettings& settings) { settings.enabled = root["enabled"] | FACTORY_NTP_ENABLED; settings.server = root["server"] | FACTORY_NTP_SERVER; settings.tzLabel = root["tz_label"] | FACTORY_NTP_TIME_ZONE_LABEL; settings.tzFormat = root["tz_format"] | FACTORY_NTP_TIME_ZONE_FORMAT; + return StateUpdateResult::CHANGED; } }; diff --git a/lib/framework/OTASettingsService.cpp b/lib/framework/OTASettingsService.cpp index f075cac..073f78b 100644 --- a/lib/framework/OTASettingsService.cpp +++ b/lib/framework/OTASettingsService.cpp @@ -1,13 +1,8 @@ #include OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _httpEndpoint(OTASettings::serialize, - OTASettings::deserialize, - this, - server, - OTA_SETTINGS_SERVICE_PATH, - securityManager), - _fsPersistence(OTASettings::serialize, OTASettings::deserialize, this, fs, OTA_SETTINGS_FILE), + _httpEndpoint(OTASettings::read, OTASettings::update, this, server, OTA_SETTINGS_SERVICE_PATH, securityManager), + _fsPersistence(OTASettings::read, OTASettings::update, this, fs, OTA_SETTINGS_FILE), _arduinoOTA(nullptr) { #ifdef ESP32 WiFi.onEvent(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), diff --git a/lib/framework/OTASettingsService.h b/lib/framework/OTASettingsService.h index 87fc3f3..c8d8609 100644 --- a/lib/framework/OTASettingsService.h +++ b/lib/framework/OTASettingsService.h @@ -34,16 +34,17 @@ class OTASettings { int port; String password; - static void serialize(OTASettings& settings, JsonObject& root) { + static void read(OTASettings& settings, JsonObject& root) { root["enabled"] = settings.enabled; root["port"] = settings.port; root["password"] = settings.password; } - static void deserialize(JsonObject& root, OTASettings& settings) { + static StateUpdateResult update(JsonObject& root, OTASettings& settings) { settings.enabled = root["enabled"] | FACTORY_OTA_ENABLED; settings.port = root["port"] | FACTORY_OTA_PORT; settings.password = root["password"] | FACTORY_OTA_PASSWORD; + return StateUpdateResult::CHANGED; } }; diff --git a/lib/framework/SecuritySettingsService.cpp b/lib/framework/SecuritySettingsService.cpp index cc01ded..d4903e7 100644 --- a/lib/framework/SecuritySettingsService.cpp +++ b/lib/framework/SecuritySettingsService.cpp @@ -1,13 +1,8 @@ #include SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : - _httpEndpoint(SecuritySettings::serialize, - SecuritySettings::deserialize, - this, - server, - SECURITY_SETTINGS_PATH, - this), - _fsPersistence(SecuritySettings::serialize, SecuritySettings::deserialize, this, fs, SECURITY_SETTINGS_FILE), + _httpEndpoint(SecuritySettings::read, SecuritySettings::update, this, server, SECURITY_SETTINGS_PATH, this), + _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE), _jwtHandler(FACTORY_JWT_SECRET) { addUpdateHandler([&](const String& originId) { configureJWTHandler(); }, false); } diff --git a/lib/framework/SecuritySettingsService.h b/lib/framework/SecuritySettingsService.h index 97c7f00..970afa3 100644 --- a/lib/framework/SecuritySettingsService.h +++ b/lib/framework/SecuritySettingsService.h @@ -29,7 +29,7 @@ class SecuritySettings { String jwtSecret; std::list users; - static void serialize(SecuritySettings& settings, JsonObject& root) { + static void read(SecuritySettings& settings, JsonObject& root) { // secret root["jwt_secret"] = settings.jwtSecret; @@ -43,7 +43,7 @@ class SecuritySettings { } } - static void deserialize(JsonObject& root, SecuritySettings& settings) { + static StateUpdateResult update(JsonObject& root, SecuritySettings& settings) { // secret settings.jwtSecret = root["jwt_secret"] | FACTORY_JWT_SECRET; @@ -57,6 +57,7 @@ class SecuritySettings { settings.users.push_back(User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true)); settings.users.push_back(User(FACTORY_GUEST_USERNAME, FACTORY_GUEST_PASSWORD, false)); } + return StateUpdateResult::CHANGED; } }; diff --git a/lib/framework/StatefulService.h b/lib/framework/StatefulService.h index e29e6fc..13d4b3f 100644 --- a/lib/framework/StatefulService.h +++ b/lib/framework/StatefulService.h @@ -2,8 +2,7 @@ #define StatefulService_h #include -#include -#include +#include #include #include @@ -16,6 +15,18 @@ #define DEFAULT_BUFFER_SIZE 1024 #endif +enum class StateUpdateResult { + CHANGED = 0, // The update changed the state and propagation should take place if required + UNCHANGED, // The state was unchanged, propagation should not take place + ERROR // There was a problem updating the state, propagation should not take place +}; + +template +using JsonStateUpdater = StateUpdateResult (*)(JsonObject& root, T& settings); + +template +using JsonStateReader = void (*)(T& settings, JsonObject& root); + typedef size_t update_handler_id_t; typedef std::function StateUpdateCallback; @@ -60,66 +71,50 @@ class StatefulService { } } - void updateWithoutPropagation(std::function callback) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); -#endif - callback(_state); -#ifdef ESP32 - xSemaphoreGiveRecursive(_accessMutex); -#endif + StateUpdateResult update(std::function stateUpdater, const String& originId) { + beginTransaction(); + StateUpdateResult result = stateUpdater(_state); + endTransaction(); + if (result == StateUpdateResult::CHANGED) { + callUpdateHandlers(originId); + } + return result; } - void updateWithoutPropagation(JsonObject& jsonObject, JsonDeserializer deserializer) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); -#endif - deserializer(jsonObject, _state); -#ifdef ESP32 - xSemaphoreGiveRecursive(_accessMutex); -#endif + StateUpdateResult updateWithoutPropagation(std::function stateUpdater) { + beginTransaction(); + StateUpdateResult result = stateUpdater(_state); + endTransaction(); + return result; } - void update(std::function callback, const String& originId) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); -#endif - callback(_state); - callUpdateHandlers(originId); -#ifdef ESP32 - xSemaphoreGiveRecursive(_accessMutex); -#endif + StateUpdateResult update(JsonObject& jsonObject, JsonStateUpdater stateUpdater, const String& originId) { + beginTransaction(); + StateUpdateResult result = stateUpdater(jsonObject, _state); + endTransaction(); + if (result == StateUpdateResult::CHANGED) { + callUpdateHandlers(originId); + } + return result; } - void update(JsonObject& jsonObject, JsonDeserializer deserializer, const String& originId) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); -#endif - deserializer(jsonObject, _state); - callUpdateHandlers(originId); -#ifdef ESP32 - xSemaphoreGiveRecursive(_accessMutex); -#endif + StateUpdateResult updateWithoutPropagation(JsonObject& jsonObject, JsonStateUpdater stateUpdater) { + beginTransaction(); + StateUpdateResult result = stateUpdater(jsonObject, _state); + endTransaction(); + return result; } - void read(std::function callback) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); -#endif - callback(_state); -#ifdef ESP32 - xSemaphoreGiveRecursive(_accessMutex); -#endif + void read(std::function stateReader) { + beginTransaction(); + stateReader(_state); + endTransaction(); } - void read(JsonObject& jsonObject, JsonSerializer serializer) { -#ifdef ESP32 - xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); -#endif - serializer(_state, jsonObject); -#ifdef ESP32 - xSemaphoreGiveRecursive(_accessMutex); -#endif + void read(JsonObject& jsonObject, JsonStateReader stateReader) { + beginTransaction(); + stateReader(_state, jsonObject); + endTransaction(); } void callUpdateHandlers(const String& originId) { @@ -131,6 +126,18 @@ class StatefulService { protected: T _state; + inline void beginTransaction() { +#ifdef ESP32 + xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); +#endif + } + + inline void endTransaction() { +#ifdef ESP32 + xSemaphoreGiveRecursive(_accessMutex); +#endif + } + private: #ifdef ESP32 SemaphoreHandle_t _accessMutex; diff --git a/lib/framework/WebSocketTxRx.h b/lib/framework/WebSocketTxRx.h index ee5bb70..d43db52 100644 --- a/lib/framework/WebSocketTxRx.h +++ b/lib/framework/WebSocketTxRx.h @@ -2,8 +2,6 @@ #define WebSocketTxRx_h #include -#include -#include #include #define WEB_SOCKET_CLIENT_ID_MSG_SIZE 128 @@ -75,7 +73,7 @@ class WebSocketConnector { template class WebSocketTx : virtual public WebSocketConnector { public: - WebSocketTx(JsonSerializer jsonSerializer, + WebSocketTx(JsonStateReader stateReader, StatefulService* statefulService, AsyncWebServer* server, char const* webSocketPath, @@ -88,19 +86,19 @@ class WebSocketTx : virtual public WebSocketConnector { securityManager, authenticationPredicate, bufferSize), - _jsonSerializer(jsonSerializer) { + _stateReader(stateReader) { WebSocketConnector::_statefulService->addUpdateHandler( [&](const String& originId) { transmitData(nullptr, originId); }, false); } - WebSocketTx(JsonSerializer jsonSerializer, + WebSocketTx(JsonStateReader stateReader, StatefulService* statefulService, AsyncWebServer* server, char const* webSocketPath, size_t bufferSize = DEFAULT_BUFFER_SIZE) : - WebSocketConnector(statefulService, server, webSocketPath, bufferSize), _jsonSerializer(jsonSerializer) { - WebSocketConnector::_statefulService->addUpdateHandler([&](const String& originId) { transmitData(nullptr, originId); }, - false); + WebSocketConnector(statefulService, server, webSocketPath, bufferSize), _stateReader(stateReader) { + WebSocketConnector::_statefulService->addUpdateHandler( + [&](const String& originId) { transmitData(nullptr, originId); }, false); } protected: @@ -118,7 +116,7 @@ class WebSocketTx : virtual public WebSocketConnector { } private: - JsonSerializer _jsonSerializer; + JsonStateReader _stateReader; void transmitId(AsyncWebSocketClient* client) { DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_CLIENT_ID_MSG_SIZE); @@ -146,7 +144,7 @@ class WebSocketTx : virtual public WebSocketConnector { root["type"] = "payload"; root["origin_id"] = originId; JsonObject payload = root.createNestedObject("payload"); - WebSocketConnector::_statefulService->read(payload, _jsonSerializer); + WebSocketConnector::_statefulService->read(payload, _stateReader); size_t len = measureJson(jsonDocument); AsyncWebSocketMessageBuffer* buffer = WebSocketConnector::_webSocket.makeBuffer(len); @@ -164,7 +162,7 @@ class WebSocketTx : virtual public WebSocketConnector { template class WebSocketRx : virtual public WebSocketConnector { public: - WebSocketRx(JsonDeserializer jsonDeserializer, + WebSocketRx(JsonStateUpdater stateUpdater, StatefulService* statefulService, AsyncWebServer* server, char const* webSocketPath, @@ -177,15 +175,15 @@ class WebSocketRx : virtual public WebSocketConnector { securityManager, authenticationPredicate, bufferSize), - _jsonDeserializer(jsonDeserializer) { + _stateUpdater(stateUpdater) { } - WebSocketRx(JsonDeserializer jsonDeserializer, + WebSocketRx(JsonStateUpdater stateUpdater, StatefulService* statefulService, AsyncWebServer* server, char const* webSocketPath, size_t bufferSize = DEFAULT_BUFFER_SIZE) : - WebSocketConnector(statefulService, server, webSocketPath, bufferSize), _jsonDeserializer(jsonDeserializer) { + WebSocketConnector(statefulService, server, webSocketPath, bufferSize), _stateUpdater(stateUpdater) { } protected: @@ -204,7 +202,7 @@ class WebSocketRx : virtual public WebSocketConnector { if (!error && jsonDocument.is()) { JsonObject jsonObject = jsonDocument.as(); WebSocketConnector::_statefulService->update( - jsonObject, _jsonDeserializer, WebSocketConnector::clientId(client)); + jsonObject, _stateUpdater, WebSocketConnector::clientId(client)); } } } @@ -212,14 +210,14 @@ class WebSocketRx : virtual public WebSocketConnector { } private: - JsonDeserializer _jsonDeserializer; + JsonStateUpdater _stateUpdater; }; template class WebSocketTxRx : public WebSocketTx, public WebSocketRx { public: - WebSocketTxRx(JsonSerializer jsonSerializer, - JsonDeserializer jsonDeserializer, + WebSocketTxRx(JsonStateReader stateReader, + JsonStateUpdater stateUpdater, StatefulService* statefulService, AsyncWebServer* server, char const* webSocketPath, @@ -232,14 +230,14 @@ class WebSocketTxRx : public WebSocketTx, public WebSocketRx { securityManager, authenticationPredicate, bufferSize), - WebSocketTx(jsonSerializer, + WebSocketTx(stateReader, statefulService, server, webSocketPath, securityManager, authenticationPredicate, bufferSize), - WebSocketRx(jsonDeserializer, + WebSocketRx(stateUpdater, statefulService, server, webSocketPath, @@ -248,15 +246,15 @@ class WebSocketTxRx : public WebSocketTx, public WebSocketRx { bufferSize) { } - WebSocketTxRx(JsonSerializer jsonSerializer, - JsonDeserializer jsonDeserializer, + WebSocketTxRx(JsonStateReader stateReader, + JsonStateUpdater stateUpdater, StatefulService* statefulService, AsyncWebServer* server, char const* webSocketPath, size_t bufferSize = DEFAULT_BUFFER_SIZE) : WebSocketConnector(statefulService, server, webSocketPath, bufferSize), - WebSocketTx(jsonSerializer, statefulService, server, webSocketPath, bufferSize), - WebSocketRx(jsonDeserializer, statefulService, server, webSocketPath, bufferSize) { + WebSocketTx(stateReader, statefulService, server, webSocketPath, bufferSize), + WebSocketRx(stateUpdater, statefulService, server, webSocketPath, bufferSize) { } protected: diff --git a/lib/framework/WiFiSettingsService.cpp b/lib/framework/WiFiSettingsService.cpp index 0f4442b..1c228c1 100644 --- a/lib/framework/WiFiSettingsService.cpp +++ b/lib/framework/WiFiSettingsService.cpp @@ -1,13 +1,8 @@ #include WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _httpEndpoint(WiFiSettings::serialize, - WiFiSettings::deserialize, - this, - server, - WIFI_SETTINGS_SERVICE_PATH, - securityManager), - _fsPersistence(WiFiSettings::serialize, WiFiSettings::deserialize, this, fs, WIFI_SETTINGS_FILE), + _httpEndpoint(WiFiSettings::read, WiFiSettings::update, this, server, WIFI_SETTINGS_SERVICE_PATH, securityManager), + _fsPersistence(WiFiSettings::read, WiFiSettings::update, this, fs, WIFI_SETTINGS_FILE), _lastConnectionAttempt(0) { // We want the device to come up in opmode=0 (WIFI_OFF), when erasing the flash this is not the default. // If needed, we save opmode=0 before disabling persistence so the device boots with WiFi disabled in the future. diff --git a/lib/framework/WiFiSettingsService.h b/lib/framework/WiFiSettingsService.h index c818ab1..fc906ed 100644 --- a/lib/framework/WiFiSettingsService.h +++ b/lib/framework/WiFiSettingsService.h @@ -37,7 +37,7 @@ class WiFiSettings { IPAddress dnsIP1; IPAddress dnsIP2; - static void serialize(WiFiSettings& settings, JsonObject& root) { + static void read(WiFiSettings& settings, JsonObject& root) { // connection settings root["ssid"] = settings.ssid; root["password"] = settings.password; @@ -52,7 +52,7 @@ class WiFiSettings { JsonUtils::writeIP(root, "dns_ip_2", settings.dnsIP2); } - static void deserialize(JsonObject& root, WiFiSettings& settings) { + static StateUpdateResult update(JsonObject& root, WiFiSettings& settings) { settings.ssid = root["ssid"] | FACTORY_WIFI_SSID; settings.password = root["password"] | FACTORY_WIFI_PASSWORD; settings.hostname = root["hostname"] | FACTORY_WIFI_HOSTNAME; @@ -78,6 +78,7 @@ class WiFiSettings { (settings.localIP == INADDR_NONE || settings.gatewayIP == INADDR_NONE || settings.subnetMask == INADDR_NONE)) { settings.staticIPConfig = false; } + return StateUpdateResult::CHANGED; } }; diff --git a/src/LightMqttSettingsService.cpp b/src/LightMqttSettingsService.cpp index 96e25eb..bddd909 100644 --- a/src/LightMqttSettingsService.cpp +++ b/src/LightMqttSettingsService.cpp @@ -1,14 +1,14 @@ #include LightMqttSettingsService::LightMqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _httpEndpoint(LightMqttSettings::serialize, - LightMqttSettings::deserialize, + _httpEndpoint(LightMqttSettings::read, + LightMqttSettings::update, this, server, LIGHT_BROKER_SETTINGS_PATH, securityManager, AuthenticationPredicates::IS_AUTHENTICATED), - _fsPersistence(LightMqttSettings::serialize, LightMqttSettings::deserialize, this, fs, LIGHT_BROKER_SETTINGS_FILE) { + _fsPersistence(LightMqttSettings::read, LightMqttSettings::update, this, fs, LIGHT_BROKER_SETTINGS_FILE) { } void LightMqttSettingsService::begin() { diff --git a/src/LightMqttSettingsService.h b/src/LightMqttSettingsService.h index 3b6a3f1..23a2218 100644 --- a/src/LightMqttSettingsService.h +++ b/src/LightMqttSettingsService.h @@ -14,16 +14,17 @@ class LightMqttSettings { String name; String uniqueId; - static void serialize(LightMqttSettings& settings, JsonObject& root) { + static void read(LightMqttSettings& settings, JsonObject& root) { root["mqtt_path"] = settings.mqttPath; root["name"] = settings.name; root["unique_id"] = settings.uniqueId; } - static void deserialize(JsonObject& root, LightMqttSettings& settings) { + static StateUpdateResult update(JsonObject& root, LightMqttSettings& settings) { settings.mqttPath = root["mqtt_path"] | ESPUtils::defaultDeviceValue("homeassistant/light/"); settings.name = root["name"] | ESPUtils::defaultDeviceValue("light-"); settings.uniqueId = root["unique_id"] | ESPUtils::defaultDeviceValue("light-"); + return StateUpdateResult::CHANGED; } }; diff --git a/src/LightStateService.cpp b/src/LightStateService.cpp index a5c7f15..7c70754 100644 --- a/src/LightStateService.cpp +++ b/src/LightStateService.cpp @@ -4,16 +4,16 @@ LightStateService::LightStateService(AsyncWebServer* server, SecurityManager* securityManager, AsyncMqttClient* mqttClient, LightMqttSettingsService* lightMqttSettingsService) : - _httpEndpoint(LightState::serialize, - LightState::deserialize, + _httpEndpoint(LightState::read, + LightState::update, this, server, LIGHT_SETTINGS_ENDPOINT_PATH, securityManager, AuthenticationPredicates::IS_AUTHENTICATED), - _mqttPubSub(LightState::haSerialize, LightState::haDeserialize, this, mqttClient), - _webSocket(LightState::serialize, - LightState::deserialize, + _mqttPubSub(LightState::haRead, LightState::haUpdate, this, mqttClient), + _webSocket(LightState::read, + LightState::update, this, server, LIGHT_SETTINGS_SOCKET_PATH, @@ -40,6 +40,7 @@ void LightStateService::begin() { } void LightStateService::onConfigUpdated() { + Serial.printf_P(PSTR("The light is now: %s\r\n"), _state.ledOn ? "on" : "off"); digitalWrite(BLINK_LED, _state.ledOn ? LED_ON : LED_OFF); } diff --git a/src/LightStateService.h b/src/LightStateService.h index bbf3ed8..f8b3059 100644 --- a/src/LightStateService.h +++ b/src/LightStateService.h @@ -31,21 +31,38 @@ class LightState { public: bool ledOn; - static void serialize(LightState& settings, JsonObject& root) { + static void read(LightState& settings, JsonObject& root) { root["led_on"] = settings.ledOn; } - static void deserialize(JsonObject& root, LightState& settings) { - settings.ledOn = root["led_on"] | DEFAULT_LED_STATE; + static StateUpdateResult update(JsonObject& root, LightState& lightState) { + boolean newState = root["led_on"] | DEFAULT_LED_STATE; + if (lightState.ledOn != newState) { + lightState.ledOn = newState; + return StateUpdateResult::CHANGED; + } + return StateUpdateResult::UNCHANGED; } - static void haSerialize(LightState& settings, JsonObject& root) { + static void haRead(LightState& settings, JsonObject& root) { root["state"] = settings.ledOn ? ON_STATE : OFF_STATE; } - static void haDeserialize(JsonObject& root, LightState& settings) { + static StateUpdateResult haUpdate(JsonObject& root, LightState& lightState) { String state = root["state"]; - settings.ledOn = strcmp(ON_STATE, state.c_str()) ? false : true; + // parse new led state + boolean newState = false; + if (state.equals(ON_STATE)) { + newState = true; + } else if (!state.equals(OFF_STATE)) { + return StateUpdateResult::ERROR; + } + // change the new state, if required + if (lightState.ledOn != newState) { + lightState.ledOn = newState; + return StateUpdateResult::CHANGED; + } + return StateUpdateResult::UNCHANGED; } };